import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../data/services/sync_service.dart'; import '../../data/services/sound_service.dart'; import '../../core/enums.dart'; import '../../core/sacred_tokens.dart'; import '../../providers.dart'; import '../admin/admin_screen.dart'; import 'main_screen.dart'; import 'adzan_screen.dart'; import 'iqomah_screen.dart'; import 'black_screen.dart'; import 'slideshow_screen.dart'; import 'jumat_screen.dart'; import 'khutbah_screen.dart'; /// The root view that orchestrates all screen states via AnimatedSwitcher. class HomeView extends ConsumerStatefulWidget { const HomeView({super.key}); @override ConsumerState createState() => _HomeViewState(); } class _HomeViewState extends ConsumerState { static const List _adminUnlockSequence = [ LogicalKeyboardKey.arrowUp, LogicalKeyboardKey.arrowUp, LogicalKeyboardKey.arrowDown, LogicalKeyboardKey.arrowDown, LogicalKeyboardKey.arrowLeft, LogicalKeyboardKey.arrowRight, LogicalKeyboardKey.arrowLeft, LogicalKeyboardKey.arrowRight, LogicalKeyboardKey.select, ]; final FocusNode _homeFocusNode = FocusNode(debugLabel: 'home_tv_root'); final List _recentKeys = []; final List _simulationShortcutKeys = []; Timer? _comboResetTimer; Timer? _simulationShortcutTimer; Timer? _autoRefreshTimer; Timer? _touchUnlockTimer; bool _isAutoRefreshRunning = false; int _touchUnlockTapCount = 0; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { _checkAutoSync(); _startAutoRefreshMonitor(); if (mounted) { _homeFocusNode.requestFocus(); } }); } @override void dispose() { _comboResetTimer?.cancel(); _simulationShortcutTimer?.cancel(); _autoRefreshTimer?.cancel(); _touchUnlockTimer?.cancel(); _homeFocusNode.dispose(); super.dispose(); } void _startAutoRefreshMonitor() { _autoRefreshTimer?.cancel(); _autoRefreshTimer = Timer.periodic( const Duration(hours: 6), (_) => _checkAutoSync(), ); } Future _checkAutoSync() async { if (_isAutoRefreshRunning || !mounted) return; _isAutoRefreshRunning = true; try { final result = await SyncService.instance.autoRefreshIfNeeded(); if (!mounted) return; if (result.synced) { debugPrint('[AutoSync] Cache refreshed successfully.'); ref.invalidate(todayScheduleProvider); ref.invalidate(scheduleCacheStatusProvider); return; } if (result.attempted) { debugPrint('[AutoSync] Refresh attempt failed. Staying on local cache.'); } } finally { _isAutoRefreshRunning = false; } } KeyEventResult _handleTvKey(FocusNode node, KeyEvent event) { if (event is! KeyDownEvent) return KeyEventResult.ignored; final key = event.logicalKey; final isSimulating = ref.read(mockTimeOffsetProvider) != Duration.zero; if (isSimulating && key == LogicalKeyboardKey.arrowLeft) { _comboResetTimer?.cancel(); _recentKeys.clear(); _simulationShortcutTimer?.cancel(); _simulationShortcutTimer = Timer( const Duration(seconds: 2), _resetSimulationShortcut, ); _simulationShortcutKeys.add(key); if (_simulationShortcutKeys.length > 2) { _simulationShortcutKeys.removeAt(0); } if (_matchesSimulationShortcut()) { _resetSimulationShortcut(); ref.read(mockTimeOffsetProvider.notifier).state = Duration.zero; WidgetsBinding.instance.addPostFrameCallback((_) async { if (!mounted) return; await Navigator.of(context).push( MaterialPageRoute( builder: (_) => const AdminScreen( initialTab: 4, focusSelectedTabOnOpen: true, ), ), ); if (mounted) { _homeFocusNode.requestFocus(); } }); } return KeyEventResult.handled; } else if (isSimulating) { _resetSimulationShortcut(); } if (isSimulating && (key == LogicalKeyboardKey.escape || key == LogicalKeyboardKey.goBack || key == LogicalKeyboardKey.browserBack)) { ref.read(mockTimeOffsetProvider.notifier).state = Duration.zero; return KeyEventResult.handled; } if (!_isComboKey(key)) { _resetCombo(); return KeyEventResult.ignored; } _comboResetTimer?.cancel(); _comboResetTimer = Timer(const Duration(seconds: 3), _resetCombo); _recentKeys.add(key); if (_recentKeys.length > _adminUnlockSequence.length) { _recentKeys.removeAt(0); } if (_matchesUnlockSequence()) { _resetCombo(); WidgetsBinding.instance.addPostFrameCallback((_) async { if (!mounted) return; await Navigator.of(context).push( MaterialPageRoute(builder: (_) => const AdminScreen()), ); if (mounted) { _homeFocusNode.requestFocus(); } }); return KeyEventResult.handled; } return KeyEventResult.ignored; } bool _isComboKey(LogicalKeyboardKey key) { return key == LogicalKeyboardKey.arrowUp || key == LogicalKeyboardKey.arrowDown || key == LogicalKeyboardKey.arrowLeft || key == LogicalKeyboardKey.arrowRight || key == LogicalKeyboardKey.select || key == LogicalKeyboardKey.enter; } bool _matchesUnlockSequence() { 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]; if (current != _adminUnlockSequence[i]) return false; } return true; } void _resetCombo() { _comboResetTimer?.cancel(); _recentKeys.clear(); } bool _matchesSimulationShortcut() { return _simulationShortcutKeys.length == 2 && _simulationShortcutKeys[0] == LogicalKeyboardKey.arrowLeft && _simulationShortcutKeys[1] == LogicalKeyboardKey.arrowLeft; } void _resetSimulationShortcut() { _simulationShortcutTimer?.cancel(); _simulationShortcutKeys.clear(); } void _resetTouchUnlock() { _touchUnlockTimer?.cancel(); _touchUnlockTapCount = 0; } void _registerTouchUnlockTap() { _touchUnlockTimer?.cancel(); _touchUnlockTimer = Timer(const Duration(seconds: 3), _resetTouchUnlock); _touchUnlockTapCount += 1; if (_touchUnlockTapCount < 5) return; _resetTouchUnlock(); WidgetsBinding.instance.addPostFrameCallback((_) async { if (!mounted) return; await Navigator.of(context).push( MaterialPageRoute(builder: (_) => const AdminScreen()), ); if (mounted) { _homeFocusNode.requestFocus(); } }); } @override Widget build(BuildContext context) { // Audio trigger listener ref.listen(screenStateProvider, (previous, next) { if (previous == null) return; // TRIGGER 1: Adzan Beep (Fires precisely when transitioning to Adzan) if (previous.state != ScreenState.adzan && next.state == ScreenState.adzan) { SoundService.instance.playAdzanBeep(); } // TRIGGER 2: 3-Second Iqomah Countdown if (next.state == ScreenState.menujuIqomah && next.iqomahRemaining != null) { // Play precisely on the tick where it is 3 seconds. if (previous.iqomahRemaining?.inSeconds != 3 && next.iqomahRemaining!.inSeconds == 3) { SoundService.instance.playIqomahCountdown(); } } }); final screenData = ref.watch(screenStateProvider); final isMainScreen = ref.watch(isMainScreenProvider); // Determine which screen to display Widget screen; switch (screenData.state) { case ScreenState.normal: case ScreenState.menujuAdzan: if (screenData.isFriday && screenData.nextPrayer?.id == 'dzuhur') { screen = const JumatScreen(key: ValueKey('jumat')); } else { screen = isMainScreen ? const MainScreen(key: ValueKey('main')) : const SlideshowScreen(key: ValueKey('slideshow')); } break; case ScreenState.kembaliNormal: screen = const MainScreen(key: ValueKey('main')); break; case ScreenState.adzan: screen = const AdzanAlertScreen(key: ValueKey('adzan')); break; case ScreenState.menujuIqomah: if (screenData.isFriday && screenData.activePrayer?.id == 'dzuhur') { screen = const KhutbahScreen(key: ValueKey('khutbah')); } else { screen = const IqomahScreen(key: ValueKey('iqomah')); } break; case ScreenState.shalat: screen = const BlackScreen(key: ValueKey('black')); break; } return Focus( autofocus: true, focusNode: _homeFocusNode, onKeyEvent: _handleTvKey, child: Scaffold( backgroundColor: SacredColors.background, body: Stack( children: [ AnimatedSwitcher( duration: const Duration(milliseconds: 800), transitionBuilder: (child, animation) { return FadeTransition(opacity: animation, child: child); }, child: screen, ), // Hidden touch fallback for phones/tablets during testing: // tap top-left area 5x quickly to open admin. Positioned( top: 0, left: 0, child: GestureDetector( behavior: HitTestBehavior.translucent, onTap: _registerTouchUnlockTap, child: const SizedBox(width: 110, height: 110), ), ), ], ), ), ); } }