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,126 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// The shell's shared motion: one curve and one duration for everything
|
||||
// that moves, a fade-through for views that replace each other, popovers
|
||||
// that grow from where they open, and mouse-friendly horizontal lists.
|
||||
|
||||
const ease = Curves.fastEaseInToSlowEaseOut;
|
||||
const easeTime = Duration(milliseconds: 320);
|
||||
|
||||
/// Fade-through length; the old view is gone after [fadeOutPart] of it.
|
||||
const fadeThroughTime = Duration(milliseconds: 360);
|
||||
const fadeOutPart = 0.35;
|
||||
|
||||
/// Fade through (Material motion): the old view is gone after the first
|
||||
/// 35%, then the new one fades and scales in; they never overlap. Key
|
||||
/// [child] by what it shows so a change of key runs the transition.
|
||||
Widget fadeThrough(Widget child, {Key? key}) => AnimatedSwitcher(
|
||||
key: key,
|
||||
duration: fadeThroughTime,
|
||||
layoutBuilder: (current, previous) => Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [...previous, ?current],
|
||||
),
|
||||
transitionBuilder: (c, anim) {
|
||||
if (c.key == child.key) {
|
||||
// Incoming (anim 0 -> 1): wait, then fade and scale up.
|
||||
final a = CurvedAnimation(
|
||||
parent: anim,
|
||||
curve: const Interval(fadeOutPart, 1, curve: ease),
|
||||
);
|
||||
return FadeTransition(
|
||||
opacity: a,
|
||||
child: ScaleTransition(scale: Tween(begin: 0.985, end: 1.0).animate(a), child: c),
|
||||
);
|
||||
}
|
||||
// Outgoing (anim 1 -> 0): fully faded by the time it reaches 0.65.
|
||||
return FadeTransition(
|
||||
opacity: CurvedAnimation(
|
||||
parent: anim,
|
||||
curve: const Interval(1 - fadeOutPart, 1, curve: Curves.easeIn),
|
||||
),
|
||||
child: c,
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
|
||||
/// A card over the window that fades and scales in from [at] (window
|
||||
/// alignment) instead of popping; a click outside or Esc closes it. Short
|
||||
/// windows: the card scrolls instead of overflowing.
|
||||
Future<T?> showPopover<T>(
|
||||
BuildContext context, {
|
||||
required Alignment at,
|
||||
required Widget child,
|
||||
bool dim = false,
|
||||
}) =>
|
||||
showGeneralDialog<T>(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
barrierLabel: 'close',
|
||||
barrierColor: dim ? Colors.black26 : Colors.transparent,
|
||||
transitionDuration: easeTime,
|
||||
pageBuilder: (context, _, _) => SafeArea(
|
||||
child: Align(
|
||||
alignment: at,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxHeight: MediaQuery.sizeOf(context).height - 32),
|
||||
child: SingleChildScrollView(child: child),
|
||||
),
|
||||
),
|
||||
),
|
||||
transitionBuilder: (context, anim, _, child) {
|
||||
final a = CurvedAnimation(parent: anim, curve: ease, reverseCurve: Curves.easeIn);
|
||||
return FadeTransition(
|
||||
opacity: a,
|
||||
child: ScaleTransition(
|
||||
scale: Tween(begin: 0.96, end: 1.0).animate(a),
|
||||
alignment: at,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
/// Horizontal lists for mouse users: drag with the mouse, and the plain
|
||||
/// (vertical) wheel moves them sideways. At either end the wheel is left
|
||||
/// to the page, so a row never traps vertical scrolling.
|
||||
class HScroll extends StatefulWidget {
|
||||
final ScrollController? controller;
|
||||
final Widget Function(ScrollController controller) builder;
|
||||
const HScroll({super.key, this.controller, required this.builder});
|
||||
|
||||
@override
|
||||
State<HScroll> createState() => _HScrollState();
|
||||
}
|
||||
|
||||
class _HScrollState extends State<HScroll> {
|
||||
ScrollController? _own;
|
||||
ScrollController get _c => widget.controller ?? (_own ??= ScrollController());
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_own?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _wheel(PointerSignalEvent e) {
|
||||
if (e is! PointerScrollEvent) return;
|
||||
final dy = e.scrollDelta.dy;
|
||||
if (dy == 0 || e.scrollDelta.dx != 0 || !_c.hasClients) return;
|
||||
final p = _c.position;
|
||||
final target = (p.pixels + dy * 1.5).clamp(p.minScrollExtent, p.maxScrollExtent);
|
||||
if (target == p.pixels) return; // at the end: let the page scroll
|
||||
GestureBinding.instance.pointerSignalResolver.register(e, (_) {
|
||||
_c.animateTo(target, duration: const Duration(milliseconds: 140), curve: Curves.easeOutCubic);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context)
|
||||
.copyWith(dragDevices: PointerDeviceKind.values.toSet()),
|
||||
child: Listener(onPointerSignal: _wheel, child: widget.builder(_c)),
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user