PhoenixKitWeb.Components.MediaCanvasViewer (phoenix_kit v2.17.0)

Copy Markdown View Source

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's phx-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 shape MediaBrowser's uploaded_files carries.
  • :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 (default true) — whether THIS viewer may change the drawings. false is read-only: every shape arrives locked (Etcher's readonly, so it renders and tooltips but cannot be moved, resized, deleted or edited), the drawing toolbar and the pencil are gone, and the server refuses etcher:annotations-changed / etcher:shape-drawn outright — Etcher's flag is UX, this is the boundary. Hosts pass their own write permission (a project member's can_write); the default keeps every existing host as it was.
  • :viewer_only (default false) — render only the canvas / composer column; suppresses the close button and the sidebar (filename + Download + metadata + comments). Used by standalone-page hosts like MediaDetail that have their own surrounding chrome (admin actions, metadata editor, file details).
  • :details_path (default nil) — when set, the sidebar shows an "Open details page" button navigating to this path. Admin-context hosts (MediaBrowser with admin={true}) pass the file's /admin/media/:uuid page; 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_comment_decorations(annotations)

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.

comments_enabled?()

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.

load_annotations_for(file_uuid)

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_target(target_type, file_uuid)

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).

render(assigns)

Callback implementation for Phoenix.LiveComponent.render/1.