tracelet_ios 0.3.0 copy "tracelet_ios: ^0.3.0" to clipboard
tracelet_ios: ^0.3.0 copied to clipboard

iOS implementation of the Tracelet background geolocation plugin.

example/lib/main.dart

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:tracelet/tracelet.dart' as tl;

void main() => runApp(const TraceletApp());

class TraceletApp extends StatelessWidget {
  const TraceletApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Tracelet iOS Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorSchemeSeed: Colors.indigo,
        useMaterial3: true,
        brightness: Brightness.light,
      ),
      darkTheme: ThemeData(
        colorSchemeSeed: Colors.indigo,
        useMaterial3: true,
        brightness: Brightness.dark,
      ),
      home: const DashboardPage(),
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Dashboard
// ─────────────────────────────────────────────────────────────────────────────

class DashboardPage extends StatefulWidget {
  const DashboardPage({super.key});

  @override
  State<DashboardPage> createState() => _DashboardPageState();
}

class _DashboardPageState extends State<DashboardPage> {
  // State
  bool _isReady = false;
  bool _isTracking = false;
  bool _isMoving = false;
  tl.Location? _lastLocation;
  tl.State? _pluginState;
  final List<_LogEntry> _log = [];

  // Subscriptions
  final List<StreamSubscription<Object?>> _subs = [];

  @override
  void dispose() {
    for (final s in _subs) {
      s.cancel();
    }
    super.dispose();
  }

  // ── Helpers ──────────────────────────────────────────────────────────────

  void _addLog(String tag, String message) {
    final now = DateTime.now();
    final ts =
        '${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}';
    setState(() {
      _log.insert(0, _LogEntry(ts, tag, message));
      if (_log.length > 200) _log.removeLast();
    });
  }

  // ── Event Subscriptions ─────────────────────────────────────────────────

  void _subscribeEvents() {
    _subs.add(
      tl.Tracelet.onLocation((loc) {
        setState(() => _lastLocation = loc);
        _addLog(
          'LOCATION',
          '${loc.coords.latitude.toStringAsFixed(6)}, ${loc.coords.longitude.toStringAsFixed(6)}  '
              'acc=${loc.coords.accuracy.toStringAsFixed(1)}m  spd=${loc.coords.speed.toStringAsFixed(1)}m/s',
        );
      }),
    );

    _subs.add(
      tl.Tracelet.onMotionChange((loc) {
        setState(() {
          _isMoving = loc.isMoving;
          _lastLocation = loc;
        });
        _addLog('MOTION', loc.isMoving ? 'MOVING' : 'STATIONARY');
      }),
    );

    _subs.add(
      tl.Tracelet.onActivityChange((evt) {
        _addLog('ACTIVITY', '${evt.activity.name} (${evt.confidence.name})');
      }),
    );

    _subs.add(
      tl.Tracelet.onProviderChange((evt) {
        _addLog(
          'PROVIDER',
          'enabled=${evt.enabled}  status=${evt.status.name}  accuracy=${evt.accuracyAuthorization.name}',
        );
      }),
    );

    _subs.add(
      tl.Tracelet.onGeofence((evt) {
        _addLog('GEOFENCE', '${evt.action.name} → ${evt.identifier}');
      }),
    );

    _subs.add(
      tl.Tracelet.onGeofencesChange((evt) {
        _addLog(
          'GEOFENCES_CHANGE',
          'on=${evt.on.length}, off=${evt.off.length}',
        );
      }),
    );

    _subs.add(
      tl.Tracelet.onHeartbeat((evt) {
        _addLog(
          'HEARTBEAT',
          '${evt.location.coords.latitude.toStringAsFixed(4)}, ${evt.location.coords.longitude.toStringAsFixed(4)}',
        );
      }),
    );

    _subs.add(
      tl.Tracelet.onHttp((evt) {
        _addLog('HTTP', 'status=${evt.status}  success=${evt.success}');
      }),
    );

    _subs.add(
      tl.Tracelet.onSchedule((state) {
        _addLog('SCHEDULE', 'enabled=${state.enabled}');
      }),
    );

    _subs.add(
      tl.Tracelet.onPowerSaveChange((on) {
        _addLog('POWER_SAVE', on ? 'ON' : 'OFF');
      }),
    );

    _subs.add(
      tl.Tracelet.onConnectivityChange((evt) {
        _addLog('CONNECTIVITY', 'connected=${evt.connected}');
      }),
    );

    _subs.add(
      tl.Tracelet.onEnabledChange((on) {
        setState(() => _isTracking = on);
        _addLog('ENABLED', on ? 'ON' : 'OFF');
      }),
    );

    _subs.add(
      tl.Tracelet.onAuthorization((evt) {
        _addLog('AUTH', 'success=${evt.success}  response=${evt.response}');
      }),
    );
  }

