forked from Foxyz/sync_replay_project
Restructure step 3: your server, Invite, version gate, AppGraph
- One server per app run (loopback, no password); Invite attaches an all-interfaces listener to the same server and Stop sharing detaches it (guests told with 4016); members, media and Up next never reset. No solo mode: you're home on your own server unless in a friend's. - Navigation: none / yours / friend; a join passes a version gate on /health first (clear "they/you need to update", update offer). - Invite as a value (host, port, name, password flag, version, fp reserved); addresses filtered (loopback, link-local, 198.18/15, bridges) and ranked; Sharing chip, Copy invite, Stop sharing. - Alone looks like a plain player (alone replaces solo). - AppGraph builds and disposes systems; widgets use AppScope; SessionState deleted; a test caps the remaining singletons. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
629be0d9ed
commit
4d00b7455e
86 files changed
+2993
-2071
No files matched your search
@@ -6,7 +6,7 @@ import 'package:sync_relay_core/core.dart';
|
||||
|
||||
import '../screens/home/ui.dart' show appName, displayVersion;
|
||||
import 'ports/app_dirs.dart';
|
||||
import '../state/session_state.dart';
|
||||
import '../app/app_graph.dart';
|
||||
|
||||
// The app's log on disk (<app support>/logs/app-YYYY-MM-DD.log; core's
|
||||
// logging.dart writes, rotates and redacts it), debugPrint routed into it,
|
||||
@@ -50,19 +50,19 @@ Future<void> startAppLogging(AppDirs dirs) async {
|
||||
/// the app log (its hosted server's records included), already redacted;
|
||||
/// with [sessionLog], the server's session log of the same hours after it
|
||||
/// (every member's shared lines and the server's, merged).
|
||||
Future<String> diagnosticsText({Duration since = const Duration(hours: 6), bool sessionLog = false}) async {
|
||||
Future<String> diagnosticsText(AppGraph graph, {Duration since = const Duration(hours: 6), bool sessionLog = false}) async {
|
||||
final b = StringBuffer()
|
||||
..writeln('$appName ${displayVersion()}')
|
||||
..writeln('OS: ${Platform.operatingSystem} ${Platform.operatingSystemVersion}')
|
||||
..writeln('Dart: ${Platform.version.split(' ').first}');
|
||||
try {
|
||||
final mpv = await SessionState.instance.coordinator.playback.mpvVersion().timeout(const Duration(seconds: 2));
|
||||
final mpv = await graph.coordinator.playback.mpvVersion().timeout(const Duration(seconds: 2));
|
||||
b.writeln('mpv: ${mpv ?? 'no player yet'}');
|
||||
} catch (_) {
|
||||
b.writeln('mpv: unknown');
|
||||
}
|
||||
try {
|
||||
final y = await SessionState.instance.ports.links.ytDlp.timeout(const Duration(seconds: 2));
|
||||
final y = await graph.ports.links.ytDlp.timeout(const Duration(seconds: 2));
|
||||
final cookies = y.cookies.read(count: false);
|
||||
b.writeln('yt-dlp: ${y.version ?? 'not ready'}${y.lastError == null ? '' : ' (${y.lastError})'}, '
|
||||
'cookies: ${cookies == null ? 'none' : 'set'}');
|
||||
@@ -74,7 +74,7 @@ Future<String> diagnosticsText({Duration since = const Duration(hours: 6), bool
|
||||
b.writeln('--- last ${since.inHours} h of the log ---');
|
||||
b.write(files?.recent(since) ?? '');
|
||||
if (sessionLog) {
|
||||
final server = await SessionState.instance.coordinator.sessionLog(hours: since.inHours);
|
||||
final server = await graph.coordinator.sessionLog(hours: since.inHours);
|
||||
b
|
||||
..writeln()
|
||||
..writeln("--- last ${since.inHours} h of the server's session log ---")
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../files/thumbnails.dart';
|
||||
import '../state/session_state.dart';
|
||||
import '../app/app_graph.dart';
|
||||
|
||||
// The one way out of the app (close button, Alt+F4, Update and restart).
|
||||
// Order matters: every mpv player goes before the engine does (a live one
|
||||
@@ -18,33 +18,43 @@ bool _shuttingDown = false;
|
||||
/// True once [shutDownApp] started.
|
||||
bool get shuttingDown => _shuttingDown;
|
||||
|
||||
/// The systems the close stops; `main` hands them over once.
|
||||
AppGraph? _graph;
|
||||
|
||||
/// Makes [graph] the one [shutDownApp] stops and returns the close
|
||||
/// request handler for the window.
|
||||
Future<void> Function() closeAppWith(AppGraph graph) {
|
||||
_graph = graph;
|
||||
return shutDownApp;
|
||||
}
|
||||
|
||||
/// Hides the window, releases every player and the sound device, leaves
|
||||
/// (the `bye` goes out first, so others see "left" at once; a hosted
|
||||
/// (the `bye` goes out first, so others see "left" at once; then your
|
||||
/// server stops), runs [beforeQuit], then closes the window. [restart]
|
||||
/// (Update and restart) leaves without the `bye`: the member stays
|
||||
/// `gone`, and the restarted app, same client id, takes it back within
|
||||
/// the grace. Every step is bounded; a second call does nothing.
|
||||
Future<void> shutDownApp({Future<void> Function()? beforeQuit, bool restart = false}) async {
|
||||
if (_shuttingDown) return;
|
||||
final graph = _graph;
|
||||
if (_shuttingDown || graph == null) return;
|
||||
_shuttingDown = true;
|
||||
final session = SessionState.instance;
|
||||
final shell = session.ports.shell;
|
||||
final shell = graph.ports.shell;
|
||||
final total = Stopwatch()..start();
|
||||
await _step('window save', shell.saveWindow(), const Duration(milliseconds: 500));
|
||||
await _step('hide', shell.hide(), const Duration(milliseconds: 500));
|
||||
await Future.wait([
|
||||
_step('players', session.coordinator.releasePlayers(), const Duration(seconds: 3)),
|
||||
_step('players', graph.coordinator.releasePlayers(), const Duration(seconds: 3)),
|
||||
_step('thumbnails', cancelAllThumbnails(), const Duration(milliseconds: 1500)),
|
||||
_step('sounds', session.sfx.dispose(), const Duration(milliseconds: 500)),
|
||||
_step('sounds', graph.sfx.dispose(), const Duration(milliseconds: 500)),
|
||||
]);
|
||||
// Windows: media_kit_video frees a player's video output on a thread of
|
||||
// its own after dispose returns; it must be done before the engine
|
||||
// stops, or the plugin's teardown waits on it for good.
|
||||
if (Platform.isWindows) await Future.delayed(const Duration(milliseconds: 300));
|
||||
// Jellyfin stop report, last diagnostics, the bye (flushed), then the
|
||||
// hosted server: 3 s in all.
|
||||
await _step('leave', session.leave(bye: !restart), const Duration(milliseconds: 2500));
|
||||
await _step('server stop', session.stopServer(), const Duration(milliseconds: 500));
|
||||
// Jellyfin stop report, last diagnostics, the bye (flushed), then your
|
||||
// server: 3 s in all.
|
||||
await _step('leave', graph.navigation.leave(bye: !restart), const Duration(milliseconds: 2500));
|
||||
await _step('server stop', graph.yourServer.stop(), const Duration(milliseconds: 500));
|
||||
if (beforeQuit != null) await _step('before quit', beforeQuit(), const Duration(seconds: 3));
|
||||
debugPrint('close: ready to quit after ${total.elapsedMilliseconds} ms');
|
||||
await shell.quit();
|
||||
|
||||
@@ -12,7 +12,7 @@ export 'system_notifications.dart';
|
||||
export 'url_opener.dart';
|
||||
|
||||
/// The platform as the app sees it: one set, made in the composition
|
||||
/// root ([SessionState]) and handed on from there.
|
||||
/// root ([AppGraph]) and handed on from there.
|
||||
class Ports {
|
||||
final ShellPort shell;
|
||||
final UrlOpener urls;
|
||||
|
||||
Reference in new issue
Block a user