defmodule Etcher do
@moduledoc """
Etcher is the annotation layer for [Fresco](https://hex.pm/packages/fresco)-based
image viewers in Phoenix.
An *etcher* is the tool that incises marks into a surface — Etcher the
library does the same digitally: users draw shapes (rectangle, circle,
polygon, freehand) on top of any Fresco viewer; the LiveView fires
events with the resulting geometry; the consumer decides what to
persist.
## Two pieces
* **Client side**: an `<Etcher.layer>` component that attaches to a
named Fresco viewer, adds a pencil button to its nav, opens a
bottom toolbar with drawing tools when active. Built from scratch
— no Annotorious dependency.
* **Server side**: a pluggable storage adapter (`Etcher.Storage`
behaviour) with a sensible default (`Etcher.Storage.Default`)
backed by a bundled `etcher_annotations` table. Consumers with
custom needs implement their own adapter.
## Quick start
Install (in your `mix.exs`):
def deps do
[
{:fresco, "~> 0.2"},
{:etcher, "~> 0.1"}
]
end
Generate the default schema:
mix etcher.gen.migration
mix ecto.migrate
Configure (in `config/config.exs`):
config :etcher, repo: MyApp.Repo
Wire the JS hook (in `assets/js/app.js`):
import "../../deps/fresco/priv/static/fresco.js"
import "../../deps/etcher/priv/static/etcher.js"
let liveSocket = new LiveSocket("/live", Socket, {
hooks: { ...window.FrescoHooks, ...window.EtcherHooks, ...colocatedHooks }
})
Drop in a LiveView:
<Fresco.viewer id="demo" src={~p"/uploads/photo.jpg"} class="w-full h-[80vh]" />
<Etcher.layer fresco_id="demo" target_type="file" target_uuid={@file.uuid} />
Then handle the `etcher:created` event in your LiveView:
def handle_event("etcher:created", attrs, socket) do
case Etcher.create_annotation(attrs) do
{:ok, annotation} ->
{:noreply, push_event(socket, "etcher:annotation-saved", annotation)}
{:error, changeset} ->
{:noreply, put_flash(socket, :error, "Failed to save annotation")}
end
end
## Custom storage
Skip the bundled schema and implement your own adapter:
defmodule MyApp.AnnotationStorage do
@behaviour Etcher.Storage
def create(attrs), do: # your insert logic
def list_for(target_type, target_uuid), do: # your query
def update(uuid, attrs), do: # your update
def delete(uuid), do: # your delete
end
Then from your event handler:
MyApp.AnnotationStorage.create(attrs)
Etcher's component doesn't run any persistence itself — it fires
events and lets the consumer decide what happens. The bundled
`Etcher.create_annotation/1` etc. are just shortcuts for consumers
who want the default backend.
See `Etcher.Layer` for the component reference, `Etcher.Storage` for
the behaviour, and `Etcher.Annotation` for the bundled schema.
"""
defdelegate layer(assigns), to: Etcher.Layer
defdelegate create_annotation(attrs), to: Etcher.Storage.Default, as: :create
defdelegate list_annotations_for(target_type, target_uuid),
to: Etcher.Storage.Default,
as: :list_for
defdelegate update_annotation(uuid, attrs), to: Etcher.Storage.Default, as: :update
defdelegate delete_annotation(uuid), to: Etcher.Storage.Default, as: :delete
end