From 059b7dd30666f1f642652b0243af8688714dca45 Mon Sep 17 00:00:00 2001 From: foxyz Date: Fri, 9 Oct 2026 23:39:48 +0300 Subject: [PATCH] Solo: no idle unready, no party corner in fullscreen; a cancelled Invite returns to solo Co-Authored-By: Claude Opus 5.5 --- .../lib/src/screens/home/connect_flow.dart | 11 +++++--- .../lib/src/session/session_coordinator.dart | 11 +++++--- .../desktop/lib/src/shell/session_player.dart | 2 +- apps/desktop/lib/src/state/session_state.dart | 26 ++++++++++++++++++- 4 files changed, 42 insertions(+), 8 deletions(-) diff --git a/apps/desktop/lib/src/screens/home/connect_flow.dart b/apps/desktop/lib/src/screens/home/connect_flow.dart index bcb05fb..d911ffa 100644 --- a/apps/desktop/lib/src/screens/home/connect_flow.dart +++ b/apps/desktop/lib/src/screens/home/connect_flow.dart @@ -97,6 +97,7 @@ class ConnectFlow extends ChangeNotifier { _notify(); return; } + final fromSolo = mode == ConnectMode.host && _session.solo.value; try { if (mode == ConnectMode.host) { await _session.host(params, form); @@ -111,10 +112,14 @@ class ConnectFlow extends ChangeNotifier { return; } if (myAttempt != _attempt) { - // Cancelled after it got through: back out. + // Cancelled after it got through: back out (an Invite: back to solo). try { - await _session.leave(); - await _session.stopServer(); + if (fromSolo) { + await _session.cancelInvite(); + } else { + await _session.leave(); + await _session.stopServer(); + } } catch (_) {} return; } diff --git a/apps/desktop/lib/src/session/session_coordinator.dart b/apps/desktop/lib/src/session/session_coordinator.dart index fcd017a..499c7c4 100644 --- a/apps/desktop/lib/src/session/session_coordinator.dart +++ b/apps/desktop/lib/src/session/session_coordinator.dart @@ -240,6 +240,9 @@ class SessionCoordinator { StreamSubscription? _statusSub; bool _manualClose = false; + /// A solo session (set by SessionState): no idle unready / sit out, + /// there is nobody to hold up. + bool solo = false; bool _playerReady = false; bool _wasPlayAllowed = false; // Last ready flag the server reported for self; restored after a @@ -949,9 +952,11 @@ class SessionCoordinator { // activity so a pause after a long watch starts a fresh idle clock. if (r != null) _readyAfterJoinBack(r, now); final sitOutWhenIdle = AppSettings.instance.idleSitsOut.peek(); - final event = _idle.tick(now, - playing: serverState.peek()?.transport == 'playing', - selfReady: selfMember.peek()?.ready ?? false); + final event = solo + ? IdleEvent.none + : _idle.tick(now, + playing: serverState.peek()?.transport == 'playing', + selfReady: selfMember.peek()?.ready ?? false); switch (event) { case IdleEvent.warn: _toastId++; diff --git a/apps/desktop/lib/src/shell/session_player.dart b/apps/desktop/lib/src/shell/session_player.dart index 1c74a7a..1e4e9f0 100644 --- a/apps/desktop/lib/src/shell/session_player.dart +++ b/apps/desktop/lib/src/shell/session_player.dart @@ -296,7 +296,7 @@ class _SessionPlayerState extends State { _status(), _progressLine(), _jellyfinOverlay(), - if (widget.fullscreen) ...[_pausedParty(), _unreadChip()], + if (widget.fullscreen) ...[if (!SessionState.instance.solo.value) _pausedParty(), _unreadChip()], ] else _miniOverlay(), // Sitting out: nothing is loaded; the card covers the diff --git a/apps/desktop/lib/src/state/session_state.dart b/apps/desktop/lib/src/state/session_state.dart index 08ce8eb..413f036 100644 --- a/apps/desktop/lib/src/state/session_state.dart +++ b/apps/desktop/lib/src/state/session_state.dart @@ -260,12 +260,13 @@ class SessionState { /// pick, seeked to where it was; if hosting fails, solo comes back. Future host(ConnectParams params, ConnectForm form) async { if (!solo.peek()) return _host(params, form: form); - final carry = _carry(); + final carry = _invited = _carry(); switching.value = true; try { await coordinator.leave(); await stopServer(); solo.value = false; + coordinator.solo = false; try { await _host(params, form: form); } catch (_) { @@ -289,6 +290,27 @@ class SessionState { } } + /// What an Invite from solo carried over, for [cancelInvite]. + ({PickedMedia pick, double position})? _invited; + + /// Invite cancelled after the hosted server already came up: back to + /// solo with the same media, not out of the session. + Future cancelInvite() async { + final carry = _invited; + switching.value = true; + try { + await coordinator.leave(); + await stopServer(); + await _host(_soloParams!, soloSession: true); + if (carry != null) await _resume(carry); + } catch (e) { + debugPrint('back to solo failed: $e'); + await leave(); + } finally { + switching.value = false; + } + } + /// Watch alone: the same hosted path, on 127.0.0.1 and a free port, no /// password, nothing put up yet. Nobody else can reach it. Future startSolo(ConnectParams params) async { @@ -394,6 +416,7 @@ class SessionState { rethrow; } solo.value = soloSession; + coordinator.solo = soloSession; route.value = AppRoute.session; } finally { if (form != null) await _saveFormQuietly(); @@ -478,6 +501,7 @@ class SessionState { await coordinator.leave(); await stopServer(); solo.value = false; + coordinator.solo = false; serverLabel.value = ''; status.value = const HealthStatus(HealthSeverity.info, 'idle'); route.value = AppRoute.home;