Se agrego el sdk para el pda de honeywell

This commit is contained in:
Alejandro Lara
2026-06-19 08:57:58 -06:00
parent 50fe9a9d36
commit d6db216d2d
21 changed files with 1038 additions and 77 deletions
+23 -1
View File
@@ -1,20 +1,42 @@
import 'package:flutter/material.dart';
import 'package:qrscanner/screens/home_screen.dart';
import 'package:qrscanner/screens/login_screen.dart';
import 'package:qrscanner/services/scan_navigation_service.dart';
import 'package:qrscanner/theme/app_theme.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
ScanNavigationService.instance.initialize();
}
@override
void dispose() {
ScanNavigationService.instance.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'QR Scanner',
debugShowCheckedModeBanner: false,
theme: AppTheme.darkTheme,
navigatorKey: ScanNavigationService.instance.navigatorKey,
navigatorObservers: [homeRouteObserver],
home: const LoginScreen(),
);
}
+68 -6
View File
@@ -3,9 +3,14 @@ import 'package:qrscanner/models/auth_session.dart';
import 'package:qrscanner/screens/login_screen.dart';
import 'package:qrscanner/screens/scanner_screen.dart';
import 'package:qrscanner/services/auth_service.dart';
import 'package:qrscanner/services/hardware_scanner_service.dart';
import 'package:qrscanner/services/scan_launch_service.dart';
import 'package:qrscanner/theme/app_theme.dart';
class HomeScreen extends StatelessWidget {
final RouteObserver<ModalRoute<void>> homeRouteObserver =
RouteObserver<ModalRoute<void>>();
class HomeScreen extends StatefulWidget {
const HomeScreen({
super.key,
required this.session,
@@ -13,7 +18,29 @@ class HomeScreen extends StatelessWidget {
final AuthSession session;
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
bool _hardwareAvailable = false;
@override
void initState() {
super.initState();
_checkHardware();
}
Future<void> _checkHardware() async {
final available = await ScanLaunchService.instance.isSupported();
if (mounted) {
setState(() => _hardwareAvailable = available);
}
}
Future<void> _logout(BuildContext context) async {
await HardwareScannerService.instance.ensureStopped();
await ScanLaunchService.instance.releaseScanner();
await AuthService().clearSession();
if (!context.mounted) return;
@@ -30,11 +57,12 @@ class HomeScreen extends StatelessWidget {
);
}
void _openScanner(BuildContext context) {
Navigator.of(context).push(
Future<void> _openScanner(BuildContext context) async {
await Navigator.of(context).push(
PageRouteBuilder<void>(
settings: const RouteSettings(name: '/scanner'),
pageBuilder: (context, animation, secondaryAnimation) =>
ScannerScreen(session: session),
ScannerScreen(session: widget.session),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
@@ -57,7 +85,7 @@ class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final expiresHours = (session.expiresIn / 3600).round();
final expiresHours = (widget.session.expiresIn / 3600).round();
return Scaffold(
body: Stack(
@@ -140,7 +168,7 @@ class HomeScreen extends StatelessWidget {
),
const SizedBox(height: 28),
Text(
'Hola, ${session.username}',
'Hola, ${widget.session.username}',
style:
Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
@@ -175,6 +203,40 @@ class HomeScreen extends StatelessWidget {
),
),
),
if (_hardwareAvailable) ...[
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 8,
),
decoration: BoxDecoration(
color: AppTheme.primary.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: AppTheme.primary.withValues(alpha: 0.25),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.barcode_reader,
size: 16,
color: AppTheme.primary.withValues(alpha: 0.9),
),
const SizedBox(width: 8),
Text(
'Escáner activo · abre la app al escanear',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.7),
fontSize: 12,
),
),
],
),
),
],
const SizedBox(height: 48),
_ScanButton(onPressed: () => _openScanner(context)),
],
+5 -16
View File
@@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
import 'package:qrscanner/config/api_config.dart';
import 'package:qrscanner/models/auth_session.dart';
import 'package:qrscanner/screens/home_screen.dart';
import 'package:qrscanner/services/auth_service.dart';
import 'package:qrscanner/services/scan_launch_service.dart';
import 'package:qrscanner/services/scan_navigation_service.dart';
import 'package:qrscanner/theme/app_theme.dart';
class LoginScreen extends StatefulWidget {
@@ -47,6 +48,7 @@ class _LoginScreenState extends State<LoginScreen>
curve: Curves.easeOutCubic,
));
_animationController.forward();
ScanLaunchService.instance.claimScanner();
_checkStoredSession();
}
@@ -54,7 +56,7 @@ class _LoginScreenState extends State<LoginScreen>
final session = await _authService.loadStoredSession();
if (!mounted || session == null) return;
_goToHome(session);
await ScanNavigationService.instance.navigateToHome(session);
}
@override
@@ -83,7 +85,7 @@ class _LoginScreenState extends State<LoginScreen>
if (!mounted) return;
if (response.success && response.accessToken != null) {
_goToHome(
await ScanNavigationService.instance.navigateToHome(
AuthSession.fromLoginResponse(
response.copyWith(
username: response.username ?? _usernameController.text.trim(),
@@ -109,19 +111,6 @@ class _LoginScreenState extends State<LoginScreen>
}
}
void _goToHome(AuthSession session) {
Navigator.of(context).pushReplacement(
PageRouteBuilder<void>(
pageBuilder: (context, animation, secondaryAnimation) =>
HomeScreen(session: session),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(opacity: animation, child: child);
},
transitionDuration: const Duration(milliseconds: 400),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
+333 -20
View File
@@ -1,25 +1,33 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:qrscanner/models/auth_session.dart';
import 'package:qrscanner/models/pass_code.dart';
import 'package:qrscanner/models/scan_response.dart';
import 'package:qrscanner/services/feedback_service.dart';
import 'package:qrscanner/services/hardware_scanner_service.dart';
import 'package:qrscanner/services/pass_code_service.dart';
import 'package:qrscanner/theme/app_theme.dart';
enum _ScanPhase { scanning, processing, success, warning }
enum _ScannerInput { loading, camera, hardware }
const double _scanWindowSize = 260;
const double _scanWindowRadius = 28;
const Alignment _scanWindowAlignment = Alignment(0, -0.12);
const Duration _resultDisplayDuration = Duration(seconds: 5);
class ScannerScreen extends StatefulWidget {
const ScannerScreen({
super.key,
required this.session,
this.initialCode,
});
final AuthSession session;
final String? initialCode;
@override
State<ScannerScreen> createState() => _ScannerScreenState();
@@ -27,21 +35,21 @@ class ScannerScreen extends StatefulWidget {
class _ScannerScreenState extends State<ScannerScreen>
with TickerProviderStateMixin {
final _scannerController = MobileScannerController(
detectionSpeed: DetectionSpeed.normal,
detectionTimeoutMs: 500,
facing: CameraFacing.back,
formats: const [BarcodeFormat.qrCode],
autoZoom: true,
);
MobileScannerController? _cameraController;
final _hardwareScanner = HardwareScannerService.instance;
final _passCodeService = PassCodeService();
final _feedbackService = FeedbackService();
_ScannerInput _inputMode = _ScannerInput.loading;
bool _hardwareAvailable = false;
bool _isInitialSetup = true;
bool _isSwitchingInput = false;
_ScanPhase _phase = _ScanPhase.scanning;
bool _isHandlingDetection = false;
String? _lastScannedCode;
String? _message;
PassCode? _passCode;
Timer? _resultAutoDismissTimer;
late final AnimationController _scanLineController;
late final AnimationController _resultController;
@@ -76,8 +84,158 @@ class _ScannerScreenState extends State<ScannerScreen>
parent: _resultController,
curve: Curves.easeOutBack,
));
_initializeScannerInput();
}
Future<void> _initializeScannerInput() async {
_hardwareAvailable = await HardwareScannerService.isAvailable();
if (!mounted) return;
if (_hardwareAvailable) {
await _activateHardwareMode(showLoading: true);
} else {
await _activateCameraMode(showLoading: true);
}
final pendingCode = widget.initialCode?.trim();
if (!mounted || pendingCode == null || pendingCode.isEmpty) return;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _processScan(pendingCode);
});
}
MobileScannerController _createCameraController() {
return MobileScannerController(
autoStart: false,
detectionSpeed: DetectionSpeed.normal,
detectionTimeoutMs: 500,
facing: CameraFacing.back,
formats: const [BarcodeFormat.qrCode],
autoZoom: true,
);
}
Future<void> _activateHardwareMode({bool showLoading = false}) async {
if (!mounted) return;
if (showLoading) {
setState(() => _inputMode = _ScannerInput.loading);
}
try {
try {
await _cameraController?.stop();
} catch (_) {}
await _cameraController?.dispose();
_cameraController = null;
_scanLineController.stop();
await Future<void>.delayed(const Duration(milliseconds: 300));
final started = await _hardwareScanner.activate(onScan: _onRawScan);
if (!mounted) return;
if (started) {
setState(() {
_inputMode = _ScannerInput.hardware;
_isInitialSetup = false;
});
return;
}
await _activateCameraMode(
showLoading: showLoading,
fallbackFromHardware: true,
);
} finally {
if (mounted && _inputMode == _ScannerInput.loading) {
setState(() {
_inputMode = _ScannerInput.hardware;
_isInitialSetup = false;
});
}
}
}
Future<void> _activateCameraMode({
bool showLoading = false,
bool fallbackFromHardware = false,
}) async {
if (!mounted) return;
if (showLoading) {
setState(() => _inputMode = _ScannerInput.loading);
}
try {
await _hardwareScanner.pause();
await Future<void>.delayed(const Duration(milliseconds: 300));
try {
await _cameraController?.stop();
} catch (_) {}
await _cameraController?.dispose();
_cameraController = _createCameraController();
if (!_scanLineController.isAnimating) {
_scanLineController.repeat();
}
if (!mounted) return;
setState(() {
_inputMode = _ScannerInput.camera;
_isInitialSetup = false;
});
await Future<void>.delayed(const Duration(milliseconds: 200));
try {
await _cameraController!.start();
} catch (_) {
if (!mounted) return;
if (!fallbackFromHardware) {
await _activateHardwareMode();
}
}
} finally {
if (mounted && _inputMode == _ScannerInput.loading) {
setState(() {
_inputMode = _ScannerInput.camera;
_isInitialSetup = false;
});
}
}
}
Future<void> _toggleScannerInput() async {
if (!_hardwareAvailable ||
_phase != _ScanPhase.scanning ||
_isSwitchingInput ||
_inputMode == _ScannerInput.loading) {
return;
}
setState(() => _isSwitchingInput = true);
try {
if (_inputMode == _ScannerInput.hardware) {
await _activateCameraMode();
} else if (_inputMode == _ScannerInput.camera) {
await _activateHardwareMode();
}
} finally {
if (mounted) {
setState(() => _isSwitchingInput = false);
} else {
_isSwitchingInput = false;
}
}
}
bool get _showsScannerInput =>
_phase == _ScanPhase.scanning ||
_phase == _ScanPhase.processing ||
_phase == _ScanPhase.success ||
_phase == _ScanPhase.warning;
Rect _scanWindowRect(Size size) {
final center = _scanWindowAlignment.alongSize(size);
return Rect.fromCenter(
@@ -87,23 +245,58 @@ class _ScannerScreenState extends State<ScannerScreen>
);
}
void _cancelAutoDismiss() {
_resultAutoDismissTimer?.cancel();
_resultAutoDismissTimer = null;
}
void _scheduleAutoDismiss() {
_cancelAutoDismiss();
_resultAutoDismissTimer = Timer(_resultDisplayDuration, () {
if (!mounted) return;
if (_phase == _ScanPhase.success || _phase == _ScanPhase.warning) {
unawaited(_scanAgain());
}
});
}
@override
void dispose() {
_cancelAutoDismiss();
_scanLineController.dispose();
_resultController.dispose();
_scannerController.dispose();
_cameraController?.dispose();
_hardwareScanner.deactivate();
_feedbackService.dispose();
super.dispose();
}
Future<void> _onDetect(BarcodeCapture capture) async {
if (_phase != _ScanPhase.scanning || _isHandlingDetection) return;
void _onRawScan(String rawValue) {
_processScan(rawValue);
}
Future<void> _onDetect(BarcodeCapture capture) async {
final rawValue = capture.barcodes.firstOrNull?.rawValue?.trim();
if (rawValue == null || rawValue.isEmpty) return;
await _processScan(rawValue);
}
Future<void> _processScan(String rawValue) async {
if (_isHandlingDetection || _phase == _ScanPhase.processing) return;
final code = _extractCode(rawValue);
if (code.isEmpty || code == _lastScannedCode) return;
if (code.isEmpty) return;
final showingResult =
_phase == _ScanPhase.success || _phase == _ScanPhase.warning;
if (showingResult) {
_cancelAutoDismiss();
_resultController.reset();
} else {
if (_phase != _ScanPhase.scanning) return;
if (code == _lastScannedCode) return;
}
_isHandlingDetection = true;
setState(() {
@@ -114,7 +307,9 @@ class _ScannerScreenState extends State<ScannerScreen>
});
_scanLineController.stop();
await _scannerController.stop();
if (_inputMode == _ScannerInput.camera) {
await _cameraController?.stop();
}
await _feedbackService.onCodeDetected();
try {
@@ -166,9 +361,15 @@ class _ScannerScreenState extends State<ScannerScreen>
}
await _resultController.forward();
_scheduleAutoDismiss();
if (_inputMode == _ScannerInput.camera) {
await _cameraController?.start();
}
}
Future<void> _scanAgain() async {
_cancelAutoDismiss();
_resultController.reset();
_isHandlingDetection = false;
setState(() {
@@ -177,8 +378,10 @@ class _ScannerScreenState extends State<ScannerScreen>
_message = null;
_passCode = null;
});
_scanLineController.repeat();
await _scannerController.start();
if (_inputMode == _ScannerInput.camera) {
_scanLineController.repeat();
await _cameraController?.start();
}
}
@override
@@ -203,16 +406,45 @@ class _ScannerScreenState extends State<ScannerScreen>
),
title: const Text('Escanear pase'),
centerTitle: true,
actions: [
if (_hardwareAvailable && _phase == _ScanPhase.scanning)
IconButton(
tooltip: _inputMode == _ScannerInput.hardware
? 'Usar cámara'
: 'Usar escáner Honeywell',
icon: _isSwitchingInput
? const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: Icon(
_inputMode == _ScannerInput.hardware
? Icons.camera_alt_outlined
: Icons.barcode_reader,
),
onPressed: _isSwitchingInput ? null : _toggleScannerInput,
),
],
),
body: Stack(
fit: StackFit.expand,
children: [
if (_phase == _ScanPhase.scanning || _phase == _ScanPhase.processing)
if (_inputMode == _ScannerInput.loading && _isInitialSetup)
const Center(
child: CircularProgressIndicator(color: AppTheme.primary),
),
if (_showsScannerInput &&
_inputMode == _ScannerInput.camera &&
_cameraController != null)
LayoutBuilder(
builder: (context, constraints) {
final scanRect = _scanWindowRect(constraints.biggest);
return MobileScanner(
controller: _scannerController,
controller: _cameraController!,
onDetect: _onDetect,
scanWindow: scanRect,
overlayBuilder: (context, constraints) {
@@ -236,8 +468,8 @@ class _ScannerScreenState extends State<ScannerScreen>
);
},
),
if (_phase != _ScanPhase.scanning && _phase != _ScanPhase.processing)
Container(color: AppTheme.backgroundDark),
if (_showsScannerInput && _inputMode == _ScannerInput.hardware)
_buildHardwareScannerView(),
AnimatedContainer(
duration: const Duration(milliseconds: 450),
curve: Curves.easeOutCubic,
@@ -256,7 +488,7 @@ class _ScannerScreenState extends State<ScannerScreen>
),
),
),
if (_phase == _ScanPhase.scanning)
if (_phase == _ScanPhase.scanning && _inputMode != _ScannerInput.loading)
SafeArea(
child: Align(
alignment: Alignment.bottomCenter,
@@ -452,11 +684,92 @@ class _ScannerScreenState extends State<ScannerScreen>
);
}
Widget _buildHardwareScannerView() {
return Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
AppTheme.backgroundDark,
Color(0xFF0D1B2A),
Color(0xFF12182B),
],
),
),
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 36),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(28),
color: AppTheme.primary.withValues(alpha: 0.12),
border: Border.all(
color: AppTheme.primary.withValues(alpha: 0.35),
),
),
child: const Icon(
Icons.barcode_reader,
size: 56,
color: AppTheme.primary,
),
),
const SizedBox(height: 28),
Text(
'Escáner Honeywell',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Text(
'Presiona el botón lateral del EDA51 para leer el código QR del pase.',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Colors.white.withValues(alpha: 0.6),
height: 1.5,
),
),
const SizedBox(height: 20),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.06),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.white.withValues(alpha: 0.08),
),
),
child: Text(
'Sensor dedicado · Honeywell ScanPal EDA51',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.45),
fontSize: 12,
),
),
),
],
),
),
),
);
}
Widget _buildHint() {
final message = _inputMode == _ScannerInput.hardware
? 'Presiona el botón lateral para escanear'
: 'Apunta la cámara al código QR del pase';
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text(
'Apunta la cámara al código QR del pase',
message,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.65),
+71 -18
View File
@@ -1,43 +1,92 @@
import 'dart:io';
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:vibration/vibration.dart';
class FeedbackService {
FeedbackService() : _player = AudioPlayer() {
_player.setReleaseMode(ReleaseMode.stop);
FeedbackService() {
_configurePlayers();
}
final AudioPlayer _player;
final AudioPlayer _scanPlayer = AudioPlayer();
final AudioPlayer _successPlayer = AudioPlayer();
final AudioPlayer _warningPlayer = AudioPlayer();
static final AudioContext _loudContext = AudioContext(
android: AudioContextAndroid(
isSpeakerphoneOn: true,
audioMode: AndroidAudioMode.normal,
contentType: AndroidContentType.sonification,
usageType: AndroidUsageType.alarm,
audioFocus: AndroidAudioFocus.gain,
),
iOS: AudioContextIOS(
category: AVAudioSessionCategory.playback,
options: {
AVAudioSessionOptions.mixWithOthers,
AVAudioSessionOptions.duckOthers,
},
),
);
Future<void> _configurePlayers() async {
for (final player in [_scanPlayer, _successPlayer, _warningPlayer]) {
await player.setReleaseMode(ReleaseMode.stop);
await player.setPlayerMode(PlayerMode.lowLatency);
await player.setVolume(1.0);
await player.setAudioContext(_loudContext);
}
}
Future<void> onCodeDetected() async {
await Future.wait([
_play('sounds/scan.wav'),
_vibratePattern(const [0, 40]),
_play(_scanPlayer, 'sounds/scan.wav'),
_vibratePattern(const [0, 80]),
HapticFeedback.mediumImpact(),
]);
}
Future<void> onSuccess() async {
await Future.wait([
_play('sounds/success.wav'),
_vibratePattern(const [0, 90, 70, 140]),
_playSuccessSequence(),
_vibratePattern(const [0, 120, 60, 180]),
HapticFeedback.heavyImpact(),
]);
await HapticFeedback.heavyImpact();
}
Future<void> onWarning() async {
await Future.wait([
_play('sounds/warning.wav'),
_vibratePattern(const [0, 120, 80, 120, 80, 120]),
_playWarningSequence(),
_vibratePattern(const [0, 180, 80, 180, 80, 220]),
HapticFeedback.heavyImpact(),
]);
await HapticFeedback.mediumImpact();
}
Future<void> _play(String asset) async {
Future<void> _playSuccessSequence() async {
await _play(_successPlayer, 'sounds/success.wav');
if (!kIsWeb && Platform.isAndroid) {
await Future<void>.delayed(const Duration(milliseconds: 120));
await _play(_successPlayer, 'sounds/success.wav');
}
}
Future<void> _playWarningSequence() async {
for (var i = 0; i < 2; i++) {
await _play(_warningPlayer, 'sounds/warning.wav');
if (i == 0) {
await Future<void>.delayed(const Duration(milliseconds: 140));
}
}
}
Future<void> _play(AudioPlayer player, String asset) async {
try {
await _player.stop();
await _player.play(AssetSource(asset));
await player.stop();
await player.play(AssetSource(asset));
} catch (_) {
// Ignore audio errors on devices without speaker routing.
await SystemSound.play(SystemSoundType.alert);
}
}
@@ -51,7 +100,7 @@ class FeedbackService {
final intensities = pattern
.asMap()
.entries
.map((entry) => entry.key.isOdd ? 180 : 0)
.map((entry) => entry.key.isOdd ? 255 : 0)
.toList();
await Vibration.vibrate(
pattern: pattern,
@@ -62,11 +111,15 @@ class FeedbackService {
await Vibration.vibrate(pattern: pattern);
} catch (_) {
await HapticFeedback.selectionClick();
await HapticFeedback.heavyImpact();
}
}
Future<void> dispose() async {
await _player.dispose();
await Future.wait([
_scanPlayer.dispose(),
_successPlayer.dispose(),
_warningPlayer.dispose(),
]);
}
}
@@ -0,0 +1,62 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:qrscanner/services/scan_launch_service.dart';
typedef HardwareScanCallback = void Function(String rawValue);
/// Integración con el escáner físico Honeywell vía Intent API nativa (EDA51).
class HardwareScannerService {
HardwareScannerService._();
static final HardwareScannerService instance = HardwareScannerService._();
final _scanLaunch = ScanLaunchService.instance;
HardwareScanCallback? _onScan;
void Function(Exception error)? _onError;
StreamSubscription<String>? _scanSubscription;
bool _active = false;
bool get isActive => _active;
static Future<bool> isAvailable() => instance._scanLaunch.isSupported();
Future<bool> activate({
required HardwareScanCallback onScan,
void Function(Exception error)? onError,
}) async {
if (kIsWeb || !Platform.isAndroid) return false;
_onScan = onScan;
_onError = onError;
await _scanLaunch.claimScanner();
await _scanSubscription?.cancel();
_scanSubscription = _scanLaunch.scanStream.listen(
(code) => _onScan?.call(code),
onError: (Object error) => _onError?.call(Exception(error.toString())),
);
_active = true;
return _scanLaunch.isSupported();
}
Future<void> pause() async {
_active = false;
}
Future<void> deactivate() async {
await _scanSubscription?.cancel();
_scanSubscription = null;
_onScan = null;
_onError = null;
_active = false;
}
Future<void> ensureStopped() async {
await deactivate();
await _scanLaunch.releaseScanner();
}
}
+62
View File
@@ -0,0 +1,62 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
class ScanLaunchService {
ScanLaunchService._();
static final ScanLaunchService instance = ScanLaunchService._();
static const _methodChannel = MethodChannel('com.example.qrscanner/scan_launch');
static const _eventChannel = EventChannel('com.example.qrscanner/scan_events');
Stream<String>? _scanStream;
bool get isAndroid => !kIsWeb && Platform.isAndroid;
Stream<String> get scanStream {
_scanStream ??= _eventChannel
.receiveBroadcastStream()
.where((event) => event is String && event.trim().isNotEmpty)
.map((event) => (event as String).trim());
return _scanStream!;
}
Future<bool> isSupported() async {
if (!isAndroid) return false;
try {
final supported = await _methodChannel.invokeMethod<bool>('isSupported');
return supported ?? false;
} catch (_) {
return false;
}
}
Future<void> claimScanner() async {
if (!isAndroid) return;
try {
await _methodChannel.invokeMethod<void>('claimScanner');
} catch (_) {}
}
Future<void> releaseScanner() async {
if (!isAndroid) return;
try {
await _methodChannel.invokeMethod<void>('releaseScanner');
} catch (_) {}
}
Future<String?> consumePendingScan() async {
if (!isAndroid) return null;
try {
final code = await _methodChannel.invokeMethod<String>('consumePendingScan');
final trimmed = code?.trim();
if (trimmed == null || trimmed.isEmpty) return null;
return trimmed;
} catch (_) {
return null;
}
}
}
+137
View File
@@ -0,0 +1,137 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:qrscanner/models/auth_session.dart';
import 'package:qrscanner/screens/home_screen.dart';
import 'package:qrscanner/screens/scanner_screen.dart';
import 'package:qrscanner/services/auth_service.dart';
import 'package:qrscanner/services/scan_launch_service.dart';
class ScanNavigationService {
ScanNavigationService._();
static final ScanNavigationService instance = ScanNavigationService._();
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
final AuthService _authService = AuthService();
StreamSubscription<String>? _scanSubscription;
String? _pendingCode;
bool _handlingScan = false;
bool _scannerOpen = false;
String? get pendingCode => _pendingCode;
void setPendingCode(String code) {
final trimmed = code.trim();
if (trimmed.isEmpty) return;
_pendingCode = trimmed;
}
String? takePendingCode() {
final code = _pendingCode;
_pendingCode = null;
return code;
}
Future<void> initialize() async {
await ScanLaunchService.instance.claimScanner();
final pending = await ScanLaunchService.instance.consumePendingScan();
if (pending != null) {
setPendingCode(pending);
}
await _scanSubscription?.cancel();
_scanSubscription = ScanLaunchService.instance.scanStream.listen(
(code) => unawaited(handleScan(code)),
);
}
Future<void> dispose() async {
await _scanSubscription?.cancel();
_scanSubscription = null;
}
Future<void> handleScan(String code, {AuthSession? session}) async {
final trimmed = code.trim();
if (trimmed.isEmpty || _handlingScan) return;
_handlingScan = true;
try {
setPendingCode(trimmed);
final activeSession = session ??
_authService.currentSession ??
await _authService.loadStoredSession();
if (activeSession == null) return;
final navigator = navigatorKey.currentState;
if (navigator == null) return;
takePendingCode();
if (_scannerOpen) return;
_scannerOpen = true;
try {
await navigator.push(
PageRouteBuilder<void>(
settings: const RouteSettings(name: '/scanner'),
pageBuilder: (context, animation, secondaryAnimation) =>
ScannerScreen(
session: activeSession,
initialCode: trimmed,
),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 0.06),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeOutCubic,
)),
child: child,
),
);
},
transitionDuration: const Duration(milliseconds: 380),
),
);
} finally {
_scannerOpen = false;
}
} finally {
_handlingScan = false;
}
}
Future<void> openPendingScanAfterAuth(AuthSession session) async {
final code = takePendingCode();
if (code == null) return;
await handleScan(code, session: session);
}
Future<void> navigateToHome(AuthSession session) async {
final navigator = navigatorKey.currentState;
if (navigator == null) return;
await navigator.pushAndRemoveUntil(
PageRouteBuilder<void>(
pageBuilder: (context, animation, secondaryAnimation) =>
HomeScreen(session: session),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(opacity: animation, child: child);
},
transitionDuration: const Duration(milliseconds: 400),
),
(route) => false,
);
await openPendingScanAfterAuth(session);
}
}