diff --git a/lib/controls/focus_mode_menu_item.dart b/lib/controls/focus_mode_menu_item.dart index 51001d3..2ed65d6 100644 --- a/lib/controls/focus_mode_menu_item.dart +++ b/lib/controls/focus_mode_menu_item.dart @@ -8,7 +8,23 @@ import '../riverpod_controllers/focus_mode.dart'; import '../routes.dart'; import 'padding.dart'; -const foreverDuration = Duration(days: 10000); +enum Difficulty { + easy(10), + medium(1000), + difficult(1000000), + extreme(1000000000); + + final int maxNumber; + + const Difficulty(this.maxNumber); + + String get label => switch (this) { + easy => 'Easy', + medium => 'Medium', + difficult => 'Difficult', + extreme => 'Extremely Difficult', + }; +} class FocusModeMenuItem extends ConsumerWidget { const FocusModeMenuItem({super.key}); @@ -27,16 +43,12 @@ class FocusModeMenuItem extends ConsumerWidget { context.pop(); context.push(ScreenPaths.focusModeDisable); } else { - final duration = await _chooseDuration(context); - if (duration == null) { + final focusMode = await _chooseFocusMode(context); + if (focusMode == null) { return; } - final disableTime = duration == foreverDuration - ? null - : DateTime.now().add(duration); - final update = FocusModeData(true, disableTime: disableTime); - ref.read(focusModeProvider.notifier).setMode(update); + ref.read(focusModeProvider.notifier).setMode(focusMode); if (context.mounted) { context.pop(); context.go(ScreenPaths.timelines); @@ -48,154 +60,216 @@ class FocusModeMenuItem extends ConsumerWidget { } } -Future _chooseDuration( +Future _chooseFocusMode( BuildContext context, ) { - var hours = 0; - var minutes = 30; + int hours = 0; + int minutes = 30; + var difficulty = Difficulty.medium; - return showDialog( + return showDialog( context: context, barrierDismissible: true, - builder: (BuildContext context) { - return AlertDialog( - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'Choose Focus Duration', - style: Theme.of(context) - .textTheme - .bodyLarge! - .copyWith(fontWeight: FontWeight.bold), - ), - const VerticalPadding(), - Wrap( - runSpacing: 10.0, - spacing: 10.0, - children: [ - ElevatedButton( - child: const Text('15 minutes'), - onPressed: () { - Navigator.pop( - context, - const Duration( - minutes: 15, - ), - ); // showDialog() returns true - }, - ), - ElevatedButton( - child: const Text('30 minutes'), - onPressed: () { - Navigator.pop( - context, - const Duration( - minutes: 30, - ), - ); // showDialog() returns true - }, - ), - ElevatedButton( - child: const Text('1 hour'), - onPressed: () { - Navigator.pop( - context, - const Duration( - hours: 1, - ), - ); // showDialog() returns true - }, - ), - ElevatedButton( - child: const Text('6 hours'), - onPressed: () { - Navigator.pop( - context, - const Duration( - hours: 6, - ), - ); // showDialog() returns true - }, - ), - ElevatedButton( - child: const Text('12 hours'), - onPressed: () { - Navigator.pop( - context, - const Duration( - hours: 12, - ), - ); // showDialog() returns true - }, - ), - ElevatedButton( - child: const Text('1 day'), - onPressed: () { - Navigator.pop( - context, - const Duration( - days: 1, - ), - ); // showDialog() returns true - }, - ), - ElevatedButton( - child: const Text('Forever'), - onPressed: () { - Navigator.pop( - context, - foreverDuration, - ); // showDialog() returns true - }, - ), - ], - ), - const VerticalPadding(), - SizedBox( - height: 100, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, + builder: (BuildContext context) => StatefulBuilder( + builder: (BuildContext context, void Function(void Function()) setState) { + return AlertDialog( + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Choose Focus Duration and Difficulty', + softWrap: true, + style: Theme.of(context) + .textTheme + .bodyLarge! + .copyWith(fontWeight: FontWeight.bold), + ), + const VerticalPadding(), + DropdownButton( + value: difficulty, + onChanged: (v) => setState(() => difficulty = v!), + items: Difficulty.values + .map((e) => DropdownMenuItem( + value: e, + child: Text(e.label), + )) + .toList(), + ), + const VerticalPadding(), + Wrap( + runSpacing: 10.0, + spacing: 10.0, children: [ - Flexible( - child: WheelChooser.integer( - initValue: hours, - onValueChanged: (v) => hours = v, - maxValue: 24, - minValue: 0, - unSelectTextStyle: const TextStyle(color: Colors.grey), - ), + ElevatedButton( + child: const Text('15 minutes'), + onPressed: () { + Navigator.pop( + context, + FocusModeData( + true, + maxNumber: difficulty.maxNumber, + disableTime: DateTime.now().add( + const Duration( + minutes: 15, + ), + ), + ), + ); // showDialog() returns true + }, ), - const Text('hours'), - Flexible( - child: WheelChooser.integer( - initValue: minutes, - onValueChanged: (v) => minutes = v, - maxValue: 59, - minValue: 0, - unSelectTextStyle: const TextStyle(color: Colors.grey), - ), + ElevatedButton( + child: const Text('30 minutes'), + onPressed: () { + Navigator.pop( + context, + FocusModeData( + true, + maxNumber: difficulty.maxNumber, + disableTime: DateTime.now().add( + const Duration( + minutes: 30, + ), + ), + ), + ); // showDialog() returns true + }, + ), + ElevatedButton( + child: const Text('1 hour'), + onPressed: () { + Navigator.pop( + context, + FocusModeData( + true, + maxNumber: difficulty.maxNumber, + disableTime: DateTime.now().add( + const Duration( + hours: 1, + ), + ), + ), + ); // showDialog() returns true + }, + ), + ElevatedButton( + child: const Text('6 hours'), + onPressed: () { + Navigator.pop( + context, + FocusModeData( + true, + maxNumber: difficulty.maxNumber, + disableTime: DateTime.now().add( + const Duration( + hours: 6, + ), + ), + ), + ); // showDialog() returns true + }, + ), + ElevatedButton( + child: const Text('12 hours'), + onPressed: () { + Navigator.pop( + context, + FocusModeData( + true, + maxNumber: difficulty.maxNumber, + disableTime: DateTime.now().add( + const Duration( + hours: 12, + ), + ), + ), + ); // showDialog() returns true + }, + ), + ElevatedButton( + child: const Text('1 day'), + onPressed: () { + Navigator.pop( + context, + FocusModeData( + true, + maxNumber: difficulty.maxNumber, + disableTime: DateTime.now().add( + const Duration( + days: 1, + ), + ), + ), + ); // showDialog() returns true + }, + ), + ElevatedButton( + child: const Text('Forever'), + onPressed: () { + Navigator.pop( + context, + FocusModeData( + true, + maxNumber: difficulty.maxNumber, + ), + ); // showDialog() returns true + }, ), - const Text('minutes'), ], ), - ) - ], - ), - actions: [ - ElevatedButton( - child: const Text('Select'), - onPressed: () { - Navigator.pop( - context, - Duration( - hours: hours, - minutes: minutes, + const VerticalPadding(), + SizedBox( + height: 100, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Flexible( + child: WheelChooser.integer( + initValue: hours, + onValueChanged: (v) => hours = v, + maxValue: 24, + minValue: 0, + unSelectTextStyle: + const TextStyle(color: Colors.grey), + ), + ), + const Text('hours'), + Flexible( + child: WheelChooser.integer( + initValue: minutes, + onValueChanged: (v) => minutes = v, + maxValue: 59, + minValue: 0, + unSelectTextStyle: + const TextStyle(color: Colors.grey), + ), + ), + const Text('minutes'), + ], ), - ); // showDialog() returns true - }, + ) + ], ), - ]); - }, + actions: [ + ElevatedButton( + child: const Text('Select'), + onPressed: () { + Navigator.pop( + context, + FocusModeData( + true, + maxNumber: difficulty.maxNumber, + disableTime: DateTime.now().add( + Duration( + hours: hours, + minutes: minutes, + ), + ), + ), + ); // showDialog() returns true + }, + ), + ]); + }, + ), ); } diff --git a/lib/models/focus_mode_data.dart b/lib/models/focus_mode_data.dart index 0b4da5c..34e1471 100644 --- a/lib/models/focus_mode_data.dart +++ b/lib/models/focus_mode_data.dart @@ -1,18 +1,24 @@ +const defaultMaxNumber = 1000; + class FocusModeData { final DateTime? disableTime; + final int maxNumber; final bool enabled; - const FocusModeData(this.enabled, {this.disableTime}); + const FocusModeData(this.enabled, + {this.maxNumber = defaultMaxNumber, this.disableTime}); factory FocusModeData.disabled() => const FocusModeData(false); factory FocusModeData.fromJson(Map json) => FocusModeData( json['enabled'], + maxNumber: json['maxNumber'] ?? defaultMaxNumber, disableTime: DateTime.tryParse(json['disableTime'] ?? ''), ); Map toJson() => { 'enabled': enabled, + 'maxNumber': maxNumber, if (disableTime != null) 'disableTime': disableTime!.toIso8601String(), }; } diff --git a/lib/screens/disable_focus_mode_screen.dart b/lib/screens/disable_focus_mode_screen.dart index d7df4d5..526ee36 100644 --- a/lib/screens/disable_focus_mode_screen.dart +++ b/lib/screens/disable_focus_mode_screen.dart @@ -12,13 +12,9 @@ import '../models/focus_mode_data.dart'; import '../riverpod_controllers/focus_mode.dart'; import '../routes.dart'; import '../utils/snackbar_builder.dart'; +import '../utils/string_utils.dart'; -const _maxNumber = 1000000; -final introMessage = - "If you guess the number I've picked from 0 to ${decimalWithCommasFormat.format(_maxNumber)} you may disable focus mode..."; - -const magicUnlockNumber = -1563948536; -final decimalWithCommasFormat = NumberFormat("#,##0.###"); +const _magicUnlockNumber = -1563948536; class GameState { final int maxNumber; @@ -41,7 +37,7 @@ class GameState { return 'You got it!'; } - bool get found => number == lastGuess || lastGuess == magicUnlockNumber; + bool get found => number == lastGuess || lastGuess == _magicUnlockNumber; const GameState({ required this.number, @@ -71,8 +67,18 @@ class _DisableFocusModeScreenState extends ConsumerState { final formKey = GlobalKey(); final guessController = TextEditingController(); - var game = GameState.newGame(_maxNumber); - var message = introMessage; + var game = GameState.newGame(1); + var maxNumber = 1; + var message = ''; + + @override + void initState() { + super.initState(); + maxNumber = ref.read(focusModeProvider).maxNumber; + game = GameState.newGame(maxNumber); + message = + "If you guess the number I've picked from 0 to ${decimalWithCommasFormat.format(maxNumber)} you may disable focus mode..."; + } @override Widget build(BuildContext context) { @@ -126,7 +132,7 @@ class _DisableFocusModeScreenState final valid = formKey.currentState?.validate() ?? false; if (!valid) { buildSnackbar(context, - 'Please enter an integer between 0 and ${decimalWithCommasFormat.format(_maxNumber)}'); + 'Please enter an integer between 0 and ${decimalWithCommasFormat.format(maxNumber)}'); return; } @@ -187,17 +193,11 @@ class ThousandsSeparatorInputFormatter extends TextInputFormatter { final decimalPart = parts[1]; // Handle edge case where decimal part is present but empty (user just typed the dot) final formattedText = '${formatter.format(integerPart)}.$decimalPart'; - return TextEditingValue( - text: formattedText, - selection: updateCursorPosition(formattedText), - ); + return newValue.copyWith(text: formattedText); } else { // No decimal part, format the whole number final newFormattedText = formatter.format(newTextAsNum); - return TextEditingValue( - text: newFormattedText, - selection: updateCursorPosition(newFormattedText), - ); + return newValue.copyWith(text: newFormattedText); } } diff --git a/lib/utils/string_utils.dart b/lib/utils/string_utils.dart index 18cc265..4914a0e 100644 --- a/lib/utils/string_utils.dart +++ b/lib/utils/string_utils.dart @@ -1,3 +1,5 @@ +import 'package:intl/intl.dart'; + extension StringUtils on String { String truncate({int length = 32}) { if (this.length <= length) { @@ -7,8 +9,10 @@ extension StringUtils on String { return '${substring(0, length)}...'; } - String stripHyperlinks() => - replaceAll(RegExp( + String stripHyperlinks() => replaceAll( + RegExp( r"(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?"), - ""); + ""); } + +final decimalWithCommasFormat = NumberFormat("#,##0.###");