132 lines
4.1 KiB
Dart
132 lines
4.1 KiB
Dart
import 'dart:math';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:crypto/crypto.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:path_provider/path_provider.dart';
|
|
|
|
import 'unsplash_service.dart';
|
|
|
|
/// Persistent Unsplash cache.
|
|
/// Stores downloaded images by keyword so background can still render offline.
|
|
class UnsplashCacheService {
|
|
UnsplashCacheService._();
|
|
static final UnsplashCacheService instance = UnsplashCacheService._();
|
|
|
|
static const int _maxCachedImagesPerKeyword = 24;
|
|
static const Duration _requestTimeout = Duration(seconds: 10);
|
|
|
|
final Random _random = Random();
|
|
|
|
String _normalizeKeyword(String keyword) {
|
|
final normalized = keyword.trim().toLowerCase().replaceAll(RegExp(r'\s+'), '_');
|
|
final safe = normalized.replaceAll(RegExp(r'[^a-z0-9_\-]'), '');
|
|
return safe.isEmpty ? 'mosque' : safe;
|
|
}
|
|
|
|
Future<Directory> _cacheRootDirectory() async {
|
|
final baseDir = await getApplicationSupportDirectory();
|
|
final cacheDir = Directory('${baseDir.path}/unsplash_cache');
|
|
if (!cacheDir.existsSync()) {
|
|
await cacheDir.create(recursive: true);
|
|
}
|
|
return cacheDir;
|
|
}
|
|
|
|
Future<Directory> _keywordDirectory(String keyword) async {
|
|
final root = await _cacheRootDirectory();
|
|
final folderName = _normalizeKeyword(keyword);
|
|
final dir = Directory('${root.path}/$folderName');
|
|
if (!dir.existsSync()) {
|
|
await dir.create(recursive: true);
|
|
}
|
|
return dir;
|
|
}
|
|
|
|
String _filenameForUrl(String url) {
|
|
final digest = sha1.convert(utf8.encode(url)).toString();
|
|
return '$digest.jpg';
|
|
}
|
|
|
|
Future<List<File>> _listCachedFiles(String keyword) async {
|
|
final dir = await _keywordDirectory(keyword);
|
|
final entities = await dir.list().toList();
|
|
final files = entities.whereType<File>().where((file) {
|
|
final ext = file.path.split('.').last.toLowerCase();
|
|
return ext == 'jpg' || ext == 'jpeg' || ext == 'png' || ext == 'webp';
|
|
}).toList();
|
|
|
|
files.sort((a, b) {
|
|
final aTime = a.statSync().modified;
|
|
final bTime = b.statSync().modified;
|
|
return bTime.compareTo(aTime);
|
|
});
|
|
return files;
|
|
}
|
|
|
|
Future<void> _pruneKeywordCache(String keyword) async {
|
|
final files = await _listCachedFiles(keyword);
|
|
if (files.length <= _maxCachedImagesPerKeyword) return;
|
|
|
|
for (final file in files.skip(_maxCachedImagesPerKeyword)) {
|
|
try {
|
|
if (file.existsSync()) {
|
|
await file.delete();
|
|
}
|
|
} catch (_) {
|
|
// Keep best effort only.
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<String?> _downloadImage(String url, Directory destinationDir) async {
|
|
try {
|
|
final uri = Uri.tryParse(url);
|
|
if (uri == null) return null;
|
|
|
|
final response = await http.get(uri).timeout(_requestTimeout);
|
|
if (response.statusCode != 200 || response.bodyBytes.isEmpty) {
|
|
return null;
|
|
}
|
|
|
|
final file = File('${destinationDir.path}/${_filenameForUrl(url)}');
|
|
await file.writeAsBytes(response.bodyBytes, flush: true);
|
|
return file.path;
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// Returns local cached image paths for the given keyword.
|
|
Future<List<String>> getCachedImagePaths(String keyword) async {
|
|
final files = await _listCachedFiles(keyword);
|
|
return files.map((file) => file.path).toList(growable: false);
|
|
}
|
|
|
|
/// Fetches fresh Unsplash URLs and persists them as local files.
|
|
/// If fetch fails (offline), returns existing local cache.
|
|
Future<List<String>> refreshKeywordCache(
|
|
String keyword, {
|
|
int downloadCount = 12,
|
|
}) async {
|
|
final urls = await UnsplashService.instance.fetchLandscapeBackgrounds(keyword);
|
|
if (urls.isEmpty) {
|
|
return getCachedImagePaths(keyword);
|
|
}
|
|
|
|
final dir = await _keywordDirectory(keyword);
|
|
final shuffled = List<String>.from(urls)..shuffle(_random);
|
|
final targetCount = downloadCount.clamp(1, urls.length);
|
|
|
|
for (final url in shuffled.take(targetCount)) {
|
|
final path = '${dir.path}/${_filenameForUrl(url)}';
|
|
if (File(path).existsSync()) continue;
|
|
await _downloadImage(url, dir);
|
|
}
|
|
|
|
await _pruneKeywordCache(keyword);
|
|
return getCachedImagePaths(keyword);
|
|
}
|
|
}
|