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:
foxyzandClaude Opus 5.5 committed 2026-10-09 00:23:46 +03:00
1 parent f790470935
commit d29c12e4f0
62 files changed
+5777 -3572

No files matched your search

+2 -55
View File
@@ -7,9 +7,8 @@ const repoUrl = 'https://git.senkochan.moe/Foxyz/sync_replay_project';
const kRadius = 12.0;
const kGap = 16.0;
const kPagePadding = EdgeInsets.fromLTRB(28, 20, 28, 28);
/// Rounded panel used by every Home/Settings section.
/// Rounded panel used by every Settings section.
class Panel extends StatelessWidget {
final Widget child;
final EdgeInsetsGeometry padding;
@@ -38,33 +37,6 @@ class Panel extends StatelessWidget {
}
}
/// Page heading + optional subtitle, same spacing everywhere.
class PageTitle extends StatelessWidget {
final String title;
final String? subtitle;
const PageTitle(this.title, {super.key, this.subtitle});
@override
Widget build(BuildContext context) {
final t = Theme.of(context).textTheme;
return Padding(
padding: const EdgeInsets.only(bottom: kGap),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: t.headlineSmall),
if (subtitle != null)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(subtitle!,
style: t.bodyMedium?.copyWith(color: Colors.grey)),
),
],
),
);
}
}
/// Small grey heading inside a panel.
class SectionLabel extends StatelessWidget {
final String text;
@@ -87,7 +59,7 @@ class SectionLabel extends StatelessWidget {
}
}
/// Text field style shared by the connect cards.
/// Outlined text field style (settings, Jellyfin sign-in).
InputDecoration fieldDecoration(String label, {String? hint, Widget? suffix}) =>
InputDecoration(
labelText: label,
@@ -97,31 +69,6 @@ InputDecoration fieldDecoration(String label, {String? hint, Widget? suffix}) =>
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
);
/// Single-item scrollable page body, width-capped so it doesn't sprawl
/// on wide windows.
class PageScroll extends StatelessWidget {
final List<Widget> children;
final double maxWidth;
const PageScroll({super.key, required this.children, this.maxWidth = 920});
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: kPagePadding,
child: Align(
alignment: Alignment.topLeft,
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxWidth),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: children,
),
),
),
);
}
}
/// Widest a single text field gets, however wide the window.
const kFieldMaxWidth = 720.0;