settings.dart 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  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. int _mcpPort = defaultMcpPort;
  15. bool _eInk = false;
  16. bool _loaded = false;
  17. /// MCP 服务器监听的端口。
  18. int get mcpPort => _mcpPort;
  19. /// 是否已从存档读过一次(界面据此决定要不要提示"设置已恢复")。
  20. bool get loaded => _loaded;
  21. /// E-Ink 模式:所有颜色换成灰阶,动画关掉。
  22. ///
  23. /// 给墨水屏/阳光下看屏幕用 —— 彩色在这种屏上会被抖成难看的网点,
  24. /// 动画则会留下拖影。
  25. bool get eInk => _eInk;
  26. Future<void> load() async {
  27. final data = await _store.load();
  28. if (data != null) {
  29. final p = (data['mcpPort'] as num?)?.toInt();
  30. if (p != null && p >= minPort && p <= maxPort) _mcpPort = p;
  31. _eInk = data['eInk'] == true;
  32. }
  33. _loaded = true;
  34. notifyListeners();
  35. }
  36. Future<void> setMcpPort(int port) async {
  37. if (port < minPort || port > maxPort || port == _mcpPort) return;
  38. _mcpPort = port;
  39. notifyListeners();
  40. await _save();
  41. }
  42. Future<void> setEInk(bool on) async {
  43. if (on == _eInk) return;
  44. _eInk = on;
  45. notifyListeners();
  46. await _save();
  47. }
  48. Future<void> _save() => _store.save(<String, dynamic>{
  49. 'mcpPort': _mcpPort,
  50. 'eInk': _eInk,
  51. });
  52. }
  53. /// 弹出设置面板。控制台顶栏的齿轮按钮走这里。
  54. Future<void> showSettingsPanel(BuildContext context) async {
  55. await showModalBottomSheet<void>(
  56. context: context,
  57. showDragHandle: true,
  58. isScrollControlled: true,
  59. builder: (_) => const _SettingsSheet(),
  60. );
  61. }
  62. class _SettingsSheet extends StatefulWidget {
  63. const _SettingsSheet();
  64. @override
  65. State<_SettingsSheet> createState() => _SettingsSheetState();
  66. }
  67. class _SettingsSheetState extends State<_SettingsSheet> {
  68. late final AppSettings _settings = context.read<AppSettings>();
  69. late final TextEditingController _port =
  70. TextEditingController(text: '${_settings.mcpPort}');
  71. String? _portError;
  72. @override
  73. void dispose() {
  74. _port.dispose();
  75. super.dispose();
  76. }
  77. void _applyPort() {
  78. final v = int.tryParse(_port.text.trim());
  79. if (v == null || v < AppSettings.minPort || v > AppSettings.maxPort) {
  80. setState(() => _portError =
  81. '端口要在 ${AppSettings.minPort}~${AppSettings.maxPort} 之间');
  82. return;
  83. }
  84. setState(() => _portError = null);
  85. _settings.setMcpPort(v);
  86. }
  87. @override
  88. Widget build(BuildContext context) {
  89. final eInk = context.watch<AppSettings>().eInk;
  90. return SafeArea(
  91. child: Padding(
  92. padding: EdgeInsets.fromLTRB(
  93. 20,
  94. 0,
  95. 20,
  96. 20 + MediaQuery.viewInsetsOf(context).bottom,
  97. ),
  98. child: Column(
  99. mainAxisSize: MainAxisSize.min,
  100. crossAxisAlignment: CrossAxisAlignment.start,
  101. children: [
  102. Text('设置', style: Theme.of(context).textTheme.titleMedium),
  103. const SizedBox(height: 16),
  104. TextField(
  105. controller: _port,
  106. keyboardType: TextInputType.number,
  107. onSubmitted: (_) => _applyPort(),
  108. onTapOutside: (_) {
  109. FocusScope.of(context).unfocus();
  110. _applyPort();
  111. },
  112. decoration: InputDecoration(
  113. labelText: 'MCP 服务器端口',
  114. helperText: '默认 ${AppSettings.defaultMcpPort}。服务器正在运行时改端口会重启它。',
  115. errorText: _portError,
  116. border: const OutlineInputBorder(),
  117. isDense: true,
  118. ),
  119. ),
  120. const SizedBox(height: 8),
  121. SwitchListTile(
  122. contentPadding: EdgeInsets.zero,
  123. value: eInk,
  124. title: const Text('E-Ink 模式'),
  125. subtitle: const Text('所有颜色换成灰阶,动画关闭'),
  126. onChanged: (v) => _settings.setEInk(v),
  127. ),
  128. ],
  129. ),
  130. ),
  131. );
  132. }
  133. }