import 'dart:convert'; import 'package:json_annotation/json_annotation.dart'; import 'package:crypto/crypto.dart'; import 'package:flutter/foundation.dart'; import '../../services/settings_service.dart'; import '../models/watch_session.dart'; import 'watch_together_relay_endpoint.dart'; part 'recent_rooms_service.g.dart'; @JsonSerializable(includeIfNull: false) class RecentRoom { final String code; final String relayScope; final String? name; @JsonKey(fromJson: _dateTimeFromMillis, toJson: _dateTimeToMillis) final DateTime lastUsed; @JsonKey(fromJson: _controlModeFromIndex, toJson: _controlModeToIndex) final ControlMode? controlMode; const RecentRoom({required this.code, required this.relayScope, this.name, required this.lastUsed, this.controlMode}); Map toJson() => _$RecentRoomToJson(this); factory RecentRoom.fromJson(Map json) => _$RecentRoomFromJson(json); RecentRoom copyWith({ String? code, String? relayScope, String? name, DateTime? lastUsed, ControlMode? controlMode, bool clearName = false, }) => RecentRoom( code: code ?? this.code, relayScope: relayScope ?? this.relayScope, name: clearName ? null : (name ?? this.name), lastUsed: lastUsed ?? this.lastUsed, controlMode: controlMode ?? this.controlMode, ); } DateTime _dateTimeFromMillis(int value) => DateTime.fromMillisecondsSinceEpoch(value); int _dateTimeToMillis(DateTime value) => value.millisecondsSinceEpoch; ControlMode? _controlModeFromIndex(int? index) { if (index == null || index < 0 || index >= ControlMode.values.length) return null; return ControlMode.values[index]; } int? _controlModeToIndex(ControlMode? value) => value?.index; class RecentRoomsService { static const int _maxRooms = 20; static List getRecentRooms({required String profileId, required WatchTogetherRelayEndpoint endpoint}) { final scope = _scopeFor(endpoint); return _load(profileId).where((room) => room.relayScope == scope).toList(growable: false); } static List _load(String profileId) { final settings = SettingsService.instanceOrNull; if (settings == null) return []; final json = settings.read(SettingsService.recentRoomsForProfile(profileId)); if (json == null) return []; try { final list = jsonDecode(json) as List; final rooms = list.map((entry) => RecentRoom.fromJson(entry as Map)).toList(); rooms.sort((a, b) => b.lastUsed.compareTo(a.lastUsed)); return rooms; } catch (_) { return []; } } static Future _save(String profileId, List rooms) async { rooms.sort((a, b) => b.lastUsed.compareTo(a.lastUsed)); if (rooms.length > _maxRooms) { rooms.removeRange(_maxRooms, rooms.length); } await SettingsService.instanceOrNull?.write( SettingsService.recentRoomsForProfile(profileId), jsonEncode(rooms.map((room) => room.toJson()).toList()), ); } static Future addOrUpdateRoom( String code, { required String profileId, required WatchTogetherRelayEndpoint endpoint, String? name, ControlMode? controlMode, }) async { final scope = _scopeFor(endpoint); final rooms = _load(profileId); final index = rooms.indexWhere((room) => room.relayScope == scope && room.code == code); if (index >= 0) { rooms[index] = rooms[index].copyWith( lastUsed: DateTime.now(), name: name ?? rooms[index].name, controlMode: controlMode, ); } else { rooms.add( RecentRoom(code: code, relayScope: scope, name: name, lastUsed: DateTime.now(), controlMode: controlMode), ); } await _save(profileId, rooms); } static Future removeRoom( String code, { required String profileId, required WatchTogetherRelayEndpoint endpoint, }) async { final scope = _scopeFor(endpoint); final rooms = _load(profileId)..removeWhere((room) => room.relayScope == scope && room.code == code); await _save(profileId, rooms); } static Future renameRoom( String code, String? name, { required String profileId, required WatchTogetherRelayEndpoint endpoint, }) async { final scope = _scopeFor(endpoint); final rooms = _load(profileId); final index = rooms.indexWhere((room) => room.relayScope == scope && room.code == code); if (index >= 0) { rooms[index] = rooms[index].copyWith(name: name, clearName: name == null); await _save(profileId, rooms); } } @visibleForTesting static String relayScopeFor(WatchTogetherRelayEndpoint endpoint) => _scopeFor(endpoint); static String _scopeFor(WatchTogetherRelayEndpoint endpoint) => sha256.convert(utf8.encode(endpoint.canonicalBaseUrl)).toString(); }