From b75238057155d8fcb057afd4f6f7768b33f0b606 Mon Sep 17 00:00:00 2001 From: Faucon <49079695+FauconSpartiate@users.noreply.github.com> Date: Sun, 29 Dec 2024 18:08:44 +0100 Subject: [PATCH] Add option to scale up tests before calculating the average --- lib/calculations/calculator.dart | 16 +- lib/calculations/year.dart | 3 + .../generated/intl/messages_de.dart | 4 + .../generated/intl/messages_en_GB.dart | 4 + .../generated/intl/messages_fr.dart | 4 + .../generated/intl/messages_lb.dart | 4 + .../generated/intl/messages_nl.dart | 4 + lib/localization/generated/l10n.dart | 20 + lib/misc/default_values.dart | 2 + lib/ui/utilities/app_theme.dart | 2 +- .../widgets/better_predictive_transition.dart | 631 +++++++++--------- lib/ui/widgets/settings_tiles.dart | 14 + pubspec.lock | 44 +- pubspec.yaml | 2 + test/calculation_test.dart | 5 + 15 files changed, 425 insertions(+), 334 deletions(-) diff --git a/lib/calculations/calculator.dart b/lib/calculations/calculator.dart index b25f444c..b5eed98b 100644 --- a/lib/calculations/calculator.dart +++ b/lib/calculations/calculator.dart @@ -99,7 +99,8 @@ class Calculator { if (data.isEmpty || isNullFilled) return null; - final double maxGrade = Manager.years.isNotEmpty ? getCurrentYear().maxGrade : getPreference("maxGrade"); + final double maxGrade = Manager.years.isNotEmpty ? getCurrentYear().maxGrade : getPreference("maxGrade"); + final bool scaleUpTests = Manager.years.isNotEmpty ? getCurrentYear().scaleUpTests : getPreference("scaleUpTests"); double totalNumerator = 0; double totalDenominator = 0; @@ -107,12 +108,17 @@ class Calculator { double totalDenominatorSpeaking = 0; for (final CalculationObject c in data.where((element) => element.numerator != null && element.denominator != 0)) { + final double maxGradeMultiplier = scaleUpTests ? maxGrade / c.denominator : 1; + + final double weightedNumerator = c.numerator! * c.weight * maxGradeMultiplier; + final double weightedDenominator = c.denominator * c.weight * maxGradeMultiplier; + if (c is Test && c.isSpeaking) { - totalNumeratorSpeaking += c.numerator! * c.weight; - totalDenominatorSpeaking += c.denominator * c.weight; + totalNumeratorSpeaking += weightedNumerator; + totalDenominatorSpeaking += weightedDenominator; } else { - totalNumerator += c.numerator! * c.weight; - totalDenominator += c.denominator * c.weight; + totalNumerator += weightedNumerator; + totalDenominator += weightedDenominator; } } diff --git a/lib/calculations/year.dart b/lib/calculations/year.dart index 40c9096b..76776e08 100644 --- a/lib/calculations/year.dart +++ b/lib/calculations/year.dart @@ -23,6 +23,7 @@ class Year extends CalculationObject { double maxGrade = DefaultValues.maxGrade; String roundingMode = DefaultValues.roundingMode; int roundTo = DefaultValues.roundTo; + bool scaleUpTests = DefaultValues.scaleUpTests; bool isYearOverview = false; bool hasBeenSortedCustom = false; @@ -174,6 +175,7 @@ class Year extends CalculationObject { maxGrade = json["maxGrade"] as double? ?? DefaultValues.maxGrade; roundingMode = json["roundingMode"] as String? ?? DefaultValues.roundingMode; roundTo = json["roundTo"] as int? ?? DefaultValues.roundTo; + scaleUpTests = (json["scaleUpTests"] as bool?) ?? DefaultValues.scaleUpTests; hasBeenSortedCustom = (json["hasBeenSortedCustom"] as bool?) ?? DefaultValues.hasBeenSortedCustom; } @@ -184,6 +186,7 @@ class Year extends CalculationObject { "maxGrade": maxGrade, "roundingMode": roundingMode, "roundTo": roundTo, + "scaleUpTests": scaleUpTests, "validatedSchoolSystem": validatedSchoolSystem, "validatedLuxSystem": validatedLuxSystem, "validatedYear": validatedYear, diff --git a/lib/localization/generated/intl/messages_de.dart b/lib/localization/generated/intl/messages_de.dart index c20c5d62..cd717252 100644 --- a/lib/localization/generated/intl/messages_de.dart +++ b/lib/localization/generated/intl/messages_de.dart @@ -181,6 +181,10 @@ class MessageLookup extends MessageLookupByLibrary { "rounding_mode": MessageLookupByLibrary.simpleMessage("Abrundungsmodus"), "save": MessageLookupByLibrary.simpleMessage("Speichern"), + "scale_up_tests": + MessageLookupByLibrary.simpleMessage("Prüfungen skalieren"), + "scale_up_tests_description": MessageLookupByLibrary.simpleMessage( + "Prüfungen zur max. Note skalieren bevor der Durchschnitt berechnet wird"), "school_system": MessageLookupByLibrary.simpleMessage("Schulsystem"), "school_termOne": MessageLookupByLibrary.simpleMessage("Schulperiode"), "school_termOther": diff --git a/lib/localization/generated/intl/messages_en_GB.dart b/lib/localization/generated/intl/messages_en_GB.dart index c717ea9f..f791cc60 100644 --- a/lib/localization/generated/intl/messages_en_GB.dart +++ b/lib/localization/generated/intl/messages_en_GB.dart @@ -174,6 +174,10 @@ class MessageLookup extends MessageLookupByLibrary { "round_to": MessageLookupByLibrary.simpleMessage("Round to"), "rounding_mode": MessageLookupByLibrary.simpleMessage("Rounding mode"), "save": MessageLookupByLibrary.simpleMessage("Save"), + "scale_up_tests": + MessageLookupByLibrary.simpleMessage("Scale up tests"), + "scale_up_tests_description": MessageLookupByLibrary.simpleMessage( + "Scale up tests to the max. grade before calculating the average"), "school_system": MessageLookupByLibrary.simpleMessage("School system"), "school_termOne": MessageLookupByLibrary.simpleMessage("School term"), "school_termOther": diff --git a/lib/localization/generated/intl/messages_fr.dart b/lib/localization/generated/intl/messages_fr.dart index d0c419bd..5c901fee 100644 --- a/lib/localization/generated/intl/messages_fr.dart +++ b/lib/localization/generated/intl/messages_fr.dart @@ -186,6 +186,10 @@ class MessageLookup extends MessageLookupByLibrary { "rounding_mode": MessageLookupByLibrary.simpleMessage("Mode arrondissage"), "save": MessageLookupByLibrary.simpleMessage("Sauvegarder"), + "scale_up_tests": + MessageLookupByLibrary.simpleMessage("Étendre les devoirs"), + "scale_up_tests_description": MessageLookupByLibrary.simpleMessage( + "Étendre les devoirs à la note max. avant de calculer la moyenne"), "school_system": MessageLookupByLibrary.simpleMessage("Système scolaire"), "school_termOne": diff --git a/lib/localization/generated/intl/messages_lb.dart b/lib/localization/generated/intl/messages_lb.dart index 11233d02..871edfbe 100644 --- a/lib/localization/generated/intl/messages_lb.dart +++ b/lib/localization/generated/intl/messages_lb.dart @@ -181,6 +181,10 @@ class MessageLookup extends MessageLookupByLibrary { "rounding_mode": MessageLookupByLibrary.simpleMessage("Ofronnungsmodus"), "save": MessageLookupByLibrary.simpleMessage("Späicheren"), + "scale_up_tests": + MessageLookupByLibrary.simpleMessage("Prüfungen skaléieren"), + "scale_up_tests_description": MessageLookupByLibrary.simpleMessage( + "Prüfungen zur max. Notte skaléieren virun dass d\'Moyenne gerechent gëtt"), "school_system": MessageLookupByLibrary.simpleMessage("Schoulsystem"), "school_termOne": MessageLookupByLibrary.simpleMessage("Schoulperiod"), "school_termOther": diff --git a/lib/localization/generated/intl/messages_nl.dart b/lib/localization/generated/intl/messages_nl.dart index fbe74b6d..4d5eb401 100644 --- a/lib/localization/generated/intl/messages_nl.dart +++ b/lib/localization/generated/intl/messages_nl.dart @@ -177,6 +177,10 @@ class MessageLookup extends MessageLookupByLibrary { "round_to": MessageLookupByLibrary.simpleMessage("Rond naar"), "rounding_mode": MessageLookupByLibrary.simpleMessage("Ronde modus"), "save": MessageLookupByLibrary.simpleMessage("Opslaan"), + "scale_up_tests": + MessageLookupByLibrary.simpleMessage("Toetsen opschalen"), + "scale_up_tests_description": MessageLookupByLibrary.simpleMessage( + "Toetsen opschalen tot het max. cijfer voordat het gemiddelde wordt berekend"), "school_system": MessageLookupByLibrary.simpleMessage("Schoolsysteem"), "school_termOne": MessageLookupByLibrary.simpleMessage("Schoolperiode"), "school_termOther": diff --git a/lib/localization/generated/l10n.dart b/lib/localization/generated/l10n.dart index 0af05888..3a383efe 100644 --- a/lib/localization/generated/l10n.dart +++ b/lib/localization/generated/l10n.dart @@ -1280,6 +1280,26 @@ class TranslationsClass { ); } + /// `Scale up tests` + String get scale_up_tests { + return Intl.message( + 'Scale up tests', + name: 'scale_up_tests', + desc: '', + args: [], + ); + } + + /// `Scale up tests to the max. grade before calculating the average` + String get scale_up_tests_description { + return Intl.message( + 'Scale up tests to the max. grade before calculating the average', + name: 'scale_up_tests_description', + desc: '', + args: [], + ); + } + /// `School system` String get school_system { return Intl.message( diff --git a/lib/misc/default_values.dart b/lib/misc/default_values.dart index f2c5b203..3dd900b2 100644 --- a/lib/misc/default_values.dart +++ b/lib/misc/default_values.dart @@ -19,6 +19,7 @@ class DefaultValues { static const String roundingMode = RoundingMode.up; static const int roundTo = 1; static const int preciseRoundToMultiplier = 10; + static const bool scaleUpTests = false; static const double weight = 1.0; static const double bonus = 1; static const double speakingWeight = 3.0; @@ -63,6 +64,7 @@ const Map defaultValues = { "roundingMode": RoundingMode.up, "roundTo": 1, "preciseRoundToMultiplier": 10, + "scaleUpTests": false, "weight": 1.0, "speakingWeight": 3.0, "examWeight": 2.0, diff --git a/lib/ui/utilities/app_theme.dart b/lib/ui/utilities/app_theme.dart index ae1da121..b8afd776 100644 --- a/lib/ui/utilities/app_theme.dart +++ b/lib/ui/utilities/app_theme.dart @@ -1,9 +1,9 @@ // Flutter imports: -import "package:dynamic_color/dynamic_color.dart"; import "package:flutter/material.dart"; // Package imports: import "package:animations/animations.dart"; +import "package:dynamic_color/dynamic_color.dart"; import "package:flex_color_scheme/flex_color_scheme.dart"; // Project imports: diff --git a/lib/ui/widgets/better_predictive_transition.dart b/lib/ui/widgets/better_predictive_transition.dart index 5510e0dd..4549e802 100644 --- a/lib/ui/widgets/better_predictive_transition.dart +++ b/lib/ui/widgets/better_predictive_transition.dart @@ -1,314 +1,317 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import "package:flutter/material.dart"; -import "package:flutter/services.dart"; -import "package:graded/ui/utilities/app_theme.dart"; - -/// Used by [PageTransitionsTheme] to define a [MaterialPageRoute] page -/// transition animation that looks like the default page transition used on -/// Android U and above when using predictive back. -/// -/// Currently predictive back is only supported on Android U and above, and if -/// this [PageTransitionsBuilder] is used by any other platform, it will fall -/// back to [SharedAxisTransitionBuilder]. -/// -/// When used on Android U and above, animates along with the back gesture to -/// reveal the destination route. Can be canceled by dragging back towards the -/// edge of the screen. -/// -/// See also: -/// -/// * [FadeUpwardsPageTransitionsBuilder], which defines a page transition -/// that's similar to the one provided by Android O. -/// * [OpenUpwardsPageTransitionsBuilder], which defines a page transition -/// that's similar to the one provided by Android P. -/// * [ZoomPageTransitionsBuilder], which defines the default page transition -/// that's similar to the one provided in Android Q. -/// * [CupertinoPageTransitionsBuilder], which defines a horizontal page -/// transition that matches native iOS page transitions. -class BetterPredictiveBackPageTransitionsBuilder extends PageTransitionsBuilder { - /// Creates an instance of a [PageTransitionsBuilder] that matches Android U's - /// predictive back transition. - const BetterPredictiveBackPageTransitionsBuilder(); - - @override - Widget buildTransitions( - PageRoute route, - BuildContext context, - Animation animation, - Animation secondaryAnimation, - Widget child, - ) { - return _PredictiveBackGestureDetector( - route: route, - builder: (BuildContext context) { - // Only do a predictive back transition when the user is performing a - // pop gesture. Otherwise, for things like button presses or other - // programmatic navigation, fall back to SharedAxisTransitionBuilder. - if (route.popGestureInProgress) { - return _PredictiveBackPageTransition( - animation: animation, - secondaryAnimation: secondaryAnimation, - getIsCurrent: () => route.isCurrent, - child: child, - ); - } - - return const SharedAxisTransitionBuilder().buildTransitions( - route, - context, - animation, - secondaryAnimation, - child, - ); - }, - ); - } -} - -class _PredictiveBackGestureDetector extends StatefulWidget { - const _PredictiveBackGestureDetector({ - required this.route, - required this.builder, - }); - - final WidgetBuilder builder; - final PredictiveBackRoute route; - - @override - State<_PredictiveBackGestureDetector> createState() => _PredictiveBackGestureDetectorState(); -} - -class _PredictiveBackGestureDetectorState extends State<_PredictiveBackGestureDetector> with WidgetsBindingObserver { - /// True when the predictive back gesture is enabled. - bool get _isEnabled { - return widget.route.isCurrent && widget.route.popGestureEnabled; - } - - /// The back event when the gesture first started. - PredictiveBackEvent? get startBackEvent => _startBackEvent; - PredictiveBackEvent? _startBackEvent; - set startBackEvent(PredictiveBackEvent? startBackEvent) { - if (_startBackEvent != startBackEvent && mounted) { - setState(() { - _startBackEvent = startBackEvent; - }); - } - } - - /// The most recent back event during the gesture. - PredictiveBackEvent? get currentBackEvent => _currentBackEvent; - PredictiveBackEvent? _currentBackEvent; - set currentBackEvent(PredictiveBackEvent? currentBackEvent) { - if (_currentBackEvent != currentBackEvent && mounted) { - setState(() { - _currentBackEvent = currentBackEvent; - }); - } - } - - // Begin WidgetsBindingObserver. - - @override - bool handleStartBackGesture(PredictiveBackEvent backEvent) { - final bool gestureInProgress = !backEvent.isButtonEvent && _isEnabled; - if (!gestureInProgress) { - return false; - } - - widget.route.handleStartBackGesture(progress: 1 - backEvent.progress); - startBackEvent = currentBackEvent = backEvent; - return true; - } - - @override - void handleUpdateBackGestureProgress(PredictiveBackEvent backEvent) { - widget.route.handleUpdateBackGestureProgress(progress: 1 - backEvent.progress); - currentBackEvent = backEvent; - } - - @override - void handleCancelBackGesture() { - widget.route.handleCancelBackGesture(); - startBackEvent = currentBackEvent = null; - } - - @override - void handleCommitBackGesture() { - widget.route.handleCommitBackGesture(); - startBackEvent = currentBackEvent = null; - } - - // End WidgetsBindingObserver. - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addObserver(this); - } - - @override - void dispose() { - WidgetsBinding.instance.removeObserver(this); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return widget.builder(context); - } -} - -/// Android's predictive back page transition. -class _PredictiveBackPageTransition extends StatelessWidget { - const _PredictiveBackPageTransition({ - required this.animation, - required this.secondaryAnimation, - required this.getIsCurrent, - required this.child, - }); - - // These values were eyeballed to match the native predictive back animation - // on a Pixel 2 running Android API 34. - static const double _scaleFullyOpened = 1.0; - static const double _scaleStartTransition = 0.95; - static const double _opacityFullyOpened = 1.0; - static const double _opacityStartTransition = 0.95; - static const double _weightForStartState = 65.0; - static const double _weightForEndState = 35.0; - static const double _screenWidthDivisionFactor = 20.0; - static const double _xShiftAdjustment = 8.0; - - final Animation animation; - final Animation secondaryAnimation; - final ValueGetter getIsCurrent; - final Widget child; - - Widget _secondaryAnimatedBuilder(BuildContext context, Widget? child) { - final Size size = MediaQuery.sizeOf(context); - final double screenWidth = size.width; - final double xShift = (screenWidth / _screenWidthDivisionFactor) - _xShiftAdjustment; - - final bool isCurrent = getIsCurrent(); - final Tween xShiftTween = isCurrent ? ConstantTween(0) : Tween(begin: xShift, end: 0); - final Animatable scaleTween = isCurrent - ? ConstantTween(_scaleFullyOpened) - : TweenSequence(>[ - TweenSequenceItem( - tween: Tween( - begin: _scaleStartTransition, - end: _scaleFullyOpened, - ), - weight: _weightForStartState, - ), - TweenSequenceItem( - tween: Tween( - begin: _scaleFullyOpened, - end: _scaleFullyOpened, - ), - weight: _weightForEndState, - ), - ]); - final Animatable fadeTween = isCurrent - ? ConstantTween(_opacityFullyOpened) - : TweenSequence(>[ - TweenSequenceItem( - tween: Tween( - begin: _opacityFullyOpened, - end: _opacityStartTransition, - ), - weight: _weightForStartState, - ), - TweenSequenceItem( - tween: Tween( - begin: _opacityFullyOpened, - end: _opacityFullyOpened, - ), - weight: _weightForEndState, - ), - ]); - - return Transform.translate( - offset: Offset(xShiftTween.animate(secondaryAnimation).value, 0), - child: Transform.scale( - scale: scaleTween.animate(secondaryAnimation).value, - child: Opacity( - opacity: fadeTween.animate(secondaryAnimation).value, - child: child, - ), - ), - ); - } - - Widget _primaryAnimatedBuilder(BuildContext context, Widget? child) { - final Size size = MediaQuery.sizeOf(context); - final double screenWidth = size.width; - final double xShift = (screenWidth / _screenWidthDivisionFactor) - _xShiftAdjustment; - - final Animatable xShiftTween = TweenSequence(>[ - TweenSequenceItem( - tween: Tween(begin: 0.0, end: 0.0), - weight: _weightForStartState, - ), - TweenSequenceItem( - tween: Tween(begin: xShift, end: 0.0), - weight: _weightForEndState, - ), - ]); - final Animatable scaleTween = TweenSequence(>[ - TweenSequenceItem( - tween: Tween( - begin: _scaleFullyOpened, - end: _scaleFullyOpened, - ), - weight: _weightForStartState, - ), - TweenSequenceItem( - tween: Tween( - begin: _scaleStartTransition, - end: _scaleFullyOpened, - ), - weight: _weightForEndState, - ), - ]); - final Animatable fadeTween = TweenSequence(>[ - TweenSequenceItem( - tween: Tween(begin: 0.0, end: 0.0), - weight: _weightForStartState, - ), - TweenSequenceItem( - tween: Tween( - begin: _opacityStartTransition, - end: _opacityFullyOpened, - ), - weight: _weightForEndState, - ), - ]); - - return Transform.translate( - offset: Offset(xShiftTween.animate(animation).value, 0), - child: Transform.scale( - scale: scaleTween.animate(animation).value, - child: Opacity( - opacity: fadeTween.animate(animation).value, - child: child, - ), - ), - ); - } - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: secondaryAnimation, - builder: _secondaryAnimatedBuilder, - child: AnimatedBuilder( - animation: animation, - builder: _primaryAnimatedBuilder, - child: child, - ), - ); - } -} +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Flutter imports: +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; + +// Project imports: +import "package:graded/ui/utilities/app_theme.dart"; + +/// Used by [PageTransitionsTheme] to define a [MaterialPageRoute] page +/// transition animation that looks like the default page transition used on +/// Android U and above when using predictive back. +/// +/// Currently predictive back is only supported on Android U and above, and if +/// this [PageTransitionsBuilder] is used by any other platform, it will fall +/// back to [SharedAxisTransitionBuilder]. +/// +/// When used on Android U and above, animates along with the back gesture to +/// reveal the destination route. Can be canceled by dragging back towards the +/// edge of the screen. +/// +/// See also: +/// +/// * [FadeUpwardsPageTransitionsBuilder], which defines a page transition +/// that's similar to the one provided by Android O. +/// * [OpenUpwardsPageTransitionsBuilder], which defines a page transition +/// that's similar to the one provided by Android P. +/// * [ZoomPageTransitionsBuilder], which defines the default page transition +/// that's similar to the one provided in Android Q. +/// * [CupertinoPageTransitionsBuilder], which defines a horizontal page +/// transition that matches native iOS page transitions. +class BetterPredictiveBackPageTransitionsBuilder extends PageTransitionsBuilder { + /// Creates an instance of a [PageTransitionsBuilder] that matches Android U's + /// predictive back transition. + const BetterPredictiveBackPageTransitionsBuilder(); + + @override + Widget buildTransitions( + PageRoute route, + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) { + return _PredictiveBackGestureDetector( + route: route, + builder: (BuildContext context) { + // Only do a predictive back transition when the user is performing a + // pop gesture. Otherwise, for things like button presses or other + // programmatic navigation, fall back to SharedAxisTransitionBuilder. + if (route.popGestureInProgress) { + return _PredictiveBackPageTransition( + animation: animation, + secondaryAnimation: secondaryAnimation, + getIsCurrent: () => route.isCurrent, + child: child, + ); + } + + return const SharedAxisTransitionBuilder().buildTransitions( + route, + context, + animation, + secondaryAnimation, + child, + ); + }, + ); + } +} + +class _PredictiveBackGestureDetector extends StatefulWidget { + const _PredictiveBackGestureDetector({ + required this.route, + required this.builder, + }); + + final WidgetBuilder builder; + final PredictiveBackRoute route; + + @override + State<_PredictiveBackGestureDetector> createState() => _PredictiveBackGestureDetectorState(); +} + +class _PredictiveBackGestureDetectorState extends State<_PredictiveBackGestureDetector> with WidgetsBindingObserver { + /// True when the predictive back gesture is enabled. + bool get _isEnabled { + return widget.route.isCurrent && widget.route.popGestureEnabled; + } + + /// The back event when the gesture first started. + PredictiveBackEvent? get startBackEvent => _startBackEvent; + PredictiveBackEvent? _startBackEvent; + set startBackEvent(PredictiveBackEvent? startBackEvent) { + if (_startBackEvent != startBackEvent && mounted) { + setState(() { + _startBackEvent = startBackEvent; + }); + } + } + + /// The most recent back event during the gesture. + PredictiveBackEvent? get currentBackEvent => _currentBackEvent; + PredictiveBackEvent? _currentBackEvent; + set currentBackEvent(PredictiveBackEvent? currentBackEvent) { + if (_currentBackEvent != currentBackEvent && mounted) { + setState(() { + _currentBackEvent = currentBackEvent; + }); + } + } + + // Begin WidgetsBindingObserver. + + @override + bool handleStartBackGesture(PredictiveBackEvent backEvent) { + final bool gestureInProgress = !backEvent.isButtonEvent && _isEnabled; + if (!gestureInProgress) { + return false; + } + + widget.route.handleStartBackGesture(progress: 1 - backEvent.progress); + startBackEvent = currentBackEvent = backEvent; + return true; + } + + @override + void handleUpdateBackGestureProgress(PredictiveBackEvent backEvent) { + widget.route.handleUpdateBackGestureProgress(progress: 1 - backEvent.progress); + currentBackEvent = backEvent; + } + + @override + void handleCancelBackGesture() { + widget.route.handleCancelBackGesture(); + startBackEvent = currentBackEvent = null; + } + + @override + void handleCommitBackGesture() { + widget.route.handleCommitBackGesture(); + startBackEvent = currentBackEvent = null; + } + + // End WidgetsBindingObserver. + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return widget.builder(context); + } +} + +/// Android's predictive back page transition. +class _PredictiveBackPageTransition extends StatelessWidget { + const _PredictiveBackPageTransition({ + required this.animation, + required this.secondaryAnimation, + required this.getIsCurrent, + required this.child, + }); + + // These values were eyeballed to match the native predictive back animation + // on a Pixel 2 running Android API 34. + static const double _scaleFullyOpened = 1.0; + static const double _scaleStartTransition = 0.95; + static const double _opacityFullyOpened = 1.0; + static const double _opacityStartTransition = 0.95; + static const double _weightForStartState = 65.0; + static const double _weightForEndState = 35.0; + static const double _screenWidthDivisionFactor = 20.0; + static const double _xShiftAdjustment = 8.0; + + final Animation animation; + final Animation secondaryAnimation; + final ValueGetter getIsCurrent; + final Widget child; + + Widget _secondaryAnimatedBuilder(BuildContext context, Widget? child) { + final Size size = MediaQuery.sizeOf(context); + final double screenWidth = size.width; + final double xShift = (screenWidth / _screenWidthDivisionFactor) - _xShiftAdjustment; + + final bool isCurrent = getIsCurrent(); + final Tween xShiftTween = isCurrent ? ConstantTween(0) : Tween(begin: xShift, end: 0); + final Animatable scaleTween = isCurrent + ? ConstantTween(_scaleFullyOpened) + : TweenSequence(>[ + TweenSequenceItem( + tween: Tween( + begin: _scaleStartTransition, + end: _scaleFullyOpened, + ), + weight: _weightForStartState, + ), + TweenSequenceItem( + tween: Tween( + begin: _scaleFullyOpened, + end: _scaleFullyOpened, + ), + weight: _weightForEndState, + ), + ]); + final Animatable fadeTween = isCurrent + ? ConstantTween(_opacityFullyOpened) + : TweenSequence(>[ + TweenSequenceItem( + tween: Tween( + begin: _opacityFullyOpened, + end: _opacityStartTransition, + ), + weight: _weightForStartState, + ), + TweenSequenceItem( + tween: Tween( + begin: _opacityFullyOpened, + end: _opacityFullyOpened, + ), + weight: _weightForEndState, + ), + ]); + + return Transform.translate( + offset: Offset(xShiftTween.animate(secondaryAnimation).value, 0), + child: Transform.scale( + scale: scaleTween.animate(secondaryAnimation).value, + child: Opacity( + opacity: fadeTween.animate(secondaryAnimation).value, + child: child, + ), + ), + ); + } + + Widget _primaryAnimatedBuilder(BuildContext context, Widget? child) { + final Size size = MediaQuery.sizeOf(context); + final double screenWidth = size.width; + final double xShift = (screenWidth / _screenWidthDivisionFactor) - _xShiftAdjustment; + + final Animatable xShiftTween = TweenSequence(>[ + TweenSequenceItem( + tween: Tween(begin: 0.0, end: 0.0), + weight: _weightForStartState, + ), + TweenSequenceItem( + tween: Tween(begin: xShift, end: 0.0), + weight: _weightForEndState, + ), + ]); + final Animatable scaleTween = TweenSequence(>[ + TweenSequenceItem( + tween: Tween( + begin: _scaleFullyOpened, + end: _scaleFullyOpened, + ), + weight: _weightForStartState, + ), + TweenSequenceItem( + tween: Tween( + begin: _scaleStartTransition, + end: _scaleFullyOpened, + ), + weight: _weightForEndState, + ), + ]); + final Animatable fadeTween = TweenSequence(>[ + TweenSequenceItem( + tween: Tween(begin: 0.0, end: 0.0), + weight: _weightForStartState, + ), + TweenSequenceItem( + tween: Tween( + begin: _opacityStartTransition, + end: _opacityFullyOpened, + ), + weight: _weightForEndState, + ), + ]); + + return Transform.translate( + offset: Offset(xShiftTween.animate(animation).value, 0), + child: Transform.scale( + scale: scaleTween.animate(animation).value, + child: Opacity( + opacity: fadeTween.animate(animation).value, + child: child, + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: secondaryAnimation, + builder: _secondaryAnimatedBuilder, + child: AnimatedBuilder( + animation: animation, + builder: _primaryAnimatedBuilder, + child: child, + ), + ); + } +} diff --git a/lib/ui/widgets/settings_tiles.dart b/lib/ui/widgets/settings_tiles.dart index 7e08b2be..0b337ee7 100644 --- a/lib/ui/widgets/settings_tiles.dart +++ b/lib/ui/widgets/settings_tiles.dart @@ -91,6 +91,20 @@ List getSettingsTiles(BuildContext context, {required CreationType type, onChanged?.call(); }, ), + SwitchSettingsTile( + title: translations.scale_up_tests, + subtitle: translations.scale_up_tests_description, + icon: Icons.scale, + settingKey: "scaleUpTests", + // ignore: avoid_redundant_argument_values + defaultValue: DefaultValues.scaleUpTests, + onChange: (value) { + getCurrentYear().scaleUpTests = value; + + Manager.calculate(); + onChanged?.call(); + }, + ), ]; } diff --git a/pubspec.lock b/pubspec.lock index 34f4e057..ac25036e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -42,10 +42,10 @@ packages: dependency: transitive description: name: archive - sha256: "6199c74e3db4fbfbd04f66d739e72fe11c8a8957d5f219f1f4482dbde6420b5a" + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "3.6.1" args: dependency: transitive description: @@ -142,6 +142,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" + url: "https://pub.dev" + source: hosted + version: "2.3.7" decimal: dependency: "direct main" description: @@ -292,10 +300,10 @@ packages: dependency: "direct dev" description: name: flutter_native_splash - sha256: "1152ab0067ca5a2ebeb862fe0a762057202cceb22b7e62692dcbabf6483891bb" + sha256: "7062602e0dbd29141fb8eb19220b5871ca650be5197ab9c1f193a28b17537bc7" url: "https://pub.dev" source: hosted - version: "2.4.3" + version: "2.4.4" flutter_svg: dependency: "direct main" description: @@ -374,10 +382,10 @@ packages: dependency: transitive description: name: image - sha256: "8346ad4b5173924b5ddddab782fc7d8a6300178c8b1dc427775405a01701c4a6" + sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d url: "https://pub.dev" source: hosted - version: "4.5.2" + version: "4.3.0" import_sorter: dependency: "direct dev" description: @@ -394,6 +402,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.19.0" + intl_utils: + dependency: "direct dev" + description: + name: intl_utils + sha256: c2b1f5c72c25512cbeef5ab015c008fc50fe7e04813ba5541c25272300484bf4 + url: "https://pub.dev" + source: hosted + version: "2.8.7" io: dependency: transitive description: @@ -482,6 +498,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.11.1" + material_symbols_icons: + dependency: "direct main" + description: + name: material_symbols_icons + sha256: "64404f47f8e0a9d20478468e5decef867a688660bad7173adcd20418d7f892c9" + url: "https://pub.dev" + source: hosted + version: "4.2801.0" meta: dependency: transitive description: @@ -610,14 +634,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" - posix: - dependency: transitive - description: - name: posix - sha256: a0117dc2167805aa9125b82eee515cc891819bac2f538c83646d355b16f58b9a - url: "https://pub.dev" - source: hosted - version: "6.0.1" provider: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 438a50fb..707f381c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,6 +25,7 @@ dependencies: sdk: flutter flutter_svg: ^2.0.0 intl: ^0.19.0 + material_symbols_icons: ^4.0.0 package_info_plus: ^8.0.0 provider: ^6.0.0 shared_preferences: ^2.0.0 @@ -37,6 +38,7 @@ dev_dependencies: flutter_native_splash: ^2.2.0 icons_launcher: ^3.0.0 import_sorter: ^4.6.0 + intl_utils: ^2.8.7 lint: ^2.0.0 test: ^1.22.0 diff --git a/test/calculation_test.dart b/test/calculation_test.dart index 4cdafecc..b0487ae9 100644 --- a/test/calculation_test.dart +++ b/test/calculation_test.dart @@ -30,6 +30,7 @@ void main() async { final List multipleItemsList = [Test(47.5, 50), Test(65.9, 70), Test(50, 55)]; final List speakingList = [Test(0, 60, isSpeaking: true), Test(60, 60)]; final List clampingList = [Test(80, 100), Test(150, 100)]; + final List scaleUpTestsList = [Test(30, 40), Test(06, 10)]; expect(Calculator.calculate([]), equals(null)); expect(Calculator.calculate(emptyList), equals(null)); @@ -44,6 +45,10 @@ void main() async { expect(Calculator.calculate(clampingList), equals(100)); expect(Calculator.calculate(clampingList, clamp: false), equals(115)); + + expect(Calculator.calculate(scaleUpTestsList, precise: true), equals(72)); + getCurrentYear().scaleUpTests = false; + expect(Calculator.calculate(scaleUpTestsList, precise: true), equals(67.5)); }); test("Number formatting", () {