main.dart 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. import 'package:flutter/material.dart';
  2. import 'package:provider/provider.dart';
  3. import 'calculator_page.dart';
  4. import 'console_page.dart';
  5. import 'settings.dart';
  6. import 'symja.dart';
  7. import 'tools_page.dart';
  8. void main() => runApp(const CalApp());
  9. class CalApp extends StatelessWidget {
  10. const CalApp({super.key});
  11. @override
  12. Widget build(BuildContext context) {
  13. return MultiProvider(
  14. providers: [
  15. ChangeNotifierProvider(create: (_) => SymjaSession()),
  16. // 设置异步读盘,读完 notifyListeners 一次,界面自己会换上 E-Ink
  17. ChangeNotifierProvider(create: (_) => AppSettings()..load()),
  18. ],
  19. child: const _App(),
  20. );
  21. }
  22. }
  23. class _App extends StatelessWidget {
  24. const _App();
  25. @override
  26. Widget build(BuildContext context) {
  27. final settings = context.watch<AppSettings>();
  28. final eInk = settings.eInk;
  29. // 近似位数只有一个来源(设置里那个滑动条):读出来直接给会话,
  30. // 计算器的"近似"和工具页的"(近似)"就都是这个位数。
  31. context.read<SymjaSession>().approximateDigits = settings.approxDigits;
  32. return MaterialApp(
  33. title: '某人的计算器2',
  34. debugShowCheckedModeBanner: false,
  35. theme: _theme(eInk),
  36. themeAnimationDuration: eInk ? Duration.zero : kThemeAnimationDuration,
  37. // **始终**套同样两层的 widget,只改参数:切 E-Ink 时若把这两层加进来/
  38. // 拿出去,Navigator 会因为"这一格的 widget 类型变了"被整个重建,
  39. // 表现是"一开 E-Ink 就被踢回首页、弹着的面板也没了"。
  40. builder: (context, child) => MediaQuery(
  41. data: MediaQuery.of(context)
  42. .copyWith(disableAnimations: eInk),
  43. child: ColorFiltered(
  44. // 关掉时用单位矩阵,等于没过滤镜(`ColorFiltered` 本身还在,
  45. // 但任何颜色乘上单位矩阵都不变)
  46. colorFilter: eInk ? _Grayscale.filter : _Grayscale.identity,
  47. child: child!,
  48. ),
  49. ),
  50. home: const ShellPage(),
  51. );
  52. }
  53. /// E-Ink 模式下额外关掉动画:路由切换和点击水波纹是屏幕上最明显的两处动画。
  54. ThemeData _theme(bool eInk) {
  55. final base = ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true);
  56. if (!eInk) return base;
  57. // 白底黑字。只靠灰阶滤镜不够 —— 配色本来就有浅灰底、浅灰卡片,
  58. // 灰阶之后还是灰,看着发闷(而且键盘的浅色按键会跟浅色底糊在一起)。
  59. // 这里把整个配色压成"白底、黑字、黑线",再让下面的控件自己描边。
  60. final cs = base.colorScheme.copyWith(
  61. primary: Colors.black,
  62. onPrimary: Colors.white,
  63. primaryContainer: Colors.white,
  64. onPrimaryContainer: Colors.black,
  65. secondary: Colors.black,
  66. onSecondary: Colors.white,
  67. secondaryContainer: const Color(0xFFE0E0E0),
  68. onSecondaryContainer: Colors.black,
  69. surface: Colors.white,
  70. onSurface: Colors.black,
  71. surfaceContainerLowest: Colors.white,
  72. surfaceContainerLow: Colors.white,
  73. surfaceContainer: Colors.white,
  74. surfaceContainerHigh: Colors.white,
  75. surfaceContainerHighest: Colors.white,
  76. onSurfaceVariant: Colors.black,
  77. outline: Colors.black,
  78. outlineVariant: const Color(0xFF9E9E9E),
  79. inverseSurface: Colors.black,
  80. onInverseSurface: Colors.white,
  81. error: Colors.black,
  82. onError: Colors.white,
  83. errorContainer: Colors.white,
  84. onErrorContainer: Colors.black,
  85. shadow: Colors.black,
  86. scrim: Colors.black,
  87. );
  88. return base.copyWith(
  89. colorScheme: cs,
  90. scaffoldBackgroundColor: Colors.white,
  91. canvasColor: Colors.white,
  92. appBarTheme: const AppBarTheme(
  93. backgroundColor: Colors.white,
  94. foregroundColor: Colors.black,
  95. elevation: 0,
  96. scrolledUnderElevation: 0,
  97. ),
  98. navigationBarTheme: const NavigationBarThemeData(
  99. backgroundColor: Colors.white,
  100. indicatorColor: Color(0xFFE0E0E0),
  101. ),
  102. // 卡片全靠描边分辨,不靠阴影(墨水屏上阴影就是一团脏灰)
  103. cardTheme: CardThemeData(
  104. color: Colors.white,
  105. elevation: 0,
  106. shape: RoundedRectangleBorder(
  107. borderRadius: BorderRadius.circular(12),
  108. side: const BorderSide(color: Color(0xFF9E9E9E)),
  109. ),
  110. ),
  111. dividerTheme: const DividerThemeData(color: Color(0xFF9E9E9E)),
  112. splashFactory: NoSplash.splashFactory,
  113. pageTransitionsTheme: const PageTransitionsTheme(
  114. builders: <TargetPlatform, PageTransitionsBuilder>{
  115. TargetPlatform.android: _NoTransition(),
  116. TargetPlatform.iOS: _NoTransition(),
  117. TargetPlatform.fuchsia: _NoTransition(),
  118. TargetPlatform.linux: _NoTransition(),
  119. TargetPlatform.macOS: _NoTransition(),
  120. TargetPlatform.windows: _NoTransition(),
  121. },
  122. ),
  123. );
  124. }
  125. }
  126. class _NoTransition extends PageTransitionsBuilder {
  127. const _NoTransition();
  128. @override
  129. Widget buildTransitions<T>(
  130. PageRoute<T> route,
  131. BuildContext context,
  132. Animation<double> animation,
  133. Animation<double> secondaryAnimation,
  134. Widget child,
  135. ) =>
  136. child;
  137. }
  138. /// 灰阶滤镜。
  139. ///
  140. /// 用**一个**颜色滤镜覆盖全局,而不是去给每个配色/每张图表换一套灰阶调色板 ——
  141. /// 那些颜色散在主题、工具卡片、绘图组件、Symja 的 PNG 里,逐个改必然漏。
  142. /// E-Ink 关掉时套的是单位矩阵(同样的 widget、同样的开销,只是颜色不变),
  143. /// 这样切换不会动到 widget 树的形状。
  144. class _Grayscale {
  145. const _Grayscale._();
  146. /// 亮度权重就是通常的 Rec.709 公式。
  147. static const ColorFilter filter = ColorFilter.matrix(<double>[
  148. 0.2126, 0.7152, 0.0722, 0, 0, //
  149. 0.2126, 0.7152, 0.0722, 0, 0, //
  150. 0.2126, 0.7152, 0.0722, 0, 0, //
  151. 0, 0, 0, 1, 0, //
  152. ]);
  153. static const ColorFilter identity = ColorFilter.matrix(<double>[
  154. 1, 0, 0, 0, 0, //
  155. 0, 1, 0, 0, 0, //
  156. 0, 0, 1, 0, 0, //
  157. 0, 0, 0, 1, 0, //
  158. ]);
  159. }
  160. /// 三个界面(Q16 的「双界面」+ 插件页):
  161. /// 计算器 —— 默认入口,卡西欧式按键
  162. /// 工具 —— 16 个插件的网格入口
  163. /// 控制台 —— 表达式 REPL
  164. ///
  165. /// 用 IndexedStack 保住各页状态:切回来时控制台的历史、计算器正在输入的
  166. /// 表达式都还在。
  167. class ShellPage extends StatefulWidget {
  168. const ShellPage({super.key});
  169. @override
  170. State<ShellPage> createState() => _ShellPageState();
  171. }
  172. class _ShellPageState extends State<ShellPage> {
  173. int _index = 0;
  174. @override
  175. Widget build(BuildContext context) {
  176. return Scaffold(
  177. body: IndexedStack(
  178. index: _index,
  179. children: const [
  180. CalculatorPage(),
  181. ToolsPage(),
  182. ConsolePage(),
  183. ],
  184. ),
  185. bottomNavigationBar: NavigationBar(
  186. selectedIndex: _index,
  187. onDestinationSelected: (i) => setState(() => _index = i),
  188. destinations: const [
  189. NavigationDestination(
  190. icon: Icon(Icons.calculate_outlined),
  191. selectedIcon: Icon(Icons.calculate),
  192. label: '计算器',
  193. ),
  194. NavigationDestination(
  195. icon: Icon(Icons.widgets_outlined),
  196. selectedIcon: Icon(Icons.widgets),
  197. label: '工具',
  198. ),
  199. NavigationDestination(
  200. icon: Icon(Icons.terminal_outlined),
  201. selectedIcon: Icon(Icons.terminal),
  202. label: '控制台',
  203. ),
  204. ],
  205. ),
  206. );
  207. }
  208. }