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 '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,28 +60,42 @@ class FocusModeMenuItem extends ConsumerWidget {
}
}
Future<Duration?> _chooseDuration(
Future<FocusModeData?> _chooseFocusMode(
BuildContext context,
) {
var hours = 0;
var minutes = 30;
int hours = 0;
int minutes = 30;
var difficulty = Difficulty.medium;
return showDialog<Duration?>(
return showDialog<FocusModeData?>(
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
builder: (BuildContext context) => StatefulBuilder(
builder: (BuildContext context, void Function(void Function()) setState) {
return AlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Choose Focus Duration',
'Choose Focus Duration and Difficulty',
softWrap: true,
style: Theme.of(context)
.textTheme
.bodyLarge!
.copyWith(fontWeight: FontWeight.bold),
),
const VerticalPadding(),
DropdownButton<Difficulty>(
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,
@ -79,9 +105,15 @@ Future<Duration?> _chooseDuration(
onPressed: () {
Navigator.pop(
context,
FocusModeData(
true,
maxNumber: difficulty.maxNumber,
disableTime: DateTime.now().add(
const Duration(
minutes: 15,
),
),
),
); // showDialog() returns true
},
),
@ -90,9 +122,15 @@ Future<Duration?> _chooseDuration(
onPressed: () {
Navigator.pop(
context,
FocusModeData(
true,
maxNumber: difficulty.maxNumber,
disableTime: DateTime.now().add(
const Duration(
minutes: 30,
),
),
),
); // showDialog() returns true
},
),
@ -101,9 +139,15 @@ Future<Duration?> _chooseDuration(
onPressed: () {
Navigator.pop(
context,
FocusModeData(
true,
maxNumber: difficulty.maxNumber,
disableTime: DateTime.now().add(
const Duration(
hours: 1,
),
),
),
); // showDialog() returns true
},
),
@ -112,9 +156,15 @@ Future<Duration?> _chooseDuration(
onPressed: () {
Navigator.pop(
context,
FocusModeData(
true,
maxNumber: difficulty.maxNumber,
disableTime: DateTime.now().add(
const Duration(
hours: 6,
),
),
),
); // showDialog() returns true
},
),
@ -123,9 +173,15 @@ Future<Duration?> _chooseDuration(
onPressed: () {
Navigator.pop(
context,
FocusModeData(
true,
maxNumber: difficulty.maxNumber,
disableTime: DateTime.now().add(
const Duration(
hours: 12,
),
),
),
); // showDialog() returns true
},
),
@ -134,9 +190,15 @@ Future<Duration?> _chooseDuration(
onPressed: () {
Navigator.pop(
context,
FocusModeData(
true,
maxNumber: difficulty.maxNumber,
disableTime: DateTime.now().add(
const Duration(
days: 1,
),
),
),
); // showDialog() returns true
},
),
@ -145,7 +207,10 @@ Future<Duration?> _chooseDuration(
onPressed: () {
Navigator.pop(
context,
foreverDuration,
FocusModeData(
true,
maxNumber: difficulty.maxNumber,
),
); // showDialog() returns true
},
),
@ -163,7 +228,8 @@ Future<Duration?> _chooseDuration(
onValueChanged: (v) => hours = v,
maxValue: 24,
minValue: 0,
unSelectTextStyle: const TextStyle(color: Colors.grey),
unSelectTextStyle:
const TextStyle(color: Colors.grey),
),
),
const Text('hours'),
@ -173,7 +239,8 @@ Future<Duration?> _chooseDuration(
onValueChanged: (v) => minutes = v,
maxValue: 59,
minValue: 0,
unSelectTextStyle: const TextStyle(color: Colors.grey),
unSelectTextStyle:
const TextStyle(color: Colors.grey),
),
),
const Text('minutes'),
@ -188,14 +255,21 @@ Future<Duration?> _chooseDuration(
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 {
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<String, dynamic> json) => FocusModeData(
json['enabled'],
maxNumber: json['maxNumber'] ?? defaultMaxNumber,
disableTime: DateTime.tryParse(json['disableTime'] ?? ''),
);
Map<String, dynamic> toJson() => {
'enabled': enabled,
'maxNumber': maxNumber,
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 '../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<DisableFocusModeScreen> {
final formKey = GlobalKey<FormState>();
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);
}
}

View file

@ -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.###");