  // ── Lifecycle ───────────────────────────────────────────────────────────

  Future<void> _initialize() async {
    try {
      _subscribeEvents();

      final state = await tl.Tracelet.ready(
        tl.Config(
          geo: const tl.GeoConfig(
            desiredAccuracy: tl.DesiredAccuracy.high,
            distanceFilter: 10,
            stationaryRadius: 25,
            locationTimeout: 60,
            activityType: tl.LocationActivityType.otherNavigation,
          ),
          app: const tl.AppConfig(
            stopOnTerminate: false,
            heartbeatInterval: 60,
          ),
          motion: const tl.MotionConfig(stopTimeout: 5),
          logger: const tl.LoggerConfig(
            logLevel: tl.LogLevel.verbose,
            debug: true,
          ),
        ),
      );

      setState(() {
        _isReady = true;
        _isTracking = state.enabled;
        _pluginState = state;
      });
      _addLog(
        'READY',
        'enabled=${state.enabled}  mode=${state.trackingMode.name}  odometer=${state.odometer.toStringAsFixed(0)}m',
      );
    } catch (e) {
      _addLog('ERROR', 'ready() failed: $e');
    }
  }

  Future<void> _start() async {
    try {
      final state = await tl.Tracelet.start();
      setState(() {
        _isTracking = state.enabled;
        _pluginState = state;
      });
      _addLog('START', 'enabled=${state.enabled}');
    } catch (e) {
      _addLog('ERROR', 'start() failed: $e');
    }
  }

  Future<void> _stop() async {
    try {
      final state = await tl.Tracelet.stop();
      setState(() {
        _isTracking = state.enabled;
        _pluginState = state;
      });
      _addLog('STOP', 'enabled=${state.enabled}');
    } catch (e) {
      _addLog('ERROR', 'stop() failed: $e');
    }
  }

  Future<void> _startGeofences() async {
    try {
      final state = await tl.Tracelet.startGeofences();
      setState(() {
        _isTracking = state.enabled;
        _pluginState = state;
      });
      _addLog('GEOFENCES_ONLY', 'started  mode=${state.trackingMode.name}');
    } catch (e) {
      _addLog('ERROR', 'startGeofences() failed: $e');
    }
  }

  // ── Location ────────────────────────────────────────────────────────────

  Future<void> _getCurrentPosition() async {
    try {
      final loc = await tl.Tracelet.getCurrentPosition(
        desiredAccuracy: tl.DesiredAccuracy.high,
        timeout: 30,
      );
      setState(() => _lastLocation = loc);
      _addLog(
        'POSITION',
        '${loc.coords.latitude.toStringAsFixed(6)}, ${loc.coords.longitude.toStringAsFixed(6)}  '
            'acc=${loc.coords.accuracy.toStringAsFixed(1)}m',
      );
    } catch (e) {
      _addLog('ERROR', 'getCurrentPosition() failed: $e');
    }
  }

  // ── One-Shot Location ───────────────────────────────────────────────────

  /// Fetch a single high-accuracy location using multi-sample collection.
  /// Collects 3 GPS samples and returns the one with best accuracy.
  /// `persist: false` means the result is NOT stored in the local database.
  Future<void> _singleFetchBestOfThree() async {
    try {
      // Ensure location permission is granted before requesting.
      final authStatus = await tl.Tracelet.requestPermission();
      if (authStatus < 2) {
        _addLog(
          'WARN',
          'Location permission denied (status=$authStatus). Cannot fetch location.',
        );
        return;
      }
      _addLog('ONE-SHOT', 'Requesting best-of-3 samples...');
      final loc = await tl.Tracelet.getCurrentPosition(
        desiredAccuracy: tl.DesiredAccuracy.high,
        timeout: 30,
        samples: 3,
        persist: false,
      );
      setState(() => _lastLocation = loc);
      _addLog(
        'ONE-SHOT',
        '${loc.coords.latitude.toStringAsFixed(6)}, ${loc.coords.longitude.toStringAsFixed(6)}  '
            'acc=${loc.coords.accuracy.toStringAsFixed(1)}m  (best of 3)',
      );
    } catch (e) {
      _addLog('ERROR', 'singleFetchBestOfThree() failed: $e');
    }
  }

