Skip to content

Repository files navigation

@yui540/comimi-react

@yui540/comimi を React で扱うための薄いラッパーライブラリです。<MangaViewer /> コンポーネントと useMangaViewer() フックの2通りの使い方を提供します。

特徴

  • コンポーネント / フック両対応 — お好みのスタイルで使えます
  • 完全な型サポート — comimi の型を再エクスポート、TypeScript で安心して書けます
  • 薄いラッパー — 機能を一切削っていません。comimi の全 API にアクセス可能
  • React 18 / 19 両対応 — StrictMode 安全
  • ESM / CJS 両対応
  • 依存ゼロ — comimi と React/ReactDOM は peer dependency

インストール

npm install @yui540/comimi-react @yui540/comimi

react / react-dom / @yui540/comimi は peer dependency です。利用側プロジェクトで別途インストールしてください。

クイックスタート

import { MangaViewer, type Manga } from "@yui540/comimi-react";

const manga: Manga = {
  id: "sample",
  title: "サンプル漫画",
  author: "yui540",
  pages: [
    { id: "p0", type: "image", src: "/pages/0.webp" },
    { id: "p1", type: "image", src: "/pages/1.webp" },
    { id: "p2", type: "image", src: "/pages/2.webp" },
  ],
};

export function App() {
  return (
    <MangaViewer
      manga={manga}
      locale="ja"
      settings={{ readingDirection: "rtl", pageTurnMode: "spread" }}
      onPageChange={({ pageIndex }) => console.log("page", pageIndex)}
      style={{ width: "100%", minHeight: 600 }}
    />
  );
}

これだけで漫画ビューワーが起動します。キーボード・ジェスチャー・設定パネル・i18n などは comimi の機能をそのまま利用できます。

使い方の選択

向いているケース
<MangaViewer /> JSX に宣言的に書きたい / classNamestyle で見た目を整えたい / 基本的な操作で十分
useMangaViewer() レイアウトをより細かく制御したい / 既存の DOM 構造に組み込みたい / ビューワー以外の UI と密に連携したい

両者は内部実装を共有しており、機能差はありません。


<MangaViewer /> コンポーネント

基本

<MangaViewer manga={manga} settings={{ readingDirection: "rtl" }} />

HTMLDivElement の属性(className / style / id / aria-* など)はすべて外側の <div> に転送されます。

ref で命令的操作

import { useRef } from "react";
import { MangaViewer, type MangaViewerHandle } from "@yui540/comimi-react";

function Reader() {
  const viewerRef = useRef<MangaViewerHandle>(null);

  return (
    <>
      <div style={{ display: "flex", gap: 8 }}>
        <button onClick={() => viewerRef.current?.previousPage()}></button>
        <button onClick={() => viewerRef.current?.nextPage()}></button>
        <button onClick={() => viewerRef.current?.goToPage(0)}>最初</button>
      </div>
      <MangaViewer ref={viewerRef} manga={manga} />
    </>
  );
}

MangaViewerHandle は comimi の MangaViewerInstance と同じ型です。マウント完了前は null


useMangaViewer() フック

import { useMangaViewer } from "@yui540/comimi-react";

function Reader() {
  const { containerRef, viewer } = useMangaViewer({
    manga,
    settings: { readingDirection: "rtl" },
    onPageChange: ({ pageIndex }) => console.log(pageIndex),
  });

  return (
    <div>
      <button onClick={() => viewer?.nextPage()}>次へ</button>
      <div ref={containerRef} style={{ minHeight: 600 }} />
    </div>
  );
}
戻り値 説明
containerRef RefObject<HTMLDivElement | null> ビューワーをマウントする div に渡す ref
viewer MangaViewerInstance | null 命令的操作用のインスタンス。マウント完了前は null
portals ReactPortal[] React コンテンツページ(後述)を描画するポータル。JSX のどこかに必ずレンダーしてください

フックで React コンテンツページを使う場合は、portals を JSX にレンダーする必要があります(描画位置はどこでも構いません。実際の描画はページ内のホスト要素に対して行われます)。<MangaViewer /> を使う場合は自動で処理されます。

const { containerRef, portals } = useMangaViewer({ manga });
return (
  <div>
    <div ref={containerRef} style={{ minHeight: 600 }} />
    {portals}
  </div>
);

React コンポーネントをページに埋め込む

画像や HTML 文字列だけでなく、React コンポーネントをそのままページの中身として渡せますtype: "html" のページに html(文字列)の代わりに contentReactNode)を指定してください。

import { MangaViewer, type ReactManga } from "@yui540/comimi-react";

