Add QR scanner

This commit is contained in:
Anton Tananaev 2025-06-23 21:32:17 -07:00
parent ee500f541f
commit 3437b670ea
7 changed files with 128 additions and 25 deletions

View file

@ -1,6 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CAMERA"/>
<application <application
tools:replace="android:label" tools:replace="android:label"
android:label="Traccar Client" android:label="Traccar Client"

View file

@ -145,6 +145,9 @@ PODS:
- GoogleUtilities/UserDefaults (8.1.0): - GoogleUtilities/UserDefaults (8.1.0):
- GoogleUtilities/Logger - GoogleUtilities/Logger
- GoogleUtilities/Privacy - GoogleUtilities/Privacy
- mobile_scanner (7.0.0):
- Flutter
- FlutterMacOS
- nanopb (3.30910.0): - nanopb (3.30910.0):
- nanopb/decode (= 3.30910.0) - nanopb/decode (= 3.30910.0)
- nanopb/encode (= 3.30910.0) - nanopb/encode (= 3.30910.0)
@ -170,6 +173,7 @@ DEPENDENCIES:
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_background_geolocation (from `.symlinks/plugins/flutter_background_geolocation/ios`) - flutter_background_geolocation (from `.symlinks/plugins/flutter_background_geolocation/ios`)
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`)
- quick_actions_ios (from `.symlinks/plugins/quick_actions_ios/ios`) - quick_actions_ios (from `.symlinks/plugins/quick_actions_ios/ios`)
- rate_my_app (from `.symlinks/plugins/rate_my_app/darwin`) - rate_my_app (from `.symlinks/plugins/rate_my_app/darwin`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
@ -209,6 +213,8 @@ EXTERNAL SOURCES:
:path: Flutter :path: Flutter
flutter_background_geolocation: flutter_background_geolocation:
:path: ".symlinks/plugins/flutter_background_geolocation/ios" :path: ".symlinks/plugins/flutter_background_geolocation/ios"
mobile_scanner:
:path: ".symlinks/plugins/mobile_scanner/darwin"
quick_actions_ios: quick_actions_ios:
:path: ".symlinks/plugins/quick_actions_ios/ios" :path: ".symlinks/plugins/quick_actions_ios/ios"
rate_my_app: rate_my_app:
@ -238,6 +244,7 @@ SPEC CHECKSUMS:
GoogleAppMeasurement: 0dfca1a4b534d123de3945e28f77869d10d0d600 GoogleAppMeasurement: 0dfca1a4b534d123de3945e28f77869d10d0d600
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851

View file

@ -41,6 +41,8 @@
<string>Location required when app is in use</string> <string>Location required when app is in use</string>
<key>NSMotionUsageDescription</key> <key>NSMotionUsageDescription</key>
<string>Motion permission helps detect when device in in-motion</string> <string>Motion permission helps detect when device in in-motion</string>
<key>NSCameraUsageDescription</key>
<string>Camera access is required to scan QR codes</string>
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<true/> <true/>
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>

89
lib/qr_code_screen.dart Normal file
View file

@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:flutter_background_geolocation/flutter_background_geolocation.dart' as bg;
import 'preferences.dart';
import 'l10n/app_localizations.dart';
class QrCodeScreen extends StatefulWidget {
const QrCodeScreen({super.key});
@override
State<QrCodeScreen> createState() => _QrCodeScreenState();
}
class _QrCodeScreenState extends State<QrCodeScreen> {
bool _scanned = false;
Future<void> _applySettings(Uri uri) async {
await Preferences.instance.setString(Preferences.url, '${uri.origin}${uri.path}');
final parameters = uri.queryParameters;
await _applyStringParameter(parameters, Preferences.id);
await _applyStringParameter(parameters, Preferences.accuracy);
await _applyIntParameter(parameters, Preferences.distance);
await _applyIntParameter(parameters, Preferences.interval);
await _applyIntParameter(parameters, Preferences.angle);
await _applyIntParameter(parameters, Preferences.heartbeat);
await _applyIntParameter(parameters, Preferences.fastestInterval);
await _applyBoolParameter(parameters, Preferences.buffer);
await _applyBoolParameter(parameters, Preferences.wakelock);
await _applyBoolParameter(parameters, Preferences.stopDetection);
await bg.BackgroundGeolocation.setConfig(Preferences.geolocationConfig());
}
Future<void> _applyStringParameter(Map<String, String> parameters, String key) async {
final value = parameters[key];
if (value != null) {
await Preferences.instance.setString(key, value);
}
}
Future<void> _applyIntParameter(Map<String, String> parameters, String key) async {
final stringValue = parameters[key];
if (stringValue != null) {
final value = int.tryParse(stringValue);
if (value != null) {
await Preferences.instance.setInt(key, value);
}
}
}
Future<void> _applyBoolParameter(Map<String, String> parameters, String key) async {
final value = parameters[key];
if (value != null) {
switch (value) {
case 'false':
await Preferences.instance.setBool(key, false);
break;
case 'true':
await Preferences.instance.setBool(key, true);
break;
}
}
}
void _onDetect(BarcodeCapture capture) async {
if (_scanned) return;
final barcode = capture.barcodes.first;
final rawValue = barcode.rawValue;
if (rawValue == null) return;
final uri = Uri.tryParse(rawValue);
if (uri == null || uri.scheme.isEmpty) return;
_scanned = true;
await _applySettings(uri);
if (mounted) Navigator.pop(context);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.settingsTitle),
),
body: MobileScanner(
fit: BoxFit.cover,
onDetect: _onDetect,
),
);
}
}

View file

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_background_geolocation/flutter_background_geolocation.dart' as bg; import 'package:flutter_background_geolocation/flutter_background_geolocation.dart' as bg;
import 'package:traccar_client/qr_code_screen.dart';
import 'package:wakelock_partial_android/wakelock_partial_android.dart'; import 'package:wakelock_partial_android/wakelock_partial_android.dart';
import 'l10n/app_localizations.dart'; import 'l10n/app_localizations.dart';
@ -17,23 +18,6 @@ class SettingsScreen extends StatefulWidget {
class _SettingsScreenState extends State<SettingsScreen> { class _SettingsScreenState extends State<SettingsScreen> {
bool advanced = false; bool advanced = false;
bool buffering = true;
bool wakelock = false;
bool stopDetection = true;
@override
void initState() {
super.initState();
_initState();
}
void _initState() async {
setState(() {
buffering = Preferences.instance.getBool(Preferences.buffer) ?? true;
wakelock = Preferences.instance.getBool(Preferences.wakelock) ?? false;
stopDetection = Preferences.instance.getBool(Preferences.stopDetection) ?? true;
});
}
String _getAccuracyLabel(String? key) { String _getAccuracyLabel(String? key) {
return switch (key) { return switch (key) {
@ -148,7 +132,18 @@ class _SettingsScreenState extends State<SettingsScreen> {
final isHighestAccuracy = Preferences.instance.getString(Preferences.accuracy) == 'highest'; final isHighestAccuracy = Preferences.instance.getString(Preferences.accuracy) == 'highest';
final distance = Preferences.instance.getInt(Preferences.distance); final distance = Preferences.instance.getInt(Preferences.distance);
return Scaffold( return Scaffold(
appBar: AppBar(title: Text(AppLocalizations.of(context)!.settingsTitle)), appBar: AppBar(
title: Text(AppLocalizations.of(context)!.settingsTitle),
actions: [
IconButton(
icon: const Icon(Icons.qr_code_scanner),
onPressed: () async {
await Navigator.push(context, MaterialPageRoute(builder: (_) => const QrCodeScreen()));
setState(() {});
},
),
],
),
body: ListView( body: ListView(
children: [ children: [
_buildListTile(AppLocalizations.of(context)!.idLabel, Preferences.id, false), _buildListTile(AppLocalizations.of(context)!.idLabel, Preferences.id, false),
@ -172,17 +167,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (advanced) if (advanced)
SwitchListTile( SwitchListTile(
title: Text(AppLocalizations.of(context)!.bufferLabel), title: Text(AppLocalizations.of(context)!.bufferLabel),
value: buffering, value: Preferences.instance.getBool(Preferences.buffer) ?? true,
onChanged: (value) async { onChanged: (value) async {
await Preferences.instance.setBool(Preferences.buffer, value); await Preferences.instance.setBool(Preferences.buffer, value);
await bg.BackgroundGeolocation.setConfig(Preferences.geolocationConfig()); await bg.BackgroundGeolocation.setConfig(Preferences.geolocationConfig());
setState(() => buffering = value); setState(() {});
}, },
), ),
if (advanced && Platform.isAndroid) if (advanced && Platform.isAndroid)
SwitchListTile( SwitchListTile(
title: Text(AppLocalizations.of(context)!.wakelockLabel), title: Text(AppLocalizations.of(context)!.wakelockLabel),
value: wakelock, value: Preferences.instance.getBool(Preferences.wakelock) ?? false,
onChanged: (value) async { onChanged: (value) async {
await Preferences.instance.setBool(Preferences.wakelock, value); await Preferences.instance.setBool(Preferences.wakelock, value);
if (value) { if (value) {
@ -193,17 +188,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
} else { } else {
WakelockPartialAndroid.release(); WakelockPartialAndroid.release();
} }
setState(() => wakelock = value); setState(() {});
}, },
), ),
if (advanced) if (advanced)
SwitchListTile( SwitchListTile(
title: Text(AppLocalizations.of(context)!.stopDetectionLabel), title: Text(AppLocalizations.of(context)!.stopDetectionLabel),
value: stopDetection, value: Preferences.instance.getBool(Preferences.stopDetection) ?? true,
onChanged: (value) async { onChanged: (value) async {
await Preferences.instance.setBool(Preferences.stopDetection, value); await Preferences.instance.setBool(Preferences.stopDetection, value);
await bg.BackgroundGeolocation.setConfig(Preferences.geolocationConfig()); await bg.BackgroundGeolocation.setConfig(Preferences.geolocationConfig());
setState(() => stopDetection = value); setState(() {});
}, },
), ),
], ],

View file

@ -277,6 +277,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.16.0" version: "1.16.0"
mobile_scanner:
dependency: "direct main"
description:
name: mobile_scanner
sha256: "54005bdea7052d792d35b4fef0f84ec5ddc3a844b250ecd48dc192fb9b4ebc95"
url: "https://pub.dev"
source: hosted
version: "7.0.1"
path: path:
dependency: transitive dependency: transitive
description: description:
@ -516,4 +524,4 @@ packages:
version: "1.1.0" version: "1.1.0"
sdks: sdks:
dart: ">=3.8.1 <4.0.0" dart: ">=3.8.1 <4.0.0"
flutter: ">=3.27.0" flutter: ">=3.29.0"

View file

@ -22,6 +22,7 @@ dependencies:
rate_my_app: ^2.3.1 rate_my_app: ^2.3.1
shared_preferences_android: ^2.4.10 shared_preferences_android: ^2.4.10
wakelock_partial_android: ^1.0.0 wakelock_partial_android: ^1.0.0
mobile_scanner: ^7.0.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: