Claude-skill-registry Download System

Background chapter downloads and offline reading

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/downloads" ~/.claude/skills/majiayu000-claude-skill-registry-download-system && rm -rf "$T"
manifest: skills/data/downloads/SKILL.md
source content

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
    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/`;

// 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
});