Files
music_library/lib/music_library_web/live/collection_live/index.ex
T
2026-05-22 16:19:54 +01:00

381 lines
12 KiB
Elixir

defmodule MusicLibraryWeb.CollectionLive.Index do
use MusicLibraryWeb, :live_view
import MusicLibraryWeb.Components.BarcodeScanner, only: [barcode_icon: 1]
import MusicLibraryWeb.Components.Pagination
import MusicLibraryWeb.LiveHelpers.Params
import MusicLibraryWeb.RecordComponents
alias MusicLibrary.Chats
alias MusicLibrary.Collection
alias MusicLibrary.Records
alias MusicLibraryWeb.Components.AddRecord
alias MusicLibraryWeb.ErrorMessages
alias MusicLibraryWeb.LiveHelpers.IndexActions
defp index_config do
%{
context_module: Collection,
default_order: "purchase",
allowed_orders: [:purchase, :alphabetical, :release],
default_records_list_params: %{query: "", page: 1, page_size: 72, order: :purchase},
purchased_at_fn: fn -> DateTime.utc_now() end,
import_page_title: gettext("Add new Record · Collection"),
section_page_title: gettext("Collection"),
import_success_toast: gettext("Record imported successfully"),
record_path_fn: fn id -> ~p"/collection/#{id}" end,
index_path_fn: fn qs -> ~p"/collection?#{qs}" end,
base_index_path: ~p"/collection"
}
end
@impl true
def render(assigns) do
~H"""
<Layouts.app
flash={@flash}
current_section={@current_section}
socket={@socket}
toasts_sync={assigns[:toasts_sync]}
>
<header class="mb-2 gap-6">
<div class="my-2 flex items-center justify-between gap-6">
<.search_form query={@record_list_params.query} />
<.button_group>
<.button
variant="solid"
size="sm"
patch={~p"/collection/import"}
>
<.icon name="hero-plus" class="icon" aria-hidden="true" data-slot="icon" />
<span class="sr-only sm:not-sr-only">{gettext("Add")}</span>
</.button>
<.button
variant="solid"
size="sm"
patch={~p"/collection/scan"}
>
<.barcode_icon class="icon fill-current" />
<span class="sr-only sm:not-sr-only">{gettext("Scan")}</span>
</.button>
<.button
variant="solid"
size="sm"
phx-click={MusicLibraryWeb.Components.Chat.open("collection-chat-sheet")}
>
<.icon
name="hero-chat-bubble-left-right"
class="icon"
aria-hidden="true"
data-slot="icon"
/>
<span class="sr-only sm:not-sr-only">{gettext("Chat")}</span>
<span :if={@chat_count > 0} class="sr-only text-xs font-medium sm:not-sr-only">
{@chat_count}
</span>
</.button>
</.button_group>
</div>
</header>
<div class="mt-6 flex items-end justify-between gap-6">
<.button_group>
<.button
patch={order_path(@record_list_params, :purchase)}
size="sm"
class={[
@record_list_params.order == :purchase && "bg-zinc-100! dark:bg-zinc-700!"
]}
>
<.icon
name="hero-banknotes-solid"
class="icon"
aria-hidden="true"
data-slot="icon"
/>
<span class="sr-only sm:not-sr-only">{gettext("Purchase")}</span>
</.button>
<.button
patch={order_path(@record_list_params, :alphabetical)}
size="sm"
class={[
@record_list_params.order == :alphabetical && "bg-zinc-100! dark:bg-zinc-700!"
]}
>
<.icon name="hero-user-solid" class="icon" aria-hidden="true" data-slot="icon" />
<span class="sr-only sm:not-sr-only">{gettext("A->Z")}</span>
</.button>
<.button
patch={order_path(@record_list_params, :release)}
size="sm"
class={[
@record_list_params.order == :release && "bg-zinc-100! dark:bg-zinc-700!"
]}
>
<.icon name="hero-calendar-days" class="icon" aria-hidden="true" data-slot="icon" />
<span class="sr-only sm:not-sr-only">{gettext("Release")}</span>
</.button>
</.button_group>
<.button_group>
<.button
phx-click="set_display"
phx-value-mode="grid"
size="sm"
class={[
@display == :grid && "bg-zinc-100! dark:bg-zinc-700!"
]}
>
<.icon
name="hero-squares-2x2"
class="icon"
aria-hidden="true"
data-slot="icon"
/>
<span class="sr-only sm:not-sr-only">
{gettext("Grid")}
</span>
</.button>
<.button
phx-click="set_display"
phx-value-mode="list"
size="sm"
class={[
@display == :list && "bg-zinc-100! dark:bg-zinc-700!"
]}
>
<.icon name="hero-list-bullet" class="icon" aria-hidden="true" data-slot="icon" />
<span class="sr-only sm:not-sr-only">
{gettext("List")}
</span>
</.button>
</.button_group>
</div>
<.record_grid
:if={@display == :grid}
id="collection"
records={@streams.records}
record_show_path={fn record -> ~p"/collection/#{record}" end}
record_edit_path={fn record -> ~p"/collection/#{record}/edit" end}
display_artist_names
density={:high}
/>
<.record_list
:if={@display == :list}
records={@streams.records}
record_show_path={fn record -> ~p"/collection/#{record}" end}
record_edit_path={fn record -> ~p"/collection/#{record}/edit" end}
/>
<.structured_modal
:if={@live_action == :edit}
id="record-modal"
on_close={JS.patch(back_path(@record_list_params))}
>
<.live_component
module={MusicLibraryWeb.Components.RecordForm}
id={@record.id}
action={@live_action}
show_purchased_at={true}
record={@record}
patch={back_path(@record_list_params)}
/>
</.structured_modal>
<.structured_modal
:if={@live_action == :import}
id="record-modal"
on_close={JS.patch(back_path(@record_list_params))}
width_class="md:max-w-4xl lg:max-w-5xl"
>
<.live_component
module={AddRecord}
id={:search}
title={@page_title}
action={@live_action}
record={@record}
patch={back_path(@record_list_params)}
initial_query={@import_query}
icon_name="hero-plus"
purchased_at_fn={@index_config.purchased_at_fn}
/>
</.structured_modal>
<.structured_modal
:if={@live_action == :barcode_scan}
id="barcode-scanner-modal"
on_close={JS.patch(back_path(@record_list_params))}
width_class="md:max-w-4xl lg:max-w-5xl"
>
<.live_component
module={MusicLibraryWeb.Components.BarcodeScanner}
id={:barcode_scanner}
title={@page_title}
action={@live_action}
patch={back_path(@record_list_params)}
/>
</.structured_modal>
<.pagination id={:bottom_pagination} pagination_params={@record_list_params} />
<.live_component
id="collection-chat"
sheet_id="collection-chat-sheet"
module={MusicLibraryWeb.Components.Chat}
title={gettext("Collection")}
entity={:collection}
musicbrainz_id={Chats.collection_musicbrainz_id()}
chat_module={MusicLibrary.Chats.CollectionChat}
chat_context={@collection_summary}
placeholder={gettext("Ask about your collection...")}
empty_prompt={gettext("Ask anything about your music collection")}
/>
<div
:if={@open_chat}
id="auto-open-chat"
phx-mounted={MusicLibraryWeb.Components.Chat.open("collection-chat-sheet")}
/>
</Layouts.app>
"""
end
@impl true
def mount(_params, _session, socket) do
if connected?(socket) do
Records.subscribe_to_index()
end
{:ok,
socket
|> assign(:current_section, :collection)
|> assign(:index_config, index_config())
|> assign(:import_query, "")
|> assign(:display, :grid)
|> assign(:open_chat, false)
|> assign(:collection_summary, {"", 0})
|> assign(:chat_count, Chats.count_chats(:collection, Chats.collection_musicbrainz_id()))
|> start_async(:collection_summary, &Collection.collection_summary/0)}
end
@impl true
def handle_params(params, _url, socket) do
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
end
defp apply_action(socket, :import, params) do
IndexActions.apply_import_action(socket, params)
end
defp apply_action(socket, :barcode_scan, params) do
socket
|> apply_fallback_index(params, :records, fn s, :index, p ->
IndexActions.apply_index_action(s, p)
end)
|> assign(:page_title, gettext("Scan barcodes · Collection"))
|> assign(:record, nil)
end
defp apply_action(socket, :edit, params) do
IndexActions.apply_edit_action(socket, params)
end
defp apply_action(socket, :index, params) do
socket
|> IndexActions.apply_index_action(params)
|> assign(:open_chat, params["chat"] == "open")
end
@impl true
def handle_info({MusicLibraryWeb.Components.RecordForm, {:saved, _record}}, socket) do
IndexActions.handle_record_saved(socket)
end
def handle_info({AddRecord, {:imported_single, record}}, socket) do
IndexActions.handle_cart_imported_single(socket, record)
end
def handle_info({AddRecord, {:imported_async, count}}, socket) do
IndexActions.handle_cart_imported_async(socket, count)
end
def handle_info({MusicLibraryWeb.Components.Chat, :chats_changed}, socket) do
chat_count = Chats.count_chats(:collection, Chats.collection_musicbrainz_id())
{:noreply, assign(socket, :chat_count, chat_count)}
end
def handle_info(:records_index_changed, socket)
when socket.assigns.live_action in [:index, :edit] do
{:noreply, IndexActions.handle_index_changed(socket)}
end
def handle_info(:records_index_changed, socket) do
{:noreply, socket}
end
@impl true
def handle_async(:collection_summary, {:ok, summary}, socket) do
{:noreply, assign(socket, :collection_summary, summary)}
end
def handle_async(:collection_summary, {:exit, _reason}, socket) do
{:noreply, socket}
end
def handle_async({:delete_record, _id}, {:ok, {:ok, _record}}, socket) do
{:noreply, socket}
end
def handle_async({:delete_record, id}, {:ok, {:error, reason}}, socket) do
record = Records.get_record!(id)
{:noreply,
socket
|> stream_insert(:records, record)
|> put_toast(:error, gettext("Failed to delete: ") <> ErrorMessages.friendly_message(reason))}
end
def handle_async({:delete_record, id}, {:exit, _reason}, socket) do
record = Records.get_record!(id)
{:noreply,
socket
|> stream_insert(:records, record)
|> put_toast(:error, gettext("Delete failed, please try again"))}
end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
IndexActions.handle_delete(socket, id)
end
def handle_event("search", %{"query" => query}, socket) do
IndexActions.handle_search(socket, query)
end
def handle_event("set_display", %{"mode" => mode}, socket) do
IndexActions.handle_set_display(socket, mode)
end
defp order_path(record_list_params, order) do
qs =
record_list_params
|> Map.take([:query])
|> Map.put(:order, order)
|> Enum.filter(fn {_, v} -> v not in ["", nil] end)
~p"/collection?#{qs}"
end
defp back_path(record_list_params) do
qs =
record_list_params
|> Map.take([:query, :page, :page_size, :order])
|> Enum.filter(fn {_, v} -> v not in ["", nil] end)
~p"/collection?#{qs}"
end
end