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

@@ -120,6 +120,10 @@ class AppSettings extends HiveObject {
@HiveField(30) @HiveField(30)
double scaleRunningText; double scaleRunningText;
// Manual day adjustment applied to displayed Hijri date.
@HiveField(31)
int hijriOffsetDays;
AppSettings({ AppSettings({
this.masjidName = 'Masjid Al-Ikhlas', this.masjidName = 'Masjid Al-Ikhlas',
this.masjidAddress = 'Jl. Kebaikan No. 1', this.masjidAddress = 'Jl. Kebaikan No. 1',
@@ -155,6 +159,7 @@ class AppSettings extends HiveObject {
this.scaleCardLabel = 1.0, this.scaleCardLabel = 1.0,
this.scaleCardBody = 1.0, this.scaleCardBody = 1.0,
this.scaleRunningText = 1.0, this.scaleRunningText = 1.0,
this.hijriOffsetDays = 0,
}); });
AppSettings copyWith({ AppSettings copyWith({
@@ -189,6 +194,7 @@ class AppSettings extends HiveObject {
double? scaleCardLabel, double? scaleCardLabel,
double? scaleCardBody, double? scaleCardBody,
double? scaleRunningText, double? scaleRunningText,
int? hijriOffsetDays,
}) { }) {
return AppSettings( return AppSettings(
masjidName: masjidName ?? this.masjidName, masjidName: masjidName ?? this.masjidName,
@@ -222,6 +228,7 @@ class AppSettings extends HiveObject {
scaleCardLabel: scaleCardLabel ?? this.scaleCardLabel, scaleCardLabel: scaleCardLabel ?? this.scaleCardLabel,
scaleCardBody: scaleCardBody ?? this.scaleCardBody, scaleCardBody: scaleCardBody ?? this.scaleCardBody,
scaleRunningText: scaleRunningText ?? this.scaleRunningText, scaleRunningText: scaleRunningText ?? this.scaleRunningText,
hijriOffsetDays: hijriOffsetDays ?? this.hijriOffsetDays,
); );
} }
} }
@@ -270,13 +277,14 @@ class AppSettingsAdapter extends TypeAdapter<AppSettings> {
scaleCardLabel: (fields[28] as num?)?.toDouble() ?? 1.0, scaleCardLabel: (fields[28] as num?)?.toDouble() ?? 1.0,
scaleCardBody: (fields[29] as num?)?.toDouble() ?? 1.0, scaleCardBody: (fields[29] as num?)?.toDouble() ?? 1.0,
scaleRunningText: (fields[30] as num?)?.toDouble() ?? 1.0, scaleRunningText: (fields[30] as num?)?.toDouble() ?? 1.0,
hijriOffsetDays: fields[31] as int? ?? 0,
); );
} }
@override @override
void write(BinaryWriter writer, AppSettings obj) { void write(BinaryWriter writer, AppSettings obj) {
writer writer
..writeByte(31) ..writeByte(32)
..writeByte(0)..write(obj.masjidName) ..writeByte(0)..write(obj.masjidName)
..writeByte(1)..write(obj.masjidAddress) ..writeByte(1)..write(obj.masjidAddress)
..writeByte(2)..write(obj.cityIdApi) ..writeByte(2)..write(obj.cityIdApi)
@@ -307,7 +315,8 @@ class AppSettingsAdapter extends TypeAdapter<AppSettings> {
..writeByte(27)..write(obj.marqueeAnimType) ..writeByte(27)..write(obj.marqueeAnimType)
..writeByte(28)..write(obj.scaleCardLabel) ..writeByte(28)..write(obj.scaleCardLabel)
..writeByte(29)..write(obj.scaleCardBody) ..writeByte(29)..write(obj.scaleCardBody)
..writeByte(30)..write(obj.scaleRunningText); ..writeByte(30)..write(obj.scaleRunningText)
..writeByte(31)..write(obj.hijriOffsetDays);
} }
} }

View File

@@ -56,6 +56,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
final _iqomahAsharCtrl = TextEditingController(); final _iqomahAsharCtrl = TextEditingController();
final _iqomahMaghribCtrl = TextEditingController(); final _iqomahMaghribCtrl = TextEditingController();
final _iqomahIsyaCtrl = TextEditingController(); final _iqomahIsyaCtrl = TextEditingController();
int _hijriOffsetDays = 0;
@override @override
void initState() { void initState() {
@@ -94,6 +95,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
_iqomahAsharCtrl.text = settings.iqomahAshar.toString(); _iqomahAsharCtrl.text = settings.iqomahAshar.toString();
_iqomahMaghribCtrl.text = settings.iqomahMaghrib.toString(); _iqomahMaghribCtrl.text = settings.iqomahMaghrib.toString();
_iqomahIsyaCtrl.text = settings.iqomahIsya.toString(); _iqomahIsyaCtrl.text = settings.iqomahIsya.toString();
_hijriOffsetDays = settings.hijriOffsetDays;
// Update preview live as admin types // Update preview live as admin types
_khatibCtrl.addListener(() => setState(() {})); _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 { Future<void> _syncData() async {
setState(() => _isSyncing = true); setState(() => _isSyncing = true);
final success = await SyncService.instance.syncMonthlyData(); final success = await SyncService.instance.syncMonthlyData();
@@ -1061,8 +1083,10 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
Widget _buildJadwalTab(double s) { Widget _buildJadwalTab(double s) {
final settings = ref.watch(settingsProvider); final settings = ref.watch(settingsProvider);
final todayScheduleOption = ref.watch(todayScheduleProvider); final todayScheduleOption = ref.watch(todayScheduleProvider);
final displayedHijri = ref.watch(hijriDateProvider).valueOrNull;
return Column( return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
@@ -1128,6 +1152,113 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
SizedBox(height: 64 * s), 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 // Jeda Waktu Iqamah Settings Card
_adminCard(s, child: Column( _adminCard(s, child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -1187,12 +1318,14 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
SizedBox(height: 32 * s), SizedBox(height: 32 * s),
// Schedule Grid // Schedule Grid
Expanded( Builder(
child: Builder(
builder: (context) { builder: (context) {
if (todayScheduleOption == null) { if (todayScheduleOption == null) {
return Center( 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)), child: Text('Data jadwal kosong. Silakan lakukan sinkronisasi.', style: GoogleFonts.manrope(fontSize: 24 * s, color: SacredColors.error)),
),
); );
} }
@@ -1208,6 +1341,8 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
}; };
return GridView.builder( return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4, crossAxisCount: 4,
crossAxisSpacing: 24 * s, crossAxisSpacing: 24 * s,
@@ -1223,8 +1358,178 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
); );
}, },
), ),
) 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),
),
),
],
),
); );
} }

View File

@@ -79,7 +79,10 @@ final todayScheduleProvider = Provider<DailyPrayerSchedule?>((ref) {
final hijriDateProvider = FutureProvider<String>((ref) async { final hijriDateProvider = FutureProvider<String>((ref) async {
final clock = ref.watch(clockProvider).valueOrNull ?? DateTime.now(); final clock = ref.watch(clockProvider).valueOrNull ?? DateTime.now();
final dateOnly = DateTime(clock.year, clock.month, clock.day); final hijriOffsetDays =
ref.watch(settingsProvider.select((s) => s.hijriOffsetDays));
final dateOnly = DateTime(clock.year, clock.month, clock.day)
.add(Duration(days: hijriOffsetDays));
try { try {
return await HijriCalendarService.instance.getHijriLabel(dateOnly); return await HijriCalendarService.instance.getHijriLabel(dateOnly);