Rename components to better reflect namespace
╭─────────────────────────────────────────────────────────╮
│ ✻ Welcome to Claude Code! │
│ │
│ /help for help, /status for your current setup │
│ │
│ cwd: /Users/cloud/github.com/cloud8421/music_library │
╰─────────────────────────────────────────────────────────╯
> Look at @lib/music_library_web/components/ and you fill find
components there. I would like you to rename the component modules so
that instead
of being called MusicLibraryWeb.<something>Component, they're called
MusicLibraryWeb.Components.<something>. For example
MusicLibraryWeb.AddRecordComponent would be called
MusicLibraryWeb.Components.AddRecord. Make sure you also update
references where these
components are used.
⎿ Listed directory lib/music_library_web/components/
⏺ I'll help you rename the component modules and update their
references. Let me first examine the components directory structure and
then create
a plan.
⏺ Bash(find
/Users/cloud/github.com/cloud8421/music_library/lib/music_library_web/components
-name "*.ex" | head -20)
⎿
/Users/cloud/github.com/cloud8421/music_library/lib/music_library_web/components/pagination_component.ex
/Users/cloud/github.com/cloud8421/music_library/lib/music_library_web/components/add_record_component.ex
/Users/cloud/github.com/cloud8421/music_library/lib/music_library_web/components/release_component.ex
… +10 lines (ctrl+o to expand)
⏺ Now let me examine a few of these files to understand the current
naming pattern:
⏺ Read(lib/music_library_web/components/add_record_component.ex)
⎿ Read 10 lines (ctrl+o to expand)
⏺ Read(lib/music_library_web/components/pagination_component.ex)
⎿ Read 10 lines (ctrl+o to expand)
⏺ Read(lib/music_library_web/components/barcode_scanner_component.ex)
⎿ Read 10 lines (ctrl+o to expand)
⏺ Now let me search for references to these components across the
codebase:
⏺ Search(pattern:
"MusicLibraryWeb\.(AddRecord|Pagination|BarcodeScanner|Notes|Release)Component")
⎿ Found 13 files (ctrl+o to expand)
⏺ Let me also check for other component patterns:
⏺ Search(pattern: "MusicLibraryWeb\.[A-Z][a-zA-Z]*Components?")
⎿ Found 34 files (ctrl+o to expand)
⏺ Now let me create a plan for this refactoring task:
⏺ Let me examine all component files to understand the current naming
pattern:
⏺ Bash(find
/Users/cloud/github.com/cloud8421/music_library/lib/music_library_web/components
-name "*.ex" -exec grep -l "defmodule MusicLibraryWeb\."
{} \;)
⎿ Interrupted · What should Claude do instead?
> Retry the find command
⏺ Bash(find
/Users/cloud/github.com/cloud8421/music_library/lib/music_library_web/components
-name "*.ex" -exec grep -l "defmodule MusicLibraryWeb\."
{} \;)
⎿
/Users/cloud/github.com/cloud8421/music_library/lib/music_library_web/components/pagination_component.ex
/Users/cloud/github.com/cloud8421/music_library/lib/music_library_web/components/add_record_component.ex
/Users/cloud/github.com/cloud8421/music_library/lib/music_library_web/components/release_component.ex
… +10 lines (ctrl+o to expand)
⏺ Now let me examine the module names in each file to identify which
ones need renaming:
⏺ Bash(grep "defmodule MusicLibraryWeb\."
/Users/cloud/github.com/cloud8421/music_library/lib/music_library_web/components/*.ex)
⎿
/Users/cloud/github.com/cloud8421/music_library/lib/music_library_web/components/add_record_component.ex:defmodule
MusicLibraryWeb.AddRecord
Component do
/Users/cloud/github.com/cloud8421/music_library/lib/music_library_web/components/artist_components.ex:defmodule
MusicLibraryWeb.ArtistCompon
… +21 lines (ctrl+o to expand)
⏺ Perfect! Now I can see which components need renaming. Based on the
current naming pattern, I need to rename the ones ending with
"Component"
to be under the "Components" namespace. Let me update my todo list and
complete the identification:
⏺ Now let me start renaming the components. I'll begin with
AddRecordComponent:
⏺ Update(lib/music_library_web/components/add_record_component.ex)
⎿ Updated lib/music_library_web/components/add_record_component.ex
with 1 addition and 1 removal
1 - defmodule MusicLibraryWeb.AddRecordComponent do
1 + defmodule MusicLibraryWeb.Components.AddRecord do
2 use MusicLibraryWeb, :live_component
3
4 import MusicLibraryWeb.RecordComponents, only:
[format_label: 1, type_label: 1]
⏺ Update(lib/music_library_web/components/barcode_scanner_component.ex)
⎿ Updated
lib/music_library_web/components/barcode_scanner_component.ex with 1
addition and 1 removal
1 - defmodule MusicLibraryWeb.BarcodeScannerComponent do
1 + defmodule MusicLibraryWeb.Components.BarcodeScanner do
2 use MusicLibraryWeb, :live_component
3
4 alias MusicBrainz.ReleaseGroupSearchResult
⏺ Update(lib/music_library_web/components/notes_component.ex)
⎿ Updated lib/music_library_web/components/notes_component.ex with 1
addition and 1 removal
1 - defmodule MusicLibraryWeb.NotesComponent do
1 + defmodule MusicLibraryWeb.Components.Notes do
2 use MusicLibraryWeb, :live_component
3
4 alias MusicLibrary.Notes
⏺ Update(lib/music_library_web/components/pagination_component.ex)
⎿ Updated lib/music_library_web/components/pagination_component.ex
with 1 addition and 1 removal
1 - defmodule MusicLibraryWeb.PaginationComponent do
1 + defmodule MusicLibraryWeb.Components.Pagination do
2 use MusicLibraryWeb, :html
3
4 attr :pagination_params, :map, required: true
⏺ Update(lib/music_library_web/components/release_component.ex)
⎿ Updated lib/music_library_web/components/release_component.ex with 1
addition and 1 removal
1 - defmodule MusicLibraryWeb.ReleaseComponent do
1 + defmodule MusicLibraryWeb.Components.Release do
2 use MusicLibraryWeb, :live_component
3 use Gettext, backend: MusicLibraryWeb.Gettext
4
⏺ Update(lib/music_library_web/components/record_form.ex)
⎿ Updated lib/music_library_web/components/record_form.ex with 1
addition and 1 removal
1 - defmodule MusicLibraryWeb.RecordForm do
1 + defmodule MusicLibraryWeb.Components.RecordForm do
2 use MusicLibraryWeb, :live_component
3
4 import MusicLibraryWeb.RecordComponents,
⏺ Now I need to update all the references across the codebase. Let me
start this process:
⏺ Now let me search for and update all references to these components.
Let me start with AddRecordComponent:
⏺ Search(pattern: "AddRecordComponent", output_mode: "content")
⎿ Found 3 lines (ctrl+o to expand)
⏺ Update(lib/music_library_web/live/artist_live/show.html.heex)
⎿ File must be read first
⏺ Read(lib/music_library_web/live/artist_live/show.html.heex)
⎿ Read 10 lines (ctrl+o to expand)
⏺ Update(lib/music_library_web/live/artist_live/show.html.heex)
⎿ Updated lib/music_library_web/live/artist_live/show.html.heex with 1
addition and 1 removal
237
on_close={JS.patch(~p"/artists/#{@artist.musicbrainz_id}")}
238 >
239 <.live_component
240 - module={MusicLibraryWeb.AddRecordComponent}
240 + module={MusicLibraryWeb.Components.AddRecord}
241 id={:search}
242 title={@page_title}
243 action={@live_action}
⏺ Read(lib/music_library_web/live/wishlist_live/index.html.heex)
⎿ Read 10 lines (ctrl+o to expand)
⏺ Update(lib/music_library_web/live/wishlist_live/index.html.heex)
⎿ Updated lib/music_library_web/live/wishlist_live/index.html.heex
with 1 addition and 1 removal
70 on_close={JS.patch(back_path(@record_list_params))}
71 >
72 <.live_component
73 - module={MusicLibraryWeb.AddRecordComponent}
73 + module={MusicLibraryWeb.Components.AddRecord}
74 id={:search}
75 title={@page_title}
76 action={@live_action}
⏺ Read(lib/music_library_web/live/collection_live/index.html.heex)
⎿ Read 10 lines (ctrl+o to expand)
⏺ Update(lib/music_library_web/live/collection_live/index.html.heex)
⎿ Updated lib/music_library_web/live/collection_live/index.html.heex
with 1 addition and 1 removal
82 on_close={JS.patch(back_path(@record_list_params))}
83 >
84 <.live_component
85 - module={MusicLibraryWeb.AddRecordComponent}
85 + module={MusicLibraryWeb.Components.AddRecord}
86 id={:search}
87 title={@page_title}
88 action={@live_action}
⏺ Now let me search for BarcodeScannerComponent references:
⏺ Search(pattern: "BarcodeScannerComponent", output_mode: "content")
⎿ Found 2 lines (ctrl+o to expand)
⏺ Read(lib/music_library_web/live/collection_live/index.ex)
⎿ Read 10 lines (ctrl+o to expand)
⏺ I notice there are also imports of PaginationComponent in the same
file. Let me update multiple references in this file:
⏺ Update(lib/music_library_web/live/collection_live/index.ex)
⎿ Updated lib/music_library_web/live/collection_live/index.ex with 2
additions and 2 removals
1 defmodule MusicLibraryWeb.CollectionLive.Index do
2 use MusicLibraryWeb, :live_view
3
4 - import MusicLibraryWeb.BarcodeScannerComponent, only:
[barcode_icon: 1]
5 - import MusicLibraryWeb.PaginationComponent
4 + import MusicLibraryWeb.Components.BarcodeScanner, only:
[barcode_icon: 1]
5 + import MusicLibraryWeb.Components.Pagination
6 import MusicLibraryWeb.RecordComponents
7
8 alias MusicLibrary.Collection
⏺ Read(lib/music_library_web/live/collection_live/index.html.heex)
⎿ Read 10 lines (ctrl+o to expand)
⏺ Update(lib/music_library_web/live/collection_live/index.html.heex)
⎿ Updated lib/music_library_web/live/collection_live/index.html.heex
with 1 addition and 1 removal
99 on_close={JS.patch(back_path(@record_list_params))}
100 >
101 <.live_component
102 - module={MusicLibraryWeb.BarcodeScannerComponent}
102 + module={MusicLibraryWeb.Components.BarcodeScanner}
103 id={:barcode_scanner}
104 title={@page_title}
105 action={@live_action}
⏺ Let me continue searching for other component references. Let me
search for all the components I renamed:
⏺ Search(pattern:
"MusicLibraryWeb\.(NotesComponent|PaginationComponent|ReleaseComponent|RecordForm)",
output_mode: "content")
⎿ Found 18 lines (ctrl+o to expand)
⏺ Let me use a more efficient approach and update all the references at
once. First, let me read the files that need updating and then apply the
changes:
⏺ Read(lib/music_library_web/live/wishlist_live/index.ex)
⎿ Read 80 lines (ctrl+o to expand)
⏺ Update(lib/music_library_web/live/wishlist_live/index.ex)
⎿ Updated lib/music_library_web/live/wishlist_live/index.ex with 2
additions and 2 removals
1 defmodule MusicLibraryWeb.WishlistLive.Index do
2 use MusicLibraryWeb, :live_view
3
4 - import MusicLibraryWeb.PaginationComponent
4 + import MusicLibraryWeb.Components.Pagination
5 import MusicLibraryWeb.RecordComponents
6
7 alias MusicLibrary.Records
...
70 end
71
72 @impl true
73 - def handle_info({MusicLibraryWeb.RecordForm, {:saved,
_record}}, socket) do
73 + def handle_info({MusicLibraryWeb.Components.RecordForm,
{:saved, _record}}, socket) do
74 {:noreply, load_and_assign_records(socket,
socket.assigns.record_list_params)}
75 end
76
⏺ Let me continue with the other files. I'll use grep and replace to do
this more efficiently:
⏺ Bash(grep -r "MusicLibraryWeb\.RecordForm"
/Users/cloud/github.com/cloud8421/music_library/lib/music_library_web/
--exclude-dir=.git)
⎿
/Users/cloud/github.com/cloud8421/music_library/lib/music_library_web/live/wishlist_live/index.html.heex:
module={MusicLibraryWeb.Recor
dForm}
/Users/cloud/github.com/cloud8421/music_library/lib/music_library_web/live/wishlist_live/show.ex:
def handle_info({MusicLibraryWeb.RecordFo
… +11 lines (ctrl+o to expand)
⏺ Let me update these files with the correct module references:
⏺ Read(lib/music_library_web/live/wishlist_live/index.html.heex)
⎿ Read 10 lines (ctrl+o to expand)
⏺ Let me use a more efficient approach by using sed to replace all
occurrences in all files at once:
⏺ Bash(find
/Users/cloud/github.com/cloud8421/music_library/lib/music_library_web
-name "*.ex" -o -name "*.heex" | xargs sed -i ''
's/MusicLibraryWeb\.RecordForm/Music…)
⎿ (No content)
⏺ Perfect! Now let me continue with the other components. Let me update
NotesComponent references:
⏺ Bash(find
/Users/cloud/github.com/cloud8421/music_library/lib/music_library_web
-name "*.ex" -o -name "*.heex" | xargs sed -i ''
's/MusicLibraryWeb\.NotesComponent/M…)
⎿ (No content)
⏺ Now let me update ReleaseComponent references:
⏺ Bash(find
/Users/cloud/github.com/cloud8421/music_library/lib/music_library_web
-name "*.ex" -o -name "*.heex" | xargs sed -i ''
's/MusicLibraryWeb\.ReleaseComponent…)
⎿ (No content)
⏺ Finally, let me update PaginationComponent references:
⏺ Bash(find
/Users/cloud/github.com/cloud8421/music_library/lib/music_library_web
-name "*.ex" -o -name "*.heex" | xargs sed -i ''
's/MusicLibraryWeb\.PaginationCompon…)
⎿ (No content)
⏺ Read(lib/music_library_web/live/scrobbled_tracks_live/index.ex)
⎿ Read 10 lines (ctrl+o to expand)
⏺ Great! Now let me verify that all the references have been updated.
Let me search for any remaining old component references:
⏺ Search(pattern:
"MusicLibraryWeb\.(AddRecordComponent|BarcodeScannerComponent|NotesComponent|PaginationComponent|ReleaseComponent|RecordForm)(?!\.C
omponents)", output_mode: "content")
⎿ Found 0 lines
⏺ Excellent! Let me also check if there are any references in the
documentation that I should update:
⏺ Search(pattern: "MusicLibraryWeb\.PaginationComponent", path: "docs",
output_mode: "content")
⎿ Found 2 lines (ctrl+o to expand)
⏺ Let me update the documentation references as well:
⏺ Read(docs/plans/scrobbled-tracks-crud.md)
⎿ Read 10 lines (ctrl+o to expand)
⏺ Update(docs/plans/scrobbled-tracks-crud.md)
⎿ Updated docs/plans/scrobbled-tracks-crud.md with 2 additions and 2
removals
84 - Album title
85 - Scrobbled date/time
86 - Edit/Delete action buttons
87 - - Pagination component at bottom using
`MusicLibraryWeb.PaginationComponent`
87 + - Pagination component at bottom using
`MusicLibraryWeb.Components.Pagination`
88 - Empty state message when no tracks found
89
90 ### Form Component Template
...
127 ### Pagination Integration
128
129 - **Pattern**: Follow `CollectionLive.Index` pagination
approach
130 - - **Component**: Reuse
`MusicLibraryWeb.PaginationComponent`
130 + - **Component**: Reuse
`MusicLibraryWeb.Components.Pagination`
131 - **Parameters**: Support `query`, `page`, `page_size`,
and `order`
132 - **URL Structure**:
`/scrobbled-tracks?page=2&query=artist&page_size=50`
133
⏺ Now let me mark the reference update task as completed and run tests
to verify everything works:
⏺ Bash(mix test --max-failures=5)
⎿ Compiling 13 files (.ex)
Generated music_library app
Running ExUnit with seed: 515502, max_cases: 20
… +5 lines (ctrl+o to expand)
⏺ Perfect! All tests pass. Let me also run the format and static checks
to make sure everything is properly formatted:
⏺ Bash(mix do format --check-formatted, credo --strict)
⎿ Checking 193 source files (this might take a while) ...
Please report incorrect results:
https://github.com/rrrene/credo/issues
… +5 lines (ctrl+o to expand)
⏺ Excellent! All static checks pass as well. Let me complete the final
todo:
⏺ Perfect! I have successfully completed the component renaming task.
Here's a summary of what was accomplished:
Component Renaming Summary
Renamed Components:
- MusicLibraryWeb.AddRecordComponent →
MusicLibraryWeb.Components.AddRecord
- MusicLibraryWeb.BarcodeScannerComponent →
MusicLibraryWeb.Components.BarcodeScanner
- MusicLibraryWeb.NotesComponent → MusicLibraryWeb.Components.Notes
- MusicLibraryWeb.PaginationComponent →
MusicLibraryWeb.Components.Pagination
- MusicLibraryWeb.ReleaseComponent →
MusicLibraryWeb.Components.Release
- MusicLibraryWeb.RecordForm → MusicLibraryWeb.Components.RecordForm
Files Updated:
- Updated module definitions in all component files
- Updated imports in LiveView modules
- Updated module={...} references in templates
- Updated handle_info callback references
- Updated function calls in templates
- Updated documentation references
Verification:
- All tests pass (198 tests, 0 failures)
- Static checks pass (format and credo)
- No remaining old component references found
The component modules are now properly organized under the
MusicLibraryWeb.Components namespace as requested.
> Note that the file names of the renamed components need to be updated
as well
⏺ You're absolutely right! I need to rename the actual files to match
the new module structure. Let me update the filenames as well.
⏺ Bash(mv
/Users/cloud/github.com/cloud8421/music_library/lib/music_library_web/components/add_record_component.ex
/Users/cloud/github.com/cloud8421/music_library/lib/…)
⎿ (No content)
⏺ Bash(mv
/Users/cloud/github.com/cloud8421/music_library/lib/music_library_web/components/barcode_scanner_component.ex
/Users/cloud/github.com/cloud8421/music_library…)
⎿ (No content)
⏺ Bash(mv
/Users/cloud/github.com/cloud8421/music_library/lib/music_library_web/components/notes_component.ex
/Users/cloud/github.com/cloud8421/music_library/lib/music…)
⎿ (No content)
⏺ Bash(mv
/Users/cloud/github.com/cloud8421/music_library/lib/music_library_web/components/pagination_component.ex
/Users/cloud/github.com/cloud8421/music_library/lib/…)
⎿ (No content)
⏺ Bash(mv
/Users/cloud/github.com/cloud8421/music_library/lib/music_library_web/components/release_component.ex
/Users/cloud/github.com/cloud8421/music_library/lib/mus…)
⎿ (No content)
⏺ Now let me verify that all files have been renamed correctly:
⏺ Bash(ls -la
/Users/cloud/github.com/cloud8421/music_library/lib/music_library_web/components/)
⎿ .rw-r--r--@ 6.4k cloud 23 Sep 09:51 add_record.ex
.rw-r--r--@ 682 cloud 23 Sep 09:51 artist_components.ex
.rw-r--r--@ 15k cloud 23 Sep 09:51 barcode_scanner.ex
… +11 lines (ctrl+o to expand)
⏺ Perfect! Now let me run tests to verify everything still works after
the file renames:
⏺ Bash(mix test --max-failures=5)
⎿ Compiling 8 files (.ex)
Generated music_library app
Running ExUnit with seed: 143097, max_cases: 20
… +5 lines (ctrl+o to expand)
⏺ Excellent! All tests still pass. Let me also run the static checks:
⏺ Bash(mix do format --check-formatted, credo --strict)
⎿ Checking 193 source files (this might take a while) ...
Please report incorrect results:
https://github.com/rrrene/credo/issues
… +5 lines (ctrl+o to expand)
⏺ Perfect! Let me complete the final todo:
⏺ Complete Component Renaming Summary
I have successfully completed the component renaming task, including
updating both the module names and the file names:
Module and File Renames:
- add_record_component.ex → add_record.ex
(MusicLibraryWeb.AddRecordComponent →
MusicLibraryWeb.Components.AddRecord)
- barcode_scanner_component.ex → barcode_scanner.ex
(MusicLibraryWeb.BarcodeScannerComponent →
MusicLibraryWeb.Components.BarcodeScanner)
- notes_component.ex → notes.ex (MusicLibraryWeb.NotesComponent →
MusicLibraryWeb.Components.Notes)
- pagination_component.ex → pagination.ex
(MusicLibraryWeb.PaginationComponent →
MusicLibraryWeb.Components.Pagination)
- release_component.ex → release.ex (MusicLibraryWeb.ReleaseComponent
→ MusicLibraryWeb.Components.Release)
- record_form.ex → kept same filename (MusicLibraryWeb.RecordForm →
MusicLibraryWeb.Components.RecordForm)
What was updated:
- Module definitions in component files
- All import statements across the codebase
- LiveView template references (module={...})
- Function calls in templates
- handle_info callback references in LiveViews
- Documentation references
Verification:
- All tests pass (198 tests, 0 failures)
- Static checks pass (format and credo)
- Application compiles successfully
All component modules are now properly organized under the
MusicLibraryWeb.Components namespace with matching file names.
This commit is contained in:
@@ -0,0 +1,436 @@
|
||||
defmodule MusicLibraryWeb.Components.BarcodeScanner 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"""
|
||||
<div class="min-w-72" id="barcode-scanner" phx-hook=".BarcodeScanner" phx-target={@myself}>
|
||||
<header>
|
||||
<h1 class="text-sm font-medium leading-6 text-zinc-700 dark:text-zinc-400">
|
||||
{gettext("Scan one or more barcodes")}
|
||||
</h1>
|
||||
</header>
|
||||
<div class="mt-4">
|
||||
<.camera_button camera={@camera} />
|
||||
<video :if={!(@camera == :denied)} class="w-full hidden h-96" id="camera-preview" playsinline />
|
||||
</div>
|
||||
|
||||
<ul class="divide-y divide-zinc-100 dark:divide-slate-300/30 mt-5">
|
||||
<li
|
||||
:for={scan_result <- @scan_results}
|
||||
id={scan_result.number}
|
||||
class="flex justify-between gap-x-6 py-5"
|
||||
phx-mounted={
|
||||
JS.transition(
|
||||
{"first:ease-in duration-300", "first:opacity-0 first:p-0 first:h-0",
|
||||
"first:opacity-100"},
|
||||
time: 300
|
||||
)
|
||||
}
|
||||
>
|
||||
<.scan_result scan_result={scan_result} />
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="mt-4 flex justify-center">
|
||||
<.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")}
|
||||
</.button>
|
||||
</div>
|
||||
|
||||
<script :type={Phoenix.LiveView.ColocatedHook} name=".BarcodeScanner">
|
||||
import { BarcodeDetector } from "barcode-detector/ponyfill";
|
||||
|
||||
const barcodeReaderSetup = async function () {
|
||||
const supportedFormats = await BarcodeDetector.getSupportedFormats();
|
||||
|
||||
return new BarcodeDetector({
|
||||
// make sure the formats are supported
|
||||
formats: supportedFormats,
|
||||
});
|
||||
};
|
||||
|
||||
export default {
|
||||
async mounted() {
|
||||
const detectedBarcodes = new Set([]);
|
||||
const barcodeDetector = await barcodeReaderSetup();
|
||||
this.cameraPreview = this.el.querySelector("#camera-preview");
|
||||
const constraints = {
|
||||
audio: false,
|
||||
video: {
|
||||
width: 800,
|
||||
height: 600,
|
||||
facingMode: {
|
||||
ideal: "environment",
|
||||
},
|
||||
},
|
||||
};
|
||||
this.el.addEventListener("camera_request", () => {
|
||||
navigator.mediaDevices
|
||||
.getUserMedia(constraints)
|
||||
.then((mediaStream) => {
|
||||
console.debug("Camera access allowed");
|
||||
this.pushEventTo(this.el, "camera_allowed", {});
|
||||
this.cameraPreview.srcObject = mediaStream;
|
||||
this.cameraPreview.onloadedmetadata = () => {
|
||||
this.cameraPreview.play();
|
||||
this.scanInterval = window.setInterval(async () => {
|
||||
const barcodes = await barcodeDetector.detect(this.cameraPreview);
|
||||
if (barcodes.length <= 0) return;
|
||||
|
||||
barcodes.forEach((barcode) => {
|
||||
if (!detectedBarcodes.has(barcode.rawValue)) {
|
||||
this.pushEventTo(this.el, "barcode_scanned", {
|
||||
number: barcode.rawValue,
|
||||
});
|
||||
detectedBarcodes.add(barcode.rawValue);
|
||||
}
|
||||
});
|
||||
}, 500);
|
||||
};
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`${err.name}: ${err.message}`);
|
||||
this.pushEventTo(this.el, "camera_denied", {});
|
||||
});
|
||||
});
|
||||
},
|
||||
destroyed() {
|
||||
if (this.cameraPreview.srcObject) {
|
||||
this.cameraPreview.srcObject.getTracks().forEach((track) => track.stop());
|
||||
window.clearInterval(this.scanInterval);
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
attr :camera, :atom, required: true, values: [:pending, :allowed, :denied]
|
||||
|
||||
def camera_button(assigns) do
|
||||
~H"""
|
||||
<button
|
||||
:if={!(@camera == :allowed)}
|
||||
id="camera-button"
|
||||
type="button"
|
||||
phx-click={
|
||||
JS.show(to: "#camera-preview")
|
||||
|> 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"
|
||||
>
|
||||
<svg
|
||||
class="mx-auto size-12 text-zinc-400"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25h-9A2.25 2.25 0 0 0 2.25 7.5v9a2.25 2.25 0 0 0 2.25 2.25Z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="mt-2 block text-sm font-semibold text-zinc-900 dark:text-zinc-400">
|
||||
{gettext("Open camera")}
|
||||
</span>
|
||||
</button>
|
||||
"""
|
||||
end
|
||||
|
||||
attr :class, :string, required: true
|
||||
|
||||
def barcode_icon(assigns) do
|
||||
~H"""
|
||||
<svg
|
||||
class={@class}
|
||||
aria-hidden="true"
|
||||
data-slot="icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
version="1.1"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 256 256"
|
||||
xml:space="preserve"
|
||||
>
|
||||
<g
|
||||
style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill-rule: nonzero; opacity: 1;"
|
||||
transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)"
|
||||
>
|
||||
<polygon
|
||||
points="90,79.14 56.12,79.14 56.12,73.14 84,73.14 84,54.67 90,54.67 "
|
||||
style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill-rule: nonzero; opacity: 1;"
|
||||
transform=" matrix(1 0 0 1 0 0) "
|
||||
/>
|
||||
<polygon
|
||||
points="33.88,79.14 0,79.14 0,54.67 6,54.67 6,73.14 33.88,73.14 "
|
||||
style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill-rule: nonzero; opacity: 1;"
|
||||
transform=" matrix(1 0 0 1 0 0) "
|
||||
/>
|
||||
<polygon
|
||||
points="6,35.33 0,35.33 0,10.86 33.88,10.86 33.88,16.86 6,16.86 "
|
||||
style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill-rule: nonzero; opacity: 1;"
|
||||
transform=" matrix(1 0 0 1 0 0) "
|
||||
/>
|
||||
<polygon
|
||||
points="90,35.33 84,35.33 84,16.86 56.12,16.86 56.12,10.86 90,10.86 "
|
||||
style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill-rule: nonzero; opacity: 1;"
|
||||
transform=" matrix(1 0 0 1 0 0) "
|
||||
/>
|
||||
<rect
|
||||
x="36.27"
|
||||
y="26.31"
|
||||
rx="0"
|
||||
ry="0"
|
||||
width="6"
|
||||
height="37.39"
|
||||
style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill-rule: nonzero; opacity: 1;"
|
||||
transform=" matrix(1 0 0 1 0 0) "
|
||||
/>
|
||||
<rect
|
||||
x="13.36"
|
||||
y="26.31"
|
||||
rx="0"
|
||||
ry="0"
|
||||
width="6"
|
||||
height="37.39"
|
||||
style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill-rule: nonzero; opacity: 1;"
|
||||
transform=" matrix(1 0 0 1 0 0) "
|
||||
/>
|
||||
<rect
|
||||
x="59.18"
|
||||
y="26.31"
|
||||
rx="0"
|
||||
ry="0"
|
||||
width="6"
|
||||
height="24.02"
|
||||
style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill-rule: nonzero; opacity: 1;"
|
||||
transform=" matrix(1 0 0 1 0 0) "
|
||||
/>
|
||||
<rect
|
||||
x="59.18"
|
||||
y="56.85"
|
||||
rx="0"
|
||||
ry="0"
|
||||
width="6"
|
||||
height="6.84"
|
||||
style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill-rule: nonzero; opacity: 1;"
|
||||
transform=" matrix(1 0 0 1 0 0) "
|
||||
/>
|
||||
<rect
|
||||
x="24.82"
|
||||
y="26.31"
|
||||
rx="0"
|
||||
ry="0"
|
||||
width="6"
|
||||
height="24.02"
|
||||
style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill-rule: nonzero; opacity: 1;"
|
||||
transform=" matrix(1 0 0 1 0 0) "
|
||||
/>
|
||||
<rect
|
||||
x="24.82"
|
||||
y="56.85"
|
||||
rx="0"
|
||||
ry="0"
|
||||
width="6"
|
||||
height="6.84"
|
||||
style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill-rule: nonzero; opacity: 1;"
|
||||
transform=" matrix(1 0 0 1 0 0) "
|
||||
/>
|
||||
<rect
|
||||
x="70.64"
|
||||
y="26.31"
|
||||
rx="0"
|
||||
ry="0"
|
||||
width="6"
|
||||
height="24.02"
|
||||
style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill-rule: nonzero; opacity: 1;"
|
||||
transform=" matrix(1 0 0 1 0 0) "
|
||||
/>
|
||||
<rect
|
||||
x="70.64"
|
||||
y="56.85"
|
||||
rx="0"
|
||||
ry="0"
|
||||
width="6"
|
||||
height="6.84"
|
||||
style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill-rule: nonzero; opacity: 1;"
|
||||
transform=" matrix(1 0 0 1 0 0) "
|
||||
/>
|
||||
<rect
|
||||
x="47.73"
|
||||
y="26.31"
|
||||
rx="0"
|
||||
ry="0"
|
||||
width="6"
|
||||
height="37.39"
|
||||
style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill-rule: nonzero; opacity: 1;"
|
||||
transform=" matrix(1 0 0 1 0 0) "
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
"""
|
||||
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"""
|
||||
<div class="w-full bg-red-50 dark:bg-red-950 p-4">
|
||||
<h1 class="text-sm leading-6 text-zinc-700 dark:text-zinc-400">
|
||||
{gettext("Barcode not found")}
|
||||
</h1>
|
||||
<h2 class="mt-1 flex font-semibold text-sm sm:text-base leading-5 text-zinc-700 dark:text-zinc-300 text-wrap">
|
||||
{@number}
|
||||
</h2>
|
||||
</div>
|
||||
"""
|
||||
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"""
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<img
|
||||
class="w-16 md:w-20 flex-none rounded-lg mr-4"
|
||||
alt={@release.release_group.title}
|
||||
src={ReleaseGroupSearchResult.thumb_url(@release.release_group)}
|
||||
onerror={"this.src = '" <> ~p"/images/cover-not-found.png" <> "';"}
|
||||
/>
|
||||
<div class="min-w-0 flex-auto">
|
||||
<h1 class="text-sm leading-6 text-zinc-700 dark:text-zinc-400">
|
||||
{@release.artists}
|
||||
</h1>
|
||||
<h2 class="mt-1 flex font-semibold text-sm sm:text-base leading-5 text-zinc-700 dark:text-zinc-300 text-wrap">
|
||||
{@release.title}
|
||||
</h2>
|
||||
<p class="mt-1 text-xs leading-5 text-zinc-500 dark:text-zinc-400">
|
||||
{release_format_label(@release)} · {Records.Record.format_release_date(@release.date)} · {RecordComponents.type_label(
|
||||
@release.release_group.type
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<.badge :if={@status == :new}>
|
||||
{gettext("New")}
|
||||
</.badge>
|
||||
<.link :if={@status == :wishlisted} navigate={~p"/wishlist/#{@record_id}"}>
|
||||
<.badge color="warning">
|
||||
{gettext("Wishlisted")}
|
||||
</.badge>
|
||||
</.link>
|
||||
<.link :if={@status == :collected} navigate={~p"/collection/#{@record_id}"}>
|
||||
<.badge color="success">
|
||||
{gettext("Collected")}
|
||||
</.badge>
|
||||
</.link>
|
||||
</div>
|
||||
"""
|
||||
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()
|
||||
|
||||
socket =
|
||||
case BarcodeScan.import_results(socket.assigns.scan_results, current_time) do
|
||||
[] ->
|
||||
put_toast(socket, :info, gettext("Records imported successfully"))
|
||||
|
||||
errors ->
|
||||
errors_summary =
|
||||
Enum.map_join(errors, "\n", fn {number, reason} ->
|
||||
"#{number}: #{inspect(reason)}"
|
||||
end)
|
||||
|
||||
put_toast(
|
||||
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
|
||||
Reference in New Issue
Block a user