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:indent/indent.dart'; import 'package:intl/intl.dart'; import '../controls/focus_mode_status_headline.dart'; import '../controls/padding.dart'; 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 _magicUnlockNumber = 1563948536; final _hintText = ''' Try using a "bisection method" to guess. For example, if the interval is 0 to 100 a first guess would be 50. If it says it is too low try 75. If it says it is too high try 25. Continue until you get the right answer. If you are stuck and the interval is very long, or forever, reach out to the help options on the website for unlocking assistance. ''' .unindent() .replaceAll('\n', ' '); class GameState { final int maxNumber; final int number; final int? lastGuess; String get hint { if (lastGuess == null) { return 'Guess a number between 0 and ${decimalWithCommasFormat.format(maxNumber)}'; } if (lastGuess! < number) { return '${decimalWithCommasFormat.format(lastGuess)} is too low. Guess a higher number'; } if (lastGuess! > number) { return '${decimalWithCommasFormat.format(lastGuess)} is too high. Guess a lower number'; } return 'You got it!'; } bool get found => number == lastGuess || lastGuess == _magicUnlockNumber; const GameState({ required this.number, required this.maxNumber, this.lastGuess, }); GameState update(int lastGuess) => GameState( number: number, maxNumber: maxNumber, lastGuess: lastGuess, ); factory GameState.newGame(int maxNumber) => GameState(number: Random().nextInt(maxNumber), maxNumber: maxNumber); } class DisableFocusModeScreen extends ConsumerStatefulWidget { const DisableFocusModeScreen({super.key}); @override ConsumerState createState() => _DisableFocusModeScreenState(); } class _DisableFocusModeScreenState extends ConsumerState { final formKey = GlobalKey(); final guessController = TextEditingController(); var game = GameState.newGame(1); var maxNumber = 1; var tryCount = 0; var message = ''; @override void initState() { super.initState(); maxNumber = ref.read(focusModeProvider).maxNumber; game = GameState.newGame(maxNumber); tryCount = 0; 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) { final focusMode = ref.watch(focusModeProvider); if (!focusMode.enabled) { context.go(ScreenPaths.timelines); } return Scaffold( appBar: AppBar( title: const Text('Disable Focus Mode?'), ), body: Center( child: Form( key: formKey, child: Padding( padding: const EdgeInsets.all(8.0), child: Column( children: [ FocusModeStatusHeadline(disableTime: focusMode.disableTime), Text( message, softWrap: true, maxLines: 10, textAlign: TextAlign.center, ), const VerticalPadding(), TextFormField( controller: guessController, keyboardType: TextInputType.number, // inputFormatters: [ // ThousandsSeparatorInputFormatter(), // ], enableInteractiveSelection: true, autovalidateMode: AutovalidateMode.onUserInteraction, 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(), borderRadius: BorderRadius.circular(5.0), ), ), ), const VerticalPadding(), ElevatedButton( onPressed: () { final valid = formKey.currentState?.validate() ?? false; if (!valid) { buildSnackbar(context, 'Please enter an integer between 0 and ${decimalWithCommasFormat.format(maxNumber)}'); return; } final guess = int.parse(guessController.text.replaceAll(',', '')); game = game.update(guess); if (game.found) { ref .read(focusModeProvider.notifier) .setMode(const FocusModeData(false)); context.go(ScreenPaths.timelines); } else { setState(() { tryCount++; message = tryCount <= 10 ? game.hint : '${game.hint}.\n\n$_hintText'; }); } }, child: const Text('Guess')) ], ), ), ), ), ); } } // 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 newValue.copyWith(text: formattedText); } else { // No decimal part, format the whole number final newFormattedText = formatter.format(newTextAsNum); return newValue.copyWith(text: newFormattedText); } } TextSelection updateCursorPosition(String text) { return TextSelection.collapsed(offset: text.length); } }