fullscreen (#360)

* fullscreen

* improvements, and error catching

* yarn prettier

* discord rpc

* fixed uninitialized options.txt

* working discord version

* incorrect boolean

* change

* merge issue; regex solution

* fixed error

* multi line mode

* moved \n to start
This commit is contained in:
Wyatt Verchere
2023-07-27 00:10:07 -07:00
committed by GitHub
parent ce01ee6a2d
commit c364468ed5
20 changed files with 367 additions and 56 deletions

View File

@@ -145,6 +145,13 @@ impl Children {
}
}
{
// Clear game played for Discord RPC
// May have other active processes, so we clear to the next running process
let state = crate::State::get().await?;
let _ = state.discord_rpc.clear_to_default(true).await;
}
// If in tauri, window should show itself again after process exists if it was hidden
#[cfg(feature = "tauri")]
{

View File

@@ -0,0 +1,167 @@
use std::sync::{atomic::AtomicBool, Arc};
use discord_rich_presence::{
activity::{Activity, Assets},
DiscordIpc, DiscordIpcClient,
};
use tokio::sync::RwLock;
use crate::State;
pub struct DiscordGuard {
client: Arc<RwLock<DiscordIpcClient>>,
connected: Arc<AtomicBool>,
}
impl DiscordGuard {
/// Initialize discord IPC client, and attempt to connect to it
/// If it fails, it will still return a DiscordGuard, but the client will be unconnected
pub async fn init() -> crate::Result<DiscordGuard> {
let mut dipc =
DiscordIpcClient::new("1084015525241311292").map_err(|e| {
crate::ErrorKind::OtherError(format!(
"Could not create Discord client {}",
e,
))
})?;
let res = dipc.connect(); // Do not need to connect to Discord to use app
let connected = if res.is_ok() {
Arc::new(AtomicBool::new(true))
} else {
Arc::new(AtomicBool::new(false))
};
let client = Arc::new(RwLock::new(dipc));
Ok(DiscordGuard { client, connected })
}
/// If the client failed connecting during init(), this will check for connection and attempt to reconnect
/// This MUST be called first in any client method that requires a connection, because those can PANIC if the client is not connected
/// (No connection is different than a failed connection, the latter will not panic and can be retried)
pub async fn retry_if_not_ready(&self) -> bool {
let mut client = self.client.write().await;
if !self.connected.load(std::sync::atomic::Ordering::Relaxed) {
if client.connect().is_ok() {
self.connected
.store(true, std::sync::atomic::Ordering::Relaxed);
return true;
}
return false;
}
true
}
/// Set the activity to the given message
pub async fn set_activity(
&self,
msg: &str,
reconnect_if_fail: bool,
) -> crate::Result<()> {
// Attempt to connect if not connected. Do not continue if it fails, as the client.set_activity can panic if it never was connected
if !self.retry_if_not_ready().await {
return Ok(());
}
let activity = Activity::new().state(msg).assets(
Assets::new()
.large_image("modrinth_simple")
.large_text("Modrinth Logo"),
);
// Attempt to set the activity
// If the existing connection fails, attempt to reconnect and try again
let mut client: tokio::sync::RwLockWriteGuard<'_, DiscordIpcClient> =
self.client.write().await;
let res = client.set_activity(activity.clone());
let could_not_set_err = |e: Box<dyn serde::ser::StdError>| {
crate::ErrorKind::OtherError(format!(
"Could not update Discord activity {}",
e,
))
};
if reconnect_if_fail {
if let Err(_e) = res {
client.reconnect().map_err(|e| {
crate::ErrorKind::OtherError(format!(
"Could not reconnect to Discord IPC {}",
e,
))
})?;
return Ok(client
.set_activity(activity)
.map_err(could_not_set_err)?); // try again, but don't reconnect if it fails again
}
} else {
res.map_err(could_not_set_err)?;
}
Ok(())
}
/// Clear the activity
pub async fn clear_activity(
&self,
reconnect_if_fail: bool,
) -> crate::Result<()> {
// Attempt to connect if not connected. Do not continue if it fails, as the client.clear_activity can panic if it never was connected
if !self.retry_if_not_ready().await {
return Ok(());
}
// Attempt to clear the activity
// If the existing connection fails, attempt to reconnect and try again
let mut client = self.client.write().await;
let res = client.clear_activity();
let could_not_clear_err = |e: Box<dyn serde::ser::StdError>| {
crate::ErrorKind::OtherError(format!(
"Could not clear Discord activity {}",
e,
))
};
if reconnect_if_fail {
if res.is_err() {
client.reconnect().map_err(|e| {
crate::ErrorKind::OtherError(format!(
"Could not reconnect to Discord IPC {}",
e,
))
})?;
return Ok(client
.clear_activity()
.map_err(could_not_clear_err)?); // try again, but don't reconnect if it fails again
}
} else {
res.map_err(could_not_clear_err)?;
}
Ok(())
}
/// Clear the activity, but if there is a running profile, set the activity to that instead
pub async fn clear_to_default(
&self,
reconnect_if_fail: bool,
) -> crate::Result<()> {
let state: Arc<tokio::sync::RwLockReadGuard<'_, State>> =
State::get().await?;
if let Some(existing_child) = state
.children
.read()
.await
.running_profile_paths()
.await?
.first()
{
self.set_activity(
&format!("Playing {}", existing_child),
reconnect_if_fail,
)
.await?;
} else {
self.clear_activity(reconnect_if_fail).await?;
}
Ok(())
}
}

View File

@@ -48,6 +48,9 @@ pub use self::java_globals::*;
mod safe_processes;
pub use self::safe_processes::*;
mod discord;
pub use self::discord::*;
// Global state
// RwLock on state only has concurrent reads, except for config dir change which takes control of the State
static LAUNCHER_STATE: OnceCell<RwLock<State>> = OnceCell::const_new();
@@ -81,6 +84,9 @@ pub struct State {
/// Launcher processes that should be safely exited on shutdown
pub(crate) safety_processes: RwLock<SafeProcesses>,
/// Discord RPC
pub discord_rpc: DiscordGuard,
/// File watcher debouncer
pub(crate) file_watcher: RwLock<Debouncer<RecommendedWatcher>>,
}
@@ -156,6 +162,9 @@ impl State {
let children = Children::new();
let auth_flow = AuthTask::new();
let safety_processes = SafeProcesses::new();
let discord_rpc = DiscordGuard::init().await?;
emit_loading(&loading_bar, 10.0, None).await?;
Ok::<RwLock<Self>, crate::Error>(RwLock::new(Self {
@@ -175,6 +184,7 @@ impl State {
children: RwLock::new(children),
auth_flow: RwLock::new(auth_flow),
tags: RwLock::new(tags),
discord_rpc,
safety_processes: RwLock::new(safety_processes),
file_watcher: RwLock::new(file_watcher),
}))

View File

@@ -149,6 +149,8 @@ pub struct Profile {
pub memory: Option<MemorySettings>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resolution: Option<WindowSize>,
#[serde(default)]
pub force_fullscreen: SetFullscreen,
#[serde(skip_serializing_if = "Option::is_none")]
pub hooks: Option<Hooks>,
pub projects: HashMap<ProjectPathId, Project>,
@@ -223,6 +225,21 @@ impl ModLoader {
}
}
#[derive(Serialize, Deserialize, Clone, Debug, Copy)]
pub enum SetFullscreen {
#[serde(rename = "Leave unset")]
LeaveUnset,
#[serde(rename = "Set windowed")]
SetWindowed,
#[serde(rename = "Set fullscreen")]
SetFullscreen,
}
impl Default for SetFullscreen {
fn default() -> Self {
Self::LeaveUnset
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct JavaSettings {
#[serde(skip_serializing_if = "Option::is_none")]
@@ -268,6 +285,7 @@ impl Profile {
java: None,
memory: None,
resolution: None,
force_fullscreen: SetFullscreen::LeaveUnset,
hooks: None,
modrinth_update_version: None,
})