|
|
| (48 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt) |
| Zeile 396: |
Zeile 396: |
|
| |
|
| }); | | }); |
|
| |
|
| |
|
| |
|
| |
|
| |
|
|
| |
|
| Zeile 458: |
Zeile 454: |
|
| |
|
| /* ========== Kategorien rekursiv einsammeln (inkl. Subkats, Namespaces) ========== */ | | /* ========== Kategorien rekursiv einsammeln (inkl. Subkats, Namespaces) ========== */ |
| // NIMMT nsStr (z. B. "*", "0|102|14"). Bei "*" wird cmnamespace NICHT gesetzt.
| |
| function fetchCategoryMembersRecursiveSingleResolved(api, catTitle, limit, outSet, pages, nsStr){ | | function fetchCategoryMembersRecursiveSingleResolved(api, catTitle, limit, outSet, pages, nsStr){ |
| var visited = {}, queue = [catTitle]; | | var visited = {}, queue = [catTitle]; |
| Zeile 468: |
Zeile 463: |
| list: 'categorymembers', | | list: 'categorymembers', |
| cmtitle: title, | | cmtitle: title, |
| // cmnamespace nur setzen, wenn NICHT "*"
| |
| cmtype: 'page|subcat', | | cmtype: 'page|subcat', |
| cmlimit: Math.min(200, limit), | | cmlimit: Math.min(200, limit), |
| Zeile 577: |
Zeile 571: |
| /* ========== Render ========== */ | | /* ========== Render ========== */ |
| function renderTopN(container, rows, N, minVotes) { | | function renderTopN(container, rows, N, minVotes) { |
| // Statusbox parken, falls keep aktiv
| |
| var keep = (container.getAttribute && container.getAttribute('data-keep-status') === 'true'); | | var keep = (container.getAttribute && container.getAttribute('data-keep-status') === 'true'); |
| var statusBox = keep ? container.querySelector('.whisky-top5__status') : null; | | var statusBox = keep ? container.querySelector('.whisky-top5__status') : null; |
| Zeile 637: |
Zeile 630: |
| container.setAttribute('data-top5-init','1'); | | container.setAttribute('data-top5-init','1'); |
|
| |
|
| // Kategorien (Zeilenumbruch ODER Semikolon getrennt)
| |
| var rawCats = container.getAttribute('data-categories') || container.getAttribute('data-category') || ''; | | var rawCats = container.getAttribute('data-categories') || container.getAttribute('data-category') || ''; |
| var parts = rawCats.split(/\n|;/), rootCats = [], i; | | var parts = rawCats.split(/\n|;/), rootCats = [], i; |
| Zeile 647: |
Zeile 639: |
| var minVotes = parseInt(container.getAttribute('data-min-votes') || '1', 10); | | var minVotes = parseInt(container.getAttribute('data-min-votes') || '1', 10); |
| var includeHidden = (container.getAttribute('data-include-hidden') === 'true'); | | var includeHidden = (container.getAttribute('data-include-hidden') === 'true'); |
| var nsStr = container.getAttribute('data-namespaces') || '0|14'; // z. B. "*", "0|102|14" | | var nsStr = container.getAttribute('data-namespaces') || '0|14'; |
|
| |
|
| var rawC = container.getAttribute('data-contests') || 'NASE,GESCHMACK,ABGANG,GESAMTEINDRUCK'; | | var rawC = container.getAttribute('data-contests') || 'NASE,GESCHMACK,ABGANG,GESAMTEINDRUCK'; |
| Zeile 658: |
Zeile 650: |
| status('Sammle Seiten …'); | | status('Sammle Seiten …'); |
|
| |
|
|
| |
| // 1) Artikel einsammeln
| |
| fetchCategoryMembersRecursiveMulti(rootCats, lim, status, nsStr).then(function(members){ | | fetchCategoryMembersRecursiveMulti(rootCats, lim, status, nsStr).then(function(members){ |
| status('Gefundene Seiten gesamt: ' + (members ? members.length : 0) + ' – lade Bewertungen …', true); | | status('Gefundene Seiten gesamt: ' + (members ? members.length : 0) + ' – lade Bewertungen …', true); |
| Zeile 703: |
Zeile 693: |
|
| |
|
| }); | | }); |
|
| |
|
| |
|
| |
|
|
| |
|
| Zeile 712: |
Zeile 700: |
| var el = this, val = parseFloat(el.getAttribute('data-rating') || '0'); | | var el = this, val = parseFloat(el.getAttribute('data-rating') || '0'); |
| if (isNaN(val)) val = 0; | | if (isNaN(val)) val = 0; |
| // clamp 0..5
| |
| val = Math.max(0, Math.min(5, val)); | | val = Math.max(0, Math.min(5, val)); |
| // set CSS variable for width percentage (0..5 -> 0..5 stars)
| |
| el.style.setProperty('--stars', (val).toString()); | | el.style.setProperty('--stars', (val).toString()); |
| el.setAttribute('aria-label', val + ' von 5 Sternen'); | | el.setAttribute('aria-label', val + ' von 5 Sternen'); |
| Zeile 721: |
Zeile 707: |
| }); | | }); |
|
| |
|
|
| |
| // --------------------------------
| |
|
| |
|
| // Force light color-scheme at document level (helps Mobile Safari) | | // Force light color-scheme at document level (helps Mobile Safari) |
| Zeile 736: |
Zeile 720: |
| } | | } |
| }); | | }); |
|
| |
| // -------------------------------------------------
| |
|
| |
|
|
| |
|
| Zeile 751: |
Zeile 733: |
| if (!chart || !block) return; | | if (!chart || !block) return; |
|
| |
|
| // Summe aller Werte berechnen
| |
| const total = chart.data.datasets.reduce((sum, ds) => | | const total = chart.data.datasets.reduce((sum, ds) => |
| sum + (ds.data || []).reduce((a, b) => a + (parseFloat(b) || 0), 0) | | sum + (ds.data || []).reduce((a, b) => a + (parseFloat(b) || 0), 0) |
| , 0); | | , 0); |
|
| |
|
| // Bestehende Anzeige entfernen (falls Neurender)
| |
| const oldInfo = block.querySelector(':scope > .chart-total-info'); | | const oldInfo = block.querySelector(':scope > .chart-total-info'); |
| if (oldInfo) oldInfo.remove(); | | if (oldInfo) oldInfo.remove(); |
|
| |
|
| // Neue Anzeige einfügen
| |
| const info = document.createElement('div'); | | const info = document.createElement('div'); |
| info.className = 'chart-total-info'; | | info.className = 'chart-total-info'; |
| Zeile 775: |
Zeile 754: |
| } | | } |
|
| |
|
|
| |
| // -------------------------------------
| |
|
| |
|
| /* === ADOS Multi-Serien-Chart (Chart.js) ============================= * | | /* === ADOS Multi-Serien-Chart (Chart.js) ============================= * |
| Zeile 784: |
Zeile 761: |
|
| |
|
| (function () { | | (function () { |
| // 1) Chart.js nur 1x laden und erst dann rendern
| |
| var _chartReady = null; | | var _chartReady = null; |
| function ensureChartJS() { | | function ensureChartJS() { |
| Zeile 791: |
Zeile 767: |
| if (window.Chart) return resolve(); | | if (window.Chart) return resolve(); |
| var s = document.createElement('script'); | | var s = document.createElement('script'); |
| s.src = 'https://cdn.jsdelivr.net/npm/chart.js'; // UMD-Bundle | | s.src = 'https://cdn.jsdelivr.net/npm/chart.js'; |
| s.async = true; | | s.async = true; |
| s.onload = function(){ resolve(); }; | | s.onload = function(){ resolve(); }; |
| Zeile 800: |
Zeile 776: |
| } | | } |
|
| |
|
| // 2) Farben je Serie (gut unterscheidbar)
| |
| var ADOS_COLORS = { | | var ADOS_COLORS = { |
| 'A Dream of Scotland': '#C2410C', // Kupferbraun | | 'A Dream of Scotland': '#C2410C', |
| 'A Dream of Ireland': '#15803D', // Flaschengrün | | 'A Dream of Ireland': '#15803D', |
| 'A Dream of... – Der Rest der Welt': '#1D4ED8', // Mittelblau | | 'A Dream of... – Der Rest der Welt': '#1D4ED8', |
| 'Friendly Mr. Z': '#9333EA', // Violett | | 'Friendly Mr. Z': '#9333EA', |
| 'Die Whisky Elfen': '#0891B2', // Türkis | | 'Die Whisky Elfen': '#0891B2', |
| 'The Fine Art of Whisky': '#CA8A04' // Goldgelb | | 'The Fine Art of Whisky': '#CA8A04' |
| }; | | }; |
| var COLOR_CYCLE = ['#2563eb','#16a34a','#f97316','#dc2626','#a855f7','#0ea5e9','#f59e0b','#10b981']; | | var COLOR_CYCLE = ['#2563eb','#16a34a','#f97316','#dc2626','#a855f7','#0ea5e9','#f59e0b','#10b981']; |
| Zeile 822: |
Zeile 797: |
| } | | } |
|
| |
|
| // 3) Tabelle (Jahr | Serie | Anzahl) -> {labels, datasets}
| |
| function buildDatasetsFromTable(tbl){ | | function buildDatasetsFromTable(tbl){ |
| var rows = Array.from(tbl.querySelectorAll('tr')); | | var rows = Array.from(tbl.querySelectorAll('tr')); |
| Zeile 828: |
Zeile 802: |
|
| |
|
| var yearsSet = new Set(); | | var yearsSet = new Set(); |
| var bySeries = new Map(); // serie -> Map(year -> count) | | var bySeries = new Map(); |
|
| |
|
| rows.slice(1).forEach(function(tr){ | | rows.slice(1).forEach(function(tr){ |
| Zeile 863: |
Zeile 837: |
| } | | } |
|
| |
|
| // 4) Einen Chart-Container rendern: nimmt die NÄCHSTE Tabelle als Datenquelle | | function renderOne(block){ |
| function renderOne(block){ | | if (block.dataset.rendered === '1') return; |
| // schon gerendert? (z. B. durch AJAX/Minerva-Reloads)
| |
| if (block.dataset.rendered === '1') return;
| |
|
| |
|
| // nächste Tabelle (auch wenn sie in einem Wrapper steckt) finden
| | var el = block.nextElementSibling, tbl = null, wrapToHide = null; |
| var el = block.nextElementSibling, tbl = null, wrapToHide = null;
| | while (el) { |
| while (el) {
| | if (/^H[1-6]$/.test(el.tagName) || (el.classList && el.classList.contains('ados-chart-multi'))) break; |
| if (/^H[1-6]$/.test(el.tagName) || (el.classList && el.classList.contains('ados-chart-multi'))) break;
| | if (el.tagName === 'TABLE') { |
| if (el.tagName === 'TABLE') {
| | tbl = el; |
| tbl = el;
| | } else if (el.querySelector) { |
| } else if (el.querySelector) {
| | var t = el.querySelector('table'); |
| var t = el.querySelector('table');
| | if (t) tbl = t; |
| if (t) tbl = t;
| | } |
| }
| | if (tbl) { |
| if (tbl) {
| | wrapToHide = tbl.parentElement; |
| // Wrapper merken – aber später nur verstecken, wenn er "quasi nur" die Tabelle enthält
| | break; |
| wrapToHide = tbl.parentElement;
| | } |
| break; | | el = el.nextElementSibling; |
| } | | } |
| el = el.nextElementSibling; | | if (!tbl) return; |
| }
| |
| if (!tbl) return;
| |
|
| |
|
| var out = buildDatasetsFromTable(tbl);
| | var out = buildDatasetsFromTable(tbl); |
| if (!out.labels.length || !out.datasets.length) return;
| | if (!out.labels.length || !out.datasets.length) return; |
|
| |
|
| // Tabelle verstecken, wenn gewünscht (nur die Tabelle – oder den Wrapper, falls er sonst leer ist)
| | var hide = (block.dataset.hideTable || '').toLowerCase() === 'true'; |
| var hide = (block.dataset.hideTable || '').toLowerCase() === 'true';
| | if (hide) { |
| if (hide) {
| | var onlyTable = wrapToHide && wrapToHide.children.length === 1 && wrapToHide.firstElementChild === tbl; |
| // Hat der Wrapper außer der Tabelle noch sichtbaren Inhalt?
| | if (onlyTable) { |
| var onlyTable = wrapToHide && wrapToHide.children.length === 1 && wrapToHide.firstElementChild === tbl;
| | wrapToHide.setAttribute('aria-hidden','true'); |
| if (onlyTable) {
| | wrapToHide.style.display = 'none'; |
| wrapToHide.setAttribute('aria-hidden','true');
| | } else { |
| wrapToHide.style.display = 'none';
| | tbl.setAttribute('aria-hidden','true'); |
| } else {
| | tbl.style.display = 'none'; |
| tbl.setAttribute('aria-hidden','true');
| | } |
| tbl.style.display = 'none';
| |
| } | | } |
| }
| |
|
| |
|
| // Zeichenfläche einsetzen (mobil/desktop)
| | var wrap = document.createElement('div'); |
| var wrap = document.createElement('div');
| | wrap.style.position = 'relative'; |
| wrap.style.position = 'relative';
| | wrap.style.width = '100%'; |
| wrap.style.width = '100%';
| | wrap.style.height = block.dataset.height || (window.matchMedia('(min-width:768px)').matches ? '450px' : '300px'); |
| wrap.style.height = block.dataset.height || (window.matchMedia('(min-width:768px)').matches ? '450px' : '300px');
| | var canvas = document.createElement('canvas'); |
| var canvas = document.createElement('canvas');
| | wrap.appendChild(canvas); |
| wrap.appendChild(canvas);
| | block.innerHTML = ''; |
| block.innerHTML = '';
| | block.appendChild(wrap); |
| block.appendChild(wrap);
| |
|
| |
|
| var type = (block.dataset.type || 'line').toLowerCase();
| | var type = (block.dataset.type || 'line').toLowerCase(); |
| var title = block.dataset.title || '';
| | var title = block.dataset.title || ''; |
| var cumulative = (block.dataset.cumulative || '').toLowerCase() === 'true';
| | var cumulative = (block.dataset.cumulative || '').toLowerCase() === 'true'; |
|
| |
|
| // optional: kumulative Werte pro Serie bauen
| | if (cumulative) { |
| if (cumulative) {
| | out.datasets = out.datasets.map(function(ds){ |
| out.datasets = out.datasets.map(function(ds){
| | var acc = 0; |
| var acc = 0;
| | return Object.assign({}, ds, { |
| return Object.assign({}, ds, {
| | data: ds.data.map(function(v){ acc += v; return acc; }) |
| data: ds.data.map(function(v){ acc += v; return acc; })
| | }); |
| }); | | }); |
| }); | | } |
| }
| |
|
| |
|
| ensureChartJS().then(function(){ | | ensureChartJS().then(function(){ |
| const chart = new Chart(canvas.getContext('2d'), {
| | const chart = new Chart(canvas.getContext('2d'), { |
| type: type,
| | type: type, |
| data: { labels: out.labels, datasets: out.datasets },
| | data: { labels: out.labels, datasets: out.datasets }, |
| options: {
| | options: { |
| responsive: true,
| | responsive: true, |
| maintainAspectRatio: false,
| | maintainAspectRatio: false, |
| interaction: { mode: 'nearest', intersect: false },
| | interaction: { mode: 'nearest', intersect: false }, |
| plugins: {
| | plugins: { |
| legend: { position: 'bottom', labels: { font: { size: 13 }, boxWidth: 20 } },
| | legend: { position: 'bottom', labels: { font: { size: 13 }, boxWidth: 20 } }, |
| title: { display: !!title, text: title, font: { size: 16 } },
| | title: { display: !!title, text: title, font: { size: 16 } }, |
| tooltip:{ backgroundColor: 'rgba(0,0,0,0.8)', titleFont: {size:14}, bodyFont: {size:13} }
| | tooltip:{ backgroundColor: 'rgba(0,0,0,0.8)', titleFont: {size:14}, bodyFont: {size:13} } |
| },
| | }, |
| scales: {
| | scales: { |
| x: { ticks: { font: { size: 12 } } },
| | x: { ticks: { font: { size: 12 } } }, |
| y: { beginAtZero: true, ticks: { precision: 0, font: { size: 12 } } }
| | y: { beginAtZero: true, ticks: { precision: 0, font: { size: 12 } } } |
| }
| | } |
| }
| | } |
| });
| | }); |
|
| |
|
| // --- NEU ---
| | const hideTotal = (block.dataset.hideTotal || '').toLowerCase() === 'true'; |
| const hideTotal = (block.dataset.hideTotal || '').toLowerCase() === 'true';
| | const oldInfo = block.querySelector(':scope > .chart-total-info'); |
| | if (oldInfo) oldInfo.remove(); |
|
| |
|
| // existierende Anzeige entfernen (falls Seite neu gerendert wird)
| | if (!hideTotal) { |
| const oldInfo = block.querySelector(':scope > .chart-total-info');
| | addTotalBelowLegend(chart, block); |
| if (oldInfo) oldInfo.remove();
| |
|
| |
|
| if (!hideTotal) {
| | if (window.ResizeObserver) { |
| // Gesamtzahl einfügen
| | const obs = new ResizeObserver(() => addTotalBelowLegend(chart, block)); |
| addTotalBelowLegend(chart, block);
| | obs.observe(chart.canvas); |
| | chart.$adosTotalObserver = obs; |
| | } |
| | } |
|
| |
|
| // bei Größenänderung (mobil <-> desktop) neu berechnen
| | block.dataset.rendered = '1'; |
| if (window.ResizeObserver) {
| | }); |
| const obs = new ResizeObserver(() => addTotalBelowLegend(chart, block)); | |
| obs.observe(chart.canvas);
| |
| chart.$adosTotalObserver = obs;
| |
| } | |
| } | | } |
|
| |
|
| block.dataset.rendered = '1';
| |
| });
| |
|
| |
| }
| |
|
| |
| // 5) Start: erst wenn DOM fertig, dann Chart.js laden, dann rendern
| |
| function boot($scope){ | | function boot($scope){ |
| var blocks = ($scope && $scope[0] ? $scope[0] : document).querySelectorAll('.ados-chart-multi'); | | var blocks = ($scope && $scope[0] ? $scope[0] : document).querySelectorAll('.ados-chart-multi'); |
| Zeile 982: |
Zeile 941: |
| mw.hook('wikipage.content').add(boot); | | mw.hook('wikipage.content').add(boot); |
| } else { | | } else { |
| // Fallback
| |
| (document.readyState === 'loading') | | (document.readyState === 'loading') |
| ? document.addEventListener('DOMContentLoaded', function(){ boot(); }) | | ? document.addEventListener('DOMContentLoaded', function(){ boot(); }) |
| Zeile 990: |
Zeile 948: |
|
| |
|
|
| |
|
| | // ==========================Scan================================== |
| | mw.loader.using('mediawiki.util').then(function () { |
| | if (mw.config.get('wgPageName') !== 'LabelScan') return; |
| | mw.loader.load('/index.php?title=MediaWiki:Gadget-LabelScan.js&action=raw&ctype=text/javascript'); |
| | mw.loader.load('/index.php?title=MediaWiki:Gadget-LabelScan.css&action=raw&ctype=text/css', 'text/css'); |
| | }); |
|
| |
|
| // ===================PopUp für Diagramm=========================
| |
|
| |
|
| | // ==========================ScanApp================================== |
| | /* ==== PWA: Manifest + Service Worker + Install-Button (ES5) ==== */ |
|
| |
|
| /* ADOS – Chart-News Popup (v1) | | /* Manifest einbinden */ |
| Info-Popup: Abfüllungen pro Jahr / pro Serie + Gesamt
| | (function () { |
| - Oben Canvas-Animation (wachsende Balken + Sparkles)
| | var link = document.createElement("link"); |
| - 1× pro Tag je Nutzer
| | link.rel = "manifest"; |
| */
| | link.href = "/app/labelscan/manifest.webmanifest"; |
| mw.loader.using(['mediawiki.util','jquery']).then(function(){
| | document.head.appendChild(link); |
| (function($, mw){
| | })(); |
| 'use strict';
| |
|
| |
|
| var CONFIG = {
| | /* Service Worker registrieren (nur wenn vorhanden) */ |
| enabled: true,
| | (function () { |
| id: 'ados_chart_news_v1', // Bei Änderungen hochzählen, damit alle es wieder sehen
| | if ("serviceWorker" in navigator) { |
| title: 'Neu: Abfüllungen im Diagramm 📊',
| | navigator.serviceWorker.register("/app/labelscan/sw.js")["catch"](function () {}); |
| introHTML:
| | } |
| '<p>Ab sofort werden <strong>alle Abfüllungen</strong> in übersichtlichen Diagrammen festgehalten – ' +
| | })(); |
| 'sie zeigen <strong>pro Jahr</strong> und <strong>pro Serie</strong> wie viele erschienen sind ' +
| |
| 'und auch <strong>wie viele insgesamt</strong>.</p>',
| |
| cta: { text: 'Zu den Diagrammen', url: 'https://ados-wiki.de/wiki/Abf%C3%BCllungen_pro_Jahr' },
| |
|
| |
|
| // Anzeige
| | /* Install-Button steuern (Button-ID: ados-install) */ |
| showOnNamespaces: 'all',
| | (function () { |
| dailyLimit: 1,
| | var installPrompt = null; |
| escToClose: true,
| |
| clickBackdropToClose: true
| |
| };
| |
|
| |
|
| if (!CONFIG.enabled) return;
| | window.addEventListener("beforeinstallprompt", function (e) { |
| | | try { e.preventDefault(); } catch (ex) {} |
| // Namespace-Filter (falls gewünscht)
| | installPrompt = e; |
| var ns = mw.config.get('wgNamespaceNumber');
| | var btn = document.getElementById("ados-install"); |
| if (CONFIG.showOnNamespaces !== 'all' &&
| | if (btn) btn.style.display = "inline-block"; |
| $.isArray(CONFIG.showOnNamespaces) &&
| | }); |
| $.inArray(ns, CONFIG.showOnNamespaces) === -1) return;
| |
| | |
| // 1×/Tag
| |
| var isAnon = (mw.config.get('wgUserName') === null);
| |
| function LSget(k){ try { return localStorage.getItem(k); } catch(e){ return null; } }
| |
| function LSset(k,v){ try { localStorage.setItem(k,v); } catch(e){} } | |
| var key = 'popup_' + CONFIG.id + (isAnon?':anon':':user'); | |
| var today = (function(d){ return d.getFullYear()+'-'+('0'+(d.getMonth()+1)).slice(-2)+'-'+('0'+d.getDate()).slice(-2); })(new Date()); | |
| if (LSget(key) === today) return; | |
| function markSeen(){ LSset(key, today); }
| |
|
| |
|
| $(function(){
| | function onReady(fn){ if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", fn); else fn(); } |
| // Overlay + Modal
| | onReady(function () { |
| var $overlay = $('<div>', {'class':'mw-popup-overlay'});
| | var btn = document.getElementById("ados-install"); |
| var $modal = $('<div>', {
| | if (!btn) return; |
| 'class':'mw-popup-modal',
| | btn.addEventListener("click", function () { |
| 'role':'dialog',
| | if (!installPrompt) return; |
| 'aria-modal':'true',
| | try { installPrompt.prompt(); } catch (ex) {} |
| 'aria-labelledby':'ados-chartnews-title'
| | installPrompt = null; |
| });
| | btn.style.display = "none"; |
| | |
| // Canvas-Bühne (Chart-Animation)
| |
| var $stage = $('<div>', {'class':'mw-fw-canvas-wrap'});
| |
| var $canvas = $('<canvas>', {'class':'mw-fw-canvas','aria-hidden':'true'});
| |
| $stage.append($canvas);
| |
| | |
| // Titel / Intro
| |
| var $title = $('<h2>', { id:'ados-chartnews-title' }).text(CONFIG.title);
| |
| var $intro = $('<div>', {'class':'mw-popup-content'}).html(CONFIG.introHTML);
| |
| | |
| // Buttons
| |
| var $btnRow = $('<div>', {'class':'mw-popup-button-row'});
| |
| if (CONFIG.cta && CONFIG.cta.url) {
| |
| $btnRow.append($('<a>', {
| |
| 'class':'mw-popup-wiki-button',
| |
| 'href': CONFIG.cta.url,
| |
| 'target': '_blank',
| |
| 'rel': 'noopener'
| |
| }).text(CONFIG.cta.text || 'Mehr'));
| |
| }
| |
| var $ok = $('<button>', {'class':'mw-popup-close', type:'button'}).text('OK');
| |
| $btnRow.append($ok);
| |
| | |
| // Zusammenbauen
| |
| $modal.append($stage, $title, $intro, $btnRow);
| |
| $('body').append($overlay, $modal);
| |
| | |
| // Schließen
| |
| function close(){
| |
| stopAnim(true);
| |
| markSeen();
| |
| $overlay.remove(); $modal.remove();
| |
| $(document).off('keydown.adoschart');
| |
| }
| |
| if (CONFIG.clickBackdropToClose) $overlay.on('click', close);
| |
| $ok.on('click', close);
| |
| if (CONFIG.escToClose){
| |
| $(document).on('keydown.adoschart', function(e){
| |
| var k = e.key || e.keyCode;
| |
| if (k==='Escape' || k==='Esc' || k===27){ e.preventDefault(); close(); }
| |
| });
| |
| }
| |
| | |
| // ===== Canvas-Animation: wachsende Balken + Sparkles =====
| |
| var canvas = $canvas[0], ctx = canvas.getContext && canvas.getContext('2d');
| |
| var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
| |
| var dpr=1, cw=0, ch=0, raf=null, started=false, t0=0, ro;
| |
| | |
| // Dummy-Daten für die Animation (nur visuell, echte Charts sind auf der Zielseite)
| |
| var bars = [
| |
| { label:'2021', value: 12, color:'#5aa3ff' },
| |
| { label:'2022', value: 18, color:'#7ccb88' },
| |
| { label:'2023', value: 26, color:'#f7b267' },
| |
| { label:'2024', value: 31, color:'#e07a5f' },
| |
| { label:'2025', value: 38, color:'#c084fc' }
| |
| ];
| |
| var maxVal = 40; // für „Gesamt“/Skalierung
| |
| var sparkles = [];
| |
| | |
| function setSize(){
| |
| var rect = $stage[0].getBoundingClientRect();
| |
| if (rect.width <= 0 || rect.height <= 0) return false;
| |
| var newDpr = Math.max(1, window.devicePixelRatio || 1);
| |
| if (cw!==rect.width || ch!==rect.height || dpr!==newDpr){
| |
| dpr=newDpr; cw=rect.width; ch=rect.height;
| |
| canvas.width = Math.floor(cw*dpr);
| |
| canvas.height = Math.floor(ch*dpr);
| |
| canvas.style.width = cw+'px';
| |
| canvas.style.height = ch+'px';
| |
| }
| |
| return true;
| |
| }
| |
| | |
| function lerp(a,b,t){ return a + (b-a)*t; }
| |
| function easeOutCubic(x){ return 1 - Math.pow(1 - x, 3); }
| |
| | |
| function addSparkle(x,y){
| |
| if (reduce) return;
| |
| for (var i=0;i<6;i++){
| |
| sparkles.push({
| |
| x:x, y:y,
| |
| vx:(Math.random()*2-1)*0.8*dpr,
| |
| vy:(Math.random()*2-1)*0.8*dpr - 0.6*dpr,
| |
| life: 36 + Math.random()*18,
| |
| age: 0
| |
| });
| |
| }
| |
| }
| |
| | |
| function frame(ts){
| |
| if (!t0) t0 = ts;
| |
| var t = (ts - t0)/1000;
| |
| | |
| // Hintergrund
| |
| ctx.globalCompositeOperation = 'source-over';
| |
| ctx.fillStyle = 'rgba(5,10,20,0.14)';
| |
| ctx.fillRect(0,0,canvas.width,canvas.height);
| |
| | |
| // Chart-Bereich
| |
| var pad = 18*dpr;
| |
| var gx = pad*2, gy = pad, gw = canvas.width - pad*3, gh = canvas.height - pad*3;
| |
| // Achse
| |
| ctx.strokeStyle = 'rgba(255,255,255,0.22)';
| |
| ctx.lineWidth = Math.max(1, 1*dpr);
| |
| ctx.beginPath();
| |
| ctx.moveTo(gx, gy+gh);
| |
| ctx.lineTo(gx+gw, gy+gh);
| |
| ctx.stroke();
| |
| | |
| // Balken animiert hochfahren
| |
| var n = bars.length;
| |
| var gap = gw * 0.08 / n;
| |
| var bw = (gw - gap*(n+1)) / n;
| |
| | |
| for (var i=0;i<n;i++){
| |
| var delay = i * 120; // leicht versetzt starten
| |
| var p = Math.max(0, (ts - t0 - delay)/900);
| |
| var prog = Math.min(1, easeOutCubic(p));
| |
| var targetH = (bars[i].value / maxVal) * gh;
| |
| var hNow = targetH * prog;
| |
| | |
| var bx = gx + gap + i*(bw + gap);
| |
| var by = gy + gh - hNow;
| |
| | |
| // Balken
| |
| var grad = ctx.createLinearGradient(bx, by, bx, gy+gh);
| |
| grad.addColorStop(0, bars[i].color);
| |
| grad.addColorStop(1, 'rgba(255,255,255,0.06)');
| |
| ctx.fillStyle = grad;
| |
| ctx.fillRect(bx, by, bw, hNow);
| |
| | |
| // Glanzkante
| |
| ctx.fillStyle = 'rgba(255,255,255,0.18)';
| |
| ctx.fillRect(bx + bw*0.72, by, Math.max(1, bw*0.08), hNow);
| |
| | |
| // Sparkles am Balkenende
| |
| if (prog > 0.95 && Math.random()<0.12){ addSparkle(bx + bw*0.5, by - 6*dpr); }
| |
| | |
| // Label
| |
| ctx.fillStyle = 'rgba(255,255,255,0.8)';
| |
| ctx.font = Math.max(10*dpr, 12) + 'px "Segoe UI", Arial';
| |
| ctx.textAlign = 'center';
| |
| ctx.fillText(bars[i].label, Math.floor(bx + bw/2), Math.floor(gy+gh + 14*dpr));
| |
| }
| |
| | |
| // Sparkles bewegen/zeichnen
| |
| var next = [];
| |
| ctx.globalCompositeOperation = 'lighter';
| |
| for (var s=0;s<sparkles.length;s++){
| |
| var sp = sparkles[s];
| |
| sp.age++;
| |
| sp.vy += 0.02*dpr;
| |
| sp.x += sp.vx;
| |
| sp.y += sp.vy;
| |
| var alpha = Math.max(0, 1 - sp.age/sp.life);
| |
| if (alpha>0){
| |
| ctx.beginPath();
| |
| ctx.fillStyle = 'rgba(255,230,160,'+alpha.toFixed(2)+')';
| |
| ctx.arc(sp.x, sp.y, Math.max(0.8, 2.2*alpha)*dpr, 0, Math.PI*2);
| |
| ctx.fill();
| |
| next.push(sp);
| |
| }
| |
| }
| |
| sparkles = next;
| |
| | |
| raf = requestAnimationFrame(frame);
| |
| } | |
| | |
| function startAnim(){
| |
| if (!ctx || reduce) return;
| |
| if (!setSize()) { setTimeout(startAnim, 50); return; }
| |
| if (started) return;
| |
| started = true; t0 = 0;
| |
| raf = requestAnimationFrame(frame);
| |
| | |
| // ResizeObserver
| |
| if ('ResizeObserver' in window) {
| |
| ro = new ResizeObserver(function(){ setSize(); });
| |
| ro.observe($stage[0]);
| |
| } else {
| |
| $(window).on('resize.adoschart', setSize);
| |
| }
| |
| } | |
| function stopAnim(remove){
| |
| if (raf){ cancelAnimationFrame(raf); raf=null; }
| |
| started = false;
| |
| if (remove){
| |
| if (ro){ ro.disconnect(); ro=null; } else { $(window).off('resize.adoschart'); }
| |
| }
| |
| }
| |
| | |
| // Start
| |
| setTimeout(startAnim, 0);
| |
| markSeen();
| |
| }); | | }); |
| })(jQuery, mw); | | }); |
| }); | | })(); |
|
| |
|
| // ==========================Scan================================== | | if ('serviceWorker' in navigator) { |
| | navigator.serviceWorker.register('/app/labelscan/sw.js').catch(function(){}); |
| | } |
|
| |
|
| // === 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;
| | mw.loader.using('mediawiki.util').then(function () { |
| if ($run.data('bound')) return; // nicht doppelt binden | | function checkNeuBadges() { |
| $run.data('bound', 1); | | var badges = document.querySelectorAll('.ados-neu-badge'); |
| | var now = new Date(); |
|
| |
|
| function setStatus(t){ $stat.text(t||''); } | | badges.forEach(function (badge) { |
| function setProgress(p){
| | var expiry = badge.getAttribute('data-expiry'); |
| if (p==null){ $prog.hide().val(0); return; }
| | if (!expiry) 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
| | var expiryDate = new Date(expiry + "T23:59:59"); |
| $btnBig.on('click', function(){ $file.trigger('click'); });
| | if (now > expiryDate) { |
| // Drag&Drop
| | badge.style.display = "none"; |
| $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
| | if (document.readyState === "loading") { |
| function loadTesseract(){
| | document.addEventListener("DOMContentLoaded", checkNeuBadges); |
| return new Promise(function(resolve, reject){
| | } else { |
| if (window.Tesseract) return resolve();
| | checkNeuBadges(); |
| 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(/"/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
| | mw.loader.load('/wiki/MediaWiki:WhiskybaseBatch.js?action=raw&ctype=text/javascript'); |
| $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.');
| |
| });
| |
| });
| |