settings.dart 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  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. // PIN code
  134. Consumer<AppState>(
  135. builder: (context, appState, _) {
  136. return SwitchListTile(
  137. dense: true,
  138. secondary: const Icon(Icons.lock),
  139. title: Text('Enable PIN code', style: const TextStyle(fontSize: 15)),
  140. value: appState.pinEnabled,
  141. onChanged: (val) async {
  142. if (val) {
  143. // Show dialog to set PIN
  144. final pin = await showDialog<String>(
  145. context: context,
  146. builder: (context) {
  147. final controller = TextEditingController();
  148. return AlertDialog(
  149. title: const Text('Set PIN'),
  150. content: TextField(
  151. controller: controller,
  152. keyboardType: TextInputType.number,
  153. obscureText: true,
  154. maxLength: 6,
  155. decoration: const InputDecoration(labelText: 'Enter a 4-6 digit PIN'),
  156. ),
  157. actions: [
  158. TextButton(
  159. onPressed: () => Navigator.pop(context),
  160. child: const Text('Cancel'),
  161. ),
  162. TextButton(
  163. onPressed: () {
  164. if (controller.text.length >= 4 && controller.text.length <= 6) {
  165. Navigator.pop(context, controller.text);
  166. }
  167. },
  168. child: const Text('Save'),
  169. ),
  170. ],
  171. );
  172. },
  173. );
  174. if (pin != null && pin.length >= 4 && pin.length <= 6) {
  175. await appState.setPin(pin);
  176. await appState.setPinEnabled(true);
  177. }
  178. } else {
  179. await appState.setPinEnabled(false);
  180. }
  181. },
  182. );
  183. },
  184. ),
  185. // Biometric auth
  186. FutureBuilder<bool>(
  187. future: LocalAuthentication().canCheckBiometrics,
  188. builder: (context, snapshot) {
  189. if (snapshot.data != true) return const SizedBox.shrink();
  190. return Consumer<AppState>(
  191. builder: (context, appState, _) {
  192. return SwitchListTile(
  193. dense: true,
  194. secondary: const Icon(Icons.fingerprint),
  195. title: Text('Enable biometric authentication', style: const TextStyle(fontSize: 15)),
  196. value: appState.biometricEnabled,
  197. onChanged: (val) => appState.setBiometricEnabled(val),
  198. );
  199. },
  200. );
  201. },
  202. ),
  203. // Source
  204. ListTile(
  205. dense: true,
  206. leading: const Icon(Icons.code),
  207. title: Text(AppLocalizations.of(context)!.source,
  208. style: const TextStyle(fontSize: 15)),
  209. onTap: () {
  210. launchURL(Constants.repoUrl);
  211. },
  212. ),
  213. // Help
  214. ListTile(
  215. dense: true,
  216. leading: isPlatformAndroid()
  217. ? const Icon(Icons.help_outline)
  218. : const Icon(CupertinoIcons.question_circle),
  219. title: Text(AppLocalizations.of(context)!.helpTitle ?? 'Help',
  220. style: const TextStyle(fontSize: 15)),
  221. onTap: () {
  222. Navigator.of(context)
  223. .pushNamed(AppRoutes.help);
  224. },
  225. ),
  226. ],
  227. ),
  228. ),
  229. ),
  230. ],
  231. ),
  232. );
  233. }
  234. }