Scrobbled tracks CRUD
- Failing tests - Warnings
This commit is contained in:
@@ -15,5 +15,6 @@ defmodule LastFm.Album do
|
||||
def changeset(album, attrs) do
|
||||
album
|
||||
|> cast(attrs, [:musicbrainz_id, :title])
|
||||
|> validate_required([:title])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -50,6 +50,7 @@ defmodule LastFm.Artist do
|
||||
:on_tour,
|
||||
:base_url
|
||||
])
|
||||
|> validate_required([:name])
|
||||
end
|
||||
|
||||
def events_url(artist) do
|
||||
|
||||
@@ -29,8 +29,8 @@ defmodule LastFm.Track do
|
||||
field :cover_url, :string
|
||||
field :scrobbled_at_label, :string
|
||||
|
||||
embeds_one :artist, Artist
|
||||
embeds_one :album, Album
|
||||
embeds_one :artist, Artist, on_replace: :update
|
||||
embeds_one :album, Album, on_replace: :update
|
||||
|
||||
field :last_fm_data, :map, default: %{}
|
||||
end
|
||||
@@ -89,6 +89,6 @@ defmodule LastFm.Track do
|
||||
])
|
||||
|> cast_embed(:artist)
|
||||
|> cast_embed(:album)
|
||||
|> validate_required([:scrobbled_at_uts])
|
||||
|> validate_required([:scrobbled_at_uts, :title])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -376,4 +376,100 @@ defmodule MusicLibrary.ScrobbleActivity do
|
||||
:last_365_days -> get_top_artists_by_days(365, opts)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Lists scrobbled tracks with pagination and search support.
|
||||
"""
|
||||
def list_tracks(params \\ %{}) do
|
||||
query = Map.get(params, :query, "")
|
||||
page = Map.get(params, :page, 1)
|
||||
page_size = Map.get(params, :page_size, 200)
|
||||
order = Map.get(params, :order, :scrobbled_at)
|
||||
|
||||
base_query = from(t in Track)
|
||||
|
||||
search_query =
|
||||
if query == "" do
|
||||
base_query
|
||||
else
|
||||
query_term = "%#{String.downcase(query)}%"
|
||||
|
||||
from t in base_query,
|
||||
where:
|
||||
like(fragment("lower(?)", t.title), ^query_term) or
|
||||
like(fragment("lower(json_extract(artist, '$.name'))"), ^query_term) or
|
||||
like(fragment("lower(json_extract(album, '$.title'))"), ^query_term)
|
||||
end
|
||||
|
||||
ordered_query =
|
||||
case order do
|
||||
:scrobbled_at ->
|
||||
from t in search_query, order_by: [desc: t.scrobbled_at_uts]
|
||||
|
||||
:title ->
|
||||
from t in search_query, order_by: [asc: t.title]
|
||||
|
||||
:artist ->
|
||||
from t in search_query, order_by: [asc: fragment("json_extract(artist, '$.name')")]
|
||||
|
||||
:album ->
|
||||
from t in search_query, order_by: [asc: fragment("json_extract(album, '$.title')")]
|
||||
end
|
||||
|
||||
offset = (page - 1) * page_size
|
||||
|
||||
from(t in ordered_query, limit: ^page_size, offset: ^offset)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single track by scrobbled_at_uts.
|
||||
"""
|
||||
def get_track!(scrobbled_at_uts) when is_integer(scrobbled_at_uts) do
|
||||
Repo.get!(Track, scrobbled_at_uts)
|
||||
end
|
||||
|
||||
def get_track!(scrobbled_at_uts) when is_binary(scrobbled_at_uts) do
|
||||
case Integer.parse(scrobbled_at_uts) do
|
||||
{id, ""} -> get_track!(id)
|
||||
_ -> raise Ecto.NoResultsError, queryable: Track
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a track with the given attributes.
|
||||
"""
|
||||
def update_track(%Track{} = track, attrs) do
|
||||
changeset = Track.changeset(track, attrs)
|
||||
Repo.update(changeset)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a track.
|
||||
"""
|
||||
def delete_track(%Track{} = track) do
|
||||
Repo.delete(track)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Counts tracks matching the search query.
|
||||
"""
|
||||
def search_tracks_count(query \\ "") do
|
||||
base_query = from(t in Track)
|
||||
|
||||
search_query =
|
||||
if query == "" do
|
||||
base_query
|
||||
else
|
||||
query_term = "%#{String.downcase(query)}%"
|
||||
|
||||
from t in base_query,
|
||||
where:
|
||||
like(fragment("lower(?)", t.title), ^query_term) or
|
||||
like(fragment("lower(json_extract(artist, '$.name'))"), ^query_term) or
|
||||
like(fragment("lower(json_extract(album, '$.title'))"), ^query_term)
|
||||
end
|
||||
|
||||
Repo.aggregate(search_query, :count, :scrobbled_at_uts)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -48,6 +48,15 @@
|
||||
<.icon name="hero-chevron-down" class="h-4 w-4" />
|
||||
</button>
|
||||
</:toggle>
|
||||
<.dropdown_link href={~p"/scrobbled-tracks"}>
|
||||
<.icon
|
||||
name="hero-musical-note"
|
||||
class="h-4 w-4 mr-2"
|
||||
aria-hidden="true"
|
||||
data-slot="icon"
|
||||
/>
|
||||
{gettext("Scrobbled Tracks")}
|
||||
</.dropdown_link>
|
||||
<.dropdown_link href={~p"/scrobble-rules"}>
|
||||
<.icon
|
||||
name="hero-adjustments-horizontal"
|
||||
|
||||
@@ -90,7 +90,10 @@ defmodule MusicLibraryWeb.PaginationComponent do
|
||||
attr :page_number, :integer, required: true
|
||||
attr :page_size, :integer, required: true
|
||||
attr :query, :string, required: true
|
||||
attr :order, :atom, values: [:alphabetical, :purchase], required: true
|
||||
|
||||
attr :order, :atom,
|
||||
values: [:alphabetical, :purchase, :scrobbled_at, :title, :artist, :album],
|
||||
required: true
|
||||
|
||||
defp next_link(assigns) do
|
||||
~H"""
|
||||
@@ -107,7 +110,10 @@ defmodule MusicLibraryWeb.PaginationComponent do
|
||||
attr :page_number, :integer, required: true
|
||||
attr :page_size, :integer, required: true
|
||||
attr :query, :string, required: true
|
||||
attr :order, :atom, values: [:alphabetical, :purchase], required: true
|
||||
|
||||
attr :order, :atom,
|
||||
values: [:alphabetical, :purchase, :scrobbled_at, :title, :artist, :album],
|
||||
required: true
|
||||
|
||||
defp prev_link(assigns) do
|
||||
~H"""
|
||||
@@ -133,7 +139,10 @@ defmodule MusicLibraryWeb.PaginationComponent do
|
||||
attr :page_size, :integer, required: true
|
||||
attr :active, :boolean, default: false
|
||||
attr :query, :string, required: true
|
||||
attr :order, :atom, values: [:alphabetical, :purchase], required: true
|
||||
|
||||
attr :order, :atom,
|
||||
values: [:alphabetical, :purchase, :scrobbled_at, :title, :artist, :album],
|
||||
required: true
|
||||
|
||||
defp numbered_link(assigns) when assigns.active do
|
||||
~H"""
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
defmodule MusicLibraryWeb.ScrobbledTracksLive.FormComponent do
|
||||
use MusicLibraryWeb, :live_component
|
||||
|
||||
alias MusicLibrary.ScrobbleActivity
|
||||
alias LastFm.Track
|
||||
|
||||
@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">
|
||||
{gettext("Edit Scrobbled Track")}
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<.simple_form
|
||||
for={@form}
|
||||
id="track-form"
|
||||
phx-target={@myself}
|
||||
phx-change="validate"
|
||||
phx-submit="save"
|
||||
>
|
||||
<.input
|
||||
field={@form[:title]}
|
||||
type="text"
|
||||
label={gettext("Track Title")}
|
||||
placeholder={gettext("Track name")}
|
||||
/>
|
||||
|
||||
<.inputs_for :let={artist} field={@form[:artist]}>
|
||||
<.input
|
||||
field={artist[:name]}
|
||||
type="text"
|
||||
label={gettext("Artist Name")}
|
||||
placeholder={gettext("Artist name")}
|
||||
/>
|
||||
</.inputs_for>
|
||||
|
||||
<.inputs_for :let={album} field={@form[:album]}>
|
||||
<.input
|
||||
field={album[:title]}
|
||||
type="text"
|
||||
label={gettext("Album Title")}
|
||||
placeholder={gettext("Album name")}
|
||||
/>
|
||||
</.inputs_for>
|
||||
|
||||
<.input
|
||||
field={@form[:scrobbled_at_label]}
|
||||
type="text"
|
||||
label={gettext("Scrobbled Date/Time")}
|
||||
placeholder={gettext("Format: DD/MM/YYYY HH:MM:SS")}
|
||||
/>
|
||||
|
||||
<.input
|
||||
field={@form[:cover_url]}
|
||||
type="url"
|
||||
label={gettext("Cover Image URL (optional)")}
|
||||
placeholder="https://example.com/cover.jpg"
|
||||
/>
|
||||
|
||||
<:actions>
|
||||
<.button phx-disable-with={gettext("Saving...")}>
|
||||
{gettext("Update Track")}
|
||||
</.button>
|
||||
</:actions>
|
||||
</.simple_form>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def update(%{track: track} = assigns, socket) do
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(assigns)
|
||||
|> assign_new(:form, fn ->
|
||||
to_form(Track.changeset(track, %{}))
|
||||
end)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("validate", %{"track" => track_params}, socket) do
|
||||
changeset = Track.changeset(socket.assigns.track, track_params)
|
||||
{:noreply, assign(socket, form: to_form(changeset, action: :validate))}
|
||||
end
|
||||
|
||||
def handle_event("save", %{"track" => track_params}, socket) do
|
||||
save_track(socket, track_params)
|
||||
end
|
||||
|
||||
defp save_track(socket, track_params) do
|
||||
case ScrobbleActivity.update_track(socket.assigns.track, track_params) do
|
||||
{:ok, track} ->
|
||||
notify_parent({:saved, track})
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_toast(:info, gettext("Track updated successfully"))
|
||||
|> push_patch(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,160 @@
|
||||
defmodule MusicLibraryWeb.ScrobbledTracksLive.Index do
|
||||
use MusicLibraryWeb, :live_view
|
||||
|
||||
import MusicLibraryWeb.PaginationComponent
|
||||
import MusicLibraryWeb.RecordComponents, only: [search_form: 1]
|
||||
|
||||
alias MusicLibrary.ScrobbleActivity
|
||||
alias LastFm.Track
|
||||
|
||||
@default_tracks_list_params %{
|
||||
query: "",
|
||||
page: 1,
|
||||
page_size: 200,
|
||||
order: :scrobbled_at
|
||||
}
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
socket =
|
||||
socket
|
||||
|> assign(:current_section, :scrobble_activity)
|
||||
|> stream_configure(:tracks, dom_id: fn %Track{scrobbled_at_uts: id} -> "tracks-#{id}" end)
|
||||
|
||||
{:ok, socket}
|
||||
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, %{"scrobbled_at_uts" => id} = params) do
|
||||
track = ScrobbleActivity.get_track!(id)
|
||||
|
||||
socket
|
||||
|> apply_fallback_index(params)
|
||||
|> assign(:page_title, gettext("Edit Track"))
|
||||
|> assign(:track, track)
|
||||
|> assign(:form, to_form(Track.changeset(track, %{})))
|
||||
end
|
||||
|
||||
defp apply_action(socket, :index, params) do
|
||||
query = params["query"] || ""
|
||||
order = parse_order(params["order"] || "scrobbled_at")
|
||||
total_tracks = ScrobbleActivity.search_tracks_count(query)
|
||||
|
||||
track_list_params =
|
||||
@default_tracks_list_params
|
||||
|> merge_query(query)
|
||||
|> merge_order(order)
|
||||
|> merge_pagination(params, total_tracks)
|
||||
|
||||
load_and_assign_tracks(socket, track_list_params)
|
||||
end
|
||||
|
||||
def apply_fallback_index(socket, params) do
|
||||
if get_in(socket.assigns, [:streams, :tracks]) == nil do
|
||||
socket
|
||||
|> apply_action(:index, params)
|
||||
else
|
||||
socket
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({MusicLibraryWeb.ScrobbledTracksLive.FormComponent, {:saved, _track}}, socket) do
|
||||
{:noreply, load_and_assign_tracks(socket, socket.assigns.track_list_params)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("delete", %{"scrobbled-at-uts" => scrobbled_at_uts}, socket) do
|
||||
track = ScrobbleActivity.get_track!(scrobbled_at_uts)
|
||||
{:ok, _} = ScrobbleActivity.delete_track(track)
|
||||
|
||||
{:noreply, stream_delete(socket, :tracks, track)}
|
||||
end
|
||||
|
||||
def handle_event("search", %{"query" => query}, socket) do
|
||||
qs =
|
||||
@default_tracks_list_params
|
||||
|> Map.put(:query, query)
|
||||
|> Map.take([:query, :page, :page_size])
|
||||
|
||||
{:noreply, push_patch(socket, to: ~p"/scrobbled-tracks?#{qs}")}
|
||||
end
|
||||
|
||||
defp parse_order("scrobbled_at"), do: :scrobbled_at
|
||||
defp parse_order("title"), do: :title
|
||||
defp parse_order("artist"), do: :artist
|
||||
defp parse_order("album"), do: :album
|
||||
defp parse_order(_), do: :scrobbled_at
|
||||
|
||||
defp merge_query(params, query), do: Map.put(params, :query, query)
|
||||
defp merge_order(params, order), do: Map.put(params, :order, order)
|
||||
|
||||
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: 200
|
||||
|
||||
defp parse_page_size(page_size) when is_binary(page_size) do
|
||||
case Integer.parse(page_size) do
|
||||
{num, ""} when num in [50, 100, 200, 500] -> num
|
||||
_ -> 200
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_page_size(_), do: 200
|
||||
|
||||
defp load_and_assign_tracks(socket, track_list_params) do
|
||||
tracks = ScrobbleActivity.list_tracks(track_list_params)
|
||||
tracks_empty? = tracks == []
|
||||
|
||||
# Add total_entries for pagination component
|
||||
track_list_params_with_total =
|
||||
Map.put(track_list_params, :total_entries, track_list_params.total_records)
|
||||
|
||||
socket
|
||||
|> assign(:track_list_params, track_list_params_with_total)
|
||||
|> assign(:tracks_empty?, tracks_empty?)
|
||||
|> assign(:page_title, gettext("Scrobbled Tracks"))
|
||||
|> stream(:tracks, tracks, reset: true)
|
||||
end
|
||||
|
||||
def order_path(track_list_params, order) do
|
||||
qs =
|
||||
track_list_params
|
||||
|> Map.put(:order, order)
|
||||
|> Map.put(:page, 1)
|
||||
|> Map.take([:query, :page, :page_size, :order])
|
||||
|
||||
~p"/scrobbled-tracks?#{qs}"
|
||||
end
|
||||
|
||||
def back_path(track_list_params) do
|
||||
qs =
|
||||
track_list_params
|
||||
|> Map.take([:query, :page, :page_size, :order])
|
||||
|
||||
~p"/scrobbled-tracks?#{qs}"
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,166 @@
|
||||
<div>
|
||||
<header class="gap-6 mb-2">
|
||||
<div class="flex items-center justify-between gap-6 mb-2 mt-2">
|
||||
<.search_form query={@track_list_params.query} />
|
||||
<div class="text-sm text-zinc-500 dark:text-zinc-400">
|
||||
{ngettext("1 track", "%{count} tracks", @track_list_params.total_records)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="flex items-end justify-between gap-6 mt-8">
|
||||
<.button_group>
|
||||
<.button
|
||||
patch={order_path(@track_list_params, :scrobbled_at)}
|
||||
size="sm"
|
||||
class={[
|
||||
@track_list_params.order == :scrobbled_at && "!bg-zinc-100 dark:!bg-zinc-700"
|
||||
]}
|
||||
>
|
||||
<.icon
|
||||
name="hero-clock-solid"
|
||||
class="icon"
|
||||
aria-hidden="true"
|
||||
data-slot="icon"
|
||||
/>
|
||||
{gettext("Scrobbled")}
|
||||
</.button>
|
||||
<.button
|
||||
patch={order_path(@track_list_params, :title)}
|
||||
size="sm"
|
||||
class={[
|
||||
@track_list_params.order == :title && "!bg-zinc-100 dark:!bg-zinc-700"
|
||||
]}
|
||||
>
|
||||
<.icon name="hero-musical-note-solid" class="icon" aria-hidden="true" data-slot="icon" />
|
||||
{gettext("Title")}
|
||||
</.button>
|
||||
<.button
|
||||
patch={order_path(@track_list_params, :artist)}
|
||||
size="sm"
|
||||
class={[
|
||||
@track_list_params.order == :artist && "!bg-zinc-100 dark:!bg-zinc-700"
|
||||
]}
|
||||
>
|
||||
<.icon name="hero-user-solid" class="icon" aria-hidden="true" data-slot="icon" />
|
||||
{gettext("Artist")}
|
||||
</.button>
|
||||
<.button
|
||||
patch={order_path(@track_list_params, :album)}
|
||||
size="sm"
|
||||
class={[
|
||||
@track_list_params.order == :album && "!bg-zinc-100 dark:!bg-zinc-700"
|
||||
]}
|
||||
>
|
||||
<.icon name="hero-musical-note-solid" class="icon" aria-hidden="true" data-slot="icon" />
|
||||
{gettext("Album")}
|
||||
</.button>
|
||||
</.button_group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:if={@tracks_empty?}
|
||||
class="mt-8 p-8 text-center bg-zinc-50 dark:bg-zinc-800 rounded-lg"
|
||||
>
|
||||
<.icon name="hero-musical-note" class="h-12 w-12 text-zinc-400 mx-auto mb-4" />
|
||||
<p class="text-zinc-600 dark:text-zinc-400">
|
||||
{gettext("No scrobbled tracks found")}
|
||||
</p>
|
||||
<p :if={@track_list_params.query != ""} class="text-sm text-zinc-500 dark:text-zinc-500 mt-2">
|
||||
{gettext("Try adjusting your search or clearing the filter")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div :if={!@tracks_empty?} class="mt-6">
|
||||
<div id="tracks" phx-update="stream" class="grid gap-4">
|
||||
<div
|
||||
id="empty-state"
|
||||
class="hidden only:block p-8 text-center bg-zinc-50 dark:bg-zinc-800 rounded-lg"
|
||||
>
|
||||
<.icon name="hero-musical-note" class="h-12 w-12 text-zinc-400 mx-auto mb-4" />
|
||||
<p class="text-zinc-600 dark:text-zinc-400">
|
||||
{gettext("No scrobbled tracks found")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:for={{id, track} <- @streams.tracks}
|
||||
id={id}
|
||||
class="bg-white dark:bg-zinc-800 p-4 rounded-lg shadow-sm border border-zinc-200 dark:border-zinc-700"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-4 flex-1 min-w-0">
|
||||
<div :if={track.cover_url && track.cover_url != ""} class="flex-shrink-0">
|
||||
<img
|
||||
class="w-12 h-12 rounded-md object-cover"
|
||||
src={track.cover_url}
|
||||
alt={"Cover for " <> track.album.title}
|
||||
onerror={"this.src = '" <> ~p"/images/cover-not-found.png" <> "';"}
|
||||
/>
|
||||
</div>
|
||||
<div :if={!track.cover_url || track.cover_url == ""} class="flex-shrink-0">
|
||||
<div class="w-12 h-12 bg-zinc-200 dark:bg-zinc-600 rounded-md flex items-center justify-center">
|
||||
<.icon name="hero-musical-note" class="h-6 w-6 text-zinc-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-sm font-medium text-zinc-900 dark:text-zinc-100 truncate">
|
||||
{track.title}
|
||||
</p>
|
||||
<p class="text-sm text-zinc-600 dark:text-zinc-400 truncate">
|
||||
{track.artist.name}
|
||||
</p>
|
||||
<p class="text-sm text-zinc-500 dark:text-zinc-500 truncate">
|
||||
{track.album.title}
|
||||
</p>
|
||||
<p class="text-xs text-zinc-400 dark:text-zinc-600 mt-1">
|
||||
{track.scrobbled_at_label}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2 ml-4">
|
||||
<.button
|
||||
patch={~p"/scrobbled-tracks/#{track.scrobbled_at_uts}/edit"}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
<.icon name="hero-pencil" class="h-4 w-4" />
|
||||
<span class="sr-only">{gettext("Edit track")}</span>
|
||||
</.button>
|
||||
<.button
|
||||
phx-click={
|
||||
JS.push("delete", value: %{"scrobbled-at-uts": track.scrobbled_at_uts})
|
||||
|> JS.hide(to: "##{id}")
|
||||
}
|
||||
data-confirm={gettext("Are you sure you want to delete this scrobbled track?")}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
class="text-red-600 hover:text-red-800 border-red-200 hover:border-red-300"
|
||||
>
|
||||
<.icon name="hero-trash" class="h-4 w-4" />
|
||||
<span class="sr-only">{gettext("Delete track")}</span>
|
||||
</.button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<.pagination id={:bottom_pagination} pagination_params={@track_list_params} />
|
||||
</div>
|
||||
|
||||
<.structured_modal
|
||||
:if={@live_action == :edit}
|
||||
id="track-modal"
|
||||
on_close={JS.patch(back_path(@track_list_params))}
|
||||
>
|
||||
<.live_component
|
||||
module={MusicLibraryWeb.ScrobbledTracksLive.FormComponent}
|
||||
id={@track.scrobbled_at_uts}
|
||||
action={@live_action}
|
||||
track={@track}
|
||||
patch={back_path(@track_list_params)}
|
||||
/>
|
||||
</.structured_modal>
|
||||
@@ -69,6 +69,9 @@ defmodule MusicLibraryWeb.Router do
|
||||
live "/online-store-templates", OnlineStoreTemplateLive.Index, :index
|
||||
live "/online-store-templates/new", OnlineStoreTemplateLive.Index, :new
|
||||
live "/online-store-templates/:id/edit", OnlineStoreTemplateLive.Index, :edit
|
||||
|
||||
live "/scrobbled-tracks", ScrobbledTracksLive.Index, :index
|
||||
live "/scrobbled-tracks/:scrobbled_at_uts/edit", ScrobbledTracksLive.Index, :edit
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user