| name | Download System |
| description | Background chapter downloads and offline reading |
Download System
Download Service
import { downloadService } from '../services/downloadService';
await downloadService.downloadChapter(manga, chapter, sourceId);
await downloadService.downloadChapters(manga, chapters, sourceId);
await downloadService.cancelDownload(chapterId);
const status = await downloadService.getDownloadStatus(chapterId);
const downloads = await downloadService.getAllDownloads();
await downloadService.deleteDownload(chapterId);
Download Status
type DownloadStatus =
| 'pending'
| 'downloading'
| 'completed'
| 'failed'
| 'cancelled';
interface Download {
mangaId: string;
chapterId: string;
status: DownloadStatus;
progress: number;
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:
module.exports = function withBackgroundActionsServiceType(config) {
return withAndroidManifest(config, (config) => {
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';
if (headlessRuntime.isAvailable()) {
const result = await headlessRuntime.runExtensionMethod(
extensionId,
'getChapterDetails',
[mangaId, chapterId]
);
}
The headless runtime:
- Executes extension JavaScript using
eval() in a sandboxed environment
- Provides mock
App object with createRequestManager, createSourceStateManager, etc.
- Uses native
fetch for 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/`;
await FileSystem.makeDirectoryAsync(
`${downloadDir}${mangaId}/${chapterId}`,
{ intermediates: true }
);
await FileSystem.downloadAsync(
imageUrl,
`${downloadDir}${mangaId}/${chapterId}/page_${index}.jpg`
);
const info = await FileSystem.getInfoAsync(path);
if (info.exists) { }
await FileSystem.deleteAsync(path, { idempotent: true });
Download Queue
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();
}
function addToQueue(task: DownloadTask) {
queue.push(task);
processQueue();
}
Notifications
import * as Notifications from 'expo-notifications';
await Notifications.scheduleNotificationAsync({
content: {
title: 'Downloading',
body: `${manga.title} - Chapter ${chapter.chapNum}`,
data: { mangaId, chapterId },
},
trigger: null,
});