diff --git a/.github/workflows/analyze.yml b/.github/workflows/analyze.yml deleted file mode 100644 index 173b0f7..0000000 --- a/.github/workflows/analyze.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Flutter Analyze - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - analyze: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2 - - run: flutter pub get - - run: flutter analyze diff --git a/.github/workflows/translation.yml b/.github/workflows/translation.yml deleted file mode 100644 index 3ae9e9f..0000000 --- a/.github/workflows/translation.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Update Translations - -on: - workflow_dispatch: - -jobs: - translation: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: transifex/cli-action@v2 - with: - token: ${{ secrets.TRANSIFEX_TOKEN }} - args: pull -a --force - - run: | - git config --global user.name "Traccar Bot" - git config --global user.email "support@traccar.org" - for f in lib/l10n/*.arb; do - [[ $f == */app_en.arb ]] && continue - diff -q "$f" lib/l10n/app_en.arb && continue - git diff --quiet -- "$f" || git add "$f" - done - git diff --cached --quiet || git commit -m "Update translations" - git push - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/SPEC.md b/SPEC.md new file mode 100644 index 0000000..984aee8 --- /dev/null +++ b/SPEC.md @@ -0,0 +1,188 @@ +# Traccar Client Fork - Agent Specification + +This spec describes how to transform a fresh fork of [traccar/traccar-client](https://github.com/traccar/traccar-client) (Flutter) into a simplified, hardcoded tracking client. Apply all changes to the `main` branch. + +## Goal + +Create a minimal Traccar Client that: +- Hard-codes all server/tracking settings (no user configuration) +- Shows only a device ID and a tracking toggle in the UI +- Removes access to settings, status, manual send, QR scanning, and password protection + +## Upstream Repository + +``` +https://github.com/traccar/traccar-client +``` + +The app is a Flutter project targeting iOS and Android from a single Dart codebase. + +--- + +## 1. Configuration File: `lib/config.dart` + +Create a new file `lib/config.dart` with compile-time constants. This is the single source of truth for all hardcoded settings. A developer forking this project only needs to edit this file to reconfigure the app. + +```dart +class AppConfig { + AppConfig._(); + + /// Traccar server URL + static const String serverUrl = 'https://c.track.rs'; + + /// Location accuracy: 'highest', 'high', 'medium', or 'low' + static const String accuracy = 'highest'; + + /// Location update interval in seconds + static const int intervalSeconds = 60; + + /// Distance filter in meters (0 = disabled) + static const int distanceFilter = 0; + + /// Whether to enable stop detection + static const bool stopDetection = false; + + /// Whether to buffer locations when offline + static const bool buffer = true; + + /// Fastest location update interval in seconds (Android) + static const int fastestIntervalSeconds = 30; +} +``` + +All other modified files must read from `AppConfig` instead of using inline literals. + +--- + +## 2. Hardcoded Settings (via `AppConfig`) + +These values are set in `lib/config.dart` and enforced at runtime: + +| Setting | Constant | Default Value | +|--------------------|-----------------------------------|--------------------------| +| Server URL | `AppConfig.serverUrl` | `https://c.track.rs` | +| Location Accuracy | `AppConfig.accuracy` | `highest` | +| Interval | `AppConfig.intervalSeconds` | `60` | +| Distance Filter | `AppConfig.distanceFilter` | `0` (disabled) | +| Stop Detection | `AppConfig.stopDetection` | `false` (disabled) | +| Buffer | `AppConfig.buffer` | `true` | +| Fastest Interval | `AppConfig.fastestIntervalSeconds`| `30` | + +--- + +## 3. File Changes + +### 3.1 `lib/preferences.dart` + +Add `import 'config.dart';` at the top. + +#### `migrate()` method +After the existing migration logic and device ID generation, force-set values from `AppConfig`: + +```dart +// After device ID generation +await instance.setString(url, AppConfig.serverUrl); +await instance.setString(accuracy, AppConfig.accuracy); +await instance.setInt(interval, AppConfig.intervalSeconds); +await instance.setInt(distance, AppConfig.distanceFilter); +await instance.setBool(buffer, instance.getBool(buffer) ?? AppConfig.buffer); +await instance.setBool(stopDetection, AppConfig.stopDetection); +await instance.setInt(fastestInterval, instance.getInt(fastestInterval) ?? AppConfig.fastestIntervalSeconds); +``` + +#### `geolocationConfig()` method +Replace dynamic preference reads with `AppConfig` references: + +- `isHighestAccuracy` = `AppConfig.accuracy == 'highest'` +- `locationUpdateInterval` = `AppConfig.intervalSeconds * 1000` (but set to `0` in the config when highest accuracy) +- `desiredAccuracy`: Use `DESIRED_ACCURACY_NAVIGATION` on iOS, `DESIRED_ACCURACY_HIGH` on Android (when highest) +- `url`: `AppConfig.serverUrl` (passed through `_formatUrl`) +- `distanceFilter`: `AppConfig.distanceFilter.toDouble()` +- `locationUpdateInterval` (in config): `0` (when highest accuracy, for continuous tracking) +- `disableElasticity`: `true` +- `disableStopDetection`: `!AppConfig.stopDetection` +- `pausesLocationUpdatesAutomatically`: `false` on iOS, `null` on Android +- `fastestLocationUpdateInterval`: `0` when highest accuracy, else `AppConfig.fastestIntervalSeconds * 1000` + +### 3.2 `lib/main_screen.dart` + +**Complete rewrite.** Replace the entire file with a minimal screen containing: + +1. **AppBar** with title `'Traccar Client'` +2. **Centered body** with two cards: + - **Device ID card**: Shows label "Device ID" and a `SelectableText` displaying the device ID in monospace bold (`headlineSmall`) + - **Tracking toggle card**: A `SwitchListTile` labeled "Continuous Tracking" with subtitle showing state ("Sending location updates" / "Location tracking disabled") +3. **Tracking toggle behavior**: + - On: calls `bg.BackgroundGeolocation.start()` then checks battery optimizations + - Off: calls `bg.BackgroundGeolocation.stop()` + - Track `isMoving` state; set `activeTrackColor` to error color when `isMoving == false` (stationary warning) +4. **State listeners**: Listen to `onEnabledChange` and `onMotionChange` from BackgroundGeolocation +5. **Battery optimization dialog**: Keep the `_checkBatteryOptimizations` method that shows a dialog using `bg.DeviceSettings` + +**Remove entirely:** +- Settings button/navigation +- Status screen button/navigation +- "Send Location" button +- Password service import and usage +- Any reference to `SettingsScreen` or `StatusScreen` navigation + +**Imports needed:** `flutter/material.dart`, `flutter/services.dart`, `traccar_client/main.dart` (for `messengerKey`), `traccar_client/preferences.dart`, `flutter_background_geolocation`, `l10n/app_localizations.dart` + +### 3.3 `lib/settings_screen.dart` + +**Complete rewrite.** Replace with a minimal read-only screen (not navigable from main UI, but kept for completeness): + +Add `import 'config.dart';` at the top. + +- Device ID display (read-only `ListTile`) +- Continuous Tracking `SwitchListTile` (functional) +- Read-only disabled `ListTile` entries displaying values from `AppConfig`: + - Server: `AppConfig.serverUrl` + - Location Accuracy: `AppConfig.accuracy` + - Distance: `AppConfig.distanceFilter` (show "Disabled" when 0) + - Interval: `'${AppConfig.intervalSeconds} seconds'` + - Stop Detection: `AppConfig.stopDetection` (show "Disabled"/"Enabled") + +### 3.4 `ios/Runner.xcodeproj/project.pbxproj` + +Find the build phase named `FlutterFire: "flutterfire upload-crashlytics-symbols"` and prepend `exit 0` to the shell script to skip it. This prevents `flutterfire: command not found` build failures. + +The `shellScript` value should start with: +``` +\n#!/bin/bash\n# Skip Firebase Crashlytics for now\nexit 0\n +``` +followed by the original script content (which will never execute). + +--- + +## 4. Files NOT Modified + +These files remain unchanged from upstream: +- `lib/main.dart` - App entry point, Firebase init, app links, rate dialog +- `lib/geolocation_service.dart` +- `lib/configuration_service.dart` +- `lib/push_service.dart` +- `lib/quick_actions.dart` +- `lib/location_cache.dart` +- `lib/status_screen.dart` - Kept in codebase but not navigable +- `lib/password_service.dart` - Kept in codebase but unused +- `lib/qr_code_screen.dart` - Kept in codebase but not navigable +- `pubspec.yaml` +- All localization files (`lib/l10n/`) +- Android platform files + +--- + +## 5. Verification Checklist + +After applying changes: + +1. `flutter pub get` succeeds +2. `flutter build ios` succeeds (no flutterfire errors) +3. `flutter build apk --release` succeeds +4. App launches showing only device ID and tracking toggle +5. No settings, status, or send location buttons visible +6. Toggling tracking on starts background location reporting to `https://c.track.rs` +7. Device ID is auto-generated (8-digit random number) and persists across launches +8. Location updates use highest accuracy with 0 distance filter +9. Stop detection is disabled (tracking never auto-pauses) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 4533c73..d8c551a 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -344,7 +344,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\n#!/bin/bash\nPATH=\"${PATH}:$FLUTTER_ROOT/bin:${PUB_CACHE}/bin:$HOME/.pub-cache/bin\"\n\nif [ -z \"$PODS_ROOT\" ] || [ ! -d \"$PODS_ROOT/FirebaseCrashlytics\" ]; then\n # Cannot use \"BUILD_DIR%/Build/*\" as per Firebase documentation, it points to \"flutter-project/build/ios/*\" path which doesn't have run script\n DERIVED_DATA_PATH=$(echo \"$BUILD_ROOT\" | sed -E 's|(.*DerivedData/[^/]+).*|\\1|')\n PATH_TO_CRASHLYTICS_UPLOAD_SCRIPT=\"${DERIVED_DATA_PATH}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run\"\nelse\n PATH_TO_CRASHLYTICS_UPLOAD_SCRIPT=\"$PODS_ROOT/FirebaseCrashlytics/run\"\nfi\n\n# Command to upload symbols script used to upload symbols to Firebase server\nflutterfire upload-crashlytics-symbols --upload-symbols-script-path=\"$PATH_TO_CRASHLYTICS_UPLOAD_SCRIPT\" --platform=ios --apple-project-path=\"${SRCROOT}\" --env-platform-name=\"${PLATFORM_NAME}\" --env-configuration=\"${CONFIGURATION}\" --env-project-dir=\"${PROJECT_DIR}\" --env-built-products-dir=\"${BUILT_PRODUCTS_DIR}\" --env-dwarf-dsym-folder-path=\"${DWARF_DSYM_FOLDER_PATH}\" --env-dwarf-dsym-file-name=\"${DWARF_DSYM_FILE_NAME}\" --env-infoplist-path=\"${INFOPLIST_PATH}\" --default-config=default\n"; + shellScript = "\n#!/bin/bash\n# Skip Firebase Crashlytics for now\nexit 0\nPATH=\"${PATH}:$FLUTTER_ROOT/bin:${PUB_CACHE}/bin:$HOME/.pub-cache/bin\"\n\nif [ -z \"$PODS_ROOT\" ] || [ ! -d \"$PODS_ROOT/FirebaseCrashlytics\" ]; then\n # Cannot use \"BUILD_DIR%/Build/*\" as per Firebase documentation, it points to \"flutter-project/build/ios/*\" path which doesn't have run script\n DERIVED_DATA_PATH=$(echo \"$BUILD_ROOT\" | sed -E 's|(.*DerivedData/[^/]+).*|\\1|')\n PATH_TO_CRASHLYTICS_UPLOAD_SCRIPT=\"${DERIVED_DATA_PATH}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run\"\nelse\n PATH_TO_CRASHLYTICS_UPLOAD_SCRIPT=\"$PODS_ROOT/FirebaseCrashlytics/run\"\nfi\n\n# Command to upload symbols script used to upload symbols to Firebase server\nflutterfire upload-crashlytics-symbols --upload-symbols-script-path=\"$PATH_TO_CRASHLYTICS_UPLOAD_SCRIPT\" --platform=ios --apple-project-path=\"${SRCROOT}\" --env-platform-name=\"${PLATFORM_NAME}\" --env-configuration=\"${CONFIGURATION}\" --env-project-dir=\"${PROJECT_DIR}\" --env-built-products-dir=\"${BUILT_PRODUCTS_DIR}\" --env-dwarf-dsym-folder-path=\"${DWARF_DSYM_FOLDER_PATH}\" --env-dwarf-dsym-file-name=\"${DWARF_DSYM_FILE_NAME}\" --env-infoplist-path=\"${INFOPLIST_PATH}\" --default-config=default\n"; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; diff --git a/lib/config.dart b/lib/config.dart new file mode 100644 index 0000000..cfe42f3 --- /dev/null +++ b/lib/config.dart @@ -0,0 +1,24 @@ +class AppConfig { + AppConfig._(); + + /// Traccar server URL + static const String serverUrl = 'https://c.track.rs'; + + /// Location accuracy: 'highest', 'high', 'medium', or 'low' + static const String accuracy = 'highest'; + + /// Location update interval in seconds + static const int intervalSeconds = 60; + + /// Distance filter in meters (0 = disabled) + static const int distanceFilter = 0; + + /// Whether to enable stop detection + static const bool stopDetection = false; + + /// Whether to buffer locations when offline + static const bool buffer = true; + + /// Fastest location update interval in seconds (Android) + static const int fastestIntervalSeconds = 30; +} diff --git a/lib/main_screen.dart b/lib/main_screen.dart index 08e0155..21ad27f 100644 --- a/lib/main_screen.dart +++ b/lib/main_screen.dart @@ -1,15 +1,10 @@ -import 'package:app_settings/app_settings.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:traccar_client/main.dart'; -import 'package:traccar_client/password_service.dart'; import 'package:traccar_client/preferences.dart'; -import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter_background_geolocation/flutter_background_geolocation.dart' as bg; import 'l10n/app_localizations.dart'; -import 'status_screen.dart'; -import 'settings_screen.dart'; class MainScreen extends StatefulWidget { const MainScreen({super.key}); @@ -74,144 +69,76 @@ class _MainScreenState extends State { } } - Widget _buildTrackingCard() { - return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ListTile( - contentPadding: EdgeInsets.zero, - title: Text(AppLocalizations.of(context)!.trackingTitle), - titleTextStyle: Theme.of(context).textTheme.headlineMedium, - ), - ListTile( - contentPadding: EdgeInsets.zero, - title: Text(AppLocalizations.of(context)!.idLabel), - subtitle: Text(Preferences.instance.getString(Preferences.id) ?? ''), - ), - SwitchListTile( - contentPadding: EdgeInsets.zero, - title: Text(AppLocalizations.of(context)!.trackingLabel), - value: trackingEnabled, - activeTrackColor: isMoving == false ? Theme.of(context).colorScheme.secondary : null, - onChanged: (bool value) async { - if (await PasswordService.authenticate(context) && mounted) { - if (value) { - try { - FirebaseCrashlytics.instance.log('tracking_toggle_start'); - await bg.BackgroundGeolocation.start(); - if (mounted) { - _checkBatteryOptimizations(context); - } - } on PlatformException catch (error) { - final providerState = await bg.BackgroundGeolocation.providerState; - final isPermissionError = providerState.status == bg.ProviderChangeEvent.AUTHORIZATION_STATUS_DENIED || - providerState.status == bg.ProviderChangeEvent.AUTHORIZATION_STATUS_RESTRICTED; + @override + Widget build(BuildContext context) { + final deviceId = Preferences.instance.getString(Preferences.id) ?? ''; + return Scaffold( + appBar: AppBar( + title: const Text('Traccar Client'), + ), + body: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Card( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + children: [ + Text( + 'Device ID', + style: Theme.of(context).textTheme.labelLarge, + ), + const SizedBox(height: 8), + SelectableText( + deviceId, + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontFamily: 'monospace', + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ), + const SizedBox(height: 16), + Card( + child: SwitchListTile( + title: const Text('Continuous Tracking'), + subtitle: Text( + trackingEnabled + ? 'Sending location updates' + : 'Location tracking disabled', + ), + value: trackingEnabled, + activeTrackColor: isMoving == false + ? Theme.of(context).colorScheme.error + : null, + onChanged: (bool value) async { + if (value) { + try { + await bg.BackgroundGeolocation.start(); + if (mounted) { + _checkBatteryOptimizations(context); + } + } on PlatformException catch (error) { if (!mounted) return; messengerKey.currentState?.showSnackBar( SnackBar( content: Text(error.message ?? error.code), - duration: const Duration(seconds: 4), - action: isPermissionError - ? SnackBarAction( - label: AppLocalizations.of(context)!.settingsTitle, - onPressed: () => AppSettings.openAppSettings( - type: AppSettingsType.settings, - ), - ) - : null, ), ); - } - } else { - FirebaseCrashlytics.instance.log('tracking_toggle_stop'); - bg.BackgroundGeolocation.stop(); - } - } - }, - ), - const SizedBox(height: 8), - OverflowBar( - spacing: 8, - children: [ - FilledButton.tonal( - onPressed: () async { - try { - await bg.BackgroundGeolocation.getCurrentPosition(samples: 1, persist: true, extras: {'manual': true}); - } on PlatformException catch (error) { - messengerKey.currentState?.showSnackBar(SnackBar(content: Text(error.message ?? error.code))); + } + } else { + bg.BackgroundGeolocation.stop(); } }, - child: Text(AppLocalizations.of(context)!.locationButton), ), - FilledButton.tonal( - onPressed: () { - Navigator.push(context, MaterialPageRoute(builder: (_) => const StatusScreen())); - }, - child: Text(AppLocalizations.of(context)!.statusButton), - ), - ], - ), - ], - ), - ), - ); - } - - Widget _buildSettingsCard() { - return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ListTile( - contentPadding: EdgeInsets.zero, - title: Text(AppLocalizations.of(context)!.settingsTitle), - titleTextStyle: Theme.of(context).textTheme.headlineMedium, - ), - ListTile( - contentPadding: EdgeInsets.zero, - title: Text(AppLocalizations.of(context)!.urlLabel), - subtitle: Text(Preferences.instance.getString(Preferences.url) ?? ''), - ), - const SizedBox(height: 8), - OverflowBar( - spacing: 8, - children: [ - FilledButton.tonal( - onPressed: () async { - if (await PasswordService.authenticate(context) && mounted) { - await Navigator.push(context, MaterialPageRoute(builder: (_) => const SettingsScreen())); - setState(() {}); - } - }, - child: Text(AppLocalizations.of(context)!.settingsButton), - ), - ], - ), - ] - ), - ), - ); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text('Traccar Client'), - ), - body: SingleChildScrollView( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - _buildTrackingCard(), - const SizedBox(height: 16), - _buildSettingsCard(), - ], + ), + ], + ), ), ), ); diff --git a/lib/preferences.dart b/lib/preferences.dart index 44ff9d0..4c5230d 100644 --- a/lib/preferences.dart +++ b/lib/preferences.dart @@ -5,6 +5,8 @@ import 'package:flutter_background_geolocation/flutter_background_geolocation.da import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences_android/shared_preferences_android.dart'; +import 'config.dart'; + class Preferences { static Future? _initFuture; static late SharedPreferencesWithCache instance; @@ -47,37 +49,35 @@ class Preferences { ); if (instance.getString(id) == null) { await instance.setString(id, (Random().nextInt(90000000) + 10000000).toString()); - await instance.setString(url, 'http://demo.traccar.org:5055'); - await instance.setString(accuracy, 'medium'); - await instance.setInt(interval, 300); - await instance.setInt(distance, 75); - await instance.setBool(buffer, true); - await instance.setBool(stopDetection, true); - await instance.setInt(fastestInterval, 30); } + await instance.setString(url, AppConfig.serverUrl); + await instance.setString(accuracy, AppConfig.accuracy); + await instance.setInt(interval, AppConfig.intervalSeconds); + await instance.setInt(distance, AppConfig.distanceFilter); + await instance.setBool(buffer, instance.getBool(buffer) ?? AppConfig.buffer); + await instance.setBool(stopDetection, AppConfig.stopDetection); + await instance.setInt(fastestInterval, instance.getInt(fastestInterval) ?? AppConfig.fastestIntervalSeconds); } static bg.Config geolocationConfig() { - final isHighestAccuracy = instance.getString(accuracy) == 'highest'; - final locationUpdateInterval = (instance.getInt(interval) ?? 0) * 1000; - final fastestLocationUpdateInterval = (instance.getInt(fastestInterval) ?? 30) * 1000; + final isHighestAccuracy = AppConfig.accuracy == 'highest'; + final locationUpdateInterval = isHighestAccuracy ? 0 : AppConfig.intervalSeconds * 1000; + final fastestLocationUpdateInterval = isHighestAccuracy ? 0 : AppConfig.fastestIntervalSeconds * 1000; final heartbeatInterval = instance.getInt(heartbeat) ?? 0; return bg.Config( isMoving: true, geolocation: bg.GeoConfig( - desiredAccuracy: switch (instance.getString(accuracy)) { + desiredAccuracy: switch (AppConfig.accuracy) { 'highest' => Platform.isIOS ? bg.DesiredAccuracy.navigation : bg.DesiredAccuracy.high, 'high' => bg.DesiredAccuracy.high, 'low' => bg.DesiredAccuracy.low, _ => bg.DesiredAccuracy.medium, }, - distanceFilter: isHighestAccuracy ? 0 : instance.getInt(distance)?.toDouble(), - locationUpdateInterval: Platform.isAndroid - ? (isHighestAccuracy ? 0 : (locationUpdateInterval > 0 ? locationUpdateInterval : null)) - : null, - fastestLocationUpdateInterval: Platform.isAndroid ? (isHighestAccuracy ? 0 : fastestLocationUpdateInterval) : null, + distanceFilter: AppConfig.distanceFilter.toDouble(), + locationUpdateInterval: Platform.isAndroid ? locationUpdateInterval : null, + fastestLocationUpdateInterval: Platform.isAndroid ? fastestLocationUpdateInterval : null, disableElasticity: true, - pausesLocationUpdatesAutomatically: Platform.isIOS ? !(isHighestAccuracy || instance.getBool(stopDetection) == false) : null, + pausesLocationUpdatesAutomatically: Platform.isIOS ? false : null, showsBackgroundLocationIndicator: Platform.isIOS ? false : null, ), app: bg.AppConfig( @@ -102,7 +102,7 @@ class Preferences { ), http: bg.HttpConfig( autoSync: false, - url: _formatUrl(instance.getString(url)), + url: _formatUrl(AppConfig.serverUrl), params: { 'device_id': instance.getString(id), }, @@ -112,10 +112,10 @@ class Preferences { logMaxDays: 1, ), activity: bg.ActivityConfig( - disableStopDetection: instance.getBool(stopDetection) == false, + disableStopDetection: !AppConfig.stopDetection, ), persistence: bg.PersistenceConfig( - maxRecordsToPersist: instance.getBool(buffer) != false ? -1 : 1, + maxRecordsToPersist: AppConfig.buffer ? -1 : 1, locationTemplate: _locationTemplate(), ), ); diff --git a/lib/settings_screen.dart b/lib/settings_screen.dart index ccc8b1c..cac0f2c 100644 --- a/lib/settings_screen.dart +++ b/lib/settings_screen.dart @@ -1,14 +1,9 @@ -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_background_geolocation/flutter_background_geolocation.dart' as bg; import 'package:traccar_client/main.dart'; -import 'package:traccar_client/password_service.dart'; -import 'package:traccar_client/qr_code_screen.dart'; -import 'package:wakelock_partial_android/wakelock_partial_android.dart'; -import 'l10n/app_localizations.dart'; +import 'config.dart'; import 'preferences.dart'; class SettingsScreen extends StatefulWidget { @@ -19,223 +14,81 @@ class SettingsScreen extends StatefulWidget { } class _SettingsScreenState extends State { - bool advanced = false; + bool trackingEnabled = false; - String _getAccuracyLabel(String? key) { - return switch (key) { - 'highest' => AppLocalizations.of(context)!.highestAccuracyLabel, - 'high' => AppLocalizations.of(context)!.highAccuracyLabel, - 'low' => AppLocalizations.of(context)!.lowAccuracyLabel, - _ => AppLocalizations.of(context)!.mediumAccuracyLabel, - }; + @override + void initState() { + super.initState(); + _initState(); } - Future _editSetting(String title, String key, bool isInt) async { - final initialValue = isInt - ? Preferences.instance.getInt(key)?.toString() ?? '0' - : Preferences.instance.getString(key) ?? ''; - - final controller = TextEditingController(text: initialValue); - final errorMessage = AppLocalizations.of(context)!.invalidValue; - - final result = await showDialog( - context: context, - builder: (context) => AlertDialog( - scrollable: true, - title: Text(title), - content: TextField( - controller: controller, - keyboardType: isInt ? TextInputType.number : TextInputType.text, - inputFormatters: isInt ? [FilteringTextInputFormatter.digitsOnly] : [], - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(AppLocalizations.of(context)!.cancelButton), - ), - TextButton( - onPressed: () => Navigator.pop(context, controller.text), - child: Text(AppLocalizations.of(context)!.saveButton), - ), - ], - ), - ); - - if (result != null && result.isNotEmpty) { - if (key == Preferences.url) { - final uri = Uri.tryParse(result); - if (uri == null || uri.host.isEmpty || !(uri.scheme == 'http' || uri.scheme == 'https')) { - messengerKey.currentState?.showSnackBar(SnackBar(content: Text(errorMessage))); - return; - } - } - if (isInt) { - int? intValue = int.tryParse(result); - if (intValue != null) { - if (key == Preferences.heartbeat && intValue > 0 && intValue < 60) { - intValue = 60; // minimum heartbeat is 60 seconds - } - await Preferences.instance.setInt(key, intValue); - } - } else { - await Preferences.instance.setString(key, result); - } - await bg.BackgroundGeolocation.setConfig(Preferences.geolocationConfig()); - setState(() {}); - } - } - - Future _changePassword() async { - final controller = TextEditingController(); - final result = await showDialog( - context: context, - builder: (context) => AlertDialog( - scrollable: true, - content: TextField( - controller: controller, - decoration: InputDecoration(labelText: AppLocalizations.of(context)!.passwordLabel), - obscureText: true, - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context, false), - child: Text(AppLocalizations.of(context)!.cancelButton), - ), - TextButton( - onPressed: () => Navigator.pop(context, true), - child: Text(AppLocalizations.of(context)!.saveButton), - ), - ], - ), - ); - if (result == true) { - await PasswordService.setPassword(controller.text); - } - } - - Widget _buildListTile(String title, String key, bool isInt) { - String? value; - if (isInt) { - final intValue = Preferences.instance.getInt(key); - if (intValue != null && intValue > 0) { - value = intValue.toString(); - } else { - value = AppLocalizations.of(context)!.disabledValue; - } - } else { - value = Preferences.instance.getString(key); - } - return ListTile( - title: Text(title), - subtitle: Text(value ?? ''), - onTap: () => _editSetting(title, key, isInt), - ); - } - - Widget _buildAccuracyListTile() { - final accuracyOptions = ['highest', 'high', 'medium', 'low']; - return ListTile( - title: Text(AppLocalizations.of(context)!.accuracyLabel), - subtitle: Text(_getAccuracyLabel(Preferences.instance.getString(Preferences.accuracy))), - onTap: () async { - final selectedAccuracy = await showDialog( - context: context, - builder: (context) => SimpleDialog( - title: Text(AppLocalizations.of(context)!.accuracyLabel), - children: accuracyOptions.map((option) => SimpleDialogOption( - child: Text(_getAccuracyLabel(option)), - onPressed: () => Navigator.pop(context, option), - )).toList(), - ), - ); - if (selectedAccuracy != null) { - await Preferences.instance.setString(Preferences.accuracy, selectedAccuracy); - await bg.BackgroundGeolocation.setConfig(Preferences.geolocationConfig()); - setState(() {}); - } - }, - ); + void _initState() async { + final state = await bg.BackgroundGeolocation.state; + setState(() { + trackingEnabled = state.enabled; + }); + bg.BackgroundGeolocation.onEnabledChange((bool enabled) { + setState(() { + trackingEnabled = enabled; + }); + }); } @override Widget build(BuildContext context) { - final isHighestAccuracy = Preferences.instance.getString(Preferences.accuracy) == 'highest'; - final distance = Preferences.instance.getInt(Preferences.distance); return Scaffold( 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(() {}); - }, - ), - ], + title: const Text('Settings'), ), body: ListView( children: [ - _buildListTile(AppLocalizations.of(context)!.idLabel, Preferences.id, false), - _buildListTile(AppLocalizations.of(context)!.urlLabel, Preferences.url, false), - _buildAccuracyListTile(), - _buildListTile(AppLocalizations.of(context)!.distanceLabel, Preferences.distance, true), - if (isHighestAccuracy || Platform.isAndroid && distance == 0) - _buildListTile(AppLocalizations.of(context)!.intervalLabel, Preferences.interval, true), - if (isHighestAccuracy) - _buildListTile(AppLocalizations.of(context)!.angleLabel, Preferences.angle, true), - _buildListTile(AppLocalizations.of(context)!.heartbeatLabel, Preferences.heartbeat, true), + ListTile( + title: const Text('Device ID'), + subtitle: Text(Preferences.instance.getString(Preferences.id) ?? ''), + enabled: false, + ), SwitchListTile( - title: Text(AppLocalizations.of(context)!.advancedLabel), - value: advanced, - onChanged: (value) { - setState(() => advanced = value); + title: const Text('Continuous Tracking'), + value: trackingEnabled, + onChanged: (bool value) async { + if (value) { + try { + await bg.BackgroundGeolocation.start(); + } on PlatformException catch (error) { + messengerKey.currentState?.showSnackBar( + SnackBar(content: Text(error.message ?? error.code)), + ); + } + } else { + bg.BackgroundGeolocation.stop(); + } }, ), - if (advanced) - _buildListTile(AppLocalizations.of(context)!.fastestIntervalLabel, Preferences.fastestInterval, true), - if (advanced) - SwitchListTile( - title: Text(AppLocalizations.of(context)!.bufferLabel), - value: Preferences.instance.getBool(Preferences.buffer) ?? true, - onChanged: (value) async { - await Preferences.instance.setBool(Preferences.buffer, value); - await bg.BackgroundGeolocation.setConfig(Preferences.geolocationConfig()); - setState(() {}); - }, - ), - if (advanced && Platform.isAndroid) - SwitchListTile( - title: Text(AppLocalizations.of(context)!.wakelockLabel), - value: Preferences.instance.getBool(Preferences.wakelock) ?? false, - onChanged: (value) async { - await Preferences.instance.setBool(Preferences.wakelock, value); - if (value) { - final state = await bg.BackgroundGeolocation.state; - if (state.isMoving == true) { - WakelockPartialAndroid.acquire(); - } - } else { - WakelockPartialAndroid.release(); - } - setState(() {}); - }, - ), - if (advanced) - SwitchListTile( - title: Text(AppLocalizations.of(context)!.stopDetectionLabel), - value: Preferences.instance.getBool(Preferences.stopDetection) ?? true, - onChanged: (value) async { - await Preferences.instance.setBool(Preferences.stopDetection, value); - await bg.BackgroundGeolocation.setConfig(Preferences.geolocationConfig()); - setState(() {}); - }, - ), - if (advanced) - ListTile( - title: Text(AppLocalizations.of(context)!.passwordLabel), - onTap: _changePassword, - ), + ListTile( + title: const Text('Server'), + subtitle: Text(AppConfig.serverUrl), + enabled: false, + ), + ListTile( + title: const Text('Location Accuracy'), + subtitle: Text(AppConfig.accuracy), + enabled: false, + ), + ListTile( + title: const Text('Distance'), + subtitle: Text(AppConfig.distanceFilter == 0 ? 'Disabled' : '${AppConfig.distanceFilter}'), + enabled: false, + ), + ListTile( + title: const Text('Interval'), + subtitle: Text('${AppConfig.intervalSeconds} seconds'), + enabled: false, + ), + ListTile( + title: const Text('Stop Detection'), + subtitle: Text(AppConfig.stopDetection ? 'Enabled' : 'Disabled'), + enabled: false, + ), ], ), ); diff --git a/pubspec.lock b/pubspec.lock index 5f90aa8..1c2a317 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: cd83f7d6bd4e4c0b0b4fef802e8796784032e1cc23d7b0e982cf5d05d9bbe182 + sha256: afe15ce18a287d2f89da95566e62892df339b1936bbe9b83587df45b944ee72a url: "https://pub.dev" source: hosted - version: "1.3.66" + version: "1.3.67" app_links: dependency: "direct main" description: @@ -165,34 +165,34 @@ packages: dependency: "direct main" description: name: firebase_analytics - sha256: "07e0a82e9b045dbd14522a0b23764e2dbcc269720e785d63c6a021133ebb766a" + sha256: "8170273394694efdf567e7e30c26457ff346377a52eb679c278f97b36b786d70" url: "https://pub.dev" source: hosted - version: "12.1.2" + version: "12.1.3" firebase_analytics_platform_interface: dependency: transitive description: name: firebase_analytics_platform_interface - sha256: "62fd3f27f342c898bd819fb97fa87c0b971e9fbe03357477282c0e14e1a40c3c" + sha256: "2c25cd85f640a47fcc15e970a04a50470f0a4d0e76f23c5bc5ec93a3d48d8775" url: "https://pub.dev" source: hosted - version: "5.0.6" + version: "5.0.7" firebase_analytics_web: dependency: transitive description: name: firebase_analytics_web - sha256: "8fc488bb008439fc3b850cfac892dec1ff4cd438eee44438919a14c5e61b9828" + sha256: e0eb2b21eccb18109af2c1d3154f9e69b45abf4816e3290a6251008e0503aca9 url: "https://pub.dev" source: hosted - version: "0.6.1+2" + version: "0.6.1+3" firebase_core: dependency: "direct main" description: name: firebase_core - sha256: "923085c881663ef685269b013e241b428e1fb03cdd0ebde265d9b40ff18abf80" + sha256: f0997fee80fbb6d2c658c5b88ae87ba1f9506b5b37126db64fc2e75d8e977fbb url: "https://pub.dev" source: hosted - version: "4.4.0" + version: "4.5.0" firebase_core_platform_interface: dependency: transitive description: @@ -205,50 +205,50 @@ packages: dependency: transitive description: name: firebase_core_web - sha256: "83e7356c704131ca4d8d8dd57e360d8acecbca38b1a3705c7ae46cc34c708084" + sha256: "856ca92bf2d75a63761286ab8e791bda3a85184c2b641764433b619647acfca6" url: "https://pub.dev" source: hosted - version: "3.4.0" + version: "3.5.0" firebase_crashlytics: dependency: "direct main" description: name: firebase_crashlytics - sha256: a6e6cb8b2ea1214533a54e4c1b11b19c40f6a29333f3ab0854a479fdc3237c5b + sha256: "2a6dc88d762af01790a05ff0cf814f7d4020050e8c69dec01962d9ed5dc1a531" url: "https://pub.dev" source: hosted - version: "5.0.7" + version: "5.0.8" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface - sha256: fc6837c4c64c48fa94cab8a872a632b9194fa9208ca76a822f424b3da945584d + sha256: "5fd59d76d691f370e42fd2b786d46078e69ed4126ca0d84b585119f55cd97937" url: "https://pub.dev" source: hosted - version: "3.8.17" + version: "3.8.18" firebase_messaging: dependency: "direct main" description: name: firebase_messaging - sha256: "06fad40ea14771e969a8f2bbce1944aa20ee2f4f57f4eca5b3ba346b65f3f644" + sha256: bd17823b70e629877904d384841cda72ed2cc197517404c0c90da5c0ba786a8c url: "https://pub.dev" source: hosted - version: "16.1.1" + version: "16.1.2" firebase_messaging_platform_interface: dependency: transitive description: name: firebase_messaging_platform_interface - sha256: "6c49e901c77e6e10e86d98e32056a087eb1ca1b93acdf58524f1961e617657b7" + sha256: "550435235cc7d53683f32bf0762c28ef8cfc20a8d36318a033676ae09526d7fb" url: "https://pub.dev" source: hosted - version: "4.7.6" + version: "4.7.7" firebase_messaging_web: dependency: transitive description: name: firebase_messaging_web - sha256: "2756f8fea583ffb9d294d15ddecb3a9ad429b023b70c9990c151fc92c54a32b3" + sha256: "6b1b93ed90309fbce91c219e3cd32aa831e8eccaf4a61f3afaea1625479275d2" url: "https://pub.dev" source: hosted - version: "4.1.2" + version: "4.1.3" flutter: dependency: "direct main" description: flutter @@ -441,10 +441,10 @@ packages: dependency: transitive description: name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.18" + version: "0.12.19" material_color_utilities: dependency: transitive description: @@ -726,10 +726,10 @@ packages: dependency: transitive description: name: test_api - sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.9" + version: "0.7.10" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 56ff9d7..dfa5f4d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,10 +14,10 @@ dependencies: intl: any flutter_background_geolocation: ^5.0.5 shared_preferences: ^2.5.4 - firebase_core: ^4.4.0 - firebase_messaging: ^16.1.1 - firebase_analytics: ^12.1.2 - firebase_crashlytics: ^5.0.7 + firebase_core: ^4.5.0 + firebase_messaging: ^16.1.2 + firebase_analytics: ^12.1.3 + firebase_crashlytics: ^5.0.8 quick_actions: ^1.1.0 rate_my_app: ^2.3.2 shared_preferences_android: ^2.4.21