import 'dart:convert'; import 'dart:math'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../runtime/runtime_models.dart'; class WebStore { static const settingsKey = 'xworkmate.web.settings.snapshot'; static const threadsKey = 'xworkmate.web.assistant.threads'; static const aiGatewayApiKeyKey = 'xworkmate.web.ai_gateway.api_key'; // Legacy remote-only keys (kept for migration fallback). static const relayTokenKey = 'xworkmate.web.relay.token'; static const relayPasswordKey = 'xworkmate.web.relay.password'; static const relayTokenProfilePrefix = 'xworkmate.web.relay.token.'; static const relayPasswordProfilePrefix = 'xworkmate.web.relay.password.'; static const relayDeviceIdentityKey = 'xworkmate.web.relay.device_identity'; static const sessionClientIdKey = 'xworkmate.web.session.client_id'; static const themeModeKey = 'xworkmate.web.theme_mode'; SharedPreferences? _prefs; Future initialize() async { _prefs ??= await SharedPreferences.getInstance(); } Future loadSettingsSnapshot() async { await initialize(); return SettingsSnapshot.fromJsonString(_prefs!.getString(settingsKey)); } Future saveSettingsSnapshot(SettingsSnapshot snapshot) async { await initialize(); await _prefs!.setString(settingsKey, snapshot.toJsonString()); } Future> loadAssistantThreadRecords() async { await initialize(); final raw = _prefs!.getString(threadsKey); if (raw == null || raw.trim().isEmpty) { return const []; } try { final decoded = jsonDecode(raw) as List; return decoded .whereType() .map( (item) => AssistantThreadRecord.fromJson(item.cast()), ) .toList(growable: false); } catch (_) { return const []; } } Future saveAssistantThreadRecords( List records, ) async { await initialize(); await _prefs!.setString( threadsKey, jsonEncode(records.map((item) => item.toJson()).toList(growable: false)), ); } Future loadAiGatewayApiKey() async { await initialize(); return (_prefs!.getString(aiGatewayApiKeyKey) ?? '').trim(); } Future saveAiGatewayApiKey(String value) async { await initialize(); await _prefs!.setString(aiGatewayApiKeyKey, value.trim()); } Future loadRelayToken({int? profileIndex}) async { await initialize(); final scopedKey = _relayTokenScopedKey(profileIndex); final scoped = (_prefs!.getString(scopedKey) ?? '').trim(); if (scoped.isNotEmpty) { return scoped; } // Backward compatibility: old builds persisted a single remote token. if (profileIndex == null || profileIndex == kGatewayRemoteProfileIndex) { return (_prefs!.getString(relayTokenKey) ?? '').trim(); } return ''; } Future saveRelayToken(String value, {int? profileIndex}) async { await initialize(); final trimmed = value.trim(); await _prefs!.setString(_relayTokenScopedKey(profileIndex), trimmed); if (profileIndex == null || profileIndex == kGatewayRemoteProfileIndex) { await _prefs!.setString(relayTokenKey, trimmed); } } Future loadRelayPassword({int? profileIndex}) async { await initialize(); final scopedKey = _relayPasswordScopedKey(profileIndex); final scoped = (_prefs!.getString(scopedKey) ?? '').trim(); if (scoped.isNotEmpty) { return scoped; } // Backward compatibility: old builds persisted a single remote password. if (profileIndex == null || profileIndex == kGatewayRemoteProfileIndex) { return (_prefs!.getString(relayPasswordKey) ?? '').trim(); } return ''; } Future saveRelayPassword(String value, {int? profileIndex}) async { await initialize(); final trimmed = value.trim(); await _prefs!.setString(_relayPasswordScopedKey(profileIndex), trimmed); if (profileIndex == null || profileIndex == kGatewayRemoteProfileIndex) { await _prefs!.setString(relayPasswordKey, trimmed); } } Future loadOrCreateWebSessionClientId() async { await initialize(); final existing = (_prefs!.getString(sessionClientIdKey) ?? '').trim(); if (existing.isNotEmpty) { return existing; } final next = _generateClientId(); await _prefs!.setString(sessionClientIdKey, next); return next; } Future loadRelayDeviceIdentity() async { await initialize(); final raw = _prefs!.getString(relayDeviceIdentityKey); if (raw == null || raw.trim().isEmpty) { return null; } try { return LocalDeviceIdentity.fromJson( (jsonDecode(raw) as Map).cast(), ); } catch (_) { return null; } } Future saveRelayDeviceIdentity(LocalDeviceIdentity identity) async { await initialize(); await _prefs!.setString( relayDeviceIdentityKey, jsonEncode(identity.toJson()), ); } Future loadThemeMode() async { await initialize(); return switch ((_prefs!.getString(themeModeKey) ?? '').trim()) { 'dark' => ThemeMode.dark, 'system' => ThemeMode.system, _ => ThemeMode.light, }; } Future saveThemeMode(ThemeMode mode) async { await initialize(); await _prefs!.setString(themeModeKey, mode.name); } static String? maskValue(String value) { final trimmed = value.trim(); if (trimmed.isEmpty) { return null; } if (trimmed.length <= 4) { return '*' * trimmed.length; } return '${trimmed.substring(0, 2)}${'*' * (trimmed.length - 4)}${trimmed.substring(trimmed.length - 2)}'; } static String _generateClientId() { final random = Random(); final timestamp = DateTime.now().microsecondsSinceEpoch.toRadixString(36); final suffix = List.generate( 4, (_) => random.nextInt(1 << 16).toRadixString(16).padLeft(4, '0'), growable: false, ).join(); return 'web-$timestamp-$suffix'; } static String _relayTokenScopedKey(int? profileIndex) { final resolved = profileIndex ?? kGatewayRemoteProfileIndex; return '$relayTokenProfilePrefix$resolved'; } static String _relayPasswordScopedKey(int? profileIndex) { final resolved = profileIndex ?? kGatewayRemoteProfileIndex; return '$relayPasswordProfilePrefix$resolved'; } }