First pass at record sets
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user