  /// Retrieve the last known location from the OS cache.
  /// No GPS hardware is activated — returns instantly.
  /// Returns null if no cached location is available.
  Future<void> _getLastKnownLocation() async {
    try {
      final loc = await tl.Tracelet.getLastKnownLocation();
      if (loc == null) {
        _addLog('LAST_KNOWN', 'No cached location available');
        return;
      }
      setState(() => _lastLocation = loc);
      _addLog(
        'LAST_KNOWN',
        '${loc.coords.latitude.toStringAsFixed(6)}, ${loc.coords.longitude.toStringAsFixed(6)}  '
            'acc=${loc.coords.accuracy.toStringAsFixed(1)}m',
      );
    } catch (e) {
      _addLog('ERROR', 'getLastKnownLocation() failed: $e');
    }
  }

  /// Re-initialize plugin with foreground service DISABLED.
  /// On iOS this has no practical effect (no foreground service concept),
  /// but demonstrates the cross-platform API.
  Future<void> _initNoForeground() async {
    try {
      for (final s in _subs) {
        s.cancel();
      }
      _subs.clear();
      _subscribeEvents();

      // Request location permission up front so one-shot calls work.
      final authStatus = await tl.Tracelet.requestPermission();
      if (authStatus < 2) {
        _addLog(
          'WARN',
          'Location permission denied (status=$authStatus). One-shot calls will fail.',
        );
      }

      final state = await tl.Tracelet.ready(
        tl.Config(
          geo: const tl.GeoConfig(
            desiredAccuracy: tl.DesiredAccuracy.high,
            distanceFilter: 10,
          ),
          app: const tl.AppConfig(
            stopOnTerminate: true,
            foregroundService: tl.ForegroundServiceConfig(
              enabled: false,
            ),
          ),
          logger: const tl.LoggerConfig(
            logLevel: tl.LogLevel.verbose,
            debug: true,
          ),
        ),
      );

      setState(() {
        _isReady = true;
        _isTracking = state.enabled;
        _pluginState = state;
      });
      _addLog(
        'READY',
        'Initialized with foregroundService.enabled=false  enabled=${state.enabled}',
      );
    } catch (e) {
      _addLog('ERROR', 'initNoForeground() failed: $e');
    }
  }

  Future<void> _changePace() async {
    try {
      final newPace = !_isMoving;
      await tl.Tracelet.changePace(newPace);
      setState(() => _isMoving = newPace);
      _addLog('PACE', newPace ? 'forced MOVING' : 'forced STATIONARY');
    } catch (e) {
      _addLog('ERROR', 'changePace() failed: $e');
    }
  }

  Future<void> _getOdometer() async {
    try {
      final meters = await tl.Tracelet.getOdometer();
      _addLog('ODOMETER', '${meters.toStringAsFixed(1)} m');
    } catch (e) {
      _addLog('ERROR', 'getOdometer() failed: $e');
    }
  }

  Future<void> _resetOdometer() async {
    try {
      final loc = await tl.Tracelet.setOdometer(0);
      _addLog(
        'ODOMETER',
        'reset at ${loc.coords.latitude.toStringAsFixed(4)}, ${loc.coords.longitude.toStringAsFixed(4)}',
      );
    } catch (e) {
      _addLog('ERROR', 'setOdometer() failed: $e');
    }
  }

  // ── Geofencing ──────────────────────────────────────────────────────────

