All checks were successful
ci/woodpecker/tag/release Pipeline was successful
Enable downloading entire series for offline reading. Includes database tables and migration for downloaded series/chapters, download orchestration with progress tracking, per-chapter EPUB and image stream downloading, a Downloaded section in the library, and offline-first reader support. Key changes: - DB migration v11 with downloaded_series and downloaded_chapters tables - Series download service with pagination, cancellation, and progress stream - Feed screen download button with status indicators and completion feedback - Library screen Downloaded section with series cards and detail screen - Reader pre-caching using local files (including two-page spread mode) - Next-in-series navigation resolves from downloaded chapters when offline - Skip image downloads and progress sync when device is offline - Settings screen shows download storage usage with clear option Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
85 lines
2.8 KiB
Dart
85 lines
2.8 KiB
Dart
import 'dart:io';
|
|
import 'package:path_provider/path_provider.dart';
|
|
import 'package:path/path.dart' as path;
|
|
|
|
/// Manages the persistent downloads directory structure
|
|
class DownloadStorageService {
|
|
/// Get the root downloads directory
|
|
Future<Directory> getDownloadsDir() async {
|
|
final appDir = await getApplicationDocumentsDirectory();
|
|
final downloadsDir = Directory(path.join(appDir.path, 'downloads'));
|
|
if (!await downloadsDir.exists()) {
|
|
await downloadsDir.create(recursive: true);
|
|
}
|
|
return downloadsDir;
|
|
}
|
|
|
|
/// Get the directory for a specific series
|
|
Future<Directory> getSeriesDir(String seriesId) async {
|
|
final downloadsDir = await getDownloadsDir();
|
|
final seriesDir = Directory(path.join(downloadsDir.path, seriesId));
|
|
if (!await seriesDir.exists()) {
|
|
await seriesDir.create(recursive: true);
|
|
}
|
|
return seriesDir;
|
|
}
|
|
|
|
/// Get the directory for an image stream chapter
|
|
Future<Directory> getChapterDir(String seriesId, String chapterId) async {
|
|
final seriesDir = await getSeriesDir(seriesId);
|
|
final chapterDir = Directory(path.join(seriesDir.path, chapterId));
|
|
if (!await chapterDir.exists()) {
|
|
await chapterDir.create(recursive: true);
|
|
}
|
|
return chapterDir;
|
|
}
|
|
|
|
/// Get the file path for an EPUB chapter
|
|
Future<String> getEpubPath(String seriesId, String chapterId) async {
|
|
final seriesDir = await getSeriesDir(seriesId);
|
|
return path.join(seriesDir.path, '$chapterId.epub');
|
|
}
|
|
|
|
/// Delete all files for a series
|
|
Future<void> deleteSeriesFiles(String seriesId) async {
|
|
final downloadsDir = await getDownloadsDir();
|
|
final seriesDir = Directory(path.join(downloadsDir.path, seriesId));
|
|
if (await seriesDir.exists()) {
|
|
await seriesDir.delete(recursive: true);
|
|
}
|
|
}
|
|
|
|
/// Delete files for a single chapter
|
|
Future<void> deleteChapterFiles(
|
|
String seriesId, String chapterId, bool isEpub) async {
|
|
if (isEpub) {
|
|
final epubPath = await getEpubPath(seriesId, chapterId);
|
|
final file = File(epubPath);
|
|
if (await file.exists()) {
|
|
await file.delete();
|
|
}
|
|
} else {
|
|
final downloadsDir = await getDownloadsDir();
|
|
final chapterDir =
|
|
Directory(path.join(downloadsDir.path, seriesId, chapterId));
|
|
if (await chapterDir.exists()) {
|
|
await chapterDir.delete(recursive: true);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Get total disk usage for all downloads
|
|
Future<int> getTotalDownloadSize() async {
|
|
final downloadsDir = await getDownloadsDir();
|
|
if (!await downloadsDir.exists()) return 0;
|
|
|
|
int totalSize = 0;
|
|
await for (final entity
|
|
in downloadsDir.list(recursive: true, followLinks: false)) {
|
|
if (entity is File) {
|
|
totalSize += await entity.length();
|
|
}
|
|
}
|
|
return totalSize;
|
|
}
|
|
}
|