170 lines
4.8 KiB
Dart
170 lines
4.8 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/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;
|
|
|
|
@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 _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(),
|
|
),
|
|
);
|
|
}
|
|
}
|