Solo: no idle unready, no party corner in fullscreen; a cancelled Invite returns to solo

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
foxyzandClaude Opus 5.5 committed 2026-10-09 23:39:48 +03:00
1 parent 0115d54ab8
commit 059b7dd306
4 files changed
+38 -4

No files matched your search

@@ -97,6 +97,7 @@ class ConnectFlow extends ChangeNotifier {
_notify(); _notify();
return; return;
} }
final fromSolo = mode == ConnectMode.host && _session.solo.value;
try { try {
if (mode == ConnectMode.host) { if (mode == ConnectMode.host) {
await _session.host(params, form); await _session.host(params, form);
@@ -111,10 +112,14 @@ class ConnectFlow extends ChangeNotifier {
return; return;
} }
if (myAttempt != _attempt) { if (myAttempt != _attempt) {
// Cancelled after it got through: back out. // Cancelled after it got through: back out (an Invite: back to solo).
try { try {
if (fromSolo) {
await _session.cancelInvite();
} else {
await _session.leave(); await _session.leave();
await _session.stopServer(); await _session.stopServer();
}
} catch (_) {} } catch (_) {}
return; return;
} }
@@ -240,6 +240,9 @@ class SessionCoordinator {
StreamSubscription? _statusSub; StreamSubscription? _statusSub;
bool _manualClose = false; 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 _playerReady = false;
bool _wasPlayAllowed = false; bool _wasPlayAllowed = false;
// Last ready flag the server reported for self; restored after a // Last ready flag the server reported for self; restored after a
@@ -949,7 +952,9 @@ class SessionCoordinator {
// activity so a pause after a long watch starts a fresh idle clock. // activity so a pause after a long watch starts a fresh idle clock.
if (r != null) _readyAfterJoinBack(r, now); if (r != null) _readyAfterJoinBack(r, now);
final sitOutWhenIdle = AppSettings.instance.idleSitsOut.peek(); final sitOutWhenIdle = AppSettings.instance.idleSitsOut.peek();
final event = _idle.tick(now, final event = solo
? IdleEvent.none
: _idle.tick(now,
playing: serverState.peek()?.transport == 'playing', playing: serverState.peek()?.transport == 'playing',
selfReady: selfMember.peek()?.ready ?? false); selfReady: selfMember.peek()?.ready ?? false);
switch (event) { switch (event) {
@@ -296,7 +296,7 @@ class _SessionPlayerState extends State<SessionPlayer> {
_status(), _status(),
_progressLine(), _progressLine(),
_jellyfinOverlay(), _jellyfinOverlay(),
if (widget.fullscreen) ...[_pausedParty(), _unreadChip()], if (widget.fullscreen) ...[if (!SessionState.instance.solo.value) _pausedParty(), _unreadChip()],
] else ] else
_miniOverlay(), _miniOverlay(),
// Sitting out: nothing is loaded; the card covers the // Sitting out: nothing is loaded; the card covers the
+25 -1
View File
@@ -260,12 +260,13 @@ class SessionState {
/// pick, seeked to where it was; if hosting fails, solo comes back. /// pick, seeked to where it was; if hosting fails, solo comes back.
Future<void> host(ConnectParams params, ConnectForm form) async { Future<void> host(ConnectParams params, ConnectForm form) async {
if (!solo.peek()) return _host(params, form: form); if (!solo.peek()) return _host(params, form: form);
final carry = _carry(); final carry = _invited = _carry();
switching.value = true; switching.value = true;
try { try {
await coordinator.leave(); await coordinator.leave();
await stopServer(); await stopServer();
solo.value = false; solo.value = false;
coordinator.solo = false;
try { try {
await _host(params, form: form); await _host(params, form: form);
} catch (_) { } 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<void> 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 /// 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. /// password, nothing put up yet. Nobody else can reach it.
Future<void> startSolo(ConnectParams params) async { Future<void> startSolo(ConnectParams params) async {
@@ -394,6 +416,7 @@ class SessionState {
rethrow; rethrow;
} }
solo.value = soloSession; solo.value = soloSession;
coordinator.solo = soloSession;
route.value = AppRoute.session; route.value = AppRoute.session;
} finally { } finally {
if (form != null) await _saveFormQuietly(); if (form != null) await _saveFormQuietly();
@@ -478,6 +501,7 @@ class SessionState {
await coordinator.leave(); await coordinator.leave();
await stopServer(); await stopServer();
solo.value = false; solo.value = false;
coordinator.solo = false;
serverLabel.value = ''; serverLabel.value = '';
status.value = const HealthStatus(HealthSeverity.info, 'idle'); status.value = const HealthStatus(HealthSeverity.info, 'idle');
route.value = AppRoute.home; route.value = AppRoute.home;