worldhopper/lib/services/image_cache_service.dart
Felipe M. 01d39e58b3
feat: add server software type abstraction with Kavita support
Introduce a strategy pattern for server-specific operations (mark as
read/unread) with Kavita as the first concrete implementation. Generic
servers fall through to the existing PSE page-fetch fallback.

- Add ServerSoftwareType enum, service registry, and Kavita/Generic
  implementations with JWT authentication and token caching
- Add DioFactory with redirect-following interceptor for REST calls
- Extend OPDSServer model with softwareType/credentials fields and
  database migrations (v9, v10)
- Add server type selection and credential fields to add/edit screens
- Fix dispose-safety in both reader screens: capture syncProgress in
  deactivate(), add null guards, await markAsRead before deleting
  progress, and wrap detached futures in try-catch
- Respect syncProgressNotifierProvider user preference in markAsRead

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 20:39:32 +01:00

97 lines
2.8 KiB
Dart

import 'dart:io';
import 'package:dio/dio.dart';
import 'package:worldhopper/services/dio_factory.dart';
import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart';
/// Service for caching publication cover images locally
class ImageCacheService {
final Directory _imageDirectory;
ImageCacheService(this._imageDirectory);
static Future<ImageCacheService> create() async {
final appDir = await getApplicationDocumentsDirectory();
final imageDir = Directory('${appDir.path}/images');
// Create subdirectories
await Directory('${imageDir.path}/covers').create(recursive: true);
await Directory('${imageDir.path}/thumbnails').create(recursive: true);
return ImageCacheService(imageDir);
}
/// Download and cache a cover image
Future<String?> cacheCoverImage(String publicationId, String imageUrl) async {
try {
final dio = createDio();
final response = await dio.get(
imageUrl,
options: Options(responseType: ResponseType.bytes),
);
if (response.statusCode != 200) return null;
final file = File('${_imageDirectory.path}/covers/$publicationId.jpg');
await file.writeAsBytes(response.data as List<int>);
return file.path;
} catch (e) {
debugPrint('Error caching cover: $e');
return null;
}
}
/// Download and cache a thumbnail image
Future<String?> cacheThumbnailImage(
String publicationId, String imageUrl) async {
try {
final dio = createDio();
final response = await dio.get(
imageUrl,
options: Options(responseType: ResponseType.bytes),
);
if (response.statusCode != 200) return null;
final file =
File('${_imageDirectory.path}/thumbnails/$publicationId.jpg');
await file.writeAsBytes(response.data as List<int>);
return file.path;
} catch (e) {
debugPrint('Error caching thumbnail: $e');
return null;
}
}
/// Check if a local image file exists
bool hasLocalImage(String? path) {
if (path == null) return false;
return File(path).existsSync();
}
/// Delete cached images for a publication
Future<void> deleteCachedImages(String publicationId) async {
final coverFile = File('${_imageDirectory.path}/covers/$publicationId.jpg');
final thumbFile =
File('${_imageDirectory.path}/thumbnails/$publicationId.jpg');
if (await coverFile.exists()) await coverFile.delete();
if (await thumbFile.exists()) await thumbFile.delete();
}
/// Get cache directory size in bytes
Future<int> getCacheSize() async {
int totalSize = 0;
await for (final entity in _imageDirectory.list(recursive: true)) {
if (entity is File) {
totalSize += await entity.length();
}
}
return totalSize;
}
}