Packages
phoenix_kit
1.7.201
1.7.216
1.7.215
1.7.214
1.7.213
1.7.212
1.7.211
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/live/users/media_detail.html.heex
<PhoenixKitWeb.Components.LayoutWrapper.app_layout
socket={@socket}
flash={@flash}
phoenix_kit_current_scope={assigns[:phoenix_kit_current_scope]}
page_title="Media Detail"
current_path={Routes.path("/admin/media/#{@file_uuid}")}
project_title={@project_title}
current_locale={@current_locale}
>
<div class="p-6">
<.admin_page_header>
<div class="flex items-center gap-3">
<%!--
Browser-back so the user returns to exactly where they were before
opening this file — the folder grid they came from (the MediaBrowser
keeps the open folder in the `?folder=` query, and the click here was
a push_navigate, so the previous history entry is that folder view).
Falls back to the media root for a direct/shared load with no history.
--%>
<button
type="button"
onclick={"if (window.history.length > 1) { window.history.back() } else { window.location.href = '#{Routes.path("/admin/media")}' }"}
class="btn btn-ghost btn-circle"
title={gettext("Back")}
aria-label={gettext("Back")}
>
<.icon name="hero-arrow-left" class="w-7 h-7" />
</button>
<h1 class="text-xl sm:text-2xl lg:text-3xl font-bold text-base-content">
Media Detail
</h1>
</div>
</.admin_page_header>
<%= if @file_data do %>
<%!-- Main Layout: 3/4 Preview + 1/4 Info --%>
<div class="flex flex-col lg:flex-row gap-4 h-[calc(100vh-12rem)]">
<%!-- Left: Fresco canvas + Etcher annotations (3/4). The viewer --%>
<%!-- column from MediaCanvasViewer in `viewer_only` mode — gives us --%>
<%!-- the same Fresco + Etcher rendering, composer popover, and --%>
<%!-- annotation persistence as the in-place modal, without its --%>
<%!-- sidebar (we keep MediaDetail's admin-specific sidebar to the --%>
<%!-- right) or modal chrome (the page header carries the back nav). --%>
<div class="flex-[3] bg-base-200 rounded-lg overflow-hidden">
<.live_component
module={PhoenixKitWeb.Components.MediaCanvasViewer}
id={"media-detail-canvas-" <> @file_data.file_uuid}
file={@file_data}
current_user={assigns[:phoenix_kit_current_user]}
parent_id="media-detail-root"
has_prev={false}
has_next={false}
viewer_only={true}
persist_rotation={true}
/>
</div>
<%!-- Right: Info Panel (1/4) --%>
<div class="flex-1 flex flex-col gap-4 overflow-y-auto overflow-x-hidden min-w-0">
<%!-- File Title Card --%>
<div class="card bg-base-100 shadow-xl">
<div class="card-body p-4">
<h2 class="text-lg font-bold break-all">{@file_data.filename}</h2>
<div class="flex gap-2 mt-2">
<span class={[
"badge",
@file_data.status == "active" && "badge-success",
@file_data.status == "processing" && "badge-info",
@file_data.status == "failed" && "badge-error"
]}>
{String.capitalize(@file_data.status)}
</span>
<span class="badge badge-primary">
{String.upcase(@file_data.file_type)}
</span>
</div>
<div class="mt-3 flex flex-col gap-2">
<%= if @file_data.status == "trashed" do %>
<button
type="button"
phx-click="restore_file"
class="btn btn-success btn-sm w-full gap-2"
>
<.icon name="hero-arrow-uturn-left" class="w-4 h-4" /> Restore
</button>
<button
type="button"
phx-click="permanently_delete_file"
data-confirm="Permanently delete this file? This cannot be undone."
class="btn btn-error btn-sm w-full gap-2"
>
<.icon name="hero-trash" class="w-4 h-4" /> Delete Forever
</button>
<% else %>
<button
type="button"
phx-click="confirm_delete"
class="btn btn-error btn-sm w-full gap-2"
>
<.icon name="hero-trash" class="w-4 h-4" /> Delete File
</button>
<% end %>
</div>
</div>
</div>
<%!-- Metadata Card --%>
<div class="card bg-base-100 shadow-xl min-w-0">
<div class="card-body p-4">
<div class="flex justify-between items-center mb-3">
<h3 class="font-semibold text-sm">Metadata</h3>
<button
type="button"
phx-click="toggle_edit"
class={["btn btn-xs", (@edit_mode && "btn-warning") || "btn-ghost"]}
>
<%= if @edit_mode do %>
<.icon name="hero-x-mark" class="w-3 h-3" />
<% else %>
<.icon name="hero-pencil" class="w-3 h-3" />
<% end %>
</button>
</div>
<%= if @edit_mode do %>
<form phx-submit="save_metadata" class="space-y-3">
<div>
<label for="title" class="label label-text text-xs">Title</label>
<input
type="text"
id="title"
name="title"
value={@file_data.title}
class="input input-bordered input-sm w-full"
placeholder="Enter title"
/>
</div>
<div>
<label for="description" class="label label-text text-xs">Description</label>
<textarea
id="description"
name="description"
class="textarea textarea-bordered textarea-sm w-full"
rows="3"
placeholder="Enter description"
>{@file_data.description}</textarea>
</div>
<div>
<label for="tags" class="label label-text text-xs">
Tags (comma-separated)
</label>
<input
type="text"
id="tags"
name="tags"
value={Enum.join(@file_data.tags, ", ")}
class="input input-bordered input-sm w-full"
placeholder="tag1, tag2"
/>
</div>
<div class="flex gap-2">
<button type="submit" class="btn btn-primary btn-xs flex-1">
<.icon name="hero-check" class="w-3 h-3" /> Save
</button>
<button type="button" phx-click="cancel_edit" class="btn btn-ghost btn-xs">
Cancel
</button>
</div>
</form>
<% else %>
<div class="space-y-2 text-xs">
<div>
<span class="font-semibold text-base-content/70">Title:</span>
<p class="text-base-content mt-1 break-all">
<%= if String.length(@file_data.title) > 0 do %>
{@file_data.title}
<% else %>
<span class="text-base-content/50 italic">None</span>
<% end %>
</p>
</div>
<div>
<span class="font-semibold text-base-content/70">Description:</span>
<p class="text-base-content mt-1 break-all">
<%= if String.length(@file_data.description) > 0 do %>
{@file_data.description}
<% else %>
<span class="text-base-content/50 italic">None</span>
<% end %>
</p>
</div>
<div>
<span class="font-semibold text-base-content/70">Tags:</span>
<div class="flex flex-wrap gap-1 mt-1">
<%= if Enum.empty?(@file_data.tags) do %>
<span class="text-base-content/50 italic">None</span>
<% else %>
<%= for tag <- @file_data.tags do %>
<span class="badge badge-sm h-auto">{tag}</span>
<% end %>
<% end %>
</div>
</div>
</div>
<% end %>
</div>
</div>
<%!-- File Details Card --%>
<div class="card bg-base-100 shadow-xl">
<div class="card-body p-4">
<h3 class="font-semibold text-sm mb-3">File Details</h3>
<div class="space-y-2 text-xs">
<div>
<span class="font-semibold text-base-content/70">Size:</span>
<p class="text-base-content mt-1">{format_file_size(@file_data.size)}</p>
</div>
<div>
<span class="font-semibold text-base-content/70">MIME Type:</span>
<p class="text-base-content mt-1 break-all">{@file_data.mime_type}</p>
</div>
<div>
<span class="font-semibold text-base-content/70">Uploaded By:</span>
<p class="text-base-content mt-1">{@file_data.user_name}</p>
</div>
<div>
<span class="font-semibold text-base-content/70">Uploaded:</span>
<p class="text-base-content mt-1">
{UtilsDate.format_datetime_with_user_format(@file_data.inserted_at)}
</p>
</div>
<div>
<span class="font-semibold text-base-content/70">Updated:</span>
<p class="text-base-content mt-1">
{UtilsDate.format_datetime_with_user_format(@file_data.updated_at)}
</p>
</div>
<div>
<span class="font-semibold text-base-content/70">File UUID:</span>
<p class="text-base-content mt-1 break-all font-mono text-[10px]">
{@file_data.file_uuid}
</p>
</div>
<%= if @file_data.mime_type == "application/pdf" and is_map(@file_data.metadata) do %>
<% pdf_meta = @file_data.metadata %>
<%= if pdf_meta["page_count"] do %>
<div>
<span class="font-semibold text-base-content/70">Pages:</span>
<p class="text-base-content mt-1">{pdf_meta["page_count"]}</p>
</div>
<% end %>
<%= if pdf_meta["author"] && pdf_meta["author"] != "" do %>
<div>
<span class="font-semibold text-base-content/70">Author:</span>
<p class="text-base-content mt-1">{pdf_meta["author"]}</p>
</div>
<% end %>
<%= if pdf_meta["title"] && pdf_meta["title"] != "" do %>
<div>
<span class="font-semibold text-base-content/70">PDF Title:</span>
<p class="text-base-content mt-1">{pdf_meta["title"]}</p>
</div>
<% end %>
<% end %>
</div>
</div>
</div>
<%!-- Comments Card — mirrors MediaCanvasViewer's sidebar embed. --%>
<%!-- `parent_module` / `parent_id` route inline annotation-title --%>
<%!-- edits back to the embedded MediaCanvasViewer LiveComponent --%>
<%!-- (already wired to handle `:annotation_title_updated`), --%>
<%!-- which writes the row, reloads its viewer_annotations, and --%>
<%!-- pushes etcher:patch-shape so the canvas tooltip refreshes. --%>
<%!-- MediaDetail's own @viewer_annotations stays as the --%>
<%!-- decoration source until next page load — acceptable since --%>
<%!-- the canvas remains the primary surface for annotations. --%>
<%= if MediaCanvasViewer.comments_enabled?() and assigns[:phoenix_kit_current_user] do %>
<div class="card bg-base-100 shadow-xl">
<div class="card-body p-4 leaf-comments-compact">
<.live_component
module={PhoenixKitComments.Web.CommentsComponent}
id={"media-detail-comments-" <> @file_data.file_uuid}
resource_type="file"
resource_uuid={@file_data.file_uuid}
current_user={assigns[:phoenix_kit_current_user]}
show_likes={true}
title={gettext("Comments")}
comment_decorations={
MediaCanvasViewer.build_comment_decorations(@viewer_annotations)
}
parent_module={MediaCanvasViewer}
parent_id={"media-detail-canvas-" <> @file_data.file_uuid}
/>
</div>
</div>
<% end %>
<%!-- Storage Locations Card --%>
<%= if not Enum.empty?(@file_data.locations) do %>
<div class="card bg-base-100 shadow-xl">
<div class="card-body p-4">
<h3 class="font-semibold text-sm mb-3">Storage Locations</h3>
<div class="space-y-2">
<%= for location <- @file_data.locations do %>
<div class="bg-base-200 rounded p-2 text-xs">
<p class="font-semibold text-base-content">{location.bucket_name}</p>
<p class="text-base-content/70 mt-1 break-all font-mono text-[10px]">
{location.path}
</p>
<p class="text-base-content/50 mt-1">
{String.upcase(location.bucket_provider)}
</p>
</div>
<% end %>
</div>
</div>
</div>
<% end %>
<%!-- Download Variants Card --%>
<%= if map_size(@file_data.urls) > 0 do %>
<div class="card bg-base-100 shadow-xl">
<div class="card-body p-4">
<div class="flex justify-between items-center mb-3">
<h3 class="font-semibold text-sm">Download Variants</h3>
<button
type="button"
phx-click="regenerate_variants"
class="btn btn-ghost btn-xs gap-1"
>
<.icon name="hero-arrow-path" class="w-3 h-3" /> Regenerate
</button>
</div>
<div class="flex flex-col gap-2">
<%!-- Sort variants with original first --%>
<% variant_order = ["original", "large", "medium", "small", "thumbnail"] %>
<% sorted_variants =
@file_data.urls
|> Enum.sort_by(fn {name, _url} ->
Enum.find_index(variant_order, &(&1 == name)) || 999
end) %>
<%= for {variant, url} <- sorted_variants do %>
<a
href={url}
target="_blank"
rel="noopener noreferrer"
class={[
"btn btn-sm gap-2 justify-start",
variant == "original" && "btn-primary",
variant != "original" && "btn-outline"
]}
>
<.icon name="hero-arrow-down-tray" class="w-4 h-4" />
<span class="flex-1 text-left">{String.capitalize(variant)}</span>
<%= if info = @file_data.variant_dimensions[variant] do %>
<span class="text-xs opacity-60">
<%= if info.dimensions do %>
{elem(info.dimensions, 0)}x{elem(info.dimensions, 1)}
<% end %>
<%= if info.size do %>
· {format_file_size(info.size)}
<% end %>
</span>
<% end %>
</a>
<% end %>
</div>
</div>
</div>
<% end %>
</div>
</div>
<% else %>
<%!-- File Not Found --%>
<div class="card bg-base-100 shadow-xl">
<div class="card-body text-center">
<.icon name="hero-photo" class="w-16 h-16 mx-auto mb-4 text-base-content/50" />
<h2 class="text-2xl font-bold text-error">File Not Found</h2>
<p class="text-base-content/70 mt-2">
The file you're looking for doesn't exist or has been deleted.
</p>
<.link navigate={Routes.path("/admin/media")} class="btn btn-primary btn-sm mt-4">
<.icon name="hero-arrow-left" class="w-4 h-4" /> Library
</.link>
</div>
</div>
<% end %>
</div>
<%!-- Delete Confirmation Modal --%>
<%= if @show_delete_modal do %>
<div class="modal modal-open">
<div class="modal-box">
<h3 class="font-bold text-lg text-warning">Move to Trash</h3>
<p class="py-4">
Move <strong>{@file_data && @file_data.filename}</strong> to trash?
</p>
<p class="text-sm text-base-content/70 pb-4">
You can restore it from the trash at any time.
</p>
<div class="modal-action">
<button type="button" phx-click="cancel_delete" class="btn btn-ghost">
Cancel
</button>
<button type="button" phx-click="delete_file" class="btn btn-warning gap-2">
<.icon name="hero-trash" class="w-4 h-4" /> Move to Trash
</button>
</div>
</div>
<div class="modal-backdrop" phx-click="cancel_delete"></div>
</div>
<% end %>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>