533 lines
20 KiB
Dart
533 lines
20 KiB
Dart
import 'dart:io';
|
|
import 'dart:ui';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
import 'package:hugeicons/hugeicons.dart';
|
|
|
|
import '../../core/hijri_date.dart';
|
|
import '../../core/sacred_tokens.dart';
|
|
import '../../providers.dart';
|
|
import '../../data/local/models.dart';
|
|
import '../admin/admin_screen.dart';
|
|
import 'unsplash_background.dart';
|
|
|
|
/// A highly polished, dedicated screen displayed specifically on Fridays
|
|
/// when transitioning towards Dzuhur (Jumat) prayer.
|
|
class JumatScreen extends ConsumerWidget {
|
|
const JumatScreen({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final clock = ref.watch(clockProvider).valueOrNull ?? DateTime.now();
|
|
final schedule = ref.watch(todayScheduleProvider);
|
|
final settings = ref.watch(settingsProvider);
|
|
final screenData = ref.watch(screenStateProvider);
|
|
final size = MediaQuery.of(context).size;
|
|
final s = size.width / 1920;
|
|
final fs = s * ref.watch(textScaleProvider);
|
|
|
|
final timeStr = DateFormat('HH:mm').format(clock);
|
|
final secStr = DateFormat(':ss').format(clock);
|
|
final dateGregorian = DateFormat('EEEE, d MMMM yyyy', 'en').format(clock);
|
|
final dateHijri =
|
|
ref.watch(hijriDateProvider).valueOrNull ?? HijriDateFormatter.format(clock);
|
|
|
|
final durToKhutbah = screenData.timeUntilNext ?? const Duration(minutes: 0);
|
|
final minToKhutbah = durToKhutbah.inMinutes;
|
|
|
|
return Container(
|
|
color: SacredColors.background,
|
|
child: Stack(
|
|
children: [
|
|
// ── Underlay: Branded local image or Unsplash ──
|
|
if (settings.brandedBgImage != null && settings.brandedBgImage!.isNotEmpty)
|
|
Positioned.fill(
|
|
child: Image.file(
|
|
File(settings.brandedBgImage!),
|
|
fit: BoxFit.cover,
|
|
color: Colors.black.withValues(alpha: 0.55),
|
|
colorBlendMode: BlendMode.darken,
|
|
errorBuilder: (_, __, ___) => const UnsplashBackground(),
|
|
),
|
|
)
|
|
else
|
|
const UnsplashBackground(),
|
|
|
|
// ── Background darkness gradient ──
|
|
Positioned.fill(
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.bottomCenter,
|
|
end: Alignment.topCenter,
|
|
colors: [
|
|
SacredColors.background.withValues(alpha: 0.8),
|
|
Colors.transparent,
|
|
SacredColors.background.withValues(alpha: 0.9),
|
|
],
|
|
stops: const [0.0, 0.4, 1.0],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
// ── Header Shell ──
|
|
Positioned(
|
|
top: 48 * s,
|
|
left: 64 * s,
|
|
right: 64 * s,
|
|
child: _buildHeader(context, settings, s),
|
|
),
|
|
|
|
// ── Main Content Canvas ──
|
|
Positioned.fill(
|
|
top: 140 * s,
|
|
bottom: 240 * s,
|
|
left: 64 * s,
|
|
right: 64 * s,
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
// ── Left Column: Clock & Primary Focus ──
|
|
Expanded(
|
|
flex: 2,
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Pill
|
|
Container(
|
|
padding: EdgeInsets.symmetric(horizontal: 24 * s, vertical: 8 * s),
|
|
decoration: BoxDecoration(
|
|
color: SacredColors.secondary.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(SacredRadii.full),
|
|
border: Border.all(color: SacredColors.secondary.withValues(alpha: 0.2)),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
_PulsingDot(color: SacredColors.secondary, size: 12 * s),
|
|
SizedBox(width: 12 * s),
|
|
Text(
|
|
'PERSIAPAN JUMAT',
|
|
style: GoogleFonts.plusJakartaSans(
|
|
fontSize: 16 * s,
|
|
fontWeight: FontWeight.w700,
|
|
color: SacredColors.secondary,
|
|
letterSpacing: 3 * s,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
SizedBox(height: 16 * s),
|
|
|
|
// Massive Clock
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
timeStr,
|
|
style: GoogleFonts.plusJakartaSans(
|
|
fontSize: 192 * s,
|
|
fontWeight: FontWeight.w800,
|
|
color: SacredColors.primary,
|
|
letterSpacing: -5 * s,
|
|
height: 1.0,
|
|
),
|
|
),
|
|
Padding(
|
|
padding: EdgeInsets.only(top: 24 * s),
|
|
child: Text(
|
|
secStr,
|
|
style: GoogleFonts.plusJakartaSans(
|
|
fontSize: 48 * s,
|
|
fontWeight: FontWeight.w700,
|
|
color: SacredColors.primary.withValues(alpha: 0.7),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
SizedBox(height: 16 * s),
|
|
|
|
// Dates
|
|
Text(
|
|
dateGregorian,
|
|
style: GoogleFonts.plusJakartaSans(
|
|
fontSize: 36 * s,
|
|
fontWeight: FontWeight.w700,
|
|
color: SacredColors.onSurface,
|
|
),
|
|
),
|
|
SizedBox(height: 4 * s),
|
|
Text(
|
|
dateHijri,
|
|
style: GoogleFonts.manrope(
|
|
fontSize: 24 * s,
|
|
fontWeight: FontWeight.w500,
|
|
color: SacredColors.secondary.withValues(alpha: 0.9),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// ── Right Column: Khutbah Info Card ──
|
|
Expanded(
|
|
flex: 1,
|
|
child: Container(
|
|
padding: EdgeInsets.all(40 * s),
|
|
decoration: BoxDecoration(
|
|
color: SacredColors.surfaceContainerHigh.withValues(alpha: 0.6),
|
|
borderRadius: BorderRadius.circular(SacredRadii.xl),
|
|
border: Border.all(color: Colors.white.withValues(alpha: 0.05)),
|
|
),
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(SacredRadii.xl),
|
|
child: BackdropFilter(
|
|
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
'NEXT EVENT',
|
|
style: GoogleFonts.manrope(
|
|
fontSize: 14 * s,
|
|
fontWeight: FontWeight.w600,
|
|
color: SacredColors.onSurfaceVariant,
|
|
letterSpacing: 3 * s,
|
|
),
|
|
),
|
|
HugeIcon(icon: HugeIcons.strokeRoundedSparkles, color: SacredColors.secondary, size: 24 * s),
|
|
],
|
|
),
|
|
SizedBox(height: 16 * s),
|
|
Text(
|
|
'MENUJU KHUTBAH',
|
|
style: GoogleFonts.plusJakartaSans(
|
|
fontSize: 42 * s,
|
|
fontWeight: FontWeight.w800,
|
|
color: SacredColors.onSurface,
|
|
height: 1.1,
|
|
),
|
|
),
|
|
SizedBox(height: 32 * s),
|
|
|
|
// Khatib Info
|
|
_buildInfoTile(
|
|
s,
|
|
icon: Icons.person_pin,
|
|
color: SacredColors.primary,
|
|
label: 'KHATIB HARI INI',
|
|
value: settings.khatibName.isEmpty ? 'Belum Diatur' : settings.khatibName
|
|
),
|
|
SizedBox(height: 24 * s),
|
|
|
|
// Countdown Info
|
|
_buildInfoTile(
|
|
s,
|
|
icon: Icons.timer,
|
|
color: SacredColors.secondary,
|
|
label: 'KHUTBAH DIMULAI DALAM',
|
|
value: minToKhutbah > 0 ? '~ $minToKhutbah Menit' : 'Sebentar Lagi'
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// ── Bottom Navigation Shell (Prayer Times) ──
|
|
if (schedule != null)
|
|
Positioned(
|
|
left: 64 * s,
|
|
right: 64 * s,
|
|
bottom: 64 * s,
|
|
child: _buildPrayerTimesRow(s, schedule),
|
|
),
|
|
|
|
// ── Footer Marquee Shell ──
|
|
Positioned(
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
child: _buildMarquee(s, fs, settings),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildHeader(BuildContext context, AppSettings settings, double s) {
|
|
return Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
// Mosque Name
|
|
GestureDetector(
|
|
onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (_) => const AdminScreen())),
|
|
child: Text(
|
|
settings.masjidName,
|
|
style: GoogleFonts.plusJakartaSans(
|
|
fontSize: 32 * s,
|
|
fontWeight: FontWeight.w800,
|
|
color: SacredColors.primary,
|
|
),
|
|
),
|
|
),
|
|
// Mosque Address
|
|
Row(
|
|
children: [
|
|
HugeIcon(icon: HugeIcons.strokeRoundedMosque01, color: SacredColors.primary, size: 24 * s),
|
|
SizedBox(width: 8 * s),
|
|
Text(
|
|
settings.masjidAddress,
|
|
style: GoogleFonts.manrope(
|
|
fontSize: 18 * s,
|
|
fontWeight: FontWeight.w500,
|
|
color: SacredColors.onSurfaceVariant,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildInfoTile(double s, {required IconData icon, required Color color, required String label, required String value}) {
|
|
return Container(
|
|
padding: EdgeInsets.all(20 * s),
|
|
decoration: BoxDecoration(
|
|
color: SacredColors.surfaceContainerHighest.withValues(alpha: 0.4),
|
|
borderRadius: BorderRadius.circular(SacredRadii.xl),
|
|
),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
padding: EdgeInsets.all(12 * s),
|
|
decoration: BoxDecoration(
|
|
color: color.withValues(alpha: 0.2),
|
|
borderRadius: BorderRadius.circular(SacredRadii.md),
|
|
),
|
|
child: Icon(icon, color: color, size: 24 * s),
|
|
),
|
|
SizedBox(width: 16 * s),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
label,
|
|
style: GoogleFonts.manrope(
|
|
fontSize: 12 * s,
|
|
fontWeight: FontWeight.w700,
|
|
color: SacredColors.onSurfaceVariant,
|
|
letterSpacing: 2 * s,
|
|
),
|
|
),
|
|
SizedBox(height: 4 * s),
|
|
Text(
|
|
value,
|
|
style: GoogleFonts.plusJakartaSans(
|
|
fontSize: 22 * s,
|
|
fontWeight: FontWeight.w700,
|
|
color: SacredColors.onSurface,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildPrayerTimesRow(double s, DailyPrayerSchedule schedule) {
|
|
return Container(
|
|
padding: EdgeInsets.symmetric(horizontal: 16 * s, vertical: 16 * s),
|
|
decoration: BoxDecoration(
|
|
color: SacredColors.surfaceContainerLow,
|
|
borderRadius: BorderRadius.circular(SacredRadii.xl),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: SacredColors.primary.withValues(alpha: 0.1),
|
|
blurRadius: 40 * s,
|
|
spreadRadius: 0,
|
|
),
|
|
],
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
children: [
|
|
_buildTimeItem(s, 'Fajr', schedule.subuh, Icons.brightness_3, false),
|
|
_buildTimeItem(s, 'Terbit', schedule.terbit, Icons.wb_twilight, false),
|
|
_buildTimeItem(s, 'JUMAT', schedule.dzuhur, Icons.wb_sunny, true),
|
|
_buildTimeItem(s, 'Asr', schedule.ashar, Icons.sunny_snowing, false),
|
|
_buildTimeItem(s, 'Maghrib', schedule.maghrib, Icons.wb_cloudy, false),
|
|
_buildTimeItem(s, 'Isha', schedule.isya, Icons.bedtime, false),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildTimeItem(double s, String name, String time, IconData icon, bool isJumat) {
|
|
if (isJumat) {
|
|
return Container(
|
|
padding: EdgeInsets.symmetric(horizontal: 48 * s, vertical: 12 * s),
|
|
decoration: BoxDecoration(
|
|
color: SacredColors.primary.withValues(alpha: 0.15),
|
|
border: Border.all(color: SacredColors.primary.withValues(alpha: 0.3)),
|
|
borderRadius: BorderRadius.circular(SacredRadii.xl),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Icon(icon, color: SacredColors.primary, size: 28 * s),
|
|
SizedBox(height: 8 * s),
|
|
Text(name, style: GoogleFonts.manrope(fontSize: 18 * s, fontWeight: FontWeight.w800, color: SacredColors.primary)),
|
|
SizedBox(height: 4 * s),
|
|
Text(time, style: GoogleFonts.plusJakartaSans(fontSize: 16 * s, fontWeight: FontWeight.w700, color: SacredColors.primary)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
return Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: 24 * s, vertical: 8 * s),
|
|
child: Column(
|
|
children: [
|
|
Icon(icon, color: SacredColors.onSurface.withValues(alpha: 0.6), size: 24 * s),
|
|
SizedBox(height: 8 * s),
|
|
Text(name, style: GoogleFonts.manrope(fontSize: 16 * s, fontWeight: FontWeight.w500, color: SacredColors.onSurface.withValues(alpha: 0.6))),
|
|
SizedBox(height: 4 * s),
|
|
Text(time, style: GoogleFonts.plusJakartaSans(fontSize: 16 * s, fontWeight: FontWeight.w700, color: SacredColors.onSurface)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildMarquee(double s, double fs, AppSettings settings) {
|
|
// Quick custom simplified marquee or fallback to settings.runningTexts
|
|
final texts = settings.runningTexts.isEmpty
|
|
? ["JUMAT MUBARAK: Luruskan dan rapatkan shaf. Harap non-aktifkan alat komunikasi."]
|
|
: settings.runningTexts;
|
|
|
|
return Container(
|
|
width: double.infinity,
|
|
height: 44 * s,
|
|
decoration: BoxDecoration(
|
|
color: Colors.black.withValues(alpha: 0.4),
|
|
),
|
|
child: ClipRect(
|
|
child: _JumatMarquee(
|
|
texts: texts,
|
|
s: s,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _PulsingDot extends StatefulWidget {
|
|
final Color color;
|
|
final double size;
|
|
const _PulsingDot({required this.color, required this.size});
|
|
|
|
@override
|
|
State<_PulsingDot> createState() => _PulsingDotState();
|
|
}
|
|
|
|
class _PulsingDotState extends State<_PulsingDot> with SingleTickerProviderStateMixin {
|
|
late AnimationController _ctrl;
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_ctrl = AnimationController(vsync: this, duration: const Duration(milliseconds: 1200))..repeat();
|
|
}
|
|
@override
|
|
void dispose() { _ctrl.dispose(); super.dispose(); }
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SizedBox(
|
|
width: widget.size, height: widget.size,
|
|
child: Stack(
|
|
children: [
|
|
FadeTransition(
|
|
opacity: Tween(begin: 0.75, end: 0.0).animate(_ctrl),
|
|
child: ScaleTransition(
|
|
scale: Tween(begin: 1.0, end: 2.0).animate(_ctrl),
|
|
child: Container(decoration: BoxDecoration(shape: BoxShape.circle, color: widget.color)),
|
|
),
|
|
),
|
|
Center(child: Container(width: widget.size, height: widget.size, decoration: BoxDecoration(shape: BoxShape.circle, color: widget.color))),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _JumatMarquee extends StatefulWidget {
|
|
final List<String> texts;
|
|
final double s;
|
|
const _JumatMarquee({required this.texts, required this.s});
|
|
@override
|
|
State<_JumatMarquee> createState() => _JumatMarqueeState();
|
|
}
|
|
|
|
class _JumatMarqueeState extends State<_JumatMarquee> with TickerProviderStateMixin {
|
|
late AnimationController _ctrl;
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_ctrl = AnimationController(vsync: this, duration: const Duration(seconds: 30))..repeat();
|
|
}
|
|
@override
|
|
void dispose() { _ctrl.dispose(); super.dispose(); }
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final joined = widget.texts.join(" • ");
|
|
final style = GoogleFonts.manrope(
|
|
fontSize: 16 * widget.s,
|
|
fontWeight: FontWeight.w600,
|
|
color: SacredColors.secondary,
|
|
letterSpacing: 2 * widget.s,
|
|
);
|
|
|
|
return LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
return Stack(
|
|
children: [
|
|
AnimatedBuilder(
|
|
animation: _ctrl,
|
|
builder: (ctx, child) {
|
|
// Ensure endless scroll mathematically
|
|
return Positioned(
|
|
left: -(_ctrl.value * constraints.maxWidth),
|
|
top: 0, bottom: 0,
|
|
child: Row(
|
|
children: [
|
|
Container(alignment: Alignment.centerLeft, width: constraints.maxWidth, child: Text(joined, style: style, maxLines: 1)),
|
|
Container(alignment: Alignment.centerLeft, width: constraints.maxWidth, child: Text(joined, style: style, maxLines: 1)),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
);
|
|
}
|
|
);
|
|
}
|
|
}
|