From 9b305ff06a15cc14ced5fee2632e813ba7e25e59 Mon Sep 17 00:00:00 2001 From: Claudio Ortolina Date: Mon, 21 Oct 2024 18:25:56 +0100 Subject: [PATCH] Add wishlist section with paginated list --- lib/music_library/wishlist.ex | 61 ++++++ .../components/layouts/app.html.heex | 3 + .../live/wishlist_live/index.ex | 99 ++++++++++ .../live/wishlist_live/index.html.heex | 187 ++++++++++++++++++ lib/music_library_web/router.ex | 2 + 5 files changed, 352 insertions(+) create mode 100644 lib/music_library/wishlist.ex create mode 100644 lib/music_library_web/live/wishlist_live/index.ex create mode 100644 lib/music_library_web/live/wishlist_live/index.html.heex diff --git a/lib/music_library/wishlist.ex b/lib/music_library/wishlist.ex new file mode 100644 index 00000000..54c38555 --- /dev/null +++ b/lib/music_library/wishlist.ex @@ -0,0 +1,61 @@ +defmodule MusicLibrary.Wishlist do + import Ecto.Query, warn: false + + alias MusicLibrary.Repo + alias MusicLibrary.Records.{Record, SearchParser} + + @fields [:id, :type, :artists, :format, :title, :release, :genres, :musicbrainz_id, :cover_hash] + + def search_records(query, opts \\ []) do + limit = Keyword.get(opts, :limit, 20) + offset = Keyword.get(opts, :offset, 0) + + search = + query + |> build_search() + |> limit(^limit) + |> offset(^offset) + |> select(^@fields) + + Repo.all(search) + end + + def search_records_count(query) do + search = build_search(query) + + Repo.aggregate(search, :count) + end + + defp build_search(query) do + {:ok, parsed_query} = SearchParser.parse(query) + + base_search = + from r in Record, + where: is_nil(r.purchased_at), + order_by: [r.artists[0]["sort_name"], r.title] + + Enum.reduce(parsed_query, base_search, fn + {:artist, artist}, search -> + search |> where([r], like(r.artists, ^"%#{artist}%")) + + {:album, album}, search -> + search |> where([r], like(r.title, ^"%#{album}%")) + + {:mbid, mbid}, search -> + search |> where([r], r.musicbrainz_id == ^mbid or like(r.artists, ^"%#{mbid}%")) + + {:format, format}, search -> + search |> where([r], r.format == ^format) + + {:type, type}, search -> + search |> where([r], r.type == ^type) + + {:query, raw_query}, search -> + search + |> where( + [r], + like(r.title, ^"%#{raw_query}%") or like(r.artists, ^"%#{raw_query}%") + ) + end) + end +end diff --git a/lib/music_library_web/components/layouts/app.html.heex b/lib/music_library_web/components/layouts/app.html.heex index ec2b6af9..caac9ad6 100644 --- a/lib/music_library_web/components/layouts/app.html.heex +++ b/lib/music_library_web/components/layouts/app.html.heex @@ -9,6 +9,9 @@ <%= gettext("Collection") %> + + <%= gettext("Wishlist") %> +
diff --git a/lib/music_library_web/live/wishlist_live/index.ex b/lib/music_library_web/live/wishlist_live/index.ex new file mode 100644 index 00000000..07248318 --- /dev/null +++ b/lib/music_library_web/live/wishlist_live/index.ex @@ -0,0 +1,99 @@ +defmodule MusicLibraryWeb.WishlistLive.Index do + use MusicLibraryWeb, :live_view + import MusicLibraryWeb.Pagination + + alias MusicLibrary.Wishlist + alias MusicLibrary.Records + + @default_records_list_params %{ + query: "", + page: 1, + page_size: 100 + } + + @impl true + def mount(_params, _session, socket) do + socket = + if static_changed?(socket) do + put_flash(socket, :warning, gettext("The application has been updated, please reload.")) + else + socket + end + + {:ok, assign(socket, :nav_section, :wishlist)} + end + + @impl true + def handle_params(params, _url, socket) do + {:noreply, apply_action(socket, socket.assigns.live_action, params)} + end + + defp apply_action(socket, :index, params) do + query = params["query"] || "" + total_records = Wishlist.search_records_count(query) + + record_list_params = + @default_records_list_params + |> merge_query(query) + |> merge_pagination(params, total_records) + + offset = page_to_offset(record_list_params.page, record_list_params.page_size) + records = Wishlist.search_records(query, limit: record_list_params.page_size, offset: offset) + + socket + |> assign(:page_title, gettext("Collection")) + |> assign(:record, nil) + |> assign(:record_list_params, record_list_params) + |> stream(:records, records, reset: true) + end + + @impl true + def handle_info({MusicLibraryWeb.RecordLive.FormComponent, {:saved, record}}, socket) do + {:noreply, stream_insert(socket, :records, record)} + end + + @impl true + def handle_event("search", %{"query" => query}, socket) do + qs = + @default_records_list_params + |> Map.put(:query, query) + |> Map.take([:query, :page, :page_size]) + |> URI.encode_query() + + {:noreply, push_patch(socket, to: ~s"/records?#{qs}")} + end + + defp merge_query(record_list_params, query) do + Map.put(record_list_params, :query, query) + end + + defp merge_pagination(record_list_params, params, total_records) do + record_list_params + |> Map.put(:page, parse_int_or_default(params["page"], record_list_params.page)) + |> Map.put( + :page_size, + parse_int_or_default(params["page_size"], record_list_params.page_size) + ) + |> Map.put(:total_entries, total_records) + end + + defp parse_int_or_default(nil, default), do: default + + defp parse_int_or_default(value, _default) when is_binary(value) do + String.to_integer(value) + end + + defp musicbrainz_url(record) do + "https://musicbrainz.org/release-group/#{record.musicbrainz_id}" + end + + defp toggle_actions_menu(record_id) do + JS.toggle(to: "#actions-#{record_id}") + |> JS.toggle_class("pointer-events-none", to: "#records > li") + end + + def close_actions_menu(record_id) do + JS.hide(to: "#actions-#{record_id}") + |> JS.remove_class("pointer-events-none", to: "#records > li") + end +end diff --git a/lib/music_library_web/live/wishlist_live/index.html.heex b/lib/music_library_web/live/wishlist_live/index.html.heex new file mode 100644 index 00000000..c7d65a1a --- /dev/null +++ b/lib/music_library_web/live/wishlist_live/index.html.heex @@ -0,0 +1,187 @@ +
+
+

