Player controls: one model, two skins (Standard, Minimal); conventional layout; controls settings

PlayerControlsModel holds the controls' state and actions once; the
Standard skin (our own, no more media_kit controls or private-state
reach-ins) and Minimal (the former uosc design, made the app's own and
credited) only draw it. One layout spec for both: play first, then
previous/next, volume, time; subtitles, audio, settings, chat,
fullscreen last; everything else in the settings menu. Settings ->
Player -> Controls: time display, size, hide delay, optional buttons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
foxyzandClaude Opus 5.5 committed 2026-10-15 17:03:15 +03:00
1 parent 959eed38a4
commit fd8064611a
40 files changed
+3820 -3975

No files matched your search

+18 -20
View File
@@ -9,9 +9,7 @@ import 'package:signals/signals_flutter.dart';
import 'package:sync_relay_core/core.dart';
import '../controls/controls_probe.dart' show ControlsHold;
import '../controls/uosc/buttons.dart' show UoscButton;
import '../controls/uosc/controls.dart' show UoscControls;
import '../controls/uosc/style.dart';
import '../controls/minimal/style.dart';
import '../files/scanner.dart' show FileKind, kindOf;
import '../jellyfin/images.dart';
import '../jellyfin/session_media.dart' show EpisodeLink;
@@ -28,16 +26,16 @@ import '../widgets/text_focus.dart' show KeyboardClaim;
import 'queue_controller.dart';
import 'queue_logic.dart';
// Up next in the player: its control in the top bar (with chat and
// fullscreen), the popover it opens (the queue, the continuation, a field
// Up next in the player: its control in the top bar (with the window
// controls), the popover it opens (the queue, the continuation, a field
// and a drop zone to add), and the end-of-media card for the first item.
// ---------------------------------------------------------------- button
/// Which controls design the Up next control sits in: it takes the look
/// Which controls skin the Up next control sits in: it takes the look
/// of that bar's buttons.
enum UpNextLook { standard, uosc }
enum UpNextLook { standard, minimal }
/// The next item's picture, "UP NEXT" over its title, a +n count and ⏭
/// (play it now) in the player's top bar, one flat control among its
@@ -66,7 +64,7 @@ class _UpNextButtonState extends State<UpNextButton> {
bool _hoverTitle = false;
QueueController get _q => widget.queue;
bool get _uosc => widget.look == UpNextLook.uosc;
bool get _minimal => widget.look == UpNextLook.minimal;
@override
void didUpdateWidget(UpNextButton oldWidget) {
@@ -135,16 +133,16 @@ class _UpNextButtonState extends State<UpNextButton> {
@override
Widget build(BuildContext context) {
// The bar's own metrics: uosc's flat 40 px boxes, or media_kit's
// round 44 px icon buttons.
final height = _uosc ? UoscControls.buttonRowHeight : 44.0;
final radius = _uosc ? UoscStyle.radius : height / 2;
final hover = _uosc ? UoscStyle.hover : Colors.white10;
final fontSize = _uosc ? UoscStyle.fontSize : 14.0;
final dim = _uosc ? UoscStyle.dim : Colors.white70;
// The bar's own metrics: the Minimal skin's rounded squares, or the
// Standard skin's round 44 px icon buttons.
final height = _minimal ? MinimalStyle.button + 2 * MinimalStyle.capsulePad : 44.0;
final radius = _minimal ? MinimalStyle.radius + MinimalStyle.capsulePad : height / 2;
final hover = _minimal ? MinimalStyle.hover : Colors.white10;
final fontSize = _minimal ? MinimalStyle.fontSize : 14.0;
final dim = _minimal ? MinimalStyle.dim : Colors.white70;
// The picture fills the button's height but for a 7 px margin.
final thumbWidth = (height - 14) * 16 / 9;
final thumbRadius = _uosc ? UoscStyle.radius : 6.0;
final thumbRadius = _minimal ? MinimalStyle.radius - 4 : 6.0;
return SignalBuilder(builder: (context) {
final items = _q.items.value;
final cont = _q.shownContinuation.value;
@@ -197,7 +195,7 @@ class _UpNextButtonState extends State<UpNextButton> {
duration: const Duration(milliseconds: 150),
height: height,
// The picture clears a round button's curve.
padding: EdgeInsets.only(left: _uosc ? 7 : 12, right: 6),
padding: EdgeInsets.only(left: _minimal ? 7 : 12, right: 6),
decoration: BoxDecoration(
color: _hoverTitle || open ? hover : Colors.transparent,
borderRadius: BorderRadius.circular(radius),
@@ -234,7 +232,7 @@ class _UpNextButtonState extends State<UpNextButton> {
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
decoration: BoxDecoration(color: UoscStyle.hover, borderRadius: BorderRadius.circular(999)),
decoration: BoxDecoration(color: MinimalStyle.hover, borderRadius: BorderRadius.circular(999)),
child: Text('+$more', style: TextStyle(color: dim, fontSize: 11)),
),
],
@@ -254,8 +252,8 @@ class _UpNextButtonState extends State<UpNextButton> {
? 'Play ${item!.title} now ($playNextKeys)'
: 'Play ${episode == null ? '' : episode.label} now ($playNextKeys)';
final VoidCallback? play = queued ? _q.skip : (episode != null ? widget.onPlayNextEpisode : null);
final playButton = _uosc
? UoscButton(key: const ValueKey('up-next-play'), icon: Icons.skip_next_rounded, tooltip: playTip, onPressed: play)
final playButton = _minimal
? MinimalButton(key: const ValueKey('up-next-play'), icon: Icons.skip_next_rounded, tooltip: playTip, onPressed: play)
: Tooltip(
message: playTip,
waitDuration: const Duration(milliseconds: 500),