mirror of
https://github.com/fly-apps/live_beats.git
synced 2024-11-21 15:41:00 +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 {
|
||||
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 {
|
||||
animation: 0.2s ease-out 0s normal forwards 1 fade-in-keys;
|
||||
}
|
||||
|
@ -75,9 +73,7 @@
|
|||
display: none;
|
||||
}
|
||||
.invalid-feedback {
|
||||
color: #a94442;
|
||||
display: block;
|
||||
margin: -1rem 0 2rem;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* LiveView specific classes for your customization */
|
||||
|
|
|
@ -27,9 +27,11 @@ Hooks.AudioPlayer = {
|
|||
this.duration = this.el.querySelector("#player-duration")
|
||||
this.progress = this.el.querySelector("#player-progress")
|
||||
let enableAudio = () => {
|
||||
document.removeEventListener("click", enableAudio)
|
||||
this.player.play().catch(error => null)
|
||||
this.player.pause()
|
||||
if(this.player.src){
|
||||
document.removeEventListener("click", enableAudio)
|
||||
this.player.play().catch(error => null)
|
||||
this.player.pause()
|
||||
}
|
||||
}
|
||||
document.addEventListener("click", enableAudio)
|
||||
this.el.addEventListener("js:listen_now", () => this.play({sync: true}))
|
||||
|
|
|
@ -187,7 +187,15 @@ defmodule LiveBeats.MediaLibrary do
|
|||
Repo.delete(song)
|
||||
end
|
||||
|
||||
def change_song(%Song{} = song, attrs \\ %{}) do
|
||||
Song.changeset(song, attrs)
|
||||
def change_song(song_or_changeset, attrs \\ %{}) do
|
||||
song_or_changeset
|
||||
|> recycle_changeset()
|
||||
|> Song.changeset(attrs)
|
||||
end
|
||||
|
||||
defp recycle_changeset(%Ecto.Changeset{} = changeset) do
|
||||
Map.merge(changeset, %{action: nil, errors: [], valid?: true})
|
||||
end
|
||||
|
||||
defp recycle_changeset(%{} = other), do: other
|
||||
end
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
defmodule LiveBeatsWeb.SongLive.SongEntry do
|
||||
defmodule LiveBeatsWeb.SongLive.SongEntryComponent do
|
||||
use LiveBeatsWeb, :live_component
|
||||
|
||||
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
|
||||
~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">
|
||||
|
@ -19,13 +23,15 @@ defmodule LiveBeatsWeb.SongLive.SongEntry do
|
|||
</label>
|
||||
<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"/>
|
||||
<%= error_tag(@errors, :title, "songs[#{@ref}][title]") %>
|
||||
</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">
|
||||
<label for="name" class="block text-xs font-medium text-gray-900">Artist</label>
|
||||
<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"/>
|
||||
<%= 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 style={"width: #{@progress}%;"} class="col-span-full bg-purple-500 dark:bg-purple-400 h-1.5 w-0 p-0">
|
||||
</div>
|
|
@ -2,7 +2,7 @@ defmodule LiveBeatsWeb.SongLive.UploadFormComponent do
|
|||
use LiveBeatsWeb, :live_component
|
||||
|
||||
alias LiveBeats.{MediaLibrary, MP3Stat}
|
||||
alias LiveBeatsWeb.SongLive.SongEntry
|
||||
alias LiveBeatsWeb.SongLive.SongEntryComponent
|
||||
|
||||
@max_songs 10
|
||||
|
||||
|
@ -12,8 +12,8 @@ defmodule LiveBeatsWeb.SongLive.UploadFormComponent do
|
|||
{:ok, %MP3Stat{} = stat} ->
|
||||
{:ok, put_stats(socket, entry_ref, stat)}
|
||||
|
||||
_ ->
|
||||
{:ok, cancel_upload(socket, :mp3, entry_ref)}
|
||||
{:error, _} ->
|
||||
{:ok, cancel_changeset_upload(socket, entry_ref, :not_accepted)}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -54,7 +54,7 @@ defmodule LiveBeatsWeb.SongLive.UploadFormComponent do
|
|||
|> push_redirect(to: Routes.song_index_path(socket, :index))}
|
||||
|
||||
{:error, _reason} ->
|
||||
{:noreply, put_flash(socket, :error, "There were problems uploading your songs")}
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -69,7 +69,6 @@ defmodule LiveBeatsWeb.SongLive.UploadFormComponent do
|
|||
new_changeset =
|
||||
acc
|
||||
|> get_changeset(ref)
|
||||
|> Ecto.Changeset.apply_changes()
|
||||
|> MediaLibrary.change_song(song_params)
|
||||
|> Map.put(:action, action)
|
||||
|
||||
|
@ -103,8 +102,12 @@ defmodule LiveBeatsWeb.SongLive.UploadFormComponent do
|
|||
update(socket, :changesets, &Map.put(&1, entry_ref, changeset))
|
||||
end
|
||||
|
||||
defp drop_changeset(socket, entry_ref) do
|
||||
update(socket, :changesets, &Map.delete(&1, entry_ref))
|
||||
end
|
||||
|
||||
defp handle_progress(:mp3, entry, socket) do
|
||||
send_update(SongEntry, id: entry.ref, progress: entry.progress)
|
||||
SongEntryComponent.send_progress(entry)
|
||||
|
||||
if entry.done? do
|
||||
async_calculate_duration(socket, entry)
|
||||
|
@ -144,21 +147,32 @@ defmodule LiveBeatsWeb.SongLive.UploadFormComponent do
|
|||
defp drop_invalid_uploads(socket) do
|
||||
%{uploads: uploads} = socket.assigns
|
||||
|
||||
{new_socket, error_messages, _index} =
|
||||
Enum.reduce(uploads.mp3.entries, {socket, [], 0}, fn entry, {socket, msgs, i} ->
|
||||
if i >= @max_songs do
|
||||
{cancel_upload(socket, :mp3, entry.ref), [{entry.client_name, :dropped} | msgs], i + 1}
|
||||
else
|
||||
case upload_errors(uploads.mp3, entry) do
|
||||
[first | _] ->
|
||||
{cancel_upload(socket, :mp3, entry.ref), [{entry.client_name, first} | msgs], i + 1}
|
||||
Enum.reduce(Enum.with_index(uploads.mp3.entries), socket, fn {entry, i}, socket ->
|
||||
if i >= @max_songs do
|
||||
cancel_changeset_upload(socket, entry.ref, :dropped)
|
||||
else
|
||||
case upload_errors(uploads.mp3, entry) do
|
||||
[first | _] ->
|
||||
cancel_changeset_upload(socket, entry.ref, first)
|
||||
|
||||
[] ->
|
||||
{socket, msgs, i + 1}
|
||||
end
|
||||
[] ->
|
||||
socket
|
||||
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
|
||||
|
|
|
@ -10,57 +10,57 @@
|
|||
phx-submit="save">
|
||||
|
||||
<div class="space-y-8 divide-y divide-gray-200 sm:space-y-5">
|
||||
<div class="space-y-2 sm:space-y-2">
|
||||
<%= for {ref, changeset} <- @changesets do %>
|
||||
<.live_component id={ref} module={SongEntry} changeset={changeset} />
|
||||
<% end %>
|
||||
<div class="space-y-2 sm:space-y-2">
|
||||
<%= for {ref, changeset} <- @changesets do %>
|
||||
<.live_component id={ref} module={SongEntryComponent} changeset={changeset} />
|
||||
<% end %>
|
||||
|
||||
<!-- upload -->
|
||||
<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}>
|
||||
<%= if Enum.any?(@error_messages) do %>
|
||||
<div class="rounded-md bg-red-50 p-4 mb-2">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<!-- upload -->
|
||||
<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}>
|
||||
<%= if Enum.any?(@error_messages) do %>
|
||||
<div class="rounded-md bg-red-50 p-4 mb-2">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<.icon name={:x_circle} class="h-5 w-5 text-red-400"/>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-800">
|
||||
Oops!
|
||||
</h3>
|
||||
<div class="mt-2 text-sm text-red-700">
|
||||
<ul role="list" class="list-disc pl-5 space-y-1">
|
||||
<%= for {client_name, error} <- @error_messages do %>
|
||||
<li><%= client_name %>: <.file_error kind={error} /></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-800">
|
||||
Oops!
|
||||
</h3>
|
||||
<div class="mt-2 text-sm text-red-700">
|
||||
<ul role="list" class="list-disc pl-5 space-y-1">
|
||||
<%= for {client_name, kind} <- @error_messages do %>
|
||||
<li><%= client_name %>: <.file_error kind={kind} /></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% 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="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">
|
||||
<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>
|
||||
<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">
|
||||
<span phx-click={JS.dispatch("click", to: "##{@uploads.mp3.ref}")}>Upload files</span>
|
||||
<%= live_file_input @uploads.mp3, class: "sr-only" %>
|
||||
</label>
|
||||
<p class="pl-1">or drag and drop</p>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500">
|
||||
MP3s up to 20MB
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /upload -->
|
||||
</div>
|
||||
<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">
|
||||
<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>
|
||||
</svg>
|
||||
<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">
|
||||
<span phx-click={JS.dispatch("click", to: "##{@uploads.mp3.ref}")}>Upload files</span>
|
||||
<%= live_file_input @uploads.mp3, class: "sr-only" %>
|
||||
</label>
|
||||
<p class="pl-1">or drag and drop</p>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500">
|
||||
MP3s up to 20MB
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /upload -->
|
||||
</div>
|
||||
</div>
|
||||
</.form>
|
||||
</div>
|
||||
|
|
|
@ -4,25 +4,38 @@ defmodule LiveBeatsWeb.ErrorHelpers do
|
|||
"""
|
||||
|
||||
use Phoenix.HTML
|
||||
import Phoenix.LiveView
|
||||
import Phoenix.LiveView.Helpers
|
||||
|
||||
@doc """
|
||||
Generates tag for inlined form input errors.
|
||||
"""
|
||||
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
|
||||
|
||||
def error_tag(errors, field, input_name) do
|
||||
Enum.map(Keyword.get_values(errors, field), fn error ->
|
||||
content_tag(:div, translate_error(error),
|
||||
class: "invalid-feedback mt-0 text-sm text-red-600 text-right",
|
||||
phx_feedback_for: input_name
|
||||
)
|
||||
end)
|
||||
def error(%{errors: errors, field: field} = assigns) do
|
||||
assigns =
|
||||
assigns
|
||||
|> assign(:error_values, Keyword.get_values(errors, field))
|
||||
|> assign_new(:class, fn -> "" 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
|
||||
|
||||
|
||||
|
||||
@doc """
|
||||
Translates an error message using gettext.
|
||||
"""
|
||||
|
|
Loading…
Reference in a new issue