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_cache_service.dart'; import '../../providers.dart'; /// Renders a securely rotating background using Unsplash API. class UnsplashBackground extends ConsumerStatefulWidget { const UnsplashBackground({super.key}); @override ConsumerState createState() => _UnsplashBackgroundState(); } class _UnsplashBackgroundState extends ConsumerState { final Random _rng = Random(); List _imagePaths = []; int _currentIndex = 0; Timer? _rotationTimer; Timer? _keywordDebounceTimer; int _fetchNonce = 0; String? _lastKeyword; int? _lastRotationHours; bool? _lastUseUnsplash; @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 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 _hydrateAndRefresh( String keyword, { bool immediate = false, }) async { _keywordDebounceTimer?.cancel(); final requestId = ++_fetchNonce; Future 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 _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); // 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 (!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(), ), ); } }