| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195 |
- 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 = 15;
- static const int minDigits = 2;
- static const int maxDigits = 20;
- 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<void> 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<void> setMcpPort(int port) async {
- if (port < minPort || port > maxPort || port == _mcpPort) return;
- _mcpPort = port;
- notifyListeners();
- await _save();
- }
- Future<void> setApproxDigits(int digits) async {
- if (digits < minDigits || digits > maxDigits || digits == _approxDigits) {
- return;
- }
- _approxDigits = digits;
- notifyListeners();
- await _save();
- }
- Future<void> setEInk(bool on) async {
- if (on == _eInk) return;
- _eInk = on;
- notifyListeners();
- await _save();
- }
- Future<void> _save() => _store.save(<String, dynamic>{
- 'mcpPort': _mcpPort,
- 'approxDigits': _approxDigits,
- 'eInk': _eInk,
- });
- }
- /// 弹出设置面板。控制台顶栏的齿轮按钮走这里。
- Future<void> showSettingsPanel(BuildContext context) async {
- await showModalBottomSheet<void>(
- 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<AppSettings>();
- 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<AppSettings>();
- 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),
- ),
- ],
- ),
- ),
- );
- }
- }
|