diff --git a/lib/features/admin/admin_screen.dart b/lib/features/admin/admin_screen.dart index 3da56b8..b406503 100644 --- a/lib/features/admin/admin_screen.dart +++ b/lib/features/admin/admin_screen.dart @@ -543,17 +543,20 @@ class _AdminScreenState extends ConsumerState { SizedBox(height: 24 * s), ], - ElevatedButton.icon( - onPressed: _saveJumat, - style: ElevatedButton.styleFrom( - backgroundColor: SacredColors.secondary, - foregroundColor: Colors.black, - padding: EdgeInsets.symmetric(horizontal: 40 * s, vertical: 20 * s), - textStyle: TextStyle(fontSize: 18 * s, fontWeight: FontWeight.bold), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(SacredRadii.lg)), + _tvActionButton( + s: s, + child: ElevatedButton.icon( + onPressed: _saveJumat, + style: ElevatedButton.styleFrom( + backgroundColor: SacredColors.secondary, + foregroundColor: Colors.black, + padding: EdgeInsets.symmetric(horizontal: 40 * s, vertical: 20 * s), + textStyle: TextStyle(fontSize: 18 * s, fontWeight: FontWeight.bold), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(SacredRadii.lg)), + ), + icon: const Icon(Icons.save_rounded), + label: const Text('SIMPAN DATA JUMAT'), ), - icon: const Icon(Icons.save_rounded), - label: const Text('SIMPAN DATA JUMAT'), ), ], )), @@ -625,16 +628,19 @@ class _AdminScreenState extends ConsumerState { children: [ _sectionLabel('Tipografi & Skala Teks', s), SizedBox(height: 12 * s), - SegmentedButton( - segments: const [ - ButtonSegment(value: 0, label: Text('Kecil')), - ButtonSegment(value: 1, label: Text('Normal')), - ButtonSegment(value: 2, label: Text('Besar')), - ], - selected: {_textScaleIndex}, - onSelectionChanged: (val) { - setState(() => _textScaleIndex = val.first); - }, + _buildSegmentedControl( + s: s, + child: SegmentedButton( + segments: const [ + ButtonSegment(value: 0, label: Text('Kecil')), + ButtonSegment(value: 1, label: Text('Normal')), + ButtonSegment(value: 2, label: Text('Besar')), + ], + selected: {_textScaleIndex}, + onSelectionChanged: (val) { + setState(() => _textScaleIndex = val.first); + }, + ), ), SizedBox(height: 28 * s), _buildTvIntStepperField( @@ -692,12 +698,11 @@ class _AdminScreenState extends ConsumerState { _sectionLabel('Background Layar Utama (Unsplash)', s), SizedBox(height: 12 * s), - SwitchListTile( - title: Text('Gunakan Foto Unsplash API', style: GoogleFonts.plusJakartaSans(fontSize: 18 * s, color: SacredColors.onSurface)), + _buildSwitchTile( + s: s, + title: 'Gunakan Foto Unsplash API', value: _useUnsplash, onChanged: (val) => setState(() => _useUnsplash = val), - activeThumbColor: SacredColors.primary, - contentPadding: EdgeInsets.zero, ), if (_useUnsplash) ...[ SizedBox(height: 12 * s), @@ -715,17 +720,20 @@ class _AdminScreenState extends ConsumerState { ], SizedBox(height: 56 * s), - ElevatedButton.icon( - onPressed: _saveTampilan, - style: ElevatedButton.styleFrom( - backgroundColor: SacredColors.primary, - foregroundColor: SacredColors.onPrimary, - padding: EdgeInsets.symmetric(horizontal: 48 * s, vertical: 24 * s), - textStyle: TextStyle(fontSize: 18 * s, fontWeight: FontWeight.bold), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(SacredRadii.lg)), + _tvActionButton( + s: s, + child: ElevatedButton.icon( + onPressed: _saveTampilan, + style: ElevatedButton.styleFrom( + backgroundColor: SacredColors.primary, + foregroundColor: SacredColors.onPrimary, + padding: EdgeInsets.symmetric(horizontal: 48 * s, vertical: 24 * s), + textStyle: TextStyle(fontSize: 18 * s, fontWeight: FontWeight.bold), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(SacredRadii.lg)), + ), + icon: HugeIcon(icon: HugeIcons.strokeRoundedFloppyDisk, color: SacredColors.onPrimary), + label: const Text('SIMPAN TAMPILAN'), ), - icon: HugeIcon(icon: HugeIcons.strokeRoundedFloppyDisk, color: SacredColors.onPrimary), - label: const Text('SIMPAN TAMPILAN'), ), ], )), @@ -775,20 +783,23 @@ class _AdminScreenState extends ConsumerState { ] else Text('Belum ada foto latar masjid.', style: GoogleFonts.manrope(fontSize: 16 * s, color: SacredColors.onSurfaceVariant)), SizedBox(height: 16 * s), - ElevatedButton.icon( - onPressed: () async { - final res = await FilePicker.platform.pickFiles(type: FileType.image); - if (res != null && res.files.single.path != null) { - setState(() => _brandedBgImage = res.files.single.path); - _saveTampilan(); - } - }, - icon: HugeIcon(icon: HugeIcons.strokeRoundedImage01, color: SacredColors.onPrimary, size: 20 * s), - label: Text('PILIH FOTO MASJID', style: TextStyle(fontSize: 16 * s)), - style: ElevatedButton.styleFrom( - backgroundColor: SacredColors.secondary, - foregroundColor: SacredColors.onSecondary, - padding: EdgeInsets.symmetric(horizontal: 24 * s, vertical: 16 * s), + _tvActionButton( + s: s, + child: ElevatedButton.icon( + onPressed: () async { + final res = await FilePicker.platform.pickFiles(type: FileType.image); + if (res != null && res.files.single.path != null) { + setState(() => _brandedBgImage = res.files.single.path); + _saveTampilan(); + } + }, + icon: HugeIcon(icon: HugeIcons.strokeRoundedImage01, color: SacredColors.onPrimary, size: 20 * s), + label: Text('PILIH FOTO MASJID', style: TextStyle(fontSize: 16 * s)), + style: ElevatedButton.styleFrom( + backgroundColor: SacredColors.secondary, + foregroundColor: SacredColors.onSecondary, + padding: EdgeInsets.symmetric(horizontal: 24 * s, vertical: 16 * s), + ), ), ), ], @@ -803,26 +814,29 @@ class _AdminScreenState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _sectionLabel('Galeri Gambar Slideshow', s), - ElevatedButton.icon( - onPressed: () async { - final res = await FilePicker.platform.pickFiles(type: FileType.image, allowMultiple: true); - if (res != null) { - setState(() { - for (var path in res.paths) { - if (path != null && !_slideshowImages.contains(path)) { - _slideshowImages.add(path); + _tvActionButton( + s: s, + child: ElevatedButton.icon( + onPressed: () async { + final res = await FilePicker.platform.pickFiles(type: FileType.image, allowMultiple: true); + if (res != null) { + setState(() { + for (var path in res.paths) { + if (path != null && !_slideshowImages.contains(path)) { + _slideshowImages.add(path); + } } - } - }); - _saveTampilan(); - } - }, - icon: HugeIcon(icon: HugeIcons.strokeRoundedPlusSign, color: SacredColors.onSecondary, size: 18 * s), - label: Text('TAMBAH FOTO', style: TextStyle(fontSize: 14 * s)), - style: ElevatedButton.styleFrom( - backgroundColor: SacredColors.secondary, - foregroundColor: SacredColors.onSecondary, - padding: EdgeInsets.symmetric(horizontal: 20 * s, vertical: 14 * s), + }); + _saveTampilan(); + } + }, + icon: HugeIcon(icon: HugeIcons.strokeRoundedPlusSign, color: SacredColors.onSecondary, size: 18 * s), + label: Text('TAMBAH FOTO', style: TextStyle(fontSize: 14 * s)), + style: ElevatedButton.styleFrom( + backgroundColor: SacredColors.secondary, + foregroundColor: SacredColors.onSecondary, + padding: EdgeInsets.symmetric(horizontal: 20 * s, vertical: 14 * s), + ), ), ), ], @@ -876,18 +890,21 @@ class _AdminScreenState extends ConsumerState { children: [ Text('Mode Animasi:', style: GoogleFonts.manrope(fontSize: 16 * s, color: SacredColors.onSurfaceVariant)), SizedBox(width: 12 * s), - SegmentedButton( - segments: [ - ButtonSegment(value: 'marquee', label: Text('Marquee', style: GoogleFonts.manrope(fontSize: 16 * s))), - ButtonSegment(value: 'fade', label: Text('Fade In-Out', style: GoogleFonts.manrope(fontSize: 16 * s))), - ], - selected: {_marqueeAnimType}, - onSelectionChanged: (val) => setState(() => _marqueeAnimType = val.first), - style: ButtonStyle( - backgroundColor: WidgetStateProperty.resolveWith((states) => - states.contains(WidgetState.selected) ? SacredColors.primary : SacredColors.surfaceContainerLowest), - foregroundColor: WidgetStateProperty.resolveWith((states) => - states.contains(WidgetState.selected) ? SacredColors.onPrimary : SacredColors.onSurfaceVariant), + _buildSegmentedControl( + s: s, + child: SegmentedButton( + segments: [ + ButtonSegment(value: 'marquee', label: Text('Marquee', style: GoogleFonts.manrope(fontSize: 16 * s))), + ButtonSegment(value: 'fade', label: Text('Fade In-Out', style: GoogleFonts.manrope(fontSize: 16 * s))), + ], + selected: {_marqueeAnimType}, + onSelectionChanged: (val) => setState(() => _marqueeAnimType = val.first), + style: ButtonStyle( + backgroundColor: WidgetStateProperty.resolveWith((states) => + states.contains(WidgetState.selected) ? SacredColors.primary : SacredColors.surfaceContainerLowest), + foregroundColor: WidgetStateProperty.resolveWith((states) => + states.contains(WidgetState.selected) ? SacredColors.onPrimary : SacredColors.onSurfaceVariant), + ), ), ), ], @@ -986,30 +1003,36 @@ class _AdminScreenState extends ConsumerState { SizedBox(height: 20 * s), Row( children: [ - OutlinedButton.icon( - onPressed: () { - setState(() { - _runningTexts.add(''); - _runningTextDurations.add(12); - }); - }, - icon: HugeIcon(icon: HugeIcons.strokeRoundedPlusSign, color: SacredColors.primary, size: 20 * s), - label: Text('TAMBAH BARIS', style: GoogleFonts.plusJakartaSans(fontSize: 16 * s, color: SacredColors.primary)), - style: OutlinedButton.styleFrom( - side: BorderSide(color: SacredColors.primary.withValues(alpha: 0.5)), - padding: EdgeInsets.symmetric(horizontal: 24 * s, vertical: 16 * s), + _tvActionButton( + s: s, + child: OutlinedButton.icon( + onPressed: () { + setState(() { + _runningTexts.add(''); + _runningTextDurations.add(12); + }); + }, + icon: HugeIcon(icon: HugeIcons.strokeRoundedPlusSign, color: SacredColors.primary, size: 20 * s), + label: Text('TAMBAH BARIS', style: GoogleFonts.plusJakartaSans(fontSize: 16 * s, color: SacredColors.primary)), + style: OutlinedButton.styleFrom( + side: BorderSide(color: SacredColors.primary.withValues(alpha: 0.5)), + padding: EdgeInsets.symmetric(horizontal: 24 * s, vertical: 16 * s), + ), ), ), SizedBox(width: 16 * s), - ElevatedButton.icon( - onPressed: _saveTampilan, - style: ElevatedButton.styleFrom( - backgroundColor: SacredColors.primary, - foregroundColor: SacredColors.onPrimary, - padding: EdgeInsets.symmetric(horizontal: 32 * s, vertical: 16 * s), + _tvActionButton( + s: s, + child: ElevatedButton.icon( + onPressed: _saveTampilan, + style: ElevatedButton.styleFrom( + backgroundColor: SacredColors.primary, + foregroundColor: SacredColors.onPrimary, + padding: EdgeInsets.symmetric(horizontal: 32 * s, vertical: 16 * s), + ), + icon: HugeIcon(icon: HugeIcons.strokeRoundedFloppyDisk, color: SacredColors.onPrimary, size: 18 * s), + label: Text('SIMPAN TEKS', style: GoogleFonts.plusJakartaSans(fontSize: 16 * s, fontWeight: FontWeight.bold)), ), - icon: HugeIcon(icon: HugeIcons.strokeRoundedFloppyDisk, color: SacredColors.onPrimary, size: 18 * s), - label: Text('SIMPAN TEKS', style: GoogleFonts.plusJakartaSans(fontSize: 16 * s, fontWeight: FontWeight.bold)), ), ], ), @@ -1025,13 +1048,126 @@ class _AdminScreenState extends ConsumerState { Widget _adminCard(double s, {required Widget child}) { return _scrollAware( controller: _scrollControllerForTab(_selectedTab), + child: _TvFocusFrame( + scale: s, + borderRadius: BorderRadius.circular(SacredRadii.xl), + child: Container( + width: double.infinity, + padding: EdgeInsets.all(36 * s), + decoration: BoxDecoration( + color: SacredColors.surfaceContainerHighest.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(SacredRadii.xl), + border: Border.all(color: SacredColors.outlineVariant.withValues(alpha: 0.2)), + ), + child: child, + ), + ), + ); + } + + Widget _tvFocusable({ + required Widget child, + required double s, + double radius = SacredRadii.md, + bool scrollAware = true, + }) { + final framed = _TvFocusFrame( + scale: s, + borderRadius: BorderRadius.circular(radius), + child: child, + ); + + if (!scrollAware) return framed; + + return _scrollAware( + controller: _scrollControllerForTab(_selectedTab), + child: framed, + ); + } + + Widget _tvActionButton({ + required Widget child, + required double s, + double radius = SacredRadii.lg, + }) { + return _tvFocusable( + child: child, + s: s, + radius: radius, + scrollAware: true, + ); + } + + Widget _buildReadonlyField(TextEditingController controller, double s) { + return _tvFocusable( + s: s, + child: TextField( + controller: controller, + readOnly: true, + style: GoogleFonts.plusJakartaSans( + fontSize: 24 * s, + color: SacredColors.onSurface, + ), + decoration: InputDecoration( + filled: true, + fillColor: SacredColors.surfaceContainerLowest, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(SacredRadii.md), + borderSide: BorderSide.none, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(SacredRadii.md), + borderSide: const BorderSide(color: SacredColors.primary, width: 2), + ), + ), + ), + ); + } + + Widget _buildSwitchTile({ + required double s, + required String title, + required bool value, + required ValueChanged onChanged, + }) { + return _tvFocusable( + s: s, + radius: SacredRadii.md, child: Container( - width: double.infinity, - padding: EdgeInsets.all(36 * s), decoration: BoxDecoration( - color: SacredColors.surfaceContainerHighest.withValues(alpha: 0.3), - borderRadius: BorderRadius.circular(SacredRadii.xl), - border: Border.all(color: SacredColors.outlineVariant.withValues(alpha: 0.2)), + color: SacredColors.surfaceContainerLowest, + borderRadius: BorderRadius.circular(SacredRadii.md), + ), + child: SwitchListTile( + title: Text( + title, + style: GoogleFonts.plusJakartaSans( + fontSize: 18 * s, + color: SacredColors.onSurface, + ), + ), + value: value, + onChanged: onChanged, + activeThumbColor: SacredColors.primary, + contentPadding: EdgeInsets.symmetric(horizontal: 12 * s), + ), + ), + ); + } + + Widget _buildSegmentedControl({ + required double s, + required Widget child, + double radius = SacredRadii.md, + }) { + return _tvFocusable( + s: s, + radius: radius, + child: Container( + padding: EdgeInsets.all(6 * s), + decoration: BoxDecoration( + color: SacredColors.surfaceContainerLowest, + borderRadius: BorderRadius.circular(radius), ), child: child, ), @@ -1095,43 +1231,40 @@ class _AdminScreenState extends ConsumerState { Row( children: [ Expanded( - child: TextField( - controller: _cityCtrl, - readOnly: true, - style: GoogleFonts.plusJakartaSans(fontSize: 24 * s, color: SacredColors.onSurface), - decoration: InputDecoration( - filled: true, - fillColor: SacredColors.surfaceContainerLowest, - border: OutlineInputBorder(borderRadius: BorderRadius.circular(SacredRadii.md), borderSide: BorderSide.none), - ), - ), + child: _buildReadonlyField(_cityCtrl, s), ), SizedBox(width: 16 * s), - ElevatedButton.icon( - onPressed: () => _showCitySearchDialog(s), - icon: HugeIcon(icon: HugeIcons.strokeRoundedSearch01, color: SacredColors.onPrimary), - label: Text('CARI KOTA', style: TextStyle(fontSize: 16 * s)), - style: ElevatedButton.styleFrom( - backgroundColor: SacredColors.secondary, - foregroundColor: SacredColors.onPrimary, - padding: EdgeInsets.symmetric(horizontal: 24 * s, vertical: 24 * s), + _tvActionButton( + s: s, + child: ElevatedButton.icon( + onPressed: () => _showCitySearchDialog(s), + icon: HugeIcon(icon: HugeIcons.strokeRoundedSearch01, color: SacredColors.onPrimary), + label: Text('CARI KOTA', style: TextStyle(fontSize: 16 * s)), + style: ElevatedButton.styleFrom( + backgroundColor: SacredColors.secondary, + foregroundColor: SacredColors.onPrimary, + padding: EdgeInsets.symmetric(horizontal: 24 * s, vertical: 24 * s), + ), ), ), ], ), SizedBox(height: 64 * s), - ElevatedButton.icon( - onPressed: _saveIdentity, - style: ElevatedButton.styleFrom( - backgroundColor: SacredColors.primary, - foregroundColor: SacredColors.onPrimary, - padding: EdgeInsets.symmetric(horizontal: 48 * s, vertical: 24 * s), - textStyle: TextStyle(fontSize: 20 * s, fontWeight: FontWeight.bold), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(SacredRadii.lg)), + _tvActionButton( + s: s, + child: ElevatedButton.icon( + onPressed: _saveIdentity, + style: ElevatedButton.styleFrom( + backgroundColor: SacredColors.primary, + foregroundColor: SacredColors.onPrimary, + padding: EdgeInsets.symmetric(horizontal: 48 * s, vertical: 24 * s), + textStyle: TextStyle(fontSize: 20 * s, fontWeight: FontWeight.bold), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(SacredRadii.lg)), + ), + icon: HugeIcon(icon: HugeIcons.strokeRoundedFloppyDisk, color: SacredColors.onPrimary), + label: const Text('SIMPAN PERUBAHAN TULISAN'), ), - icon: HugeIcon(icon: HugeIcons.strokeRoundedFloppyDisk, color: SacredColors.onPrimary), - label: const Text('SIMPAN PERUBAHAN TULISAN'), ), ], ), @@ -1201,19 +1334,22 @@ class _AdminScreenState extends ConsumerState { ], ), ), - ElevatedButton.icon( - onPressed: _isSyncing ? null : _syncData, - style: ElevatedButton.styleFrom( - backgroundColor: SacredColors.secondary, - foregroundColor: SacredColors.onSecondary, - padding: EdgeInsets.symmetric(horizontal: 48 * s, vertical: 32 * s), - textStyle: TextStyle(fontSize: 20 * s, fontWeight: FontWeight.bold), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(SacredRadii.lg)), + _tvActionButton( + s: s, + child: ElevatedButton.icon( + onPressed: _isSyncing ? null : _syncData, + style: ElevatedButton.styleFrom( + backgroundColor: SacredColors.secondary, + foregroundColor: SacredColors.onSecondary, + padding: EdgeInsets.symmetric(horizontal: 48 * s, vertical: 32 * s), + textStyle: TextStyle(fontSize: 20 * s, fontWeight: FontWeight.bold), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(SacredRadii.lg)), + ), + icon: _isSyncing + ? SizedBox(width: 24*s, height: 24*s, child: const CircularProgressIndicator(color: SacredColors.onSecondary, strokeWidth: 3)) + : HugeIcon(icon: HugeIcons.strokeRoundedCloudDownload, color: SacredColors.onSecondary), + label: Text(_isSyncing ? 'MENYINKRONKAN...' : 'SINKRONKAN DATA BULAN INI'), ), - icon: _isSyncing - ? SizedBox(width: 24*s, height: 24*s, child: const CircularProgressIndicator(color: SacredColors.onSecondary, strokeWidth: 3)) - : HugeIcon(icon: HugeIcons.strokeRoundedCloudDownload, color: SacredColors.onSecondary), - label: Text(_isSyncing ? 'MENYINKRONKAN...' : 'SINKRONKAN DATA BULAN INI'), ) ], ), @@ -1297,28 +1433,34 @@ class _AdminScreenState extends ConsumerState { SizedBox(height: 16 * s), Row( children: [ - OutlinedButton.icon( - onPressed: () { - setState(() { - _hijriOffsetDays = 0; - }); - }, - icon: const Icon(Icons.refresh), - label: const Text('RESET OFFSET'), + _tvActionButton( + s: s, + child: OutlinedButton.icon( + onPressed: () { + setState(() { + _hijriOffsetDays = 0; + }); + }, + icon: const Icon(Icons.refresh), + label: const Text('RESET OFFSET'), + ), ), SizedBox(width: 16 * s), - ElevatedButton.icon( - onPressed: _saveHijriSettings, - style: ElevatedButton.styleFrom( - backgroundColor: SacredColors.primary, - foregroundColor: SacredColors.onPrimary, - padding: EdgeInsets.symmetric( - horizontal: 28 * s, - vertical: 18 * s, + _tvActionButton( + s: s, + child: ElevatedButton.icon( + onPressed: _saveHijriSettings, + style: ElevatedButton.styleFrom( + backgroundColor: SacredColors.primary, + foregroundColor: SacredColors.onPrimary, + padding: EdgeInsets.symmetric( + horizontal: 28 * s, + vertical: 18 * s, + ), ), + icon: const Icon(Icons.save_rounded), + label: const Text('SIMPAN OFFSET HIJRIAH'), ), - icon: const Icon(Icons.save_rounded), - label: const Text('SIMPAN OFFSET HIJRIAH'), ), ], ), @@ -1463,17 +1605,20 @@ class _AdminScreenState extends ConsumerState { ], ), SizedBox(height: 32 * s), - ElevatedButton.icon( - onPressed: _saveJadwalTimingSettings, - style: ElevatedButton.styleFrom( - backgroundColor: SacredColors.secondary, - foregroundColor: Colors.black, - padding: EdgeInsets.symmetric(horizontal: 40 * s, vertical: 20 * s), - textStyle: TextStyle(fontSize: 18 * s, fontWeight: FontWeight.bold), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(SacredRadii.lg)), + _tvActionButton( + s: s, + child: ElevatedButton.icon( + onPressed: _saveJadwalTimingSettings, + style: ElevatedButton.styleFrom( + backgroundColor: SacredColors.secondary, + foregroundColor: Colors.black, + padding: EdgeInsets.symmetric(horizontal: 40 * s, vertical: 20 * s), + textStyle: TextStyle(fontSize: 18 * s, fontWeight: FontWeight.bold), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(SacredRadii.lg)), + ), + icon: const Icon(Icons.timer), + label: const Text('SIMPAN PENGATURAN JADWAL'), ), - icon: const Icon(Icons.timer), - label: const Text('SIMPAN PENGATURAN JADWAL'), ), ], )), @@ -1822,7 +1967,10 @@ class _AdminScreenState extends ConsumerState { return _scrollAware( controller: _scrollControllerForTab(_selectedTab), - child: Container( + child: _TvFocusFrame( + scale: s, + borderRadius: BorderRadius.circular(SacredRadii.md), + child: Container( padding: EdgeInsets.all(16 * s), decoration: BoxDecoration( color: SacredColors.surfaceContainerLowest, @@ -1941,6 +2089,7 @@ class _AdminScreenState extends ConsumerState { ), ], ), + ), ), ); } @@ -1948,39 +2097,43 @@ class _AdminScreenState extends ConsumerState { Widget _buildTextField(String label, TextEditingController ctrl, double s, {int maxLines = 1}) { return _scrollAware( controller: _scrollControllerForTab(_selectedTab), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: GoogleFonts.manrope( - fontSize: 16 * s, - fontWeight: FontWeight.w600, - color: SacredColors.onSurfaceVariant, - ), - ), - SizedBox(height: 12 * s), - TextField( - controller: ctrl, - maxLines: maxLines, - style: GoogleFonts.plusJakartaSans( - fontSize: 24 * s, - color: SacredColors.onSurface, - ), - decoration: InputDecoration( - filled: true, - fillColor: SacredColors.surfaceContainerLowest, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(SacredRadii.md), - borderSide: BorderSide(color: SacredColors.outlineVariant.withValues(alpha: 0.5)), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(SacredRadii.md), - borderSide: const BorderSide(color: SacredColors.primary, width: 2), + child: _TvFocusFrame( + scale: s, + borderRadius: BorderRadius.circular(SacredRadii.md), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: GoogleFonts.manrope( + fontSize: 16 * s, + fontWeight: FontWeight.w600, + color: SacredColors.onSurfaceVariant, ), ), + SizedBox(height: 12 * s), + TextField( + controller: ctrl, + maxLines: maxLines, + style: GoogleFonts.plusJakartaSans( + fontSize: 24 * s, + color: SacredColors.onSurface, + ), + decoration: InputDecoration( + filled: true, + fillColor: SacredColors.surfaceContainerLowest, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(SacredRadii.md), + borderSide: BorderSide(color: SacredColors.outlineVariant.withValues(alpha: 0.5)), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(SacredRadii.md), + borderSide: const BorderSide(color: SacredColors.primary, width: 2), + ), + ), + ), + ], ), - ], ), ); } @@ -2038,7 +2191,9 @@ class _AdminScreenState extends ConsumerState { const step = 0.05; const presets = [0.75, 1.0, 1.25, 1.5]; - return Container( + return _tvFocusable( + s: s, + child: Container( padding: EdgeInsets.all(16 * s), decoration: BoxDecoration( color: SacredColors.surfaceContainerLowest, @@ -2140,32 +2295,38 @@ class _AdminScreenState extends ConsumerState { ), ], ), + ), ); } Widget _tvStepBtn({required double s, required String label, required VoidCallback onPressed}) { - return Material( - color: Colors.transparent, - child: InkWell( - focusColor: SacredColors.primary.withValues(alpha: 0.35), - hoverColor: SacredColors.primary.withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(SacredRadii.sm), - onTap: onPressed, - child: Container( - width: 42 * s, - height: 38 * s, - alignment: Alignment.center, - decoration: BoxDecoration( - border: Border.all(color: SacredColors.outlineVariant.withValues(alpha: 0.4)), - borderRadius: BorderRadius.circular(SacredRadii.sm), - color: SacredColors.surfaceContainerHighest, - ), - child: Text( - label, - style: GoogleFonts.manrope( - fontSize: 15 * s, - fontWeight: FontWeight.w700, - color: SacredColors.onSurface, + return _tvFocusable( + s: s, + radius: SacredRadii.sm, + scrollAware: false, + child: Material( + color: Colors.transparent, + child: InkWell( + focusColor: SacredColors.primary.withValues(alpha: 0.35), + hoverColor: SacredColors.primary.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(SacredRadii.sm), + onTap: onPressed, + child: Container( + width: 42 * s, + height: 38 * s, + alignment: Alignment.center, + decoration: BoxDecoration( + border: Border.all(color: SacredColors.outlineVariant.withValues(alpha: 0.4)), + borderRadius: BorderRadius.circular(SacredRadii.sm), + color: SacredColors.surfaceContainerHighest, + ), + child: Text( + label, + style: GoogleFonts.manrope( + fontSize: 15 * s, + fontWeight: FontWeight.w700, + color: SacredColors.onSurface, + ), ), ), ), @@ -2255,30 +2416,34 @@ class _AdminScreenState extends ConsumerState { } Widget _simulasiCard({required double s, required String title, required dynamic icon, required String desc, required VoidCallback onTap}) { - return InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(SacredRadii.lg), - child: Container( - width: 320 * s, - padding: EdgeInsets.all(24 * s), - decoration: BoxDecoration( - color: SacredColors.surfaceContainerLowest, - borderRadius: BorderRadius.circular(SacredRadii.lg), - border: Border.all(color: SacredColors.outlineVariant.withValues(alpha: 0.5)), - boxShadow: [ - BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 10 * s, offset: Offset(0, 4 * s)), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - HugeIcon(icon: icon, color: SacredColors.primary, size: 40 * s), - SizedBox(height: 16 * s), - Text(title, style: GoogleFonts.plusJakartaSans(fontSize: 20 * s, fontWeight: FontWeight.bold, color: SacredColors.onSurface)), - SizedBox(height: 8 * s), - Text(desc, style: GoogleFonts.manrope(fontSize: 14 * s, color: SacredColors.onSurfaceVariant)), - ], + return _tvFocusable( + s: s, + radius: SacredRadii.lg, + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(SacredRadii.lg), + child: Container( + width: 320 * s, + padding: EdgeInsets.all(24 * s), + decoration: BoxDecoration( + color: SacredColors.surfaceContainerLowest, + borderRadius: BorderRadius.circular(SacredRadii.lg), + border: Border.all(color: SacredColors.outlineVariant.withValues(alpha: 0.5)), + boxShadow: [ + BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 10 * s, offset: Offset(0, 4 * s)), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + HugeIcon(icon: icon, color: SacredColors.primary, size: 40 * s), + SizedBox(height: 16 * s), + Text(title, style: GoogleFonts.plusJakartaSans(fontSize: 20 * s, fontWeight: FontWeight.bold, color: SacredColors.onSurface)), + SizedBox(height: 8 * s), + Text(desc, style: GoogleFonts.manrope(fontSize: 14 * s, color: SacredColors.onSurfaceVariant)), + ], + ), ), ), ); @@ -2358,6 +2523,67 @@ class _NavButton extends StatefulWidget { State<_NavButton> createState() => _NavButtonState(); } +class _TvFocusFrame extends StatefulWidget { + final Widget child; + final double scale; + final BorderRadius borderRadius; + + const _TvFocusFrame({ + required this.child, + required this.scale, + required this.borderRadius, + }); + + @override + State<_TvFocusFrame> createState() => _TvFocusFrameState(); +} + +class _TvFocusFrameState extends State<_TvFocusFrame> { + bool _hasFocus = false; + + @override + Widget build(BuildContext context) { + final s = widget.scale; + + return Focus( + canRequestFocus: false, + descendantsAreFocusable: true, + onFocusChange: (value) { + if (_hasFocus != value) { + setState(() => _hasFocus = value); + } + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 140), + curve: Curves.easeOutCubic, + padding: EdgeInsets.all(_hasFocus ? 4 * s : 0), + decoration: BoxDecoration( + color: _hasFocus + ? SacredColors.primary.withValues(alpha: 0.08) + : Colors.transparent, + borderRadius: widget.borderRadius, + border: Border.all( + color: _hasFocus + ? SacredColors.primary.withValues(alpha: 0.7) + : Colors.transparent, + width: _hasFocus ? 2 : 0, + ), + boxShadow: _hasFocus + ? [ + BoxShadow( + color: SacredColors.primary.withValues(alpha: 0.18), + blurRadius: 18 * s, + spreadRadius: 1 * s, + ), + ] + : null, + ), + child: widget.child, + ), + ); + } +} + class _NavButtonState extends State<_NavButton> { bool _isFocused = false;