relatica/lib/screens/disable_focus_mode_screen.dart

230 lines
7.5 KiB
Dart

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<DisableFocusModeScreen> createState() =>
_DisableFocusModeScreenState();
}
class _DisableFocusModeScreenState
extends ConsumerState<DisableFocusModeScreen> {
final formKey = GlobalKey<FormState>();
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);
}
}