Replace vocsy_epub_viewer with flutter_epub_viewer to provide an integrated reading experience within the app instead of separate native FolioReader takeover. Key changes: - Replace vocsy_epub_viewer with flutter_epub_viewer (1.2.2) for WebView-based EPUB rendering - Add EpubReaderScreen with CFI-based position tracking and progress restoration - Add EpubDownloadService with download progress tracking - Update database schema to add epub_location field for CFI storage - Enhance ReadingProgress model to support EPUB locations as JSON - Update PublicationDetailScreen to route to appropriate reader (EPUB vs image) - Add EPUB reader route configuration - Update Android configuration (NDK 25.1.8937393, network permissions) - Add loading progress bar showing download and processing status Benefits: - Integrated Flutter widget stays within app - CFI-based position tracking works cross-platform - Chapter navigation and search functionality - Better UI customization and theme integration Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
137 lines
3.9 KiB
Dart
137 lines
3.9 KiB
Dart
import 'dart:io';
|
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
|
import 'package:path_provider/path_provider.dart';
|
|
import 'package:path/path.dart' as path;
|
|
import 'package:worldhopper/models/opds_entry.dart';
|
|
import 'package:worldhopper/models/opds_server.dart';
|
|
import 'package:worldhopper/services/auth_service.dart';
|
|
|
|
/// Service for downloading and caching EPUB files
|
|
class EpubDownloadService {
|
|
final AuthService _authService;
|
|
final CacheManager _cacheManager;
|
|
|
|
EpubDownloadService({
|
|
AuthService? authService,
|
|
CacheManager? cacheManager,
|
|
}) : _authService = authService ?? AuthService(),
|
|
_cacheManager = cacheManager ?? DefaultCacheManager();
|
|
|
|
/// Download EPUB file from server with authentication
|
|
/// Returns the local file path of the downloaded EPUB
|
|
Future<File> downloadEpub(
|
|
OPDSEntry entry,
|
|
OPDSServer server, {
|
|
void Function(double progress)? onProgress,
|
|
}) async {
|
|
final acquisitionLink = entry.acquisitionLink;
|
|
if (acquisitionLink == null) {
|
|
throw Exception('No acquisition link found for entry: ${entry.title}');
|
|
}
|
|
|
|
final url = acquisitionLink.href;
|
|
if (url == null || url.isEmpty) {
|
|
throw Exception('Invalid acquisition link URL');
|
|
}
|
|
|
|
// Check if file is already cached
|
|
final fileInfo = await _cacheManager.getFileFromCache(url);
|
|
if (fileInfo != null && await fileInfo.file.exists()) {
|
|
return fileInfo.file;
|
|
}
|
|
|
|
// Create authenticated Dio client
|
|
final dio = _authService.createAuthenticatedClient(server);
|
|
|
|
// Download file
|
|
final cacheDir = await getTemporaryDirectory();
|
|
final fileName = '${entry.id}.epub';
|
|
final filePath = path.join(cacheDir.path, fileName);
|
|
final file = File(filePath);
|
|
|
|
await dio.download(
|
|
url,
|
|
filePath,
|
|
onReceiveProgress: (received, total) {
|
|
if (total != -1 && onProgress != null) {
|
|
final progress = received / total;
|
|
onProgress(progress);
|
|
}
|
|
},
|
|
);
|
|
|
|
// Store in cache manager
|
|
await _cacheManager.putFile(
|
|
url,
|
|
await file.readAsBytes(),
|
|
fileExtension: 'epub',
|
|
);
|
|
|
|
return file;
|
|
}
|
|
|
|
/// Get cached EPUB file if it exists
|
|
Future<File?> getCachedEpub(OPDSEntry entry) async {
|
|
final acquisitionLink = entry.acquisitionLink;
|
|
if (acquisitionLink == null) return null;
|
|
|
|
final url = acquisitionLink.href;
|
|
if (url == null || url.isEmpty) return null;
|
|
|
|
final fileInfo = await _cacheManager.getFileFromCache(url);
|
|
if (fileInfo != null && await fileInfo.file.exists()) {
|
|
return fileInfo.file;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// Remove cached EPUB file
|
|
Future<void> removeCachedEpub(OPDSEntry entry) async {
|
|
final acquisitionLink = entry.acquisitionLink;
|
|
if (acquisitionLink == null) return;
|
|
|
|
final url = acquisitionLink.href;
|
|
if (url == null || url.isEmpty) return;
|
|
|
|
await _cacheManager.removeFile(url);
|
|
}
|
|
|
|
/// Check if EPUB is cached
|
|
Future<bool> isEpubCached(OPDSEntry entry) async {
|
|
final acquisitionLink = entry.acquisitionLink;
|
|
if (acquisitionLink == null) return false;
|
|
|
|
final url = acquisitionLink.href;
|
|
if (url == null || url.isEmpty) return false;
|
|
|
|
final fileInfo = await _cacheManager.getFileFromCache(url);
|
|
return fileInfo != null && await fileInfo.file.exists();
|
|
}
|
|
|
|
/// Get download progress for an EPUB
|
|
Stream<double> getDownloadProgress(OPDSEntry entry, OPDSServer server) async* {
|
|
final acquisitionLink = entry.acquisitionLink;
|
|
if (acquisitionLink == null) {
|
|
throw Exception('No acquisition link found for entry: ${entry.title}');
|
|
}
|
|
|
|
final url = acquisitionLink.href;
|
|
if (url == null || url.isEmpty) {
|
|
throw Exception('Invalid acquisition link URL');
|
|
}
|
|
|
|
double progress = 0.0;
|
|
yield progress;
|
|
|
|
await downloadEpub(
|
|
entry,
|
|
server,
|
|
onProgress: (p) {
|
|
progress = p;
|
|
},
|
|
);
|
|
|
|
yield progress;
|
|
}
|
|
}
|