import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'session_store.dart'; /// 全局设置。目前两项:MCP 端口、E-Ink 模式。 /// /// 存成单独一个 `settings.json`(见 [SessionStore]),不跟会话存档混在一起 —— /// 会话存档每次求值都会重写,设置却是偶尔改一次的东西。 class AppSettings extends ChangeNotifier { AppSettings({SessionStore? store}) : _store = store ?? SessionStore(fileName: 'settings.json'); final SessionStore _store; static const int defaultMcpPort = 8734; static const int minPort = 1024; static const int maxPort = 65535; /// 近似值保留几位**小数**(工具页和计算器的"近似"都听这个)。 static const int defaultApproxDigits = 10; static const int minDigits = 0; static const int maxDigits = 15; int _mcpPort = defaultMcpPort; int _approxDigits = defaultApproxDigits; bool _eInk = false; bool _loaded = false; /// MCP 服务器监听的端口。 int get mcpPort => _mcpPort; /// 近似值位数。计算器顶栏的"近似"用它,工具页的"(近似)"那行也用它。 int get approxDigits => _approxDigits; /// 是否已从存档读过一次(界面据此决定要不要提示"设置已恢复")。 bool get loaded => _loaded; /// E-Ink 模式:所有颜色换成灰阶,动画关掉。 /// /// 给墨水屏/阳光下看屏幕用 —— 彩色在这种屏上会被抖成难看的网点, /// 动画则会留下拖影。 bool get eInk => _eInk; Future load() async { final data = await _store.load(); if (data != null) { final p = (data['mcpPort'] as num?)?.toInt(); if (p != null && p >= minPort && p <= maxPort) _mcpPort = p; final d = (data['approxDigits'] as num?)?.toInt(); if (d != null && d >= minDigits && d <= maxDigits) _approxDigits = d; _eInk = data['eInk'] == true; } _loaded = true; notifyListeners(); } Future setMcpPort(int port) async { if (port < minPort || port > maxPort || port == _mcpPort) return; _mcpPort = port; notifyListeners(); await _save(); } Future setApproxDigits(int digits) async { if (digits < minDigits || digits > maxDigits || digits == _approxDigits) { return; } _approxDigits = digits; notifyListeners(); await _save(); } Future setEInk(bool on) async { if (on == _eInk) return; _eInk = on; notifyListeners(); await _save(); } Future _save() => _store.save({ 'mcpPort': _mcpPort, 'approxDigits': _approxDigits, 'eInk': _eInk, }); } /// 弹出设置面板。控制台顶栏的齿轮按钮走这里。 Future showSettingsPanel(BuildContext context) async { await showModalBottomSheet( context: context, showDragHandle: true, isScrollControlled: true, builder: (_) => const _SettingsSheet(), ); } class _SettingsSheet extends StatefulWidget { const _SettingsSheet(); @override State<_SettingsSheet> createState() => _SettingsSheetState(); } class _SettingsSheetState extends State<_SettingsSheet> { late final AppSettings _settings = context.read(); late final TextEditingController _port = TextEditingController( text: '${_settings.mcpPort}', ); String? _portError; @override void dispose() { _port.dispose(); super.dispose(); } void _applyPort() { final v = int.tryParse(_port.text.trim()); if (v == null || v < AppSettings.minPort || v > AppSettings.maxPort) { setState( () => _portError = '端口要在 ${AppSettings.minPort}~${AppSettings.maxPort} 之间', ); return; } setState(() => _portError = null); _settings.setMcpPort(v); } @override Widget build(BuildContext context) { final settings = context.watch(); final eInk = settings.eInk; final digits = settings.approxDigits; return SafeArea( child: Padding( padding: EdgeInsets.fromLTRB( 20, 0, 20, 20 + MediaQuery.viewInsetsOf(context).bottom, ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('设置', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 16), TextField( controller: _port, keyboardType: TextInputType.number, onSubmitted: (_) => _applyPort(), onTapOutside: (_) { FocusScope.of(context).unfocus(); _applyPort(); }, decoration: InputDecoration( labelText: 'MCP 服务器端口', helperText: '默认 ${AppSettings.defaultMcpPort}。服务器正在运行时改端口会重启它。', errorText: _portError, border: const OutlineInputBorder(), isDense: true, ), ), const SizedBox(height: 8), Row( children: [ const Text('近似值小数位数'), const Spacer(), Text( '$digits', style: const TextStyle(fontFamily: 'monospace'), ), ], ), Slider( value: digits.toDouble(), min: AppSettings.minDigits.toDouble(), max: AppSettings.maxDigits.toDouble(), divisions: AppSettings.maxDigits - AppSettings.minDigits, label: '$digits', onChanged: (v) => _settings.setApproxDigits(v.round()), ), const Text( '工具页的「(近似)」和计算器的「近似」都用这个位数。', style: TextStyle(fontSize: 12), ), const SizedBox(height: 8), SwitchListTile( contentPadding: EdgeInsets.zero, value: eInk, title: const Text('E-Ink 模式'), subtitle: const Text('所有颜色换成灰阶,动画关闭'), onChanged: (v) => _settings.setEInk(v), ), ], ), ), ); } }