1.0 shell: one window with a party strip; protocol 9 profile + avatars by hash
The shell (lib/src/shell/) replaces the Home and session screens:
- Party strip: profile, Library / Schedule / Settings tabs, Join and
Host cards (hosting no longer takes media), status-ring avatar stack
with the member list, chat & activity panel, Leave.
- The real session player is mounted once and shown full, as a
draggable corner-snapping mini player, or fullscreen (window_manager,
party and toasts overlaid); the Video is never mounted twice.
- Profile popover with inline rename, colour and a crop picker (256 px
PNG), pushed live to the session; avatar toasts; waiting pill.
- Motion kit (fastEaseInToSlowEaseOut, fade-through, popovers,
mouse-drag/wheel horizontal scrolling).
- Library = the existing picker for now (restyled next round); Schedule
= the new SchedulePage; Settings = left nav over the existing panels.
Protocol 9 (packages/core, packages/server, sync client):
- {"t":"profile"} renames / changes the picture mid-session ("rename",
"avatar" feed actions); names are cleaned the same way at hello.
- state.members carry avatarHash; pictures are served once from
/a/<sid>/<token>/<hash> (PNG/JPEG/WebP/GIF by magic bytes only) and
cached by hash in the app (AvatarCache).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f790470935
commit
d29c12e4f0
62 files changed
+5777
-3572
No files matched your search
@@ -9,12 +9,12 @@ import 'package:watch_together/src/jellyfin/browse_scope.dart';
|
||||
import 'package:watch_together/src/jellyfin/progress.dart';
|
||||
import 'package:watch_together/src/jellyfin/session_media.dart';
|
||||
import 'package:watch_together/src/jellyfin/track_prefs.dart';
|
||||
import 'package:watch_together/src/screens/home/host_tab.dart' show hostJellyfinMedia;
|
||||
import 'package:watch_together/src/screens/home/media_picker.dart';
|
||||
import 'package:watch_together/src/screens/session/jellyfin_widgets.dart';
|
||||
import 'package:watch_together/src/screens/session/swap_panel.dart';
|
||||
import 'package:watch_together/src/screens/session_screen.dart' show viaHostOf;
|
||||
import 'package:watch_together/src/session/session_coordinator.dart' show AutoAdvance;
|
||||
import 'package:watch_together/src/shell/library_tab.dart' show putUpJellyfinMedia;
|
||||
import 'package:watch_together/src/shell/party_strip.dart' show viaHostOf;
|
||||
|
||||
import 'jellyfin_accounts_test.dart';
|
||||
import 'jellyfin_browser_test.dart' show app, setSize, settle;
|
||||
@@ -125,16 +125,16 @@ void main() {
|
||||
title: 'Rice and Thieves and Heroes',
|
||||
subtitle: 'Black Summoner · S1E6',
|
||||
runtime: Duration(minutes: 23));
|
||||
final media = hostJellyfinMedia(accounts, pick);
|
||||
final media = putUpJellyfinMedia(accounts, pick);
|
||||
expect(media.url, isNull);
|
||||
expect(media.jellyfin!.itemId, episodeId);
|
||||
expect(media.jellyfin!.subtitle, 'Black Summoner · S1E6');
|
||||
expect(media.jellyfin!.runtimeSecs, 23 * 60);
|
||||
expect(jsonEncode(SwapIntent(media: media).toJson()), isNot(contains('TEST_ACCESS_TOKEN')));
|
||||
expect(() => hostJellyfinMedia(accounts, const JellyfinMedia(serverId: 'gone', baseUrl: 'x', itemId: 'i', title: 't')),
|
||||
expect(() => putUpJellyfinMedia(accounts, const JellyfinMedia(serverId: 'gone', baseUrl: 'x', itemId: 'i', title: 't')),
|
||||
throwsFormatException);
|
||||
accounts.markExpired(fixtureServerId);
|
||||
expect(() => hostJellyfinMedia(accounts, pick), throwsFormatException);
|
||||
expect(() => putUpJellyfinMedia(accounts, pick), throwsFormatException);
|
||||
});
|
||||
|
||||
test('a pasted Jellyfin link becomes a ref (ServerId from /System/Info/Public, no api_key)',
|
||||
@@ -532,7 +532,7 @@ void main() {
|
||||
swapped.add(m);
|
||||
return null;
|
||||
},
|
||||
onClose: () => closed++,
|
||||
onDone: () => closed++,
|
||||
)));
|
||||
await settle(tester);
|
||||
// A guest's file goes through the server's relay.
|
||||
@@ -553,7 +553,7 @@ void main() {
|
||||
expect(find.byKey(const ValueKey('media-jellyfin')), findsOneWidget);
|
||||
|
||||
await tester.pumpWidget(app(SwapPanel(
|
||||
controller: c, accounts: accounts, isHost: true, onSwap: (_) async => 'x', onClose: () {})));
|
||||
controller: c, accounts: accounts, isHost: true, onSwap: (_) async => 'x')));
|
||||
await settle(tester);
|
||||
expect(find.text('File'), findsOneWidget);
|
||||
await tester.tap(find.text('File'));
|
||||
@@ -580,7 +580,6 @@ void main() {
|
||||
isHost: false,
|
||||
jellyfinStart: BrowseStart.seriesOf(fixtureServerId, ep),
|
||||
onSwap: (_) async => null,
|
||||
onClose: () {},
|
||||
)));
|
||||
await settle(tester, 10);
|
||||
expect(tester.widget<ChoiceChip>(find.byKey(const ValueKey('season-$season2Id'))).selected,
|
||||
|
||||
Reference in new issue
Block a user