defmodule MusicLibraryWeb.BarcodeScannerComponent do
use MusicLibraryWeb, :live_component
alias MusicBrainz.ReleaseGroupSearchResult
alias MusicLibrary.BarcodeScan
alias MusicLibrary.Records
alias MusicLibraryWeb.RecordComponents
require Logger
@impl true
def mount(socket) do
{:ok,
socket
|> assign(:camera, :pending)
|> assign(:scan_results, [])}
end
@impl true
def render(assigns) do
~H"""
{gettext("Scan one or more barcodes")}
<.camera_button camera={@camera} />
<.scan_result scan_result={scan_result} />
<.button
disabled={length(@scan_results) == 0}
phx-disable-with={gettext("Adding...")}
phx-click={JS.push("import_releases", target: "#barcode-scanner")}
>
{gettext("Add releases")}
"""
end
attr :camera, :atom, required: true, values: [:pending, :allowed, :denied]
def camera_button(assigns) do
~H"""
JS.dispatch("camera_request", to: "#barcode-scanner")
|> JS.hide(to: "#camera-button")
}
class="relative block w-full h-96 rounded-lg border-2 border-dashed border-zinc-300 p-12 text-center hover:border-zinc-400 outline-hidden"
>
{gettext("Open camera")}
"""
end
attr :class, :string, required: true
def barcode_icon(assigns) do
~H"""
"""
end
attr :scan_result, BarcodeScan.Result, required: true
defp scan_result(assigns) do
~H"""
<.barcode_not_found :if={@scan_result.status == :not_found} number={@scan_result.number} />
<.release
:if={@scan_result.status != :not_found}
release={@scan_result.release}
record_id={@scan_result.record_id}
status={@scan_result.status}
/>
"""
end
attr :number, :string, required: true
defp barcode_not_found(assigns) do
~H"""
{gettext("Barcode not found")}
{@number}
"""
end
attr :release, MusicBrainz.ReleaseSearchResult, required: true
attr :record_id, :string
attr :status, :atom, required: true, values: [:collected, :wishlisted, :new]
defp release(assigns) do
~H"""
~p"/images/cover-not-found.png" <> "';"}
/>
{@release.artists}
{@release.title}
{release_format_label(@release)} · {Records.Record.format_release(@release.date)} · {RecordComponents.type_label(
@release.release_group.type
)}
<.badge :if={@status == :new} color={:gray} text={gettext("New")} />
<.link :if={@status == :wishlisted} navigate={~p"/wishlist/#{@record_id}"}>
<.badge color={:yellow} text={gettext("Wishlisted")} />
<.link :if={@status == :collected} navigate={~p"/collection/#{@record_id}"}>
<.badge color={:green} text={gettext("Collected")} />
"""
end
@impl true
def handle_event("camera_allowed", _params, socket) do
{:noreply, assign(socket, camera: :allowed)}
end
def handle_event("camera_denied", _params, socket) do
{:noreply, assign(socket, camera: :denied)}
end
def handle_event("barcode_scanned", %{"number" => number}, socket) do
socket =
case BarcodeScan.scan(number) do
{:ok, scan_result} ->
assign(socket, :scan_results, [scan_result | socket.assigns.scan_results])
{:error, reason} ->
Logger.error(fn ->
"Failed to search release for barcode #{number}: #{inspect(reason)}"
end)
put_flash(
socket,
:error,
gettext("Failed to search release for barcode %{number}", number: number)
)
end
{:noreply, socket}
end
def handle_event("import_releases", _params, socket) do
current_time = DateTime.utc_now()
socket =
case BarcodeScan.import_results(socket.assigns.scan_results, current_time) do
[] ->
put_flash(socket, :info, gettext("Records imported successfully"))
errors ->
errors_summary =
Enum.map_join(errors, "\n", fn {number, reason} ->
"#{number}: #{inspect(reason)}"
end)
put_flash(
socket,
:error,
gettext("Some records could not be imported: %{summary}", summary: errors_summary)
)
end
qs = %{order: :purchase}
{:noreply,
socket
|> assign(:scan_results, [])
|> push_patch(to: ~p"/collection?#{qs}")}
end
defp release_format_label(release) do
release
|> MusicBrainz.ReleaseSearchResult.format()
|> RecordComponents.format_label()
end
end