import 'dart:async'; import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../data/services/unsplash_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 _urls = []; 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 _fetchImages(settings.unsplashKeyword, immediate: true); } _startTimer(settings.unsplashRotationHours); } Future _fetchImages(String keyword, {bool immediate = false}) async { _keywordDebounceTimer?.cancel(); Future runFetch() async { if (!ref.read(settingsProvider).useUnsplashBackground) return; final requestId = ++_fetchNonce; final urls = await UnsplashService.instance.fetchLandscapeBackgrounds( keyword, ); if (!mounted || requestId != _fetchNonce) return; if (urls.isNotEmpty) { urls.shuffle(_rng); setState(() { _urls = urls; _currentIndex = _rng.nextInt(_urls.length); }); } } if (immediate) { await runFetch(); return; } _keywordDebounceTimer = Timer( const Duration(milliseconds: 1200), () { unawaited(runFetch()); }, ); } void _nextRandomImage() { if (_urls.isNotEmpty && mounted) { if (_urls.length <= 1) return; var nextIndex = _currentIndex; while (nextIndex == _currentIndex) { nextIndex = _rng.nextInt(_urls.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 && _urls.isEmpty) { _fetchImages(settings.unsplashKeyword, immediate: true); } } if (settings.unsplashKeyword != _lastKeyword) { _lastKeyword = settings.unsplashKeyword; if (settings.useUnsplashBackground) { _fetchImages(settings.unsplashKeyword); } } if (settings.unsplashRotationHours != _lastRotationHours) { _lastRotationHours = settings.unsplashRotationHours; _startTimer(settings.unsplashRotationHours); } if (!settings.useUnsplashBackground || _urls.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]), 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(), ), ); } }