763 lines
29 KiB
Rust
763 lines
29 KiB
Rust
use chrono::{DateTime, Utc};
|
|
use freya::animation::{AnimNum, Function, OnFinish, use_animation};
|
|
use freya::prelude::*;
|
|
use freya::query::{MutationCapability, MutationStateData, UseMutation};
|
|
use oneclient_auth::{AccountKind, MinecraftAccount};
|
|
use uuid::Uuid;
|
|
|
|
use super::{section_header, settings_page};
|
|
use crate::components::{
|
|
Avatar, Button, Icon, IconType, OverlayPopup, PlayerModel, TextInput, use_microsoft_login,
|
|
};
|
|
use crate::hooks::{
|
|
AddOfflineAccountKeys, RefreshAccountKeys, RemoveAccountKeys, SetDefaultAccountKeys,
|
|
accounts_have_microsoft, try_accounts, try_default_account, use_accounts,
|
|
use_add_offline_account, use_current_account, use_refresh_account, use_remove_account,
|
|
use_set_default_account,
|
|
};
|
|
use crate::theme::colors;
|
|
use crate::ui::border_all_color;
|
|
|
|
/// The shader scales the model off `res.y` alone so height makes the player bigger width only needs to clear the arms
|
|
const MODEL_WIDTH_PX: f32 = 160.;
|
|
const MODEL_HEIGHT_PX: f32 = 264.;
|
|
|
|
const AVATAR_SIZE_PX: f32 = 36.;
|
|
|
|
#[derive(PartialEq)]
|
|
pub struct SettingsAccounts;
|
|
|
|
impl Component for SettingsAccounts {
|
|
fn render(&self) -> impl IntoElement {
|
|
let accounts_query = use_accounts();
|
|
let default_query = use_current_account();
|
|
|
|
let msa = use_microsoft_login();
|
|
let add_offline = use_add_offline_account();
|
|
let set_default = use_set_default_account();
|
|
let remove = use_remove_account();
|
|
let refresh = use_refresh_account();
|
|
|
|
let username = use_state(String::new);
|
|
let mut show_offline = use_state(|| false);
|
|
let edit_account = use_state(|| None::<(Uuid, String)>);
|
|
let edit_name = use_state(|| String::new());
|
|
|
|
let accounts = try_accounts(&accounts_query).unwrap_or_default();
|
|
let default_account = try_default_account(&default_query);
|
|
let default_id = default_account.as_ref().map(|a| a.id);
|
|
let has_microsoft = accounts_have_microsoft(&accounts);
|
|
|
|
let offline_name = username.read().trim().to_string();
|
|
let offline_uuid = (!offline_name.is_empty())
|
|
.then(|| oneclient_auth::offline_uuid(&offline_name).to_string());
|
|
|
|
let offline_error = mutation_err_text(&add_offline);
|
|
|
|
let on_confirm_offline = move |_| {
|
|
let name = username.peek().trim().to_string();
|
|
if name.is_empty() {
|
|
return;
|
|
}
|
|
let mut show_offline = show_offline;
|
|
let mut username = username;
|
|
spawn(async move {
|
|
let reader = add_offline
|
|
.mutate_async(AddOfflineAccountKeys { username: name })
|
|
.await;
|
|
if matches!(
|
|
&*reader.state(),
|
|
MutationStateData::Settled { res: Ok(_), .. }
|
|
) {
|
|
show_offline.set(false);
|
|
username.set(String::new());
|
|
}
|
|
});
|
|
};
|
|
|
|
let mut rows: Vec<Element> = accounts
|
|
.iter()
|
|
.map(|account| {
|
|
AccountRow {
|
|
id: account.id,
|
|
username: account.username.clone(),
|
|
kind: account.kind,
|
|
expires: account.expires,
|
|
is_default: Some(account.id) == default_id,
|
|
set_default,
|
|
remove,
|
|
refresh,
|
|
edit_account,
|
|
edit_name,
|
|
}
|
|
.into_element()
|
|
})
|
|
.collect();
|
|
if rows.is_empty() {
|
|
rows.push(empty_state());
|
|
}
|
|
|
|
settings_page()
|
|
.child(hero(
|
|
default_account,
|
|
has_microsoft,
|
|
msa.pending,
|
|
msa.error.clone(),
|
|
move |_| show_offline.set(true),
|
|
{
|
|
let msa = msa.clone();
|
|
move |_| msa.start()
|
|
},
|
|
))
|
|
.child(section_header("YOUR ACCOUNTS"))
|
|
.children(rows)
|
|
.maybe_child(show_offline.read().then(|| {
|
|
offline_dialog(
|
|
username,
|
|
offline_uuid,
|
|
offline_error,
|
|
on_confirm_offline,
|
|
show_offline,
|
|
)
|
|
}))
|
|
.maybe_child(edit_account.read().as_ref().map(|(id, _)| {
|
|
let id = *id;
|
|
let mut edit_account = edit_account;
|
|
let edit_name = edit_name;
|
|
OverlayPopup::new()
|
|
.on_close(move |()| edit_account.set(None))
|
|
.child(
|
|
rect()
|
|
.width(Size::window_percent(100.))
|
|
.height(Size::window_percent(100.))
|
|
.center()
|
|
.child(
|
|
rect()
|
|
.vertical()
|
|
.width(Size::px(380.))
|
|
.max_width(Size::window_percent(90.))
|
|
.spacing(16.)
|
|
.padding(Gaps::new_all(20.))
|
|
.corner_radius(CornerRadius::new_all(16.))
|
|
.background(colors::page_elevated())
|
|
.border(border_all_color(1., colors::component_border()))
|
|
.child(
|
|
label()
|
|
.text("Edit offline username")
|
|
.font_size(18.)
|
|
.font_weight(FontWeight::SEMI_BOLD)
|
|
.color(colors::fg_primary()),
|
|
)
|
|
.child(
|
|
rect()
|
|
.vertical()
|
|
.width(Size::fill())
|
|
.spacing(6.)
|
|
.child(field_label("New Username"))
|
|
.child(TextInput::new(edit_name).placeholder("Enter new username")),
|
|
)
|
|
.child(
|
|
rect()
|
|
.horizontal()
|
|
.width(Size::fill())
|
|
.main_align(Alignment::End)
|
|
.spacing(8.)
|
|
.child(
|
|
Button::new()
|
|
.ghost()
|
|
.on_press(move |_| edit_account.set(None))
|
|
.text("Cancel"),
|
|
)
|
|
.child(
|
|
Button::new()
|
|
.primary()
|
|
.on_press(move |_| {
|
|
let new_name = edit_name.peek().trim().to_string();
|
|
if !new_name.is_empty() {
|
|
let mut edit_account = edit_account;
|
|
spawn(async move {
|
|
if let Ok(state) = crate::launcher::state() {
|
|
let _ = state.auth.remove_account(id).await;
|
|
if let Ok(new_acc) = state.auth.add_offline_account(new_name).await {
|
|
let _ = state.auth.set_default_account(Some(new_acc.id)).await;
|
|
crate::hooks::invalidate_auth_queries(Some(new_acc.id)).await;
|
|
}
|
|
}
|
|
edit_account.set(None);
|
|
});
|
|
}
|
|
})
|
|
.text("Save"),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
}))
|
|
.maybe_child(msa.popup())
|
|
.into_element()
|
|
}
|
|
}
|
|
|
|
fn hero(
|
|
account: Option<MinecraftAccount>,
|
|
_has_microsoft: bool,
|
|
microsoft_pending: bool,
|
|
error: Option<String>,
|
|
on_open_offline: impl FnMut(Event<PressEventData>) + 'static,
|
|
on_add_microsoft: impl FnMut(Event<PressEventData>) + 'static,
|
|
) -> impl IntoElement {
|
|
let (name, subtitle) = match &account {
|
|
Some(account) => (account.username.clone(), kind_label(account.kind)),
|
|
None => (
|
|
"No active account".to_string(),
|
|
"Add an account to start playing",
|
|
),
|
|
};
|
|
|
|
rect()
|
|
.horizontal()
|
|
.width(Size::fill())
|
|
.content(Content::Flex)
|
|
.cross_align(Alignment::Center)
|
|
.spacing(16.)
|
|
.padding(Gaps::new_all(16.))
|
|
.corner_radius(CornerRadius::new_all(12.))
|
|
.background(colors::page_elevated())
|
|
.child(model_frame(account.as_ref().map(|account| account.id)))
|
|
.child(
|
|
rect()
|
|
.vertical()
|
|
.width(Size::flex(1.0))
|
|
.spacing(16.)
|
|
.child(
|
|
rect()
|
|
.vertical()
|
|
.width(Size::fill())
|
|
.spacing(2.)
|
|
.child(
|
|
label()
|
|
.text("ACTIVE ACCOUNT")
|
|
.font_size(11.)
|
|
.font_weight(FontWeight::MEDIUM)
|
|
.color(colors::fg_secondary()),
|
|
)
|
|
.child(
|
|
label()
|
|
.text(name)
|
|
.font_size(24.)
|
|
.font_weight(FontWeight::SEMI_BOLD)
|
|
.max_lines(1)
|
|
.color(colors::fg_primary()),
|
|
)
|
|
.child(
|
|
label()
|
|
.text(subtitle)
|
|
.font_size(12.)
|
|
.color(colors::fg_secondary()),
|
|
),
|
|
)
|
|
.child(
|
|
rect()
|
|
.vertical()
|
|
.width(Size::fill())
|
|
.spacing(8.)
|
|
.child(
|
|
// The two buttons overflow the ~245px beside the model at the 800px minimum so they wrap
|
|
rect()
|
|
.horizontal()
|
|
.width(Size::fill())
|
|
.content(Content::wrap_spacing(8.))
|
|
.spacing(8.)
|
|
.child(
|
|
Button::new()
|
|
.primary()
|
|
.enabled(!microsoft_pending)
|
|
.on_press(on_add_microsoft)
|
|
.child(Icon::new(IconType::Globe01).size(16.))
|
|
.text(if microsoft_pending {
|
|
"Signing in..."
|
|
} else {
|
|
"Add Microsoft"
|
|
}),
|
|
)
|
|
.child(
|
|
Button::new()
|
|
.secondary()
|
|
.enabled(true)
|
|
.on_press(on_open_offline)
|
|
.child(Icon::new(IconType::Plus).size(16.))
|
|
.text("Add offline"),
|
|
)
|
|
.child(
|
|
Button::new()
|
|
.secondary()
|
|
.on_press(|_| {
|
|
let _ = freya::router::RouterContext::get().push(crate::Route::AccountSkins {});
|
|
})
|
|
.child(Icon::new(IconType::Colors).size(16.))
|
|
.text("Change Skin"),
|
|
),
|
|
)
|
|
.map(error, |el, msg| {
|
|
el.child(hint_line(IconType::AlertTriangle, msg, colors::danger()))
|
|
})
|
|
.maybe(false, |el| {
|
|
el.child(hint_line(
|
|
IconType::InfoCircle,
|
|
"Add a Microsoft account before creating offline accounts."
|
|
.to_string(),
|
|
colors::fg_secondary(),
|
|
))
|
|
}),
|
|
),
|
|
)
|
|
.into_element()
|
|
}
|
|
|
|
fn model_frame(id: Option<Uuid>) -> impl IntoElement {
|
|
rect()
|
|
.vertical()
|
|
.cross_align(Alignment::Center)
|
|
.spacing(6.)
|
|
.child(
|
|
rect()
|
|
.width(Size::px(MODEL_WIDTH_PX))
|
|
.height(Size::px(MODEL_HEIGHT_PX))
|
|
.center()
|
|
.overflow(Overflow::Clip)
|
|
.corner_radius(CornerRadius::new_all(12.))
|
|
.background(colors::component_bg())
|
|
.border(border_all_color(1., colors::component_border()))
|
|
.child(match id {
|
|
Some(id) => PlayerModel::new(id)
|
|
.yaw(-0.5)
|
|
.width(Size::fill())
|
|
.height(Size::fill())
|
|
.into_element(),
|
|
None => Icon::new(IconType::Users01)
|
|
.size(28.)
|
|
.color(colors::fg_secondary())
|
|
.into_element(),
|
|
}),
|
|
)
|
|
.maybe_child(id.map(|_| {
|
|
label()
|
|
.text("Drag to rotate")
|
|
.font_size(10.)
|
|
.color(colors::fg_secondary())
|
|
.into_element()
|
|
}))
|
|
.into_element()
|
|
}
|
|
|
|
fn mutation_err_text<M>(mutation: &UseMutation<M>) -> Option<String>
|
|
where
|
|
M: MutationCapability,
|
|
M::Err: std::fmt::Display,
|
|
{
|
|
match &*mutation.read().state() {
|
|
MutationStateData::Settled { res: Err(err), .. } => Some(err.to_string()),
|
|
MutationStateData::Loading {
|
|
res: Some(Err(err)),
|
|
} => Some(err.to_string()),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn offline_dialog(
|
|
username: State<String>,
|
|
uuid_preview: Option<String>,
|
|
error: Option<String>,
|
|
on_confirm: impl FnMut(Event<PressEventData>) + 'static,
|
|
mut show_offline: State<bool>,
|
|
) -> impl IntoElement {
|
|
OverlayPopup::new()
|
|
.on_close(move |()| show_offline.set(false))
|
|
.child(
|
|
rect()
|
|
.width(Size::window_percent(100.))
|
|
.height(Size::window_percent(100.))
|
|
.center()
|
|
.child(
|
|
rect()
|
|
.vertical()
|
|
.width(Size::px(380.))
|
|
.max_width(Size::window_percent(90.))
|
|
.spacing(16.)
|
|
.padding(Gaps::new_all(20.))
|
|
.corner_radius(CornerRadius::new_all(16.))
|
|
.background(colors::page_elevated())
|
|
.border(border_all_color(1., colors::component_border()))
|
|
.child(
|
|
label()
|
|
.text("Add offline account")
|
|
.font_size(18.)
|
|
.font_weight(FontWeight::SEMI_BOLD)
|
|
.color(colors::fg_primary()),
|
|
)
|
|
.child(
|
|
rect()
|
|
.vertical()
|
|
.width(Size::fill())
|
|
.spacing(6.)
|
|
.child(field_label("Username"))
|
|
.child(TextInput::new(username).placeholder("Offline username")),
|
|
)
|
|
.child(
|
|
rect()
|
|
.vertical()
|
|
.width(Size::fill())
|
|
.spacing(6.)
|
|
.child(field_label("UUID"))
|
|
.child(
|
|
label()
|
|
.text(uuid_preview.unwrap_or_else(|| "—".to_string()))
|
|
.font_size(12.)
|
|
.color(colors::fg_secondary()),
|
|
),
|
|
)
|
|
.map(error, |el, msg| {
|
|
el.child(hint_line(IconType::AlertTriangle, msg, colors::danger()))
|
|
})
|
|
.child(
|
|
rect()
|
|
.horizontal()
|
|
.width(Size::fill())
|
|
.main_align(Alignment::End)
|
|
.spacing(8.)
|
|
.child(
|
|
Button::new()
|
|
.ghost()
|
|
.on_press(move |_| show_offline.set(false))
|
|
.text("Cancel"),
|
|
)
|
|
.child(
|
|
Button::new()
|
|
.primary()
|
|
.on_press(on_confirm)
|
|
.child(Icon::new(IconType::Plus).size(16.))
|
|
.text("Add account"),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
.into_element()
|
|
}
|
|
|
|
fn field_label(text: &str) -> impl IntoElement {
|
|
label()
|
|
.text(text.to_string())
|
|
.font_size(11.)
|
|
.font_weight(FontWeight::MEDIUM)
|
|
.color(colors::fg_secondary())
|
|
.into_element()
|
|
}
|
|
|
|
/// Sits in whatever is left of the hero beside the model so the text must wrap
|
|
fn hint_line(icon: IconType, text: String, color: Color) -> impl IntoElement {
|
|
rect()
|
|
.horizontal()
|
|
.width(Size::fill())
|
|
.content(Content::Flex)
|
|
.cross_align(Alignment::Center)
|
|
.spacing(6.)
|
|
.child(Icon::new(icon).size(13.).color(color))
|
|
.child(
|
|
label()
|
|
.text(text)
|
|
.font_size(12.)
|
|
.width(Size::flex(1.0))
|
|
.color(color),
|
|
)
|
|
.into_element()
|
|
}
|
|
|
|
const REFRESH_SPIN_TIME: u64 = 800;
|
|
|
|
const REFRESHING_OPACITY: f32 = 0.7;
|
|
|
|
struct AccountRow {
|
|
id: Uuid,
|
|
username: String,
|
|
kind: AccountKind,
|
|
expires: DateTime<Utc>,
|
|
is_default: bool,
|
|
set_default: crate::hooks::UseSetDefaultAccount,
|
|
remove: crate::hooks::UseRemoveAccount,
|
|
refresh: crate::hooks::UseRefreshAccount,
|
|
edit_account: State<Option<(Uuid, String)>>,
|
|
edit_name: State<String>,
|
|
}
|
|
|
|
impl PartialEq for AccountRow {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
self.id == other.id
|
|
&& self.username == other.username
|
|
&& self.kind == other.kind
|
|
&& self.expires == other.expires
|
|
&& self.is_default == other.is_default
|
|
&& self.edit_account == other.edit_account
|
|
&& self.edit_name == other.edit_name
|
|
}
|
|
}
|
|
|
|
impl Component for AccountRow {
|
|
fn render_key(&self) -> DiffKey {
|
|
DiffKey::from(&self.id)
|
|
}
|
|
|
|
fn render(&self) -> impl IntoElement {
|
|
let id = self.id;
|
|
let is_default = self.is_default;
|
|
let set_default = self.set_default;
|
|
let remove = self.remove;
|
|
let refresh = self.refresh;
|
|
|
|
let is_microsoft = self.kind == AccountKind::Microsoft;
|
|
let expired = is_microsoft && self.expires <= Utc::now();
|
|
|
|
let mut refreshing = use_state(|| false);
|
|
let is_refreshing = *refreshing.read();
|
|
|
|
let spin = use_animation(|conf| {
|
|
conf.on_finish(OnFinish::restart());
|
|
AnimNum::new(0., 360.)
|
|
.time(REFRESH_SPIN_TIME)
|
|
.function(Function::Linear)
|
|
});
|
|
|
|
use_side_effect_with_deps(&is_refreshing, move |&is_refreshing| {
|
|
let mut spin = spin;
|
|
if is_refreshing {
|
|
spin.start();
|
|
} else {
|
|
spin.reset();
|
|
}
|
|
});
|
|
|
|
let rotation = if is_refreshing {
|
|
spin.get().value()
|
|
} else {
|
|
0.
|
|
};
|
|
|
|
let border_color = if expired {
|
|
colors::danger()
|
|
} else if is_default {
|
|
colors::brand()
|
|
} else {
|
|
colors::component_border()
|
|
};
|
|
|
|
rect()
|
|
.horizontal()
|
|
.width(Size::fill())
|
|
.cross_align(Alignment::Center)
|
|
.content(Content::Flex)
|
|
.spacing(12.)
|
|
.padding(Gaps::new_all(12.))
|
|
.corner_radius(CornerRadius::new_all(12.))
|
|
.background(colors::page_elevated())
|
|
.border(border_all_color(1., border_color))
|
|
.opacity(if is_refreshing {
|
|
REFRESHING_OPACITY
|
|
} else {
|
|
1.
|
|
})
|
|
.a11y_role(AccessibilityRole::Button)
|
|
.maybe(!is_default, |el| {
|
|
el.on_press(move |_| set_default.mutate(SetDefaultAccountKeys { id: Some(id) }))
|
|
})
|
|
.child(
|
|
Avatar::new(id.to_string())
|
|
.width(Size::px(AVATAR_SIZE_PX))
|
|
.height(Size::px(AVATAR_SIZE_PX)),
|
|
)
|
|
.child(
|
|
rect()
|
|
.vertical()
|
|
.width(Size::flex(1.0))
|
|
.spacing(4.)
|
|
.child(
|
|
rect()
|
|
.horizontal()
|
|
.cross_align(Alignment::Center)
|
|
.spacing(6.)
|
|
.child(
|
|
label()
|
|
.text(self.username.clone())
|
|
.font_size(15.)
|
|
.font_weight(FontWeight::MEDIUM)
|
|
.max_lines(1)
|
|
.color(colors::fg_primary()),
|
|
)
|
|
.maybe_child(is_default.then(default_badge))
|
|
.maybe_child(expired.then(expired_badge)),
|
|
)
|
|
// The kind rides with the id a third badge and a full uuid do not both fit
|
|
.child(
|
|
label()
|
|
.text(format!("{} · {id}", kind_label(self.kind)))
|
|
.font_size(11.)
|
|
.max_lines(1)
|
|
.color(colors::fg_secondary()),
|
|
),
|
|
)
|
|
.maybe_child(is_microsoft.then(|| {
|
|
Button::new()
|
|
.ghost()
|
|
.icon()
|
|
.on_press(move |e: Event<PressEventData>| {
|
|
e.stop_propagation();
|
|
if *refreshing.peek() {
|
|
return;
|
|
}
|
|
refreshing.set(true);
|
|
spawn(async move {
|
|
refresh.mutate_async(RefreshAccountKeys { id }).await;
|
|
refreshing.set(false);
|
|
});
|
|
})
|
|
.child(
|
|
rect().rotate(rotation).child(
|
|
Icon::new(IconType::RefreshCw01)
|
|
.size(18.)
|
|
.color(if expired {
|
|
colors::danger()
|
|
} else {
|
|
colors::fg_secondary()
|
|
}),
|
|
),
|
|
)
|
|
.into_element()
|
|
}))
|
|
.child(
|
|
Button::new()
|
|
.ghost()
|
|
.icon()
|
|
.on_press(move |e: Event<PressEventData>| {
|
|
e.stop_propagation();
|
|
let _ = freya::router::RouterContext::get().push(crate::Route::AccountSkins {});
|
|
})
|
|
.child(
|
|
Icon::new(IconType::Colors)
|
|
.size(18.)
|
|
.color(colors::fg_primary()),
|
|
),
|
|
)
|
|
.child({
|
|
let is_offline = !is_microsoft;
|
|
let mut edit_acc_state = self.edit_account;
|
|
let mut edit_name_state = self.edit_name;
|
|
let username_val = self.username.clone();
|
|
let id_val = self.id;
|
|
Button::new()
|
|
.ghost()
|
|
.icon()
|
|
.enabled(is_offline)
|
|
.on_press(move |e: Event<PressEventData>| {
|
|
e.stop_propagation();
|
|
edit_name_state.set(username_val.clone());
|
|
edit_acc_state.set(Some((id_val, username_val.clone())));
|
|
})
|
|
.child(
|
|
Icon::new(IconType::Pencil01)
|
|
.size(18.)
|
|
.color(if is_offline {
|
|
colors::fg_primary()
|
|
} else {
|
|
colors::fg_secondary()
|
|
}),
|
|
)
|
|
})
|
|
.child(
|
|
Button::new()
|
|
.ghost()
|
|
.icon()
|
|
.on_press(move |e: Event<PressEventData>| {
|
|
e.stop_propagation();
|
|
remove.mutate(RemoveAccountKeys { id });
|
|
})
|
|
.child(
|
|
Icon::new(IconType::Trash01)
|
|
.size(18.)
|
|
.color(colors::fg_secondary()),
|
|
),
|
|
)
|
|
.into_element()
|
|
}
|
|
}
|
|
|
|
fn kind_label(kind: AccountKind) -> &'static str {
|
|
match kind {
|
|
AccountKind::Microsoft => "Microsoft",
|
|
AccountKind::Offline => "Offline",
|
|
}
|
|
}
|
|
|
|
fn default_badge() -> impl IntoElement {
|
|
badge(
|
|
Icon::new(IconType::CheckCircle)
|
|
.size(12.)
|
|
.color(colors::brand())
|
|
.into_element(),
|
|
"Default".to_string(),
|
|
colors::brand(),
|
|
colors::brand(),
|
|
)
|
|
}
|
|
|
|
fn expired_badge() -> impl IntoElement {
|
|
badge(
|
|
Icon::new(IconType::AlertTriangle)
|
|
.size(12.)
|
|
.color(colors::danger())
|
|
.into_element(),
|
|
"Expired".to_string(),
|
|
colors::danger(),
|
|
colors::danger(),
|
|
)
|
|
}
|
|
|
|
fn badge(icon: impl IntoElement, text: String, border: Color, fg: Color) -> impl IntoElement {
|
|
rect()
|
|
.horizontal()
|
|
.cross_align(Alignment::Center)
|
|
.spacing(4.)
|
|
.padding(Gaps::new_symmetric(2., 8.))
|
|
.corner_radius(CornerRadius::new_all(999.))
|
|
.border(border_all_color(1., border))
|
|
.background(colors::component_bg())
|
|
.child(icon)
|
|
.child(
|
|
label()
|
|
.text(text)
|
|
.font_size(10.)
|
|
.font_weight(FontWeight::MEDIUM)
|
|
.color(fg),
|
|
)
|
|
.into_element()
|
|
}
|
|
|
|
fn empty_state() -> Element {
|
|
rect()
|
|
.vertical()
|
|
.width(Size::fill())
|
|
.center()
|
|
.padding(Gaps::new_all(32.))
|
|
.spacing(8.)
|
|
.corner_radius(CornerRadius::new_all(12.))
|
|
.background(colors::page_elevated())
|
|
.child(
|
|
Icon::new(IconType::Users01)
|
|
.size(32.)
|
|
.color(colors::fg_secondary()),
|
|
)
|
|
.child(
|
|
label()
|
|
.text("No accounts yet. Add one above.")
|
|
.font_size(14.)
|
|
.color(colors::fg_secondary()),
|
|
)
|
|
.into_element()
|
|
}
|