Skip to content

tachibana-shin/hoyomi

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

📱 Hoyomi — Your Ultimate Anime & Manga Companion

Hoyomi

Hoyomi is a modern, open-source app to watch anime and read manga in one seamless experience. Designed to be lightweight, fast, and fully cross-platform, Hoyomi runs smoothly on Android, iOS, Linux, macOS, Windows, and even on the web.

Warning

App is development

Multiple service support. Please check this:

3rd party services are now supported with the power of typescript. Check out the example here hoyomi_bridge_ts/example or hoyomi-plugin-animehay


✨ Features

  • 🎥 Stream anime with subtitle support (custom fonts, colors, styles)
  • 📚 Read manga from multiple sources in a clean, scrollable reader
  • 🌐 Multi-platform: Mobile, desktop & web support out of the box
  • 🎨 Highly customizable UI: Dark/light themes, font settings, subtitle styles
  • 💬 Offline support: Cache and view anime/manga offline (development)
  • 🧩 Plugin-friendly: Extend functionality with community-built integrations
  • 🔐 Privacy first: No tracking, no ads, no data collection

🍃 Download

Get the app from our releases page.

iOS Sideloading Sources

         

If your device runs iOS 14.0 beta 2 – 16.6.1, 16.7 RC (20H18), or 17.0, you’re eligible to be a first-class citizen of TrollStore 🚀. Install it for free, then enjoy Hoyomi with permanent installation — no resigning, no revokes, forever free.


🛡️ Open Source & Community Driven

Hoyomi is fully open-source, actively maintained by the community. We welcome contributions and ideas. You’re not just a user — you’re part of the project.


📦 Available On

  • ✅ Android
  • ✅ iOS
  • ✅ Windows (.exe)
  • ✅ Linux (.deb, .AppImage)
  • ✅ macOS (.dmg)
  • ✅ Web (PWA)

📝 Privacy & EULA

Hoyomi does not collect, store, or share your personal data. Your activity stays on your device. See our full Privacy Policy and EULA for details.


❤️ Built with Flutter. Made with love in Japan-inspired spirit.

Screenshot preview

Support platform

  • Android
  • Isar for android SDK <= 23
  • iOS side via TrollStore
  • iOS side by other methods

Creating New Plugins

Hoyomi's architecture allows for easy extension through custom plugins written in TypeScript. You can create new comic or eiga services by following these general steps:

  1. Define your service interface: Create a TypeScript file that defines the methods and data structures for your comic or eiga service, adhering to the hoyomi_bridge_ts conventions.
  2. Implement the service logic: Write the actual logic for fetching data, parsing content, and handling interactions within your TypeScript service.
  3. Register your plugin: Use the hoyomi_bridge_ts to register your implemented service, making it available to the Hoyomi application.

Refer to the existing examples for detailed implementation patterns:

Here's a simplified example of an Eiga plugin:

// example_eiga_plugin.ts
import {
  ABEigaService,
  createOImage,
  registerPlugin,
  StatusEnum,
  type EigaCategory,
  type EigaEpisode,
  type EigaEpisodes,
  type EigaHome,
  type MetaEiga,
  type ServerSource,
  type ServiceInit,
  type SourceVideo
} from "hoyomi_bridge_ts";

class MyCustomEigaService extends ABEigaService {
  override init: ServiceInit = {
    name: "My Custom Eiga",
    faviconUrl: createOImage("https://example.com/favicon.ico"),
    rootUrl: "https://example.com"
  };

  async getURL(eigaId: string, chapterId?: string): Promise<string> {
    // Logic to get the URL for a specific eiga or episode
    return `https://example.com/eiga/${eigaId}`;
  }

  async home(): Promise<EigaHome> {
    // Logic to fetch home page data (categories, popular eiga, etc.)
    return {
      categories: [
        {
          name: "Popular Eiga",
          items: [
            {
              name: "Example Eiga 1",
              eigaId: "example-1",
              image: createOImage("https://example.com/image1.jpg")
            }
          ]
        }
      ]
    };
  }

