mirror of
https://gitlab.com/mysocialportal/relatica
synced 2024-10-18 13:33:32 +00:00
Merge branch 'focus-mode-tweaks' into 'main'
Focus mode tweaks See merge request mysocialportal/relatica!63
This commit is contained in:
commit
c4f48106a0
8 changed files with 362 additions and 171 deletions
|
@ -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,28 +60,42 @@ 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(
|
||||||
|
builder: (BuildContext context, void Function(void Function()) setState) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Choose Focus Duration',
|
'Choose Focus Duration and Difficulty',
|
||||||
|
softWrap: true,
|
||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
.bodyLarge!
|
.bodyLarge!
|
||||||
.copyWith(fontWeight: FontWeight.bold),
|
.copyWith(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
const VerticalPadding(),
|
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(
|
Wrap(
|
||||||
runSpacing: 10.0,
|
runSpacing: 10.0,
|
||||||
spacing: 10.0,
|
spacing: 10.0,
|
||||||
|
@ -79,9 +105,15 @@ Future<Duration?> _chooseDuration(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(
|
Navigator.pop(
|
||||||
context,
|
context,
|
||||||
|
FocusModeData(
|
||||||
|
true,
|
||||||
|
maxNumber: difficulty.maxNumber,
|
||||||
|
disableTime: DateTime.now().add(
|
||||||
const Duration(
|
const Duration(
|
||||||
minutes: 15,
|
minutes: 15,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
); // showDialog() returns true
|
); // showDialog() returns true
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -90,9 +122,15 @@ Future<Duration?> _chooseDuration(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(
|
Navigator.pop(
|
||||||
context,
|
context,
|
||||||
|
FocusModeData(
|
||||||
|
true,
|
||||||
|
maxNumber: difficulty.maxNumber,
|
||||||
|
disableTime: DateTime.now().add(
|
||||||
const Duration(
|
const Duration(
|
||||||
minutes: 30,
|
minutes: 30,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
); // showDialog() returns true
|
); // showDialog() returns true
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -101,9 +139,15 @@ Future<Duration?> _chooseDuration(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(
|
Navigator.pop(
|
||||||
context,
|
context,
|
||||||
|
FocusModeData(
|
||||||
|
true,
|
||||||
|
maxNumber: difficulty.maxNumber,
|
||||||
|
disableTime: DateTime.now().add(
|
||||||
const Duration(
|
const Duration(
|
||||||
hours: 1,
|
hours: 1,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
); // showDialog() returns true
|
); // showDialog() returns true
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -112,9 +156,15 @@ Future<Duration?> _chooseDuration(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(
|
Navigator.pop(
|
||||||
context,
|
context,
|
||||||
|
FocusModeData(
|
||||||
|
true,
|
||||||
|
maxNumber: difficulty.maxNumber,
|
||||||
|
disableTime: DateTime.now().add(
|
||||||
const Duration(
|
const Duration(
|
||||||
hours: 6,
|
hours: 6,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
); // showDialog() returns true
|
); // showDialog() returns true
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -123,9 +173,15 @@ Future<Duration?> _chooseDuration(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(
|
Navigator.pop(
|
||||||
context,
|
context,
|
||||||
|
FocusModeData(
|
||||||
|
true,
|
||||||
|
maxNumber: difficulty.maxNumber,
|
||||||
|
disableTime: DateTime.now().add(
|
||||||
const Duration(
|
const Duration(
|
||||||
hours: 12,
|
hours: 12,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
); // showDialog() returns true
|
); // showDialog() returns true
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -134,9 +190,15 @@ Future<Duration?> _chooseDuration(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(
|
Navigator.pop(
|
||||||
context,
|
context,
|
||||||
|
FocusModeData(
|
||||||
|
true,
|
||||||
|
maxNumber: difficulty.maxNumber,
|
||||||
|
disableTime: DateTime.now().add(
|
||||||
const Duration(
|
const Duration(
|
||||||
days: 1,
|
days: 1,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
); // showDialog() returns true
|
); // showDialog() returns true
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -145,7 +207,10 @@ Future<Duration?> _chooseDuration(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(
|
Navigator.pop(
|
||||||
context,
|
context,
|
||||||
foreverDuration,
|
FocusModeData(
|
||||||
|
true,
|
||||||
|
maxNumber: difficulty.maxNumber,
|
||||||
|
),
|
||||||
); // showDialog() returns true
|
); // showDialog() returns true
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -163,7 +228,8 @@ Future<Duration?> _chooseDuration(
|
||||||
onValueChanged: (v) => hours = v,
|
onValueChanged: (v) => hours = v,
|
||||||
maxValue: 24,
|
maxValue: 24,
|
||||||
minValue: 0,
|
minValue: 0,
|
||||||
unSelectTextStyle: const TextStyle(color: Colors.grey),
|
unSelectTextStyle:
|
||||||
|
const TextStyle(color: Colors.grey),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Text('hours'),
|
const Text('hours'),
|
||||||
|
@ -173,7 +239,8 @@ Future<Duration?> _chooseDuration(
|
||||||
onValueChanged: (v) => minutes = v,
|
onValueChanged: (v) => minutes = v,
|
||||||
maxValue: 59,
|
maxValue: 59,
|
||||||
minValue: 0,
|
minValue: 0,
|
||||||
unSelectTextStyle: const TextStyle(color: Colors.grey),
|
unSelectTextStyle:
|
||||||
|
const TextStyle(color: Colors.grey),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Text('minutes'),
|
const Text('minutes'),
|
||||||
|
@ -188,14 +255,21 @@ Future<Duration?> _chooseDuration(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(
|
Navigator.pop(
|
||||||
context,
|
context,
|
||||||
|
FocusModeData(
|
||||||
|
true,
|
||||||
|
maxNumber: difficulty.maxNumber,
|
||||||
|
disableTime: DateTime.now().add(
|
||||||
Duration(
|
Duration(
|
||||||
hours: hours,
|
hours: hours,
|
||||||
minutes: minutes,
|
minutes: minutes,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
); // showDialog() returns true
|
); // showDialog() returns true
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ class _FocusModeStatusHeadlineState extends State<FocusModeStatusHeadline> {
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_updateTimeUntil();
|
_updateTimeUntil();
|
||||||
updateTimer = Timer.periodic(Duration(seconds: 1), (_) {
|
updateTimer = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_updateTimeUntil();
|
_updateTimeUntil();
|
||||||
});
|
});
|
||||||
|
@ -31,7 +31,6 @@ class _FocusModeStatusHeadlineState extends State<FocusModeStatusHeadline> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
print('Disposing');
|
|
||||||
updateTimer?.cancel();
|
updateTimer?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,6 @@ import 'package:provider/provider.dart';
|
||||||
import 'app_theme.dart';
|
import 'app_theme.dart';
|
||||||
import 'di_initialization.dart';
|
import 'di_initialization.dart';
|
||||||
import 'globals.dart';
|
import 'globals.dart';
|
||||||
import 'riverpod_controllers/focus_mode.dart';
|
|
||||||
import 'routes.dart';
|
import 'routes.dart';
|
||||||
import 'services/auth_service.dart';
|
import 'services/auth_service.dart';
|
||||||
import 'services/blocks_manager.dart';
|
import 'services/blocks_manager.dart';
|
||||||
|
@ -82,8 +81,6 @@ class _AppState extends fr.ConsumerState<App> {
|
||||||
refreshListenable: authService,
|
refreshListenable: authService,
|
||||||
redirect: (context, state) async {
|
redirect: (context, state) async {
|
||||||
final loggedIn = authService.loggedIn;
|
final loggedIn = authService.loggedIn;
|
||||||
final focusMode = ref.read(focusModeProvider);
|
|
||||||
print('Focus mode? $focusMode');
|
|
||||||
|
|
||||||
if (!loggedIn && authService.initializing) {
|
if (!loggedIn && authService.initializing) {
|
||||||
return ScreenPaths.splash;
|
return ScreenPaths.splash;
|
||||||
|
|
|
@ -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(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.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/focus_mode_status_headline.dart';
|
||||||
import '../controls/padding.dart';
|
import '../controls/padding.dart';
|
||||||
|
@ -10,6 +13,20 @@ 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 _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 {
|
class GameState {
|
||||||
final int maxNumber;
|
final int maxNumber;
|
||||||
|
@ -18,21 +35,21 @@ class GameState {
|
||||||
|
|
||||||
String get hint {
|
String get hint {
|
||||||
if (lastGuess == null) {
|
if (lastGuess == null) {
|
||||||
return 'Guess a number between 0 and $maxNumber';
|
return 'Guess a number between 0 and ${decimalWithCommasFormat.format(maxNumber)}';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastGuess! < number) {
|
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) {
|
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!';
|
return 'You got it!';
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get found => number == lastGuess;
|
bool get found => number == lastGuess || lastGuess == _magicUnlockNumber;
|
||||||
|
|
||||||
const GameState({
|
const GameState({
|
||||||
required this.number,
|
required this.number,
|
||||||
|
@ -50,10 +67,6 @@ class GameState {
|
||||||
GameState(number: Random().nextInt(maxNumber), maxNumber: maxNumber);
|
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 {
|
class DisableFocusModeScreen extends ConsumerStatefulWidget {
|
||||||
const DisableFocusModeScreen({super.key});
|
const DisableFocusModeScreen({super.key});
|
||||||
|
|
||||||
|
@ -66,8 +79,20 @@ 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 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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -87,15 +112,33 @@ class _DisableFocusModeScreenState
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
FocusModeStatusHeadline(disableTime: focusMode.disableTime),
|
FocusModeStatusHeadline(disableTime: focusMode.disableTime),
|
||||||
Text(message),
|
Text(
|
||||||
|
message,
|
||||||
|
softWrap: true,
|
||||||
|
maxLines: 10,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
const VerticalPadding(),
|
const VerticalPadding(),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: guessController,
|
controller: guessController,
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
|
// inputFormatters: [
|
||||||
|
// ThousandsSeparatorInputFormatter(),
|
||||||
|
// ],
|
||||||
|
enableInteractiveSelection: true,
|
||||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
validator: (value) => int.tryParse(value!) == null
|
validator: (value) {
|
||||||
? 'Please enter a number'
|
if (value == null) {
|
||||||
: 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(
|
decoration: InputDecoration(
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderSide: const BorderSide(),
|
borderSide: const BorderSide(),
|
||||||
|
@ -109,11 +152,12 @@ 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 $_maxNumber');
|
'Please enter an integer between 0 and ${decimalWithCommasFormat.format(maxNumber)}');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final guess = int.parse(guessController.text);
|
final guess =
|
||||||
|
int.parse(guessController.text.replaceAll(',', ''));
|
||||||
game = game.update(guess);
|
game = game.update(guess);
|
||||||
if (game.found) {
|
if (game.found) {
|
||||||
ref
|
ref
|
||||||
|
@ -122,7 +166,10 @@ class _DisableFocusModeScreenState
|
||||||
context.go(ScreenPaths.timelines);
|
context.go(ScreenPaths.timelines);
|
||||||
} else {
|
} else {
|
||||||
setState(() {
|
setState(() {
|
||||||
message = game.hint;
|
tryCount++;
|
||||||
|
message = tryCount <= 10
|
||||||
|
? game.hint
|
||||||
|
: '${game.hint}.\n\n$_hintText';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -135,3 +182,49 @@ 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 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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.###");
|
||||||
|
|
16
pubspec.lock
16
pubspec.lock
|
@ -744,6 +744,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.1+1"
|
version: "0.2.1+1"
|
||||||
|
indent:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: indent
|
||||||
|
sha256: "819319a5c185f7fe412750c798953378b37a0d0d32564ce33e7c5acfd1372d2a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.0"
|
||||||
|
intl:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: intl
|
||||||
|
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.19.0"
|
||||||
io:
|
io:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -59,6 +59,8 @@ dependencies:
|
||||||
uuid: ^4.4.2
|
uuid: ^4.4.2
|
||||||
video_player: ^2.9.1
|
video_player: ^2.9.1
|
||||||
wheel_chooser: ^1.1.2
|
wheel_chooser: ^1.1.2
|
||||||
|
intl: ^0.19.0
|
||||||
|
indent: ^2.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
Loading…
Reference in a new issue