diff --git a/packages/oneclient_app/src/hooks/active_cluster.rs b/packages/oneclient_app/src/hooks/active_cluster.rs index b9df83f1..312c48fb 100644 --- a/packages/oneclient_app/src/hooks/active_cluster.rs +++ b/packages/oneclient_app/src/hooks/active_cluster.rs @@ -45,6 +45,8 @@ pub fn use_start_maximized() -> bool { consume_root_context::().0 } +pub const BROWSER_COMPAT_DEFAULT: bool = true; + #[derive(Clone)] pub struct BrowserCompatState(pub State); diff --git a/packages/oneclient_app/src/hooks/mod.rs b/packages/oneclient_app/src/hooks/mod.rs index 09c379c0..3c867a8a 100644 --- a/packages/oneclient_app/src/hooks/mod.rs +++ b/packages/oneclient_app/src/hooks/mod.rs @@ -14,10 +14,10 @@ pub use overlay_claims::{ pub use view_state::{PersistedView, use_view_state}; pub use active_cluster::{ - ActiveClusterState, BrowserCompatState, BrowserStateStore, BrowserTypeState, BrowserUiState, - LinkConfirmState, OnboardingSelectionState, SplashState, StartMaximizedState, - use_active_cluster_id, use_browser_compat, use_browser_state_store, use_browser_type, - use_link_confirm, use_onboarding_selection, use_provide_active_cluster, + ActiveClusterState, BROWSER_COMPAT_DEFAULT, BrowserCompatState, BrowserStateStore, + BrowserTypeState, BrowserUiState, LinkConfirmState, OnboardingSelectionState, SplashState, + StartMaximizedState, use_active_cluster_id, use_browser_compat, use_browser_state_store, + use_browser_type, use_link_confirm, use_onboarding_selection, use_provide_active_cluster, use_provide_browser_compat, use_provide_browser_state, use_provide_browser_type, use_provide_link_confirm, use_provide_onboarding_selection, use_provide_splash, use_provide_start_maximized, use_splash, use_start_maximized, diff --git a/packages/oneclient_app/src/layout/app_shell.rs b/packages/oneclient_app/src/layout/app_shell.rs index fbf7dbe3..6ec9ee2a 100644 --- a/packages/oneclient_app/src/layout/app_shell.rs +++ b/packages/oneclient_app/src/layout/app_shell.rs @@ -21,8 +21,8 @@ use oneclient_core::images::BACKGROUND_IMAGE_EDGE; use oneclient_db::models::ClusterId; use crate::hooks::{ - ActiveClusterState, BrowserCompatState, BrowserStateStore, BrowserTypeState, - use_active_cluster_id, use_clusters, use_game_snapshot, use_launcher, + ActiveClusterState, BROWSER_COMPAT_DEFAULT, BrowserCompatState, BrowserStateStore, + BrowserTypeState, use_active_cluster_id, use_clusters, use_game_snapshot, use_launcher, use_provide_active_cluster, use_provide_browser_compat, use_provide_browser_state, use_provide_browser_type, use_splash, }; @@ -39,7 +39,7 @@ impl Component for AppShell { let active_cluster = use_state(|| None::); use_provide_active_cluster(ActiveClusterState(active_cluster)); - let browser_compat = use_state(|| true); + let browser_compat = use_state(|| BROWSER_COMPAT_DEFAULT); use_provide_browser_compat(BrowserCompatState(browser_compat)); let browser_state = use_state(HashMap::new); diff --git a/packages/oneclient_app/src/view/app/settings/apis.rs b/packages/oneclient_app/src/view/app/settings/apis.rs index 13dd6729..d90128af 100644 --- a/packages/oneclient_app/src/view/app/settings/apis.rs +++ b/packages/oneclient_app/src/view/app/settings/apis.rs @@ -3,7 +3,7 @@ use freya::prelude::*; use super::settings_page; use crate::components::{IconType, TextInput}; use crate::hooks::{use_dispatch, use_settings_snapshot}; -use crate::view::app::settings::{section_header, settings_row}; +use crate::view::app::settings::{resettable, section_header, settings_row}; fn normalize(value: &str) -> Option { let trimmed = value.trim(); @@ -49,12 +49,12 @@ impl Component for SettingsApis { first.set(false); return; } - let mut next = settings.clone(); - next.modrinth_api_key = normalize(&modrinth); - next.curseforge_api_key = normalize(&curseforge); - next.custom_api_endpoint = normalize(&endpoint); - next.custom_meta_url_base = normalize(&meta_url_base); - dispatch.set_settings(next); + dispatch.edit_settings(|settings| { + settings.modrinth_api_key = normalize(&modrinth); + settings.curseforge_api_key = normalize(&curseforge); + settings.custom_api_endpoint = normalize(&endpoint); + settings.custom_meta_url_base = normalize(&meta_url_base); + }); }); settings_page() @@ -63,34 +63,50 @@ impl Component for SettingsApis { IconType::Key01, "Modrinth", "Personal access token used for Modrinth requests.", - TextInput::new(modrinth_key) - .placeholder("Optional") - .width(Size::px(220.)), + resettable( + TextInput::new(modrinth_key) + .placeholder("Optional") + .width(Size::px(220.)), + modrinth_key, + String::new(), + ), )) .child(settings_row( IconType::Key01, "CurseForge", "API key used for CurseForge requests.", - TextInput::new(curseforge_key) - .placeholder("Default") - .width(Size::px(220.)), + resettable( + TextInput::new(curseforge_key) + .placeholder("Default") + .width(Size::px(220.)), + curseforge_key, + String::new(), + ), )) .child(section_header("ADVANCED")) .child(settings_row( IconType::Globe01, "Custom API Endpoint", "Override the default OneClient backend endpoint.", - TextInput::new(custom_api_endpoint) - .placeholder("Default") - .width(Size::px(220.)), + resettable( + TextInput::new(custom_api_endpoint) + .placeholder("Default") + .width(Size::px(220.)), + custom_api_endpoint, + String::new(), + ), )) .child(settings_row( IconType::Globe01, "Custom Meta URL Base", "Override the default bundles and versions data host.", - TextInput::new(custom_meta_url_base) - .placeholder("Default") - .width(Size::px(220.)), + resettable( + TextInput::new(custom_meta_url_base) + .placeholder("Default") + .width(Size::px(220.)), + custom_meta_url_base, + String::new(), + ), )) .into_element() } diff --git a/packages/oneclient_app/src/view/app/settings/appearance.rs b/packages/oneclient_app/src/view/app/settings/appearance.rs index accfc8b6..194ae317 100644 --- a/packages/oneclient_app/src/view/app/settings/appearance.rs +++ b/packages/oneclient_app/src/view/app/settings/appearance.rs @@ -1,11 +1,12 @@ use freya::prelude::*; +use oneclient_core::settings::LauncherSettings; use super::settings_page; use crate::components::{Icon, IconType, toggle}; use crate::hooks::{use_dispatch, use_settings_snapshot}; use crate::theme::colors; use crate::ui::border_all_color; -use crate::view::app::settings::{settings_row, settings_row_disabled}; +use crate::view::app::settings::{resettable, settings_row, settings_row_disabled}; #[derive(Clone, Copy)] struct ThemePreview { @@ -52,6 +53,7 @@ impl Component for SettingsAppearance { let selected_theme = use_state(|| 0usize); let settings = use_settings_snapshot().settings; + let defaults = LauncherSettings::default(); let dispatch = use_dispatch(); let dynamic_bg = use_state({ @@ -72,10 +74,10 @@ impl Component for SettingsAppearance { first.set(false); return; } - let mut next = settings.clone(); - next.dynamic_background_enabled = parallax; - next.animations_enabled = animations; - dispatch.set_settings(next); + dispatch.edit_settings(|settings| { + settings.dynamic_background_enabled = parallax; + settings.animations_enabled = animations; + }); }); settings_page() @@ -88,8 +90,11 @@ impl Component for SettingsAppearance { .padding(Gaps::new(8., 0., 0., 0.)) .child(accent_color_row()) .child(custom_theme_row()) - .child(dynamic_background_row(dynamic_bg)) - .child(animations_row(animations_on)), + .child(dynamic_background_row( + dynamic_bg, + defaults.dynamic_background_enabled, + )) + .child(animations_row(animations_on, defaults.animations_enabled)), ) .child( rect().padding(Gaps::new(8., 0., 0., 0.)).child( @@ -245,20 +250,20 @@ fn custom_theme_row() -> impl IntoElement { ) } -fn dynamic_background_row(enabled: State) -> impl IntoElement { +fn dynamic_background_row(enabled: State, default: bool) -> impl IntoElement { settings_row( IconType::Eye, "Parallax background", "Make the home screen background drift with your cursor. Turning it off keeps the background but disables the motion.", - toggle(enabled), + resettable(toggle(enabled), enabled, default), ) } -fn animations_row(animations_on: State) -> impl IntoElement { +fn animations_row(animations_on: State, default: bool) -> impl IntoElement { settings_row( IconType::Play, "Animations", "Disable all launcher animations and transitions.", - toggle(animations_on), + resettable(toggle(animations_on), animations_on, default), ) } diff --git a/packages/oneclient_app/src/view/app/settings/developer.rs b/packages/oneclient_app/src/view/app/settings/developer.rs index 3c28b5d1..cbbafaef 100644 --- a/packages/oneclient_app/src/view/app/settings/developer.rs +++ b/packages/oneclient_app/src/view/app/settings/developer.rs @@ -1,11 +1,14 @@ use freya::prelude::*; use freya::router::RouterContext; +use oneclient_core::settings::LauncherSettings; use super::settings_page; use crate::Route; use crate::components::{IconType, link_button, toggle}; -use crate::hooks::{use_browser_compat, use_dispatch, use_settings_snapshot}; -use crate::view::app::settings::{section_header, settings_row}; +use crate::hooks::{ + BROWSER_COMPAT_DEFAULT, use_browser_compat, use_dispatch, use_settings_snapshot, +}; +use crate::view::app::settings::{resettable, section_header, settings_row}; use crate::view::console::open_log_console; #[derive(PartialEq)] @@ -14,6 +17,7 @@ pub struct SettingsDeveloper; impl Component for SettingsDeveloper { fn render(&self) -> impl IntoElement { let settings = use_settings_snapshot().settings; + let defaults = LauncherSettings::default(); let dispatch = use_dispatch(); let log_debug = use_state({ @@ -28,9 +32,7 @@ impl Component for SettingsDeveloper { first.set(false); return; } - let mut next = settings.clone(); - next.log_debug = enabled; - dispatch.set_settings(next); + dispatch.edit_settings(|settings| settings.log_debug = enabled); }); let browser_compat = use_browser_compat(); @@ -41,14 +43,18 @@ impl Component for SettingsDeveloper { IconType::SearchMd, "Compatible content only", "Filter the content browser to the active cluster's version and loader.", - toggle(browser_compat), + resettable( + toggle(browser_compat), + browser_compat, + BROWSER_COMPAT_DEFAULT, + ), )) .child(section_header("DEV TOOLS")) .child(settings_row( IconType::Sliders04, "Log Debug Info", "WARNING! This requires a restart to apply. Logs out debug info.", - toggle(log_debug), + resettable(toggle(log_debug), log_debug, defaults.log_debug), )) .child(settings_row( IconType::Terminal, diff --git a/packages/oneclient_app/src/view/app/settings/launcher.rs b/packages/oneclient_app/src/view/app/settings/launcher.rs index 119a424a..ec689bfd 100644 --- a/packages/oneclient_app/src/view/app/settings/launcher.rs +++ b/packages/oneclient_app/src/view/app/settings/launcher.rs @@ -3,10 +3,10 @@ use std::path::{Path, PathBuf}; use freya::prelude::*; use freya::router::RouterContext; use oneclient_core::relocate::RelocationPlan; -use oneclient_core::settings::{LaunchBehaviour, data_dir}; +use oneclient_core::settings::{LaunchBehaviour, LauncherSettings, data_dir}; use oneclient_core::storage::format_bytes; -use super::{section_header, settings_page, settings_row}; +use super::{resettable, section_header, settings_page, settings_row}; use crate::Route; use crate::components::{ Button, Dropdown, Icon, IconType, OverlayPopup, open_folder_button, toggle, @@ -24,6 +24,7 @@ pub struct SettingsLauncher; impl Component for SettingsLauncher { fn render(&self) -> impl IntoElement { let settings = use_settings_snapshot().settings; + let defaults = LauncherSettings::default(); let dispatch = use_dispatch(); let discord_rpc = use_state({ @@ -51,28 +52,32 @@ impl Component for SettingsLauncher { move || v }); + let launch_behaviour = use_state({ + let v = settings.launch_behaviour; + move || v + }); + let mut first = use_state(|| true); - { - let dispatch = dispatch.clone(); - use_side_effect(move || { - let discord = *discord_rpc.read(); - let crash = *crash_reporting.read(); - let maximized = *start_maximized.read(); - let background = *run_in_background.read(); - let tray = *show_tray_icon.read(); - if *first.peek() { - first.set(false); - return; - } - dispatch.edit_settings(|next| { - next.discord_enabled = discord; - next.crash_reporting = crash; - next.start_maximized = maximized; - next.run_in_background = background; - next.show_tray_icon = tray; - }); + use_side_effect(move || { + let discord = *discord_rpc.read(); + let crash = *crash_reporting.read(); + let maximized = *start_maximized.read(); + let background = *run_in_background.read(); + let tray = *show_tray_icon.read(); + let behaviour = *launch_behaviour.read(); + if *first.peek() { + first.set(false); + return; + } + dispatch.edit_settings(|next| { + next.discord_enabled = discord; + next.crash_reporting = crash; + next.start_maximized = maximized; + next.run_in_background = background; + next.show_tray_icon = tray; + next.launch_behaviour = behaviour; }); - } + }); // The only way back for someone who declined during onboarding let consent_summary = if settings.declined_tos { @@ -94,19 +99,27 @@ impl Component for SettingsLauncher { IconType::Link03, "Discord RPC", "Enable Discord Rich Presence.", - toggle(discord_rpc), + resettable(toggle(discord_rpc), discord_rpc, defaults.discord_enabled), )) .child(settings_row( IconType::AlertTriangle, "Crash Reporting", "Send anonymous crash and error reports to help fix bugs. Applies on restart.", - toggle(crash_reporting), + resettable( + toggle(crash_reporting), + crash_reporting, + defaults.crash_reporting, + ), )) .child(settings_row( IconType::Maximize01, "Start Maximized", "Open the launcher window maximized. Applies on restart.", - toggle(start_maximized), + resettable( + toggle(start_maximized), + start_maximized, + defaults.start_maximized, + ), )) .child(settings_row( IconType::File02, @@ -119,19 +132,31 @@ impl Component for SettingsLauncher { IconType::Eye, "While Playing", "What the launcher window does once a game is running.", - launch_behaviour_field(settings.launch_behaviour, dispatch), + resettable( + launch_behaviour_field(launch_behaviour), + launch_behaviour, + defaults.launch_behaviour, + ), )) .child(settings_row( IconType::Moon01, "Run in Background", "Closing the window keeps OneClient running. Open it again from the tray icon or by launching it.", - toggle(run_in_background), + resettable( + toggle(run_in_background), + run_in_background, + defaults.run_in_background, + ), )) .child(settings_row( IconType::LayoutTop, "Show Tray Icon", "Show OneClient in the system tray or menu bar. Applies on restart.", - toggle(show_tray_icon), + resettable( + toggle(show_tray_icon), + show_tray_icon, + defaults.show_tray_icon, + ), )) .child(section_header("FOLDERS AND FILES")) .child(DataFolder.into_element()) @@ -139,18 +164,18 @@ impl Component for SettingsLauncher { } } -fn launch_behaviour_field(selected: LaunchBehaviour, dispatch: Actions) -> impl IntoElement { +fn launch_behaviour_field(mut selected: State) -> impl IntoElement { let options: Vec = LaunchBehaviour::ALL .iter() .map(|behaviour| behaviour.label().to_string()) .collect(); - Dropdown::new(selected.label(), options) + Dropdown::new(selected.read().label(), options) .width(Size::px(220.)) .height(Size::px(34.)) .on_select(move |idx: usize| { if let Some(behaviour) = LaunchBehaviour::ALL.get(idx).copied() { - dispatch.edit_settings(|settings| settings.launch_behaviour = behaviour); + selected.set(behaviour); } }) } diff --git a/packages/oneclient_app/src/view/app/settings/minecraft.rs b/packages/oneclient_app/src/view/app/settings/minecraft.rs index d55db3d2..ee29059d 100644 --- a/packages/oneclient_app/src/view/app/settings/minecraft.rs +++ b/packages/oneclient_app/src/view/app/settings/minecraft.rs @@ -2,17 +2,15 @@ use freya::prelude::*; use oneclient_common::Patch; #[cfg(any(target_os = "linux", windows))] use oneclient_core::settings::SettingsOsExtra; -use oneclient_core::settings::{PackageUpdateMode, ProfileUpdate, Resolution}; +use oneclient_core::settings::{GameSettingsProfile, PackageUpdateMode, ProfileUpdate, Resolution}; use super::settings_page; -#[cfg(any(target_os = "linux", windows))] -use crate::components::toggle_controlled; use crate::components::{ Dropdown, Icon, IconType, TextInput, memory_field, toggle, validate_number, }; use crate::hooks::{use_dispatch, use_settings_snapshot}; use crate::theme::colors; -use crate::view::app::settings::{section_header, settings_row}; +use crate::view::app::settings::{resettable, resettable_all, section_header, settings_row}; #[derive(PartialEq)] pub struct SettingsMinecraft; @@ -21,6 +19,7 @@ impl Component for SettingsMinecraft { fn render(&self) -> impl IntoElement { let settings = use_settings_snapshot().settings; let profile = settings.global_game_settings.clone(); + let defaults = GameSettingsProfile::default_global_profile(); let dispatch = use_dispatch(); let fullscreen = use_state({ @@ -61,9 +60,18 @@ impl Component for SettingsMinecraft { let v = profile.hook_post.clone().unwrap_or_default(); move || v }); + let update_mode = use_state({ + let v = profile.browser_update_mode.unwrap_or_default(); + move || v + }); + + #[cfg(any(target_os = "linux", windows))] + let discrete_gpu = use_state({ + let v = profile.use_discrete_gpu(); + move || v + }); let mut first = use_state(|| true); - let batched = dispatch.clone(); use_side_effect(move || { let update = build_update( *fullscreen.read(), @@ -74,51 +82,92 @@ impl Component for SettingsMinecraft { &pre_launch_command.read(), &wrapper_command.read(), &post_exit_command.read(), + *update_mode.read(), ); + #[cfg(any(target_os = "linux", windows))] + let gpu = *discrete_gpu.read(); if *first.peek() { first.set(false); return; } - batched.update_global_profile(update); + + #[cfg(any(target_os = "linux", windows))] + let update = with_discrete_gpu(update, gpu); + + dispatch.update_global_profile(update); }); + #[cfg(any(target_os = "linux", windows))] + let discrete_gpu_default = SettingsOsExtra::default().use_discrete_gpu.unwrap_or(true); + let page = settings_page() .child(section_header("GAME")) .child(settings_row( IconType::Maximize01, "Force Fullscreen", "Force Minecraft to start in fullscreen mode.", - toggle(fullscreen), + resettable( + toggle(fullscreen), + fullscreen, + defaults.force_fullscreen.unwrap_or(false), + ), )) .child(settings_row( IconType::LayoutTop, "Resolution", "The game window resolution in pixels.", - resolution_field(width, height), + resettable_all( + resolution_field(width, height), + vec![ + ( + width, + defaults + .resolution + .map(|r| r.width.to_string()) + .unwrap_or_default(), + ), + ( + height, + defaults + .resolution + .map(|r| r.height.to_string()) + .unwrap_or_default(), + ), + ], + ), )) .child(settings_row( IconType::Database01, "Memory", "The amount of memory in megabytes allocated for the game. Presets leave 2 GB for the system.", - memory_field(memory, "Default", oneclient_common::default_mem_max()), + resettable( + memory_field(memory, "Default", oneclient_common::default_mem_max()), + memory, + String::new(), + ), )) .child(settings_row( IconType::Terminal, "JVM Arguments", "Extra arguments passed to Java. Separate them with spaces; quote values containing spaces.", - TextInput::new(jvm_args) - .placeholder("-XX:+UseG1GC") - .expandable(true) - .width(Size::px(220.)), + resettable( + TextInput::new(jvm_args) + .placeholder("-XX:+UseG1GC") + .expandable(true) + .width(Size::px(220.)), + jvm_args, + defaults.launch_args.clone().unwrap_or_default(), + ), )) .child(section_header("CONTENT")) .child(settings_row( IconType::RefreshCw01, "Browser Package Updates", "What to do when content you installed from the browser has a newer version. Packs from bundles are not affected.", - update_mode_field( - profile.browser_update_mode.unwrap_or_default(), - dispatch.clone(), + resettable( + update_mode_field(update_mode), + update_mode, + defaults.browser_update_mode.unwrap_or_default(), ), )) .child(section_header("PROCESS")) @@ -126,64 +175,68 @@ impl Component for SettingsMinecraft { IconType::FilePlus02, "Pre-Launch Command", "Command to run before launching the game.", - TextInput::new(pre_launch_command) - .placeholder("echo 'Game started'") - .expandable(true) - .width(Size::px(220.)), + resettable( + TextInput::new(pre_launch_command) + .placeholder("echo 'Game started'") + .expandable(true) + .width(Size::px(220.)), + pre_launch_command, + defaults.hook_pre.clone().unwrap_or_default(), + ), )) .child(settings_row( IconType::ParagraphWrap, "Wrapper Command", "Command to run when launching the game.", - TextInput::new(wrapper_command) - .placeholder("gamescope") - .expandable(true) - .width(Size::px(220.)), + resettable( + TextInput::new(wrapper_command) + .placeholder("gamescope") + .expandable(true) + .width(Size::px(220.)), + wrapper_command, + defaults.hook_wrapper.clone().unwrap_or_default(), + ), )) .child(settings_row( IconType::FileX02, "Post-Exit Command", "Command to run after exiting the game.", - TextInput::new(post_exit_command) - .placeholder("echo 'Game exited'") - .expandable(true) - .width(Size::px(220.)), + resettable( + TextInput::new(post_exit_command) + .placeholder("echo 'Game exited'") + .expandable(true) + .width(Size::px(220.)), + post_exit_command, + defaults.hook_post.clone().unwrap_or_default(), + ), )); #[cfg(any(target_os = "linux", windows))] let page = page .child(section_header("GRAPHICS")) - .child(discrete_gpu_row(profile.os_extra.clone(), dispatch)); + .child(settings_row( + IconType::Rocket02, + "Use Discrete GPU", + "Render the game on the dedicated graphics card. Does nothing on a machine with only one GPU, and draws noticeably more power on a laptop.", + resettable(toggle(discrete_gpu), discrete_gpu, discrete_gpu_default), + )); page.into_element() } } #[cfg(any(target_os = "linux", windows))] -fn discrete_gpu_row( - os_extra: Option, - dispatch: crate::Actions, -) -> impl IntoElement { - let current = os_extra.unwrap_or_default(); - let on = current.use_discrete_gpu.unwrap_or(true); +fn with_discrete_gpu(mut update: ProfileUpdate, on: bool) -> ProfileUpdate { + let base = crate::launcher::state() + .ok() + .and_then(|state| state.settings.read().global_game_settings.os_extra.clone()) + .unwrap_or_default(); - let on_toggle: EventHandler<()> = (move |()| { - dispatch.update_global_profile(ProfileUpdate { - os_extra: Patch::Set(SettingsOsExtra { - use_discrete_gpu: Some(!on), - ..current.clone() - }), - ..Default::default() - }); - }) - .into(); - - settings_row( - IconType::Rocket02, - "Use Discrete GPU", - "Render the game on the dedicated graphics card. Does nothing on a machine with only one GPU, and draws noticeably more power on a laptop.", - toggle_controlled(on, on_toggle), - ) + update.os_extra = Patch::Set(SettingsOsExtra { + use_discrete_gpu: Some(on), + ..base + }); + update } #[allow(clippy::too_many_arguments)] @@ -196,6 +249,7 @@ fn build_update( pre: &str, wrapper: &str, post: &str, + update_mode: PackageUpdateMode, ) -> ProfileUpdate { let resolution = match (width.trim(), height.trim()) { ("", "") => Patch::Clear, @@ -218,26 +272,23 @@ fn build_update( hook_pre: command_patch(pre), hook_wrapper: command_patch(wrapper), hook_post: command_patch(post), + browser_update_mode: Patch::Set(update_mode), ..Default::default() } } -/// Dispatched separately from [`build_update`] which debounces keystrokes a dropdown has no intermediate states -fn update_mode_field(selected: PackageUpdateMode, dispatch: crate::Actions) -> impl IntoElement { +fn update_mode_field(mut selected: State) -> impl IntoElement { let options: Vec = PackageUpdateMode::ALL .iter() .map(|mode| mode.label().to_string()) .collect(); - Dropdown::new(selected.label(), options) + Dropdown::new(selected.read().label(), options) .width(Size::px(220.)) .height(Size::px(34.)) .on_select(move |idx: usize| { if let Some(mode) = PackageUpdateMode::ALL.get(idx).copied() { - dispatch.update_global_profile(ProfileUpdate { - browser_update_mode: Patch::Set(mode), - ..Default::default() - }); + selected.set(mode); } }) } diff --git a/packages/oneclient_app/src/view/app/settings/mod.rs b/packages/oneclient_app/src/view/app/settings/mod.rs index 5f41b7b1..37172085 100644 --- a/packages/oneclient_app/src/view/app/settings/mod.rs +++ b/packages/oneclient_app/src/view/app/settings/mod.rs @@ -24,7 +24,7 @@ pub use minecraft::SettingsMinecraft; pub use storage::SettingsStorage; use crate::{ - components::{Icon, IconType}, + components::{Button, Icon, IconType}, theme::colors, }; @@ -104,3 +104,61 @@ fn settings_row_inner( .child(trailing) .into_element() } + +#[derive(PartialEq)] +struct ResetButton { + values: Vec<(State, T)>, +} + +impl Component for ResetButton { + fn render(&self) -> impl IntoElement { + let at_default = self + .values + .iter() + .all(|(value, default)| *value.read() == *default); + + let values = self.values.clone(); + + Button::new() + .ghost() + .icon() + .disabled(at_default) + .alt(if at_default { + "Already at the default" + } else { + "Reset to default" + }) + .on_press(move |_| { + for (value, default) in &values { + let mut value = *value; + value.set(default.clone()); + } + }) + .child( + Icon::new(IconType::RefreshCcw02) + .size(14.) + .color(colors::fg_secondary()), + ) + } +} + +pub fn resettable( + control: impl IntoElement, + value: State, + default: T, +) -> impl IntoElement { + resettable_all(control, vec![(value, default)]) +} + +pub fn resettable_all( + control: impl IntoElement, + values: Vec<(State, T)>, +) -> impl IntoElement { + rect() + .horizontal() + .cross_align(Alignment::Center) + .spacing(8.) + .child(control) + .child(ResetButton { values }) + .into_element() +}