import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:hugeicons/hugeicons.dart'; import '../../core/sacred_tokens.dart'; import '../../providers.dart'; import '../../data/services/sync_service.dart'; import '../../data/services/myquran_service.dart'; import 'package:file_picker/file_picker.dart'; import 'dart:io'; class AdminScreen extends ConsumerStatefulWidget { const AdminScreen({super.key}); @override ConsumerState createState() => _AdminScreenState(); } class _AdminScreenState extends ConsumerState { final _masjidNameCtrl = TextEditingController(); final _masjidAddressCtrl = TextEditingController(); final _cityCtrl = TextEditingController(); // Displays DisplayName or CityID final _mainDurCtrl = TextEditingController(); final _slideDurCtrl = TextEditingController(); int _selectedTab = 0; bool _isSyncing = false; int _textScaleIndex = 1; List _slideshowImages = []; bool _useUnsplash = false; final _unsplashKeywordCtrl = TextEditingController(); final _unsplashRotationCtrl = TextEditingController(); // Branded background String? _brandedBgImage; // Running text repeater String _marqueeAnimType = 'marquee'; List _runningTexts = []; List _runningTextDurations = []; // Granular text group scales double _scaleCardLabel = 1.0; double _scaleCardBody = 1.0; double _scaleRunningText = 1.0; // Jumat fields final _khatibCtrl = TextEditingController(); final _imamCtrl = TextEditingController(); // Iqomah Jeda fields final _iqomahSubuhCtrl = TextEditingController(); final _iqomahDzuhurCtrl = TextEditingController(); final _iqomahAsharCtrl = TextEditingController(); final _iqomahMaghribCtrl = TextEditingController(); final _iqomahIsyaCtrl = TextEditingController(); int _hijriOffsetDays = 0; @override void initState() { super.initState(); final settings = ref.read(settingsProvider); _masjidNameCtrl.text = settings.masjidName; _masjidAddressCtrl.text = settings.masjidAddress; _cityCtrl.text = '${settings.cityDisplayName} (${settings.cityIdApi})'; _mainDurCtrl.text = settings.mainScreenDurationSec.toString(); _slideDurCtrl.text = settings.slideDurationSec.toString(); _textScaleIndex = settings.textScaleIndex; _slideshowImages = List.from(settings.slideshowImages); _useUnsplash = settings.useUnsplashBackground; _unsplashKeywordCtrl.text = settings.unsplashKeyword; _unsplashRotationCtrl.text = settings.unsplashRotationHours.toString(); _brandedBgImage = settings.brandedBgImage; _marqueeAnimType = settings.marqueeAnimType; _runningTexts = List.from(settings.runningTexts); _runningTextDurations = List.from( settings.runningTextDurations.isNotEmpty ? settings.runningTextDurations : List.filled(settings.runningTexts.length, 12), ); // Ensure durations list length matches texts while (_runningTextDurations.length < _runningTexts.length) { _runningTextDurations.add(12); } _scaleCardLabel = settings.scaleCardLabel; _scaleCardBody = settings.scaleCardBody; _scaleRunningText = settings.scaleRunningText; _khatibCtrl.text = settings.khatibName; _imamCtrl.text = settings.imamName; _iqomahSubuhCtrl.text = settings.iqomahSubuh.toString(); _iqomahDzuhurCtrl.text = settings.iqomahDzuhur.toString(); _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(() {})); _imamCtrl.addListener(() => setState(() {})); } @override void dispose() { _masjidNameCtrl.dispose(); _masjidAddressCtrl.dispose(); _cityCtrl.dispose(); _mainDurCtrl.dispose(); _slideDurCtrl.dispose(); _unsplashKeywordCtrl.dispose(); _unsplashRotationCtrl.dispose(); _khatibCtrl.dispose(); _imamCtrl.dispose(); _iqomahSubuhCtrl.dispose(); _iqomahDzuhurCtrl.dispose(); _iqomahAsharCtrl.dispose(); _iqomahMaghribCtrl.dispose(); _iqomahIsyaCtrl.dispose(); super.dispose(); } Future _saveIdentity() async { await ref.read(settingsProvider.notifier).updateSettings((s) { s.masjidName = _masjidNameCtrl.text.trim(); s.masjidAddress = _masjidAddressCtrl.text.trim(); // cityId is saved instantly when selected from dialog return s; }); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Pengaturan berhasil disimpan', style: GoogleFonts.manrope()), backgroundColor: SacredColors.primaryContainer, ), ); } } Future _saveTampilan() async { await ref.read(settingsProvider.notifier).updateSettings((s) { s.textScaleIndex = _textScaleIndex; s.slideshowImages = List.from(_slideshowImages); s.mainScreenDurationSec = int.tryParse(_mainDurCtrl.text.trim()) ?? 15; s.slideDurationSec = int.tryParse(_slideDurCtrl.text.trim()) ?? 10; s.useUnsplashBackground = _useUnsplash; s.unsplashKeyword = _unsplashKeywordCtrl.text.trim().isEmpty ? 'mosque' : _unsplashKeywordCtrl.text.trim(); s.unsplashRotationHours = int.tryParse(_unsplashRotationCtrl.text.trim()) ?? 6; s.brandedBgImage = _brandedBgImage; s.runningTexts = List.from(_runningTexts); s.runningTextDurations = List.from(_runningTextDurations); s.marqueeAnimType = _marqueeAnimType; s.scaleCardLabel = _scaleCardLabel; s.scaleCardBody = _scaleCardBody; s.scaleRunningText = _scaleRunningText; return s; }); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Pengaturan Tampilan berhasil disimpan', style: GoogleFonts.manrope()), backgroundColor: SacredColors.primaryContainer, ), ); } } Future _saveIqomahSettings() async { await ref.read(settingsProvider.notifier).updateSettings((s) { s.iqomahSubuh = int.tryParse(_iqomahSubuhCtrl.text.trim()) ?? 15; s.iqomahDzuhur = int.tryParse(_iqomahDzuhurCtrl.text.trim()) ?? 10; s.iqomahAshar = int.tryParse(_iqomahAsharCtrl.text.trim()) ?? 10; s.iqomahMaghrib = int.tryParse(_iqomahMaghribCtrl.text.trim()) ?? 10; s.iqomahIsya = int.tryParse(_iqomahIsyaCtrl.text.trim()) ?? 10; return s; }); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Jeda Iqamah berhasil disimpan', style: GoogleFonts.manrope()), backgroundColor: SacredColors.primaryContainer, ), ); } } 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(); setState(() => _isSyncing = false); if (mounted) { ref.invalidate(todayScheduleProvider); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( success ? 'Sinkronisasi jadwal berhasil' : 'Sinkronisasi gagal. Periksa koneksi internet.', style: GoogleFonts.manrope()), backgroundColor: success ? SacredColors.primaryContainer : SacredColors.errorContainer, ), ); } } Future _showCitySearchDialog(double s) async { final queryCtrl = TextEditingController(); List> results = []; bool isSearching = false; await showDialog( context: context, builder: (ctx) { return StatefulBuilder( builder: (context, setDialogState) { return Dialog( backgroundColor: SacredColors.surfaceContainerLowest, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(SacredRadii.xl)), child: Container( width: 800 * s, height: 600 * s, padding: EdgeInsets.all(40 * s), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Cari Kota / Kabupaten', style: GoogleFonts.plusJakartaSans( fontSize: 32 * s, fontWeight: FontWeight.bold, color: SacredColors.primary)), SizedBox(height: 24 * s), Row( children: [ Expanded( child: TextField( controller: queryCtrl, style: GoogleFonts.manrope(fontSize: 24 * s, color: SacredColors.onSurface), decoration: InputDecoration( hintText: 'Misal: Yogyakarta', filled: true, fillColor: SacredColors.surfaceContainerLow, border: OutlineInputBorder(borderRadius: BorderRadius.circular(SacredRadii.md)), ), ), ), SizedBox(width: 16 * s), ElevatedButton.icon( onPressed: () async { final query = queryCtrl.text.trim(); if (query.isEmpty) return; setDialogState(() => isSearching = true); final res = await MyQuranSholatService.instance.searchCity(query); setDialogState(() { results = res; isSearching = false; }); }, icon: isSearching ? SizedBox(width: 20*s, height: 20*s, child: const CircularProgressIndicator(color: SacredColors.onPrimary, strokeWidth: 2)) : const HugeIcon(icon: HugeIcons.strokeRoundedSearch01, color: SacredColors.onPrimary), label: Text('CARI', style: GoogleFonts.plusJakartaSans(fontSize: 20*s, fontWeight: FontWeight.bold)), style: ElevatedButton.styleFrom( backgroundColor: SacredColors.primary, foregroundColor: SacredColors.onPrimary, padding: EdgeInsets.symmetric(horizontal: 32 * s, vertical: 24 * s), ), ), ], ), SizedBox(height: 32 * s), Expanded( child: results.isEmpty && !isSearching ? Center(child: Text('Tidak ada hasil', style: GoogleFonts.manrope(fontSize: 20 * s, color: SacredColors.onSurfaceVariant))) : ListView.builder( itemCount: results.length, itemBuilder: (context, index) { final city = results[index]; return Padding( padding: EdgeInsets.only(bottom: 8 * s), child: ListTile( title: Text(city['lokasi'] ?? '', style: GoogleFonts.plusJakartaSans(fontSize: 24 * s, color: SacredColors.onSurface)), subtitle: Text('ID: ${city['id']}', style: GoogleFonts.manrope(fontSize: 18 * s, color: SacredColors.primary)), tileColor: SacredColors.surfaceContainerLow, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(SacredRadii.sm)), contentPadding: EdgeInsets.symmetric(horizontal: 24 * s, vertical: 8 * s), onTap: () async { final id = city['id'].toString(); final loc = city['lokasi'].toString(); await ref.read(settingsProvider.notifier).updateSettings((s) { s.cityIdApi = id; s.cityDisplayName = loc; return s; }); if (!mounted || !ctx.mounted) return; setState(() { _cityCtrl.text = '$loc ($id)'; }); Navigator.pop(ctx); }, ), ); }, ), ), ], ), ), ); } ); }, ); } @override Widget build(BuildContext context) { final size = MediaQuery.of(context).size; final s = size.width / 1920; return Scaffold( backgroundColor: SacredColors.background, appBar: AppBar( backgroundColor: SacredColors.surfaceContainerLowest, title: Text( 'PENGATURAN SISTEM', style: GoogleFonts.plusJakartaSans( fontSize: 24 * s, fontWeight: FontWeight.w700, color: SacredColors.onSurface, letterSpacing: 2 * s, ), ), iconTheme: const IconThemeData(color: SacredColors.primary), ), body: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Nav rail area Container( width: 350 * s, color: SacredColors.surfaceContainerLow, padding: EdgeInsets.all(32 * s), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _NavButton( title: 'IDENTITAS MASJID', icon: HugeIcons.strokeRoundedHome01, isActive: _selectedTab == 0, scale: s, onTap: () => setState(() => _selectedTab = 0), ), SizedBox(height: 16 * s), _NavButton( title: 'JADWAL & SINKRONISASI', icon: HugeIcons.strokeRoundedCalendar01, isActive: _selectedTab == 1, scale: s, onTap: () => setState(() => _selectedTab = 1), ), SizedBox(height: 16 * s), _NavButton( title: 'TAMPILAN & MEDIA', icon: HugeIcons.strokeRoundedImage01, isActive: _selectedTab == 2, scale: s, onTap: () => setState(() => _selectedTab = 2), ), SizedBox(height: 16 * s), _NavButton( title: 'PENGATURAN JUMAT', icon: HugeIcons.strokeRoundedCalendar01, isActive: _selectedTab == 3, scale: s, onTap: () => setState(() => _selectedTab = 3), ), SizedBox(height: 16 * s), _NavButton( title: 'SIMULASI', icon: HugeIcons.strokeRoundedClock01, isActive: _selectedTab == 4, scale: s, onTap: () => setState(() => _selectedTab = 4), ), ], ), ), // Content area Expanded( child: Padding( padding: EdgeInsets.all(64 * s), child: _selectedTab == 0 ? _buildIdentityTab(s) : _selectedTab == 1 ? _buildJadwalTab(s) : _selectedTab == 2 ? _buildTampilanTab(s) : _selectedTab == 3 ? _buildJumatTab(s) : _buildSimulasiTab(s), ), ), ], ), ); } Future _saveJumat() async { await ref.read(settingsProvider.notifier).updateSettings((s) { s.khatibName = _khatibCtrl.text.trim(); s.imamName = _imamCtrl.text.trim(); return s; }); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Data Jumat berhasil disimpan', style: GoogleFonts.manrope()), backgroundColor: SacredColors.primaryContainer, ), ); } } Widget _buildJumatTab(double s) { return SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Pengaturan Jumat', style: GoogleFonts.plusJakartaSans( fontSize: 48 * s, fontWeight: FontWeight.w700, color: SacredColors.secondary), ), SizedBox(height: 8 * s), Text( 'Data di bawah akan tampil setiap hari Jumat: pada layar utama (banner bawah jam) dan layar Persiapan Khutbah.', style: GoogleFonts.manrope(fontSize: 16 * s, color: SacredColors.onSurfaceVariant), ), SizedBox(height: 40 * s), _adminCard(s, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _sectionLabel('Petugas Shalat Jumat', s), SizedBox(height: 8 * s), Text( 'Nama Khatib dan Imam tampil di layar utama setiap Jumat dan di layar Persiapan Khutbah.', style: GoogleFonts.manrope(fontSize: 14 * s, color: SacredColors.onSurfaceVariant), ), SizedBox(height: 24 * s), _buildTextField('Nama Khatib Minggu Ini', _khatibCtrl, s), SizedBox(height: 16 * s), _buildTextField('Nama Imam Minggu Ini', _imamCtrl, s), SizedBox(height: 32 * s), // Preview chip if (_khatibCtrl.text.isNotEmpty || _imamCtrl.text.isNotEmpty) ...[ Text('Preview tampilan:', style: GoogleFonts.manrope(fontSize: 14 * s, color: SacredColors.onSurfaceVariant)), SizedBox(height: 10 * s), Container( padding: EdgeInsets.all(20 * s), decoration: BoxDecoration( color: SacredColors.background, borderRadius: BorderRadius.circular(SacredRadii.lg), border: Border.all(color: SacredColors.secondary.withValues(alpha: 0.2)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.star_rounded, color: SacredColors.secondary, size: 16 * s), SizedBox(width: 8 * s), Text('JUMAT MUBARAK', style: GoogleFonts.plusJakartaSans( fontSize: 14 * s, fontWeight: FontWeight.w800, color: SacredColors.secondary, letterSpacing: 2)), SizedBox(width: 8 * s), Icon(Icons.star_rounded, color: SacredColors.secondary, size: 16 * s), SizedBox(width: 24 * s), if (_khatibCtrl.text.isNotEmpty) Text('KHATIB ${_khatibCtrl.text}', style: GoogleFonts.manrope( fontSize: 14 * s, color: SacredColors.onSurface)), if (_khatibCtrl.text.isNotEmpty && _imamCtrl.text.isNotEmpty) Text(' | ', style: GoogleFonts.manrope(fontSize: 14 * s, color: SacredColors.onSurfaceVariant)), if (_imamCtrl.text.isNotEmpty) Text('IMAM ${_imamCtrl.text}', style: GoogleFonts.manrope( fontSize: 14 * s, color: SacredColors.onSurface)), ], ), ), SizedBox(height: 24 * s), ], ElevatedButton.icon( onPressed: _saveJumat, style: ElevatedButton.styleFrom( backgroundColor: SacredColors.secondary, foregroundColor: Colors.black, 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'), ), ], )), SizedBox(height: 32 * s), // Info box _adminCard(s, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _sectionLabel('Kapan Digunakan?', s), SizedBox(height: 16 * s), _infoRow(Icons.tv, 'Layar Utama (Jumat)', 'Banner bawah jam berubah ke JUMAT MUBARAK, nama khatib & imam tampil di bawahnya.', s), SizedBox(height: 12 * s), _infoRow(Icons.timer_outlined, 'Layar Persiapan Khutbah', 'Saat menuju iqomah Dzuhur di hari Jumat, layar menampilkan judul PERSIAPAN KHUTBAH beserta nama petugas.', s), SizedBox(height: 12 * s), _infoRow(Icons.info_outline, 'Durasi Blank Screen', 'Durasi Black Screen setelah shalat Jumat dapat diatur di tab Jadwal & Sinkronisasi.', s), ], )), ], ), ); } Widget _infoRow(IconData icon, String title, String desc, double s) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(icon, color: SacredColors.secondary, size: 22 * s), SizedBox(width: 12 * s), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: GoogleFonts.manrope(fontSize: 15 * s, fontWeight: FontWeight.w700, color: SacredColors.onSurface)), SizedBox(height: 4 * s), Text(desc, style: GoogleFonts.manrope(fontSize: 13 * s, color: SacredColors.onSurfaceVariant)), ], ), ), ], ); } Widget _buildTampilanTab(double s) { return SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Pengaturan Tampilan & Media', style: GoogleFonts.plusJakartaSans( fontSize: 48 * s, fontWeight: FontWeight.w700, color: SacredColors.primary, ), ), SizedBox(height: 48 * s), // ── Row 1: General settings + Background ── Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Left: Typography & Timers Expanded( child: _adminCard(s, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _sectionLabel('Tipografi & Skala Teks', s), SizedBox(height: 12 * s), DropdownButtonFormField( initialValue: _textScaleIndex, onChanged: (val) => setState(() => _textScaleIndex = val ?? 1), items: const [ DropdownMenuItem(value: 0, child: Text('Kecil (Small)')), DropdownMenuItem(value: 1, child: Text('Normal (Medium)')), DropdownMenuItem(value: 2, child: Text('Besar (Large)')), ], style: GoogleFonts.plusJakartaSans(fontSize: 22 * s, color: SacredColors.onSurface), dropdownColor: SacredColors.surfaceContainerHighest, decoration: InputDecoration( filled: true, fillColor: SacredColors.surfaceContainerLowest, border: OutlineInputBorder(borderRadius: BorderRadius.circular(SacredRadii.md)), ), ), SizedBox(height: 28 * s), _buildTextField('Durasi Layar Utama (Detik)', _mainDurCtrl, s), SizedBox(height: 24 * s), _buildTextField('Durasi Tiap Slideshow (Detik)', _slideDurCtrl, s), SizedBox(height: 40 * s), _sectionLabel('Ukuran Teks Per Kelompok', s), SizedBox(height: 8 * s), Text( 'Kontrol ukuran teks secara spesifik per kelompok, terlepas dari skala global di atas.', style: GoogleFonts.manrope(fontSize: 14 * s, color: SacredColors.onSurfaceVariant), ), SizedBox(height: 20 * s), _scaleSlider( s: s, label: 'Label Shalat (Nama: SUBUH, DZUHUR…)', value: _scaleCardLabel, onChanged: (v) => setState(() => _scaleCardLabel = v), ), SizedBox(height: 16 * s), _scaleSlider( s: s, label: 'Waktu & Iqamah pada kartu jadwal', value: _scaleCardBody, onChanged: (v) => setState(() => _scaleCardBody = v), ), SizedBox(height: 16 * s), _scaleSlider( s: s, label: 'Teks Berjalan (Running Text)', value: _scaleRunningText, onChanged: (v) => setState(() => _scaleRunningText = v), ), SizedBox(height: 40 * s), _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)), value: _useUnsplash, onChanged: (val) => setState(() => _useUnsplash = val), activeThumbColor: SacredColors.primary, contentPadding: EdgeInsets.zero, ), if (_useUnsplash) ...[ SizedBox(height: 12 * s), _buildTextField('Kata Kunci (Contoh: mosque, architecture)', _unsplashKeywordCtrl, s), SizedBox(height: 12 * s), _buildTextField('Rotasi Foto (Jam)', _unsplashRotationCtrl, s), ], SizedBox(height: 56 * s), ElevatedButton.icon( onPressed: _saveTampilan, style: ElevatedButton.styleFrom( backgroundColor: SacredColors.primary, foregroundColor: SacredColors.onPrimary, 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'), ), ], )), ), SizedBox(width: 32 * s), // Right: Branded Background + Slideshow Expanded( child: Column( children: [ // Branded Background Card _adminCard(s, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _sectionLabel('Foto Latar Utama (Branding Masjid)', s), SizedBox(height: 16 * s), if (_brandedBgImage != null && _brandedBgImage!.isNotEmpty) ...[ ClipRRect( borderRadius: BorderRadius.circular(SacredRadii.md), child: Image.file( File(_brandedBgImage!), height: 120 * s, width: double.infinity, fit: BoxFit.cover, ), ), SizedBox(height: 12 * s), Row( children: [ Expanded( child: Text( _brandedBgImage!.split('/').last, style: GoogleFonts.manrope(fontSize: 14 * s, color: SacredColors.onSurfaceVariant), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), IconButton( icon: HugeIcon(icon: HugeIcons.strokeRoundedDelete01, color: SacredColors.error, size: 22 * s), onPressed: () { setState(() => _brandedBgImage = null); _saveTampilan(); }, ), ], ), ] else Text('Belum ada foto latar masjid.', style: GoogleFonts.manrope(fontSize: 16 * s, color: SacredColors.onSurfaceVariant)), SizedBox(height: 16 * s), ElevatedButton.icon( onPressed: () async { final res = await FilePicker.platform.pickFiles(type: FileType.image); 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( backgroundColor: SacredColors.secondary, foregroundColor: SacredColors.onSecondary, padding: EdgeInsets.symmetric(horizontal: 24 * s, vertical: 16 * s), ), ), ], )), SizedBox(height: 24 * s), // Slideshow Gallery Card _adminCard(s, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _sectionLabel('Galeri Gambar Slideshow', s), ElevatedButton.icon( onPressed: () async { final res = await FilePicker.platform.pickFiles(type: FileType.image, allowMultiple: true); if (res != null) { setState(() { for (var path in res.paths) { if (path != null && !_slideshowImages.contains(path)) { _slideshowImages.add(path); } } }); _saveTampilan(); } }, icon: HugeIcon(icon: HugeIcons.strokeRoundedPlusSign, color: SacredColors.onSecondary, size: 18 * s), label: Text('TAMBAH FOTO', style: TextStyle(fontSize: 14 * s)), style: ElevatedButton.styleFrom( backgroundColor: SacredColors.secondary, foregroundColor: SacredColors.onSecondary, padding: EdgeInsets.symmetric(horizontal: 20 * s, vertical: 14 * s), ), ), ], ), SizedBox(height: 16 * s), if (_slideshowImages.isEmpty) Text('Belum ada gambar slideshow.', style: GoogleFonts.manrope(fontSize: 16 * s, color: SacredColors.onSurfaceVariant)) else ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: _slideshowImages.length, itemBuilder: (context, idx) { final path = _slideshowImages[idx]; return ListTile( leading: ClipRRect( borderRadius: BorderRadius.circular(SacredRadii.sm), child: Image.file(File(path), width: 56 * s, height: 56 * s, fit: BoxFit.cover), ), title: Text(path.split('/').last, maxLines: 1, overflow: TextOverflow.ellipsis, style: GoogleFonts.manrope(fontSize: 16 * s, color: SacredColors.onSurface)), trailing: IconButton( icon: HugeIcon(icon: HugeIcons.strokeRoundedDelete01, color: SacredColors.error, size: 20 * s), onPressed: () { setState(() => _slideshowImages.removeAt(idx)); _saveTampilan(); }, ), ); }, ), ], )), ], ), ), ], ), SizedBox(height: 40 * s), // ── Row 2: Running Text Repeater ── _adminCard(s, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _sectionLabel('Running Text / Pengumuman', s), Row( children: [ Text('Mode Animasi:', style: GoogleFonts.manrope(fontSize: 16 * s, color: SacredColors.onSurfaceVariant)), SizedBox(width: 12 * s), SegmentedButton( 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))), ], selected: {_marqueeAnimType}, onSelectionChanged: (val) => setState(() => _marqueeAnimType = val.first), style: ButtonStyle( backgroundColor: WidgetStateProperty.resolveWith((states) => states.contains(WidgetState.selected) ? SacredColors.primary : SacredColors.surfaceContainerLowest), foregroundColor: WidgetStateProperty.resolveWith((states) => states.contains(WidgetState.selected) ? SacredColors.onPrimary : SacredColors.onSurfaceVariant), ), ), ], ), ], ), SizedBox(height: 24 * s), // Repeater list if (_runningTexts.isEmpty) Padding( padding: EdgeInsets.symmetric(vertical: 16 * s), child: Text('Belum ada teks. Klik TAMBAH untuk menambah baris.', style: GoogleFonts.manrope(fontSize: 16 * s, color: SacredColors.onSurfaceVariant)), ) else ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: _runningTexts.length, separatorBuilder: (_, __) => SizedBox(height: 12 * s), itemBuilder: (context, idx) { final textCtrl = TextEditingController(text: _runningTexts[idx]) ..selection = TextSelection.fromPosition(TextPosition(offset: _runningTexts[idx].length)); final durCtrl = TextEditingController(text: _runningTextDurations[idx].toString()); return Container( padding: EdgeInsets.all(20 * s), decoration: BoxDecoration( color: SacredColors.surfaceContainerLowest, borderRadius: BorderRadius.circular(SacredRadii.md), border: Border.all(color: SacredColors.outlineVariant.withValues(alpha: 0.3)), ), child: Row( children: [ Container( width: 32 * s, height: 32 * s, alignment: Alignment.center, decoration: BoxDecoration( color: SacredColors.surfaceContainerHighest, shape: BoxShape.circle, ), child: Text('${idx + 1}', style: GoogleFonts.manrope(fontSize: 14 * s, fontWeight: FontWeight.w700, color: SacredColors.primary)), ), SizedBox(width: 16 * s), Expanded( flex: 5, child: TextField( controller: textCtrl, style: GoogleFonts.plusJakartaSans(fontSize: 20 * s, color: SacredColors.onSurface), decoration: InputDecoration( hintText: 'Teks pengumuman...', filled: true, fillColor: SacredColors.surfaceContainerLow, border: OutlineInputBorder(borderRadius: BorderRadius.circular(SacredRadii.sm), borderSide: BorderSide.none), isDense: true, contentPadding: EdgeInsets.symmetric(horizontal: 16 * s, vertical: 14 * s), ), onChanged: (val) => _runningTexts[idx] = val, ), ), SizedBox(width: 12 * s), SizedBox( width: 100 * s, child: TextField( controller: durCtrl, keyboardType: TextInputType.number, style: GoogleFonts.plusJakartaSans(fontSize: 20 * s, color: SacredColors.onSurface), decoration: InputDecoration( hintText: 'Detik', filled: true, fillColor: SacredColors.surfaceContainerLow, border: OutlineInputBorder(borderRadius: BorderRadius.circular(SacredRadii.sm), borderSide: BorderSide.none), isDense: true, contentPadding: EdgeInsets.symmetric(horizontal: 16 * s, vertical: 14 * s), suffixText: 'dtk', ), onChanged: (val) => _runningTextDurations[idx] = int.tryParse(val) ?? 12, ), ), SizedBox(width: 8 * s), IconButton( icon: HugeIcon(icon: HugeIcons.strokeRoundedDelete01, color: SacredColors.error, size: 22 * s), onPressed: () { setState(() { _runningTexts.removeAt(idx); _runningTextDurations.removeAt(idx); }); }, ), ], ), ); }, ), SizedBox(height: 20 * s), Row( children: [ OutlinedButton.icon( onPressed: () { 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( side: BorderSide(color: SacredColors.primary.withValues(alpha: 0.5)), padding: EdgeInsets.symmetric(horizontal: 24 * s, vertical: 16 * s), ), ), SizedBox(width: 16 * s), ElevatedButton.icon( onPressed: _saveTampilan, style: ElevatedButton.styleFrom( 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)), ), ], ), ], )), SizedBox(height: 40 * s), ], ), ); } Widget _adminCard(double s, {required Widget child}) { return 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 _sectionLabel(String label, double s) { return Text( label, style: GoogleFonts.plusJakartaSans( fontSize: 20 * s, fontWeight: FontWeight.w700, color: SacredColors.primary, ), ); } Widget _buildIdentityTab(double s) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Identitas & Lokasi Masjid', style: GoogleFonts.plusJakartaSans( fontSize: 48 * s, fontWeight: FontWeight.w700, color: SacredColors.primary, ), ), SizedBox(height: 48 * s), Container( width: 800 * s, padding: EdgeInsets.all(40 * 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: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildTextField('Nama Masjid', _masjidNameCtrl, s), SizedBox(height: 32 * s), _buildTextField('Alamat Lengkap', _masjidAddressCtrl, s, maxLines: 2), SizedBox(height: 32 * s), // City API Config Text( 'Lokasi Jadwal Shalat (MyQuran API)', style: GoogleFonts.manrope( fontSize: 16 * s, fontWeight: FontWeight.w600, color: SacredColors.onSurfaceVariant, ), ), SizedBox(height: 12 * s), 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), ), ), ), SizedBox(width: 16 * s), ElevatedButton.icon( onPressed: () => _showCitySearchDialog(s), icon: HugeIcon(icon: HugeIcons.strokeRoundedSearch01, color: SacredColors.onPrimary), label: Text('CARI KOTA', style: TextStyle(fontSize: 16 * s)), style: ElevatedButton.styleFrom( backgroundColor: SacredColors.secondary, foregroundColor: SacredColors.onPrimary, padding: EdgeInsets.symmetric(horizontal: 24 * s, vertical: 24 * s), ), ), ], ), SizedBox(height: 64 * s), ElevatedButton.icon( onPressed: _saveIdentity, style: ElevatedButton.styleFrom( backgroundColor: SacredColors.primary, foregroundColor: SacredColors.onPrimary, 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'), ), ], ), ), ], ); } Widget _buildJadwalTab(double s) { final settings = ref.watch(settingsProvider); final todayScheduleOption = ref.watch(todayScheduleProvider); final displayedHijri = ref.watch(hijriDateProvider).valueOrNull; return SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Jadwal & Sinkronisasi', style: GoogleFonts.plusJakartaSans( fontSize: 48 * s, fontWeight: FontWeight.w700, color: SacredColors.primary, ), ), SizedBox(height: 48 * s), // Sync Card Container( width: double.infinity, padding: EdgeInsets.all(40 * s), decoration: BoxDecoration( color: SacredColors.surfaceContainerLow, borderRadius: BorderRadius.circular(SacredRadii.xl), border: Border.all(color: SacredColors.outlineVariant.withValues(alpha: 0.4)), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Status Data Jadwal', style: GoogleFonts.manrope(fontSize: 20 * s, fontWeight: FontWeight.w700, color: SacredColors.onSurfaceVariant, letterSpacing: 1 * s), ), SizedBox(height: 24 * s), Row( children: [ _buildStatusRow('Terakhir Sync', settings.lastSyncDate ?? 'Belum pernah', HugeIcons.strokeRoundedClock01, s), SizedBox(width: 48 * s), _buildStatusRow('Sumber Data', 'api.myquran.com', HugeIcons.strokeRoundedDatabase01, s), SizedBox(width: 48 * s), _buildStatusRow('Lokasi Data', settings.cityDisplayName, HugeIcons.strokeRoundedLocation01, s), ], ), ], ), ), ElevatedButton.icon( onPressed: _isSyncing ? null : _syncData, style: ElevatedButton.styleFrom( backgroundColor: SacredColors.secondary, foregroundColor: SacredColors.onSecondary, 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'), ) ], ), ), 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, children: [ _sectionLabel('Jeda Waktu Iqamah (Menit)', s), SizedBox(height: 8 * s), Text( 'Tentukan durasi hitung mundur dari Adzan selesai (1 menit setelah masuk waktu) hingga iqamah. Selama jeda ini, jamaah dapat melakukan shalat sunnah.', style: GoogleFonts.manrope(fontSize: 14 * s, color: SacredColors.onSurfaceVariant), ), SizedBox(height: 32 * s), Row( children: [ Expanded(child: _buildTextField('Iqamah Subuh', _iqomahSubuhCtrl, s)), SizedBox(width: 16 * s), Expanded(child: _buildTextField('Iqamah Dzuhur', _iqomahDzuhurCtrl, s)), SizedBox(width: 16 * s), Expanded(child: _buildTextField('Iqamah Ashar', _iqomahAsharCtrl, s)), ], ), SizedBox(height: 16 * s), Row( children: [ Expanded(child: _buildTextField('Iqamah Maghrib', _iqomahMaghribCtrl, s)), SizedBox(width: 16 * s), Expanded(child: _buildTextField('Iqamah Isya', _iqomahIsyaCtrl, s)), SizedBox(width: 16 * s), Expanded(child: SizedBox()), // spacer ], ), SizedBox(height: 32 * s), ElevatedButton.icon( onPressed: _saveIqomahSettings, style: ElevatedButton.styleFrom( backgroundColor: SacredColors.secondary, foregroundColor: Colors.black, 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 JEDA IQAMAH'), ), ], )), SizedBox(height: 64 * s), Text( 'Pratinjau Jadwal Hari Ini', style: GoogleFonts.plusJakartaSans( fontSize: 32 * s, fontWeight: FontWeight.w700, color: SacredColors.onSurface, ), ), SizedBox(height: 32 * s), // Schedule Grid 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( 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), ), ), ], ), ); } Widget _buildPrayerCard(String name, String time, double s) { return Container( decoration: BoxDecoration( color: SacredColors.surfaceContainerLowest.withValues(alpha: 0.5), borderRadius: BorderRadius.circular(SacredRadii.lg), border: Border.all(color: SacredColors.outlineVariant.withValues(alpha: 0.3)), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.2), blurRadius: 10 * s, offset: Offset(0, 4 * s), ) ] ), padding: EdgeInsets.symmetric(horizontal: 32 * s, vertical: 24 * s), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ Text( name, style: GoogleFonts.manrope( fontSize: 18 * s, fontWeight: FontWeight.w600, color: SacredColors.onSurfaceVariant, letterSpacing: 2 * s, ), ), SizedBox(height: 8 * s), Text( time, style: GoogleFonts.plusJakartaSans( fontSize: 42 * s, fontWeight: FontWeight.w800, color: SacredColors.primary, ), ), ], ), ); } Widget _buildTextField(String label, TextEditingController ctrl, double s, {int maxLines = 1}) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: GoogleFonts.manrope( fontSize: 16 * s, fontWeight: FontWeight.w600, color: SacredColors.onSurfaceVariant, ), ), 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), ), ), ), ], ); } Widget _buildStatusRow(String label, String value, dynamic icon, double s) { return Row( mainAxisSize: MainAxisSize.min, children: [ Container( padding: EdgeInsets.all(12 * s), decoration: BoxDecoration( color: SacredColors.surfaceContainerHighest, borderRadius: BorderRadius.circular(SacredRadii.sm), ), child: HugeIcon(icon: icon, color: SacredColors.secondary, size: 24 * s), ), SizedBox(width: 16 * s), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: GoogleFonts.manrope(fontSize: 12 * s, color: SacredColors.onSurfaceVariant)), Text(value, style: GoogleFonts.plusJakartaSans(fontSize: 18 * s, fontWeight: FontWeight.w600, color: SacredColors.onSurface)), ], ), ], ); } Widget _scaleSlider({ required double s, required String label, required double value, required ValueChanged onChanged, }) { final pct = (value * 100).round(); const step = 0.05; const presets = [0.75, 1.0, 1.25, 1.5]; 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(label, 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('$pct%', style: GoogleFonts.manrope( fontSize: 16 * s, fontWeight: FontWeight.w800, color: SacredColors.primary)), ), ], ), SizedBox(height: 14 * s), // TV-remote control row Row( children: [ _tvStepBtn(s: s, label: '−−', onPressed: () => onChanged((value - step * 4).clamp(0.5, 2.0))), SizedBox(width: 6 * s), _tvStepBtn(s: s, label: '−', onPressed: () => onChanged((value - step).clamp(0.5, 2.0))), 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: ((value - 0.5) / 1.5).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: () => onChanged((value + step).clamp(0.5, 2.0))), SizedBox(width: 6 * s), _tvStepBtn(s: s, label: '++', onPressed: () => onChanged((value + step * 4).clamp(0.5, 2.0))), ], ), SizedBox(height: 12 * s), // Quick preset chips Row( children: [ Text('Cepat: ', style: GoogleFonts.manrope(fontSize: 12 * s, color: SacredColors.onSurfaceVariant)), ...presets.map((p) { final isActive = (value - p).abs() < 0.02; return Padding( padding: EdgeInsets.only(right: 8 * s), child: InkWell( focusColor: SacredColors.primary.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(SacredRadii.sm), onTap: () => onChanged(p), 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('${(p * 100).round()}%', style: GoogleFonts.manrope( fontSize: 13 * s, fontWeight: FontWeight.w600, color: isActive ? SacredColors.onPrimary : SacredColors.onSurfaceVariant)), ), ), ); }), ], ), SizedBox(height: 6 * s), Text( 'TV Remote: gunakan ↑↓ untuk pindah fokus, tekan OK pada −/+ untuk mengubah nilai.', style: GoogleFonts.manrope(fontSize: 11 * s, color: SacredColors.onSurfaceVariant.withValues(alpha: 0.7)), ), ], ), ); } Widget _tvStepBtn({required double s, required String label, required VoidCallback onPressed}) { return Material( color: Colors.transparent, child: InkWell( focusColor: SacredColors.primary.withValues(alpha: 0.35), hoverColor: SacredColors.primary.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(SacredRadii.sm), onTap: onPressed, child: Container( width: 42 * s, height: 38 * s, alignment: Alignment.center, decoration: BoxDecoration( border: Border.all(color: SacredColors.outlineVariant.withValues(alpha: 0.4)), borderRadius: BorderRadius.circular(SacredRadii.sm), color: SacredColors.surfaceContainerHighest, ), child: Text( label, style: GoogleFonts.manrope( fontSize: 15 * s, fontWeight: FontWeight.w700, color: SacredColors.onSurface, ), ), ), ), ); } Widget _buildSimulasiTab(double s) { return SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Mode Simulasi Pengembang', style: GoogleFonts.plusJakartaSans( fontSize: 48 * s, fontWeight: FontWeight.w700, color: SacredColors.primary, ), ), SizedBox(height: 16 * s), Text( 'Gunakan tombol di bawah ini untuk melihat pratinjau bagaimana aplikasi bereaksi terhadap berbagai waktu dan status tanpa harus menunggu waktu sebenarnya.\nFitur ini bekerja dengan menggeser waktu aplikasi (Time Travel).', style: GoogleFonts.manrope(fontSize: 18 * s, color: SacredColors.onSurfaceVariant), ), SizedBox(height: 48 * s), Wrap( spacing: 24 * s, runSpacing: 24 * s, children: [ _simulasiCard( s: s, title: 'Reset Waktu Asli', icon: HugeIcons.strokeRoundedHome01, desc: 'Kembali ke waktu saat ini secara sinkron dengan jam sistem.', onTap: () => _simulateTimeOffset(Duration.zero), ), _simulasiCard( s: s, title: 'Menuju Adzan', icon: HugeIcons.strokeRoundedClock01, desc: 'Melompat ke 2 menit sebelum Adzan Dzuhur hari ini.', onTap: () => _simulateEvent('pre_adzan'), ), _simulasiCard( s: s, title: 'Selama Adzan', icon: HugeIcons.strokeRoundedMegaphone01, desc: 'Melompat ke tepat waktu Adzan Dzuhur berkumandang.', onTap: () => _simulateEvent('adzan'), ), _simulasiCard( s: s, title: 'Menuju Iqomah', icon: HugeIcons.strokeRoundedTimer02, desc: 'Melompat ke saat waktu iqomah sedang menghitung mundur (1 menit setelah Adzan).', onTap: () => _simulateEvent('iqomah'), ), _simulasiCard( s: s, title: 'Persiapan Jumat', icon: HugeIcons.strokeRoundedCalendar03, desc: 'Menyimulasikan layar khusus persiapan Jumat (30 menit sebelum Adzan Dzuhur).', onTap: () => _simulateEvent('jumat_incoming'), ), _simulasiCard( s: s, title: 'Khutbah Berlangsung', icon: HugeIcons.strokeRoundedUserGroup, desc: 'Menyimulasikan layar saat Khutbah sedang berlangsung tanpa hitungan mundur (2 menit setelah Adzan Dzuhur).', onTap: () => _simulateEvent('jumat_khutbah'), ), _simulasiCard( s: s, title: 'Mode Shalat', icon: HugeIcons.strokeRoundedMoon02, desc: 'Layar menjadi hitam atau gelap selama shalat berlangsung.', onTap: () => _simulateEvent('shalat'), ), ], ), ], ), ); } Widget _simulasiCard({required double s, required String title, required dynamic icon, required String desc, required VoidCallback onTap}) { return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(SacredRadii.lg), child: Container( width: 320 * s, padding: EdgeInsets.all(24 * s), decoration: BoxDecoration( color: SacredColors.surfaceContainerLowest, borderRadius: BorderRadius.circular(SacredRadii.lg), border: Border.all(color: SacredColors.outlineVariant.withValues(alpha: 0.5)), boxShadow: [ BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 10 * s, offset: Offset(0, 4 * s)), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ HugeIcon(icon: icon, color: SacredColors.primary, size: 40 * s), 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)), ], ), ), ); } void _simulateTimeOffset(Duration offset) { ref.read(mockTimeOffsetProvider.notifier).state = offset; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Offset Waktu disetel ke: ${offset.inMinutes} Menit', style: GoogleFonts.manrope())), ); } void _simulateEvent(String eventType) { final schedule = ref.read(todayScheduleProvider); if (schedule == null) return; // We simulate using schedule.dzuhur final dzuhurStr = schedule.dzuhur; final parts = dzuhurStr.split(':'); final realNow = DateTime.now(); final dzuhurTime = DateTime(realNow.year, realNow.month, realNow.day, int.parse(parts[0]), int.parse(parts[1])); DateTime targetTime; switch (eventType) { case 'pre_adzan': targetTime = dzuhurTime.subtract(const Duration(minutes: 2)); break; case 'adzan': targetTime = dzuhurTime; break; case 'iqomah': targetTime = dzuhurTime.add(const Duration(seconds: 45)); // During iqomah break; case 'jumat_incoming': int diff = DateTime.friday - realNow.weekday; DateTime nextFriday = realNow.add(Duration(days: diff)); // Target: next Friday at dzuhur time - 30 minutes targetTime = DateTime(nextFriday.year, nextFriday.month, nextFriday.day, dzuhurTime.hour, dzuhurTime.minute).subtract(const Duration(minutes: 30)); break; case 'jumat_khutbah': int diff = DateTime.friday - realNow.weekday; DateTime nextFriday = realNow.add(Duration(days: diff)); // Target: next Friday at dzuhur time + 3 minutes (safely past 2-min Adzan) targetTime = DateTime(nextFriday.year, nextFriday.month, nextFriday.day, dzuhurTime.hour, dzuhurTime.minute).add(const Duration(minutes: 3)); break; case 'shalat': // Shalat mode usually happens after iqomah ends final settings = ref.read(settingsProvider); targetTime = dzuhurTime.add(Duration(minutes: settings.iqomahDzuhur + 1)); break; default: targetTime = realNow; } final offset = targetTime.difference(realNow); _simulateTimeOffset(offset); } } class _NavButton extends StatelessWidget { final String title; final dynamic icon; final bool isActive; final double scale; final VoidCallback onTap; const _NavButton({ required this.title, required this.icon, required this.isActive, required this.scale, required this.onTap, }); @override Widget build(BuildContext context) { final s = scale; return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(SacredRadii.lg), child: Container( width: double.infinity, padding: EdgeInsets.symmetric(horizontal: 24 * s, vertical: 24 * s), decoration: BoxDecoration( color: isActive ? SacredColors.primaryContainer : Colors.transparent, borderRadius: BorderRadius.circular(SacredRadii.lg), border: isActive ? Border.all(color: SacredColors.primary.withValues(alpha: 0.3)) : null, ), child: Row( children: [ HugeIcon( icon: icon, color: isActive ? SacredColors.onPrimaryContainer : SacredColors.onSurfaceVariant, size: 28 * s, ), SizedBox(width: 20 * s), Expanded( child: Text( title, style: GoogleFonts.plusJakartaSans( fontSize: 18 * s, fontWeight: FontWeight.bold, color: isActive ? SacredColors.onPrimaryContainer : SacredColors.onSurfaceVariant, letterSpacing: 1 * s, ), ), ), ], ), ), ); } }