From c1eade84ab03313d639f1a84db586267d138670a Mon Sep 17 00:00:00 2001 From: dwindown Date: Tue, 7 Apr 2026 11:27:39 +0700 Subject: [PATCH] Add midnight fallback notice, overnight simulations, and keyboard log filter --- lib/features/admin/admin_screen.dart | 3212 ++++++++++++++------------ lib/features/home/adzan_screen.dart | 32 +- lib/features/home/home_view.dart | 22 +- lib/features/home/jumat_screen.dart | 232 +- lib/features/home/main_screen.dart | 38 +- lib/main.dart | 23 +- lib/providers.dart | 43 +- pubspec.yaml | 2 +- 8 files changed, 1989 insertions(+), 1615 deletions(-) diff --git a/lib/features/admin/admin_screen.dart b/lib/features/admin/admin_screen.dart index 890370d..88cb6bc 100644 --- a/lib/features/admin/admin_screen.dart +++ b/lib/features/admin/admin_screen.dart @@ -37,13 +37,13 @@ class _AdminScreenState extends ConsumerState { final _masjidNameCtrl = TextEditingController(); final _masjidAddressCtrl = TextEditingController(); final _cityCtrl = TextEditingController(); // Displays DisplayName or CityID - + final _mainDurCtrl = TextEditingController(); final _slideDurCtrl = TextEditingController(); final _slidesPerMainCtrl = TextEditingController(); final _mainHeroDurCtrl = TextEditingController(); final _textSlideDurCtrl = TextEditingController(); - + int _selectedTab = 0; bool _isSyncing = false; int _textScaleIndex = 1; @@ -147,7 +147,7 @@ class _AdminScreenState extends ConsumerState { _simulasiFocusNodes = [ _simulasiEntryFocusNode, ...List.generate( - 8, + 10, (index) => FocusNode(debugLabel: 'simulasi_row_${index + 1}'), ), ]; @@ -194,7 +194,7 @@ class _AdminScreenState extends ConsumerState { _scaleTextSlideCenter = settings.scaleTextSlideCenter; _khatibCtrl.text = settings.khatibName; _imamCtrl.text = settings.imamName; - + _iqomahSubuhCtrl.text = settings.iqomahSubuh.toString(); _iqomahDzuhurCtrl.text = settings.iqomahDzuhur.toString(); _iqomahAsharCtrl.text = settings.iqomahAshar.toString(); @@ -204,7 +204,7 @@ class _AdminScreenState extends ConsumerState { _blankNormalCtrl.text = settings.blankScreenNormal.toString(); _blankJumatCtrl.text = settings.blankScreenJumat.toString(); _hijriOffsetDays = settings.hijriOffsetDays; - + _mainDurCtrl.addListener(_queueTampilanAutoSave); _slideDurCtrl.addListener(_queueTampilanAutoSave); _slidesPerMainCtrl.addListener(_queueTampilanAutoSave); @@ -329,16 +329,20 @@ class _AdminScreenState extends ConsumerState { s.slideshowImages = List.from(_slideshowImages); s.mainScreenDurationSec = int.tryParse(_mainDurCtrl.text.trim()) ?? 15; s.slideDurationSec = int.tryParse(_slideDurCtrl.text.trim()) ?? 10; - s.slideshowPatternMode = SlideshowPatternMode.isValid(_slideshowPatternMode) - ? _slideshowPatternMode - : SlideshowPatternMode.alternating; + s.slideshowPatternMode = + SlideshowPatternMode.isValid(_slideshowPatternMode) + ? _slideshowPatternMode + : SlideshowPatternMode.alternating; s.slideshowSlidesPerMain = (int.tryParse(_slidesPerMainCtrl.text.trim()) ?? 2) .clamp(1, 20) .toInt(); s.useUnsplashBackground = _useUnsplash; - s.unsplashKeyword = _unsplashKeywordCtrl.text.trim().isEmpty ? 'mosque' : _unsplashKeywordCtrl.text.trim(); - s.unsplashRotationHours = int.tryParse(_unsplashRotationCtrl.text.trim()) ?? 6; + s.unsplashKeyword = _unsplashKeywordCtrl.text.trim().isEmpty + ? 'mosque' + : _unsplashKeywordCtrl.text.trim(); + s.unsplashRotationHours = + int.tryParse(_unsplashRotationCtrl.text.trim()) ?? 6; s.brandedBgImage = _brandedBgImage; s.scaleCardLabel = _scaleCardLabel; s.scaleCardBody = _scaleCardBody; @@ -416,7 +420,8 @@ class _AdminScreenState extends ConsumerState { ); } on TvMediaPickerUnavailable catch (error) { if (!mounted) return const []; - final detected = error.handlers.map((handler) => handler.label).join(', '); + final detected = + error.handlers.map((handler) => handler.label).join(', '); final supported = [ 'File Commander', 'X-plore', @@ -635,7 +640,8 @@ class _AdminScreenState extends ConsumerState { _updateDownloadProgress = 0; }); - final installResult = await UpdateService.instance.downloadAndTriggerInstall( + final installResult = + await UpdateService.instance.downloadAndTriggerInstall( remote, onProgress: (progress) { if (!mounted) return; @@ -718,7 +724,8 @@ class _AdminScreenState extends ConsumerState { final query = queryCtrl.text.trim(); if (query.isEmpty) return; setDialogState(() => isSearching = true); - final res = await MyQuranSholatService.instance.searchCity(query); + final res = + await MyQuranSholatService.instance.searchCity(query); for (final node in resultFocusNodes) { node.dispose(); } @@ -885,7 +892,8 @@ class _AdminScreenState extends ConsumerState { CircularProgressIndicator( color: hasFocus ? SacredColors.onPrimary - : SacredColors.onSecondary, + : SacredColors + .onSecondary, strokeWidth: 2, ), ) @@ -894,7 +902,8 @@ class _AdminScreenState extends ConsumerState { .strokeRoundedSearch01, color: hasFocus ? SacredColors.onPrimary - : SacredColors.onSecondary, + : SacredColors + .onSecondary, ), SizedBox(width: 12 * s), Text( @@ -1005,8 +1014,7 @@ class _AdminScreenState extends ConsumerState { duration: const Duration( milliseconds: 140, ), - curve: - Curves.easeOutCubic, + curve: Curves.easeOutCubic, padding: EdgeInsets.symmetric( horizontal: 24 * s, @@ -1027,13 +1035,13 @@ class _AdminScreenState extends ConsumerState { ? SacredColors .primary .withValues( - alpha: 0.95, - ) + alpha: 0.95, + ) : SacredColors .outlineVariant .withValues( - alpha: 0.35, - ), + alpha: 0.35, + ), width: hasFocus ? 3 : 1, ), boxShadow: hasFocus @@ -1042,9 +1050,8 @@ class _AdminScreenState extends ConsumerState { color: SacredColors .primary .withValues( - alpha: - 0.28, - ), + alpha: 0.28, + ), blurRadius: 24 * s, spreadRadius: @@ -1192,128 +1199,128 @@ class _AdminScreenState extends ConsumerState { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Nav rail area - Container( - width: 350 * s, - color: SacredColors.surfaceContainerLow, - padding: EdgeInsets.all(32 * s), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _NavButton( - title: 'IDENTITAS MASJID', - icon: HugeIcons.strokeRoundedHome01, - isActive: _selectedTab == 0, - scale: s, - focusNode: _navFocusNodes[0], - onFocusChange: (focused) { - if (focused) _setSelectedTab(0); - }, - onKeyEvent: (node, event) => _handleNavKey(0, event), - onTap: () => setState(() => _selectedTab = 0), - ), - SizedBox(height: 16 * s), - _NavButton( - title: 'JADWAL & SINKRONISASI', - icon: HugeIcons.strokeRoundedCalendar01, - isActive: _selectedTab == 1, - scale: s, - focusNode: _navFocusNodes[1], - onFocusChange: (focused) { - if (focused) _setSelectedTab(1); - }, - onKeyEvent: (node, event) => _handleNavKey(1, event), - onTap: () => setState(() => _selectedTab = 1), - ), - SizedBox(height: 16 * s), - _NavButton( - title: 'TAMPILAN & MEDIA', - icon: HugeIcons.strokeRoundedImage01, - isActive: _selectedTab == 2, - scale: s, - focusNode: _navFocusNodes[2], - onFocusChange: (focused) { - if (focused) _setSelectedTab(2); - }, - onKeyEvent: (node, event) => _handleNavKey(2, event), - onTap: () => setState(() => _selectedTab = 2), - ), - SizedBox(height: 16 * s), - _NavButton( - title: 'PENGUMUMAN', - icon: HugeIcons.strokeRoundedNotification03, - isActive: _selectedTab == 3, - scale: s, - focusNode: _navFocusNodes[3], - onFocusChange: (focused) { - if (focused) _setSelectedTab(3); - }, - onKeyEvent: (node, event) => _handleNavKey(3, event), - onTap: () => setState(() => _selectedTab = 3), - ), - SizedBox(height: 16 * s), - _NavButton( - title: 'PENGATURAN JUMAT', - icon: HugeIcons.strokeRoundedCalendar01, - isActive: _selectedTab == 4, - scale: s, - focusNode: _navFocusNodes[4], - onFocusChange: (focused) { - if (focused) _setSelectedTab(4); - }, - onKeyEvent: (node, event) => _handleNavKey(4, event), - onTap: () => setState(() => _selectedTab = 4), - ), - SizedBox(height: 16 * s), - _NavButton( - title: 'SIMULASI', - icon: HugeIcons.strokeRoundedClock01, - isActive: _selectedTab == 5, - scale: s, - focusNode: _navFocusNodes[5], - onFocusChange: (focused) { - if (focused) _setSelectedTab(5); - }, - onKeyEvent: (node, event) => _handleNavKey(5, event), - onTap: () => setState(() => _selectedTab = 5), - ), - SizedBox(height: 16 * s), - _NavButton( - title: 'TENTANG', - icon: HugeIcons.strokeRoundedInformationCircle, - isActive: _selectedTab == 6, - scale: s, - focusNode: _navFocusNodes[6], - onFocusChange: (focused) { - if (focused) _setSelectedTab(6); - }, - onKeyEvent: (node, event) => _handleNavKey(6, event), - onTap: () => setState(() => _selectedTab = 6), - ), - ], + // Nav rail area + Container( + width: 350 * s, + color: SacredColors.surfaceContainerLow, + padding: EdgeInsets.all(32 * s), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _NavButton( + title: 'IDENTITAS MASJID', + icon: HugeIcons.strokeRoundedHome01, + isActive: _selectedTab == 0, + scale: s, + focusNode: _navFocusNodes[0], + onFocusChange: (focused) { + if (focused) _setSelectedTab(0); + }, + onKeyEvent: (node, event) => _handleNavKey(0, event), + onTap: () => setState(() => _selectedTab = 0), + ), + SizedBox(height: 16 * s), + _NavButton( + title: 'JADWAL & SINKRONISASI', + icon: HugeIcons.strokeRoundedCalendar01, + isActive: _selectedTab == 1, + scale: s, + focusNode: _navFocusNodes[1], + onFocusChange: (focused) { + if (focused) _setSelectedTab(1); + }, + onKeyEvent: (node, event) => _handleNavKey(1, event), + onTap: () => setState(() => _selectedTab = 1), + ), + SizedBox(height: 16 * s), + _NavButton( + title: 'TAMPILAN & MEDIA', + icon: HugeIcons.strokeRoundedImage01, + isActive: _selectedTab == 2, + scale: s, + focusNode: _navFocusNodes[2], + onFocusChange: (focused) { + if (focused) _setSelectedTab(2); + }, + onKeyEvent: (node, event) => _handleNavKey(2, event), + onTap: () => setState(() => _selectedTab = 2), + ), + SizedBox(height: 16 * s), + _NavButton( + title: 'PENGUMUMAN', + icon: HugeIcons.strokeRoundedNotification03, + isActive: _selectedTab == 3, + scale: s, + focusNode: _navFocusNodes[3], + onFocusChange: (focused) { + if (focused) _setSelectedTab(3); + }, + onKeyEvent: (node, event) => _handleNavKey(3, event), + onTap: () => setState(() => _selectedTab = 3), + ), + SizedBox(height: 16 * s), + _NavButton( + title: 'PENGATURAN JUMAT', + icon: HugeIcons.strokeRoundedCalendar01, + isActive: _selectedTab == 4, + scale: s, + focusNode: _navFocusNodes[4], + onFocusChange: (focused) { + if (focused) _setSelectedTab(4); + }, + onKeyEvent: (node, event) => _handleNavKey(4, event), + onTap: () => setState(() => _selectedTab = 4), + ), + SizedBox(height: 16 * s), + _NavButton( + title: 'SIMULASI', + icon: HugeIcons.strokeRoundedClock01, + isActive: _selectedTab == 5, + scale: s, + focusNode: _navFocusNodes[5], + onFocusChange: (focused) { + if (focused) _setSelectedTab(5); + }, + onKeyEvent: (node, event) => _handleNavKey(5, event), + onTap: () => setState(() => _selectedTab = 5), + ), + SizedBox(height: 16 * s), + _NavButton( + title: 'TENTANG', + icon: HugeIcons.strokeRoundedInformationCircle, + isActive: _selectedTab == 6, + scale: s, + focusNode: _navFocusNodes[6], + onFocusChange: (focused) { + if (focused) _setSelectedTab(6); + }, + onKeyEvent: (node, event) => _handleNavKey(6, event), + onTap: () => setState(() => _selectedTab = 6), + ), + ], + ), ), - ), - // Content area - Expanded( - child: Padding( - padding: EdgeInsets.all(64 * s), - child: _selectedTab == 0 - ? _buildIdentityTab(s) - : _selectedTab == 1 - ? _buildJadwalTab(s) - : _selectedTab == 2 - ? _buildTampilanTab(s) - : _selectedTab == 3 - ? _buildPengumumanTab(s) - : _selectedTab == 4 - ? _buildJumatTab(s) - : _selectedTab == 5 - ? _buildSimulasiTab(s) - : _buildTentangTab(s), + // Content area + Expanded( + child: Padding( + padding: EdgeInsets.all(64 * s), + child: _selectedTab == 0 + ? _buildIdentityTab(s) + : _selectedTab == 1 + ? _buildJadwalTab(s) + : _selectedTab == 2 + ? _buildTampilanTab(s) + : _selectedTab == 3 + ? _buildPengumumanTab(s) + : _selectedTab == 4 + ? _buildJumatTab(s) + : _selectedTab == 5 + ? _buildSimulasiTab(s) + : _buildTentangTab(s), + ), ), - ), - ], + ], ), ), ); @@ -1745,103 +1752,142 @@ class _AdminScreenState extends ConsumerState { child: SingleChildScrollView( controller: _jumatScrollController, child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Pengaturan Jumat', - style: GoogleFonts.plusJakartaSans( - fontSize: 48 * s, fontWeight: FontWeight.w700, color: SacredColors.secondary), - ), - SizedBox(height: 8 * s), - Text( - 'Data di bawah akan tampil setiap hari Jumat: pada layar utama (banner bawah jam) dan layar Persiapan Khutbah.', - style: GoogleFonts.manrope(fontSize: 16 * s, color: SacredColors.onSurfaceVariant), - ), - SizedBox(height: 40 * s), - - _adminCard(s, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _sectionLabel('Petugas Shalat Jumat', s), + Text( + 'Pengaturan Jumat', + style: GoogleFonts.plusJakartaSans( + fontSize: 48 * s, + fontWeight: FontWeight.w700, + color: SacredColors.secondary), + ), SizedBox(height: 8 * s), Text( - 'Nama Khatib dan Imam tampil di layar utama setiap Jumat dan di layar Persiapan Khutbah.', - style: GoogleFonts.manrope(fontSize: 14 * s, color: SacredColors.onSurfaceVariant), + 'Data di bawah akan tampil setiap hari Jumat: pada layar utama (banner bawah jam) dan layar Persiapan Khutbah.', + style: GoogleFonts.manrope( + fontSize: 16 * s, color: SacredColors.onSurfaceVariant), ), - SizedBox(height: 24 * s), - _buildTextField( - 'Nama Khatib Minggu Ini', - _khatibCtrl, - s, - focusNode: _jumatFocusNodes[0], - onMoveLeft: () => _focusNavTab(_selectedTab), - onMoveDown: () => _focusJumatRow(1), - ), - SizedBox(height: 16 * s), - _buildTextField( - 'Nama Imam Minggu Ini', - _imamCtrl, - s, - focusNode: _jumatFocusNodes[1], - onMoveLeft: () => _focusNavTab(_selectedTab), - onMoveUp: () => _focusJumatRow(0), - ), + SizedBox(height: 40 * s), + + _adminCard(s, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _sectionLabel('Petugas Shalat Jumat', s), + SizedBox(height: 8 * s), + Text( + 'Nama Khatib dan Imam tampil di layar utama setiap Jumat dan di layar Persiapan Khutbah.', + style: GoogleFonts.manrope( + fontSize: 14 * s, + color: SacredColors.onSurfaceVariant), + ), + SizedBox(height: 24 * s), + _buildTextField( + 'Nama Khatib Minggu Ini', + _khatibCtrl, + s, + focusNode: _jumatFocusNodes[0], + onMoveLeft: () => _focusNavTab(_selectedTab), + onMoveDown: () => _focusJumatRow(1), + ), + SizedBox(height: 16 * s), + _buildTextField( + 'Nama Imam Minggu Ini', + _imamCtrl, + s, + focusNode: _jumatFocusNodes[1], + onMoveLeft: () => _focusNavTab(_selectedTab), + onMoveUp: () => _focusJumatRow(0), + ), + SizedBox(height: 32 * s), + + // Preview chip + if (_khatibCtrl.text.isNotEmpty || + _imamCtrl.text.isNotEmpty) ...[ + Text('Preview tampilan:', + style: GoogleFonts.manrope( + fontSize: 14 * s, + color: SacredColors.onSurfaceVariant)), + SizedBox(height: 10 * s), + Container( + padding: EdgeInsets.all(20 * s), + decoration: BoxDecoration( + color: SacredColors.background, + borderRadius: BorderRadius.circular(SacredRadii.lg), + border: Border.all( + color: SacredColors.secondary + .withValues(alpha: 0.2)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.star_rounded, + color: SacredColors.secondary, size: 16 * s), + SizedBox(width: 8 * s), + Text('JUMAT MUBARAK', + style: GoogleFonts.plusJakartaSans( + fontSize: 14 * s, + fontWeight: FontWeight.w800, + color: SacredColors.secondary, + letterSpacing: 2)), + SizedBox(width: 8 * s), + Icon(Icons.star_rounded, + color: SacredColors.secondary, size: 16 * s), + SizedBox(width: 24 * s), + if (_khatibCtrl.text.isNotEmpty) + Text('KHATIB ${_khatibCtrl.text}', + style: GoogleFonts.manrope( + fontSize: 14 * s, + color: SacredColors.onSurface)), + if (_khatibCtrl.text.isNotEmpty && + _imamCtrl.text.isNotEmpty) + Text(' | ', + style: GoogleFonts.manrope( + fontSize: 14 * s, + color: SacredColors.onSurfaceVariant)), + if (_imamCtrl.text.isNotEmpty) + Text('IMAM ${_imamCtrl.text}', + style: GoogleFonts.manrope( + fontSize: 14 * s, + color: SacredColors.onSurface)), + ], + ), + ), + SizedBox(height: 24 * s), + ], + ], + )), + SizedBox(height: 32 * s), - // Preview chip - if (_khatibCtrl.text.isNotEmpty || _imamCtrl.text.isNotEmpty) ...[ - Text('Preview tampilan:', style: GoogleFonts.manrope(fontSize: 14 * s, color: SacredColors.onSurfaceVariant)), - SizedBox(height: 10 * s), - Container( - padding: EdgeInsets.all(20 * s), - decoration: BoxDecoration( - color: SacredColors.background, - borderRadius: BorderRadius.circular(SacredRadii.lg), - border: Border.all(color: SacredColors.secondary.withValues(alpha: 0.2)), - ), - child: Row( - mainAxisSize: MainAxisSize.min, + // Info box + _adminCard(s, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon(Icons.star_rounded, color: SacredColors.secondary, size: 16 * s), - SizedBox(width: 8 * s), - Text('JUMAT MUBARAK', style: GoogleFonts.plusJakartaSans( - fontSize: 14 * s, fontWeight: FontWeight.w800, color: SacredColors.secondary, letterSpacing: 2)), - SizedBox(width: 8 * s), - Icon(Icons.star_rounded, color: SacredColors.secondary, size: 16 * s), - SizedBox(width: 24 * s), - if (_khatibCtrl.text.isNotEmpty) - Text('KHATIB ${_khatibCtrl.text}', style: GoogleFonts.manrope( - fontSize: 14 * s, color: SacredColors.onSurface)), - if (_khatibCtrl.text.isNotEmpty && _imamCtrl.text.isNotEmpty) - Text(' | ', style: GoogleFonts.manrope(fontSize: 14 * s, color: SacredColors.onSurfaceVariant)), - if (_imamCtrl.text.isNotEmpty) - Text('IMAM ${_imamCtrl.text}', style: GoogleFonts.manrope( - fontSize: 14 * s, color: SacredColors.onSurface)), + _sectionLabel('Kapan Digunakan?', s), + SizedBox(height: 16 * s), + _infoRow( + Icons.tv, + 'Layar Utama (Jumat)', + 'Banner bawah jam berubah ke JUMAT MUBARAK, nama khatib & imam tampil di bawahnya.', + s), + SizedBox(height: 12 * s), + _infoRow( + Icons.timer_outlined, + 'Layar Persiapan Khutbah', + 'Saat menuju iqomah Dzuhur di hari Jumat, layar menampilkan judul PERSIAPAN KHUTBAH beserta nama petugas.', + s), + SizedBox(height: 12 * s), + _infoRow( + Icons.info_outline, + 'Durasi Blank Screen', + 'Durasi Black Screen setelah shalat Jumat dapat diatur di tab Jadwal & Sinkronisasi.', + s), ], - ), - ), - SizedBox(height: 24 * s), - ], + )), ], - )), - - SizedBox(height: 32 * s), - - // Info box - _adminCard(s, child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _sectionLabel('Kapan Digunakan?', s), - SizedBox(height: 16 * s), - _infoRow(Icons.tv, 'Layar Utama (Jumat)', 'Banner bawah jam berubah ke JUMAT MUBARAK, nama khatib & imam tampil di bawahnya.', s), - SizedBox(height: 12 * s), - _infoRow(Icons.timer_outlined, 'Layar Persiapan Khutbah', 'Saat menuju iqomah Dzuhur di hari Jumat, layar menampilkan judul PERSIAPAN KHUTBAH beserta nama petugas.', s), - SizedBox(height: 12 * s), - _infoRow(Icons.info_outline, 'Durasi Blank Screen', 'Durasi Black Screen setelah shalat Jumat dapat diatur di tab Jadwal & Sinkronisasi.', s), - ], - )), - ], - ), + ), ), ), ); @@ -1857,7 +1903,11 @@ class _AdminScreenState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(title, style: GoogleFonts.manrope(fontSize: 15 * s, fontWeight: FontWeight.w700, color: SacredColors.onSurface)), + Text(title, + style: GoogleFonts.manrope( + fontSize: 15 * s, + fontWeight: FontWeight.w700, + color: SacredColors.onSurface)), SizedBox(height: 4 * s), Text( desc, @@ -1915,428 +1965,471 @@ class _AdminScreenState extends ConsumerState { child: SingleChildScrollView( controller: _tampilanScrollController, child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Pengaturan Tampilan & Media', - style: GoogleFonts.plusJakartaSans( - fontSize: 48 * s, - fontWeight: FontWeight.w700, - color: SacredColors.primary, - ), - ), - SizedBox(height: 48 * s), - _adminCard( - s, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _sectionLabel('Tipografi & Skala Teks', s), - SizedBox(height: 12 * s), - _buildTvChoiceField( - s: s, - rowIndex: textScaleRow, - label: 'Skala Teks Global', - options: const ['Kecil', 'Normal', 'Besar'], - selectedIndex: _textScaleIndex, - onChanged: (index) { - setState(() => _textScaleIndex = index); - _queueTampilanAutoSave(); - }, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Pengaturan Tampilan & Media', + style: GoogleFonts.plusJakartaSans( + fontSize: 48 * s, + fontWeight: FontWeight.w700, + color: SacredColors.primary, ), - SizedBox(height: 28 * s), - _buildTvIntStepperField( - s: s, - label: 'Durasi Layar Utama', - focusNode: _tampilanFocusNode(mainDurationRow), - controller: _mainDurCtrl, - fallback: 15, - min: 5, - max: 120, - suffix: 'detik', - onMoveLeft: () => _focusNavTab(_selectedTab), - onMoveUp: () => _focusTampilanRow(textScaleRow), - onMoveDown: () => _focusTampilanRow(slideDurationRow), - ), - SizedBox(height: 24 * s), - _buildTvIntStepperField( - s: s, - label: 'Durasi Tiap Slideshow', - focusNode: _tampilanFocusNode(slideDurationRow), - controller: _slideDurCtrl, - fallback: 10, - min: 5, - max: 120, - suffix: 'detik', - onMoveLeft: () => _focusNavTab(_selectedTab), - onMoveUp: () => _focusTampilanRow(mainDurationRow), - onMoveDown: () => _focusTampilanRow(slideshowPatternRow), - ), - SizedBox(height: 24 * s), - _buildTvChoiceField( - s: s, - rowIndex: slideshowPatternRow, - label: 'Pola Rotasi Slideshow', - options: const ['Main-1-Main', 'Main-N-Main'], - selectedIndex: - _slideshowPatternMode == SlideshowPatternMode.burst - ? 1 - : 0, - onChanged: (index) { - setState(() { - _slideshowPatternMode = index == 1 - ? SlideshowPatternMode.burst - : SlideshowPatternMode.alternating; - if (_slideshowPatternMode == SlideshowPatternMode.burst && - _slidesPerMainCtrl.text.trim().isEmpty) { - _slidesPerMainCtrl.text = '2'; - } - }); - _queueTampilanAutoSave( - message: 'Pola slideshow otomatis tersimpan', - ); - }, - ), - if (_slideshowPatternMode == SlideshowPatternMode.burst) ...[ - SizedBox(height: 16 * s), - _buildTvIntStepperField( - s: s, - label: 'Jumlah Slide antar Main', - focusNode: _tampilanFocusNode(slidesPerMainRow!), - controller: _slidesPerMainCtrl, - fallback: 2, - min: 1, - max: 20, - suffix: 'slide', - onMoveLeft: () => _focusNavTab(_selectedTab), - onMoveUp: () => _focusTampilanRow(slideshowPatternRow), - onMoveDown: () => _focusTampilanRow(scaleLabelRow), - ), - ], - SizedBox(height: 40 * s), - _sectionLabel('Ukuran Teks Per Kelompok', s), - SizedBox(height: 8 * s), - Text( - 'Kontrol ukuran teks secara spesifik per kelompok, terlepas dari skala global di atas.', - style: GoogleFonts.manrope(fontSize: 14 * s, color: SacredColors.onSurfaceVariant), - ), - SizedBox(height: 20 * s), - _scaleSlider( - s: s, - label: 'Label Shalat (Nama: SUBUH, DZUHUR…)', - focusNode: _tampilanFocusNode(scaleLabelRow), - value: _scaleCardLabel, - onChanged: (v) { - setState(() => _scaleCardLabel = v); - _queueTampilanAutoSave(); - }, - onMoveLeft: () => _focusNavTab(_selectedTab), - onMoveUp: () => _focusTampilanRow( - slidesPerMainRow ?? slideshowPatternRow, - ), - onMoveDown: () => _focusTampilanRow(scaleBodyRow), - ), - SizedBox(height: 16 * s), - _scaleSlider( - s: s, - label: 'Waktu & Iqamah pada kartu jadwal', - focusNode: _tampilanFocusNode(scaleBodyRow), - value: _scaleCardBody, - onChanged: (v) { - setState(() => _scaleCardBody = v); - _queueTampilanAutoSave(); - }, - onMoveLeft: () => _focusNavTab(_selectedTab), - onMoveUp: () => _focusTampilanRow(scaleLabelRow), - onMoveDown: () => _focusTampilanRow(scaleRunningRow), - ), - SizedBox(height: 16 * s), - _scaleSlider( - s: s, - label: 'Teks Berjalan (Running Text)', - focusNode: _tampilanFocusNode(scaleRunningRow), - value: _scaleRunningText, - onChanged: (v) { - setState(() => _scaleRunningText = v); - _queueTampilanAutoSave(); - }, - onMoveLeft: () => _focusNavTab(_selectedTab), - onMoveUp: () => _focusTampilanRow(scaleBodyRow), - onMoveDown: () => _focusTampilanRow(scaleTopHeaderRow), - ), - SizedBox(height: 16 * s), - _scaleSlider( - s: s, - label: 'Header Atas (Identitas & Tanggal)', - focusNode: _tampilanFocusNode(scaleTopHeaderRow), - value: _scaleTopHeader, - onChanged: (v) { - setState(() => _scaleTopHeader = v); - _queueTampilanAutoSave(); - }, - onMoveLeft: () => _focusNavTab(_selectedTab), - onMoveUp: () => _focusTampilanRow(scaleRunningRow), - onMoveDown: () => _focusTampilanRow( - removeBrandedBgRow ?? pickBrandedBgRow, - ), - ), - ], - ), - ), - SizedBox(height: 24 * s), - _adminCard( - s, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _sectionLabel('Foto Latar Utama (Branding Masjid)', s), - SizedBox(height: 16 * s), - if (_brandedBgImage != null && _brandedBgImage!.isNotEmpty) ...[ - ClipRRect( - borderRadius: BorderRadius.circular(SacredRadii.md), - child: Image.file( - File(_brandedBgImage!), - height: 180 * s, - width: double.infinity, - fit: BoxFit.cover, - errorBuilder: (_, __, ___) => Container( - height: 180 * s, - width: double.infinity, - color: SacredColors.surfaceContainerLowest, - alignment: Alignment.center, - child: Icon( - Icons.broken_image, - size: 36 * s, - color: SacredColors.onSurfaceVariant, - ), - ), + ), + SizedBox(height: 48 * s), + _adminCard( + s, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _sectionLabel('Tipografi & Skala Teks', s), + SizedBox(height: 12 * s), + _buildTvChoiceField( + s: s, + rowIndex: textScaleRow, + label: 'Skala Teks Global', + options: const ['Kecil', 'Normal', 'Besar'], + selectedIndex: _textScaleIndex, + onChanged: (index) { + setState(() => _textScaleIndex = index); + _queueTampilanAutoSave(); + }, ), - ), - SizedBox(height: 12 * s), - Text( - _brandedBgImage!.split('/').last, - style: GoogleFonts.manrope(fontSize: 14 * s, color: SacredColors.onSurfaceVariant), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - SizedBox(height: 12 * s), - _buildTampilanActionButton( - rowIndex: removeBrandedBgRow!, - s: s, - onActivate: () { - setState(() => _brandedBgImage = null); - _queueTampilanAutoSave( - message: 'Foto latar otomatis dihapus dan tersimpan', - ); - }, - child: OutlinedButton.icon( - onPressed: () { - setState(() => _brandedBgImage = null); + SizedBox(height: 28 * s), + _buildTvIntStepperField( + s: s, + label: 'Durasi Layar Utama', + focusNode: _tampilanFocusNode(mainDurationRow), + controller: _mainDurCtrl, + fallback: 15, + min: 5, + max: 120, + suffix: 'detik', + onMoveLeft: () => _focusNavTab(_selectedTab), + onMoveUp: () => _focusTampilanRow(textScaleRow), + onMoveDown: () => _focusTampilanRow(slideDurationRow), + ), + SizedBox(height: 24 * s), + _buildTvIntStepperField( + s: s, + label: 'Durasi Tiap Slideshow', + focusNode: _tampilanFocusNode(slideDurationRow), + controller: _slideDurCtrl, + fallback: 10, + min: 5, + max: 120, + suffix: 'detik', + onMoveLeft: () => _focusNavTab(_selectedTab), + onMoveUp: () => _focusTampilanRow(mainDurationRow), + onMoveDown: () => _focusTampilanRow(slideshowPatternRow), + ), + SizedBox(height: 24 * s), + _buildTvChoiceField( + s: s, + rowIndex: slideshowPatternRow, + label: 'Pola Rotasi Slideshow', + options: const ['Main-1-Main', 'Main-N-Main'], + selectedIndex: + _slideshowPatternMode == SlideshowPatternMode.burst + ? 1 + : 0, + onChanged: (index) { + setState(() { + _slideshowPatternMode = index == 1 + ? SlideshowPatternMode.burst + : SlideshowPatternMode.alternating; + if (_slideshowPatternMode == + SlideshowPatternMode.burst && + _slidesPerMainCtrl.text.trim().isEmpty) { + _slidesPerMainCtrl.text = '2'; + } + }); _queueTampilanAutoSave( - message: 'Foto latar otomatis dihapus dan tersimpan', + message: 'Pola slideshow otomatis tersimpan', ); }, - icon: HugeIcon(icon: HugeIcons.strokeRoundedDelete01, color: SacredColors.error, size: 20 * s), - label: Text( - 'HAPUS FOTO LATAR', - style: GoogleFonts.plusJakartaSans( + ), + if (_slideshowPatternMode == + SlideshowPatternMode.burst) ...[ + SizedBox(height: 16 * s), + _buildTvIntStepperField( + s: s, + label: 'Jumlah Slide antar Main', + focusNode: _tampilanFocusNode(slidesPerMainRow!), + controller: _slidesPerMainCtrl, + fallback: 2, + min: 1, + max: 20, + suffix: 'slide', + onMoveLeft: () => _focusNavTab(_selectedTab), + onMoveUp: () => _focusTampilanRow(slideshowPatternRow), + onMoveDown: () => _focusTampilanRow(scaleLabelRow), + ), + ], + SizedBox(height: 40 * s), + _sectionLabel('Ukuran Teks Per Kelompok', s), + SizedBox(height: 8 * s), + Text( + 'Kontrol ukuran teks secara spesifik per kelompok, terlepas dari skala global di atas.', + style: GoogleFonts.manrope( fontSize: 14 * s, - fontWeight: FontWeight.w700, - color: SacredColors.error, + color: SacredColors.onSurfaceVariant), + ), + SizedBox(height: 20 * s), + _scaleSlider( + s: s, + label: 'Label Shalat (Nama: SUBUH, DZUHUR…)', + focusNode: _tampilanFocusNode(scaleLabelRow), + value: _scaleCardLabel, + onChanged: (v) { + setState(() => _scaleCardLabel = v); + _queueTampilanAutoSave(); + }, + onMoveLeft: () => _focusNavTab(_selectedTab), + onMoveUp: () => _focusTampilanRow( + slidesPerMainRow ?? slideshowPatternRow, + ), + onMoveDown: () => _focusTampilanRow(scaleBodyRow), + ), + SizedBox(height: 16 * s), + _scaleSlider( + s: s, + label: 'Waktu & Iqamah pada kartu jadwal', + focusNode: _tampilanFocusNode(scaleBodyRow), + value: _scaleCardBody, + onChanged: (v) { + setState(() => _scaleCardBody = v); + _queueTampilanAutoSave(); + }, + onMoveLeft: () => _focusNavTab(_selectedTab), + onMoveUp: () => _focusTampilanRow(scaleLabelRow), + onMoveDown: () => _focusTampilanRow(scaleRunningRow), + ), + SizedBox(height: 16 * s), + _scaleSlider( + s: s, + label: 'Teks Berjalan (Running Text)', + focusNode: _tampilanFocusNode(scaleRunningRow), + value: _scaleRunningText, + onChanged: (v) { + setState(() => _scaleRunningText = v); + _queueTampilanAutoSave(); + }, + onMoveLeft: () => _focusNavTab(_selectedTab), + onMoveUp: () => _focusTampilanRow(scaleBodyRow), + onMoveDown: () => _focusTampilanRow(scaleTopHeaderRow), + ), + SizedBox(height: 16 * s), + _scaleSlider( + s: s, + label: 'Header Atas (Identitas & Tanggal)', + focusNode: _tampilanFocusNode(scaleTopHeaderRow), + value: _scaleTopHeader, + onChanged: (v) { + setState(() => _scaleTopHeader = v); + _queueTampilanAutoSave(); + }, + onMoveLeft: () => _focusNavTab(_selectedTab), + onMoveUp: () => _focusTampilanRow(scaleRunningRow), + onMoveDown: () => _focusTampilanRow( + removeBrandedBgRow ?? pickBrandedBgRow, + ), + ), + ], + ), + ), + SizedBox(height: 24 * s), + _adminCard( + s, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _sectionLabel('Foto Latar Utama (Branding Masjid)', s), + SizedBox(height: 16 * s), + if (_brandedBgImage != null && + _brandedBgImage!.isNotEmpty) ...[ + ClipRRect( + borderRadius: BorderRadius.circular(SacredRadii.md), + child: Image.file( + File(_brandedBgImage!), + height: 180 * s, + width: double.infinity, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => Container( + height: 180 * s, + width: double.infinity, + color: SacredColors.surfaceContainerLowest, + alignment: Alignment.center, + child: Icon( + Icons.broken_image, + size: 36 * s, + color: SacredColors.onSurfaceVariant, + ), + ), + ), + ), + SizedBox(height: 12 * s), + Text( + _brandedBgImage!.split('/').last, + style: GoogleFonts.manrope( + fontSize: 14 * s, + color: SacredColors.onSurfaceVariant), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + SizedBox(height: 12 * s), + _buildTampilanActionButton( + rowIndex: removeBrandedBgRow!, + s: s, + onActivate: () { + setState(() => _brandedBgImage = null); + _queueTampilanAutoSave( + message: + 'Foto latar otomatis dihapus dan tersimpan', + ); + }, + child: OutlinedButton.icon( + onPressed: () { + setState(() => _brandedBgImage = null); + _queueTampilanAutoSave( + message: + 'Foto latar otomatis dihapus dan tersimpan', + ); + }, + icon: HugeIcon( + icon: HugeIcons.strokeRoundedDelete01, + color: SacredColors.error, + size: 20 * s), + label: Text( + 'HAPUS FOTO LATAR', + style: GoogleFonts.plusJakartaSans( + fontSize: 14 * s, + fontWeight: FontWeight.w700, + color: SacredColors.error, + ), + ), + ), + ), + ] else + Text('Belum ada foto latar masjid.', + style: GoogleFonts.manrope( + fontSize: 16 * s, + color: SacredColors.onSurfaceVariant)), + SizedBox(height: 16 * s), + _buildTampilanActionButton( + rowIndex: pickBrandedBgRow, + s: s, + onActivate: _pickBrandedImage, + child: ElevatedButton.icon( + onPressed: _pickBrandedImage, + icon: HugeIcon( + icon: HugeIcons.strokeRoundedImage01, + color: SacredColors.onPrimary, + size: 20 * s), + label: Text('PILIH FOTO MASJID', + style: TextStyle(fontSize: 16 * s)), + style: _tvElevatedActionStyle( + s: s, + normalBackground: SacredColors.secondary, + normalForeground: SacredColors.onPrimary, + padding: EdgeInsets.symmetric( + horizontal: 24 * s, vertical: 16 * s), + fontSize: 16 * s, ), ), ), - ), - ] else - Text('Belum ada foto latar masjid.', style: GoogleFonts.manrope(fontSize: 16 * s, color: SacredColors.onSurfaceVariant)), - SizedBox(height: 16 * s), - _buildTampilanActionButton( - rowIndex: pickBrandedBgRow, - s: s, - onActivate: _pickBrandedImage, - child: ElevatedButton.icon( - onPressed: _pickBrandedImage, - icon: HugeIcon(icon: HugeIcons.strokeRoundedImage01, color: SacredColors.onPrimary, size: 20 * s), - label: Text('PILIH FOTO MASJID', style: TextStyle(fontSize: 16 * s)), - style: _tvElevatedActionStyle( + ], + ), + ), + SizedBox(height: 24 * s), + _adminCard( + s, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _sectionLabel('Background Layar Utama (Unsplash)', s), + SizedBox(height: 12 * s), + _buildTvBoolField( s: s, - normalBackground: SacredColors.secondary, - normalForeground: SacredColors.onPrimary, - padding: EdgeInsets.symmetric(horizontal: 24 * s, vertical: 16 * s), - fontSize: 16 * s, + rowIndex: useUnsplashRow, + label: 'Gunakan Foto Unsplash API', + value: _useUnsplash, + onChanged: (val) { + final previous = _useUnsplash; + setState(() => _useUnsplash = val); + _queueTampilanAutoSave(); + if (!previous && val) { + _focusTampilanRow(useUnsplashRow + 1); + } else if (previous && !val) { + _focusTampilanRow(useUnsplashRow); + } + }, + trueLabel: 'Aktif', + falseLabel: 'Nonaktif', ), - ), + if (_useUnsplash) ...[ + SizedBox(height: 12 * s), + _buildTextField( + 'Kata Kunci (Contoh: mosque, architecture)', + _unsplashKeywordCtrl, + s, + focusNode: _tampilanFocusNode(unsplashKeywordRow!), + onMoveLeft: () => _focusNavTab(_selectedTab), + onMoveUp: () => _focusTampilanRow(useUnsplashRow), + onMoveDown: () => + _focusTampilanRow(unsplashRotationRow!), + ), + SizedBox(height: 12 * s), + _buildTvIntStepperField( + s: s, + label: 'Rotasi Foto', + focusNode: _tampilanFocusNode(unsplashRotationRow!), + controller: _unsplashRotationCtrl, + fallback: 6, + min: 1, + max: 24, + suffix: 'jam', + onMoveLeft: () => _focusNavTab(_selectedTab), + onMoveUp: () => _focusTampilanRow(unsplashKeywordRow!), + onMoveDown: () => + _focusTampilanRow(addSlideshowImageRow), + ), + ], + ], ), - ], - ), - ), - SizedBox(height: 24 * s), - _adminCard( - s, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _sectionLabel('Background Layar Utama (Unsplash)', s), - SizedBox(height: 12 * s), - _buildTvBoolField( - s: s, - rowIndex: useUnsplashRow, - label: 'Gunakan Foto Unsplash API', - value: _useUnsplash, - onChanged: (val) { - final previous = _useUnsplash; - setState(() => _useUnsplash = val); - _queueTampilanAutoSave(); - if (!previous && val) { - _focusTampilanRow(useUnsplashRow + 1); - } else if (previous && !val) { - _focusTampilanRow(useUnsplashRow); - } - }, - trueLabel: 'Aktif', - falseLabel: 'Nonaktif', - ), - if (_useUnsplash) ...[ - SizedBox(height: 12 * s), - _buildTextField( - 'Kata Kunci (Contoh: mosque, architecture)', - _unsplashKeywordCtrl, - s, - focusNode: _tampilanFocusNode(unsplashKeywordRow!), - onMoveLeft: () => _focusNavTab(_selectedTab), - onMoveUp: () => _focusTampilanRow(useUnsplashRow), - onMoveDown: () => _focusTampilanRow(unsplashRotationRow!), - ), - SizedBox(height: 12 * s), - _buildTvIntStepperField( - s: s, - label: 'Rotasi Foto', - focusNode: _tampilanFocusNode(unsplashRotationRow!), - controller: _unsplashRotationCtrl, - fallback: 6, - min: 1, - max: 24, - suffix: 'jam', - onMoveLeft: () => _focusNavTab(_selectedTab), - onMoveUp: () => _focusTampilanRow(unsplashKeywordRow!), - onMoveDown: () => _focusTampilanRow(addSlideshowImageRow), - ), - ], - ], - ), - ), - SizedBox(height: 24 * s), - _adminCard( - s, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _sectionLabel('Galeri Gambar Slideshow', s), - SizedBox(height: 16 * s), - _buildTampilanActionButton( - rowIndex: addSlideshowImageRow, - s: s, - onActivate: _pickSlideshowImages, - child: ElevatedButton.icon( - onPressed: _pickSlideshowImages, - icon: HugeIcon(icon: HugeIcons.strokeRoundedPlusSign, color: SacredColors.onPrimary, size: 18 * s), - label: Text('TAMBAH FOTO', style: TextStyle(fontSize: 14 * s)), - style: _tvElevatedActionStyle( + ), + SizedBox(height: 24 * s), + _adminCard( + s, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _sectionLabel('Galeri Gambar Slideshow', s), + SizedBox(height: 16 * s), + _buildTampilanActionButton( + rowIndex: addSlideshowImageRow, s: s, - normalBackground: SacredColors.secondary, - normalForeground: SacredColors.onPrimary, - padding: EdgeInsets.symmetric(horizontal: 20 * s, vertical: 14 * s), - fontSize: 14 * s, - ), - ), - ), - SizedBox(height: 16 * s), - if (_slideshowImages.isEmpty) - Text('Belum ada gambar slideshow.', style: GoogleFonts.manrope(fontSize: 16 * s, color: SacredColors.onSurfaceVariant)) - else - ListView.separated( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: _slideshowImages.length, - separatorBuilder: (_, __) => SizedBox(height: 12 * s), - itemBuilder: (context, idx) { - final path = _slideshowImages[idx]; - return Container( - padding: EdgeInsets.all(16 * s), - decoration: BoxDecoration( - color: SacredColors.surfaceContainerLowest, - borderRadius: BorderRadius.circular(SacredRadii.md), - border: Border.all(color: SacredColors.outlineVariant.withValues(alpha: 0.3)), + onActivate: _pickSlideshowImages, + child: ElevatedButton.icon( + onPressed: _pickSlideshowImages, + icon: HugeIcon( + icon: HugeIcons.strokeRoundedPlusSign, + color: SacredColors.onPrimary, + size: 18 * s), + label: Text('TAMBAH FOTO', + style: TextStyle(fontSize: 14 * s)), + style: _tvElevatedActionStyle( + s: s, + normalBackground: SacredColors.secondary, + normalForeground: SacredColors.onPrimary, + padding: EdgeInsets.symmetric( + horizontal: 20 * s, vertical: 14 * s), + fontSize: 14 * s, ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(SacredRadii.sm), - child: Image.file( - File(path), - width: double.infinity, - height: 120 * s, - fit: BoxFit.cover, - errorBuilder: (_, __, ___) => Container( - width: double.infinity, - height: 120 * s, - color: SacredColors.surfaceContainerHigh, - alignment: Alignment.center, - child: Icon( - Icons.broken_image, - size: 32 * s, - color: SacredColors.onSurfaceVariant, + ), + ), + SizedBox(height: 16 * s), + if (_slideshowImages.isEmpty) + Text('Belum ada gambar slideshow.', + style: GoogleFonts.manrope( + fontSize: 16 * s, + color: SacredColors.onSurfaceVariant)) + else + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: _slideshowImages.length, + separatorBuilder: (_, __) => SizedBox(height: 12 * s), + itemBuilder: (context, idx) { + final path = _slideshowImages[idx]; + return Container( + padding: EdgeInsets.all(16 * s), + decoration: BoxDecoration( + color: SacredColors.surfaceContainerLowest, + borderRadius: + BorderRadius.circular(SacredRadii.md), + border: Border.all( + color: SacredColors.outlineVariant + .withValues(alpha: 0.3)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: + BorderRadius.circular(SacredRadii.sm), + child: Image.file( + File(path), + width: double.infinity, + height: 120 * s, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => Container( + width: double.infinity, + height: 120 * s, + color: SacredColors.surfaceContainerHigh, + alignment: Alignment.center, + child: Icon( + Icons.broken_image, + size: 32 * s, + color: SacredColors.onSurfaceVariant, + ), + ), ), ), - ), - ), - SizedBox(height: 10 * s), - Text( - path.split('/').last, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: GoogleFonts.manrope(fontSize: 14 * s, color: SacredColors.onSurface), - ), - SizedBox(height: 10 * s), - _buildTampilanActionButton( - rowIndex: slideshowDeleteRows[idx], - s: s, - onActivate: () { - setState(() => _slideshowImages.removeAt(idx)); - _queueTampilanAutoSave( - message: 'Galeri slideshow otomatis tersimpan', - ); - }, - child: OutlinedButton.icon( - onPressed: () { - setState(() => _slideshowImages.removeAt(idx)); - _queueTampilanAutoSave( - message: 'Galeri slideshow otomatis tersimpan', - ); - }, - icon: HugeIcon(icon: HugeIcons.strokeRoundedDelete01, color: SacredColors.error, size: 18 * s), - label: Text( - 'HAPUS FOTO', - style: GoogleFonts.plusJakartaSans( - fontSize: 13 * s, - fontWeight: FontWeight.w700, - color: SacredColors.error, + SizedBox(height: 10 * s), + Text( + path.split('/').last, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: GoogleFonts.manrope( + fontSize: 14 * s, + color: SacredColors.onSurface), + ), + SizedBox(height: 10 * s), + _buildTampilanActionButton( + rowIndex: slideshowDeleteRows[idx], + s: s, + onActivate: () { + setState( + () => _slideshowImages.removeAt(idx)); + _queueTampilanAutoSave( + message: + 'Galeri slideshow otomatis tersimpan', + ); + }, + child: OutlinedButton.icon( + onPressed: () { + setState( + () => _slideshowImages.removeAt(idx)); + _queueTampilanAutoSave( + message: + 'Galeri slideshow otomatis tersimpan', + ); + }, + icon: HugeIcon( + icon: HugeIcons.strokeRoundedDelete01, + color: SacredColors.error, + size: 18 * s), + label: Text( + 'HAPUS FOTO', + style: GoogleFonts.plusJakartaSans( + fontSize: 13 * s, + fontWeight: FontWeight.w700, + color: SacredColors.error, + ), + ), ), ), - ), + ], ), - ], - ), - ); - }, - ), - ], - ), + ); + }, + ), + ], + ), + ), + SizedBox(height: 40 * s), + ], ), - SizedBox(height: 40 * s), - ], - ), ), ), ); @@ -2401,7 +2494,8 @@ class _AdminScreenState extends ConsumerState { max: 120, suffix: 'detik', onMoveLeft: () => _focusNavTab(_selectedTab), - onMoveDown: () => _focusPengumumanRow(announcementDurationRow), + onMoveDown: () => + _focusPengumumanRow(announcementDurationRow), onValueChanged: _queuePengumumanAutoSave, ), SizedBox(height: 24 * s), @@ -2430,7 +2524,8 @@ class _AdminScreenState extends ConsumerState { _queuePengumumanAutoSave(); }, onMoveLeft: () => _focusNavTab(_selectedTab), - onMoveUp: () => _focusPengumumanRow(announcementDurationRow), + onMoveUp: () => + _focusPengumumanRow(announcementDurationRow), onMoveDown: () => _focusPengumumanRow( _textSlides.isEmpty ? addTextSlideRow @@ -2465,7 +2560,8 @@ class _AdminScreenState extends ConsumerState { padding: EdgeInsets.all(20 * s), decoration: BoxDecoration( color: SacredColors.surfaceContainerLowest, - borderRadius: BorderRadius.circular(SacredRadii.md), + borderRadius: + BorderRadius.circular(SacredRadii.md), border: Border.all( color: SacredColors.outlineVariant .withValues(alpha: 0.3), @@ -2502,7 +2598,8 @@ class _AdminScreenState extends ConsumerState { ), controller: textCtrl, maxLines: 3, - onMoveLeft: () => _focusNavTab(_selectedTab), + onMoveLeft: () => + _focusNavTab(_selectedTab), onMoveUp: () => _focusPengumumanRow( idx == 0 ? announcementDurationRow @@ -2628,8 +2725,9 @@ class _AdminScreenState extends ConsumerState { s: s, focusNode: _pengumumanFocusNode(marqueeModeRow), label: 'Mode Animasi Running Text', - valueLabel: - _marqueeAnimType == 'fade' ? 'Fade In-Out' : 'Marquee', + valueLabel: _marqueeAnimType == 'fade' + ? 'Fade In-Out' + : 'Marquee', progress: _marqueeAnimType == 'fade' ? 1 : 0, helperText: 'Tekan OK untuk mulai ubah. Saat aktif, gunakan ← → untuk memilih.', @@ -2672,11 +2770,11 @@ class _AdminScreenState extends ConsumerState { itemCount: _runningTexts.length, separatorBuilder: (_, __) => SizedBox(height: 12 * s), itemBuilder: (context, idx) { - final textCtrl = - TextEditingController(text: _runningTexts[idx]) - ..selection = TextSelection.fromPosition( - TextPosition(offset: _runningTexts[idx].length), - ); + final textCtrl = TextEditingController( + text: _runningTexts[idx]) + ..selection = TextSelection.fromPosition( + TextPosition(offset: _runningTexts[idx].length), + ); final durCtrl = TextEditingController( text: _runningTextDurations[idx].toString(), ); @@ -2684,7 +2782,8 @@ class _AdminScreenState extends ConsumerState { padding: EdgeInsets.all(20 * s), decoration: BoxDecoration( color: SacredColors.surfaceContainerLowest, - borderRadius: BorderRadius.circular(SacredRadii.md), + borderRadius: + BorderRadius.circular(SacredRadii.md), border: Border.all( color: SacredColors.outlineVariant .withValues(alpha: 0.3), @@ -2720,7 +2819,8 @@ class _AdminScreenState extends ConsumerState { runningTextTextRows[idx], ), controller: textCtrl, - onMoveLeft: () => _focusNavTab(_selectedTab), + onMoveLeft: () => + _focusNavTab(_selectedTab), onMoveUp: () => _focusPengumumanRow( idx == 0 ? marqueeModeRow @@ -2757,7 +2857,8 @@ class _AdminScreenState extends ConsumerState { ), controller: durCtrl, keyboardType: TextInputType.number, - onMoveLeft: () => _focusNavTab(_selectedTab), + onMoveLeft: () => + _focusNavTab(_selectedTab), onMoveUp: () => _focusPengumumanRow( runningTextTextRows[idx], ), @@ -2895,7 +2996,8 @@ class _AdminScreenState extends ConsumerState { decoration: BoxDecoration( color: SacredColors.surfaceContainerHighest.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(SacredRadii.xl), - border: Border.all(color: SacredColors.outlineVariant.withValues(alpha: 0.2)), + border: Border.all( + color: SacredColors.outlineVariant.withValues(alpha: 0.2)), ), child: child, ), @@ -3089,7 +3191,8 @@ class _AdminScreenState extends ConsumerState { padding: EdgeInsets.all(isFocused ? 5 * s : 0), decoration: BoxDecoration( color: isFocused - ? SacredColors.surfaceContainerLow.withValues(alpha: 0.96) + ? SacredColors.surfaceContainerLow + .withValues(alpha: 0.96) : Colors.transparent, borderRadius: BorderRadius.circular(SacredRadii.lg), border: Border.all( @@ -3101,7 +3204,8 @@ class _AdminScreenState extends ConsumerState { boxShadow: isFocused ? [ BoxShadow( - color: SacredColors.primary.withValues(alpha: 0.28), + color: SacredColors.primary + .withValues(alpha: 0.28), blurRadius: 24 * s, spreadRadius: 2 * s, ), @@ -3160,7 +3264,8 @@ class _AdminScreenState extends ConsumerState { padding: EdgeInsets.all(isFocused ? 5 * s : 0), decoration: BoxDecoration( color: isFocused - ? SacredColors.surfaceContainerLow.withValues(alpha: 0.96) + ? SacredColors.surfaceContainerLow + .withValues(alpha: 0.96) : Colors.transparent, borderRadius: BorderRadius.circular(SacredRadii.lg), border: Border.all( @@ -3172,7 +3277,8 @@ class _AdminScreenState extends ConsumerState { boxShadow: isFocused ? [ BoxShadow( - color: SacredColors.primary.withValues(alpha: 0.28), + color: SacredColors.primary + .withValues(alpha: 0.28), blurRadius: 24 * s, spreadRadius: 2 * s, ), @@ -3300,7 +3406,8 @@ class _AdminScreenState extends ConsumerState { padding: EdgeInsets.all(isFocused ? 5 * s : 0), decoration: BoxDecoration( color: isFocused - ? SacredColors.surfaceContainerLow.withValues(alpha: 0.96) + ? SacredColors.surfaceContainerLow + .withValues(alpha: 0.96) : Colors.transparent, borderRadius: BorderRadius.circular(SacredRadii.lg), border: Border.all( @@ -3312,7 +3419,8 @@ class _AdminScreenState extends ConsumerState { boxShadow: isFocused ? [ BoxShadow( - color: SacredColors.primary.withValues(alpha: 0.28), + color: SacredColors.primary + .withValues(alpha: 0.28), blurRadius: 24 * s, spreadRadius: 2 * s, ), @@ -3454,8 +3562,6 @@ class _AdminScreenState extends ConsumerState { ); } - - Widget _buildIdentityTab(double s) { final nameRow = 0; final addressRow = 1; @@ -3469,138 +3575,138 @@ class _AdminScreenState extends ConsumerState { child: SingleChildScrollView( controller: _identityScrollController, child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Identitas & Lokasi Masjid', - style: GoogleFonts.plusJakartaSans( - fontSize: 48 * s, - fontWeight: FontWeight.w700, - color: SacredColors.primary, - ), - ), - SizedBox(height: 48 * s), - _adminCard( - s, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildTextField( - 'Nama Masjid', - _masjidNameCtrl, - s, - focusNode: _identityFocusNodes[nameRow], - onMoveLeft: () => _focusNavTab(_selectedTab), - onMoveDown: () => _focusIdentityRow(addressRow), + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Identitas & Lokasi Masjid', + style: GoogleFonts.plusJakartaSans( + fontSize: 48 * s, + fontWeight: FontWeight.w700, + color: SacredColors.primary, ), - SizedBox(height: 32 * s), - _buildTextField( - 'Alamat Lengkap', - _masjidAddressCtrl, - s, - maxLines: 2, - focusNode: _identityFocusNodes[addressRow], - onMoveLeft: () => _focusNavTab(_selectedTab), - onMoveUp: () => _focusIdentityRow(nameRow), - onMoveDown: () => _focusIdentityRow(searchRow), - ), - SizedBox(height: 32 * s), - Text( - 'Lokasi Jadwal Shalat (MyQuran API)', - style: GoogleFonts.manrope( - fontSize: 16 * s, - fontWeight: FontWeight.w600, - color: SacredColors.onSurfaceVariant, - ), - ), - SizedBox(height: 12 * s), - _buildReadonlyField( - _cityCtrl, - s, - focusable: false, - ), - SizedBox(height: 16 * s), - _scrollAware( - controller: _identityScrollController, - child: Focus( - focusNode: _identityFocusNodes[searchRow], - onKeyEvent: (node, event) => _handleIdentityActionKey( - searchRow, - event, - onActivate: () => _showCitySearchDialog(s), + ), + SizedBox(height: 48 * s), + _adminCard( + s, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTextField( + 'Nama Masjid', + _masjidNameCtrl, + s, + focusNode: _identityFocusNodes[nameRow], + onMoveLeft: () => _focusNavTab(_selectedTab), + onMoveDown: () => _focusIdentityRow(addressRow), ), - child: ListenableBuilder( - listenable: _identityFocusNodes[searchRow], - builder: (context, _) { - final isFocused = - _identityFocusNodes[searchRow].hasFocus; - return AnimatedScale( - scale: isFocused ? 1.01 : 1.0, - duration: const Duration(milliseconds: 140), - curve: Curves.easeOutCubic, - child: AnimatedContainer( - duration: const Duration(milliseconds: 140), - curve: Curves.easeOutCubic, - padding: EdgeInsets.all(isFocused ? 5 * s : 0), - decoration: BoxDecoration( - color: isFocused - ? SacredColors.surfaceContainerLow - .withValues(alpha: 0.96) - : Colors.transparent, - borderRadius: - BorderRadius.circular(SacredRadii.lg), - border: Border.all( - color: isFocused - ? SacredColors.primary - .withValues(alpha: 0.95) - : Colors.transparent, - width: isFocused ? 3 : 0, - ), - boxShadow: isFocused - ? [ - BoxShadow( - color: SacredColors.primary - .withValues(alpha: 0.28), - blurRadius: 24 * s, - spreadRadius: 2 * s, - ), - ] - : null, - ), - child: ElevatedButton.icon( - onPressed: () => _showCitySearchDialog(s), - icon: HugeIcon( - icon: HugeIcons.strokeRoundedSearch01, - color: isFocused - ? SacredColors.onPrimary - : SacredColors.onPrimary, - ), - label: Text( - 'CARI KOTA', - style: TextStyle(fontSize: 16 * s), - ), - style: _tvElevatedActionStyle( - s: s, - normalBackground: SacredColors.secondary, - normalForeground: SacredColors.onPrimary, - padding: EdgeInsets.symmetric( - horizontal: 24 * s, - vertical: 24 * s, + SizedBox(height: 32 * s), + _buildTextField( + 'Alamat Lengkap', + _masjidAddressCtrl, + s, + maxLines: 2, + focusNode: _identityFocusNodes[addressRow], + onMoveLeft: () => _focusNavTab(_selectedTab), + onMoveUp: () => _focusIdentityRow(nameRow), + onMoveDown: () => _focusIdentityRow(searchRow), + ), + SizedBox(height: 32 * s), + Text( + 'Lokasi Jadwal Shalat (MyQuran API)', + style: GoogleFonts.manrope( + fontSize: 16 * s, + fontWeight: FontWeight.w600, + color: SacredColors.onSurfaceVariant, + ), + ), + SizedBox(height: 12 * s), + _buildReadonlyField( + _cityCtrl, + s, + focusable: false, + ), + SizedBox(height: 16 * s), + _scrollAware( + controller: _identityScrollController, + child: Focus( + focusNode: _identityFocusNodes[searchRow], + onKeyEvent: (node, event) => _handleIdentityActionKey( + searchRow, + event, + onActivate: () => _showCitySearchDialog(s), + ), + child: ListenableBuilder( + listenable: _identityFocusNodes[searchRow], + builder: (context, _) { + final isFocused = + _identityFocusNodes[searchRow].hasFocus; + return AnimatedScale( + scale: isFocused ? 1.01 : 1.0, + duration: const Duration(milliseconds: 140), + curve: Curves.easeOutCubic, + child: AnimatedContainer( + duration: const Duration(milliseconds: 140), + curve: Curves.easeOutCubic, + padding: EdgeInsets.all(isFocused ? 5 * s : 0), + decoration: BoxDecoration( + color: isFocused + ? SacredColors.surfaceContainerLow + .withValues(alpha: 0.96) + : Colors.transparent, + borderRadius: + BorderRadius.circular(SacredRadii.lg), + border: Border.all( + color: isFocused + ? SacredColors.primary + .withValues(alpha: 0.95) + : Colors.transparent, + width: isFocused ? 3 : 0, + ), + boxShadow: isFocused + ? [ + BoxShadow( + color: SacredColors.primary + .withValues(alpha: 0.28), + blurRadius: 24 * s, + spreadRadius: 2 * s, + ), + ] + : null, + ), + child: ElevatedButton.icon( + onPressed: () => _showCitySearchDialog(s), + icon: HugeIcon( + icon: HugeIcons.strokeRoundedSearch01, + color: isFocused + ? SacredColors.onPrimary + : SacredColors.onPrimary, + ), + label: Text( + 'CARI KOTA', + style: TextStyle(fontSize: 16 * s), + ), + style: _tvElevatedActionStyle( + s: s, + normalBackground: SacredColors.secondary, + normalForeground: SacredColors.onPrimary, + padding: EdgeInsets.symmetric( + horizontal: 24 * s, + vertical: 24 * s, + ), + fontSize: 16 * s, + ), ), - fontSize: 16 * s, ), - ), - ), - ); - }, + ); + }, + ), + ), ), - ), + ], ), - ], - ), + ), + ], ), - ], - ), ), ), ); @@ -3614,187 +3720,204 @@ class _AdminScreenState extends ConsumerState { final cacheRangeLabel = cacheStatus.hasData ? '${_formatCacheDate(cacheStatus.startDate)} - ${_formatCacheDate(cacheStatus.endDate)}' : 'Belum ada data'; - + return SingleChildScrollView( controller: _jadwalScrollController, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'Jadwal & Sinkronisasi', - style: GoogleFonts.plusJakartaSans( - fontSize: 48 * s, - fontWeight: FontWeight.w700, - color: SacredColors.primary, + Text( + 'Jadwal & Sinkronisasi', + style: GoogleFonts.plusJakartaSans( + fontSize: 48 * s, + fontWeight: FontWeight.w700, + color: SacredColors.primary, + ), ), - ), - SizedBox(height: 48 * s), - - // Sync Card - Container( - width: double.infinity, - padding: EdgeInsets.all(40 * s), - decoration: BoxDecoration( - color: SacredColors.surfaceContainerLow, - borderRadius: BorderRadius.circular(SacredRadii.xl), - border: Border.all(color: SacredColors.outlineVariant.withValues(alpha: 0.4)), - ), - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Status Data Jadwal', - style: GoogleFonts.manrope(fontSize: 20 * s, fontWeight: FontWeight.w700, color: SacredColors.onSurfaceVariant, letterSpacing: 1 * s), - ), - SizedBox(height: 24 * s), - Wrap( - spacing: 48 * s, - runSpacing: 20 * s, - children: [ - _buildStatusRow('Terakhir Sync', settings.lastSyncDate ?? 'Belum pernah', HugeIcons.strokeRoundedClock01, s), - _buildStatusRow('Sumber Data', 'api.myquran.com', HugeIcons.strokeRoundedDatabase01, s), - _buildStatusRow('Lokasi Data', settings.cityDisplayName, HugeIcons.strokeRoundedLocation01, s), - _buildStatusRow('Cache Tersimpan', cacheRangeLabel, HugeIcons.strokeRoundedCalendar03, s), - _buildStatusRow('Jumlah Hari', cacheStatus.hasData ? '${cacheStatus.cachedDays} hari' : '0 hari', HugeIcons.strokeRoundedTaskDaily01, s), - _buildStatusRow('Status Update', _buildCacheUpdateLabel(cacheStatus, todayScheduleOption != null), HugeIcons.strokeRoundedAlert02, s), - ], - ), - ], + SizedBox(height: 48 * s), + + // Sync Card + Container( + width: double.infinity, + padding: EdgeInsets.all(40 * s), + decoration: BoxDecoration( + color: SacredColors.surfaceContainerLow, + borderRadius: BorderRadius.circular(SacredRadii.xl), + border: Border.all( + color: SacredColors.outlineVariant.withValues(alpha: 0.4)), + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Status Data Jadwal', + style: GoogleFonts.manrope( + fontSize: 20 * s, + fontWeight: FontWeight.w700, + color: SacredColors.onSurfaceVariant, + letterSpacing: 1 * s), + ), + SizedBox(height: 24 * s), + Wrap( + spacing: 48 * s, + runSpacing: 20 * s, + children: [ + _buildStatusRow( + 'Terakhir Sync', + settings.lastSyncDate ?? 'Belum pernah', + HugeIcons.strokeRoundedClock01, + s), + _buildStatusRow('Sumber Data', 'api.myquran.com', + HugeIcons.strokeRoundedDatabase01, s), + _buildStatusRow( + 'Lokasi Data', + settings.cityDisplayName, + HugeIcons.strokeRoundedLocation01, + s), + _buildStatusRow('Cache Tersimpan', cacheRangeLabel, + HugeIcons.strokeRoundedCalendar03, s), + _buildStatusRow( + 'Jumlah Hari', + cacheStatus.hasData + ? '${cacheStatus.cachedDays} hari' + : '0 hari', + HugeIcons.strokeRoundedTaskDaily01, + s), + _buildStatusRow( + 'Status Update', + _buildCacheUpdateLabel( + cacheStatus, todayScheduleOption != null), + HugeIcons.strokeRoundedAlert02, + s), + ], + ), + ], + ), ), - ), - ], + ], + ), ), - ), - - SizedBox(height: 20 * s), - _buildJadwalActionButton( - rowIndex: 0, - s: s, - onActivate: _isSyncing ? () {} : _syncData, - builder: (isFocused) => _buildTvPrimaryActionSurface( + + SizedBox(height: 20 * s), + _buildJadwalActionButton( + rowIndex: 0, s: s, - isFocused: isFocused, - icon: _isSyncing - ? SizedBox( - width: 24 * s, - height: 24 * s, - child: CircularProgressIndicator( + onActivate: _isSyncing ? () {} : _syncData, + builder: (isFocused) => _buildTvPrimaryActionSurface( + s: s, + isFocused: isFocused, + icon: _isSyncing + ? SizedBox( + width: 24 * s, + height: 24 * s, + child: CircularProgressIndicator( + color: isFocused + ? SacredColors.onPrimary + : SacredColors.onSecondary, + strokeWidth: 3, + ), + ) + : HugeIcon( + icon: HugeIcons.strokeRoundedCloudDownload, color: isFocused ? SacredColors.onPrimary : SacredColors.onSecondary, - strokeWidth: 3, ), - ) - : HugeIcon( - icon: HugeIcons.strokeRoundedCloudDownload, - color: isFocused - ? SacredColors.onPrimary - : SacredColors.onSecondary, - ), - label: _isSyncing - ? 'MENYINKRONKAN...' - : 'SINKRONKAN DATA BULAN INI', + label: + _isSyncing ? 'MENYINKRONKAN...' : 'SINKRONKAN DATA BULAN INI', + ), ), - ), - - SizedBox(height: 64 * s), - _adminCard( - s, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _sectionLabel('Kalender Hijriah', s), - SizedBox(height: 8 * s), - Text( - 'Sesuaikan tampilan tanggal Hijriah jika hasil rukyat lokal masjid berbeda dari nilai default API.', - style: GoogleFonts.manrope( - fontSize: 14 * s, - color: SacredColors.onSurfaceVariant, - ), - ), - SizedBox(height: 24 * s), - Container( - width: double.infinity, - padding: EdgeInsets.all(24 * s), - decoration: BoxDecoration( - color: SacredColors.surfaceContainerHighest.withValues(alpha: 0.3), - borderRadius: BorderRadius.circular(SacredRadii.lg), - border: Border.all( - color: SacredColors.outlineVariant.withValues(alpha: 0.2), + SizedBox(height: 64 * s), + + _adminCard( + s, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _sectionLabel('Kalender Hijriah', s), + SizedBox(height: 8 * s), + Text( + 'Sesuaikan tampilan tanggal Hijriah jika hasil rukyat lokal masjid berbeda dari nilai default API.', + style: GoogleFonts.manrope( + fontSize: 14 * s, + color: SacredColors.onSurfaceVariant, ), ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Tanggal tampil saat ini', - style: GoogleFonts.manrope( - fontSize: 14 * s, - fontWeight: FontWeight.w600, - color: SacredColors.onSurfaceVariant, + SizedBox(height: 24 * s), + Container( + width: double.infinity, + padding: EdgeInsets.all(24 * s), + decoration: BoxDecoration( + color: SacredColors.surfaceContainerHighest + .withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(SacredRadii.lg), + border: Border.all( + color: SacredColors.outlineVariant.withValues(alpha: 0.2), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Tanggal tampil saat ini', + style: GoogleFonts.manrope( + fontSize: 14 * s, + fontWeight: FontWeight.w600, + color: SacredColors.onSurfaceVariant, + ), ), + SizedBox(height: 8 * s), + Text( + displayedHijri ?? 'Memuat tanggal Hijriah...', + style: GoogleFonts.plusJakartaSans( + fontSize: 28 * s, + fontWeight: FontWeight.w700, + color: SacredColors.onSurface, + ), + ), + ], + ), + Container( + padding: EdgeInsets.symmetric( + horizontal: 16 * s, + vertical: 10 * s, ), - SizedBox(height: 8 * s), - Text( - displayedHijri ?? 'Memuat tanggal Hijriah...', + decoration: BoxDecoration( + color: SacredColors.primary.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(SacredRadii.full), + ), + child: Text( + 'Offset ${_hijriOffsetDays >= 0 ? '+' : ''}$_hijriOffsetDays hari', style: GoogleFonts.plusJakartaSans( - fontSize: 28 * s, + fontSize: 16 * s, fontWeight: FontWeight.w700, - color: SacredColors.onSurface, + color: SacredColors.primary, ), ), - ], - ), - Container( - padding: EdgeInsets.symmetric( - horizontal: 16 * s, - vertical: 10 * s, ), - decoration: BoxDecoration( - color: SacredColors.primary.withValues(alpha: 0.12), - borderRadius: BorderRadius.circular(SacredRadii.full), - ), - child: Text( - 'Offset ${_hijriOffsetDays >= 0 ? '+' : ''}$_hijriOffsetDays hari', - style: GoogleFonts.plusJakartaSans( - fontSize: 16 * s, - fontWeight: FontWeight.w700, - color: SacredColors.primary, - ), - ), - ), - ], + ], + ), ), - ), - SizedBox(height: 20 * s), - _buildHijriOffsetControl( - s, - focusNode: _jadwalFocusNodes[1], - onMoveLeft: () => _focusNavTab(_selectedTab), - onMoveUp: () => _focusJadwalRow(0), - onMoveDown: () => _focusJadwalRow(2), - ), - SizedBox(height: 16 * s), - _buildJadwalActionButton( - rowIndex: 2, - s: s, - onActivate: () { - setState(() { - _hijriOffsetDays = 0; - }); - _queueJadwalAutoSave( - message: 'Offset Hijriah direset dan otomatis tersimpan', - ); - }, - child: OutlinedButton.icon( - onPressed: () { + SizedBox(height: 20 * s), + _buildHijriOffsetControl( + s, + focusNode: _jadwalFocusNodes[1], + onMoveLeft: () => _focusNavTab(_selectedTab), + onMoveUp: () => _focusJadwalRow(0), + onMoveDown: () => _focusJadwalRow(2), + ), + SizedBox(height: 16 * s), + _buildJadwalActionButton( + rowIndex: 2, + s: s, + onActivate: () { setState(() { _hijriOffsetDays = 0; }); @@ -3802,217 +3925,233 @@ class _AdminScreenState extends ConsumerState { message: 'Offset Hijriah direset dan otomatis tersimpan', ); }, - icon: const Icon(Icons.refresh), - label: const Text('RESET OFFSET'), + child: OutlinedButton.icon( + onPressed: () { + setState(() { + _hijriOffsetDays = 0; + }); + _queueJadwalAutoSave( + message: + 'Offset Hijriah direset dan otomatis tersimpan', + ); + }, + icon: const Icon(Icons.refresh), + label: const Text('RESET OFFSET'), + ), ), - ), - ], + ], + ), ), - ), - SizedBox(height: 64 * s), + SizedBox(height: 64 * s), - // Waktu & Durasi Card - _adminCard(s, child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _sectionLabel('Waktu & Durasi', s), - SizedBox(height: 8 * s), - Text( - 'Seluruh pengaturan angka utama untuk alur jadwal ditangani dengan stepper agar nyaman dipakai dengan remote Android TV.', - style: GoogleFonts.manrope(fontSize: 14 * s, color: SacredColors.onSurfaceVariant), - ), - SizedBox(height: 32 * s), - _buildTvIntStepperField( - s: s, - label: 'Pra-Adzan', - focusNode: _jadwalFocusNodes[3], - controller: _preAdzanLeadCtrl, - fallback: 10, - min: 0, - max: 60, - suffix: 'menit', - onMoveLeft: () => _focusNavTab(_selectedTab), - onMoveUp: () => _focusJadwalRow(2), - onMoveDown: () => _focusJadwalRow(4), - onValueChanged: _queueJadwalAutoSave, - ), - SizedBox(height: 16 * s), - _buildTvIntStepperField( - s: s, - label: 'Blank Screen Normal', - focusNode: _jadwalFocusNodes[4], - controller: _blankNormalCtrl, - fallback: 15, - min: 0, - max: 120, - suffix: 'menit', - onMoveLeft: () => _focusNavTab(_selectedTab), - onMoveUp: () => _focusJadwalRow(3), - onMoveDown: () => _focusJadwalRow(5), - onValueChanged: _queueJadwalAutoSave, - ), - SizedBox(height: 16 * s), - _buildTvIntStepperField( - s: s, - label: 'Blank Screen Jumat', - focusNode: _jadwalFocusNodes[5], - controller: _blankJumatCtrl, - fallback: 45, - min: 0, - max: 180, - suffix: 'menit', - onMoveLeft: () => _focusNavTab(_selectedTab), - onMoveUp: () => _focusJadwalRow(4), - onMoveDown: () => _focusJadwalRow(6), - onValueChanged: _queueJadwalAutoSave, - ), - SizedBox(height: 28 * s), - Text( - 'Jeda Waktu Iqamah (Menit)', - style: GoogleFonts.manrope( - fontSize: 16 * s, - fontWeight: FontWeight.w700, - color: SacredColors.onSurface, - ), - ), - SizedBox(height: 8 * s), - Text( - 'Tentukan durasi hitung mundur dari selesai Adzan hingga iqamah untuk tiap shalat fardhu.', - style: GoogleFonts.manrope(fontSize: 14 * s, color: SacredColors.onSurfaceVariant), - ), - SizedBox(height: 24 * s), - _buildTvIntStepperField( - s: s, - label: 'Iqamah Subuh', - focusNode: _jadwalFocusNodes[6], - controller: _iqomahSubuhCtrl, - fallback: 15, - min: 0, - max: 60, - suffix: 'menit', - onMoveLeft: () => _focusNavTab(_selectedTab), - onMoveUp: () => _focusJadwalRow(5), - onMoveDown: () => _focusJadwalRow(7), - onValueChanged: _queueJadwalAutoSave, - ), - SizedBox(height: 16 * s), - _buildTvIntStepperField( - s: s, - label: 'Iqamah Dzuhur', - focusNode: _jadwalFocusNodes[7], - controller: _iqomahDzuhurCtrl, - fallback: 10, - min: 0, - max: 60, - suffix: 'menit', - onMoveLeft: () => _focusNavTab(_selectedTab), - onMoveUp: () => _focusJadwalRow(6), - onMoveDown: () => _focusJadwalRow(8), - onValueChanged: _queueJadwalAutoSave, - ), - SizedBox(height: 16 * s), - _buildTvIntStepperField( - s: s, - label: 'Iqamah Ashar', - focusNode: _jadwalFocusNodes[8], - controller: _iqomahAsharCtrl, - fallback: 10, - min: 0, - max: 60, - suffix: 'menit', - onMoveLeft: () => _focusNavTab(_selectedTab), - onMoveUp: () => _focusJadwalRow(7), - onMoveDown: () => _focusJadwalRow(9), - onValueChanged: _queueJadwalAutoSave, - ), - SizedBox(height: 16 * s), - _buildTvIntStepperField( - s: s, - label: 'Iqamah Maghrib', - focusNode: _jadwalFocusNodes[9], - controller: _iqomahMaghribCtrl, - fallback: 7, - min: 0, - max: 60, - suffix: 'menit', - onMoveLeft: () => _focusNavTab(_selectedTab), - onMoveUp: () => _focusJadwalRow(8), - onMoveDown: () => _focusJadwalRow(10), - onValueChanged: _queueJadwalAutoSave, - ), - SizedBox(height: 16 * s), - _buildTvIntStepperField( - s: s, - label: 'Iqamah Isya', - focusNode: _jadwalFocusNodes[10], - controller: _iqomahIsyaCtrl, - fallback: 10, - min: 0, - max: 60, - suffix: 'menit', - onMoveLeft: () => _focusNavTab(_selectedTab), - onMoveUp: () => _focusJadwalRow(9), - onValueChanged: _queueJadwalAutoSave, - ), - ], - )), + // Waktu & Durasi Card + _adminCard(s, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _sectionLabel('Waktu & Durasi', s), + SizedBox(height: 8 * s), + Text( + 'Seluruh pengaturan angka utama untuk alur jadwal ditangani dengan stepper agar nyaman dipakai dengan remote Android TV.', + style: GoogleFonts.manrope( + fontSize: 14 * s, color: SacredColors.onSurfaceVariant), + ), + SizedBox(height: 32 * s), + _buildTvIntStepperField( + s: s, + label: 'Pra-Adzan', + focusNode: _jadwalFocusNodes[3], + controller: _preAdzanLeadCtrl, + fallback: 10, + min: 0, + max: 60, + suffix: 'menit', + onMoveLeft: () => _focusNavTab(_selectedTab), + onMoveUp: () => _focusJadwalRow(2), + onMoveDown: () => _focusJadwalRow(4), + onValueChanged: _queueJadwalAutoSave, + ), + SizedBox(height: 16 * s), + _buildTvIntStepperField( + s: s, + label: 'Blank Screen Normal', + focusNode: _jadwalFocusNodes[4], + controller: _blankNormalCtrl, + fallback: 15, + min: 0, + max: 120, + suffix: 'menit', + onMoveLeft: () => _focusNavTab(_selectedTab), + onMoveUp: () => _focusJadwalRow(3), + onMoveDown: () => _focusJadwalRow(5), + onValueChanged: _queueJadwalAutoSave, + ), + SizedBox(height: 16 * s), + _buildTvIntStepperField( + s: s, + label: 'Blank Screen Jumat', + focusNode: _jadwalFocusNodes[5], + controller: _blankJumatCtrl, + fallback: 45, + min: 0, + max: 180, + suffix: 'menit', + onMoveLeft: () => _focusNavTab(_selectedTab), + onMoveUp: () => _focusJadwalRow(4), + onMoveDown: () => _focusJadwalRow(6), + onValueChanged: _queueJadwalAutoSave, + ), + SizedBox(height: 28 * s), + Text( + 'Jeda Waktu Iqamah (Menit)', + style: GoogleFonts.manrope( + fontSize: 16 * s, + fontWeight: FontWeight.w700, + color: SacredColors.onSurface, + ), + ), + SizedBox(height: 8 * s), + Text( + 'Tentukan durasi hitung mundur dari selesai Adzan hingga iqamah untuk tiap shalat fardhu.', + style: GoogleFonts.manrope( + fontSize: 14 * s, color: SacredColors.onSurfaceVariant), + ), + SizedBox(height: 24 * s), + _buildTvIntStepperField( + s: s, + label: 'Iqamah Subuh', + focusNode: _jadwalFocusNodes[6], + controller: _iqomahSubuhCtrl, + fallback: 15, + min: 0, + max: 60, + suffix: 'menit', + onMoveLeft: () => _focusNavTab(_selectedTab), + onMoveUp: () => _focusJadwalRow(5), + onMoveDown: () => _focusJadwalRow(7), + onValueChanged: _queueJadwalAutoSave, + ), + SizedBox(height: 16 * s), + _buildTvIntStepperField( + s: s, + label: 'Iqamah Dzuhur', + focusNode: _jadwalFocusNodes[7], + controller: _iqomahDzuhurCtrl, + fallback: 10, + min: 0, + max: 60, + suffix: 'menit', + onMoveLeft: () => _focusNavTab(_selectedTab), + onMoveUp: () => _focusJadwalRow(6), + onMoveDown: () => _focusJadwalRow(8), + onValueChanged: _queueJadwalAutoSave, + ), + SizedBox(height: 16 * s), + _buildTvIntStepperField( + s: s, + label: 'Iqamah Ashar', + focusNode: _jadwalFocusNodes[8], + controller: _iqomahAsharCtrl, + fallback: 10, + min: 0, + max: 60, + suffix: 'menit', + onMoveLeft: () => _focusNavTab(_selectedTab), + onMoveUp: () => _focusJadwalRow(7), + onMoveDown: () => _focusJadwalRow(9), + onValueChanged: _queueJadwalAutoSave, + ), + SizedBox(height: 16 * s), + _buildTvIntStepperField( + s: s, + label: 'Iqamah Maghrib', + focusNode: _jadwalFocusNodes[9], + controller: _iqomahMaghribCtrl, + fallback: 7, + min: 0, + max: 60, + suffix: 'menit', + onMoveLeft: () => _focusNavTab(_selectedTab), + onMoveUp: () => _focusJadwalRow(8), + onMoveDown: () => _focusJadwalRow(10), + onValueChanged: _queueJadwalAutoSave, + ), + SizedBox(height: 16 * s), + _buildTvIntStepperField( + s: s, + label: 'Iqamah Isya', + focusNode: _jadwalFocusNodes[10], + controller: _iqomahIsyaCtrl, + fallback: 10, + min: 0, + max: 60, + suffix: 'menit', + onMoveLeft: () => _focusNavTab(_selectedTab), + onMoveUp: () => _focusJadwalRow(9), + onValueChanged: _queueJadwalAutoSave, + ), + ], + )), - SizedBox(height: 64 * s), - - Text( - 'Pratinjau Jadwal Hari Ini', - style: GoogleFonts.plusJakartaSans( - fontSize: 32 * s, - fontWeight: FontWeight.w700, - color: SacredColors.onSurface, + SizedBox(height: 64 * s), + + Text( + 'Pratinjau Jadwal Hari Ini', + style: GoogleFonts.plusJakartaSans( + fontSize: 32 * s, + fontWeight: FontWeight.w700, + color: SacredColors.onSurface, + ), ), - ), - SizedBox(height: 32 * s), - - // Schedule Grid - Builder( - builder: (context) { - if (todayScheduleOption == null) { - return Padding( - padding: EdgeInsets.symmetric(vertical: 24 * s), - child: Center( - child: Text('Data jadwal kosong. Silakan lakukan sinkronisasi.', style: GoogleFonts.manrope(fontSize: 24 * s, color: SacredColors.error)), + SizedBox(height: 32 * s), + + // Schedule Grid + Builder( + builder: (context) { + if (todayScheduleOption == null) { + return Padding( + padding: EdgeInsets.symmetric(vertical: 24 * s), + child: Center( + child: Text( + 'Data jadwal kosong. Silakan lakukan sinkronisasi.', + style: GoogleFonts.manrope( + fontSize: 24 * s, color: SacredColors.error)), + ), + ); + } + + final prayerMap = { + 'IMSAK': todayScheduleOption.imsak, + 'SUBUH': todayScheduleOption.subuh, + 'TERBIT': todayScheduleOption.terbit, + 'DHUHA': todayScheduleOption.dhuha, + 'DZUHUR': todayScheduleOption.dzuhur, + 'ASHAR': todayScheduleOption.ashar, + 'MAGHRIB': todayScheduleOption.maghrib, + 'ISYA': todayScheduleOption.isya, + }; + + return GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + crossAxisSpacing: 24 * s, + mainAxisSpacing: 24 * s, + childAspectRatio: 2.2, // wide rectangular Google Stitch cards ), + itemCount: prayerMap.length, + itemBuilder: (context, index) { + final key = prayerMap.keys.elementAt(index); + final time = prayerMap[key]!; + return _buildPrayerCard(key, time, s); + }, ); - } - - final prayerMap = { - 'IMSAK': todayScheduleOption.imsak, - 'SUBUH': todayScheduleOption.subuh, - 'TERBIT': todayScheduleOption.terbit, - 'DHUHA': todayScheduleOption.dhuha, - 'DZUHUR': todayScheduleOption.dzuhur, - 'ASHAR': todayScheduleOption.ashar, - 'MAGHRIB': todayScheduleOption.maghrib, - 'ISYA': todayScheduleOption.isya, - }; - - return GridView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 4, - crossAxisSpacing: 24 * s, - mainAxisSpacing: 24 * s, - childAspectRatio: 2.2, // wide rectangular Google Stitch cards - ), - itemCount: prayerMap.length, - itemBuilder: (context, index) { - final key = prayerMap.keys.elementAt(index); - final time = prayerMap[key]!; - return _buildPrayerCard(key, time, s); - }, - ); - }, - ), - SizedBox(height: 32 * s), - ], + }, + ), + SizedBox(height: 32 * s), + ], ), ); } @@ -4032,7 +4171,8 @@ class _AdminScreenState extends ConsumerState { label: 'Offset Hari Hijriah', valueLabel: '${_hijriOffsetDays >= 0 ? '+' : ''}$_hijriOffsetDays hari', progress: (_hijriOffsetDays - minOffset) / (maxOffset - minOffset), - helperText: 'Tekan OK untuk masuk mode ubah. Saat aktif, gunakan ← → untuk geser 1 hari.', + helperText: + 'Tekan OK untuk masuk mode ubah. Saat aktif, gunakan ← → untuk geser 1 hari.', onMoveLeft: onMoveLeft, onMoveUp: onMoveUp, onMoveDown: onMoveDown, @@ -4055,7 +4195,8 @@ class _AdminScreenState extends ConsumerState { ); }, onProgressChanged: (nextProgress) { - final mapped = (minOffset + ((maxOffset - minOffset) * nextProgress)).round(); + final mapped = + (minOffset + ((maxOffset - minOffset) * nextProgress)).round(); final clamped = mapped.clamp(minOffset, maxOffset).toInt(); if (clamped == _hijriOffsetDays) return; setState(() { @@ -4072,17 +4213,17 @@ class _AdminScreenState extends ConsumerState { Widget _buildPrayerCard(String name, String time, double s) { return Container( decoration: BoxDecoration( - color: SacredColors.surfaceContainerLowest.withValues(alpha: 0.5), - borderRadius: BorderRadius.circular(SacredRadii.lg), - border: Border.all(color: SacredColors.outlineVariant.withValues(alpha: 0.3)), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.2), - blurRadius: 10 * s, - offset: Offset(0, 4 * s), - ) - ] - ), + color: SacredColors.surfaceContainerLowest.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(SacredRadii.lg), + border: Border.all( + color: SacredColors.outlineVariant.withValues(alpha: 0.3)), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.2), + blurRadius: 10 * s, + offset: Offset(0, 4 * s), + ) + ]), padding: EdgeInsets.symmetric(horizontal: 32 * s, vertical: 24 * s), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -4315,7 +4456,8 @@ class _AdminScreenState extends ConsumerState { color: SacredColors.surfaceContainerHighest, borderRadius: BorderRadius.circular(SacredRadii.sm), ), - child: HugeIcon(icon: icon, color: SacredColors.secondary, size: 24 * s), + child: + HugeIcon(icon: icon, color: SacredColors.secondary, size: 24 * s), ), SizedBox(width: 16 * s), Column( @@ -4380,7 +4522,8 @@ class _AdminScreenState extends ConsumerState { label: label, valueLabel: '$pct%', progress: ((value - 0.5) / 1.5).clamp(0.0, 1.0), - helperText: 'Tekan OK untuk mulai ubah. Saat aktif, gunakan ← → untuk mengubah skala 5%.', + helperText: + 'Tekan OK untuk mulai ubah. Saat aktif, gunakan ← → untuk mengubah skala 5%.', onMoveLeft: onMoveLeft, onMoveUp: onMoveUp, onMoveDown: onMoveDown, @@ -4388,7 +4531,8 @@ class _AdminScreenState extends ConsumerState { onDecrement: () => onChanged((value - step).clamp(0.5, 2.0)), onProgressChanged: (nextProgress) { final mapped = (0.5 + (1.5 * nextProgress)).clamp(0.5, 2.0); - final snapped = (((mapped / step).round() * step).clamp(0.5, 2.0)).toDouble(); + final snapped = + (((mapped / step).round() * step).clamp(0.5, 2.0)).toDouble(); onChanged(snapped); }, ); @@ -4407,182 +4551,216 @@ class _AdminScreenState extends ConsumerState { child: SingleChildScrollView( controller: _simulasiScrollController, child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Mode Simulasi Pengembang', - style: GoogleFonts.plusJakartaSans( - fontSize: 48 * s, - fontWeight: FontWeight.w700, - color: SacredColors.primary, - ), - ), - SizedBox(height: 16 * s), - Text( - 'Gunakan tombol di bawah ini untuk melihat pratinjau bagaimana aplikasi bereaksi terhadap berbagai waktu dan status tanpa harus menunggu waktu sebenarnya.\nFitur ini bekerja dengan menggeser waktu aplikasi (Time Travel).', - style: GoogleFonts.manrope( - fontSize: 20 * s, - fontWeight: FontWeight.w500, - height: 1.35, - color: SacredColors.onSurfaceVariant, - ), - ), - SizedBox(height: 48 * s), - Container( - width: double.infinity, - padding: EdgeInsets.all(20 * s), - decoration: BoxDecoration( - color: isSimulating - ? SacredColors.error.withValues(alpha: 0.12) - : SacredColors.surfaceContainerLowest, - borderRadius: BorderRadius.circular(SacredRadii.lg), - border: Border.all( - color: isSimulating - ? SacredColors.error.withValues(alpha: 0.45) - : SacredColors.outlineVariant.withValues(alpha: 0.3), - ), - ), - child: Row( - children: [ - HugeIcon( - icon: isSimulating - ? HugeIcons.strokeRoundedAlert02 - : HugeIcons.strokeRoundedCheckmarkCircle02, - color: isSimulating - ? SacredColors.error - : SacredColors.primary, - size: 28 * s, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Mode Simulasi Pengembang', + style: GoogleFonts.plusJakartaSans( + fontSize: 48 * s, + fontWeight: FontWeight.w700, + color: SacredColors.primary, ), - SizedBox(width: 16 * s), - Expanded( - child: Text( - isSimulating - ? 'Simulasi aktif (${simulatedMinutes >= 0 ? '+' : ''}$simulatedMinutes menit). Gunakan kartu pertama untuk keluar dan kembali ke waktu asli.' - : 'Simulasi tidak aktif. Pilih salah satu skenario di bawah untuk mulai menguji tampilan layar.', - style: GoogleFonts.manrope( - fontSize: 16 * s, - fontWeight: FontWeight.w700, - color: SacredColors.onSurface, - ), + ), + SizedBox(height: 16 * s), + Text( + 'Gunakan tombol di bawah ini untuk melihat pratinjau bagaimana aplikasi bereaksi terhadap berbagai waktu dan status tanpa harus menunggu waktu sebenarnya.\nFitur ini bekerja dengan menggeser waktu aplikasi (Time Travel).', + style: GoogleFonts.manrope( + fontSize: 20 * s, + fontWeight: FontWeight.w500, + height: 1.35, + color: SacredColors.onSurfaceVariant, + ), + ), + SizedBox(height: 48 * s), + Container( + width: double.infinity, + padding: EdgeInsets.all(20 * s), + decoration: BoxDecoration( + color: isSimulating + ? SacredColors.error.withValues(alpha: 0.12) + : SacredColors.surfaceContainerLowest, + borderRadius: BorderRadius.circular(SacredRadii.lg), + border: Border.all( + color: isSimulating + ? SacredColors.error.withValues(alpha: 0.45) + : SacredColors.outlineVariant.withValues(alpha: 0.3), ), ), - ], - ), + child: Row( + children: [ + HugeIcon( + icon: isSimulating + ? HugeIcons.strokeRoundedAlert02 + : HugeIcons.strokeRoundedCheckmarkCircle02, + color: isSimulating + ? SacredColors.error + : SacredColors.primary, + size: 28 * s, + ), + SizedBox(width: 16 * s), + Expanded( + child: Text( + isSimulating + ? 'Simulasi aktif (${simulatedMinutes >= 0 ? '+' : ''}$simulatedMinutes menit). Gunakan kartu pertama untuk keluar dan kembali ke waktu asli.' + : 'Simulasi tidak aktif. Pilih salah satu skenario di bawah untuk mulai menguji tampilan layar.', + style: GoogleFonts.manrope( + fontSize: 16 * s, + fontWeight: FontWeight.w700, + color: SacredColors.onSurface, + ), + ), + ), + ], + ), + ), + SizedBox(height: 24 * s), + _simulasiCard( + s: s, + title: isSimulating + ? 'Keluar dari Simulasi' + : 'Gunakan Waktu Asli', + icon: isSimulating + ? HugeIcons.strokeRoundedCancelCircle + : HugeIcons.strokeRoundedHome01, + desc: isSimulating + ? 'Matikan mode simulasi dan kembali ke waktu sistem saat ini.' + : 'Pastikan aplikasi berjalan menggunakan waktu asli perangkat.', + onTap: () => _activateSimulation( + () => _simulateTimeOffset(Duration.zero), + ), + focusNode: _simulasiFocusNodes[0], + rowIndex: 0, + ), + SizedBox(height: 16 * s), + _simulasiCard( + s: s, + title: '15 Detik Sebelum Adzan', + icon: HugeIcons.strokeRoundedNotification03, + desc: + 'Melompat ke 15 detik sebelum Adzan Dzuhur untuk memeriksa transisi terakhir menuju Adzan.', + onTap: () => _activateSimulation( + () => _simulateEvent('pre_adzan_15'), + ), + focusNode: _simulasiFocusNodes[1], + rowIndex: 1, + ), + SizedBox(height: 16 * s), + _simulasiCard( + s: s, + title: 'Menuju Adzan', + icon: HugeIcons.strokeRoundedClock01, + desc: 'Melompat ke 2 menit sebelum Adzan Dzuhur hari ini.', + onTap: () => _activateSimulation( + () => _simulateEvent('pre_adzan'), + ), + focusNode: _simulasiFocusNodes[2], + rowIndex: 2, + ), + SizedBox(height: 16 * s), + _simulasiCard( + s: s, + title: 'Selama Adzan', + icon: HugeIcons.strokeRoundedMegaphone01, + desc: 'Melompat ke tepat waktu Adzan Dzuhur berkumandang.', + onTap: () => _activateSimulation( + () => _simulateEvent('adzan'), + ), + focusNode: _simulasiFocusNodes[3], + rowIndex: 3, + ), + SizedBox(height: 16 * s), + _simulasiCard( + s: s, + title: '15 Detik Sebelum Iqamah', + icon: HugeIcons.strokeRoundedTimer02, + desc: + 'Melompat ke 15 detik sebelum Iqamah Dzuhur untuk memeriksa hitungan mundur terakhir.', + onTap: () => _activateSimulation( + () => _simulateEvent('pre_iqomah_15'), + ), + focusNode: _simulasiFocusNodes[4], + rowIndex: 4, + ), + SizedBox(height: 16 * s), + _simulasiCard( + s: s, + title: 'Menuju Iqomah', + icon: HugeIcons.strokeRoundedTimer02, + desc: + 'Melompat ke saat waktu iqomah sedang menghitung mundur (1 menit setelah Adzan).', + onTap: () => _activateSimulation( + () => _simulateEvent('iqomah'), + ), + focusNode: _simulasiFocusNodes[5], + rowIndex: 5, + ), + SizedBox(height: 16 * s), + _simulasiCard( + s: s, + title: '10 Detik Sebelum Ganti Hari', + icon: HugeIcons.strokeRoundedCalendar03, + desc: + 'Melompat ke 23:59:50 untuk menguji peralihan tanggal, Hijriah, dan fallback jadwal saat masuk 00:00.', + onTap: () => _activateSimulation( + () => _simulateEvent('overnight_pre_midnight'), + ), + focusNode: _simulasiFocusNodes[6], + rowIndex: 6, + ), + SizedBox(height: 16 * s), + _simulasiCard( + s: s, + title: '10 Detik Setelah Ganti Hari', + icon: HugeIcons.strokeRoundedClock01, + desc: + 'Melompat ke 00:00:10 hari berikutnya untuk mengecek jadwal hari baru dan notifikasi "Menunggu Update Data".', + onTap: () => _activateSimulation( + () => _simulateEvent('overnight_post_midnight'), + ), + focusNode: _simulasiFocusNodes[7], + rowIndex: 7, + ), + SizedBox(height: 16 * s), + _simulasiCard( + s: s, + title: 'Persiapan Jumat', + icon: HugeIcons.strokeRoundedCalendar03, + desc: + 'Menyimulasikan layar khusus persiapan Jumat (30 menit sebelum Adzan Dzuhur).', + onTap: () => _activateSimulation( + () => _simulateEvent('jumat_incoming'), + ), + focusNode: _simulasiFocusNodes[8], + rowIndex: 8, + ), + SizedBox(height: 16 * s), + _simulasiCard( + s: s, + title: 'Khutbah Berlangsung', + icon: HugeIcons.strokeRoundedUserGroup, + desc: + 'Menyimulasikan layar saat Khutbah sedang berlangsung tanpa hitungan mundur (2 menit setelah Adzan Dzuhur).', + onTap: () => _activateSimulation( + () => _simulateEvent('jumat_khutbah'), + ), + focusNode: _simulasiFocusNodes[9], + rowIndex: 9, + ), + SizedBox(height: 16 * s), + _simulasiCard( + s: s, + title: 'Mode Shalat', + icon: HugeIcons.strokeRoundedMoon02, + desc: + 'Layar menjadi hitam atau gelap selama shalat berlangsung.', + onTap: () => _activateSimulation( + () => _simulateEvent('shalat'), + ), + focusNode: _simulasiFocusNodes[10], + rowIndex: 10, + ), + ], ), - SizedBox(height: 24 * s), - _simulasiCard( - s: s, - title: isSimulating ? 'Keluar dari Simulasi' : 'Gunakan Waktu Asli', - icon: isSimulating - ? HugeIcons.strokeRoundedCancelCircle - : HugeIcons.strokeRoundedHome01, - desc: isSimulating - ? 'Matikan mode simulasi dan kembali ke waktu sistem saat ini.' - : 'Pastikan aplikasi berjalan menggunakan waktu asli perangkat.', - onTap: () => _activateSimulation( - () => _simulateTimeOffset(Duration.zero), - ), - focusNode: _simulasiFocusNodes[0], - rowIndex: 0, - ), - SizedBox(height: 16 * s), - _simulasiCard( - s: s, - title: '15 Detik Sebelum Adzan', - icon: HugeIcons.strokeRoundedNotification03, - desc: 'Melompat ke 15 detik sebelum Adzan Dzuhur untuk memeriksa transisi terakhir menuju Adzan.', - onTap: () => _activateSimulation( - () => _simulateEvent('pre_adzan_15'), - ), - focusNode: _simulasiFocusNodes[1], - rowIndex: 1, - ), - SizedBox(height: 16 * s), - _simulasiCard( - s: s, - title: 'Menuju Adzan', - icon: HugeIcons.strokeRoundedClock01, - desc: 'Melompat ke 2 menit sebelum Adzan Dzuhur hari ini.', - onTap: () => _activateSimulation( - () => _simulateEvent('pre_adzan'), - ), - focusNode: _simulasiFocusNodes[2], - rowIndex: 2, - ), - SizedBox(height: 16 * s), - _simulasiCard( - s: s, - title: 'Selama Adzan', - icon: HugeIcons.strokeRoundedMegaphone01, - desc: 'Melompat ke tepat waktu Adzan Dzuhur berkumandang.', - onTap: () => _activateSimulation( - () => _simulateEvent('adzan'), - ), - focusNode: _simulasiFocusNodes[3], - rowIndex: 3, - ), - SizedBox(height: 16 * s), - _simulasiCard( - s: s, - title: '15 Detik Sebelum Iqamah', - icon: HugeIcons.strokeRoundedTimer02, - desc: 'Melompat ke 15 detik sebelum Iqamah Dzuhur untuk memeriksa hitungan mundur terakhir.', - onTap: () => _activateSimulation( - () => _simulateEvent('pre_iqomah_15'), - ), - focusNode: _simulasiFocusNodes[4], - rowIndex: 4, - ), - SizedBox(height: 16 * s), - _simulasiCard( - s: s, - title: 'Menuju Iqomah', - icon: HugeIcons.strokeRoundedTimer02, - desc: 'Melompat ke saat waktu iqomah sedang menghitung mundur (1 menit setelah Adzan).', - onTap: () => _activateSimulation( - () => _simulateEvent('iqomah'), - ), - focusNode: _simulasiFocusNodes[5], - rowIndex: 5, - ), - SizedBox(height: 16 * s), - _simulasiCard( - s: s, - title: 'Persiapan Jumat', - icon: HugeIcons.strokeRoundedCalendar03, - desc: 'Menyimulasikan layar khusus persiapan Jumat (30 menit sebelum Adzan Dzuhur).', - onTap: () => _activateSimulation( - () => _simulateEvent('jumat_incoming'), - ), - focusNode: _simulasiFocusNodes[6], - rowIndex: 6, - ), - SizedBox(height: 16 * s), - _simulasiCard( - s: s, - title: 'Khutbah Berlangsung', - icon: HugeIcons.strokeRoundedUserGroup, - desc: 'Menyimulasikan layar saat Khutbah sedang berlangsung tanpa hitungan mundur (2 menit setelah Adzan Dzuhur).', - onTap: () => _activateSimulation( - () => _simulateEvent('jumat_khutbah'), - ), - focusNode: _simulasiFocusNodes[7], - rowIndex: 7, - ), - SizedBox(height: 16 * s), - _simulasiCard( - s: s, - title: 'Mode Shalat', - icon: HugeIcons.strokeRoundedMoon02, - desc: 'Layar menjadi hitam atau gelap selama shalat berlangsung.', - onTap: () => _activateSimulation( - () => _simulateEvent('shalat'), - ), - focusNode: _simulasiFocusNodes[8], - rowIndex: 8, - ), - ], - ), ), ), ); @@ -4895,7 +5073,8 @@ class _AdminScreenState extends ConsumerState { color: SacredColors.surfaceContainerLowest, borderRadius: BorderRadius.circular(SacredRadii.lg), border: Border.all( - color: SacredColors.outlineVariant.withValues(alpha: 0.5), + color: + SacredColors.outlineVariant.withValues(alpha: 0.5), ), boxShadow: [ BoxShadow( @@ -4954,13 +5133,14 @@ class _AdminScreenState extends ConsumerState { if (schedule == null) return; // We simulate using schedule.dzuhur final dzuhurStr = schedule.dzuhur; - + final parts = dzuhurStr.split(':'); final realNow = DateTime.now(); - final dzuhurTime = DateTime(realNow.year, realNow.month, realNow.day, int.parse(parts[0]), int.parse(parts[1])); - + final dzuhurTime = DateTime(realNow.year, realNow.month, realNow.day, + int.parse(parts[0]), int.parse(parts[1])); + DateTime targetTime; - + switch (eventType) { case 'pre_adzan_15': targetTime = dzuhurTime.subtract(const Duration(seconds: 15)); @@ -4978,29 +5158,45 @@ class _AdminScreenState extends ConsumerState { .subtract(const Duration(seconds: 15)); break; case 'iqomah': - targetTime = dzuhurTime.add(const Duration(seconds: 45)); // During iqomah + targetTime = + dzuhurTime.add(const Duration(seconds: 45)); // During iqomah break; case 'jumat_incoming': int diff = DateTime.friday - realNow.weekday; DateTime nextFriday = realNow.add(Duration(days: diff)); // Target: next Friday at dzuhur time - 30 minutes - targetTime = DateTime(nextFriday.year, nextFriday.month, nextFriday.day, dzuhurTime.hour, dzuhurTime.minute).subtract(const Duration(minutes: 30)); + targetTime = DateTime(nextFriday.year, nextFriday.month, nextFriday.day, + dzuhurTime.hour, dzuhurTime.minute) + .subtract(const Duration(minutes: 30)); break; case 'jumat_khutbah': int diff = DateTime.friday - realNow.weekday; DateTime nextFriday = realNow.add(Duration(days: diff)); // Target: next Friday at dzuhur time + 3 minutes (safely past 2-min Adzan) - targetTime = DateTime(nextFriday.year, nextFriday.month, nextFriday.day, dzuhurTime.hour, dzuhurTime.minute).add(const Duration(minutes: 3)); + targetTime = DateTime(nextFriday.year, nextFriday.month, nextFriday.day, + dzuhurTime.hour, dzuhurTime.minute) + .add(const Duration(minutes: 3)); + break; + case 'overnight_pre_midnight': + final nextMidnight = + DateTime(realNow.year, realNow.month, realNow.day + 1); + targetTime = nextMidnight.subtract(const Duration(seconds: 10)); + break; + case 'overnight_post_midnight': + final nextMidnight = + DateTime(realNow.year, realNow.month, realNow.day + 1); + targetTime = nextMidnight.add(const Duration(seconds: 10)); break; case 'shalat': // Shalat mode usually happens after iqomah ends final settings = ref.read(settingsProvider); - targetTime = dzuhurTime.add(Duration(minutes: settings.iqomahDzuhur + 1)); + targetTime = + dzuhurTime.add(Duration(minutes: settings.iqomahDzuhur + 1)); break; default: targetTime = realNow; } - + final offset = targetTime.difference(realNow); _simulateTimeOffset(offset); @@ -5265,169 +5461,178 @@ class _TvAdjustTileState extends State<_TvAdjustTile> { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - Expanded( - child: Text( - widget.label, - style: GoogleFonts.plusJakartaSans( - fontSize: 18 * s, - fontWeight: FontWeight.w700, - color: SacredColors.onSurfaceVariant, - letterSpacing: 0.4 * s, + Row( + children: [ + Expanded( + child: Text( + widget.label, + style: GoogleFonts.plusJakartaSans( + fontSize: 18 * s, + fontWeight: FontWeight.w700, + color: SacredColors.onSurfaceVariant, + letterSpacing: 0.4 * s, + ), ), ), - ), - Container( - padding: EdgeInsets.symmetric( - horizontal: 14 * s, - vertical: 6 * s, - ), - decoration: BoxDecoration( - color: SacredColors.surfaceContainerHighest, - borderRadius: BorderRadius.circular(SacredRadii.sm), - border: Border.all( - color: SacredColors.primary.withValues(alpha: 0.35), + Container( + padding: EdgeInsets.symmetric( + horizontal: 14 * s, + vertical: 6 * s, ), - ), - child: Text( - widget.valueLabel, - style: GoogleFonts.manrope( - fontSize: 16 * s, - fontWeight: FontWeight.w800, - color: SacredColors.primary, - ), - ), - ), - ], - ), - SizedBox(height: 14 * s), - Row( - children: [ - GestureDetector( - onTap: () { - _focusNode.requestFocus(); - widget.onDecrement(); - }, - child: Container( - width: 36 * s, - height: 36 * s, - alignment: Alignment.center, decoration: BoxDecoration( - color: _isEditing - ? SacredColors.surfaceContainerHigh - : SacredColors.surfaceContainerHighest, + color: SacredColors.surfaceContainerHighest, borderRadius: BorderRadius.circular(SacredRadii.sm), border: Border.all( - color: _isEditing - ? SacredColors.primary.withValues(alpha: 0.8) - : SacredColors.outlineVariant.withValues(alpha: 0.35), + color: SacredColors.primary.withValues(alpha: 0.35), ), ), child: Text( - '←', + widget.valueLabel, style: GoogleFonts.manrope( fontSize: 16 * s, fontWeight: FontWeight.w800, - color: SacredColors.onSurface, + color: SacredColors.primary, ), ), ), - ), - SizedBox(width: 12 * s), - Expanded( - child: LayoutBuilder( - builder: (context, constraints) { - final barWidth = constraints.maxWidth; - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTapDown: (details) { - _focusNode.requestFocus(); - _updateFromTouchPosition(details.localPosition.dx, barWidth); - }, - onHorizontalDragStart: widget.onProgressChanged == null - ? null - : (_) { - _focusNode.requestFocus(); - setState(() => _isEditing = true); - }, - onHorizontalDragUpdate: widget.onProgressChanged == null - ? null - : (details) => _updateFromTouchPosition( - details.localPosition.dx, - barWidth, + ], + ), + SizedBox(height: 14 * s), + Row( + children: [ + GestureDetector( + onTap: () { + _focusNode.requestFocus(); + widget.onDecrement(); + }, + child: Container( + width: 36 * s, + height: 36 * s, + alignment: Alignment.center, + decoration: BoxDecoration( + color: _isEditing + ? SacredColors.surfaceContainerHigh + : SacredColors.surfaceContainerHighest, + borderRadius: BorderRadius.circular(SacredRadii.sm), + border: Border.all( + color: _isEditing + ? SacredColors.primary.withValues(alpha: 0.8) + : SacredColors.outlineVariant + .withValues(alpha: 0.35), + ), + ), + child: Text( + '←', + style: GoogleFonts.manrope( + fontSize: 16 * s, + fontWeight: FontWeight.w800, + color: SacredColors.onSurface, + ), + ), + ), + ), + SizedBox(width: 12 * s), + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + final barWidth = constraints.maxWidth; + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTapDown: (details) { + _focusNode.requestFocus(); + _updateFromTouchPosition( + details.localPosition.dx, barWidth); + }, + onHorizontalDragStart: + widget.onProgressChanged == null + ? null + : (_) { + _focusNode.requestFocus(); + setState(() => _isEditing = true); + }, + onHorizontalDragUpdate: + widget.onProgressChanged == null + ? null + : (details) => _updateFromTouchPosition( + details.localPosition.dx, + barWidth, + ), + onHorizontalDragEnd: + widget.onProgressChanged == null + ? null + : (_) => setState(() => _isEditing = false), + onHorizontalDragCancel: + widget.onProgressChanged == null + ? null + : () => setState(() => _isEditing = false), + child: Container( + height: 6 * s, + decoration: BoxDecoration( + color: SacredColors.outlineVariant + .withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(3 * s), + ), + child: FractionallySizedBox( + alignment: Alignment.centerLeft, + widthFactor: widget.progress.clamp(0.0, 1.0), + child: Container( + decoration: BoxDecoration( + color: SacredColors.primary, + borderRadius: BorderRadius.circular(3 * s), ), - onHorizontalDragEnd: widget.onProgressChanged == null - ? null - : (_) => setState(() => _isEditing = false), - onHorizontalDragCancel: widget.onProgressChanged == null - ? null - : () => setState(() => _isEditing = false), - child: Container( - height: 6 * s, - decoration: BoxDecoration( - color: SacredColors.outlineVariant.withValues(alpha: 0.2), - borderRadius: BorderRadius.circular(3 * s), - ), - child: FractionallySizedBox( - alignment: Alignment.centerLeft, - widthFactor: widget.progress.clamp(0.0, 1.0), - child: Container( - decoration: BoxDecoration( - color: SacredColors.primary, - borderRadius: BorderRadius.circular(3 * s), ), ), ), - ), - ); + ); + }, + ), + ), + SizedBox(width: 12 * s), + GestureDetector( + onTap: () { + _focusNode.requestFocus(); + widget.onIncrement(); }, - ), - ), - SizedBox(width: 12 * s), - GestureDetector( - onTap: () { - _focusNode.requestFocus(); - widget.onIncrement(); - }, - child: Container( - width: 36 * s, - height: 36 * s, - alignment: Alignment.center, - decoration: BoxDecoration( - color: _isEditing - ? SacredColors.surfaceContainerHigh - : SacredColors.surfaceContainerHighest, - borderRadius: BorderRadius.circular(SacredRadii.sm), - border: Border.all( + child: Container( + width: 36 * s, + height: 36 * s, + alignment: Alignment.center, + decoration: BoxDecoration( color: _isEditing - ? SacredColors.primary.withValues(alpha: 0.8) - : SacredColors.outlineVariant.withValues(alpha: 0.35), + ? SacredColors.surfaceContainerHigh + : SacredColors.surfaceContainerHighest, + borderRadius: BorderRadius.circular(SacredRadii.sm), + border: Border.all( + color: _isEditing + ? SacredColors.primary.withValues(alpha: 0.8) + : SacredColors.outlineVariant + .withValues(alpha: 0.35), + ), ), - ), - child: Text( - '→', - style: GoogleFonts.manrope( - fontSize: 16 * s, - fontWeight: FontWeight.w800, - color: SacredColors.onSurface, + child: Text( + '→', + style: GoogleFonts.manrope( + fontSize: 16 * s, + fontWeight: FontWeight.w800, + color: SacredColors.onSurface, + ), ), ), ), - ), - ], - ), - SizedBox(height: 10 * s), - Text( - _isEditing - ? 'Mode ubah aktif. Gunakan ← → lalu tekan OK untuk selesai.' - : widget.helperText, - style: GoogleFonts.manrope( - fontSize: 15 * s, - fontWeight: FontWeight.w500, - color: SacredColors.onSurfaceVariant.withValues(alpha: 0.88), + ], + ), + SizedBox(height: 10 * s), + Text( + _isEditing + ? 'Mode ubah aktif. Gunakan ← → lalu tekan OK untuk selesai.' + : widget.helperText, + style: GoogleFonts.manrope( + fontSize: 15 * s, + fontWeight: FontWeight.w500, + color: + SacredColors.onSurfaceVariant.withValues(alpha: 0.88), + ), ), - ), ], ), ), @@ -5440,8 +5645,7 @@ class _TvAdjustTileState extends State<_TvAdjustTile> { class _TvEditableTextTileState extends State<_TvEditableTextTile> { late final FocusNode _fallbackFocusNode = FocusNode(debugLabel: 'tv_edit_tile'); - late final FocusNode _textFocusNode = - FocusNode(debugLabel: 'tv_edit_text'); + late final FocusNode _textFocusNode = FocusNode(debugLabel: 'tv_edit_text'); bool _isFocused = false; bool _isEditing = false; @@ -5510,8 +5714,7 @@ class _TvEditableTextTileState extends State<_TvEditableTextTile> { key == LogicalKeyboardKey.numpadEnter || key == LogicalKeyboardKey.space || key == LogicalKeyboardKey.gameButtonA; - if (!_isEditing && - isActivateKey) { + if (!_isEditing && isActivateKey) { _startEditing(); return KeyEventResult.handled; } @@ -5545,9 +5748,7 @@ class _TvEditableTextTileState extends State<_TvEditableTextTile> { return KeyEventResult.handled; } - if (_isEditing && - widget.maxLines == 1 && - isActivateKey) { + if (_isEditing && widget.maxLines == 1 && isActivateKey) { _finishEditing(); return KeyEventResult.handled; } @@ -5638,7 +5839,8 @@ class _TvEditableTextTileState extends State<_TvEditableTextTile> { border: OutlineInputBorder( borderRadius: BorderRadius.circular(SacredRadii.md), borderSide: BorderSide( - color: SacredColors.outlineVariant.withValues(alpha: 0.5), + color: SacredColors.outlineVariant + .withValues(alpha: 0.5), ), ), focusedBorder: OutlineInputBorder( @@ -5665,7 +5867,8 @@ class _TvEditableTextTileState extends State<_TvEditableTextTile> { style: GoogleFonts.manrope( fontSize: 15 * s, fontWeight: FontWeight.w500, - color: SacredColors.onSurfaceVariant.withValues(alpha: 0.88), + color: + SacredColors.onSurfaceVariant.withValues(alpha: 0.88), ), ), ], @@ -5770,7 +5973,8 @@ class _NavButtonState extends State<_NavButton> { duration: const Duration(milliseconds: 140), curve: Curves.easeOutCubic, width: double.infinity, - padding: EdgeInsets.symmetric(horizontal: 24 * s, vertical: 24 * s), + padding: + EdgeInsets.symmetric(horizontal: 24 * s, vertical: 24 * s), decoration: BoxDecoration( color: highlight ? SacredColors.surfaceContainerLow @@ -5794,27 +5998,27 @@ class _NavButtonState extends State<_NavButton> { ), child: Row( children: [ - HugeIcon( - icon: widget.icon, - color: highlight - ? SacredColors.onSurface - : SacredColors.onSurfaceVariant, - size: 28 * s, - ), - SizedBox(width: 20 * s), - Expanded( - child: Text( - widget.title, - style: GoogleFonts.plusJakartaSans( - fontSize: 18 * s, - fontWeight: FontWeight.bold, - color: highlight - ? SacredColors.onSurface - : SacredColors.onSurfaceVariant, - letterSpacing: 1 * s, + HugeIcon( + icon: widget.icon, + color: highlight + ? SacredColors.onSurface + : SacredColors.onSurfaceVariant, + size: 28 * s, + ), + SizedBox(width: 20 * s), + Expanded( + child: Text( + widget.title, + style: GoogleFonts.plusJakartaSans( + fontSize: 18 * s, + fontWeight: FontWeight.bold, + color: highlight + ? SacredColors.onSurface + : SacredColors.onSurfaceVariant, + letterSpacing: 1 * s, + ), ), ), - ), ], ), ), diff --git a/lib/features/home/adzan_screen.dart b/lib/features/home/adzan_screen.dart index 0f6fa4c..3542c39 100644 --- a/lib/features/home/adzan_screen.dart +++ b/lib/features/home/adzan_screen.dart @@ -13,14 +13,14 @@ class AdzanAlertScreen extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final screenData = ref.watch(screenStateProvider); final clock = ref.watch(clockProvider).valueOrNull ?? DateTime.now(); - final schedule = ref.watch(todayScheduleProvider); + final schedule = ref.watch(runtimeScheduleProvider).schedule; final settings = ref.watch(settingsProvider); final size = MediaQuery.of(context).size; final s = size.width / 1920; - final prayerLabel = screenData.activePrayer - ?.displayLabel(isFriday: screenData.isFriday) ?? - ''; + final prayerLabel = + screenData.activePrayer?.displayLabel(isFriday: screenData.isFriday) ?? + ''; final timeStr = '${clock.hour.toString().padLeft(2, '0')}:${clock.minute.toString().padLeft(2, '0')}'; final secStr = clock.second.toString().padLeft(2, '0'); @@ -54,8 +54,8 @@ class AdzanAlertScreen extends ConsumerWidget { child: Center( child: Opacity( opacity: 0.03, - child: Icon(Icons.mosque, size: 500 * s, - color: SacredColors.onSurface), + child: Icon(Icons.mosque, + size: 500 * s, color: SacredColors.onSurface), ), ), ), @@ -66,8 +66,8 @@ class AdzanAlertScreen extends ConsumerWidget { left: 0, right: 0, child: Container( - padding: EdgeInsets.symmetric( - horizontal: 64 * s, vertical: 24 * s), + padding: + EdgeInsets.symmetric(horizontal: 64 * s, vertical: 24 * s), color: SacredColors.background, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -136,7 +136,8 @@ class AdzanAlertScreen extends ConsumerWidget { .withValues(alpha: 0.4), borderRadius: BorderRadius.circular(SacredRadii.full), border: Border.all( - color: SacredColors.outlineVariant.withValues(alpha: 0.15), + color: + SacredColors.outlineVariant.withValues(alpha: 0.15), ), ), child: Row( @@ -168,8 +169,8 @@ class AdzanAlertScreen extends ConsumerWidget { style: GoogleFonts.plusJakartaSans( fontSize: 48 * s, fontWeight: FontWeight.w700, - color: SacredColors.onSurface - .withValues(alpha: 0.5), + color: + SacredColors.onSurface.withValues(alpha: 0.5), ), ), Text( @@ -253,9 +254,8 @@ class AdzanAlertScreen extends ConsumerWidget { style: GoogleFonts.plusJakartaSans( fontSize: isActive ? 32 * fs : 28 * fs, fontWeight: isActive ? FontWeight.w700 : FontWeight.w600, - color: isActive - ? SacredColors.primary - : SacredColors.onSurface, + color: + isActive ? SacredColors.primary : SacredColors.onSurface, ), ), ], @@ -307,8 +307,8 @@ class _PulsingIconState extends State<_PulsingIcon> height: 200 * s, decoration: BoxDecoration( shape: BoxShape.circle, - color: SacredColors.secondary - .withValues(alpha: 0.1 * _ctrl.value), + color: + SacredColors.secondary.withValues(alpha: 0.1 * _ctrl.value), boxShadow: [ BoxShadow( blurRadius: 60 * s * _ctrl.value, diff --git a/lib/features/home/home_view.dart b/lib/features/home/home_view.dart index b18ac9e..9fa1e42 100644 --- a/lib/features/home/home_view.dart +++ b/lib/features/home/home_view.dart @@ -93,12 +93,14 @@ class _HomeViewState extends ConsumerState { if (result.synced) { debugPrint('[AutoSync] Cache refreshed successfully.'); ref.invalidate(todayScheduleProvider); + ref.invalidate(runtimeScheduleProvider); ref.invalidate(scheduleCacheStatusProvider); return; } if (result.attempted) { - debugPrint('[AutoSync] Refresh attempt failed. Staying on local cache.'); + debugPrint( + '[AutoSync] Refresh attempt failed. Staying on local cache.'); } } finally { _isAutoRefreshRunning = false; @@ -211,7 +213,10 @@ class _HomeViewState extends ConsumerState { BackgroundRotateAction? _matchManualRotateSequence() { if (_recentKeys.length < 3) return null; - final tail = _recentKeys.sublist(_recentKeys.length - 3).map(_normalizedComboKey).toList(); + final tail = _recentKeys + .sublist(_recentKeys.length - 3) + .map(_normalizedComboKey) + .toList(); if (tail[0] == LogicalKeyboardKey.arrowRight && tail[1] == LogicalKeyboardKey.arrowRight && tail[2] == LogicalKeyboardKey.select) { @@ -294,14 +299,17 @@ class _HomeViewState extends ConsumerState { if (previous == null) return; // TRIGGER 1: Adzan Beep (Fires precisely when transitioning to Adzan) - if (previous.state != ScreenState.adzan && next.state == ScreenState.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) { + 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) { + if (previous.iqomahRemaining?.inSeconds != 3 && + next.iqomahRemaining!.inSeconds == 3) { SoundService.instance.playIqomahCountdown(); } } @@ -319,8 +327,8 @@ class _HomeViewState extends ConsumerState { if (screenData.isFriday && screenData.nextPrayer?.id == 'dzuhur') { screen = const JumatScreen(key: ValueKey('jumat')); } else { - screen = isMainScreen - ? const MainScreen(key: ValueKey('main')) + screen = isMainScreen + ? const MainScreen(key: ValueKey('main')) : SlideshowScreen(key: ValueKey('slideshow-$rotationIndex')); } break; diff --git a/lib/features/home/jumat_screen.dart b/lib/features/home/jumat_screen.dart index 2d22396..bb124f9 100644 --- a/lib/features/home/jumat_screen.dart +++ b/lib/features/home/jumat_screen.dart @@ -21,7 +21,7 @@ class JumatScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final clock = ref.watch(clockProvider).valueOrNull ?? DateTime.now(); - final schedule = ref.watch(todayScheduleProvider); + final schedule = ref.watch(runtimeScheduleProvider).schedule; final settings = ref.watch(settingsProvider); final screenData = ref.watch(screenStateProvider); final size = MediaQuery.of(context).size; @@ -31,8 +31,8 @@ class JumatScreen extends ConsumerWidget { final timeStr = DateFormat('HH:mm').format(clock); final secStr = DateFormat(':ss').format(clock); final dateGregorian = DateFormat('EEEE, d MMMM yyyy', 'en').format(clock); - final dateHijri = - ref.watch(hijriDateProvider).valueOrNull ?? HijriDateFormatter.format(clock); + final dateHijri = ref.watch(hijriDateProvider).valueOrNull ?? + HijriDateFormatter.format(clock); final durToKhutbah = screenData.timeUntilNext ?? const Duration(minutes: 0); final minToKhutbah = durToKhutbah.inMinutes; @@ -42,7 +42,8 @@ class JumatScreen extends ConsumerWidget { child: Stack( children: [ // ── Underlay: Branded local image or Unsplash ── - if (settings.brandedBgImage != null && settings.brandedBgImage!.isNotEmpty) + if (settings.brandedBgImage != null && + settings.brandedBgImage!.isNotEmpty) Positioned.fill( child: Image.file( File(settings.brandedBgImage!), @@ -99,16 +100,20 @@ class JumatScreen extends ConsumerWidget { children: [ // Pill Container( - padding: EdgeInsets.symmetric(horizontal: 24 * s, vertical: 8 * s), + padding: EdgeInsets.symmetric( + horizontal: 24 * s, vertical: 8 * s), decoration: BoxDecoration( color: SacredColors.secondary.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(SacredRadii.full), - border: Border.all(color: SacredColors.secondary.withValues(alpha: 0.2)), + border: Border.all( + color: SacredColors.secondary + .withValues(alpha: 0.2)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ - _PulsingDot(color: SacredColors.secondary, size: 12 * s), + _PulsingDot( + color: SacredColors.secondary, size: 12 * s), SizedBox(width: 12 * s), Text( 'PERSIAPAN JUMAT', @@ -122,9 +127,9 @@ class JumatScreen extends ConsumerWidget { ], ), ), - + SizedBox(height: 16 * s), - + // Massive Clock Row( crossAxisAlignment: CrossAxisAlignment.start, @@ -146,15 +151,16 @@ class JumatScreen extends ConsumerWidget { style: GoogleFonts.plusJakartaSans( fontSize: 48 * s, fontWeight: FontWeight.w700, - color: SacredColors.primary.withValues(alpha: 0.7), + color: + SacredColors.primary.withValues(alpha: 0.7), ), ), ), ], ), - + SizedBox(height: 16 * s), - + // Dates Text( dateGregorian, @@ -176,16 +182,18 @@ class JumatScreen extends ConsumerWidget { ], ), ), - + // ── Right Column: Khutbah Info Card ── Expanded( flex: 1, child: Container( padding: EdgeInsets.all(40 * s), decoration: BoxDecoration( - color: SacredColors.surfaceContainerHigh.withValues(alpha: 0.6), + color: SacredColors.surfaceContainerHigh + .withValues(alpha: 0.6), borderRadius: BorderRadius.circular(SacredRadii.xl), - border: Border.all(color: Colors.white.withValues(alpha: 0.05)), + border: Border.all( + color: Colors.white.withValues(alpha: 0.05)), ), child: ClipRRect( borderRadius: BorderRadius.circular(SacredRadii.xl), @@ -207,7 +215,10 @@ class JumatScreen extends ConsumerWidget { letterSpacing: 3 * s, ), ), - HugeIcon(icon: HugeIcons.strokeRoundedSparkles, color: SacredColors.secondary, size: 24 * s), + HugeIcon( + icon: HugeIcons.strokeRoundedSparkles, + color: SacredColors.secondary, + size: 24 * s), ], ), SizedBox(height: 16 * s), @@ -221,25 +232,25 @@ class JumatScreen extends ConsumerWidget { ), ), SizedBox(height: 32 * s), - + // Khatib Info - _buildInfoTile( - s, - icon: Icons.person_pin, - color: SacredColors.primary, - label: 'KHATIB HARI INI', - value: settings.khatibName.isEmpty ? 'Belum Diatur' : settings.khatibName - ), + _buildInfoTile(s, + icon: Icons.person_pin, + color: SacredColors.primary, + label: 'KHATIB HARI INI', + value: settings.khatibName.isEmpty + ? 'Belum Diatur' + : settings.khatibName), SizedBox(height: 24 * s), - + // Countdown Info - _buildInfoTile( - s, - icon: Icons.timer, - color: SacredColors.secondary, - label: 'KHUTBAH DIMULAI DALAM', - value: minToKhutbah > 0 ? '~ $minToKhutbah Menit' : 'Sebentar Lagi' - ), + _buildInfoTile(s, + icon: Icons.timer, + color: SacredColors.secondary, + label: 'KHUTBAH DIMULAI DALAM', + value: minToKhutbah > 0 + ? '~ $minToKhutbah Menit' + : 'Sebentar Lagi'), ], ), ), @@ -277,7 +288,8 @@ class JumatScreen extends ConsumerWidget { children: [ // Mosque Name GestureDetector( - onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (_) => const AdminScreen())), + onTap: () => Navigator.of(context) + .push(MaterialPageRoute(builder: (_) => const AdminScreen())), child: Text( settings.masjidName, style: GoogleFonts.plusJakartaSans( @@ -290,7 +302,10 @@ class JumatScreen extends ConsumerWidget { // Mosque Address Row( children: [ - HugeIcon(icon: HugeIcons.strokeRoundedMosque01, color: SacredColors.primary, size: 24 * s), + HugeIcon( + icon: HugeIcons.strokeRoundedMosque01, + color: SacredColors.primary, + size: 24 * s), SizedBox(width: 8 * s), Text( settings.masjidAddress, @@ -306,7 +321,11 @@ class JumatScreen extends ConsumerWidget { ); } - Widget _buildInfoTile(double s, {required IconData icon, required Color color, required String label, required String value}) { + Widget _buildInfoTile(double s, + {required IconData icon, + required Color color, + required String label, + required String value}) { return Container( padding: EdgeInsets.all(20 * s), decoration: BoxDecoration( @@ -373,32 +392,44 @@ class JumatScreen extends ConsumerWidget { mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _buildTimeItem(s, 'Fajr', schedule.subuh, Icons.brightness_3, false), - _buildTimeItem(s, 'Terbit', schedule.terbit, Icons.wb_twilight, false), + _buildTimeItem( + s, 'Terbit', schedule.terbit, Icons.wb_twilight, false), _buildTimeItem(s, 'JUMAT', schedule.dzuhur, Icons.wb_sunny, true), _buildTimeItem(s, 'Asr', schedule.ashar, Icons.sunny_snowing, false), - _buildTimeItem(s, 'Maghrib', schedule.maghrib, Icons.wb_cloudy, false), + _buildTimeItem( + s, 'Maghrib', schedule.maghrib, Icons.wb_cloudy, false), _buildTimeItem(s, 'Isha', schedule.isya, Icons.bedtime, false), ], ), ); } - Widget _buildTimeItem(double s, String name, String time, IconData icon, bool isJumat) { + Widget _buildTimeItem( + double s, String name, String time, IconData icon, bool isJumat) { if (isJumat) { return Container( padding: EdgeInsets.symmetric(horizontal: 48 * s, vertical: 12 * s), decoration: BoxDecoration( color: SacredColors.primary.withValues(alpha: 0.15), - border: Border.all(color: SacredColors.primary.withValues(alpha: 0.3)), + border: + Border.all(color: SacredColors.primary.withValues(alpha: 0.3)), borderRadius: BorderRadius.circular(SacredRadii.xl), ), child: Column( children: [ Icon(icon, color: SacredColors.primary, size: 28 * s), SizedBox(height: 8 * s), - Text(name, style: GoogleFonts.manrope(fontSize: 18 * s, fontWeight: FontWeight.w800, color: SacredColors.primary)), + Text(name, + style: GoogleFonts.manrope( + fontSize: 18 * s, + fontWeight: FontWeight.w800, + color: SacredColors.primary)), SizedBox(height: 4 * s), - Text(time, style: GoogleFonts.plusJakartaSans(fontSize: 16 * s, fontWeight: FontWeight.w700, color: SacredColors.primary)), + Text(time, + style: GoogleFonts.plusJakartaSans( + fontSize: 16 * s, + fontWeight: FontWeight.w700, + color: SacredColors.primary)), ], ), ); @@ -407,11 +438,21 @@ class JumatScreen extends ConsumerWidget { padding: EdgeInsets.symmetric(horizontal: 24 * s, vertical: 8 * s), child: Column( children: [ - Icon(icon, color: SacredColors.onSurface.withValues(alpha: 0.6), size: 24 * s), + Icon(icon, + color: SacredColors.onSurface.withValues(alpha: 0.6), + size: 24 * s), SizedBox(height: 8 * s), - Text(name, style: GoogleFonts.manrope(fontSize: 16 * s, fontWeight: FontWeight.w500, color: SacredColors.onSurface.withValues(alpha: 0.6))), + Text(name, + style: GoogleFonts.manrope( + fontSize: 16 * s, + fontWeight: FontWeight.w500, + color: SacredColors.onSurface.withValues(alpha: 0.6))), SizedBox(height: 4 * s), - Text(time, style: GoogleFonts.plusJakartaSans(fontSize: 16 * s, fontWeight: FontWeight.w700, color: SacredColors.onSurface)), + Text(time, + style: GoogleFonts.plusJakartaSans( + fontSize: 16 * s, + fontWeight: FontWeight.w700, + color: SacredColors.onSurface)), ], ), ); @@ -419,8 +460,10 @@ class JumatScreen extends ConsumerWidget { Widget _buildMarquee(double s, double fs, AppSettings settings) { // Quick custom simplified marquee or fallback to settings.runningTexts - final texts = settings.runningTexts.isEmpty - ? ["JUMAT MUBARAK: Luruskan dan rapatkan shaf. Harap non-aktifkan alat komunikasi."] + final texts = settings.runningTexts.isEmpty + ? [ + "JUMAT MUBARAK: Luruskan dan rapatkan shaf. Harap non-aktifkan alat komunikasi." + ] : settings.runningTexts; return Container( @@ -448,29 +491,45 @@ class _PulsingDot extends StatefulWidget { State<_PulsingDot> createState() => _PulsingDotState(); } -class _PulsingDotState extends State<_PulsingDot> with SingleTickerProviderStateMixin { +class _PulsingDotState extends State<_PulsingDot> + with SingleTickerProviderStateMixin { late AnimationController _ctrl; @override void initState() { super.initState(); - _ctrl = AnimationController(vsync: this, duration: const Duration(milliseconds: 1200))..repeat(); + _ctrl = AnimationController( + vsync: this, duration: const Duration(milliseconds: 1200)) + ..repeat(); } + @override - void dispose() { _ctrl.dispose(); super.dispose(); } + void dispose() { + _ctrl.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return SizedBox( - width: widget.size, height: widget.size, + width: widget.size, + height: widget.size, child: Stack( children: [ FadeTransition( opacity: Tween(begin: 0.75, end: 0.0).animate(_ctrl), child: ScaleTransition( scale: Tween(begin: 1.0, end: 2.0).animate(_ctrl), - child: Container(decoration: BoxDecoration(shape: BoxShape.circle, color: widget.color)), + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, color: widget.color)), ), ), - Center(child: Container(width: widget.size, height: widget.size, decoration: BoxDecoration(shape: BoxShape.circle, color: widget.color))), + Center( + child: Container( + width: widget.size, + height: widget.size, + decoration: BoxDecoration( + shape: BoxShape.circle, color: widget.color))), ], ), ); @@ -485,15 +544,23 @@ class _JumatMarquee extends StatefulWidget { State<_JumatMarquee> createState() => _JumatMarqueeState(); } -class _JumatMarqueeState extends State<_JumatMarquee> with TickerProviderStateMixin { +class _JumatMarqueeState extends State<_JumatMarquee> + with TickerProviderStateMixin { late AnimationController _ctrl; @override void initState() { super.initState(); - _ctrl = AnimationController(vsync: this, duration: const Duration(seconds: 30))..repeat(); + _ctrl = + AnimationController(vsync: this, duration: const Duration(seconds: 30)) + ..repeat(); } + @override - void dispose() { _ctrl.dispose(); super.dispose(); } + void dispose() { + _ctrl.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final joined = widget.texts.join(" • "); @@ -503,30 +570,35 @@ class _JumatMarqueeState extends State<_JumatMarquee> with TickerProviderStateMi color: SacredColors.secondary, letterSpacing: 2 * widget.s, ); - - return LayoutBuilder( - builder: (context, constraints) { - return Stack( - children: [ - AnimatedBuilder( - animation: _ctrl, - builder: (ctx, child) { - // Ensure endless scroll mathematically - return Positioned( - left: -(_ctrl.value * constraints.maxWidth), - top: 0, bottom: 0, - child: Row( - children: [ - Container(alignment: Alignment.centerLeft, width: constraints.maxWidth, child: Text(joined, style: style, maxLines: 1)), - Container(alignment: Alignment.centerLeft, width: constraints.maxWidth, child: Text(joined, style: style, maxLines: 1)), - ], - ), - ); - }, - ), - ], - ); - } - ); + + return LayoutBuilder(builder: (context, constraints) { + return Stack( + children: [ + AnimatedBuilder( + animation: _ctrl, + builder: (ctx, child) { + // Ensure endless scroll mathematically + return Positioned( + left: -(_ctrl.value * constraints.maxWidth), + top: 0, + bottom: 0, + child: Row( + children: [ + Container( + alignment: Alignment.centerLeft, + width: constraints.maxWidth, + child: Text(joined, style: style, maxLines: 1)), + Container( + alignment: Alignment.centerLeft, + width: constraints.maxWidth, + child: Text(joined, style: style, maxLines: 1)), + ], + ), + ); + }, + ), + ], + ); + }); } } diff --git a/lib/features/home/main_screen.dart b/lib/features/home/main_screen.dart index 348a730..5549306 100644 --- a/lib/features/home/main_screen.dart +++ b/lib/features/home/main_screen.dart @@ -28,7 +28,8 @@ class MainScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final clock = ref.watch(clockProvider).valueOrNull ?? DateTime.now(); - final schedule = ref.watch(todayScheduleProvider); + final runtimeSchedule = ref.watch(runtimeScheduleProvider); + final schedule = runtimeSchedule.schedule; final settings = ref.watch(settingsProvider); final screenData = ref.watch(screenStateProvider); final size = MediaQuery.of(context).size; @@ -118,6 +119,8 @@ class MainScreen extends ConsumerWidget { settings, dateGregorian, dateHijri, + showWaitingUpdateNotice: + runtimeSchedule.isFallbackFromPreviousDay, inlineClockText: centerSlide.isPrimary ? null : '$timeStr$secStr', ), @@ -166,7 +169,7 @@ class MainScreen extends ConsumerWidget { Widget _buildHeader(BuildContext context, double s, double fs, AppSettings settings, String dateGregorian, String dateHijri, - {String? inlineClockText}) { + {required bool showWaitingUpdateNotice, String? inlineClockText}) { final hScale = settings.scaleTopHeader; final showInlineClock = inlineClockText != null && inlineClockText.isNotEmpty; @@ -252,6 +255,37 @@ class MainScreen extends ConsumerWidget { ), ), + if (showWaitingUpdateNotice) + Expanded( + flex: 2, + child: Align( + alignment: Alignment.centerRight, + child: Container( + margin: EdgeInsets.only(right: 16 * s), + padding: EdgeInsets.symmetric( + horizontal: 18 * s, + vertical: 9 * s, + ), + decoration: BoxDecoration( + color: SacredColors.secondary.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(SacredRadii.full), + border: Border.all( + color: SacredColors.secondary.withValues(alpha: 0.7), + ), + ), + child: Text( + 'Menunggu Update Data', + style: GoogleFonts.plusJakartaSans( + fontSize: 16 * s * hScale, + fontWeight: FontWeight.w700, + color: SacredColors.secondary, + letterSpacing: 0.3 * s, + ), + ), + ), + ), + ), + // Right: Hijri date + mosque icon Expanded( flex: 3, diff --git a/lib/main.dart b/lib/main.dart index afc8125..ef8c509 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -13,15 +13,35 @@ import 'core/sacred_tokens.dart'; import 'data/local/models.dart'; import 'features/home/home_view.dart'; +const String _duplicateKeyDownAssertion = + 'A KeyDownEvent is dispatched, but the state shows that the physical key is already pressed.'; + +bool _isIgnorableKeyboardAssertion(Object error, [StackTrace? stack]) { + final text = error.toString(); + if (!text.contains(_duplicateKeyDownAssertion)) return false; + final stackText = stack?.toString() ?? ''; + return stackText.isEmpty || stackText.contains('hardware_keyboard.dart'); +} + +bool _isIgnorableFlutterAssertion(FlutterErrorDetails details) { + return _isIgnorableKeyboardAssertion(details.exception, details.stack); +} + void main() { runZonedGuarded(() async { WidgetsFlutterBinding.ensureInitialized(); FlutterError.onError = (details) { + if (_isIgnorableFlutterAssertion(details)) { + return; + } FlutterError.presentError(details); debugPrint('[Fatal][FlutterError] ${details.exceptionAsString()}'); }; PlatformDispatcher.instance.onError = (error, stack) { + if (_isIgnorableKeyboardAssertion(error, stack)) { + return true; + } debugPrint('[Fatal][PlatformDispatcher] $error'); debugPrintStack(stackTrace: stack); return true; @@ -104,8 +124,7 @@ Future _sanitizeMediaSettings(Box settingsBox) async { ? settings.brandedBgImage : null; - final needsSave = - validSlides.length != settings.slideshowImages.length || + final needsSave = validSlides.length != settings.slideshowImages.length || brandedBg != settings.brandedBgImage; if (!needsSave) return; diff --git a/lib/providers.dart b/lib/providers.dart index 6e879e7..79a488d 100644 --- a/lib/providers.dart +++ b/lib/providers.dart @@ -29,8 +29,7 @@ class BackgroundRotateCommand { action = BackgroundRotateAction.random; } -final backgroundRotateCommandProvider = - StateProvider( +final backgroundRotateCommandProvider = StateProvider( (ref) => const BackgroundRotateCommand.initial(), ); @@ -101,6 +100,44 @@ final todayScheduleProvider = Provider((ref) { return SyncService.instance.getTodaySchedule(clock); }); +class RuntimeScheduleData { + final DailyPrayerSchedule? schedule; + final bool isFallbackFromPreviousDay; + + const RuntimeScheduleData({ + required this.schedule, + this.isFallbackFromPreviousDay = false, + }); + + const RuntimeScheduleData.empty() + : schedule = null, + isFallbackFromPreviousDay = false; +} + +/// Runtime schedule used by main display/state machine. +/// If today's data is missing after midnight, we temporarily fallback to yesterday +/// so screen remains operational while showing a waiting-for-update notice. +final runtimeScheduleProvider = Provider((ref) { + final clock = ref.watch(clockProvider).valueOrNull; + if (clock == null) return const RuntimeScheduleData.empty(); + + final today = SyncService.instance.getTodaySchedule(clock); + if (today != null) { + return RuntimeScheduleData(schedule: today); + } + + final previousDay = clock.subtract(const Duration(days: 1)); + final fallback = SyncService.instance.getTodaySchedule(previousDay); + if (fallback != null) { + return RuntimeScheduleData( + schedule: fallback, + isFallbackFromPreviousDay: true, + ); + } + + return const RuntimeScheduleData.empty(); +}); + final scheduleCacheStatusProvider = Provider((ref) { final clock = ref.watch(clockProvider).valueOrNull ?? DateTime.now(); return SyncService.instance.getCacheStatus(clock); @@ -149,7 +186,7 @@ class ScreenStateData { final screenStateProvider = Provider((ref) { final clock = ref.watch(clockProvider).valueOrNull ?? DateTime.now(); - final schedule = ref.watch(todayScheduleProvider); + final schedule = ref.watch(runtimeScheduleProvider).schedule; final settings = ref.watch(settingsProvider); final isFriday = clock.weekday == DateTime.friday; diff --git a/pubspec.yaml b/pubspec.yaml index ff4adb5..398236d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: jamshalat_masjid_screen description: Smart Digital Prayer Clock for Android TV Box publish_to: 'none' -version: 1.0.14+15 +version: 1.0.15+16 environment: sdk: '>=3.0.0 <4.0.0'