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:
foxyzandClaude Opus 5.5 committed 2026-10-15 22:52:52 +03:00
1 parent 629be0d9ed
commit 4d00b7455e
86 files changed
+2993 -2071

No files matched your search

+10 -6
View File
@@ -7,9 +7,13 @@ import 'package:starsync/src/controls/model/controls_model.dart';
import 'package:starsync/src/controls/model/controls_prefs.dart';
import 'package:starsync/src/controls/session_controls.dart';
import 'package:starsync/src/playback/media_marks.dart';
import 'package:starsync/src/state/session_state.dart';
import 'package:starsync/src/state/settings.dart';
import 'test_graph.dart';
/// This file's composition root (see test_graph.dart).
final _graph = testGraph();
// The controls model and its layout spec: which controls the bar has, in
// which order, what goes to ⚙; and both skins drawing the same order.
@@ -64,7 +68,7 @@ void main() {
final all = menuActions(const ControlsSnapshot(hasChapters: true, hasDuration: true, hasLink: true), none);
expect(all.first, ControlAction.chapters);
expect(all, containsAllInOrder([ControlAction.skipForward, ControlAction.copyLink]));
expect(menuActions(const ControlsSnapshot(solo: true), none), isNot(contains(ControlAction.markAllReady)),
expect(menuActions(const ControlsSnapshot(alone: true), none), isNot(contains(ControlAction.markAllReady)),
reason: 'nobody to mark ready');
});
@@ -74,7 +78,7 @@ void main() {
expect(controlIcon(BarControl.playPause, const ControlsSnapshot(play: PlayState.waiting)), Icons.check_rounded);
expect(controlTooltip(BarControl.playPause, const ControlsSnapshot(play: PlayState.playing)),
'Pause for everyone (Space)');
expect(controlTooltip(BarControl.playPause, const ControlsSnapshot(solo: true)), 'Play (Space)');
expect(controlTooltip(BarControl.playPause, const ControlsSnapshot(alone: true)), 'Play (Space)');
});
test('episode buttons: the label and the keys', () {
@@ -98,7 +102,7 @@ void main() {
group('both skins draw the model', () {
final s = AppSettings.instance;
final coord = SessionState.instance.coordinator;
final coord = _graph.coordinator;
tearDown(() {
s.controlsSkin.value = ControlsSkin.standard;
@@ -131,13 +135,13 @@ void main() {
shown.dispose();
hold.dispose();
});
await tester.pumpWidget(MaterialApp(
await tester.pumpWidget(scoped(_graph, MaterialApp(
theme: ThemeData.dark(),
home: Scaffold(
backgroundColor: Colors.black,
body: SessionControls(model: model, shown: shown, hold: hold, onDoubleClick: () {}),
),
));
)));
await tester.pump();
return model;
}