name: Download System description: Background chapter downloads and offline reading
Download System
Download Service
import { downloadService } from '../services/downloadService';
// Download a chapter
await downloadService.downloadChapter(manga, chapter, sourceId);
// Download multiple chapters
await downloadService.downloadChapters(manga, chapters, sourceId);
// Cancel download
await downloadService.cancelDownload(chapterId);
// Get download status
const status = await downloadService.getDownloadStatus(chapterId);
// Get all downloads
const downloads = await downloadService.getAllDownloads();
// Delete downloaded chapter
await downloadService.deleteDownload(chapterId);
Download Status
type DownloadStatus =
| 'pending'
| 'downloading'
| 'completed'
| 'failed'
| 'cancelled';
interface Download {
mangaId: string;
chapterId: string;
status: DownloadStatus;
progress: number; // 0-100
totalPages: number;
downloadedPages: number;
error?: string;
}
Background Downloads (Android)
Uses react-native-background-actions:
import BackgroundService from 'react-native-background-actions';
const options = {
taskName: 'Download',
taskTitle: 'Downloading chapters',
taskDesc: 'Chapter 1 of 10',
taskIcon: { name: 'ic_launcher', type: 'mipmap' },
progressBar: { max: 100, value: 0 },
};
await BackgroundService.start(downloadTask, options);
await BackgroundService.updateNotification({ taskDesc: 'Chapter 2 of 10' });
await BackgroundService.stop();
Foreground Service (Android 14+)
Required config in plugins/withBackgroundActionsServiceType.js:
// Adds foregroundServiceType="dataSync" to AndroidManifest.xml
module.exports = function withBackgroundActionsServiceType(config) {
return withAndroidManifest(config, (config) => {
// Modify service declaration
return config;
});
};
Headless Extension Runtime
When the app goes to background, the ExtensionRunner WebView may become unavailable.
The headlessExtensionRuntime.ts provides a fallback that runs extensions directly in
the React Native JS engine:
import { headlessRuntime } from '../services/headlessExtensionRuntime';
// Check if headless runtime is available
if (headlessRuntime.isAvailable()) {
// Run extension method without WebView
const result = await headlessRuntime.runExtensionMethod(
extensionId,
'getChapterDetails',
[mangaId, chapterId]
);
}
The headless runtime:
- Executes extension JavaScript using
eval()in a sandboxed environment - Provides mock
Appobject withcreateRequestManager,createSourceStateManager, etc. - Uses native
fetchfor HTTP requests (no CORS in React Native) - Falls back automatically when WebView bridge is unavailable
Storage Structure
documentDirectory/
└── downloads/
└── {mangaId}/
└── {chapterId}/
├── page_001.jpg
├── page_002.jpg
└── ...
File Operations
import * as FileSystem from 'expo-file-system';
const downloadDir = `${FileSystem.documentDirectory}downloads/`;
// Create directory
await FileSystem.makeDirectoryAsync(
`${downloadDir}${mangaId}/${chapterId}`,
{ intermediates: true }
);
// Download image
await FileSystem.downloadAsync(
imageUrl,
`${downloadDir}${mangaId}/${chapterId}/page_${index}.jpg`
);
// Check if exists
const info = await FileSystem.getInfoAsync(path);
if (info.exists) { /* ... */ }
// Delete
await FileSystem.deleteAsync(path, { idempotent: true });
Download Queue
// Queue management
const queue: DownloadTask[] = [];
let isProcessing = false;
async function processQueue() {
if (isProcessing || queue.length === 0) return;
isProcessing = true;
const task = queue.shift();
await downloadChapter(task);
isProcessing = false;
processQueue(); // Process next
}
function addToQueue(task: DownloadTask) {
queue.push(task);
processQueue();
}
Notifications
import * as Notifications from 'expo-notifications';
// Show progress notification
await Notifications.scheduleNotificationAsync({
content: {
title: 'Downloading',
body: `${manga.title} - Chapter ${chapter.chapNum}`,
data: { mangaId, chapterId },
},
trigger: null, // Immediate
});