Add difficulty levels to focus mode

This commit is contained in:
Hank Grabowski 2024-08-29 20:31:43 -04:00
parent 39fa0fae08
commit fe348a8020
4 changed files with 251 additions and 167 deletions

View file

@ -8,7 +8,23 @@ import '../riverpod_controllers/focus_mode.dart';
import '../routes.dart'; import '../routes.dart';
import 'padding.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 { class FocusModeMenuItem extends ConsumerWidget {
const FocusModeMenuItem({super.key}); const FocusModeMenuItem({super.key});
@ -27,16 +43,12 @@ class FocusModeMenuItem extends ConsumerWidget {
context.pop(); context.pop();
context.push(ScreenPaths.focusModeDisable); context.push(ScreenPaths.focusModeDisable);
} else { } else {
final duration = await _chooseDuration(context); final focusMode = await _chooseFocusMode(context);
if (duration == null) { if (focusMode == null) {
return; return;
} }
final disableTime = duration == foreverDuration ref.read(focusModeProvider.notifier).setMode(focusMode);
? null
: DateTime.now().add(duration);
final update = FocusModeData(true, disableTime: disableTime);
ref.read(focusModeProvider.notifier).setMode(update);
if (context.mounted) { if (context.mounted) {
context.pop(); context.pop();
context.go(ScreenPaths.timelines); context.go(ScreenPaths.timelines);
@ -48,154 +60,216 @@ class FocusModeMenuItem extends ConsumerWidget {
} }
} }
Future<Duration?> _chooseDuration( Future<FocusModeData?> _chooseFocusMode(
BuildContext context, BuildContext context,
) { ) {
var hours = 0; int hours = 0;
var minutes = 30; int minutes = 30;
var difficulty = Difficulty.medium;
return showDialog<Duration?>( return showDialog<FocusModeData?>(
context: context, context: context,
barrierDismissible: true, barrierDismissible: true,
builder: (BuildContext context) { builder: (BuildContext context) => StatefulBuilder(
return AlertDialog( builder: (BuildContext context, void Function(void Function()) setState) {
content: Column( return AlertDialog(
mainAxisSize: MainAxisSize.min, content: Column(
children: [ mainAxisSize: MainAxisSize.min,
Text( children: [
'Choose Focus Duration', Text(
style: Theme.of(context) 'Choose Focus Duration and Difficulty',
.textTheme softWrap: true,
.bodyLarge! style: Theme.of(context)
.copyWith(fontWeight: FontWeight.bold), .textTheme
), .bodyLarge!
const VerticalPadding(), .copyWith(fontWeight: FontWeight.bold),
Wrap( ),
runSpacing: 10.0, const VerticalPadding(),
spacing: 10.0, DropdownButton<Difficulty>(
children: [ value: difficulty,
ElevatedButton( onChanged: (v) => setState(() => difficulty = v!),
child: const Text('15 minutes'), items: Difficulty.values
onPressed: () { .map((e) => DropdownMenuItem(
Navigator.pop( value: e,
context, child: Text(e.label),
const Duration( ))
minutes: 15, .toList(),
), ),
); // showDialog() returns true const VerticalPadding(),
}, Wrap(
), runSpacing: 10.0,
ElevatedButton( spacing: 10.0,
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,
children: [ children: [
Flexible( ElevatedButton(
child: WheelChooser.integer( child: const Text('15 minutes'),
initValue: hours, onPressed: () {
onValueChanged: (v) => hours = v, Navigator.pop(
maxValue: 24, context,
minValue: 0, FocusModeData(
unSelectTextStyle: const TextStyle(color: Colors.grey), true,
), maxNumber: difficulty.maxNumber,
disableTime: DateTime.now().add(
const Duration(
minutes: 15,
),
),
),
); // showDialog() returns true
},
), ),
const Text('hours'), ElevatedButton(
Flexible( child: const Text('30 minutes'),
child: WheelChooser.integer( onPressed: () {
initValue: minutes, Navigator.pop(
onValueChanged: (v) => minutes = v, context,
maxValue: 59, FocusModeData(
minValue: 0, true,
unSelectTextStyle: const TextStyle(color: Colors.grey), 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'),
], ],
), ),
) const VerticalPadding(),
], SizedBox(
), height: 100,
actions: [ child: Row(
ElevatedButton( mainAxisAlignment: MainAxisAlignment.spaceEvenly,
child: const Text('Select'), children: [
onPressed: () { Flexible(
Navigator.pop( child: WheelChooser.integer(
context, initValue: hours,
Duration( onValueChanged: (v) => hours = v,
hours: hours, maxValue: 24,
minutes: minutes, 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
},
),
]);
},
),
); );
} }

View file

@ -1,18 +1,24 @@
const defaultMaxNumber = 1000;
class FocusModeData { class FocusModeData {
final DateTime? disableTime; final DateTime? disableTime;
final int maxNumber;
final bool enabled; 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.disabled() => const FocusModeData(false);
factory FocusModeData.fromJson(Map<String, dynamic> json) => FocusModeData( factory FocusModeData.fromJson(Map<String, dynamic> json) => FocusModeData(
json['enabled'], json['enabled'],
maxNumber: json['maxNumber'] ?? defaultMaxNumber,
disableTime: DateTime.tryParse(json['disableTime'] ?? ''), disableTime: DateTime.tryParse(json['disableTime'] ?? ''),
); );
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
'enabled': enabled, 'enabled': enabled,
'maxNumber': maxNumber,
if (disableTime != null) 'disableTime': disableTime!.toIso8601String(), if (disableTime != null) 'disableTime': disableTime!.toIso8601String(),
}; };
} }

View file

@ -12,13 +12,9 @@ import '../models/focus_mode_data.dart';
import '../riverpod_controllers/focus_mode.dart'; import '../riverpod_controllers/focus_mode.dart';
import '../routes.dart'; import '../routes.dart';
import '../utils/snackbar_builder.dart'; import '../utils/snackbar_builder.dart';
import '../utils/string_utils.dart';
const _maxNumber = 1000000; const _magicUnlockNumber = -1563948536;
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.###");
class GameState { class GameState {
final int maxNumber; final int maxNumber;
@ -41,7 +37,7 @@ class GameState {
return 'You got it!'; return 'You got it!';
} }
bool get found => number == lastGuess || lastGuess == magicUnlockNumber; bool get found => number == lastGuess || lastGuess == _magicUnlockNumber;
const GameState({ const GameState({
required this.number, required this.number,
@ -71,8 +67,18 @@ class _DisableFocusModeScreenState
extends ConsumerState<DisableFocusModeScreen> { extends ConsumerState<DisableFocusModeScreen> {
final formKey = GlobalKey<FormState>(); final formKey = GlobalKey<FormState>();
final guessController = TextEditingController(); final guessController = TextEditingController();
var game = GameState.newGame(_maxNumber); var game = GameState.newGame(1);
var message = introMessage; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -126,7 +132,7 @@ class _DisableFocusModeScreenState
final valid = formKey.currentState?.validate() ?? false; final valid = formKey.currentState?.validate() ?? false;
if (!valid) { if (!valid) {
buildSnackbar(context, buildSnackbar(context,
'Please enter an integer between 0 and ${decimalWithCommasFormat.format(_maxNumber)}'); 'Please enter an integer between 0 and ${decimalWithCommasFormat.format(maxNumber)}');
return; return;
} }
@ -187,17 +193,11 @@ class ThousandsSeparatorInputFormatter extends TextInputFormatter {
final decimalPart = parts[1]; final decimalPart = parts[1];
// Handle edge case where decimal part is present but empty (user just typed the dot) // Handle edge case where decimal part is present but empty (user just typed the dot)
final formattedText = '${formatter.format(integerPart)}.$decimalPart'; final formattedText = '${formatter.format(integerPart)}.$decimalPart';
return TextEditingValue( return newValue.copyWith(text: formattedText);
text: formattedText,
selection: updateCursorPosition(formattedText),
);
} else { } else {
// No decimal part, format the whole number // No decimal part, format the whole number
final newFormattedText = formatter.format(newTextAsNum); final newFormattedText = formatter.format(newTextAsNum);
return TextEditingValue( return newValue.copyWith(text: newFormattedText);
text: newFormattedText,
selection: updateCursorPosition(newFormattedText),
);
} }
} }

View file

@ -1,3 +1,5 @@
import 'package:intl/intl.dart';
extension StringUtils on String { extension StringUtils on String {
String truncate({int length = 32}) { String truncate({int length = 32}) {
if (this.length <= length) { if (this.length <= length) {
@ -7,8 +9,10 @@ extension StringUtils on String {
return '${substring(0, length)}...'; return '${substring(0, length)}...';
} }
String stripHyperlinks() => String stripHyperlinks() => replaceAll(
replaceAll(RegExp( RegExp(
r"(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?"), r"(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?"),
""); "");
} }
final decimalWithCommasFormat = NumberFormat("#,##0.###");