+ <%= gettext("All Records") %> +

+
+
+ +
+ <.link patch={~p"/records/import"}> + <.button><%= gettext("Import") %> + +
+
+ +

+ <%= gettext( + "Showing %{visible} of %{total} records", + %{visible: Enum.count(@streams.records), total: @record_list_params.total_entries} + ) + |> raw() %> +

+
+ + + +<.modal :if={@live_action == :edit} id="record-modal" show on_cancel={JS.patch(~p"/records")}> + <.live_component + module={MusicLibraryWeb.RecordLive.FormComponent} + id={@record.id} + title={@page_title} + action={@live_action} + record={@record} + patch={~p"/records"} + /> + + +<.modal :if={@live_action == :import} id="record-modal" show on_cancel={JS.patch(~p"/records")}> + <.live_component + module={MusicLibraryWeb.RecordLive.ImportComponent} + id={:search} + title={@page_title} + action={@live_action} + record={@record} + patch={~p"/records"} + initial_query="" + /> + + +<.pagination id={:bottom_pagination} pagination_params={@record_list_params} /> diff --git a/lib/music_library_web/router.ex b/lib/music_library_web/router.ex index 224d123c..7c6e7d94 100644 --- a/lib/music_library_web/router.ex +++ b/lib/music_library_web/router.ex @@ -37,6 +37,8 @@ defmodule MusicLibraryWeb.Router do live "/records/:id", RecordLive.Show, :show live "/records/:id/show/edit", RecordLive.Show, :edit + + live "/wishlist", WishlistLive.Index, :index end end