feat: watch together recent rooms, plex usernames, action toasts, shorter codes

close #855
This commit is contained in:
edde746
2026-04-13 14:02:21 +02:00
parent 17eb37cb37
commit b81f6eaa96
38 changed files with 748 additions and 139 deletions
@@ -50,6 +50,9 @@ class WatchTogetherProvider with ChangeNotifier {
Timer? _hostReconnectTimer;
bool _isWaitingForHostReconnect = false;
// Debounce map for action events (peerId+type → last emission timestamp)
final Map<String, int> _lastActionEventMs = {};
/// Generate a random display name for this session
static String _generateDisplayName() {
const adjectives = ['Happy', 'Sleepy', 'Sunny', 'Cozy', 'Chill', 'Swift', 'Brave', 'Calm', 'Jolly', 'Lucky'];
@@ -180,6 +183,8 @@ class WatchTogetherProvider with ChangeNotifier {
/// Create a new watch together session as host
Future<String> createSession({
required ControlMode controlMode,
String? displayName,
String? sessionId,
String? mediaRatingKey,
String? mediaServerId,
String? mediaTitle,
@@ -195,10 +200,10 @@ class WatchTogetherProvider with ChangeNotifier {
_setupPeerServiceListeners();
try {
final sessionId = await _peerService!.createSession();
final createdSessionId = await _peerService!.createSession(sessionId: sessionId);
_session = WatchSession.createAsHost(
sessionId: sessionId,
sessionId: createdSessionId,
hostPeerId: _peerService!.myPeerId!,
controlMode: controlMode,
mediaRatingKey: mediaRatingKey,
@@ -206,8 +211,7 @@ class WatchTogetherProvider with ChangeNotifier {
mediaTitle: mediaTitle,
).copyWith(state: SessionState.connected);
// Generate a random display name and add self to participants
_displayName = _generateDisplayName();
_displayName = displayName ?? _generateDisplayName();
_participants.add(Participant(peerId: _peerService!.myPeerId!, displayName: _displayName, isHost: true));
_syncManager = WatchTogetherSyncManager(
@@ -219,9 +223,9 @@ class WatchTogetherProvider with ChangeNotifier {
_wireSyncStateChanges();
notifyListeners();
appLogger.d('WatchTogether: Session created: $sessionId');
appLogger.d('WatchTogether: Session created: $createdSessionId');
return sessionId;
return createdSessionId;
} catch (e) {
appLogger.e('WatchTogether: Failed to create session', error: e);
_session = _session?.copyWith(state: SessionState.error, errorMessage: e.toString());
@@ -231,7 +235,7 @@ class WatchTogetherProvider with ChangeNotifier {
}
/// Join an existing session as guest
Future<void> joinSession(String sessionId) async {
Future<void> joinSession(String sessionId, {String? displayName}) async {
// Clean up any existing session
await leaveSession();
_lastHandledCurrentPlaybackKey = null;
@@ -251,8 +255,7 @@ class WatchTogetherProvider with ChangeNotifier {
// Session will be fully configured when we receive sessionConfig from host
_session = _session!.copyWith(state: SessionState.connected, hostPeerId: 'wt-${sessionId.toUpperCase()}');
// Generate a random display name for this session
_displayName = _generateDisplayName();
_displayName = displayName ?? _generateDisplayName();
_syncManager = WatchTogetherSyncManager(
peerService: _peerService!,
@@ -285,6 +288,32 @@ class WatchTogetherProvider with ChangeNotifier {
}
}
/// Enter a room by code — joins if it exists, creates if empty.
///
/// Returns `true` if the user became the host.
Future<bool> enterRoom(String sessionId, {ControlMode controlMode = ControlMode.anyone, String? displayName}) async {
// Probe the relay to determine if the room exists, then delegate to
// the existing createSession / joinSession which handle all setup.
final customRelayUrl = SettingsService.instanceOrNull?.getCustomRelayUrl();
final probe = WatchTogetherPeerService(customBaseUrl: customRelayUrl);
try {
final becameHost = await probe.joinOrCreateSession(sessionId);
await probe.disconnect();
probe.dispose();
if (becameHost) {
await createSession(controlMode: controlMode, displayName: displayName, sessionId: sessionId);
} else {
await joinSession(sessionId, displayName: displayName);
}
return becameHost;
} catch (e) {
await probe.disconnect();
probe.dispose();
rethrow;
}
}
/// Leave the current session
Future<void> leaveSession() async {
if (_session == null) return;
@@ -320,6 +349,7 @@ class WatchTogetherProvider with ChangeNotifier {
_participants.clear();
_isSyncing = false;
_lastHandledCurrentPlaybackKey = null;
_lastActionEventMs.clear();
notifyListeners();
appLogger.d('WatchTogether: Session left');
@@ -493,11 +523,39 @@ class WatchTogetherProvider with ChangeNotifier {
// Handled at sync manager level (host responds with config)
break;
case SyncMessageType.play:
_emitActionEvent(message.peerId, ParticipantEventType.resumed);
break;
case SyncMessageType.pause:
_emitActionEvent(message.peerId, ParticipantEventType.paused);
break;
case SyncMessageType.seek:
_emitActionEvent(message.peerId, ParticipantEventType.seeked);
break;
default:
break;
}
}
/// Emit an action event for a remote peer (with 1s debounce per peer+type)
void _emitActionEvent(String? peerId, ParticipantEventType type) {
if (peerId == null || peerId == _peerService?.myPeerId) return;
final key = '$peerId:${type.name}';
final now = DateTime.now().millisecondsSinceEpoch;
final last = _lastActionEventMs[key] ?? 0;
if (now - last < 1000) return;
_lastActionEventMs[key] = now;
final name = _participants.where((p) => p.peerId == peerId).map((p) => p.displayName).firstOrNull;
if (name != null) {
_participantEventController.add(ParticipantEvent(displayName: name, type: type));
}
}
/// Handle session config from host (guest only)
/// This is handled at provider level so it's processed even before player is attached
void _handleSessionConfig(SyncMessage message) {
@@ -679,7 +737,7 @@ class WatchTogetherProvider with ChangeNotifier {
}
/// Type of participant event
enum ParticipantEventType { joined, left }
enum ParticipantEventType { joined, left, paused, resumed, seeked }
/// Event emitted when a participant joins or leaves
class ParticipantEvent {