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>
135 lines
3.7 KiB
Dart
135 lines
3.7 KiB
Dart
import 'package:dio/dio.dart';
|
|
import 'package:worldhopper/models/opds_feed.dart';
|
|
import 'package:worldhopper/services/opds_service.dart';
|
|
import 'package:worldhopper/services/url_helper.dart';
|
|
import 'package:worldhopper/repositories/server_repository.dart';
|
|
|
|
/// Repository for managing OPDS feeds
|
|
class OPDSRepository {
|
|
final OPDSService _service;
|
|
final ServerRepository _serverRepository;
|
|
|
|
// Simple in-memory cache
|
|
final Map<String, OPDSFeed> _feedCache = {};
|
|
final Map<String, DateTime> _cacheTimestamps = {};
|
|
final Duration _cacheDuration = const Duration(minutes: 5);
|
|
|
|
OPDSRepository({
|
|
OPDSService? service,
|
|
ServerRepository? serverRepository,
|
|
}) : _service = service ?? OPDSService(),
|
|
_serverRepository = serverRepository ?? ServerRepository();
|
|
|
|
/// Fetch a feed with caching
|
|
Future<OPDSFeed> fetchFeed(
|
|
String serverId,
|
|
String url, {
|
|
bool forceRefresh = false,
|
|
CancelToken? cancelToken,
|
|
}) async {
|
|
// Fetch server from database first to resolve URL
|
|
final server = await _serverRepository.getServer(serverId);
|
|
if (server == null) {
|
|
throw Exception('Server not found: $serverId');
|
|
}
|
|
|
|
// Resolve relative URLs against server base URL
|
|
final resolvedUrl = UrlHelper.resolveUrl(server.url, url);
|
|
final cacheKey = '$serverId:$resolvedUrl';
|
|
|
|
// Check cache if not forcing refresh
|
|
if (!forceRefresh && _isCached(cacheKey)) {
|
|
return _feedCache[cacheKey]!;
|
|
}
|
|
|
|
// Fetch feed from network
|
|
final parsedFeed = await _service.fetchFeed(
|
|
server,
|
|
resolvedUrl,
|
|
cancelToken: cancelToken,
|
|
);
|
|
final feed = parsedFeed.feed;
|
|
|
|
// Update cache
|
|
_feedCache[cacheKey] = feed;
|
|
_cacheTimestamps[cacheKey] = DateTime.now();
|
|
|
|
// Update server's last synced timestamp
|
|
await _serverRepository.updateLastSynced(serverId, DateTime.now());
|
|
|
|
return feed;
|
|
}
|
|
|
|
/// Fetch the root feed for a server
|
|
Future<OPDSFeed> fetchRootFeed(
|
|
String serverId, {
|
|
bool forceRefresh = false,
|
|
CancelToken? cancelToken,
|
|
}) async {
|
|
final server = await _serverRepository.getServer(serverId);
|
|
if (server == null) {
|
|
throw Exception('Server not found: $serverId');
|
|
}
|
|
|
|
return fetchFeed(
|
|
serverId,
|
|
server.url,
|
|
forceRefresh: forceRefresh,
|
|
cancelToken: cancelToken,
|
|
);
|
|
}
|
|
|
|
/// Test connection to a server
|
|
Future<bool> testConnection(String serverId) async {
|
|
final server = await _serverRepository.getServer(serverId);
|
|
if (server == null) {
|
|
throw Exception('Server not found: $serverId');
|
|
}
|
|
|
|
return await _service.testConnection(server);
|
|
}
|
|
|
|
/// Clear cache for a specific feed
|
|
void clearFeedCache(String serverId, String url) {
|
|
final cacheKey = '$serverId:$url';
|
|
_feedCache.remove(cacheKey);
|
|
_cacheTimestamps.remove(cacheKey);
|
|
}
|
|
|
|
/// Clear cache for a specific server
|
|
void clearServerCache(String serverId) {
|
|
_feedCache.removeWhere((key, _) => key.startsWith('$serverId:'));
|
|
_cacheTimestamps.removeWhere((key, _) => key.startsWith('$serverId:'));
|
|
}
|
|
|
|
/// Clear all cache
|
|
void clearAllCache() {
|
|
_feedCache.clear();
|
|
_cacheTimestamps.clear();
|
|
}
|
|
|
|
/// Check if a feed is cached and still valid
|
|
bool _isCached(String cacheKey) {
|
|
if (!_feedCache.containsKey(cacheKey)) {
|
|
return false;
|
|
}
|
|
|
|
final timestamp = _cacheTimestamps[cacheKey];
|
|
if (timestamp == null) {
|
|
return false;
|
|
}
|
|
|
|
final age = DateTime.now().difference(timestamp);
|
|
return age < _cacheDuration;
|
|
}
|
|
|
|
/// Clear service clients (when credentials change)
|
|
void clearServiceClients() {
|
|
_service.clearClients();
|
|
}
|
|
|
|
/// Clear service client for a specific server
|
|
void clearServiceClient(String serverId) {
|
|
_service.clearClient(serverId);
|
|
}
|
|
}
|