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
Signed-off-by: cls_宁波本机 <908705107@qq.com>
177 lines
5.5 KiB
Dart
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);
|
|
}
|
|
}
|