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
@@ -0,0 +1,79 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:sync_relay_core/core.dart' show HelloMedia;
|
||||
|
||||
import '../jellyfin/accounts.dart';
|
||||
import '../jellyfin/browse_scope.dart' show BrowseStart;
|
||||
import '../jellyfin/session_media.dart';
|
||||
import '../screens/home/media_picker.dart';
|
||||
import '../screens/session/swap_panel.dart';
|
||||
|
||||
/// What putting up a Jellyfin pick sends: its ref (no URL, no token).
|
||||
/// Throws [FormatException] with a user-facing message when the server is
|
||||
/// not linked any more or its sign-in expired.
|
||||
HelloMedia putUpJellyfinMedia(JellyfinAccounts accounts, JellyfinMedia media) {
|
||||
if (accounts.account(media.serverId) == null) {
|
||||
throw const FormatException('that Jellyfin server is not linked any more — pick again');
|
||||
}
|
||||
if (accounts.statusOf(media.serverId) == JellyfinAccountStatus.expired) {
|
||||
throw const FormatException('Jellyfin sign-in expired — reconnect it in Settings → Jellyfin & integrations');
|
||||
}
|
||||
return mediaForPick(media);
|
||||
}
|
||||
|
||||
/// Brings back the last Jellyfin pick (kept in the Jellyfin box) once the
|
||||
/// accounts are read, unless something else was picked meanwhile or
|
||||
/// [alive] says the picker is gone.
|
||||
Future<void> restoreLastPick(
|
||||
MediaPickerController c, JellyfinAccounts accounts, bool Function() alive) async {
|
||||
final hadMedia = c.selection != null;
|
||||
try {
|
||||
await accounts.ensureLoaded();
|
||||
} catch (_) {
|
||||
return;
|
||||
}
|
||||
final pick = accounts.lastPick;
|
||||
if (!alive() || pick == null || c.jellyfin != null) return;
|
||||
if (accounts.account(pick.serverId) == null) return;
|
||||
final source = c.source;
|
||||
c.pickJellyfin(pick);
|
||||
if (hadMedia) c.source = source;
|
||||
}
|
||||
|
||||
/// Library (round 1): the media picker as the "put it up" view. Offline
|
||||
/// it browses, with a hint that watching needs a server.
|
||||
class LibraryTab extends StatelessWidget {
|
||||
final MediaPickerController controller;
|
||||
final JellyfinAccounts accounts;
|
||||
final bool online;
|
||||
final bool isHost;
|
||||
final BrowseStart? jellyfinStart;
|
||||
/// Puts the pick up; returns an error text, or null when sent.
|
||||
final Future<String?> Function(PickedMedia media) onPutUp;
|
||||
final VoidCallback? onPutUpDone;
|
||||
final VoidCallback? onOpenIntegrations;
|
||||
const LibraryTab({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.accounts,
|
||||
required this.online,
|
||||
required this.isHost,
|
||||
required this.onPutUp,
|
||||
this.onPutUpDone,
|
||||
this.jellyfinStart,
|
||||
this.onOpenIntegrations,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SwapPanel(
|
||||
controller: controller,
|
||||
accounts: accounts,
|
||||
isHost: isHost,
|
||||
jellyfinStart: jellyfinStart,
|
||||
onSwap: onPutUp,
|
||||
onDone: onPutUpDone,
|
||||
onOpenIntegrations: onOpenIntegrations,
|
||||
disabledHint: online ? null : 'Join or host to watch together',
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user