feat(tv-ui): split pengumuman tab and refine main text-slide behavior
This commit is contained in:
@@ -50,15 +50,19 @@ class AppSettings extends HiveObject {
|
||||
|
||||
// Blank screen durations
|
||||
@HiveField(12)
|
||||
int blankScreenNormal; // minutes
|
||||
int blankScreenNormal; // minutes
|
||||
|
||||
@HiveField(13)
|
||||
int blankScreenJumat; // minutes
|
||||
int blankScreenJumat; // minutes
|
||||
|
||||
// Running text items
|
||||
@HiveField(14)
|
||||
List<String> runningTexts;
|
||||
|
||||
// Center text-slide items (separate from running ticker at bottom).
|
||||
@HiveField(35)
|
||||
List<String> textSlides;
|
||||
|
||||
// Friday officers
|
||||
@HiveField(15)
|
||||
String khatibName;
|
||||
@@ -81,6 +85,14 @@ class AppSettings extends HiveObject {
|
||||
@HiveField(32)
|
||||
String? lastAutoSyncAttemptDate;
|
||||
|
||||
// Center hero block duration on main screen (seconds).
|
||||
@HiveField(33)
|
||||
int mainCenterSlideDurationSec;
|
||||
|
||||
// Per-text announcement slide duration on main screen (seconds).
|
||||
@HiveField(34)
|
||||
int announcementSlideDurationSec;
|
||||
|
||||
// Slideshow image paths (local)
|
||||
@HiveField(20)
|
||||
List<String> slideshowImages;
|
||||
@@ -128,6 +140,14 @@ class AppSettings extends HiveObject {
|
||||
@HiveField(31)
|
||||
int hijriOffsetDays;
|
||||
|
||||
// Group: Top header (identity + date on upper area)
|
||||
@HiveField(36)
|
||||
double scaleTopHeader;
|
||||
|
||||
// Group: Center text slides (pengumuman in main area)
|
||||
@HiveField(37)
|
||||
double scaleTextSlideCenter;
|
||||
|
||||
AppSettings({
|
||||
this.masjidName = 'Masjid Al-Ikhlas',
|
||||
this.masjidAddress = 'Jl. Kebaikan No. 1',
|
||||
@@ -147,12 +167,18 @@ class AppSettings extends HiveObject {
|
||||
'Mohon luruskan dan rapatkan shaf',
|
||||
'Kajian rutin setiap Ahad pagi',
|
||||
],
|
||||
this.textSlides = const [
|
||||
'Mohon luruskan dan rapatkan shaf',
|
||||
'Kajian rutin setiap Ahad pagi',
|
||||
],
|
||||
this.khatibName = 'Ust. Fulan, S.Ag',
|
||||
this.imamName = 'Ust. Alan, Lc',
|
||||
this.mainScreenDurationSec = 15,
|
||||
this.slideDurationSec = 10,
|
||||
this.lastSyncDate,
|
||||
this.lastAutoSyncAttemptDate,
|
||||
this.mainCenterSlideDurationSec = 10,
|
||||
this.announcementSlideDurationSec = 7,
|
||||
this.slideshowImages = const [],
|
||||
this.textScaleIndex = 1,
|
||||
this.useUnsplashBackground = false,
|
||||
@@ -165,6 +191,8 @@ class AppSettings extends HiveObject {
|
||||
this.scaleCardBody = 1.0,
|
||||
this.scaleRunningText = 1.0,
|
||||
this.hijriOffsetDays = 0,
|
||||
this.scaleTopHeader = 1.0,
|
||||
this.scaleTextSlideCenter = 1.0,
|
||||
});
|
||||
|
||||
AppSettings copyWith({
|
||||
@@ -183,12 +211,15 @@ class AppSettings extends HiveObject {
|
||||
int? blankScreenNormal,
|
||||
int? blankScreenJumat,
|
||||
List<String>? runningTexts,
|
||||
List<String>? textSlides,
|
||||
String? khatibName,
|
||||
String? imamName,
|
||||
int? mainScreenDurationSec,
|
||||
int? slideDurationSec,
|
||||
String? lastSyncDate,
|
||||
String? lastAutoSyncAttemptDate,
|
||||
int? mainCenterSlideDurationSec,
|
||||
int? announcementSlideDurationSec,
|
||||
List<String>? slideshowImages,
|
||||
int? textScaleIndex,
|
||||
bool? useUnsplashBackground,
|
||||
@@ -201,6 +232,8 @@ class AppSettings extends HiveObject {
|
||||
double? scaleCardBody,
|
||||
double? scaleRunningText,
|
||||
int? hijriOffsetDays,
|
||||
double? scaleTopHeader,
|
||||
double? scaleTextSlideCenter,
|
||||
}) {
|
||||
return AppSettings(
|
||||
masjidName: masjidName ?? this.masjidName,
|
||||
@@ -218,18 +251,26 @@ class AppSettings extends HiveObject {
|
||||
blankScreenNormal: blankScreenNormal ?? this.blankScreenNormal,
|
||||
blankScreenJumat: blankScreenJumat ?? this.blankScreenJumat,
|
||||
runningTexts: runningTexts ?? this.runningTexts,
|
||||
textSlides: textSlides ?? this.textSlides,
|
||||
khatibName: khatibName ?? this.khatibName,
|
||||
imamName: imamName ?? this.imamName,
|
||||
mainScreenDurationSec: mainScreenDurationSec ?? this.mainScreenDurationSec,
|
||||
mainScreenDurationSec:
|
||||
mainScreenDurationSec ?? this.mainScreenDurationSec,
|
||||
slideDurationSec: slideDurationSec ?? this.slideDurationSec,
|
||||
lastSyncDate: lastSyncDate ?? this.lastSyncDate,
|
||||
lastAutoSyncAttemptDate:
|
||||
lastAutoSyncAttemptDate ?? this.lastAutoSyncAttemptDate,
|
||||
mainCenterSlideDurationSec:
|
||||
mainCenterSlideDurationSec ?? this.mainCenterSlideDurationSec,
|
||||
announcementSlideDurationSec:
|
||||
announcementSlideDurationSec ?? this.announcementSlideDurationSec,
|
||||
slideshowImages: slideshowImages ?? this.slideshowImages,
|
||||
textScaleIndex: textScaleIndex ?? this.textScaleIndex,
|
||||
useUnsplashBackground: useUnsplashBackground ?? this.useUnsplashBackground,
|
||||
useUnsplashBackground:
|
||||
useUnsplashBackground ?? this.useUnsplashBackground,
|
||||
unsplashKeyword: unsplashKeyword ?? this.unsplashKeyword,
|
||||
unsplashRotationHours: unsplashRotationHours ?? this.unsplashRotationHours,
|
||||
unsplashRotationHours:
|
||||
unsplashRotationHours ?? this.unsplashRotationHours,
|
||||
brandedBgImage: brandedBgImage ?? this.brandedBgImage,
|
||||
runningTextDurations: runningTextDurations ?? this.runningTextDurations,
|
||||
marqueeAnimType: marqueeAnimType ?? this.marqueeAnimType,
|
||||
@@ -237,6 +278,9 @@ class AppSettings extends HiveObject {
|
||||
scaleCardBody: scaleCardBody ?? this.scaleCardBody,
|
||||
scaleRunningText: scaleRunningText ?? this.scaleRunningText,
|
||||
hijriOffsetDays: hijriOffsetDays ?? this.hijriOffsetDays,
|
||||
scaleTopHeader: scaleTopHeader ?? this.scaleTopHeader,
|
||||
scaleTextSlideCenter:
|
||||
scaleTextSlideCenter ?? this.scaleTextSlideCenter,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -253,6 +297,7 @@ class AppSettingsAdapter extends TypeAdapter<AppSettings> {
|
||||
for (int i = 0; i < numOfFields; i++) {
|
||||
fields[reader.readByte()] = reader.read();
|
||||
}
|
||||
final runningTexts = (fields[14] as List?)?.cast<String>() ?? const [];
|
||||
return AppSettings(
|
||||
masjidName: fields[0] as String? ?? 'Masjid Al-Ikhlas',
|
||||
masjidAddress: fields[1] as String? ?? 'Jl. Kebaikan No. 1',
|
||||
@@ -268,13 +313,17 @@ class AppSettingsAdapter extends TypeAdapter<AppSettings> {
|
||||
preAdzanLead: fields[11] as int? ?? 10,
|
||||
blankScreenNormal: fields[12] as int? ?? 15,
|
||||
blankScreenJumat: fields[13] as int? ?? 45,
|
||||
runningTexts: (fields[14] as List?)?.cast<String>() ?? const [],
|
||||
runningTexts: runningTexts,
|
||||
textSlides:
|
||||
(fields[35] as List?)?.cast<String>() ?? List<String>.from(runningTexts),
|
||||
khatibName: fields[15] as String? ?? '',
|
||||
imamName: fields[16] as String? ?? '',
|
||||
mainScreenDurationSec: fields[17] as int? ?? 15,
|
||||
slideDurationSec: fields[18] as int? ?? 10,
|
||||
lastSyncDate: fields[19] as String?,
|
||||
lastAutoSyncAttemptDate: fields[32] as String?,
|
||||
mainCenterSlideDurationSec: fields[33] as int? ?? 10,
|
||||
announcementSlideDurationSec: fields[34] as int? ?? 7,
|
||||
slideshowImages: (fields[20] as List?)?.cast<String>() ?? const [],
|
||||
textScaleIndex: fields[21] as int? ?? 1,
|
||||
useUnsplashBackground: fields[22] as bool? ?? false,
|
||||
@@ -287,46 +336,91 @@ class AppSettingsAdapter extends TypeAdapter<AppSettings> {
|
||||
scaleCardBody: (fields[29] as num?)?.toDouble() ?? 1.0,
|
||||
scaleRunningText: (fields[30] as num?)?.toDouble() ?? 1.0,
|
||||
hijriOffsetDays: fields[31] as int? ?? 0,
|
||||
scaleTopHeader: (fields[36] as num?)?.toDouble() ?? 1.0,
|
||||
scaleTextSlideCenter: (fields[37] as num?)?.toDouble() ?? 1.0,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, AppSettings obj) {
|
||||
writer
|
||||
..writeByte(38)
|
||||
..writeByte(0)
|
||||
..write(obj.masjidName)
|
||||
..writeByte(1)
|
||||
..write(obj.masjidAddress)
|
||||
..writeByte(2)
|
||||
..write(obj.cityIdApi)
|
||||
..writeByte(3)
|
||||
..write(obj.cityDisplayName)
|
||||
..writeByte(4)
|
||||
..write(obj.showImsak)
|
||||
..writeByte(5)
|
||||
..write(obj.showTerbit)
|
||||
..writeByte(6)
|
||||
..write(obj.iqomahSubuh)
|
||||
..writeByte(7)
|
||||
..write(obj.iqomahDzuhur)
|
||||
..writeByte(8)
|
||||
..write(obj.iqomahAshar)
|
||||
..writeByte(9)
|
||||
..write(obj.iqomahMaghrib)
|
||||
..writeByte(10)
|
||||
..write(obj.iqomahIsya)
|
||||
..writeByte(11)
|
||||
..write(obj.preAdzanLead)
|
||||
..writeByte(12)
|
||||
..write(obj.blankScreenNormal)
|
||||
..writeByte(13)
|
||||
..write(obj.blankScreenJumat)
|
||||
..writeByte(14)
|
||||
..write(obj.runningTexts)
|
||||
..writeByte(35)
|
||||
..write(obj.textSlides)
|
||||
..writeByte(15)
|
||||
..write(obj.khatibName)
|
||||
..writeByte(16)
|
||||
..write(obj.imamName)
|
||||
..writeByte(17)
|
||||
..write(obj.mainScreenDurationSec)
|
||||
..writeByte(18)
|
||||
..write(obj.slideDurationSec)
|
||||
..writeByte(19)
|
||||
..write(obj.lastSyncDate)
|
||||
..writeByte(32)
|
||||
..write(obj.lastAutoSyncAttemptDate)
|
||||
..writeByte(33)
|
||||
..writeByte(0)..write(obj.masjidName)
|
||||
..writeByte(1)..write(obj.masjidAddress)
|
||||
..writeByte(2)..write(obj.cityIdApi)
|
||||
..writeByte(3)..write(obj.cityDisplayName)
|
||||
..writeByte(4)..write(obj.showImsak)
|
||||
..writeByte(5)..write(obj.showTerbit)
|
||||
..writeByte(6)..write(obj.iqomahSubuh)
|
||||
..writeByte(7)..write(obj.iqomahDzuhur)
|
||||
..writeByte(8)..write(obj.iqomahAshar)
|
||||
..writeByte(9)..write(obj.iqomahMaghrib)
|
||||
..writeByte(10)..write(obj.iqomahIsya)
|
||||
..writeByte(11)..write(obj.preAdzanLead)
|
||||
..writeByte(12)..write(obj.blankScreenNormal)
|
||||
..writeByte(13)..write(obj.blankScreenJumat)
|
||||
..writeByte(14)..write(obj.runningTexts)
|
||||
..writeByte(15)..write(obj.khatibName)
|
||||
..writeByte(16)..write(obj.imamName)
|
||||
..writeByte(17)..write(obj.mainScreenDurationSec)
|
||||
..writeByte(18)..write(obj.slideDurationSec)
|
||||
..writeByte(19)..write(obj.lastSyncDate)
|
||||
..writeByte(32)..write(obj.lastAutoSyncAttemptDate)
|
||||
..writeByte(20)..write(obj.slideshowImages)
|
||||
..writeByte(21)..write(obj.textScaleIndex)
|
||||
..writeByte(22)..write(obj.useUnsplashBackground)
|
||||
..writeByte(23)..write(obj.unsplashKeyword)
|
||||
..writeByte(24)..write(obj.unsplashRotationHours)
|
||||
..writeByte(25)..write(obj.brandedBgImage)
|
||||
..writeByte(26)..write(obj.runningTextDurations)
|
||||
..writeByte(27)..write(obj.marqueeAnimType)
|
||||
..writeByte(28)..write(obj.scaleCardLabel)
|
||||
..writeByte(29)..write(obj.scaleCardBody)
|
||||
..writeByte(30)..write(obj.scaleRunningText)
|
||||
..writeByte(31)..write(obj.hijriOffsetDays);
|
||||
..write(obj.mainCenterSlideDurationSec)
|
||||
..writeByte(34)
|
||||
..write(obj.announcementSlideDurationSec)
|
||||
..writeByte(20)
|
||||
..write(obj.slideshowImages)
|
||||
..writeByte(21)
|
||||
..write(obj.textScaleIndex)
|
||||
..writeByte(22)
|
||||
..write(obj.useUnsplashBackground)
|
||||
..writeByte(23)
|
||||
..write(obj.unsplashKeyword)
|
||||
..writeByte(24)
|
||||
..write(obj.unsplashRotationHours)
|
||||
..writeByte(25)
|
||||
..write(obj.brandedBgImage)
|
||||
..writeByte(26)
|
||||
..write(obj.runningTextDurations)
|
||||
..writeByte(27)
|
||||
..write(obj.marqueeAnimType)
|
||||
..writeByte(28)
|
||||
..write(obj.scaleCardLabel)
|
||||
..writeByte(29)
|
||||
..write(obj.scaleCardBody)
|
||||
..writeByte(30)
|
||||
..write(obj.scaleRunningText)
|
||||
..writeByte(31)
|
||||
..write(obj.hijriOffsetDays)
|
||||
..writeByte(36)
|
||||
..write(obj.scaleTopHeader)
|
||||
..writeByte(37)
|
||||
..write(obj.scaleTextSlideCenter);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,14 +520,23 @@ class DailyPrayerScheduleAdapter extends TypeAdapter<DailyPrayerSchedule> {
|
||||
void write(BinaryWriter writer, DailyPrayerSchedule obj) {
|
||||
writer
|
||||
..writeByte(9)
|
||||
..writeByte(0)..write(obj.date)
|
||||
..writeByte(1)..write(obj.imsak)
|
||||
..writeByte(2)..write(obj.subuh)
|
||||
..writeByte(3)..write(obj.terbit)
|
||||
..writeByte(4)..write(obj.dhuha)
|
||||
..writeByte(5)..write(obj.dzuhur)
|
||||
..writeByte(6)..write(obj.ashar)
|
||||
..writeByte(7)..write(obj.maghrib)
|
||||
..writeByte(8)..write(obj.isya);
|
||||
..writeByte(0)
|
||||
..write(obj.date)
|
||||
..writeByte(1)
|
||||
..write(obj.imsak)
|
||||
..writeByte(2)
|
||||
..write(obj.subuh)
|
||||
..writeByte(3)
|
||||
..write(obj.terbit)
|
||||
..writeByte(4)
|
||||
..write(obj.dhuha)
|
||||
..writeByte(5)
|
||||
..write(obj.dzuhur)
|
||||
..writeByte(6)
|
||||
..write(obj.ashar)
|
||||
..writeByte(7)
|
||||
..write(obj.maghrib)
|
||||
..writeByte(8)
|
||||
..write(obj.isya);
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -59,10 +59,13 @@ class SettingsNotifier extends StateNotifier<AppSettings> {
|
||||
final textScaleProvider = Provider<double>((ref) {
|
||||
final index = ref.watch(settingsProvider.select((s) => s.textScaleIndex));
|
||||
switch (index) {
|
||||
case 0: return 0.85; // Small
|
||||
case 2: return 1.15; // Large
|
||||
case 0:
|
||||
return 0.85; // Small
|
||||
case 2:
|
||||
return 1.15; // Large
|
||||
case 1:
|
||||
default: return 1.0; // Medium
|
||||
default:
|
||||
return 1.0; // Medium
|
||||
}
|
||||
});
|
||||
|
||||
@@ -103,11 +106,11 @@ final hijriDateProvider = FutureProvider<String>((ref) async {
|
||||
/// Computed state that tells the UI which screen to display.
|
||||
class ScreenStateData {
|
||||
final ScreenState state;
|
||||
final PrayerName? activePrayer; // Current or next prayer
|
||||
final PrayerName? activePrayer; // Current or next prayer
|
||||
final PrayerName? nextPrayer;
|
||||
final Duration? timeUntilNext; // Countdown to next prayer time
|
||||
final Duration? iqomahRemaining; // Countdown during iqomah state
|
||||
final Duration? blankRemaining; // Countdown during shalat/blank state
|
||||
final Duration? timeUntilNext; // Countdown to next prayer time
|
||||
final Duration? iqomahRemaining; // Countdown during iqomah state
|
||||
final Duration? blankRemaining; // Countdown during shalat/blank state
|
||||
final bool isFriday;
|
||||
final DateTime now;
|
||||
|
||||
@@ -151,12 +154,18 @@ final screenStateProvider = Provider<ScreenStateData>((ref) {
|
||||
|
||||
int iqomahMinutes(PrayerName p) {
|
||||
switch (p) {
|
||||
case PrayerName.subuh: return settings.iqomahSubuh;
|
||||
case PrayerName.dzuhur: return settings.iqomahDzuhur;
|
||||
case PrayerName.ashar: return settings.iqomahAshar;
|
||||
case PrayerName.maghrib: return settings.iqomahMaghrib;
|
||||
case PrayerName.isya: return settings.iqomahIsya;
|
||||
default: return 10;
|
||||
case PrayerName.subuh:
|
||||
return settings.iqomahSubuh;
|
||||
case PrayerName.dzuhur:
|
||||
return settings.iqomahDzuhur;
|
||||
case PrayerName.ashar:
|
||||
return settings.iqomahAshar;
|
||||
case PrayerName.maghrib:
|
||||
return settings.iqomahMaghrib;
|
||||
case PrayerName.isya:
|
||||
return settings.iqomahIsya;
|
||||
default:
|
||||
return 10;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,8 +181,7 @@ final screenStateProvider = Provider<ScreenStateData>((ref) {
|
||||
adzanTime.subtract(Duration(minutes: settings.preAdzanLead));
|
||||
final iqomahDuration = Duration(minutes: iqomahMinutes(prayer.key));
|
||||
final iqomahEnd = adzanTime.add(iqomahDuration);
|
||||
final blankEnd =
|
||||
iqomahEnd.add(Duration(minutes: blankMinutes()));
|
||||
final blankEnd = iqomahEnd.add(Duration(minutes: blankMinutes()));
|
||||
|
||||
// STATE: SHALAT (Black Screen)
|
||||
if (clock.isAfter(iqomahEnd) && clock.isBefore(blankEnd)) {
|
||||
@@ -246,6 +254,9 @@ final screenStateProvider = Provider<ScreenStateData>((ref) {
|
||||
// ROTATION PROVIDER (for Normal state slideshow)
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
/// Elapsed seconds in the active rotation phase (main/slideshow).
|
||||
final rotationElapsedProvider = StateProvider<int>((ref) => 0);
|
||||
|
||||
/// Controls the rotation between main screen and slideshow views.
|
||||
final rotationIndexProvider =
|
||||
StateNotifierProvider<RotationNotifier, int>((ref) {
|
||||
@@ -264,36 +275,59 @@ class RotationNotifier extends StateNotifier<int> {
|
||||
void _startRotation() {
|
||||
_timer?.cancel();
|
||||
_elapsed = 0;
|
||||
_ref.read(rotationElapsedProvider.notifier).state = 0;
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||
final screenState = _ref.read(screenStateProvider);
|
||||
if (screenState.state != ScreenState.normal) {
|
||||
// Don't rotate during special states, reset elapsed
|
||||
_elapsed = 0;
|
||||
_ref.read(rotationElapsedProvider.notifier).state = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
_elapsed++;
|
||||
final settings = _ref.read(settingsProvider);
|
||||
final validSlides = settings.slideshowImages.where((i) => i.trim().isNotEmpty).toList();
|
||||
final validSlides =
|
||||
settings.slideshowImages.where((i) => i.trim().isNotEmpty).toList();
|
||||
final hasContent = validSlides.isNotEmpty;
|
||||
|
||||
_elapsed++;
|
||||
if (!hasContent) {
|
||||
_elapsed = 0;
|
||||
final duration = _resolveMainPhaseDuration(settings);
|
||||
if (_elapsed >= duration) {
|
||||
_elapsed = 0;
|
||||
}
|
||||
_ref.read(rotationElapsedProvider.notifier).state = _elapsed;
|
||||
if (state != 0) state = 0; // force main screen state
|
||||
return;
|
||||
}
|
||||
|
||||
final isMainScreen = state % 2 == 0;
|
||||
final duration = isMainScreen
|
||||
? settings.mainScreenDurationSec
|
||||
: settings.slideDurationSec;
|
||||
? _resolveMainPhaseDuration(settings)
|
||||
: settings.slideDurationSec.clamp(1, 600);
|
||||
|
||||
if (_elapsed >= duration) {
|
||||
_elapsed = 0;
|
||||
state = state + 1;
|
||||
}
|
||||
_ref.read(rotationElapsedProvider.notifier).state = _elapsed;
|
||||
});
|
||||
}
|
||||
|
||||
int _resolveMainPhaseDuration(AppSettings settings) {
|
||||
final centerSlides = settings.textSlides
|
||||
.map((text) => text.trim())
|
||||
.where((text) => text.isNotEmpty)
|
||||
.toList();
|
||||
if (centerSlides.isEmpty) {
|
||||
return settings.mainScreenDurationSec.clamp(1, 600);
|
||||
}
|
||||
|
||||
final heroDuration = settings.mainCenterSlideDurationSec.clamp(1, 600);
|
||||
final perAnnouncement = settings.announcementSlideDurationSec.clamp(1, 600);
|
||||
return heroDuration + (perAnnouncement * centerSlides.length);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
@@ -306,11 +340,14 @@ class RotationNotifier extends StateNotifier<int> {
|
||||
/// Unsplash background is disabled — no point rotating to an empty slide.
|
||||
final isMainScreenProvider = Provider<bool>((ref) {
|
||||
final settings = ref.watch(settingsProvider);
|
||||
final validSlides = settings.slideshowImages.where((i) => i.trim().isNotEmpty).toList();
|
||||
// Keep rotation notifier alive even when slideshow media is empty,
|
||||
// because main-screen text slides depend on rotation elapsed time.
|
||||
final index = ref.watch(rotationIndexProvider);
|
||||
final validSlides =
|
||||
settings.slideshowImages.where((i) => i.trim().isNotEmpty).toList();
|
||||
final hasContent = validSlides.isNotEmpty;
|
||||
if (!hasContent) return true; // always stay on main screen
|
||||
|
||||
final index = ref.watch(rotationIndexProvider);
|
||||
// Even = main, Odd = slideshow
|
||||
return index % 2 == 0;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user