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

+13 -8
View File
@@ -12,9 +12,14 @@ import 'package:starsync/src/jellyfin/browse_scope.dart';
import 'package:starsync/src/jellyfin/picker_source.dart';
import 'package:starsync/src/state/settings.dart';
import 'test_graph.dart';
import 'jellyfin_accounts_test.dart'
show FakeJellyfin, arifuretaId, fixtureServerId, linkedAccounts, season2Id;
/// This file's composition root (see test_graph.dart).
final _graph = testGraph();
Future<void> setSize(WidgetTester tester, Size size) async {
tester.view.physicalSize = size;
tester.view.devicePixelRatio = 1;
@@ -55,7 +60,7 @@ Future<(Picks, JellyfinAccounts, FakeJellyfin, BrowseController)> pumpBrowser(
final controller = BrowseController(accounts, now: () => fixtureNow);
addTearDown(controller.dispose);
final picks = Picks();
await tester.pumpWidget(app(StatefulBuilder(
await tester.pumpWidget(scoped(_graph, app(StatefulBuilder(
builder: (context, setState) => JellyfinBrowser(
accounts: accounts,
controller: controller,
@@ -63,7 +68,7 @@ Future<(Picks, JellyfinAccounts, FakeJellyfin, BrowseController)> pumpBrowser(
onSelect: (s) => setState(() => picks.selected.add(s)),
onPutUp: picks.putUp.add,
),
)));
))));
await settle(tester);
return (picks, accounts, fake, controller);
}
@@ -154,8 +159,8 @@ void main() {
addTearDown(controller.dispose);
AppSettings.instance.hideReReleases.value = true;
addTearDown(() => AppSettings.instance.hideReReleases.value = true);
await tester.pumpWidget(app(JellyfinBrowser(
accounts: accounts, controller: controller, onSelect: (_) {}, onPutUp: (_) {})));
await tester.pumpWidget(scoped(_graph, app(JellyfinBrowser(
accounts: accounts, controller: controller, onSelect: (_) {}, onPutUp: (_) {}))));
await settle(tester);
expect(find.text('Fresh Show'), findsWidgets);
expect(find.text('S1E1'), findsWidgets);
@@ -298,9 +303,9 @@ void main() {
final accounts = JellyfinAccounts(
store: MemoryJellyfinStore(), httpFactory: () => fake, deviceName: 'Test');
await accounts.ensureLoaded(validate: false);
await tester.pumpWidget(app(SingleChildScrollView(
await tester.pumpWidget(scoped(_graph, app(SingleChildScrollView(
child: JellyfinAccountsPanel(
accounts: accounts, pollInterval: const Duration(milliseconds: 100)))));
accounts: accounts, pollInterval: const Duration(milliseconds: 100))))));
await tester.tap(find.byKey(const ValueKey('jf-add-server')));
await tester.pump();
await tester.enterText(find.byKey(const ValueKey('jf-address')), 'example.invalid');
@@ -351,8 +356,8 @@ void main() {
await setSize(tester, const Size(1200, 900));
final accounts = JellyfinAccounts(store: MemoryJellyfinStore(), httpFactory: () => FakeJellyfin());
await accounts.ensureLoaded(validate: false);
await tester.pumpWidget(app(JellyfinPickerSource(
accounts: accounts, onSelect: (_) {}, onPutUp: (_) {}, onOpenIntegrations: () {})));
await tester.pumpWidget(scoped(_graph, app(JellyfinPickerSource(
accounts: accounts, onSelect: (_) {}, onPutUp: (_) {}, onOpenIntegrations: () {}))));
await tester.pump();
expect(find.byKey(const ValueKey('jf-sign-in')), findsOneWidget);
expect(find.text('Open Integrations'), findsOneWidget);