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>
This commit is contained in:
Maciek 2026-09-16 20:29:14 +02:00 • committed by GitHub
parent b4f067483a
commit a00e11d204
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 793 additions and 26 deletions

View file

@ -0,0 +1,3 @@
<svg width="100%" height="100%" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 15V17M12 11V17M16 7V17M6.8 21H17.2C18.88 21 19.72 21 20.36 20.67C20.93 20.39 21.39 19.93 21.67 19.36C22 18.72 22 17.88 22 16.2V7.8C22 6.12 22 5.28 21.67 4.64C21.39 4.07 20.93 3.61 20.36 3.33C19.72 3 18.88 3 17.2 3H6.8C5.12 3 4.28 3 3.64 3.33C3.07 3.61 2.61 4.07 2.33 4.64C2 5.28 2 6.12 2 7.8V16.2C2 17.88 2 18.72 2.33 19.36C2.61 19.93 3.07 20.39 3.64 20.67C4.28 21 5.12 21 6.8 21Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 590 B

View file

@ -0,0 +1 @@
<svg role="img" viewBox="-11.2 -8.5 149 113" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M81.15,0c-1.2376,2.1973-2.3489,4.4704-3.3591,6.794-9.5975-1.4396-19.3718-1.4396-28.9945,0-.985-2.3236-2.1216-4.5967-3.3591-6.794-9.0166,1.5407-17.8059,4.2431-26.1405,8.0568C2.779,32.5304-1.6914,56.3725.5312,79.8863c9.6732,7.1476,20.5083,12.603,32.0505,16.0884,2.6014-3.4854,4.8998-7.1981,6.8698-11.0623-3.738-1.3891-7.3497-3.1318-10.8098-5.1523.9092-.6567,1.7932-1.3386,2.6519-1.9953,20.281,9.547,43.7696,9.547,64.0758,0,.8587.7072,1.7427,1.3891,2.6519,1.9953-3.4601,2.0457-7.0718,3.7632-10.835,5.1776,1.97,3.8642,4.2683,7.5769,6.8698,11.0623,11.5419-3.4854,22.3769-8.9156,32.0509-16.0631,2.626-27.2771-4.496-50.9172-18.817-71.8548C98.9811,4.2684,90.1918,1.5659,81.1752.0505l-.0252-.0505ZM42.2802,65.4144c-6.2383,0-11.4159-5.6575-11.4159-12.6535s4.9755-12.6788,11.3907-12.6788,11.5169,5.708,11.4159,12.6788c-.101,6.9708-5.026,12.6535-11.3907,12.6535ZM84.3576,65.4144c-6.2637,0-11.3907-5.6575-11.3907-12.6535s4.9755-12.6788,11.3907-12.6788,11.4917,5.708,11.3906,12.6788c-.101,6.9708-5.026,12.6535-11.3906,12.6535Z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,3 @@
<svg width="100%" height="100%" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 17L21 12M21 12L16 7M21 12H9M9 21H7.8C6.12 21 5.28 21 4.64 20.67C4.07 20.39 3.61 19.93 3.33 19.36C3 18.72 3 17.88 3 16.2V7.8C3 6.12 3 5.28 3.33 4.64C3.61 4.07 4.07 3.61 4.64 3.33C5.28 3 6.12 3 7.8 3H9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 410 B

View file

@ -0,0 +1,3 @@
<svg width="100%" height="100%" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.2 12.63A7.2 7.2 0 1 1 11.37 4.8A5.6 5.6 0 0 0 19.2 12.63Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 270 B

View file

@ -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,

View file

@ -56,6 +56,7 @@ pub struct Button {
tooltip: Option<Box<str>>,
tooltip_placement: TooltipPlacement,
on_press: Option<EventHandler<Event<PressEventData>>>,
overflow: Overflow,
elements: Vec<Element>,
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<f32>) -> 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());

View file

@ -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(&current);
let unusable = query_error(&current).is_some();
let account = try_default_account(&current).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<EventHandler<Event<PressEventData>>>,
}
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<EventHandler<Event<PressEventData>>>) -> 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(&current);
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<EventHandler<Event<PressEventData>>>,
}
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<EventHandler<Event<PressEventData>>>) -> 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),
)
}
}

View file

@ -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,

View file

@ -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};

View file

@ -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.))

View file

@ -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);
}

View file

@ -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()
}

View file

@ -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)

View file

@ -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<LoginProgress>,
pub installs: InstallState,
pub storage_scan: Option<StorageScanProgress>,