forked from Foxyz/sync_replay_project
Player overlay as one piece, Up next always warm and on every source, Jellyfin never unmarks a watched episode
- Overlay: one layout for both designs (top bar with Up next as one of its buttons; one action band above the controls for the status pill, skips, next episode and end cards), one glass recipe (AppGlass) and one pill (FloatingPill); hovering any of them, or the open queue, holds the controls up. - Up next: the first item (or the first continuation episode) is kept resolved on the app that puts it up, refreshed before it expires; Schedule and drop/paste can queue too. - Jellyfin: WatchSession (core) counts real watched coverage; an episode is watched at 80 % or at its ending after half; a played one is re-marked after Playing (10.11 unmarks on open) and never gets a position; a peek leaves the old resume point; part-way resume points stay under 85 %. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6b10cae4e6
commit
771b961b32
27 files changed
+1396
-558
No files matched your search
@@ -22,6 +22,9 @@ abstract final class AppMenuStyle {
|
||||
static const border = Color(0x1FFFFFFF);
|
||||
static const hover = Color(0x14FFFFFF);
|
||||
static const focus = Color(0x24FFFFFF);
|
||||
/// The player's pills over the picture: lighter, so less of it is hidden.
|
||||
static const pillGlass = Color(0x8C000000);
|
||||
static const blur = 16.0;
|
||||
static const radius = 12.0;
|
||||
static const rowRadius = 8.0;
|
||||
static const rowHeight = 36.0;
|
||||
@@ -53,6 +56,53 @@ abstract final class AppMenuStyle {
|
||||
);
|
||||
}
|
||||
|
||||
/// The app's one frosted surface: a blur under a dark tint, a hairline
|
||||
/// border and (cards, menus) a soft shadow. Menus, popovers and the
|
||||
/// player's cards use it as is; the player's pills with [pill].
|
||||
class AppGlass extends StatelessWidget {
|
||||
final Widget child;
|
||||
final double radius;
|
||||
final Color color;
|
||||
final bool shadow;
|
||||
const AppGlass({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.radius = AppMenuStyle.radius,
|
||||
this.color = AppMenuStyle.glass,
|
||||
this.shadow = true,
|
||||
});
|
||||
|
||||
/// A stadium over the picture: the lighter tint, no shadow.
|
||||
const AppGlass.pill({super.key, required this.child})
|
||||
: radius = 999,
|
||||
color = AppMenuStyle.pillGlass,
|
||||
shadow = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final r = BorderRadius.circular(radius);
|
||||
final glass = ClipRRect(
|
||||
borderRadius: r,
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: AppMenuStyle.blur, sigmaY: AppMenuStyle.blur),
|
||||
child: Material(
|
||||
color: color,
|
||||
shape: RoundedRectangleBorder(borderRadius: r, side: const BorderSide(color: AppMenuStyle.border, width: 0.5)),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (!shadow) return glass;
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: r,
|
||||
boxShadow: const [BoxShadow(color: Color(0x66000000), blurRadius: 24, offset: Offset(0, 8))],
|
||||
),
|
||||
child: glass,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// One row: [label], a dim [hint] at the right, [checked] = the current
|
||||
/// one (null: the menu has no checks for this row).
|
||||
class AppMenuItem<T> {
|
||||
@@ -220,39 +270,24 @@ class _AppMenuPanelState extends State<_AppMenuPanel> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final checks = widget.items.any((i) => i.checked != null);
|
||||
final r = BorderRadius.circular(AppMenuStyle.radius);
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: r,
|
||||
boxShadow: const [BoxShadow(color: Color(0x66000000), blurRadius: 24, offset: Offset(0, 8))],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: r,
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16),
|
||||
child: Material(
|
||||
key: const ValueKey('app-menu'),
|
||||
color: AppMenuStyle.glass,
|
||||
shape: AppMenuStyle.shape,
|
||||
child: IntrinsicWidth(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
for (final (i, item) in widget.items.indexed)
|
||||
_MenuRow(
|
||||
key: i == _start ? _current : null,
|
||||
item: item,
|
||||
checks: checks,
|
||||
autofocus: i == _start,
|
||||
onTap: () => widget.onPick(i),
|
||||
),
|
||||
],
|
||||
return AppGlass(
|
||||
key: const ValueKey('app-menu'),
|
||||
child: IntrinsicWidth(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
for (final (i, item) in widget.items.indexed)
|
||||
_MenuRow(
|
||||
key: i == _start ? _current : null,
|
||||
item: item,
|
||||
checks: checks,
|
||||
autofocus: i == _start,
|
||||
onTap: () => widget.onPick(i),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in new issue
Block a user