Added qr scanner
This commit is contained in:
@@ -1,58 +1,78 @@
|
||||
import { Controller } from "@hotwired/stimulus";
|
||||
import QrScanner from "qr-scanner"; // Importiert das saubere, einzelne Modul
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["input", "preview", "modal"];
|
||||
|
||||
connect() {
|
||||
this.html5QrCode = null;
|
||||
this.qrScanner = null;
|
||||
}
|
||||
|
||||
// Öffnet das Modal und startet den Kamera-Stream
|
||||
startCamera(event) {
|
||||
event.preventDefault();
|
||||
|
||||
// Modal anzeigen
|
||||
// 1. Modal anzeigen
|
||||
this.modalTarget.classList.remove("hidden");
|
||||
|
||||
// Neue Instanz auf dem Preview-Div mit der ID des Elements erzeugen
|
||||
this.html5QrCode = new Html5Qrcode(this.previewTarget.id);
|
||||
// 2. Ein HTML5 <video> Element holen oder erstellen (wird für den Stream gebraucht)
|
||||
const videoElement = this.previewTarget.querySelector("video") || this.createVideoElement();
|
||||
|
||||
// FPS-Rate und Scan-Rahmen (250x250px) festlegen
|
||||
const config = { fps: 10, qrbox: { width: 250, height: 250 } };
|
||||
// 3. Scanner initialisieren
|
||||
this.qrScanner = new QrScanner(
|
||||
videoElement,
|
||||
(result) => {
|
||||
// SUCCESS: Code erkannt!
|
||||
this.handleScanSuccess(result.data);
|
||||
},
|
||||
{
|
||||
onDecodeError: (error) => { /* Loop-Fehler während der Suche ignorieren */ },
|
||||
highlightScanRegion: true, // Zeichnet einen schicken gelben/grünen Scan-Rahmen ins Bild
|
||||
highlightCodeOutline: true,
|
||||
maxScansPerSecond: 10
|
||||
}
|
||||
);
|
||||
|
||||
this.html5QrCode
|
||||
.start(
|
||||
{ facingMode: "environment" }, // Erzwingt die rückseitige Hauptkamera bei Handys
|
||||
config,
|
||||
(decodedText, decodedResult) => {
|
||||
// SUCCESS: Code erkannt!
|
||||
this.inputTarget.value = decodedText; // Trägt die ID (z.B. 10024) ins Textfeld ein
|
||||
|
||||
// NEU: Simuliert das Tippen, damit dein search-form Controller die Live-Suche sofort startet!
|
||||
this.inputTarget.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
|
||||
this.stopCamera(); // Stoppt die Kamera und schließt das Fenster
|
||||
},
|
||||
(errorMessage) => {
|
||||
// Kontinuierlicher Scan-Loop (Fehler ignorieren, wenn kein QR-Code im Bild ist)
|
||||
},
|
||||
)
|
||||
.catch((err) => {
|
||||
console.error("Kamera-Zugriff verweigert oder blockiert:", err);
|
||||
});
|
||||
// 4. Kamera starten
|
||||
this.qrScanner.start().catch((err) => {
|
||||
alert("Kamera-Zugriff verweigert, blockiert oder unverschlüsselte Verbindung!");
|
||||
console.error("Kamera-Fehler:", err);
|
||||
this.modalTarget.classList.add("hidden");
|
||||
});
|
||||
}
|
||||
|
||||
// Schließt das Modal und beendet den Stream sauber
|
||||
// Verarbeitet die gescannte ID und startet das search-form
|
||||
handleScanSuccess(decodedText) {
|
||||
this.inputTarget.value = decodedText;
|
||||
|
||||
// Simuliert das Tippen für deinen Live-Filter
|
||||
this.inputTarget.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
|
||||
this.stopCamera();
|
||||
}
|
||||
|
||||
// Beendet den Stream sauber und schließt das Fenster
|
||||
stopCamera() {
|
||||
if (this.html5QrCode && this.html5QrCode.isScanning) {
|
||||
this.html5QrCode
|
||||
.stop()
|
||||
.then(() => {
|
||||
this.modalTarget.classList.add("hidden");
|
||||
})
|
||||
.catch((err) => console.error("Fehler beim Beenden des Streams:", err));
|
||||
} else {
|
||||
this.modalTarget.classList.add("hidden");
|
||||
if (this.qrScanner) {
|
||||
this.qrScanner.stop();
|
||||
this.qrScanner.destroy();
|
||||
this.qrScanner = null;
|
||||
}
|
||||
this.modalTarget.classList.add("hidden");
|
||||
}
|
||||
|
||||
// Hilfsmethode: Erstellt das Video-Tag im Vorschau-Fenster
|
||||
createVideoElement() {
|
||||
const video = document.createElement("video");
|
||||
video.autoplay = true;
|
||||
video.muted = true;
|
||||
video.playsInline = true;
|
||||
video.className = "w-full h-full object-cover rounded-xl";
|
||||
this.previewTarget.appendChild(video);
|
||||
return video;
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.stopCamera();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,10 @@
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="search-reader" data-scanner-target="preview" class="w-full aspect-square rounded-xl overflow-hidden border border-gray-200 bg-gray-50"></div>
|
||||
<!-- DAS VORSCHAUFENSTER FÜR DIE KAMERA IN DEINER _search_bar.html.erb -->
|
||||
<div data-scanner-target="preview" class="w-full aspect-square rounded-xl overflow-hidden border border-gray-200 bg-gray-50 flex items-center justify-center">
|
||||
<!-- Das <video>-Tag wird von Stimulus hier automatisch injiziert -->
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 text-center">Halte den QR-Code des Geräts ruhig in den Scan-Rahmen.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<title><%= content_for(:title) || "Vault171" %></title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="application-name" content="Vault17">
|
||||
<meta name="application-name" content="Vault171">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<%= csrf_meta_tags %>
|
||||
<%= csp_meta_tag %>
|
||||
@@ -75,9 +75,10 @@
|
||||
<svg class="h-5 w-5 shrink-0 ml-0.5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" /></svg>
|
||||
<span class="collapse-text ml-3 transition-all duration-300 ease-in-out max-w-[180px] opacity-100 overflow-hidden whitespace-nowrap">Wareneingang</span>
|
||||
<% end %>
|
||||
|
||||
<%= link_to categories_path, class: nav_link_class("categories") do %>
|
||||
<svg class="h-5 w-5 shrink-0 ml-0.5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621 Clyde5.04 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" /></svg>
|
||||
<svg class="h-5 w-5 shrink-0 ml-0.5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12.75V12A2.25 2.25 0 0 1 4.5 9.75h15A2.25 2.25 0 0 1 21.75 12v.75m-19.5 0A2.25 2.25 0 0 0 2.25 15v2.25m0-4.5A2.25 2.25 0 0 1 4.5 12h15a2.25 2.25 0 0 1 2.25 2.25m-19.5 0v4.5A2.25 2.25 0 0 0 4.5 21h15a2.25 2.25 0 0 0 2.25-2.25V15M2.25 12V6a2.25 2.25 0 0 1 2.25-2.25h2.25c.59 0 1.157.234 1.576.652L9.75 5.85a2.25 2.25 0 0 0 1.576.652h6.924a2.25 2.25 0 0 1 2.25 2.25v3.25" />
|
||||
</svg>
|
||||
<span class="collapse-text ml-3 transition-all duration-300 ease-in-out max-w-[180px] opacity-100 overflow-hidden whitespace-nowrap">Kategorien</span>
|
||||
<% end %>
|
||||
</nav>
|
||||
@@ -110,11 +111,11 @@
|
||||
|
||||
</aside>
|
||||
|
||||
<!-- HAUPTBEREICH (Inhaltsfläche rückt bei verkleinerter Sidebar nach) -->
|
||||
<div class="main-content flex-1 flex flex-col min-h-screen w-full transition-all duration-300 ease-in-out pl-0 md:pl-64">
|
||||
<!-- HAUPTBEREICH (Fixiert die Gesamthöhe auf den Bildschirm und steuert das Scrollen intern) -->
|
||||
<div class="main-content flex-1 flex flex-col h-screen w-full transition-all duration-300 ease-in-out pl-0 md:pl-64 overflow-hidden">
|
||||
|
||||
<!-- OBERE LEISTE (Top Bar) -->
|
||||
<header class="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-4 md:px-6 sticky top-0 z-30">
|
||||
<!-- OBERE LEISTE (Bleibt durch 'sticky' oben, da der umschließende Container nicht mehr mitwächst) -->
|
||||
<header class="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-4 md:px-6 sticky top-0 z-30 shrink-0">
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- BURGER BUTTON (Nur mobil sichtbar) -->
|
||||
<label for="mobile-sidebar-toggle" class="md:hidden p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg transition cursor-pointer">
|
||||
@@ -127,21 +128,13 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- INHALT DER JEWEILIGEN VIEW -->
|
||||
<main class="p-4 md:p-6 flex-1 w-full mx-auto px-4 md:px-8">
|
||||
<!-- Platzhalter für Flash-Meldungen (z.B. erfolgreiches Speichern)
|
||||
<% flash.each do |type, message| %>
|
||||
<div class="mb-4 p-4 text-sm rounded-lg border <%= type == 'notice' ? 'bg-green-50 text-green-800 border-green-200' : 'bg-red-50 text-red-800 border-red-200' %>">
|
||||
<%= message %>
|
||||
</div>
|
||||
<% end %>
|
||||
-->
|
||||
<%= render "layouts/flash" %>
|
||||
<%= render "layouts/flash" %>
|
||||
|
||||
<!-- INHALTSFLÄCHE (Nur dieser Bereich scrollt jetzt flexibel nach unten weg!) -->
|
||||
<main class="p-4 md:p-6 flex-1 w-full mx-auto px-4 md:px-8 overflow-y-auto bg-gray-50/30">
|
||||
<%= yield %>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Globaler Platzhalter für das optionale Turbo-Frame-Modal (z.B. neue Personen anlegen) -->
|
||||
<%= turbo_frame_tag "modal" %>
|
||||
|
||||
@@ -5,3 +5,4 @@ pin "@hotwired/turbo-rails", to: "turbo.min.js"
|
||||
pin "@hotwired/stimulus", to: "stimulus.min.js"
|
||||
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
|
||||
pin_all_from "app/javascript/controllers", under: "controllers"
|
||||
pin "qr-scanner" # @1.4.2
|
||||
|
||||
4
vendor/javascript/qr-scanner.js
vendored
Normal file
4
vendor/javascript/qr-scanner.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user