|
|
| (38 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt) |
| Zeile 1: |
Zeile 1: |
| <html> | | <html> |
| <div id="ados-labelscan" class="ados-scan"> | | <div id="ados-labelscan"> |
| <h2>📸 Abfüllung scannen</h2>
| |
| <p>Foto der Front-Labelseite aufnehmen oder wählen. Die Erkennung läuft vollständig lokal im Browser.</p>
| |
|
| |
|
| <div class="ados-scan__uploader" id="ados-scan-drop"> | | <!-- Kopf --> |
| <button id="ados-scan-bigbtn" type="button" class="ados-scan__btn">
| | <div class="card" style="margin-bottom:10px;"> |
| <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 2l1.5 2H14l1.5-2H19a2 2 0 012 2v14a2 2 0 01-2 2H5a2 2 0 01-2-2V4a2 2 0 012-2h4zM12 7a5 5 0 100 10 5 5 0 000-10zm0 2.2A2.8 2.8 0 1112 15a2.8 2.8 0 010-5.6z"/></svg>
| | <h2>📸 Abfüllung scannen</h2> |
| <span>Foto aufnehmen / wählen</span>
| | <p class="sub">Foto des Frontlabels aufnehmen oder auswählen. Der Abgleich erfolgt lokal im Browser – keine Uploads.</p> |
| <small>Oder Bild hierher ziehen</small>
| |
| </button> | |
| <!-- verstecktes echtes File-Input -->
| |
| <input id="ados-scan-file" type="file" accept="image/*" capture="environment" style="display:none">
| |
| </div> | | </div> |
|
| |
|
| <div class="ados-scan__progress" aria-live="polite"> | | <div class="scan-grid"> |
| <div class="ados-scan__status" id="ados-scan-status">Bereit.</div>
| |
| <progress id="ados-scan-progress" max="1" value="0" style="display:none"></progress>
| |
| </div>
| |
|
| |
|
| <div id="ados-scan-preview" class="ados-scan__preview" aria-hidden="true"></div>
| | <!-- Linke Spalte --> |
| | <div class="col"> |
|
| |
|
| <div class="ados-scan__actions">
| | <div class="card"> |
| <button id="ados-scan-run" type="button" class="ados-scan__run">Erkennen & suchen</button>
| |
| </div>
| |
|
| |
|
| <h3>Vorschläge</h3>
| | <!-- Dropzone (nur für Drag & Drop) --> |
| <div id="ados-scan-results" class="ados-scan__results"></div>
| | <label id="ados-scan-drop" class="ados-drop" aria-label="Bild hier ablegen"> |
| </div> | | <div class="icon">🖼️</div> |
| </html> | | <div><b>Bild hierher ziehen</b> oder unten auswählen</div> |
| {{#tag:html|
| | <div class="help">Tipp: frontal, gute Beleuchtung, kein Blitz-Reflex.</div> |
| <script> | | </label> |
| (function(){
| |
| document.addEventListener('DOMContentLoaded',function(){
| |
|
| |
|
| var run = document.getElementById('ados-scan-run');
| | <!-- getrennte Buttons + Inputs --> |
| var file = document.getElementById('ados-scan-file');
| | <div class="ados-scan__pick" style="margin-top:10px; display:flex; gap:6px; justify-content:center;"> |
| var big = document.getElementById('ados-scan-bigbtn');
| | <button id="ados-scan-btn-camera" type="button" class="btn">📷 Foto aufnehmen</button> |
| var prev = document.getElementById('ados-scan-preview');
| | <button id="ados-scan-btn-gallery" type="button" class="btn">🖼️ Bild wählen</button> |
| var stat = document.getElementById('ados-scan-status');
| | </div> |
| var prog = document.getElementById('ados-scan-progress');
| |
|
| |
|
| if(!run || !file){ console.warn('[LabelScan] UI-Elemente fehlen'); return; }
| | <input id="ados-scan-file-camera" type="file" accept="image/*" capture="environment" hidden> |
| | <input id="ados-scan-file-gallery" type="file" accept="image/*" hidden> |
|
| |
|
| function setStatus(t){ stat.textContent = t || ''; }
| | <!-- Status + Progress --> |
| function setProgress(p){
| | <div class="statusbar" style="margin-top:8px;"> |
| if(p==null){ prog.style.display='none'; prog.value=0; return; }
| | <div id="ados-scan-status">Bereit.</div> |
| prog.style.display=''; prog.value=Math.max(0,Math.min(1,p));
| | <progress id="ados-scan-progress" max="1" value="0" hidden></progress> |
| }
| | </div> |
| function showPreview(f){
| |
| var url = URL.createObjectURL(f);
| |
| prev.innerHTML = '<img alt="Vorschau" style="max-width:100%;border-radius:10px;box-shadow:0 2px 10px rgba(0,0,0,.06)" src="'+url+'">';
| |
| prev.setAttribute('aria-hidden','false');
| |
| }
| |
|
| |
|
| // großer Button öffnet echte Dateiauswahl
| | <!-- Aktionen --> |
| if (big) big.addEventListener('click', function(){ file.click(); });
| | <div class="action-row" style="margin-top:8px; display:flex; gap:8px; justify-content:center;"> |
| // Vorschau bei Auswahl
| | <button id="ados-scan-run" type="button" class="btn btn-primary">🔍 Erkennen & suchen</button> |
| file.addEventListener('change', function(){ if (this.files && this.files[0]) showPreview(this.files[0]); });
| | <button id="ados-scan-reset" type="button" class="btn btn-ghost">Zurücksetzen</button> |
| | </div> |
|
| |
|
| // Drag&Drop (optional, falls du #ados-scan-drop nutzt)
| | </div> |
| var drop = document.getElementById('ados-scan-drop');
| |
| if (drop){
| |
| drop.addEventListener('dragover', function(e){ e.preventDefault(); drop.classList.add('dragover'); });
| |
| drop.addEventListener('dragleave', function(){ drop.classList.remove('dragover'); });
| |
| drop.addEventListener('drop', function(e){
| |
| e.preventDefault(); drop.classList.remove('dragover');
| |
| if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length){
| |
| file.files = e.dataTransfer.files;
| |
| showPreview(file.files[0]);
| |
| }
| |
| });
| |
| }
| |
|
| |
|
| // Tesseract lazy-load mit Fallback-CDN
| | <!-- Vorschau --> |
| function loadTesseract(){
| | <div id="ados-scan-preview" class="card preview" aria-live="polite" aria-atomic="true"> |
| return new Promise(function(resolve,reject){ | | <div class="note">Noch keine Vorschau. Wähle ein Foto.</div> |
| if (window.Tesseract) return resolve();
| | </div> |
| var s=document.createElement('script');
| |
| s.src='https://cdn.jsdelivr.net/npm/tesseract.js@5/dist/tesseract.min.js';
| |
| s.async=true;
| |
| s.onload=resolve;
| |
| s.onerror=function(){ | |
| var s2=document.createElement('script');
| |
| s2.src='https://unpkg.com/tesseract.js@5/dist/tesseract.min.js';
| |
| s2.async=true;
| |
| s2.onload=resolve;
| |
| s2.onerror=function(){ reject(new Error('Tesseract konnte nicht geladen werden')); };
| |
| document.head.appendChild(s2);
| |
| };
| |
| document.head.appendChild(s);
| |
| }); | |
| }
| |
|
| |
|
| // Heuristik -> Query
| | <div class="note" style="margin-top:6px;"> |
| function extractHints(text){
| | 🔒 Deine Fotos bleiben auf deinem Gerät. Es wird nichts auf den Server hochgeladen. |
| var raw = (text||'').replace(/\s+/g,' ').trim(); | | </div> |
| var words = (raw.match(/\b[A-ZÄÖÜ][A-Za-zÄÖÜäöüß\-]{3,}\b/g) || []).slice(0,5);
| |
| var ages = (raw.match(/\b([1-9]\d?)\s?(?:years?|yo|jahr|jahre)\b/gi) || []).map(function(s){ var m=s.match(/[1-9]\d?/); return m?m[0]:null; }).filter(Boolean);
| |
| var years = (raw.match(/\b(19|20)\d{2}\b/g) || []);
| |
| return {words:words, ages:ages, years:years};
| |
| }
| |
| function buildSearchQuery(h){
| |
| var parts=[];
| |
| h.words.forEach(function(w){ parts.push('"' + w + '"'); });
| |
| h.ages.forEach(function(a){ parts.push('"' + a + '"'); });
| |
| h.years.forEach(function(y){ parts.push('"' + y + '"'); }); | |
| if (!parts.length) parts.push('Whisky');
| |
| return parts.join(' ');
| |
| }
| |
|
| |
|
| function renderResults(items){ | | </div> |
| var wrap = document.getElementById('ados-scan-results');
| |
| wrap.innerHTML = '';
| |
| if (!items.length){
| |
| wrap.innerHTML = '<div class="ados-hit">Keine klaren Treffer. Bitte anderes Foto oder manuell suchen.</div>';
| |
| return;
| |
| }
| |
| items.slice(0,8).forEach(function(h){
| |
| var title = h.title || h.Seitentitel || '';
| |
| var link = mw.util.getUrl( title.replace(/ /g,'_') );
| |
| var snip = (h.snippet||'').replace(/<\/?span[^>]*>/g,'').replace(/"/g,'"');
| |
| var div = document.createElement('div');
| |
| div.className = 'ados-hit';
| |
| div.style.border='1px solid #eee'; div.style.borderRadius='10px'; div.style.padding='10px'; div.style.background='#fafafa';
| |
| div.innerHTML = '<b><a href="'+link+'">'+mw.html.escape(title)+'</a></b>' + (snip ? '<div class="meta" style="color:#666">'+snip+'</div>' : '');
| |
| wrap.appendChild(div);
| |
| });
| |
| }
| |
|
| |
|
| // Klick-Handler: Erkennen & suchen | | <!-- Rechte Spalte --> |
| run.addEventListener('click', function(ev){ | | <div class="col"> |
| ev.preventDefault(); | | <div class="card"> |
| if (!(file.files && file.files[0])){ alert('Bitte ein Foto auswählen oder aufnehmen.'); return; }
| | <h3 style="margin:0 0 .4rem;">🔎 Ergebnisse</h3> |
| var f = file.files[0];
| | <div id="ados-scan-results" class="results"> |
| | | <div class="empty">Hier erscheinen passende Abfüllungen mit Link ins Wiki.</div> |
| (async function(){
| | </div> |
| try{ | | </div> |
| run.disabled=true; run.textContent='Erkenne …';
| | <div class="card"> |
| setStatus('Erkenne Label …'); | | <h3 style="margin:0 0 .4rem;">Tipps</h3> |
| await loadTesseract();
| | <ul style="margin:.25rem 0 .2rem 1.1rem;"> |
| setProgress(0);
| | <li>Frontlabel direkt fotografieren</li> |
| | | <li>Gute Beleuchtung</li> |
| var result = await Tesseract.recognize(f, 'deu+eng', {
| | <li>Label möglichst das ganze Bild füllen</li> |
| logger: function(m){
| | <li>Bei Spiegelungen: leicht schräg, aber nah</li> |
| if (m && m.status === 'recognizing text' && typeof m.progress === 'number') setProgress(m.progress);
| | <li>keine ähnlichen Flaschen im Hintergrund die im Fokus sind</li> |
| }
| | </ul> |
| });
| | </div> |
| | | </div> |
| setProgress(null);
| | <button id="ados-install" class="mw-ui-button" style="display:none">📲 LabelScan installieren</button> |
| setStatus('Suche im Wiki …');
| | </div> |
| | | </div> |
| var text = (result && result.data && result.data.text) || '';
| | </html> |
| var hints = extractHints(text);
| |
| var query = buildSearchQuery(hints);
| |
| | |
| mw.loader.using('mediawiki.api').then(async function(){
| |
| var api = new mw.Api();
| |
| var r = await api.get({ action:'query', list:'search', srsearch:query, srlimit:10, formatversion:2 });
| |
| var hits = (r.query && r.query.search) || [];
| |
| renderResults(hits);
| |
| setStatus('Fertig.');
| |
| run.disabled=false; run.textContent='Erkennen & suchen';
| |
| }); | |
| | |
| } catch(e){
| |
| console.error('[LabelScan] Fehler:', e); | |
| setProgress(null); | |
| setStatus('Fehler bei Erkennung/Suche. Bitte erneut versuchen.');
| |
| run.disabled=false; run.textContent='Erkennen & suchen';
| |
| } | |
| })(); | |
| }); | |
| | |
| console.log('LabelScan inline bound');
| |
| }); | |
| })();
| |
| </script> | |
| }}
| |