Rename all OPDS-prefixed models to generic names (Server, Feed, Entry, Link, StreamLink), expand the ServerSoftware interface to cover all server interactions, and implement a full Kavita REST API client that replaces the OPDS delegation. - Rename OPDS* models to generic names across ~60 files - Add database migrations 13 (opds_id → entry_id) and 14 (unify credentials) - Create OPDSServerSoftware wrapping existing OPDS services - Create KavitaApiClient for direct Kavita REST API calls - Create KavitaFeedMapper to convert Kavita JSON to Feed/Entry models - Rewrite KavitaServerSoftware to use native API (no OPDS delegation) - Unify server credentials (remove softwareUsername/softwarePassword) - Simplify add/edit server UI to single auth section - Add test connection button to server add/edit screens - Add progress indicator to PublicationCard using local and server data - Eliminate softwareType branching in reader screens - Add EntryProgress and fetchEntryProgress to ServerSoftware interface - Fix Entry.acquisitionLink crash on empty links - Add 59 new tests covering models, services, and providers
151 lines
4.9 KiB
Dart
151 lines
4.9 KiB
Dart
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
import 'package:uuid/uuid.dart';
|
|
import 'package:worldhopper/database/database.dart';
|
|
import 'package:worldhopper/models/enhanced_metadata.dart';
|
|
import 'package:worldhopper/models/entry.dart';
|
|
import 'package:worldhopper/models/publication.dart';
|
|
import 'package:worldhopper/repositories/publication_repository.dart';
|
|
import 'package:worldhopper/repositories/reading_progress_repository.dart';
|
|
import 'package:worldhopper/services/image_cache_service.dart';
|
|
|
|
part 'publication_cache_provider.g.dart';
|
|
|
|
/// Provider for PublicationRepository instance
|
|
@riverpod
|
|
PublicationRepository publicationRepository(PublicationRepositoryRef ref) {
|
|
return PublicationRepository(DatabaseHelper.instance);
|
|
}
|
|
|
|
/// Provider for ImageCacheService instance
|
|
@riverpod
|
|
Future<ImageCacheService> imageCacheService(ImageCacheServiceRef ref) async {
|
|
return await ImageCacheService.create();
|
|
}
|
|
|
|
/// Provider for ReadingProgressRepository instance (if not already provided elsewhere)
|
|
@riverpod
|
|
ReadingProgressRepository readingProgressRepository(
|
|
ReadingProgressRepositoryRef ref) {
|
|
return ReadingProgressRepository();
|
|
}
|
|
|
|
/// Provider for publication cache operations
|
|
@riverpod
|
|
class PublicationCache extends _$PublicationCache {
|
|
@override
|
|
FutureOr<void> build() {}
|
|
|
|
/// Cache a publication from OPDS entry
|
|
Future<Publication> cachePublication({
|
|
required String serverId,
|
|
required Entry entry,
|
|
required EnhancedMetadata metadata,
|
|
bool downloadImages = true,
|
|
}) async {
|
|
final repository = ref.read(publicationRepositoryProvider);
|
|
final imageService = await ref.read(imageCacheServiceProvider.future);
|
|
|
|
// Check if already cached
|
|
final existing = await repository.getPublication(serverId, entry.id);
|
|
|
|
String? coverPath = existing?.coverPath;
|
|
String? thumbnailPath = existing?.thumbnailPath;
|
|
|
|
// Download images if needed
|
|
if (downloadImages) {
|
|
final publicationId = existing?.id ?? const Uuid().v4();
|
|
|
|
if (entry.coverUrl != null && coverPath == null) {
|
|
coverPath = await imageService.cacheCoverImage(
|
|
publicationId,
|
|
entry.coverUrl!,
|
|
);
|
|
}
|
|
|
|
if (entry.thumbnailUrl != null && thumbnailPath == null) {
|
|
thumbnailPath = await imageService.cacheThumbnailImage(
|
|
publicationId,
|
|
entry.thumbnailUrl!,
|
|
);
|
|
}
|
|
}
|
|
|
|
// Create or update publication
|
|
final publication = existing != null
|
|
? existing.copyWith(
|
|
title: entry.title,
|
|
authors: entry.authors,
|
|
summary: entry.summary,
|
|
content: entry.content,
|
|
coverPath: coverPath ?? existing.coverPath,
|
|
thumbnailPath: thumbnailPath ?? existing.thumbnailPath,
|
|
coverUrl: entry.coverUrl,
|
|
thumbnailUrl: entry.thumbnailUrl,
|
|
series: metadata.series,
|
|
seriesPosition: metadata.seriesPosition,
|
|
publisher: metadata.publisher,
|
|
language: metadata.language,
|
|
isbn: metadata.isbn,
|
|
categories: entry.categories,
|
|
links: entry.links,
|
|
streamLink: entry.streamLink,
|
|
published: entry.published,
|
|
updated: entry.updated,
|
|
lastCachedAt: DateTime.now(),
|
|
lastAccessedAt: DateTime.now(),
|
|
)
|
|
: Publication.fromEntry(
|
|
id: const Uuid().v4(),
|
|
serverId: serverId,
|
|
entry: entry,
|
|
metadata: metadata,
|
|
coverPath: coverPath,
|
|
thumbnailPath: thumbnailPath,
|
|
);
|
|
|
|
await repository.savePublication(publication);
|
|
return publication;
|
|
}
|
|
|
|
/// Update last accessed timestamp
|
|
Future<void> touchPublication(String publicationId) async {
|
|
final repository = ref.read(publicationRepositoryProvider);
|
|
await repository.updateLastAccessed(publicationId);
|
|
}
|
|
|
|
/// Clean up stale cache entries
|
|
Future<int> cleanupStaleCache() async {
|
|
final repository = ref.read(publicationRepositoryProvider);
|
|
final imageService = await ref.read(imageCacheServiceProvider.future);
|
|
final progressRepo = ref.read(readingProgressRepositoryProvider);
|
|
|
|
final stalePublications = await repository.getStalePublications();
|
|
int deletedCount = 0;
|
|
|
|
for (final pub in stalePublications) {
|
|
// Check if linked to reading progress
|
|
final hasProgress =
|
|
await progressRepo.getProgress(pub.entryId, pub.serverId);
|
|
|
|
if (hasProgress == null) {
|
|
// Safe to delete
|
|
await imageService.deleteCachedImages(pub.id);
|
|
await repository.deletePublication(pub.id);
|
|
deletedCount++;
|
|
}
|
|
}
|
|
|
|
return deletedCount;
|
|
}
|
|
}
|
|
|
|
/// Provider for single publication lookup
|
|
@riverpod
|
|
Future<Publication?> cachedPublication(
|
|
CachedPublicationRef ref,
|
|
String serverId,
|
|
String entryId,
|
|
) async {
|
|
final repository = ref.watch(publicationRepositoryProvider);
|
|
return repository.getPublication(serverId, entryId);
|
|
}
|