From 435a2c51779ce998248d9914c3f45ba1cabf90c5 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 20 Oct 2025 01:51:55 +0530 Subject: [PATCH 1/7] Add multiplier lock functionality with double-click/hold gestures --- lib/widget/x01/number_field_select.dart | 49 +++++++++++++++++++++---- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/lib/widget/x01/number_field_select.dart b/lib/widget/x01/number_field_select.dart index 03f0a79..e9a7953 100644 --- a/lib/widget/x01/number_field_select.dart +++ b/lib/widget/x01/number_field_select.dart @@ -12,6 +12,7 @@ class FieldSelect extends StatefulWidget { class _FieldSelectState extends State { HitMultiplier hitMultiplier = HitMultiplier.single; + bool isMultiplierLocked = false; @override Widget build(BuildContext context) { @@ -64,6 +65,15 @@ class _FieldSelectState extends State { }); } + void toggleMultiplierLock(HitMultiplier hm) { + setState(() { + isMultiplierLocked = !isMultiplierLocked; + if (isMultiplierLocked) { + hitMultiplier = hm; + } + }); + } + return Container( padding: const EdgeInsets.symmetric(horizontal: 15.0), alignment: Alignment.center, @@ -77,20 +87,26 @@ class _FieldSelectState extends State { _MultiplierButton( style: buttonStyle, onPressed: setHitMultiplier, + onDoubleTap: toggleMultiplierLock, hitMultiplier: HitMultiplier.single, current: hitMultiplier, + isLocked: isMultiplierLocked && hitMultiplier == HitMultiplier.single, ), _MultiplierButton( style: buttonStyle, onPressed: setHitMultiplier, + onDoubleTap: toggleMultiplierLock, hitMultiplier: HitMultiplier.double, current: hitMultiplier, + isLocked: isMultiplierLocked && hitMultiplier == HitMultiplier.double, ), _MultiplierButton( style: buttonStyle, onPressed: setHitMultiplier, + onDoubleTap: toggleMultiplierLock, hitMultiplier: HitMultiplier.triple, current: hitMultiplier, + isLocked: isMultiplierLocked && hitMultiplier == HitMultiplier.triple, ), ], ), @@ -105,7 +121,9 @@ class _FieldSelectState extends State { onPressed: (hit) { widget.onSelect(hit); setState(() { - hitMultiplier = HitMultiplier.single; + if (!isMultiplierLocked) { + hitMultiplier = HitMultiplier.single; + } }); }, hitMult: hitMultiplier, @@ -121,15 +139,19 @@ class _FieldSelectState extends State { class _MultiplierButton extends StatelessWidget { final Function(HitMultiplier) onPressed; + final Function(HitMultiplier) onDoubleTap; final HitMultiplier hitMultiplier; final HitMultiplier current; final ButtonStyle style; + final bool isLocked; const _MultiplierButton( {required this.style, required this.onPressed, + required this.onDoubleTap, required this.hitMultiplier, - required this.current}); + required this.current, + this.isLocked = false}); @override Widget build(BuildContext context) { @@ -138,11 +160,24 @@ class _MultiplierButton extends StatelessWidget { height: 50, margin: const EdgeInsets.all(2.5), padding: EdgeInsets.zero, - child: ElevatedButton( - onPressed: - current == hitMultiplier ? null : () => onPressed(hitMultiplier), - style: style, - child: Text(hitMultiplier.text), + child: GestureDetector( + onDoubleTap: () => onDoubleTap(hitMultiplier), + onLongPress: () => onDoubleTap(hitMultiplier), + child: ElevatedButton( + onPressed: + current == hitMultiplier && !isLocked ? null : () => onPressed(hitMultiplier), + style: style.copyWith( + backgroundColor: isLocked + ? WidgetStateProperty.all(Colors.orange) + : style.backgroundColor, + ), + child: Text( + hitMultiplier.text, + style: TextStyle( + fontWeight: isLocked ? FontWeight.bold : FontWeight.normal, + ), + ), + ), ), )); } From 4145101a1542db4343e40f781159cfeec61e4d2a Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 21 Oct 2025 14:21:06 +0530 Subject: [PATCH 2/7] Added isMultiplierLocked state and toggleMultiplierLock handler and handle single tap vs double tap functionality --- lib/widget/x01/number_field_select.dart | 90 +++++++++++++++++++++---- 1 file changed, 78 insertions(+), 12 deletions(-) diff --git a/lib/widget/x01/number_field_select.dart b/lib/widget/x01/number_field_select.dart index e9a7953..072f947 100644 --- a/lib/widget/x01/number_field_select.dart +++ b/lib/widget/x01/number_field_select.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:dart_dart/logic/constant/fields.dart'; import 'package:flutter/material.dart'; @@ -160,26 +161,91 @@ class _MultiplierButton extends StatelessWidget { height: 50, margin: const EdgeInsets.all(2.5), padding: EdgeInsets.zero, - child: GestureDetector( + child: _MultiplierButtonWithGestures( + onPressed: () => onPressed(hitMultiplier), onDoubleTap: () => onDoubleTap(hitMultiplier), onLongPress: () => onDoubleTap(hitMultiplier), - child: ElevatedButton( - onPressed: - current == hitMultiplier && !isLocked ? null : () => onPressed(hitMultiplier), - style: style.copyWith( - backgroundColor: isLocked - ? WidgetStateProperty.all(Colors.orange) - : style.backgroundColor, - ), + style: style, + text: hitMultiplier.text, + isLocked: isLocked, + ), + )); + } +} + +class _MultiplierButtonWithGestures extends StatefulWidget { + final VoidCallback onPressed; + final VoidCallback onDoubleTap; + final VoidCallback onLongPress; + final ButtonStyle style; + final String text; + final bool isLocked; + + const _MultiplierButtonWithGestures({ + required this.onPressed, + required this.onDoubleTap, + required this.onLongPress, + required this.style, + required this.text, + required this.isLocked, + }); + + @override + State<_MultiplierButtonWithGestures> createState() => _MultiplierButtonWithGesturesState(); +} + +class _MultiplierButtonWithGesturesState extends State<_MultiplierButtonWithGestures> { + Timer? _tapTimer; + int _tapCount = 0; + + @override + void dispose() { + _tapTimer?.cancel(); + super.dispose(); + } + + void _handleTap() { + _tapCount++; + if (_tapCount == 1) { + _tapTimer = Timer(const Duration(milliseconds: 300), () { + if (_tapCount == 1) { + widget.onPressed(); + } + _tapCount = 0; + }); + } else if (_tapCount == 2) { + _tapTimer?.cancel(); + widget.onDoubleTap(); + _tapCount = 0; + } + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: _handleTap, + onLongPress: () { + widget.onLongPress(); + }, + child: Container( + height: 50, + decoration: BoxDecoration( + color: widget.isLocked + ? Colors.orange + : widget.style.backgroundColor?.resolve({}), + borderRadius: BorderRadius.circular(10.0), + ), + child: Center( child: Text( - hitMultiplier.text, + widget.text, style: TextStyle( - fontWeight: isLocked ? FontWeight.bold : FontWeight.normal, + color: widget.style.foregroundColor?.resolve({}), + fontWeight: widget.isLocked ? FontWeight.bold : FontWeight.normal, ), ), ), ), - )); + ); } } From 928dc2273cc09465f662ef17b76e96e25ec23f2d Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 21 Oct 2025 16:37:22 +0530 Subject: [PATCH 3/7] add await period for gesture detection --- test/widget/x01/main_test.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/widget/x01/main_test.dart b/test/widget/x01/main_test.dart index 4c7471d..ce806c8 100644 --- a/test/widget/x01/main_test.dart +++ b/test/widget/x01/main_test.dart @@ -14,6 +14,10 @@ Future press(WidgetTester tester, HitNumber number, HitMultiplier multipli final multButton = find.text(hit.multiplier.text); await tester.tap(multButton); await tester.pumpAndSettle(); + + // Wait for the timer to complete (for single tap detection) + await tester.pump(const Duration(milliseconds: 500)); + await tester.pumpAndSettle(); final buttons = find.byType(HitButton).evaluate().map((e) => e.widget as HitButton); final button = buttons.firstWhere((b) => b.hit == hit); From 0ef3a399cf8b2736b16bc2f4746dedda679ba9cb Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 22 Oct 2025 17:26:04 +0530 Subject: [PATCH 4/7] added tests for change code --- test/widget/x01/main_test.dart | 75 ++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/test/widget/x01/main_test.dart b/test/widget/x01/main_test.dart index ce806c8..67b1510 100644 --- a/test/widget/x01/main_test.dart +++ b/test/widget/x01/main_test.dart @@ -139,4 +139,79 @@ void main() { expect(find.text('140.3'), findsAtLeast(2)); }); }); + + group('Multiplier Lock Tests', () { + testWidgets('Single tap changes multiplier', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: FieldSelect(onSelect: (hit) {}), + ), + ), + ); + + await tester.tap(find.text('x3')); + await tester.pumpAndSettle(); + await tester.pump(const Duration(milliseconds: 500)); + await tester.pumpAndSettle(); + + final hitButtons = find.byType(HitButton).evaluate().map((e) => e.widget as HitButton); + final tripleButtons = hitButtons.where((b) => b.hitMult == HitMultiplier.triple); + expect(tripleButtons.length, greaterThan(0)); + }); + + testWidgets('Double tap locks multiplier', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: FieldSelect(onSelect: (hit) {}), + ), + ), + ); + + await tester.tap(find.text('x3')); + await tester.pumpAndSettle(); + await tester.tap(find.text('x3')); + await tester.pumpAndSettle(); + + expect(find.text('x3'), findsOneWidget); + }); + + testWidgets('Long press locks multiplier', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: FieldSelect(onSelect: (hit) {}), + ), + ), + ); + + await tester.longPress(find.text('x3')); + await tester.pumpAndSettle(); + + expect(find.text('x3'), findsOneWidget); + }); + + testWidgets('Locked multiplier persists', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: FieldSelect(onSelect: (hit) {}), + ), + ), + ); + + await tester.longPress(find.text('x3')); + await tester.pumpAndSettle(); + + final hitButtons = find.byType(HitButton).evaluate().map((e) => e.widget as HitButton); + final twentyButton = hitButtons.firstWhere((b) => b.hitNum == HitNumber.twenty); + await tester.tap(find.byWidget(twentyButton)); + await tester.pumpAndSettle(); + + final updatedHitButtons = find.byType(HitButton).evaluate().map((e) => e.widget as HitButton); + final tripleButtons = updatedHitButtons.where((b) => b.hitMult == HitMultiplier.triple); + expect(tripleButtons.length, greaterThan(0)); + }); + }); } From bfdb7e1bb7ae702beab2ecfa52386f96a3dd8eca Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 25 Oct 2025 00:02:38 +0530 Subject: [PATCH 5/7] persist orage color on locked buttons --- lib/widget/x01/number_field_select.dart | 29 +++++++++++++++---------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/lib/widget/x01/number_field_select.dart b/lib/widget/x01/number_field_select.dart index 072f947..7a96afb 100644 --- a/lib/widget/x01/number_field_select.dart +++ b/lib/widget/x01/number_field_select.dart @@ -14,6 +14,7 @@ class FieldSelect extends StatefulWidget { class _FieldSelectState extends State { HitMultiplier hitMultiplier = HitMultiplier.single; bool isMultiplierLocked = false; + HitMultiplier? lockedMultiplier; @override Widget build(BuildContext context) { @@ -60,17 +61,23 @@ class _FieldSelectState extends State { [HitNumber.bull, HitNumber.miss], ]; - void setHitMultiplier(HitMultiplier hm) { - setState(() { - hitMultiplier = hm; - }); - } + void setHitMultiplier(HitMultiplier hm) { + setState(() { + hitMultiplier = hm; + }); + } void toggleMultiplierLock(HitMultiplier hm) { setState(() { - isMultiplierLocked = !isMultiplierLocked; - if (isMultiplierLocked) { + if (isMultiplierLocked && lockedMultiplier == hm) { + // Unlock if tapping the same locked multiplier + isMultiplierLocked = false; + lockedMultiplier = null; + } else { + // Lock the new multiplier + isMultiplierLocked = true; hitMultiplier = hm; + lockedMultiplier = hm; } }); } @@ -91,7 +98,7 @@ class _FieldSelectState extends State { onDoubleTap: toggleMultiplierLock, hitMultiplier: HitMultiplier.single, current: hitMultiplier, - isLocked: isMultiplierLocked && hitMultiplier == HitMultiplier.single, + isLocked: isMultiplierLocked && lockedMultiplier == HitMultiplier.single, ), _MultiplierButton( style: buttonStyle, @@ -99,7 +106,7 @@ class _FieldSelectState extends State { onDoubleTap: toggleMultiplierLock, hitMultiplier: HitMultiplier.double, current: hitMultiplier, - isLocked: isMultiplierLocked && hitMultiplier == HitMultiplier.double, + isLocked: isMultiplierLocked && lockedMultiplier == HitMultiplier.double, ), _MultiplierButton( style: buttonStyle, @@ -107,7 +114,7 @@ class _FieldSelectState extends State { onDoubleTap: toggleMultiplierLock, hitMultiplier: HitMultiplier.triple, current: hitMultiplier, - isLocked: isMultiplierLocked && hitMultiplier == HitMultiplier.triple, + isLocked: isMultiplierLocked && lockedMultiplier == HitMultiplier.triple, ), ], ), @@ -207,7 +214,7 @@ class _MultiplierButtonWithGesturesState extends State<_MultiplierButtonWithGest void _handleTap() { _tapCount++; if (_tapCount == 1) { - _tapTimer = Timer(const Duration(milliseconds: 300), () { + _tapTimer = Timer(const Duration(milliseconds: 150), () { if (_tapCount == 1) { widget.onPressed(); } From 7f43d99632e69593707e9300a9a5f85d59761626 Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 25 Oct 2025 00:06:03 +0530 Subject: [PATCH 6/7] fix double tap --- lib/widget/x01/number_field_select.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/widget/x01/number_field_select.dart b/lib/widget/x01/number_field_select.dart index 7a96afb..4436a88 100644 --- a/lib/widget/x01/number_field_select.dart +++ b/lib/widget/x01/number_field_select.dart @@ -70,11 +70,9 @@ class _FieldSelectState extends State { void toggleMultiplierLock(HitMultiplier hm) { setState(() { if (isMultiplierLocked && lockedMultiplier == hm) { - // Unlock if tapping the same locked multiplier isMultiplierLocked = false; lockedMultiplier = null; } else { - // Lock the new multiplier isMultiplierLocked = true; hitMultiplier = hm; lockedMultiplier = hm; @@ -214,7 +212,7 @@ class _MultiplierButtonWithGesturesState extends State<_MultiplierButtonWithGest void _handleTap() { _tapCount++; if (_tapCount == 1) { - _tapTimer = Timer(const Duration(milliseconds: 150), () { + _tapTimer = Timer(const Duration(milliseconds: 200), () { if (_tapCount == 1) { widget.onPressed(); } @@ -231,6 +229,9 @@ class _MultiplierButtonWithGesturesState extends State<_MultiplierButtonWithGest Widget build(BuildContext context) { return GestureDetector( onTap: _handleTap, + onDoubleTap: () { + widget.onDoubleTap(); + }, onLongPress: () { widget.onLongPress(); }, From 859f1f8c01c1ab30c2312e0312b45e7b0d1e2feb Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 25 Oct 2025 23:39:03 +0530 Subject: [PATCH 7/7] fix double tap lock persistance --- lib/widget/x01/number_field_select.dart | 10 +++-- test/widget/x01/main_test.dart | 55 +++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/lib/widget/x01/number_field_select.dart b/lib/widget/x01/number_field_select.dart index 4436a88..67341d3 100644 --- a/lib/widget/x01/number_field_select.dart +++ b/lib/widget/x01/number_field_select.dart @@ -63,16 +63,21 @@ class _FieldSelectState extends State { void setHitMultiplier(HitMultiplier hm) { setState(() { - hitMultiplier = hm; + if (!isMultiplierLocked) { + hitMultiplier = hm; + } }); } void toggleMultiplierLock(HitMultiplier hm) { setState(() { - if (isMultiplierLocked && lockedMultiplier == hm) { + if (isMultiplierLocked) { + // If already locked, unlock and switch to new multiplier isMultiplierLocked = false; lockedMultiplier = null; + hitMultiplier = hm; } else { + // If not locked, lock the new multiplier isMultiplierLocked = true; hitMultiplier = hm; lockedMultiplier = hm; @@ -220,7 +225,6 @@ class _MultiplierButtonWithGesturesState extends State<_MultiplierButtonWithGest }); } else if (_tapCount == 2) { _tapTimer?.cancel(); - widget.onDoubleTap(); _tapCount = 0; } } diff --git a/test/widget/x01/main_test.dart b/test/widget/x01/main_test.dart index 67b1510..adaae0d 100644 --- a/test/widget/x01/main_test.dart +++ b/test/widget/x01/main_test.dart @@ -213,5 +213,60 @@ void main() { final tripleButtons = updatedHitButtons.where((b) => b.hitMult == HitMultiplier.triple); expect(tripleButtons.length, greaterThan(0)); }); + + testWidgets('Switching multiplier while locked unlocks and switches', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: FieldSelect(onSelect: (hit) {}), + ), + ), + ); + await tester.longPress(find.text('x3')); + await tester.pumpAndSettle(); + await tester.tap(find.text('x1')); + await tester.pumpAndSettle(); + await tester.tap(find.text('x1')); + await tester.pumpAndSettle(); + final hitButtons = find.byType(HitButton).evaluate().map((e) => e.widget as HitButton); + final singleButtons = hitButtons.where((b) => b.hitMult == HitMultiplier.single); + expect(singleButtons.length, greaterThan(0)); + }); + testWidgets('Single tap while locked does nothing', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: FieldSelect(onSelect: (hit) {}), + ), + ), + ); + await tester.longPress(find.text('x3')); + await tester.pumpAndSettle(); + await tester.tap(find.text('x1')); + await tester.pumpAndSettle(); + await tester.pump(const Duration(milliseconds: 500)); + await tester.pumpAndSettle(); + final hitButtons = find.byType(HitButton).evaluate().map((e) => e.widget as HitButton); + final tripleButtons = hitButtons.where((b) => b.hitMult == HitMultiplier.triple); + expect(tripleButtons.length, greaterThan(0)); + }); + testWidgets('setHitMultiplier while locked is ignored', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: FieldSelect(onSelect: (hit) {}), + ), + ), + ); + await tester.longPress(find.text('x3')); + await tester.pumpAndSettle(); + await tester.tap(find.text('x1')); + await tester.pumpAndSettle(); + await tester.pump(const Duration(milliseconds: 500)); + await tester.pumpAndSettle(); + final hitButtons = find.byType(HitButton).evaluate().map((e) => e.widget as HitButton); + final tripleButtons = hitButtons.where((b) => b.hitMult == HitMultiplier.triple); + expect(tripleButtons.length, greaterThan(0)); + }); }); }