Packages
phoenix_kit
1.7.112
1.7.210
1.7.209
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
Current section
Files
lib/phoenix_kit_web/components/media_viewer.ex
defmodule PhoenixKitWeb.Components.MediaViewer do
@moduledoc """
Standalone image lightbox ("slide box") LiveComponent.
Renders a modal showing one image from an ordered set, with prev/next
navigation (chevrons + ←/→ keys), Escape/backdrop close, and a Download link.
Images render via `<.image_set>` (responsive `<picture>`).
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 the
component is 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.
- `variants_map` — optional `%{uuid => variants}`; resolved internally if nil/omitted.
Note: `%{}` (empty map) is truthy and counts as pre-resolved — pass `nil` or omit to
trigger internal resolution.
- `file_structs` — optional `[%Storage.File{}]`; resolved internally if nil/omitted.
Note: `[]` (empty list) is truthy and counts as pre-resolved — pass `nil` or omit to
trigger internal resolution.
- `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
import PhoenixKit.Modules.Shared.Components.ImageSet
@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_new(:variants_map, fn -> assigns[:variants_map] end)
|> assign_new(:file_structs, fn -> assigns[:file_structs] end)
|> resolve_data()
{:ok, socket}
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 resolve_data(socket) do
files = socket.assigns.files
variants_map =
socket.assigns.variants_map ||
safe(fn -> Storage.list_image_set_variants_for_files(files) end, %{})
file_structs =
socket.assigns.file_structs ||
safe(fn -> Storage.get_files(files) end, [])
assign(socket, variants_map: variants_map, file_structs: file_structs)
end
defp safe(fun, fallback) do
fun.()
rescue
e in [DBConnection.ConnectionError, Ecto.Query.CastError] ->
Logger.warning("MediaViewer: could not load data — #{Exception.message(e)}")
fallback
end
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
assign(socket, :current_uuid, uuid)
else
_ -> socket
end
end
defp download_url_for(uuid, variants) do
case Enum.find(variants, &(&1.variant_name == "original")) do
%{url: url} ->
url
_ ->
try do
URLSigner.signed_url(uuid, "original", locale: :none)
rescue
e in [ArgumentError, FunctionClauseError, KeyError] ->
Logger.warning(
"MediaViewer: could not sign download URL for #{uuid} — #{Exception.message(e)}"
)
nil
end
end
end
defp file_name_for(files, uuid) do
case Enum.find(files, &(&1.uuid == uuid)) do
%{original_file_name: name} when is_binary(name) -> name
%{file_name: name} when is_binary(name) -> name
_ -> uuid
end
end
end