settings.dart 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. import 'package:flutter/widgets.dart';
  2. import 'package:flutter/material.dart' show Icons, ListTile, Material, ThemeMode, SimpleDialogOption, SimpleDialog, showDialog, SwitchListTile, AlertDialog, InputDecoration, TextField, TextButton;
  3. import 'package:flutter/cupertino.dart' show CupertinoIcons;
  4. import '../l10n/app_localizations.dart';
  5. import '../state/app_state.dart';
  6. import '../ui/adaptive.dart' show AppScaffold, isPlatformAndroid;
  7. import '../helper/url.dart' show launchURL;
  8. import 'package:package_info_plus/package_info_plus.dart' show PackageInfo;
  9. import '../config/routes.dart';
  10. import '../l10n/constants.dart';
  11. import 'package:provider/provider.dart';
  12. import 'package:local_auth/local_auth.dart';
  13. import 'package:flutter_secure_storage/flutter_secure_storage.dart';
  14. /// Settings page.
  15. class SettingsPage extends StatelessWidget {
  16. const SettingsPage({super.key});
  17. @override
  18. Widget build(BuildContext context) {
  19. return AppScaffold(
  20. title: Text(AppLocalizations.of(context)!.settingsTitle),
  21. body: Column(
  22. children: <Widget>[
  23. // App Info
  24. Padding(
  25. padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 15),
  26. child: Row(
  27. mainAxisAlignment: MainAxisAlignment.center,
  28. children: <Widget>[
  29. // App image
  30. Container(
  31. height: 125,
  32. width: 150,
  33. padding: const EdgeInsets.symmetric(horizontal: 10),
  34. child: const DecoratedBox(
  35. decoration: BoxDecoration(
  36. image: DecorationImage(
  37. image: AssetImage('graphics/icon.png'),
  38. ),
  39. ),
  40. ),
  41. ),
  42. SizedBox(
  43. width: 160,
  44. child: Column(
  45. crossAxisAlignment: CrossAxisAlignment.start,
  46. children: <Widget>[
  47. // Name
  48. Padding(
  49. padding: const EdgeInsets.symmetric(vertical: 7),
  50. child: Text(AppLocalizations.of(context)!.appName,
  51. style: const TextStyle(fontSize: 25))),
  52. // Version of app
  53. FutureBuilder(
  54. future: PackageInfo.fromPlatform(),
  55. builder: (BuildContext context,
  56. AsyncSnapshot<PackageInfo> snapshot) {
  57. if (snapshot.hasData) {
  58. return Text(snapshot.data!.version,
  59. style: const TextStyle(fontSize: 14));
  60. }
  61. return Text(AppLocalizations.of(context)!.loading);
  62. },
  63. ),
  64. ],
  65. ),
  66. )
  67. ],
  68. ),
  69. ),
  70. const SizedBox(height: 5),
  71. // List of options
  72. Expanded(
  73. child: Material(
  74. child: ListView(
  75. physics: const NeverScrollableScrollPhysics(),
  76. children: [
  77. // Theme selection
  78. Consumer<AppState>(
  79. builder: (context, appState, _) {
  80. return ListTile(
  81. dense: true,
  82. leading: const Icon(Icons.brightness_6),
  83. title: Text(AppLocalizations.of(context)!.themeMode ?? 'Theme', style: const TextStyle(fontSize: 15)),
  84. subtitle: Text(
  85. appState.themeMode == ThemeMode.system
  86. ? (AppLocalizations.of(context)!.themeSystem ?? 'System')
  87. : appState.themeMode == ThemeMode.light
  88. ? (AppLocalizations.of(context)!.themeLight ?? 'Light')
  89. : (AppLocalizations.of(context)!.themeDark ?? 'Dark'),
  90. ),
  91. onTap: () async {
  92. final selected = await showDialog<ThemeMode>(
  93. context: context,
  94. builder: (context) {
  95. return SimpleDialog(
  96. title: Text(AppLocalizations.of(context)!.themeMode ?? 'Theme'),
  97. children: [
  98. SimpleDialogOption(
  99. child: Text(AppLocalizations.of(context)!.themeSystem ?? 'System'),
  100. onPressed: () => Navigator.pop(context, ThemeMode.system),
  101. ),
  102. SimpleDialogOption(
  103. child: Text(AppLocalizations.of(context)!.themeLight ?? 'Light'),
  104. onPressed: () => Navigator.pop(context, ThemeMode.light),
  105. ),
  106. SimpleDialogOption(
  107. child: Text(AppLocalizations.of(context)!.themeDark ?? 'Dark'),
  108. onPressed: () => Navigator.pop(context, ThemeMode.dark),
  109. ),
  110. ],
  111. );
  112. },
  113. );
  114. if (selected != null) {
  115. appState.setThemeMode(selected);
  116. }
  117. },
  118. );
  119. },
  120. ),
  121. // Screen capture prevention
  122. Consumer<AppState>(
  123. builder: (context, appState, _) {
  124. return SwitchListTile(
  125. dense: true,
  126. secondary: const Icon(Icons.security),
  127. title: Text(AppLocalizations.of(context)!.preventScreenCapture ?? 'Prevent screen capture', style: const TextStyle(fontSize: 15)),
  128. value: appState.screenCapturePrevented,
  129. onChanged: (val) => appState.setScreenCapturePrevented(val),
  130. );
  131. },
  132. ),
  133. // Show/hide icons
  134. Consumer<AppState>(
  135. builder: (context, appState, _) {
  136. return SwitchListTile(
  137. dense: true,
  138. secondary: const Icon(Icons.image),
  139. title: Text('Show icons on cards', style: const TextStyle(fontSize: 15)),
  140. value: appState.showIcons,
  141. onChanged: (val) => appState.setShowIcons(val),
  142. );
  143. },
  144. ),
  145. // Force monochrome icons
  146. Consumer<AppState>(
  147. builder: (context, appState, _) {
  148. return SwitchListTile(
  149. dense: true,
  150. secondary: const Icon(Icons.format_paint),
  151. title: Text('Force monochrome icons', style: const TextStyle(fontSize: 15)),
  152. value: appState.forceMonochromeIcons,
  153. onChanged: (val) => appState.setForceMonochromeIcons(val),
  154. );
  155. },
  156. ),
  157. // PIN code
  158. Consumer<AppState>(
  159. builder: (context, appState, _) {
  160. return SwitchListTile(
  161. dense: true,
  162. secondary: const Icon(Icons.lock),
  163. title: Text('Enable PIN code', style: const TextStyle(fontSize: 15)),
  164. value: appState.pinEnabled,
  165. onChanged: (val) async {
  166. if (val) {
  167. // Show dialog to set PIN
  168. final pin = await showDialog<String>(
  169. context: context,
  170. builder: (context) {
  171. final controller = TextEditingController();
  172. return AlertDialog(
  173. title: const Text('Set PIN'),
  174. content: TextField(
  175. controller: controller,
  176. keyboardType: TextInputType.number,
  177. obscureText: true,
  178. maxLength: 6,
  179. decoration: const InputDecoration(labelText: 'Enter a 4-6 digit PIN'),
  180. ),
  181. actions: [
  182. TextButton(
  183. onPressed: () => Navigator.pop(context),
  184. child: const Text('Cancel'),
  185. ),
  186. TextButton(
  187. onPressed: () {
  188. if (controller.text.length >= 4 && controller.text.length <= 6) {
  189. Navigator.pop(context, controller.text);
  190. }
  191. },
  192. child: const Text('Save'),
  193. ),
  194. ],
  195. );
  196. },
  197. );
  198. if (pin != null && pin.length >= 4 && pin.length <= 6) {
  199. await appState.setPin(pin);
  200. await appState.setPinEnabled(true);
  201. }
  202. } else {
  203. await appState.setPinEnabled(false);
  204. }
  205. },
  206. );
  207. },
  208. ),
  209. // Biometric auth
  210. FutureBuilder<bool>(
  211. future: LocalAuthentication().canCheckBiometrics,
  212. builder: (context, snapshot) {
  213. if (snapshot.data != true) return const SizedBox.shrink();
  214. return Consumer<AppState>(
  215. builder: (context, appState, _) {
  216. return SwitchListTile(
  217. dense: true,
  218. secondary: const Icon(Icons.fingerprint),
  219. title: Text('Enable biometric authentication', style: const TextStyle(fontSize: 15)),
  220. value: appState.biometricEnabled,
  221. onChanged: (val) => appState.setBiometricEnabled(val),
  222. );
  223. },
  224. );
  225. },
  226. ),
  227. // Source
  228. ListTile(
  229. dense: true,
  230. leading: const Icon(Icons.code),
  231. title: Text(AppLocalizations.of(context)!.source,
  232. style: const TextStyle(fontSize: 15)),
  233. onTap: () {
  234. launchURL(Constants.repoUrl);
  235. },
  236. ),
  237. // Help
  238. ListTile(
  239. dense: true,
  240. leading: isPlatformAndroid()
  241. ? const Icon(Icons.help_outline)
  242. : const Icon(CupertinoIcons.question_circle),
  243. title: Text(AppLocalizations.of(context)!.helpTitle ?? 'Help',
  244. style: const TextStyle(fontSize: 15)),
  245. onTap: () {
  246. Navigator.of(context)
  247. .pushNamed(AppRoutes.help);
  248. },
  249. ),
  250. ],
  251. ),
  252. ),
  253. ),
  254. ],
  255. ),
  256. );
  257. }
  258. }