PhoenixKitWeb.Components.MediaBrowser (phoenix_kit v2.40.1)

Copy Markdown View Source

MediaBrowser LiveComponent — embeddable media management UI.

Provides a full media browser with folder navigation, file upload, search, and selection tools. Operates in two modes:

  • Uncontrolled (default): all navigation state is owned internally. No URL sync.
  • Controlled (opt-in): when on_navigate is set to a truthy value, navigation events (navigate_folder, search, clear_search, set_page) emit {PhoenixKitWeb.Components.MediaBrowser, id, {:navigate, params}} to the parent LiveView process instead of mutating local state. The parent must call push_patch and feed URL params back via send_update with a :nav_params key.

Enabling uploads

The fastest path is the Embed helper — one use line gives you uploads, the validate-channel stub, and the message delegator:

defmodule MyAppWeb.MediaPage do
  use MyAppWeb, :live_view
  use PhoenixKitWeb.Components.MediaBrowser.Embed
end

Manual setup (if you prefer explicit wiring) is three calls:

def mount(_params, _session, socket) do
  {:ok, PhoenixKitWeb.Components.MediaBrowser.setup_uploads(socket)}
end

def handle_event("validate", _params, socket), do: {:noreply, socket}

def handle_info({__MODULE__, _, _} = msg, socket) do
  PhoenixKitWeb.Components.MediaBrowser.handle_parent_info(msg, socket)
end

Usage (uncontrolled)

<.live_component
  module={PhoenixKitWeb.Components.MediaBrowser}
  id="media-browser"
  phoenix_kit_current_user={@phoenix_kit_current_user}
/>

Picking into a typed slot

only_file_type restricts the browser to one kind for its whole lifetime — the listing is filtered to it, the type-filter control is hidden, and an off-type upload is refused, so the other kinds are simply not reachable:

<.live_component
  module={PhoenixKitWeb.Components.MediaBrowser}
  id="audio-picker"
  select_mode
  only_file_type="audio"
  phoenix_kit_current_user={@phoenix_kit_current_user}
/>

Use it wherever the selection fills a typed field. Offering everything and validating afterwards works, but it lets someone choose a PNG for an audio slot and only find out on the rejection — and if a consumer forgets that re-check, the PNG lands in the field and renders as a dead <audio>.

Values: image, video, document, audio, archive, other.

Usage (controlled — URL-sync driven by parent)

<.live_component
  module={PhoenixKitWeb.Components.MediaBrowser}
  id="media-browser"
  phoenix_kit_current_user={@phoenix_kit_current_user}
  on_navigate={true}
/>

The parent LiveView must implement:

def handle_info({PhoenixKitWeb.Components.MediaBrowser, "media-browser",
                 {:navigate, params}}, socket) do
  # push_patch to update URL, handle_params will send_update back
end

params carries :file — the uuid of the file open in the modal viewer, or nil when it is closed. Put it in the URL and hand it back in :nav_params and a refresh, or the browser's Back button, reopens or closes the viewer. A host that leaves the key out of :nav_params altogether keeps the viewer local to the component; only an explicit file: nil closes it.

Required attributes

  • id — unique DOM id (required by LiveComponent)

Optional attributes

  • phoenix_kit_current_user — logged-in user struct (for upload attribution)

  • scope_folder_id — constrain the browser to a virtual root folder

  • on_navigate — when truthy, enables controlled mode (URL-sync via parent)

  • admin — clicking a file always opens the in-place modal viewer (image / video / PDF / icon + metadata + Download + prev/next navigation). When true, the viewer sidebar additionally shows an "Open details page" button linking to /admin/media/:uuid, and rotations made in the viewer persist to the file row. Bulk-select is still reachable via the toolbar's Select button — once select_mode is on, clicks toggle selection instead of opening the modal.

  • readonly (default false) — embed the browser for viewing only. Every write-capable affordance is hidden — the upload zone and drag-drop upload input, the toolbar's Add Media / Select / New folder controls (including the stacks-view "+" tile and the sidebar's own "+"/rename/drag affordances), every folder and file kebab menu item except Download, and every data-draggable-* / data-drop-* drag attribute. Navigation, search, sorting, the modal viewer, and downloads keep working. When featured is also set, the featured tile's star still renders (display only) but the other tiles' star toggles and the kebab's Set/Unset featured item do not.

    The hidden markup is a courtesy, not the boundary: every mutating handle_event clause (upload, rename, move, trash, new folder, bulk-select entry, rotate, featured toggle, open the image editor) refuses outright when readonly is set, because the event is still reachable from a console — same reasoning as the only_file_type lock on set_file_filter. The same applies one level down, inside the modal viewer's MediaCanvasViewer: readonly passes can_annotate={false} (Etcher shapes lock), edit_target={nil} AND details_path={nil} (title/alt/description save refuses only when BOTH are nil), and persist_rotation={false}.

    Typical use: embedding the browser on a page where the viewer should only look, not touch —

    <.live_component
      module={PhoenixKitWeb.Components.MediaBrowser}
      id="order-files-viewonly"
      scope_folder_id={@order.storage_folder_uuid}
      readonly
    />
  • featured — nil (default, feature off) or `%{uuid: uuidnil,
    label: String.t()nil}` naming the host's own featured-image

    pointer. label is the star badge's tooltip; falls back to gettext "Featured image" when nil/absent. When set, image tiles/rows gain a "Set as featured" / "Unset featured" kebab item (grid, list and stack views) and a star toggle in the top-left corner of every image tile (grid and stack views): a solid star on the matching tile (data-role="featured-badge", a click clears it), an outline star on the others (data-role="featured-toggle", a click moves the pointer there). In select mode, the trash, or readonly, only the matching tile shows its star, as a plain badge. The modal viewer's sidebar (for image files) shows the same toggle. The browser never persists anything itself: choosing or clearing a featured image sends {__MODULE__, id, {:set_featured, uuid | nil}} to the host process — the {MediaBrowser, id, payload} channel {:navigate, _} already uses — and optimistically moves the badge locally. ⚠️ A host that funnels every {MediaBrowser, _, _} message into handle_parent_info/2 must match {:set_featured, _} before it: that function handles only its own two internal messages and ends in a catch-all, so the choice is swallowed silently — no crash, no log, the star flips in the UI and nothing is ever persisted. The host persists the choice and, if the write is rejected or the pointer changes elsewhere, corrects it with a later featured assign (e.g. via send_update).

Summary

Functions

Catch-all handler for parent LiveViews to delegate MediaBrowser messages.

Callback implementation for Phoenix.LiveComponent.render/1.

Handles the upload setup message from the MediaBrowser component.

Functions

handle_event(event, params, socket)

Callback implementation for Phoenix.LiveComponent.handle_event/3.

handle_parent_info(arg1, socket)

Catch-all handler for parent LiveViews to delegate MediaBrowser messages.

render(assigns)

Callback implementation for Phoenix.LiveComponent.render/1.

setup_uploads(socket)

Handles the upload setup message from the MediaBrowser component.

Parent LiveViews embedding MediaBrowser should add this to their handle_info:

def handle_info({PhoenixKitWeb.Components.MediaBrowser, _id, :setup_uploads}, socket) do
  {:noreply, PhoenixKitWeb.Components.MediaBrowser.setup_uploads(socket)}
end

Or use the catch-all delegator:

def handle_info({PhoenixKitWeb.Components.MediaBrowser, _, _} = msg, socket) do
  PhoenixKitWeb.Components.MediaBrowser.handle_parent_info(msg, socket)
end

update(assigns, socket)

Callback implementation for Phoenix.LiveComponent.update/2.