Manhwa.Store behaviour (manhwa v0.1.0)

Copy Markdown View Source

The persistence contract between the reader and the host application.

The reader owns how it reads — page/scroll engines, settings UI, progress ticking, chapter navigation. The host owns what is read and where state lives: where chapter lists and page URLs come from, and which table stores per-series reader state. This behaviour is that seam.

Configure your implementation:

config :manhwa, store: MyApp.ReaderStore

Series identity

A series is an opaque list of path segments, taken verbatim from the reader URL. With series_segments: 2 in the router macro a URL like /reader/manga/mangadex/one-piece/1057 yields series = ["mangadex", "one-piece"]; with 1 segment it's a single slug. The reader never interprets the segments — your store does.

Users

user is whatever your :current_user config function returns for the conn — the reader threads it through untouched (it may be nil for anonymous readers if your pipeline allows them).

Reader state

reader_state/2 returns a map (or any struct — string keys are not supported) with any subset of these keys; missing keys fall back to reader defaults:

  • :title — display title for the series
  • :reading_mode / :reading_mode_mobile / :reading_mode_desktop"paged" | "double_page" | "scroll"; per-device slots win over the generic one
  • :reading_direction"rtl" | "ltr"

  • :reading_rotation0 | 90 | 180 | 270

  • :smart_crop — boolean
  • :comments_visible — boolean
  • :tap_zone_preset / :tap_zone_preset_mobile / :tap_zone_preset_desktop
  • :last_chapter / :last_page / :last_page_fraction — resume state

Settings

put_setting/5 receives one of the setting atoms :reading_mode | :reading_direction | :reading_rotation | :smart_crop | :comments_visible | :tap_zone_preset with an already-validated value, plus a device slot ("mobile" | "desktop" | "all" | nil) for the per-device settings (:reading_mode, :tap_zone_preset).

Summary

Callbacks

Page list for a chapter. opts[:dims] hints the dimension fidelity the caller needs — :precise (paged reading; per-page dims drive spread pairing) or :fast (strip reading; sampled/estimated dims are fine). Stores may ignore the hint; pages missing dims get reader defaults.

Authorize + build request headers for proxying an external image URL. Return :forbidden for hosts you don't recognize (the proxy 403s).

Ordered chapter identifiers for the series (reading order).

Page overrides for the paged reader (manga package). Optional.

Persist a validated setting (see module doc).

Per-series reader state (see module doc). nil when nothing is saved.

Persist reading progress. Called on the reader's progress tick with %{page: pos_integer, fraction: float, total_pages: pos_integer | nil, elapsed_seconds: non_neg_integer}.

Forward-only snap of the resume pointer to chapter — fired when the user enters via an explicit jump-ahead link (?from_max=1).

Toggle a page override ("insert_blank_before" | "solo"). Optional. Returns whether the override is now :added or :removed.

True when the series segments identify content this store can serve.

Types

chapter()

@type chapter() :: String.t()

page()

@type page() :: %{
  :url => String.t(),
  optional(:width) => pos_integer() | nil,
  optional(:height) => pos_integer() | nil
}

series()

@type series() :: [String.t()]

user()

@type user() :: term()

Callbacks

fetch_pages(user, series, chapter, opts)

@callback fetch_pages(user(), series(), chapter(), opts :: keyword()) ::
  {:ok, [page()]} | {:error, term()}

Page list for a chapter. opts[:dims] hints the dimension fidelity the caller needs — :precise (paged reading; per-page dims drive spread pairing) or :fast (strip reading; sampled/estimated dims are fine). Stores may ignore the hint; pages missing dims get reader defaults.

image_request(url, user)

@callback image_request(url :: String.t(), user()) ::
  {:ok, headers :: [{String.t(), String.t()}]} | :forbidden

Authorize + build request headers for proxying an external image URL. Return :forbidden for hosts you don't recognize (the proxy 403s).

list_chapters(user, series)

@callback list_chapters(user(), series()) :: [chapter()]

Ordered chapter identifiers for the series (reading order).

list_page_overrides(user, series, chapter)

(optional)
@callback list_page_overrides(user(), series(), chapter()) :: [
  %{page_index: pos_integer(), kind: String.t()}
]

Page overrides for the paged reader (manga package). Optional.

put_setting(user, series, setting, value, device)

@callback put_setting(
  user(),
  series(),
  setting :: atom(),
  value :: term(),
  device :: String.t() | nil
) ::
  :ok | {:ok, term()} | {:error, term()}

Persist a validated setting (see module doc).

reader_state(user, series)

@callback reader_state(user(), series()) :: map() | nil

Per-series reader state (see module doc). nil when nothing is saved.

save_progress(user, series, chapter, progress)

@callback save_progress(user(), series(), chapter(), progress :: map()) ::
  :ok | {:ok, term()} | {:error, term()}

Persist reading progress. Called on the reader's progress tick with %{page: pos_integer, fraction: float, total_pages: pos_integer | nil, elapsed_seconds: non_neg_integer}.

snap_to_chapter(user, series, chapter)

(optional)
@callback snap_to_chapter(user(), series(), chapter()) ::
  :ok | {:ok, term()} | {:error, term()}

Forward-only snap of the resume pointer to chapter — fired when the user enters via an explicit jump-ahead link (?from_max=1).

toggle_page_override(user, series, chapter, page_index, kind)

(optional)
@callback toggle_page_override(
  user(),
  series(),
  chapter(),
  page_index :: pos_integer(),
  kind :: String.t()
) ::
  {:ok, :added | :removed} | {:error, term()}

Toggle a page override ("insert_blank_before" | "solo"). Optional. Returns whether the override is now :added or :removed.

valid_series?(series)

@callback valid_series?(series()) :: boolean()

True when the series segments identify content this store can serve.