Packages

phoenix_kit

1.7.131
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
phoenix_kit lib phoenix_kit_web components media_canvas_viewer.html.heex
Raw

lib/phoenix_kit_web/components/media_canvas_viewer.html.heex

<div id={@id} class="contents">
<%!-- --%>
<%!-- MediaCanvasViewer — rendered inside a parent's modal shell (a <dialog> --%>
<%!-- in MediaViewer, a <div class="modal"> in MediaBrowser). Owns the --%>
<%!-- per-file content: close button, prev/next chevrons, image canvas / --%>
<%!-- video / pdf / icon, composer popover, and sidebar (filename, Download, --%>
<%!-- metadata, comments thread). --%>
<%!-- --%>
<%!-- All click events on the close + chevron buttons target the parent --%>
<%!-- LiveComponent (`phx-target={"#" <> @parent_id}`) — the parent owns --%>
<%!-- modal open/close state and file navigation, this component just --%>
<%!-- displays whichever file's been handed to it. --%>
<%!-- --%>
<% f = @file %>
<%!-- Close button — top-right of modal-box. Suppressed in viewer_only --%>
<%!-- mode (standalone-page hosts provide their own back navigation). --%>
<button
:if={not @viewer_only}
type="button"
phx-click="close_viewer"
phx-target={"##{@parent_id}"}
class="btn btn-ghost btn-sm btn-circle absolute top-3 right-3 z-30 bg-base-100 hover:bg-base-200 shadow-lg ring-1 ring-base-300"
title={gettext("Close")}
>
<.icon name="hero-x-mark" class="w-4 h-4" />
</button>
<div class="flex flex-col lg:flex-row h-full overflow-hidden">
<%!-- Viewer column (image / video / pdf / icon) --%>
<div class="flex-[7] relative flex items-center justify-center bg-base-200 overflow-hidden p-0 lg:p-2 min-h-[40vh] lg:min-h-0">
<%!-- Chevron positioning sits on a wrapper div, not the button. --%>
<%!-- daisyUI's active-state CSS replaces `transform` with --%>
<%!-- `scale(0.97)` on click, which would clobber a -translate-y-1/2 --%>
<%!-- on the button itself and make it jump down 50% of its height. --%>
<%= if @has_prev do %>
<div class="absolute left-2 top-1/2 -translate-y-1/2 z-10">
<button
type="button"
phx-click="step_viewer"
phx-value-dir="prev"
phx-target={"##{@parent_id}"}
class="btn btn-circle btn-ghost bg-base-100/80 hover:bg-base-100 shadow-md"
title={gettext("Previous (←)")}
>
<.icon name="hero-chevron-left" class="w-5 h-5" />
</button>
</div>
<% end %>
<%= if @has_next do %>
<div class="absolute right-2 top-1/2 -translate-y-1/2 z-10">
<button
type="button"
phx-click="step_viewer"
phx-value-dir="next"
phx-target={"##{@parent_id}"}
class="btn btn-circle btn-ghost bg-base-100/80 hover:bg-base-100 shadow-md"
title={gettext("Next (→)")}
>
<.icon name="hero-chevron-right" class="w-5 h-5" />
</button>
</div>
<% end %>
<%= cond do %>
<% is_binary(f.mime_type) and String.starts_with?(f.mime_type, "image/") -> %>
<%!-- Fresco 0.5 canvas + Etcher 0.3 layer. <Fresco.canvas> hosts --%>
<%!-- the image as a single-image canvas built in --%>
<%!-- build_viewer_canvas/2 with the file's annotations stuffed --%>
<%!-- into extensions.etcher — Etcher hydrates on mount via --%>
<%!-- handle.getExtension("etcher"). --%>
<%= if @viewer_canvas do %>
<Fresco.canvas
id={"media-zoom-" <> f.file_uuid}
canvas={@viewer_canvas}
class="w-full h-full lg:rounded"
theme={:inherit}
/>
<Etcher.layer
fresco_id={"media-zoom-" <> f.file_uuid}
colors={@etcher_colors}
line_params={@etcher_line_params}
tools={[
:grabber,
:rectangle,
:circle,
:polygon,
:freehand,
:marker,
:callout,
:text,
:dimension,
:line,
:eraser
]}
/>
<% end %>
<% is_binary(f.mime_type) and String.starts_with?(f.mime_type, "video/") -> %>
<video
src={f.urls["original"]}
controls
class="max-w-full max-h-full rounded"
preload="metadata"
>
{gettext("Your browser does not support the video tag.")}
</video>
<% f.mime_type == "application/pdf" -> %>
<iframe
src={f.urls["original"]}
sandbox="allow-same-origin"
class="w-full h-full rounded border-0"
title={f.filename}
>
</iframe>
<% true -> %>
<div class="flex flex-col items-center justify-center text-base-content/50 py-12">
<.icon name={file_icon(f.file_type)} class="w-32 h-32 mb-4" />
<p class="text-2xl font-semibold">
{String.upcase(f.file_type || gettext("FILE"))}
</p>
<p class="text-sm text-base-content/70 mt-2">{f.mime_type}</p>
</div>
<% end %>
<%!-- Floating annotation composer — positioned above the just-drawn --%>
<%!-- shape by the AnnotationComposerPosition JS hook (it looks up --%>
<%!-- the shape via [data-uuid="…"]). Sidebar comments stay visible --%>
<%!-- alongside; this popover is for the new shape only. --%>
<%= if @composing_annotation_uuid && @current_user do %>
<div
id={"annotation-composer-popover-" <> @composing_annotation_uuid}
phx-hook="AnnotationComposerPosition"
class="absolute z-30 w-80 max-w-[calc(100%-1rem)] bg-base-100 rounded-lg shadow-xl border border-base-300 p-3"
style="left: 0px; top: 0px;"
>
<div class="flex items-center gap-2 text-sm font-medium mb-2">
<.icon name="hero-pencil-square" class="w-4 h-4 text-warning" />
<span>{gettext("Add a note to your annotation")}</span>
</div>
<p class="text-xs text-base-content/60 mb-3">
{gettext(
"Post to add the note. Cancel to skip — you can edit or delete the annotation later via the toolbar."
)}
</p>
<.live_component
module={PhoenixKitWeb.Components.AnnotationComposer}
id={"annotation-composer-" <> @composing_annotation_uuid}
annotation_uuid={@composing_annotation_uuid}
file_uuid={f.file_uuid}
current_user={@current_user}
parent_id={@id}
parent_module={PhoenixKitWeb.Components.MediaCanvasViewer}
/>
</div>
<% end %>
</div>
<%!-- Sidebar: filename + download + metadata + comments. Suppressed --%>
<%!-- in viewer_only mode — standalone-page hosts render their own --%>
<%!-- sidebar with admin actions / metadata editor / file details. --%>
<div
:if={not @viewer_only}
class="flex-[3] flex flex-col gap-3 p-4 overflow-y-auto lg:min-w-[280px] bg-base-100"
>
<h2 class="text-base font-bold break-all leading-tight pr-10">
{f.filename}
</h2>
<%= if f.urls["original"] do %>
<a
href={f.urls["original"]}
download={f.filename}
target="_blank"
rel="noopener"
class="btn btn-primary btn-sm gap-2 self-start"
>
<.icon name="hero-arrow-down-tray" class="w-4 h-4" /> {gettext("Download")}
</a>
<% end %>
<div class="text-sm space-y-2 mt-1">
<div>
<span class="text-base-content/60">{gettext("Type:")}</span>
<span class="ml-1 badge badge-ghost">
{String.upcase(f.file_type || gettext("FILE"))}
</span>
</div>
<div class="break-all">
<span class="text-base-content/60">{gettext("MIME:")}</span>
<span class="ml-1 font-mono text-xs">{f.mime_type}</span>
</div>
<div>
<span class="text-base-content/60">{gettext("Size:")}</span>
<span class="ml-1">{format_file_size(f.size)}</span>
</div>
<div>
<span class="text-base-content/60">{gettext("Uploaded:")}</span>
<span class="ml-1">
{Calendar.strftime(f.inserted_at, "%b %d, %Y at %H:%M")}
</span>
</div>
</div>
<%!-- Comments section — gated on the Comments module being --%>
<%!-- installed AND enabled, plus a logged-in user. Keyed by --%>
<%!-- file_uuid so prev/next remounts the component cleanly. --%>
<%!-- --%>
<%!-- The wrapper applies a compact-mode override: shrinks the --%>
<%!-- comments component's typography, paddings, and avatars --%>
<%!-- via descendant selectors so it fits inside the sidebar --%>
<%!-- without truncation. The component itself isn't modified. --%>
<%= if comments_enabled?() and @current_user do %>
<div class="border-t border-base-300 pt-3 mt-3 leaf-comments-compact">
<.live_component
module={PhoenixKitComments.Web.CommentsComponent}
id={"media-comments-" <> f.file_uuid}
resource_type="file"
resource_uuid={f.file_uuid}
current_user={@current_user}
show_likes={true}
title={gettext("Comments")}
comment_decorations={build_comment_decorations(@viewer_annotations)}
parent_module={PhoenixKitWeb.Components.MediaCanvasViewer}
parent_id={@id}
/>
</div>
<% end %>
</div>
</div>
</div>