Adapt to new unyo-core api

This commit is contained in:
2026-01-03 22:29:37 +00:00
committed by Kevin Rodrigues Borges
parent 2ed846ed7b
commit 61e522abe3
14 changed files with 2354 additions and 221 deletions

117
AGENTS.md Normal file
View File

@@ -0,0 +1,117 @@
# AGENTS.md
Flutter FFI package bridging to Aniyomi (Kotlin/Java) via JNI.
## Project Type
- **Flutter package** (not an app) with FFI JNI bindings
- Uses `package:jni` and `package:jnigen` for Java interop
- Main export: `lib/aniyomi_bridge.dart`
## Flutter Version
Managed via FVM (Flutter Version Management):
- **Flutter 3.38.1** (see `.fvmrc`)
- Use `fvm flutter` commands or ensure Flutter 3.38.1 is active
## Key Commands
```bash
# Install dependencies
fvm flutter pub get
# Run tests
fvm flutter test
# Generate JNI bindings (jnigen)
fvm flutter pub run jnigen --config jnigen/aniyomibridge.yaml
fvm flutter pub run jnigen --config jnigen/headers.yaml
# ... (see jnigen/*.yaml for all configs)
# Run example app
fvm flutter run -C example
# Analyze
fvm flutter analyze
# Format
fvm dart format .
```
## Architecture
```
lib/
├── aniyomi_bridge.dart # Main API class (AniyomiBridge)
├── jni_isolate.dart # JNI isolate worker (JVM runs in separate isolate)
├── jni_isolate_message.dart # Message types for isolate communication
├── models/ # Dart models (e.g., extension.dart)
└── jmodels/ # JNI-generated Dart bindings (AUTO-GENERATED)
├── janiyomibridge.dart # Generated from AniyomiBridge Java class
├── jsanime.dart # Generated from SAnime
├── jsepisode.dart # Generated from SEpisode
├── jvideo.dart # Generated from Video
└── ... (other j*.dart files)
jnigen/
├── aniyomibridge.yaml # Config for generating janiyomibridge.dart
├── headers.yaml # Config for jheaders.dart (okhttp3.Headers)
├── sanime.yaml, video.yaml, etc. # Per-class generation configs
assets/
└── aniyomibridge-core.jar # Java library (source of JNI bindings)
jre/
├── linux/customjre/ # Custom JRE for Linux builds
└── macos/customjre/ # Custom JRE for macOS builds
```
## Code Generation (JNIGen)
The `lib/jmodels/j*.dart` files are **auto-generated** from Java classes via JNIGen:
- Source JAR: `assets/aniyomibridge-core.jar`
- Configs: `jnigen/*.yaml` (one per Java class)
- Generated classes mirror `eu.kanade.tachiyomi.*` and `dev.k3vinb5.aniyomibridge.*`
**Never manually edit `jmodels/j*.dart` files**—regenerate via:
```bash
for config in jnigen/*.yaml; do
fvm flutter pub run jnigen --config "$config"
done
```
## JRE Requirements
This package requires a **custom JRE** bundled with the host application:
```bash
# Debug builds
./installJREDebug.sh linux # or macos
# Release builds
./installJRERelease.sh linux # or macos
```
The scripts copy `jre/<platform>/customjre/` into the Flutter build output.
## Testing
- Test directory: `test/` (currently empty)
- Example app: `example/` (test the package here)
## Key Dependencies
- `jni: ^0.14.2` / `jnigen: ^0.14.2` — JNI bindings
- `logger: ^2.6.2` — Logging
- `path_provider: ^2.1.5`, `path`, `crypto` — Storage utilities
## Isolate Model
JNI operations run in a **separate isolate** (`jni_isolate.dart`) to avoid blocking the main thread. All Java calls are dispatched via `JniIsolateMessage` and results returned through `ReceivePort`.
## Important Notes
- **Do not commit `pubspec.lock`** — it's gitignored per Dart library conventions
- `analysis_options.yaml` uses `package:flutter_lints/flutter.yaml`
- The package exports extension loading from remote URLs (see `example/lib/main.dart`)

625
JNI_SPECIFICATION.md Normal file
View File