  Future<void> _addGeofenceAtCurrentLocation() async {
    if (_lastLocation == null) {
      _addLog('WARN', 'No location yet — get a position first');
      return;
    }
    try {
      final loc = _lastLocation!;
      final id = 'geo_${DateTime.now().millisecondsSinceEpoch}';
      await tl.Tracelet.addGeofence(
        tl.Geofence(
          identifier: id,
          latitude: loc.coords.latitude,
          longitude: loc.coords.longitude,
          radius: 200,
          notifyOnEntry: true,
          notifyOnExit: true,
          notifyOnDwell: true,
          loiteringDelay: 30000,
        ),
      );
      _addLog(
        'GEOFENCE+',
        '$id  r=200m  at ${loc.coords.latitude.toStringAsFixed(4)}, ${loc.coords.longitude.toStringAsFixed(4)}',
      );
    } catch (e) {
      _addLog('ERROR', 'addGeofence() failed: $e');
    }
  }

  Future<void> _listGeofences() async {
    try {
      final fences = await tl.Tracelet.getGeofences();
      _addLog('GEOFENCES', '${fences.length} registered');
      for (final f in fences) {
        _addLog(
          '  FENCE',
          '${f.identifier}  (${f.latitude.toStringAsFixed(4)}, ${f.longitude.toStringAsFixed(4)})  r=${f.radius}m',
        );
      }
    } catch (e) {
      _addLog('ERROR', 'getGeofences() failed: $e');
    }
  }

  Future<void> _removeAllGeofences() async {
    try {
      await tl.Tracelet.removeGeofences();
      _addLog('GEOFENCES', 'all removed');
    } catch (e) {
      _addLog('ERROR', 'removeGeofences() failed: $e');
    }
  }

  // ── Persistence ─────────────────────────────────────────────────────────

  Future<void> _getCount() async {
    try {
      final count = await tl.Tracelet.getCount();
      _addLog('DB', '$count locations stored');
    } catch (e) {
      _addLog('ERROR', 'getCount() failed: $e');
    }
  }

  Future<void> _getLocations() async {
    try {
      final locs = await tl.Tracelet.getLocations();
      _addLog('DB', '${locs.length} locations retrieved');
      for (final l in locs.take(5)) {
        _addLog(
          '  LOC',
          '${l.coords.latitude.toStringAsFixed(4)}, ${l.coords.longitude.toStringAsFixed(4)} @ ${l.timestamp}',
        );
      }
      if (locs.length > 5) {
        _addLog('  ...', '${locs.length - 5} more');
      }
    } catch (e) {
      _addLog('ERROR', 'getLocations() failed: $e');
    }
  }

  Future<void> _destroyLocations() async {
    try {
      await tl.Tracelet.destroyLocations();
      _addLog('DB', 'all locations destroyed');
    } catch (e) {
      _addLog('ERROR', 'destroyLocations() failed: $e');
    }
  }

  // ── Utility ─────────────────────────────────────────────────────────────

  Future<void> _getState() async {
    try {
      final state = await tl.Tracelet.getState();
      setState(() => _pluginState = state);
      _addLog(
        'STATE',
        'enabled=${state.enabled}  mode=${state.trackingMode.name}  '
            'odometer=${state.odometer.toStringAsFixed(0)}m  scheduler=${state.schedulerEnabled}',
      );
    } catch (e) {
      _addLog('ERROR', 'getState() failed: $e');
    }
  }

  Future<void> _getProviderState() async {
    try {
      final p = await tl.Tracelet.getProviderState();
      _addLog(
        'PROVIDER',
        'enabled=${p.enabled}  status=${p.status.name}  accuracy=${p.accuracyAuthorization.name}',
      );
    } catch (e) {
      _addLog('ERROR', 'getProviderState() failed: $e');
    }
  }

  Future<void> _requestPermission() async {
    try {
      final result = await tl.Tracelet.requestPermission();
      _addLog('PERMISSION', 'result=$result');
    } catch (e) {
      _addLog('ERROR', 'requestPermission() failed: $e');
    }
  }

  Future<void> _requestTempFullAccuracy() async {
    try {
      final result = await tl.Tracelet.requestTemporaryFullAccuracy(
        'TemporaryFullAccuracy',
      );
      _addLog('ACCURACY', 'temporary full accuracy result=$result');
    } catch (e) {
      _addLog('ERROR', 'requestTemporaryFullAccuracy() failed: $e');
    }
  }

