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>
97 lines
2.8 KiB
Dart
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;
|
|
}
|
|
}
|