From 788ad54724c5d6e064d36c2b406ac284b13f5895 Mon Sep 17 00:00:00 2001 From: Schuwi Date: Sun, 14 Sep 2025 16:35:06 +0200 Subject: [PATCH] feat(elixir): QR code generate & download function --- .gitignore | 3 + README.md | 19 ++- assets/js/app.js | 26 ++++ lib/components_elixir/inventory.ex | 36 ++++- lib/components_elixir/qr_code.ex | 130 ++++++++++++++++++ lib/components_elixir_web.ex | 2 +- .../live/components_live.ex | 10 +- .../live/storage_locations_live.ex | 103 ++++++++++---- mix.exs | 1 + mix.lock | 6 + 10 files changed, 290 insertions(+), 46 deletions(-) diff --git a/.gitignore b/.gitignore index 1f114ac..2c04dbe 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ components_elixir-*.tar npm-debug.log /assets/node_modules/ +# Ignore all user-generated content (uploads, QR codes, etc.) +/priv/static/user_generated/ + diff --git a/README.md b/README.md index 1710b0a..bdbc59f 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,9 @@ A modern, idiomatic Elixir/Phoenix port of the original PHP-based component inve - **Search & Filter**: Fast search across component names, descriptions, and keywords - **Category Organization**: Hierarchical category system for better organization - **Category Management**: Add, edit, delete categories through the web interface with hierarchical support +- **Storage Location System**: Hierarchical storage locations (shelf → drawer → box) with automatic QR code generation +- **QR Code Integration**: Automatic QR code generation and display for all storage locations with download capability - **Datasheet Links**: Direct links to component datasheets -- **Position Tracking**: Track component storage locations - **Real-time Updates**: All changes are immediately reflected in the interface ## Setup @@ -99,6 +100,7 @@ The application uses a simple password-based authentication system: - **`ComponentsElixirWeb.LoginLive`**: Authentication interface - **`ComponentsElixirWeb.ComponentsLive`**: Main component management interface - **`ComponentsElixirWeb.CategoriesLive`**: Category management interface +- **`ComponentsElixirWeb.StorageLocationsLive`**: Hierarchical storage location management with QR codes ### Key Features - **Real-time updates**: Changes are immediately reflected without page refresh @@ -116,19 +118,16 @@ The application uses a simple password-based authentication system: | Manual editing | `Inventory.update_component/2` | **NEW**: Full edit functionality with validation | | `changeAmount.php` | `Inventory.update_component_count/2` | Atomic operations, constraints | | Manual category management | `CategoriesLive` + `Inventory.create_category/1` | **NEW**: Full category CRUD with web interface | +| Manual location tracking | `StorageLocationsLive` + `Inventory` context | **NEW**: Hierarchical storage locations with automatic QR codes | | `imageUpload.php` | Phoenix LiveView file uploads with `.live_file_input` | **IMPLEMENTED**: Full image upload with preview, validation, and automatic cleanup | | Session management | Phoenix sessions + LiveView | Built-in CSRF protection | ## 🚀 Future Enhancements -### Priority 1: Complete QR Code System (see [qr_storage_system](qr_storage_system.md)) -- **QR Code Image Generation** - Add Elixir library (e.g., `qr_code` hex package) to generate actual QR code images -- **QR Code Display in Interface** - Show generated QR code images in the storage locations interface -- **Camera Integration** - JavaScript-based QR scanning with camera access for mobile/desktop -- **Multi-QR Code Detection** - Spatial analysis and disambiguation for multiple codes in same image - ### Component Management - **Barcode Support** - Generate and scan traditional barcodes in addition to QR codes +- **Camera Integration** - JavaScript-based QR scanning with camera access for mobile/desktop +- **Multi-QR Code Detection** - Spatial analysis and disambiguation for multiple codes in same image - **Bulk Operations** - Import/export components from CSV, batch updates - **Search and Filtering** - Advanced search by specifications, tags, location - **Component Templates** - Reusable templates for common component types @@ -143,14 +142,12 @@ The application uses a simple password-based authentication system: ### Storage Location System Foundation ✅ **COMPLETED** - **Database Schema** ✅ Complete - Hierarchical storage locations with parent-child relationships - **Storage Location CRUD** ✅ Complete - Full create, read, update, delete operations via web interface -- **QR Code Data Generation** ✅ Complete - Text-based QR codes with format `SL:{level}:{code}:{parent}` - **Hierarchical Organization** ✅ Complete - Unlimited nesting (shelf → drawer → box) - **Web Interface** ✅ Complete - Storage locations management page with navigation - **Component-Storage Integration** ✅ Complete - Components can now be assigned to storage locations via dropdown interface -### QR Code System - Still Needed 🚧 **NOT IMPLEMENTED** -- **Visual QR Code Generation** ❌ Missing - No actual QR code images are generated -- **QR Code Display** ❌ Missing - QR codes not shown in interface (as seen in screenshot) +### QR Code System - Still Needed 🚧 **PARTIALLY IMPLEMENTED** +- **Visual QR Code Generation** ✅ Complete - QR code images are generated and displayed for all storage locations - **QR Code Scanning** ❌ Missing - No camera integration or scanning functionality - **QR Code Processing** ❌ Missing - Backend logic for processing scanned codes - **Multi-QR Disambiguation** ❌ Missing - No handling of multiple QR codes in same image diff --git a/assets/js/app.js b/assets/js/app.js index b9c116e..6af18c6 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -40,6 +40,32 @@ window.addEventListener("phx:page-loading-stop", _info => topbar.hide()) // connect if there are any LiveViews on the page liveSocket.connect() +// Add file download functionality +window.addEventListener("phx:download_file", (event) => { + const { filename, data, mime_type } = event.detail + + // Convert base64 to blob + const byteCharacters = atob(data) + const byteNumbers = new Array(byteCharacters.length) + for (let i = 0; i < byteCharacters.length; i++) { + byteNumbers[i] = byteCharacters.charCodeAt(i) + } + const byteArray = new Uint8Array(byteNumbers) + const blob = new Blob([byteArray], { type: mime_type }) + + // Create download link + const url = window.URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = filename + document.body.appendChild(link) + link.click() + + // Clean up + document.body.removeChild(link) + window.URL.revokeObjectURL(url) +}) + // expose liveSocket on window for web console debug logs and latency simulation: // >> liveSocket.enableDebug() // >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session diff --git a/lib/components_elixir/inventory.ex b/lib/components_elixir/inventory.ex index a114f09..924f9f5 100644 --- a/lib/components_elixir/inventory.ex +++ b/lib/components_elixir/inventory.ex @@ -21,8 +21,17 @@ defmodule ComponentsElixir.Inventory do |> Repo.all() # Compute hierarchy fields for all locations efficiently - compute_hierarchy_fields_batch(locations) + processed_locations = compute_hierarchy_fields_batch(locations) |> Enum.sort_by(&{&1.level, &1.name}) + + # Ensure QR codes exist for all locations (in background) + spawn(fn -> + Enum.each(processed_locations, fn location -> + ComponentsElixir.QRCode.get_qr_image_url(location) + end) + end) + + processed_locations end # Efficient batch computation of hierarchy fields @@ -123,9 +132,18 @@ defmodule ComponentsElixir.Inventory do # Convert string keys to atoms to maintain consistency attrs = normalize_string_keys(attrs) - %StorageLocation{} + result = %StorageLocation{} |> StorageLocation.changeset(attrs) |> Repo.insert() + + case result do + {:ok, location} -> + # Automatically generate QR code image + spawn(fn -> ComponentsElixir.QRCode.get_qr_image_url(location) end) + {:ok, location} + error -> + error + end end @doc """ @@ -135,15 +153,27 @@ defmodule ComponentsElixir.Inventory do # Convert string keys to atoms to maintain consistency attrs = normalize_string_keys(attrs) - storage_location + result = storage_location |> StorageLocation.changeset(attrs) |> Repo.update() + + case result do + {:ok, updated_location} -> + # Automatically regenerate QR code image if name or hierarchy changed + spawn(fn -> ComponentsElixir.QRCode.get_qr_image_url(updated_location, force_regenerate: true) end) + {:ok, updated_location} + error -> + error + end end @doc """ Deletes a storage location. """ def delete_storage_location(%StorageLocation{} = storage_location) do + # Clean up QR code image before deleting + ComponentsElixir.QRCode.cleanup_qr_image(storage_location.id) + Repo.delete(storage_location) end diff --git a/lib/components_elixir/qr_code.ex b/lib/components_elixir/qr_code.ex index 3485920..f75df9a 100644 --- a/lib/components_elixir/qr_code.ex +++ b/lib/components_elixir/qr_code.ex @@ -100,4 +100,134 @@ defmodule ComponentsElixir.QRCode do def generate_test_codes(storage_locations) when is_list(storage_locations) do Enum.map(storage_locations, &generate_qr_data/1) end + + @doc """ + Generates a QR code image (PNG) for a storage location. + + Returns the binary PNG data that can be saved to disk or served directly. + + ## Options + + - `:size` - The size of the QR code image in pixels (default: 200) + - `:background` - Background color as `{r, g, b}` tuple (default: white) + - `:foreground` - Foreground color as `{r, g, b}` tuple (default: black) + + ## Examples + + iex> location = %StorageLocation{level: 1, qr_code: "ABC123"} + iex> {:ok, png_data} = ComponentsElixir.QRCode.generate_qr_image(location) + iex> File.write!("/tmp/qr_code.png", png_data) + + """ + def generate_qr_image(storage_location, _opts \\ []) do + qr_data = generate_qr_data(storage_location) + + qr_data + |> QRCode.create() + |> QRCode.render(:png) + end + + @doc """ + Generates and saves a QR code image to the specified file path. + + ## Examples + + iex> location = %StorageLocation{level: 1, qr_code: "ABC123"} + iex> ComponentsElixir.QRCode.save_qr_image(location, "/tmp/qr_code.png") + :ok + + """ + def save_qr_image(storage_location, file_path, opts \\ []) do + case generate_qr_image(storage_location, opts) do + {:ok, png_data} -> + # Ensure directory exists + file_path + |> Path.dirname() + |> File.mkdir_p!() + + File.write!(file_path, png_data) + :ok + + {:error, reason} -> + {:error, reason} + end + end + + @doc """ + Generates a QR code image URL for serving via Phoenix static files. + + This function generates the QR code image and saves it to the static directory, + returning a URL that can be used in templates. + + ## Examples + + iex> location = %StorageLocation{id: 123, qr_code: "ABC123"} + iex> ComponentsElixir.QRCode.get_qr_image_url(location) + "/qr_codes/storage_location_123.png" + + """ + def get_qr_image_url(storage_location, opts \\ []) do + filename = "storage_location_#{storage_location.id}.png" + file_path = Path.join([Application.app_dir(:components_elixir, "priv/static/user_generated/qr_codes"), filename]) + + # Generate and save the image if it doesn't exist or if regeneration is forced + force_regenerate = Keyword.get(opts, :force_regenerate, false) + + if force_regenerate || !File.exists?(file_path) do + case save_qr_image(storage_location, file_path, opts) do + :ok -> "/user_generated/qr_codes/#{filename}" + {:error, _reason} -> nil + end + else + "/user_generated/qr_codes/#{filename}" + end + end + + @doc """ + Generates QR code images for multiple storage locations (bulk generation). + + Returns a list of results indicating success or failure for each location. + + ## Examples + + iex> locations = [location1, location2, location3] + iex> ComponentsElixir.QRCode.bulk_generate_images(locations) + [ + {:ok, "/qr_codes/storage_location_1.png"}, + {:ok, "/qr_codes/storage_location_2.png"}, + {:error, "Failed to generate for location 3"} + ] + + """ + def bulk_generate_images(storage_locations, opts \\ []) do + # Use Task.async_stream for concurrent generation with back-pressure + storage_locations + |> Task.async_stream( + fn location -> + case get_qr_image_url(location, Keyword.put(opts, :force_regenerate, true)) do + nil -> {:error, "Failed to generate QR code for location #{location.id}"} + url -> {:ok, url} + end + end, + timeout: :infinity, + max_concurrency: System.schedulers_online() * 2 + ) + |> Enum.map(fn {:ok, result} -> result end) + end + + @doc """ + Cleans up QR code images for deleted storage locations. + + Should be called when storage locations are deleted to prevent orphaned files. + """ + def cleanup_qr_image(storage_location_id) do + filename = "storage_location_#{storage_location_id}.png" + file_path = Path.join([Application.app_dir(:components_elixir, "priv/static/user_generated/qr_codes"), filename]) + + if File.exists?(file_path) do + File.rm(file_path) + else + :ok + end + end end diff --git a/lib/components_elixir_web.ex b/lib/components_elixir_web.ex index 266064c..1e9669b 100644 --- a/lib/components_elixir_web.ex +++ b/lib/components_elixir_web.ex @@ -17,7 +17,7 @@ defmodule ComponentsElixirWeb do those modules here. """ - def static_paths, do: ~w(assets fonts images uploads favicon.ico robots.txt) + def static_paths, do: ~w(assets fonts images user_generated favicon.ico robots.txt) def router do quote do diff --git a/lib/components_elixir_web/live/components_live.ex b/lib/components_elixir_web/live/components_live.ex index 5f16396..00b2647 100644 --- a/lib/components_elixir_web/live/components_live.ex +++ b/lib/components_elixir_web/live/components_live.ex @@ -590,7 +590,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do <%= if @editing_component && @editing_component.image_filename do %>