function App() {
  const [likes, setLikes] = useState(0);

  const manga: ReactManga = useMemo(
    () => ({
      id: "sample",
      title: "サンプル",
      pages: [
        { id: "p0", type: "image", src: "/pages/0.webp" },
        {
          id: "p1",
          type: "html",
          content: (
            <div style={{ padding: 24 }}>
              <h2>あとがき</h2>
              <button onClick={() => setLikes((n) => n + 1)}>
                いいね {likes}
              </button>
            </div>
          ),
        },
      ],
    }),
    [likes],
  );

  return <MangaViewer manga={manga} style={{ minHeight: 600 }} />;
}
  • content は React の ポータルで描画されるため、useState などの状態・イベントハンドラ・hooks がそのまま動きます。
  • content の中身が変わると即座に反映されます(上の例の「いいね」カウントなど)。ページ構成(ID・並び順・画像・HTML 文字列)が変わらない限り、ビューワーのリセットや再読み込みは発生しません。
  • manga の型は Manga ではなく ReactManga を使ってください(pagescontent を許可した型)。
  • content を指定したページでは html は無視されます(content 未指定時のフォールバックとして併用も可能)。

⚠️ この機能は @yui540/comimi 0.3.2 以降が必要です(ページに React のホスト要素をマウントする HtmlPage.element を利用します)。


API リファレンス

Props / Options

<MangaViewer /> の props と useMangaViewer() のオプションは共通です。

