Add TV-friendly Hijri offset control and fix admin schedule scrolling

This commit is contained in:
dwindown
2026-03-30 21:52:39 +07:00
parent 33810bb4bd
commit 9b126646a9
3 changed files with 357 additions and 40 deletions

View File

@@ -56,6 +56,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
final _iqomahAsharCtrl = TextEditingController();
final _iqomahMaghribCtrl = TextEditingController();
final _iqomahIsyaCtrl = TextEditingController();
int _hijriOffsetDays = 0;
@override
void initState() {
@@ -94,6 +95,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
_iqomahAsharCtrl.text = settings.iqomahAshar.toString();
_iqomahMaghribCtrl.text = settings.iqomahMaghrib.toString();
_iqomahIsyaCtrl.text = settings.iqomahIsya.toString();
_hijriOffsetDays = settings.hijriOffsetDays;
// Update preview live as admin types
_khatibCtrl.addListener(() => setState(() {}));
@@ -184,6 +186,26 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
}
}
Future<void> _saveHijriSettings() async {
await ref.read(settingsProvider.notifier).updateSettings((s) {
s.hijriOffsetDays = _hijriOffsetDays;
return s;
});
if (mounted) {
ref.invalidate(hijriDateProvider);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Offset Hijriah disimpan: ${_hijriOffsetDays >= 0 ? '+' : ''}$_hijriOffsetDays hari',
style: GoogleFonts.manrope(),
),
backgroundColor: SacredColors.primaryContainer,
),
);
}
}
Future<void> _syncData() async {
setState(() => _isSyncing = true);
final success = await SyncService.instance.syncMonthlyData();
@@ -1061,10 +1083,12 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
Widget _buildJadwalTab(double s) {
final settings = ref.watch(settingsProvider);
final todayScheduleOption = ref.watch(todayScheduleProvider);
final displayedHijri = ref.watch(hijriDateProvider).valueOrNull;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Jadwal & Sinkronisasi',
style: GoogleFonts.plusJakartaSans(
@@ -1128,6 +1152,113 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
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),
),
),
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,
),
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),
SizedBox(height: 16 * s),
Row(
children: [
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,
),
),
icon: const Icon(Icons.save_rounded),
label: const Text('SIMPAN OFFSET HIJRIAH'),
),
],
),
],
),
),
SizedBox(height: 64 * s),
// Jeda Waktu Iqamah Settings Card
_adminCard(s, child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -1187,44 +1318,218 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
SizedBox(height: 32 * s),
// Schedule Grid
Expanded(
child: Builder(
builder: (context) {
if (todayScheduleOption == null) {
return Center(
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(
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),
],
),
);
}
Widget _buildHijriOffsetControl(double s) {
const minOffset = -3;
const maxOffset = 3;
final valueLabel =
'${_hijriOffsetDays >= 0 ? '+' : ''}$_hijriOffsetDays hari';
final progress = (_hijriOffsetDays - minOffset) / (maxOffset - minOffset);
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.25),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Offset Hari Hijriah',
style: GoogleFonts.manrope(
fontSize: 15 * s,
fontWeight: FontWeight.w500,
color: SacredColors.onSurface,
),
),
),
Container(
padding: EdgeInsets.symmetric(
horizontal: 14 * s,
vertical: 5 * s,
),
decoration: BoxDecoration(
color: SacredColors.primary.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(SacredRadii.sm),
),
child: Text(
valueLabel,
style: GoogleFonts.manrope(
fontSize: 16 * s,
fontWeight: FontWeight.w800,
color: SacredColors.primary,
),
),
),
],
),
SizedBox(height: 14 * s),
Row(
children: [
_tvStepBtn(
s: s,
label: '',
onPressed: () {
setState(() {
_hijriOffsetDays =
(_hijriOffsetDays - 1).clamp(minOffset, maxOffset);
});
},
),
SizedBox(width: 10 * s),
Expanded(
child: Stack(
alignment: Alignment.centerLeft,
children: [
Container(
height: 6 * s,
decoration: BoxDecoration(
color: SacredColors.outlineVariant.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(3 * s),
),
),
FractionallySizedBox(
widthFactor: progress.clamp(0.0, 1.0),
child: Container(
height: 6 * s,
decoration: BoxDecoration(
color: SacredColors.primary,
borderRadius: BorderRadius.circular(3 * s),
),
),
),
],
),
),
SizedBox(width: 10 * s),
_tvStepBtn(
s: s,
label: '+',
onPressed: () {
setState(() {
_hijriOffsetDays =
(_hijriOffsetDays + 1).clamp(minOffset, maxOffset);
});
},
),
],
),
SizedBox(height: 12 * s),
Row(
children: [
Text(
'Preset: ',
style: GoogleFonts.manrope(
fontSize: 12 * s,
color: SacredColors.onSurfaceVariant,
),
),
...[-2, -1, 0, 1, 2].map((offset) {
final isActive = _hijriOffsetDays == offset;
final label = '${offset >= 0 ? '+' : ''}$offset';
return Padding(
padding: EdgeInsets.only(right: 8 * s),
child: InkWell(
focusColor: SacredColors.primary.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(SacredRadii.sm),
onTap: () => setState(() => _hijriOffsetDays = offset),
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 12 * s,
vertical: 6 * s,
),
decoration: BoxDecoration(
color: isActive
? SacredColors.primary
: SacredColors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(SacredRadii.sm),
border: isActive
? null
: Border.all(
color: SacredColors.outlineVariant.withValues(
alpha: 0.3,
),
),
),
child: Text(
label,
style: GoogleFonts.manrope(
fontSize: 13 * s,
fontWeight: FontWeight.w600,
color: isActive
? SacredColors.onPrimary
: SacredColors.onSurfaceVariant,
),
),
),
),
);
}),
],
),
SizedBox(height: 6 * s),
Text(
'TV Remote: fokus ke tombol atau + lalu tekan OK untuk ubah satu hari.',
style: GoogleFonts.manrope(
fontSize: 11 * s,
color: SacredColors.onSurfaceVariant.withValues(alpha: 0.7),
),
),
],
),
);
}