Per-file viewer LiveComponent: <Fresco.canvas> + <Etcher.layer> for
images, video / PDF / icon fallback for other types, plus a sidebar
with filename + Download + metadata + comments thread. Owns the
annotation lifecycle (composer popover, persistence sync via
EtcherAdapter, the patch-shape / delete-shape JS bridges).
Shared between MediaBrowser (admin file grid → click image → modal)
and MediaViewer (lightbox embedded by MediaGallery and other
consumers). Both parents own their own modal shell + prev/next
navigation; this component is just the per-file content.
Required assigns
:id— DOM id, must include the file uuid so the parent's prev/next navigation remounts the component on file change (and Fresco'sphx-update="ignore"canvas DOM is replaced wholesale with the new file's image).:file— curated file map (%{file_uuid, mime_type, urls, filename, file_type, size, inserted_at, ...}). Same shapeMediaBrowser'suploaded_filescarries.:current_user— logged-in user; nil-tolerant. When nil the composer + comments thread don't render (no user to attribute a comment to).:parent_id— DOM id of the outer LiveComponent (the modal host). Currently unused by this component, but kept on the assigns map so future cross-component send_updates have a target without re-plumbing the API.
A board instead of a file
Pass :board (and no :file) to draw on an EMPTY canvas — a projects
whiteboard, say: %{target_type: "projects_whiteboard", target_uuid: board.uuid, width: 1920, height: 1080, background: nil}. Fresco renders
the scene with zero images (infinite_canvas), Etcher draws over it,
and every shape persists against the target pair instead of a file
(V183). No file means no rotation, no thumbnail job, no comment thread
and no sidebar — the board is the canvas and its tools. The id should
include the target uuid for the same remount reason as a file.
Optional assigns
:can_annotate(defaulttrue) — whether THIS viewer may change the drawings.falseis read-only: every shape arrives locked (Etcher'sreadonly, so it renders and tooltips but cannot be moved, resized, deleted or edited), the drawing toolbar and the pencil are gone, and the server refusesetcher:annotations-changed/etcher:shape-drawnoutright — Etcher's flag is UX, this is the boundary. Hosts pass their own write permission (a project member'scan_write); the default keeps every existing host as it was.:viewer_only(defaultfalse) — render only the canvas / composer column; suppresses the close button and the sidebar (filename + Download + metadata + comments). Used by standalone-page hosts likeMediaDetailthat have their own surrounding chrome (admin actions, metadata editor, file details).:details_path(defaultnil) — when set, the sidebar shows an "Open details page" button navigating to this path. Admin-context hosts (MediaBrowserwithadmin={true}) pass the file's/admin/media/:uuidpage; public hosts leave it off.
Summary
Functions
Build the comment_decorations registry that CommentsComponent
uses to render external labels above comments. The comments
package speaks a generic %{metadata_key => %{value => entry}}
vocabulary; we package annotation titles under the
"annotation_uuid" metadata key (matching the back-reference
comments already store via metadata["annotation_uuid"]).
Runtime check for whether the optional phoenix_kit_comments package
is installed AND enabled. Public so standalone-page hosts can gate
their own embed of CommentsComponent the same way the sidebar does.
Loads annotations for a file into the curated map shape this component
uses internally (uuid / kind / geometry / style / metadata with
comment_* + title injected). Public so standalone-page hosts like
MediaDetail can build their own decoration registry off the same
shape without re-querying the schema by hand.
load_annotations_for/1 for any target: a file goes through the
comment-preview loader; any other target has no comment threads, so
its rows carry the zero-comment shape (badge 0, no preview).
Callback implementation for Phoenix.LiveComponent.render/1.
Functions
Build the comment_decorations registry that CommentsComponent
uses to render external labels above comments. The comments
package speaks a generic %{metadata_key => %{value => entry}}
vocabulary; we package annotation titles under the
"annotation_uuid" metadata key (matching the back-reference
comments already store via metadata["annotation_uuid"]).
Source for the label: load_annotations_for/1 projects the
schema's title column into metadata["title"] (so Etcher's
JS tooltip can read it as a single map). We read from the same
place rather than bolt a top-level field onto the curated
annotation map. Only entries with non-empty titles are
included; an annotation without a title contributes nothing
(and the comment renders without a label block).
Public so standalone-page hosts (MediaDetail) can build the
same registry against the annotations they loaded via
load_annotations_for/1.
Runtime check for whether the optional phoenix_kit_comments package
is installed AND enabled. Public so standalone-page hosts can gate
their own embed of CommentsComponent the same way the sidebar does.
Loads annotations for a file into the curated map shape this component
uses internally (uuid / kind / geometry / style / metadata with
comment_* + title injected). Public so standalone-page hosts like
MediaDetail can build their own decoration registry off the same
shape without re-querying the schema by hand.
load_annotations_for/1 for any target: a file goes through the
comment-preview loader; any other target has no comment threads, so
its rows carry the zero-comment shape (badge 0, no preview).
Callback implementation for Phoenix.LiveComponent.render/1.