  async getCategory(params: {
    categoryId: string;
    page: number;
    filters: { [key: string]: string[] | null };
  }): Promise<EigaCategory> {
    // Logic to fetch eiga within a specific category
    return {
      name: "Category Name",
      url: "",
      items: [],
      page: params.page,
      totalItems: 0,
      totalPages: 0
    };
  }

  async getDetails(eigaId: string): Promise<MetaEiga> {
    // Logic to fetch detailed information about an eiga
    return {
      name: "Example Eiga Details",
      image: createOImage("https://example.com/details.jpg"),
      status: StatusEnum.Ongoing,
      genres: ["Action", "Adventure"],
      description: "A brief description of the eiga.",
      seasons: []
    };
  }

  async getEpisodes(eigaId: string): Promise<EigaEpisodes> {
    // Logic to fetch episodes for an eiga
    return {
      episodes: [
        {
          name: "Episode 1",
          episodeId: "ep-1"
        }
      ]
    };
  }

  async getSource(params: {
    eigaId: string;
    episode: EigaEpisode;
    server?: ServerSource;
  }): Promise<SourceVideo> {
    // Logic to get video source for an episode
    return {
      src: "https://example.com/video.mp4",
      url: "https://example.com/video.mp4",
      type: "video/mp4"
    };
  }

  async search(params: {
    keyword: string;
    page: number;
    filters: { [key: string]: string[] | null };
    quick: boolean;
  }): Promise<EigaCategory> {
    // Logic to search for eiga
    return {
      name: `Search results for "${params.keyword}"`,
      url: "",
      items: [],
      page: params.page,
      totalItems: 0,
      totalPages: 0
    };
  }
}

registerPlugin(MyCustomEigaService);

Here's a simplified example of a Comic plugin:

// example_comic_plugin.ts
import {
  ABComicService,
  ComicModes,
  createOImage,
  registerPlugin,
  StatusEnum,
  type ComicCategory,
  type ComicHome,
  type MetaComic,
  type OImage,
  type ServiceInit
} from "hoyomi_bridge_ts";

class MyCustomComicService extends ABComicService {
  override init: ServiceInit = {
    name: "My Custom Comic",
    faviconUrl: createOImage("https://example.com/comic-favicon.ico"),
    rootUrl: "https://example.com/comic"
  };

  async getURL(comicId: string, chapterId?: string): Promise<string> {
    // Logic to get the URL for a specific comic or chapter
    return `https://example.com/comic/${comicId}`;
  }

  async home(): Promise<ComicHome> {
    // Logic to fetch home page data (categories, popular comics, etc.)
    return {
      categories: [
        {
          name: "Popular Comics",
          items: [
            {
              name: "Example Comic 1",
              comicId: "comic-1",
              image: createOImage("https://example.com/comic1.jpg")
            }
          ]
        }
      ]
    };
  }

  async getCategory(params: {
    categoryId: string;
    page: number;
    filters: { [key: string]: string[] | null };
  }): Promise<ComicCategory> {
    // Logic to fetch comics within a specific category
    return {
      name: "Comic Category Name",
      url: "",
      items: [],
      page: params.page,
      totalItems: 0,
      totalPages: 0
    };
  }

  async getDetails(comicId: string): Promise<MetaComic> {
    // Logic to fetch detailed information about a comic
    return {
      name: "Example Comic Details",
      image: createOImage("https://example.com/comic-details.jpg"),
      status: StatusEnum.Ongoing,
      genres: ["Fantasy", "Adventure"],
      description: "A brief description of the comic.",
      chapters: [
        {
          name: "Chapter 1",
          chapterId: "ch-1",
          time: new Date(),
          order: 1,
        }
      ],
      lastModified: new Date()
    };
  }

