diff --git a/crates/app-iced/src/app/state.rs b/crates/app-iced/src/app/state.rs index a28809f..8de1625 100644 --- a/crates/app-iced/src/app/state.rs +++ b/crates/app-iced/src/app/state.rs @@ -360,16 +360,9 @@ impl App { self.lang(), &self.content, ); - // 账户页(M114):与 `view` 同一次门控重建;皮肤解码走缓存,所以重复重建不重解图。 - // 语言同样跟着 M106 的 `LangState` 走(M114 早期钉死 `Lang::En` 的写法已删)。 - let lang = self.lang(); - self.account = pages::account::Page::build( - &self.state, - &self.account_form, - &mut self.avatars, - lang, - width, - ); + // 账户页(M114):与 `view` 同一次门控重建(实现见 `view/account_page.rs`, + // 皮肤解码走缓存,所以重复重建不重解图)。 + self.rebuild_account_page(width); self.view_dirty = false; self.refreshes += 1; } diff --git a/crates/app-iced/src/app/update.rs b/crates/app-iced/src/app/update.rs index 3215568..c498a3a 100644 --- a/crates/app-iced/src/app/update.rs +++ b/crates/app-iced/src/app/update.rs @@ -44,6 +44,7 @@ use super::CAPTURE_AT_FRAME; mod account; mod agent; +mod boot_env; mod content; mod download; mod page_messages; @@ -178,7 +179,7 @@ impl App { // 取证开关 `ULL_START_ROUTE`:截图/冒烟直接落到某一页(`cli.rs` 不在本 mission 的 scope, // 而「外观」分组只在设置页上,出矩阵必须能直达)。未知值忽略。 - if let Some(route) = env_start_route() { + if let Some(route) = boot_env::env_start_route() { eprintln!("[boot] ULL_START_ROUTE → {route:?}"); self.state.navigate(route); // 直达某一页时不要再被「自动选中第一条实例」拉回详情页。 @@ -383,25 +384,5 @@ fn heartbeat() -> Task { }) } -/// 取证开关 `ULL_START_ROUTE` 的取值解析(`settings` / `instances` / `download` / `content` / `logs`)。 -/// -/// 为什么是环境变量:`crates/app-iced/src/cli.rs` 不在 M93 的 scope,而外观矩阵必须在**设置页**上 -/// 出图。语义与 `ULL_APPEARANCE_*` / `ULL_TICK_TRACE` 一致:只影响这一次进程。 -fn env_start_route() -> Option { - match std::env::var("ULL_START_ROUTE").ok()?.trim() { - "instances" => Some(Route::Instances), - "download" => Some(Route::Download), - "logs" => Some(Route::Logs), - "settings" => Some(Route::Settings), - // M104:AI 独立页也要能直达(三态矩阵的 Detached 那张) - "agent" => Some(Route::Agent), - // M109:内容库(截图矩阵要能直达;页面内的四条侧栏不在路由里) - "content" => Some(Route::Content), - // M114:账户页的 D30 矩阵要能直达(`cli.rs` 不在本 mission 的 scope) - "accounts" => Some(Route::Accounts), - _ => None, - } -} - #[cfg(test)] mod download_wiring; diff --git a/crates/app-iced/src/app/update/boot_env.rs b/crates/app-iced/src/app/update/boot_env.rs new file mode 100644 index 0000000..65690a9 --- /dev/null +++ b/crates/app-iced/src/app/update/boot_env.rs @@ -0,0 +1,26 @@ +//! 启动期的取证环境变量解析(`ULL_START_ROUTE`)。 +//! +//! 从 `app/update.rs` 原样搬出(那个文件在 M105/M106/M109/M113 之后贴着 400 行硬线): +//! **纯函数、无状态**,只是取值表长到了七条。 + +use unlucky_presenter::router::Route; + +/// 取证开关 `ULL_START_ROUTE` 的取值解析(`settings` / `instances` / `download` / `content` / `logs`)。 +/// +/// 为什么是环境变量:`crates/app-iced/src/cli.rs` 不在 M93 的 scope,而外观矩阵必须在**设置页**上 +/// 出图。语义与 `ULL_APPEARANCE_*` / `ULL_TICK_TRACE` 一致:只影响这一次进程。 +pub(super) fn env_start_route() -> Option { + match std::env::var("ULL_START_ROUTE").ok()?.trim() { + "instances" => Some(Route::Instances), + "download" => Some(Route::Download), + "logs" => Some(Route::Logs), + "settings" => Some(Route::Settings), + // M104:AI 独立页也要能直达(三态矩阵的 Detached 那张) + "agent" => Some(Route::Agent), + // M109:内容库(截图矩阵要能直达;页面内的四条侧栏不在路由里) + "content" => Some(Route::Content), + // M114:账户页的 D30 矩阵要能直达(`cli.rs` 不在本 mission 的 scope) + "accounts" => Some(Route::Accounts), + _ => None, + } +} diff --git a/crates/app-iced/src/app/view/account_page.rs b/crates/app-iced/src/app/view/account_page.rs index 7f72327..08e13e1 100644 --- a/crates/app-iced/src/app/view/account_page.rs +++ b/crates/app-iced/src/app/view/account_page.rs @@ -10,12 +10,28 @@ use iced::Element; use crate::app::Message; use crate::pages; +use crate::pages::account::Page as AccountPage; use crate::shell::{self, agent, NavItem, RailAccount}; use super::super::state::App; use super::super::APP_TITLE; impl App { + /// 账户页视图模型的重建(`refresh_view` 的账户那一步)。 + /// + /// 与 `view` 同一次门控;语言跟着 M106 的 `LangState` 走(M114 早期钉死 `Lang::En` + /// 的写法已删),宽度用 M104 的中栏可用宽。放在这里是为了让 `app/state.rs` 守住 400 行。 + pub(in crate::app) fn rebuild_account_page(&mut self, width: f32) { + let lang = self.lang(); + self.account = AccountPage::build( + &self.state, + &self.account_form, + &mut self.avatars, + lang, + width, + ); + } + /// 账户页(走 `pages::account` 自己的树,而不是 `pages::Screen` 那四页的 `Model`)。 pub(super) fn account_page<'a>( &'a self, diff --git a/crates/i18n/src/catalog.rs b/crates/i18n/src/catalog.rs index 3b870f3..a83532d 100644 --- a/crates/i18n/src/catalog.rs +++ b/crates/i18n/src/catalog.rs @@ -112,8 +112,8 @@ macro_rules! finish { ({ $($acc:tt)* }) => { catalog! { $($acc)* } }; } -mod agent; mod account; +mod agent; mod content; mod core_text; mod download; @@ -132,8 +132,8 @@ mod golden; #[cfg(test)] mod tests; -use agent::agent_entries; use account::account_entries; +use agent::agent_entries; use content::content_entries; use core_text::core_text_entries; use download::{download_entries, version_entries}; diff --git a/crates/presenter/src/dto/events.rs b/crates/presenter/src/dto/events.rs index e9d5d39..c4508a7 100644 --- a/crates/presenter/src/dto/events.rs +++ b/crates/presenter/src/dto/events.rs @@ -10,10 +10,31 @@ use crate::dialogs::{DialogId, DialogRequest}; use crate::dto::CreateRequest; use crate::dto::{ // 主线(M109 的 Content* / M113 的 Pack*)与 M114 的 Authlib*/AccountNotice 的并集。 - AccountNotice, AccountView, AgentEvent, AuthlibLoginView, AuthlibRequest, ContentInstallRequest, - ContentQuery, ContentResults, DeviceLoginView, DiagnosisReport, InstallProgressView, InstanceId, - InstanceRuntime, InstanceSummary, JavaPageView, LoaderKind, LogLine, LogOutcome, LogSession, - NoticeView, PackCommand, PackEvent, SessionId, SettingGroupView, VersionCategory, + AccountNotice, + AccountView, + AgentEvent, + AuthlibLoginView, + AuthlibRequest, + ContentInstallRequest, + ContentQuery, + ContentResults, + DeviceLoginView, + DiagnosisReport, + InstallProgressView, + InstanceId, + InstanceRuntime, + InstanceSummary, + JavaPageView, + LoaderKind, + LogLine, + LogOutcome, + LogSession, + NoticeView, + PackCommand, + PackEvent, + SessionId, + SettingGroupView, + VersionCategory, VersionSummary, }; use crate::toast::Toast; diff --git a/crates/presenter/src/host/accounts.rs b/crates/presenter/src/host/accounts.rs index 043b14c..8e3b613 100644 --- a/crates/presenter/src/host/accounts.rs +++ b/crates/presenter/src/host/accounts.rs @@ -32,6 +32,8 @@ use super::opener; /// 第三方轨(服务发现 / 密码登录 / 皮肤抓取)。 pub mod authlib; +mod refresh; + mod failure; pub use failure::AccountFailure; diff --git a/crates/presenter/src/host/accounts/refresh.rs b/crates/presenter/src/host/accounts/refresh.rs new file mode 100644 index 0000000..cb805d6 --- /dev/null +++ b/crates/presenter/src/host/accounts/refresh.rs @@ -0,0 +1,66 @@ +//! 账户列表的刷新与皮肤抓取(`impl CoreHost` 的这一段,从 `host/mod.rs` 拆出来守 400 行)。 +//! +//! 为什么在这里:`host/mod.rs` 在 M109/M113 之后已经贴着 400 行硬线,而这三段是**账户专属** +//! 的编排(读账户库 → 盖皮肤 → 起抓取线程)。放回 `host/accounts.rs` 也不行——那个文件同样 +//! 380 行。归属仍是 accounts(`mod accounts { mod refresh; }`),只是按功能再分一层。 + +use crate::dto::{AccountKind, CoreEvent}; + +use super::super::{accounts, job, CoreHost, HostTask}; + +impl CoreHost { + pub(in crate::host) fn refresh_accounts(&mut self) { + self.accounts = accounts::accounts(&self.env, self.active_account.as_deref()); + if self.active_account.is_none() { + // 默认选中第一个账户(没有账户时保持 None = 离线占位) + self.active_account = self.accounts.first().map(|account| account.id.clone()); + self.accounts = accounts::accounts(&self.env, self.active_account.as_deref()); + } + self.attach_skins(); + self.request_skins(); + self.send(CoreEvent::AccountsChanged(self.accounts.clone())); + } + + /// 把已抓到的皮肤盖到账户视图上。 + /// + /// 只盖第三方轨:离线档案没有皮肤,微软轨不做真头像(M114 口径)—— + /// 「没有皮肤就画首字母块」,绝不给假皮肤/白块。 + pub(in crate::host) fn attach_skins(&mut self) { + for account in &mut self.accounts { + if account.kind != AccountKind::Authlib { + account.skin = None; + continue; + } + if let Some(skin) = self.skins.get(&account.id) { + account.skin = skin.clone(); + } + } + } + + /// 给还没抓过皮肤的第三方轨账户起后台抓取(每个账户只抓一次)。 + /// + /// 抓不到就记 `None`:错误在面板上不显示(皮肤是装饰,不是业务失败),但也不会重试刷屏。 + fn request_skins(&mut self) { + let mut pending = Vec::new(); + for account in &self.accounts { + if account.kind != AccountKind::Authlib || account.server_url.trim().is_empty() { + continue; + } + if self.skin_requested.insert(account.id.clone()) { + pending.push(( + account.id.clone(), + account.server_url.clone(), + account.profile_id.clone(), + )); + } + } + for (id, server, profile) in pending { + let tx = self.tx.clone(); + let env = self.env.clone(); + job::spawn("account-skin", move || { + let skin = accounts::fetch_skin(&env, &server, &profile).ok(); + let _ = tx.send(HostTask::Avatar { id, skin }); + }); + } + } +} diff --git a/crates/presenter/src/host/job.rs b/crates/presenter/src/host/job.rs index 871b21e..74b497c 100644 --- a/crates/presenter/src/host/job.rs +++ b/crates/presenter/src/host/job.rs @@ -8,9 +8,25 @@ use std::sync::mpsc::{Receiver, Sender}; use crate::dto::{ // 主线(M109 ContentResults / M105 LoaderKind / M113 PackEvent)与 M114 的账户轨并集。 - AccountView, AuthlibLoginView, ContentResults, DeviceLoginView, DiagnosisReport, - InstallProgressView, InstanceId, InstanceRuntime, JavaPageView, LaunchFinished, LiveLogBatch, - LoaderKind, LogLine, LogOutcome, LogSession, NoticeView, PackEvent, SessionId, SkinImage, + AccountView, + AuthlibLoginView, + ContentResults, + DeviceLoginView, + DiagnosisReport, + InstallProgressView, + InstanceId, + InstanceRuntime, + JavaPageView, + LaunchFinished, + LiveLogBatch, + LoaderKind, + LogLine, + LogOutcome, + LogSession, + NoticeView, + PackEvent, + SessionId, + SkinImage, VersionSummary, }; use crate::toast::Toast; diff --git a/crates/presenter/src/host/mod.rs b/crates/presenter/src/host/mod.rs index 9bc3578..ed55b51 100644 --- a/crates/presenter/src/host/mod.rs +++ b/crates/presenter/src/host/mod.rs @@ -37,8 +37,8 @@ use std::collections::{BTreeMap, BTreeSet, VecDeque}; use std::time::Duration; use crate::dto::{ - AccountKind, AccountView, AppCommand, AppEvent, Backend, CoreEvent, DiagnosisReport, - InstallProgressView, InstanceRuntime, JavaChainStep, NoticeView, SkinImage, + AccountView, AppCommand, AppEvent, Backend, CoreEvent, DiagnosisReport, InstallProgressView, + InstanceRuntime, JavaChainStep, NoticeView, SkinImage, }; use crate::mock::MockBackend; use crate::toast::{Toast, ToastKind}; @@ -222,61 +222,6 @@ impl CoreHost { self.send(CoreEvent::InstancesChanged(instances)); } - fn refresh_accounts(&mut self) { - self.accounts = accounts::accounts(&self.env, self.active_account.as_deref()); - if self.active_account.is_none() { - // 默认选中第一个账户(没有账户时保持 None = 离线占位) - self.active_account = self.accounts.first().map(|account| account.id.clone()); - self.accounts = accounts::accounts(&self.env, self.active_account.as_deref()); - } - self.attach_skins(); - self.request_skins(); - self.send(CoreEvent::AccountsChanged(self.accounts.clone())); - } - - /// 把已抓到的皮肤盖到账户视图上。 - /// - /// 只盖第三方轨:离线档案没有皮肤,微软轨不做真头像(M114 口径)—— - /// 「没有皮肤就画首字母块」,绝不给假皮肤/白块。 - fn attach_skins(&mut self) { - for account in &mut self.accounts { - if account.kind != AccountKind::Authlib { - account.skin = None; - continue; - } - if let Some(skin) = self.skins.get(&account.id) { - account.skin = skin.clone(); - } - } - } - - /// 给还没抓过皮肤的第三方轨账户起后台抓取(每个账户只抓一次)。 - /// - /// 抓不到就记 `None`:错误在面板上不显示(皮肤是装饰,不是业务失败),但也不会重试刷屏。 - fn request_skins(&mut self) { - let mut pending = Vec::new(); - for account in &self.accounts { - if account.kind != AccountKind::Authlib || account.server_url.trim().is_empty() { - continue; - } - if self.skin_requested.insert(account.id.clone()) { - pending.push(( - account.id.clone(), - account.server_url.clone(), - account.profile_id.clone(), - )); - } - } - for (id, server, profile) in pending { - let tx = self.tx.clone(); - let env = self.env.clone(); - job::spawn("account-skin", move || { - let skin = accounts::fetch_skin(&env, &server, &profile).ok(); - let _ = tx.send(HostTask::Avatar { id, skin }); - }); - } - } - fn refresh_java(&mut self) { let page = java::page( &self.env, diff --git a/crates/presenter/src/state.rs b/crates/presenter/src/state.rs index 2513378..5fad98a 100644 --- a/crates/presenter/src/state.rs +++ b/crates/presenter/src/state.rs @@ -7,9 +7,24 @@ use crate::agent::AgentRail; use crate::dialogs::DialogQueue; use crate::dto::{ // 主线(M105 的 `LoaderKind` / M113 的 `PackState`)与 M114 的 `AuthlibLoginView` 并集。 - AccountView, AgentEvent, AppEvent, AuthlibLoginView, CoreEvent, DeviceLoginView, - DiagnosisReport, InstallProgressView, InstanceRuntime, InstanceSummary, JavaPageView, - LoaderKind, LogLine, LogSession, NoticeView, PackState, Progress, SettingGroupView, + AccountView, + AgentEvent, + AppEvent, + AuthlibLoginView, + CoreEvent, + DeviceLoginView, + DiagnosisReport, + InstallProgressView, + InstanceRuntime, + InstanceSummary, + JavaPageView, + LoaderKind, + LogLine, + LogSession, + NoticeView, + PackState, + Progress, + SettingGroupView, VersionSummary, }; use crate::prefs::UiPrefs; @@ -25,27 +40,9 @@ use crate::wizard::WizardState; // 所以这里没有 `mod ai;` / `pub use ai::AiSession;`——M114 r1 曾把旧结构搬到 `state/ai.rs`, // 该文件已随 rebase 删除,**以 main 的 agent 契约为准**。 mod accounts; +mod tiles; -/// 磁贴网格单元:位置由 [`grid_positions`] 算好,UI 侧只消费 row/col。 -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct TileCell<'a> { - /// 实例数据 - pub instance: &'a InstanceSummary, - /// 网格行号(0 起) - pub row: usize, - /// 网格列号(0 起) - pub col: usize, -} - -/// 纯函数:把 n 个磁贴摊到 `columns` 列的网格上,返回 `(row, col)`。 -/// -/// 特意放在 Rust 侧:声明式 UI 的表达式里做整数除法容易引出隐式浮点,且这段逻辑可以单测。 -pub fn grid_positions(count: usize, columns: usize) -> Vec<(usize, usize)> { - let columns = columns.max(1); - (0..count) - .map(|index| (index / columns, index % columns)) - .collect() -} +pub use tiles::{grid_positions, TileCell}; /// 实时日志最多保留的行数(滚动窗口;更多行由日志页的筛选/历史会话承担)。 pub const MAX_LIVE_LINES: usize = 2_000; diff --git a/crates/presenter/src/state/tiles.rs b/crates/presenter/src/state/tiles.rs new file mode 100644 index 0000000..da8988b --- /dev/null +++ b/crates/presenter/src/state/tiles.rs @@ -0,0 +1,27 @@ +//! 磁贴网格的纯函数与单元类型(从 `state.rs` 原样搬出,守 400 行硬线)。 +//! +//! **纯搬移**:类型、函数、文档一字未改,只是换了文件;`state.rs` 用 `pub use` 再导出, +//! 于是 `crate::state::{TileCell, grid_positions}` 这两个老路径继续可用。 + +use crate::dto::InstanceSummary; + +/// 磁贴网格单元:位置由 [`grid_positions`] 算好,UI 侧只消费 row/col。 +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct TileCell<'a> { + /// 实例数据 + pub instance: &'a InstanceSummary, + /// 网格行号(0 起) + pub row: usize, + /// 网格列号(0 起) + pub col: usize, +} + +/// 纯函数:把 n 个磁贴摊到 `columns` 列的网格上,返回 `(row, col)`。 +/// +/// 特意放在 Rust 侧:声明式 UI 的表达式里做整数除法容易引出隐式浮点,且这段逻辑可以单测。 +pub fn grid_positions(count: usize, columns: usize) -> Vec<(usize, usize)> { + let columns = columns.max(1); + (0..count) + .map(|index| (index / columns, index % columns)) + .collect() +}