Packages
phoenix_kit
1.7.178
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_selector.ex
defmodule PhoenixKitWeb.Live.Users.MediaSelector do
@moduledoc """
Generic media selector LiveView.
Provides a reusable interface for selecting media files from anywhere in the admin panel.
Supports both single and multiple selection modes.
## Usage
# Navigate to selector with query params
/admin/media/selector?return_to=/admin/publishing/edit&mode=single&filter=image
## Query Parameters
- `return_to` - URL to navigate back to (required)
- `mode` - "single" or "multiple" (default: "single")
- `selected` - Comma-separated pre-selected file IDs (optional)
- `filter` - "image", "video", "all" (default: "all")
- `page` - Page number for pagination (default: "1")
"""
use PhoenixKitWeb, :live_view
require Logger
alias PhoenixKit.Modules.Storage
alias PhoenixKit.Modules.Storage.{File, FileInstance, URLSigner}
alias PhoenixKit.Settings
alias PhoenixKit.Users.Auth
alias PhoenixKit.Utils.Format
alias PhoenixKit.Utils.Routes
import Ecto.Query
@per_page 30
def mount(params, _session, socket) do
# Handle locale
locale =
params["locale"] || socket.assigns[:current_locale]
# Get project title
project_title = Settings.get_project_title()
# Parse query parameters
return_to = params["return_to"] || "/"
mode = parse_mode(params["mode"])
selected_uuids = parse_selected_uuids(params["selected"])
filter = parse_filter(params["filter"])
page = String.to_integer(params["page"] || "1")
# Allow file uploads
socket =
socket
|> assign(:current_locale, locale)
|> assign(:current_path, Routes.path("/admin/media/selector"))
|> assign(:project_title, project_title)
|> assign(:page_title, "Select Media")
|> assign(:return_to, return_to)
|> assign(:selection_mode, mode)
|> assign(:selected_uuids, selected_uuids)
|> assign(:file_type_filter, filter)
|> assign(:search_query, "")
|> assign(:current_page, page)
|> assign(:per_page, @per_page)
|> allow_upload(:media_files,
accept: :any,
max_entries: 10,
auto_upload: true,
progress: &handle_progress/3
)
{:ok, socket}
end
def handle_params(params, _uri, socket) do
page = String.to_integer(params["page"] || "1")
# Load files based on current filters and page
{files, total_count} = load_files(socket, page)
total_pages = ceil(total_count / socket.assigns.per_page)
socket =
socket
|> assign(:current_page, page)
|> assign(:uploaded_files, files)
|> assign(:total_count, total_count)
|> assign(:total_pages, total_pages)
{:noreply, socket}
end
def handle_event("toggle_selection", %{"file-uuid" => file_uuid}, socket) do
selected_uuids = socket.assigns.selected_uuids
mode = socket.assigns.selection_mode
new_selected_uuids =
case mode do
:single ->
# Single mode: replace selection
MapSet.new([file_uuid])
:multiple ->
# Multiple mode: toggle selection
if MapSet.member?(selected_uuids, file_uuid) do
MapSet.delete(selected_uuids, file_uuid)
else
MapSet.put(selected_uuids, file_uuid)
end
end
{:noreply, assign(socket, :selected_uuids, new_selected_uuids)}
end
def handle_event("confirm_selection", _params, socket) do
return_to = socket.assigns.return_to
selected_uuids = socket.assigns.selected_uuids
# Build return URL with selected_media param
selected_media_param = selected_uuids |> MapSet.to_list() |> Enum.join(",")
return_url =
if String.contains?(return_to, "?") do
"#{return_to}&selected_media=#{selected_media_param}"
else
"#{return_to}?selected_media=#{selected_media_param}"
end
{:noreply, push_navigate(socket, to: return_url)}
end
def handle_event("cancel_selection", _params, socket) do
{:noreply, push_navigate(socket, to: socket.assigns.return_to)}
end
def handle_event("search", %{"search" => %{"query" => query}}, socket) do
socket =
socket
|> assign(:search_query, query)
|> assign(:current_page, 1)
# Reload files with search query
{files, total_count} = load_files(socket, 1)
total_pages = ceil(total_count / socket.assigns.per_page)
socket =
socket
|> assign(:uploaded_files, files)
|> assign(:total_count, total_count)
|> assign(:total_pages, total_pages)
{:noreply, socket}
end
def handle_event("filter_type", %{"filter" => filter}, socket) do
parsed_filter = parse_filter(filter)
socket =
socket
|> assign(:file_type_filter, parsed_filter)
|> assign(:current_page, 1)
# Reload files with new filter
{files, total_count} = load_files(socket, 1)
total_pages = ceil(total_count / socket.assigns.per_page)
socket =
socket
|> assign(:uploaded_files, files)
|> assign(:total_count, total_count)
|> assign(:total_pages, total_pages)
{:noreply, socket}
end
def handle_event("validate", _params, socket) do
{:noreply, socket}
end
def handle_event("save", _params, socket) do
# Files are auto-uploaded, just reload the list
{files, total_count} = load_files(socket, 1)
total_pages = ceil(total_count / socket.assigns.per_page)
socket =
socket
|> assign(:current_page, 1)
|> assign(:uploaded_files, files)
|> assign(:total_count, total_count)
|> assign(:total_pages, total_pages)
|> put_flash(:info, "Files uploaded successfully")
{:noreply, socket}
end
defp handle_progress(:media_files, entry, socket) do
if entry.done? do
# Process the completed upload
consume_uploaded_entry(socket, entry, fn %{path: path} ->
process_upload(socket, path, entry)
end)
end
{:noreply, socket}
end
defp process_upload(socket, path, entry) do
# Get file info
ext = Path.extname(entry.client_name) |> String.replace_leading(".", "")
mime_type = entry.client_type || MIME.from_path(entry.client_name)
file_type = determine_file_type(mime_type)
# Get current user
current_user = socket.assigns[:phoenix_kit_current_user]
user_uuid = if current_user, do: current_user.uuid, else: nil
# Calculate hash
file_hash = Auth.calculate_file_hash(path)
# Store file in storage
case Storage.store_file_in_buckets(
path,
file_type,
user_uuid,
file_hash,
ext,
entry.client_name
) do
{:ok, file, :duplicate} ->
Logger.info("Duplicate file uploaded: #{file.uuid}")
{:ok, file.uuid}
{:ok, file} ->
Logger.info("New file uploaded: #{file.uuid}")
{:ok, file.uuid}
{:error, reason} ->
Logger.error("Upload failed: #{inspect(reason)}")
{:error, reason}
end
end
defp load_files(socket, page) do
repo = PhoenixKit.Config.get_repo()
per_page = socket.assigns.per_page
filter = socket.assigns.file_type_filter
search = socket.assigns.search_query
# Build query
query = from(f in File, order_by: [desc: f.inserted_at])
# Apply file type filter
query =
case filter do
:image -> where(query, [f], f.file_type == "image")
:video -> where(query, [f], f.file_type == "video")
:all -> query
end
# Apply search filter
query =
if search != "" do
search_pattern = "%#{search}%"
where(query, [f], ilike(f.original_file_name, ^search_pattern))
else
query
end
# Get total count
total_count = repo.aggregate(query, :count, :uuid)
# Get paginated files
offset = (page - 1) * per_page
files =
query
|> limit(^per_page)
|> offset(^offset)
|> repo.all()
# Batch load instances to avoid N+1
file_uuids = Enum.map(files, & &1.uuid)
instances_by_file =
if Enum.any?(file_uuids) do
from(fi in FileInstance,
where: fi.file_uuid in ^file_uuids
)
|> repo.all()
|> Enum.group_by(& &1.file_uuid)
else
%{}
end
# Convert to file data maps
files_with_urls =
Enum.map(files, fn file ->
instances = Map.get(instances_by_file, file.uuid, [])
urls = generate_urls_from_instances(instances, file.uuid)
%{
file_uuid: file.uuid,
filename: file.original_file_name || file.file_name || "Unknown",
original_filename: file.original_file_name,
file_type: file.file_type,
mime_type: file.mime_type,
size: file.size || 0,
status: file.status,
urls: urls,
width: get_width_from_instances(instances),
height: get_height_from_instances(instances)
}
end)
{files_with_urls, total_count}
end
defp generate_urls_from_instances(instances, file_uuid) do
Enum.reduce(instances, %{}, fn instance, acc ->
url = URLSigner.signed_url(file_uuid, instance.variant_name)
Map.put(acc, instance.variant_name, url)
end)
end
defp get_width_from_instances(instances) do
case Enum.find(instances, &(&1.variant_name == "original")) do
nil -> nil
instance -> instance.width
end
end
defp get_height_from_instances(instances) do
case Enum.find(instances, &(&1.variant_name == "original")) do
nil -> nil
instance -> instance.height
end
end
defp determine_file_type(mime_type) do
cond do
String.starts_with?(mime_type, "image/") -> "image"
String.starts_with?(mime_type, "video/") -> "video"
true -> "other"
end
end
defp parse_mode(nil), do: :single
defp parse_mode("single"), do: :single
defp parse_mode("multiple"), do: :multiple
defp parse_mode(_), do: :single
defp parse_selected_uuids(nil), do: MapSet.new()
defp parse_selected_uuids(selected_string) when is_binary(selected_string) do
selected_string
|> String.split(",")
|> Enum.map(&String.trim/1)
|> Enum.reject(&(&1 == ""))
|> MapSet.new()
end
defp parse_selected_uuids(_), do: MapSet.new()
defp parse_filter(nil), do: :all
defp parse_filter("image"), do: :image
defp parse_filter("video"), do: :video
defp parse_filter("all"), do: :all
defp parse_filter(_), do: :all
defp format_file_size(bytes), do: Format.bytes(bytes, decimals: 2, unknown: "0 B")
defp pagination_range(current_page, total_pages) do
cond do
total_pages <= 7 ->
Enum.to_list(1..total_pages)
current_page <= 4 ->
[1, 2, 3, 4, 5, :ellipsis, total_pages]
current_page >= total_pages - 3 ->
[1, :ellipsis | Enum.to_list((total_pages - 4)..total_pages)]
true ->
[1, :ellipsis, current_page - 1, current_page, current_page + 1, :ellipsis, total_pages]
end
end
end