diff --git a/lib/data/local/models.dart b/lib/data/local/models.dart index db86daa..77777c0 100644 --- a/lib/data/local/models.dart +++ b/lib/data/local/models.dart @@ -120,6 +120,10 @@ class AppSettings extends HiveObject { @HiveField(30) double scaleRunningText; + // Manual day adjustment applied to displayed Hijri date. + @HiveField(31) + int hijriOffsetDays; + AppSettings({ this.masjidName = 'Masjid Al-Ikhlas', this.masjidAddress = 'Jl. Kebaikan No. 1', @@ -155,6 +159,7 @@ class AppSettings extends HiveObject { this.scaleCardLabel = 1.0, this.scaleCardBody = 1.0, this.scaleRunningText = 1.0, + this.hijriOffsetDays = 0, }); AppSettings copyWith({ @@ -189,6 +194,7 @@ class AppSettings extends HiveObject { double? scaleCardLabel, double? scaleCardBody, double? scaleRunningText, + int? hijriOffsetDays, }) { return AppSettings( masjidName: masjidName ?? this.masjidName, @@ -222,6 +228,7 @@ class AppSettings extends HiveObject { scaleCardLabel: scaleCardLabel ?? this.scaleCardLabel, scaleCardBody: scaleCardBody ?? this.scaleCardBody, scaleRunningText: scaleRunningText ?? this.scaleRunningText, + hijriOffsetDays: hijriOffsetDays ?? this.hijriOffsetDays, ); } } @@ -270,13 +277,14 @@ class AppSettingsAdapter extends TypeAdapter { scaleCardLabel: (fields[28] as num?)?.toDouble() ?? 1.0, scaleCardBody: (fields[29] as num?)?.toDouble() ?? 1.0, scaleRunningText: (fields[30] as num?)?.toDouble() ?? 1.0, + hijriOffsetDays: fields[31] as int? ?? 0, ); } @override void write(BinaryWriter writer, AppSettings obj) { writer - ..writeByte(31) + ..writeByte(32) ..writeByte(0)..write(obj.masjidName) ..writeByte(1)..write(obj.masjidAddress) ..writeByte(2)..write(obj.cityIdApi) @@ -307,7 +315,8 @@ class AppSettingsAdapter extends TypeAdapter { ..writeByte(27)..write(obj.marqueeAnimType) ..writeByte(28)..write(obj.scaleCardLabel) ..writeByte(29)..write(obj.scaleCardBody) - ..writeByte(30)..write(obj.scaleRunningText); + ..writeByte(30)..write(obj.scaleRunningText) + ..writeByte(31)..write(obj.hijriOffsetDays); } } diff --git a/lib/features/admin/admin_screen.dart b/lib/features/admin/admin_screen.dart index 4cd162e..21802e2 100644 --- a/lib/features/admin/admin_screen.dart +++ b/lib/features/admin/admin_screen.dart @@ -56,6 +56,7 @@ class _AdminScreenState extends ConsumerState { final _iqomahAsharCtrl = TextEditingController(); final _iqomahMaghribCtrl = TextEditingController(); final _iqomahIsyaCtrl = TextEditingController(); + int _hijriOffsetDays = 0; @override void initState() { @@ -94,6 +95,7 @@ class _AdminScreenState extends ConsumerState { _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 { } } + Future _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 _syncData() async { setState(() => _isSyncing = true); final success = await SyncService.instance.syncMonthlyData(); @@ -1061,10 +1083,12 @@ class _AdminScreenState extends ConsumerState { 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 { 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 { 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), + ), + ), + ], + ), ); } diff --git a/lib/providers.dart b/lib/providers.dart index 1570bbf..5917432 100644 --- a/lib/providers.dart +++ b/lib/providers.dart @@ -79,7 +79,10 @@ final todayScheduleProvider = Provider((ref) { final hijriDateProvider = FutureProvider((ref) async { 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 { return await HijriCalendarService.instance.getHijriLabel(dateOnly);