Refactor Dockerfile for Bun and update sidebar structure; enhance documentation and styling
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
FROM node:20-alpine AS build
|
||||
FROM oven/bun:alpine AS build
|
||||
WORKDIR /app
|
||||
COPY website/package*.json ./
|
||||
RUN npm ci
|
||||
COPY website/package.json website/bun.lock ./
|
||||
RUN bun install --frozen-lockfile
|
||||
COPY website/ ./
|
||||
RUN npm run build
|
||||
RUN bun run build
|
||||
|
||||
FROM nginx:alpine
|
||||
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
16876
reports/2026-05-04_01-59-09/report.json
Normal file
16876
reports/2026-05-04_01-59-09/report.json
Normal file
File diff suppressed because one or more lines are too long
290
scripts/generate_extension_health_report.py
Executable file
290
scripts/generate_extension_health_report.py
Executable file
@@ -0,0 +1,290 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate a Docusaurus documentation page from an Aniyomi extension batch test JSON report.
|
||||
|
||||
Usage:
|
||||
python3 scripts/generate_extension_health_report.py <path/to/report.json>
|
||||
|
||||
The script reads the report JSON and writes a formatted Markdown file to:
|
||||
website/docs/extensions-compatibility.md
|
||||
|
||||
Example:
|
||||
python3 scripts/generate_extension_health_report.py reports/2026-05-04_01-59-09/report.json
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
from collections import Counter, defaultdict
|
||||
import statistics
|
||||
|
||||
|
||||
def fmt_duration(ms):
|
||||
"""Format a duration in milliseconds to a human-readable string."""
|
||||
if ms < 0:
|
||||
return '—'
|
||||
if ms < 1000:
|
||||
return f"{ms}ms"
|
||||
return f"{ms / 1000:.1f}s"
|
||||
|
||||
|
||||
def extract_short_error(error_msg):
|
||||
"""Extract a concise error description from a full stack trace."""
|
||||
if not error_msg:
|
||||
return '—'
|
||||
lines = error_msg.strip().split('\n')
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if 'UnknownHostException' in line:
|
||||
return 'UnknownHostException'
|
||||
if 'HttpException: HTTP error' in line:
|
||||
return line.split('HttpException: ')[1][:30]
|
||||
if 'ConnectException: Connection refused' in line:
|
||||
return 'Connection refused'
|
||||
if 'ConnectException' in line:
|
||||
return 'ConnectException'
|
||||
if 'CloudflareBypassException' in line or 'Failed to bypass Cloudflare' in line:
|
||||
return 'Cloudflare bypass failed'
|
||||
if 'SSL' in line or 'Certificate' in line:
|
||||
return 'SSL/TLS error'
|
||||
if 'SocketTimeoutException' in line or 'timeout' in line.lower():
|
||||
return 'Timeout'
|
||||
if 'IllegalArgumentException' in line:
|
||||
if 'extension settings' in line.lower():
|
||||
return 'Missing settings'
|
||||
return 'IllegalArgumentException'
|
||||
if 'NullPointerException' in line:
|
||||
return 'NullPointerException'
|
||||
if 'NoSuchMethodError' in line:
|
||||
return 'NoSuchMethodError'
|
||||
if 'AbstractMethodError' in line:
|
||||
return 'AbstractMethodError'
|
||||
if 'Episode list not found' in line:
|
||||
return 'Episode list not found'
|
||||
if 'JSON token' in line or 'JSON input' in line:
|
||||
return 'JSON parsing error'
|
||||
if 'IndexOutOfBoundsException' in line:
|
||||
return 'IndexOutOfBoundsException'
|
||||
if 'IOException' in line:
|
||||
return 'IOException'
|
||||
# Fallback: first meaningful line
|
||||
for line in lines:
|
||||
if line and not line.startswith('at ') and not line.startswith('\tat '):
|
||||
return line[:60]
|
||||
return 'See error details'
|
||||
|
||||
|
||||
def generate_report(json_path, output_path):
|
||||
"""Generate the Markdown report from a JSON batch test result."""
|
||||
with open(json_path) as f:
|
||||
data = json.load(f)
|
||||
|
||||
meta = data['meta']
|
||||
summary = data['summary']
|
||||
results = data['results']
|
||||
|
||||
# Repository stats
|
||||
repo_pass = Counter(r['repoId'] for r in results if r['overallStatus'] == 'PASS')
|
||||
repo_fail = Counter(r['repoId'] for r in results if r['overallStatus'] == 'FAIL')
|
||||
|
||||
# Stage stats
|
||||
stage_stats = {}
|
||||
for stage in ['load', 'search', 'episodes', 'videos']:
|
||||
passes = sum(1 for r in results if r[stage]['status'] == 'PASS')
|
||||
fails = sum(1 for r in results if r[stage]['status'] == 'FAIL')
|
||||
pendings = sum(1 for r in results if r[stage]['status'] == 'PENDING')
|
||||
durations = [r[stage]['durationMs'] for r in results if r[stage]['durationMs'] >= 0]
|
||||
stage_stats[stage] = {
|
||||
'pass': passes,
|
||||
'fail': fails,
|
||||
'pending': pendings,
|
||||
'median': statistics.median(durations) if durations else 0,
|
||||
'max': max(durations) if durations else 0,
|
||||
'min': min(durations) if durations else 0,
|
||||
}
|
||||
|
||||
# Working extensions (all that passed every stage)
|
||||
working_extensions = []
|
||||
for r in results:
|
||||
if r['overallStatus'] == 'PASS':
|
||||
total = sum(r[stage]['durationMs'] for stage in ['load', 'search', 'episodes', 'videos'])
|
||||
working_extensions.append({
|
||||
'name': r['name'],
|
||||
'repo': r['repoId'],
|
||||
'lang': r['lang'],
|
||||
'total': total,
|
||||
'load': r['load']['durationMs'],
|
||||
'search': r['search']['durationMs'],
|
||||
'episodes': r['episodes']['durationMs'],
|
||||
'videos': r['videos']['durationMs'],
|
||||
'search_results': r['searchResultCount'],
|
||||
'episode_results': r['episodeResultCount'],
|
||||
'video_results': r['videoResultCount'],
|
||||
})
|
||||
working_extensions.sort(key=lambda x: x['total'], reverse=True)
|
||||
|
||||
# Failures grouped by stage
|
||||
failures_by_stage = defaultdict(list)
|
||||
for r in results:
|
||||
if r['overallStatus'] == 'FAIL':
|
||||
failures_by_stage[r['failedAtStage']].append(r)
|
||||
|
||||
# Build markdown
|
||||
md = f"""---
|
||||
title: Extension Compatibility
|
||||
description: Automated batch test results for Aniyomi extensions ({meta['timestamp'][:10]})
|
||||
---
|
||||
|
||||
# Extension Compatibility
|
||||
|
||||
This report presents the results of an automated batch test run against **{summary['total']}** Aniyomi extensions across the **yuzono** and **kohiden** repositories. The test validates each extension through four stages: loading, searching, episode listing, and video extraction.
|
||||
|
||||
:::info Test Methodology
|
||||
Each extension is tested with the query **`{meta['query']}`** using **{meta['concurrency']}** concurrent threads. A failure at any stage prevents subsequent stages from running. Results depend on network conditions, geo-location, and the current state of third-party sites.
|
||||
:::
|
||||
|
||||
## Test Configuration
|
||||
|
||||
| Parameter | Value |
|
||||
|-----------|-------|
|
||||
| **Timestamp** | `{meta['timestamp']}` |
|
||||
| **Query** | `{meta['query']}` |
|
||||
| **Concurrency** | {meta['concurrency']} threads |
|
||||
| **Duration** | {fmt_duration(meta['totalDurationMs'])} |
|
||||
| **Repositories** | {', '.join(meta['repos'])} |
|
||||
| **Total Extensions** | {summary['total']} |
|
||||
|
||||
## Overall Summary
|
||||
|
||||
| Metric | Count | Percentage |
|
||||
|--------|-------|------------|
|
||||
| **Pass** | {summary['pass']} | {summary['pass'] / summary['total'] * 100:.1f}% |
|
||||
| **Fail** | {summary['fail']} | {summary['fail'] / summary['total'] * 100:.1f}% |
|
||||
| **Skip** | {summary['skip']} | {summary['skip'] / summary['total'] * 100:.1f}% |
|
||||
| **Total** | {summary['total']} | 100.0% |
|
||||
|
||||
### By Repository
|
||||
|
||||
| Repository | Total | Pass | Fail | Pass Rate |
|
||||
|------------|-------|------|------|-----------|
|
||||
| yuzono | {repo_pass['yuzono'] + repo_fail['yuzono']} | {repo_pass['yuzono']} | {repo_fail['yuzono']} | {repo_pass['yuzono'] / (repo_pass['yuzono'] + repo_fail['yuzono']) * 100:.1f}% |
|
||||
| kohiden | {repo_pass['kohiden'] + repo_fail['kohiden']} | {repo_pass['kohiden']} | {repo_fail['kohiden']} | {repo_pass['kohiden'] / (repo_pass['kohiden'] + repo_fail['kohiden']) * 100:.1f}% |
|
||||
|
||||
## Working Extensions
|
||||
|
||||
The {len(working_extensions)} extensions that passed all four stages, sorted by total test duration:
|
||||
|
||||
| Extension | Repository | Language | Load | Search | Episodes | Videos | Total | Results (S/E/V) |
|
||||
|-----------|------------|----------|------|--------|----------|--------|-------|-----------------|
|
||||
"""
|
||||
for ext in working_extensions:
|
||||
md += (f"| {ext['name']} | {ext['repo']} | {ext['lang']} | {fmt_duration(ext['load'])} | "
|
||||
f"{fmt_duration(ext['search'])} | {fmt_duration(ext['episodes'])} | "
|
||||
f"{fmt_duration(ext['videos'])} | {fmt_duration(ext['total'])} | "
|
||||
f"{ext['search_results']}/{ext['episode_results']}/{ext['video_results']} |\n")
|
||||
|
||||
md += f"""
|
||||
## Stage Breakdown
|
||||
|
||||
Each extension progresses through four stages. The tables below show how many extensions passed, failed, or never reached each stage.
|
||||
|
||||
| Stage | Pass | Fail | Pending | Median Time | Max Time |
|
||||
|-------|------|------|---------|-------------|----------|
|
||||
| Load | {stage_stats['load']['pass']} | {stage_stats['load']['fail']} | {stage_stats['load']['pending']} | {fmt_duration(int(stage_stats['load']['median']))} | {fmt_duration(stage_stats['load']['max'])} |
|
||||
| Search | {stage_stats['search']['pass']} | {stage_stats['search']['fail']} | {stage_stats['search']['pending']} | {fmt_duration(int(stage_stats['search']['median']))} | {fmt_duration(stage_stats['search']['max'])} |
|
||||
| Episodes | {stage_stats['episodes']['pass']} | {stage_stats['episodes']['fail']} | {stage_stats['episodes']['pending']} | {fmt_duration(int(stage_stats['episodes']['median']))} | {fmt_duration(stage_stats['episodes']['max'])} |
|
||||
| Videos | {stage_stats['videos']['pass']} | {stage_stats['videos']['fail']} | {stage_stats['videos']['pending']} | {fmt_duration(int(stage_stats['videos']['median']))} | {fmt_duration(stage_stats['videos']['max'])} |
|
||||
|
||||
### Failure Distribution
|
||||
|
||||
| Stage | Failures | Percentage of Total |
|
||||
|-------|----------|---------------------|
|
||||
| Load | {summary['failAtLoad']} | {summary['failAtLoad'] / summary['total'] * 100:.1f}% |
|
||||
| Search | {summary['failAtSearch']} | {summary['failAtSearch'] / summary['total'] * 100:.1f}% |
|
||||
| Episodes | {summary['failAtEpisodes']} | {summary['failAtEpisodes'] / summary['total'] * 100:.1f}% |
|
||||
| Videos | {summary['failAtVideos']} | {summary['failAtVideos'] / summary['total'] * 100:.1f}% |
|
||||
|
||||
## Failures by Stage
|
||||
|
||||
:::danger Load Failures ({len(failures_by_stage.get('load', []))})
|
||||
Extensions that failed to initialize.
|
||||
:::
|
||||
|
||||
| Extension | Repository | Error |
|
||||
|-----------|------------|-------|
|
||||
"""
|
||||
for r in failures_by_stage.get('load', []):
|
||||
md += f"| {r['name']} | {r['repoId']} | {extract_short_error(r.get('errorMessage', ''))} |\n"
|
||||
|
||||
md += f"""
|
||||
:::danger Search Failures ({len(failures_by_stage.get('search', []))})
|
||||
Extensions that loaded successfully but failed to return search results.
|
||||
:::
|
||||
|
||||
| Extension | Repository | Error |
|
||||
|-----------|------------|-------|
|
||||
"""
|
||||
for r in failures_by_stage.get('search', [])[:50]:
|
||||
md += f"| {r['name']} | {r['repoId']} | {extract_short_error(r.get('errorMessage', ''))} |\n"
|
||||
if len(failures_by_stage.get('search', [])) > 50:
|
||||
md += f"| ... | ... | *and {len(failures_by_stage['search']) - 50} more* |\n"
|
||||
|
||||
md += f"""
|
||||
:::caution Episode Failures ({len(failures_by_stage.get('episodes', []))})
|
||||
Extensions that passed search but failed to retrieve episode lists.
|
||||
:::
|
||||
|
||||
| Extension | Repository | Error |
|
||||
|-----------|------------|-------|
|
||||
"""
|
||||
for r in failures_by_stage.get('episodes', []):
|
||||
md += f"| {r['name']} | {r['repoId']} | {extract_short_error(r.get('errorMessage', ''))} |\n"
|
||||
|
||||
md += f"""
|
||||
:::caution Video Failures ({len(failures_by_stage.get('videos', []))})
|
||||
Extensions that passed episodes but failed to extract video URLs.
|
||||
:::
|
||||
|
||||
| Extension | Repository | Error |
|
||||
|-----------|------------|-------|
|
||||
"""
|
||||
for r in failures_by_stage.get('videos', []):
|
||||
md += f"| {r['name']} | {r['repoId']} | {extract_short_error(r.get('errorMessage', ''))} |\n"
|
||||
|
||||
md += """
|
||||
## Notes
|
||||
|
||||
- **Test Environment**: Results are highly dependent on the test runner's network conditions, DNS resolution, and geographic location.
|
||||
- **Cloudflare**: Many failures are due to Cloudflare protection. These extensions may work fine in the actual Unyo app with proper WebView support.
|
||||
- **Configuration Required**: Some extensions (e.g., Google Drive, Jellyfin) require user settings that were not provided during the automated test.
|
||||
- **Site Changes**: Extensions relying on third-party websites may break when those sites change their layout or domain.
|
||||
- **API Mismatches**: Errors like `NoSuchMethodError` or `AbstractMethodError` indicate the extension was built against a different version of the Aniyomi/Tachiyomi API than what the bridge provides.
|
||||
|
||||
*Report generated automatically from batch test results.*
|
||||
"""
|
||||
|
||||
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||||
with open(output_path, 'w') as f:
|
||||
f.write(md)
|
||||
|
||||
print(f"Generated {output_path}")
|
||||
print(f"Total lines: {len(md.splitlines())}")
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python3 generate_extension_health_report.py <path/to/report.json>")
|
||||
sys.exit(1)
|
||||
|
||||
json_path = sys.argv[1]
|
||||
if not os.path.exists(json_path):
|
||||
print(f"Error: File not found: {json_path}")
|
||||
sys.exit(1)
|
||||
|
||||
# Default output path: website/docs/extensions-compatibility.md
|
||||
output_path = os.path.join('website', 'docs', 'extensions-compatibility.md')
|
||||
generate_report(json_path, output_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
749
tools.yaml
Normal file
749
tools.yaml
Normal file
@@ -0,0 +1,749 @@
|
||||
---
|
||||
version: v1.2
|
||||
tools:
|
||||
## Access Groups
|
||||
## An access group is the equivalent of an Endpoint Group in Portainer.
|
||||
## ------------------------------------------------------------
|
||||
- name: listAccessGroups
|
||||
description: List all available access groups
|
||||
annotations:
|
||||
title: List Access Groups
|
||||
readOnlyHint: true
|
||||
destructiveHint: false
|
||||
idempotentHint: true
|
||||
openWorldHint: false
|
||||
- name: createAccessGroup
|
||||
description: Create a new access group. Use access groups when you want to define
|
||||
accesses on more than one environment. Otherwise, define the accesses on
|
||||
the environment level.
|
||||
parameters:
|
||||
- name: name
|
||||
description: The name of the access group
|
||||
type: string
|
||||
required: true
|
||||
- name: environmentIds
|
||||
description: "The IDs of the environments that are part of the access group.
|
||||
Must include all the environment IDs that are part of the group - this
|
||||
includes new environments and the existing environments that are
|
||||
already associated with the group. Example: [1, 2, 3]"
|
||||
type: array
|
||||
items:
|
||||
type: number
|
||||
annotations:
|
||||
title: Create Access Group
|
||||
readOnlyHint: false
|
||||
destructiveHint: false
|
||||
idempotentHint: false
|
||||
openWorldHint: false
|
||||
- name: updateAccessGroupName
|
||||
description: Update the name of an existing access group.
|
||||
parameters:
|
||||
- name: id
|
||||
description: The ID of the access group to update
|
||||
type: number
|
||||
required: true
|
||||
- name: name
|
||||
description: The name of the access group
|
||||
type: string
|
||||
required: true
|
||||
annotations:
|
||||
title: Update Access Group Name
|
||||
readOnlyHint: false
|
||||
destructiveHint: false
|
||||
idempotentHint: true
|
||||
openWorldHint: false
|
||||
- name: updateAccessGroupUserAccesses
|
||||
description: Update the user accesses of an existing access group.
|
||||
parameters:
|
||||
- name: id
|
||||
description: The ID of the access group to update
|
||||
type: number
|
||||
required: true
|
||||
- name: userAccesses
|
||||
description: "The user accesses that are associated with all the environments in
|
||||
the access group. The ID is the user ID of the user in Portainer.
|
||||
Example: [{id: 1, access: 'environment_administrator'}, {id: 2,
|
||||
access: 'standard_user'}]"
|
||||
type: array
|
||||
required: true
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
description: The ID of the user
|
||||
type: number
|
||||
access:
|
||||
description: The access level of the user. Can be environment_administrator,
|
||||
helpdesk_user, standard_user, readonly_user or operator_user
|
||||
type: string
|
||||
enum:
|
||||
- environment_administrator
|
||||
- helpdesk_user
|
||||
- standard_user
|
||||
- readonly_user
|
||||
- operator_user
|
||||
annotations:
|
||||
title: Update Access Group User Accesses
|
||||
readOnlyHint: false
|
||||
destructiveHint: false
|
||||
idempotentHint: true
|
||||
openWorldHint: false
|
||||
- name: updateAccessGroupTeamAccesses
|
||||
description: Update the team accesses of an existing access group.
|
||||
parameters:
|
||||
- name: id
|
||||
description: The ID of the access group to update
|
||||
type: number
|
||||
required: true
|
||||
- name: teamAccesses
|
||||
description: "The team accesses that are associated with all the environments in
|
||||
the access group. The ID is the team ID of the team in Portainer.
|
||||
Example: [{id: 1, access: 'environment_administrator'}, {id: 2,
|
||||
access: 'standard_user'}]"
|
||||
type: array
|
||||
required: true
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
description: The ID of the team
|
||||
type: number
|
||||
access:
|
||||
description: The access level of the team. Can be environment_administrator,
|
||||
helpdesk_user, standard_user, readonly_user or operator_user
|
||||
type: string
|
||||
enum:
|
||||
- environment_administrator
|
||||
- helpdesk_user
|
||||
- standard_user
|
||||
- readonly_user
|
||||
- operator_user
|
||||
annotations:
|
||||
title: Update Access Group Team Accesses
|
||||
readOnlyHint: false
|
||||
destructiveHint: false
|
||||
idempotentHint: true
|
||||
openWorldHint: false
|
||||
- name: addEnvironmentToAccessGroup
|
||||
description: Add an environment to an access group.
|
||||
parameters:
|
||||
- name: id
|
||||
description: The ID of the access group to update
|
||||
type: number
|
||||
required: true
|
||||
- name: environmentId
|
||||
description: The ID of the environment to add to the access group
|
||||
type: number
|
||||
required: true
|
||||
annotations:
|
||||
title: Add Environment To Access Group
|
||||
readOnlyHint: false
|
||||
destructiveHint: false
|
||||
idempotentHint: true
|
||||
openWorldHint: false
|
||||
- name: removeEnvironmentFromAccessGroup
|
||||
description: Remove an environment from an access group.
|
||||
parameters:
|
||||
- name: id
|
||||
description: The ID of the access group to update
|
||||
type: number
|
||||
required: true
|
||||
- name: environmentId
|
||||
description: The ID of the environment to remove from the access group
|
||||
type: number
|
||||
required: true
|
||||
annotations:
|
||||
title: Remove Environment From Access Group
|
||||
readOnlyHint: false
|
||||
destructiveHint: true
|
||||
idempotentHint: true
|
||||
openWorldHint: false
|
||||
## Environment
|
||||
## ------------------------------------------------------------
|
||||
- name: listEnvironments
|
||||
description: List all available environments
|
||||
annotations:
|
||||
title: List Environments
|
||||
readOnlyHint: true
|
||||
destructiveHint: false
|
||||
idempotentHint: true
|
||||
openWorldHint: false
|
||||
- name: updateEnvironmentTags
|
||||
description: Update the tags associated with an environment
|
||||
parameters:
|
||||
- name: id
|
||||
description: The ID of the environment to update
|
||||
type: number
|
||||
required: true
|
||||
- name: tagIds
|
||||
description: >-
|
||||
The IDs of the tags that are associated with the environment.
|
||||
Must include all the tag IDs that should be associated with the environment - this includes new tags and existing tags.
|
||||
Providing an empty array will remove all tags.
|
||||
Example: [1, 2, 3]
|
||||
type: array
|
||||
required: true
|
||||
items:
|
||||
type: number
|
||||
annotations:
|
||||
title: Update Environment Tags
|
||||
readOnlyHint: false
|
||||
destructiveHint: false
|
||||
idempotentHint: true
|
||||
openWorldHint: false
|
||||
- name: updateEnvironmentUserAccesses
|
||||
description: Update the user access policies of an environment
|
||||
parameters:
|
||||
- name: id
|
||||
description: The ID of the environment to update
|
||||
type: number
|
||||
required: true
|
||||
- name: userAccesses
|
||||
description: >-
|
||||
The user accesses that are associated with the environment.
|
||||
The ID is the user ID of the user in Portainer.
|
||||
Must include all the access policies for all users that should be associated with the environment.
|
||||
Providing an empty array will remove all user accesses.
|
||||
Example: [{id: 1, access: 'environment_administrator'}, {id: 2, access: 'standard_user'}]
|
||||
type: array
|
||||
required: true
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
description: The ID of the user
|
||||
type: number
|
||||
access:
|
||||
description: The access level of the user
|
||||
type: string
|
||||
enum:
|
||||
- environment_administrator
|
||||
- helpdesk_user
|
||||
- standard_user
|
||||
- readonly_user
|
||||
- operator_user
|
||||
annotations:
|
||||
title: Update Environment User Accesses
|
||||
readOnlyHint: false
|
||||
destructiveHint: false
|
||||
idempotentHint: true
|
||||
openWorldHint: false
|
||||
- name: updateEnvironmentTeamAccesses
|
||||
description: Update the team access policies of an environment
|
||||
parameters:
|
||||
- name: id
|
||||
description: The ID of the environment to update
|
||||
type: number
|
||||
required: true
|
||||
- name: teamAccesses
|
||||
description: >-
|
||||
The team accesses that are associated with the environment.
|
||||
The ID is the team ID of the team in Portainer.
|
||||
Must include all the access policies for all teams that should be associated with the environment.
|
||||
Providing an empty array will remove all team accesses.
|
||||
Example: [{id: 1, access: 'environment_administrator'}, {id: 2, access: 'standard_user'}]
|
||||
type: array
|
||||
required: true
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
description: The ID of the team
|
||||
type: number
|
||||
access:
|
||||
description: The access level of the team
|
||||
type: string
|
||||
enum:
|
||||
- environment_administrator
|
||||
- helpdesk_user
|
||||
- standard_user
|
||||
- readonly_user
|
||||
- operator_user
|
||||
annotations:
|
||||
title: Update Environment Team Accesses
|
||||
readOnlyHint: false
|
||||
destructiveHint: false
|
||||
idempotentHint: true
|
||||
openWorldHint: false
|
||||
## Environment Groups
|
||||
## An environment group is the equivalent of an Edge Group in Portainer.
|
||||
## ------------------------------------------------------------
|
||||
- name: createEnvironmentGroup
|
||||
description: Create a new environment group. Environment groups are the equivalent of Edge Groups in Portainer.
|
||||
parameters:
|
||||
- name: name
|
||||
description: The name of the environment group
|
||||
type: string
|
||||
required: true
|
||||
- name: environmentIds
|
||||
description: The IDs of the environments to add to the group
|
||||
type: array
|
||||
required: true
|
||||
items:
|
||||
type: number
|
||||
annotations:
|
||||
title: Create Environment Group
|
||||
readOnlyHint: false
|
||||
destructiveHint: false
|
||||
idempotentHint: false
|
||||
openWorldHint: false
|
||||
- name: listEnvironmentGroups
|
||||
description: List all available environment groups. Environment groups are the equivalent of Edge Groups in Portainer.
|
||||
annotations:
|
||||
title: List Environment Groups
|
||||
readOnlyHint: true
|
||||
destructiveHint: false
|
||||
idempotentHint: true
|
||||
openWorldHint: false
|
||||
- name: updateEnvironmentGroupName
|
||||
description: Update the name of an environment group. Environment groups are the equivalent of Edge Groups in Portainer.
|
||||
parameters:
|
||||
- name: id
|
||||
description: The ID of the environment group to update
|
||||
type: number
|
||||
required: true
|
||||
- name: name
|
||||
description: The new name for the environment group
|
||||
type: string
|
||||
required: true
|
||||
annotations:
|
||||
title: Update Environment Group Name
|
||||
readOnlyHint: false
|
||||
destructiveHint: false
|
||||
idempotentHint: true
|
||||
openWorldHint: false
|
||||
- name: updateEnvironmentGroupEnvironments
|
||||
description: Update the environments associated with an environment group. Environment groups are the equivalent of Edge Groups in Portainer.
|
||||
parameters:
|
||||
- name: id
|
||||
description: The ID of the environment group to update
|
||||
type: number
|
||||
required: true
|
||||
- name: environmentIds
|
||||
description: >-
|
||||
The IDs of the environments that should be part of the group.
|
||||
Must include all environment IDs that should be associated with the group.
|
||||
Providing an empty array will remove all environments from the group.
|
||||
Example: [1, 2, 3]
|
||||
type: array
|
||||
required: true
|
||||
items:
|
||||
type: number
|
||||
annotations:
|
||||
title: Update Environment Group Environments
|
||||
readOnlyHint: false
|
||||
destructiveHint: false
|
||||
idempotentHint: true
|
||||
openWorldHint: false
|
||||
- name: updateEnvironmentGroupTags
|
||||
description: Update the tags associated with an environment group. Environment groups are the equivalent of Edge Groups in Portainer.
|
||||
parameters:
|
||||
- name: id
|
||||
description: The ID of the environment group to update
|
||||
type: number
|
||||
required: true
|
||||
- name: tagIds
|
||||
description: >-
|
||||
The IDs of the tags that should be associated with the group.
|
||||
Must include all tag IDs that should be associated with the group.
|
||||
Providing an empty array will remove all tags from the group.
|
||||
Example: [1, 2, 3]
|
||||
type: array
|
||||
required: true
|
||||
items:
|
||||
type: number
|
||||
annotations:
|
||||
title: Update Environment Group Tags
|
||||
readOnlyHint: false
|
||||
destructiveHint: false
|
||||
idempotentHint: true
|
||||
openWorldHint: false
|
||||
## Settings
|
||||
## ------------------------------------------------------------
|
||||
- name: getSettings
|
||||
description: Get the settings of the Portainer instance
|
||||
annotations:
|
||||
title: Get Settings
|
||||
readOnlyHint: true
|
||||
destructiveHint: false
|
||||
idempotentHint: true
|
||||
openWorldHint: false
|
||||
## Stacks
|
||||
## ------------------------------------------------------------
|
||||
- name: listStacks
|
||||
description: List all available stacks
|
||||
annotations:
|
||||
title: List Stacks
|
||||
readOnlyHint: true
|
||||
destructiveHint: false
|
||||
idempotentHint: true
|
||||
openWorldHint: false
|
||||
- name: getStackFile
|
||||
description: Get the compose file for a specific stack ID
|
||||
parameters:
|
||||
- name: id
|
||||
description: The ID of the stack to get the compose file for
|
||||
type: number
|
||||
required: true
|
||||
annotations:
|
||||
title: Get Stack File
|
||||
readOnlyHint: true
|
||||
destructiveHint: false
|
||||
idempotentHint: true
|
||||
openWorldHint: false
|
||||
- name: createStack
|
||||
description: Create a new stack
|
||||
parameters:
|
||||
- name: name
|
||||
description: Name of the stack. Stack name must only consist of lowercase alpha
|
||||
characters, numbers, hyphens, or underscores as well as start with a
|
||||
lowercase character or number
|
||||
type: string
|
||||
required: true
|
||||
- name: file
|
||||
description: >-
|
||||
Content of the stack file. The file must be a valid
|
||||
docker-compose.yml file. example: services:
|
||||
web:
|
||||
image:nginx
|
||||
type: string
|
||||
required: true
|
||||
- name: environmentGroupIds
|
||||
description: "The IDs of the environment groups that the stack belongs to. Must
|
||||
include at least one environment group ID. Example: [1, 2, 3]"
|
||||
type: array
|
||||
required: true
|
||||
items:
|
||||
type: number
|
||||
annotations:
|
||||
title: Create Stack
|
||||
readOnlyHint: false
|
||||
destructiveHint: false
|
||||
idempotentHint: false
|
||||
openWorldHint: false
|
||||
- name: updateStack
|
||||
description: Update an existing stack
|
||||
parameters:
|
||||
- name: id
|
||||
description: The ID of the stack to update
|
||||
type: number
|
||||
required: true
|
||||
- name: file
|
||||
description: >-
|
||||
Content of the stack file. The file must be a valid
|
||||
docker-compose.yml file. example: version: 3
|
||||
services:
|
||||
web:
|
||||
image:nginx
|
||||
type: string
|
||||
required: true
|
||||
- name: environmentGroupIds
|
||||
description: "The IDs of the environment groups that the stack belongs to. Must
|
||||
include at least one environment group ID. Example: [1, 2, 3]"
|
||||
type: array
|
||||
required: true
|
||||
items:
|
||||
type: number
|
||||
annotations:
|
||||
title: Update Stack
|
||||
readOnlyHint: false
|
||||
destructiveHint: false
|
||||
idempotentHint: true
|
||||
openWorldHint: false
|
||||
## Tags
|
||||
## ------------------------------------------------------------
|
||||
- name: createEnvironmentTag
|
||||
description: Create a new environment tag
|
||||
parameters:
|
||||
- name: name
|
||||
description: The name of the tag
|
||||
type: string
|
||||
required: true
|
||||
annotations:
|
||||
title: Create Environment Tag
|
||||
readOnlyHint: false
|
||||
destructiveHint: false
|
||||
idempotentHint: false
|
||||
openWorldHint: false
|
||||
- name: listEnvironmentTags
|
||||
description: List all available environment tags
|
||||
annotations:
|
||||
title: List Environment Tags
|
||||
readOnlyHint: true
|
||||
destructiveHint: false
|
||||
idempotentHint: true
|
||||
openWorldHint: false
|
||||
## Teams
|
||||
## ------------------------------------------------------------
|
||||
- name: createTeam
|
||||
description: Create a new team
|
||||
parameters:
|
||||
- name: name
|
||||
description: The name of the team
|
||||
type: string
|
||||
required: true
|
||||
annotations:
|
||||
title: Create Team
|
||||
readOnlyHint: false
|
||||
destructiveHint: false
|
||||
idempotentHint: false
|
||||
openWorldHint: false
|
||||
- name: listTeams
|
||||
description: List all available teams
|
||||
annotations:
|
||||
title: List Teams
|
||||
readOnlyHint: true
|
||||
destructiveHint: false
|
||||
idempotentHint: true
|
||||
openWorldHint: false
|
||||
- name: updateTeamName
|
||||
description: Update the name of an existing team
|
||||
parameters:
|
||||
- name: id
|
||||
description: The ID of the team to update
|
||||
type: number
|
||||
required: true
|
||||
- name: name
|
||||
description: The new name of the team
|
||||
type: string
|
||||
required: true
|
||||
annotations:
|
||||
title: Update Team Name
|
||||
readOnlyHint: false
|
||||
destructiveHint: false
|
||||
idempotentHint: true
|
||||
openWorldHint: false
|
||||
- name: updateTeamMembers
|
||||
description: Update the members of an existing team
|
||||
parameters:
|
||||
- name: id
|
||||
description: The ID of the team to update
|
||||
type: number
|
||||
required: true
|
||||
- name: userIds
|
||||
description: "The IDs of the users that are part of the team. Must include all
|
||||
the user IDs that are part of the team - this includes new users and
|
||||
the existing users that are already associated with the team. Example:
|
||||
[1, 2, 3]"
|
||||
type: array
|
||||
required: true
|
||||
items:
|
||||
type: number
|
||||
annotations:
|
||||
title: Update Team Members
|
||||
readOnlyHint: false
|
||||
destructiveHint: false
|
||||
idempotentHint: true
|
||||
openWorldHint: false
|
||||
|
||||
## Users
|
||||
## ------------------------------------------------------------
|
||||
- name: listUsers
|
||||
description: List all available users
|
||||
annotations:
|
||||
title: List Users
|
||||
readOnlyHint: true
|
||||
destructiveHint: false
|
||||
idempotentHint: true
|
||||
openWorldHint: false
|
||||
- name: updateUserRole
|
||||
description: Update an existing user
|
||||
parameters:
|
||||
- name: id
|
||||
description: The ID of the user to update
|
||||
type: number
|
||||
required: true
|
||||
- name: role
|
||||
description: The role of the user. Can be admin, user or edge_admin
|
||||
type: string
|
||||
required: true
|
||||
enum:
|
||||
- admin
|
||||
- user
|
||||
- edge_admin
|
||||
annotations:
|
||||
title: Update User Role
|
||||
readOnlyHint: false
|
||||
destructiveHint: false
|
||||
idempotentHint: true
|
||||
openWorldHint: false
|
||||
|
||||
## Docker Proxy
|
||||
## ------------------------------------------------------------
|
||||
- name: dockerProxy
|
||||
description: Proxy Docker requests to a specific Portainer environment.
|
||||
This tool can be used with any Docker API operation as documented in the Docker Engine API specification (https://docs.docker.com/reference/api/engine/version/v1.48/).
|
||||
parameters:
|
||||
- name: environmentId
|
||||
description: The ID of the environment to proxy Docker requests to
|
||||
type: number
|
||||
required: true
|
||||
- name: method
|
||||
description: The HTTP method to use to proxy the Docker API operation
|
||||
type: string
|
||||
required: true
|
||||
enum:
|
||||
- GET
|
||||
- POST
|
||||
- PUT
|
||||
- DELETE
|
||||
- HEAD
|
||||
- name: dockerAPIPath
|
||||
description: "The route of the Docker API operation to proxy. Must include the leading slash. Example: /containers/json"
|
||||
type: string
|
||||
required: true
|
||||
- name: queryParams
|
||||
description: "The query parameters to include in the Docker API operation. Must be an array of key-value pairs.
|
||||
Example: [{key: 'all', value: 'true'}, {key: 'filter', value: 'dangling'}]"
|
||||
type: array
|
||||
required: false
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
description: The key of the query parameter
|
||||
value:
|
||||
type: string
|
||||
description: The value of the query parameter
|
||||
- name: headers
|
||||
description: "The headers to include in the Docker API operation. Must be an array of key-value pairs.
|
||||
Example: [{key: 'Content-Type', value: 'application/json'}]"
|
||||
type: array
|
||||
required: false
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
description: The key of the header
|
||||
value:
|
||||
type: string
|
||||
description: The value of the header
|
||||
- name: body
|
||||
description: "The body of the Docker API operation to proxy. Must be a JSON string.
|
||||
Example: {'Image': 'nginx:latest', 'Name': 'my-container'}"
|
||||
type: string
|
||||
required: false
|
||||
annotations:
|
||||
title: Docker Proxy
|
||||
readOnlyHint: true
|
||||
destructiveHint: true
|
||||
idempotentHint: true
|
||||
openWorldHint: false
|
||||
|
||||
## Kubernetes Proxy
|
||||
## ------------------------------------------------------------
|
||||
- name: kubernetesProxy
|
||||
description: Proxy Kubernetes requests to a specific Portainer environment.
|
||||
This tool can be used with any Kubernetes API operation as documented in the Kubernetes API specification (https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/).
|
||||
parameters:
|
||||
- name: environmentId
|
||||
description: The ID of the environment to proxy Kubernetes requests to
|
||||
type: number
|
||||
required: true
|
||||
- name: method
|
||||
description: The HTTP method to use to proxy the Kubernetes API operation
|
||||
type: string
|
||||
required: true
|
||||
enum:
|
||||
- GET
|
||||
- POST
|
||||
- PUT
|
||||
- DELETE
|
||||
- HEAD
|
||||
- name: kubernetesAPIPath
|
||||
description: "The route of the Kubernetes API operation to proxy. Must include the leading slash. Example: /api/v1/namespaces/default/pods"
|
||||
type: string
|
||||
required: true
|
||||
- name: queryParams
|
||||
description: "The query parameters to include in the Kubernetes API operation. Must be an array of key-value pairs.
|
||||
Example: [{key: 'watch', value: 'true'}, {key: 'fieldSelector', value: 'metadata.name=my-pod'}]"
|
||||
type: array
|
||||
required: false
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
description: The key of the query parameter
|
||||
value:
|
||||
type: string
|
||||
description: The value of the query parameter
|
||||
- name: headers
|
||||
description: "The headers to include in the Kubernetes API operation. Must be an array of key-value pairs.
|
||||
Example: [{key: 'Content-Type', value: 'application/json'}]"
|
||||
type: array
|
||||
required: false
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
description: The key of the header
|
||||
value:
|
||||
type: string
|
||||
description: The value of the header
|
||||
- name: body
|
||||
description: "The body of the Kubernetes API operation to proxy. Must be a JSON string.
|
||||
Example: {'apiVersion': 'v1', 'kind': 'Pod', 'metadata': {'name': 'my-pod'}}"
|
||||
type: string
|
||||
required: false
|
||||
annotations:
|
||||
title: Kubernetes Proxy
|
||||
readOnlyHint: true
|
||||
destructiveHint: true
|
||||
idempotentHint: true
|
||||
openWorldHint: false
|
||||
- name: getKubernetesResourceStripped
|
||||
description: >-
|
||||
Proxy GET requests to a specific Portainer environment for Kubernetes resources,
|
||||
and automatically strips verbose metadata fields (such as 'managedFields') from the API response
|
||||
to reduce its size. This tool is intended for retrieving Kubernetes resource
|
||||
information where a leaner payload is desired.
|
||||
This tool can be used with any GET Kubernetes API operation as documented
|
||||
in the Kubernetes API specification (https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/).
|
||||
For other methods (POST, PUT, DELETE, HEAD), use the 'kubernetesProxy' tool.
|
||||
parameters:
|
||||
- name: environmentId
|
||||
description: The ID of the environment to proxy Kubernetes GET requests to
|
||||
type: number
|
||||
required: true
|
||||
- name: kubernetesAPIPath
|
||||
description: "The route of the Kubernetes API GET operation to proxy. Must include the leading slash. Example: /api/v1/namespaces/default/pods"
|
||||
type: string
|
||||
required: true
|
||||
- name: queryParams
|
||||
description: "The query parameters to include in the Kubernetes API operation. Must be an array of key-value pairs.
|
||||
Example: [{key: 'watch', value: 'true'}, {key: 'fieldSelector', value: 'metadata.name=my-pod'}]"
|
||||
type: array
|
||||
required: false
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
description: The key of the query parameter
|
||||
value:
|
||||
type: string
|
||||
description: The value of the query parameter
|
||||
- name: headers
|
||||
description: "The headers to include in the Kubernetes API operation. Must be an array of key-value pairs.
|
||||
Example: [{key: 'Accept', value: 'application/json'}]"
|
||||
type: array
|
||||
required: false
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
description: The key of the header
|
||||
value:
|
||||
type: string
|
||||
description: The value of the header
|
||||
annotations:
|
||||
title: Get Kubernetes Resource (Stripped)
|
||||
readOnlyHint: true
|
||||
destructiveHint: false
|
||||
idempotentHint: true
|
||||
openWorldHint: false
|
||||
2637
website/bun.lock
Normal file
2637
website/bun.lock
Normal file
File diff suppressed because it is too large
Load Diff
312
website/docs/architecture.md
Normal file
312
website/docs/architecture.md
Normal file
@@ -0,0 +1,312 @@
|
||||
---
|
||||
title: Architecture
|
||||
---
|
||||
|
||||
# Unyo Architecture Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
Unyo is a Flutter-based cross-platform desktop application following **Clean Architecture** with a strict five-layer separation of concerns. State management is handled via the **BLoC/Cubit** pattern, dependency injection via **GetIt**, and cross-feature communication via a **Notifier** system built on RxDart.
|
||||
|
||||
---
|
||||
|
||||
## Layer Structure
|
||||
|
||||
```
|
||||
lib/
|
||||
├── presentation/ # UI: screens, widgets, dialogs, drawers, views
|
||||
├── application/ # State: cubits, states, effect mixin
|
||||
├── domain/ # Business entities and repository interfaces (pure Dart)
|
||||
├── data/ # Repository implementations, data models
|
||||
├── core/ # Infrastructure: DI, routing, services, theme
|
||||
└── main.dart # Entry point and bootstrap
|
||||
```
|
||||
|
||||
### Dependency Rules
|
||||
|
||||
```
|
||||
Presentation (lib/presentation/)
|
||||
└── depends on ──> Application (lib/application/)
|
||||
└── depends on ──> Domain (lib/domain/)
|
||||
↑ implements
|
||||
Data (lib/data/) ──────────────────────────────────┘
|
||||
|
||||
Infrastructure (lib/core/)
|
||||
└── used by all layers ──> Application, Data, Presentation
|
||||
```
|
||||
|
||||
| Layer | Directory | Responsibilities |
|
||||
|---|---|---|
|
||||
| **Presentation** | `lib/presentation/` | Screens, widgets, dialogs, drawers |
|
||||
| **Application** | `lib/application/` | Cubits, states, business logic orchestration |
|
||||
| **Domain** | `lib/domain/` | Entities, repository abstract interfaces |
|
||||
| **Data** | `lib/data/` | Repository implementations, API models |
|
||||
| **Infrastructure** | `lib/core/` | DI, routing, HTTP, theming, effects |
|
||||
|
||||
---
|
||||
|
||||
## Application Startup
|
||||
|
||||
The bootstrap sequence in `lib/main.dart` runs in a strict order because some services depend on Hive being initialized first.
|
||||
|
||||
```
|
||||
main()
|
||||
├── setupLocator() [register services, notifiers, repositories, cubits]
|
||||
├── Hive.init() + registerAdapters()
|
||||
├── setupLocatorAfterHiveInit() [register ExtensionRepositoryAniyomi]
|
||||
├── waitUntilReadyToShow(WindowOptions)
|
||||
└── runApp(EasyLocalization > ScreenUtilInit > MyApp)
|
||||
```
|
||||
|
||||
`MyApp` wraps `MaterialApp.router` in a `StreamBuilder` on `ThemeService.theme$`, so the entire app re-renders reactively when the theme changes.
|
||||
|
||||
---
|
||||
|
||||
## Dependency Injection
|
||||
|
||||
All services are registered in `lib/core/di/locator.dart` using a single `GetIt` instance (`sl`).
|
||||
|
||||
### Registration Types
|
||||
|
||||
| Type | Usage |
|
||||
|---|---|
|
||||
| `registerSingleton` | Created immediately, lives for the app lifetime |
|
||||
| `registerLazySingleton` | Created on first access, lives for the app lifetime |
|
||||
| `registerFactory` | New instance created on every `sl<T>()` call (used for Cubits) |
|
||||
|
||||
### Registered Services
|
||||
|
||||
| Service | Class | Type |
|
||||
|---|---|---|
|
||||
| Logger | `Logger` | Singleton |
|
||||
| HTTP Client | `HttpService` | Lazy Singleton |
|
||||
| GraphQL Client | `GraphQLService` | Lazy Singleton |
|
||||
| Effect Handler | `AppEffectHandler` | Lazy Singleton |
|
||||
| Color Extractor | `ColorImageService` | Lazy Singleton |
|
||||
| Extension Bridge | `AniyomiBridge` | Singleton |
|
||||
| Torrent | `TorrentService` | Singleton |
|
||||
| Settings | `SettingsService` | Singleton |
|
||||
| Theme | `ThemeService` | Singleton |
|
||||
|
||||
### Registered Repositories
|
||||
|
||||
| Repository | Purpose |
|
||||
|---|---|
|
||||
| `UserRepositoryLocal` | Local user persistence (Hive) |
|
||||
| `UserRepositoryAnilist` | AniList user API |
|
||||
| `AnimeRepositoryAnilist` | Anime data from AniList |
|
||||
| `MangaRepositoryAnilist` | Manga data from AniList |
|
||||
| `EpisodeRepositoryAnizip` | Episode metadata from AniZip |
|
||||
| `ExtensionRepositoryAniyomi` | Extension install/load/remove |
|
||||
|
||||
### Registered Cubits
|
||||
|
||||
All Cubits are registered as **factories** (new instance per screen):
|
||||
|
||||
`LoginCubit`, `HomeCubit`, `TabsCubit`, `AnimeCubit`, `MangaCubit`, `ExtensionsCubit`, `MediaListCubit`, `CalendarCubit`, `AnimeDetailsCubit`, `MangaDetailsCubit`, `SettingsCubit`, `AnimeAdvancedSearchCubit`, `MangaAdvancedSearchCubit`, `VideoCubit`
|
||||
|
||||
---
|
||||
|
||||
## State Management (BLoC/Cubit)
|
||||
|
||||
Each feature has a dedicated Cubit in `lib/application/cubits/`. Cubits hold immutable state objects (generated with `freezed`) and expose methods that either `emit` new state or trigger **effects**.
|
||||
|
||||
### Cubit → State → UI Flow
|
||||
|
||||
```
|
||||
User Action
|
||||
└──> Cubit Method
|
||||
├── emit(state.copyWith(...))
|
||||
│ └──> Freezed State
|
||||
│ └──> BlocBuilder rebuilds Widget
|
||||
└── EffectMixin
|
||||
└──> AppEffectHandler
|
||||
└──> navigation / dialog / snackbar
|
||||
```
|
||||
|
||||
### Effect System
|
||||
|
||||
Side effects (navigation, dialogs, snackbars) are handled declaratively via `EffectMixin` in `lib/application/cubits/effect_mixin.dart`. Cubits emit effects through `AppEffectHandler` rather than directly calling navigation or UI code, keeping business logic free of Flutter framework dependencies.
|
||||
|
||||
---
|
||||
|
||||
## Cross-Feature Communication (Notifiers)
|
||||
|
||||
Features communicate with each other through **Notifiers** — RxDart `BehaviorSubject` wrappers registered as lazy singletons in the DI container. This avoids tight coupling between Cubits.
|
||||
|
||||
```
|
||||
LoginCubit ──> UserNotifier (loggedUser)
|
||||
├──> HomeCubit
|
||||
├──> AnimeDetailsCubit
|
||||
└──> VideoCubit
|
||||
|
||||
AnimeNotifier ──> AnimeCubit
|
||||
└──> HomeCubit
|
||||
|
||||
ReloadNotifier ──> HomeCubit
|
||||
└──> AnimeCubit
|
||||
```
|
||||
|
||||
### All Registered Notifiers
|
||||
|
||||
| Notifier | Instance Name | Purpose |
|
||||
|---|---|---|
|
||||
| `UserNotifier` | `loggedUserNotifier` | Broadcasts the currently logged-in user |
|
||||
| `UserNotifier` | `newUserNotifier` | Broadcasts a newly created/selected user |
|
||||
| `MenuBarNotifier` | — | Sidebar/menu bar state |
|
||||
| `AnimeNotifier` | — | Current anime list data |
|
||||
| `MangaNotifier` | — | Current manga list data |
|
||||
| `AnimeGenresNotifier` | — | Anime genre filter state |
|
||||
| `MangaGenresNotifier` | — | Manga genre filter state |
|
||||
| `MediaListNotifier` | — | User's media list |
|
||||
| `VideoInfoNotifier` | — | Current video playback info |
|
||||
| `EpisodesInfoNotifier` | — | Episode metadata |
|
||||
| `EpisodesNotifier` | — | Episode list |
|
||||
| `MediaListEntryNotifier` | — | Single media list entry |
|
||||
| `ExtensionNotifier` | — | Active extension state |
|
||||
| `ReloadNotifier` | — | Triggers data reloads across features |
|
||||
|
||||
---
|
||||
|
||||
## Domain Layer
|
||||
|
||||
All business entities live in `lib/domain/entities/` as `freezed` sealed/union classes. They are pure Dart with no framework dependencies.
|
||||
|
||||
| Entity | File |
|
||||
|---|---|
|
||||
| `User` | `user.dart` |
|
||||
| `Anime` | `anime.dart` |
|
||||
| `AnimeDetails` | `anime_details.dart` |
|
||||
| `Manga` | `manga.dart` |
|
||||
| `MangaDetails` | `manga_details.dart` |
|
||||
| `MediaList` | `media_list.dart` |
|
||||
| `MediaListEntry` | `media_list_entry.dart` |
|
||||
| `Extension` | `extension.dart` |
|
||||
| `EpisodeInfo` | `episode_info.dart` |
|
||||
| `VideoInfo` | `video_info.dart` |
|
||||
| `Settings` | `settings.dart` |
|
||||
| `AiringEpisode` | `airing_episode.dart` |
|
||||
| `MediaCharacter` | `media_character.dart` |
|
||||
|
||||
---
|
||||
|
||||
## Data Layer
|
||||
|
||||
Repository implementations in `lib/data/repositories/` implement the abstract interfaces defined in the domain layer. Data models in `lib/data/models/` extend domain entities and add JSON serialization.
|
||||
|
||||
### Repository Pattern
|
||||
|
||||
```
|
||||
Cubit
|
||||
└──> calls interface ──> Abstract Repository (domain)
|
||||
↑ implements
|
||||
Data Repository (data) ────────────┘
|
||||
├──> HttpService / GraphQLService
|
||||
├──> Hive Box
|
||||
└──> AniyomiBridge
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Infrastructure Layer (`lib/core/`)
|
||||
|
||||
| Directory | Contents |
|
||||
|---|---|
|
||||
| `core/di/` | `locator.dart` — GetIt service registration |
|
||||
| `core/router/` | `AppRouter` — `auto_route` type-safe navigation |
|
||||
| `core/services/api/` | `HttpService`, `GraphQLService` |
|
||||
| `core/services/effects/` | `AppEffectHandler` |
|
||||
| `core/services/torrent/` | `TorrentService` (Go native library) |
|
||||
| `core/services/media/` | Media-related services |
|
||||
| `core/services/video/` | Video playback services |
|
||||
| `core/theme/` | `ThemeService`, `ColorImageService` |
|
||||
| `core/enums/` | Shared enums (e.g., `ExtensionType`) |
|
||||
| `core/notification/` | All Notifier classes |
|
||||
|
||||
### Native Integrations
|
||||
|
||||
| Integration | Purpose |
|
||||
|---|---|
|
||||
| `AniyomiBridge` (`k3vinb5_aniyomi_bridge`) | Runs Aniyomi/Tachiyomi extension APKs in a local JVM process |
|
||||
| `libmtorrentserver` (Go, `c-shared`) | Peer-to-peer torrent streaming compiled as a native shared library |
|
||||
| `fvp` / `video_player` | High-performance video playback via mdk-sdk |
|
||||
|
||||
---
|
||||
|
||||
## Presentation Layer
|
||||
|
||||
```
|
||||
lib/presentation/
|
||||
├── screens/ # Top-level @RoutePage() widgets (one per route)
|
||||
├── views/ # Sub-views composed inside screens
|
||||
├── widgets/ # Reusable UI components (styled buttons, text fields, etc.)
|
||||
├── dialogs/ # Modal dialogs
|
||||
└── drawers/ # Sidebar/navigation drawers
|
||||
```
|
||||
|
||||
Each screen uses `BlocProvider` to create its Cubit from the DI container and `BlocBuilder`/`BlocListener` to consume state.
|
||||
|
||||
---
|
||||
|
||||
## Code Generation
|
||||
|
||||
All generated files (`.g.dart`, `.freezed.dart`) are produced by running:
|
||||
|
||||
```bash
|
||||
flutter pub run build_runner build --delete-conflicting-outputs
|
||||
```
|
||||
|
||||
| Generator | Purpose |
|
||||
|---|---|
|
||||
| `freezed` | Immutable state classes and domain entities |
|
||||
| `json_serializable` | JSON serialization for data models |
|
||||
| `hive_ce_generator` | Hive type adapters (`hive_registrar.g.dart`) |
|
||||
| `auto_route_generator` | Type-safe route definitions |
|
||||
|
||||
---
|
||||
|
||||
## Full System Diagram
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Presentation │
|
||||
│ Screens / Views ──> Cubits │
|
||||
│ Widgets / Dialogs │
|
||||
└────────────────────────┬────────────────────────────────────┘
|
||||
│
|
||||
┌────────────────────────▼────────────────────────────────────┐
|
||||
│ Application │
|
||||
│ Cubits (BLoC) ──> Freezed States │
|
||||
│ EffectMixin ──> AppEffectHandler │
|
||||
└────────────────────────┬────────────────────────────────────┘
|
||||
│ calls interface
|
||||
┌────────────────────────▼────────────────────────────────────┐
|
||||
│ Domain │
|
||||
│ Entities (pure Dart) │
|
||||
│ Repository Interfaces │
|
||||
└────────────────────────┬────────────────────────────────────┘
|
||||
│ implements
|
||||
┌────────────────────────▼────────────────────────────────────┐
|
||||
│ Data │
|
||||
│ Repository Implementations │
|
||||
│ Data Models (JSON) ──> HttpService / GraphQLService │
|
||||
│ ──> Hive Box │
|
||||
│ ──> AniyomiBridge │
|
||||
└────────────────────────┬────────────────────────────────────┘
|
||||
│
|
||||
┌────────────────────────▼────────────────────────────────────┐
|
||||
│ Infrastructure │
|
||||
│ GetIt (sl) ──> injects Cubits & Repositories │
|
||||
│ AppRouter (auto_route) │
|
||||
│ ThemeService ──> reactive theme │
|
||||
│ Notifiers (RxDart) ──> cross-feature communication │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌────────────────────────▼────────────────────────────────────┐
|
||||
│ External │
|
||||
│ AniList GraphQL API │
|
||||
│ AniZip API │
|
||||
│ Extension Repository (JSON) │
|
||||
│ Local JVM Process (AniyomiBridge) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
486
website/docs/extensions-compatibility.md
Normal file
486
website/docs/extensions-compatibility.md
Normal file
@@ -0,0 +1,486 @@
|
||||
---
|
||||
title: Extension Compatibility
|
||||
description: Automated batch test results for Aniyomi extensions (2026-05-04)
|
||||
---
|
||||
|
||||
# Extension Compatibility
|
||||
|
||||
This report presents the results of an automated batch test run against **500** Aniyomi extensions across the **yuzono** and **kohiden** repositories. The test validates each extension through four stages: loading, searching, episode listing, and video extraction.
|
||||
|
||||
:::info Test Methodology
|
||||
Each extension is tested with the query **`Solo Leveling`** using **64** concurrent threads. A failure at any stage prevents subsequent stages from running. Results depend on network conditions, geo-location, and the current state of third-party sites.
|
||||
:::
|
||||
|
||||
## Test Configuration
|
||||
|
||||
| Parameter | Value |
|
||||
|-----------|-------|
|
||||
| **Timestamp** | `2026-05-04_01-59-09` |
|
||||
| **Query** | `Solo Leveling` |
|
||||
| **Concurrency** | 64 threads |
|
||||
| **Duration** | 171.4s |
|
||||
| **Repositories** | yuzono, kohiden |
|
||||
| **Total Extensions** | 500 |
|
||||
|
||||
## Overall Summary
|
||||
|
||||
| Metric | Count | Percentage |
|
||||
|--------|-------|------------|
|
||||
| **Pass** | 287 | 57.4% |
|
||||
| **Fail** | 213 | 42.6% |
|
||||
| **Skip** | 0 | 0.0% |
|
||||
| **Total** | 500 | 100.0% |
|
||||
|
||||
### By Repository
|
||||
|
||||
| Repository | Total | Pass | Fail | Pass Rate |
|
||||
|------------|-------|------|------|-----------|
|
||||
| yuzono | 260 | 146 | 114 | 56.2% |
|
||||
| kohiden | 240 | 141 | 99 | 58.8% |
|
||||
|
||||
## Working Extensions
|
||||
|
||||
The 287 extensions that passed all four stages, sorted by total test duration:
|
||||
|
||||
| Extension | Repository | Language | Load | Search | Episodes | Videos | Total | Results (S/E/V) |
|
||||
|-----------|------------|----------|------|--------|----------|--------|-------|-----------------|
|
||||
| Aniyomi: VerAnimes | kohiden | es | 1.7s | 226ms | 128ms | 123.0s | 125.0s | 3/13/0 |
|
||||
| Aniyomi: Jkanime | yuzono | es | 4.1s | 491ms | 429ms | 84.5s | 89.5s | 2/12/8 |
|
||||
| Aniyomi: AnimePlay | kohiden | pt-BR | 203ms | 1.4s | 923ms | 62.3s | 64.8s | 6/13/3 |
|
||||
| Aniyomi: FRAnime | yuzono | fr | 4.1s | 2.7s | 12ms | 30.4s | 37.2s | 4/13/0 |
|
||||
| Aniyomi: AnimeMovil | kohiden | es | 265ms | 651ms | 845ms | 32.1s | 33.9s | 10/3/2 |
|
||||
| Aniyomi: AllAnime | yuzono | en | 110ms | 552ms | 158ms | 33.0s | 33.8s | 2/13/17 |
|
||||
| Aniyomi: AnimeMovil | yuzono | es | 3.0s | 1.7s | 1.0s | 27.1s | 32.8s | 10/3/3 |
|
||||
| Aniyomi: Docchi | kohiden | pl | 591ms | 129ms | 425ms | 30.5s | 31.6s | 3/13/21 |
|
||||
| Aniyomi: BLZone | yuzono | en | 30.1s | 1.1s | — | — | 31.1s | 0/-1/-1 |
|
||||
| Aniyomi: FrAnime | kohiden | fr | 258ms | 264ms | 4ms | 30.3s | 30.8s | 4/13/0 |
|
||||
| Aniyomi: Gnula | yuzono | es | 6.4s | 349ms | 145ms | 23.6s | 30.5s | 24/1/3 |
|
||||
| Aniyomi: AnimeFenix | yuzono | es | 28.7s | 878ms | 188ms | — | 29.8s | 10/0/-1 |
|
||||
| Aniyomi: HDS | kohiden | fr | 219ms | 17.5s | 1.9s | 8.3s | 28.0s | 30/1/0 |
|
||||
| Aniyomi: VerPelisTop | yuzono | es | 3.4s | 2.1s | 1.1s | 20.9s | 27.5s | 30/1/0 |
|
||||
| Aniyomi: DeTodoPeliculas | yuzono | es | 25.0s | 1.4s | — | — | 26.4s | 0/-1/-1 |
|
||||
| Aniyomi: EstrenosDoramas | yuzono | es | 25.2s | 1.1s | — | — | 26.2s | 0/-1/-1 |
|
||||
| Aniyomi: PelisForte | yuzono | es | 21.9s | 1.6s | 716ms | 795ms | 25.0s | 20/1/0 |
|
||||
| Aniyomi: MhdFlix | kohiden | es | 273ms | 939ms | 2.6s | 20.8s | 24.6s | 2/25/9 |
|
||||
| Aniyomi: Vostfree | yuzono | fr | 2.5s | 1.4s | 321ms | 20.2s | 24.4s | 12/10/1 |
|
||||
| Aniyomi: Latanime | yuzono | es | 23.0s | 272ms | 268ms | 827ms | 24.4s | 4/13/4 |
|
||||
| Aniyomi: HDS | yuzono | fr | 2.7s | 9.9s | 1.5s | 10.1s | 24.3s | 30/1/1 |
|
||||
| Aniyomi: PelisPlusPh | yuzono | es | 21.5s | 373ms | 138ms | 1.9s | 23.9s | 2/1/3 |
|
||||
| Aniyomi: AnimeFLV | yuzono | es | 3.2s | 336ms | 203ms | 19.9s | 23.7s | 2/13/2 |
|
||||
| Aniyomi: VerPelisTop | kohiden | es | 638ms | 317ms | 59ms | 22.6s | 23.6s | 30/1/0 |
|
||||
| Aniyomi: PelisPlusHD | yuzono | es | 21.4s | 334ms | 135ms | 1.7s | 23.6s | 2/1/0 |
|
||||
| Aniyomi: PelisPlusTo | yuzono | es | 20.7s | 176ms | 19ms | 1.2s | 22.1s | 46/16/2 |
|
||||
| Aniyomi: SeriesKao | yuzono | es | 21.2s | 423ms | — | — | 21.6s | 0/-1/-1 |
|
||||
| Aniyomi: Gnula | kohiden | es | 1.4s | 81ms | 13ms | 19.9s | 21.4s | 24/1/3 |
|
||||
| Aniyomi: Vostfree | kohiden | fr | 564ms | 990ms | 12ms | 19.4s | 21.0s | 12/10/1 |
|
||||
| Aniyomi: Jkanime | kohiden | es | 246ms | 286ms | 277ms | 20.2s | 21.0s | 2/12/5 |
|
||||
| Aniyomi: VerAnimes | yuzono | es | 20.0s | 371ms | 143ms | 393ms | 20.9s | 3/13/0 |
|
||||
| Aniyomi: AnimeFLV | kohiden | es | 512ms | 336ms | 154ms | 19.8s | 20.8s | 2/13/4 |
|
||||
| Aniyomi: SupJav | kohiden | all | 538ms | 101ms | 0ms | 20.2s | 20.8s | 24/1/4 |
|
||||
| Aniyomi: LuciferDonghua | yuzono | en | 6.1s | 8.7s | 1.2s | 2.5s | 18.4s | 10/18/0 |
|
||||
| Aniyomi: Kawaiifu | yuzono | en | 5.1s | 2.5s | 3.4s | 1.8s | 12.8s | 10/12/3 |
|
||||
| Aniyomi: Anime-Loads | yuzono | de | 2.8s | 3.8s | 793ms | 4.8s | 12.1s | 3/12/2 |
|
||||
| Aniyomi: hanime.tv | yuzono | en | 11.1s | 434ms | 245ms | 281ms | 12.1s | 2/17/1 |
|
||||
| Aniyomi: Pandrama | yuzono | es | 2.5s | 9.4s | — | — | 11.8s | 0/-1/-1 |
|
||||
| Aniyomi: ANIMEWORLD.tv | yuzono | it | 10.6s | 540ms | 360ms | 367ms | 11.8s | 4/13/1 |
|
||||
| Aniyomi: LaMovie | kohiden | es | 1.0s | 1.7s | 2.8s | 5.5s | 11.1s | 1/25/3 |
|
||||
| Aniyomi: HiAnimeWs (Unoriginal) | yuzono | en | 7.2s | 398ms | 770ms | 2.1s | 10.5s | 11/12/18 |
|
||||
| Aniyomi: LuciferDonghua | kohiden | en | 613ms | 8.9s | 205ms | 617ms | 10.4s | 10/18/0 |
|
||||
| Aniyomi: LMAnime | yuzono | all | 2.6s | 5.2s | 564ms | 1.7s | 10.1s | 10/26/5 |
|
||||
| Aniyomi: haho.moe | yuzono | en | 3.6s | 2.6s | 2.1s | 1.6s | 9.9s | 2/3/1 |
|
||||
| Aniyomi: Myanime | yuzono | en | 6.3s | 537ms | 1.9s | 604ms | 9.4s | 10/40/6 |
|
||||
| Aniyomi: NimeGami | yuzono | id | 2.2s | 3.9s | 313ms | 2.3s | 8.7s | 10/6/4 |
|
||||
| Aniyomi: KissKH | yuzono | en | 4.8s | 207ms | 159ms | 3.4s | 8.6s | 3/26/1 |
|
||||
| Aniyomi: AnimeToast | yuzono | de | 1.9s | 1.2s | 674ms | 4.7s | 8.5s | 4/13/0 |
|
||||
| Aniyomi: JPFilms | yuzono | en | 5.5s | 2.1s | 737ms | — | 8.4s | 7/0/-1 |
|
||||
| Aniyomi: Doramasflix | yuzono | es | 5.0s | 577ms | 533ms | 2.0s | 8.1s | 2/1/2 |
|
||||
| Aniyomi: HexaWatch | yuzono | en | 8.0s | 10ms | — | — | 8.0s | 0/-1/-1 |
|
||||
| Aniyomi: Animenix | yuzono | es | 3.6s | 4.1s | — | — | 7.8s | 0/-1/-1 |
|
||||
| Aniyomi: MetroSeries | yuzono | es | 2.0s | 948ms | 577ms | 4.2s | 7.7s | 2/1/2 |
|
||||
| Aniyomi: KissAnime | yuzono | en | 6.9s | 236ms | 211ms | 311ms | 7.7s | 4/13/0 |
|
||||
| Aniyomi: YFlix | yuzono | en | 4.5s | 393ms | 654ms | 2.0s | 7.6s | 30/1/2 |
|
||||
| Aniyomi: Docchi | yuzono | pl | 4.3s | 392ms | 436ms | 2.5s | 7.6s | 3/13/21 |
|
||||
| Aniyomi: Egy Dead | yuzono | ar | 2.4s | 1.4s | 2.9s | 736ms | 7.5s | 48/50/0 |
|
||||
| Aniyomi: AnimeYT.es | yuzono | es | 3.9s | 1.6s | 601ms | 1.3s | 7.4s | 3/13/1 |
|
||||
| Aniyomi: AnimeKai | kohiden | en | 5ms | 264ms | 575ms | 6.6s | 7.4s | 11/12/0 |
|
||||
| Aniyomi: LMAnime | kohiden | all | 579ms | 5.1s | 402ms | 1.3s | 7.4s | 10/26/5 |
|
||||
| Aniyomi: desu-online | yuzono | pl | 2.5s | 2.9s | 526ms | 1.3s | 7.2s | 2/13/1 |
|
||||
| Aniyomi: Animetsu | yuzono | all | 3.4s | 725ms | 338ms | 2.7s | 7.1s | 3/12/18 |
|
||||
| Aniyomi: Rule34Video | yuzono | en | 2.6s | 3.9s | 0ms | 563ms | 7.1s | 24/1/5 |
|
||||
| Aniyomi: Samehadaku | kohiden | id | 499ms | 6.5s | — | — | 7.0s | 0/-1/-1 |
|
||||
| Aniyomi: Jav Guru | yuzono | all | 2.4s | 3.2s | 0ms | 1.3s | 6.9s | 24/1/0 |
|
||||
| Aniyomi: DattebayoBR | kohiden | pt-BR | 157ms | 769ms | 3.3s | 2.5s | 6.7s | 5/1/3 |
|
||||
| Aniyomi: Hstream | yuzono | en | 4.4s | 1.1s | 424ms | 552ms | 6.6s | 9/1/3 |
|
||||
| Aniyomi: CineCalidad | yuzono | es | 2.8s | 443ms | 285ms | 2.9s | 6.4s | 1/12/2 |
|
||||
| Aniyomi: AniGo | yuzono | en | 2.2s | 574ms | 742ms | 2.8s | 6.3s | 11/12/18 |
|
||||
| Aniyomi: NimeGami | kohiden | id | 1.1s | 3.0s | 134ms | 1.9s | 6.2s | 10/6/4 |
|
||||
| Aniyomi: Oppai Stream | yuzono | en | 3.8s | 787ms | 818ms | 752ms | 6.1s | 1/1/2 |
|
||||
| Aniyomi: haho.moe | kohiden | en | 500ms | 1.9s | 2.2s | 1.5s | 6.1s | 2/3/1 |
|
||||
| Aniyomi: AniSama | yuzono | fr | 4.2s | 1.7s | — | — | 5.9s | 0/-1/-1 |
|
||||
| Aniyomi: Animenosub | yuzono | en | 1.2s | 2.6s | 599ms | 1.5s | 5.9s | 3/13/2 |
|
||||
| Aniyomi: Einfach | yuzono | de | 2.1s | 3.8s | — | — | 5.9s | 0/-1/-1 |
|
||||
| Aniyomi: AniWorld | kohiden | de | 218ms | 249ms | 405ms | 4.9s | 5.8s | 1/26/15 |
|
||||
| Aniyomi: PinoyMoviePedia | yuzono | en | 4.4s | 1.0s | 143ms | 238ms | 5.8s | 30/1/0 |
|
||||
| Aniyomi: XPrime | yuzono | en | 5.1s | 62ms | 74ms | 459ms | 5.7s | 4/1/0 |
|
||||
| Aniyomi: HentaiTk | yuzono | es | 2.5s | 2.0s | 342ms | 766ms | 5.7s | 1/1/0 |
|
||||
| Aniyomi: asia2tv | yuzono | ar | 2.3s | 1.4s | 542ms | 1.3s | 5.6s | 7/48/0 |
|
||||
| Aniyomi: AnimeXin | yuzono | all | 2.9s | 2.7s | — | — | 5.6s | 0/-1/-1 |
|
||||
| Aniyomi: Anitube | yuzono | pt-BR | 1.1s | 312ms | 267ms | 3.8s | 5.5s | 5/1/0 |
|
||||
| Aniyomi: BeatZ Anime | yuzono | es | 1.2s | 4.3s | — | — | 5.5s | 0/-1/-1 |
|
||||
| Aniyomi: JavGG | yuzono | all | 2.4s | 391ms | 145ms | 2.5s | 5.3s | 40/1/2 |
|
||||
| Aniyomi: AnimeKhor | yuzono | en | 1.3s | 2.3s | 611ms | 1.1s | 5.3s | 4/5/6 |
|
||||
| Aniyomi: Arab Seed | yuzono | ar | 2.4s | 3.0s | — | — | 5.3s | 0/-1/-1 |
|
||||
| Aniyomi: MonosChinos | yuzono | es | 3.7s | 671ms | 277ms | 691ms | 5.3s | 3/12/1 |
|
||||
| Aniyomi: AnimesROLL | kohiden | pt-BR | 1.1s | 1.9s | 928ms | 1.3s | 5.2s | 1/25/0 |
|
||||
| Aniyomi: AniWorld | yuzono | de | 2.3s | 357ms | 363ms | 2.1s | 5.2s | 1/26/20 |
|
||||
| Aniyomi: LACartoons | yuzono | es | 3.0s | 537ms | 216ms | 1.4s | 5.2s | 1/64/4 |
|
||||
| Aniyomi: MissAV | yuzono | all | 2.3s | 1.9s | 0ms | 915ms | 5.1s | 24/1/4 |
|
||||
| Aniyomi: Kool | yuzono | de | 2.0s | 864ms | 732ms | 1.5s | 5.1s | 1/1/0 |
|
||||
| Aniyomi: KimoiTV | yuzono | en | 3.5s | 1.6s | — | — | 5.1s | 0/-1/-1 |
|
||||
| Aniyomi: Torrentio Anime (Torrent / Debrid) | yuzono | all | 3.4s | 492ms | 528ms | 657ms | 5.1s | 3/12/48 |
|
||||
| Aniyomi: HiAnime (Dead) | yuzono | en | 4.9s | 210ms | — | — | 5.1s | 0/-1/-1 |
|
||||
| Aniyomi: MetroSeries | kohiden | es | 233ms | 640ms | 531ms | 3.7s | 5.1s | 2/1/2 |
|
||||
| Aniyomi: HomeCine | yuzono | es | 2.9s | 2.1s | — | — | 5.0s | 0/-1/-1 |
|
||||
| Aniyomi: MoviesMod | yuzono | en | 4.5s | 517ms | — | — | 5.0s | 0/-1/-1 |
|
||||
| Aniyomi: Anime-Sama | yuzono | fr | 3.0s | 505ms | 639ms | 831ms | 5.0s | 2/13/4 |
|
||||
| Aniyomi: AniZone | yuzono | all | 1.4s | 1.3s | 1.2s | 1.0s | 4.9s | 2/13/3 |
|
||||
| Aniyomi: AnimeKai | yuzono | en | 112ms | 628ms | 968ms | 3.1s | 4.8s | 11/12/18 |
|
||||
| Aniyomi: UHD Movies | yuzono | en | 3.5s | 1.2s | — | — | 4.8s | 0/-1/-1 |
|
||||
| Aniyomi: zeroanime | yuzono | es | 2.9s | 1.8s | — | — | 4.7s | 0/-1/-1 |
|
||||
| Aniyomi: My Running Man | yuzono | en | 3.3s | 549ms | 414ms | 386ms | 4.7s | 10/1/0 |
|
||||
| Aniyomi: LocoPelis | yuzono | es | 3.3s | 653ms | 351ms | 288ms | 4.6s | 6/1/0 |
|
||||
| Aniyomi: AnimeIndo | yuzono | id | 2.9s | 1.8s | — | — | 4.6s | 0/-1/-1 |
|
||||
| Aniyomi: AnimeID | yuzono | es | 3.7s | 855ms | — | — | 4.6s | 0/-1/-1 |
|
||||
| Aniyomi: MiniOppai | yuzono | id | 1.7s | 2.8s | — | — | 4.5s | 0/-1/-1 |
|
||||
| Aniyomi: Okanime | yuzono | ar | 2.3s | 1.5s | 726ms | — | 4.5s | 24/0/-1 |
|
||||
| Aniyomi: Animeyt | yuzono | es | 3.2s | 1.3s | — | — | 4.4s | 0/-1/-1 |
|
||||
| Aniyomi: Einfach | kohiden | de | 1.5s | 2.9s | — | — | 4.4s | 0/-1/-1 |
|
||||
| Aniyomi: DonghuaStream | yuzono | en | 3.5s | 957ms | — | — | 4.4s | 0/-1/-1 |
|
||||
| Aniyomi: EnNovelas (Dead) | yuzono | es | 2.1s | 1.6s | 595ms | — | 4.3s | 3/0/-1 |
|
||||
| Aniyomi: Animenosub | kohiden | en | 1.6s | 2.1s | 104ms | 537ms | 4.3s | 3/13/0 |
|
||||
| Aniyomi: FilmPalast | yuzono | de | 1.5s | 639ms | 296ms | 1.8s | 4.2s | 24/1/2 |
|
||||
| Aniyomi: French Anime | yuzono | fr | 2.8s | 620ms | 325ms | 445ms | 4.1s | 4/13/2 |
|
||||
| Aniyomi: desu-online | kohiden | pl | 192ms | 2.4s | 398ms | 1.0s | 4.1s | 2/13/1 |
|
||||
| Aniyomi: ChineseAnime | yuzono | all | 2.8s | 1.2s | — | — | 3.9s | 0/-1/-1 |
|
||||
| Aniyomi: LegionAnime | yuzono | es | 2.8s | 733ms | 189ms | 168ms | 3.9s | 2/13/0 |
|
||||
| Aniyomi: AnimeUnity | yuzono | it | 1.8s | 862ms | 306ms | 916ms | 3.9s | 4/13/3 |
|
||||
| Aniyomi: Wbijam | yuzono | pl | 2.8s | 1.1s | — | — | 3.9s | 0/-1/-1 |
|
||||
| Aniyomi: Anime Saturn | yuzono | it | 1.7s | 582ms | 542ms | 1.1s | 3.9s | 4/13/5 |
|
||||
| Aniyomi: Anitube | kohiden | pt-BR | 224ms | 253ms | 233ms | 3.1s | 3.8s | 5/1/3 |
|
||||
| Aniyomi: Jable | yuzono | all | 1.1s | 1.8s | 0ms | 922ms | 3.8s | 24/1/1 |
|
||||
| Aniyomi: edytjedhgmdhm | yuzono | en | 3.5s | 331ms | — | — | 3.8s | 0/-1/-1 |
|
||||
| Aniyomi: MyKdrama | yuzono | fr | 2.5s | 1.3s | — | — | 3.8s | 0/-1/-1 |
|
||||
| Aniyomi: Animenix | kohiden | es | 236ms | 3.5s | — | — | 3.8s | 0/-1/-1 |
|
||||
| Aniyomi: ANIMEWORLD.tv | kohiden | it | 2.2s | 587ms | 509ms | 405ms | 3.7s | 4/13/1 |
|
||||
| Aniyomi: Oploverz | yuzono | id | 1.8s | 1.9s | — | — | 3.7s | 0/-1/-1 |
|
||||
| Aniyomi: AnimeXin | kohiden | all | 1.6s | 2.0s | — | — | 3.5s | 0/-1/-1 |
|
||||
| Aniyomi: HentaiTk | kohiden | es | 325ms | 1.9s | 272ms | 1.1s | 3.5s | 1/1/0 |
|
||||
| Aniyomi: MhdFlix | yuzono | es | 2.1s | 1.4s | — | — | 3.5s | 0/-1/-1 |
|
||||
| Aniyomi: VeoHentai | yuzono | es | 1.7s | 711ms | 290ms | 782ms | 3.5s | 36/1/1 |
|
||||
| Aniyomi: Nivod | yuzono | zh | 658ms | 1.6s | 443ms | 824ms | 3.5s | 3/11/12 |
|
||||
| Aniyomi: Anikyuu | kohiden | pt-BR | 227ms | 1.0s | 604ms | 1.6s | 3.5s | 4/13/0 |
|
||||
| Aniyomi: Türk Anime TV | yuzono | tr | 1.2s | 534ms | 129ms | 1.5s | 3.4s | 3/12/3 |
|
||||
| Aniyomi: AnimeBalkan | yuzono | sr | 629ms | 1.8s | 205ms | 770ms | 3.4s | 1/25/1 |
|
||||
| Aniyomi: Cinemathek | yuzono | de | 2.2s | 1.1s | — | — | 3.3s | 0/-1/-1 |
|
||||
| Aniyomi: Xiaoxintv | yuzono | zh | 539ms | 2.4s | 95ms | 185ms | 3.3s | 2/13/1 |
|
||||
| Aniyomi: Samehadaku | yuzono | id | 1.5s | 608ms | 291ms | 802ms | 3.2s | 2/12/1 |
|
||||
| Aniyomi: AnimeVostFr | yuzono | fr | 2.3s | 917ms | — | — | 3.2s | 0/-1/-1 |
|
||||
| Aniyomi: Serieskao | kohiden | es | 2.2s | 180ms | 122ms | 701ms | 3.2s | 2/1/0 |
|
||||
| Aniyomi: VerAni.me | kohiden | es | 1.5s | 845ms | 181ms | 685ms | 3.2s | 1/25/2 |
|
||||
| Aniyomi: AsyaAnimeleri | kohiden | tr | 530ms | 845ms | 549ms | 1.3s | 3.2s | 3/1/7 |
|
||||
| Aniyomi: Hentai Saturn | yuzono | it | 1.2s | 1.1s | 315ms | 592ms | 3.2s | 2/6/1 |
|
||||
| Aniyomi: Torrentio (Torrent / Debrid) | yuzono | all | 2.7s | 211ms | 170ms | 135ms | 3.2s | 40/25/0 |
|
||||
| Aniyomi: AnimeKhor | kohiden | en | 1.6s | 1.1s | 76ms | 487ms | 3.2s | 4/5/6 |
|
||||
| Aniyomi: NeoNime | yuzono | id | 2.2s | 927ms | — | — | 3.2s | 0/-1/-1 |
|
||||
| Aniyomi: AniZone | kohiden | all | 506ms | 1.1s | 870ms | 690ms | 3.1s | 2/13/3 |
|
||||
| Aniyomi: AnimeOnsen | yuzono | all | 1.6s | 1.1s | 202ms | 172ms | 3.1s | 13/12/1 |
|
||||
| Aniyomi: Hackstore | yuzono | es | 1.8s | 408ms | 245ms | 649ms | 3.1s | 1/25/0 |
|
||||
| Aniyomi: Doramasflix | kohiden | es | 283ms | 539ms | 461ms | 1.8s | 3.1s | 2/1/2 |
|
||||
| Aniyomi: BestDubbedAnime | yuzono | en | 585ms | 2.5s | — | — | 3.1s | 0/-1/-1 |
|
||||
| Aniyomi: AnimeLek | yuzono | ar | 1.9s | 1.2s | — | — | 3.1s | 0/-1/-1 |
|
||||
| Aniyomi: AnimePlayer | yuzono | pt-BR | 1.2s | 892ms | 440ms | 499ms | 3.0s | 2/12/0 |
|
||||
| Aniyomi: Anime1.me | yuzono | zh-hant | 1.3s | 1.1s | 180ms | 392ms | 3.0s | 1/1/1 |
|
||||
| Aniyomi: AnimePahe | yuzono | en | 645ms | 927ms | 535ms | 880ms | 3.0s | 8/12/9 |
|
||||
| Aniyomi: WIT ANIME | yuzono | ar | 2.1s | 614ms | 254ms | — | 3.0s | 2/0/-1 |
|
||||
| Aniyomi: AnimeBalkan | kohiden | sr | 228ms | 1.7s | 183ms | 842ms | 2.9s | 1/25/1 |
|
||||
| Aniyomi: Myanime | kohiden | en | 1.4s | 178ms | 828ms | 462ms | 2.9s | 10/40/6 |
|
||||
| Aniyomi: EstrenosDoramas | kohiden | es | 1.8s | 1.1s | — | — | 2.9s | 0/-1/-1 |
|
||||
| Aniyomi: SushiAnimes | kohiden | pt-BR | 238ms | 1.2s | 572ms | 866ms | 2.9s | 2/25/1 |
|
||||
| Aniyomi: AnimeGG | yuzono | en | 1.3s | 655ms | 321ms | 598ms | 2.9s | 3/12/5 |
|
||||
| Aniyomi: Xvideos | yuzono | all | 1.5s | 724ms | 409ms | 137ms | 2.8s | 26/1/3 |
|
||||
| Aniyomi: Donghua no Sekai | yuzono | pt-BR | 590ms | 1.4s | 179ms | 618ms | 2.8s | 9/15/3 |
|
||||
| Aniyomi: asia2tv | kohiden | ar | 553ms | 1.1s | 240ms | 824ms | 2.8s | 7/48/0 |
|
||||
| Aniyomi: MundoDonghua | yuzono | es | 2.2s | 503ms | — | — | 2.7s | 0/-1/-1 |
|
||||
| Aniyomi: BetterAnimeIo | kohiden | pt-BR | 210ms | 998ms | 60ms | 1.5s | 2.7s | 1/25/2 |
|
||||
| Aniyomi: AniSAGA | yuzono | hi | 1.5s | 1.2s | — | — | 2.7s | 0/-1/-1 |
|
||||
| Aniyomi: AnimeOnsen | kohiden | all | 688ms | 1.7s | 152ms | 149ms | 2.7s | 4/12/1 |
|
||||
| Aniyomi: Xnxx | yuzono | all | 1.2s | 986ms | 298ms | 166ms | 2.7s | 36/1/3 |
|
||||
| Aniyomi: Meus Animes | kohiden | pt-BR | 219ms | 451ms | 1.1s | 957ms | 2.7s | 1/23/2 |
|
||||
| Aniyomi: Kool | kohiden | de | 548ms | 541ms | 530ms | 1.1s | 2.7s | 1/1/0 |
|
||||
| Aniyomi: Aniweek | yuzono | ko | 2.1s | 554ms | — | — | 2.7s | 0/-1/-1 |
|
||||
| Aniyomi: BeatZ Anime | kohiden | es | 422ms | 2.2s | — | — | 2.6s | 0/-1/-1 |
|
||||
| Aniyomi: AnimeYT.es | kohiden | es | 217ms | 1.3s | 342ms | 728ms | 2.5s | 3/13/1 |
|
||||
| Aniyomi: Hentaila | yuzono | es | 2.1s | 375ms | — | — | 2.5s | 0/-1/-1 |
|
||||
| Aniyomi: Top Animes | kohiden | pt-BR | 604ms | 515ms | 288ms | 1.1s | 2.5s | 4/13/3 |
|
||||
| Aniyomi: Q1N | yuzono | pt-BR | 1.4s | 669ms | 204ms | 182ms | 2.5s | 67/1/0 |
|
||||
| Aniyomi: DeTodoPeliculas | kohiden | es | 1.3s | 1.2s | — | — | 2.5s | 0/-1/-1 |
|
||||
| Aniyomi: Oppai Stream | kohiden | en | 430ms | 506ms | 697ms | 809ms | 2.4s | 1/1/2 |
|
||||
| Aniyomi: Anime Saturn | kohiden | it | 188ms | 698ms | 507ms | 1.0s | 2.4s | 4/13/5 |
|
||||
| Aniyomi: Anime-Stream | yuzono | de | 1.8s | 588ms | — | — | 2.3s | 0/-1/-1 |
|
||||
| Aniyomi: Kawaiifu | kohiden | en | 181ms | 736ms | 925ms | 433ms | 2.3s | 10/12/3 |
|
||||
| Aniyomi: Subsplease | yuzono | all | 1.2s | 414ms | 482ms | 114ms | 2.2s | 26/26/3 |
|
||||
| Aniyomi: Egy Dead | kohiden | ar | 508ms | 362ms | 956ms | 344ms | 2.2s | 48/50/0 |
|
||||
| Aniyomi: Wiflix | kohiden | fr | 218ms | 1.9s | — | — | 2.1s | 0/-1/-1 |
|
||||
| Aniyomi: Hentai Saturn | kohiden | it | 330ms | 836ms | 368ms | 609ms | 2.1s | 2/6/1 |
|
||||
| Aniyomi: Anime1.me | kohiden | zh-hant | 1.1s | 530ms | 187ms | 293ms | 2.1s | 1/1/1 |
|
||||
| Aniyomi: DarkMahou | yuzono | pt-BR | 1.4s | 635ms | — | — | 2.1s | 0/-1/-1 |
|
||||
| Aniyomi: LACartoons | kohiden | es | 590ms | 351ms | 166ms | 937ms | 2.0s | 1/64/4 |
|
||||
| Aniyomi: MiniOppai | kohiden | id | 240ms | 1.8s | — | — | 2.0s | 0/-1/-1 |
|
||||
| Aniyomi: AnimesOnlineVip | yuzono | pt-BR | 805ms | 1.2s | — | — | 2.0s | 0/-1/-1 |
|
||||
| Aniyomi: Anime Srbija | yuzono | sr | 1.4s | 446ms | 78ms | 86ms | 2.0s | 2/13/1 |
|
||||
| Aniyomi: JPFilms | kohiden | en | 215ms | 1.6s | 197ms | — | 2.0s | 7/0/-1 |
|
||||
| Aniyomi: Animes Digital | yuzono | pt-BR | 1.3s | 708ms | — | — | 2.0s | 0/-1/-1 |
|
||||
| Aniyomi: Donghua no Sekai | kohiden | pt-BR | 532ms | 1.0s | 139ms | 276ms | 2.0s | 9/15/3 |
|
||||
| Aniyomi: PelisForte | kohiden | es | 1.7s | 241ms | — | — | 1.9s | 0/-1/-1 |
|
||||
| Aniyomi: HDFilmCehennemi | yuzono | tr | 1.2s | 339ms | 0ms | 449ms | 1.9s | 5/1/0 |
|
||||
| Aniyomi: Pi Fansubs | yuzono | pt-BR | 620ms | 1.1s | 75ms | 125ms | 1.9s | 30/1/0 |
|
||||
| Aniyomi: Pandrama | kohiden | es | 527ms | 1.4s | — | — | 1.9s | 0/-1/-1 |
|
||||
| Aniyomi: Goyabu | yuzono | pt-BR | 848ms | 729ms | 299ms | — | 1.9s | 6/0/-1 |
|
||||
| Aniyomi: HentaisTube | yuzono | pt-BR | 1.1s | 729ms | — | — | 1.9s | 0/-1/-1 |
|
||||
| Aniyomi: Xfani | yuzono | zh | 466ms | 1.4s | — | — | 1.9s | 0/-1/-1 |
|
||||
| Aniyomi: UAKino | yuzono | uk | 634ms | 361ms | 271ms | 589ms | 1.9s | 1/2/3 |
|
||||
| Aniyomi: zeroanime | kohiden | es | 261ms | 1.6s | — | — | 1.8s | 0/-1/-1 |
|
||||
| Aniyomi: AnimePlayer | kohiden | pt-BR | 496ms | 595ms | 307ms | 424ms | 1.8s | 2/12/0 |
|
||||
| Aniyomi: Jav Guru | kohiden | all | 1.5s | 215ms | 0ms | 127ms | 1.8s | 24/1/0 |
|
||||
| Aniyomi: VeoHentai | kohiden | es | 242ms | 560ms | 241ms | 753ms | 1.8s | 36/1/1 |
|
||||
| Aniyomi: AnimeToast | kohiden | de | 518ms | 270ms | 91ms | 874ms | 1.8s | 4/13/0 |
|
||||
| Aniyomi: AniSama | kohiden | fr | 244ms | 1.5s | — | — | 1.7s | 0/-1/-1 |
|
||||
| Aniyomi: Anime Fire | yuzono | pt-BR | 1.2s | 328ms | 32ms | 182ms | 1.7s | 4/13/1 |
|
||||
| Aniyomi: Hackstore | kohiden | es | 289ms | 324ms | 159ms | 954ms | 1.7s | 1/25/0 |
|
||||
| Aniyomi: AnimeZ | kohiden | en | 389ms | 1.3s | — | — | 1.7s | 0/-1/-1 |
|
||||
| Aniyomi: Goyabu | kohiden | pt-BR | 173ms | 654ms | 161ms | 728ms | 1.7s | 6/12/2 |
|
||||
| Aniyomi: AnimeUnity | kohiden | it | 190ms | 661ms | 254ms | 610ms | 1.7s | 4/13/3 |
|
||||
| Aniyomi: NeoNime | kohiden | id | 993ms | 702ms | — | — | 1.7s | 0/-1/-1 |
|
||||
| Aniyomi: CineCalidad | kohiden | es | 296ms | 232ms | 218ms | 889ms | 1.6s | 1/12/2 |
|
||||
| Aniyomi: KimoiTV | kohiden | en | 494ms | 1.1s | — | — | 1.6s | 0/-1/-1 |
|
||||
| Aniyomi: Hstream | kohiden | en | 501ms | 333ms | 331ms | 442ms | 1.6s | 25/1/3 |
|
||||
| Aniyomi: AnimeTake | yuzono | en | 818ms | 782ms | — | — | 1.6s | 0/-1/-1 |
|
||||
| Aniyomi: Oploverz | kohiden | id | 177ms | 1.4s | — | — | 1.6s | 0/-1/-1 |
|
||||
| Aniyomi: Xfani | kohiden | zh | 210ms | 1.4s | — | — | 1.6s | 0/-1/-1 |
|
||||
| Aniyomi: FanPelis | yuzono | es | 912ms | 650ms | — | — | 1.6s | 0/-1/-1 |
|
||||
| Aniyomi: AnimesHouse | yuzono | pt-BR | 910ms | 594ms | — | — | 1.5s | 0/-1/-1 |
|
||||
| Aniyomi: French Anime | kohiden | fr | 547ms | 504ms | 193ms | 231ms | 1.5s | 4/13/0 |
|
||||
| Aniyomi: EnNovelas | kohiden | es | 232ms | 1.2s | — | — | 1.5s | 0/-1/-1 |
|
||||
| Aniyomi: UAKino | kohiden | uk | 423ms | 317ms | 151ms | 577ms | 1.5s | 1/2/3 |
|
||||
| Aniyomi: ChineseAnime | kohiden | all | 624ms | 840ms | — | — | 1.5s | 0/-1/-1 |
|
||||
| Aniyomi: KissAnime | kohiden | en | 1.0s | 138ms | 71ms | 237ms | 1.4s | 4/13/0 |
|
||||
| Aniyomi: MY CIMA | kohiden | ar | 430ms | 1.0s | — | — | 1.4s | 0/-1/-1 |
|
||||
| Aniyomi: Rule34Video | kohiden | en | 183ms | 762ms | 0ms | 477ms | 1.4s | 24/1/5 |
|
||||
| Aniyomi: Wbijam | kohiden | pl | 837ms | 568ms | — | — | 1.4s | 0/-1/-1 |
|
||||
| Aniyomi: Anime Srbija | kohiden | sr | 1.0s | 245ms | 63ms | 66ms | 1.4s | 2/13/1 |
|
||||
| Aniyomi: UHD Movies | kohiden | en | 431ms | 958ms | — | — | 1.4s | 0/-1/-1 |
|
||||
| Aniyomi: Pi Fansubs | kohiden | pt-BR | 204ms | 1.1s | 60ms | 46ms | 1.4s | 30/1/0 |
|
||||
| Aniyomi: Jable | kohiden | all | 405ms | 750ms | 0ms | 171ms | 1.3s | 24/1/1 |
|
||||
| Aniyomi: Animeyt | kohiden | es | 232ms | 1.0s | — | — | 1.3s | 0/-1/-1 |
|
||||
| Aniyomi: AnimeID | kohiden | es | 568ms | 679ms | — | — | 1.2s | 0/-1/-1 |
|
||||
| Aniyomi: hanime.tv | kohiden | en | 492ms | 241ms | 137ms | 362ms | 1.2s | 2/6/3 |
|
||||
| Aniyomi: Hanime1 | yuzono | zh | 524ms | 707ms | — | — | 1.2s | 0/-1/-1 |
|
||||
| Aniyomi: Arab Seed | kohiden | ar | 517ms | 712ms | — | — | 1.2s | 0/-1/-1 |
|
||||
| Aniyomi: AnimeIndo | kohiden | id | 239ms | 989ms | — | — | 1.2s | 0/-1/-1 |
|
||||
| Aniyomi: Okanime | kohiden | ar | 512ms | 461ms | 224ms | — | 1.2s | 24/0/-1 |
|
||||
| Aniyomi: AV1Encodes | kohiden | en | 321ms | 244ms | 332ms | 299ms | 1.2s | 1/25/2 |
|
||||
| Aniyomi: FilmPalast | kohiden | de | 428ms | 88ms | 19ms | 633ms | 1.2s | 24/1/2 |
|
||||
| Aniyomi: BestDubbedAnime | kohiden | en | 206ms | 957ms | — | — | 1.2s | 0/-1/-1 |
|
||||
| Aniyomi: MonosChinos | kohiden | es | 231ms | 476ms | 451ms | — | 1.2s | 3/0/-1 |
|
||||
| Aniyomi: Hanime1 | kohiden | zh | 244ms | 309ms | 293ms | 302ms | 1.1s | 1/32/3 |
|
||||
| Aniyomi: AnimeGG | kohiden | en | 179ms | 297ms | 163ms | 499ms | 1.1s | 3/12/5 |
|
||||
| Aniyomi: AnimeLek | kohiden | ar | 423ms | 715ms | — | — | 1.1s | 0/-1/-1 |
|
||||
| Aniyomi: LegionAnime | kohiden | es | 246ms | 459ms | 279ms | 149ms | 1.1s | 2/13/0 |
|
||||
| Aniyomi: AnimeVostFr | kohiden | fr | 204ms | 916ms | — | — | 1.1s | 0/-1/-1 |
|
||||
| Aniyomi: Cinemathek | kohiden | de | 226ms | 848ms | — | — | 1.1s | 0/-1/-1 |
|
||||
| Aniyomi: Animefenix | kohiden | es | 338ms | 425ms | 290ms | — | 1.1s | 10/0/-1 |
|
||||
| Aniyomi: Nivod | kohiden | zh | 195ms | 808ms | 14ms | 26ms | 1.0s | 3/11/12 |
|
||||
| Aniyomi: MyKdrama | kohiden | fr | 204ms | 835ms | — | — | 1.0s | 0/-1/-1 |
|
||||
| Aniyomi: HDFilmCehennemi | kohiden | tr | 248ms | 357ms | 0ms | 407ms | 1.0s | 5/1/0 |
|
||||
| Aniyomi: Samato | kohiden | es | 209ms | 803ms | — | — | 1.0s | 0/-1/-1 |
|
||||
| Aniyomi: WIT ANIME | kohiden | ar | 562ms | 276ms | 152ms | — | 989ms | 2/0/-1 |
|
||||
| Aniyomi: Subsplease | kohiden | all | 404ms | 147ms | 327ms | 86ms | 964ms | 26/26/3 |
|
||||
| Aniyomi: FanPelis | kohiden | es | 509ms | 451ms | — | — | 958ms | 0/-1/-1 |
|
||||
| Aniyomi: AnimeTake | kohiden | en | 591ms | 367ms | — | — | 956ms | 0/-1/-1 |
|
||||
| Aniyomi: iyf | yuzono | zh | 511ms | 230ms | 82ms | 120ms | 943ms | 2/14/1 |
|
||||
| Aniyomi: PinoyMoviePedia | kohiden | en | 226ms | 488ms | 74ms | 153ms | 941ms | 30/1/0 |
|
||||
| Aniyomi: LocoPelis | kohiden | es | 235ms | 342ms | 205ms | 155ms | 937ms | 6/1/0 |
|
||||
| Aniyomi: HentaiLA | kohiden | es | 586ms | 337ms | — | — | 921ms | 0/-1/-1 |
|
||||
| Aniyomi: HentaisTube | kohiden | pt-BR | 320ms | 600ms | — | — | 918ms | 0/-1/-1 |
|
||||
| Aniyomi: AnimeAv1 | kohiden | es | 188ms | 283ms | 138ms | 297ms | 906ms | 5/13/3 |
|
||||
| Aniyomi: MoviesMod | kohiden | en | 522ms | 352ms | — | — | 872ms | 0/-1/-1 |
|
||||
| Aniyomi: Torrentio (Torrent / Debrid) | kohiden | all | 543ms | 116ms | 71ms | 109ms | 839ms | 40/25/0 |
|
||||
| Aniyomi: Torrentio Anime (Torrent / Debrid) | kohiden | all | 366ms | 194ms | 164ms | 96ms | 820ms | 3/12/48 |
|
||||
| Aniyomi: AnimesOnlineVip | kohiden | pt-BR | 208ms | 601ms | — | — | 807ms | 0/-1/-1 |
|
||||
| Aniyomi: Wcofun | kohiden | en | 441ms | 333ms | — | — | 772ms | 0/-1/-1 |
|
||||
| Aniyomi: WCOStream | kohiden | en | 186ms | 492ms | 89ms | — | 766ms | 4/0/-1 |
|
||||
| Aniyomi: DarkMahou | kohiden | pt-BR | 229ms | 484ms | — | — | 711ms | 0/-1/-1 |
|
||||
| Aniyomi: JavGG | kohiden | all | 530ms | 99ms | 38ms | 36ms | 703ms | 40/1/0 |
|
||||
| Aniyomi: MundoDonghua | kohiden | es | 308ms | 390ms | — | — | 696ms | 0/-1/-1 |
|
||||
| Aniyomi: HomeCine | kohiden | es | 289ms | 313ms | — | — | 600ms | 0/-1/-1 |
|
||||
| Aniyomi: My Running Man | kohiden | en | 218ms | 156ms | 91ms | 89ms | 554ms | 10/1/0 |
|
||||
| Aniyomi: az-animex | kohiden | es | 213ms | 204ms | 98ms | — | 514ms | 2/0/-1 |
|
||||
| Aniyomi: Aniweek | kohiden | ko | 181ms | 333ms | — | — | 512ms | 0/-1/-1 |
|
||||
| Aniyomi: edytjedhgmdhm | kohiden | en | 412ms | 96ms | — | — | 506ms | 0/-1/-1 |
|
||||
| Aniyomi: Anime-Stream | kohiden | de | 410ms | 87ms | — | — | 495ms | 0/-1/-1 |
|
||||
| Aniyomi: Anime Fire | kohiden | pt-BR | 171ms | 173ms | 17ms | 127ms | 488ms | 4/13/1 |
|
||||
| Aniyomi: iyf | kohiden | zh | 220ms | 119ms | 47ms | 49ms | 435ms | 2/14/1 |
|
||||
| Aniyomi: Xiaoxintv | kohiden | zh | 185ms | 154ms | 20ms | 22ms | 381ms | 2/13/1 |
|
||||
|
||||
## Stage Breakdown
|
||||
|
||||
Each extension progresses through four stages. The tables below show how many extensions passed, failed, or never reached each stage.
|
||||
|
||||
| Stage | Pass | Fail | Pending | Median Time | Max Time |
|
||||
|-------|------|------|---------|-------------|----------|
|
||||
| Load | 496 | 4 | 0 | 1.1s | 31.2s |
|
||||
| Search | 325 | 171 | 4 | 670ms | 60.6s |
|
||||
| Episodes | 217 | 14 | 175 | 285ms | 30.2s |
|
||||
| Videos | 180 | 24 | 189 | 759ms | 123.0s |
|
||||
|
||||
### Failure Distribution
|
||||
|
||||
| Stage | Failures | Percentage of Total |
|
||||
|-------|----------|---------------------|
|
||||
| Load | 4 | 0.8% |
|
||||
| Search | 171 | 34.2% |
|
||||
| Episodes | 14 | 2.8% |
|
||||
| Videos | 24 | 4.8% |
|
||||
|
||||
## Failures by Stage
|
||||
|
||||
:::danger Load Failures (4)
|
||||
Extensions that failed to initialize.
|
||||
:::
|
||||
|
||||
| Extension | Repository | Error |
|
||||
|-----------|------------|-------|
|
||||
| Aniyomi: Sudatchi NSFW | yuzono | NoSuchMethodError |
|
||||
| Aniyomi: Tuktuk Cinema | yuzono | NullPointerException |
|
||||
| Aniyomi: Sudatchi | yuzono | NoSuchMethodError |
|
||||
| Aniyomi: Aniwave.se | yuzono | NullPointerException |
|
||||
|
||||
:::danger Search Failures (171)
|
||||
Extensions that loaded successfully but failed to return search results.
|
||||
:::
|
||||
|
||||
| Extension | Repository | Error |
|
||||
|-----------|------------|-------|
|
||||
| Aniyomi: Google Drive | yuzono | Missing settings |
|
||||
| Aniyomi: MyReadingManga | yuzono | IllegalArgumentException |
|
||||
| Aniyomi: Debrid Index | yuzono | HTTP error 400 |
|
||||
| Aniyomi: GoogleDriveIndex | yuzono | Missing settings |
|
||||
| Aniyomi: AnimeWorld India | yuzono | UnknownHostException |
|
||||
| Aniyomi: Movie4k | yuzono | UnknownHostException |
|
||||
| Aniyomi: Serienstream | yuzono | HTTP error 404 |
|
||||
| Aniyomi: Newgrounds | yuzono | HTTP error 403 |
|
||||
| Aniyomi: RouVideo | yuzono | NullPointerException |
|
||||
| Aniyomi: StreamingCommunity | yuzono | NullPointerException |
|
||||
| Aniyomi: AllAnimeChi | yuzono | dev.k3vinb5.aniyomibridge.exceptions.UnyoCoreException: Fail |
|
||||
| Aniyomi: AsiaFlix | yuzono | JSON parsing error |
|
||||
| Aniyomi: AnimeParadise | yuzono | HTTP error 404 |
|
||||
| Aniyomi: AnimeOwl (Dead) | yuzono | JSON parsing error |
|
||||
| Aniyomi: Kinoking | yuzono | HTTP error 404 |
|
||||
| Aniyomi: DopeFlix | yuzono | HTTP error 521 |
|
||||
| Aniyomi: PutLocker | yuzono | UnknownHostException |
|
||||
| Aniyomi: MovHub | yuzono | ConnectException |
|
||||
| Aniyomi: KickAssAnime | yuzono | HTTP error 403 |
|
||||
| Aniyomi: Jkhentai | yuzono | UnknownHostException |
|
||||
| Aniyomi: Cineplus123 | yuzono | SSL/TLS error |
|
||||
| Aniyomi: Hentaijk | yuzono | HTTP error 404 |
|
||||
| Aniyomi: Jellyfin | yuzono | dev.k3vinb5.aniyomibridge.exceptions.UnyoCoreException: Fail |
|
||||
| Aniyomi: Cuevana | yuzono | IllegalArgumentException |
|
||||
| Aniyomi: Stremio | yuzono | dev.k3vinb5.aniyomibridge.exceptions.UnyoCoreException: Fail |
|
||||
| Aniyomi: Tiodonghua.com | yuzono | UnknownHostException |
|
||||
| Aniyomi: SuperStream | yuzono | JSON parsing error |
|
||||
| Aniyomi: EmpireStreaming | yuzono | JSON parsing error |
|
||||
| Aniyomi: VoirCartoon | yuzono | UnknownHostException |
|
||||
| Aniyomi: AniWatchtv | yuzono | HTTP error 522 |
|
||||
| Aniyomi: Wiflix | yuzono | UnknownHostException |
|
||||
| Aniyomi: YoMovies | yuzono | UnknownHostException |
|
||||
| Aniyomi: Kuronime | yuzono | HTTP error 403 |
|
||||
| Aniyomi: DopeBox | yuzono | HTTP error 522 |
|
||||
| Aniyomi: AniPlay | yuzono | JSON parsing error |
|
||||
| Aniyomi: VVVVID | yuzono | dev.k3vinb5.aniyomibridge.exceptions.UnyoCoreException: Fail |
|
||||
| Aniyomi: Kaido | yuzono | HTTP error 522 |
|
||||
| Aniyomi: AnimeQ | yuzono | dev.k3vinb5.aniyomibridge.exceptions.UnyoCoreException: Fail |
|
||||
| Aniyomi: AnimesBr (Dead) | yuzono | UnknownHostException |
|
||||
| Aniyomi: SFlix | yuzono | HTTP error 522 |
|
||||
| Aniyomi: 9AnimeTV | yuzono | HTTP error 522 |
|
||||
| Aniyomi: Anime-Base | yuzono | NullPointerException |
|
||||
| Aniyomi: Anime Blkom | yuzono | Cloudflare bypass failed |
|
||||
| Aniyomi: AnimesOnlineCC | yuzono | HTTP error 403 |
|
||||
| Aniyomi: ArabAnime | yuzono | Cloudflare bypass failed |
|
||||
| Aniyomi: Animes CX | yuzono | HTTP error 404 |
|
||||
| Aniyomi: FASELHD | yuzono | Cloudflare bypass failed |
|
||||
| Aniyomi: Anime4up | yuzono | Cloudflare bypass failed |
|
||||
| Aniyomi: Animerco | yuzono | Cloudflare bypass failed |
|
||||
| Aniyomi: Better Anime | yuzono | NullPointerException |
|
||||
| ... | ... | *and 121 more* |
|
||||
|
||||
:::caution Episode Failures (14)
|
||||
Extensions that passed search but failed to retrieve episode lists.
|
||||
:::
|
||||
|
||||
| Extension | Repository | Error |
|
||||
|-----------|------------|-------|
|
||||
| Aniyomi: PTorrent (Torrent) | yuzono | ConnectException |
|
||||
| Aniyomi: Nyaa (Torrent) | yuzono | ConnectException |
|
||||
| Aniyomi: Hentai Torrent (Torrent) | yuzono | ConnectException |
|
||||
| Aniyomi: AniPlay | yuzono | Episode list not found |
|
||||
| Aniyomi: az-animex | yuzono | JSON parsing error |
|
||||
| Aniyomi: Tokuzilla | yuzono | NullPointerException |
|
||||
| Aniyomi: OtakuDesu | yuzono | HTTP error 403 |
|
||||
| Aniyomi: UFDub | yuzono | IllegalArgumentException |
|
||||
| Aniyomi: AniPlay | kohiden | Episode list not found |
|
||||
| Aniyomi: Doramasyt | yuzono | Cloudflare bypass failed |
|
||||
| Aniyomi: Tokuzilla | kohiden | NullPointerException |
|
||||
| Aniyomi: OtakuDesu | kohiden | HTTP error 403 |
|
||||
| Aniyomi: UFDub | kohiden | IllegalArgumentException |
|
||||
| Aniyomi: Doramasyt | kohiden | Cloudflare bypass failed |
|
||||
|
||||
:::caution Video Failures (24)
|
||||
Extensions that passed episodes but failed to extract video URLs.
|
||||
:::
|
||||
|
||||
| Extension | Repository | Error |
|
||||
|-----------|------------|-------|
|
||||
| Aniyomi: AniList | yuzono | dev.k3vinb5.aniyomibridge.exceptions.UnyoCoreException: Fail |
|
||||
| Aniyomi: Cimaleek | yuzono | HTTP error 404 |
|
||||
| Aniyomi: HentaiMama | yuzono | IllegalArgumentException |
|
||||
| Aniyomi: WcoForever | yuzono | dev.k3vinb5.aniyomibridge.exceptions.UnyoCoreException: Fail |
|
||||
| Aniyomi: WcoTv | yuzono | dev.k3vinb5.aniyomibridge.exceptions.UnyoCoreException: Fail |
|
||||
| Aniyomi: WcoAnimeDub | yuzono | dev.k3vinb5.aniyomibridge.exceptions.UnyoCoreException: Fail |
|
||||
| Aniyomi: Wcofun | yuzono | dev.k3vinb5.aniyomibridge.exceptions.UnyoCoreException: Fail |
|
||||
| Aniyomi: WcoAnimeSub | yuzono | dev.k3vinb5.aniyomibridge.exceptions.UnyoCoreException: Fail |
|
||||
| Aniyomi: WCOStream | yuzono | dev.k3vinb5.aniyomibridge.exceptions.UnyoCoreException: Fail |
|
||||
| Aniyomi: Mapple | yuzono | dev.k3vinb5.aniyomibridge.exceptions.UnyoCoreException: Fail |
|
||||
| Aniyomi: Moflix-Stream | yuzono | dev.k3vinb5.aniyomibridge.exceptions.UnyoCoreException: Fail |
|
||||
| Aniyomi: SupJav | yuzono | Cloudflare bypass failed |
|
||||
| Aniyomi: JetAnime | yuzono | IllegalArgumentException |
|
||||
| Aniyomi: MissAV | kohiden | IllegalArgumentException |
|
||||
| Aniyomi: Cimaleek | kohiden | HTTP error 404 |
|
||||
| Aniyomi: AllAnime | kohiden | JSON parsing error |
|
||||
| Aniyomi: HentaiMama | kohiden | IllegalArgumentException |
|
||||
| Aniyomi: AnimePahe | kohiden | AbstractMethodError |
|
||||
| Aniyomi: KissKH | kohiden | HTTP error 403 |
|
||||
| Aniyomi: Anime-Loads | kohiden | dev.k3vinb5.aniyomibridge.exceptions.UnyoCoreException: Fail |
|
||||
| Aniyomi: Moflix-Stream | kohiden | dev.k3vinb5.aniyomibridge.exceptions.UnyoCoreException: Fail |
|
||||
| Aniyomi: Latanime | kohiden | dev.k3vinb5.aniyomibridge.exceptions.UnyoCoreException: Fail |
|
||||
| Aniyomi: Türk Anime TV | kohiden | IllegalArgumentException |
|
||||
| Aniyomi: JetAnime | kohiden | IllegalArgumentException |
|
||||
|
||||
## Notes
|
||||
|
||||
- **Test Environment**: Results are highly dependent on the test runner's network conditions, DNS resolution, and geographic location.
|
||||
- **Cloudflare**: Many failures are due to Cloudflare protection. These extensions may work fine in the actual Unyo app with proper WebView support.
|
||||
- **Configuration Required**: Some extensions (e.g., Google Drive, Jellyfin) require user settings that were not provided during the automated test.
|
||||
- **Site Changes**: Extensions relying on third-party websites may break when those sites change their layout or domain.
|
||||
- **API Mismatches**: Errors like `NoSuchMethodError` or `AbstractMethodError` indicate the extension was built against a different version of the Aniyomi/Tachiyomi API than what the bridge provides.
|
||||
|
||||
*Report generated automatically from batch test results.*
|
||||
@@ -2,54 +2,166 @@
|
||||
title: Extensions
|
||||
---
|
||||
|
||||
# Extensions
|
||||
# Unyo Extension System Documentation
|
||||
|
||||
Unyo uses the Aniyomi extension ecosystem to provide access to hundreds of anime and manga sources. There are two types of extensions:
|
||||
## Overview
|
||||
|
||||
## Remote Extensions
|
||||
Unyo uses a dual extension system to provide content sources for anime and manga. Extensions are based on the [Aniyomi](https://github.com/aniyomiorg/aniyomi) and [Tachiyomi](https://github.com/tachiyomiorg/tachiyomi) ecosystems and are loaded as Android `.apk` files executed locally via the `AniyomiBridge` process.
|
||||
|
||||
Remote extensions are powered by a custom backend API that runs [Aniyomi](https://github.com/aniyomiorg/aniyomi) extensions on the server. This means:
|
||||
There are two extension categories:
|
||||
|
||||
- **No additional setup needed** — they work out of the box.
|
||||
- **Server-dependent** — availability depends on the server's uptime and location.
|
||||
- **Fewer extensions** — not all extensions work in a remote environment.
|
||||
| Category | Type Constant | Content |
|
||||
|----------|--------------|---------|
|
||||
| `ANIYOMI` | `ExtensionType.ANIYOMI` | Anime sources |
|
||||
| `TACHIYOMI` | `ExtensionType.TACHIYOMI` | Manga sources |
|
||||
|
||||
If you want a source added, open an issue on the [Unyo GitHub](https://github.com/K3vinb5/Unyo/issues) with the extension name, and it will be evaluated for integration.
|
||||
---
|
||||
|
||||
## Local Extensions
|
||||
## Extension Repositories
|
||||
|
||||
Local extensions run the extension API directly on your computer using a modular system, also based on Aniyomi extensions.
|
||||
Extensions are fetched from remote JSON index files. The default repository URLs are:
|
||||
|
||||
### Advantages
|
||||
| Type | Default URL |
|
||||
|---------|-------------|
|
||||
| Anime (Aniyomi) | `https://gitea.k3vinb5.dev/Backups/kohi-den-extensions/raw/branch/main/index.min.json` |
|
||||
| Manga (Tachiyomi) | `https://gitea.k3vinb5.dev/Backups/keiyoushi-extensions/raw/branch/repo/index.min.json` |
|
||||
|
||||
- **Faster response times** — no network round-trip to a remote server.
|
||||
- **More extensions** — many extensions that don't work remotely work locally.
|
||||
- **No server dependency** — works even if the remote backend is down.
|
||||
These URLs are stored per-user in `SettingsModel` and can be changed in the app's Settings screen.
|
||||
|
||||
### Disadvantages
|
||||
### Changing the Repository URL
|
||||
|
||||
- **Requires Java 17+** — you need Java 17 or later installed on your machine.
|
||||
- **More RAM usage** — approximately 100–200 MB additional memory consumption.
|
||||
1. Open **Settings** in Unyo.
|
||||
2. Find the **Aniyomi extensions repository URL** or **Tachiyomi extensions repository URL** field.
|
||||
3. Enter the URL to your custom `index.min.json` file.
|
||||
|
||||
### Java Installation
|
||||
The URL must point to a JSON index file with the following structure (for anime):
|
||||
|
||||
If you don't have Java 17+ installed, download it here:
|
||||
```json
|
||||
{
|
||||
"list": [
|
||||
{
|
||||
"name": "Aniyomi: Source Name",
|
||||
"pkg": "eu.kanade.tachiyomi.animeextension.en.sourcename",
|
||||
"apk": "tachiyomi-en.sourcename-v1.0.0.apk",
|
||||
"lang": "en",
|
||||
"version": "1.0.0",
|
||||
"nsfw": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Platform | Download |
|
||||
|----------|----------|
|
||||
| Linux | [oracle.com/java/technologies/downloads/#jdk17-linux](https://www.oracle.com/java/technologies/downloads/#jdk17-linux) |
|
||||
| macOS | [oracle.com/java/technologies/downloads/#jdk17-mac](https://www.oracle.com/java/technologies/downloads/#jdk17-mac) |
|
||||
| Windows | [oracle.com/java/technologies/downloads/#jdk17-windows](https://www.oracle.com/java/technologies/downloads/#jdk17-windows) |
|
||||
The APK files must be hosted at `<base_url>/apk/<apk_filename>` and icons at `<base_url>/icon/<pkg>.png`, relative to the `index.min.json` URL.
|
||||
|
||||
### Installing Local Extensions
|
||||
---
|
||||
|
||||
1. Open Unyo and go to **Settings → Extensions**.
|
||||
2. Enable **Local Extensions**.
|
||||
3. Browse available extensions from the [Unyo-Extensions](https://github.com/K3vinb5/Unyo-Extensions) repository directly within the app.
|
||||
4. Install the ones you want.
|
||||
## Installing Extensions (In-App)
|
||||
|
||||
## Which Should I Use?
|
||||
The **Extensions screen** provides a tabbed interface with four tabs:
|
||||
|
||||
If you have Java 17+ installed, **local extensions** are recommended for the best experience — faster, more reliable, and more sources available.
|
||||
1. **Available Anime** — Anime extensions available to install
|
||||
2. **Available Manga** — Manga extensions available to install
|
||||
3. **Installed Anime** — Currently installed anime extensions
|
||||
4. **Installed Manga** — Currently installed manga extensions
|
||||
|
||||
If you prefer a simpler setup or can't install Java, **remote extensions** work great and require no extra configuration.
|
||||
### To install an extension:
|
||||
|
||||
1. Navigate to the **Extensions** screen from the sidebar.
|
||||
2. Select the **Available Anime** or **Available Manga** tab.
|
||||
3. Find the extension you want and click the **download** button.
|
||||
4. The extension is downloaded, saved locally, and loaded into the `AniyomiBridge` process automatically.
|
||||
|
||||
### To remove an extension:
|
||||
|
||||
1. Navigate to the **Installed Anime** or **Installed Manga** tab.
|
||||
2. Click the **delete** button on the extension you want to remove.
|
||||
3. The extension is unloaded from the bridge and deleted from local storage.
|
||||
|
||||
---
|
||||
|
||||
## How Extensions Work Internally
|
||||
|
||||
### Extension Data Model
|
||||
|
||||
Each extension is represented by an `ExtensionModel` with the following fields:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-----------|----------|-------------|
|
||||
| `name` | `String` | Display name of the extension |
|
||||
| `pkg` | `String` | Unique package identifier (e.g., `eu.kanade.tachiyomi.animeextension.en.sourcename`) |
|
||||
| `apk` | `String` | Full URL to the `.apk` file |
|
||||
| `icon` | `String` | URL to the extension's icon image |
|
||||
| `lang` | `String` | Language code (e.g., `en`, `ja`) |
|
||||
| `version` | `String` | Version string (e.g., `1.0.0`) |
|
||||
| `nsfw` | `int` | `0` = SFW, `1` = NSFW |
|
||||
| `type` | `String` | `"Aniyomi"` or `"Tachiyomi"` |
|
||||
|
||||
### Installation Flow
|
||||
|
||||
```
|
||||
User clicks Install
|
||||
|
|
||||
v
|
||||
ExtensionsCubit.downloadExtension()
|
||||
|
|
||||
v
|
||||
ExtensionRepositoryAniyomi.addExtension()
|
||||
|
|
||||
+---> Hive.put("${name}-${version}", extension) [persists metadata]
|
||||
|
|
||||
+---> AniyomiBridge.loadAnimeExtension(apkUrl) [loads into JVM process]
|
||||
```
|
||||
|
||||
Extensions are persisted in a Hive box named `aniyomiExtensions` using the key format `${name}-${version}`. On app startup, all previously installed extensions are automatically reloaded into the `AniyomiBridge`.
|
||||
|
||||
### Removal Flow
|
||||
|
||||
```
|
||||
User clicks Remove
|
||||
|
|
||||
v
|
||||
ExtensionsCubit.removeExtension()
|
||||
|
|
||||
v
|
||||
ExtensionRepositoryAniyomi.removeExtension()
|
||||
|
|
||||
+---> Hive.delete("${name}-${version}")
|
||||
|
|
||||
+---> AniyomiBridge.unloadAnimeExtension(shortPkg, version)
|
||||
```
|
||||
|
||||
The `shortPkg` passed to the bridge is the last segment of the package name (e.g., `sourcename` from `eu.kanade.tachiyomi.animeextension.en.sourcename`).
|
||||
|
||||
---
|
||||
|
||||
## Architecture Summary
|
||||
|
||||
```
|
||||
ExtensionsScreen (UI)
|
||||
└── ExtensionsCubit (State Management)
|
||||
└── ExtensionRepositoryAniyomi (Data Layer)
|
||||
├── Remote JSON index → fetches available extensions
|
||||
├── Hive Box → persists installed extensions
|
||||
└── AniyomiBridge → executes extension APKs (Java process)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Adding a Custom Extension Repository
|
||||
|
||||
If you want to host your own extension repository:
|
||||
|
||||
1. Create an `index.min.json` file following the structure shown above.
|
||||
2. Host the APK files at `<your_base_url>/apk/`.
|
||||
3. Host the icon PNGs at `<your_base_url>/icon/` named `<pkg>.png`.
|
||||
4. In Unyo Settings, update the repository URL to point to your `index.min.json`.
|
||||
|
||||
> Extensions must be valid Aniyomi (anime) or Tachiyomi (manga) `.apk` extensions compatible with the `k3vinb5_aniyomi_bridge` package.
|
||||
|
||||
---
|
||||
|
||||
## Requesting a New Extension
|
||||
|
||||
If you want a specific source added to the default repository, open an issue on the [Unyo GitHub repository](https://github.com/K3vinb5/Unyo/issues) with the name of the Aniyomi/Tachiyomi extension you'd like integrated.
|
||||
|
||||
> Note: Not all Aniyomi extensions are guaranteed to work due to differences between the Android and desktop environments. You can look at the current [extension compatibility status](./extensions-compatibility) to see which extensions have been tested.
|
||||
|
||||
@@ -2,41 +2,147 @@
|
||||
title: Getting Started
|
||||
---
|
||||
|
||||
# Getting Started
|
||||
# Getting Started with Unyo
|
||||
|
||||
Once you've installed Unyo, here's how to get the most out of it.
|
||||
## System Requirements
|
||||
|
||||
| Requirement | Details |
|
||||
|---|---|
|
||||
| **OS** | Windows, macOS, or Linux (x64 / ARM64) |
|
||||
| **Minimum window size** | 1200 × 675 px |
|
||||
|
||||
> ARM64 is supported on Linux and macOS. Windows ARM64 is not currently supported.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
Download the latest release from the [GitHub Releases](https://github.com/K3vinb5/Unyo/releases/latest) page.
|
||||
|
||||
### Windows
|
||||
|
||||
| Package | How to use |
|
||||
|---|---|
|
||||
| `unyo-{version}-windows-setup.exe` | Run the Inno Setup installer and follow the wizard |
|
||||
| `Unyo-{version}-windows.zip` | Extract and run `unyo.exe` |
|
||||
|
||||
### macOS
|
||||
|
||||
| Package | How to use |
|
||||
|---|---|
|
||||
| `Unyo-{version}-macos.dmg` | Open the DMG and drag `unyo.app` to Applications |
|
||||
| `Unyo-{version}-macos.zip` | Extract and run the `.app` bundle |
|
||||
|
||||
> On first launch, macOS may block the app since it is not code-signed. Right-click the app and choose **Open** to bypass Gatekeeper.
|
||||
|
||||
### Linux
|
||||
|
||||
| Package | How to use |
|
||||
|---|---|
|
||||
| `Unyo-{version}-linux.zip` | Extract and run the `unyo` executable |
|
||||
| `Unyo-{version}-linux.AppImage` | `chmod +x` then run directly |
|
||||
| `unyo-{version}-linux.deb` | `sudo apt install ./unyo-{version}-linux.deb` |
|
||||
|
||||
---
|
||||
|
||||
## First Launch
|
||||
|
||||
When you open Unyo for the first time, you'll be greeted by the home screen. From here you can browse anime, manga, and manage your watchlist.
|
||||
When Unyo starts, it runs the following bootstrap sequence before showing the UI:
|
||||
|
||||
## AniList Login
|
||||
```
|
||||
main()
|
||||
├── setupLocator() — register services, notifiers, repositories, cubits
|
||||
├── Hive.init() + registerAdapters()
|
||||
├── setupLocatorAfterHiveInit() — register ExtensionRepositoryAniyomi
|
||||
├── Set minimum window size (1200×675)
|
||||
└── Navigate to /login (initial route)
|
||||
```
|
||||
|
||||
Unyo integrates tightly with AniList to sync your watch and read progress.
|
||||
The app renders at a base design resolution of **1280×720 px** and scales all UI elements proportionally.
|
||||
|
||||
1. Open **Settings** from the sidebar.
|
||||
2. Navigate to the **Account** section.
|
||||
3. Click **Login with AniList**.
|
||||
4. Authorize Unyo in your browser.
|
||||
---
|
||||
|
||||
:::important
|
||||
Your AniList profile must be set to **Public** or **Semi-Public** for Unyo to pull your anime and manga lists. Private profiles are currently not supported.
|
||||
:::
|
||||
## Adding an Account
|
||||
|
||||
## MyAnimeList (MAL) Sync
|
||||
On the Login screen, click the **+** button to open the account creation dialog. Three account types are available:
|
||||
|
||||
Unyo also supports syncing with MyAnimeList. Both services can be active simultaneously — your progress will be synced to whichever tracker you prefer.
|
||||
### AniList *(Recommended)*
|
||||
|
||||
## Browsing Content
|
||||
AniList uses OAuth 2.0 with a local loopback server:
|
||||
|
||||
- **Anime** — Browse trending and popular anime, or search for a specific title. Tap any title to see details, episodes, and available sources.
|
||||
- **Manga** — Switch to the manga tab to browse and read manga with the same smooth experience.
|
||||
1. Select **AniList** in the dialog and click **Confirm**.
|
||||
2. Unyo starts a local HTTP server on `localhost:9999` and opens your system browser to the AniList authorization page.
|
||||
3. Authorize the app on AniList — it redirects back to `http://localhost:9999/auth` with an authorization code.
|
||||
4. Unyo exchanges the code for an access token, fetches your profile, and saves the account to local storage.
|
||||
5. The local server shuts down automatically after the callback is handled.
|
||||
|
||||
## Settings
|
||||
Your account is stored in a Hive box (`anilistUsers`) and persists across restarts. On subsequent logins, Unyo checks whether your access token has expired and refreshes it automatically using the stored authorization code.
|
||||
|
||||
Unyo offers a range of customization options:
|
||||
### MyAnimeList
|
||||
|
||||
- **Theme** — Switch between light and dark mode.
|
||||
- **Default source** — Set your preferred anime or manga source.
|
||||
- **Notifications** — Enable or disable push notifications for new episodes.
|
||||
- **Extensions** — Manage your remote and local extensions.
|
||||
> **Not yet implemented.** The account type card is visible in the UI but selecting it and confirming does nothing.
|
||||
|
||||
### Local Account
|
||||
|
||||
A local account requires no external service. It stores data only on your machine with no list synchronization. Select **Local Account** and click **Confirm**.
|
||||
|
||||
---
|
||||
|
||||
## Logging In
|
||||
|
||||
After adding an account, it appears as an avatar on the Login screen. Click the avatar to log in.
|
||||
|
||||
On login, Unyo:
|
||||
1. Verifies the AniList token is not expired (and refreshes it if needed).
|
||||
2. Extracts colors from your AniList banner image to generate the UI theme (if **Use wallpaper as theme color** is enabled in settings).
|
||||
3. Navigates to the main `/tabs` view.
|
||||
|
||||
---
|
||||
|
||||
## Main Navigation
|
||||
|
||||
After logging in, the app is organized into the following tabs (accessible from the sidebar):
|
||||
|
||||
| Tab | Route | Description |
|
||||
|---|---|---|
|
||||
| **Home** | `/tabs/home` | Currently watching/reading list |
|
||||
| **Anime** | `/tabs/anime` | Browse and discover anime |
|
||||
| **Manga** | `/tabs/manga` | Browse and discover manga |
|
||||
| **Extensions** | `/tabs/extensions` | Install and manage content source extensions |
|
||||
| **Settings** | `/tabs/settings` | Configure the application |
|
||||
|
||||
Additional screens are accessible from within the app:
|
||||
|
||||
| Screen | Route |
|
||||
|---|---|
|
||||
| Media List | `/userlist` |
|
||||
| Calendar | `/calendar` |
|
||||
| Anime Search | `/animesearch` |
|
||||
| Manga Search | `/mangasearch` |
|
||||
| Anime Details | `/animedetails` |
|
||||
| Manga Details | `/mangadetails` |
|
||||
| Video Player | `/video` |
|
||||
|
||||
---
|
||||
|
||||
## Setting Up Extensions
|
||||
|
||||
Extensions are the content sources for anime and manga. Go to the **Extensions** tab to install them.
|
||||
|
||||
- **Available Anime** — browse and install anime sources (Aniyomi-compatible)
|
||||
- **Available Manga** — browse and install manga sources (Tachiyomi-compatible)
|
||||
- **Installed Anime / Installed Manga** — manage what is already installed
|
||||
|
||||
See the [Extension System documentation](./extensions.md) for full details.
|
||||
|
||||
---
|
||||
|
||||
## Localization
|
||||
|
||||
Currently, only **English (en-GB)** is supported. The locale is set at startup and cannot be changed at runtime.
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Extension System](./extensions.md) — how to install, remove, and configure custom extension repositories
|
||||
|
||||
140
website/docs/metadata-providers.md
Normal file
140
website/docs/metadata-providers.md
Normal file
@@ -0,0 +1,140 @@
|
||||
---
|
||||
title: Metadata Providers
|
||||
---
|
||||
|
||||
# Metadata Providers Documentation
|
||||
|
||||
Unyo uses two independent provider systems for metadata:
|
||||
|
||||
- **Media Metadata Providers** — supply anime and manga titles, covers, descriptions, scores, and list data
|
||||
- **Episode Metadata Providers** — supply episode-level data (titles, thumbnails, air dates, skip timings)
|
||||
|
||||
Both are configurable per-user in **Settings → Common**.
|
||||
|
||||
---
|
||||
|
||||
## Media Metadata Providers
|
||||
|
||||
Controlled by the `Service` enum in `lib/core/enums/service.dart`. The active service is stored in `SettingsModel.service` and defaults to `anilist`.
|
||||
|
||||
Changing the service triggers a `ReloadType.newMetadataService` event, which causes all screens that display media data to reload.
|
||||
|
||||
### Provider Overview
|
||||
|
||||
| Provider | Enum Value | Status | Title Languages |
|
||||
|---|---|---|---|
|
||||
| **AniList** | `Service.anilist` | Fully implemented | `Romaji`, `English`, `Native`, `UserPreferred` |
|
||||
| **MyAnimeList** | `Service.mal` | Planned (stub) | *(none)* |
|
||||
| **Kitsu** | `Service.kitsu` | Planned (stub) | *(none)* |
|
||||
| **Shikimori** | `Service.shikimori` | Planned (stub) | *(none)* |
|
||||
| **Simkl** | `Service.simkl` | Planned (stub) | *(none)* |
|
||||
|
||||
---
|
||||
|
||||
### AniList *(Implemented)*
|
||||
|
||||
- **Repository:** `AnimeRepositoryAnilist` / `MangaRepositoryAnilist`
|
||||
- **Protocol:** GraphQL via `GraphQLService`
|
||||
- **Endpoint:** `https://graphql.anilist.co`
|
||||
- **Authentication:** Bearer token (OAuth 2.0, required for list mutations and private data)
|
||||
|
||||
**Supported operations:**
|
||||
|
||||
| Operation | Method |
|
||||
|---|---|
|
||||
| Trending anime/manga | `getTrendingAnimes` / `getTrendingMangas` |
|
||||
| Popular anime/manga | `getPopularAnimes` / `getPopularMangas` |
|
||||
| Recently released | `getRecentlyReleasedAnimes` |
|
||||
| Recently completed | `getRecentlyCompletedAnimes` / `getRecentlyCompletedMangas` |
|
||||
| Upcoming | `getUpcomingAnimes` / `getUpcomingMangas` |
|
||||
| Anime/manga details | `getAnimeDetails` / `getMangaDetails` |
|
||||
| Advanced search | `performAnimeAdvancedSearch` / `performMangaAdvancedSearch` |
|
||||
| Calendar releases | `getCalendarReleases` |
|
||||
| Media list entry (read) | `getMediaListEntry` |
|
||||
| Media list entry (write) | `updateMediaListEntry` |
|
||||
| User watching/reading list | `getUserWatchingList` / `getUserReadingList` |
|
||||
|
||||
---
|
||||
|
||||
### MyAnimeList, Kitsu, Shikimori, Simkl *(Planned)*
|
||||
|
||||
These providers are registered in the `Service` enum and appear in the Settings dropdown, but are **not yet implemented**. In all Cubit fetch methods, their `switch` cases are empty no-ops. Their `MediaService` subclasses (`MyAnimeListMediaService`, `KitsuMediaService`, `ShikimoriMediaService`, `SimklMediaService`) return an empty `titleLanguages` list, which is the indicator that no title language options are available.
|
||||
|
||||
Selecting any of these services will result in no data being loaded on the Anime, Manga, Home, or Calendar screens.
|
||||
|
||||
---
|
||||
|
||||
## Episode Metadata Providers
|
||||
|
||||
Controlled by the `EpisodeService` enum in `lib/core/enums/episode_service.dart`. The active service is stored in `SettingsModel.episodeService` and defaults to `anizip`.
|
||||
|
||||
Episode metadata is used in the anime details screen to display per-episode titles, thumbnails, and air dates, and to provide alternative cover images.
|
||||
|
||||
### Provider Overview
|
||||
|
||||
| Provider | Enum Value | Status | Title Languages |
|
||||
|---|---|---|---|
|
||||
| **AniZip** | `EpisodeService.anizip` | Fully implemented | `Native`, `English` |
|
||||
| **Kitsu** | `EpisodeService.kitsu` | Planned (stub) | *(none)* |
|
||||
|
||||
---
|
||||
|
||||
### AniZip *(Implemented)*
|
||||
|
||||
- **Repository:** `EpisodeRepositoryAnizip`
|
||||
- **Protocol:** HTTP REST via `HttpService`
|
||||
- **Endpoint:** `https://api.ani.zip` (configured via `config.anizipBaseEndpoint`)
|
||||
|
||||
AniZip supports lookup by either AniList ID or MAL ID. When the active media service is AniList, the AniList ID is used directly. For all other services, the MAL ID from the media entity is used as a fallback.
|
||||
|
||||
**Supported operations:**
|
||||
|
||||
| Operation | Method | Endpoint |
|
||||
|---|---|---|
|
||||
| Fetch episode info | `getEpisodeInfo` | `/mappings?anilist_id=<id>` or `/mappings?mal_id=<id>` |
|
||||
| Fetch alternative image | `getAlternativeImage` | `/mappings?anilist_id=<id>` or `/mappings?mal_id=<id>` |
|
||||
|
||||
---
|
||||
|
||||
### Kitsu *(Planned)*
|
||||
|
||||
`KitsuEpisodeMediaService` exists in `lib/core/services/media/episode_service.dart` but returns an empty `titleLanguages` list. All `EpisodeService.kitsu` cases in `AnimeDetailsCubit._getEpisodesDetails` and `_getAlternativeImage` are empty no-ops (log-only).
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
Both provider systems follow the same factory pattern:
|
||||
|
||||
```
|
||||
Settings (Service / EpisodeService enum)
|
||||
|
|
||||
v
|
||||
MediaServiceFactory / EpisodeMediaServiceFactory
|
||||
|
|
||||
+---> Returns MediaService / EpisodeMediaService
|
||||
| (provides titleLanguages for the Settings dropdown)
|
||||
|
|
||||
v
|
||||
Cubit switch(loggedUser.settings.service)
|
||||
|
|
||||
+---> Service.anilist → AnimeRepositoryAnilist / MangaRepositoryAnilist
|
||||
+---> EpisodeService.anizip → EpisodeRepositoryAnizip
|
||||
+---> All others → no-op (planned)
|
||||
```
|
||||
|
||||
The `titleLanguages` property on each `MediaService` / `EpisodeMediaService` subclass drives the **Default Media Title Language** and **Default Episode Title Language** dropdowns in Settings. An empty list means the provider has no title language options yet.
|
||||
|
||||
---
|
||||
|
||||
## Summary Table
|
||||
|
||||
| Provider | Type | Status | Setting Key |
|
||||
|---|---|---|---|
|
||||
| AniList | Media | Implemented | `Settings.service = anilist` |
|
||||
| MyAnimeList | Media | Planned | `Settings.service = mal` |
|
||||
| Kitsu | Media | Planned | `Settings.service = kitsu` |
|
||||
| Shikimori | Media | Planned | `Settings.service = shikimori` |
|
||||
| Simkl | Media | Planned | `Settings.service = simkl` |
|
||||
| AniZip | Episode | Implemented | `Settings.episodeService = anizip` |
|
||||
| Kitsu | Episode | Planned | `Settings.episodeService = kitsu` |
|
||||
214
website/docs/settings.md
Normal file
214
website/docs/settings.md
Normal file
@@ -0,0 +1,214 @@
|
||||
---
|
||||
title: Settings
|
||||
description: Full reference for all configurable options in Unyo.
|
||||
---
|
||||
|
||||
# Settings Documentation
|
||||
|
||||
The Settings screen is accessible from the sidebar. All settings are persisted per-user in local storage and take effect immediately unless noted otherwise.
|
||||
|
||||
---
|
||||
|
||||
## Accounts
|
||||
|
||||
> **Status:** Placeholder — account management settings are not yet implemented.
|
||||
|
||||
Sub-sections are reserved for:
|
||||
- **AniList** — manage connected AniList accounts
|
||||
- **MyAnimeList** — manage connected MAL accounts *(planned)*
|
||||
- **Shikimori** — manage connected Shikimori accounts *(planned)*
|
||||
|
||||
---
|
||||
|
||||
## Common
|
||||
|
||||
General application-wide settings.
|
||||
|
||||
### Media Metadata Service
|
||||
|
||||
| Property | Value |
|
||||
|---|---|
|
||||
| Type | Dropdown |
|
||||
| Default | `Anilist` |
|
||||
| Options | `Anilist`, `Mal`, `Kitsu`, `Shikimori`, `Simkl` |
|
||||
|
||||
Selects the service used to fetch anime and manga metadata (titles, covers, descriptions, scores, etc.). Changing this triggers a full data reload.
|
||||
|
||||
### Episode Metadata Service
|
||||
|
||||
| Property | Value |
|
||||
|---|---|
|
||||
| Type | Dropdown |
|
||||
| Default | `Anizip` |
|
||||
| Options | `Anizip`, `Kitsu` |
|
||||
|
||||
Selects the service used to fetch episode-level metadata (episode titles, thumbnails, air dates). This also determines the available options for **Default Episode Title Language**.
|
||||
|
||||
### Language Settings
|
||||
|
||||
#### Language
|
||||
|
||||
| Property | Value |
|
||||
|---|---|
|
||||
| Type | Dropdown |
|
||||
| Default | `English` |
|
||||
| Options | `English` |
|
||||
|
||||
Application UI language. Only English is currently available.
|
||||
|
||||
#### Default Media Title Language
|
||||
|
||||
| Property | Value |
|
||||
|---|---|
|
||||
| Type | Dropdown |
|
||||
| Default | `userPreferred` |
|
||||
| Options | Depends on the selected **Media Metadata Service** |
|
||||
|
||||
Controls the language used to display anime and manga titles throughout the app. The available options are provided by the active metadata service.
|
||||
|
||||
#### Default Episode Title Language
|
||||
|
||||
| Property | Value |
|
||||
|---|---|
|
||||
| Type | Dropdown |
|
||||
| Default | `en` |
|
||||
| Options | Depends on the selected **Episode Metadata Service** |
|
||||
|
||||
Controls the language used to display episode titles in the player and episode list.
|
||||
|
||||
### Discord RPC
|
||||
|
||||
| Property | Value |
|
||||
|---|---|
|
||||
| Type | Toggle |
|
||||
| Default | Enabled |
|
||||
|
||||
Enables or disables Discord Rich Presence. When enabled, Unyo broadcasts what you are currently watching or reading to your Discord status.
|
||||
|
||||
### NSFW Content
|
||||
|
||||
| Property | Value |
|
||||
|---|---|
|
||||
| Type | Toggle |
|
||||
| Default | Disabled |
|
||||
|
||||
Shows or hides adult (18+) content throughout the app, including in search results and media lists.
|
||||
|
||||
---
|
||||
|
||||
## Theme
|
||||
|
||||
Appearance settings for the UI color scheme.
|
||||
|
||||
### Choose Color
|
||||
|
||||
| Property | Value |
|
||||
|---|---|
|
||||
| Type | Dialog (color picker) |
|
||||
|
||||
Opens a color picker dialog. The selected color is used as the primary accent color for the entire UI theme. This overrides the wallpaper-based theme.
|
||||
|
||||
### Use Wallpaper as Theme Color
|
||||
|
||||
| Property | Value |
|
||||
|---|---|
|
||||
| Type | Toggle |
|
||||
| Default | Enabled |
|
||||
|
||||
When enabled, Unyo extracts the dominant colors from your AniList profile banner image and uses them to generate the UI theme automatically. Enabling this overrides any manually chosen color.
|
||||
|
||||
> **Note:** This setting only applies to AniList accounts. Local accounts are not affected.
|
||||
|
||||
---
|
||||
|
||||
## Player
|
||||
|
||||
Video player settings.
|
||||
|
||||
### Skip Opening Automatically
|
||||
|
||||
| Property | Value |
|
||||
|---|---|
|
||||
| Type | Toggle |
|
||||
| Default | Disabled |
|
||||
|
||||
When enabled, the player automatically skips the opening sequence of an episode when skip timing data is available (provided by AniSkip).
|
||||
|
||||
### Skip Ending Automatically
|
||||
|
||||
| Property | Value |
|
||||
|---|---|
|
||||
| Type | Toggle |
|
||||
| Default | Disabled |
|
||||
|
||||
When enabled, the player automatically skips the ending/credits sequence of an episode when skip timing data is available.
|
||||
|
||||
### Default Manual Skip Time
|
||||
|
||||
| Property | Value |
|
||||
|---|---|
|
||||
| Type | Slider |
|
||||
| Default | `85` seconds |
|
||||
| Range | `60` – `120` seconds |
|
||||
|
||||
Sets the number of seconds skipped when using the manual skip button in the player (e.g., for skipping openings manually when automatic skip data is unavailable).
|
||||
|
||||
### Enable Auto-Play Next Episode
|
||||
|
||||
| Property | Value |
|
||||
|---|---|
|
||||
| Type | Toggle |
|
||||
| Default | Disabled |
|
||||
|
||||
When enabled, the "play next episode" pop-up that appears at the end of an episode is automatically accepted without requiring user interaction.
|
||||
|
||||
### Enable OpenSubtitles.org Integration
|
||||
|
||||
| Property | Value |
|
||||
|---|---|
|
||||
| Type | Toggle |
|
||||
| Default | Disabled |
|
||||
|
||||
When enabled, Unyo attempts to automatically fetch subtitles from [OpenSubtitles.org](https://www.opensubtitles.org) for the currently playing video, when available.
|
||||
|
||||
---
|
||||
|
||||
## Reader
|
||||
|
||||
> **Status:** Placeholder — manga reader settings are not yet implemented.
|
||||
|
||||
---
|
||||
|
||||
## Extensions
|
||||
|
||||
Extension repository management. See the [Extension System documentation](./extensions.md) for full details.
|
||||
|
||||
### Aniyomi Extensions Repository
|
||||
|
||||
| Property | Value |
|
||||
|---|---|
|
||||
| Type | Dialog (text field) |
|
||||
| Default | `https://gitea.k3vinb5.dev/Backups/kohi-den-extensions/raw/branch/main/index.min.json` |
|
||||
|
||||
Sets the URL of the JSON index file used to populate the **Available Anime** extensions list. Must point to a valid `index.min.json` compatible with the Aniyomi extension format.
|
||||
|
||||
### Tachiyomi Extensions Repository
|
||||
|
||||
| Property | Value |
|
||||
|---|---|
|
||||
| Type | Dialog (text field) |
|
||||
| Default | `https://gitea.k3vinb5.dev/Backups/keiyoushi-extensions/raw/branch/repo/index.min.json` |
|
||||
|
||||
Sets the URL of the JSON index file used to populate the **Available Manga** extensions list. Must point to a valid `index.min.json` compatible with the Tachiyomi extension format.
|
||||
|
||||
---
|
||||
|
||||
## About
|
||||
|
||||
> **Status:** Placeholder — app information is not yet implemented.
|
||||
|
||||
---
|
||||
|
||||
## Settings Storage
|
||||
|
||||
All settings are stored per-user in a local [Hive CE](https://pub.dev/packages/hive_ce) database as part of the `SettingsModel`. Settings are saved immediately on change and survive app restarts. There is no "Save" button — every change is applied and persisted instantly.
|
||||
@@ -27,7 +27,7 @@ const config = {
|
||||
({
|
||||
docs: {
|
||||
sidebarPath: require.resolve('./sidebars.js'),
|
||||
editUrl: 'https://github.com/K3vinb5/Unyo/tree/rewrite/website',
|
||||
editUrl: 'https://gitea.k3vinb5.dev/Unyo/unyo-web/src/branch/master/website',
|
||||
},
|
||||
blog: false,
|
||||
theme: {
|
||||
@@ -57,12 +57,23 @@ const config = {
|
||||
label: 'Docs',
|
||||
position: 'left',
|
||||
},
|
||||
{
|
||||
href: 'https://kuma.k3vinb5.dev/status/unyo',
|
||||
label: 'Status',
|
||||
position: 'left',
|
||||
},
|
||||
{
|
||||
href: 'https://github.com/K3vinb5/Unyo',
|
||||
label: 'GitHub',
|
||||
position: 'right',
|
||||
className: 'header-github-link',
|
||||
},
|
||||
{
|
||||
href: 'https://gitea.k3vinb5.dev/Unyo',
|
||||
label: 'Gitea',
|
||||
position: 'right',
|
||||
className: 'header-gitea-link',
|
||||
},
|
||||
],
|
||||
},
|
||||
prism: {
|
||||
|
||||
@@ -3,10 +3,42 @@
|
||||
/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
|
||||
const sidebars = {
|
||||
docs: [
|
||||
'intro',
|
||||
'installation',
|
||||
'getting-started',
|
||||
'extensions',
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Getting Started',
|
||||
collapsed: false,
|
||||
items: [
|
||||
'intro',
|
||||
'installation',
|
||||
'getting-started',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Configuration',
|
||||
collapsed: false,
|
||||
items: [
|
||||
'settings',
|
||||
'metadata-providers',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Extensions',
|
||||
collapsed: false,
|
||||
items: [
|
||||
'extensions',
|
||||
'extensions-compatibility',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Reference',
|
||||
collapsed: false,
|
||||
items: [
|
||||
'architecture',
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ const screenshots = [
|
||||
{ src: '/img/screenshots/settings.png', alt: 'Unyo settings' },
|
||||
];
|
||||
|
||||
const MIN_INDEX = 2;
|
||||
const MAX_INDEX = screenshots.length - 3;
|
||||
const MIN_INDEX = 1;
|
||||
const MAX_INDEX = screenshots.length - 2;
|
||||
|
||||
function getChildWidth() {
|
||||
return window.innerWidth <= 600 ? 400 + 20 : 520 + 20;
|
||||
|
||||
@@ -68,6 +68,9 @@ body {
|
||||
/* ====== NAVBAR ====== */
|
||||
|
||||
.navbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 200;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-bottom: 1px solid rgba(168, 85, 247, 0.12);
|
||||
@@ -130,6 +133,34 @@ body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ====== GITEA LINK ====== */
|
||||
|
||||
.header-gitea-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.header-gitea-link::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' fill='%239b8ec4' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M18.5 3H6c-1.1 0-2 .9-2 2v5.71c0 3.83 2.95 7.18 6.78 7.29 3.96.12 7.22-3.06 7.22-7v-1h.5c1.93 0 3.5-1.57 3.5-3.5S20.43 3 18.5 3zM16 8.99c0 2.2-1.8 3.99-3.99 3.99-2.2 0-4-1.8-4-4V5h8v3.99zm2.5-1.99h-1.5v-2h1.5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5z'/%3E%3C/svg%3E");
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
transition: filter 0.2s ease;
|
||||
transform: translateY(2px);
|
||||
}
|
||||
|
||||
.header-gitea-link:hover::before {
|
||||
filter: brightness(1.4);
|
||||
}
|
||||
|
||||
.header-gitea-link svg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ====== SIDEBAR ====== */
|
||||
|
||||
.theme-doc-sidebar-container {
|
||||
@@ -224,6 +255,7 @@ body {
|
||||
.theme-doc-markdown th,
|
||||
.theme-doc-markdown td {
|
||||
border: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.theme-doc-markdown th {
|
||||
@@ -275,11 +307,9 @@ body {
|
||||
|
||||
/* ====== MOBILE MENU ====== */
|
||||
|
||||
html:not(:has(.theme-doc-sidebar-container)) .navbar__toggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navbar-sidebar {
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
background-color: var(--unyo-bg-surface);
|
||||
}
|
||||
|
||||
@@ -297,6 +327,26 @@ html:not(:has(.theme-doc-sidebar-container)) .navbar__toggle {
|
||||
font-size: 1rem;
|
||||
color: var(--unyo-text);
|
||||
}
|
||||
|
||||
.theme-doc-sidebar-container {
|
||||
z-index: 201;
|
||||
}
|
||||
}
|
||||
|
||||
/* ====== SCROLL TO TOP ====== */
|
||||
|
||||
.theme-back-to-top-button {
|
||||
background-color: #a855f7 !important;
|
||||
box-shadow: 0 4px 16px rgba(168, 85, 247, 0.35) !important;
|
||||
}
|
||||
|
||||
.theme-back-to-top-button:hover {
|
||||
background-color: #9333ea !important;
|
||||
box-shadow: 0 6px 24px rgba(168, 85, 247, 0.5) !important;
|
||||
}
|
||||
|
||||
.theme-back-to-top-button svg {
|
||||
fill: #0d0a1a !important;
|
||||
}
|
||||
|
||||
/* ====== OVERFLOW UTILITY ====== */
|
||||
|
||||
Reference in New Issue
Block a user