Files
buzz/mobile/lib/features/pairing/pairing_qr_scanner.dart
T
cls 9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
feat: import Chinese-localized Buzz source snapshot
Signed-off-by: cls_宁波本机 <908705107@qq.com>
2026-08-13 18:34:25 +08:00

177 lines
5.5 KiB
Dart

import 'dart:async';
import 'dart:math' as math;
import 'dart:ui' show lerpDouble;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import '../../shared/l10n/l10n.dart';
import '../../shared/theme/theme.dart';
part 'pairing_qr_scanner/dynamic_island_portal.dart';
part 'pairing_qr_scanner/fallback_scanner.dart';
part 'pairing_qr_scanner/scanner_camera.dart';
const _qrScannerPlatformChannel = MethodChannel('buzz/qr_scanner');
/// Returns whether the current iPhone should use the Dynamic Island scanner.
///
/// Android, iPad, and iPhones without a Dynamic Island always use the standard
/// full-screen scanner.
Future<bool> usesDynamicIslandQrScannerPortal() async {
if (defaultTargetPlatform != TargetPlatform.iOS) {
return false;
}
try {
return await _qrScannerPlatformChannel.invokeMethod<bool>(
'usesDynamicIslandQrScannerPortal',
) ??
false;
} on PlatformException {
return false;
} on MissingPluginException {
return false;
}
}
/// Opens the Dynamic Island QR scanner portal.
///
/// Callers determine device support with
/// [usesDynamicIslandQrScannerPortal]. Standard devices reveal the camera
/// behind their existing app surface with [FallbackPairingQrScanner].
Future<String?> showDynamicIslandPairingQrScanner(BuildContext context) {
return Navigator.of(context).push<String>(
PageRouteBuilder<String>(
opaque: false,
barrierColor: Colors.transparent,
barrierDismissible: false,
transitionDuration: Duration.zero,
reverseTransitionDuration: Duration.zero,
pageBuilder: (_, _, _) => const _DynamicIslandQrScannerPortal(),
),
);
}
Future<void> _setDynamicIslandScannerStatusBarHidden(bool hidden) async {
try {
await _qrScannerPlatformChannel.invokeMethod<void>(
'setDynamicIslandScannerStatusBarHidden',
hidden,
);
} on PlatformException {
// The scanner still works if the native status-bar bridge is unavailable.
} on MissingPluginException {
// Widget tests and non-iOS embedders do not register the bridge.
}
}
Future<void> _performDynamicIslandQrScanSuccessHaptic() async {
try {
await _qrScannerPlatformChannel.invokeMethod<void>(
'performDynamicIslandQrScanSuccessHaptic',
);
} on PlatformException {
// Scanning should still complete if haptics are unavailable.
} on MissingPluginException {
// Widget tests and non-iOS embedders do not register the bridge.
}
}
String? _firstScannedValue(BarcodeCapture capture) {
for (final barcode in capture.barcodes) {
final value = barcode.rawValue;
if (value != null && value.isNotEmpty) {
return value;
}
}
return null;
}
/// Geometry for the iPhone Dynamic Island scanner portal.
///
/// The collapsed and expanded frames share one top edge. This makes the camera
/// grow down from the hardware cutout instead of scaling around the center of
/// its final square.
@visibleForTesting
class DynamicIslandQrScannerGeometry {
/// Creates geometry for a viewport and its top safe-area inset.
const DynamicIslandQrScannerGeometry({
required this.viewport,
required this.safeAreaTop,
});
/// The full logical-pixel size available to the portal route.
final Size viewport;
/// The top safe-area inset reported by iOS.
final double safeAreaTop;
static const _collapsedWidth = 120.0;
static const _collapsedHeight = 36.0;
static const _fallbackTop = 11.0;
static const _referenceSafeAreaTop = 59.0;
static const _edgeMargin = 15.0;
static const _expandedCornerRadius = 40.0;
static const _scannerFadeStart = 0.18;
static const _introLabelFadeEnd = 0.42;
/// The physical-island-aligned top edge used throughout the morph.
double get top =>
_fallbackTop + math.max(0, safeAreaTop - _referenceSafeAreaTop);
/// The portal frame before the opening animation starts.
Rect get collapsedFrame => Rect.fromLTWH(
(viewport.width - _collapsedWidth) / 2,
top,
_collapsedWidth,
_collapsedHeight,
);
/// The square camera frame after the opening animation completes.
Rect get expandedFrame {
final maxHeight = math.max(
_collapsedHeight,
viewport.height - top - _edgeMargin,
);
final size = math.min(viewport.width - (_edgeMargin * 2), maxHeight);
return Rect.fromLTWH(_edgeMargin, top, size, size);
}
/// Returns the top-anchored portal frame at [progress].
Rect frameAt(double progress) {
final t = progress.clamp(0.0, 1.0);
final start = collapsedFrame;
final end = expandedFrame;
return Rect.fromLTRB(
lerpDouble(start.left, end.left, t)!,
top,
lerpDouble(start.right, end.right, t)!,
lerpDouble(start.bottom, end.bottom, t)!,
);
}
/// Returns the portal corner radius at [progress].
double cornerRadiusAt(double progress) {
final t = progress.clamp(0.0, 1.0);
return lerpDouble(_collapsedHeight / 2, _expandedCornerRadius, t)!;
}
/// Returns the camera opacity after its delayed entrance.
double scannerOpacityAt(double progress) {
return ((progress - _scannerFadeStart) / (1 - _scannerFadeStart)).clamp(
0.0,
1.0,
);
}
/// Returns the opacity of the compact prompt inside the island pill.
double introLabelOpacityAt(double progress) {
return (1 - (progress / _introLabelFadeEnd)).clamp(0.0, 1.0);
}
}