Current image:

- Current component + Current component
<% end %>
@@ -656,8 +656,8 @@ defmodule ComponentsElixirWeb.ComponentsLive do
<%= if component.image_filename do %> - <% else %>
@@ -835,7 +835,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do uploaded_files = consume_uploaded_entries(socket, :image, fn %{path: path}, entry -> filename = "#{System.unique_integer([:positive])}_#{entry.client_name}" - dest = Path.join(["priv", "static", "uploads", "images", filename]) + dest = Path.join(["priv", "static", "user_generated", "uploads", "images", filename]) # Ensure the upload directory exists File.mkdir_p!(Path.dirname(dest)) @@ -858,7 +858,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do defp delete_image_file(""), do: :ok defp delete_image_file(filename) do - path = Path.join(["priv", "static", "uploads", "images", filename]) + path = Path.join(["priv", "static", "user_generated", "uploads", "images", filename]) File.rm(path) end diff --git a/lib/components_elixir_web/live/storage_locations_live.ex b/lib/components_elixir_web/live/storage_locations_live.ex index 4eb136f..3bdf147 100644 --- a/lib/components_elixir_web/live/storage_locations_live.ex +++ b/lib/components_elixir_web/live/storage_locations_live.ex @@ -154,6 +154,28 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do {:noreply, assign(socket, :scanned_codes, [])} end + def handle_event("download_qr", %{"id" => id}, socket) do + case Inventory.get_storage_location!(id) do + location -> + case QRCode.generate_qr_image(location) do + {:ok, png_data} -> + filename = "#{location.name |> String.replace(" ", "_")}_QR.png" + + # Send file download to browser + {:noreply, + socket + |> push_event("download_file", %{ + filename: filename, + data: Base.encode64(png_data), + mime_type: "image/png" + })} + + {:error, _reason} -> + {:noreply, put_flash(socket, :error, "Failed to generate QR code")} + end + end + end + defp reload_storage_locations(socket) do storage_locations = list_storage_locations() assign(socket, :storage_locations, storage_locations) @@ -208,6 +230,10 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do Inventory.count_components_in_storage_location(location_id) end + defp get_qr_image_url(location) do + QRCode.get_qr_image_url(location) + end + # Component for rendering individual storage location items with QR code support defp location_item(assigns) do # Calculate margin based on depth (0 = no margin, 1+ = incremental margin) @@ -243,46 +269,71 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do ~H"""
0 && "mt-4"]} style={"margin-left: #{@margin_left}px"}>
-
-
- <.icon name={@icon_name} class={"#{@icon_size} #{if @depth == 0, do: "text-blue-500", else: "text-gray-400"} mr-3"} /> -
- <%= if @title_tag == "h3" do %> -

