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 '../../core/enums.dart'; import '../../providers.dart'; import '../../data/local/models.dart'; import 'unsplash_background.dart'; import 'dart:io'; /// FORMAT HELPER: Duration → "HH:MM:SS" String _fmtDuration(Duration d) { final h = d.inHours.toString().padLeft(2, '0'); final m = (d.inMinutes % 60).toString().padLeft(2, '0'); final s = (d.inSeconds % 60).toString().padLeft(2, '0'); if (d.inHours > 0) return '$h:$m:$s'; return '$m:$s'; } /// The primary display — clock, prayer cards, countdown, marquee. class MainScreen extends ConsumerWidget { const MainScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final clock = ref.watch(clockProvider).valueOrNull ?? DateTime.now(); final runtimeSchedule = ref.watch(runtimeScheduleProvider); final schedule = runtimeSchedule.schedule; 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', 'id').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) 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 // ── Underlay 2: API Unsplash Landscape (fallback) ── const UnsplashBackground(), // ── Background radial tint overlay ── Positioned.fill( child: Container( decoration: BoxDecoration( gradient: RadialGradient( center: Alignment.center, radius: 0.8, colors: [ SacredColors.primary.withValues(alpha: 0.15), SacredColors.background, ], ), ), ), ), // ── Vignette ── Positioned.fill( child: DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ SacredColors.background.withValues(alpha: 0.8), Colors.transparent, SacredColors.background.withValues(alpha: 0.9), ], stops: const [0.0, 0.4, 1.0], ), ), ), ), // ── Main content column ── Padding( padding: EdgeInsets.symmetric(horizontal: 64 * s), child: Column( children: [ // ── HEADER ── _buildHeader( context, s, fs, settings, dateGregorian, dateHijri, showWaitingUpdateNotice: runtimeSchedule.isFallbackFromPreviousDay, inlineClockText: centerSlide.isPrimary ? null : '$timeStr$secStr', ), // ── CENTER: Clock + Countdown ── Expanded( child: Center( child: centerSlide.isPrimary ? _buildPrimaryCenter( s, fs, timeStr, secStr, screenData, schedule, settings, ) : _buildAnnouncementCenter( s, fs, settings, centerSlide, centerTextSlides, ), ), ), // ── FOOTER: Prayer Cards ── if (schedule != null) _buildPrayerCardsRow( s, fs, schedule, screenData, settings, clock), SizedBox(height: 16 * s), // ── MARQUEE ── _buildMarquee(s, fs, settings), SizedBox(height: 12 * s), ], ), ), ], ), ); } Widget _buildHeader(BuildContext context, double s, double fs, AppSettings settings, String dateGregorian, String dateHijri, {required bool showWaitingUpdateNotice, 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( children: [ // Left: Mosque name + address 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 * 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, ), ), ), ], ), ], ), ), ), 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), ), ), child: Text( inlineClockText, style: GoogleFonts.plusJakartaSans( fontSize: 30 * s * hScale, fontWeight: FontWeight.w800, color: SacredColors.onSurface, letterSpacing: -1 * s, ), ), ), ), ), if (showWaitingUpdateNotice) Expanded( flex: 2, child: Align( alignment: Alignment.centerRight, child: Container( margin: EdgeInsets.only(right: 16 * s), padding: EdgeInsets.symmetric( horizontal: 18 * s, vertical: 9 * s, ), decoration: BoxDecoration( color: SacredColors.secondary.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(SacredRadii.full), border: Border.all( color: SacredColors.secondary.withValues(alpha: 0.7), ), ), child: Text( 'Menunggu Update Data', style: GoogleFonts.plusJakartaSans( fontSize: 16 * s * hScale, fontWeight: FontWeight.w700, color: SacredColors.secondary, letterSpacing: 0.3 * 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: Padding( padding: EdgeInsets.all(11 * s * hScale), child: HugeIcon( icon: HugeIcons.strokeRoundedCalendar03, color: SacredColors.secondary, size: 16 * s * hScale, ), ), ), ], ), ), ), ], ), ); } _CenterSlideState _resolveCenterSlide({ required AppSettings settings, required int elapsedInMainWindowSec, required List 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, 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), if (schedule != null) Padding( padding: EdgeInsets.only(top: 8 * s), child: _buildSecondaryTimes(s, fs, schedule, settings), ), ], ); } Widget _buildAnnouncementCenter( double s, double fs, AppSettings settings, _CenterSlideState state, List 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( 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(SacredColors.primary), ), ), ), ], ), ), ), ), ); } Widget _buildCountdownPill(double s, double fs, ScreenStateData screenData) { return Container( padding: EdgeInsets.symmetric(horizontal: 24 * s, vertical: 8 * s), decoration: BoxDecoration( borderRadius: BorderRadius.circular(SacredRadii.full), border: Border.all( color: SacredColors.primary.withValues(alpha: 0.2), width: 1), color: SacredColors.primary.withValues(alpha: 0.05), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ // Pulsing dot _PulsingDot(color: SacredColors.secondary, size: 10 * s), SizedBox(width: 12 * s), Text( 'Menuju Adzan ${screenData.nextPrayer?.displayLabel(isFriday: screenData.isFriday) ?? ""}: ' '${_fmtDuration(screenData.timeUntilNext!)}', style: GoogleFonts.plusJakartaSans( fontSize: 20 * fs, fontWeight: FontWeight.w700, color: SacredColors.secondary, letterSpacing: 0.5 * s, ), ), ], ), ); } Widget _buildSecondaryTimes( double s, double fs, DailyPrayerSchedule schedule, AppSettings settings) { final items = <_SecondaryTimeItem>[]; if (settings.showImsak) { items.add(_SecondaryTimeItem('Imsak', schedule.imsak)); } if (settings.showTerbit) { items.add(_SecondaryTimeItem('Terbit', schedule.terbit)); } items.add(_SecondaryTimeItem('Dhuha', schedule.dhuha)); final labelScale = settings.scaleCardLabel; final bodyScale = settings.scaleCardBody; return Row( mainAxisSize: MainAxisSize.min, children: [ for (int i = 0; i < items.length; i++) ...[ Column( children: [ Text( items[i].label.toUpperCase(), style: GoogleFonts.manrope( // Keep hierarchy smaller than bottom prayer cards, but follow the same scale percentage. fontSize: 10 * fs * labelScale, fontWeight: FontWeight.w700, color: SacredColors.onSurfaceVariant, letterSpacing: 3 * s, ), ), SizedBox(height: 4 * s * bodyScale), Text( items[i].time, style: GoogleFonts.plusJakartaSans( // Keep hierarchy smaller than bottom prayer cards, but follow the same scale percentage. fontSize: 28 * fs * bodyScale, fontWeight: FontWeight.w600, color: SacredColors.onSurface, ), ), ], ), if (i < items.length - 1) ...[ Padding( padding: EdgeInsets.symmetric(horizontal: 24 * s), child: Container( width: 1, height: 40 * s * bodyScale, color: SacredColors.outlineVariant.withValues(alpha: 0.3)), ), ], ], ], ); } Widget _buildPrayerCardsRow(double s, double fs, DailyPrayerSchedule schedule, ScreenStateData screenData, AppSettings settings, DateTime clock) { final prayers = [ _PrayerCardData(PrayerName.subuh, schedule.subuh, 'Iqamah ${_addMinutes(schedule.subuh, settings.iqomahSubuh)}'), _PrayerCardData(PrayerName.dzuhur, schedule.dzuhur, 'Iqamah ${_addMinutes(schedule.dzuhur, settings.iqomahDzuhur)}'), _PrayerCardData(PrayerName.ashar, schedule.ashar, 'Iqamah ${_addMinutes(schedule.ashar, settings.iqomahAshar)}'), _PrayerCardData(PrayerName.maghrib, schedule.maghrib, 'Iqamah ${_addMinutes(schedule.maghrib, settings.iqomahMaghrib)}'), _PrayerCardData(PrayerName.isya, schedule.isya, 'Iqamah ${_addMinutes(schedule.isya, settings.iqomahIsya)}'), ]; return SizedBox( height: (140 * s * settings.scaleCardBody).clamp(110 * s, 240 * s), child: Row( children: [ for (int i = 0; i < prayers.length; i++) ...[ Expanded( child: _PrayerCard( data: prayers[i], isActive: screenData.nextPrayer == prayers[i].name, isFriday: screenData.isFriday, s: s, fs: fs, scaleLabel: settings.scaleCardLabel, scaleBody: settings.scaleCardBody, ), ), if (i < prayers.length - 1) SizedBox(width: 12 * s), ], ], ), ); } Widget _buildMarquee(double s, double fs, AppSettings settings) { final texts = settings.runningTexts; if (texts.isEmpty) return const SizedBox.shrink(); // Pad durations list to match texts length final durations = List.generate( texts.length, (i) => (i < settings.runningTextDurations.length) ? settings.runningTextDurations[i] : 12, ); return Container( width: double.infinity, height: 44 * s, decoration: BoxDecoration( color: SacredColors.background.withValues(alpha: 0.9), border: Border( top: BorderSide( color: SacredColors.surfaceContainerHighest.withValues(alpha: 0.1), ), ), ), child: ClipRect( child: _RunningTextWidget( texts: texts, durations: durations, animType: settings.marqueeAnimType, style: GoogleFonts.manrope( fontSize: 16 * fs * settings.scaleRunningText, fontWeight: FontWeight.w500, color: SacredColors.secondary, letterSpacing: 0.8 * s, ), ), ), ); } String _addMinutes(String time, int minutes) { final parts = time.split(':'); final h = int.parse(parts[0]); final m = int.parse(parts[1]); final dt = DateTime(2000, 1, 1, h, m).add(Duration(minutes: minutes)); return DateFormat('HH:mm').format(dt); } } // ─── 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; _SecondaryTimeItem(this.label, this.time); } class _PrayerCardData { final PrayerName name; final String time; final String iqomahLabel; _PrayerCardData(this.name, this.time, this.iqomahLabel); } class _PrayerCard extends StatelessWidget { final _PrayerCardData data; final bool isActive; 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 const _PrayerCard({ required this.data, required this.isActive, required this.isFriday, required this.s, required this.fs, this.scaleLabel = 1.0, this.scaleBody = 1.0, }); @override Widget build(BuildContext context) { final label = data.name.displayLabel(isFriday: isFriday); return AnimatedContainer( duration: const Duration(milliseconds: 300), padding: EdgeInsets.all(16 * s), decoration: BoxDecoration( color: isActive ? SacredColors.primaryContainer : SacredColors.surfaceContainerLow, borderRadius: BorderRadius.circular(SacredRadii.xl), border: isActive ? Border.all(color: SacredColors.primary, width: 2 * s) : null, boxShadow: isActive ? [ BoxShadow( color: SacredColors.primary.withValues(alpha: 0.1), blurRadius: 40 * s, ), ] : null, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( label.toUpperCase(), style: GoogleFonts.manrope( fontSize: 12 * fs * scaleLabel, fontWeight: FontWeight.w700, color: isActive ? SacredColors.onPrimaryContainer : SacredColors.onSurfaceVariant, letterSpacing: 2 * s, ), ), if (isActive) Icon(Icons.notifications_active, color: SacredColors.onPrimaryContainer, size: 16 * s), ], ), Text( data.time, style: GoogleFonts.plusJakartaSans( fontSize: 32 * fs * scaleBody, fontWeight: FontWeight.w700, color: isActive ? SacredColors.onPrimaryContainer : SacredColors.onSurface, ), ), Text( data.iqomahLabel, style: GoogleFonts.manrope( fontSize: 10 * fs * scaleBody, color: isActive ? SacredColors.onPrimaryContainer.withValues(alpha: 0.8) : SacredColors.onSurfaceVariant, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ); } } 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, ), ), ), ], ), ); } } // ─── Running Text Widget (marquee + fade modes) ─── class _RunningTextWidget extends StatefulWidget { final List texts; final List durations; // per-item seconds final String animType; // 'marquee' or 'fade' final TextStyle style; const _RunningTextWidget({ required this.texts, required this.durations, required this.animType, required this.style, }); @override State<_RunningTextWidget> createState() => _RunningTextWidgetState(); } class _RunningTextWidgetState extends State<_RunningTextWidget> with TickerProviderStateMixin { int _index = 0; bool _disposed = false; late AnimationController _fadeCtrl; late AnimationController _scrollCtrl; @override void initState() { super.initState(); _fadeCtrl = AnimationController( vsync: this, duration: const Duration(milliseconds: 600), ); _scrollCtrl = AnimationController(vsync: this); _startCycle(); } void _startCycle() async { try { 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 (_disposed) break; if (mounted) { setState(() { _index = (_index + 1) % widget.texts.length; }); } } } on TickerCanceled { // Widget disposed while animation was running — exit cleanly } catch (e) { if (!_disposed) rethrow; } } @override void dispose() { _disposed = true; _fadeCtrl.dispose(); _scrollCtrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final text = widget.texts[_index]; if (widget.animType == 'fade') { return Center( child: FadeTransition( opacity: _fadeCtrl, child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.info_outline, color: SacredColors.secondary, size: 16), const SizedBox(width: 8), Text(text, style: widget.style, maxLines: 1), ], ), ), ); } // Marquee mode return AnimatedBuilder( animation: _scrollCtrl, builder: (context, child) { final width = MediaQuery.of(context).size.width; final offset = _scrollCtrl.value * (width + 600); return Transform.translate( offset: Offset(width - offset, 0), child: child, ); }, child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.info_outline, color: SacredColors.secondary, size: 16), 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), const SizedBox(width: 80), ], ), ); } }