feat(app-iced): M114 账户页三轨登录(微软正版 / 第三方皮肤站 / 本地档案)
- 账户页:当前账户卡 + 全部账户列表 + 空态三入口 + 皮肤区;五项动作(添加/切换/删除/重新登录/校验) - 三轨添加流程:设备码(用户码+链接+倒计时+过期重试+取消)、授权码(浏览器+可复制链接+取消)、 本地档案(名字校验)、第三方站(服务地址 + 服务发现校验 + 密码登录,密码掩码且用完即弃) - 导航:账户为一等页,入口是轨底头像位(未登录=描边剪影 / 已登录=首字母块+名字;窄高收成图标条) - 凭据红线:DTO 侧形状脱敏 + 状态层二次脱敏 + 密码掩码;Inject-JWT 测试断言视图模型干净(已做变异取证) - presenter:authlib 登录/服务发现/皮肤抓取(复用下载引擎的 HTTP 客户端,无新增依赖)、 皮肤原图经 DTO 到 UI 裁剪成头像(既有 image 依赖,未改 Cargo.toml) - i18n:新增 account 域 51 条中英文案(离线档案口径全量写入),金表同步
This commit is contained in:
1 parent
e7b04c4afe
commit
1ff00bbe50
43 files changed
+4343
-117
No files matched your search
@@ -0,0 +1,349 @@
|
||||
//! 微软登录面板:设备码(用户码 + 链接 + **倒计时** + 过期重试)与授权码(浏览器 + 可复制链接 + 取消)。
|
||||
//!
|
||||
//! ## 为什么不是「一个转圈」
|
||||
//!
|
||||
//! 设备码流的等待期可以长到 15 分钟。只画转圈的话,用户既不知道要做什么、也不知道还剩多久,
|
||||
//! 更不知道已经过期了——M114 把这三件事都做成可见状态:用户码(可复制)、链接(可复制/打开)、
|
||||
//! 倒计时(`{secs}s left`)与过期后的「已过期,重试」。授权码流同理,另外必须能**取消**。
|
||||
|
||||
use iced::widget::{column, container, row};
|
||||
use iced::{Alignment, Background, Border, Element, Length};
|
||||
|
||||
use unlucky_design::color::Role;
|
||||
use unlucky_design::shape::ShapeScale;
|
||||
use unlucky_design::spacing::Space;
|
||||
use unlucky_design::typography::TypeRole;
|
||||
use unlucky_i18n::{Catalog, Key};
|
||||
|
||||
use crate::app::components::buttons::{self, Button};
|
||||
use crate::pages::frame;
|
||||
use crate::theme::Ui;
|
||||
|
||||
use crate::pages::account::model::DevicePanel;
|
||||
use crate::pages::Message as PageMessage;
|
||||
|
||||
use super::Message as Account;
|
||||
|
||||
/// 微软登录面板。
|
||||
pub fn panel<'a>(
|
||||
ui: &'a Ui,
|
||||
device: &'a DevicePanel,
|
||||
catalog: Catalog,
|
||||
) -> Element<'a, PageMessage> {
|
||||
match device {
|
||||
DevicePanel::Hidden => idle(ui, catalog),
|
||||
DevicePanel::Requesting { message } => notice(ui, message, catalog),
|
||||
DevicePanel::Code {
|
||||
code,
|
||||
link,
|
||||
remaining,
|
||||
expired,
|
||||
..
|
||||
} => device_code(ui, code, link, *remaining, *expired, catalog),
|
||||
DevicePanel::Browser {
|
||||
link,
|
||||
remaining,
|
||||
expired,
|
||||
..
|
||||
} => browser(ui, link, *remaining, *expired, catalog),
|
||||
DevicePanel::Failed { message } => failed(ui, message, catalog),
|
||||
DevicePanel::Succeeded { name } => succeeded(ui, name, catalog),
|
||||
}
|
||||
}
|
||||
|
||||
/// 空闲:给出两条流的入口(GUI 默认授权码流,设备码是备选)。
|
||||
fn idle<'a>(ui: &'a Ui, catalog: Catalog) -> Element<'a, PageMessage> {
|
||||
let body = column![row![
|
||||
buttons::filled(
|
||||
ui,
|
||||
Button::new(
|
||||
catalog.text(Key::AccountDeviceBrowser),
|
||||
Account::StartBrowser.into_page(),
|
||||
)
|
||||
),
|
||||
buttons::outlined(
|
||||
ui,
|
||||
Button::new(
|
||||
catalog.text(Key::AccountDeviceUseCode),
|
||||
Account::StartDevice.into_page(),
|
||||
)
|
||||
),
|
||||
buttons::text(
|
||||
ui,
|
||||
Button::new(
|
||||
catalog.text(Key::AccountDeviceCancel),
|
||||
Account::Close.into_page()
|
||||
)
|
||||
),
|
||||
]
|
||||
.spacing(ui.space(Space::X2))
|
||||
.align_y(Alignment::Center),]
|
||||
.spacing(ui.space(Space::X3));
|
||||
frame::card(ui, body)
|
||||
}
|
||||
|
||||
/// 「正在申请/正在校验」:一句状态 + 取消。
|
||||
fn notice<'a>(ui: &'a Ui, message: &'a str, catalog: Catalog) -> Element<'a, PageMessage> {
|
||||
let body = column![
|
||||
frame::text_role(
|
||||
ui,
|
||||
TypeRole::BodyMedium,
|
||||
false,
|
||||
message,
|
||||
ui.color(Role::OnSurfaceVariant),
|
||||
),
|
||||
row![buttons::text(
|
||||
ui,
|
||||
Button::new(
|
||||
catalog.text(Key::AccountDeviceCancel),
|
||||
Account::Cancel.into_page()
|
||||
)
|
||||
)]
|
||||
.align_y(Alignment::Center),
|
||||
]
|
||||
.spacing(ui.space(Space::X3));
|
||||
frame::card(ui, body)
|
||||
}
|
||||
|
||||
/// 设备码等待态:**码 + 链接 + 倒计时**。
|
||||
fn device_code<'a>(
|
||||
ui: &'a Ui,
|
||||
code: &'a str,
|
||||
link: &'a str,
|
||||
remaining: Option<u64>,
|
||||
expired: bool,
|
||||
catalog: Catalog,
|
||||
) -> Element<'a, PageMessage> {
|
||||
let mut actions = row![
|
||||
buttons::tonal(
|
||||
ui,
|
||||
Button::new(
|
||||
catalog.text(Key::AccountDeviceCodeCopy),
|
||||
Account::Copy(code.to_string()).into_page(),
|
||||
)
|
||||
),
|
||||
buttons::outlined(
|
||||
ui,
|
||||
Button::new(
|
||||
catalog.text(Key::AccountDeviceLinkCopy),
|
||||
Account::Copy(link.to_string()).into_page(),
|
||||
)
|
||||
),
|
||||
]
|
||||
.spacing(ui.space(Space::X2))
|
||||
.align_y(Alignment::Center);
|
||||
|
||||
if expired {
|
||||
actions = actions.push(buttons::filled(
|
||||
ui,
|
||||
Button::new(
|
||||
catalog.text(Key::AccountDeviceRetry),
|
||||
Account::StartDevice.into_page(),
|
||||
),
|
||||
));
|
||||
}
|
||||
actions = actions.push(buttons::text(
|
||||
ui,
|
||||
Button::new(
|
||||
catalog.text(Key::AccountDeviceCancel),
|
||||
Account::Cancel.into_page(),
|
||||
),
|
||||
));
|
||||
|
||||
let countdown = countdown_line(ui, remaining, expired, catalog);
|
||||
|
||||
let body = column![
|
||||
frame::text_role(
|
||||
ui,
|
||||
TypeRole::LabelMedium,
|
||||
false,
|
||||
catalog.text(Key::AccountDeviceCodeLabel),
|
||||
ui.color(Role::OnSurfaceVariant),
|
||||
),
|
||||
frame::text_role(
|
||||
ui,
|
||||
TypeRole::HeadlineSmall,
|
||||
true,
|
||||
code,
|
||||
ui.color(Role::OnSurface),
|
||||
),
|
||||
frame::text_role(
|
||||
ui,
|
||||
TypeRole::BodySmall,
|
||||
false,
|
||||
link,
|
||||
ui.color(Role::OnSurfaceVariant),
|
||||
),
|
||||
countdown,
|
||||
frame::text_role(
|
||||
ui,
|
||||
TypeRole::BodySmall,
|
||||
false,
|
||||
catalog.text(Key::AccountDeviceHint),
|
||||
ui.color(Role::OnSurfaceVariant),
|
||||
),
|
||||
actions,
|
||||
]
|
||||
.spacing(ui.space(Space::X3));
|
||||
frame::card(ui, body)
|
||||
}
|
||||
|
||||
/// 授权码等待态:浏览器已拉起,链接可复制;过期可重试。
|
||||
fn browser<'a>(
|
||||
ui: &'a Ui,
|
||||
link: &'a str,
|
||||
remaining: Option<u64>,
|
||||
expired: bool,
|
||||
catalog: Catalog,
|
||||
) -> Element<'a, PageMessage> {
|
||||
let mut actions = row![buttons::outlined(
|
||||
ui,
|
||||
Button::new(
|
||||
catalog.text(Key::AccountDeviceLinkCopy),
|
||||
Account::Copy(link.to_string()).into_page(),
|
||||
)
|
||||
)]
|
||||
.spacing(ui.space(Space::X2))
|
||||
.align_y(Alignment::Center);
|
||||
if expired {
|
||||
actions = actions.push(buttons::filled(
|
||||
ui,
|
||||
Button::new(
|
||||
catalog.text(Key::AccountDeviceRetry),
|
||||
Account::StartBrowser.into_page(),
|
||||
),
|
||||
));
|
||||
}
|
||||
actions = actions.push(buttons::text(
|
||||
ui,
|
||||
Button::new(
|
||||
catalog.text(Key::AccountDeviceCancel),
|
||||
Account::Cancel.into_page(),
|
||||
),
|
||||
));
|
||||
|
||||
let body = column![
|
||||
frame::text_role(
|
||||
ui,
|
||||
TypeRole::BodyMedium,
|
||||
false,
|
||||
catalog.text(Key::AccountDeviceBrowserHint),
|
||||
ui.color(Role::OnSurface),
|
||||
),
|
||||
frame::text_role(
|
||||
ui,
|
||||
TypeRole::BodySmall,
|
||||
false,
|
||||
link,
|
||||
ui.color(Role::OnSurfaceVariant),
|
||||
),
|
||||
countdown_line(ui, remaining, expired, catalog),
|
||||
actions,
|
||||
]
|
||||
.spacing(ui.space(Space::X3));
|
||||
frame::card(ui, body)
|
||||
}
|
||||
|
||||
/// 倒计时 / 过期提示(**这一行是「不得只转圈」的落点**)。
|
||||
fn countdown_line<'a>(
|
||||
ui: &'a Ui,
|
||||
remaining: Option<u64>,
|
||||
expired: bool,
|
||||
catalog: Catalog,
|
||||
) -> Element<'a, PageMessage> {
|
||||
if expired {
|
||||
return frame::text_role(
|
||||
ui,
|
||||
TypeRole::LabelMedium,
|
||||
true,
|
||||
catalog.text(Key::AccountDeviceExpired),
|
||||
ui.color(Role::Error),
|
||||
);
|
||||
}
|
||||
let text = remaining.map_or_else(String::new, |secs| {
|
||||
catalog.render(Key::AccountDeviceCountdown, &[("secs", &secs.to_string())])
|
||||
});
|
||||
frame::text_role_owned(
|
||||
ui,
|
||||
TypeRole::LabelMedium,
|
||||
false,
|
||||
text,
|
||||
ui.color(Role::OnSurfaceVariant),
|
||||
)
|
||||
}
|
||||
|
||||
/// 失败态(`ErrorContainer` 面板:不是一句灰字)。
|
||||
fn failed<'a>(ui: &'a Ui, message: &'a str, catalog: Catalog) -> Element<'a, PageMessage> {
|
||||
let body = column![
|
||||
frame::text_role(
|
||||
ui,
|
||||
TypeRole::TitleSmall,
|
||||
true,
|
||||
catalog.text(Key::AccountLoginFailed),
|
||||
ui.color(Role::OnErrorContainer),
|
||||
),
|
||||
frame::text_role(
|
||||
ui,
|
||||
TypeRole::BodyMedium,
|
||||
false,
|
||||
message,
|
||||
ui.color(Role::OnErrorContainer),
|
||||
),
|
||||
row![
|
||||
buttons::tonal(
|
||||
ui,
|
||||
Button::new(
|
||||
catalog.text(Key::AccountDeviceRetry),
|
||||
Account::StartDevice.into_page(),
|
||||
)
|
||||
),
|
||||
buttons::text(
|
||||
ui,
|
||||
Button::new(
|
||||
catalog.text(Key::AccountDialogClose),
|
||||
Account::Close.into_page()
|
||||
)
|
||||
),
|
||||
]
|
||||
.spacing(ui.space(Space::X2))
|
||||
.align_y(Alignment::Center),
|
||||
]
|
||||
.spacing(ui.space(Space::X3));
|
||||
|
||||
container(body)
|
||||
.width(Length::Fill)
|
||||
.padding(ui.metrics().card_padding)
|
||||
.style(move |_theme| container::Style {
|
||||
background: Some(Background::Color(ui.color(Role::ErrorContainer))),
|
||||
border: Border {
|
||||
radius: ui.radius_dp(ShapeScale::Medium),
|
||||
..Border::default()
|
||||
},
|
||||
..container::Style::default()
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 成功态。
|
||||
fn succeeded<'a>(ui: &'a Ui, name: &'a str, catalog: Catalog) -> Element<'a, PageMessage> {
|
||||
let text = format!("{} · {name}", catalog.text(Key::AccountActiveChip));
|
||||
let body = column![
|
||||
frame::text_role_owned(
|
||||
ui,
|
||||
TypeRole::BodyMedium,
|
||||
true,
|
||||
text,
|
||||
ui.color(Role::OnSurface),
|
||||
),
|
||||
row![buttons::text(
|
||||
ui,
|
||||
Button::new(
|
||||
catalog.text(Key::AccountDialogClose),
|
||||
Account::Close.into_page()
|
||||
)
|
||||
)]
|
||||
.align_y(Alignment::Center),
|
||||
]
|
||||
.spacing(ui.space(Space::X3));
|
||||
frame::card(ui, body)
|
||||
}
|
||||
Reference in new issue
Block a user