feat(tv-ui): add slideshow pattern mode and improve admin readability
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user