# `PhoenixKitWeb.Components.MediaBrowser`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.40.1/lib/phoenix_kit_web/components/media_browser.ex#L1)

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: uuid | nil,
  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`).

# `handle_event`

# `handle_parent_info`

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

# `render`

# `setup_uploads`

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`

---

*Consult [api-reference.md](api-reference.md) for complete listing*