  Future<void> _getSensors() async {
    try {
      final s = await tl.Tracelet.getSensors();
      _addLog(
        'SENSORS',
        'platform=${s.platform}  accelerometer=${s.accelerometer}  gyroscope=${s.gyroscope}  magnetometer=${s.magnetometer}  significantMotion=${s.significantMotion}',
      );
    } catch (e) {
      _addLog('ERROR', 'getSensors() failed: $e');
    }
  }

  Future<void> _getDeviceInfo() async {
    try {
      final d = await tl.Tracelet.getDeviceInfo();
      _addLog(
        'DEVICE',
        'model=${d.model}  platform=${d.platform}  version=${d.version}  manufacturer=${d.manufacturer}',
      );
    } catch (e) {
      _addLog('ERROR', 'getDeviceInfo() failed: $e');
    }
  }

  Future<void> _isPowerSaveMode() async {
    try {
      final on = await tl.Tracelet.isPowerSaveMode;
      _addLog('BATTERY', 'power save mode: ${on ? "ON" : "OFF"}');
    } catch (e) {
      _addLog('ERROR', 'isPowerSaveMode failed: $e');
    }
  }

  // ── Logging ─────────────────────────────────────────────────────────────

  Future<void> _getLog() async {
    try {
      final log = await tl.Tracelet.getLog();
      _addLog(
        'LOG',
        '${log.length} chars  (last 200): ${log.substring(log.length > 200 ? log.length - 200 : 0)}',
      );
    } catch (e) {
      _addLog('ERROR', 'getLog() failed: $e');
    }
  }

  Future<void> _destroyLog() async {
    try {
      await tl.Tracelet.destroyLog();
      _addLog('LOG', 'destroyed');
    } catch (e) {
      _addLog('ERROR', 'destroyLog() failed: $e');
    }
  }

  Future<void> _emailLog() async {
    try {
      await tl.Tracelet.emailLog('test@example.com');
      _addLog('LOG', 'email sent');
    } catch (e) {
      _addLog('ERROR', 'emailLog() failed: $e');
    }
  }

  Future<void> _httpSync() async {
    try {
      final locs = await tl.Tracelet.sync();
      _addLog('SYNC', '${locs.length} locations synced');
    } catch (e) {
      _addLog('ERROR', 'sync() failed: $e');
    }
  }

  // ── Build ───────────────────────────────────────────────────────────────

