mirror of
https://github.com/fly-apps/live_beats.git
synced 2024-11-21 23:50:59 +00:00
Fix changeset handling by recycling
This commit is contained in:
parent
5f593dfaf2
commit
6358b0bb3b
7 changed files with 130 additions and 91 deletions
|
@ -11,9 +11,7 @@
|
||||||
.fade-out-scale {
|
.fade-out-scale {
|
||||||
animation: 0.2s ease-out 0s normal forwards 1 fade-out-scale-keys;
|
animation: 0.2s ease-out 0s normal forwards 1 fade-out-scale-keys;
|
||||||
}
|
}
|
||||||
.slide-in-right {
|
|
||||||
animation: slide-in-right-keys 0.2s forwards;
|
|
||||||
}
|
|
||||||
.fade-in {
|
.fade-in {
|
||||||
animation: 0.2s ease-out 0s normal forwards 1 fade-in-keys;
|
animation: 0.2s ease-out 0s normal forwards 1 fade-in-keys;
|
||||||
}
|
}
|
||||||
|
@ -75,9 +73,7 @@
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.invalid-feedback {
|
.invalid-feedback {
|
||||||
color: #a94442;
|
display: inline-block;
|
||||||
display: block;
|
|
||||||
margin: -1rem 0 2rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* LiveView specific classes for your customization */
|
/* LiveView specific classes for your customization */
|
||||||
|
|
|
@ -27,9 +27,11 @@ Hooks.AudioPlayer = {
|
||||||
this.duration = this.el.querySelector("#player-duration")
|
this.duration = this.el.querySelector("#player-duration")
|
||||||
this.progress = this.el.querySelector("#player-progress")
|
this.progress = this.el.querySelector("#player-progress")
|
||||||
let enableAudio = () => {
|
let enableAudio = () => {
|
||||||
document.removeEventListener("click", enableAudio)
|
if(this.player.src){
|
||||||
this.player.play().catch(error => null)
|
document.removeEventListener("click", enableAudio)
|
||||||
this.player.pause()
|
this.player.play().catch(error => null)
|
||||||
|
this.player.pause()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
document.addEventListener("click", enableAudio)
|
document.addEventListener("click", enableAudio)
|
||||||
this.el.addEventListener("js:listen_now", () => this.play({sync: true}))
|
this.el.addEventListener("js:listen_now", () => this.play({sync: true}))
|
||||||
|
|
|
@ -187,7 +187,15 @@ defmodule LiveBeats.MediaLibrary do
|
||||||
Repo.delete(song)
|
Repo.delete(song)
|
||||||
end
|
end
|
||||||
|
|
||||||
def change_song(%Song{} = song, attrs \\ %{}) do
|
def change_song(song_or_changeset, attrs \\ %{}) do
|
||||||
Song.changeset(song, attrs)
|
song_or_changeset
|
||||||
|
|> recycle_changeset()
|
||||||
|
|> Song.changeset(attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp recycle_changeset(%Ecto.Changeset{} = changeset) do
|
||||||
|
Map.merge(changeset, %{action: nil, errors: [], valid?: true})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp recycle_changeset(%{} = other), do: other
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
defmodule LiveBeatsWeb.SongLive.SongEntry do
|
defmodule LiveBeatsWeb.SongLive.SongEntryComponent do
|
||||||
use LiveBeatsWeb, :live_component
|
use LiveBeatsWeb, :live_component
|
||||||
|
|
||||||
alias LiveBeats.MP3Stat
|
alias LiveBeats.MP3Stat
|
||||||
|
|
||||||
|
def send_progress(%Phoenix.LiveView.UploadEntry{} = entry) do
|
||||||
|
send_update(__MODULE__, id: entry.ref, progress: entry.progress)
|
||||||
|
end
|
||||||
|
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div class="sm:grid sm:grid-cols-2 sm:gap-2 sm:items-start sm:border-t sm:border-gray-200 sm:pt-2">
|
<div class="sm:grid sm:grid-cols-2 sm:gap-2 sm:items-start sm:border-t sm:border-gray-200 sm:pt-2">
|
||||||
|
@ -19,13 +23,15 @@ defmodule LiveBeatsWeb.SongLive.SongEntry do
|
||||||
</label>
|
</label>
|
||||||
<input type="text" name={"songs[#{@ref}][title]"} value={@title}
|
<input type="text" name={"songs[#{@ref}][title]"} value={@title}
|
||||||
class="block w-full border-0 p-0 text-gray-900 placeholder-gray-500 focus:ring-0 sm:text-sm"/>
|
class="block w-full border-0 p-0 text-gray-900 placeholder-gray-500 focus:ring-0 sm:text-sm"/>
|
||||||
<%= error_tag(@errors, :title, "songs[#{@ref}][title]") %>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="border border-gray-300 rounded-md px-3 py-2 mt-2 shadow-sm focus-within:ring-1 focus-within:ring-indigo-600 focus-within:border-indigo-600">
|
<div class="border border-gray-300 rounded-md px-3 py-2 mt-2 shadow-sm focus-within:ring-1 focus-within:ring-indigo-600 focus-within:border-indigo-600">
|
||||||
<label for="name" class="block text-xs font-medium text-gray-900">Artist</label>
|
<label for="name" class="block text-xs font-medium text-gray-900">Artist</label>
|
||||||
<input type="text" name={"songs[#{@ref}][artist]"} value={@artist}
|
<input type="text" name={"songs[#{@ref}][artist]"} value={@artist}
|
||||||
class="block w-full border-0 p-0 text-gray-900 placeholder-gray-500 focus:ring-0 sm:text-sm"/>
|
class="block w-full border-0 p-0 text-gray-900 placeholder-gray-500 focus:ring-0 sm:text-sm"/>
|
||||||
<%= error_tag(@errors, :artist, "songs[#{@ref}][artist]") %>
|
</div>
|
||||||
|
<div class="col-span-full sm:grid sm:grid-cols-2 sm:gap-2 sm:items-start">
|
||||||
|
<.error input_name={"songs[#{@ref}][title]"} field={:title} errors={@errors}/>
|
||||||
|
<.error input_name={"songs[#{@ref}][artist]"} field={:artist} errors={@errors}/>
|
||||||
</div>
|
</div>
|
||||||
<div style={"width: #{@progress}%;"} class="col-span-full bg-purple-500 dark:bg-purple-400 h-1.5 w-0 p-0">
|
<div style={"width: #{@progress}%;"} class="col-span-full bg-purple-500 dark:bg-purple-400 h-1.5 w-0 p-0">
|
||||||
</div>
|
</div>
|
|
@ -2,7 +2,7 @@ defmodule LiveBeatsWeb.SongLive.UploadFormComponent do
|
||||||
use LiveBeatsWeb, :live_component
|
use LiveBeatsWeb, :live_component
|
||||||
|
|
||||||
alias LiveBeats.{MediaLibrary, MP3Stat}
|
alias LiveBeats.{MediaLibrary, MP3Stat}
|
||||||
alias LiveBeatsWeb.SongLive.SongEntry
|
alias LiveBeatsWeb.SongLive.SongEntryComponent
|
||||||
|
|
||||||
@max_songs 10
|
@max_songs 10
|
||||||
|
|
||||||
|
@ -12,8 +12,8 @@ defmodule LiveBeatsWeb.SongLive.UploadFormComponent do
|
||||||
{:ok, %MP3Stat{} = stat} ->
|
{:ok, %MP3Stat{} = stat} ->
|
||||||
{:ok, put_stats(socket, entry_ref, stat)}
|
{:ok, put_stats(socket, entry_ref, stat)}
|
||||||
|
|
||||||
_ ->
|
{:error, _} ->
|
||||||
{:ok, cancel_upload(socket, :mp3, entry_ref)}
|
{:ok, cancel_changeset_upload(socket, entry_ref, :not_accepted)}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ defmodule LiveBeatsWeb.SongLive.UploadFormComponent do
|
||||||
|> push_redirect(to: Routes.song_index_path(socket, :index))}
|
|> push_redirect(to: Routes.song_index_path(socket, :index))}
|
||||||
|
|
||||||
{:error, _reason} ->
|
{:error, _reason} ->
|
||||||
{:noreply, put_flash(socket, :error, "There were problems uploading your songs")}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -69,7 +69,6 @@ defmodule LiveBeatsWeb.SongLive.UploadFormComponent do
|
||||||
new_changeset =
|
new_changeset =
|
||||||
acc
|
acc
|
||||||
|> get_changeset(ref)
|
|> get_changeset(ref)
|
||||||
|> Ecto.Changeset.apply_changes()
|
|
||||||
|> MediaLibrary.change_song(song_params)
|
|> MediaLibrary.change_song(song_params)
|
||||||
|> Map.put(:action, action)
|
|> Map.put(:action, action)
|
||||||
|
|
||||||
|
@ -103,8 +102,12 @@ defmodule LiveBeatsWeb.SongLive.UploadFormComponent do
|
||||||
update(socket, :changesets, &Map.put(&1, entry_ref, changeset))
|
update(socket, :changesets, &Map.put(&1, entry_ref, changeset))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp drop_changeset(socket, entry_ref) do
|
||||||
|
update(socket, :changesets, &Map.delete(&1, entry_ref))
|
||||||
|
end
|
||||||
|
|
||||||
defp handle_progress(:mp3, entry, socket) do
|
defp handle_progress(:mp3, entry, socket) do
|
||||||
send_update(SongEntry, id: entry.ref, progress: entry.progress)
|
SongEntryComponent.send_progress(entry)
|
||||||
|
|
||||||
if entry.done? do
|
if entry.done? do
|
||||||
async_calculate_duration(socket, entry)
|
async_calculate_duration(socket, entry)
|
||||||
|
@ -144,21 +147,32 @@ defmodule LiveBeatsWeb.SongLive.UploadFormComponent do
|
||||||
defp drop_invalid_uploads(socket) do
|
defp drop_invalid_uploads(socket) do
|
||||||
%{uploads: uploads} = socket.assigns
|
%{uploads: uploads} = socket.assigns
|
||||||
|
|
||||||
{new_socket, error_messages, _index} =
|
Enum.reduce(Enum.with_index(uploads.mp3.entries), socket, fn {entry, i}, socket ->
|
||||||
Enum.reduce(uploads.mp3.entries, {socket, [], 0}, fn entry, {socket, msgs, i} ->
|
if i >= @max_songs do
|
||||||
if i >= @max_songs do
|
cancel_changeset_upload(socket, entry.ref, :dropped)
|
||||||
{cancel_upload(socket, :mp3, entry.ref), [{entry.client_name, :dropped} | msgs], i + 1}
|
else
|
||||||
else
|
case upload_errors(uploads.mp3, entry) do
|
||||||
case upload_errors(uploads.mp3, entry) do
|
[first | _] ->
|
||||||
[first | _] ->
|
cancel_changeset_upload(socket, entry.ref, first)
|
||||||
{cancel_upload(socket, :mp3, entry.ref), [{entry.client_name, first} | msgs], i + 1}
|
|
||||||
|
|
||||||
[] ->
|
[] ->
|
||||||
{socket, msgs, i + 1}
|
socket
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end)
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
assign(new_socket, error_messages: error_messages)
|
defp cancel_changeset_upload(socket, entry_ref, reason) do
|
||||||
|
entry = get_entry!(socket, entry_ref)
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> cancel_upload(:mp3, entry.ref)
|
||||||
|
|> drop_changeset(entry.ref)
|
||||||
|
|> update(:error_messages, &(&1 ++ [{entry.client_name, reason}]))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_entry!(socket, entry_ref) do
|
||||||
|
Enum.find(socket.assigns.uploads.mp3.entries, fn entry -> entry.ref == entry_ref end) ||
|
||||||
|
raise "no entry found for ref #{inspect(entry_ref)}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,57 +10,57 @@
|
||||||
phx-submit="save">
|
phx-submit="save">
|
||||||
|
|
||||||
<div class="space-y-8 divide-y divide-gray-200 sm:space-y-5">
|
<div class="space-y-8 divide-y divide-gray-200 sm:space-y-5">
|
||||||
<div class="space-y-2 sm:space-y-2">
|
<div class="space-y-2 sm:space-y-2">
|
||||||
<%= for {ref, changeset} <- @changesets do %>
|
<%= for {ref, changeset} <- @changesets do %>
|
||||||
<.live_component id={ref} module={SongEntry} changeset={changeset} />
|
<.live_component id={ref} module={SongEntryComponent} changeset={changeset} />
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<!-- upload -->
|
<!-- upload -->
|
||||||
<div class="sm:grid sm:border-t sm:border-gray-200 sm:pt-5">
|
<div class="sm:grid sm:border-t sm:border-gray-200 sm:pt-5">
|
||||||
<div class="mt-1 sm:mt-0" phx-drop-target={@uploads.mp3.ref}>
|
<div class="mt-1 sm:mt-0" phx-drop-target={@uploads.mp3.ref}>
|
||||||
<%= if Enum.any?(@error_messages) do %>
|
<%= if Enum.any?(@error_messages) do %>
|
||||||
<div class="rounded-md bg-red-50 p-4 mb-2">
|
<div class="rounded-md bg-red-50 p-4 mb-2">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
<.icon name={:x_circle} class="h-5 w-5 text-red-400"/>
|
<.icon name={:x_circle} class="h-5 w-5 text-red-400"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-3">
|
<div class="ml-3">
|
||||||
<h3 class="text-sm font-medium text-red-800">
|
<h3 class="text-sm font-medium text-red-800">
|
||||||
Oops!
|
Oops!
|
||||||
</h3>
|
</h3>
|
||||||
<div class="mt-2 text-sm text-red-700">
|
<div class="mt-2 text-sm text-red-700">
|
||||||
<ul role="list" class="list-disc pl-5 space-y-1">
|
<ul role="list" class="list-disc pl-5 space-y-1">
|
||||||
<%= for {client_name, error} <- @error_messages do %>
|
<%= for {client_name, kind} <- @error_messages do %>
|
||||||
<li><%= client_name %>: <.file_error kind={error} /></li>
|
<li><%= client_name %>: <.file_error kind={kind} /></li>
|
||||||
<% end %>
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<div class="max-w-lg flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md">
|
<div class="max-w-lg flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md">
|
||||||
<div class="space-y-1 text-center">
|
<div class="space-y-1 text-center">
|
||||||
<svg class="mx-auto h-12 w-12 text-gray-400" stroke="currentColor" fill="none" viewBox="0 0 48 48" aria-hidden="true">
|
<svg class="mx-auto h-12 w-12 text-gray-400" stroke="currentColor" fill="none" viewBox="0 0 48 48" aria-hidden="true">
|
||||||
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
</svg>
|
</svg>
|
||||||
<div class="flex text-sm text-gray-600">
|
<div class="flex text-sm text-gray-600">
|
||||||
<label for="file-upload" class="relative cursor-pointer bg-white rounded-md font-medium text-indigo-600 hover:text-indigo-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-indigo-500">
|
<label for="file-upload" class="relative cursor-pointer bg-white rounded-md font-medium text-indigo-600 hover:text-indigo-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-indigo-500">
|
||||||
<span phx-click={JS.dispatch("click", to: "##{@uploads.mp3.ref}")}>Upload files</span>
|
<span phx-click={JS.dispatch("click", to: "##{@uploads.mp3.ref}")}>Upload files</span>
|
||||||
<%= live_file_input @uploads.mp3, class: "sr-only" %>
|
<%= live_file_input @uploads.mp3, class: "sr-only" %>
|
||||||
</label>
|
</label>
|
||||||
<p class="pl-1">or drag and drop</p>
|
<p class="pl-1">or drag and drop</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-gray-500">
|
<p class="text-xs text-gray-500">
|
||||||
MP3s up to 20MB
|
MP3s up to 20MB
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- /upload -->
|
<!-- /upload -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</.form>
|
</.form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,25 +4,38 @@ defmodule LiveBeatsWeb.ErrorHelpers do
|
||||||
"""
|
"""
|
||||||
|
|
||||||
use Phoenix.HTML
|
use Phoenix.HTML
|
||||||
|
import Phoenix.LiveView
|
||||||
|
import Phoenix.LiveView.Helpers
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Generates tag for inlined form input errors.
|
Generates tag for inlined form input errors.
|
||||||
"""
|
"""
|
||||||
def error_tag(form, field) do
|
def error_tag(form, field) do
|
||||||
error_tag(form.errors, field, input_name(form, field))
|
error(%{errors: form.errors, field: field, input_name: input_name(form, field)})
|
||||||
end
|
end
|
||||||
|
|
||||||
def error_tag(errors, field, input_name) do
|
def error(%{errors: errors, field: field} = assigns) do
|
||||||
Enum.map(Keyword.get_values(errors, field), fn error ->
|
assigns =
|
||||||
content_tag(:div, translate_error(error),
|
assigns
|
||||||
class: "invalid-feedback mt-0 text-sm text-red-600 text-right",
|
|> assign(:error_values, Keyword.get_values(errors, field))
|
||||||
phx_feedback_for: input_name
|
|> assign_new(:class, fn -> "" end)
|
||||||
)
|
|
||||||
end)
|
~H"""
|
||||||
|
<%= for error <- @error_values do %>
|
||||||
|
<div
|
||||||
|
phx-feedback-for={@input_name}
|
||||||
|
class={"invalid-feedback -mt-1 pl-2 text-sm text-white bg-red-600 rounded-md #{@class}"}
|
||||||
|
>
|
||||||
|
<%= translate_error(error) %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= if Enum.empty?(@error_values) do %>
|
||||||
|
<div class={"invalid-feedback h-0 #{@class}"}></div>
|
||||||
|
<% end %>
|
||||||
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Translates an error message using gettext.
|
Translates an error message using gettext.
|
||||||
"""
|
"""
|
||||||
|
|
Loading…
Reference in a new issue