diff --git a/lib/screens/disable_focus_mode_screen.dart b/lib/screens/disable_focus_mode_screen.dart index fa06ad4..d7df4d5 100644 --- a/lib/screens/disable_focus_mode_screen.dart +++ b/lib/screens/disable_focus_mode_screen.dart @@ -1,8 +1,10 @@ import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; import '../controls/focus_mode_status_headline.dart'; import '../controls/padding.dart'; @@ -11,6 +13,13 @@ import '../riverpod_controllers/focus_mode.dart'; import '../routes.dart'; import '../utils/snackbar_builder.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.###"); + class GameState { final int maxNumber; final int number; @@ -18,21 +27,21 @@ class GameState { String get hint { if (lastGuess == null) { - return 'Guess a number between 0 and $maxNumber'; + return 'Guess a number between 0 and ${decimalWithCommasFormat.format(maxNumber)}'; } if (lastGuess! < number) { - return '$lastGuess is too low. Guess a higher number'; + return '${decimalWithCommasFormat.format(lastGuess)} is too low. Guess a higher number'; } if (lastGuess! > number) { - return '$lastGuess is too high. Guess a lower number'; + return '${decimalWithCommasFormat.format(lastGuess)} is too high. Guess a lower number'; } return 'You got it!'; } - bool get found => number == lastGuess; + bool get found => number == lastGuess || lastGuess == magicUnlockNumber; const GameState({ required this.number, @@ -50,10 +59,6 @@ class GameState { GameState(number: Random().nextInt(maxNumber), maxNumber: maxNumber); } -const _maxNumber = 100; -const introMessage = - "If you guess the number I've picked from 0 to $_maxNumber you may disable focus mode..."; - class DisableFocusModeScreen extends ConsumerStatefulWidget { const DisableFocusModeScreen({super.key}); @@ -92,10 +97,22 @@ class _DisableFocusModeScreenState TextFormField( controller: guessController, keyboardType: TextInputType.number, + inputFormatters: [ + ThousandsSeparatorInputFormatter(), + ], autovalidateMode: AutovalidateMode.onUserInteraction, - validator: (value) => int.tryParse(value!) == null - ? 'Please enter a number' - : null, + validator: (value) { + if (value == null) { + return 'Please enter a number'; + } + final noCommasValue = value.replaceAll(',', ''); + final intValue = int.tryParse(noCommasValue); + if (intValue == null) { + return 'Please enter a number'; + } + + return null; + }, decoration: InputDecoration( border: OutlineInputBorder( borderSide: const BorderSide(), @@ -109,11 +126,12 @@ class _DisableFocusModeScreenState final valid = formKey.currentState?.validate() ?? false; if (!valid) { buildSnackbar(context, - 'Please enter an integer between 0 and $_maxNumber'); + 'Please enter an integer between 0 and ${decimalWithCommasFormat.format(_maxNumber)}'); return; } - final guess = int.parse(guessController.text); + final guess = + int.parse(guessController.text.replaceAll(',', '')); game = game.update(guess); if (game.found) { ref @@ -135,3 +153,55 @@ class _DisableFocusModeScreenState ); } } + +// Copy/pasted from https://medium.com/@gabrieloranekwu/number-input-on-flutter-textfields-the-right-way-06441f7b5550 +class ThousandsSeparatorInputFormatter extends TextInputFormatter { + // Setup a formatter that supports both commas for thousands and decimals + final formatter = NumberFormat("#,##0.###"); + + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, TextEditingValue newValue) { + if (newValue.text.isEmpty) { + return newValue; + } + + if (newValue.text == '-') { + return newValue; + } + + // Remove commas to check the new input and for parsing + final newText = newValue.text.replaceAll(',', ''); + // Try parsing the input as a double + final num? newTextAsNum = num.tryParse(newText); + + if (newTextAsNum == null) { + return oldValue; // Return old value if new value is not a number + } + + // Split the input into whole number and decimal parts + final parts = newText.split('.'); + if (parts.length > 1) { + // If there's a decimal part, format accordingly + final integerPart = int.tryParse(parts[0]) ?? 0; + 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), + ); + } else { + // No decimal part, format the whole number + final newFormattedText = formatter.format(newTextAsNum); + return TextEditingValue( + text: newFormattedText, + selection: updateCursorPosition(newFormattedText), + ); + } + } + + TextSelection updateCursorPosition(String text) { + return TextSelection.collapsed(offset: text.length); + } +} diff --git a/pubspec.lock b/pubspec.lock index 20ea19b..9954b08 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -744,6 +744,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.1+1" + intl: + dependency: "direct main" + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" io: dependency: transitive description: @@ -772,18 +780,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: @@ -828,10 +836,10 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" media_kit: dependency: "direct main" description: @@ -908,10 +916,10 @@ packages: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" mime: dependency: transitive description: @@ -1433,10 +1441,10 @@ packages: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.2" time_machine: dependency: "direct main" description: @@ -1625,10 +1633,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.2.5" volume_controller: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d277726..1befb29 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -59,6 +59,7 @@ dependencies: uuid: ^4.4.2 video_player: ^2.9.1 wheel_chooser: ^1.1.2 + intl: ^0.19.0 dev_dependencies: flutter_test: