Packages

phoenix_kit

1.7.113
1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib phoenix_kit_web components media_viewer.ex
Raw

lib/phoenix_kit_web/components/media_viewer.ex

defmodule PhoenixKitWeb.Components.MediaViewer do
@moduledoc """
Standalone media lightbox ("slide box") LiveComponent.
Renders a `<dialog>` modal showing one file from an ordered set, with
prev/next navigation (chevrons + ←/→ keys) and Escape/backdrop close.
Per-file content (the pan/zoom canvas with annotations + comments
thread for images, video/PDF/icon fallback otherwise) is delegated to
the shared `PhoenixKitWeb.Components.MediaCanvasViewer` child
LiveComponent — the same one MediaBrowser uses, so admins get the
full annotation experience here too.
Reusable on its own or embedded by `MediaGallery`.
## Attrs
- `id` — required
- `files` — ordered list of file UUIDs (the navigable set)
- `current` — initial UUID to show (must be in `files`). Treated as
a seed: once mounted, internal navigation state takes precedence
over subsequent `current` attr changes. This is intentional when
the component is mount-gated (`:if={...}`) so it remounts fresh
on each open. A standalone consumer that keeps the component
mounted and wants to jump to a different image programmatically
must unmount and remount it.
- `current_user` — required for the composer / comments thread to
render. nil-tolerant (lightbox falls back to view-only).
- `notify` — optional `{module, id}`; see Close below
## Close
On close, if `notify: {module, id}` is set:
`send_update(module, id: id, media_viewer_closed: true)`.
Otherwise: `send(self(), {__MODULE__, id, :closed})`.
"""
use PhoenixKitWeb, :live_component
require Logger
alias PhoenixKit.Modules.Storage
alias PhoenixKit.Modules.Storage.URLSigner
@impl true
def update(assigns, socket) do
socket =
socket
|> assign(:id, assigns.id)
|> assign(:files, assigns[:files] || [])
|> assign_new(:current_uuid, fn -> assigns[:current] end)
|> assign(:notify, assigns[:notify])
|> assign(:current_user, assigns[:current_user])
{:ok, assign(socket, :current_file, curate_file(socket.assigns.current_uuid))}
end
@impl true
def handle_event("close_viewer", _params, socket) do
close(socket)
{:noreply, socket}
end
def handle_event("step_viewer", %{"dir" => "prev"}, socket),
do: {:noreply, step_viewer(socket, :prev)}
def handle_event("step_viewer", %{"dir" => "next"}, socket),
do: {:noreply, step_viewer(socket, :next)}
def handle_event("viewer_keydown", %{"key" => "Escape"}, socket) do
close(socket)
{:noreply, socket}
end
def handle_event("viewer_keydown", %{"key" => "ArrowLeft"}, socket),
do: {:noreply, step_viewer(socket, :prev)}
def handle_event("viewer_keydown", %{"key" => "ArrowRight"}, socket),
do: {:noreply, step_viewer(socket, :next)}
def handle_event("viewer_keydown", _params, socket), do: {:noreply, socket}
# ── private ──────────────────────────────────────────────────────────
defp close(socket) do
case socket.assigns.notify do
{module, id} -> send_update(module, id: id, media_viewer_closed: true)
_ -> send(self(), {__MODULE__, socket.assigns.id, :closed})
end
end
defp step_viewer(socket, direction) do
current = socket.assigns.current_uuid
list = socket.assigns.files
with idx when is_integer(idx) <- Enum.find_index(list, &(&1 == current)),
next_idx <- if(direction == :prev, do: idx - 1, else: idx + 1),
true <- next_idx >= 0 and next_idx < length(list),
uuid when is_binary(uuid) <- Enum.at(list, next_idx) do
socket
|> assign(:current_uuid, uuid)
|> assign(:current_file, curate_file(uuid))
else
_ -> socket
end
end
# Build the curated file map MediaCanvasViewer expects. Slim version
# of MediaBrowser's `enrich_files/1` — same shape minus the
# folder_path (lightbox doesn't show folder breadcrumbs). One File
# row + one FileInstance query per current file open; the prev/next
# navigation re-resolves on each step.
defp curate_file(nil), do: nil
defp curate_file(file_uuid) when is_binary(file_uuid) do
case safe(fn -> Storage.get_file(file_uuid) end, nil) do
nil ->
nil
file ->
instances = safe(fn -> Storage.list_file_instances(file_uuid) end, [])
urls = signed_urls(file_uuid, instances)
%{
file_uuid: file.uuid,
filename: file.original_file_name || file.file_name || "Unknown",
file_type: file.file_type,
mime_type: file.mime_type,
size: file.size || 0,
inserted_at: file.inserted_at,
width: file.width,
height: file.height,
urls: urls
}
end
end
defp signed_urls(file_uuid, instances) do
Enum.reduce(instances, %{}, fn instance, acc ->
case safe(fn -> URLSigner.signed_url(file_uuid, instance.variant_name) end, nil) do
nil -> acc
url -> Map.put(acc, instance.variant_name, url)
end
end)
end
defp safe(fun, fallback) do
fun.()
rescue
e in [
DBConnection.ConnectionError,
DBConnection.OwnershipError,
Ecto.Query.CastError,
ArgumentError,
FunctionClauseError,
KeyError
] ->
Logger.warning("MediaViewer: data resolve failed — #{Exception.message(e)}")
fallback
end
end