Shaders: Anime4K presets in-session and in Settings
Vendored Anime4K v4.0.1 GLSL (MIT), extracted to app support once per version. Presets A/B/C/A+A/B+B/C+A in HQ and Fast per Anime4K's mpv templates, plus custom files. Shader button in the video controls (works in fullscreen), Ctrl+1..6 / Ctrl+0, live apply with read-back, failures fall back to Off with a toast. Render mode detected from mpv's GL log; warns when on the software path. Docs include the Jellyfin client section. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4814bfb561
commit
fb2927c094
28 files changed
+6516
-53
No files matched your search
@@ -5,6 +5,10 @@ import 'package:signals/signals_flutter.dart';
|
||||
import 'package:sync_relay_core/core.dart';
|
||||
|
||||
import '../session/health.dart';
|
||||
import '../session/session_coordinator.dart';
|
||||
import '../shaders/shader_menu.dart';
|
||||
import '../shaders/shader_presets.dart';
|
||||
import '../state/settings.dart';
|
||||
import '../state/session_state.dart';
|
||||
import '../update/updater.dart';
|
||||
import '../widgets/dialogs.dart';
|
||||
@@ -38,7 +42,12 @@ class _SessionScreenState extends State<SessionScreen> {
|
||||
late final void Function() _stopKickWatch;
|
||||
late final void Function() _stopSheetWatch;
|
||||
late final void Function() _stopPlayerWatch;
|
||||
late final void Function() _stopShaderWatch;
|
||||
late final FocusNode _noFocus;
|
||||
bool _shaderMenuOpen = false;
|
||||
// Own toast ids, negative: the coordinator counts up from 1.
|
||||
int _toastSeq = 0;
|
||||
int _shaderNoticeSeen = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -57,6 +66,13 @@ class _SessionScreenState extends State<SessionScreen> {
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
}
|
||||
});
|
||||
// Shader messages (preset changes, load failures) become toasts.
|
||||
_stopShaderWatch = effect(() {
|
||||
final n = session.coordinator.playback.shaderNotice.value;
|
||||
if (n == null || n.id == _shaderNoticeSeen) return;
|
||||
_shaderNoticeSeen = n.id;
|
||||
_toast(n.text);
|
||||
});
|
||||
// The fullscreen route holds its own `Video` of the old player: leave
|
||||
// it before the player goes away, re-enter once the new one is up.
|
||||
_stopPlayerWatch = effect(() {
|
||||
@@ -88,6 +104,25 @@ class _SessionScreenState extends State<SessionScreen> {
|
||||
coord.reloadVideo();
|
||||
}
|
||||
|
||||
void _toast(String text) {
|
||||
final coord = SessionState.instance.coordinator;
|
||||
coord.toasts.value = [
|
||||
...coord.toasts.peek(),
|
||||
ToastItem(id: --_toastSeq, text: text, at: DateTime.now().toUtc()),
|
||||
];
|
||||
}
|
||||
|
||||
/// Menu, Ctrl+0..6: saved as the choice (also used on the next join)
|
||||
/// and applied live. A null quality keeps the saved one.
|
||||
void _pickShader(ShaderPreset preset, ShaderQuality? quality) {
|
||||
final s = AppSettings.instance;
|
||||
if (quality != null) s.setShaderQuality(quality);
|
||||
s.setShaderPreset(preset);
|
||||
final coord = SessionState.instance.coordinator;
|
||||
coord.markInput();
|
||||
coord.playback.applyShaders(announce: true);
|
||||
}
|
||||
|
||||
void _toggleFullscreen() {
|
||||
if (_isFullscreen.peek()) {
|
||||
if (mounted) {
|
||||
@@ -106,6 +141,7 @@ class _SessionScreenState extends State<SessionScreen> {
|
||||
_stopKickWatch();
|
||||
_stopSheetWatch();
|
||||
_stopPlayerWatch();
|
||||
_stopShaderWatch();
|
||||
_chat.dispose();
|
||||
_chatFocus.dispose();
|
||||
_swapLink.dispose();
|
||||
@@ -122,6 +158,14 @@ class _SessionScreenState extends State<SessionScreen> {
|
||||
return false;
|
||||
}
|
||||
final coord = SessionState.instance.coordinator;
|
||||
// Ctrl+0..6: Anime4K's own mpv keys (off, modes A..C+A).
|
||||
final digit = _shaderKeys.indexOf(event.logicalKey);
|
||||
if (digit >= 0 && HardwareKeyboard.instance.isControlPressed) {
|
||||
_lastKeyAt = now;
|
||||
_pickShader(
|
||||
digit == 0 ? ShaderPreset.off : ShaderPreset.modes[digit - 1], null);
|
||||
return true;
|
||||
}
|
||||
if (event.logicalKey == LogicalKeyboardKey.keyR &&
|
||||
HardwareKeyboard.instance.isControlPressed) {
|
||||
_lastKeyAt = now;
|
||||
@@ -150,6 +194,10 @@ class _SessionScreenState extends State<SessionScreen> {
|
||||
return true;
|
||||
}
|
||||
if (event.logicalKey == LogicalKeyboardKey.escape) {
|
||||
if (_shaderMenuOpen) {
|
||||
if (mounted) Navigator.of(context, rootNavigator: true).maybePop();
|
||||
return true;
|
||||
}
|
||||
if (_sheetOpen.peek()) {
|
||||
_sheetOpen.value = false;
|
||||
return true;
|
||||
@@ -212,6 +260,12 @@ class _SessionScreenState extends State<SessionScreen> {
|
||||
_ReloadVideoControlButton(focusNode: _noFocus, onPressed: _reloadVideo),
|
||||
const MaterialDesktopPositionIndicator(),
|
||||
const Spacer(),
|
||||
ShaderControlButton(
|
||||
focusNode: _noFocus,
|
||||
renderMode: coord.playback.renderMode,
|
||||
onPick: _pickShader,
|
||||
onMenuOpen: (open) => _shaderMenuOpen = open,
|
||||
),
|
||||
const MaterialDesktopFullscreenButton(),
|
||||
],
|
||||
);
|
||||
@@ -527,6 +581,16 @@ class _SessionScreenState extends State<SessionScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
const _shaderKeys = [
|
||||
LogicalKeyboardKey.digit0,
|
||||
LogicalKeyboardKey.digit1,
|
||||
LogicalKeyboardKey.digit2,
|
||||
LogicalKeyboardKey.digit3,
|
||||
LogicalKeyboardKey.digit4,
|
||||
LogicalKeyboardKey.digit5,
|
||||
LogicalKeyboardKey.digit6,
|
||||
];
|
||||
|
||||
class _ReadyControlButton extends StatelessWidget {
|
||||
final FocusNode focusNode;
|
||||
const _ReadyControlButton({required this.focusNode});
|
||||
|
||||
Reference in new issue
Block a user