Improve visible focus styling in admin content
This commit is contained in:
@@ -543,7 +543,9 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
SizedBox(height: 24 * s),
|
||||
],
|
||||
|
||||
ElevatedButton.icon(
|
||||
_tvActionButton(
|
||||
s: s,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _saveJumat,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: SacredColors.secondary,
|
||||
@@ -555,6 +557,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
icon: const Icon(Icons.save_rounded),
|
||||
label: const Text('SIMPAN DATA JUMAT'),
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
|
||||
@@ -625,7 +628,9 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
children: [
|
||||
_sectionLabel('Tipografi & Skala Teks', s),
|
||||
SizedBox(height: 12 * s),
|
||||
SegmentedButton<int>(
|
||||
_buildSegmentedControl(
|
||||
s: s,
|
||||
child: SegmentedButton<int>(
|
||||
segments: const [
|
||||
ButtonSegment(value: 0, label: Text('Kecil')),
|
||||
ButtonSegment(value: 1, label: Text('Normal')),
|
||||
@@ -636,6 +641,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
setState(() => _textScaleIndex = val.first);
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(height: 28 * s),
|
||||
_buildTvIntStepperField(
|
||||
s: s,
|
||||
@@ -692,12 +698,11 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
|
||||
_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,7 +720,9 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
],
|
||||
|
||||
SizedBox(height: 56 * s),
|
||||
ElevatedButton.icon(
|
||||
_tvActionButton(
|
||||
s: s,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _saveTampilan,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: SacredColors.primary,
|
||||
@@ -727,6 +734,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
icon: HugeIcon(icon: HugeIcons.strokeRoundedFloppyDisk, color: SacredColors.onPrimary),
|
||||
label: const Text('SIMPAN TAMPILAN'),
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
),
|
||||
@@ -775,7 +783,9 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
] else
|
||||
Text('Belum ada foto latar masjid.', style: GoogleFonts.manrope(fontSize: 16 * s, color: SacredColors.onSurfaceVariant)),
|
||||
SizedBox(height: 16 * s),
|
||||
ElevatedButton.icon(
|
||||
_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) {
|
||||
@@ -791,6 +801,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
padding: EdgeInsets.symmetric(horizontal: 24 * s, vertical: 16 * s),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
SizedBox(height: 24 * s),
|
||||
@@ -803,7 +814,9 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_sectionLabel('Galeri Gambar Slideshow', s),
|
||||
ElevatedButton.icon(
|
||||
_tvActionButton(
|
||||
s: s,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
final res = await FilePicker.platform.pickFiles(type: FileType.image, allowMultiple: true);
|
||||
if (res != null) {
|
||||
@@ -825,6 +838,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
padding: EdgeInsets.symmetric(horizontal: 20 * s, vertical: 14 * s),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 16 * s),
|
||||
@@ -876,7 +890,9 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
children: [
|
||||
Text('Mode Animasi:', style: GoogleFonts.manrope(fontSize: 16 * s, color: SacredColors.onSurfaceVariant)),
|
||||
SizedBox(width: 12 * s),
|
||||
SegmentedButton<String>(
|
||||
_buildSegmentedControl(
|
||||
s: s,
|
||||
child: SegmentedButton<String>(
|
||||
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))),
|
||||
@@ -890,6 +906,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
states.contains(WidgetState.selected) ? SacredColors.onPrimary : SacredColors.onSurfaceVariant),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -986,7 +1003,9 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
SizedBox(height: 20 * s),
|
||||
Row(
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
_tvActionButton(
|
||||
s: s,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_runningTexts.add('');
|
||||
@@ -1000,8 +1019,11 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
padding: EdgeInsets.symmetric(horizontal: 24 * s, vertical: 16 * s),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16 * s),
|
||||
ElevatedButton.icon(
|
||||
_tvActionButton(
|
||||
s: s,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _saveTampilan,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: SacredColors.primary,
|
||||
@@ -1011,6 +1033,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
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,6 +1048,9 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
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),
|
||||
@@ -1035,6 +1061,116 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
),
|
||||
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(
|
||||
decoration: BoxDecoration(
|
||||
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,19 +1231,12 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
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(
|
||||
_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)),
|
||||
@@ -1117,11 +1246,14 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
padding: EdgeInsets.symmetric(horizontal: 24 * s, vertical: 24 * s),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
SizedBox(height: 64 * s),
|
||||
ElevatedButton.icon(
|
||||
_tvActionButton(
|
||||
s: s,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _saveIdentity,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: SacredColors.primary,
|
||||
@@ -1133,6 +1265,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
icon: HugeIcon(icon: HugeIcons.strokeRoundedFloppyDisk, color: SacredColors.onPrimary),
|
||||
label: const Text('SIMPAN PERUBAHAN TULISAN'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1201,7 +1334,9 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
_tvActionButton(
|
||||
s: s,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _isSyncing ? null : _syncData,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: SacredColors.secondary,
|
||||
@@ -1214,6 +1349,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
? 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,7 +1433,9 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
SizedBox(height: 16 * s),
|
||||
Row(
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
_tvActionButton(
|
||||
s: s,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_hijriOffsetDays = 0;
|
||||
@@ -1306,8 +1444,11 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('RESET OFFSET'),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16 * s),
|
||||
ElevatedButton.icon(
|
||||
_tvActionButton(
|
||||
s: s,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _saveHijriSettings,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: SacredColors.primary,
|
||||
@@ -1320,6 +1461,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
icon: const Icon(Icons.save_rounded),
|
||||
label: const Text('SIMPAN OFFSET HIJRIAH'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -1463,7 +1605,9 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
],
|
||||
),
|
||||
SizedBox(height: 32 * s),
|
||||
ElevatedButton.icon(
|
||||
_tvActionButton(
|
||||
s: s,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _saveJadwalTimingSettings,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: SacredColors.secondary,
|
||||
@@ -1475,6 +1619,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
icon: const Icon(Icons.timer),
|
||||
label: const Text('SIMPAN PENGATURAN JADWAL'),
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
|
||||
@@ -1822,6 +1967,9 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
|
||||
return _scrollAware(
|
||||
controller: _scrollControllerForTab(_selectedTab),
|
||||
child: _TvFocusFrame(
|
||||
scale: s,
|
||||
borderRadius: BorderRadius.circular(SacredRadii.md),
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(16 * s),
|
||||
decoration: BoxDecoration(
|
||||
@@ -1942,12 +2090,16 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTextField(String label, TextEditingController ctrl, double s, {int maxLines = 1}) {
|
||||
return _scrollAware(
|
||||
controller: _scrollControllerForTab(_selectedTab),
|
||||
child: _TvFocusFrame(
|
||||
scale: s,
|
||||
borderRadius: BorderRadius.circular(SacredRadii.md),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -1982,6 +2134,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2038,7 +2191,9 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
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,11 +2295,16 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _tvStepBtn({required double s, required String label, required VoidCallback onPressed}) {
|
||||
return Material(
|
||||
return _tvFocusable(
|
||||
s: s,
|
||||
radius: SacredRadii.sm,
|
||||
scrollAware: false,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
focusColor: SacredColors.primary.withValues(alpha: 0.35),
|
||||
@@ -2170,6 +2330,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2255,7 +2416,10 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
}
|
||||
|
||||
Widget _simulasiCard({required double s, required String title, required dynamic icon, required String desc, required VoidCallback onTap}) {
|
||||
return InkWell(
|
||||
return _tvFocusable(
|
||||
s: s,
|
||||
radius: SacredRadii.lg,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(SacredRadii.lg),
|
||||
child: Container(
|
||||
@@ -2281,6 +2445,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user