  @override
  Widget build(BuildContext context) {
    final cs = Theme.of(context).colorScheme;

    return Scaffold(
      appBar: AppBar(
        title: const Text('Tracelet iOS'),
        centerTitle: true,
        actions: [
          IconButton(
            tooltip: 'Clear log',
            onPressed: () => setState(() => _log.clear()),
            icon: const Icon(Icons.delete_sweep),
          ),
        ],
      ),
      body: Column(
        children: [
          // ─── Status Card ───────────────────────────────────────────────
          _StatusCard(
            isReady: _isReady,
            isTracking: _isTracking,
            isMoving: _isMoving,
            location: _lastLocation,
            state: _pluginState,
          ),

          // ─── Action Sections ───────────────────────────────────────────
          Expanded(
            child: ListView(
              padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
              children: [
                // ── Init ──
                if (!_isReady)
                  Padding(
                    padding: const EdgeInsets.symmetric(vertical: 8),
                    child: FilledButton.icon(
                      onPressed: _initialize,
                      icon: const Icon(Icons.power_settings_new),
                      label: const Text('Initialize Tracelet'),
                    ),
                  ),

                if (_isReady) ...[
                  // ── Lifecycle ──
                  _Section(
                    title: 'Lifecycle',
                    color: cs.primary,
                    children: [
                      _Chip('Start', Icons.play_arrow, _start),
                      _Chip('Stop', Icons.stop, _stop),
                      _Chip('Geofences Only', Icons.fence, _startGeofences),
                      _Chip('Get State', Icons.info_outline, _getState),
                    ],
                  ),

                  // ── Permissions ──
                  _Section(
                    title: 'Permissions',
                    color: cs.tertiary,
                    children: [
                      _Chip('Request Perm', Icons.shield, _requestPermission),
                      _Chip(
                        'Temp Full Accuracy',
                        Icons.gps_fixed,
                        _requestTempFullAccuracy,
                      ),
                      _Chip(
                        'Provider State',
                        Icons.settings_input_antenna,
                        _getProviderState,
                      ),
                    ],
                  ),

                  // ── Location ──
                  _Section(
                    title: 'Location',
                    color: cs.secondary,
                    children: [
                      _Chip(
                        'Get Position',
                        Icons.my_location,
                        _getCurrentPosition,
                      ),
                      _Chip(
                        _isMoving ? 'Pace → Still' : 'Pace → Move',
                        Icons.directions_walk,
                        _changePace,
                      ),
                      _Chip('Odometer', Icons.speed, _getOdometer),
                      _Chip('Reset Odo', Icons.restart_alt, _resetOdometer),
                    ],
                  ),

                  // ── One-Shot Location ──
                  _Section(
                    title: 'One-Shot Location',
                    color: Colors.deepPurple,
                    children: [
                      _Chip(
                        'Best of 3',
                        Icons.gps_fixed,
                        _singleFetchBestOfThree,
                      ),
                      _Chip(
                        'Last Known',
                        Icons.history,
                        _getLastKnownLocation,
                      ),
                      _Chip(
                        'Init No FG',
                        Icons.notifications_off,
                        _initNoForeground,
                      ),
                    ],
                  ),

                  // ── Geofencing ──
                  _Section(
                    title: 'Geofencing',
                    color: Colors.orange,
                    children: [
                      _Chip(
                        '+ Geofence Here',
                        Icons.add_location_alt,
                        _addGeofenceAtCurrentLocation,
                      ),
                      _Chip('List Geofences', Icons.list, _listGeofences),
                      _Chip(
                        'Remove All',
                        Icons.delete_forever,
                        _removeAllGeofences,
                      ),
                    ],
                  ),

                  // ── Persistence ──
                  _Section(
                    title: 'Persistence',
                    color: Colors.teal,
                    children: [
                      _Chip('Count', Icons.numbers, _getCount),
                      _Chip('List Locations', Icons.storage, _getLocations),
                      _Chip('Destroy All', Icons.delete, _destroyLocations),
                      _Chip('HTTP Sync', Icons.cloud_upload, _httpSync),
                    ],
                  ),

                  // ── Utility ──
                  _Section(
                    title: 'Utility',
                    color: Colors.purple,
                    children: [
                      _Chip('Sensors', Icons.sensors, _getSensors),
                      _Chip('Device Info', Icons.phone_android, _getDeviceInfo),
                      _Chip(
                        'Power Save?',
                        Icons.battery_saver,
                        _isPowerSaveMode,
                      ),
                    ],
                  ),

                  // ── Logging ──
                  _Section(
                    title: 'Logging',
                    color: Colors.brown,
                    children: [
                      _Chip('Get Log', Icons.article, _getLog),
                      _Chip('Destroy Log', Icons.delete_outline, _destroyLog),
                      _Chip('Email Log', Icons.email, _emailLog),
                    ],
                  ),

                  const Divider(),

                  // ── Event Log ──
                  Padding(
                    padding: const EdgeInsets.only(bottom: 4),
                    child: Text(
                      'Event Log (${_log.length})',
                      style: Theme.of(context).textTheme.titleSmall,
                    ),
                  ),
                  ..._log.map((entry) => _LogTile(entry: entry)),
                  const SizedBox(height: 80),
                ],
              ],
            ),
          ),
        ],
      ),
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Status Card widget
// ─────────────────────────────────────────────────────────────────────────────

class _StatusCard extends StatelessWidget {
  const _StatusCard({
    required this.isReady,
    required this.isTracking,
    required this.isMoving,
    this.location,
    this.state,
  });

  final bool isReady;
  final bool isTracking;
  final bool isMoving;
  final tl.Location? location;
  final tl.State? state;

  @override
  Widget build(BuildContext context) {
    final cs = Theme.of(context).colorScheme;
    final bg = isTracking
        ? (isMoving ? cs.primaryContainer : cs.tertiaryContainer)
        : cs.surfaceContainerHighest;

    return Card(
      margin: const EdgeInsets.all(12),
      color: bg,
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Icon(
                  isTracking
                      ? (isMoving
                            ? Icons.directions_run
                            : Icons.accessibility_new)
                      : Icons.location_off,
                  size: 32,
                ),
                const SizedBox(width: 8),
                Text(
                  !isReady
                      ? 'Not Initialized'
                      : isTracking
                      ? (isMoving ? 'MOVING' : 'STATIONARY')
                      : 'STOPPED',
                  style: Theme.of(context).textTheme.titleMedium?.copyWith(
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ],
            ),
            if (location != null) ...[
              const SizedBox(height: 8),
              Text(
                '${location!.coords.latitude.toStringAsFixed(6)}, '
                '${location!.coords.longitude.toStringAsFixed(6)}',
                style: Theme.of(
                  context,
                ).textTheme.bodyLarge?.copyWith(fontFamily: 'monospace'),
              ),
              const SizedBox(height: 4),
              Wrap(
                spacing: 16,
                children: [
                  Text('Acc: ${location!.coords.accuracy.toStringAsFixed(1)}m'),
                  Text('Spd: ${location!.coords.speed.toStringAsFixed(1)} m/s'),
                  Text('Alt: ${location!.coords.altitude.toStringAsFixed(0)}m'),
                  if (state != null)
                    Text('Odo: ${state!.odometer.toStringAsFixed(0)}m'),
                ],
              ),
            ],
          ],
        ),
      ),
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Section header with chips
// ─────────────────────────────────────────────────────────────────────────────

class _Section extends StatelessWidget {
  const _Section({
    required this.title,
    required this.color,
    required this.children,
  });

  final String title;
  final Color color;
  final List<_Chip> children;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(top: 8, bottom: 4),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            title,
            style: Theme.of(context).textTheme.labelLarge?.copyWith(
              color: color,
              fontWeight: FontWeight.bold,
            ),
          ),
          const SizedBox(height: 4),
          Wrap(
            spacing: 6,
            runSpacing: 6,
            children: children.map((c) {
              return ActionChip(
                avatar: Icon(c.icon, size: 18),
                label: Text(c.label),
                onPressed: c.onPressed,
              );
            }).toList(),
          ),
        ],
      ),
    );
  }
}