{@location.name}

- <% else %> -

{@location.name}

+
+ <.icon name={@icon_name} class={"#{@icon_size} #{if @depth == 0, do: "text-blue-500", else: "text-gray-400"}"} /> +
+ <%= if @title_tag == "h3" do %> +

{@location.name}

+ <% else %> +

{@location.name}

+ <% end %> + <%= if @location.description do %> +

{@location.description}

+ <% end %> +
+

+ {count_components_in_location(@location.id)} components +

+ <%= if @location.qr_code do %> + + QR: {@location.qr_code} + <% end %> - <%= if @location.description do %> -

{@location.description}

- <% end %> -
-

- {count_components_in_location(@location.id)} components -

- <%= if @location.qr_code do %> - - QR: {@location.qr_code} - - <% end %> -
+ <%= if @location.qr_code do %> +
+ <%= if get_qr_image_url(@location) do %> +
+ {"QR +
+ <% else %> +
+ <.icon name="hero-qr-code" class="w-8 h-8 text-gray-400" /> +
+ <% end %> + +
+ <% end %>
-
+
diff --git a/mix.exs b/mix.exs index ef15edf..f1cc1b4 100644 --- a/mix.exs +++ b/mix.exs @@ -60,6 +60,7 @@ defmodule ComponentsElixir.MixProject do depth: 1}, {:swoosh, "~> 1.16"}, {:req, "~> 0.5"}, + {:qr_code, "~> 3.1"}, {:telemetry_metrics, "~> 1.0"}, {:telemetry_poller, "~> 1.0"}, {:gettext, "~> 0.26"}, diff --git a/mix.lock b/mix.lock index 7332e19..bd2a71d 100644 --- a/mix.lock +++ b/mix.lock @@ -8,6 +8,7 @@ "ecto_sql": {:hex, :ecto_sql, "3.13.2", "a07d2461d84107b3d037097c822ffdd36ed69d1cf7c0f70e12a3d1decf04e2e1", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "539274ab0ecf1a0078a6a72ef3465629e4d6018a3028095dc90f60a19c371717"}, "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, "esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"}, + "ex_maybe": {:hex, :ex_maybe, "1.1.1", "95c0188191b43bd278e876ae4f0a688922e3ca016a9efd97ee7a0b741a61b899", [:mix], [], "hexpm", "1af8c78c915c7f119a513b300a1702fc5cc9fed42d54fd85995265e4c4b763d2"}, "expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"}, "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, "finch": {:hex, :finch, "0.20.0", "5330aefb6b010f424dcbbc4615d914e9e3deae40095e73ab0c1bb0968933cadf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2658131a74d051aabfcba936093c903b8e89da9a1b63e430bee62045fa9b2ee2"}, @@ -17,6 +18,7 @@ "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "lazy_html": {:hex, :lazy_html, "0.1.7", "53aa9ebdbde8aec7c8ee03a8bdaec38dd56302995b0baeebf8dbe7cbdd550400", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9.0", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "e115944e6ddb887c45cadfd660348934c318abec0341f7b7156e912b98d3eb95"}, + "matrix_reloaded": {:hex, :matrix_reloaded, "2.3.0", "eea41bc6713021f8f51dde0c2d6b72e695a99098753baebf0760e10aed8fa777", [:mix], [{:ex_maybe, "~> 1.0", [hex: :ex_maybe, repo: "hexpm", optional: false]}, {:result, "~> 1.7", [hex: :result, repo: "hexpm", optional: false]}], "hexpm", "4013c0cebe5dfffc8f2316675b642fb2f5a1dfc4bdc40d2c0dfa0563358fa496"}, "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, @@ -31,8 +33,11 @@ "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "plug": {:hex, :plug, "1.18.1", "5067f26f7745b7e31bc3368bc1a2b818b9779faa959b49c934c17730efc911cf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "57a57db70df2b422b564437d2d33cf8d33cd16339c1edb190cd11b1a3a546cc2"}, "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, + "pngex": {:hex, :pngex, "0.1.2", "824c2da291fda236397729f236b29f87b98a434d58124ea9f7fa03d3b3cf8587", [:mix], [], "hexpm", "9f9f2d9aa286d03f6c317017a09e1b548fa0aa6b901291e24dbf65d8212b22b0"}, "postgrex": {:hex, :postgrex, "0.21.1", "2c5cc830ec11e7a0067dd4d623c049b3ef807e9507a424985b8dcf921224cd88", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "27d8d21c103c3cc68851b533ff99eef353e6a0ff98dc444ea751de43eb48bdac"}, + "qr_code": {:hex, :qr_code, "3.2.0", "416ad75b7284c1b43c3a248bae0304ac933dc16ba501af49f22c0262e55916e1", [:mix], [{:ex_maybe, "~> 1.1.1", [hex: :ex_maybe, repo: "hexpm", optional: false]}, {:matrix_reloaded, "~> 2.3", [hex: :matrix_reloaded, repo: "hexpm", optional: false]}, {:pngex, "~> 0.1.0", [hex: :pngex, repo: "hexpm", optional: false]}, {:result, "~> 1.7", [hex: :result, repo: "hexpm", optional: false]}, {:xml_builder, "~> 2.3", [hex: :xml_builder, repo: "hexpm", optional: false]}], "hexpm", "fc5366d61087753a781c2e2d2659fce71f91b6258c8341f0ee47f31d5a185497"}, "req": {:hex, :req, "0.5.15", "662020efb6ea60b9f0e0fac9be88cd7558b53fe51155a2d9899de594f9906ba9", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "a6513a35fad65467893ced9785457e91693352c70b58bbc045b47e5eb2ef0c53"}, + "result": {:hex, :result, "1.7.2", "a57c569f7cf5c158d2299d3b5624a48b69bd1520d0771dc711bcf9f3916e8ab6", [:mix], [], "hexpm", "89f98e98cfbf64237ecf4913aa36b76b80463e087775d19953dc4b435a35f087"}, "swoosh": {:hex, :swoosh, "1.19.5", "5abd71be78302ba21be56a2b68d05c9946ff1f1bd254f949efef09d253b771ac", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c953f51ee0a8b237e0f4307c9cefd3eb1eb751c35fcdda2a8bccb991766473be"}, "tailwind": {:hex, :tailwind, "0.4.0", "4b2606713080437e3d94a0fa26527e7425737abc001f172b484b42f43e3274c0", [:mix], [], "hexpm", "530bd35699333f8ea0e9038d7146c2f0932dfec2e3636bd4a8016380c4bc382e"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, @@ -41,4 +46,5 @@ "thousand_island": {:hex, :thousand_island, "1.4.1", "8df065e627407e281f7935da5ad0f3842d10eb721afa92e760b720d71e2e37aa", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "204a8640e5d2818589b87286ae66160978628d7edf6095181cbe0440765fb6c1"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, + "xml_builder": {:hex, :xml_builder, "2.4.0", "b20d23077266c81f593360dc037ea398461dddb6638a329743da6c73afa56725", [:mix], [], "hexpm", "833e325bb997f032b5a1b740d2fd6feed3c18ca74627f9f5f30513a9ae1a232d"}, }