  async getPages(comicId: string, chapterId: string): Promise<OImage[]> {
    // Logic to get image pages for a specific chapter
    return [
      createOImage("https://example.com/comic/page1.jpg"),
      createOImage("https://example.com/comic/page2.jpg"),
    ];
  }

  async search(params: {
    keyword: string;
    page: number;
    filters: { [key: string]: string[] | null };
    quick: boolean;
  }): Promise<ComicCategory> {
    // Logic to search for comics
    return {
      name: `Search results for "${params.keyword}"`,
      url: "",
      items: [],
      page: params.page,
      totalItems: 0,
      totalPages: 0
    };
  }

  getComicModes(comic: MetaComic): ComicModes {
    // Define how the comic should be read (e.g., leftToRight, rightToLeft, webtoon)
    return ComicModes.leftToRight;
  }
}

registerPlugin(MyCustomComicService);

Todo

  • Add background image for details_comic

  • Add information book for reader

  • Fix logic fake page

  • Page eiga details

  • Fix zoomer read manga

  • Responsive for video player

  • AppBar all page

  • API comment for eiga

  • API follow anime

  • API notification

  • A11y manga reader

  • API playlist

  • API playlist online

  • Search icon for all section

  • Bottom sheet show all options

  • Add multiple server in eiga

Development

Prerequisites

Step 1: Set up the Firebase project

The first step is to set up the Firebase project and enable Google sign-in. if you already have a flutter project, you can skip this step.

  1. Go to the Firebase console and create a new project.
  2. Click on the Authentication link in the left-hand menu, then click on the Sign-in Method tab.
  3. Enable the Google sign-in method.

Step 2: Configure the OAuth client

You need your application's client ID and Secret from Google Cloud Console to enable Google sign-in. If you’ve it already then skip this step.

  • To get the client ID and secret, follow the steps from the given link.
  • Choose a web application.
  • In the Authorized redirect URIs and Authorized JavaScript origin, enter the URL http://localhost

Setup Serverless

To deploy the serverless application, you need to set up a serverless provider. Here are the steps to set up Deno:

Step 1: Configure the Firebase Admin

Please goto Project Settings image

  1. Click to Generate new private key
  2. Paste file download to serverless/service-account-key.json

Step 2: Configre the Postgres database

The server required database for working

  1. Run cd serverless
  2. Add DATABASE_URL from setting project to .env

Setup Application

Step 1 (Required in mobile platform)

Tip

This project depends Firebase. Please first run

flutterfire configure

and configuring file /android/app/google-services.json, /ios/Runner/GoogleService-Info.plist (two file auto create by flutterfire)

  • /android/app/google-services.json required for Android
  • /ios/Runner/GoogleService-Info.plist required for iOS

Tip

NOTE (If you development for iOS)

Please edit CFBundleURLSchemes in ios/Runner/Info.plist

Step 2 (Required in desktop platform)

Goto https://console.cloud.google.com/apis/credentials and get Client ID and Client secret from OAuth 2.0 Client IDs (Use Web application)

Set to .env

GOOGLE_CLIENT_ID=<Client ID>
GOOGLE_CLIENT_SECRET=<Client secret>

Step 3

Set to .env

BASE_API_GENERAL=<URL base API general serverless>

GitHub Actions

To release the application, the following secrets must be provided:

General

  • ENV_CONTENT - Content of the .env file. (not encode base64)

For Android

  • KEYSTORE_CONTENT - Base64-encoded content of the keystore.jks file.
  • KEYSTORE_PASSWORD - Password used to sign the keystore.jks file.
  • KEYSTORE_ALIAS - Alias used to sign the keystore.jks file.
  • GOOGLE_SERVICES_JSON - Base64-encoded content of the google-services.json file (automatically generated by flutterfire).

For iOS

  • GOOGLE_SERVICE_INFO_PLIST - Base64-encoded content of the GoogleService-Info.plist file (automatically generated by flutterfire).

Note: The google-services.json (for Android) and GoogleService-Info.plist (for iOS) files are automatically created when you run the flutterfire configure command during Firebase setup.