Stabilisation quick fixes: orderly close, visible version and update band, chat pop-ups on, Windows shaders and hwdec, render logs

- Close: hide the window, dispose every player, thumbnail job and the
  sound engine, leave, then close normally; Linux runner _exit (the
  NVIDIA EGL exit handler crashed 10/10 closes, 0/41 now); Windows tears
  down inside the message loop, then TerminateProcess; watchdogs.
- Version under the Settings tab; an update band instead of a dot,
  never over a video; checks at start and every 4 hours.
- Chat pop-ups default to All again.
- Shader read-back compared as a list (old mpv joins with ','), so
  Windows stops turning shaders off; Windows hwdec auto-copy.
- Render diagnostics (`mpv: render` lines); RAM-only demuxer cache pin
  silences the file-cache warning; a volume readout test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
foxyzandClaude Opus 5.5 committed 2026-10-15 18:01:44 +03:00
1 parent e537f9d5f8
commit ea607c1a52
29 files changed
+873 -168

No files matched your search

+25 -1
View File
@@ -128,6 +128,14 @@ class AppShellState extends State<AppShell> with SingleTickerProviderStateMixin
SessionState.instance.route.value == AppRoute.session &&
SessionState.instance.coordinator.serverState.value?.media != null);
/// A video is on in the session (not finished): the update bar stays
/// off the full player.
final _watching = computed(() {
final s = SessionState.instance;
final r = s.coordinator.serverState.value;
return s.route.value == AppRoute.session && r?.media != null && r?.transport != 'finished';
});
PlayerMode get mode => _mode;
PlayerMode get shownMode => _shown;
bool get fullscreen => _fullscreen;
@@ -226,10 +234,12 @@ class AppShellState extends State<AppShell> with SingleTickerProviderStateMixin
stop();
}
HardwareKeyboard.instance.removeHandler(_onKey);
_updateTimer?.cancel();
_fade.dispose();
_online.dispose();
_alone.dispose();
_hasMedia.dispose();
_watching.dispose();
_flow.dispose();
_join.dispose();
_host.dispose();
@@ -245,9 +255,17 @@ class AppShellState extends State<AppShell> with SingleTickerProviderStateMixin
if (mounted) f();
});
Timer? _updateTimer;
/// At start, then every [updateCheckEvery] while the app runs (when the
/// auto-check setting is on).
Future<void> _autoCheck() async {
final s = AppSettings.instance;
await s.ensureLoaded();
if (!mounted) return;
_updateTimer = Timer.periodic(updateCheckEvery, (_) {
if (s.autoCheckUpdates.peek()) unawaited(runUpdateCheck(widget.updates, true));
});
if (s.autoCheckUpdates.peek()) await runUpdateCheck(widget.updates);
}
@@ -616,6 +634,7 @@ class AppShellState extends State<AppShell> with SingleTickerProviderStateMixin
final online = _online.value;
final alone = _alone.value;
final hasMedia = _hasMedia.value;
final onPlayer = _shown == PlayerMode.full && _watching.value;
return Column(
children: [
if (!_fullscreen)
@@ -632,9 +651,14 @@ class AppShellState extends State<AppShell> with SingleTickerProviderStateMixin
onInvite: () => _connect(ConnectMode.host),
onProfile: () => showProfileCard(context, onOpenSettings: _openSettings, updates: widget.updates),
onShowPlayer: hasMedia && _mode == PlayerMode.none ? () => _show(PlayerMode.mini) : null,
updates: widget.updates,
windowControls: widget.windowControls,
),
AnimatedSize(
duration: easeTime,
curve: ease,
alignment: Alignment.topCenter,
child: _fullscreen || onPlayer ? const SizedBox(width: double.infinity) : UpdateBar(controller: widget.updates),
),
Expanded(
key: const ValueKey('body'),
child: Row(
+53 -26
View File
@@ -5,11 +5,9 @@ import 'package:signals/signals_flutter.dart';
import 'package:sync_relay_core/core.dart';
import 'package:window_manager/window_manager.dart';
import '../screens/home/ui.dart' show appName, displayVersion;
import '../screens/home/update_indicator.dart';
import '../screens/home/ui.dart' show appName, displayVersion, shortVersion;
import '../session/delivery.dart';
import '../state/session_state.dart';
import '../update/update_controller.dart';
import '../widgets/app_keys.dart';
import '../widgets/app_menu.dart';
import '../widgets/copy_open.dart';
@@ -56,7 +54,6 @@ class PartyStrip extends StatelessWidget {
/// Online with something up but the player closed: brings it back.
final VoidCallback? onShowPlayer;
final UpdateController? updates;
/// Off in tests: the drag area and window buttons need window_manager.
final bool windowControls;
@@ -73,7 +70,6 @@ class PartyStrip extends StatelessWidget {
required this.onProfile,
this.onInvite,
this.onShowPlayer,
this.updates,
this.windowControls = true,
});
@@ -125,8 +121,10 @@ class PartyStrip extends StatelessWidget {
label: t.label,
selected: t == tab,
compact: compact,
// The version, a rest of the mouse away.
// The version: always in sight under Settings,
// the full one a rest of the mouse away.
tooltip: t == ShellTab.settings ? '$appName ${displayVersion()}' : null,
caption: t == ShellTab.settings ? shortVersion() : null,
onTap: () => onTab(t),
),
],
@@ -135,7 +133,6 @@ class PartyStrip extends StatelessWidget {
),
),
),
UpdateIndicator(controller: updates),
const SizedBox(width: 8),
if (online && onShowPlayer != null)
IconButton(
@@ -369,6 +366,8 @@ class StripTab extends StatelessWidget {
final VoidCallback onTap;
/// Shown after resting on the tab.
final String? tooltip;
/// Small text under the tab (the version under Settings).
final String? caption;
const StripTab({
super.key,
required this.label,
@@ -376,6 +375,7 @@ class StripTab extends StatelessWidget {
required this.onTap,
this.compact = false,
this.tooltip,
this.caption,
});
@override
@@ -391,28 +391,55 @@ class StripTab extends StatelessWidget {
padding: EdgeInsets.symmetric(horizontal: compact ? 8 : 12),
child: SizedBox(
height: partyStripHeight,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
const SizedBox(height: 3),
Text(
label,
style: TextStyle(
fontWeight: selected ? FontWeight.w700 : FontWeight.w500,
color: selected ? scheme.primary : scheme.onSurfaceVariant,
),
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 3),
Text(
label,
style: TextStyle(
fontWeight: selected ? FontWeight.w700 : FontWeight.w500,
color: selected ? scheme.primary : scheme.onSurfaceVariant,
),
),
const SizedBox(height: 4),
AnimatedContainer(
duration: easeTime,
curve: ease,
height: 3,
width: selected ? 22 : 0,
decoration: BoxDecoration(
color: scheme.primary,
borderRadius: BorderRadius.circular(2),
),
),
],
),
const SizedBox(height: 4),
AnimatedContainer(
duration: easeTime,
curve: ease,
height: 3,
width: selected ? 22 : 0,
decoration: BoxDecoration(
color: scheme.primary,
borderRadius: BorderRadius.circular(2),
if (caption case final c?)
Positioned(
left: 0,
right: 0,
bottom: 2,
height: 13,
child: OverflowBox(
maxWidth: 160,
child: Text(
c,
key: const ValueKey('strip-version'),
maxLines: 1,
softWrap: false,
style: TextStyle(
fontSize: 10,
height: 1.2,
color: scheme.onSurfaceVariant.withValues(alpha: 0.7),
),
),
),
),
),
],
),
),