settings_screen.dart 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  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. appBar: AppBar(
  129. backgroundColor: Colors.white,
  130. centerTitle: true,
  131. title: Text(
  132. AppLocalizations.of(context)!.setting,
  133. style: TextStyle(color: Colors.black87, fontWeight: FontWeight.bold, fontSize: 24),
  134. ),
  135. elevation: 1,
  136. leading: IconButton(
  137. icon: const Icon(Icons.arrow_back_outlined, color: Colors.black87),
  138. onPressed: () {
  139. audio.playSfx(SfxType.click);
  140. Navigator.pop(context);
  141. },
  142. ),
  143. ),
  144. body: SingleChildScrollView(
  145. scrollDirection: Axis.vertical,
  146. child: Padding(
  147. padding: const EdgeInsets.all(10),
  148. child: Column(children: _buildAll(configs!)),
  149. ),
  150. ),
  151. );
  152. }
  153. List<Widget> _buildAll(List<List<SettingItem>> groups) {
  154. return groups.map((e) => _buildGroup(e)).toList();
  155. }
  156. Widget _buildGroup(List<SettingItem> list) {
  157. return Padding(
  158. padding: const EdgeInsets.only(top: 10),
  159. child: Material(
  160. elevation: 1,
  161. borderRadius: BorderRadius.circular(10),
  162. child: Padding(
  163. padding: const EdgeInsets.all(10),
  164. child: Column(children: list.map((e) => _buildItem(e)).toList()),
  165. ),
  166. ),
  167. );
  168. }
  169. Widget _buildItem(SettingItem item) {
  170. final audio = context.read<JcAudioController>();
  171. return Row(
  172. mainAxisAlignment: MainAxisAlignment.start,
  173. crossAxisAlignment: CrossAxisAlignment.center,
  174. children: [
  175. Icon(item.icon, color: item.iconColor, size: 24),
  176. if (item.type == SettingItemType.toggle)
  177. Expanded(
  178. child: Padding(
  179. padding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
  180. // child: Text(item.text, style: TextStyle(color: item.textColor, fontWeight: FontWeight.bold)),
  181. child: TextButton(
  182. onPressed: null,
  183. style: TextButton.styleFrom(padding: EdgeInsets.zero, alignment: Alignment.centerLeft),
  184. child: Text(item.text, style: TextStyle(color: item.textColor, fontSize: 16)),
  185. ),
  186. ),
  187. ),
  188. if (item.type == SettingItemType.dropdown)
  189. Expanded(
  190. child: Padding(
  191. padding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
  192. child: TextButton(
  193. onPressed: null,
  194. style: TextButton.styleFrom(padding: EdgeInsets.zero, alignment: Alignment.centerLeft),
  195. child: Text(item.text, style: TextStyle(color: item.textColor, fontSize: 16)),
  196. ),
  197. ),
  198. ),
  199. if (item.type == SettingItemType.action)
  200. Expanded(
  201. child: Padding(
  202. padding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
  203. child: SizedBox(
  204. width: double.infinity,
  205. child: TextButton(
  206. onPressed: () {
  207. audio.playSfx(SfxType.click);
  208. item.action?.call();
  209. },
  210. style: TextButton.styleFrom(padding: EdgeInsets.zero, alignment: Alignment.centerLeft),
  211. child: Text(item.text, style: TextStyle(color: item.textColor, fontSize: 16)),
  212. ),
  213. ),
  214. ),
  215. ),
  216. if (item.type == SettingItemType.toggle)
  217. Switch(
  218. activeTrackColor: SkinHelper.color5,
  219. inactiveTrackColor: Colors.white,
  220. inactiveThumbColor: SkinHelper.color4,
  221. value: item.toggleValue?.call() ?? false,
  222. onChanged: (value) {
  223. audio.playSfx(SfxType.click);
  224. item.onToggle?.call(value);
  225. setState(() {});
  226. },
  227. ),
  228. if (item.type == SettingItemType.dropdown)
  229. DropdownButton(
  230. value: item.dropdownValue?.call() ?? 0,
  231. items: (item.dropdownList?.call())!.map((e) => DropdownMenuItem(value: e, child: Text(e.toString()))).toList(),
  232. onChanged: (value) {
  233. audio.playSfx(SfxType.click);
  234. item.onDropdown?.call(value ?? 0);
  235. setState(() {});
  236. },
  237. ),
  238. ],
  239. );
  240. }
  241. }
  242. enum SettingItemType { toggle, action, dropdown }
  243. typedef ToggleCallback = Function(bool val);
  244. typedef ToggleValueGetter = bool Function();
  245. typedef DropdownCallback = Function(int val);
  246. typedef DropdownValueGetter = int Function();
  247. typedef DropdownListGetter = List<int> Function();
  248. class SettingItem {
  249. final SettingItemType type;
  250. final IconData icon;
  251. final String text;
  252. final String? configKey;
  253. ToggleCallback? onToggle;
  254. ToggleValueGetter? toggleValue;
  255. VoidCallback? action;
  256. DropdownCallback? onDropdown;
  257. DropdownValueGetter? dropdownValue;
  258. DropdownListGetter? dropdownList;
  259. Color iconColor;
  260. Color textColor;
  261. SettingItem(
  262. this.type,
  263. this.icon,
  264. this.text, {
  265. this.configKey,
  266. this.iconColor = const Color.fromARGB(255, 8, 66, 9),
  267. this.textColor = Colors.black87,
  268. this.action,
  269. this.onToggle,
  270. this.toggleValue,
  271. this.onDropdown,
  272. this.dropdownList,
  273. this.dropdownValue,
  274. });
  275. }