settings_screen.dart 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. import 'dart:io';
  2. import 'package:flutter/material.dart';
  3. import 'package:package_info_plus/package_info_plus.dart';
  4. import 'package:provider/provider.dart';
  5. import 'package:puzzleweave/audio/jc_audio_controller.dart';
  6. import 'package:puzzleweave/l10n/app_localizations.dart';
  7. import 'package:puzzleweave/settings/settings_controller.dart';
  8. import 'package:puzzleweave/skin/skin.dart';
  9. import 'package:url_launcher/url_launcher.dart';
  10. import 'package:launch_review_latest/launch_review_latest.dart';
  11. class SettingScreen extends StatefulWidget {
  12. const SettingScreen({super.key});
  13. @override
  14. State<StatefulWidget> createState() => _SettingScreenState();
  15. static PageRouteBuilder buildRoute() {
  16. return PageRouteBuilder(
  17. pageBuilder: (context, animation, secondaryAnimation) {
  18. return const SettingScreen();
  19. },
  20. transitionsBuilder: (context, animation, secondaryAnimation, child) {
  21. return SlideTransition(
  22. position: Tween(begin: const Offset(1, 0), end: Offset.zero).animate(animation),
  23. child: child,
  24. );
  25. },
  26. );
  27. }
  28. }
  29. class _SettingScreenState extends State<SettingScreen> {
  30. List<List<SettingItem>>? configs;
  31. @override
  32. void initState() {
  33. super.initState();
  34. }
  35. @override
  36. didChangeDependencies() {
  37. super.didChangeDependencies();
  38. final settings = context.watch<SettingsController>();
  39. configs = [
  40. [
  41. SettingItem(
  42. SettingItemType.toggle,
  43. Icons.volume_up_rounded,
  44. AppLocalizations.of(context)!.sound,
  45. onToggle: (val) {
  46. settings.toggleSound();
  47. },
  48. toggleValue: () => settings.sound.value,
  49. ),
  50. SettingItem(
  51. SettingItemType.toggle,
  52. Icons.audiotrack_rounded,
  53. AppLocalizations.of(context)!.backgroundMusic,
  54. onToggle: (val) {
  55. settings.toggleMusic();
  56. },
  57. toggleValue: () => settings.music.value,
  58. ),
  59. SettingItem(
  60. SettingItemType.toggle,
  61. Icons.vibration_rounded,
  62. AppLocalizations.of(context)!.touchFeedback,
  63. onToggle: (val) {
  64. settings.toggleVibrate();
  65. },
  66. toggleValue: () => settings.vibrate.value,
  67. ),
  68. ],
  69. [
  70. SettingItem(SettingItemType.action, Icons.reviews_rounded, AppLocalizations.of(context)!.giveMeAReview, action: _launchReview),
  71. SettingItem(SettingItemType.action, Icons.pest_control_rounded, AppLocalizations.of(context)!.bugReport, action: _bugReportAction),
  72. SettingItem(SettingItemType.action, Icons.add_comment_rounded, AppLocalizations.of(context)!.featureRequest, action: _featureRequest),
  73. ],
  74. [
  75. SettingItem(
  76. SettingItemType.action,
  77. Icons.info_rounded,
  78. AppLocalizations.of(context)!.termOfService,
  79. action: () {
  80. _linkLaunch('https://longreachai.net/game/terms_of_service.html');
  81. },
  82. ),
  83. SettingItem(
  84. SettingItemType.action,
  85. Icons.description_rounded,
  86. AppLocalizations.of(context)!.privacyPolicy,
  87. action: () {
  88. _linkLaunch('https://longreachai.net/game/privacy_policy.html');
  89. },
  90. ),
  91. ],
  92. ];
  93. }
  94. Future<String> _versionInfo() async {
  95. final pkgInfo = await PackageInfo.fromPlatform();
  96. final output = {
  97. 'OS Version': '${Platform.operatingSystem} ${Platform.operatingSystemVersion} ',
  98. 'App Version': '${pkgInfo.version} ${pkgInfo.buildNumber}',
  99. };
  100. return output.keys.map((e) => '$e: ${output[e]}').join("<br/>");
  101. }
  102. _bugReportAction() async {
  103. final infos = await _versionInfo();
  104. Uri url = Uri(scheme: 'mailto', path: 'whippanyplex@outlook.com', query: 'subject=Bug Report - Jigsort Solitaire &body=<br/>------------------<br/>$infos');
  105. if (!await launchUrl(url, mode: LaunchMode.externalApplication)) {
  106. throw Exception('Could not launch $url');
  107. }
  108. }
  109. _featureRequest() async {
  110. Uri url = Uri(scheme: 'mailto', path: 'whippanyplex@outlook.com', query: 'subject=Feature Request - Jigsort Solitaire');
  111. if (!await launchUrl(url, mode: LaunchMode.externalApplication)) {
  112. throw Exception('Could not launch $url');
  113. }
  114. }
  115. _linkLaunch(String link) async {
  116. Uri url = Uri.parse(link);
  117. if (!await launchUrl(url, mode: LaunchMode.externalApplication)) {
  118. throw Exception('Could not launch $url');
  119. }
  120. }
  121. _launchReview() {
  122. LaunchReviewLatest.launch(androidAppId: 'jigsort.solitaire.jigsaw.match.games', iOSAppId: '6499138123');
  123. }
  124. @override
  125. Widget build(BuildContext context) {
  126. final audio = context.read<JcAudioController>();
  127. return Scaffold(
  128. backgroundColor: SkinHelper.colorWhite,
  129. appBar: AppBar(
  130. backgroundColor: SkinHelper.colorWhite,
  131. centerTitle: true,
  132. title: Text(
  133. AppLocalizations.of(context)!.setting,
  134. style: TextStyle(color: Colors.black87, fontWeight: FontWeight.bold, fontSize: 24),
  135. ),
  136. // elevation: 1,
  137. leading: IconButton(
  138. icon: const Icon(Icons.arrow_back_outlined, color: Colors.black87),
  139. onPressed: () {
  140. audio.playSfx(SfxType.click);
  141. Navigator.pop(context);
  142. },
  143. ),
  144. ),
  145. body: SingleChildScrollView(
  146. scrollDirection: Axis.vertical,
  147. child: Padding(
  148. padding: const EdgeInsets.all(10),
  149. child: Column(children: _buildAll(configs!)),
  150. ),
  151. ),
  152. );
  153. }
  154. List<Widget> _buildAll(List<List<SettingItem>> groups) {
  155. return groups.map((e) => _buildGroup(e)).toList();
  156. }
  157. Widget _buildGroup(List<SettingItem> list) {
  158. return Padding(
  159. padding: const EdgeInsets.only(top: 10),
  160. child: Material(
  161. elevation: 1,
  162. borderRadius: BorderRadius.circular(10),
  163. child: Padding(
  164. padding: const EdgeInsets.all(10),
  165. child: Column(children: list.map((e) => _buildItem(e)).toList()),
  166. ),
  167. ),
  168. );
  169. }
  170. Widget _buildItem(SettingItem item) {
  171. final audio = context.read<JcAudioController>();
  172. return Row(
  173. mainAxisAlignment: MainAxisAlignment.start,
  174. crossAxisAlignment: CrossAxisAlignment.center,
  175. children: [
  176. Icon(item.icon, color: item.iconColor, size: 24),
  177. if (item.type == SettingItemType.toggle)
  178. Expanded(
  179. child: Padding(
  180. padding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
  181. // child: Text(item.text, style: TextStyle(color: item.textColor, fontWeight: FontWeight.bold)),
  182. child: TextButton(
  183. onPressed: null,
  184. style: TextButton.styleFrom(padding: EdgeInsets.zero, alignment: Alignment.centerLeft),
  185. child: Text(item.text, style: TextStyle(color: item.textColor, fontSize: 16)),
  186. ),
  187. ),
  188. ),
  189. if (item.type == SettingItemType.dropdown)
  190. Expanded(
  191. child: Padding(
  192. padding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
  193. child: TextButton(
  194. onPressed: null,
  195. style: TextButton.styleFrom(padding: EdgeInsets.zero, alignment: Alignment.centerLeft),
  196. child: Text(item.text, style: TextStyle(color: item.textColor, fontSize: 16)),
  197. ),
  198. ),
  199. ),
  200. if (item.type == SettingItemType.action)
  201. Expanded(
  202. child: Padding(
  203. padding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
  204. child: SizedBox(
  205. width: double.infinity,
  206. child: TextButton(
  207. onPressed: () {
  208. audio.playSfx(SfxType.click);
  209. item.action?.call();
  210. },
  211. style: TextButton.styleFrom(padding: EdgeInsets.zero, alignment: Alignment.centerLeft),
  212. child: Text(item.text, style: TextStyle(color: item.textColor, fontSize: 16)),
  213. ),
  214. ),
  215. ),
  216. ),
  217. if (item.type == SettingItemType.toggle)
  218. Switch(
  219. activeTrackColor: SkinHelper.color5,
  220. inactiveTrackColor: Colors.white,
  221. inactiveThumbColor: SkinHelper.color4,
  222. value: item.toggleValue?.call() ?? false,
  223. onChanged: (value) {
  224. audio.playSfx(SfxType.click);
  225. item.onToggle?.call(value);
  226. setState(() {});
  227. },
  228. ),
  229. if (item.type == SettingItemType.dropdown)
  230. DropdownButton(
  231. value: item.dropdownValue?.call() ?? 0,
  232. items: (item.dropdownList?.call())!.map((e) => DropdownMenuItem(value: e, child: Text(e.toString()))).toList(),
  233. onChanged: (value) {
  234. audio.playSfx(SfxType.click);
  235. item.onDropdown?.call(value ?? 0);
  236. setState(() {});
  237. },
  238. ),
  239. ],
  240. );
  241. }
  242. }
  243. enum SettingItemType { toggle, action, dropdown }
  244. typedef ToggleCallback = Function(bool val);
  245. typedef ToggleValueGetter = bool Function();
  246. typedef DropdownCallback = Function(int val);
  247. typedef DropdownValueGetter = int Function();
  248. typedef DropdownListGetter = List<int> Function();
  249. class SettingItem {
  250. final SettingItemType type;
  251. final IconData icon;
  252. final String text;
  253. final String? configKey;
  254. ToggleCallback? onToggle;
  255. ToggleValueGetter? toggleValue;
  256. VoidCallback? action;
  257. DropdownCallback? onDropdown;
  258. DropdownValueGetter? dropdownValue;
  259. DropdownListGetter? dropdownList;
  260. Color iconColor;
  261. Color textColor;
  262. SettingItem(
  263. this.type,
  264. this.icon,
  265. this.text, {
  266. this.configKey,
  267. this.iconColor = const Color.fromARGB(255, 8, 66, 9),
  268. this.textColor = Colors.black87,
  269. this.action,
  270. this.onToggle,
  271. this.toggleValue,
  272. this.onDropdown,
  273. this.dropdownList,
  274. this.dropdownValue,
  275. });
  276. }