设置页面集中管理应用主题、节点连接、媒体质量、AI 集成等全局配置。关键数据通过 SharedPreferences 持久化,状态变更实时通过 GetX/GetIt 响应式传播到各界面。
| 文件 | 说明 |
|---|---|
lib/pages/settings/settings_page.dart |
主页面(SettingsPage,TabController) |
lib/pages/settings/components/theme_settings_tab.dart |
主题设置 Tab |
lib/pages/settings/components/node_settings_tab.dart |
节点设置 Tab |
lib/pages/settings/components/media_settings_tab.dart |
媒体库设置 Tab |
lib/pages/settings/components/ollama_settings_tab.dart |
Ollama AI 设置 Tab |
lib/pages/settings/components/settings_tab_placeholder.dart |
占位 Tab(账户、通知、书籍) |
lib/core/services/node/node_settings_service.dart |
节点配置 Service(GetxService) |
lib/core/services/media_prefs_service.dart |
媒体偏好 Service(GetIt 单例) |
SettingsPage(TabController,6 个 Tab)
├── 主题设置 → ThemeSettingsTab
├── 节点设置 → NodeSettingsTab
├── 资源库 → _ResourcesSettingsTab(嵌套 TabController)
│ ├── 媒体设置 → MediaSettingsTab
│ └── 书籍设置 → SettingsTabPlaceholder(待实现)
├── Ollama 设置 → OllamaSettingsTab
├── 账户设置 → SettingsTabPlaceholder(待实现)
└── 通知设置 → SettingsTabPlaceholder(待实现)
ThemeSettingsTab)SharedPreferences)| 设置项 | Key | 默认值 |
|---|---|---|
| 主题模式 | theme_mode |
ThemeMode.system |
| 强调色 | accent_color |
主色(蓝色) |
| 字体缩放 | font_scale |
1.0 |
修改后直接更新 AppTheme 响应式变量,MaterialApp 内部 Obx 监听后立即重建:
AppTheme.themeModeObs.value = ThemeMode.dark;
AppTheme.accentColorObs.value = Colors.purple;
AppTheme.fontScaleObs.value = 1.2;
| 颜色名 | 对应常量 |
|---|---|
| 默认蓝 | LightColors.primary |
| 紫 | LightColors.purple |
| 靛蓝 | LightColors.indigo |
| 天蓝 | LightColors.blue |
| 青 | LightColors.cyan |
| 薄荷 | LightColors.mint |
| 绿 | LightColors.green |
| 黄 | LightColors.yellow |
| 橙 | LightColors.orange |
| 红 | LightColors.red |
NodeSettingsTab + NodeSettingsService)| 设置项 | SharedPreferences Key | 默认值 |
|---|---|---|
| 启用本地节点 | node_local_enabled |
false |
| 节点名称 | node_local_name |
'本机节点' |
| 监听端口 | node_local_port |
17888 |
| 本机授权码 | node_local_auth_code |
首次加载自动生成 |
启动/停止流程:
// 启动本地 Rust HTTP 服务器
await http_bridge_api.startNodeServer(
host: '0.0.0.0',
port: localNodePort.value,
name: localNodeName.value,
authCode: localNodeAuthCode.value, // 空串 = 不启用授权校验
);
// → Rust crate::node_server::start_node_server(host, port, name, auth_code)
// Rust 侧只保存 sha256(auth_code),端口/授权码变更需 restartLocalNodeServer() 重建监听
本地节点 HTTP 服务提供以下路由(由 Rust node_server 模块实现):
| 路由 | 方法 | 说明 |
|---|---|---|
/health |
GET | 健康检查,返回节点名称和端口(免授权) |
/node/call |
POST | 功能调用分发({ action, params } JSON Body) |
/node/media |
GET | 媒体文件服务(支持 Range 请求) |
/node/upload |
POST | 文件上传(multipart/form-data) |
/node/upload/archive |
POST | 归档上传(请求体为 zip 原始字节,?dest= 指定已存在的绝对目录,节点流式落盘后解压) |
节点配置授权码后,除 /health 与 OPTIONS 预检外的请求一律要求凭据,缺失/错误直接 401(且在读 body 之前拒绝):
X-SW-Auth: sha256(授权码),由 Dio 拦截器 _NodeAuthInterceptor 按 URL 匹配节点自动注入;/node/media 额外接受 ?sw_auth=<sha256(授权码)>,因为图片/视频 URL 会直接交给 Image.network 与播放器,无法带自定义请求头;详见 docs/node_server_security.md §6。
// 节点配置
class NodeEndpoint {
String id; // 节点唯一 ID(格式:node_<timestamp>_<rand>)
String name; // 显示名称
String apiBaseUrl; // 基础 URL(如 http://192.168.1.100:17888)
bool enabled; // 是否启用
bool supportsMove; // 是否支持集合移动
bool supportsCoverUpdate; // 是否支持封面更新
String authCode; // 对端授权码明文('' = 该节点不校验)
}
持久化:SharedPreferences key node_remote_nodes(JSON 数组)。
节点被判定「本轮运行不再自动请求」时进入熔断集合(_circuitBreakedNodes,内存态、不落盘):
_quickProbeNode,逐个候选地址)仍无响应 → 加入熔断集合并打日志
节点已熔断: <名称>(请求超时且 N 个候选地址均无响应);连通性检测走同样口径
(快速档 200ms → 确认档 1500ms 复探),两条路径都会输出可定位原因的日志_ensureNodeReachable,同节点并发共用一次探测)——
节点活着就当场解除并继续正常请求,确实不通才抛 StateError('节点已熔断…')resetNodeCircuitBreaker(nodeId) + 重新连通性检测recheckOfflineNodes() 由媒体库后台刷远程那一轮
(_refreshRemoteBackground,即进页面 / 点工具栏刷新)先跑一次,只探判为离线或已熔断的启用节点,
全在线时零流量;探通的节点补预热元数据并 libraryMutationTick++ 通知已打开的页面重取。
没有后台定时轮询——节点长期不可达时定时复探只会每 20 秒刷一串超时日志,救不活任何东西服务端配合:
ping在连接线程就地回答(不进共享 tokio runtime 队列),并发打满时回503而不是静默断连。否则节点一忙,探测就排在缩略图/导入任务后面超时,健康节点会被错误熔断。 客户端对 503/429 退避重试 2 次(_nodeBusyRetries)。
// 检测单个节点(手动操作)
await nodeSettingsService.checkNodeConnectivity(nodeId);
// 检测全部节点(启动时一轮 + 由调用方按需触发)
await nodeSettingsService.refreshNodeConnectivity();
结果存储:
nodeConnectivity[nodeId]:true = 在线nodeConnectivityError[nodeId]:错误信息文本节点同时填了 lanApiBaseUrl(内网)和 apiBaseUrl(外网)时,effectiveApiBaseUrl 恒等于内网地址——
内网一旦失效(换网段、对端不在了),取数会每轮先付一遍连接超时,图片/上传 URL 更是直接指向死地址,
表现为”外网明明通,媒体库里却没有这个节点的内容”。因此服务层额外记一份 最近应答过的那一路:
checkNodeConnectivity(并发探 LAN/WAN,取第一个应答,LAN 优先)、_reprobeNode、
_quickProbeNode、以及 _performNodeCall 拿到任意 HTTP 应答时getNodeEffectiveBaseUrl / buildNodeMediaUrl / buildNodeUploadUrl /
归档上传地址,以及 power_stats、aliyun_ddns 的节点端点[首选, LAN, WAN] 去重后的全集,所以上一次失败也能就地换到另一路节点首次成功响应后,若实际生效的 URL 与配置不同(例如补全了端口号 17888),会自动将修正后的地址写回配置,下次直接使用正确地址。
| 状态变量 | 说明 |
|---|---|
appRxKbps |
应用下行速率(kB/s) |
appTxKbps |
应用上行速率(kB/s) |
nodeRequestCount |
本地节点累计请求数 |
流量数据由节点请求/响应时通过 _recordAppTraffic(txBytes, rxBytes) 实时累积,Dashboard 页面每秒调用 syncTrafficDisplayNow() 刷新展示值,1.5 秒无新流量后自动归零。
相同 nodeId + action + params 的并发请求会复用同一个 Future(_inFlightNodeCalls),避免在快速刷新时向同一节点重复发起相同请求。
MediaSettingsTab + MediaPrefsService)| 设置项 | SharedPreferences Key | 默认值 | 说明 |
|---|---|---|---|
| 缩略图质量(1-5) | media_thumb_quality |
3 | 控制 ffmpeg 缩放分辨率和质量参数 |
| 缩略图并发量(1-20) | media_thumb_concurrency |
2 | 同时生成缩略图的最大任务数 |
| 远程封面宽度(px) | media_remote_cover_width |
240 | 节点返回封面图的最大宽度 |
| 远程图片预览宽度(px) | media_remote_image_width |
0 | 0 = 原图 |
| 本地图片预览宽度(px) | media_local_preview_width |
480 | 本地加载时的最大宽度 |
| 缓存大小上限(字节) | media_cache_limit_bytes |
1 GB | 超出后清理最旧缓存 |
缩略图质量等级对照:
| 等级 | 分辨率(宽) | qscale | 帧采样数 |
|---|---|---|---|
| 1(最低) | 120 px | 10 | 3 帧 |
| 2 | 180 px | 8 | 5 帧 |
| 3(默认) | 240 px | 6 | 6 帧 |
| 4 | 360 px | 4 | 8 帧 |
| 5(最高) | 480 px | 2 | 10 帧 |
OllamaSettingsTab)配置本地或远程 Ollama 服务,用于 AI 辅助功能(书籍摘要、智能搜索等,未来功能)。
| 设置项 | Key | 默认值 |
|---|---|---|
| Ollama 地址 | ollama_base_url |
http://localhost:11434 |
| 默认模型 | ollama_model |
— |
所有设置均通过 SharedPreferences 持久化,Service 在应用启动时通过 init() 方法同步加载,无 Server 端依赖:
// 应用启动流程
GetIt.instance.registerSingletonAsync<NodeSettingsService>(() async {
final service = NodeSettingsService();
await service.init(); // 加载 SharedPreferences + 启动本地节点(若已启用)
return service;
});