defmodule MusicLibraryWeb.Components.BarcodeScanner do use MusicLibraryWeb, :live_component alias MusicBrainz.ReleaseGroupSearchResult alias MusicLibrary.BarcodeScan alias MusicLibrary.Records alias MusicLibraryWeb.ErrorMessages 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} />
<.button variant="solid" 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""" """ 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"""
{@release.release_group.title} ~p"/images/cover-not-found.png" <> "';"} />

{@release.artists}

{@release.title}

{release_format_label(@release)} · {Records.Record.format_release_date(@release.date)} · {RecordComponents.type_label( @release.release_group.type )}

<.badge :if={@status == :new}> {gettext("New")} <.link :if={@status == :wishlisted} navigate={~p"/wishlist/#{@record_id}"}> <.badge color="warning"> {gettext("Wishlisted")} <.link :if={@status == :collected} navigate={~p"/collection/#{@record_id}"}> <.badge color="success"> {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_toast( 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() scan_results = socket.assigns.scan_results socket = if BarcodeScan.should_import_async?(scan_results) do {:ok, sync_errors, async_count} = BarcodeScan.import_results_async(scan_results, current_time) socket |> maybe_toast_errors(sync_errors) |> put_toast( :info, ngettext( "Importing %{count} record in the background...", "Importing %{count} records in the background...", async_count, count: async_count ) ) else case BarcodeScan.import_results(scan_results, current_time) do [] -> put_toast(socket, :info, gettext("Records imported successfully")) errors -> maybe_toast_errors(socket, errors) end end qs = %{order: :purchase} {:noreply, socket |> assign(:scan_results, []) |> push_patch(to: ~p"/collection?#{qs}")} end defp maybe_toast_errors(socket, []), do: socket defp maybe_toast_errors(socket, errors) do errors_summary = Enum.map_join(errors, "\n", fn {number, reason} -> "#{number}: #{ErrorMessages.friendly_message(reason)}" end) put_toast( socket, :error, gettext("Some records could not be imported: %{summary}", summary: errors_summary) ) end defp release_format_label(release) do release |> MusicBrainz.ReleaseSearchResult.format() |> RecordComponents.format_label() end end