First pass at record sets

This commit is contained in:
Claudio Ortolina
2026-02-05 11:46:52 +00:00
parent b17fa006d5
commit 419b093101
13 changed files with 1089 additions and 0 deletions
+144
View File
@@ -0,0 +1,144 @@
defmodule MusicLibrary.RecordSets do
import Ecto.Query, warn: false
alias MusicLibrary.RecordSets.{RecordSet, RecordSetItem}
alias MusicLibrary.Repo
def list_record_sets(opts \\ []) do
offset = Keyword.get(opts, :offset, 0)
limit = Keyword.get(opts, :limit, 20)
from(rs in RecordSet,
order_by: [desc: rs.updated_at],
offset: ^offset,
limit: ^limit,
preload: [items: :record]
)
|> Repo.all()
end
def count_record_sets do
Repo.aggregate(RecordSet, :count)
end
def get_record_set!(id) do
RecordSet
|> Repo.get!(id)
|> Repo.preload(items: :record)
end
def create_record_set(attrs) do
%RecordSet{}
|> RecordSet.changeset(attrs)
|> Repo.insert()
|> case do
{:ok, record_set} -> {:ok, Repo.preload(record_set, items: :record)}
error -> error
end
end
def update_record_set(%RecordSet{} = record_set, attrs) do
record_set
|> RecordSet.changeset(attrs)
|> Repo.update()
|> case do
{:ok, record_set} -> {:ok, Repo.preload(record_set, [items: :record], force: true)}
error -> error
end
end
def delete_record_set(%RecordSet{} = record_set) do
Repo.delete(record_set)
end
def change_record_set(%RecordSet{} = record_set, attrs \\ %{}) do
RecordSet.changeset(record_set, attrs)
end
def add_record_to_set(%RecordSet{} = record_set, record_id) do
next_position =
from(i in RecordSetItem,
where: i.record_set_id == ^record_set.id,
select: coalesce(max(i.position), -1)
)
|> Repo.one!()
|> Kernel.+(1)
%RecordSetItem{}
|> RecordSetItem.changeset(%{position: next_position})
|> Ecto.Changeset.put_change(:record_set_id, record_set.id)
|> Ecto.Changeset.put_change(:record_id, record_id)
|> Ecto.Changeset.unique_constraint([:record_set_id, :record_id])
|> Repo.insert()
|> case do
{:ok, _item} -> {:ok, get_record_set!(record_set.id)}
error -> error
end
end
def remove_record_from_set(%RecordSet{} = record_set, record_id) do
item =
from(i in RecordSetItem,
where: i.record_set_id == ^record_set.id and i.record_id == ^record_id
)
|> Repo.one!()
Repo.delete(item)
recompact_positions(record_set.id)
{:ok, get_record_set!(record_set.id)}
end
def move_record_in_set(%RecordSet{} = record_set, record_id, direction)
when direction in [:up, :down] do
items =
from(i in RecordSetItem,
where: i.record_set_id == ^record_set.id,
order_by: [asc: i.position]
)
|> Repo.all()
index = Enum.find_index(items, fn i -> i.record_id == record_id end)
swap_index =
case direction do
:up -> index - 1
:down -> index + 1
end
if index && swap_index >= 0 && swap_index < length(items) do
item_a = Enum.at(items, index)
item_b = Enum.at(items, swap_index)
Repo.transaction(fn ->
item_a
|> Ecto.Changeset.change(position: item_b.position)
|> Repo.update!()
item_b
|> Ecto.Changeset.change(position: item_a.position)
|> Repo.update!()
end)
end
{:ok, get_record_set!(record_set.id)}
end
defp recompact_positions(record_set_id) do
items =
from(i in RecordSetItem,
where: i.record_set_id == ^record_set_id,
order_by: [asc: i.position]
)
|> Repo.all()
Enum.with_index(items, fn item, index ->
if item.position != index do
item
|> Ecto.Changeset.change(position: index)
|> Repo.update!()
end
end)
end
end
@@ -0,0 +1,24 @@
defmodule MusicLibrary.RecordSets.RecordSet do
use Ecto.Schema
import Ecto.Changeset
alias MusicLibrary.RecordSets.RecordSetItem
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "record_sets" do
field :name, :string
field :description, :string
has_many :items, RecordSetItem, preload_order: [asc: :position]
timestamps(type: :utc_datetime)
end
def changeset(record_set, attrs) do
record_set
|> cast(attrs, [:name, :description])
|> validate_required([:name])
end
end
@@ -0,0 +1,25 @@
defmodule MusicLibrary.RecordSets.RecordSetItem do
use Ecto.Schema
import Ecto.Changeset
alias MusicLibrary.Records.Record
alias MusicLibrary.RecordSets.RecordSet
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "record_set_items" do
field :position, :integer
belongs_to :record_set, RecordSet
belongs_to :record, Record
timestamps(type: :utc_datetime)
end
def changeset(record_set_item, attrs) do
record_set_item
|> cast(attrs, [:position])
|> validate_required([:position])
end
end
@@ -35,6 +35,19 @@
/>
{gettext("Wishlist")}
</.nav_link>
<.nav_link
route={~p"/record-sets"}
section={:record_sets}
current_section={@current_section}
>
<.icon
name="hero-rectangle-stack"
class="max-sm:hidden h-4 w-4 mr-2"
aria-hidden="true"
data-slot="icon"
/>
{gettext("Sets")}
</.nav_link>
</div>
</div>
<div class="flex items-center">
@@ -0,0 +1,95 @@
defmodule MusicLibraryWeb.RecordSetLive.Form do
use MusicLibraryWeb, :live_component
alias MusicLibrary.RecordSets
@impl true
def render(assigns) do
~H"""
<div>
<header class="mb-6">
<h1 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100">
{@title}
</h1>
</header>
<.simple_form
for={@form}
id="record-set-form"
phx-target={@myself}
phx-change="validate"
phx-submit="save"
>
<.input
field={@form[:name]}
type="text"
label={gettext("Name")}
placeholder={gettext("e.g. Favorites, Road Trip, Sunday Morning")}
/>
<.textarea
field={@form[:description]}
label={gettext("Description (optional)")}
placeholder={gettext("What is this set about?")}
/>
<:actions>
<.button phx-disable-with={gettext("Saving...")}>
{gettext("Save Set")}
</.button>
</:actions>
</.simple_form>
</div>
"""
end
@impl true
def update(%{record_set: record_set} = assigns, socket) do
{:ok,
socket
|> assign(assigns)
|> assign_new(:form, fn ->
to_form(RecordSets.change_record_set(record_set))
end)}
end
@impl true
def handle_event("validate", %{"record_set" => record_set_params}, socket) do
changeset =
RecordSets.change_record_set(socket.assigns.record_set, record_set_params)
{:noreply, assign(socket, form: to_form(changeset, action: :validate))}
end
def handle_event("save", %{"record_set" => record_set_params}, socket) do
save_record_set(socket, socket.assigns.action, record_set_params)
end
defp save_record_set(socket, :edit, record_set_params) do
case RecordSets.update_record_set(socket.assigns.record_set, record_set_params) do
{:ok, record_set} ->
notify_parent({:updated, record_set})
put_toast!(:info, gettext("Record set updated successfully"))
{:noreply, push_patch(socket, to: socket.assigns.patch)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
end
defp save_record_set(socket, :new, record_set_params) do
case RecordSets.create_record_set(record_set_params) do
{:ok, record_set} ->
notify_parent({:created, record_set})
put_toast!(:info, gettext("Record set created successfully"))
{:noreply, push_patch(socket, to: socket.assigns.patch)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
end
defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
end
@@ -0,0 +1,184 @@
defmodule MusicLibraryWeb.RecordSetLive.Index do
use MusicLibraryWeb, :live_view
import MusicLibraryWeb.Components.Pagination
alias MusicLibrary.RecordSets
alias MusicLibrary.RecordSets.RecordSet
@default_list_params %{
page: 1,
page_size: 20,
query: "",
order: :updated_at
}
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, :current_section, :record_sets)}
end
@impl true
def handle_params(params, _url, socket) do
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
end
defp apply_action(socket, :edit, %{"id" => id} = params) do
socket
|> apply_fallback_index(params)
|> assign(:page_title, gettext("Edit Set"))
|> assign(:record_set, RecordSets.get_record_set!(id))
end
defp apply_action(socket, :new, params) do
socket
|> apply_fallback_index(params)
|> assign(:page_title, gettext("New Set"))
|> assign(:record_set, %RecordSet{})
end
defp apply_action(socket, :add_record, %{"id" => id} = params) do
socket
|> apply_fallback_index(params)
|> assign(:page_title, gettext("Add Record"))
|> assign(:record_set, RecordSets.get_record_set!(id))
end
defp apply_action(socket, :index, params) do
total_sets = RecordSets.count_record_sets()
list_params =
@default_list_params
|> merge_pagination(params, total_sets)
load_and_assign_sets(socket, list_params)
end
defp apply_fallback_index(socket, params) do
if get_in(socket.assigns, [:streams, :record_sets]) == nil do
socket
|> apply_action(:index, params)
else
socket
end
end
defp merge_pagination(params, url_params, total_records) do
page = parse_page(url_params["page"])
page_size = parse_page_size(url_params["page_size"])
params
|> Map.put(:page, page)
|> Map.put(:page_size, page_size)
|> Map.put(:total_records, total_records)
end
defp parse_page(nil), do: 1
defp parse_page(page) when is_binary(page) do
case Integer.parse(page) do
{num, ""} when num > 0 -> num
_ -> 1
end
end
defp parse_page(_), do: 1
defp parse_page_size(nil), do: 20
defp parse_page_size(page_size) when is_binary(page_size) do
case Integer.parse(page_size) do
{num, ""} when num in [20, 50, 100] -> num
_ -> 20
end
end
defp parse_page_size(_), do: 20
defp load_and_assign_sets(socket, list_params) do
offset = page_to_offset(list_params.page, list_params.page_size)
sets =
RecordSets.list_record_sets(
offset: offset,
limit: list_params.page_size
)
list_params_with_total =
Map.put(list_params, :total_entries, list_params.total_records)
socket
|> assign(:list_params, list_params_with_total)
|> assign(:page_title, gettext("Record Sets"))
|> assign(:record_set, nil)
|> stream(:record_sets, sets, reset: true)
end
def back_path(list_params) do
qs =
list_params
|> Map.take([:page, :page_size])
~p"/record-sets?#{qs}"
end
@impl true
def handle_info(
{MusicLibraryWeb.RecordSetLive.Form, {:created, record_set}},
socket
) do
{:noreply,
socket
|> stream_insert(:record_sets, record_set, at: 0)
|> load_and_assign_sets(socket.assigns.list_params)}
end
def handle_info(
{MusicLibraryWeb.RecordSetLive.Form, {:updated, record_set}},
socket
) do
{:noreply, stream_insert(socket, :record_sets, record_set)}
end
def handle_info(
{MusicLibraryWeb.RecordSetLive.RecordPicker, {:added, record_set}},
socket
) do
{:noreply,
socket
|> stream_insert(:record_sets, record_set)
|> push_patch(to: back_path(socket.assigns.list_params))}
end
@impl true
def handle_event("delete_set", %{"id" => id}, socket) do
record_set = RecordSets.get_record_set!(id)
{:ok, _} = RecordSets.delete_record_set(record_set)
{:noreply,
socket
|> stream_delete(:record_sets, record_set)
|> load_and_assign_sets(socket.assigns.list_params)}
end
def handle_event("remove_record", %{"set-id" => set_id, "record-id" => record_id}, socket) do
record_set = RecordSets.get_record_set!(set_id)
{:ok, updated_set} = RecordSets.remove_record_from_set(record_set, record_id)
{:noreply, stream_insert(socket, :record_sets, updated_set)}
end
def handle_event("move_up", %{"set-id" => set_id, "record-id" => record_id}, socket) do
record_set = RecordSets.get_record_set!(set_id)
{:ok, updated_set} = RecordSets.move_record_in_set(record_set, record_id, :up)
{:noreply, stream_insert(socket, :record_sets, updated_set)}
end
def handle_event("move_down", %{"set-id" => set_id, "record-id" => record_id}, socket) do
record_set = RecordSets.get_record_set!(set_id)
{:ok, updated_set} = RecordSets.move_record_in_set(record_set, record_id, :down)
{:noreply, stream_insert(socket, :record_sets, updated_set)}
end
end
@@ -0,0 +1,202 @@
<Layouts.app flash={@flash} current_section={@current_section} socket={@socket}>
<header class="mb-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-zinc-900 dark:text-zinc-100">
{gettext("Record Sets")}
</h1>
</div>
<div>
<.button
variant="solid"
size="sm"
patch={~p"/record-sets/new"}
>
<.icon name="hero-plus" class="icon" aria-hidden="true" data-slot="icon" />
{gettext("New Set")}
</.button>
</div>
</div>
</header>
<div class="mt-6 space-y-6">
<ul phx-update="stream" id="record-sets-list" class="space-y-6">
<li
id="no-record-sets"
class="hidden only:block p-8 text-center bg-zinc-50 dark:bg-zinc-800 rounded-lg"
>
<.icon name="hero-rectangle-stack" class="h-12 w-12 text-zinc-400 mx-auto mb-4" />
<p class="text-zinc-600 dark:text-zinc-400">
{gettext("No record sets yet")}
</p>
</li>
<li
:for={{dom_id, record_set} <- @streams.record_sets}
id={dom_id}
class="bg-white dark:bg-zinc-900 rounded-lg border border-zinc-200 dark:border-zinc-700 p-4"
>
<div class="flex items-center justify-between mb-3">
<div>
<h2 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100">
{record_set.name}
</h2>
<p :if={record_set.description} class="text-sm text-zinc-500 dark:text-zinc-400 mt-1">
{record_set.description}
</p>
</div>
<div class="flex items-center gap-2">
<span class="text-xs text-zinc-500 dark:text-zinc-400">
{ngettext("1 record", "%{count} records", length(record_set.items))}
</span>
<.dropdown id={"set-actions-#{record_set.id}"} placement="bottom-end">
<:toggle>
<.button variant="ghost">
<span class="sr-only">{gettext("Actions")}</span>
<.icon
name="hero-ellipsis-vertical"
class="h-5 w-5 text-zinc-500 dark:text-zinc-400 cursor-pointer"
aria-hidden="true"
data-slot="icon"
/>
</.button>
</:toggle>
<.dropdown_link
id={"set-actions-#{record_set.id}-edit"}
patch={
~p"/record-sets/#{record_set}/edit?#{Map.take(@list_params, [:page, :page_size])}"
}
>
{gettext("Edit")}
</.dropdown_link>
<.separator />
<.dropdown_button
phx-click="delete_set"
phx-value-id={record_set.id}
data-confirm={gettext("Are you sure?")}
class={[
"text-red-900! hover:bg-red-50! dark:text-red-500! dark:hover:bg-red-900/30! dark:hover:text-red-600!"
]}
>
{gettext("Delete")}
</.dropdown_button>
</.dropdown>
</div>
</div>
<div class="flex items-center gap-3 overflow-x-auto pb-2">
<div
:for={item <- record_set.items}
class="flex-none w-24 sm:w-32 group relative"
>
<.link navigate={~p"/collection/#{item.record}"}>
<MusicLibraryWeb.RecordComponents.record_cover
record={item.record}
class="rounded-lg aspect-square object-cover"
width={256}
/>
</.link>
<div class="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity">
<.dropdown
id={"item-actions-#{record_set.id}-#{item.record.id}"}
placement="bottom-end"
>
<:toggle>
<span class="rounded-full bg-zinc-900/60 p-0.5 cursor-pointer">
<.icon
name="hero-ellipsis-vertical"
class="h-4 w-4 text-white"
aria-hidden="true"
data-slot="icon"
/>
</span>
</:toggle>
<.dropdown_button
:if={item.position > 0}
phx-click="move_up"
phx-value-set-id={record_set.id}
phx-value-record-id={item.record.id}
>
{gettext("Move left")}
</.dropdown_button>
<.dropdown_button
:if={item.position < length(record_set.items) - 1}
phx-click="move_down"
phx-value-set-id={record_set.id}
phx-value-record-id={item.record.id}
>
{gettext("Move right")}
</.dropdown_button>
<.separator />
<.dropdown_button
phx-click="remove_record"
phx-value-set-id={record_set.id}
phx-value-record-id={item.record.id}
data-confirm={gettext("Remove this record from the set?")}
class={[
"text-red-900! hover:bg-red-50! dark:text-red-500! dark:hover:bg-red-900/30! dark:hover:text-red-600!"
]}
>
{gettext("Remove")}
</.dropdown_button>
</.dropdown>
</div>
<p class="mt-1 text-xs text-zinc-700 dark:text-zinc-300 truncate">
{item.record.title}
</p>
</div>
<.link
patch={~p"/record-sets/#{record_set}/add-record"}
class={[
"flex-none w-24 sm:w-32 aspect-square",
"border-2 border-dashed border-zinc-300 dark:border-zinc-600",
"rounded-lg flex items-center justify-center",
"hover:border-zinc-400 dark:hover:border-zinc-500",
"hover:bg-zinc-50 dark:hover:bg-zinc-800",
"transition-colors cursor-pointer"
]}
>
<.icon
name="hero-plus"
class="h-8 w-8 text-zinc-400 dark:text-zinc-500"
aria-hidden="true"
data-slot="icon"
/>
</.link>
</div>
</li>
</ul>
<.pagination id={:bottom_pagination} pagination_params={@list_params} />
</div>
<.structured_modal
:if={@live_action in [:new, :edit]}
id="record-set-modal"
on_close={JS.patch(back_path(@list_params))}
>
<.live_component
module={MusicLibraryWeb.RecordSetLive.Form}
id={@record_set.id || :new}
title={@page_title}
action={@live_action}
record_set={@record_set}
patch={back_path(@list_params)}
/>
</.structured_modal>
<.structured_modal
:if={@live_action == :add_record}
id="record-picker-modal"
on_close={JS.patch(back_path(@list_params))}
>
<.live_component
module={MusicLibraryWeb.RecordSetLive.RecordPicker}
id={"record-picker-#{@record_set.id}"}
title={@page_title}
record_set={@record_set}
patch={back_path(@list_params)}
/>
</.structured_modal>
</Layouts.app>
@@ -0,0 +1,143 @@
defmodule MusicLibraryWeb.RecordSetLive.RecordPicker do
use MusicLibraryWeb, :live_component
alias MusicLibrary.Collection
alias MusicLibrary.Records.Record
@impl true
def render(assigns) do
~H"""
<div>
<header class="mb-6">
<h1 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100">
{@title}
</h1>
<p class="text-sm text-zinc-500 dark:text-zinc-400 mt-1">
{gettext("Search your collection to add a record to this set.")}
</p>
</header>
<form
class="w-full sm:w-1/3"
for={@query}
phx-submit="search"
phx-change="search"
phx-target={@myself}
>
<.input
type="search"
size="sm"
id={:query}
name={:query}
value={@query}
placeholder={gettext("Search")}
phx-debounce="500"
autocomplete="off"
autofocus
/>
</form>
<ul
:if={@query != ""}
class="mt-4 divide-y divide-zinc-100 dark:divide-zinc-700 max-h-96 overflow-y-auto"
>
<li
:if={@results == []}
class="py-4 text-center text-sm text-zinc-500 dark:text-zinc-400"
>
{gettext("No records found")}
</li>
<li
:for={record <- @results}
class="flex items-center gap-3 py-3 px-2 hover:bg-zinc-50 dark:hover:bg-zinc-800 cursor-pointer rounded-lg"
phx-click="add_record"
phx-target={@myself}
phx-value-record-id={record.id}
>
<div class="w-12 flex-none">
<MusicLibraryWeb.RecordComponents.record_cover
record={record}
class="rounded aspect-square object-cover"
width={96}
/>
</div>
<div class="min-w-0 flex-auto">
<p class="text-sm font-medium text-zinc-900 dark:text-zinc-100 truncate">
{record.title}
</p>
<p class="text-xs text-zinc-500 dark:text-zinc-400 truncate">
{Record.artist_names(record)}
</p>
</div>
<div class="flex-none">
<.icon
name="hero-plus-circle"
class="h-5 w-5 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300"
aria-hidden="true"
data-slot="icon"
/>
</div>
</li>
</ul>
</div>
"""
end
@impl true
def update(assigns, socket) do
existing_record_ids =
MapSet.new(assigns.record_set.items, fn item -> item.record.id end)
{:ok,
socket
|> assign(assigns)
|> assign(:query, "")
|> assign(:results, [])
|> assign(:existing_record_ids, existing_record_ids)}
end
@impl true
def handle_event("search", %{"query" => query}, socket) do
results =
if String.trim(query) == "" do
[]
else
query
|> Collection.search_records(limit: 20)
|> Enum.reject(fn record ->
MapSet.member?(socket.assigns.existing_record_ids, record.id)
end)
end
{:noreply,
socket
|> assign(:query, query)
|> assign(:results, results)}
end
def handle_event("add_record", %{"record-id" => record_id}, socket) do
record_set = socket.assigns.record_set
case MusicLibrary.RecordSets.add_record_to_set(record_set, record_id) do
{:ok, updated_set} ->
existing_record_ids = MapSet.put(socket.assigns.existing_record_ids, record_id)
results =
Enum.reject(socket.assigns.results, fn record -> record.id == record_id end)
notify_parent({:added, updated_set})
put_toast!(:info, gettext("Record added to set"))
{:noreply,
socket
|> assign(:record_set, updated_set)
|> assign(:existing_record_ids, existing_record_ids)
|> assign(:results, results)}
{:error, _changeset} ->
put_toast!(:error, gettext("Could not add record to set"))
{:noreply, socket}
end
end
defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
end
+5
View File
@@ -65,6 +65,11 @@ defmodule MusicLibraryWeb.Router do
live "/artists/:musicbrainz_id/import", ArtistLive.Show, :import
live "/artists/:musicbrainz_id/edit", ArtistLive.Show, :edit
live "/record-sets", RecordSetLive.Index, :index
live "/record-sets/new", RecordSetLive.Index, :new
live "/record-sets/:id/edit", RecordSetLive.Index, :edit
live "/record-sets/:id/add-record", RecordSetLive.Index, :add_record
live "/scrobble-rules", ScrobbleRulesLive.Index, :index
live "/scrobble-rules/new", ScrobbleRulesLive.Index, :new
live "/scrobble-rules/:id/edit", ScrobbleRulesLive.Index, :edit
+110
View File
@@ -14,6 +14,7 @@ msgstr ""
#: lib/music_library_web/components/record_components.ex
#: lib/music_library_web/live/collection_live/show.html.heex
#: lib/music_library_web/live/online_store_template_live/index.html.heex
#: lib/music_library_web/live/record_set_live/index.html.heex
#: lib/music_library_web/live/scrobble_rules_live/index.html.heex
#: lib/music_library_web/live/scrobbled_tracks_live/index.html.heex
#: lib/music_library_web/live/wishlist_live/show.html.heex
@@ -38,6 +39,7 @@ msgstr ""
#: lib/music_library_web/components/record_components.ex
#: lib/music_library_web/live/collection_live/show.html.heex
#: lib/music_library_web/live/online_store_template_live/index.html.heex
#: lib/music_library_web/live/record_set_live/index.html.heex
#: lib/music_library_web/live/scrobble_rules_live/index.html.heex
#: lib/music_library_web/live/scrobbled_tracks_live/index.html.heex
#: lib/music_library_web/live/wishlist_live/show.html.heex
@@ -52,6 +54,7 @@ msgstr ""
#: lib/music_library_web/live/collection_live/show.ex
#: lib/music_library_web/live/collection_live/show.html.heex
#: lib/music_library_web/live/online_store_template_live/index.html.heex
#: lib/music_library_web/live/record_set_live/index.html.heex
#: lib/music_library_web/live/scrobble_rules_live/index.html.heex
#: lib/music_library_web/live/scrobbled_tracks_live/index.html.heex
#: lib/music_library_web/live/wishlist_live/show.ex
@@ -175,6 +178,7 @@ msgstr ""
#: lib/music_library_web/components/record_form.ex
#: lib/music_library_web/live/artist_live/form.ex
#: lib/music_library_web/live/online_store_template_live/form.ex
#: lib/music_library_web/live/record_set_live/form.ex
#: lib/music_library_web/live/scrobble_rules_live/form.ex
#: lib/music_library_web/live/scrobbled_tracks_live/form.ex
#, elixir-autogen, elixir-format
@@ -182,6 +186,7 @@ msgid "Saving..."
msgstr ""
#: lib/music_library_web/components/core_components.ex
#: lib/music_library_web/live/record_set_live/record_picker.ex
#, elixir-autogen, elixir-format
msgid "Search"
msgstr ""
@@ -390,6 +395,7 @@ msgid "On Tour"
msgstr ""
#: lib/music_library_web/components/record_components.ex
#: lib/music_library_web/live/record_set_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "1 record"
msgid_plural "%{count} records"
@@ -899,6 +905,7 @@ msgstr ""
#: lib/music_library_web/live/artist_live/show.html.heex
#: lib/music_library_web/live/collection_live/show.html.heex
#: lib/music_library_web/live/online_store_template_live/index.html.heex
#: lib/music_library_web/live/record_set_live/index.html.heex
#: lib/music_library_web/live/scrobble_rules_live/index.html.heex
#: lib/music_library_web/live/scrobbled_tracks_live/index.html.heex
#: lib/music_library_web/live/wishlist_live/show.html.heex
@@ -977,6 +984,7 @@ msgid "Artist Name"
msgstr ""
#: lib/music_library_web/live/online_store_template_live/form.ex
#: lib/music_library_web/live/record_set_live/form.ex
#: lib/music_library_web/live/scrobble_rules_live/form.ex
#, elixir-autogen, elixir-format
msgid "Description (optional)"
@@ -1687,3 +1695,105 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "Regenerate embeddings"
msgstr ""
#: lib/music_library_web/live/record_set_live/index.ex
#, elixir-autogen, elixir-format
msgid "Add Record"
msgstr ""
#: lib/music_library_web/live/record_set_live/record_picker.ex
#, elixir-autogen, elixir-format
msgid "Could not add record to set"
msgstr ""
#: lib/music_library_web/live/record_set_live/index.ex
#, elixir-autogen, elixir-format
msgid "Edit Set"
msgstr ""
#: lib/music_library_web/live/record_set_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "Move left"
msgstr ""
#: lib/music_library_web/live/record_set_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "Move right"
msgstr ""
#: lib/music_library_web/live/record_set_live/form.ex
#, elixir-autogen, elixir-format
msgid "Name"
msgstr ""
#: lib/music_library_web/live/record_set_live/index.ex
#: lib/music_library_web/live/record_set_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "New Set"
msgstr ""
#: lib/music_library_web/live/record_set_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "No record sets yet"
msgstr ""
#: lib/music_library_web/live/record_set_live/record_picker.ex
#, elixir-autogen, elixir-format
msgid "No records found"
msgstr ""
#: lib/music_library_web/live/record_set_live/index.ex
#: lib/music_library_web/live/record_set_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "Record Sets"
msgstr ""
#: lib/music_library_web/live/record_set_live/record_picker.ex
#, elixir-autogen, elixir-format
msgid "Record added to set"
msgstr ""
#: lib/music_library_web/live/record_set_live/form.ex
#, elixir-autogen, elixir-format
msgid "Record set created successfully"
msgstr ""
#: lib/music_library_web/live/record_set_live/form.ex
#, elixir-autogen, elixir-format
msgid "Record set updated successfully"
msgstr ""
#: lib/music_library_web/live/record_set_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "Remove"
msgstr ""
#: lib/music_library_web/live/record_set_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "Remove this record from the set?"
msgstr ""
#: lib/music_library_web/live/record_set_live/form.ex
#, elixir-autogen, elixir-format
msgid "Save Set"
msgstr ""
#: lib/music_library_web/live/record_set_live/record_picker.ex
#, elixir-autogen, elixir-format
msgid "Search your collection to add a record to this set."
msgstr ""
#: lib/music_library_web/components/layouts/app.html.heex
#, elixir-autogen, elixir-format
msgid "Sets"
msgstr ""
#: lib/music_library_web/live/record_set_live/form.ex
#, elixir-autogen, elixir-format
msgid "What is this set about?"
msgstr ""
#: lib/music_library_web/live/record_set_live/form.ex
#, elixir-autogen, elixir-format
msgid "e.g. Favorites, Road Trip, Sunday Morning"
msgstr ""
+110
View File
@@ -14,6 +14,7 @@ msgstr ""
#: lib/music_library_web/components/record_components.ex
#: lib/music_library_web/live/collection_live/show.html.heex
#: lib/music_library_web/live/online_store_template_live/index.html.heex
#: lib/music_library_web/live/record_set_live/index.html.heex
#: lib/music_library_web/live/scrobble_rules_live/index.html.heex
#: lib/music_library_web/live/scrobbled_tracks_live/index.html.heex
#: lib/music_library_web/live/wishlist_live/show.html.heex
@@ -38,6 +39,7 @@ msgstr ""
#: lib/music_library_web/components/record_components.ex
#: lib/music_library_web/live/collection_live/show.html.heex
#: lib/music_library_web/live/online_store_template_live/index.html.heex
#: lib/music_library_web/live/record_set_live/index.html.heex
#: lib/music_library_web/live/scrobble_rules_live/index.html.heex
#: lib/music_library_web/live/scrobbled_tracks_live/index.html.heex
#: lib/music_library_web/live/wishlist_live/show.html.heex
@@ -52,6 +54,7 @@ msgstr ""
#: lib/music_library_web/live/collection_live/show.ex
#: lib/music_library_web/live/collection_live/show.html.heex
#: lib/music_library_web/live/online_store_template_live/index.html.heex
#: lib/music_library_web/live/record_set_live/index.html.heex
#: lib/music_library_web/live/scrobble_rules_live/index.html.heex
#: lib/music_library_web/live/scrobbled_tracks_live/index.html.heex
#: lib/music_library_web/live/wishlist_live/show.ex
@@ -175,6 +178,7 @@ msgstr ""
#: lib/music_library_web/components/record_form.ex
#: lib/music_library_web/live/artist_live/form.ex
#: lib/music_library_web/live/online_store_template_live/form.ex
#: lib/music_library_web/live/record_set_live/form.ex
#: lib/music_library_web/live/scrobble_rules_live/form.ex
#: lib/music_library_web/live/scrobbled_tracks_live/form.ex
#, elixir-autogen, elixir-format
@@ -182,6 +186,7 @@ msgid "Saving..."
msgstr ""
#: lib/music_library_web/components/core_components.ex
#: lib/music_library_web/live/record_set_live/record_picker.ex
#, elixir-autogen, elixir-format
msgid "Search"
msgstr ""
@@ -390,6 +395,7 @@ msgid "On Tour"
msgstr ""
#: lib/music_library_web/components/record_components.ex
#: lib/music_library_web/live/record_set_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "1 record"
msgid_plural "%{count} records"
@@ -899,6 +905,7 @@ msgstr ""
#: lib/music_library_web/live/artist_live/show.html.heex
#: lib/music_library_web/live/collection_live/show.html.heex
#: lib/music_library_web/live/online_store_template_live/index.html.heex
#: lib/music_library_web/live/record_set_live/index.html.heex
#: lib/music_library_web/live/scrobble_rules_live/index.html.heex
#: lib/music_library_web/live/scrobbled_tracks_live/index.html.heex
#: lib/music_library_web/live/wishlist_live/show.html.heex
@@ -977,6 +984,7 @@ msgid "Artist Name"
msgstr ""
#: lib/music_library_web/live/online_store_template_live/form.ex
#: lib/music_library_web/live/record_set_live/form.ex
#: lib/music_library_web/live/scrobble_rules_live/form.ex
#, elixir-autogen, elixir-format
msgid "Description (optional)"
@@ -1687,3 +1695,105 @@ msgstr ""
#, elixir-autogen, elixir-format, fuzzy
msgid "Regenerate embeddings"
msgstr ""
#: lib/music_library_web/live/record_set_live/index.ex
#, elixir-autogen, elixir-format
msgid "Add Record"
msgstr ""
#: lib/music_library_web/live/record_set_live/record_picker.ex
#, elixir-autogen, elixir-format
msgid "Could not add record to set"
msgstr ""
#: lib/music_library_web/live/record_set_live/index.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Edit Set"
msgstr ""
#: lib/music_library_web/live/record_set_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "Move left"
msgstr ""
#: lib/music_library_web/live/record_set_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "Move right"
msgstr ""
#: lib/music_library_web/live/record_set_live/form.ex
#, elixir-autogen, elixir-format
msgid "Name"
msgstr ""
#: lib/music_library_web/live/record_set_live/index.ex
#: lib/music_library_web/live/record_set_live/index.html.heex
#, elixir-autogen, elixir-format, fuzzy
msgid "New Set"
msgstr ""
#: lib/music_library_web/live/record_set_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "No record sets yet"
msgstr ""
#: lib/music_library_web/live/record_set_live/record_picker.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "No records found"
msgstr ""
#: lib/music_library_web/live/record_set_live/index.ex
#: lib/music_library_web/live/record_set_live/index.html.heex
#, elixir-autogen, elixir-format, fuzzy
msgid "Record Sets"
msgstr ""
#: lib/music_library_web/live/record_set_live/record_picker.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Record added to set"
msgstr ""
#: lib/music_library_web/live/record_set_live/form.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Record set created successfully"
msgstr ""
#: lib/music_library_web/live/record_set_live/form.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Record set updated successfully"
msgstr ""
#: lib/music_library_web/live/record_set_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "Remove"
msgstr ""
#: lib/music_library_web/live/record_set_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "Remove this record from the set?"
msgstr ""
#: lib/music_library_web/live/record_set_live/form.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Save Set"
msgstr ""
#: lib/music_library_web/live/record_set_live/record_picker.ex
#, elixir-autogen, elixir-format
msgid "Search your collection to add a record to this set."
msgstr ""
#: lib/music_library_web/components/layouts/app.html.heex
#, elixir-autogen, elixir-format
msgid "Sets"
msgstr ""
#: lib/music_library_web/live/record_set_live/form.ex
#, elixir-autogen, elixir-format
msgid "What is this set about?"
msgstr ""
#: lib/music_library_web/live/record_set_live/form.ex
#, elixir-autogen, elixir-format
msgid "e.g. Favorites, Road Trip, Sunday Morning"
msgstr ""
@@ -0,0 +1,13 @@
defmodule MusicLibrary.Repo.Migrations.CreateRecordSets do
use Ecto.Migration
def change do
create table(:record_sets, primary_key: false) do
add :id, :binary_id, primary_key: true
add :name, :string, null: false
add :description, :text
timestamps(type: :utc_datetime)
end
end
end
@@ -0,0 +1,21 @@
defmodule MusicLibrary.Repo.Migrations.CreateRecordSetItems do
use Ecto.Migration
def change do
create table(:record_set_items, primary_key: false) do
add :id, :binary_id, primary_key: true
add :position, :integer, null: false
add :record_set_id, references(:record_sets, type: :binary_id, on_delete: :delete_all),
null: false
add :record_id, references(:records, type: :binary_id, on_delete: :delete_all), null: false
timestamps(type: :utc_datetime)
end
create index(:record_set_items, [:record_set_id])
create unique_index(:record_set_items, [:record_set_id, :record_id])
create index(:record_set_items, [:record_set_id, :position])
end
end