settings.dart 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. import 'package:flutter/material.dart';
  2. import 'package:provider/provider.dart';
  3. import 'session_store.dart';
  4. /// 全局设置。目前两项:MCP 端口、E-Ink 模式。
  5. ///
  6. /// 存成单独一个 `settings.json`(见 [SessionStore]),不跟会话存档混在一起 ——
  7. /// 会话存档每次求值都会重写,设置却是偶尔改一次的东西。
  8. class AppSettings extends ChangeNotifier {
  9. AppSettings({SessionStore? store}) : _store = store ?? SessionStore(fileName: 'settings.json');
  10. final SessionStore _store;
  11. static const int defaultMcpPort = 8734;
  12. static const int minPort = 1024;
  13. static const int maxPort = 65535;
  14. /// 近似值保留几位有效数字(工具页和计算器的"近似"都听这个)。
  15. static const int defaultApproxDigits = 15;
  16. static const int minDigits = 2;
  17. static const int maxDigits = 20;
  18. int _mcpPort = defaultMcpPort;
  19. int _approxDigits = defaultApproxDigits;
  20. bool _eInk = false;
  21. bool _loaded = false;
  22. /// MCP 服务器监听的端口。
  23. int get mcpPort => _mcpPort;
  24. /// 近似值位数。计算器顶栏的"近似"用它,工具页的"(近似)"那行也用它。
  25. int get approxDigits => _approxDigits;
  26. /// 是否已从存档读过一次(界面据此决定要不要提示"设置已恢复")。
  27. bool get loaded => _loaded;
  28. /// E-Ink 模式:所有颜色换成灰阶,动画关掉。
  29. ///
  30. /// 给墨水屏/阳光下看屏幕用 —— 彩色在这种屏上会被抖成难看的网点,
  31. /// 动画则会留下拖影。
  32. bool get eInk => _eInk;
  33. Future<void> load() async {
  34. final data = await _store.load();
  35. if (data != null) {
  36. final p = (data['mcpPort'] as num?)?.toInt();
  37. if (p != null && p >= minPort && p <= maxPort) _mcpPort = p;
  38. final d = (data['approxDigits'] as num?)?.toInt();
  39. if (d != null && d >= minDigits && d <= maxDigits) _approxDigits = d;
  40. _eInk = data['eInk'] == true;
  41. }
  42. _loaded = true;
  43. notifyListeners();
  44. }
  45. Future<void> setMcpPort(int port) async {
  46. if (port < minPort || port > maxPort || port == _mcpPort) return;
  47. _mcpPort = port;
  48. notifyListeners();
  49. await _save();
  50. }
  51. Future<void> setApproxDigits(int digits) async {
  52. if (digits < minDigits || digits > maxDigits || digits == _approxDigits) {
  53. return;
  54. }
  55. _approxDigits = digits;
  56. notifyListeners();
  57. await _save();
  58. }
  59. Future<void> setEInk(bool on) async {
  60. if (on == _eInk) return;
  61. _eInk = on;
  62. notifyListeners();
  63. await _save();
  64. }
  65. Future<void> _save() => _store.save(<String, dynamic>{
  66. 'mcpPort': _mcpPort,
  67. 'approxDigits': _approxDigits,
  68. 'eInk': _eInk,
  69. });
  70. }
  71. /// 弹出设置面板。控制台顶栏的齿轮按钮走这里。
  72. Future<void> showSettingsPanel(BuildContext context) async {
  73. await showModalBottomSheet<void>(
  74. context: context,
  75. showDragHandle: true,
  76. isScrollControlled: true,
  77. builder: (_) => const _SettingsSheet(),
  78. );
  79. }
  80. class _SettingsSheet extends StatefulWidget {
  81. const _SettingsSheet();
  82. @override
  83. State<_SettingsSheet> createState() => _SettingsSheetState();
  84. }
  85. class _SettingsSheetState extends State<_SettingsSheet> {
  86. late final AppSettings _settings = context.read<AppSettings>();
  87. late final TextEditingController _port =
  88. TextEditingController(text: '${_settings.mcpPort}');
  89. String? _portError;
  90. @override
  91. void dispose() {
  92. _port.dispose();
  93. super.dispose();
  94. }
  95. void _applyPort() {
  96. final v = int.tryParse(_port.text.trim());
  97. if (v == null || v < AppSettings.minPort || v > AppSettings.maxPort) {
  98. setState(() => _portError =
  99. '端口要在 ${AppSettings.minPort}~${AppSettings.maxPort} 之间');
  100. return;
  101. }
  102. setState(() => _portError = null);
  103. _settings.setMcpPort(v);
  104. }
  105. @override
  106. Widget build(BuildContext context) {
  107. final settings = context.watch<AppSettings>();
  108. final eInk = settings.eInk;
  109. final digits = settings.approxDigits;
  110. return SafeArea(
  111. child: Padding(
  112. padding: EdgeInsets.fromLTRB(
  113. 20,
  114. 0,
  115. 20,
  116. 20 + MediaQuery.viewInsetsOf(context).bottom,
  117. ),
  118. child: Column(
  119. mainAxisSize: MainAxisSize.min,
  120. crossAxisAlignment: CrossAxisAlignment.start,
  121. children: [
  122. Text('设置', style: Theme.of(context).textTheme.titleMedium),
  123. const SizedBox(height: 16),
  124. TextField(
  125. controller: _port,
  126. keyboardType: TextInputType.number,
  127. onSubmitted: (_) => _applyPort(),
  128. onTapOutside: (_) {
  129. FocusScope.of(context).unfocus();
  130. _applyPort();
  131. },
  132. decoration: InputDecoration(
  133. labelText: 'MCP 服务器端口',
  134. helperText: '默认 ${AppSettings.defaultMcpPort}。服务器正在运行时改端口会重启它。',
  135. errorText: _portError,
  136. border: const OutlineInputBorder(),
  137. isDense: true,
  138. ),
  139. ),
  140. const SizedBox(height: 8),
  141. Row(
  142. children: [
  143. const Text('近似值位数'),
  144. const Spacer(),
  145. Text('$digits',
  146. style: const TextStyle(fontFamily: 'monospace')),
  147. ],
  148. ),
  149. Slider(
  150. value: digits.toDouble(),
  151. min: AppSettings.minDigits.toDouble(),
  152. max: AppSettings.maxDigits.toDouble(),
  153. divisions: AppSettings.maxDigits - AppSettings.minDigits,
  154. label: '$digits',
  155. onChanged: (v) => _settings.setApproxDigits(v.round()),
  156. ),
  157. const Text('工具页的「(近似)」和计算器的「近似」都用这个位数。',
  158. style: TextStyle(fontSize: 12)),
  159. const SizedBox(height: 8),
  160. SwitchListTile(
  161. contentPadding: EdgeInsets.zero,
  162. value: eInk,
  163. title: const Text('E-Ink 模式'),
  164. subtitle: const Text('所有颜色换成灰阶,动画关闭'),
  165. onChanged: (v) => _settings.setEInk(v),
  166. ),
  167. ],
  168. ),
  169. ),
  170. );
  171. }
  172. }