class _Chip {
  const _Chip(this.label, this.icon, this.onPressed);
  final String label;
  final IconData icon;
  final VoidCallback onPressed;
}

// ─────────────────────────────────────────────────────────────────────────────
// Log entry model & tile
// ─────────────────────────────────────────────────────────────────────────────

class _LogEntry {
  const _LogEntry(this.time, this.tag, this.message);
  final String time;
  final String tag;
  final String message;
}

class _LogTile extends StatelessWidget {
  const _LogTile({required this.entry});
  final _LogEntry entry;

  Color _tagColor(String tag) {
    return switch (tag) {
      'LOCATION' => Colors.blue,
      'MOTION' => Colors.deepOrange,
      'ACTIVITY' => Colors.purple,
      'PROVIDER' => Colors.teal,
      'GEOFENCE' || 'GEOFENCES_CHANGE' || 'GEOFENCE+' => Colors.orange,
      'HTTP' || 'SYNC' => Colors.cyan,
      'HEARTBEAT' => Colors.pink,
      'ERROR' || 'WARN' => Colors.red,
      'READY' || 'START' || 'STOP' => Colors.green,
      _ => Colors.grey,
    };
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 1),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          SizedBox(
            width: 52,
            child: Text(
              entry.time,
              style: const TextStyle(
                fontSize: 11,
                color: Colors.grey,
                fontFamily: 'monospace',
              ),
            ),
          ),
          Container(
            padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
            decoration: BoxDecoration(
              color: _tagColor(entry.tag).withAlpha(30),
              borderRadius: BorderRadius.circular(3),
            ),
            child: Text(
              entry.tag,
              style: TextStyle(
                fontSize: 10,
                fontWeight: FontWeight.bold,
                color: _tagColor(entry.tag),
              ),
            ),
          ),
          const SizedBox(width: 4),
          Expanded(
            child: Text(
              entry.message,
              style: const TextStyle(fontSize: 11, fontFamily: 'monospace'),
            ),
          ),
        ],
      ),
    );
  }
}
1
likes
0
points
243
downloads

Publisher

verified publisherikolvi.com

Weekly Downloads

iOS implementation of the Tracelet background geolocation plugin.

Homepage
Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter, tracelet_platform_interface

More

Packages that depend on tracelet_ios

Packages that implement tracelet_ios