@@ -0,0 +1,625 @@
# AniyomiBridge JNI Specification
> **Version:** 2.0 (Post-Multi-Module Refactor)
> **Target:** Flutter JNI integration for `aniyomibridge-core-1.0.0.jar`
> **Main Class:** `dev.k3vinb5.aniyomibridge.AniyomiBridge`
---
## Overview
This document specifies the public Java API exposed by `AniyomiBridge.java` for consumption via JNI from Flutter. It covers every method, its signature, the data models that cross the JNI boundary, and the behavioral changes introduced during the multi-module refactor.
**Key architectural fact:** `AniyomiBridge` is a **singleton-like stateful object**. It maintains internal maps of loaded anime/manga sources keyed by `packageName`. You create one instance and call methods on it. All source lookup is done internally via these maps.
---
## Table of Contents
1. [Initialization](#1-initialization)
2. [Extension Lifecycle (Anime)](#2-extension-lifecycle-anime)
3. [Extension Lifecycle (Manga)](#3-extension-lifecycle-manga)
4. [Querying Loaded Extensions](#4-querying-loaded-extensions)
5. [Preferences API](#5-preferences-api)
6. [Content Fetching (Anime)](#6-content-fetching-anime)
7. [Content Fetching (Manga)](#7-content-fetching-manga)
8. [Data Models](#8-data-models)
9. [Error Handling](#9-error-handling)
10. [Breaking Changes from v1](#10-breaking-changes-from-v1)
11. [JNI Implementation Notes](#11-jni-implementation-notes)
---
## 1. Initialization
### `static void init()`
Must be called **once** before any other operation. Performs global JVM-level setup:
- Installs ByteBuddy agent for runtime bytecode patching
- Initializes Injekt dependency injection module
**JNI signature:**
```
(env)->CallStaticVoidMethod(aniyomiBridgeClass, initMethod);
```
**Thread safety:** Safe to call from any thread, but should only be called once. Not synchronized externally — caller must ensure single invocation.
**Throws:** Nothing directly, but ByteBuddy installation can fail if the JVM doesn't support instrumentation.
---
## 2. Extension Lifecycle (Anime)
### `void loadAnimeExtension(String extensionUrl, String basePath, String packageName)`
Downloads an APK from `extensionUrl`, converts it to JAR via dex2jar, loads the main extension class, and registers it as an `AnimeHttpSource`.
| Parameter | Description |
|-----------|-------------|
| `extensionUrl` | Full HTTPS URL to the `.apk` file |
| `basePath` | Local directory path where the converted `.jar` will be cached |
| `packageName` | The Android package name (e.g., `eu.kanade.tachiyomi.animeextension.en.animepahe`). Used as the **internal key** for all subsequent lookups. |
**Behavior:**
- Downloads APK if not already present
- Converts APK → JAR using dex2jar
- Inspects JAR to find the main extension class
- Loads class via isolated `URLClassLoader`
- Applies ByteBuddy patches to the extension's JVM classes
- If the source implements `ConfigurableAnimeSource`, automatically calls `setupPreferenceScreen()` with a managed `PreferenceScreen`
- Stores the source in an internal `ConcurrentHashMap` keyed by `packageName`
**Throws:** `UnyoCoreException` on any failure (download, dex2jar, class loading, casting).
---
### `void unloadAnimeExtension(String packageName)`
Unloads an anime extension and cleans up resources.
| Parameter | Description |
|-----------|-------------|
| `packageName` | The same `packageName` passed to `loadAnimeExtension` |
**Behavior:**
- Closes the extension's `URLClassLoader`
- Deletes the cached JAR file from disk
- Removes the source from internal maps
**Throws:** `UnyoCoreException` on cleanup failure.
---
## 3. Extension Lifecycle (Manga)
### `void loadMangaExtension(String extensionUrl, String basePath, String packageName)`
Identical to `loadAnimeExtension` but for manga sources implementing `CatalogueSource`.
| Parameter | Description |
|-----------|-------------|
| `extensionUrl` | Full HTTPS URL to the `.apk` file |
| `basePath` | Local directory path where the converted `.jar` will be cached |
| `packageName` | Android package name, used as internal key |
**Behavior:** Same as anime version, but casts to `CatalogueSource` and calls `ConfigurableSource.setupPreferenceScreen()` if applicable.
**Throws:** `UnyoCoreException`
---
### `void unloadMangaExtension(String packageName)`
Unloads a manga extension.
| Parameter | Description |
|-----------|-------------|
| `packageName` | The same `packageName` passed to `loadMangaExtension` |
**Throws:** `UnyoCoreException`
---
## 4. Querying Loaded Extensions
### `List<String> getAnimeLoadedExtensions()`
Returns a list of loaded anime extension package names.
**Return:** `java.util.List<java.lang.String>` — package names of all currently loaded anime sources.
---
### `List<String> getMangaLoadedExtensions()`
Returns a list of loaded manga extension package names.
**Return:** `java.util.List<java.lang.String>` — package names of all currently loaded manga sources.
---
## 5. Preferences API
> **New in v2.** Previously, preferences were not exposed to Flutter. Now they are fully readable and writable.
When a `ConfigurableAnimeSource` or `ConfigurableSource` is loaded, `AniyomiBridge` automatically calls `setupPreferenceScreen(screen)` and creates a `PreferenceScreen` backed by file-based `SharedPreferences` (stored under `~/.aniyomibridge/shared_prefs/<source-id>.xml`).
### `List<PreferenceItem> getAnimePreferences(String packageName)`
Reads all preferences registered by an anime extension.
| Parameter | Description |
|-----------|-------------|
| `packageName` | Package name of the loaded anime extension |
**Return:** `java.util.List<PreferenceItem>`
**Behavior:**
- Returns empty list if source is not configurable
- Iterates all `Preference` objects on the source's `PreferenceScreen`
- Reads current value from `SharedPreferences` (or default if never set)
- Returns typed values based on preference type
**Supported preference types:**
| Type | Java Type | Value Type |
|------|-----------|------------|
| `SwitchPreferenceCompat` | `Boolean` | `true` / `false` |
| `ListPreference` | `String` | Selected entry value |
| `EditTextPreference` | `String` | User input text |
| `MultiSelectListPreference` | `Set<String>` | Selected values |
| `Preference` (fallback) | `null` | No value |
---
### `void setAnimePreference(String packageName, String key, Object value)`
Sets a single preference value for an anime extension.
| Parameter | Description |
|-----------|-------------|
| `packageName` | Package name of the loaded anime extension |
| `key` | Preference key (from `getAnimePreferences`) |
| `value` | Value to set. Must match the expected type: `Boolean`, `String`, or `Set<String>` |
**Throws:** `UnyoCoreException` if source not configurable, key not found, or type mismatch.
---
### `void setAnimePreferences(String packageName, Map<String, Object> values)`
Batch-updates multiple preferences for an anime extension.
| Parameter | Description |
|-----------|-------------|
| `packageName` | Package name of the loaded anime extension |
| `values` | `java.util.Map` of `key → value` pairs |
**Behavior:** Iterates the map and calls `setAnimePreference` logic for each entry. Fails fast on first error.
**Throws:** `UnyoCoreException`
> **Note:** Manga preferences API is **not yet exposed** in `AniyomiBridge.java`. If needed, the same pattern applies using `CatalogueSource` / `ConfigurableSource`.
---
## 6. Content Fetching (Anime)
All content-fetching methods are **synchronous blocking calls** that internally run Kotlin coroutines via `runBlocking`. Call them from background threads in Flutter.
### `List<SAnime> getAnimeSearchResults(String query, int page, String packageName)`
Searches for anime using the specified extension.
| Parameter | Description |
|-----------|-------------|
| `query` | Search query string |
| `page` | Page number (1-based) |
| `packageName` | Package name of the loaded anime extension |
**Return:** `java.util.List<SAnime>`
**Throws:** `UnyoCoreException` wrapping any network or parsing error.
---
### `List<SEpisode> getEpisodeList(SAnime anime, String packageName)`
Fetches the episode list for a given anime.
| Parameter | Description |
|-----------|-------------|
| `anime` | `SAnime` object (from search results or previous call) |
| `packageName` | Package name of the loaded anime extension |
**Return:** `java.util.List<SEpisode>`
**Throws:** `UnyoCoreException`
---
### `List<Video> getVideoList(SEpisode episode, String packageName)`
Fetches video URLs/metadata for a given episode.
| Parameter | Description |
|-----------|-------------|
| `episode` | `SEpisode` object |
| `packageName` | Package name of the loaded anime extension |
**Return:** `java.util.List<Video>`
**Throws:** `UnyoCoreException`
> **Important:** The `Video` class contains `okhttp3.Headers` which is **not serializable by default**. If you need to pass video data across JNI/Flutter isolates, consider using `SerializableVideo` (see Data Models).
---
## 7. Content Fetching (Manga)
### `List<SManga> getMangaSearchResults(String query, int page, String packageName)`
Searches for manga.
| Parameter | Description |
|-----------|-------------|
| `query` | Search query string |
| `page` | Page number (1-based) |
| `packageName` | Package name of the loaded manga extension |
**Return:** `java.util.List<SManga>`
**Throws:** `UnyoCoreException`
---
### `List<SChapter> getChapterList(SManga manga, String packageName)`
Fetches chapter list for a manga.
| Parameter | Description |
|-----------|-------------|
| `manga` | `SManga` object |
| `packageName` | Package name of the loaded manga extension |
**Return:** `java.util.List<SChapter>`
**Throws:** `UnyoCoreException`
---
### `List<Page> getPageList(SChapter chapter, String packageName)`
Fetches image page URLs for a chapter.
| Parameter | Description |
|-----------|-------------|
| `chapter` | `SChapter` object |
| `packageName` | Package name of the loaded manga extension |
**Return:** `java.util.List<Page>`
**Throws:** `UnyoCoreException`
---
## 8. Data Models
### `PreferenceItem`
**Package:** `dev.k3vinb5.aniyomibridge.PreferenceItem`
**Fields:**
| Field | Type | Description |
|-------|------|-------------|
| `key` | `String` | Unique preference identifier |
| `title` | `String` | Human-readable title |
| `type` | `String` | One of: `SwitchPreferenceCompat`, `ListPreference`, `EditTextPreference`, `MultiSelectListPreference`, `Preference` |
| `value` | `Object` | Current value (`Boolean`, `String`, or `Set<String>`) |
| `entries` | `List<String>` | Display labels (for List/MultiSelect) |
| `entryValues` | `List<String>` | Internal values (for List/MultiSelect) |
| `summary` | `String` | Optional description text |
**JNI tip:** Access fields directly (they are `public`). No getters/setters.
---
### `SAnime`
**Package:** `eu.kanade.tachiyomi.animesource.model.SAnime`
**Interface fields (all `public`):**
| Field | Type | Notes |
|-------|------|-------|
| `url` | `String` | Relative or absolute URL to the anime page |
| `title` | `String` | Display title |
| `artist` | `String?` | Nullable |
| `author` | `String?` | Nullable |
| `description` | `String?` | Nullable |
| `genre` | `String?` | Nullable; comma-separated genres |
| `status` | `int` | `UNKNOWN=0`, `ONGOING=1`, `COMPLETED=2`, `LICENSED=3`, `PUBLISHING_FINISHED=4`, `CANCELLED=5`, `ON_HIATUS=6` |
| `thumbnail_url` | `String?` | Cover image URL |
| `update_strategy` | `AnimeUpdateStrategy` | Enum |
| `fetch_type` | `FetchType` | Enum |
| `season_number` | `double` | Season number |
| `initialized` | `boolean` | Whether full metadata has been fetched |
**Method:** `List<String> getGenres()` — parses `genre` string into a list.
---
### `SEpisode`
**Package:** `eu.kanade.tachiyomi.animesource.model.SEpisode`
**Interface fields:**
| Field | Type | Notes |
|-------|------|-------|
| `url` | `String` | Episode page URL |
| `name` | `String` | Episode title/number |
| `date_upload` | `long` | Unix timestamp (ms) |
| `episode_number` | `float` | Numeric episode number |
| `scanlator` | `String?` | Nullable |
---
### `Video`
**Package:** `eu.kanade.tachiyomi.animesource.model.Video`
**Fields:**
| Field | Type | Notes |
|-------|------|-------|
| `videoUrl` | `String` | Direct video stream URL |
| `videoTitle` | `String` | Quality label (e.g., "1080p") |
| `resolution` | `Integer?` | Nullable resolution in pixels |
| `bitrate` | `Integer?` | Nullable bitrate |
| `headers` | `okhttp3.Headers?` | HTTP headers required for playback |
| `preferred` | `boolean` | Whether this is the preferred quality |
| `subtitleTracks` | `List<Track>` | Subtitle tracks |
| `audioTracks` | `List<Track>` | Audio tracks |
| `timestamps` | `List<TimeStamp>` | Chapter/intro markers |
| `mpvArgs` | `List<Pair<String,String>>` | MPV player arguments |
| `ffmpegStreamArgs` | `List<Pair<String,String>>` | FFmpeg stream arguments |
| `ffmpegVideoArgs` | `List<Pair<String,String>>` | FFmpeg video arguments |
| `internalData` | `String` | Opaque internal data from extractor |
| `initialized` | `boolean` | Whether video metadata is resolved |
**Deprecation notice:** Old extensions may use `quality` (maps to `videoTitle`) and `url` (maps to internal `videoPageUrl`). New code should use `videoTitle` and `videoUrl`.
---
### `SManga`
**Package:** `eu.kanade.tachiyomi.source.model.SManga`
Identical structure to `SAnime` but without `fetch_type`, `season_number`, and `update_strategy` is `UpdateStrategy` instead of `AnimeUpdateStrategy`.
---
### `SChapter`
**Package:** `eu.kanade.tachiyomi.source.model.SChapter`
Identical structure to `SEpisode` but `chapter_number` instead of `episode_number`.
---
### `Page`
**Package:** `eu.kanade.tachiyomi.source.model.Page`
**Fields:**
| Field | Type | Notes |
|-------|------|-------|
| `index` | `int` | Zero-based page index |
| `url` | `String` | Page URL (may be relative) |
| `imageUrl` | `String?` | Resolved direct image URL |
| `number` | `int` | Human-readable page number (`index + 1`) |
---
## 9. Error Handling
### `UnyoCoreException`
**Package:** `dev.k3vinb5.aniyomibridge.exceptions.UnyoCoreException`
Extends `RuntimeException`.
**Constructors:**
- `UnyoCoreException(String message)`
- `UnyoCoreException(String message, Throwable cause)`
**Behavior:** Every public method in `AniyomiBridge` wraps failures in `UnyoCoreException` with the full Java stack trace in the message string. When catching via JNI, you get both the user-friendly message and the developer stack trace.
**Recommended Flutter handling:**
```dart
// Pseudocode — depends on your JNI bindings
try {
final results = bridge.getAnimeSearchResults(query, page, packageName);
} on JniException catch (e) {
// e.message contains both the summary and the full Java stack trace
log.error(e.message);
}
```
---
## 10. Breaking Changes from v1
If your Flutter code was written against the pre-refactor API (before multi-module split), these are the critical changes:
### 1. Extension keys changed from `extensionName` to `packageName`
**Before:**
```java
loadAnimeExtension(String extensionUrl, String basePath)
// Key was extracted from URL: "animepahe", "torrentio", etc.
unloadAnimeExtension(String extensionName, String version, String basePath)
// You passed extensionName + version, bridge constructed filename
```
**After:**
```java
loadAnimeExtension(String extensionUrl, String basePath, String packageName)
// You pass the full Android package name as key
unloadAnimeExtension(String packageName)
// Just packageName. Bridge tracks the JAR path internally.
```
**Migration:**
- Stop extracting extension names from URLs
- Use the Android package name (from the extension repo JSON) as the key everywhere
- Update your Flutter model to pass `packageName` to all methods
### 2. `getAnimeSearchResults` / `getMangaSearchResults` signature change
**Before:**
```java
getAnimeSearchResults(String query, int page, String animeSource)
// 'animeSource' was the extension name
```
**After:**
```java
getAnimeSearchResults(String query, int page, String packageName)
// 'packageName' is the full package name
```
**Migration:** Pass `packageName` instead of extension name.
### 3. Content fetching methods signature change
**Before:**
```java
getEpisodeList(SAnime anime, String animeSource)
getVideoList(SEpisode episode, String animeSource)
```
**After:**
```java
getEpisodeList(SAnime anime, String packageName)
getVideoList(SEpisode episode, String packageName)
```
**Migration:** Same as above — replace `animeSource` string with `packageName`.
### 4. Removal of `correctedNames` map
**Before:** The bridge internally mapped names like `"zoro" → "hianime"` and `"aniwatch" → "aniwatchtv"`.
**After:** No internal name correction. You must use the exact `packageName`.
**Migration:** Remove any logic in Flutter that relied on these aliases. Use package names directly from the repo metadata.
### 5. Unload methods no longer take `version` or `basePath`
**Before:**
```java
unloadAnimeExtension(String extensionName, String version, String basePath)
```
**After:**
```java
unloadAnimeExtension(String packageName)
```
**Migration:** Simpler — just pass `packageName`.
### 6. Preferences API is new
**Before:** No way to read/write extension preferences from Flutter.
**After:** Full `getAnimePreferences`, `setAnimePreference`, `setAnimePreferences` API.
**Migration:** If your app previously couldn't handle configurable sources, now you can/should expose these preferences in your UI.
### 7. `MediaSourceUtils` now uses per-source filters
**Before:**
```kotlin
animeSource.getSearchAnime(page, query, filters = defaultAnimeFilterList)
```
**After:**
```kotlin
animeSource.getSearchAnime(page, query, filters = animeSource.getFilterList())
```
**Impact:** Search results may differ if an extension had custom filters. This is generally more correct but could change behavior for extensions that rely on non-default filter states.
---
## 11. JNI Implementation Notes
### Class Loading
The fat JAR (`aniyomibridge-core-1.0.0.jar`) contains everything including the Android SDK stubs, Kotlin stdlib, and all dependencies. Load it with a standard `URLClassLoader` or from the classpath.
**Important:** The `Main.class` (`dev.k3vinb5.aniyomibridge.Main`) is excluded from the fat JAR (consumed by external Flutter app). The class you instantiate is `dev.k3vinb5.aniyomibridge.AniyomiBridge`.
### Threading
All content-fetching methods use `kotlinx.coroutines.runBlocking` internally. They **will block the calling thread**. In Flutter, always call these methods through a background isolate or `compute()` / `Isolate.run()`.
The internal maps (`animeSources`, `mangaSources`, `classLoaders`) use `ConcurrentHashMap`, so `load*` and `unload*` are thread-safe. However, you should not load and unload the same extension concurrently.
### Object Lifetimes
`SAnime`, `SEpisode`, `SManga`, `SChapter` are `Serializable`. You can safely pass them across JNI boundaries and cache them in Dart if needed.
`Video` contains `okhttp3.Headers` which is **not serializable**. If you need to serialize video data (e.g., for Dart isolates or JSON), convert `Headers` to a `Map<String, List<String>>` or use the `SerializableVideo` Kotlin helper class.
### Memory
Each loaded extension gets its own `URLClassLoader`. When you call `unload*Extension`, the classloader is closed and the JAR file is deleted. This should release most memory, but some classes may remain in the JVM's metaspace until GC.
### Logging
The bridge uses JBoss Logging. Logs go to stdout/stderr by default. In a Flutter desktop app, you'll see them in the console. Consider redirecting `java.util.logging` or JBoss LogManager to your Dart logging system if you need structured logs.
---
## Appendix: Quick Reference — Method Signatures
```java
// Initialization
public static void init()
// Anime lifecycle
public void loadAnimeExtension(String extensionUrl, String basePath, String packageName)
public void unloadAnimeExtension(String packageName)
// Manga lifecycle
public void loadMangaExtension(String extensionUrl, String basePath, String packageName)
public void unloadMangaExtension(String packageName)
// Query loaded
public List<String> getAnimeLoadedExtensions()
public List<String> getMangaLoadedExtensions()
// Preferences (anime only, currently)
public List<PreferenceItem> getAnimePreferences(String packageName)
public void setAnimePreference(String packageName, String key, Object value)
public void setAnimePreferences(String packageName, Map<String, Object> values)
// Anime content
public List<SAnime> getAnimeSearchResults(String query, int page, String packageName)
public List<SEpisode> getEpisodeList(SAnime anime, String packageName)
public List<Video> getVideoList(SEpisode episode, String packageName)
// Manga content
public List<SManga> getMangaSearchResults(String query, int page, String packageName)
public List<SChapter> getChapterList(SManga manga, String packageName)
public List<Page> getPageList(SChapter chapter, String packageName)
```

Binary file not shown.

Binary file not shown.

View File

@@ -1,16 +1,757 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart' as path;
import 'package:k3vinb5_aniyomi_bridge/aniyomi_bridge.dart';
import 'package:k3vinb5_aniyomi_bridge/jmodels/jpreferenceitem.dart';
import 'package:k3vinb5_aniyomi_bridge/jmodels/jsanime.dart';
import 'package:k3vinb5_aniyomi_bridge/jmodels/jsepisode.dart';
import 'package:k3vinb5_aniyomi_bridge/jmodels/jvideo.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
AniyomiBridge bridge = AniyomiBridge();
await Future.delayed(Duration(milliseconds: 1000));
await bridge.loadAnimeExtension("https://gitea.k3vinb5.dev/Backups/kohi-den-extensions/raw/branch/main/apk/aniyomi-all.animeonsen-v14.7.apk");
List<JSAnime> animes = await bridge.getAnimeSearchResults("Shingeki", 1, "animeonsen");
print(animes);
animes.forEach((anime) => print(anime.getTitle().toDartString()));
runApp(const SizedBox());
void main() {
runApp(const BridgeTesterApp());
}
class BridgeTesterApp extends StatelessWidget {
const BridgeTesterApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'AniyomiBridge Tester',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const TesterHomePage(),
);
}
}
// ------------------------------------------------------------------
// Data models
// ------------------------------------------------------------------
class RepoExtension {
final String name;
final String pkg;
final String apk;
final String lang;
final int code;
final String version;
final int nsfw;
final List<SourceInfo> sources;
RepoExtension({
required this.name,
required this.pkg,
required this.apk,
required this.lang,
required this.code,
required this.version,
required this.nsfw,
required this.sources,
});
factory RepoExtension.fromJson(Map<String, dynamic> json) {
return RepoExtension(
name: json['name'] ?? '',
pkg: json['pkg'] ?? '',
apk: json['apk'] ?? '',
lang: json['lang'] ?? '',
code: json['code'] ?? 0,
version: json['version'] ?? '',
nsfw: json['nsfw'] ?? 0,
sources: (json['sources'] as List<dynamic>? ?? [])
.map((s) => SourceInfo.fromJson(s))
.toList(),
);
}
}
class SourceInfo {
final String name;
final String lang;
final String id;
final String baseUrl;
SourceInfo({required this.name, required this.lang, required this.id, required this.baseUrl});
factory SourceInfo.fromJson(Map<String, dynamic> json) {
return SourceInfo(
name: json['name'] ?? '',
lang: json['lang'] ?? '',
id: json['id']?.toString() ?? '',
baseUrl: json['baseUrl'] ?? '',
);
}
}
class LogEntry {
final DateTime timestamp;
final String message;
final bool isError;
LogEntry({required this.timestamp, required this.message, this.isError = false});
}
// ------------------------------------------------------------------
// Main page
// ------------------------------------------------------------------
class TesterHomePage extends StatefulWidget {
const TesterHomePage({super.key});
@override
State<TesterHomePage> createState() => _TesterHomePageState();
}
class _TesterHomePageState extends State<TesterHomePage> {
late final AniyomiBridge _bridge;
final List<LogEntry> _logs = [];
// Navigation
int _selectedPage = 0;
// Repo state
final _repoUrlCtrl = TextEditingController(
text: 'https://raw.githubusercontent.com/aniyomiorg/aniyomi-extensions/repo/index.min.json',
);
List<RepoExtension> _repoExtensions = [];
List<RepoExtension> _filteredExtensions = [];
bool _isFetchingRepo = false;
String? _repoBaseUrl;
// Search / content state
final _searchCtrl = TextEditingController();
String? _activePackageName;
String? _activeExtensionName;
List<JSAnime> _animeResults = [];
List<JSEpisode> _episodeResults = [];
List<JVideo> _videoResults = [];
List<JPreferenceItem> _preferences = [];
List<String> _loadedPackages = [];
final Set<String> _installingPackages = {};
@override
void initState() {
super.initState();
_bridge = AniyomiBridge();
Future.delayed(const Duration(seconds: 2), () {
_log('Bridge initialized ${_bridge.isReady()}');
_refreshLoaded();
});
}
void _log(String message, {bool isError = false}) {
setState(() {
_logs.insert(0, LogEntry(
timestamp: DateTime.now(),
message: message,
isError: isError,
));
if (_logs.length > 200) _logs.removeLast();
});
}
Future<void> _run(Future<void> Function() action, String label) async {
setState(() {});
try {
await action();
_log('$label: OK');
} catch (e, st) {
_log('$label: ERROR\n$e', isError: true);
debugPrint(st.toString());
}
}
Future<void> _fetchRepo() async {
final url = _repoUrlCtrl.text.trim();
if (url.isEmpty) return;
setState(() => _isFetchingRepo = true);
try {
final uri = Uri.parse(url);
_repoBaseUrl = '${uri.scheme}://${uri.host}${path.dirname(uri.path)}';
final response = await http.get(uri).timeout(const Duration(seconds: 15));
if (response.statusCode != 200) {
throw Exception('HTTP ${response.statusCode}');
}
final List<dynamic> jsonList = jsonDecode(response.body);
final extensions = jsonList.map((j) => RepoExtension.fromJson(j)).toList();
setState(() {
_repoExtensions = extensions;
_filteredExtensions = extensions;
});
_log('Fetched ${extensions.length} extensions from repo');
} catch (e) {
_log('Fetch repo failed: $e', isError: true);
} finally {
setState(() => _isFetchingRepo = false);
}
}
void _filterExtensions(String query) {
final q = query.toLowerCase();
setState(() {
_filteredExtensions = _repoExtensions.where((ext) {
return ext.name.toLowerCase().contains(q) ||
ext.pkg.toLowerCase().contains(q) ||
ext.lang.toLowerCase().contains(q);
}).toList();
});
}
Future<void> _installExtension(RepoExtension ext) async {
if (_repoBaseUrl == null || _installingPackages.contains(ext.pkg)) return;
setState(() => _installingPackages.add(ext.pkg));
final apkUrl = '$_repoBaseUrl/${ext.apk}';
try {
await _bridge.loadAnimeExtension(apkUrl, ext.pkg);
setState(() {
_activePackageName = ext.pkg;
_activeExtensionName = ext.name;
});
_log('Installed ${ext.name} (${ext.pkg})');
await _refreshLoaded();
} catch (e) {
_log('Install ${ext.name} failed: $e', isError: true);
} finally {
setState(() => _installingPackages.remove(ext.pkg));
}
}
Future<void> _unloadExtension(String packageName) async {
await _run(() => _bridge.unloadAnimeExtension(packageName), 'Unload $packageName');
if (_activePackageName == packageName) {
setState(() {
_activePackageName = null;
_activeExtensionName = null;
});
}
await _refreshLoaded();
}
Future<void> _searchAnime() async {
final query = _searchCtrl.text.trim();
final pkg = _activePackageName;
if (query.isEmpty || pkg == null || pkg.isEmpty) return;
await _run(() async {
final results = await _bridge.getAnimeSearchResults(query, 1, pkg);
setState(() => _animeResults = results);
}, 'Search anime');
}
Future<void> _getEpisodes(JSAnime anime) async {
final pkg = _activePackageName;
if (pkg == null) return;
await _run(() async {
final episodes = await _bridge.getEpisodeList(anime, pkg);
setState(() => _episodeResults = episodes);
}, 'Get episodes');
}
Future<void> _getVideos(JSEpisode episode) async {
final pkg = _activePackageName;
if (pkg == null) return;
await _run(() async {
final videos = await _bridge.getVideoList(episode, pkg);
setState(() => _videoResults = videos);
}, 'Get videos');
}
Future<void> _getPreferences() async {
final pkg = _activePackageName;
if (pkg == null) return;
await _run(() async {
final prefs = await _bridge.getAnimePreferences(pkg);
setState(() => _preferences = prefs);
}, 'Get preferences');
}
Future<void> _refreshLoaded() async {
try {
final loaded = await _bridge.getAnimeLoadedExtensions();
setState(() => _loadedPackages = loaded);
} catch (e) {
_log('Refresh loaded: $e', isError: true);
}
}
// ------------------------------------------------------------------
// Build
// ------------------------------------------------------------------
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('AniyomiBridge Tester'),
actions: [
if (_isFetchingRepo || _installingPackages.isNotEmpty)
const Padding(
padding: EdgeInsets.all(16),
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
),
),
],
),
bottomNavigationBar: NavigationBar(
selectedIndex: _selectedPage,
onDestinationSelected: (i) => setState(() => _selectedPage = i),
destinations: const [
NavigationDestination(icon: Icon(Icons.cloud_download), label: 'Repo'),
NavigationDestination(icon: Icon(Icons.extension), label: 'Installed'),
NavigationDestination(icon: Icon(Icons.search), label: 'Search'),
NavigationDestination(icon: Icon(Icons.settings), label: 'Prefs'),
NavigationDestination(icon: Icon(Icons.terminal), label: 'Logs'),
],
),
body: IndexedStack(
index: _selectedPage,
children: [
_buildRepoTab(),
_buildInstalledTab(),
_buildSearchTab(),
_buildPrefsTab(),
_buildLogsTab(),
],
),
);
}
// ------------------------------------------------------------------
// Repo tab
// ------------------------------------------------------------------
Widget _buildRepoTab() {
return Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Expanded(
child: TextField(
controller: _repoUrlCtrl,
decoration: const InputDecoration(
labelText: 'Repo JSON URL',
hintText: 'https://.../index.min.json',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.link),
),
),
),
const SizedBox(width: 8),
ElevatedButton.icon(
onPressed: _isFetchingRepo ? null : _fetchRepo,
icon: const Icon(Icons.download),
label: const Text('Fetch'),
),
],
),
),
if (_repoExtensions.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: TextField(
decoration: const InputDecoration(
hintText: 'Filter extensions...',
prefixIcon: Icon(Icons.filter_list),
border: OutlineInputBorder(),
),
onChanged: _filterExtensions,
),
),
Expanded(
child: _filteredExtensions.isEmpty
? const Center(child: Text('No extensions. Enter a repo URL and tap Fetch.'))
: ListView.builder(
itemCount: _filteredExtensions.length,
itemBuilder: (context, i) {
final ext = _filteredExtensions[i];
final isLoaded = _loadedPackages.contains(ext.pkg);
final isInstalling = _installingPackages.contains(ext.pkg);
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: ListTile(
leading: CircleAvatar(
backgroundColor: isLoaded ? Colors.green : Colors.grey,
child: Icon(
isLoaded ? Icons.check : Icons.extension,
color: Colors.white,
size: 18,
),
),
title: Text(ext.name),
subtitle: Text('${ext.lang} | v${ext.version} | ${ext.pkg}'),
trailing: isInstalling
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: isLoaded
? IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () => _unloadExtension(ext.pkg),
)
: ElevatedButton(
onPressed: () => _installExtension(ext),
child: const Text('Install'),
),
onTap: isLoaded
? () {
setState(() {
_activePackageName = ext.pkg;
_activeExtensionName = ext.name;
_selectedPage = 2; // Go to search
});
}
: null,
),
);
},
),
),
],
);
}
// ------------------------------------------------------------------
// Installed tab
// ------------------------------------------------------------------
Widget _buildInstalledTab() {
return _loadedPackages.isEmpty
? const Center(child: Text('No extensions loaded'))
: ListView.builder(
itemCount: _loadedPackages.length,
itemBuilder: (context, i) {
final pkg = _loadedPackages[i];
final ext = _repoExtensions.firstWhere(
(e) => e.pkg == pkg,
orElse: () => RepoExtension(
name: pkg.split('.').last,
pkg: pkg,
apk: '',
lang: '',
code: 0,
version: '',
nsfw: 0,
sources: [],
),
);
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: ListTile(
leading: const Icon(Icons.check_circle, color: Colors.green),
title: Text(ext.name),
subtitle: Text(pkg),
trailing: Wrap(
spacing: 4,
children: [
ElevatedButton(
onPressed: () {
setState(() {
_activePackageName = pkg;
_activeExtensionName = ext.name;
_selectedPage = 2;
});
},
child: const Text('Search'),
),
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () => _unloadExtension(pkg),
),
],
),
),
);
},
);
}
// ------------------------------------------------------------------
// Search tab
// ------------------------------------------------------------------
Widget _buildSearchTab() {
return Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_activeExtensionName != null)
Text(
'Active: $_activeExtensionName',
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: TextField(
controller: _searchCtrl,
decoration: const InputDecoration(
hintText: 'Search anime...',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.search),
),
onSubmitted: (_) => _searchAnime(),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: _activePackageName == null ? null : _searchAnime,
child: const Text('Search'),
),
],
),
],
),
),
Expanded(
child: _animeResults.isEmpty
? const Center(child: Text('No results'))
: ListView.builder(
itemCount: _animeResults.length,
itemBuilder: (context, i) {
final anime = _animeResults[i];
final title = anime.getTitle().toDartString();
return ListTile(
leading: const Icon(Icons.movie),
title: Text(title),
subtitle: Text(
anime.getUrl().toDartString(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: const Icon(Icons.chevron_right),
onTap: () => _showEpisodes(anime, title),
);
},
),
),
],
);
}
void _showEpisodes(JSAnime anime, String title) {
_getEpisodes(anime);
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (_) => DraggableScrollableSheet(
initialChildSize: 0.6,
minChildSize: 0.3,
maxChildSize: 0.9,
expand: false,
builder: (context, scrollCtrl) {
return Column(
children: [
AppBar(
title: Text(title),
automaticallyImplyLeading: false,
),
Expanded(
child: _episodeResults.isEmpty
? const Center(child: CircularProgressIndicator())
: ListView.builder(
controller: scrollCtrl,
itemCount: _episodeResults.length,
itemBuilder: (context, i) {
final ep = _episodeResults[i];
return ListTile(
leading: CircleAvatar(
child: Text('${ep.getEpisode_number()}'),
),
title: Text(ep.getName().toDartString()),
subtitle: Text(
ep.getUrl().toDartString(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: const Icon(Icons.play_arrow),
onTap: () {
Navigator.pop(context);
_getVideos(ep);
_showVideos(ep.getName().toDartString());
},
);
},
),
),
],
);
},
),
);
}
void _showVideos(String title) {
showDialog(
context: context,
builder: (_) => AlertDialog(
title: Text('Videos: $title'),
content: SizedBox(
width: double.maxFinite,
child: _videoResults.isEmpty
? const Text('No videos found')
: ListView.builder(
shrinkWrap: true,
itemCount: _videoResults.length,
itemBuilder: (context, i) {
final vid = _videoResults[i];
return ListTile(
leading: const Icon(Icons.videocam),
title: Text(vid.getVideoTitle().toDartString()),
subtitle: Text(
vid.getVideoUrl().toDartString(),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
trailing: vid.getPreferred()
? const Icon(Icons.star, color: Colors.amber)
: null,
);
},
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Close'),
),
],
),
);
}
// ------------------------------------------------------------------
// Prefs tab
// ------------------------------------------------------------------
Widget _buildPrefsTab() {
return Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Expanded(
child: Text(
_activeExtensionName == null
? 'No extension selected'
: 'Extension: $_activeExtensionName',
style: Theme.of(context).textTheme.titleSmall,
),
),
ElevatedButton(
onPressed: _activePackageName == null ? null : _getPreferences,
child: const Text('Load Prefs'),
),
],
),
),
Expanded(
child: _preferences.isEmpty
? const Center(child: Text('No preferences loaded'))
: ListView.builder(
itemCount: _preferences.length,
itemBuilder: (context, i) {
final pref = _preferences[i];
final key = pref.key?.toDartString() ?? '';
final title = pref.title?.toDartString() ?? key;
final type = pref.type$1?.toDartString() ?? 'unknown';
final value = pref.value;
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: ListTile(
title: Text(title),
subtitle: Text('[$type] $key'),
trailing: Text(value?.toString() ?? 'null'),
),
);
},
),
),
],
);
}
// ------------------------------------------------------------------
// Logs tab
// ------------------------------------------------------------------
Widget _buildLogsTab() {
return Column(
children: [
Padding(
padding: const EdgeInsets.all(8),
child: Row(
children: [
TextButton.icon(
onPressed: () => setState(() => _logs.clear()),
icon: const Icon(Icons.clear_all),
label: const Text('Clear'),
),
const Spacer(),
Text('${_logs.length} entries'),
],
),
),
Expanded(
child: _logs.isEmpty
? const Center(child: Text('No logs'))
: ListView.builder(
reverse: true,
itemCount: _logs.length,
itemBuilder: (context, i) {
final log = _logs[i];
return Container(
color: log.isError ? Colors.red.shade50 : null,
child: ListTile(
dense: true,
leading: Icon(
log.isError ? Icons.error : Icons.info,
color: log.isError ? Colors.red : Colors.blue,
size: 18,
),
title: Text(
log.message,
style: TextStyle(
fontSize: 12,
color: log.isError ? Colors.red.shade900 : null,
fontFamily: 'monospace',
),
),
subtitle: Text(
'${log.timestamp.hour.toString().padLeft(2, '0')}:${log.timestamp.minute.toString().padLeft(2, '0')}:${log.timestamp.second.toString().padLeft(2, '0')}',
style: const TextStyle(fontSize: 10),
),
),
);
},
),
),
],
);
}
}

