settings_screen.dart 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725
  1. import 'package:flutter/material.dart';
  2. import 'package:url_launcher/url_launcher.dart';
  3. import '../utils/colors.dart';
  4. import '../utils/haptic_utils.dart';
  5. import '../utils/settings_manager.dart';
  6. import 'components/animated_background.dart';
  7. import 'google_play_games_widget.dart';
  8. class SettingsScreen extends StatefulWidget {
  9. const SettingsScreen({super.key});
  10. @override
  11. State<SettingsScreen> createState() => _SettingsScreenState();
  12. }
  13. class _SettingsScreenState extends State<SettingsScreen> {
  14. bool _musicEnabled = true;
  15. bool _hapticsEnabled = true;
  16. double _bgmVolume = 0.4;
  17. double _sfxVolume = 0.6;
  18. @override
  19. void initState() {
  20. super.initState();
  21. _loadSettings();
  22. }
  23. Future<void> _loadSettings() async {
  24. setState(() {
  25. _musicEnabled = SettingsManager.isMusicEnabled;
  26. _hapticsEnabled = SettingsManager.isHapticsEnabled;
  27. _bgmVolume = SettingsManager.bgmVolume;
  28. _sfxVolume = SettingsManager.sfxVolume;
  29. });
  30. }
  31. @override
  32. Widget build(BuildContext context) {
  33. return Scaffold(
  34. backgroundColor: ZenColors.appBackground,
  35. body: AnimatedBackground(
  36. child: SafeArea(
  37. child: Column(
  38. children: [
  39. // Header
  40. Padding(
  41. padding: const EdgeInsets.all(16.0),
  42. child: Row(
  43. children: [
  44. IconButton(
  45. onPressed: () => Navigator.of(context).pop(),
  46. icon: const Icon(
  47. Icons.arrow_back,
  48. color: ZenColors.primaryText,
  49. size: 28,
  50. ),
  51. style: IconButton.styleFrom(
  52. backgroundColor: ZenColors.black.withValues(alpha: 0.3),
  53. shape: const CircleBorder(),
  54. ),
  55. ),
  56. const SizedBox(width: 16),
  57. const Text(
  58. 'Settings',
  59. style: TextStyle(
  60. color: ZenColors.primaryText,
  61. fontSize: 28,
  62. fontWeight: FontWeight.bold,
  63. letterSpacing: 1.0,
  64. ),
  65. ),
  66. ],
  67. ),
  68. ),
  69. // Settings List
  70. Expanded(
  71. child: SingleChildScrollView(
  72. padding: const EdgeInsets.symmetric(horizontal: 20.0),
  73. child: Column(
  74. children: [
  75. const SizedBox(height: 20),
  76. // Audio Section
  77. _buildSectionHeader('Audio'),
  78. _buildSettingTile(
  79. icon: Icons.music_note,
  80. title: 'Background Music',
  81. subtitle: 'Enable relaxing ambient sounds',
  82. value: _musicEnabled,
  83. onChanged: _toggleMusic,
  84. ),
  85. _buildVolumeSlider(
  86. icon: Icons.volume_up,
  87. title: 'Music Volume',
  88. value: _bgmVolume,
  89. onChanged: _setBgmVolume,
  90. ),
  91. const SizedBox(height: 10),
  92. _buildVolumeSlider(
  93. icon: Icons.volume_up,
  94. title: 'Sound Effects Volume',
  95. value: _sfxVolume,
  96. onChanged: _setSfxVolume,
  97. ),
  98. const SizedBox(height: 30),
  99. // Feedback Section
  100. _buildSectionHeader('Feedback'),
  101. _buildSettingTile(
  102. icon: Icons.vibration,
  103. title: 'Haptic Feedback',
  104. subtitle: 'Feel gentle vibrations on tap',
  105. value: _hapticsEnabled,
  106. onChanged: _toggleHaptics,
  107. ),
  108. const SizedBox(height: 30),
  109. // Tutorial Section
  110. _buildSectionHeader('Help'),
  111. _buildActionTile(
  112. icon: Icons.help_outline,
  113. title: 'Show Tutorial',
  114. subtitle: 'Learn how to use ZenTap',
  115. onTap: _showTutorial,
  116. ),
  117. const SizedBox(height: 30),
  118. // Google Play Games Section
  119. _buildSectionHeader('Google Play Games'),
  120. const GooglePlayGamesWidget(),
  121. const SizedBox(height: 30),
  122. // Support Section
  123. _buildSectionHeader('Support ZenTap'),
  124. _buildDonationTile(),
  125. const SizedBox(height: 30),
  126. // About Section
  127. Padding(
  128. padding: const EdgeInsets.only(bottom: 20),
  129. child: Column(
  130. children: [
  131. Text(
  132. 'ZenTap v1.0.2',
  133. style: TextStyle(
  134. color: ZenColors.mutedText,
  135. fontSize: 14,
  136. ),
  137. ),
  138. const SizedBox(height: 8),
  139. Text(
  140. 'A stress relief tapping game',
  141. style: TextStyle(
  142. color: ZenColors.mutedText,
  143. fontSize: 12,
  144. fontStyle: FontStyle.italic,
  145. ),
  146. ),
  147. ],
  148. ),
  149. ),
  150. ],
  151. ),
  152. ),
  153. ),
  154. ],
  155. ),
  156. ),
  157. ),
  158. );
  159. }
  160. Widget _buildSectionHeader(String title) {
  161. return Align(
  162. alignment: Alignment.centerLeft,
  163. child: Text(
  164. title,
  165. style: TextStyle(
  166. color: ZenColors.secondaryText,
  167. fontSize: 16,
  168. fontWeight: FontWeight.w600,
  169. letterSpacing: 0.5,
  170. ),
  171. ),
  172. );
  173. }
  174. Widget _buildSettingTile({
  175. required IconData icon,
  176. required String title,
  177. required String subtitle,
  178. required bool value,
  179. required ValueChanged<bool> onChanged,
  180. }) {
  181. return Container(
  182. margin: const EdgeInsets.only(top: 12),
  183. padding: const EdgeInsets.all(16),
  184. decoration: BoxDecoration(
  185. color: ZenColors.uiElements.withValues(alpha: 0.3),
  186. borderRadius: BorderRadius.circular(12),
  187. border: Border.all(
  188. color: ZenColors.uiElements.withValues(alpha: 0.2),
  189. width: 1,
  190. ),
  191. ),
  192. child: Row(
  193. children: [
  194. Icon(
  195. icon,
  196. color: ZenColors.primaryText,
  197. size: 24,
  198. ),
  199. const SizedBox(width: 16),
  200. Expanded(
  201. child: Column(
  202. crossAxisAlignment: CrossAxisAlignment.start,
  203. children: [
  204. Text(
  205. title,
  206. style: const TextStyle(
  207. color: ZenColors.primaryText,
  208. fontSize: 16,
  209. fontWeight: FontWeight.w500,
  210. ),
  211. ),
  212. const SizedBox(height: 2),
  213. Text(
  214. subtitle,
  215. style: TextStyle(
  216. color: ZenColors.secondaryText,
  217. fontSize: 13,
  218. ),
  219. ),
  220. ],
  221. ),
  222. ),
  223. Switch(
  224. value: value,
  225. onChanged: onChanged,
  226. activeColor: ZenColors.buttonBackground,
  227. activeTrackColor: ZenColors.buttonBackground.withValues(alpha: 0.3),
  228. inactiveThumbColor: ZenColors.mutedText,
  229. inactiveTrackColor: ZenColors.mutedText.withValues(alpha: 0.2),
  230. ),
  231. ],
  232. ),
  233. );
  234. }
  235. Widget _buildActionTile({
  236. required IconData icon,
  237. required String title,
  238. required String subtitle,
  239. required VoidCallback onTap,
  240. }) {
  241. return Container(
  242. margin: const EdgeInsets.only(top: 12),
  243. child: Material(
  244. color: ZenColors.uiElements.withValues(alpha: 0.3),
  245. borderRadius: BorderRadius.circular(12),
  246. child: InkWell(
  247. onTap: onTap,
  248. borderRadius: BorderRadius.circular(12),
  249. child: Container(
  250. padding: const EdgeInsets.all(16),
  251. decoration: BoxDecoration(
  252. borderRadius: BorderRadius.circular(12),
  253. border: Border.all(
  254. color: ZenColors.uiElements.withValues(alpha: 0.2),
  255. width: 1,
  256. ),
  257. ),
  258. child: Row(
  259. children: [
  260. Icon(
  261. icon,
  262. color: ZenColors.primaryText,
  263. size: 24,
  264. ),
  265. const SizedBox(width: 16),
  266. Expanded(
  267. child: Column(
  268. crossAxisAlignment: CrossAxisAlignment.start,
  269. children: [
  270. Text(
  271. title,
  272. style: const TextStyle(
  273. color: ZenColors.primaryText,
  274. fontSize: 16,
  275. fontWeight: FontWeight.w500,
  276. ),
  277. ),
  278. const SizedBox(height: 2),
  279. Text(
  280. subtitle,
  281. style: TextStyle(
  282. color: ZenColors.secondaryText,
  283. fontSize: 13,
  284. ),
  285. ),
  286. ],
  287. ),
  288. ),
  289. Icon(
  290. Icons.arrow_forward_ios,
  291. color: ZenColors.mutedText,
  292. size: 16,
  293. ),
  294. ],
  295. ),
  296. ),
  297. ),
  298. ),
  299. );
  300. }
  301. Future<void> _toggleMusic(bool value) async {
  302. setState(() {
  303. _musicEnabled = value;
  304. });
  305. await SettingsManager.setMusicEnabled(value);
  306. if (_hapticsEnabled) {
  307. await HapticUtils.vibrate(duration: 50);
  308. }
  309. }
  310. Future<void> _setBgmVolume(double value) async {
  311. setState(() {
  312. _bgmVolume = value;
  313. });
  314. await SettingsManager.setBgmVolume(value);
  315. SettingsManager.applyBgmVolume();
  316. }
  317. Future<void> _setSfxVolume(double value) async {
  318. setState(() {
  319. _sfxVolume = value;
  320. });
  321. await SettingsManager.setSfxVolume(value);
  322. }
  323. Future<void> _toggleHaptics(bool value) async {
  324. setState(() {
  325. _hapticsEnabled = value;
  326. });
  327. await SettingsManager.setHapticsEnabled(value);
  328. // Give immediate feedback if enabling haptics
  329. if (value) {
  330. await HapticUtils.vibrate(duration: 100);
  331. }
  332. }
  333. void _showTutorial() {
  334. if (_hapticsEnabled) {
  335. HapticUtils.vibrate(duration: 50);
  336. }
  337. showDialog(
  338. context: context,
  339. builder: (context) => _buildTutorialDialog(),
  340. );
  341. }
  342. void _showDonationDialog() {
  343. if (_hapticsEnabled) {
  344. HapticUtils.vibrate(duration: 50);
  345. }
  346. showDialog(
  347. context: context,
  348. builder: (context) => _buildDonationDialog(),
  349. );
  350. }
  351. Future<void> _openDonationLink(String url) async {
  352. if (_hapticsEnabled) {
  353. HapticUtils.vibrate(duration: 30);
  354. }
  355. try {
  356. final Uri uri = Uri.parse(url);
  357. if (await canLaunchUrl(uri)) {
  358. await launchUrl(uri, mode: LaunchMode.externalApplication);
  359. Navigator.of(context).pop(); // Close dialog after opening link
  360. } else {
  361. // Show error if URL can't be launched
  362. if (mounted) {
  363. ScaffoldMessenger.of(context).showSnackBar(
  364. SnackBar(
  365. content: const Text('Could not open donation link'),
  366. backgroundColor: ZenColors.mutedText,
  367. behavior: SnackBarBehavior.floating,
  368. ),
  369. );
  370. }
  371. }
  372. } catch (e) {
  373. // Show error on exception
  374. if (mounted) {
  375. ScaffoldMessenger.of(context).showSnackBar(
  376. SnackBar(
  377. content: const Text('Error opening donation link'),
  378. backgroundColor: ZenColors.mutedText,
  379. behavior: SnackBarBehavior.floating,
  380. ),
  381. );
  382. }
  383. }
  384. }
  385. Widget _buildTutorialDialog() {
  386. return AlertDialog(
  387. backgroundColor: ZenColors.uiElements,
  388. shape: RoundedRectangleBorder(
  389. borderRadius: BorderRadius.circular(20),
  390. ),
  391. title: const Text(
  392. 'How to Use ZenTap',
  393. style: TextStyle(
  394. color: ZenColors.primaryText,
  395. fontSize: 22,
  396. fontWeight: FontWeight.bold,
  397. ),
  398. ),
  399. content: Column(
  400. mainAxisSize: MainAxisSize.min,
  401. crossAxisAlignment: CrossAxisAlignment.start,
  402. children: [
  403. _buildTutorialStep(
  404. icon: Icons.touch_app,
  405. text: 'Tap anywhere on the screen to pop bubbles',
  406. ),
  407. const SizedBox(height: 16),
  408. _buildTutorialStep(
  409. icon: Icons.stars,
  410. text: 'Earn Relaxation Points in Play mode',
  411. ),
  412. const SizedBox(height: 16),
  413. _buildTutorialStep(
  414. icon: Icons.self_improvement,
  415. text: 'Choose Zen Mode for pure relaxation',
  416. ),
  417. const SizedBox(height: 16),
  418. _buildTutorialStep(
  419. icon: Icons.pause,
  420. text: 'Tap pause anytime to take a break',
  421. ),
  422. ],
  423. ),
  424. actions: [
  425. ElevatedButton(
  426. onPressed: () => Navigator.of(context).pop(),
  427. style: ElevatedButton.styleFrom(
  428. backgroundColor: ZenColors.buttonBackground,
  429. foregroundColor: ZenColors.buttonText,
  430. shape: RoundedRectangleBorder(
  431. borderRadius: BorderRadius.circular(12),
  432. ),
  433. ),
  434. child: const Text('Got it!'),
  435. ),
  436. ],
  437. );
  438. }
  439. Widget _buildVolumeSlider({
  440. required IconData icon,
  441. required String title,
  442. required double value,
  443. required ValueChanged<double> onChanged,
  444. }) {
  445. return Container(
  446. margin: const EdgeInsets.only(top: 8),
  447. padding: const EdgeInsets.all(16),
  448. decoration: BoxDecoration(
  449. color: ZenColors.uiElements.withValues(alpha: 0.3),
  450. borderRadius: BorderRadius.circular(12),
  451. border: Border.all(
  452. color: ZenColors.uiElements.withValues(alpha: 0.2),
  453. width: 1,
  454. ),
  455. ),
  456. child: Row(
  457. children: [
  458. Icon(
  459. icon,
  460. color: ZenColors.primaryText,
  461. size: 24,
  462. ),
  463. const SizedBox(width: 16),
  464. Expanded(
  465. child: Column(
  466. crossAxisAlignment: CrossAxisAlignment.start,
  467. children: [
  468. Text(
  469. title,
  470. style: const TextStyle(
  471. color: ZenColors.primaryText,
  472. fontSize: 16,
  473. fontWeight: FontWeight.w500,
  474. ),
  475. ),
  476. const SizedBox(height: 8),
  477. Slider(
  478. value: value,
  479. onChanged: onChanged,
  480. min: 0.0,
  481. max: 1.0,
  482. divisions: 10,
  483. label: '${(value * 100).round()}%',
  484. activeColor: ZenColors.buttonBackground,
  485. inactiveColor: ZenColors.mutedText.withValues(alpha: 0.2),
  486. ),
  487. ],
  488. ),
  489. ),
  490. ],
  491. ),
  492. );
  493. }
  494. Widget _buildTutorialStep({
  495. required IconData icon,
  496. required String text,
  497. }) {
  498. return Row(
  499. children: [
  500. Icon(
  501. icon,
  502. color: ZenColors.buttonBackground,
  503. size: 20,
  504. ),
  505. const SizedBox(width: 12),
  506. Expanded(
  507. child: Text(
  508. text,
  509. style: TextStyle(
  510. color: ZenColors.secondaryText,
  511. fontSize: 14,
  512. ),
  513. ),
  514. ),
  515. ],
  516. );
  517. }
  518. Widget _buildDonationTile() {
  519. return _buildActionTile(
  520. icon: Icons.favorite,
  521. title: 'Support Development',
  522. subtitle: 'Help keep ZenTap free and ad-free',
  523. onTap: _showDonationDialog,
  524. );
  525. }
  526. Widget _buildDonationDialog() {
  527. return AlertDialog(
  528. backgroundColor: ZenColors.uiElements,
  529. shape: RoundedRectangleBorder(
  530. borderRadius: BorderRadius.circular(20),
  531. ),
  532. title: Row(
  533. children: [
  534. Icon(
  535. Icons.favorite,
  536. color: ZenColors.red,
  537. size: 24,
  538. ),
  539. const SizedBox(width: 12),
  540. const Text(
  541. 'Support ZenTap',
  542. style: TextStyle(
  543. color: ZenColors.primaryText,
  544. fontSize: 22,
  545. fontWeight: FontWeight.bold,
  546. ),
  547. ),
  548. ],
  549. ),
  550. content: Column(
  551. mainAxisSize: MainAxisSize.min,
  552. crossAxisAlignment: CrossAxisAlignment.start,
  553. children: [
  554. Text(
  555. 'ZenTap is free and ad-free. If you enjoy using it, consider supporting development:',
  556. style: TextStyle(
  557. color: ZenColors.secondaryText,
  558. fontSize: 14,
  559. height: 1.4,
  560. ),
  561. ),
  562. const SizedBox(height: 20),
  563. // Ko-fi Button
  564. _buildDonationButton(
  565. icon: Icons.coffee,
  566. title: 'Buy me a coffee',
  567. subtitle: 'Ko-fi (one-time)',
  568. color: const Color(0xFF13C3FF),
  569. onTap: () => _openDonationLink('https://ko-fi.com/fsociety_hu'),
  570. ),
  571. const SizedBox(height: 12),
  572. // PayPal Button
  573. _buildDonationButton(
  574. icon: Icons.payment,
  575. title: 'PayPal Donation',
  576. subtitle: 'Direct donation',
  577. color: const Color(0xFF0070BA),
  578. onTap: () => _openDonationLink('https://paypal.me/fsocietyhu'),
  579. ),
  580. const SizedBox(height: 12),
  581. // GitHub Sponsors Button
  582. _buildDonationButton(
  583. icon: Icons.code,
  584. title: 'GitHub Sponsors',
  585. subtitle: 'Monthly support',
  586. color: const Color(0xFFEA4AAA),
  587. onTap: () => _openDonationLink('https://github.com/sponsors/fszontagh'),
  588. ),
  589. const SizedBox(height: 16),
  590. Text(
  591. 'Thank you for supporting indie development! 💜',
  592. style: TextStyle(
  593. color: ZenColors.secondaryText,
  594. fontSize: 12,
  595. fontStyle: FontStyle.italic,
  596. ),
  597. textAlign: TextAlign.center,
  598. ),
  599. ],
  600. ),
  601. actions: [
  602. TextButton(
  603. onPressed: () => Navigator.of(context).pop(),
  604. child: Text(
  605. 'Maybe later',
  606. style: TextStyle(
  607. color: ZenColors.mutedText,
  608. ),
  609. ),
  610. ),
  611. ],
  612. );
  613. }
  614. Widget _buildDonationButton({
  615. required IconData icon,
  616. required String title,
  617. required String subtitle,
  618. required Color color,
  619. required VoidCallback onTap,
  620. }) {
  621. return Material(
  622. color: color.withValues(alpha: 0.1),
  623. borderRadius: BorderRadius.circular(12),
  624. child: InkWell(
  625. onTap: onTap,
  626. borderRadius: BorderRadius.circular(12),
  627. child: Container(
  628. padding: const EdgeInsets.all(12),
  629. decoration: BoxDecoration(
  630. borderRadius: BorderRadius.circular(12),
  631. border: Border.all(
  632. color: color.withValues(alpha: 0.3),
  633. width: 1,
  634. ),
  635. ),
  636. child: Row(
  637. children: [
  638. Icon(
  639. icon,
  640. color: color,
  641. size: 20,
  642. ),
  643. const SizedBox(width: 12),
  644. Expanded(
  645. child: Column(
  646. crossAxisAlignment: CrossAxisAlignment.start,
  647. children: [
  648. Text(
  649. title,
  650. style: TextStyle(
  651. color: ZenColors.primaryText,
  652. fontSize: 14,
  653. fontWeight: FontWeight.w500,
  654. ),
  655. ),
  656. Text(
  657. subtitle,
  658. style: TextStyle(
  659. color: ZenColors.secondaryText,
  660. fontSize: 12,
  661. ),
  662. ),
  663. ],
  664. ),
  665. ),
  666. Icon(
  667. Icons.open_in_new,
  668. color: color,
  669. size: 16,
  670. ),
  671. ],
  672. ),
  673. ),
  674. ),
  675. );
  676. }
  677. }