From a00e11d2049375c50067ac65930faa7307983c3c Mon Sep 17 00:00:00 2001 From: Maciek <108873945+rozwader@users.noreply.github.com> Date: Wed, 16 Sep 2026 20:29:14 +0200 Subject: [PATCH] feat: control center and smaller navbar link gaps (#832) * feat: added a control center * feat: added no account signed in handling; added a modal on sign out clicked * feat: use the discord icon for the discord rpc quick setting * style: fade out disabled quick setting tiles * style: drop the 'Active' suffix from the control center account subtitle * feat: added a gear indicator to the navbar avatar button * add small gear icon on avatar --------- Co-authored-by: LynithDev <61880709+LynithDev@users.noreply.github.com> --- .../assets/icons/bar-chart-square-02.svg | 3 + .../oneclient_app/assets/icons/discord.svg | 1 + .../oneclient_app/assets/icons/log-out-01.svg | 3 + .../oneclient_app/assets/icons/moon-01.svg | 3 + .../src/components/account_switcher.rs | 54 +- .../oneclient_app/src/components/button.rs | 9 +- .../src/components/control_center.rs | 634 ++++++++++++++++++ .../oneclient_app/src/components/icons.rs | 4 + packages/oneclient_app/src/components/mod.rs | 2 + .../src/components/navbar/app_navbar.rs | 58 +- packages/oneclient_app/src/hooks/actions.rs | 39 ++ packages/oneclient_app/src/hooks/mod.rs | 4 + .../oneclient_app/src/layout/root_layout.rs | 3 +- packages/oneclient_app/src/state.rs | 2 + 14 files changed, 793 insertions(+), 26 deletions(-) create mode 100644 packages/oneclient_app/assets/icons/bar-chart-square-02.svg create mode 100644 packages/oneclient_app/assets/icons/discord.svg create mode 100644 packages/oneclient_app/assets/icons/log-out-01.svg create mode 100644 packages/oneclient_app/assets/icons/moon-01.svg create mode 100644 packages/oneclient_app/src/components/control_center.rs diff --git a/packages/oneclient_app/assets/icons/bar-chart-square-02.svg b/packages/oneclient_app/assets/icons/bar-chart-square-02.svg new file mode 100644 index 00000000..50591e14 --- /dev/null +++ b/packages/oneclient_app/assets/icons/bar-chart-square-02.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/oneclient_app/assets/icons/discord.svg b/packages/oneclient_app/assets/icons/discord.svg new file mode 100644 index 00000000..81605170 --- /dev/null +++ b/packages/oneclient_app/assets/icons/discord.svg @@ -0,0 +1 @@ + diff --git a/packages/oneclient_app/assets/icons/log-out-01.svg b/packages/oneclient_app/assets/icons/log-out-01.svg new file mode 100644 index 00000000..14cdf901 --- /dev/null +++ b/packages/oneclient_app/assets/icons/log-out-01.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/oneclient_app/assets/icons/moon-01.svg b/packages/oneclient_app/assets/icons/moon-01.svg new file mode 100644 index 00000000..d6cbd869 --- /dev/null +++ b/packages/oneclient_app/assets/icons/moon-01.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/oneclient_app/src/components/account_switcher.rs b/packages/oneclient_app/src/components/account_switcher.rs index 673b7dbf..0f18cc61 100644 --- a/packages/oneclient_app/src/components/account_switcher.rs +++ b/packages/oneclient_app/src/components/account_switcher.rs @@ -107,11 +107,19 @@ impl Component for AccountPanel { Color::from_argb(120, 0, 0, 0), ))) .child( - label() - .text("Accounts") - .font_size(18.) - .font_weight(FontWeight::MEDIUM) - .a11y_role(AccessibilityRole::TitleBar), + rect() + .horizontal() + .width(Size::fill()) + .cross_align(Alignment::Center) + .spacing(6.) + .child(BackToControlCenter) + .child( + label() + .text("Accounts") + .font_size(18.) + .font_weight(FontWeight::MEDIUM) + .a11y_role(AccessibilityRole::TitleBar), + ), ) .child(divider()) .child(rows) @@ -120,6 +128,42 @@ impl Component for AccountPanel { } } +#[derive(PartialEq)] +struct BackToControlCenter; + +impl Component for BackToControlCenter { + fn render(&self) -> impl IntoElement { + let dispatch = use_dispatch(); + let mut hovered = use_state(|| false); + + let back = move |_| { + dispatch.close_account_switcher(); + dispatch.open_control_center(); + }; + + rect() + .width(Size::px(28.)) + .height(Size::px(28.)) + .center() + .corner_radius(CornerRadius::new_all(8.)) + .background(Color::RED.with_a(0)) + .maybe(*hovered.read(), |el| { + el.background(colors::ghost_overlay_hover()) + }) + .cursor(CursorIcon::Pointer) + .a11y_role(AccessibilityRole::Button) + .a11y_alt("Back to control center") + .on_pointer_enter(move |_| hovered.set(true)) + .on_pointer_leave(move |_| hovered.set(false)) + .on_press(back) + .child( + Icon::new(IconType::ArrowLeft) + .size(18.) + .color(colors::fg_secondary()), + ) + } +} + struct AccountRow { account: MinecraftAccount, active: bool, diff --git a/packages/oneclient_app/src/components/button.rs b/packages/oneclient_app/src/components/button.rs index 30a28be8..b4276b9a 100644 --- a/packages/oneclient_app/src/components/button.rs +++ b/packages/oneclient_app/src/components/button.rs @@ -56,6 +56,7 @@ pub struct Button { tooltip: Option>, tooltip_placement: TooltipPlacement, on_press: Option>>, + overflow: Overflow, elements: Vec, key: DiffKey, @@ -117,6 +118,7 @@ impl Button { tooltip_placement: TooltipPlacement::default(), on_press: None, elements: Vec::new(), + overflow: Overflow::Clip, key: DiffKey::None, cursor_icon: CursorIcon::Pointer, } @@ -156,6 +158,11 @@ impl Button { self } + pub fn overflow(mut self, overflow: Overflow) -> Self { + self.overflow = overflow; + self + } + pub fn font_size(mut self, font_size: impl Into) -> Self { self.text_style.font_size = Some(font_size.into().into()); self @@ -314,7 +321,7 @@ impl Component for Button { .a11y_focusable(enabled() && self.focusable) .a11y_role(AccessibilityRole::Button) .map(self.alt.clone(), |rect, alt| rect.a11y_alt(alt)) - .overflow(Overflow::Clip) + .overflow(self.overflow) .layout(self.layout.clone()) .text_style(self.text_style.clone()) .children(self.elements.clone()); diff --git a/packages/oneclient_app/src/components/control_center.rs b/packages/oneclient_app/src/components/control_center.rs new file mode 100644 index 00000000..1fce04f4 --- /dev/null +++ b/packages/oneclient_app/src/components/control_center.rs @@ -0,0 +1,634 @@ +use freya::{ + animation::{AnimNum, Ease, OnCreation, use_animation}, + prelude::*, + router::RouterContext, +}; + +use crate::{ + Route, + components::{Avatar, Button, Icon, IconType, OVERLAY_MAX_LEVEL, OverlayPopup}, + hooks::{ + RemoveAccountKeys, query_error, query_is_loading, try_accounts, try_default_account, + use_accounts, use_control_center_open, use_current_account, use_dispatch, + use_remove_account, use_settings_snapshot, + }, + theme::colors, + ui::{border_all_color, divider}, +}; + +const PANEL_WIDTH: f32 = 344.; + +#[derive(PartialEq)] +pub struct ControlCenter; + +impl Component for ControlCenter { + fn render(&self) -> impl IntoElement { + let open = use_control_center_open(); + let dispatch = use_dispatch(); + + if !open { + return rect().into_element(); + } + + OverlayPopup::new() + .position(Position::new_global().top(72.).right(40.)) + .on_close(move |()| dispatch.close_control_center()) + .child(ControlPanel) + .into_element() + } +} + +#[derive(PartialEq)] +struct ControlPanel; + +impl Component for ControlPanel { + fn render(&self) -> impl IntoElement { + let intro = use_animation(|conf| { + conf.on_creation(OnCreation::Run); + AnimNum::new(0., 1.).time(200).ease(Ease::Out) + }); + let progress = intro.read().value(); + + rect() + .vertical() + .width(Size::px(PANEL_WIDTH)) + .padding(Gaps::new_all(12.)) + .spacing(10.) + .opacity(progress) + .margin(Gaps::new((1.0 - progress) * -8.0, 0., 0., 0.)) + .background(colors::page_elevated().with_a(220)) + .blur(12.) + .corner_radius(CornerRadius::new_all(14.)) + .border( + Border::new() + .fill(colors::component_border()) + .width(1.) + .alignment(BorderAlignment::Inner), + ) + .shadow(Shadow::from(( + 0., + 8., + 32., + 0., + Color::from_argb(120, 0, 0, 0), + ))) + .child(AccountHeader) + .child(divider()) + .child( + rect() + .vertical() + .width(Size::fill()) + .spacing(2.) + .child(NavRow { + icon: IconType::BarChartSquare02, + row_label: "Stats", + target: Route::Stats {}, + }) + .child(NavRow { + icon: IconType::Settings02, + row_label: "Settings", + target: Route::SettingsLauncher {}, + }), + ) + .child(section_label("QUICK SETTINGS")) + .child(QuickSettings) + .child(divider()) + .child(PanelFooter) + } +} + +fn section_label(text: &'static str) -> impl IntoElement { + label() + .text(text) + .font_size(11.) + .letter_spacing(0.8) + .font_weight(FontWeight::SEMI_BOLD) + .color(colors::fg_secondary()) +} + +#[derive(PartialEq)] +struct AccountHeader; + +impl Component for AccountHeader { + fn render(&self) -> impl IntoElement { + let current = use_current_account(); + let accounts_query = use_accounts(); + let dispatch = use_dispatch(); + let mut hovered = use_state(|| false); + + let loading = query_is_loading(¤t); + let unusable = query_error(¤t).is_some(); + let account = try_default_account(¤t).or_else(|| { + unusable + .then(|| try_accounts(&accounts_query).unwrap_or_default().into_iter().next()) + .flatten() + }); + + let uuid = account + .as_ref() + .map(|account| account.id.to_string()) + .unwrap_or_else(|| uuid::Uuid::nil().to_string()); + let username = match account.as_ref() { + Some(account) => account.username.clone(), + None if loading => "Loading…".to_string(), + None => "Not signed in".to_string(), + }; + let subtitle = match (account.as_ref(), unusable) { + (_, true) => "Microsoft Account required", + (Some(account), false) if account.is_microsoft() => "Microsoft account", + (Some(_), false) => "Offline account", + (None, false) if loading => "Checking accounts", + (None, false) => "No account selected", + }; + let subtitle_color = if unusable { + colors::danger() + } else { + colors::fg_secondary() + }; + + let signed_out = account.is_none() && !loading; + let action_label = if signed_out { "Sign In" } else { "Switch" }; + let open_switcher = move |_| { + dispatch.close_control_center(); + if signed_out { + let _ = RouterContext::get().push(Route::SettingsAccounts {}); + } else { + dispatch.open_account_switcher(); + } + }; + + rect() + .horizontal() + .width(Size::fill()) + .content(Content::Flex) + .cross_align(Alignment::Center) + .spacing(10.) + .padding(Gaps::new_all(4.)) + .child( + Avatar::new(uuid) + .width(Size::px(40.)) + .height(Size::px(40.)), + ) + .child( + rect() + .vertical() + .width(Size::flex(1.0)) + .spacing(2.) + .child( + label() + .text(username) + .font_size(16.) + .font_weight(FontWeight::MEDIUM) + .max_lines(1) + .color(colors::fg_primary()), + ) + .child( + label() + .text(subtitle) + .font_size(12.) + .max_lines(1) + .color(subtitle_color), + ), + ) + .child( + rect() + .horizontal() + .cross_align(Alignment::Center) + .spacing(6.) + .padding(Gaps::new_symmetric(6., 8.)) + .corner_radius(CornerRadius::new_all(8.)) + .background(Color::RED.with_a(0)) + .maybe(*hovered.read(), |el| { + el.background(colors::ghost_overlay_hover()) + }) + .cursor(CursorIcon::Pointer) + .a11y_role(AccessibilityRole::Button) + .on_pointer_enter(move |_| hovered.set(true)) + .on_pointer_leave(move |_| hovered.set(false)) + .on_press(open_switcher) + .child( + label() + .text(action_label) + .font_size(13.) + .color(colors::fg_secondary()), + ) + .child( + Icon::new(IconType::ArrowRight) + .size(14.) + .color(colors::fg_secondary()), + ), + ) + } +} + +#[derive(PartialEq)] +struct NavRow { + icon: IconType, + row_label: &'static str, + target: Route, +} + +impl Component for NavRow { + fn render(&self) -> impl IntoElement { + let dispatch = use_dispatch(); + let mut hovered = use_state(|| false); + + let target = self.target.clone(); + let go = move |_| { + dispatch.close_control_center(); + let _ = RouterContext::get().push(target.clone()); + }; + + rect() + .horizontal() + .width(Size::fill()) + .height(Size::px(42.)) + .content(Content::Flex) + .cross_align(Alignment::Center) + .spacing(12.) + .padding(Gaps::new_symmetric(0., 8.)) + .corner_radius(CornerRadius::new_all(8.)) + .background(Color::RED.with_a(0)) + .maybe(*hovered.read(), |el| { + el.background(colors::ghost_overlay_hover()) + }) + .cursor(CursorIcon::Pointer) + .a11y_role(AccessibilityRole::Button) + .on_pointer_enter(move |_| hovered.set(true)) + .on_pointer_leave(move |_| hovered.set(false)) + .on_press(go) + .child( + Icon::new(self.icon) + .size(20.) + .color(colors::fg_secondary()), + ) + .child( + label() + .text(self.row_label) + .font_size(15.) + .width(Size::flex(1.0)) + .color(colors::fg_primary()), + ) + .child( + Icon::new(IconType::ArrowRight) + .size(16.) + .color(colors::fg_secondary()), + ) + } +} + +#[derive(PartialEq)] +struct QuickSettings; + +impl Component for QuickSettings { + fn render(&self) -> impl IntoElement { + let settings = use_settings_snapshot().settings; + let dispatch = use_dispatch(); + + let discord_on = settings.discord_enabled; + let toggle_discord = move |_| { + dispatch.edit_settings(|s| s.discord_enabled = !s.discord_enabled); + }; + + rect() + .horizontal() + .width(Size::fill()) + .content(Content::Flex) + .spacing(8.) + .child( + QuickTile::new(IconType::Discord, "Discord RPC", discord_on) + .on_press(toggle_discord), + ) + .child(QuickTile::new(IconType::Moon01, "Close on launch", false).disabled()) + } +} + +struct QuickTile { + icon: IconType, + title: &'static str, + on: bool, + enabled: bool, + on_press: Option>>, +} + +impl PartialEq for QuickTile { + fn eq(&self, other: &Self) -> bool { + self.icon == other.icon + && self.title == other.title + && self.on == other.on + && self.enabled == other.enabled + } +} + +impl QuickTile { + fn new(icon: IconType, title: &'static str, on: bool) -> Self { + Self { + icon, + title, + on, + enabled: true, + on_press: None, + } + } + + fn disabled(mut self) -> Self { + self.enabled = false; + self + } + + fn on_press(mut self, on_press: impl Into>>) -> Self { + self.on_press = Some(on_press.into()); + self + } +} + +impl Component for QuickTile { + fn render(&self) -> impl IntoElement { + let mut hovered = use_state(|| false); + + let on = self.on; + let enabled = self.enabled; + let handler = self.on_press.clone(); + + let background = if !enabled { + colors::component_bg().with_a(120) + } else if on { + if *hovered.read() { + colors::brand_hover() + } else { + colors::brand() + } + } else if *hovered.read() { + colors::component_bg_hover() + } else { + colors::component_bg() + }; + + let title_color = if enabled { + colors::fg_primary() + } else { + colors::fg_primary_disabled() + }; + + let sub_color = if !enabled { + colors::fg_primary_disabled() + } else if on { + colors::fg_primary().with_a(200) + } else { + colors::fg_secondary() + }; + + rect() + .horizontal() + .width(Size::flex(1.0)) + .height(Size::px(64.)) + .content(Content::Flex) + .cross_align(Alignment::Center) + .spacing(10.) + .padding(Gaps::new_symmetric(0., 12.)) + .corner_radius(CornerRadius::new_all(10.)) + .background(background) + .maybe(!enabled, |el| el.opacity(0.45)) + .maybe(!on || !enabled, |el| { + el.border( + Border::new() + .fill(colors::component_border()) + .width(1.) + .alignment(BorderAlignment::Inner), + ) + }) + .maybe(enabled, |el| { + el.cursor(CursorIcon::Pointer) + .a11y_role(AccessibilityRole::Button) + .on_pointer_enter(move |_| hovered.set(true)) + .on_pointer_leave(move |_| hovered.set(false)) + }) + .map(handler, |el, handler| el.on_press(move |e| handler.call(e))) + .child(Icon::new(self.icon).size(24.).color(title_color)) + .child( + rect() + .vertical() + .width(Size::flex(1.0)) + .child( + label() + .text(self.title) + .font_size(12.) + .font_weight(FontWeight::MEDIUM) + .max_lines(1) + .color(title_color), + ) + .child( + label() + .text(if on { "On" } else { "Off" }) + .font_size(11.) + .color(sub_color), + ), + ) + } +} + +#[derive(PartialEq)] +struct PanelFooter; + +impl Component for PanelFooter { + fn render(&self) -> impl IntoElement { + let settings = use_settings_snapshot().settings; + let current = use_current_account(); + let remove = use_remove_account(); + + let mut confirming = use_state(|| false); + + let folder = settings + .data_dir + .clone() + .or_else(|| oneclient_core::settings::data_dir::default_path().ok()); + + let account = try_default_account(¤t); + let account_id = account.as_ref().map(|account| account.id); + let username = account + .as_ref() + .map(|account| account.username.clone()) + .unwrap_or_default(); + + let confirm_sign_out = move |_| { + if let Some(id) = account_id { + remove.mutate(RemoveAccountKeys { id }); + } + confirming.set(false); + }; + + rect() + .vertical() + .width(Size::fill()) + .child( + rect() + .horizontal() + .width(Size::fill()) + .content(Content::Flex) + .cross_align(Alignment::Center) + .child( + footer_action( + IconType::Folder, + "Open launcher folder", + colors::fg_primary(), + ) + .on_press(move |_| { + if let Some(folder) = folder.clone() { + std::fs::create_dir_all(&folder).ok(); + crate::platform::open_path(&folder.to_string_lossy()); + } + }), + ) + .child(rect().width(Size::flex(1.0))) + .map(account_id, |el, _| { + el.child( + footer_action( + IconType::LogOut01, + "Sign out", + colors::fg_primary(), + ) + .on_press(move |_| confirming.set(true)), + ) + }), + ) + .maybe(*confirming.read(), |el| { + el.child( + OverlayPopup::new() + .overlay_level(OVERLAY_MAX_LEVEL) + .on_close(move |_| confirming.set(false)) + .child( + rect() + .width(Size::window_percent(100.)) + .height(Size::window_percent(100.)) + .center() + .child( + rect() + .vertical() + .width(Size::px(400.)) + .max_width(Size::window_percent(90.)) + .spacing(14.) + .padding(Gaps::new_all(20.)) + .corner_radius(CornerRadius::new_all(14.)) + .background(colors::page_elevated()) + .border(border_all_color( + 1., + colors::component_border(), + )) + .child( + rect() + .horizontal() + .cross_align(Alignment::Center) + .spacing(10.) + .child( + Icon::new(IconType::AlertTriangle) + .size(20.) + .color(colors::code_warn()), + ) + .child( + label() + .text("Sign out?") + .font_size(16.) + .font_weight(FontWeight::SEMI_BOLD) + .color(colors::fg_primary()), + ), + ) + .child( + label() + .text(format!( + "{username} will be removed from OneClient. You will need to sign in again to use this account." + )) + .font_size(12.) + .max_lines(4) + .width(Size::fill()) + .color(colors::fg_secondary()), + ) + .child( + rect() + .horizontal() + .width(Size::fill()) + .main_align(Alignment::End) + .spacing(8.) + .child( + Button::new() + .secondary() + .text("Cancel") + .on_press(move |_| { + confirming.set(false) + }), + ) + .child( + Button::new() + .danger() + .text("Sign out") + .on_press(confirm_sign_out), + ), + ), + ), + ), + ) + }) + } +} + +fn footer_action(icon: IconType, text: &'static str, color: Color) -> FooterAction { + FooterAction { + icon, + text, + color, + on_press: None, + } +} + +struct FooterAction { + icon: IconType, + text: &'static str, + color: Color, + on_press: Option>>, +} + +impl PartialEq for FooterAction { + fn eq(&self, other: &Self) -> bool { + self.icon == other.icon && self.text == other.text && self.color == other.color + } +} + +impl FooterAction { + fn on_press(mut self, on_press: impl Into>>) -> Self { + self.on_press = Some(on_press.into()); + self + } +} + +impl Component for FooterAction { + fn render(&self) -> impl IntoElement { + let mut hovered = use_state(|| false); + let handler = self.on_press.clone(); + + rect() + .horizontal() + .cross_align(Alignment::Center) + .spacing(8.) + .height(Size::px(34.)) + .padding(Gaps::new_symmetric(0., 8.)) + .corner_radius(CornerRadius::new_all(8.)) + .background(Color::RED.with_a(0)) + .maybe(*hovered.read(), |el| { + el.background(colors::ghost_overlay_hover()) + }) + .cursor(CursorIcon::Pointer) + .a11y_role(AccessibilityRole::Button) + .on_pointer_enter(move |_| hovered.set(true)) + .on_pointer_leave(move |_| hovered.set(false)) + .map(handler, |el, handler| { + el.on_press(move |e| handler.call(e)) + }) + .child(Icon::new(self.icon).size(16.).color(self.color)) + .child( + label() + .text(self.text) + .font_size(13.) + .max_lines(1) + .color(self.color), + ) + } +} diff --git a/packages/oneclient_app/src/components/icons.rs b/packages/oneclient_app/src/components/icons.rs index 4847de28..0675cf11 100644 --- a/packages/oneclient_app/src/components/icons.rs +++ b/packages/oneclient_app/src/components/icons.rs @@ -87,6 +87,7 @@ pub enum IconType { ArrowLeft, ArrowRight, Brush01, + BarChartSquare02, Bell01, Calendar, CheckCircle, @@ -102,6 +103,7 @@ pub enum IconType { Copy01, Curseforge, Database01, + Discord, DotsGrid, DotsVertical, Download01, @@ -123,10 +125,12 @@ pub enum IconType { Link03, LinkExternal01, Loading02, + LogOut01, Maximize01, MessageTextSquare01, Minus, Modrinth, + Moon01, OnboardingAccount, OnboardingComplete, OnboardingLanguage, diff --git a/packages/oneclient_app/src/components/mod.rs b/packages/oneclient_app/src/components/mod.rs index de7cdae1..5dd33b9e 100644 --- a/packages/oneclient_app/src/components/mod.rs +++ b/packages/oneclient_app/src/components/mod.rs @@ -1,4 +1,5 @@ mod account_switcher; +mod control_center; mod active_cluster_panel; mod avatar; mod button; @@ -47,6 +48,7 @@ mod version_card; pub mod upload_mclogs; pub use account_switcher::AccountSwitcher; +pub use control_center::ControlCenter; pub use active_cluster_panel::ActiveClusterPanel; pub use avatar::Avatar; pub use button::{Button, ButtonSize, ButtonVariant, link_button, open_folder_button}; diff --git a/packages/oneclient_app/src/components/navbar/app_navbar.rs b/packages/oneclient_app/src/components/navbar/app_navbar.rs index 3be4be2f..304f967d 100644 --- a/packages/oneclient_app/src/components/navbar/app_navbar.rs +++ b/packages/oneclient_app/src/components/navbar/app_navbar.rs @@ -280,12 +280,8 @@ impl Component for NavbarRight { notif_dispatch.toggle_notification_center(); }; - let open_account_switcher = move |_| { - dispatch.toggle_account_switcher(); - }; - - let open_settings = |_| { - let _ = RouterContext::get().push(Route::SettingsLauncher {}); + let open_control_center = move |_| { + dispatch.toggle_control_center(); }; let open_stats = |_| { @@ -312,25 +308,49 @@ impl Component for NavbarRight { ) .child( super::navbar_button() - .tooltip("Settings") - .child(Icon::new(IconType::Settings02).size(20.)) - .on_press(open_settings), - ) - .child( - super::navbar_button() + .overflow(Overflow::None) + .tooltip("Control Center") .padding(0.0) - .tooltip("Switch account") - .on_press(open_account_switcher) - .child( - Avatar::new(account_uuid) - .width(Size::px(24.)) - .height(Size::px(24.)), - ), + .on_press(open_control_center) + .child(avatar_with_gear(account_uuid)), ) .child(super::window_controls()) } } +fn avatar_with_gear(uuid: String) -> impl IntoElement { + rect() + .width(Size::px(28.)) + .height(Size::px(28.)) + .center() + .child( + Avatar::new(uuid) + .width(Size::px(28.)) + .height(Size::px(28.)), + ) + .child( + rect() + .position(Position::new_absolute().bottom(-7.).right(-7.)) + .width(Size::px(16.)) + .height(Size::px(16.)) + .corner_radius(CornerRadius::from(7.)) + .background(theme::colors::page_elevated()) + .border( + Border::new() + .fill(theme::colors::component_border()) + .width(1.) + .alignment(BorderAlignment::Inner), + ) + .layer(Layer::Relative(3)) + .center() + .child( + Icon::new(IconType::Settings02) + .size(12.) + .color(theme::colors::fg_secondary()), + ), + ) +} + fn notification_bell(unread: usize) -> impl IntoElement { rect() .width(Size::px(20.)) diff --git a/packages/oneclient_app/src/hooks/actions.rs b/packages/oneclient_app/src/hooks/actions.rs index b83a9ce7..33443417 100644 --- a/packages/oneclient_app/src/hooks/actions.rs +++ b/packages/oneclient_app/src/hooks/actions.rs @@ -255,6 +255,16 @@ impl Actions { }); } + pub fn edit_settings(&self, edit: impl FnOnce(&mut LauncherSettings)) { + let Some(updated) = self.mutate_settings(edit) else { + return; + }; + if let Ok(state) = launcher::state() { + state.discord.set_enabled(updated.discord_enabled); + } + self.persist(updated); + } + pub fn set_settings(&self, settings: LauncherSettings) { let Some(updated) = self.mutate_settings(|s| *s = settings) else { return; @@ -634,6 +644,13 @@ impl Actions { .account_switcher_open = !open; } + pub fn open_account_switcher(&self) { + self.station + .clone() + .write_channel(AppChannel::AccountSwitcher) + .account_switcher_open = true; + } + pub fn close_account_switcher(&self) { self.station .clone() @@ -641,6 +658,28 @@ impl Actions { .account_switcher_open = false; } + pub fn toggle_control_center(&self) { + let open = self.station.peek().control_center_open; + self.station + .clone() + .write_channel(AppChannel::ControlCenter) + .control_center_open = !open; + } + + pub fn open_control_center(&self) { + self.station + .clone() + .write_channel(AppChannel::ControlCenter) + .control_center_open = true; + } + + pub fn close_control_center(&self) { + self.station + .clone() + .write_channel(AppChannel::ControlCenter) + .control_center_open = false; + } + pub fn close_notification_center(&self) { self.with_engine(|state| state.center_open = false); } diff --git a/packages/oneclient_app/src/hooks/mod.rs b/packages/oneclient_app/src/hooks/mod.rs index c2e76d39..cf024931 100644 --- a/packages/oneclient_app/src/hooks/mod.rs +++ b/packages/oneclient_app/src/hooks/mod.rs @@ -116,6 +116,10 @@ pub fn use_account_switcher_open() -> bool { .account_switcher_open } +pub fn use_control_center_open() -> bool { + use_radio(AppChannel::ControlCenter).read().control_center_open +} + pub fn use_game_snapshot() -> GameState { use_radio(AppChannel::Game).read().game.clone() } diff --git a/packages/oneclient_app/src/layout/root_layout.rs b/packages/oneclient_app/src/layout/root_layout.rs index a291e302..8f481f69 100644 --- a/packages/oneclient_app/src/layout/root_layout.rs +++ b/packages/oneclient_app/src/layout/root_layout.rs @@ -2,7 +2,7 @@ use freya::prelude::*; use freya::router::*; use crate::components::{ - AccountSwitcher, ClusterUpdatePopup, GenericPromptOverlay, JavaPromptOverlay, OptionalModsPopup, MicrosoftJavaPromptOverlay, + AccountSwitcher, ClusterUpdatePopup, ControlCenter, GenericPromptOverlay, JavaPromptOverlay, OptionalModsPopup, MicrosoftJavaPromptOverlay, NotificationCenter, PackageUpdatePopup, SplashCurtain, StatusBar, Toasts, TooltipHost, UpdatePromptOverlay, use_provide_tooltips, }; @@ -74,6 +74,7 @@ impl Component for RootLayout { ) .child(NotificationCenter) .child(AccountSwitcher) + .child(ControlCenter) .child(Toasts) .child(UpdatePromptOverlay) .child(JavaPromptOverlay) diff --git a/packages/oneclient_app/src/state.rs b/packages/oneclient_app/src/state.rs index 51eeed8b..dc04a493 100644 --- a/packages/oneclient_app/src/state.rs +++ b/packages/oneclient_app/src/state.rs @@ -20,6 +20,7 @@ pub enum AppChannel { Notifications, Game, AccountSwitcher, + ControlCenter, MicrosoftLogin, Installs, StorageScan, @@ -42,6 +43,7 @@ pub struct AppState { pub center_open: bool, pub game: GameState, pub account_switcher_open: bool, + pub control_center_open: bool, pub microsoft_login: Option, pub installs: InstallState, pub storage_scan: Option,