Improve visible focus styling in admin content

This commit is contained in:
dwindown
2026-03-30 22:49:40 +07:00
parent 18958be720
commit 6aa315dc91

View File

@@ -543,17 +543,20 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
SizedBox(height: 24 * s), SizedBox(height: 24 * s),
], ],
ElevatedButton.icon( _tvActionButton(
onPressed: _saveJumat, s: s,
style: ElevatedButton.styleFrom( child: ElevatedButton.icon(
backgroundColor: SacredColors.secondary, onPressed: _saveJumat,
foregroundColor: Colors.black, style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 40 * s, vertical: 20 * s), backgroundColor: SacredColors.secondary,
textStyle: TextStyle(fontSize: 18 * s, fontWeight: FontWeight.bold), foregroundColor: Colors.black,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(SacredRadii.lg)), 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<AdminScreen> {
children: [ children: [
_sectionLabel('Tipografi & Skala Teks', s), _sectionLabel('Tipografi & Skala Teks', s),
SizedBox(height: 12 * s), SizedBox(height: 12 * s),
SegmentedButton<int>( _buildSegmentedControl(
segments: const [ s: s,
ButtonSegment(value: 0, label: Text('Kecil')), child: SegmentedButton<int>(
ButtonSegment(value: 1, label: Text('Normal')), segments: const [
ButtonSegment(value: 2, label: Text('Besar')), ButtonSegment(value: 0, label: Text('Kecil')),
], ButtonSegment(value: 1, label: Text('Normal')),
selected: {_textScaleIndex}, ButtonSegment(value: 2, label: Text('Besar')),
onSelectionChanged: (val) { ],
setState(() => _textScaleIndex = val.first); selected: {_textScaleIndex},
}, onSelectionChanged: (val) {
setState(() => _textScaleIndex = val.first);
},
),
), ),
SizedBox(height: 28 * s), SizedBox(height: 28 * s),
_buildTvIntStepperField( _buildTvIntStepperField(
@@ -692,12 +698,11 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
_sectionLabel('Background Layar Utama (Unsplash)', s), _sectionLabel('Background Layar Utama (Unsplash)', s),
SizedBox(height: 12 * s), SizedBox(height: 12 * s),
SwitchListTile( _buildSwitchTile(
title: Text('Gunakan Foto Unsplash API', style: GoogleFonts.plusJakartaSans(fontSize: 18 * s, color: SacredColors.onSurface)), s: s,
title: 'Gunakan Foto Unsplash API',
value: _useUnsplash, value: _useUnsplash,
onChanged: (val) => setState(() => _useUnsplash = val), onChanged: (val) => setState(() => _useUnsplash = val),
activeThumbColor: SacredColors.primary,
contentPadding: EdgeInsets.zero,
), ),
if (_useUnsplash) ...[ if (_useUnsplash) ...[
SizedBox(height: 12 * s), SizedBox(height: 12 * s),
@@ -715,17 +720,20 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
], ],
SizedBox(height: 56 * s), SizedBox(height: 56 * s),
ElevatedButton.icon( _tvActionButton(
onPressed: _saveTampilan, s: s,
style: ElevatedButton.styleFrom( child: ElevatedButton.icon(
backgroundColor: SacredColors.primary, onPressed: _saveTampilan,
foregroundColor: SacredColors.onPrimary, style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 48 * s, vertical: 24 * s), backgroundColor: SacredColors.primary,
textStyle: TextStyle(fontSize: 18 * s, fontWeight: FontWeight.bold), foregroundColor: SacredColors.onPrimary,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(SacredRadii.lg)), 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<AdminScreen> {
] else ] else
Text('Belum ada foto latar masjid.', style: GoogleFonts.manrope(fontSize: 16 * s, color: SacredColors.onSurfaceVariant)), Text('Belum ada foto latar masjid.', style: GoogleFonts.manrope(fontSize: 16 * s, color: SacredColors.onSurfaceVariant)),
SizedBox(height: 16 * s), SizedBox(height: 16 * s),
ElevatedButton.icon( _tvActionButton(
onPressed: () async { s: s,
final res = await FilePicker.platform.pickFiles(type: FileType.image); child: ElevatedButton.icon(
if (res != null && res.files.single.path != null) { onPressed: () async {
setState(() => _brandedBgImage = res.files.single.path); final res = await FilePicker.platform.pickFiles(type: FileType.image);
_saveTampilan(); 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( icon: HugeIcon(icon: HugeIcons.strokeRoundedImage01, color: SacredColors.onPrimary, size: 20 * s),
backgroundColor: SacredColors.secondary, label: Text('PILIH FOTO MASJID', style: TextStyle(fontSize: 16 * s)),
foregroundColor: SacredColors.onSecondary, style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 24 * s, vertical: 16 * s), backgroundColor: SacredColors.secondary,
foregroundColor: SacredColors.onSecondary,
padding: EdgeInsets.symmetric(horizontal: 24 * s, vertical: 16 * s),
),
), ),
), ),
], ],
@@ -803,26 +814,29 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
_sectionLabel('Galeri Gambar Slideshow', s), _sectionLabel('Galeri Gambar Slideshow', s),
ElevatedButton.icon( _tvActionButton(
onPressed: () async { s: s,
final res = await FilePicker.platform.pickFiles(type: FileType.image, allowMultiple: true); child: ElevatedButton.icon(
if (res != null) { onPressed: () async {
setState(() { final res = await FilePicker.platform.pickFiles(type: FileType.image, allowMultiple: true);
for (var path in res.paths) { if (res != null) {
if (path != null && !_slideshowImages.contains(path)) { setState(() {
_slideshowImages.add(path); for (var path in res.paths) {
if (path != null && !_slideshowImages.contains(path)) {
_slideshowImages.add(path);
}
} }
} });
}); _saveTampilan();
_saveTampilan(); }
} },
}, icon: HugeIcon(icon: HugeIcons.strokeRoundedPlusSign, color: SacredColors.onSecondary, size: 18 * s),
icon: HugeIcon(icon: HugeIcons.strokeRoundedPlusSign, color: SacredColors.onSecondary, size: 18 * s), label: Text('TAMBAH FOTO', style: TextStyle(fontSize: 14 * s)),
label: Text('TAMBAH FOTO', style: TextStyle(fontSize: 14 * s)), style: ElevatedButton.styleFrom(
style: ElevatedButton.styleFrom( backgroundColor: SacredColors.secondary,
backgroundColor: SacredColors.secondary, foregroundColor: SacredColors.onSecondary,
foregroundColor: SacredColors.onSecondary, padding: EdgeInsets.symmetric(horizontal: 20 * s, vertical: 14 * s),
padding: EdgeInsets.symmetric(horizontal: 20 * s, vertical: 14 * s), ),
), ),
), ),
], ],
@@ -876,18 +890,21 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
children: [ children: [
Text('Mode Animasi:', style: GoogleFonts.manrope(fontSize: 16 * s, color: SacredColors.onSurfaceVariant)), Text('Mode Animasi:', style: GoogleFonts.manrope(fontSize: 16 * s, color: SacredColors.onSurfaceVariant)),
SizedBox(width: 12 * s), SizedBox(width: 12 * s),
SegmentedButton<String>( _buildSegmentedControl(
segments: [ s: s,
ButtonSegment(value: 'marquee', label: Text('Marquee', style: GoogleFonts.manrope(fontSize: 16 * s))), child: SegmentedButton<String>(
ButtonSegment(value: 'fade', label: Text('Fade In-Out', style: GoogleFonts.manrope(fontSize: 16 * s))), segments: [
], ButtonSegment(value: 'marquee', label: Text('Marquee', style: GoogleFonts.manrope(fontSize: 16 * s))),
selected: {_marqueeAnimType}, ButtonSegment(value: 'fade', label: Text('Fade In-Out', style: GoogleFonts.manrope(fontSize: 16 * s))),
onSelectionChanged: (val) => setState(() => _marqueeAnimType = val.first), ],
style: ButtonStyle( selected: {_marqueeAnimType},
backgroundColor: WidgetStateProperty.resolveWith((states) => onSelectionChanged: (val) => setState(() => _marqueeAnimType = val.first),
states.contains(WidgetState.selected) ? SacredColors.primary : SacredColors.surfaceContainerLowest), style: ButtonStyle(
foregroundColor: WidgetStateProperty.resolveWith((states) => backgroundColor: WidgetStateProperty.resolveWith((states) =>
states.contains(WidgetState.selected) ? SacredColors.onPrimary : SacredColors.onSurfaceVariant), 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<AdminScreen> {
SizedBox(height: 20 * s), SizedBox(height: 20 * s),
Row( Row(
children: [ children: [
OutlinedButton.icon( _tvActionButton(
onPressed: () { s: s,
setState(() { child: OutlinedButton.icon(
_runningTexts.add(''); onPressed: () {
_runningTextDurations.add(12); 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( icon: HugeIcon(icon: HugeIcons.strokeRoundedPlusSign, color: SacredColors.primary, size: 20 * s),
side: BorderSide(color: SacredColors.primary.withValues(alpha: 0.5)), label: Text('TAMBAH BARIS', style: GoogleFonts.plusJakartaSans(fontSize: 16 * s, color: SacredColors.primary)),
padding: EdgeInsets.symmetric(horizontal: 24 * s, vertical: 16 * s), 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), SizedBox(width: 16 * s),
ElevatedButton.icon( _tvActionButton(
onPressed: _saveTampilan, s: s,
style: ElevatedButton.styleFrom( child: ElevatedButton.icon(
backgroundColor: SacredColors.primary, onPressed: _saveTampilan,
foregroundColor: SacredColors.onPrimary, style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 32 * s, vertical: 16 * s), 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<AdminScreen> {
Widget _adminCard(double s, {required Widget child}) { Widget _adminCard(double s, {required Widget child}) {
return _scrollAware( return _scrollAware(
controller: _scrollControllerForTab(_selectedTab), 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<bool> onChanged,
}) {
return _tvFocusable(
s: s,
radius: SacredRadii.md,
child: Container( child: Container(
width: double.infinity,
padding: EdgeInsets.all(36 * s),
decoration: BoxDecoration( decoration: BoxDecoration(
color: SacredColors.surfaceContainerHighest.withValues(alpha: 0.3), color: SacredColors.surfaceContainerLowest,
borderRadius: BorderRadius.circular(SacredRadii.xl), borderRadius: BorderRadius.circular(SacredRadii.md),
border: Border.all(color: SacredColors.outlineVariant.withValues(alpha: 0.2)), ),
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, child: child,
), ),
@@ -1095,43 +1231,40 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
Row( Row(
children: [ children: [
Expanded( Expanded(
child: TextField( child: _buildReadonlyField(_cityCtrl, s),
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),
),
),
), ),
SizedBox(width: 16 * s), SizedBox(width: 16 * s),
ElevatedButton.icon( _tvActionButton(
onPressed: () => _showCitySearchDialog(s), s: s,
icon: HugeIcon(icon: HugeIcons.strokeRoundedSearch01, color: SacredColors.onPrimary), child: ElevatedButton.icon(
label: Text('CARI KOTA', style: TextStyle(fontSize: 16 * s)), onPressed: () => _showCitySearchDialog(s),
style: ElevatedButton.styleFrom( icon: HugeIcon(icon: HugeIcons.strokeRoundedSearch01, color: SacredColors.onPrimary),
backgroundColor: SacredColors.secondary, label: Text('CARI KOTA', style: TextStyle(fontSize: 16 * s)),
foregroundColor: SacredColors.onPrimary, style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 24 * s, vertical: 24 * s), backgroundColor: SacredColors.secondary,
foregroundColor: SacredColors.onPrimary,
padding: EdgeInsets.symmetric(horizontal: 24 * s, vertical: 24 * s),
),
), ),
), ),
], ],
), ),
SizedBox(height: 64 * s), SizedBox(height: 64 * s),
ElevatedButton.icon( _tvActionButton(
onPressed: _saveIdentity, s: s,
style: ElevatedButton.styleFrom( child: ElevatedButton.icon(
backgroundColor: SacredColors.primary, onPressed: _saveIdentity,
foregroundColor: SacredColors.onPrimary, style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 48 * s, vertical: 24 * s), backgroundColor: SacredColors.primary,
textStyle: TextStyle(fontSize: 20 * s, fontWeight: FontWeight.bold), foregroundColor: SacredColors.onPrimary,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(SacredRadii.lg)), 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<AdminScreen> {
], ],
), ),
), ),
ElevatedButton.icon( _tvActionButton(
onPressed: _isSyncing ? null : _syncData, s: s,
style: ElevatedButton.styleFrom( child: ElevatedButton.icon(
backgroundColor: SacredColors.secondary, onPressed: _isSyncing ? null : _syncData,
foregroundColor: SacredColors.onSecondary, style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 48 * s, vertical: 32 * s), backgroundColor: SacredColors.secondary,
textStyle: TextStyle(fontSize: 20 * s, fontWeight: FontWeight.bold), foregroundColor: SacredColors.onSecondary,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(SacredRadii.lg)), 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<AdminScreen> {
SizedBox(height: 16 * s), SizedBox(height: 16 * s),
Row( Row(
children: [ children: [
OutlinedButton.icon( _tvActionButton(
onPressed: () { s: s,
setState(() { child: OutlinedButton.icon(
_hijriOffsetDays = 0; onPressed: () {
}); setState(() {
}, _hijriOffsetDays = 0;
icon: const Icon(Icons.refresh), });
label: const Text('RESET OFFSET'), },
icon: const Icon(Icons.refresh),
label: const Text('RESET OFFSET'),
),
), ),
SizedBox(width: 16 * s), SizedBox(width: 16 * s),
ElevatedButton.icon( _tvActionButton(
onPressed: _saveHijriSettings, s: s,
style: ElevatedButton.styleFrom( child: ElevatedButton.icon(
backgroundColor: SacredColors.primary, onPressed: _saveHijriSettings,
foregroundColor: SacredColors.onPrimary, style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric( backgroundColor: SacredColors.primary,
horizontal: 28 * s, foregroundColor: SacredColors.onPrimary,
vertical: 18 * s, 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<AdminScreen> {
], ],
), ),
SizedBox(height: 32 * s), SizedBox(height: 32 * s),
ElevatedButton.icon( _tvActionButton(
onPressed: _saveJadwalTimingSettings, s: s,
style: ElevatedButton.styleFrom( child: ElevatedButton.icon(
backgroundColor: SacredColors.secondary, onPressed: _saveJadwalTimingSettings,
foregroundColor: Colors.black, style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 40 * s, vertical: 20 * s), backgroundColor: SacredColors.secondary,
textStyle: TextStyle(fontSize: 18 * s, fontWeight: FontWeight.bold), foregroundColor: Colors.black,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(SacredRadii.lg)), 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<AdminScreen> {
return _scrollAware( return _scrollAware(
controller: _scrollControllerForTab(_selectedTab), controller: _scrollControllerForTab(_selectedTab),
child: Container( child: _TvFocusFrame(
scale: s,
borderRadius: BorderRadius.circular(SacredRadii.md),
child: Container(
padding: EdgeInsets.all(16 * s), padding: EdgeInsets.all(16 * s),
decoration: BoxDecoration( decoration: BoxDecoration(
color: SacredColors.surfaceContainerLowest, color: SacredColors.surfaceContainerLowest,
@@ -1941,6 +2089,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
), ),
], ],
), ),
),
), ),
); );
} }
@@ -1948,39 +2097,43 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
Widget _buildTextField(String label, TextEditingController ctrl, double s, {int maxLines = 1}) { Widget _buildTextField(String label, TextEditingController ctrl, double s, {int maxLines = 1}) {
return _scrollAware( return _scrollAware(
controller: _scrollControllerForTab(_selectedTab), controller: _scrollControllerForTab(_selectedTab),
child: Column( child: _TvFocusFrame(
crossAxisAlignment: CrossAxisAlignment.start, scale: s,
children: [ borderRadius: BorderRadius.circular(SacredRadii.md),
Text( child: Column(
label, crossAxisAlignment: CrossAxisAlignment.start,
style: GoogleFonts.manrope( children: [
fontSize: 16 * s, Text(
fontWeight: FontWeight.w600, label,
color: SacredColors.onSurfaceVariant, style: GoogleFonts.manrope(
), fontSize: 16 * s,
), fontWeight: FontWeight.w600,
SizedBox(height: 12 * s), color: SacredColors.onSurfaceVariant,
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),
), ),
), ),
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<AdminScreen> {
const step = 0.05; const step = 0.05;
const presets = [0.75, 1.0, 1.25, 1.5]; const presets = [0.75, 1.0, 1.25, 1.5];
return Container( return _tvFocusable(
s: s,
child: Container(
padding: EdgeInsets.all(16 * s), padding: EdgeInsets.all(16 * s),
decoration: BoxDecoration( decoration: BoxDecoration(
color: SacredColors.surfaceContainerLowest, color: SacredColors.surfaceContainerLowest,
@@ -2140,32 +2295,38 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
), ),
], ],
), ),
),
); );
} }
Widget _tvStepBtn({required double s, required String label, required VoidCallback onPressed}) { Widget _tvStepBtn({required double s, required String label, required VoidCallback onPressed}) {
return Material( return _tvFocusable(
color: Colors.transparent, s: s,
child: InkWell( radius: SacredRadii.sm,
focusColor: SacredColors.primary.withValues(alpha: 0.35), scrollAware: false,
hoverColor: SacredColors.primary.withValues(alpha: 0.15), child: Material(
borderRadius: BorderRadius.circular(SacredRadii.sm), color: Colors.transparent,
onTap: onPressed, child: InkWell(
child: Container( focusColor: SacredColors.primary.withValues(alpha: 0.35),
width: 42 * s, hoverColor: SacredColors.primary.withValues(alpha: 0.15),
height: 38 * s, borderRadius: BorderRadius.circular(SacredRadii.sm),
alignment: Alignment.center, onTap: onPressed,
decoration: BoxDecoration( child: Container(
border: Border.all(color: SacredColors.outlineVariant.withValues(alpha: 0.4)), width: 42 * s,
borderRadius: BorderRadius.circular(SacredRadii.sm), height: 38 * s,
color: SacredColors.surfaceContainerHighest, alignment: Alignment.center,
), decoration: BoxDecoration(
child: Text( border: Border.all(color: SacredColors.outlineVariant.withValues(alpha: 0.4)),
label, borderRadius: BorderRadius.circular(SacredRadii.sm),
style: GoogleFonts.manrope( color: SacredColors.surfaceContainerHighest,
fontSize: 15 * s, ),
fontWeight: FontWeight.w700, child: Text(
color: SacredColors.onSurface, label,
style: GoogleFonts.manrope(
fontSize: 15 * s,
fontWeight: FontWeight.w700,
color: SacredColors.onSurface,
),
), ),
), ),
), ),
@@ -2255,30 +2416,34 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
} }
Widget _simulasiCard({required double s, required String title, required dynamic icon, required String desc, required VoidCallback onTap}) { Widget _simulasiCard({required double s, required String title, required dynamic icon, required String desc, required VoidCallback onTap}) {
return InkWell( return _tvFocusable(
onTap: onTap, s: s,
borderRadius: BorderRadius.circular(SacredRadii.lg), radius: SacredRadii.lg,
child: Container( child: InkWell(
width: 320 * s, onTap: onTap,
padding: EdgeInsets.all(24 * s), borderRadius: BorderRadius.circular(SacredRadii.lg),
decoration: BoxDecoration( child: Container(
color: SacredColors.surfaceContainerLowest, width: 320 * s,
borderRadius: BorderRadius.circular(SacredRadii.lg), padding: EdgeInsets.all(24 * s),
border: Border.all(color: SacredColors.outlineVariant.withValues(alpha: 0.5)), decoration: BoxDecoration(
boxShadow: [ color: SacredColors.surfaceContainerLowest,
BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 10 * s, offset: Offset(0, 4 * s)), borderRadius: BorderRadius.circular(SacredRadii.lg),
], border: Border.all(color: SacredColors.outlineVariant.withValues(alpha: 0.5)),
), boxShadow: [
child: Column( BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 10 * s, offset: Offset(0, 4 * s)),
crossAxisAlignment: CrossAxisAlignment.start, ],
mainAxisSize: MainAxisSize.min, ),
children: [ child: Column(
HugeIcon(icon: icon, color: SacredColors.primary, size: 40 * s), crossAxisAlignment: CrossAxisAlignment.start,
SizedBox(height: 16 * s), mainAxisSize: MainAxisSize.min,
Text(title, style: GoogleFonts.plusJakartaSans(fontSize: 20 * s, fontWeight: FontWeight.bold, color: SacredColors.onSurface)), children: [
SizedBox(height: 8 * s), HugeIcon(icon: icon, color: SacredColors.primary, size: 40 * s),
Text(desc, style: GoogleFonts.manrope(fontSize: 14 * s, color: SacredColors.onSurfaceVariant)), 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(); 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> { class _NavButtonState extends State<_NavButton> {
bool _isFocused = false; bool _isFocused = false;