feat(tv-ui): split pengumuman tab and refine main text-slide behavior

This commit is contained in:
dwindown
2026-04-03 22:03:18 +07:00
parent 14c3850092
commit af82418c09
6 changed files with 1523 additions and 517 deletions

View File

@@ -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;
});