feat(tv-ui): add slideshow pattern mode and improve admin readability

This commit is contained in:
dwindown
2026-04-06 09:23:42 +07:00
parent 185c55a143
commit 414450125d
6 changed files with 312 additions and 34 deletions

View File

@@ -49,6 +49,10 @@ class _HomeViewState extends ConsumerState<HomeView> {
bool _isAutoRefreshRunning = false;
int _touchUnlockTapCount = 0;
LogicalKeyboardKey _normalizedComboKey(LogicalKeyboardKey key) {
return key == LogicalKeyboardKey.enter ? LogicalKeyboardKey.select : key;
}
@override
void initState() {
super.initState();
@@ -162,6 +166,13 @@ class _HomeViewState extends ConsumerState<HomeView> {
_recentKeys.removeAt(0);
}
final manualAction = _matchManualRotateSequence();
if (manualAction != null) {
_dispatchManualBackgroundRotate(manualAction);
_resetCombo();
return KeyEventResult.handled;
}
if (_matchesUnlockSequence()) {
_resetCombo();
WidgetsBinding.instance.addPostFrameCallback((_) async {
@@ -192,14 +203,50 @@ class _HomeViewState extends ConsumerState<HomeView> {
if (_recentKeys.length != _adminUnlockSequence.length) return false;
for (var i = 0; i < _adminUnlockSequence.length; i++) {
final current = _recentKeys[i] == LogicalKeyboardKey.enter
? LogicalKeyboardKey.select
: _recentKeys[i];
final current = _normalizedComboKey(_recentKeys[i]);
if (current != _adminUnlockSequence[i]) return false;
}
return true;
}
BackgroundRotateAction? _matchManualRotateSequence() {
if (_recentKeys.length < 3) return null;
final tail = _recentKeys.sublist(_recentKeys.length - 3).map(_normalizedComboKey).toList();
if (tail[0] == LogicalKeyboardKey.arrowRight &&
tail[1] == LogicalKeyboardKey.arrowRight &&
tail[2] == LogicalKeyboardKey.select) {
return BackgroundRotateAction.next;
}
if (tail[0] == LogicalKeyboardKey.arrowLeft &&
tail[1] == LogicalKeyboardKey.arrowLeft &&
tail[2] == LogicalKeyboardKey.select) {
return BackgroundRotateAction.previous;
}
if (tail[0] == LogicalKeyboardKey.arrowDown &&
tail[1] == LogicalKeyboardKey.arrowDown &&
tail[2] == LogicalKeyboardKey.select) {
return BackgroundRotateAction.random;
}
return null;
}
void _dispatchManualBackgroundRotate(BackgroundRotateAction action) {
final screenData = ref.read(screenStateProvider);
final isMainScreen = ref.read(isMainScreenProvider);
if (!isMainScreen ||
!(screenData.state == ScreenState.normal ||
screenData.state == ScreenState.menujuAdzan)) {
return;
}
final notifier = ref.read(backgroundRotateCommandProvider.notifier);
final current = notifier.state;
notifier.state = BackgroundRotateCommand(
nonce: current.nonce + 1,
action: action,
);
}
void _resetCombo() {
_comboResetTimer?.cancel();
_recentKeys.clear();
@@ -262,6 +309,7 @@ class _HomeViewState extends ConsumerState<HomeView> {
final screenData = ref.watch(screenStateProvider);
final isMainScreen = ref.watch(isMainScreenProvider);
final rotationIndex = ref.watch(rotationIndexProvider);
// Determine which screen to display
Widget screen;
@@ -273,7 +321,7 @@ class _HomeViewState extends ConsumerState<HomeView> {
} else {
screen = isMainScreen
? const MainScreen(key: ValueKey('main'))
: const SlideshowScreen(key: ValueKey('slideshow'));
: SlideshowScreen(key: ValueKey('slideshow-$rotationIndex'));
}
break;
case ScreenState.kembaliNormal:

View File

@@ -4,6 +4,7 @@ import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../data/local/models.dart';
import '../../data/services/unsplash_cache_service.dart';
import '../../providers.dart';
@@ -25,6 +26,7 @@ class _UnsplashBackgroundState extends ConsumerState<UnsplashBackground> {
String? _lastKeyword;
int? _lastRotationHours;
bool? _lastUseUnsplash;
int _lastHandledRotateNonce = 0;
@override
void initState() {
@@ -106,6 +108,50 @@ class _UnsplashBackgroundState extends ConsumerState<UnsplashBackground> {
}
}
void _showNextImage() {
if (_imagePaths.length <= 1 || !mounted) return;
setState(() {
_currentIndex = (_currentIndex + 1) % _imagePaths.length;
});
}
void _showPreviousImage() {
if (_imagePaths.length <= 1 || !mounted) return;
setState(() {
_currentIndex = (_currentIndex - 1 + _imagePaths.length) % _imagePaths.length;
});
}
Future<void> _handleManualRotate(
BackgroundRotateAction action,
AppSettings settings,
) async {
if (!settings.useUnsplashBackground) return;
if (_imagePaths.isEmpty) {
final cachedPaths = await UnsplashCacheService.instance.getCachedImagePaths(
settings.unsplashKeyword,
);
if (!mounted) return;
if (cachedPaths.isNotEmpty) {
_applyImagePaths(cachedPaths);
}
}
if (_imagePaths.isEmpty) return;
switch (action) {
case BackgroundRotateAction.next:
_showNextImage();
break;
case BackgroundRotateAction.previous:
_showPreviousImage();
break;
case BackgroundRotateAction.random:
_nextRandomImage();
break;
}
}
void _startTimer(int hours) {
_rotationTimer?.cancel();
if (hours <= 0) return;
@@ -125,6 +171,7 @@ class _UnsplashBackgroundState extends ConsumerState<UnsplashBackground> {
@override
Widget build(BuildContext context) {
final settings = ref.watch(settingsProvider);
final rotateCommand = ref.watch(backgroundRotateCommandProvider);
// Watch for config changes
if (settings.useUnsplashBackground != _lastUseUnsplash) {
@@ -146,6 +193,14 @@ class _UnsplashBackgroundState extends ConsumerState<UnsplashBackground> {
_startTimer(settings.unsplashRotationHours);
}
if (rotateCommand.nonce != _lastHandledRotateNonce) {
_lastHandledRotateNonce = rotateCommand.nonce;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_handleManualRotate(rotateCommand.action, settings);
});
}
if (!settings.useUnsplashBackground || _imagePaths.isEmpty) {
return const SizedBox.shrink(); // Fallback to flat background handled underneath
}