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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 367 additions and 56 deletions

48
Cargo.lock generated
View File

@ -575,7 +575,7 @@ checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f"
dependencies = [
"byteorder",
"fnv",
"uuid",
"uuid 1.4.0",
]
[[package]]
@ -986,7 +986,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d"
dependencies = [
"serde",
"uuid",
"uuid 1.4.0",
]
[[package]]
@ -1107,6 +1107,18 @@ dependencies = [
"winapi",
]
[[package]]
name = "discord-rich-presence"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47fc4beffb85ee1461588499073a4d9c20dcc7728c4b13d6b282ab6c508947e5"
dependencies = [
"serde",
"serde_derive",
"serde_json",
"uuid 0.8.2",
]
[[package]]
name = "dispatch"
version = "0.2.0"
@ -2522,7 +2534,7 @@ dependencies = [
"crash-handler",
"minidumper",
"thiserror",
"uuid",
"uuid 1.4.0",
]
[[package]]
@ -3810,7 +3822,7 @@ dependencies = [
"thiserror",
"time 0.3.22",
"url",
"uuid",
"uuid 1.4.0",
]
[[package]]
@ -4299,7 +4311,7 @@ dependencies = [
"serde",
"tao-macros",
"unicode-segmentation",
"uuid",
"uuid 1.4.0",
"windows 0.39.0",
"windows-implement",
"x11-dl",
@ -4382,7 +4394,7 @@ dependencies = [
"time 0.3.22",
"tokio",
"url",
"uuid",
"uuid 1.4.0",
"webkit2gtk",
"webview2-com",
"windows 0.39.0",
@ -4428,7 +4440,7 @@ dependencies = [
"tauri-utils",
"thiserror",
"time 0.3.22",
"uuid",
"uuid 1.4.0",
"walkdir",
]
@ -4506,7 +4518,7 @@ dependencies = [
"tauri-utils",
"thiserror",
"url",
"uuid",
"uuid 1.4.0",
"webview2-com",
"windows 0.39.0",
]
@ -4524,7 +4536,7 @@ dependencies = [
"raw-window-handle",
"tauri-runtime",
"tauri-utils",
"uuid",
"uuid 1.4.0",
"webkit2gtk",
"webview2-com",
"windows 0.39.0",
@ -4606,6 +4618,7 @@ dependencies = [
"chrono",
"daedalus",
"dirs 5.0.1",
"discord-rich-presence",
"dunce",
"futures",
"indicatif",
@ -4633,7 +4646,7 @@ dependencies = [
"tracing-error 0.1.2",
"tracing-subscriber 0.2.25",
"url",
"uuid",
"uuid 1.4.0",
"whoami",
"winreg 0.50.0",
"zip",
@ -4661,7 +4674,7 @@ dependencies = [
"tracing-futures",
"tracing-subscriber 0.3.17",
"url",
"uuid",
"uuid 1.4.0",
"webbrowser",
"winreg 0.11.0",
]
@ -4695,7 +4708,7 @@ dependencies = [
"tracing",
"tracing-error 0.1.2",
"url",
"uuid",
"uuid 1.4.0",
"window-shadows",
]
@ -4725,7 +4738,7 @@ dependencies = [
"tracing-error 0.1.2",
"tracing-subscriber 0.2.25",
"url",
"uuid",
"uuid 1.4.0",
"webbrowser",
]
@ -5209,6 +5222,15 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "uuid"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
dependencies = [
"getrandom 0.2.10",
]
[[package]]
name = "uuid"
version = "1.4.0"

View File

@ -55,6 +55,8 @@ dunce = "1.0.3"
whoami = "1.4.0"
discord-rich-presence = "0.2.3"
[target.'cfg(windows)'.dependencies]
winreg = "0.50.0"

View File

@ -27,7 +27,7 @@ pub mod prelude {
jre, metadata, pack, process,
profile::{self, create, Profile},
settings,
state::JavaGlobals,
state::{JavaGlobals, SetFullscreen},
state::{ProfilePathId, ProjectPathId},
util::{
io::{canonicalize, IOError},

View File

@ -1,7 +1,6 @@
use std::{collections::HashMap, path::PathBuf};
use serde::{Deserialize, Serialize};
use tokio::fs;
use crate::{
event::LoadingBarId,
@ -106,7 +105,7 @@ pub struct ATLauncherMod {
// Check if folder has a instance.json that parses
pub async fn is_valid_atlauncher(instance_folder: PathBuf) -> bool {
let instance: String =
fs::read_to_string(&instance_folder.join("instance.json"))
io::read_to_string(&instance_folder.join("instance.json"))
.await
.unwrap_or("".to_string());
let instance: Result<ATInstance, serde_json::Error> =

View File

@ -1,7 +1,6 @@
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use tokio::fs;
use crate::{
prelude::{ModLoader, ProfilePathId},
@ -42,7 +41,7 @@ pub struct MinecraftInstance {
// Check if folder has a minecraftinstance.json that parses
pub async fn is_valid_curseforge(instance_folder: PathBuf) -> bool {
let minecraftinstance: String =
fs::read_to_string(&instance_folder.join("minecraftinstance.json"))
io::read_to_string(&instance_folder.join("minecraftinstance.json"))
.await
.unwrap_or("".to_string());
let minecraftinstance: Result<MinecraftInstance, serde_json::Error> =

View File

@ -1,7 +1,6 @@
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use tokio::fs;
use crate::{
prelude::{ModLoader, ProfilePathId},
@ -32,7 +31,7 @@ pub struct GDLauncherLoader {
// Check if folder has a config.json that parses
pub async fn is_valid_gdlauncher(instance_folder: PathBuf) -> bool {
let config: String =
fs::read_to_string(&instance_folder.join("config.json"))
io::read_to_string(&instance_folder.join("config.json"))
.await
.unwrap_or("".to_string());
let config: Result<GDLauncherConfig, serde_json::Error> =

View File

@ -1,7 +1,6 @@
use std::path::{Path, PathBuf};
use serde::{de, Deserialize, Serialize};
use tokio::fs;
use crate::{
pack::{
@ -126,7 +125,7 @@ pub async fn is_valid_mmc(instance_folder: PathBuf) -> bool {
let instance_cfg = instance_folder.join("instance.cfg");
let mmc_pack = instance_folder.join("mmc-pack.json");
let mmc_pack = match fs::read_to_string(&mmc_pack).await {
let mmc_pack = match io::read_to_string(&mmc_pack).await {
Ok(mmc_pack) => mmc_pack,
Err(_) => return false,
};

View File

@ -66,45 +66,46 @@ pub async fn get_importable_instances(
}
// Import an instance from a launcher type and base path
// Note: this *deletes* the submitted empty profile
#[theseus_macros::debug_pin]
#[tracing::instrument]
pub async fn import_instance(
profile_path: ProfilePathId,
profile_path: ProfilePathId, // This should be a blank profile
launcher_type: ImportLauncherType,
base_path: PathBuf,
instance_folder: String,
) -> crate::Result<()> {
tracing::debug!("Importing instance from {instance_folder}");
match launcher_type {
let res = match launcher_type {
ImportLauncherType::MultiMC | ImportLauncherType::PrismLauncher => {
mmc::import_mmc(
base_path, // path to base mmc folder
instance_folder, // instance folder in mmc_base_path
profile_path, // path to profile
base_path, // path to base mmc folder
instance_folder, // instance folder in mmc_base_path
profile_path.clone(), // path to profile
)
.await?;
.await
}
ImportLauncherType::ATLauncher => {
atlauncher::import_atlauncher(
base_path, // path to atlauncher folder
instance_folder, // instance folder in atlauncher
profile_path, // path to profile
base_path, // path to atlauncher folder
instance_folder, // instance folder in atlauncher
profile_path.clone(), // path to profile
)
.await?;
.await
}
ImportLauncherType::GDLauncher => {
gdlauncher::import_gdlauncher(
base_path.join("instances").join(instance_folder), // path to gdlauncher folder
profile_path, // path to profile
profile_path.clone(), // path to profile
)
.await?;
.await
}
ImportLauncherType::Curseforge => {
curseforge::import_curseforge(
base_path.join("Instances").join(instance_folder), // path to curseforge folder
profile_path, // path to profile
profile_path.clone(), // path to profile
)
.await?;
.await
}
ImportLauncherType::Unknown => {
return Err(crate::ErrorKind::InputError(
@ -112,6 +113,16 @@ pub async fn import_instance(
)
.into());
}
};
// If import failed, delete the profile
match res {
Ok(_) => {}
Err(e) => {
tracing::warn!("Import failed: {:?}", e);
let _ = crate::api::profile::remove(&profile_path).await;
return Err(e);
}
}
// Check existing managed packs for potential updates

View File

@ -121,7 +121,7 @@ pub async fn wait_for_by_uuid(uuid: &Uuid) -> crate::Result<()> {
}
}
// Kill a running child process directly, and wait for it to be killed
// Kill a running child process directly
#[tracing::instrument(skip(running))]
pub async fn kill(running: &mut MinecraftChild) -> crate::Result<()> {
running
@ -131,7 +131,7 @@ pub async fn kill(running: &mut MinecraftChild) -> crate::Result<()> {
.kill()
.await
.map_err(IOError::from)?;
wait_for(running).await
Ok(())
}
// Await on the completion of a child process directly

View File

@ -7,7 +7,9 @@ use crate::event::LoadingBarType;
use crate::pack::install_from::{
EnvType, PackDependency, PackFile, PackFileHash, PackFormat,
};
use crate::prelude::{JavaVersion, ProfilePathId, ProjectPathId};
use crate::prelude::{
JavaVersion, ProfilePathId, ProjectPathId, SetFullscreen,
};
use crate::state::ProjectMetadata;
use crate::util::io::{self, IOError};
@ -838,9 +840,23 @@ pub async fn run_credentials(
None
};
// Any options.txt settings that we want set, add here
let mut mc_set_options: Vec<(String, String)> = Vec::new();
match profile.force_fullscreen {
SetFullscreen::LeaveUnset => {}
SetFullscreen::SetWindowed => {
mc_set_options
.push(("fullscreen".to_string(), "false".to_string()));
}
SetFullscreen::SetFullscreen => {
mc_set_options.push(("fullscreen".to_string(), "true".to_string()));
}
}
let mc_process = crate::launcher::launch_minecraft(
java_args,
env_args,
&mc_set_options,
wrapper,
&memory,
&resolution,

View File

@ -49,6 +49,9 @@ pub enum ErrorKind {
#[error("Incorrect Sha1 hash for download: {0} != {1}")]
HashError(String, String),
#[error("Regex error: {0}")]
RegexError(#[from] regex::Error),
#[error("Paths stored in the database need to be valid UTF-8: {0}")]
UTFError(std::path::PathBuf),

View File

@ -2,6 +2,7 @@
use crate::event::emit::{emit_loading, init_or_edit_loading};
use crate::event::{LoadingBarId, LoadingBarType};
use crate::jre::{self, JAVA_17_KEY, JAVA_18PLUS_KEY, JAVA_8_KEY};
use crate::launcher::io::IOError;
use crate::prelude::JavaVersion;
use crate::state::ProfileInstallStage;
use crate::util::io;
@ -164,6 +165,16 @@ pub async fn install_minecraft(
)
})?;
// Test jre version
let java_version = jre::check_jre(java_version.path.clone().into())
.await?
.ok_or_else(|| {
crate::ErrorKind::LauncherError(format!(
"Java path invalid or non-functional: {}",
java_version.path
))
})?;
// Download minecraft (5-90)
download::download_minecraft(
&state,
@ -246,6 +257,7 @@ pub async fn install_minecraft(
)?)
.output()
.await
.map_err(|e| IOError::with_path(e, &java_version.path))
.map_err(|err| {
crate::ErrorKind::LauncherError(format!(
"Error running processor: {err}",
@ -291,6 +303,7 @@ pub async fn install_minecraft(
pub async fn launch_minecraft(
java_args: &[String],
env_args: &[(String, String)],
mc_set_options: &[(String, String)],
wrapper: &Option<String>,
memory: &st::MemorySettings,
resolution: &st::WindowSize,
@ -440,6 +453,33 @@ pub async fn launch_minecraft(
}
command.envs(env_args);
// Overwrites the minecraft options.txt file with the settings from the profile
// Uses 'a:b' syntax which is not quite yaml
use regex::Regex;
let options_path = instance_path.join("options.txt");
let mut options_string = String::new();
if options_path.exists() {
options_string = io::read_to_string(&options_path).await?;
}
for (key, value) in mc_set_options {
let re = Regex::new(&format!(r"(?m)^{}:.*$", regex::escape(key)))?;
// check if the regex exists in the file
if !re.is_match(&options_string) {
// The key was not found in the file, so append it
options_string.push_str(&format!("\n{}:{}", key, value));
} else {
let replaced_string = re
.replace_all(&options_string, &format!("{}:{}", key, value))
.to_string();
options_string = replaced_string;
}
}
io::write(&options_path, options_string).await?;
// Get Modrinth logs directories
let datetime_string =
chrono::Local::now().format("%Y%m%d_%H%M%S").to_string();
@ -501,6 +541,14 @@ pub async fn launch_minecraft(
}
}
{
// Add game played to discord rich presence
let _ = state
.discord_rpc
.set_activity(&format!("Playing {}", profile.metadata.name), true)
.await;
}
// Create Minecraft child by inserting it into the state
// This also spawns the process and prepares the subsequent processes
let mut state_children = state.children.write().await;

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,
})

View File

@ -46,11 +46,9 @@ pub async fn get_all_jre() -> Result<Vec<JavaVersion>, JREError> {
];
for java_path in java_paths {
let Ok(java_subpaths) = std::fs::read_dir(java_path) else {continue };
for java_subpath in java_subpaths {
if let Ok(java_subpath) = java_subpath {
let path = java_subpath.path();
jre_paths.insert(path.join("bin"));
}
for java_subpath in java_subpaths.flatten() {
let path = java_subpath.path();
jre_paths.insert(path.join("bin"));
}
}
@ -93,19 +91,17 @@ pub async fn get_all_jre() -> Result<Vec<JavaVersion>, JREError> {
pub fn get_paths_from_jre_winregkey(jre_key: RegKey) -> HashSet<PathBuf> {
let mut jre_paths = HashSet::new();
for subkey in jre_key.enum_keys() {
if let Ok(subkey) = subkey {
if let Ok(subkey) = jre_key.open_subkey(subkey) {
let subkey_value_names =
[r"JavaHome", r"InstallationPath", r"\\hotspot\\MSI"];
for subkey in jre_key.enum_keys().flatten() {
if let Ok(subkey) = jre_key.open_subkey(subkey) {
let subkey_value_names =
[r"JavaHome", r"InstallationPath", r"\\hotspot\\MSI"];
for subkey_value in subkey_value_names {
let path: Result<String, std::io::Error> =
subkey.get_value(subkey_value);
let Ok(path) = path else {continue};
for subkey_value in subkey_value_names {
let path: Result<String, std::io::Error> =
subkey.get_value(subkey_value);
let Ok(path) = path else {continue};
jre_paths.insert(PathBuf::from(path).join("bin"));
}
jre_paths.insert(PathBuf::from(path).join("bin"));
}
}
}

View File

@ -29,6 +29,7 @@ pub async fn import_get_importable_instances(
}
/// Import an instance from a launcher type and base path
/// profile_path should be a blank profile for this purpose- if the function fails, it will be deleted
/// eg: import_instance(ImportLauncherType::MultiMC, PathBuf::from("C:/MultiMC"), "Instance 1")
#[tauri::command]
pub async fn import_import_instance(

View File

@ -276,6 +276,7 @@ pub struct EditProfile {
pub memory: Option<MemorySettings>,
pub resolution: Option<WindowSize>,
pub hooks: Option<Hooks>,
pub force_fullscreen: Option<SetFullscreen>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
@ -315,6 +316,9 @@ pub async fn profile_edit(
prof.java = edit_profile.java.clone();
prof.memory = edit_profile.memory;
prof.resolution = edit_profile.resolution;
if let Some(force_fullscreen) = edit_profile.force_fullscreen {
prof.force_fullscreen = force_fullscreen;
}
prof.hooks = edit_profile.hooks.clone();
prof.metadata.date_modified = chrono::Utc::now();

View File

@ -182,6 +182,9 @@
<span class="label__title size-card-header">Window</span>
</h3>
</div>
<div class="adjacent-input">
<DropdownSelect v-model="forceFullscreen" :options="fullscreenOptions" />
</div>
<div class="adjacent-input">
<Checkbox v-model="overrideWindowSettings" label="Override global window settings" />
</div>
@ -439,10 +442,12 @@ const maxMemory = Math.floor((await get_max_memory().catch(handleError)) / 1024)
const overrideWindowSettings = ref(!!props.instance.resolution)
const resolution = ref(props.instance.resolution ?? globalSettings.game_resolution)
const overrideHooks = ref(!!props.instance.hooks)
const hooks = ref(props.instance.hooks ?? globalSettings.hooks)
const fullscreenOptions = ref(['Leave unchanged', 'Set windowed', 'Set fullscreen'])
const forceFullscreen = ref(props.instance.force_fullscreen)
watch(
[
title,
@ -458,6 +463,7 @@ watch(
memory,
overrideWindowSettings,
resolution,
forceFullscreen,
overrideHooks,
hooks,
],
@ -505,6 +511,10 @@ watch(
editProfile.resolution = resolution.value
}
if (forceFullscreen.value) {
editProfile.force_fullscreen = forceFullscreen.value
}
if (overrideHooks.value) {
editProfile.hooks = hooks.value
}