Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 121 additions & 8 deletions lib/widget/x01/number_field_select.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'dart:async';
import 'package:dart_dart/logic/constant/fields.dart';
import 'package:flutter/material.dart';

Expand All @@ -12,6 +13,8 @@ class FieldSelect extends StatefulWidget {

class _FieldSelectState extends State<FieldSelect> {
HitMultiplier hitMultiplier = HitMultiplier.single;
bool isMultiplierLocked = false;
HitMultiplier? lockedMultiplier;

@override
Widget build(BuildContext context) {
Expand Down Expand Up @@ -58,9 +61,27 @@ class _FieldSelectState extends State<FieldSelect> {
[HitNumber.bull, HitNumber.miss],
];

void setHitMultiplier(HitMultiplier hm) {
setState(() {
void setHitMultiplier(HitMultiplier hm) {
setState(() {
if (!isMultiplierLocked) {
hitMultiplier = hm;
}
});
}

void toggleMultiplierLock(HitMultiplier hm) {
setState(() {
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;
}
});
}

Expand All @@ -77,20 +98,26 @@ class _FieldSelectState extends State<FieldSelect> {
_MultiplierButton(
style: buttonStyle,
onPressed: setHitMultiplier,
onDoubleTap: toggleMultiplierLock,
hitMultiplier: HitMultiplier.single,
current: hitMultiplier,
isLocked: isMultiplierLocked && lockedMultiplier == HitMultiplier.single,
),
_MultiplierButton(
style: buttonStyle,
onPressed: setHitMultiplier,
onDoubleTap: toggleMultiplierLock,
hitMultiplier: HitMultiplier.double,
current: hitMultiplier,
isLocked: isMultiplierLocked && lockedMultiplier == HitMultiplier.double,
),
_MultiplierButton(
style: buttonStyle,
onPressed: setHitMultiplier,
onDoubleTap: toggleMultiplierLock,
hitMultiplier: HitMultiplier.triple,
current: hitMultiplier,
isLocked: isMultiplierLocked && lockedMultiplier == HitMultiplier.triple,
),
],
),
Expand All @@ -105,7 +132,9 @@ class _FieldSelectState extends State<FieldSelect> {
onPressed: (hit) {
widget.onSelect(hit);
setState(() {
hitMultiplier = HitMultiplier.single;
if (!isMultiplierLocked) {
hitMultiplier = HitMultiplier.single;
}
});
},
hitMult: hitMultiplier,
Expand All @@ -121,15 +150,19 @@ class _FieldSelectState extends State<FieldSelect> {

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) {
Expand All @@ -138,16 +171,96 @@ class _MultiplierButton extends StatelessWidget {
height: 50,
margin: const EdgeInsets.all(2.5),
padding: EdgeInsets.zero,
child: ElevatedButton(
onPressed:
current == hitMultiplier ? null : () => onPressed(hitMultiplier),
child: _MultiplierButtonWithGestures(
onPressed: () => onPressed(hitMultiplier),
onDoubleTap: () => onDoubleTap(hitMultiplier),
onLongPress: () => onDoubleTap(hitMultiplier),
style: style,
child: Text(hitMultiplier.text),
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: 200), () {
if (_tapCount == 1) {
widget.onPressed();
}
_tapCount = 0;
});
} else if (_tapCount == 2) {
_tapTimer?.cancel();
_tapCount = 0;
}
}

@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _handleTap,
onDoubleTap: () {
widget.onDoubleTap();
},
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(
widget.text,
style: TextStyle(
color: widget.style.foregroundColor?.resolve({}),
fontWeight: widget.isLocked ? FontWeight.bold : FontWeight.normal,
),
),
),
),
);
}
}

class HitButton extends StatelessWidget {
final Function(Hit) onPressed;
final HitNumber hitNum;
Expand Down
134 changes: 134 additions & 0 deletions test/widget/x01/main_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ Future<void> 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);
Expand Down Expand Up @@ -135,4 +139,134 @@ 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));
});

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));
});
});
}