settings.dart 6.2 KB

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