diff --git a/crates/app-iced/src/app/update/account.rs b/crates/app-iced/src/app/update/account.rs index 0277f01..d248487 100644 --- a/crates/app-iced/src/app/update/account.rs +++ b/crates/app-iced/src/app/update/account.rs @@ -168,6 +168,17 @@ pub(super) fn force_account_state(app: &mut App) { app.state.device_login_started = Some(std::time::Instant::now() - std::time::Duration::from_secs(3_600)); } + // 第三方站**失败**态:面板打开 + mock 那边给出的 `AuthlibLoginChanged(Failed)` + "authlib-failed" => { + app.account_form.panel = crate::pages::account::Panel::Authlib; + app.account_form.server = "https://example.invalid/api/yggdrasil/".to_string(); + app.account_form.login = "alex@example.com".to_string(); + } + // 离线档案失败态:面板打开(横幅由 mock 的 notice 场景给) + "notice" => { + app.account_form.panel = crate::pages::account::Panel::Offline; + app.account_form.offline_name = "ab".to_string(); + } // 第三方站表单:直接把面板打开并预填地址 "authlib" | "authlib-form" => { app.account_form.panel = crate::pages::account::Panel::Authlib; diff --git a/crates/app-iced/src/app/view/account_page.rs b/crates/app-iced/src/app/view/account_page.rs index c85fa03..7f72327 100644 --- a/crates/app-iced/src/app/view/account_page.rs +++ b/crates/app-iced/src/app/view/account_page.rs @@ -33,7 +33,8 @@ impl App { }, APP_TITLE, background, - // M106:账户页的文案跟着**生效语言**走(早期钉死 `Lang::En` 的写法已删)。 + // M106:账户页的文案跟着**生效语言**走(`LangState::boot` 自己读 `ULL_LANG`, + // 所以 M114 早期那个「截图专用语言开关」已删——两套开关必然漂)。 pages::account::view(&self.ui, metrics, &self.account, self.lang()).map(Message::Page), agent::docked(&self.ui, &self.agent, self.width) .map(|element| element.map(Message::Agent)), diff --git a/crates/app-iced/src/pages/account.rs b/crates/app-iced/src/pages/account.rs index 7f55b4e..a7ef912 100644 --- a/crates/app-iced/src/pages/account.rs +++ b/crates/app-iced/src/pages/account.rs @@ -37,6 +37,9 @@ pub mod widgets; #[cfg(test)] mod tests; +#[cfg(test)] +#[path = "account/tests_text.rs"] +mod tests_text; pub use model::{Form, Hint, Notice, Page, Panel, Track}; diff --git a/crates/app-iced/src/pages/account/tests.rs b/crates/app-iced/src/pages/account/tests.rs index 044b3f0..e60a0fa 100644 --- a/crates/app-iced/src/pages/account/tests.rs +++ b/crates/app-iced/src/pages/account/tests.rs @@ -14,8 +14,8 @@ use unlucky_presenter::dto::{ use unlucky_presenter::state::AppState; use super::avatar::Cache; -use super::login::failure_body_key; use super::model::{AuthlibPanel, DevicePanel, Form, Hint, Page, Panel, Track}; +use super::tests_text::{has_cjk, on_screen}; fn account(name: &str, kind: AccountKind, active: bool) -> AccountView { AccountView { @@ -37,84 +37,6 @@ fn build(state: &AppState, form: &Form) -> Page { Page::build(state, form, &mut Cache::default(), Lang::En, 1180.0) } -/// 页面**会画什么**:模型里的数据 + 视图层对码的 i18n 查表结果。 -fn on_screen(page: &Page) -> String { - let catalog = Lang::En.catalog(); - let mut out = String::new(); - for row in &page.rows { - out.push_str(&row.name); - out.push_str(&row.monogram); - out.push_str(&row.kind_label); - out.push_str(&row.added); - out.push_str(&row.detail); - } - out.push_str(&page.form.offline_name); - out.push_str(&page.form.server); - if let Some(notice) = &page.notice { - // 与 `widgets::banner` 同源:码 → i18n(主题名拼进去) - out.push_str(match notice.code { - AccountNoticeCode::OfflineNameInvalid => { - catalog.text(Key::AccountNoticeOfflineNameInvalid) - } - AccountNoticeCode::AccountExists => catalog.text(Key::AccountNoticeAccountExists), - AccountNoticeCode::StoreFailed => catalog.text(Key::AccountNoticeStoreFailed), - AccountNoticeCode::NotFound => catalog.text(Key::AccountNoticeNotFound), - AccountNoticeCode::Other => catalog.text(Key::AccountNoticeOther), - AccountNoticeCode::Added => catalog.text(Key::AccountNoticeAdded), - AccountNoticeCode::Removed => catalog.text(Key::AccountNoticeRemoved), - AccountNoticeCode::Switched => catalog.text(Key::AccountNoticeSwitched), - }); - out.push_str(¬ice.subject); - } - if let Some(hint) = page.hint { - out.push_str(match hint { - Hint::AuthlibReauth => catalog.text(Key::AccountHintAuthlibReauth), - Hint::OfflineNoRelogin => catalog.text(Key::AccountHintOfflineNoRelogin), - }); - } - match &page.device { - DevicePanel::Hidden => {} - DevicePanel::Requesting { .. } => out.push_str(catalog.text(Key::AccountDeviceRequesting)), - DevicePanel::Code { - code, - link, - remaining, - expired, - .. - } => { - out.push_str(code); - out.push_str(link); - if *expired { - out.push_str(catalog.text(Key::AccountDeviceExpired)); - } else if let Some(secs) = remaining { - let clock = format!("{:02}:{:02}", secs / 60, secs % 60); - out.push_str(&catalog.render(Key::AccountDeviceCountdown, &[("time", &clock)])); - } - } - DevicePanel::Browser { link, .. } => out.push_str(link), - DevicePanel::Failed { failure, .. } => { - out.push_str(catalog.text(failure_body_key(*failure))); - out.push_str(&catalog.render(Key::AccountFailureCode, &[("code", failure.code())])); - } - DevicePanel::Succeeded { name } => { - out.push_str(&catalog.render(Key::AccountLoginSucceeded, &[("name", name)])); - } - } - match &page.authlib { - AuthlibPanel::Idle => {} - AuthlibPanel::Running { .. } => out.push_str(catalog.text(Key::AccountServerChecking)), - AuthlibPanel::Ready { server_name, .. } => out.push_str(server_name), - AuthlibPanel::Failed { failure, .. } => out.push_str(failure.code()), - AuthlibPanel::Succeeded { name } => out.push_str(name), - } - out -} - -/// 有没有中日韩统一表意文字(语言泄漏的最直接判据)。 -fn has_cjk(text: &str) -> bool { - text.chars().any(|c| matches!(c, '\u{3400}'..='\u{4DBF}' | '\u{4E00}'..='\u{9FFF}' | '\u{3000}'..='\u{303F}' | '\u{FF00}'..='\u{FFEF}')) -} - const JWT: &str = "eyJhbGciOiJSUzI1NiIsIng1dCI6IkFCQyJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.c2lnbmF0dXJlLXZhbHVl"; diff --git a/crates/app-iced/src/pages/account/tests_text.rs b/crates/app-iced/src/pages/account/tests_text.rs new file mode 100644 index 0000000..b518458 --- /dev/null +++ b/crates/app-iced/src/pages/account/tests_text.rs @@ -0,0 +1,89 @@ +//! 「**页面会画什么**」的取样器:把视图模型 + 视图层对码的 i18n 查表结果拼成一串。 +//! +//! 为什么单独一个文件:`pages/account/tests.rs` 贴着 400 行硬线,而这段是**纯取样** +//! (不给任何断言)——断言都在 `tests.rs` 里。语言泄漏(复审 r1 P1-1)与凭据红线两条判据 +//! 都依赖它:只有把「模型字段 + i18n 文案」一起取样,才能证明「英文界面里没有中文」。 + +use unlucky_i18n::{Key, Lang}; +use unlucky_presenter::dto::AccountNoticeCode; + +use super::login::failure_body_key; +use super::model::{AuthlibPanel, DevicePanel, Hint, Page}; + +/// 页面**会画什么**:模型里的数据 + 视图层对码的 i18n 查表结果。 +pub(super) fn on_screen(page: &Page) -> String { + let catalog = Lang::En.catalog(); + let mut out = String::new(); + for row in &page.rows { + out.push_str(&row.name); + out.push_str(&row.monogram); + out.push_str(&row.kind_label); + out.push_str(&row.added); + out.push_str(&row.detail); + } + out.push_str(&page.form.offline_name); + out.push_str(&page.form.server); + if let Some(notice) = &page.notice { + // 与 `widgets::banner` 同源:码 → i18n(主题名拼进去) + out.push_str(match notice.code { + AccountNoticeCode::OfflineNameInvalid => { + catalog.text(Key::AccountNoticeOfflineNameInvalid) + } + AccountNoticeCode::AccountExists => catalog.text(Key::AccountNoticeAccountExists), + AccountNoticeCode::StoreFailed => catalog.text(Key::AccountNoticeStoreFailed), + AccountNoticeCode::NotFound => catalog.text(Key::AccountNoticeNotFound), + AccountNoticeCode::Other => catalog.text(Key::AccountNoticeOther), + AccountNoticeCode::Added => catalog.text(Key::AccountNoticeAdded), + AccountNoticeCode::Removed => catalog.text(Key::AccountNoticeRemoved), + AccountNoticeCode::Switched => catalog.text(Key::AccountNoticeSwitched), + }); + out.push_str(¬ice.subject); + } + if let Some(hint) = page.hint { + out.push_str(match hint { + Hint::AuthlibReauth => catalog.text(Key::AccountHintAuthlibReauth), + Hint::OfflineNoRelogin => catalog.text(Key::AccountHintOfflineNoRelogin), + }); + } + match &page.device { + DevicePanel::Hidden => {} + DevicePanel::Requesting { .. } => out.push_str(catalog.text(Key::AccountDeviceRequesting)), + DevicePanel::Code { + code, + link, + remaining, + expired, + .. + } => { + out.push_str(code); + out.push_str(link); + if *expired { + out.push_str(catalog.text(Key::AccountDeviceExpired)); + } else if let Some(secs) = remaining { + let clock = format!("{:02}:{:02}", secs / 60, secs % 60); + out.push_str(&catalog.render(Key::AccountDeviceCountdown, &[("time", &clock)])); + } + } + DevicePanel::Browser { link, .. } => out.push_str(link), + DevicePanel::Failed { failure, .. } => { + out.push_str(catalog.text(failure_body_key(*failure))); + out.push_str(&catalog.render(Key::AccountFailureCode, &[("code", failure.code())])); + } + DevicePanel::Succeeded { name } => { + out.push_str(&catalog.render(Key::AccountLoginSucceeded, &[("name", name)])); + } + } + match &page.authlib { + AuthlibPanel::Idle => {} + AuthlibPanel::Running { .. } => out.push_str(catalog.text(Key::AccountServerChecking)), + AuthlibPanel::Ready { server_name, .. } => out.push_str(server_name), + AuthlibPanel::Failed { failure, .. } => out.push_str(failure.code()), + AuthlibPanel::Succeeded { name } => out.push_str(name), + } + out +} + +/// 有没有中日韩统一表意文字(语言泄漏的最直接判据)。 +pub(super) fn has_cjk(text: &str) -> bool { + text.chars().any(|c| matches!(c, '\u{3400}'..='\u{4DBF}' | '\u{4E00}'..='\u{9FFF}' | '\u{3000}'..='\u{303F}' | '\u{FF00}'..='\u{FFEF}')) +} diff --git a/crates/presenter/src/mock.rs b/crates/presenter/src/mock.rs index 3376f88..7953a9a 100644 --- a/crates/presenter/src/mock.rs +++ b/crates/presenter/src/mock.rs @@ -80,6 +80,9 @@ impl MockBackend { if let Some(view) = accounts_screen::device_login(&scenario) { self.emit(AppEvent::Core(CoreEvent::DeviceLoginChanged(view))); } + if let Some(view) = accounts_screen::authlib_login(&scenario) { + self.emit(AppEvent::Core(CoreEvent::AuthlibLoginChanged(view))); + } if let Some(notice) = accounts_screen::notice(&scenario) { self.emit(AppEvent::Core(CoreEvent::AccountNoticeChanged(Some( notice, diff --git a/crates/presenter/src/mock/accounts.rs b/crates/presenter/src/mock/accounts.rs index 91772ce..ed43ee0 100644 --- a/crates/presenter/src/mock/accounts.rs +++ b/crates/presenter/src/mock/accounts.rs @@ -12,7 +12,8 @@ //! M114 的口径)。所以截图里皮肤区显示的是首字母块 + 「没有可用皮肤」。 use crate::dto::{ - AccountKind, AccountNotice, AccountNoticeCode, AccountView, DeviceLoginView, LoginFailure, + AccountKind, AccountNotice, AccountNoticeCode, AccountView, AuthlibLoginView, DeviceLoginView, + LoginFailure, }; /// 场景名(`ULL_ACCOUNT_STATE`;未知值按 `list` 处理)。 @@ -80,6 +81,23 @@ pub(super) fn device_login(scenario: &str) -> Option { } } +/// 场景对应的**第三方站**登录状态(`authlib-failed` = 地址不是 authlib 服务器)。 +/// +/// 原文(中文诊断)只进日志;页面据 `failure` 码出 i18n(复审 r1 P1-1 的第三方轨那一半)。 +pub(super) fn authlib_login(scenario: &str) -> Option { + match scenario { + "authlib-failed" => Some(AuthlibLoginView { + phase: crate::dto::DeviceLoginPhase::Failed, + server: "https://example.invalid/api/yggdrasil/".to_string(), + server_name: String::new(), + message: "服务器返回 HTTP 404(不像可用的 authlib 认证服务器)".to_string(), + failure: Some(LoginFailure::NotAuthlibServer), + account_name: String::new(), + }), + _ => None, + } +} + /// 场景对应的横幅(`notice` = 离线名字不合法那条;`added` = 添加成功的正向样例)。 pub(super) fn notice(scenario: &str) -> Option { match scenario {