Initial project import and stabilization baseline

This commit is contained in:
dwindown
2026-03-30 21:28:44 +07:00
commit ad33b01231
186 changed files with 20445 additions and 0 deletions

View File

@@ -0,0 +1,530 @@
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 = 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,
),
)
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)),
],
),
);
},
),
],
);
}
);
}
}