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, []) |> assign(:cart_expanded?, true)} end @impl true def render(assigns) do ~H"""

{gettext("Scan one or more barcodes")}

<.camera_button camera={@camera} />
""" 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 :result, BarcodeScan.Result, required: true attr :myself, :any, required: true defp cart_item(%{result: %{status: :not_found}} = assigns) do ~H""" <.barcode_icon class="w-12 h-12 flex-none rounded-md p-2 bg-red-50 text-red-400 dark:bg-red-950" />

{@result.number}

{gettext("Barcode not found")}

<.status_badge status={:not_found} />
""" end defp cart_item(assigns) do ~H""" {@result.release.release_group.title} ~p"/images/cover-not-found.png" <> "';"} />

{@result.release.artists}

{@result.release.title}

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

<.status_badge status={@result.status} record_id={@result.record_id} />
""" end attr :status, :atom, required: true, values: [:new, :wishlisted, :collected, :not_found] attr :record_id, :string, default: nil defp status_badge(%{status: :new} = assigns) do ~H""" <.badge>{gettext("New")} """ end defp status_badge(%{status: :wishlisted} = assigns) do ~H""" <.link navigate={~p"/wishlist/#{@record_id}"}> <.badge color="warning">{gettext("Wishlisted")} """ end defp status_badge(%{status: :collected} = assigns) do ~H""" <.link navigate={~p"/collection/#{@record_id}"}> <.badge color="success">{gettext("Collected")} """ end defp status_badge(%{status: :not_found} = assigns) do ~H""" <.badge color="danger">{gettext("Not found")} """ 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("remove_result", %{"number" => number}, socket) do scan_results = Enum.reject(socket.assigns.scan_results, &(&1.number == number)) {:noreply, socket |> assign(:scan_results, scan_results) |> push_event("remove_barcode", %{number: number})} end def handle_event("clear_results", _params, socket) do {:noreply, socket |> assign(:scan_results, []) |> push_event("clear_barcodes", %{})} end def handle_event("toggle_cart", _params, socket) do {:noreply, assign(socket, :cart_expanded?, not socket.assigns.cart_expanded?)} 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