- 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>
96 lines
4.2 KiB
Dart
96 lines
4.2 KiB
Dart
import 'package:flutter/widgets.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:starsync/src/platform/ports/player_host.dart' show RenderMode;
|
|
import 'package:starsync/src/screens/home/ui.dart' show devToolsFor;
|
|
import 'package:starsync/src/shell/video_view.dart';
|
|
|
|
// The video view's asks of mpv: the output size ("Sharp video and
|
|
// subtitles") and the dev experiment's subtitle lift.
|
|
|
|
void main() {
|
|
group('videoOutputSize', () {
|
|
test('the view in physical pixels on the GPU', () {
|
|
expect(videoOutputSize(const Size(1280, 720), 1.5, on: true, mode: RenderMode.gpu), (1920, 1080));
|
|
expect(videoOutputSize(const Size(384, 216), 2, on: true, mode: RenderMode.gpu), (768, 432));
|
|
});
|
|
|
|
test("the video's own size when off, not on the GPU yet, or in software", () {
|
|
expect(videoOutputSize(const Size(1280, 720), 1, on: false, mode: RenderMode.gpu), isNull);
|
|
for (final m in [RenderMode.unknown, RenderMode.software, RenderMode.softwareGl]) {
|
|
expect(videoOutputSize(const Size(1280, 720), 1, on: true, mode: m), isNull, reason: m.name);
|
|
}
|
|
expect(videoOutputSize(const Size(4, 4), 1, on: true, mode: RenderMode.gpu), isNull);
|
|
});
|
|
});
|
|
|
|
group('subtitleLiftPos', () {
|
|
test('a picture filling the view lifts by the controls', () {
|
|
// 108 px of controls + 8 px gap in a 1080 px picture: 10.74 %.
|
|
final pos = subtitleLiftPos(view: const Size(1920, 1080), videoW: 1920, videoH: 1080, controls: 108);
|
|
expect(pos, closeTo(100 - 116 / 1080 * 100, 1e-9));
|
|
});
|
|
|
|
test('a letterbox under the picture counts toward the room', () {
|
|
// 2.39:1 in 16:9: 803 px picture, 138 px bar below it.
|
|
final none = subtitleLiftPos(view: const Size(1920, 1080), videoW: 1920, videoH: 803, controls: 100);
|
|
expect(none, 100);
|
|
final some = subtitleLiftPos(view: const Size(1920, 1080), videoW: 1920, videoH: 803, controls: 200);
|
|
expect(some, lessThan(100));
|
|
});
|
|
|
|
test('unknown video size: as if it filled the view; never past half', () {
|
|
expect(subtitleLiftPos(view: const Size(800, 450), controls: 90), closeTo(100 - 98 / 450 * 100, 1e-9));
|
|
expect(subtitleLiftPos(view: const Size(800, 100), controls: 500), 50);
|
|
expect(subtitleLiftPos(view: Size.zero, controls: 90), 100);
|
|
});
|
|
});
|
|
|
|
group('subtitle lift steps', () {
|
|
test('a few large eased-out steps, the last exactly on target', () {
|
|
final up = liftSteps(0, 100);
|
|
expect(up, hasLength(liftStepCount));
|
|
expect(up.last, 100);
|
|
// Eased out: the first step moves most, each one less.
|
|
for (var i = 1; i < up.length; i++) {
|
|
expect(up[i], greaterThan(up[i - 1]));
|
|
if (i > 1) expect(up[i] - up[i - 1], lessThan(up[i - 1] - (i > 1 ? up[i - 2] : 0)));
|
|
}
|
|
expect(up.first, greaterThan(30), reason: 'most of the way at once');
|
|
final down = liftSteps(100, 0);
|
|
expect(down.last, 0);
|
|
expect(down.first, lessThan(70));
|
|
expect(liftSteps(40, 40.2), isEmpty);
|
|
});
|
|
|
|
testWidgets('SubtitleLift steps toward the latest target, liftStepGap apart', (tester) async {
|
|
final lift = SubtitleLift();
|
|
addTearDown(lift.dispose);
|
|
final seen = <double>[];
|
|
lift.addListener(() => seen.add(lift.value));
|
|
lift.animateTo(80);
|
|
expect(seen, hasLength(1), reason: 'the first step at once');
|
|
await tester.pump(liftStepGap * (liftStepCount - 1));
|
|
expect(seen, liftSteps(0, 80));
|
|
expect(lift.value, 80);
|
|
await tester.pump(liftStepGap * 4);
|
|
expect(seen, hasLength(liftStepCount), reason: 'nothing after the target');
|
|
// A new target mid-way starts from where it is.
|
|
seen.clear();
|
|
lift.animateTo(0);
|
|
await tester.pump(liftStepGap);
|
|
final from = lift.value, n = seen.length;
|
|
expect(from, inExclusiveRange(0, 80));
|
|
lift.animateTo(80);
|
|
await tester.pump(liftStepGap * liftStepCount);
|
|
expect(seen.sublist(n), liftSteps(from, 80));
|
|
});
|
|
});
|
|
|
|
test('dev tools: dev versions and debug runs only', () {
|
|
expect(devToolsFor(version: '1.3.3-dev.2', debug: false), isTrue);
|
|
expect(devToolsFor(version: '0.0.0-dev', debug: false), isTrue);
|
|
expect(devToolsFor(version: '1.3.2', debug: true), isTrue);
|
|
expect(devToolsFor(version: '1.3.2', debug: false), isFalse);
|
|
});
|
|
}
|