MediaWiki:Gadget-LabelScan.js
Hinweis: Leere nach dem Veröffentlichen den Browser-Cache, um die Änderungen sehen zu können.
- Firefox/Safari: Umschalttaste drücken und gleichzeitig Aktualisieren anklicken oder entweder Strg+F5 oder Strg+R (⌘+R auf dem Mac) drücken
- Google Chrome: Umschalttaste+Strg+R (⌘+Umschalttaste+R auf dem Mac) drücken
- Edge: Strg+F5 drücken oder Strg drücken und gleichzeitig Aktualisieren anklicken
// --- Index laden (robust, mit Fallback, ohne Cache) ------------------------
async function loadLabelIndex() {
const page = 'MediaWiki:Gadget-LabelScan-index.json';
// Primär: canonical raw-URL
const url1 = mw.util.getUrl(page, {
action: 'raw',
ctype: 'application/json',
maxage: 0,
smaxage: 0
});
// Fallback: wgScript (index.php) + Cachebuster
const url2 = (mw.config.get('wgScript') || '/index.php') +
'?title=' + encodeURIComponent(page) +
'&action=raw&ctype=application/json&_=' + Date.now();
const tried = [];
async function tryFetch(url) {
tried.push(url);
const r = await fetch(url, { cache: 'no-store' });
if (!r.ok) throw new Error('HTTP ' + r.status);
let text = await r.text();
// BOM entfernen, falls vorhanden
if (text.charCodeAt(0) === 0xFEFF) text = text.slice(1);
let data;
try { data = JSON.parse(text); }
catch (e) {
// Manchmal kommt <pre>…</pre> o.Ä. – hart strippen:
const stripped = text.replace(/^.*?\[/s, '[').replace(/\].*$/s, ']');
data = JSON.parse(stripped);
}
if (!Array.isArray(data)) throw new Error('Kein Array im Index');
return data;
}
try {
try {
return await tryFetch(url1);
} catch (e1) {
console.warn('[LabelScan] Primäre Raw-URL fehlgeschlagen:', e1?.message);
return await tryFetch(url2);
}
} catch (e) {
console.error('[LabelScan] Konnte Index nicht laden:', e?.message, '\nVersucht:', tried);
throw e;
}
}
// Beim Start laden:
loadLabelIndex().then(list => {
window.ADOS_LABEL_INDEX = list;
console.log(`[LabelScan] Index geladen: ${list.length} Einträge`);
}).catch(() => {
const box = document.getElementById('ados-scan-results');
if (box) box.innerHTML =
'<div class="ados-hit">Index konnte nicht geladen werden. Bitte Seite neu laden (Strg+F5) oder Administrator informieren.</div>';
});
/* global mw */
(function () {
'use strict';
// =========== UI Helpers ===========
function $(id){ return document.getElementById(id); }
function setStatus(t){ const el=$('ados-scan-status'); if(el) el.textContent=t||''; }
function setProgress(p){ const bar=$('ados-scan-progress'); if(!bar) return; if(p==null){bar.hidden=true;bar.value=0;} else {bar.hidden=false;bar.value=p;} }
function showPreview(file){
const prev=$('ados-scan-preview'); if(!prev) return;
const url=URL.createObjectURL(file);
prev.innerHTML = `<img src="${url}" alt="Vorschau" style="max-width:100%; border-radius:8px; border:1px solid #ccc;">`;
setStatus('Bild bereit.');
}
function esc(s){ return mw.html.escape(String(s||'')); }
// =========== Datei-Auswahl Zustand ===========
let PICKED_FILE = null;
function getSelectedFile() {
const cam=$('ados-scan-file-camera'), gal=$('ados-scan-file-gallery');
return PICKED_FILE || (cam&&cam.files&&cam.files[0]) || (gal&&gal.files&&gal.files[0]) || null;
}
// =========== aHash (64-bit, 16 Hex-Zeichen) ===========
async function fileToImage (file) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = URL.createObjectURL(file);
});
}
function computeAHash(img) {
// auf 8x8 skalieren, grau, Durchschnitt -> 64 bits
const S=8;
const c=document.createElement('canvas'); c.width=c.height=S;
const ctx=c.getContext('2d');
ctx.drawImage(img, 0, 0, S, S);
const data = ctx.getImageData(0,0,S,S).data;
const gray=new Array(S*S);
for(let i=0, j=0;i<data.length;i+=4, j++){
gray[j]= 0.299*data[i]+0.587*data[i+1]+0.114*data[i+2];
}
const avg = gray.reduce((a,b)=>a+b,0)/gray.length;
let bits = '';
for(let k=0;k<gray.length;k++){
bits += (gray[k] > avg) ? '1' : '0';
}
// 64 bits -> 16 Hex
let hex='';
for(let i=0;i<64;i+=4){
hex += parseInt(bits.slice(i,i+4),2).toString(16);
}
return hex;
}
function hammingHex(h1, h2){
const n = Math.min(h1.length, h2.length);
let d=0;
for(let i=0;i<n;i++){
const x = parseInt(h1[i],16) ^ parseInt(h2[i],16);
d += x.toString(2).replace(/0/g,'').length;
}
return d + (h1.length>n? (h1.length-n)*4 : 0) + (h2.length>n? (h2.length-n)*4 : 0);
}
// =========== Index laden ===========
async function loadIndex(){
const url = mw.util.getUrl('MediaWiki:Gadget-LabelScan-index.json', { action:'raw', ctype:'application/json' });
const res = await fetch(url, { cache: 'no-store' });
if (!res.ok) throw new Error('Index konnte nicht geladen werden: '+res.status);
return res.json();
}
// =========== Ergebnisse rendern ===========
function renderResults(items){
const box = $('ados-scan-results');
if (!box) return;
box.innerHTML = '';
if (!items || !items.length){
box.innerHTML = '<div class="empty">Keine klaren Treffer. Bitte anderes Foto oder manuell suchen.</div>';
return;
}
items.forEach(it=>{
const href = mw.util.getUrl(it.title.replace(/ /g,'_'));
box.insertAdjacentHTML('beforeend', `
<div class="ados-hit" style="display:inline-block; width:170px; margin:8px; text-align:center;">
<a href="${href}">
<img src="${it.thumb}" alt="${esc(it.title)}" style="width:150px; border-radius:8px; border:1px solid #ccc; background:#fff;">
<div style="margin-top:6px; font-weight:600;">${esc(it.title)}</div>
</a>
<div style="font-size:.85em; color:#666;">Distanz: ${it.dist}</div>
</div>
`);
});
}
// =========== Verkettete Erkennung ===========
async function runMatchWorkflow(){
const file = getSelectedFile();
if (!file) { alert('Bitte Foto aufnehmen oder Datei wählen.'); return; }
try {
setStatus('Bereite Bild vor …'); setProgress(null); // (keine echte Progressanzeige nötig)
const img = await fileToImage(file);
setStatus('Berechne Fingerabdruck …');
const ahash = computeAHash(img);
setStatus('Lade Index …');
const index = await loadIndex();
setStatus('Vergleiche …');
const scored = index.map(it => {
const dist = hammingHex(ahash, String(it.phash||''));
return { ...it, dist };
}).sort((a,b)=>a.dist - b.dist);
// kleine Heuristik: nur „plausible“ Treffer anzeigen
const BEST = scored.slice(0, 8);
const THRESH = 18; // ~ gut unterscheidbar bei 64-bit aHash
const filtered = BEST.filter(x => x.dist <= THRESH);
renderResults(filtered.length ? filtered : BEST);
setStatus('Fertig.');
} catch (e) {
console.error('[LabelScan]', e);
setStatus('Fehler bei Erkennung.');
} finally {
setProgress(null);
}
}
// =========== Dropzone, Buttons, Aktionen ===========
function wireUI(){
// Buttons → Inputs
const btnCam=$('ados-scan-btn-camera'), inCam=$('ados-scan-file-camera');
const btnGal=$('ados-scan-btn-gallery'), inGal=$('ados-scan-file-gallery');
if (btnCam && inCam){
btnCam.addEventListener('click', e=>{ e.preventDefault(); e.stopPropagation(); inCam.click(); });
inCam.addEventListener('change', ()=>{ if(inCam.files && inCam.files[0]) { PICKED_FILE=inCam.files[0]; showPreview(PICKED_FILE); }});
}
if (btnGal && inGal){
btnGal.addEventListener('click', e=>{ e.preventDefault(); e.stopPropagation(); inGal.click(); });
inGal.addEventListener('change', ()=>{ if(inGal.files && inGal.files[0]) { PICKED_FILE=inGal.files[0]; showPreview(PICKED_FILE); }});
}
// Dropzone
const drop = $('ados-scan-drop');
if (drop){
const stop = ev => { ev.preventDefault(); ev.stopPropagation(); };
['dragenter','dragover','dragleave','drop'].forEach(evt => drop.addEventListener(evt, stop));
drop.addEventListener('drop', ev=>{
const f = ev.dataTransfer && ev.dataTransfer.files && ev.dataTransfer.files[0];
if (f){ PICKED_FILE=f; showPreview(f); }
});
}
// Run
const run=$('ados-scan-run');
if (run) run.addEventListener('click', e=>{ e.preventDefault(); runMatchWorkflow(); });
// Reset
const reset=$('ados-scan-reset');
if (reset) reset.addEventListener('click', ()=>{
PICKED_FILE=null;
if ($('ados-scan-file-camera')) $('ados-scan-file-camera').value='';
if ($('ados-scan-file-gallery')) $('ados-scan-file-gallery').value='';
if ($('ados-scan-preview')) $('ados-scan-preview').innerHTML='<div class="note">Noch keine Vorschau. Wähle ein Foto.</div>';
if ($('ados-scan-results')) $('ados-scan-results').innerHTML='<div class="empty">Hier erscheinen passende Abfüllungen mit Link ins Wiki.</div>';
setStatus('Bereit.');
setProgress(null);
});
}
// =========== Start ===========
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', wireUI);
} else {
wireUI();
}
})();