Require a two-finger double tap for connection options

Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
Signed-off-by: kenny lopez <klopez4212@gmail.com>
This commit is contained in:
kenny lopez
2026-10-26 19:15:58 +00:00
parent 86e250a541
commit f83702333d
4 changed files with 106 additions and 4 deletions

View File

@@ -14,6 +14,7 @@ import 'package:flutter/foundation.dart';
import '../../shared/widgets/frosted_app_bar.dart';
import 'package:flutter/services.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:buzz/shared/theme/buzz_icons.dart';
@@ -36,6 +37,7 @@ part 'pairing_page/onboarding_background.dart';
part 'pairing_page/onboarding_colors.dart';
part 'pairing_page/onboarding_glass_button.dart';
part 'pairing_page/pairing_welcome_view.dart';
part 'pairing_page/two_finger_double_tap.dart';
part 'pairing_page/community_link_form.dart';
part 'pairing_page/onboarding_connection_view.dart';
part 'pairing_page/sas_verification_view.dart';

View File

@@ -345,10 +345,7 @@ class _PairingWelcomeView extends HookWidget {
child: Column(
children: [
Expanded(
// Keep double-tap recognition off the action buttons so
// their single taps do not wait for the double-tap timeout.
child: GestureDetector(
behavior: HitTestBehavior.opaque,
child: _TwoFingerDoubleTapSurface(
onDoubleTap: canRevealOptions ? revealOptions : null,
child: Center(
child: Column(

View File

@@ -0,0 +1,83 @@
part of '../pairing_page.dart';
class _TwoFingerDoubleTapSurface extends HookWidget {
const _TwoFingerDoubleTapSurface({required this.child, this.onDoubleTap});
final Widget child;
final VoidCallback? onDoubleTap;
@override
Widget build(BuildContext context) {
final taps = useMemoized(_TwoFingerTapTracker.new, [onDoubleTap != null]);
return Listener(
behavior: HitTestBehavior.opaque,
onPointerDown: onDoubleTap == null ? null : taps.down,
onPointerMove: onDoubleTap == null ? null : taps.move,
onPointerCancel: onDoubleTap == null ? null : (_) => taps.cancel(),
onPointerUp: onDoubleTap == null
? null
: (event) => taps.up(event, onDoubleTap!),
child: child,
);
}
}
// Observe two short, stationary two-finger chords without competing with taps
// or scrolling in Flutter's gesture arena.
class _TwoFingerTapTracker {
final _starts = <int, Offset>{};
final _down = <int>{};
Duration _started = Duration.zero;
Duration? _previousEnd;
Offset? _previousCenter;
bool _valid = true;
void down(PointerDownEvent event) {
if (_down.isEmpty) {
_starts.clear();
_started = event.timeStamp;
_valid = true;
}
_starts[event.pointer] = event.position;
_down.add(event.pointer);
if (_starts.length > 2) _valid = false;
}
void move(PointerMoveEvent event) {
final start = _starts[event.pointer];
if (start != null && (event.position - start).distance > kTouchSlop) {
_valid = false;
}
}
void cancel() {
_down.clear();
_starts.clear();
_previousEnd = null;
_previousCenter = null;
_valid = false;
}
void up(PointerUpEvent event, VoidCallback onDoubleTap) {
if (!_down.remove(event.pointer) || _down.isNotEmpty) return;
if (!_valid ||
_starts.length != 2 ||
event.timeStamp - _started > kDoubleTapTimeout) {
cancel();
return;
}
final center = (_starts.values.first + _starts.values.last) / 2;
final gap = _previousEnd == null ? null : _started - _previousEnd!;
final isDouble =
gap != null &&
gap >= kDoubleTapMinTime &&
gap <= kDoubleTapTimeout &&
(center - _previousCenter!).distance <= kDoubleTapSlop;
_previousEnd = event.timeStamp;
_previousCenter = center;
if (isDouble) {
cancel();
onDoubleTap();
}
}
}

View File

@@ -298,7 +298,27 @@ void main() {
}
Future<void> revealOptions(WidgetTester tester) async {
// Ordinary double taps must not expose the hidden connection options.
await tester.tapAt(const Offset(24, 120));
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(const Offset(24, 120));
await tester.pumpAndSettle();
expect(find.text('Use a pairing code').hitTestable(), findsNothing);
for (var tap = 0; tap < 2; tap++) {
final first = await tester.createGesture(pointer: 11);
final second = await tester.createGesture(pointer: 12);
await first.down(
const Offset(24, 120),
timeStamp: Duration(milliseconds: tap * 70),
);
await second.down(
const Offset(70, 120),
timeStamp: Duration(milliseconds: tap * 70),
);
await tester.pump(const Duration(milliseconds: 20));
await first.up(timeStamp: Duration(milliseconds: tap * 70 + 20));
await second.up(timeStamp: Duration(milliseconds: tap * 70 + 20));
await tester.pump(const Duration(milliseconds: 50));
}
}