feat: navigation store feat: implement OPDS entry caching system Add comprehensive caching for OPDS publications with local image storage and Dublin Core Terms metadata extraction. This enables offline viewing, fixes Library screen to show book titles and covers, and improves overall UX. - Add publications table with full metadata (title, authors, series, publisher, ISBN, language) - Implement local image cache service for covers and thumbnails - Extract DCTerms metadata (series, publisher, language, ISBN) from OPDS feeds - Link reading progress to cached publications - Update Library screen to display cached publication data and covers - Cache publications automatically when starting to read - Fix navigation stack issues by using context.push() instead of context.go() - Add explicit back button to EPUB reader with proper PopScope handling - Implement UNIQUE constraint on reading_progress to prevent duplicates - Save reading progress on screen dispose for both EPUB and image readers Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
95 lines
2.6 KiB
Dart
95 lines
2.6 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:dio/dio.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 = Dio();
|
|
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) {
|
|
print('Error caching cover: $e');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// Download and cache a thumbnail image
|
|
Future<String?> cacheThumbnailImage(
|
|
String publicationId, String imageUrl) async {
|
|
try {
|
|
final dio = Dio();
|
|
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) {
|
|
print('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;
|
|
}
|
|
}
|