with one click
download-system
Background chapter downloads and offline reading
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Menu
Background chapter downloads and offline reading
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Based on SOC occupation classification
Image caching and storage management with expo-image
Reusable component patterns and styling
URL schemes and deep link handling
Common patterns for Expo SDK 54 and React Native development
Paperback-compatible extension runtime and source API
Multi-language support with i18n-js
| name | Download System |
| description | Background chapter downloads and offline reading |
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);
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;
}
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();
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;
});
};
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:
eval() in a sandboxed environmentApp object with createRequestManager, createSourceStateManager, etc.fetch for HTTP requests (no CORS in React Native)documentDirectory/
└── downloads/
└── {mangaId}/
└── {chapterId}/
├── page_001.jpg
├── page_002.jpg
└── ...
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 });
// 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();
}
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
});