Files
jamshalat-masjid-screen/lib/features/home/unsplash_background.dart

225 lines
6.3 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../data/local/models.dart';
import '../../data/services/unsplash_cache_service.dart';
import '../../providers.dart';
/// Renders a securely rotating background using Unsplash API.
class UnsplashBackground extends ConsumerStatefulWidget {
const UnsplashBackground({super.key});
@override
ConsumerState<UnsplashBackground> createState() => _UnsplashBackgroundState();
}
class _UnsplashBackgroundState extends ConsumerState<UnsplashBackground> {
final Random _rng = Random();
List<String> _imagePaths = [];
int _currentIndex = 0;
Timer? _rotationTimer;
Timer? _keywordDebounceTimer;
int _fetchNonce = 0;
String? _lastKeyword;
int? _lastRotationHours;
bool? _lastUseUnsplash;
int _lastHandledRotateNonce = 0;
@override
void initState() {
super.initState();
_initDataAndTimer();
}
void _initDataAndTimer() async {
final settings = ref.read(settingsProvider);
_lastKeyword = settings.unsplashKeyword;
_lastRotationHours = settings.unsplashRotationHours;
_lastUseUnsplash = settings.useUnsplashBackground;
if (settings.useUnsplashBackground) {
await _hydrateAndRefresh(settings.unsplashKeyword, immediate: true);
}
_startTimer(settings.unsplashRotationHours);
}
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 cachedPaths = await UnsplashCacheService.instance.getCachedImagePaths(
keyword,
);
if (!mounted || requestId != _fetchNonce) return;
if (cachedPaths.isNotEmpty) {
_applyImagePaths(cachedPaths);
}
final refreshedPaths = await UnsplashCacheService.instance
.refreshKeywordCache(keyword);
if (!mounted || requestId != _fetchNonce) return;
if (refreshedPaths.isNotEmpty) {
_applyImagePaths(refreshedPaths);
}
}
if (immediate) {
await runFetch();
return;
}
_keywordDebounceTimer = Timer(
const Duration(milliseconds: 1200),
() {
unawaited(runFetch());
},
);
}
void _nextRandomImage() {
if (_imagePaths.isNotEmpty && mounted) {
if (_imagePaths.length <= 1) return;
var nextIndex = _currentIndex;
while (nextIndex == _currentIndex) {
nextIndex = _rng.nextInt(_imagePaths.length);
}
setState(() => _currentIndex = nextIndex);
}
}
void _showNextImage() {
if (_imagePaths.length <= 1 || !mounted) return;
setState(() {
_currentIndex = (_currentIndex + 1) % _imagePaths.length;
});
}
void _showPreviousImage() {
if (_imagePaths.length <= 1 || !mounted) return;
setState(() {
_currentIndex = (_currentIndex - 1 + _imagePaths.length) % _imagePaths.length;
});
}
Future<void> _handleManualRotate(
BackgroundRotateAction action,
AppSettings settings,
) async {
if (!settings.useUnsplashBackground) return;
if (_imagePaths.isEmpty) {
final cachedPaths = await UnsplashCacheService.instance.getCachedImagePaths(
settings.unsplashKeyword,
);
if (!mounted) return;
if (cachedPaths.isNotEmpty) {
_applyImagePaths(cachedPaths);
}
}
if (_imagePaths.isEmpty) return;
switch (action) {
case BackgroundRotateAction.next:
_showNextImage();
break;
case BackgroundRotateAction.previous:
_showPreviousImage();
break;
case BackgroundRotateAction.random:
_nextRandomImage();
break;
}
}
void _startTimer(int hours) {
_rotationTimer?.cancel();
if (hours <= 0) return;
_rotationTimer = Timer.periodic(Duration(hours: hours), (_) {
_nextRandomImage();
});
}
@override
void dispose() {
_rotationTimer?.cancel();
_keywordDebounceTimer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final settings = ref.watch(settingsProvider);
final rotateCommand = ref.watch(backgroundRotateCommandProvider);
// Watch for config changes
if (settings.useUnsplashBackground != _lastUseUnsplash) {
_lastUseUnsplash = settings.useUnsplashBackground;
if (settings.useUnsplashBackground && _imagePaths.isEmpty) {
_hydrateAndRefresh(settings.unsplashKeyword, immediate: true);
}
}
if (settings.unsplashKeyword != _lastKeyword) {
_lastKeyword = settings.unsplashKeyword;
if (settings.useUnsplashBackground) {
_hydrateAndRefresh(settings.unsplashKeyword);
}
}
if (settings.unsplashRotationHours != _lastRotationHours) {
_lastRotationHours = settings.unsplashRotationHours;
_startTimer(settings.unsplashRotationHours);
}
if (rotateCommand.nonce != _lastHandledRotateNonce) {
_lastHandledRotateNonce = rotateCommand.nonce;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_handleManualRotate(rotateCommand.action, settings);
});
}
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.file(
File(_imagePaths[_currentIndex]),
key: ValueKey(_imagePaths[_currentIndex]),
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
// Soft opacity behind the MainScreen's dark glass vignette
color: Colors.black.withValues(alpha: 0.5),
colorBlendMode: BlendMode.darken,
errorBuilder: (_, __, ___) => const SizedBox.shrink(),
),
);
}
}