MediaWiki:Common.js: Unterschied zwischen den Versionen

Keine Bearbeitungszusammenfassung
Keine Bearbeitungszusammenfassung
Zeile 1.245: Zeile 1.245:


// ==========================Scan==================================
// ==========================Scan==================================
// === LabelScan – Foto->OCR->Suche (UI+Progress) =============================
mw.loader.using(['mediawiki.api', 'mediawiki.util']).then(function(){
  mw.hook('wikipage.content').add(function($content){
    if (mw.config.get('wgPageName') !== 'LabelScan') return;
    var $btnBig = $content.find('#ados-scan-bigbtn');
    var $file  = $content.find('#ados-scan-file');
    var $run    = $content.find('#ados-scan-run');
    var $drop  = $content.find('#ados-scan-drop');
    var $prev  = $content.find('#ados-scan-preview');
    var $stat  = $content.find('#ados-scan-status');
    var $prog  = $content.find('#ados-scan-progress');
    var $res    = $content.find('#ados-scan-results');
    if (!$btnBig.length || !$file.length || !$run.length) return;
    if ($run.data('bound')) return; // nicht doppelt binden
    $run.data('bound', 1);
    function setStatus(t){ $stat.text(t||''); }
    function setProgress(p){
      if (p==null){ $prog.hide().val(0); return; }
      $prog.show().val(Math.max(0, Math.min(1, p)));
    }
    function showPreview(file){
      var url = URL.createObjectURL(file);
      $prev.html('<img alt="Vorschau" src="'+url+'">').attr('aria-hidden','false');
    }
    // Datei wählen
    $btnBig.on('click', function(){ $file.trigger('click'); });
    // Drag&Drop
    $drop.on('dragover', function(e){ e.preventDefault(); $drop.addClass('dragover'); });
    $drop.on('dragleave', function(e){ $drop.removeClass('dragover'); });
    $drop.on('drop', function(e){
      e.preventDefault(); $drop.removeClass('dragover');
      if (e.originalEvent && e.originalEvent.dataTransfer && e.originalEvent.dataTransfer.files.length){
        $file[0].files = e.originalEvent.dataTransfer.files;
        showPreview($file[0].files[0]);
      }
    });
    // Vorschau bei Auswahl
    $file.on('change', function(){ if (this.files && this.files[0]) showPreview(this.files[0]); });
    // Tesseract lazy-load
    function loadTesseract(){
      return new Promise(function(resolve, reject){
        if (window.Tesseract) return resolve();
        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(){
          // Fallback-CDN
          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
    function extractHints(text){
      const raw = (text||'').replace(/\s+/g,' ').trim();
      const words = Array.from(new Set(raw.match(/\b[A-ZÄÖÜ][A-Za-zÄÖÜäöüß\-]{3,}\b/g) || []));
      const ages  = Array.from(new Set(raw.match(/\b([1-9]\d?)\s?(?:years?|yo|jahr|jahre)\b/gi) || []))
                      .map(s => (s.match(/[1-9]\d?/)||[])[0]);
      const years = Array.from(new Set(raw.match(/\b(19|20)\d{2}\b/g) || []));
      return {words, ages, years};
    }
    function buildSearchQuery(h){
      const parts = [];
      (h.words||[]).slice(0,5).forEach(w => parts.push('"'+w+'"'));
      (h.ages||[]).forEach(a => parts.push('"'+a+'"'));
      (h.years||[]).forEach(y => parts.push('"'+y+'"'));
      if (!parts.length) parts.push('Whisky');
      return parts.join(' ');
    }
    function renderResults(items){
      $res.empty();
      if (!items.length){
        $res.html('<div class="ados-hit">Keine klaren Treffer. Bitte anderes Foto oder manuell suchen.</div>');
        return;
      }
      items.slice(0,8).forEach(function(it){
        var title = it.title || it.Seitentitel || '';
        var link  = mw.util.getUrl( title.replace(/ /g,'_') );
        var snip  = (it.snippet||'').replace(/<\/?span[^>]*>/g,'').replace(/&quot;/g,'"');
        $res.append(
          '<div class="ados-hit">'
          + '<b><a href="'+link+'">'+mw.html.escape(title)+'</a></b>'
          + (snip ? '<div class="meta" style="color:#666">'+snip+'</div>' : '')
          + '</div>'
        );
      });
    }
    async function runOCR(file){
      await loadTesseract();
      setProgress(0);
      const result = await Tesseract.recognize(file, 'deu+eng', {
        logger: function(m){
          if (m && m.status === 'recognizing text' && typeof m.progress === 'number'){
            setProgress(m.progress);
          }
        }
      });
      setProgress(null);
      return (result && result.data && result.data.text) || '';
    }
    async function searchWiki(query, limit){
      const api = new mw.Api();
      const res = await api.get({
        action:'query', list:'search', srsearch:query,
        srlimit:limit||10, srwhat:'text', formatversion:2
      });
      return (res.query && res.query.search) || [];
    }
    // Start
    $run.on('click', async function(ev){
      ev.preventDefault();
      if (!($file[0].files && $file[0].files[0])){ alert('Bitte ein Foto auswählen oder aufnehmen.'); return; }
      const file = $file[0].files[0];
      try{
        $run.prop('disabled', true).text('Erkenne …');
        setStatus('Erkenne Label …');
        const text = await runOCR(file);
        setStatus('Suche im Wiki …');
        const hints = extractHints(text);
        const query = buildSearchQuery(hints);
        const hits  = await searchWiki(query, 12);
        renderResults(hits);
        setStatus('Fertig.');
      } catch(e){
        console.error('[LabelScan] Fehler:', e);
        setStatus('Fehler bei Erkennung/Suche. Bitte erneut versuchen.');
      } finally {
        $run.prop('disabled', false).text('Erkennen & suchen');
      }
    });
    console.log('[LabelScan] UI gebunden.');
  });
});