/// Ported from `com.rippr.Config`. /// /// Runtime configuration. The upload endpoint is intentionally empty by default — /// recording must work with no server at all, and uploading is opt-in. library; import 'dart:io'; import 'dart:math'; import 'package:shared_preferences/shared_preferences.dart'; import '../domain/models.dart' show UnitSystem; const _keyEndpoint = 'upload_endpoint'; const _keyDeviceId = 'device_id'; const _keyMapEnabled = 'map_enabled'; const _keyUnitSystem = 'unit_system'; /// Countries that did not adopt metric for everyday distances. Not exhaustive — a /// best-effort default, not a claim of authority. Anyone can override it in Settings. const _imperialCountryCodes = {'US', 'LR', 'MM'}; class Config { Config(this._prefs); final SharedPreferences _prefs; static Future load() async => Config(await SharedPreferences.getInstance()); String get uploadEndpoint => _prefs.getString(_keyEndpoint) ?? ''; Future setUploadEndpoint(String url) => _prefs.setString(_keyEndpoint, url.trim()); /// Whether trip detail renders a map at all. /// /// When off the map widget is never created, so no tile is ever requested — a real /// short-circuit, not a hidden view. The map only ever exists inside a visible screen's /// lifecycle; the recorder never touches it. bool get mapEnabled => _prefs.getBool(_keyMapEnabled) ?? true; Future setMapEnabled(bool enabled) => _prefs.setBool(_keyMapEnabled, enabled); /// Metric unless the device's own locale says otherwise. Explicit user choice, once /// made, always wins over the locale guess. UnitSystem get unitSystem { final stored = _prefs.getString(_keyUnitSystem); if (stored != null) { return UnitSystem.values.firstWhere( (u) => u.name == stored, orElse: () => _localeDefaultUnitSystem(), ); } return _localeDefaultUnitSystem(); } Future setUnitSystem(UnitSystem system) => _prefs.setString(_keyUnitSystem, system.name); static UnitSystem _localeDefaultUnitSystem() { // "en_US", "en_US.UTF-8" and similar; the region is the second underscore- or // hyphen-delimited segment when present. final parts = Platform.localeName.split(RegExp('[_-]')); final region = parts.length > 1 ? parts[1].toUpperCase() : ''; return _imperialCountryCodes.contains(region) ? UnitSystem.imperial : UnitSystem.metric; } /// Stable per-install id so a server can distinguish riders in a group. String get deviceId { final existing = _prefs.getString(_keyDeviceId); if (existing != null) return existing; final generated = _randomId(); // Fire-and-forget: the value is returned immediately either way, and a lost write // only costs a new id next launch. _prefs.setString(_keyDeviceId, generated); return generated; } /// A UUID v4, without pulling in a package for sixteen bytes. static String _randomId() { final rng = Random.secure(); final bytes = List.generate(16, (_) => rng.nextInt(256)); bytes[6] = (bytes[6] & 0x0f) | 0x40; bytes[8] = (bytes[8] & 0x3f) | 0x80; String hex(int start, int end) => bytes .sublist(start, end) .map((b) => b.toRadixString(16).padLeft(2, '0')) .join(); return '${hex(0, 4)}-${hex(4, 6)}-${hex(6, 8)}-${hex(8, 10)}-${hex(10, 16)}'; } }