Initial project import and stabilization baseline
This commit is contained in:
201
lib/features/home/slideshow_screen.dart
Normal file
201
lib/features/home/slideshow_screen.dart
Normal file
@@ -0,0 +1,201 @@
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
import '../../core/sacred_tokens.dart';
|
||||
import '../../providers.dart';
|
||||
|
||||
class SlideshowScreen extends ConsumerStatefulWidget {
|
||||
const SlideshowScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<SlideshowScreen> createState() => _SlideshowScreenState();
|
||||
}
|
||||
|
||||
class _SlideshowScreenState extends ConsumerState<SlideshowScreen> {
|
||||
static int _globalImageIndex = 0;
|
||||
int _localImageIndex = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final settings = ref.read(settingsProvider);
|
||||
if (settings.slideshowImages.isNotEmpty) {
|
||||
_localImageIndex = _globalImageIndex;
|
||||
_globalImageIndex = (_globalImageIndex + 1) % settings.slideshowImages.length;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
final s = size.width / 1920;
|
||||
|
||||
final screenData = ref.watch(screenStateProvider);
|
||||
final settings = ref.watch(settingsProvider);
|
||||
|
||||
Widget renderImage() {
|
||||
if (settings.slideshowImages.isEmpty) {
|
||||
return _buildErrorImage(s);
|
||||
}
|
||||
|
||||
final imgPath = settings.slideshowImages[_localImageIndex % settings.slideshowImages.length];
|
||||
return Image.file(
|
||||
File(imgPath),
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (ctx, err, stack) => _buildErrorImage(s),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// 1. Poster Image
|
||||
renderImage(),
|
||||
|
||||
// 2. Subtle Dark Gradient Overlay at bottom so the "glass bar" pops out cleanly
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
height: 300 * s,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: [
|
||||
Colors.black.withValues(alpha: 0.8),
|
||||
Colors.transparent,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 3. Glassmorphic Bottom Bar (Always showing Clock and Next Prayer)
|
||||
Positioned(
|
||||
left: 64 * s,
|
||||
right: 64 * s,
|
||||
bottom: 64 * s,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(SacredRadii.xl),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 40, sigmaY: 40),
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 48 * s, vertical: 32 * s),
|
||||
decoration: BoxDecoration(
|
||||
color: SacredColors.surfaceContainerLowest.withValues(alpha: 0.4),
|
||||
borderRadius: BorderRadius.circular(SacredRadii.xl),
|
||||
border: Border.all(color: Colors.white.withValues(alpha: 0.1)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// Left: Mosque Name
|
||||
Text(
|
||||
settings.masjidName,
|
||||
style: GoogleFonts.plusJakartaSans(
|
||||
fontSize: 32 * s,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Colors.white,
|
||||
letterSpacing: 1 * s,
|
||||
),
|
||||
),
|
||||
|
||||
// Center: Clock
|
||||
_buildMiniClock(s, screenData),
|
||||
|
||||
// Right: Next Prayer
|
||||
if (screenData.nextPrayer != null && screenData.timeUntilNext != null)
|
||||
_buildNextPrayer(s, screenData),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMiniClock(double s, ScreenStateData data) {
|
||||
final timeStr = "${data.now.hour.toString().padLeft(2, '0')}:${data.now.minute.toString().padLeft(2, '0')}";
|
||||
final secStr = data.now.second.toString().padLeft(2, '0');
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
timeStr,
|
||||
style: GoogleFonts.plusJakartaSans(
|
||||
fontSize: 64 * s,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Colors.white,
|
||||
height: 1.0,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8 * s),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: 8 * s),
|
||||
child: Text(
|
||||
secStr,
|
||||
style: GoogleFonts.plusJakartaSans(
|
||||
fontSize: 32 * s,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: SacredColors.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNextPrayer(double s, ScreenStateData data) {
|
||||
final dur = data.timeUntilNext!;
|
||||
final h = dur.inHours;
|
||||
final m = (dur.inMinutes % 60);
|
||||
final countDownStr = h > 0 ? "-$h jam $m mnt" : "-$m mnt";
|
||||
final prayerTitle = data.nextPrayer!.id.toUpperCase();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'MENJELANG $prayerTitle',
|
||||
style: GoogleFonts.plusJakartaSans(
|
||||
fontSize: 20 * s,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: SacredColors.onSurfaceVariant.withValues(alpha: 0.9),
|
||||
letterSpacing: 2 * s,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4 * s),
|
||||
Text(
|
||||
countDownStr,
|
||||
style: GoogleFonts.manrope(
|
||||
fontSize: 28 * s,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: SacredColors.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorImage(double s) {
|
||||
return Container(
|
||||
color: SacredColors.surfaceContainerLow,
|
||||
child: Center(
|
||||
child: Icon(Icons.broken_image, size: 64 * s, color: SacredColors.onSurfaceVariant),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user