View File

@@ -108,7 +108,7 @@ packages:
source: sdk
version: "0.0.0"
http:
dependency: transitive
dependency: "direct main"
description:
name: http
sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
@@ -235,7 +235,7 @@ packages:
source: hosted
version: "2.2.0"
path:
dependency: transitive
dependency: "direct main"
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"

View File

@@ -34,6 +34,8 @@ dependencies:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
http: ^1.2.0
path: ^1.9.0
k3vinb5_aniyomi_bridge:
path: ../

View File

@@ -0,0 +1,9 @@
output:
dart:
path: ../lib/jmodels/jpreferenceitem.dart
structure: single_file
class_path:
- '../assets/aniyomibridge-core.jar'
classes:
- 'dev.k3vinb5.aniyomibridge.PreferenceItem'

View File

@@ -17,6 +17,7 @@ import 'package:k3vinb5_aniyomi_bridge/jmodels/jsepisode.dart';
import 'package:k3vinb5_aniyomi_bridge/jmodels/jsmanga.dart';
import 'package:k3vinb5_aniyomi_bridge/jmodels/jschapter.dart';
import 'package:k3vinb5_aniyomi_bridge/jmodels/jpage.dart';
import 'package:k3vinb5_aniyomi_bridge/jmodels/jpreferenceitem.dart';
class AniyomiBridge {
// static consts
@@ -94,11 +95,11 @@ class AniyomiBridge {
return _isReady;
}
Future<List<JSAnime>> getAnimeSearchResults(String query, int page, String source) async {
Future<List<JSAnime>> getAnimeSearchResults(String query, int page, String packageName) async {
Map<String, dynamic> args = {
'query': query,
'page': page,
'source': source,
'source': packageName,
};
Future<List<JSAnime>>? cachedResponse = _getCachedResponse<Future<List<JSAnime>>>(JniIsolateMessageType.getAnimeSearchResults, args);
if (cachedResponse != null) return cachedResponse;
@@ -113,11 +114,11 @@ class AniyomiBridge {
return response;
}
Future<List<JSManga>> getMangaSearchResults(String query, int page, String source) async {
Future<List<JSManga>> getMangaSearchResults(String query, int page, String packageName) async {
Map<String, dynamic> args = {
'query': query,
'page': page,
'source': source,
'source': packageName,
};
Future<List<JSManga>>? cachedResponse = _getCachedResponse<Future<List<JSManga>>>(JniIsolateMessageType.getMangaSearchResults, args);
if (cachedResponse != null) return cachedResponse;
@@ -132,10 +133,10 @@ class AniyomiBridge {
return response;
}
Future<List<JSEpisode>> getEpisodeList(JSAnime sAnime, String source) async {
Future<List<JSEpisode>> getEpisodeList(JSAnime sAnime, String packageName) async {
Map<String, dynamic> args = {
'sAnime': sAnime,
'source': source,
'source': packageName,
};
Future<List<JSEpisode>>? cachedResponse = _getCachedResponse<Future<List<JSEpisode>>>(JniIsolateMessageType.getEpisodeList, args);
if (cachedResponse != null) return cachedResponse;
@@ -150,10 +151,10 @@ class AniyomiBridge {
return response;
}
Future<List<JSChapter>> getChapterList(JSManga sManga, String source) async {
Future<List<JSChapter>> getChapterList(JSManga sManga, String packageName) async {
Map<String, dynamic> args = {
'sManga': sManga,
'source': source,
'source': packageName,
};
Future<List<JSChapter>>? cachedResponse = _getCachedResponse<Future<List<JSChapter>>>(JniIsolateMessageType.getChapterList, args);
if (cachedResponse != null) return cachedResponse;
@@ -168,10 +169,10 @@ class AniyomiBridge {
return response;
}
Future<List<JVideo>> getVideoList(JSEpisode sEpisode, String source) async {
Future<List<JVideo>> getVideoList(JSEpisode sEpisode, String packageName) async {
Map<String, dynamic> args = {
'sEpisode': sEpisode,
'source': source,
'source': packageName,
};
Future<List<JVideo>>? cachedResponse = _getCachedResponse<Future<List<JVideo>>>(JniIsolateMessageType.getVideoList, args);
if (cachedResponse != null) return cachedResponse;
@@ -186,10 +187,10 @@ class AniyomiBridge {
return response;
}
Future<List<JPage>> getPageList(JSChapter sChapter, String source) async {
Future<List<JPage>> getPageList(JSChapter sChapter, String packageName) async {
Map<String, dynamic> args = {
'sChapter': sChapter,
'source': source,
'source': packageName,
};
Future<List<JPage>>? cachedResponse = _getCachedResponse<Future<List<JPage>>>(JniIsolateMessageType.getPageList, args);
if (cachedResponse != null) return cachedResponse;
@@ -204,117 +205,143 @@ class AniyomiBridge {
return response;
}
Future<void> loadAnimeExtension(String extensionUrl) async {
Future<void> loadAnimeExtension(String extensionUrl, String packageName) async {
return _sendJniIsolateMessage<void>(
JniIsolateMessage(
id: _random.nextInt(1 << 32),
type: JniIsolateMessageType.loadAnimeExtension,
args: {
'extensionUrl': extensionUrl,
'packageName': packageName,
},
),
);
}
Future<void> unloadAnimeExtension(String extensionName, String extensionVersion) async {
Future<void> unloadAnimeExtension(String packageName) async {
return _sendJniIsolateMessage<void>(
JniIsolateMessage(
id: _random.nextInt(1 << 32),
type: JniIsolateMessageType.unloadAnimeExtension,
args: {
'extensionName': extensionName,
'extensionVersion': extensionVersion,
'packageName': packageName,
},
),
);
}
Future<void> loadMangaExtension(String extensionUrl) async {
Future<void> loadMangaExtension(String extensionUrl, String packageName) async {
return _sendJniIsolateMessage<void>(
JniIsolateMessage(
id: _random.nextInt(1 << 32),
type: JniIsolateMessageType.loadMangaExtension,
args: {
'extensionUrl': extensionUrl,
'packageName': packageName,
},
),
);
}
Future<void> unloadMangaExtension(String extensionName, String extensionVersion) async {
Future<void> unloadMangaExtension(String packageName) async {
return _sendJniIsolateMessage<void>(
JniIsolateMessage(
id: _random.nextInt(1 << 32),
type: JniIsolateMessageType.unloadMangaExtension,
args: {
'extensionName': extensionName,
'extensionVersion': extensionVersion,
'packageName': packageName,
},
),
);
}
Future<List<JPreferenceItem>> getAnimePreferences(String packageName) async {
return _sendJniIsolateMessage<List<JPreferenceItem>>(
JniIsolateMessage(
id: _random.nextInt(1 << 32),
type: JniIsolateMessageType.getAnimePreferences,
args: {'packageName': packageName},
),
);
}
Future<void> setAnimePreference(String packageName, String key, dynamic value) async {
return _sendJniIsolateMessage<void>(
JniIsolateMessage(
id: _random.nextInt(1 << 32),
type: JniIsolateMessageType.setAnimePreference,
args: {'packageName': packageName, 'key': key, 'value': value},
),
);
}
Future<void> setAnimePreferences(String packageName, Map<String, dynamic> values) async {
return _sendJniIsolateMessage<void>(
JniIsolateMessage(
id: _random.nextInt(1 << 32),
type: JniIsolateMessageType.setAnimePreferences,
args: {'packageName': packageName, 'values': values},
),
);
}
Future<List<String>> getAnimeLoadedExtensions() async {
return _sendJniIsolateMessage<List<String>>(
JniIsolateMessage(
id: _random.nextInt(1 << 32),
type: JniIsolateMessageType.getAnimeLoadedExtensions,
args: {},
),
);
}
Future<List<String>> getMangaLoadedExtensions() async {
return _sendJniIsolateMessage<List<String>>(
JniIsolateMessage(
id: _random.nextInt(1 << 32),
type: JniIsolateMessageType.getMangaLoadedExtensions,
args: {},
),
);
}
Future<Set<Extension>> getInstalledAnimeExtensions() async {
Directory animeExtDir = Directory(
path.join(_supportDirectoryPath, _aniyomiBridgeAnimeExtensionsDir),
);
Set<Extension> extensions = await animeExtDir
.list()
.where((entityFileSystem) => entityFileSystem is File)
.where((file) => path.extension(file.path) == ".jar")
.map((jarFile) => path.basename(jarFile.path))
.map(
(jarFileName) => Extension(
name: jarFileName.split("-")[0],
version: jarFileName.split("-")[1],
),
)
.toSet();
return extensions;
if (!await animeExtDir.exists()) return {};
return _scanExtensionsRecursively(animeExtDir);
}
Future<Set<Extension>> getInstalledMangaExtensions() async {
Directory mangaExtDir = Directory(
path.join(_supportDirectoryPath, _aniyomiBridgeMangaExtensionsDir),
);
Set<Extension> extensions = await mangaExtDir
.list()
.where((entityFileSystem) => entityFileSystem is File)
.where((file) => path.extension(file.path) == ".jar")
.map((jarFile) => path.basename(jarFile.path))
.map(
(jarFileName) => Extension(
name: jarFileName.split("-")[0],
version: jarFileName.split("-")[1],
),
)
.toSet();
return extensions;
if (!await mangaExtDir.exists()) return {};
return _scanExtensionsRecursively(mangaExtDir);
}
// List<String>? getLoadedAnimeExtensions() {
// JList<JString?>? loadedExtensions = _jAniyomiBridge
// .getAnimeLoadedExtensions();
// if (loadedExtensions == null) {
// return null;
// }
// return loadedExtensions
// .cast<JString>()
// .map((jStr) => jStr.toDartString())
// .toList();
// }
//
// List<String>? getLoadedMangaExtensions() {
// JList<JString?>? loadedExtensions = _jAniyomiBridge
// .getMangaLoadedExtensions();
// if (loadedExtensions == null) {
// return null;
// }
// return loadedExtensions
// .cast<JString>()
// .map((jStr) => jStr.toDartString())
// .toList();
// }
Future<Set<Extension>> _scanExtensionsRecursively(Directory rootDir) async {
final extensions = <Extension>{};
await for (final entity in rootDir.list(recursive: true)) {
if (entity is! File) continue;
if (path.extension(entity.path) != '.jar') continue;
final jarFileName = path.basename(entity.path);
final nameWithoutExt = jarFileName.replaceAll('.jar', '');
final firstDash = nameWithoutExt.indexOf('-');
final name = firstDash >= 0 ? nameWithoutExt.substring(0, firstDash) : nameWithoutExt;
final version = firstDash >= 0 ? nameWithoutExt.substring(firstDash + 1) : '';
extensions.add(Extension(
name: name,
version: version,
packageName: '', // Still cannot determine from filename alone
));
}
return extensions;
}
Future<T> _sendJniIsolateMessage<T>(JniIsolateMessage message) {
final completer = Completer<T>();

View File

@@ -36,7 +36,7 @@ import 'dart:core' as core$_;
import 'package:jni/_internal.dart' as jni$_;
import 'package:jni/jni.dart' as jni$_;
/// from: `dev.k3vinb5.aniyomibridge.AniyomiBridge`
/// from: `dev.k3vinb5.aniyomibridge.JAniyomiBridge`
class JAniyomiBridge extends jni$_.JObject {
@jni$_.internal
@core$_.override
@@ -48,21 +48,27 @@ class JAniyomiBridge extends jni$_.JObject {
super.fromReference(reference);
static final _class = jni$_.JClass.forName(
r'dev/k3vinb5/aniyomibridge/AniyomiBridge',
r'dev/k3vinb5/aniyomibridge/JAniyomiBridge',
);
/// The type which includes information such as the signature of this class.
static const nullableType = $AniyomiBridge$NullableType();
static const type = $AniyomiBridge$Type();
static final _id_COROUTINE_UTILS = _class.staticFieldId(
r'COROUTINE_UTILS',
r'Ldev/k3vinb5/aniyomibridge/utils/CoroutineUtils;',
static const nullableType = $JAniyomiBridge$NullableType();
static const type = $JAniyomiBridge$Type();
static final _id_classLoaders = _class.staticFieldId(
r'classLoaders',
r'Ljava/util/Map;',
);
/// from: `static public final dev.k3vinb5.aniyomibridge.utils.CoroutineUtils COROUTINE_UTILS`
/// from: `static public final java.util.Map<java.lang.String, java.lang.ClassLoader> classLoaders`
/// The returned object must be released after use, by calling the [release] method.
static jni$_.JObject? get COROUTINE_UTILS =>
_id_COROUTINE_UTILS.get(_class, const jni$_.JObjectNullableType());
static jni$_.JMap<jni$_.JString?, jni$_.JObject?>? get classLoaders =>
_id_classLoaders.get(
_class,
const jni$_.JMapNullableType<jni$_.JString?, jni$_.JObject?>(
jni$_.JStringNullableType(),
jni$_.JObjectNullableType(),
),
);
static final _id_new$ = _class.constructorId(r'()V');
@@ -115,48 +121,10 @@ class JAniyomiBridge extends jni$_.JObject {
static final _id_loadAnimeExtension = _class.instanceMethodId(
r'loadAnimeExtension',
r'(Ljava/lang/String;Ljava/lang/String;)V',
r'(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V',
);
static final _loadAnimeExtension =
jni$_.ProtectedJniExtensions.lookup<
jni$_.NativeFunction<
jni$_.JThrowablePtr Function(
jni$_.Pointer<jni$_.Void>,
jni$_.JMethodIDPtr,
jni$_.VarArgs<
(jni$_.Pointer<jni$_.Void>, jni$_.Pointer<jni$_.Void>)
>,
)
>
>('globalEnv_CallVoidMethod')
.asFunction<
jni$_.JThrowablePtr Function(
jni$_.Pointer<jni$_.Void>,
jni$_.JMethodIDPtr,
jni$_.Pointer<jni$_.Void>,
jni$_.Pointer<jni$_.Void>,
)
>();
/// from: `public void loadAnimeExtension(java.lang.String string, java.lang.String string1)`
void loadAnimeExtension(jni$_.JString? string, jni$_.JString? string1) {
final _$string = string?.reference ?? jni$_.jNullReference;
final _$string1 = string1?.reference ?? jni$_.jNullReference;
_loadAnimeExtension(
reference.pointer,
_id_loadAnimeExtension as jni$_.JMethodIDPtr,
_$string.pointer,
_$string1.pointer,
).check();
}
static final _id_unloadAnimeExtension = _class.instanceMethodId(
r'unloadAnimeExtension',
r'(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V',
);
static final _unloadAnimeExtension =
jni$_.ProtectedJniExtensions.lookup<
jni$_.NativeFunction<
jni$_.JThrowablePtr Function(
@@ -182,8 +150,8 @@ class JAniyomiBridge extends jni$_.JObject {
)
>();
/// from: `public void unloadAnimeExtension(java.lang.String string, java.lang.String string1, java.lang.String string2)`
void unloadAnimeExtension(
/// from: `public void loadAnimeExtension(java.lang.String string, java.lang.String string1, java.lang.String string2)`
void loadAnimeExtension(
jni$_.JString? string,
jni$_.JString? string1,
jni$_.JString? string2,
@@ -191,59 +159,54 @@ class JAniyomiBridge extends jni$_.JObject {
final _$string = string?.reference ?? jni$_.jNullReference;
final _$string1 = string1?.reference ?? jni$_.jNullReference;
final _$string2 = string2?.reference ?? jni$_.jNullReference;
_unloadAnimeExtension(
_loadAnimeExtension(
reference.pointer,
_id_unloadAnimeExtension as jni$_.JMethodIDPtr,
_id_loadAnimeExtension as jni$_.JMethodIDPtr,
_$string.pointer,
_$string1.pointer,
_$string2.pointer,
).check();
}
static final _id_unloadAnimeExtension = _class.instanceMethodId(
r'unloadAnimeExtension',
r'(Ljava/lang/String;)V',
);
static final _unloadAnimeExtension =
jni$_.ProtectedJniExtensions.lookup<
jni$_.NativeFunction<
jni$_.JThrowablePtr Function(
jni$_.Pointer<jni$_.Void>,
jni$_.JMethodIDPtr,
jni$_.VarArgs<(jni$_.Pointer<jni$_.Void>,)>,
)
>
>('globalEnv_CallVoidMethod')
.asFunction<
jni$_.JThrowablePtr Function(
jni$_.Pointer<jni$_.Void>,
jni$_.JMethodIDPtr,
jni$_.Pointer<jni$_.Void>,
)
>();
/// from: `public void unloadAnimeExtension(java.lang.String string)`
void unloadAnimeExtension(jni$_.JString? string) {
final _$string = string?.reference ?? jni$_.jNullReference;
_unloadAnimeExtension(
reference.pointer,
_id_unloadAnimeExtension as jni$_.JMethodIDPtr,
_$string.pointer,
).check();
}
static final _id_loadMangaExtension = _class.instanceMethodId(
r'loadMangaExtension',
r'(Ljava/lang/String;Ljava/lang/String;)V',
r'(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V',
);
static final _loadMangaExtension =
jni$_.ProtectedJniExtensions.lookup<
jni$_.NativeFunction<
jni$_.JThrowablePtr Function(
jni$_.Pointer<jni$_.Void>,
jni$_.JMethodIDPtr,
jni$_.VarArgs<
(jni$_.Pointer<jni$_.Void>, jni$_.Pointer<jni$_.Void>)
>,
)
>
>('globalEnv_CallVoidMethod')
.asFunction<
jni$_.JThrowablePtr Function(
jni$_.Pointer<jni$_.Void>,
jni$_.JMethodIDPtr,
jni$_.Pointer<jni$_.Void>,
jni$_.Pointer<jni$_.Void>,
)
>();
/// from: `public void loadMangaExtension(java.lang.String string, java.lang.String string1)`
void loadMangaExtension(jni$_.JString? string, jni$_.JString? string1) {
final _$string = string?.reference ?? jni$_.jNullReference;
final _$string1 = string1?.reference ?? jni$_.jNullReference;
_loadMangaExtension(
reference.pointer,
_id_loadMangaExtension as jni$_.JMethodIDPtr,
_$string.pointer,
_$string1.pointer,
).check();
}
static final _id_unloadMangaExtension = _class.instanceMethodId(
r'unloadMangaExtension',
r'(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V',
);
static final _unloadMangaExtension =
jni$_.ProtectedJniExtensions.lookup<
jni$_.NativeFunction<
jni$_.JThrowablePtr Function(
@@ -269,8 +232,8 @@ class JAniyomiBridge extends jni$_.JObject {
)
>();
/// from: `public void unloadMangaExtension(java.lang.String string, java.lang.String string1, java.lang.String string2)`
void unloadMangaExtension(
/// from: `public void loadMangaExtension(java.lang.String string, java.lang.String string1, java.lang.String string2)`
void loadMangaExtension(
jni$_.JString? string,
jni$_.JString? string1,
jni$_.JString? string2,
@@ -278,12 +241,45 @@ class JAniyomiBridge extends jni$_.JObject {
final _$string = string?.reference ?? jni$_.jNullReference;
final _$string1 = string1?.reference ?? jni$_.jNullReference;
final _$string2 = string2?.reference ?? jni$_.jNullReference;
_loadMangaExtension(
reference.pointer,
_id_loadMangaExtension as jni$_.JMethodIDPtr,
_$string.pointer,
_$string1.pointer,
_$string2.pointer,
).check();
}
static final _id_unloadMangaExtension = _class.instanceMethodId(
r'unloadMangaExtension',
r'(Ljava/lang/String;)V',
);
static final _unloadMangaExtension =
jni$_.ProtectedJniExtensions.lookup<
jni$_.NativeFunction<
jni$_.JThrowablePtr Function(
jni$_.Pointer<jni$_.Void>,
jni$_.JMethodIDPtr,
jni$_.VarArgs<(jni$_.Pointer<jni$_.Void>,)>,
)
>
>('globalEnv_CallVoidMethod')
.asFunction<
jni$_.JThrowablePtr Function(
jni$_.Pointer<jni$_.Void>,
jni$_.JMethodIDPtr,
jni$_.Pointer<jni$_.Void>,
)
>();
/// from: `public void unloadMangaExtension(java.lang.String string)`
void unloadMangaExtension(jni$_.JString? string) {
final _$string = string?.reference ?? jni$_.jNullReference;
_unloadMangaExtension(
reference.pointer,
_id_unloadMangaExtension as jni$_.JMethodIDPtr,
_$string.pointer,
_$string1.pointer,
_$string2.pointer,
).check();
}
@@ -355,6 +351,204 @@ class JAniyomiBridge extends jni$_.JObject {
);
}
static final _id_getInstalledAnimeExtensions = _class.instanceMethodId(
r'getInstalledAnimeExtensions',
r'()Ljava/util/Map;',
);
static final _getInstalledAnimeExtensions =
jni$_.ProtectedJniExtensions.lookup<
jni$_.NativeFunction<
jni$_.JniResult Function(
jni$_.Pointer<jni$_.Void>,
jni$_.JMethodIDPtr,
)
>
>('globalEnv_CallObjectMethod')
.asFunction<
jni$_.JniResult Function(
jni$_.Pointer<jni$_.Void>,
jni$_.JMethodIDPtr,
)
>();
/// from: `public java.util.Map<java.lang.String, java.lang.String> getInstalledAnimeExtensions()`
/// The returned object must be released after use, by calling the [release] method.
jni$_.JMap<jni$_.JString?, jni$_.JString?>? getInstalledAnimeExtensions() {
return _getInstalledAnimeExtensions(
reference.pointer,
_id_getInstalledAnimeExtensions as jni$_.JMethodIDPtr,
).object<jni$_.JMap<jni$_.JString?, jni$_.JString?>?>(
const jni$_.JMapNullableType<jni$_.JString?, jni$_.JString?>(
jni$_.JStringNullableType(),
jni$_.JStringNullableType(),
),
);
}
static final _id_getInstalledMangaExtensions = _class.instanceMethodId(
r'getInstalledMangaExtensions',
r'()Ljava/util/Map;',
);
static final _getInstalledMangaExtensions =
jni$_.ProtectedJniExtensions.lookup<
jni$_.NativeFunction<
jni$_.JniResult Function(
jni$_.Pointer<jni$_.Void>,
jni$_.JMethodIDPtr,
)
>
>('globalEnv_CallObjectMethod')
.asFunction<
jni$_.JniResult Function(
jni$_.Pointer<jni$_.Void>,
jni$_.JMethodIDPtr,
)
>();
/// from: `public java.util.Map<java.lang.String, java.lang.String> getInstalledMangaExtensions()`
/// The returned object must be released after use, by calling the [release] method.
jni$_.JMap<jni$_.JString?, jni$_.JString?>? getInstalledMangaExtensions() {
return _getInstalledMangaExtensions(
reference.pointer,
_id_getInstalledMangaExtensions as jni$_.JMethodIDPtr,
).object<jni$_.JMap<jni$_.JString?, jni$_.JString?>?>(
const jni$_.JMapNullableType<jni$_.JString?, jni$_.JString?>(
jni$_.JStringNullableType(),
jni$_.JStringNullableType(),
),
);
}
static final _id_getAnimePreferences = _class.instanceMethodId(
r'getAnimePreferences',
r'(Ljava/lang/String;)Ljava/util/List;',
);
static final _getAnimePreferences =
jni$_.ProtectedJniExtensions.lookup<
jni$_.NativeFunction<
jni$_.JniResult Function(
jni$_.Pointer<jni$_.Void>,
jni$_.JMethodIDPtr,
jni$_.VarArgs<(jni$_.Pointer<jni$_.Void>,)>,
)
>
>('globalEnv_CallObjectMethod')
.asFunction<
jni$_.JniResult Function(
jni$_.Pointer<jni$_.Void>,
jni$_.JMethodIDPtr,
jni$_.Pointer<jni$_.Void>,
)
>();
/// from: `public java.util.List<dev.k3vinb5.aniyomibridge.PreferenceItem> getAnimePreferences(java.lang.String string)`
/// The returned object must be released after use, by calling the [release] method.
jni$_.JList<jni$_.JObject?>? getAnimePreferences(jni$_.JString? string) {
final _$string = string?.reference ?? jni$_.jNullReference;
return _getAnimePreferences(
reference.pointer,
_id_getAnimePreferences as jni$_.JMethodIDPtr,
_$string.pointer,
).object<jni$_.JList<jni$_.JObject?>?>(
const jni$_.JListNullableType<jni$_.JObject?>(
jni$_.JObjectNullableType(),
),
);
}
static final _id_setAnimePreference = _class.instanceMethodId(
r'setAnimePreference',
r'(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)V',
);
static final _setAnimePreference =
jni$_.ProtectedJniExtensions.lookup<
jni$_.NativeFunction<
jni$_.JThrowablePtr Function(
jni$_.Pointer<jni$_.Void>,
jni$_.JMethodIDPtr,
jni$_.VarArgs<
(
jni$_.Pointer<jni$_.Void>,
jni$_.Pointer<jni$_.Void>,
jni$_.Pointer<jni$_.Void>,
)
>,
)
>
>('globalEnv_CallVoidMethod')
.asFunction<
jni$_.JThrowablePtr Function(
jni$_.Pointer<jni$_.Void>,
jni$_.JMethodIDPtr,
jni$_.Pointer<jni$_.Void>,
jni$_.Pointer<jni$_.Void>,
jni$_.Pointer<jni$_.Void>,
)
>();
/// from: `public void setAnimePreference(java.lang.String string, java.lang.String string1, java.lang.Object object)`
void setAnimePreference(
jni$_.JString? string,
jni$_.JString? string1,
jni$_.JObject? object,
) {
final _$string = string?.reference ?? jni$_.jNullReference;
final _$string1 = string1?.reference ?? jni$_.jNullReference;
final _$object = object?.reference ?? jni$_.jNullReference;
_setAnimePreference(
reference.pointer,
_id_setAnimePreference as jni$_.JMethodIDPtr,
_$string.pointer,
_$string1.pointer,
_$object.pointer,
).check();
}
static final _id_setAnimePreferences = _class.instanceMethodId(
r'setAnimePreferences',
r'(Ljava/lang/String;Ljava/util/Map;)V',
);
static final _setAnimePreferences =
jni$_.ProtectedJniExtensions.lookup<
jni$_.NativeFunction<
jni$_.JThrowablePtr Function(
jni$_.Pointer<jni$_.Void>,
jni$_.JMethodIDPtr,
jni$_.VarArgs<
(jni$_.Pointer<jni$_.Void>, jni$_.Pointer<jni$_.Void>)
>,
)
>
>('globalEnv_CallVoidMethod')
.asFunction<
jni$_.JThrowablePtr Function(
jni$_.Pointer<jni$_.Void>,
jni$_.JMethodIDPtr,
jni$_.Pointer<jni$_.Void>,
jni$_.Pointer<jni$_.Void>,
)
>();
/// from: `public void setAnimePreferences(java.lang.String string, java.util.Map<java.lang.String, java.lang.Object> map)`
void setAnimePreferences(
jni$_.JString? string,
jni$_.JMap<jni$_.JString?, jni$_.JObject?>? map,
) {
final _$string = string?.reference ?? jni$_.jNullReference;
final _$map = map?.reference ?? jni$_.jNullReference;
_setAnimePreferences(
reference.pointer,
_id_setAnimePreferences as jni$_.JMethodIDPtr,
_$string.pointer,
_$map.pointer,
).check();
}
static final _id_getAnimeSearchResults = _class.instanceMethodId(
r'getAnimeSearchResults',
r'(Ljava/lang/String;ILjava/lang/String;)Ljava/util/List;',
@@ -646,13 +840,13 @@ class JAniyomiBridge extends jni$_.JObject {
}
}
final class $AniyomiBridge$NullableType extends jni$_.JObjType<JAniyomiBridge?> {
final class $JAniyomiBridge$NullableType extends jni$_.JObjType<JAniyomiBridge?> {
@jni$_.internal
const $AniyomiBridge$NullableType();
const $JAniyomiBridge$NullableType();
@jni$_.internal
@core$_.override
String get signature => r'Ldev/k3vinb5/aniyomibridge/AniyomiBridge;';
String get signature => r'Ldev/k3vinb5/aniyomibridge/JAniyomiBridge;';
@jni$_.internal
@core$_.override
@@ -671,22 +865,22 @@ final class $AniyomiBridge$NullableType extends jni$_.JObjType<JAniyomiBridge?>
final superCount = 1;
@core$_.override
int get hashCode => ($AniyomiBridge$NullableType).hashCode;
int get hashCode => ($JAniyomiBridge$NullableType).hashCode;
@core$_.override
bool operator ==(Object other) {
return other.runtimeType == ($AniyomiBridge$NullableType) &&
other is $AniyomiBridge$NullableType;
return other.runtimeType == ($JAniyomiBridge$NullableType) &&
other is $JAniyomiBridge$NullableType;
}
}
final class $AniyomiBridge$Type extends jni$_.JObjType<JAniyomiBridge> {
final class $JAniyomiBridge$Type extends jni$_.JObjType<JAniyomiBridge> {
@jni$_.internal
const $AniyomiBridge$Type();
const $JAniyomiBridge$Type();
@jni$_.internal
@core$_.override
String get signature => r'Ldev/k3vinb5/aniyomibridge/AniyomiBridge;';
String get signature => r'Ldev/k3vinb5/aniyomibridge/JAniyomiBridge;';
@jni$_.internal
@core$_.override
@@ -699,18 +893,18 @@ final class $AniyomiBridge$Type extends jni$_.JObjType<JAniyomiBridge> {
@jni$_.internal
@core$_.override
jni$_.JObjType<JAniyomiBridge?> get nullableType =>
const $AniyomiBridge$NullableType();
const $JAniyomiBridge$NullableType();
@jni$_.internal
@core$_.override
final superCount = 1;
@core$_.override
int get hashCode => ($AniyomiBridge$Type).hashCode;
int get hashCode => ($JAniyomiBridge$Type).hashCode;
@core$_.override
bool operator ==(Object other) {
return other.runtimeType == ($AniyomiBridge$Type) &&
other is $AniyomiBridge$Type;
return other.runtimeType == ($JAniyomiBridge$Type) &&
other is $JAniyomiBridge$Type;
}
}

View File

@@ -0,0 +1,309 @@
// AUTO GENERATED BY JNIGEN 0.14.2. DO NOT EDIT!
// ignore_for_file: annotate_overrides
// ignore_for_file: argument_type_not_assignable
// ignore_for_file: camel_case_extensions
// ignore_for_file: camel_case_types
// ignore_for_file: constant_identifier_names
// ignore_for_file: comment_references
// ignore_for_file: doc_directive_unknown
// ignore_for_file: file_names
// ignore_for_file: inference_failure_on_untyped_parameter
// ignore_for_file: invalid_internal_annotation
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: library_prefixes
// ignore_for_file: lines_longer_than_80_chars
// ignore_for_file: no_leading_underscores_for_library_prefixes
// ignore_for_file: no_leading_underscores_for_local_identifiers
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: only_throw_errors
// ignore_for_file: overridden_fields
// ignore_for_file: prefer_double_quotes
// ignore_for_file: unintended_html_in_doc_comment
// ignore_for_file: unnecessary_cast
// ignore_for_file: unnecessary_non_null_assertion
// ignore_for_file: unnecessary_parenthesis
// ignore_for_file: unused_element
// ignore_for_file: unused_field
// ignore_for_file: unused_import
// ignore_for_file: unused_local_variable
// ignore_for_file: unused_shown_name
// ignore_for_file: use_super_parameters
import 'dart:core' show Object, String, bool, double, int;
import 'dart:core' as core$_;
import 'package:jni/_internal.dart' as jni$_;
import 'package:jni/jni.dart' as jni$_;
/// from: `dev.k3vinb5.aniyomibridge.JPreferenceItem`
class JPreferenceItem extends jni$_.JObject {
@jni$_.internal
@core$_.override
final jni$_.JObjType<JPreferenceItem> $type;
@jni$_.internal
JPreferenceItem.fromReference(jni$_.JReference reference)
: $type = type,
super.fromReference(reference);
static final _class = jni$_.JClass.forName(
r'dev/k3vinb5/aniyomibridge/JPreferenceItem',
);
/// The type which includes information such as the signature of this class.
static const nullableType = $JPreferenceItem$NullableType();
static const type = $JPreferenceItem$Type();
static final _id_key = _class.instanceFieldId(r'key', r'Ljava/lang/String;');
/// from: `public java.lang.String key`
/// The returned object must be released after use, by calling the [release] method.
jni$_.JString? get key =>
_id_key.get(this, const jni$_.JStringNullableType());
/// from: `public java.lang.String key`
/// The returned object must be released after use, by calling the [release] method.
set key(jni$_.JString? value) =>
_id_key.set(this, const jni$_.JStringNullableType(), value);
static final _id_title = _class.instanceFieldId(
r'title',
r'Ljava/lang/String;',
);
/// from: `public java.lang.String title`
/// The returned object must be released after use, by calling the [release] method.
jni$_.JString? get title =>
_id_title.get(this, const jni$_.JStringNullableType());
/// from: `public java.lang.String title`
/// The returned object must be released after use, by calling the [release] method.
set title(jni$_.JString? value) =>
_id_title.set(this, const jni$_.JStringNullableType(), value);
static final _id_type$1 = _class.instanceFieldId(
r'type',
r'Ljava/lang/String;',
);
/// from: `public java.lang.String type`
/// The returned object must be released after use, by calling the [release] method.
jni$_.JString? get type$1 =>
_id_type$1.get(this, const jni$_.JStringNullableType());
/// from: `public java.lang.String type`
/// The returned object must be released after use, by calling the [release] method.
set type$1(jni$_.JString? value) =>
_id_type$1.set(this, const jni$_.JStringNullableType(), value);
static final _id_value = _class.instanceFieldId(
r'value',
r'Ljava/lang/Object;',
);
/// from: `public java.lang.Object value`
/// The returned object must be released after use, by calling the [release] method.
jni$_.JObject? get value =>
_id_value.get(this, const jni$_.JObjectNullableType());
/// from: `public java.lang.Object value`
/// The returned object must be released after use, by calling the [release] method.
set value(jni$_.JObject? value) =>
_id_value.set(this, const jni$_.JObjectNullableType(), value);
static final _id_entries = _class.instanceFieldId(
r'entries',
r'Ljava/util/List;',
);
/// from: `public java.util.List<java.lang.String> entries`
/// The returned object must be released after use, by calling the [release] method.
jni$_.JList<jni$_.JString?>? get entries => _id_entries.get(
this,
const jni$_.JListNullableType<jni$_.JString?>(jni$_.JStringNullableType()),
);
/// from: `public java.util.List<java.lang.String> entries`
/// The returned object must be released after use, by calling the [release] method.
set entries(jni$_.JList<jni$_.JString?>? value) => _id_entries.set(
this,
const jni$_.JListNullableType<jni$_.JString?>(jni$_.JStringNullableType()),
value,
);
static final _id_entryValues = _class.instanceFieldId(
r'entryValues',
r'Ljava/util/List;',
);
/// from: `public java.util.List<java.lang.String> entryValues`
/// The returned object must be released after use, by calling the [release] method.
jni$_.JList<jni$_.JString?>? get entryValues => _id_entryValues.get(
this,
const jni$_.JListNullableType<jni$_.JString?>(jni$_.JStringNullableType()),
);
/// from: `public java.util.List<java.lang.String> entryValues`
/// The returned object must be released after use, by calling the [release] method.
set entryValues(jni$_.JList<jni$_.JString?>? value) => _id_entryValues.set(
this,
const jni$_.JListNullableType<jni$_.JString?>(jni$_.JStringNullableType()),
value,
);
static final _id_summary = _class.instanceFieldId(
r'summary',
r'Ljava/lang/String;',
);
/// from: `public java.lang.String summary`
/// The returned object must be released after use, by calling the [release] method.
jni$_.JString? get summary =>
_id_summary.get(this, const jni$_.JStringNullableType());
/// from: `public java.lang.String summary`
/// The returned object must be released after use, by calling the [release] method.
set summary(jni$_.JString? value) =>
_id_summary.set(this, const jni$_.JStringNullableType(), value);
static final _id_new$ = _class.constructorId(
r'(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;Ljava/util/List;Ljava/util/List;Ljava/lang/String;)V',
);
static final _new$ =
jni$_.ProtectedJniExtensions.lookup<
jni$_.NativeFunction<
jni$_.JniResult Function(
jni$_.Pointer<jni$_.Void>,
jni$_.JMethodIDPtr,
jni$_.VarArgs<
(
jni$_.Pointer<jni$_.Void>,
jni$_.Pointer<jni$_.Void>,
jni$_.Pointer<jni$_.Void>,
jni$_.Pointer<jni$_.Void>,
jni$_.Pointer<jni$_.Void>,
jni$_.Pointer<jni$_.Void>,
jni$_.Pointer<jni$_.Void>,
)
>,
)
>
>('globalEnv_NewObject')
.asFunction<
jni$_.JniResult Function(
jni$_.Pointer<jni$_.Void>,
jni$_.JMethodIDPtr,
jni$_.Pointer<jni$_.Void>,
jni$_.Pointer<jni$_.Void>,
jni$_.Pointer<jni$_.Void>,
jni$_.Pointer<jni$_.Void>,
jni$_.Pointer<jni$_.Void>,
jni$_.Pointer<jni$_.Void>,
jni$_.Pointer<jni$_.Void>,
)
>();
/// from: `public void <init>(java.lang.String string, java.lang.String string1, java.lang.String string2, java.lang.Object object, java.util.List<java.lang.String> list, java.util.List<java.lang.String> list1, java.lang.String string3)`
/// The returned object must be released after use, by calling the [release] method.
factory JPreferenceItem(
jni$_.JString? string,
jni$_.JString? string1,
jni$_.JString? string2,
jni$_.JObject? object,
jni$_.JList<jni$_.JString?>? list,
jni$_.JList<jni$_.JString?>? list1,
jni$_.JString? string3,
) {
final _$string = string?.reference ?? jni$_.jNullReference;
final _$string1 = string1?.reference ?? jni$_.jNullReference;
final _$string2 = string2?.reference ?? jni$_.jNullReference;
final _$object = object?.reference ?? jni$_.jNullReference;
final _$list = list?.reference ?? jni$_.jNullReference;
final _$list1 = list1?.reference ?? jni$_.jNullReference;
final _$string3 = string3?.reference ?? jni$_.jNullReference;
return JPreferenceItem.fromReference(
_new$(
_class.reference.pointer,
_id_new$ as jni$_.JMethodIDPtr,
_$string.pointer,
_$string1.pointer,
_$string2.pointer,
_$object.pointer,
_$list.pointer,
_$list1.pointer,
_$string3.pointer,
).reference,
);
}
}
final class $JPreferenceItem$NullableType
extends jni$_.JObjType<JPreferenceItem?> {
@jni$_.internal
const $JPreferenceItem$NullableType();
@jni$_.internal
@core$_.override
String get signature => r'Ldev/k3vinb5/aniyomibridge/JPreferenceItem;';
@jni$_.internal
@core$_.override
JPreferenceItem? fromReference(jni$_.JReference reference) =>
reference.isNull ? null : JPreferenceItem.fromReference(reference);
@jni$_.internal
@core$_.override
jni$_.JObjType get superType => const jni$_.JObjectNullableType();
@jni$_.internal
@core$_.override
jni$_.JObjType<JPreferenceItem?> get nullableType => this;
@jni$_.internal
@core$_.override
final superCount = 1;
@core$_.override
int get hashCode => ($JPreferenceItem$NullableType).hashCode;
@core$_.override
bool operator ==(Object other) {
return other.runtimeType == ($JPreferenceItem$NullableType) &&
other is $JPreferenceItem$NullableType;
}
}
final class $JPreferenceItem$Type extends jni$_.JObjType<JPreferenceItem> {
@jni$_.internal
const $JPreferenceItem$Type();
@jni$_.internal
@core$_.override
String get signature => r'Ldev/k3vinb5/aniyomibridge/JPreferenceItem;';
@jni$_.internal
@core$_.override
JPreferenceItem fromReference(jni$_.JReference reference) =>
JPreferenceItem.fromReference(reference);
@jni$_.internal
@core$_.override
jni$_.JObjType get superType => const jni$_.JObjectNullableType();
@jni$_.internal
@core$_.override
jni$_.JObjType<JPreferenceItem?> get nullableType =>
const $JPreferenceItem$NullableType();
@jni$_.internal
@core$_.override
final superCount = 1;
@core$_.override
int get hashCode => ($JPreferenceItem$Type).hashCode;
@core$_.override
bool operator ==(Object other) {
return other.runtimeType == ($JPreferenceItem$Type) &&
other is $JPreferenceItem$Type;
}
}

View File

@@ -7,6 +7,7 @@ import 'package:path/path.dart' as path;
import 'jmodels/janiyomibridge.dart';
import 'jmodels/jpage.dart';
import 'jmodels/jpreferenceitem.dart';
import 'jmodels/jsanime.dart';
import 'jmodels/jschapter.dart';
import 'jmodels/jsepisode.dart';
@@ -21,7 +22,6 @@ class JniIsolate {
static late Isolate _jvmIsolate;
static late SendPort _jvmIsolateSendPort;
// Instance variables
late final Directory _supportDirectory;
late final String _supportDirectoryPath;
Future<(bool, SendPort)> initJniIsolate(
@@ -31,7 +31,6 @@ class JniIsolate {
String dyLibDir,
List<String> classPath,
) async {
_supportDirectory = supportDirectory;
_supportDirectoryPath = supportDirectory.path;
_jvmIsolate = await Isolate.spawn(
_jniWorkerEntryPoint, {
@@ -138,6 +137,7 @@ class JniIsolate {
_loadAnimeExtension(
jAniyomiBridge,
message.args['extensionUrl'] as String,
message.args['packageName'] as String,
);
mainIsolateSendPort.send(
JniIsolateResponse(id: message.id),
@@ -146,6 +146,7 @@ class JniIsolate {
_loadMangaExtension(
jAniyomiBridge,
message.args['extensionUrl'] as String,
message.args['packageName'] as String,
);
mainIsolateSendPort.send(
JniIsolateResponse(id: message.id),
@@ -153,8 +154,7 @@ class JniIsolate {
case JniIsolateMessageType.unloadAnimeExtension:
_unloadAnimeExtension(
jAniyomiBridge,
message.args['extensionName'] as String,
message.args['extensionVersion'] as String,
message.args['packageName'] as String,
);
mainIsolateSendPort.send(
JniIsolateResponse(id: message.id),
@@ -162,12 +162,48 @@ class JniIsolate {
case JniIsolateMessageType.unloadMangaExtension:
_unloadMangaExtension(
jAniyomiBridge,
message.args['extensionName'] as String,
message.args['extensionVersion'] as String,
message.args['packageName'] as String,
);
mainIsolateSendPort.send(
JniIsolateResponse(id: message.id),
);
case JniIsolateMessageType.getAnimePreferences:
List<JPreferenceItem> results = _getAnimePreferences(
jAniyomiBridge,
message.args['packageName'] as String,
);
mainIsolateSendPort.send(
JniIsolateResponse(id: message.id, results: results),
);
case JniIsolateMessageType.setAnimePreference:
_setAnimePreference(
jAniyomiBridge,
message.args['packageName'] as String,
message.args['key'] as String,
message.args['value'],
);
mainIsolateSendPort.send(
JniIsolateResponse(id: message.id),
);
case JniIsolateMessageType.setAnimePreferences:
_setAnimePreferences(
jAniyomiBridge,
message.args['packageName'] as String,
message.args['values'] as Map<String, dynamic>,
);
mainIsolateSendPort.send(
JniIsolateResponse(id: message.id),
);
case JniIsolateMessageType.getAnimeLoadedExtensions:
List<String> results = _getAnimeLoadedExtensions(jAniyomiBridge);
mainIsolateSendPort.send(
JniIsolateResponse(id: message.id, results: results),
);
case JniIsolateMessageType.getMangaLoadedExtensions:
List<String> results = _getMangaLoadedExtensions(jAniyomiBridge);
mainIsolateSendPort.send(
JniIsolateResponse(id: message.id, results: results),
);
}
} catch (e, stackTrace) {
mainIsolateSendPort.send(
@@ -190,7 +226,7 @@ class JniIsolate {
}
}
dispose() {
void dispose() {
_jvmIsolate.kill(priority: Isolate.immediate);
}
@@ -242,14 +278,14 @@ class JniIsolate {
}
List<JSChapter> _getChapterList(JAniyomiBridge jAniyomiBridge, JSManga sManga, String source) {
JList<JObject?>? episodeList = jAniyomiBridge.getEpisodeList(
JList<JObject?>? chapterList = jAniyomiBridge.getChapterList(
sManga,
JString.fromString(source),
);
if (episodeList == null) {
if (chapterList == null) {
return [];
}
return episodeList
return chapterList
.cast<JObject?>()
.where(_jObjIsNotNull)
.map((jObj) => jObj!.as<JSChapter>(JSChapter.type))
@@ -272,58 +308,125 @@ class JniIsolate {
}
List<JPage> _getPageList(JAniyomiBridge jAniyomiBridge, JSChapter sChapter, String source) {
JList<JObject?>? videoList = jAniyomiBridge.getVideoList(
JList<JObject?>? pageList = jAniyomiBridge.getPageList(
sChapter,
JString.fromString(source),
);
if (videoList == null) {
if (pageList == null) {
return [];
}
return videoList
return pageList
.cast<JObject?>()
.where(_jObjIsNotNull)
.map((jObj) => jObj!.as<JPage>(JPage.type))
.toList();
}
void _loadAnimeExtension(JAniyomiBridge jAniyomiBridge, String extensionUrl) {
void _loadAnimeExtension(JAniyomiBridge jAniyomiBridge, String extensionUrl, String packageName) {
final repoName = _resolveRepoNameFromUrl(extensionUrl);
jAniyomiBridge.loadAnimeExtension(
JString.fromString(extensionUrl),
JString.fromString(
path.join(_supportDirectoryPath, _aniyomiBridgeAnimeExtensionsDir),
path.join(_supportDirectoryPath, _aniyomiBridgeAnimeExtensionsDir, repoName),
),
JString.fromString(packageName),
);
}
void _unloadAnimeExtension(JAniyomiBridge jAniyomiBridge, String extensionName, String extensionVersion) {
void _unloadAnimeExtension(JAniyomiBridge jAniyomiBridge, String packageName) {
jAniyomiBridge.unloadAnimeExtension(
JString.fromString(extensionName),
JString.fromString(extensionVersion),
JString.fromString(
path.join(_supportDirectoryPath, _aniyomiBridgeAnimeExtensionsDir),
),
JString.fromString(packageName),
);
}
void _loadMangaExtension(JAniyomiBridge jAniyomiBridge, String extensionUrl) {
void _loadMangaExtension(JAniyomiBridge jAniyomiBridge, String extensionUrl, String packageName) {
final repoName = _resolveRepoNameFromUrl(extensionUrl);
jAniyomiBridge.loadMangaExtension(
JString.fromString(extensionUrl),
JString.fromString(
path.join(_supportDirectoryPath, _aniyomiBridgeMangaExtensionsDir),
path.join(_supportDirectoryPath, _aniyomiBridgeMangaExtensionsDir, repoName),
),
JString.fromString(packageName),
);
}
void _unloadMangaExtension(JAniyomiBridge jAniyomiBridge, String extensionName, String extensionVersion) {
void _unloadMangaExtension(JAniyomiBridge jAniyomiBridge, String packageName) {
jAniyomiBridge.unloadMangaExtension(
JString.fromString(extensionName),
JString.fromString(extensionVersion),
JString.fromString(
path.join(_supportDirectoryPath, _aniyomiBridgeMangaExtensionsDir),
),
JString.fromString(packageName),
);
}
/// Resolves a repo name from an extension URL to use as a subdirectory.
/// Extracts the hostname and sanitizes it for filesystem use.
String _resolveRepoNameFromUrl(String extensionUrl) {
try {
final uri = Uri.parse(extensionUrl);
final host = uri.host;
if (host.isNotEmpty) {
// Sanitize: replace non-alphanumeric with underscore
return host.replaceAll(RegExp(r'[^a-zA-Z0-9._-]'), '_');
}
} catch (_) {
// Fall through to default
}
return 'unknown';
}
List<JPreferenceItem> _getAnimePreferences(JAniyomiBridge jAniyomiBridge, String packageName) {
JList<JObject?>? prefs = jAniyomiBridge.getAnimePreferences(JString.fromString(packageName));
if (prefs == null) return [];
return prefs
.cast<JObject?>()
.where(_jObjIsNotNull)
.map((jObj) => jObj!.as<JPreferenceItem>(JPreferenceItem.type))
.toList();
}
void _setAnimePreference(JAniyomiBridge jAniyomiBridge, String packageName, String key, dynamic value) {
JObject jValue;
if (value is bool) {
jValue = JBoolean(value);
} else if (value is String) {
jValue = JString.fromString(value);
} else if (value is Set || value is List) {
// Java side splits String by ";" for MultiSelectListPreference
jValue = JString.fromString(value.join(';'));
} else {
throw UnsupportedError('Unsupported preference value type: ${value.runtimeType}');
}
jAniyomiBridge.setAnimePreference(
JString.fromString(packageName),
JString.fromString(key),
jValue,
);
}
void _setAnimePreferences(JAniyomiBridge jAniyomiBridge, String packageName, Map<String, dynamic> values) {
for (final entry in values.entries) {
_setAnimePreference(jAniyomiBridge, packageName, entry.key, entry.value);
}
}
List<String> _getAnimeLoadedExtensions(JAniyomiBridge jAniyomiBridge) {
JList<JObject?>? loaded = jAniyomiBridge.getAnimeLoadedExtensions();
if (loaded == null) return [];
return loaded
.cast<JObject?>()
.where(_jObjIsNotNull)
.map((jObj) => (jObj!.as<JString>(JString.type)).toDartString())
.toList();
}
List<String> _getMangaLoadedExtensions(JAniyomiBridge jAniyomiBridge) {
JList<JObject?>? loaded = jAniyomiBridge.getMangaLoadedExtensions();
if (loaded == null) return [];
return loaded
.cast<JObject?>()
.where(_jObjIsNotNull)
.map((jObj) => (jObj!.as<JString>(JString.type)).toDartString())
.toList();
}
bool Function(JObject? jObj) get _jObjIsNotNull =>
(jObj) => jObj != null;
}

View File

@@ -32,4 +32,9 @@ enum JniIsolateMessageType {
loadMangaExtension,
unloadAnimeExtension,
unloadMangaExtension,
getAnimePreferences,
setAnimePreference,
setAnimePreferences,
getAnimeLoadedExtensions,
getMangaLoadedExtensions,
}

View File

@@ -1,6 +1,7 @@
class Extension {
final String name;
final String version;
final String packageName;
Extension({required this.name, required this.version});
Extension({required this.name, required this.version, required this.packageName});
}