feat(offline-first): persist hijri+unsplash cache and scale secondary times
This commit is contained in:
@@ -537,6 +537,8 @@ class MainScreen extends ConsumerWidget {
|
||||
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,
|
||||
@@ -547,17 +549,19 @@ class MainScreen extends ConsumerWidget {
|
||||
Text(
|
||||
items[i].label.toUpperCase(),
|
||||
style: GoogleFonts.manrope(
|
||||
fontSize: 10 * fs,
|
||||
// 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),
|
||||
SizedBox(height: 4 * s * bodyScale),
|
||||
Text(
|
||||
items[i].time,
|
||||
style: GoogleFonts.plusJakartaSans(
|
||||
fontSize: 28 * fs,
|
||||
// Keep hierarchy smaller than bottom prayer cards, but follow the same scale percentage.
|
||||
fontSize: 28 * fs * bodyScale,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: SacredColors.onSurface,
|
||||
),
|
||||
@@ -569,7 +573,7 @@ class MainScreen extends ConsumerWidget {
|
||||
padding: EdgeInsets.symmetric(horizontal: 24 * s),
|
||||
child: Container(
|
||||
width: 1,
|
||||
height: 40 * s,
|
||||
height: 40 * s * bodyScale,
|
||||
color: SacredColors.outlineVariant.withValues(alpha: 0.3)),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../data/services/unsplash_service.dart';
|
||||
import '../../data/services/unsplash_cache_service.dart';
|
||||
import '../../providers.dart';
|
||||
|
||||
/// Renders a securely rotating background using Unsplash API.
|
||||
@@ -16,7 +17,7 @@ class UnsplashBackground extends ConsumerStatefulWidget {
|
||||
|
||||
class _UnsplashBackgroundState extends ConsumerState<UnsplashBackground> {
|
||||
final Random _rng = Random();
|
||||
List<String> _urls = [];
|
||||
List<String> _imagePaths = [];
|
||||
int _currentIndex = 0;
|
||||
Timer? _rotationTimer;
|
||||
Timer? _keywordDebounceTimer;
|
||||
@@ -38,26 +39,46 @@ class _UnsplashBackgroundState extends ConsumerState<UnsplashBackground> {
|
||||
_lastUseUnsplash = settings.useUnsplashBackground;
|
||||
|
||||
if (settings.useUnsplashBackground) {
|
||||
await _fetchImages(settings.unsplashKeyword, immediate: true);
|
||||
await _hydrateAndRefresh(settings.unsplashKeyword, immediate: true);
|
||||
}
|
||||
_startTimer(settings.unsplashRotationHours);
|
||||
}
|
||||
|
||||
Future<void> _fetchImages(String keyword, {bool immediate = false}) async {
|
||||
void _applyImagePaths(List<String> paths) {
|
||||
final next = paths.where((path) => path.trim().isNotEmpty).toList();
|
||||
if (next.isEmpty) return;
|
||||
|
||||
next.shuffle(_rng);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_imagePaths = next;
|
||||
_currentIndex = _rng.nextInt(_imagePaths.length);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _hydrateAndRefresh(
|
||||
String keyword, {
|
||||
bool immediate = false,
|
||||
}) async {
|
||||
_keywordDebounceTimer?.cancel();
|
||||
final requestId = ++_fetchNonce;
|
||||
|
||||
Future<void> runFetch() async {
|
||||
if (!ref.read(settingsProvider).useUnsplashBackground) return;
|
||||
final requestId = ++_fetchNonce;
|
||||
final urls = await UnsplashService.instance.fetchLandscapeBackgrounds(
|
||||
|
||||
final cachedPaths = await UnsplashCacheService.instance.getCachedImagePaths(
|
||||
keyword,
|
||||
);
|
||||
if (!mounted || requestId != _fetchNonce) return;
|
||||
if (urls.isNotEmpty) {
|
||||
urls.shuffle(_rng);
|
||||
setState(() {
|
||||
_urls = urls;
|
||||
_currentIndex = _rng.nextInt(_urls.length);
|
||||
});
|
||||
if (cachedPaths.isNotEmpty) {
|
||||
_applyImagePaths(cachedPaths);
|
||||
}
|
||||
|
||||
final refreshedPaths = await UnsplashCacheService.instance
|
||||
.refreshKeywordCache(keyword);
|
||||
if (!mounted || requestId != _fetchNonce) return;
|
||||
if (refreshedPaths.isNotEmpty) {
|
||||
_applyImagePaths(refreshedPaths);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,11 +96,11 @@ class _UnsplashBackgroundState extends ConsumerState<UnsplashBackground> {
|
||||
}
|
||||
|
||||
void _nextRandomImage() {
|
||||
if (_urls.isNotEmpty && mounted) {
|
||||
if (_urls.length <= 1) return;
|
||||
if (_imagePaths.isNotEmpty && mounted) {
|
||||
if (_imagePaths.length <= 1) return;
|
||||
var nextIndex = _currentIndex;
|
||||
while (nextIndex == _currentIndex) {
|
||||
nextIndex = _rng.nextInt(_urls.length);
|
||||
nextIndex = _rng.nextInt(_imagePaths.length);
|
||||
}
|
||||
setState(() => _currentIndex = nextIndex);
|
||||
}
|
||||
@@ -108,15 +129,15 @@ class _UnsplashBackgroundState extends ConsumerState<UnsplashBackground> {
|
||||
// Watch for config changes
|
||||
if (settings.useUnsplashBackground != _lastUseUnsplash) {
|
||||
_lastUseUnsplash = settings.useUnsplashBackground;
|
||||
if (settings.useUnsplashBackground && _urls.isEmpty) {
|
||||
_fetchImages(settings.unsplashKeyword, immediate: true);
|
||||
if (settings.useUnsplashBackground && _imagePaths.isEmpty) {
|
||||
_hydrateAndRefresh(settings.unsplashKeyword, immediate: true);
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.unsplashKeyword != _lastKeyword) {
|
||||
_lastKeyword = settings.unsplashKeyword;
|
||||
if (settings.useUnsplashBackground) {
|
||||
_fetchImages(settings.unsplashKeyword);
|
||||
_hydrateAndRefresh(settings.unsplashKeyword);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,16 +146,16 @@ class _UnsplashBackgroundState extends ConsumerState<UnsplashBackground> {
|
||||
_startTimer(settings.unsplashRotationHours);
|
||||
}
|
||||
|
||||
if (!settings.useUnsplashBackground || _urls.isEmpty) {
|
||||
if (!settings.useUnsplashBackground || _imagePaths.isEmpty) {
|
||||
return const SizedBox.shrink(); // Fallback to flat background handled underneath
|
||||
}
|
||||
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(seconds: 3),
|
||||
transitionBuilder: (child, animation) => FadeTransition(opacity: animation, child: child),
|
||||
child: Image.network(
|
||||
_urls[_currentIndex],
|
||||
key: ValueKey(_urls[_currentIndex]),
|
||||
child: Image.file(
|
||||
File(_imagePaths[_currentIndex]),
|
||||
key: ValueKey(_imagePaths[_currentIndex]),
|
||||
fit: BoxFit.cover,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
|
||||
Reference in New Issue
Block a user