settings.dart 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  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. // Language selection
  78. Consumer<AppState>(
  79. builder: (context, appState, _) {
  80. final currentLocale = appState.locale.languageCode;
  81. return ListTile(
  82. dense: true,
  83. leading: const Icon(Icons.language),
  84. title: Text('Language', style: const TextStyle(fontSize: 15)),
  85. subtitle: Text(currentLocale == 'fa' ? 'فارسی' : 'English'),
  86. onTap: () async {
  87. final selected = await showDialog<String>(
  88. context: context,
  89. builder: (context) {
  90. return SimpleDialog(
  91. title: const Text('Select Language'),
  92. children: [
  93. SimpleDialogOption(
  94. child: const Text('English'),
  95. onPressed: () => Navigator.pop(context, 'en'),
  96. ),
  97. SimpleDialogOption(
  98. child: const Text('فارسی'),
  99. onPressed: () => Navigator.pop(context, 'fa'),
  100. ),
  101. ],
  102. );
  103. },
  104. );
  105. if (selected != null && selected != currentLocale) {
  106. appState.setLocale(Locale(selected));
  107. }
  108. },
  109. );
  110. },
  111. ),
  112. // Theme selection
  113. Consumer<AppState>(
  114. builder: (context, appState, _) {
  115. return ListTile(
  116. dense: true,
  117. leading: const Icon(Icons.brightness_6),
  118. title: Text(AppLocalizations.of(context)!.themeMode ?? 'Theme', style: const TextStyle(fontSize: 15)),
  119. subtitle: Text(
  120. appState.themeMode == ThemeMode.system
  121. ? (AppLocalizations.of(context)!.themeSystem ?? 'System')
  122. : appState.themeMode == ThemeMode.light
  123. ? (AppLocalizations.of(context)!.themeLight ?? 'Light')
  124. : (AppLocalizations.of(context)!.themeDark ?? 'Dark'),
  125. ),
  126. onTap: () async {
  127. final selected = await showDialog<ThemeMode>(
  128. context: context,
  129. builder: (context) {
  130. return SimpleDialog(
  131. title: Text(AppLocalizations.of(context)!.themeMode ?? 'Theme'),
  132. children: [
  133. SimpleDialogOption(
  134. child: Text(AppLocalizations.of(context)!.themeSystem ?? 'System'),
  135. onPressed: () => Navigator.pop(context, ThemeMode.system),
  136. ),
  137. SimpleDialogOption(
  138. child: Text(AppLocalizations.of(context)!.themeLight ?? 'Light'),
  139. onPressed: () => Navigator.pop(context, ThemeMode.light),
  140. ),
  141. SimpleDialogOption(
  142. child: Text(AppLocalizations.of(context)!.themeDark ?? 'Dark'),
  143. onPressed: () => Navigator.pop(context, ThemeMode.dark),
  144. ),
  145. ],
  146. );
  147. },
  148. );
  149. if (selected != null) {
  150. appState.setThemeMode(selected);
  151. }
  152. },
  153. );
  154. },
  155. ),
  156. // Screen capture prevention
  157. Consumer<AppState>(
  158. builder: (context, appState, _) {
  159. return SwitchListTile(
  160. dense: true,
  161. secondary: const Icon(Icons.security),
  162. title: Text(AppLocalizations.of(context)!.preventScreenCapture ?? 'Prevent screen capture', style: const TextStyle(fontSize: 15)),
  163. value: appState.screenCapturePrevented,
  164. onChanged: (val) => appState.setScreenCapturePrevented(val),
  165. );
  166. },
  167. ),
  168. // Show/hide icons
  169. Consumer<AppState>(
  170. builder: (context, appState, _) {
  171. return SwitchListTile(
  172. dense: true,
  173. secondary: const Icon(Icons.image),
  174. title: Text('Show icons on cards', style: const TextStyle(fontSize: 15)),
  175. value: appState.showIcons,
  176. onChanged: (val) => appState.setShowIcons(val),
  177. );
  178. },
  179. ),
  180. // Force monochrome icons
  181. Consumer<AppState>(
  182. builder: (context, appState, _) {
  183. return SwitchListTile(
  184. dense: true,
  185. secondary: const Icon(Icons.format_paint),
  186. title: Text('Force monochrome icons', style: const TextStyle(fontSize: 15)),
  187. value: appState.forceMonochromeIcons,
  188. onChanged: (val) => appState.setForceMonochromeIcons(val),
  189. );
  190. },
  191. ),
  192. // PIN code
  193. Consumer<AppState>(
  194. builder: (context, appState, _) {
  195. return SwitchListTile(
  196. dense: true,
  197. secondary: const Icon(Icons.lock),
  198. title: Text('Enable PIN code', style: const TextStyle(fontSize: 15)),
  199. value: appState.pinEnabled,
  200. onChanged: (val) async {
  201. if (val) {
  202. // Show dialog to set PIN
  203. final pin = await showDialog<String>(
  204. context: context,
  205. builder: (context) {
  206. final controller = TextEditingController();
  207. return AlertDialog(
  208. title: const Text('Set PIN'),
  209. content: TextField(
  210. controller: controller,
  211. keyboardType: TextInputType.number,
  212. obscureText: true,
  213. maxLength: 6,
  214. decoration: const InputDecoration(labelText: 'Enter a 4-6 digit PIN'),
  215. ),
  216. actions: [
  217. TextButton(
  218. onPressed: () => Navigator.pop(context),
  219. child: const Text('Cancel'),
  220. ),
  221. TextButton(
  222. onPressed: () {
  223. if (controller.text.length >= 4 && controller.text.length <= 6) {
  224. Navigator.pop(context, controller.text);
  225. }
  226. },
  227. child: const Text('Save'),
  228. ),
  229. ],
  230. );
  231. },
  232. );
  233. if (pin != null && pin.length >= 4 && pin.length <= 6) {
  234. await appState.setPin(pin);
  235. await appState.setPinEnabled(true);
  236. }
  237. } else {
  238. await appState.setPinEnabled(false);
  239. }
  240. },
  241. );
  242. },
  243. ),
  244. // Biometric auth
  245. FutureBuilder<bool>(
  246. future: LocalAuthentication().canCheckBiometrics,
  247. builder: (context, snapshot) {
  248. if (snapshot.data != true) return const SizedBox.shrink();
  249. return Consumer<AppState>(
  250. builder: (context, appState, _) {
  251. return SwitchListTile(
  252. dense: true,
  253. secondary: const Icon(Icons.fingerprint),
  254. title: Text('Enable biometric authentication', style: const TextStyle(fontSize: 15)),
  255. value: appState.biometricEnabled,
  256. onChanged: (val) => appState.setBiometricEnabled(val),
  257. );
  258. },
  259. );
  260. },
  261. ),
  262. // Source
  263. ListTile(
  264. dense: true,
  265. leading: const Icon(Icons.code),
  266. title: Text(AppLocalizations.of(context)!.source,
  267. style: const TextStyle(fontSize: 15)),
  268. onTap: () {
  269. launchURL(Constants.repoUrl);
  270. },
  271. ),
  272. // Help
  273. ListTile(
  274. dense: true,
  275. leading: isPlatformAndroid()
  276. ? const Icon(Icons.help_outline)
  277. : const Icon(CupertinoIcons.question_circle),
  278. title: Text(AppLocalizations.of(context)!.helpTitle ?? 'Help',
  279. style: const TextStyle(fontSize: 15)),
  280. onTap: () {
  281. Navigator.of(context)
  282. .pushNamed(AppRoutes.help);
  283. },
  284. ),
  285. ],
  286. ),
  287. ),
  288. ),
  289. ],
  290. ),
  291. );
  292. }
  293. }