名前 説明
manga ReactManga 必須。表示する漫画データ。pagestype: "html" ページには contentReactNode)を指定可能
initialPageIndex number 初期表示するページのインデックス
locale string UI 言語 ("ja" / "en" / "zh-CN" / "ko" / "th" / "id" または独自キー)
translations TranslationMap 翻訳キーの上書き / 追加
settings Partial<ViewerSettings> 読書方向・見開き・ズーム範囲など
storage { enabled?: boolean; databaseName?: string } IndexedDB による設定・進捗の永続化
resolvePageSrc PageSrcResolver ページ画像 URL の動的解決(DRM / 認証付き fetch)
lockLayoutMode boolean レイアウトモードを settings.layoutMode に固定
mascot MascotOption マスコット画像の差し替え({ src } / { render } / false
hiddenSettings HideableControl[] 非表示にする UI 項目
forceSettings (keyof ViewerSettings)[] 保存値より初期値を優先する設定キー
onReady (e: { manga }) => void ビューワー準備完了時(保存値の復元後)
onMangaChange (e: { manga }) => void setManga / setPages で作品が差し替わった時
onPageChange (e: { pageIndex, page }) => void ページ変更時
onSettingsChange (e: { settings }) => void 設定変更時
onLayoutChange (e: { layoutMode }) => void 表示モード変更時
onOverlayChange (e: { visible }) => void オーバーレイの表示 / 非表示
onPanelChange (e: { panel }) => void メニュー・設定などのパネルの開閉
onAutoPageTurnChange (e: { enabled }) => void 自動再生の開始 / 停止
onZoomChange (e: { scale, panX, panY }) => void ズーム倍率・パン位置の変化(パン中は連続発火)
onFavoritesChange (e: { pageIds, pageId?, added? }) => void 「ここすき!」の変更
onNotification (e: { message, tone }) => void トースト通知の表示
onPageLoadError (e: { pageIndex, page }) => void ページ画像の読み込み失敗
onDestroy () => void 破棄時

イベント props は comimi の ViewerEventMap から on<EventName> の形で自動生成されます(型は ViewerEventProps)。各イベントの詳細は comimi のドキュメント を参照してください。

各設定値のデフォルトと意味は comimi のドキュメント を参照してください。

MangaViewerHandle / viewer インスタンスメソッド

comimi の MangaViewerInstance がそのまま公開されます。主なもの:

メソッド 説明
goToPage(pageIndex) 指定ページへジャンプ(範囲外はクランプ)
nextPage() / previousPage() 次 / 前のページへ
getCurrentPageIndex() / getPageCount() 現在ページ index / 総ページ数
setManga(manga) / setPages(pages) 漫画・ページ配列の差し替え
updateSettings(partial) 設定をマージ更新
setLayoutMode(mode) / toggleFullscreen() 表示モードの切替
toggleOverlay(force?) / setOverlayVisible(visible) オーバーレイの表示切替
setPanel(panel) メニュー・設定・ページ一覧・ここすき!などのパネルを開閉("none" で閉じる)
toggleAutoPageTurn() / setAutoPageTurn(enabled) 自動再生のオン / オフ
setZoom(scale, panX?, panY?) / resetZoom() ズーム
toggleFavorite(i) / addFavorite(i) / removeFavorite(i) / isFavorite(i) 「ここすき!」の操作
getFavoritePageIds() / setFavorites(ids) / clearFavorites() 「ここすき!」一覧の取得・置き換え
notify(message, tone?) トースト通知を表示
getState() 現在の ViewerState を取得(読み取り専用)
getElement() / isMobileViewport() ルート要素 / モバイル幅判定
on(eventName, handler) / once(eventName, handler) イベント購読。戻り値は解除関数
destroy() 明示的な破棄(通常は unmount で自動実行)

全メソッドは comimi のドキュメント を参照してください。


動作仕様

プロップ更新の挙動

プロップ 変更時の挙動
manga ページ構成(ID・並び順・画像 src・HTML 文字列など)が変わると setManga() を呼び出してリロード。React content だけの変更ではリロードせず、ポータル経由で即反映
settings 参照が変わると updateSettings() でマージ
その他 初回マウント時のみ反映(下記)

locale / translations / mascot / resolvePageSrc / lockLayoutMode / storage / initialPageIndex は comimi 側に動的更新の API が無いため、マウント時の値が固定されます。実行時に切り替えたい場合は key プロップで再マウントしてください。

<MangaViewer key={locale} manga={manga} locale={locale} />

不要な再生成を避ける

manga はページ構成のシグネチャ(ID・並び順・画像 src・HTML 文字列など)で変更検知するため、毎レンダーで新しいオブジェクトを渡しても構成が同じならリロードは走りません。一方 settings は参照比較なので、毎レンダーで新しいオブジェクトを渡すと毎回 updateSettings が走ります。useMemo か外部定数で安定化させてください。

const settings = useMemo(() => ({ readingDirection: "rtl" }), []);

イベントハンドラの安定性

onPageChange 等のイベント props はすべて内部 ref に保持されるため、毎レンダーで関数を作り直しても問題ありません(ビューワーは再生成されず、常に最新のハンドラが呼ばれます)。ハンドラを後から追加・削除しても再マウントは起きません。

StrictMode

React 18+ の StrictMode による開発時の二重マウントに対応しています。destroy() 後に再生成されるため、副作用の重複は発生しません。


レシピ

DRM / 認証付き fetch(resolvePageSrc

ページ画像をその場で復号したり、Authorization ヘッダ付きで取得して Blob URL に変換できます。

<MangaViewer
  manga={manga}
  resolvePageSrc={async ({ page }) => {
    const res = await fetch(page.src, {
      headers: { Authorization: `Bearer ${getToken()}` },
    });
    const blob = await res.blob();
    return URL.createObjectURL(blob);
  }}
/>
  • 解決結果は page.id 単位でキャッシュされ、再描画では再呼び出しされません
  • Blob URL の revokeObjectURL はライブラリ側で行わないので、利用側で管理してください

レイアウトモードを固定

<MangaViewer
  manga={manga}
  settings={{ layoutMode: "browserFullscreen" }}
  lockLayoutMode
/>

レイアウト切替の UI とキーボードショートカット (N / W / F / Esc) が無効化されます。

マスコットの差し替え

<MangaViewer manga={manga} mascot={{ src: "/my-mascot.png", alt: "Mascot" }} />

完全カスタムにしたい場合は { render: () => HTMLElement }、非表示にしたい場合は false を渡します。

ViewerState をリアクティブに購読

組み込みの状態購読フックは提供していません。コールバックや viewer.on() で必要な値だけ React state に持ち上げてください。

function Reader() {
  const [pageIndex, setPageIndex] = useState(0);

  return (
    <MangaViewer
      manga={manga}
      onPageChange={(e) => setPageIndex(e.pageIndex)}
    />
  );
}

注意点

SSR / Next.js

comimi は DOM・IndexedDB・Fullscreen API などを使用するため、サーバーサイドでレンダリングできません。Next.js の App Router や Pages Router で利用する場合は、クライアント側で動的 import するか "use client" を付与してください。

// Next.js (App Router)
"use client";
import { MangaViewer } from "@yui540/comimi-react";

型のエクスポート

comimi 側の主要な型に加え、React コンテンツ用の型(ReactManga / ReactMangaPage / ReactHtmlPage)も本パッケージからエクスポートされています。

import type {
  // React 用
  ReactManga,
  ReactMangaPage,
  ReactHtmlPage,
  // comimi 再エクスポート
  Manga,
  MangaPage,
  ImagePage,
  HtmlPage,
  ViewerSettings,
  ViewerState,
  ReadingDirection,
  LayoutMode,
  PageTurnMode,
  TranslationMap,
  ViewerEventMap,
  ViewerPanel,
  NotificationTone,
  MascotOption,
  MascotAreaOptions,
  MangaViewerInstance,
  MangaViewerOptions,
  // イベント props の型
  ViewerEventProps,
} from "@yui540/comimi-react";

開発

npm install
npm run dev        # examples/preview を Vite で起動
npm run typecheck
npm run build      # dist/ に ESM・CJS・.d.ts を出力

examples/preview/ には <MangaViewer /> 版と useMangaViewer() 版をタブで切り替えられるプレビューが含まれています。


ライセンス

MIT

クレジット

本ライブラリは yui540/comimi のラッパーです。ビューワー本体の機能・デザイン・サンプル画像はすべて comimi 本体のものです。

About

Open-source comic reader library for React

Resources

Stars

27 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages