feat: watch together recent rooms, plex usernames, action toasts, shorter codes
close #855
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import '../../services/settings_service.dart';
|
||||
|
||||
class RecentRoom {
|
||||
final String code;
|
||||
final String? name;
|
||||
final DateTime lastUsed;
|
||||
|
||||
const RecentRoom({required this.code, this.name, required this.lastUsed});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'code': code,
|
||||
if (name != null) 'name': name,
|
||||
'lastUsed': lastUsed.millisecondsSinceEpoch,
|
||||
};
|
||||
|
||||
factory RecentRoom.fromJson(Map<String, dynamic> json) => RecentRoom(
|
||||
code: json['code'] as String,
|
||||
name: json['name'] as String?,
|
||||
lastUsed: DateTime.fromMillisecondsSinceEpoch(json['lastUsed'] as int),
|
||||
);
|
||||
|
||||
RecentRoom copyWith({String? code, String? name, DateTime? lastUsed, bool clearName = false}) => RecentRoom(
|
||||
code: code ?? this.code,
|
||||
name: clearName ? null : (name ?? this.name),
|
||||
lastUsed: lastUsed ?? this.lastUsed,
|
||||
);
|
||||
}
|
||||
|
||||
class RecentRoomsService {
|
||||
static const int _maxRooms = 20;
|
||||
|
||||
static List<RecentRoom> getRecentRooms() {
|
||||
final json = SettingsService.instanceOrNull?.getRecentRooms();
|
||||
if (json == null) return [];
|
||||
try {
|
||||
final list = jsonDecode(json) as List<dynamic>;
|
||||
final rooms = list.map((e) => RecentRoom.fromJson(e as Map<String, dynamic>)).toList();
|
||||
rooms.sort((a, b) => b.lastUsed.compareTo(a.lastUsed));
|
||||
return rooms;
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _save(List<RecentRoom> rooms) async {
|
||||
rooms.sort((a, b) => b.lastUsed.compareTo(a.lastUsed));
|
||||
if (rooms.length > _maxRooms) rooms.removeRange(_maxRooms, rooms.length);
|
||||
await SettingsService.instanceOrNull?.setRecentRooms(jsonEncode(rooms.map((r) => r.toJson()).toList()));
|
||||
}
|
||||
|
||||
static Future<void> addOrUpdateRoom(String code, {String? name}) async {
|
||||
final rooms = getRecentRooms();
|
||||
final index = rooms.indexWhere((r) => r.code == code);
|
||||
if (index >= 0) {
|
||||
rooms[index] = rooms[index].copyWith(lastUsed: DateTime.now(), name: name ?? rooms[index].name);
|
||||
} else {
|
||||
rooms.add(RecentRoom(code: code, name: name, lastUsed: DateTime.now()));
|
||||
}
|
||||
await _save(rooms);
|
||||
}
|
||||
|
||||
static Future<void> removeRoom(String code) async {
|
||||
final rooms = getRecentRooms();
|
||||
rooms.removeWhere((r) => r.code == code);
|
||||
await _save(rooms);
|
||||
}
|
||||
|
||||
static Future<void> renameRoom(String code, String? name) async {
|
||||
final rooms = getRecentRooms();
|
||||
final index = rooms.indexWhere((r) => r.code == code);
|
||||
if (index >= 0) {
|
||||
rooms[index] = rooms[index].copyWith(name: name, clearName: name == null);
|
||||
await _save(rooms);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:uuid/uuid.dart';
|
||||
import '../../utils/future_extensions.dart';
|
||||
@@ -98,9 +99,11 @@ class WatchTogetherPeerService with KeepaliveMixin {
|
||||
/// List of connected peer IDs
|
||||
List<String> get connectedPeers => _connectedPeers.toList();
|
||||
|
||||
/// Generate a short, readable session ID
|
||||
String _generateSessionId() {
|
||||
return const Uuid().v4().substring(0, 8).toUpperCase();
|
||||
/// Generate a short, readable session ID (5 alphanumeric chars)
|
||||
static String _generateSessionId() {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
final random = Random.secure();
|
||||
return String.fromCharCodes(List.generate(5, (_) => chars.codeUnitAt(random.nextInt(chars.length))));
|
||||
}
|
||||
|
||||
/// Connect to the relay WebSocket and set up the message listener.
|
||||
@@ -308,13 +311,14 @@ class WatchTogetherPeerService with KeepaliveMixin {
|
||||
/// Create a new session as host
|
||||
///
|
||||
/// Returns the session ID that others can use to join.
|
||||
Future<String> createSession() async {
|
||||
/// If [sessionId] is provided, uses that instead of generating a new one.
|
||||
Future<String> createSession({String? sessionId}) async {
|
||||
if (_channel != null) {
|
||||
await disconnect();
|
||||
}
|
||||
|
||||
_isHost = true;
|
||||
_sessionId = _generateSessionId();
|
||||
_sessionId = sessionId?.toUpperCase() ?? _generateSessionId();
|
||||
_myPeerId = 'wt-$_sessionId';
|
||||
_reconnectAttempts = 0;
|
||||
|
||||
@@ -380,6 +384,20 @@ class WatchTogetherPeerService with KeepaliveMixin {
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to join a session; if it doesn't exist, create it as host.
|
||||
///
|
||||
/// Returns `true` if the user became the host (room was empty).
|
||||
Future<bool> joinOrCreateSession(String sessionId) async {
|
||||
try {
|
||||
await joinSession(sessionId);
|
||||
return false; // joined as guest
|
||||
} on PeerError catch (e) {
|
||||
if (e.type != PeerErrorType.serverError) rethrow;
|
||||
await createSession(sessionId: sessionId);
|
||||
return true; // created as host
|
||||
}
|
||||
}
|
||||
|
||||
/// Broadcast a message to all connected peers
|
||||
void broadcast(SyncMessage message) {
|
||||
final payload = message.toJson();
|
||||
|
||||
Reference in New Issue
Block a user