feat(tv-ui): split pengumuman tab and refine main text-slide behavior
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -126,7 +126,7 @@ class _HomeViewState extends ConsumerState<HomeView> {
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const AdminScreen(
|
||||
initialTab: 4,
|
||||
initialTab: 5,
|
||||
focusSelectedTabOnOpen: true,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -38,15 +38,26 @@ class MainScreen extends ConsumerWidget {
|
||||
final timeStr = DateFormat('HH:mm').format(clock);
|
||||
final secStr = DateFormat(':ss').format(clock);
|
||||
final dateGregorian = DateFormat('EEEE, d MMMM yyyy', 'id').format(clock);
|
||||
final dateHijri =
|
||||
ref.watch(hijriDateProvider).valueOrNull ?? HijriDateFormatter.format(clock);
|
||||
final dateHijri = ref.watch(hijriDateProvider).valueOrNull ??
|
||||
HijriDateFormatter.format(clock);
|
||||
final rotationElapsed = ref.watch(rotationElapsedProvider);
|
||||
final centerTextSlides = settings.textSlides
|
||||
.map((text) => text.trim())
|
||||
.where((text) => text.isNotEmpty)
|
||||
.toList();
|
||||
final centerSlide = _resolveCenterSlide(
|
||||
settings: settings,
|
||||
elapsedInMainWindowSec: rotationElapsed,
|
||||
announcements: centerTextSlides,
|
||||
);
|
||||
|
||||
return Container(
|
||||
color: SacredColors.background,
|
||||
child: Stack(
|
||||
children: [
|
||||
// ── Underlay 1: Branded local image (highest priority if set) ──
|
||||
if (settings.brandedBgImage != null && settings.brandedBgImage!.isNotEmpty)
|
||||
if (settings.brandedBgImage != null &&
|
||||
settings.brandedBgImage!.isNotEmpty)
|
||||
Positioned.fill(
|
||||
child: Image.file(
|
||||
File(settings.brandedBgImage!),
|
||||
@@ -100,97 +111,38 @@ class MainScreen extends ConsumerWidget {
|
||||
child: Column(
|
||||
children: [
|
||||
// ── HEADER ──
|
||||
_buildHeader(context, s, fs, settings, dateGregorian, dateHijri),
|
||||
_buildHeader(
|
||||
context,
|
||||
s,
|
||||
fs,
|
||||
settings,
|
||||
dateGregorian,
|
||||
dateHijri,
|
||||
inlineClockText:
|
||||
centerSlide.isPrimary ? null : '$timeStr$secStr',
|
||||
),
|
||||
|
||||
// ── CENTER: Clock + Countdown ──
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Countdown pill
|
||||
if (screenData.nextPrayer != null &&
|
||||
screenData.timeUntilNext != null)
|
||||
_buildCountdownPill(s, fs, screenData),
|
||||
|
||||
SizedBox(height: 16 * s),
|
||||
|
||||
// Massive Clock
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
timeStr,
|
||||
style: GoogleFonts.plusJakartaSans(
|
||||
fontSize: 180 * s,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: SacredColors.onSurface,
|
||||
letterSpacing: -6 * s,
|
||||
height: 1.0,
|
||||
shadows: [
|
||||
Shadow(
|
||||
blurRadius: 40 * s,
|
||||
color:
|
||||
SacredColors.primary.withValues(alpha: 0.2),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 24 * s),
|
||||
child: Text(
|
||||
secStr,
|
||||
style: GoogleFonts.plusJakartaSans(
|
||||
fontSize: 72 * s,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: SacredColors.primary,
|
||||
letterSpacing: -1 * s,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Decorative line
|
||||
Container(
|
||||
width: 240 * s,
|
||||
height: 2 * s,
|
||||
margin: EdgeInsets.only(top: 12 * s),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
SacredColors.primary.withValues(alpha: 0.4),
|
||||
Colors.transparent,
|
||||
],
|
||||
),
|
||||
child: centerSlide.isPrimary
|
||||
? _buildPrimaryCenter(
|
||||
s,
|
||||
fs,
|
||||
timeStr,
|
||||
secStr,
|
||||
dateGregorian,
|
||||
screenData,
|
||||
schedule,
|
||||
settings,
|
||||
)
|
||||
: _buildAnnouncementCenter(
|
||||
s,
|
||||
fs,
|
||||
settings,
|
||||
centerSlide,
|
||||
centerTextSlides,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 16 * s),
|
||||
|
||||
// Date line
|
||||
Text(
|
||||
dateGregorian,
|
||||
style: GoogleFonts.manrope(
|
||||
fontSize: 24 * fs,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: SacredColors.onSurfaceVariant,
|
||||
letterSpacing: 1 * s,
|
||||
),
|
||||
),
|
||||
|
||||
// Secondary times (Imsak, Terbit, Dhuha)
|
||||
if (schedule != null)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 24 * s),
|
||||
child: _buildSecondaryTimes(s, fs, schedule, settings),
|
||||
),
|
||||
|
||||
// Removed FRIDAY SPECIAL PANEL since its handled by dedicated JumatScreen
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -213,108 +165,349 @@ class MainScreen extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(
|
||||
BuildContext context,
|
||||
double s,
|
||||
double fs,
|
||||
AppSettings settings,
|
||||
String dateGregorian,
|
||||
String dateHijri,
|
||||
) {
|
||||
Widget _buildHeader(BuildContext context, double s, double fs,
|
||||
AppSettings settings, String dateGregorian, String dateHijri,
|
||||
{String? inlineClockText}) {
|
||||
final hScale = settings.scaleTopHeader;
|
||||
final showInlineClock =
|
||||
inlineClockText != null && inlineClockText.isNotEmpty;
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(top: 24 * s, bottom: 8 * s),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// Left: Mosque name + address
|
||||
Padding(
|
||||
padding: EdgeInsets.all(8.0 * s),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
settings.masjidName,
|
||||
style: GoogleFonts.plusJakartaSans(
|
||||
fontSize: 32 * s,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: SacredColors.primary,
|
||||
letterSpacing: -0.5 * s,
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(8.0 * s),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
settings.masjidName,
|
||||
style: GoogleFonts.plusJakartaSans(
|
||||
fontSize: 32 * s * hScale,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: SacredColors.primary,
|
||||
letterSpacing: -0.5 * s,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4 * s),
|
||||
Row(
|
||||
children: [
|
||||
HugeIcon(
|
||||
icon: HugeIcons.strokeRoundedLocation01,
|
||||
color: SacredColors.secondary,
|
||||
size: 16 * s,
|
||||
),
|
||||
SizedBox(width: 4 * s),
|
||||
Text(
|
||||
settings.masjidAddress,
|
||||
style: GoogleFonts.manrope(
|
||||
fontSize: 14 * fs,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: SacredColors.onSurface.withValues(alpha: 0.7),
|
||||
letterSpacing: 0.5 * s,
|
||||
SizedBox(height: 4 * s),
|
||||
Row(
|
||||
children: [
|
||||
HugeIcon(
|
||||
icon: HugeIcons.strokeRoundedLocation01,
|
||||
color: SacredColors.secondary,
|
||||
size: 16 * s * hScale,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
SizedBox(width: 4 * s),
|
||||
Expanded(
|
||||
child: Text(
|
||||
settings.masjidAddress,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: GoogleFonts.manrope(
|
||||
fontSize: 14 * fs * hScale,
|
||||
fontWeight: FontWeight.w500,
|
||||
color:
|
||||
SacredColors.onSurface.withValues(alpha: 0.7),
|
||||
letterSpacing: 0.5 * s,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Right: Hijri date + mosque icon
|
||||
Row(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
dateHijri,
|
||||
style: GoogleFonts.plusJakartaSans(
|
||||
fontSize: 20 * s,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: SacredColors.onSurface,
|
||||
if (showInlineClock)
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Center(
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 22 * s,
|
||||
vertical: 10 * s,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: SacredColors.surfaceContainerLow
|
||||
.withValues(alpha: 0.86),
|
||||
borderRadius: BorderRadius.circular(SacredRadii.full),
|
||||
border: Border.all(
|
||||
color: SacredColors.primary.withValues(alpha: 0.32),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
dateGregorian.toUpperCase(),
|
||||
style: GoogleFonts.manrope(
|
||||
fontSize: 12 * fs,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: SacredColors.onSurfaceVariant,
|
||||
letterSpacing: 2 * s,
|
||||
child: Text(
|
||||
inlineClockText,
|
||||
style: GoogleFonts.plusJakartaSans(
|
||||
fontSize: 30 * s * hScale,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: SacredColors.onSurface,
|
||||
letterSpacing: -1 * s,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Right: Hijri date + mosque icon
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
dateHijri,
|
||||
style: GoogleFonts.plusJakartaSans(
|
||||
fontSize: 20 * s * hScale,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: SacredColors.onSurface,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
dateGregorian.toUpperCase(),
|
||||
style: GoogleFonts.manrope(
|
||||
fontSize: 12 * fs * hScale,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: SacredColors.onSurfaceVariant,
|
||||
letterSpacing: 2 * s,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(width: 16 * s),
|
||||
Container(
|
||||
width: 48 * s * hScale,
|
||||
height: 48 * s * hScale,
|
||||
decoration: BoxDecoration(
|
||||
color: SacredColors.surfaceContainerHighest,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: HugeIcon(
|
||||
icon: HugeIcons.strokeRoundedHome01,
|
||||
color: SacredColors.secondary,
|
||||
size: 28 * s * hScale,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(width: 16 * s),
|
||||
Container(
|
||||
width: 48 * s,
|
||||
height: 48 * s,
|
||||
decoration: BoxDecoration(
|
||||
color: SacredColors.surfaceContainerHighest,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: HugeIcon(
|
||||
icon: HugeIcons.strokeRoundedHome01,
|
||||
color: SacredColors.secondary,
|
||||
size: 28 * s,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_CenterSlideState _resolveCenterSlide({
|
||||
required AppSettings settings,
|
||||
required int elapsedInMainWindowSec,
|
||||
required List<String> announcements,
|
||||
}) {
|
||||
final heroDuration = settings.mainCenterSlideDurationSec.clamp(1, 600);
|
||||
final announcementDuration =
|
||||
settings.announcementSlideDurationSec.clamp(1, 600);
|
||||
final totalMainDuration = announcements.isEmpty
|
||||
? settings.mainScreenDurationSec.clamp(1, 600)
|
||||
: heroDuration + (announcements.length * announcementDuration);
|
||||
final elapsed = elapsedInMainWindowSec % totalMainDuration;
|
||||
|
||||
if (elapsed < heroDuration || announcements.isEmpty) {
|
||||
return _CenterSlideState.primary(heroDuration: heroDuration);
|
||||
}
|
||||
|
||||
final elapsedAfterHero = elapsed - heroDuration;
|
||||
final offset = elapsedAfterHero ~/ announcementDuration;
|
||||
final announcementIndex = offset % announcements.length;
|
||||
final elapsedInSlide = elapsedAfterHero % announcementDuration;
|
||||
|
||||
return _CenterSlideState.announcement(
|
||||
announcementIndex: announcementIndex,
|
||||
elapsedInSlideSec: elapsedInSlide,
|
||||
slideDurationSec: announcementDuration,
|
||||
totalAnnouncements: announcements.length,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPrimaryCenter(
|
||||
double s,
|
||||
double fs,
|
||||
String timeStr,
|
||||
String secStr,
|
||||
String dateGregorian,
|
||||
ScreenStateData screenData,
|
||||
DailyPrayerSchedule? schedule,
|
||||
AppSettings settings,
|
||||
) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (screenData.nextPrayer != null && screenData.timeUntilNext != null)
|
||||
_buildCountdownPill(s, fs, screenData),
|
||||
SizedBox(height: 16 * s),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
timeStr,
|
||||
style: GoogleFonts.plusJakartaSans(
|
||||
fontSize: 180 * s,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: SacredColors.onSurface,
|
||||
letterSpacing: -6 * s,
|
||||
height: 1.0,
|
||||
shadows: [
|
||||
Shadow(
|
||||
blurRadius: 40 * s,
|
||||
color: SacredColors.primary.withValues(alpha: 0.2),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 24 * s),
|
||||
child: Text(
|
||||
secStr,
|
||||
style: GoogleFonts.plusJakartaSans(
|
||||
fontSize: 72 * s,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: SacredColors.primary,
|
||||
letterSpacing: -1 * s,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
width: 240 * s,
|
||||
height: 2 * s,
|
||||
margin: EdgeInsets.only(top: 12 * s),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
SacredColors.primary.withValues(alpha: 0.4),
|
||||
Colors.transparent,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16 * s),
|
||||
Text(
|
||||
dateGregorian,
|
||||
style: GoogleFonts.manrope(
|
||||
fontSize: 24 * fs,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: SacredColors.onSurfaceVariant,
|
||||
letterSpacing: 1 * s,
|
||||
),
|
||||
),
|
||||
if (schedule != null)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 24 * s),
|
||||
child: _buildSecondaryTimes(s, fs, schedule, settings),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAnnouncementCenter(
|
||||
double s,
|
||||
double fs,
|
||||
AppSettings settings,
|
||||
_CenterSlideState state,
|
||||
List<String> announcements,
|
||||
) {
|
||||
final slideScale = settings.scaleTextSlideCenter;
|
||||
final index = state.announcementIndex ?? 0;
|
||||
final text = announcements[index];
|
||||
final progress = state.slideDurationSec <= 0
|
||||
? 0.0
|
||||
: (state.elapsedInSlideSec / state.slideDurationSec).clamp(0.0, 1.0);
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: 1320 * s),
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
transitionBuilder: (child, animation) {
|
||||
final slide = Tween<Offset>(
|
||||
begin: const Offset(0.16, 0),
|
||||
end: Offset.zero,
|
||||
).animate(
|
||||
CurvedAnimation(parent: animation, curve: Curves.easeOutCubic));
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: SlideTransition(position: slide, child: child),
|
||||
);
|
||||
},
|
||||
child: SizedBox(
|
||||
key: ValueKey('announcement-$index-$text'),
|
||||
width: double.infinity,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24 * s, vertical: 20 * s),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'PENGUMUMAN ${index + 1}/${state.totalAnnouncements}',
|
||||
style: GoogleFonts.plusJakartaSans(
|
||||
fontSize: 20 * fs * slideScale,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: SacredColors.secondary,
|
||||
letterSpacing: 1.2 * s,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20 * s),
|
||||
Text(
|
||||
text,
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.plusJakartaSans(
|
||||
fontSize: 72 * fs * slideScale,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: SacredColors.onSurface,
|
||||
height: 1.15,
|
||||
shadows: [
|
||||
Shadow(
|
||||
blurRadius: 32 * s,
|
||||
color: SacredColors.background.withValues(alpha: 0.65),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 24 * s),
|
||||
SizedBox(
|
||||
width: 900 * s,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(SacredRadii.full),
|
||||
child: LinearProgressIndicator(
|
||||
value: progress,
|
||||
minHeight: 6 * s,
|
||||
backgroundColor:
|
||||
SacredColors.outlineVariant.withValues(alpha: 0.25),
|
||||
valueColor:
|
||||
AlwaysStoppedAnimation<Color>(SacredColors.primary),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCountdownPill(double s, double fs, ScreenStateData screenData) {
|
||||
return Container(
|
||||
padding:
|
||||
EdgeInsets.symmetric(horizontal: 24 * s, vertical: 8 * s),
|
||||
padding: EdgeInsets.symmetric(horizontal: 24 * s, vertical: 8 * s),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(SacredRadii.full),
|
||||
border: Border.all(
|
||||
@@ -393,8 +586,6 @@ class MainScreen extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
Widget _buildPrayerCardsRow(double s, double fs, DailyPrayerSchedule schedule,
|
||||
ScreenStateData screenData, AppSettings settings, DateTime clock) {
|
||||
final prayers = [
|
||||
@@ -489,6 +680,46 @@ class MainScreen extends ConsumerWidget {
|
||||
|
||||
// ─── Supporting widgets ───
|
||||
|
||||
class _CenterSlideState {
|
||||
final bool isPrimary;
|
||||
final int? announcementIndex;
|
||||
final int elapsedInSlideSec;
|
||||
final int slideDurationSec;
|
||||
final int totalAnnouncements;
|
||||
|
||||
const _CenterSlideState._({
|
||||
required this.isPrimary,
|
||||
this.announcementIndex,
|
||||
required this.elapsedInSlideSec,
|
||||
required this.slideDurationSec,
|
||||
required this.totalAnnouncements,
|
||||
});
|
||||
|
||||
factory _CenterSlideState.primary({required int heroDuration}) {
|
||||
return _CenterSlideState._(
|
||||
isPrimary: true,
|
||||
elapsedInSlideSec: 0,
|
||||
slideDurationSec: heroDuration,
|
||||
totalAnnouncements: 0,
|
||||
);
|
||||
}
|
||||
|
||||
factory _CenterSlideState.announcement({
|
||||
required int announcementIndex,
|
||||
required int elapsedInSlideSec,
|
||||
required int slideDurationSec,
|
||||
required int totalAnnouncements,
|
||||
}) {
|
||||
return _CenterSlideState._(
|
||||
isPrimary: false,
|
||||
announcementIndex: announcementIndex,
|
||||
elapsedInSlideSec: elapsedInSlideSec,
|
||||
slideDurationSec: slideDurationSec,
|
||||
totalAnnouncements: totalAnnouncements,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SecondaryTimeItem {
|
||||
final String label;
|
||||
final String time;
|
||||
@@ -508,8 +739,8 @@ class _PrayerCard extends StatelessWidget {
|
||||
final bool isFriday;
|
||||
final double s;
|
||||
final double fs;
|
||||
final double scaleLabel; // controls prayer name label size
|
||||
final double scaleBody; // controls time + iqomah text size
|
||||
final double scaleLabel; // controls prayer name label size
|
||||
final double scaleBody; // controls time + iqomah text size
|
||||
|
||||
const _PrayerCard({
|
||||
required this.data,
|
||||
@@ -590,7 +821,6 @@ class _PrayerCard extends StatelessWidget {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -664,7 +894,7 @@ class _PulsingDotState extends State<_PulsingDot>
|
||||
class _RunningTextWidget extends StatefulWidget {
|
||||
final List<String> texts;
|
||||
final List<int> durations; // per-item seconds
|
||||
final String animType; // 'marquee' or 'fade'
|
||||
final String animType; // 'marquee' or 'fade'
|
||||
final TextStyle style;
|
||||
|
||||
const _RunningTextWidget({
|
||||
@@ -698,34 +928,34 @@ class _RunningTextWidgetState extends State<_RunningTextWidget>
|
||||
|
||||
void _startCycle() async {
|
||||
try {
|
||||
while (!_disposed) {
|
||||
if (widget.texts.isEmpty) {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
continue;
|
||||
}
|
||||
final dur = widget.durations[_index];
|
||||
while (!_disposed) {
|
||||
if (widget.texts.isEmpty) {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
continue;
|
||||
}
|
||||
final dur = widget.durations[_index];
|
||||
|
||||
if (widget.animType == 'fade') {
|
||||
if (_disposed) break;
|
||||
await _fadeCtrl.forward().orCancel;
|
||||
if (_disposed) break;
|
||||
await Future.delayed(Duration(seconds: dur));
|
||||
if (_disposed) break;
|
||||
await _fadeCtrl.reverse().orCancel;
|
||||
} else {
|
||||
if (_disposed) break;
|
||||
_scrollCtrl.duration = Duration(seconds: dur);
|
||||
_scrollCtrl.reset();
|
||||
await _scrollCtrl.forward().orCancel;
|
||||
}
|
||||
if (widget.animType == 'fade') {
|
||||
if (_disposed) break;
|
||||
await _fadeCtrl.forward().orCancel;
|
||||
if (_disposed) break;
|
||||
await Future.delayed(Duration(seconds: dur));
|
||||
if (_disposed) break;
|
||||
await _fadeCtrl.reverse().orCancel;
|
||||
} else {
|
||||
if (_disposed) break;
|
||||
_scrollCtrl.duration = Duration(seconds: dur);
|
||||
_scrollCtrl.reset();
|
||||
await _scrollCtrl.forward().orCancel;
|
||||
}
|
||||
|
||||
if (_disposed) break;
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_index = (_index + 1) % widget.texts.length;
|
||||
});
|
||||
if (_disposed) break;
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_index = (_index + 1) % widget.texts.length;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} on TickerCanceled {
|
||||
// Widget disposed while animation was running — exit cleanly
|
||||
} catch (e) {
|
||||
@@ -741,7 +971,6 @@ class _RunningTextWidgetState extends State<_RunningTextWidget>
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final text = widget.texts[_index];
|
||||
@@ -753,8 +982,7 @@ class _RunningTextWidgetState extends State<_RunningTextWidget>
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.info_outline,
|
||||
color: SacredColors.secondary, size: 16),
|
||||
Icon(Icons.info_outline, color: SacredColors.secondary, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Text(text, style: widget.style, maxLines: 1),
|
||||
],
|
||||
@@ -781,7 +1009,8 @@ class _RunningTextWidgetState extends State<_RunningTextWidget>
|
||||
const SizedBox(width: 8),
|
||||
Text(text, style: widget.style, maxLines: 1),
|
||||
const SizedBox(width: 80),
|
||||
Icon(Icons.circle, color: SacredColors.secondary.withValues(alpha: 0.4), size: 6),
|
||||
Icon(Icons.circle,
|
||||
color: SacredColors.secondary.withValues(alpha: 0.4), size: 6),
|
||||
const SizedBox(width: 80),
|
||||
],
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user