forked from Foxyz/sync_replay_project
Player: quick chat box, danmaku notifications, the player panel, smooth subtitle lift
- Quick chat (Enter in the full player, whatever plays): a box at the bottom centre in the control bar's place, compact and widening with the text, sharing its draft with the chat panel; Enter sends, Esc or an empty Enter closes, it closes by itself after 8 s without typing (the draft stays). Keys go to it; the Enter that opened it no longer reaches a focused button. Typing never shows the cursor or the controls: the frame wakes only on real mouse movement and holds the controls hidden while the box or its echo is up. Your sent line shows as an echo (or flies as your own danmaku line); your own chat toast is not repeated. - Subtitle lift: always for the box, for the control bar as a setting (off); 4 eased-out sub-pos steps 42 ms apart instead of one per frame. - Notifications: Toasts / Danmaku (off by default): chat and short notices fly over the full player, problems stay toasts; every knob of the look plus boldness, persisted, with a live preview strip in Settings. - Player panel: ⚙ → Player settings docks a foldable glass panel at the right (tracks, subtitle size and lift, picture, volume, notifications, controls) in place of the card; Esc closes it. - Danmaku preview: the mock dialogue follows the lift; the stand-in page has the chat box too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6048fbe7d1
commit
69fae18fdc
29 files changed
+2755
-581
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart' show ValueListenable;
|
||||
import 'package:flutter/gestures.dart' show kPrimaryMouseButton;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -10,7 +11,10 @@ import 'controls_probe.dart' show ControlsHold;
|
||||
// without any (or the mouse leaving) hides them and the cursor. Holds keep
|
||||
// them up: a seek drag (no hover events arrive), an open menu (a route
|
||||
// takes the mouse), and the session's [ControlsHold] (the mouse on a
|
||||
// floating pill or card, the Up next popover).
|
||||
// floating pill or card, the Up next popover). Only real mouse movement
|
||||
// wakes them: a widget appearing or going under a still pointer (the
|
||||
// quick chat box) does not, nor does typing. [ControlsFrame.suppress]
|
||||
// hides them while something takes their place (the quick chat box).
|
||||
|
||||
class ControlsFrame extends StatefulWidget {
|
||||
/// Draws the controls; [visible] false while they are hidden.
|
||||
@@ -28,6 +32,11 @@ class ControlsFrame extends StatefulWidget {
|
||||
|
||||
/// Keeps them up while held.
|
||||
final ControlsHold? holdBy;
|
||||
|
||||
/// While true the controls are drawn hidden and [shown] says so; the
|
||||
/// cursor still follows the mouse. They come back as they would have
|
||||
/// been once it is false again.
|
||||
final ValueListenable<bool>? suppress;
|
||||
const ControlsFrame({
|
||||
super.key,
|
||||
required this.builder,
|
||||
@@ -37,6 +46,7 @@ class ControlsFrame extends StatefulWidget {
|
||||
this.hideAfter = const Duration(seconds: 3),
|
||||
this.shown,
|
||||
this.holdBy,
|
||||
this.suppress,
|
||||
});
|
||||
|
||||
/// The frame around [context], for widgets that keep it up (a drag, a
|
||||
@@ -62,16 +72,23 @@ class ControlsFrameState extends State<ControlsFrame> {
|
||||
int _holds = 0;
|
||||
bool _heldBy = false;
|
||||
Timer? _timer;
|
||||
// Where the mouse was last seen, to tell real movement from a widget
|
||||
// appearing or going under it.
|
||||
Offset? _lastPos;
|
||||
|
||||
bool get visible => _visible;
|
||||
bool get _suppressed => widget.suppress?.value ?? false;
|
||||
|
||||
/// The controls are drawn (up and not suppressed).
|
||||
bool get visible => _visible && !_suppressed;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.holdBy?.addListener(_holdByChanged);
|
||||
widget.suppress?.addListener(_suppressChanged);
|
||||
// Not during this build: an overlay listening to [shown] may be built.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) widget.shown?.value = true;
|
||||
if (mounted) widget.shown?.value = visible;
|
||||
});
|
||||
_hideLater();
|
||||
}
|
||||
@@ -84,12 +101,17 @@ class ControlsFrameState extends State<ControlsFrame> {
|
||||
widget.holdBy?.addListener(_holdByChanged);
|
||||
_holdByChanged();
|
||||
}
|
||||
if (oldWidget.suppress != widget.suppress) {
|
||||
oldWidget.suppress?.removeListener(_suppressChanged);
|
||||
widget.suppress?.addListener(_suppressChanged);
|
||||
}
|
||||
if (oldWidget.hideAfter != widget.hideAfter && _visible) _hideLater();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.holdBy?.removeListener(_holdByChanged);
|
||||
widget.suppress?.removeListener(_suppressChanged);
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -101,15 +123,29 @@ class ControlsFrameState extends State<ControlsFrame> {
|
||||
hold(on);
|
||||
}
|
||||
|
||||
void _suppressChanged() {
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
widget.shown?.value = visible;
|
||||
}
|
||||
|
||||
/// Show the controls and restart the hide countdown.
|
||||
void wake() {
|
||||
if (!_visible && mounted) {
|
||||
setState(() => _visible = true);
|
||||
widget.shown?.value = true;
|
||||
widget.shown?.value = visible;
|
||||
}
|
||||
_hideLater();
|
||||
}
|
||||
|
||||
/// A hover or enter at [pos]: wakes only when the mouse really moved.
|
||||
void _moved(Offset pos) {
|
||||
final last = _lastPos;
|
||||
_lastPos = pos;
|
||||
if (last != null && (pos - last).distance < 0.5) return;
|
||||
wake();
|
||||
}
|
||||
|
||||
/// Keep the controls up while [on] (paired calls).
|
||||
void hold(bool on) {
|
||||
_holds = on ? _holds + 1 : (_holds - 1).clamp(0, 1 << 20);
|
||||
@@ -141,9 +177,10 @@ class ControlsFrameState extends State<ControlsFrame> {
|
||||
Widget build(BuildContext context) {
|
||||
return MouseRegion(
|
||||
cursor: _visible ? SystemMouseCursors.basic : SystemMouseCursors.none,
|
||||
onEnter: (_) => wake(),
|
||||
onHover: (_) => wake(),
|
||||
onExit: (_) {
|
||||
onEnter: (e) => _moved(e.position),
|
||||
onHover: (e) => _moved(e.position),
|
||||
onExit: (e) {
|
||||
_lastPos = e.position;
|
||||
_timer?.cancel();
|
||||
_hide();
|
||||
},
|
||||
@@ -154,12 +191,12 @@ class ControlsFrameState extends State<ControlsFrame> {
|
||||
Positioned.fill(
|
||||
child: DoubleClickListener(
|
||||
onDoubleClick: widget.onDoubleClick!,
|
||||
skipTop: _visible ? widget.barTop : 0,
|
||||
skipBottom: _visible ? widget.barBottom : 0,
|
||||
skipTop: visible ? widget.barTop : 0,
|
||||
skipBottom: visible ? widget.barBottom : 0,
|
||||
child: const ColoredBox(color: Color(0x00000000), child: SizedBox.expand()),
|
||||
),
|
||||
),
|
||||
widget.builder(context, _visible),
|
||||
widget.builder(context, visible),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user