Protocol 11: sit out replaces kicking; nudge

Anyone can sit anyone out (host included); only you bring yourself back.
Out members stay in chat and the party, never block or count, and load no
media; a card with the live position and Join back (Space) replaces the
video. The mini player's x sits you out; optionally idle does too. Nudge:
the server forwards it to the target only (3/s, 20/min per sender); the
target hears the nudge sound, gets a toast and a taskbar attention
request. Kick, its messages, errors and dialog are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
foxyzandClaude Opus 5.5 committed 2026-10-09 21:05:35 +03:00
1 parent dbfd351ee4
commit 8b81da4069
46 files changed
+855 -599

No files matched your search

+3 -15
View File
@@ -144,7 +144,7 @@ class ServerCrashed implements Exception {
}
/// A fresh per-hosting key: the app's own client sends it in hello to be
/// recognised as the (unkickable) host.
/// recognised as the host (`members[].host`).
String newHostKey() {
final r = Random.secure();
return base64Url.encode([for (var i = 0; i < 24; i++) r.nextInt(256)]);
@@ -159,7 +159,8 @@ class SessionState {
final status = signal(const HealthStatus(HealthSeverity.info, 'idle'));
final SfxService sfx = SynthSfx(
volume: () => AppSettings.instance.soundVolume.peek(),
enabled: (k) => !AppSettings.instance.soundsOff.peek().contains(k.name),
// A nudge is an explicit poke: it ignores its switch (not the volume).
enabled: (k) => k == SfxKind.nudge || !AppSettings.instance.soundsOff.peek().contains(k.name),
presetFor: soundPresetFor,
);
late final SessionCoordinator coordinator = SessionCoordinator(sfx: sfx);
@@ -183,7 +184,6 @@ class SessionState {
@visibleForTesting
ServerIsolate? get hostedServer => _server;
void Function()? _stopKickWatch;
// Bumped by host/join/cancelPending: a cancelled setup notices at its
// next step and unwinds instead of connecting.
int _op = 0;
@@ -309,7 +309,6 @@ class SessionState {
if (crash != null && e is! ConnectCancelled) throw ServerCrashed(crash);
rethrow;
}
_watchKick();
route.value = AppRoute.session;
} finally {
await _saveFormQuietly();
@@ -353,15 +352,6 @@ class SessionState {
await stopServer();
}
/// A kicked host must not keep serving.
void _watchKick() {
_stopKickWatch?.call();
_stopKickWatch = effect(() {
// Microtask: stopServer disposes this effect, not from inside it.
if (coordinator.kickReason.value != null) scheduleMicrotask(stopServer);
});
}
/// Saved whatever the outcome: a failed join is exactly when the user
/// wants the form back.
Future<void> _saveFormQuietly() async {
@@ -387,8 +377,6 @@ class SessionState {
Future<void> stopServer() async {
coordinator.hostFile = null;
_stopKickWatch?.call();
_stopKickWatch = null;
final server = _server;
_server = null;
await server?.stop();
+10
View File
@@ -25,6 +25,7 @@ const _keySoundsOff = 'soundsOff';
const _keySoundPreset = 'soundPreset';
const _keySoundPresetFor = 'soundPresetFor';
const _keyUnreadyOnSwap = 'unreadyOnSwap';
const _keyIdleSitsOut = 'idleSitsOut';
enum HwdecMode { auto, off, custom }
@@ -39,6 +40,8 @@ class AppSettings {
/// Session: send ready=false when the video changes (a swap keeps the
/// server's ready flags).
final unreadyOnSwap = signal(false);
/// Session: the idle auto-unready sits this member out instead.
final idleSitsOut = signal(false);
/// Player design: the uosc controls (`controls/uosc/`) instead of the
/// standard ones (media_kit's with our bars). Applies live.
final mpvControls = signal(false);
@@ -95,6 +98,8 @@ class AppSettings {
if (advance is bool) autoAdvance.value = advance;
final unready = box.get(_keyUnreadyOnSwap);
if (unready is bool) unreadyOnSwap.value = unready;
final idleOut = box.get(_keyIdleSitsOut);
if (idleOut is bool) idleSitsOut.value = idleOut;
final mpv = box.get(_keyMpvControls);
if (mpv is bool) mpvControls.value = mpv;
final hideRe = box.get(_keyHideReReleases);
@@ -156,6 +161,11 @@ class AppSettings {
_put(_keyUnreadyOnSwap, on);
}
void setIdleSitsOut(bool on) {
idleSitsOut.value = on;
_put(_keyIdleSitsOut, on);
}
void setMpvControls(bool on) {
mpvControls.value = on;
_put(_keyMpvControls, on);