(function() { 'use strict'; const browserApi = globalThis.browser || null; const chromeApi = globalThis.chrome || null; const VENDOO_BRAND_ICON = 'data:image/png;base64,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'; function sendRuntimeMessage(message) { if (browserApi) return browserApi.runtime.sendMessage(message); return new Promise((resolve, reject) => chromeApi.runtime.sendMessage(message, response => { const error = chromeApi.runtime.lastError; if (error) reject(new Error(error.message)); else resolve(response); })); } function storageGet(keys) { return browserApi ? browserApi.storage.local.get(keys) : new Promise(resolve => chromeApi.storage.local.get(keys, resolve)); } function storageSet(values) { return browserApi ? browserApi.storage.local.set(values) : new Promise(resolve => chromeApi.storage.local.set(values, resolve)); } const STATE = { serverUrl: 'http://localhost:8124', connected: false, queue: [], published: [], selected: new Set(), batchDelay: 45, batchMode: 'draft', batchRunning: false, batchAbort: false, batchProgress: { current: 0, total: 0, currentId: null }, }; let panel, listEl; let serverCategories = null; async function loadServerCategories() { try { const data = await api('/api/categories/vinted'); serverCategories = data; if (data.tree) window.VINTED_CATEGORIES = data.tree; if (data.flat) window.VINTED_CATEGORIES_FLAT = data.flat; } catch {} } async function serverMatchCategory(query) { if (!query) return []; try { return await api(`/api/categories/vinted/match?q=${encodeURIComponent(query)}`); } catch { return typeof matchCategory === 'function' ? matchCategory(query) : []; } } // --- API via Background Service Worker --- async function api(path, method, body) { const resp = await sendRuntimeMessage({ type: 'api', serverUrl: STATE.serverUrl, path, method: method || 'GET', body }); if (!resp) throw new Error('Keine Antwort vom Extension-Background'); if (!resp.ok) throw new Error(resp.data?.error || resp.error || `HTTP ${resp.status}`); return resp.data; } async function fetchImage(url) { const resp = await sendRuntimeMessage({ type: 'fetch-image', url }); if (!resp || !resp.ok) throw new Error(resp?.error || 'Bild laden fehlgeschlagen'); return resp; } const IMAGE_CACHE = new Map(); function buildUploadUrl(filename) { if (!filename) return ''; const safePath = String(filename).split('/').map(encodeURIComponent).join('/'); return `${STATE.serverUrl.replace(/\/$/, '')}/uploads/${safePath}`; } function extensionPhotoMarkup(filename, className, altText) { if (!filename) return ``; return `${attr(altText || '')}`; } async function resolveExtensionPhoto(filename) { if (!filename) return ''; if (IMAGE_CACHE.has(filename)) return IMAGE_CACHE.get(filename); const promise = fetchImage(buildUploadUrl(filename)) .then(result => result.dataUrl) .catch(error => { IMAGE_CACHE.delete(filename); throw error; }); IMAGE_CACHE.set(filename, promise); return promise; } function hydrateExtensionImages(root) { if (!root) return; root.querySelectorAll('img[data-vendoo-photo]').forEach(async image => { const filename = image.dataset.vendooPhoto; try { image.src = await resolveExtensionPhoto(filename); image.classList.remove('al-image-loading', 'al-image-error'); image.classList.add('al-image-ready'); } catch { image.removeAttribute('src'); image.classList.remove('al-image-loading'); image.classList.add('al-image-error'); image.alt = 'Produktbild konnte nicht geladen werden'; } }); } // --- Helpers --- function toast(msg, type) { let t = document.querySelector('.al-toast'); if (!t) { t = document.createElement('div'); t.className = 'al-toast'; document.body.appendChild(t); } t.textContent = msg; t.className = 'al-toast ' + (type || ''); requestAnimationFrame(() => t.classList.add('show')); setTimeout(() => t.classList.remove('show'), 3000); } function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } async function loadSettings() { try { const d = await storageGet(['serverUrl', 'batchDelay', 'batchMode']); if (d.serverUrl) STATE.serverUrl = d.serverUrl; if (d.batchDelay) STATE.batchDelay = parseInt(d.batchDelay) || 45; if (d.batchMode) STATE.batchMode = d.batchMode; } catch {} } async function checkConnection() { try { await api('/api/providers'); STATE.connected = true; loadServerCategories(); } catch { STATE.connected = false; } updateStatusBadge(); } function detectDiagnosticBrowser() { const ua = navigator.userAgent || ''; if (/Edg\//.test(ua)) return 'edge'; if (/Firefox\//.test(ua)) return 'firefox'; if (/Safari\//.test(ua) && !/Chrome\//.test(ua)) return 'safari'; return 'chrome'; } async function sendDiagnosticHeartbeat() { if (!STATE.connected) return; try { const stored = await storageGet(['vendooDiagnosticClientId']); const clientId = stored.vendooDiagnosticClientId || (globalThis.crypto?.randomUUID?.() || `vendoo-${Date.now()}-${Math.random().toString(16).slice(2)}`); if (!stored.vendooDiagnosticClientId) await storageSet({ vendooDiagnosticClientId: clientId }); await api('/api/extensions/heartbeat', 'POST', { client_id: clientId, browser: detectDiagnosticBrowser(), version: '1.9.0', platform: 'vinted-content', server_url: STATE.serverUrl, capabilities: { autofill: true, photos: true, publishing_queue: true, diagnostics: true }, }); } catch {} } function updateStatusBadge() { const badge = panel?.querySelector('.al-hd-status'); if (!badge) return; badge.textContent = STATE.connected ? 'Verbunden' : 'Offline'; badge.className = 'al-hd-status ' + (STATE.connected ? 'live' : 'off'); } // --- Data --- async function loadQueue() { if (!STATE.connected) return; try { const data = await api('/api/extension/queue?platform=vinted'); STATE.queue = data.queue || []; STATE.published = data.published || []; const overviewCount = panel?.querySelector('#al-queue-overview-count'); const overviewTitle = panel?.querySelector('#al-queue-overview-title'); if (overviewCount) overviewCount.textContent = String(STATE.queue.length); if (overviewTitle) overviewTitle.textContent = STATE.queue.length ? 'Bereit zum Ausfüllen' : 'Alles erledigt'; renderQueue(); updateFooter(); } catch (e) { toast('Laden fehlgeschlagen: ' + e.message, 'error'); } } // --- UI --- function createPanel() { panel = document.createElement('div'); panel.id = 'al-panel'; panel.innerHTML = `
...
0 ausgewählt
Vinted WorkflowBereit zum AusfüllenWähle einen Artikel und übertrage die Daten kontrolliert.
0offen
Batch-Modus
„Nur ausfüllen“ ist bewusst eine Einzelaktion und speichert oder veröffentlicht nichts.
Pause zwischen Artikeln (Sek.)
Server URL
`; document.body.appendChild(panel); listEl = panel.querySelector('#al-queue-list'); bindEvents(); setupDrag(); setupResize(); } function bindEvents() { panel.querySelectorAll('.al-tab').forEach(tab => tab.addEventListener('click', () => switchTab(tab.dataset.tab)) ); panel.querySelector('#al-refresh').addEventListener('click', () => { checkConnection().then(() => { if (STATE.connected) loadQueue(); }); }); panel.querySelector('#al-minimize').addEventListener('click', minimize); panel.querySelector('#al-close').addEventListener('click', close); panel.querySelector('#al-select-all').addEventListener('change', e => { if (e.target.checked) STATE.queue.forEach(l => STATE.selected.add(l.id)); else STATE.selected.clear(); renderQueue(); updateFooter(); }); panel.querySelector('#al-start-batch').addEventListener('click', () => { if (STATE.batchRunning) { STATE.batchAbort = true; return; } startBatch(); }); panel.querySelector('#al-delay').addEventListener('change', e => { STATE.batchDelay = Math.max(10, Math.min(300, parseInt(e.target.value) || 45)); storageSet({ batchDelay: String(STATE.batchDelay) }); }); panel.querySelector('#al-batch-mode').addEventListener('change', e => { STATE.batchMode = e.target.value; storageSet({ batchMode: STATE.batchMode }); updateFooter(); }); panel.querySelector('#al-test-conn').addEventListener('click', async () => { STATE.serverUrl = panel.querySelector('#al-server-url').value.replace(/\/$/, ''); await storageSet({ serverUrl: STATE.serverUrl }); await checkConnection(); toast(STATE.connected ? 'Verbunden!' : 'Server nicht erreichbar', STATE.connected ? 'success' : 'error'); if (STATE.connected) loadQueue(); }); panel.querySelector('#al-open-app').addEventListener('click', () => { window.open(STATE.serverUrl, '_blank'); }); } function setupDrag() { const hd = panel.querySelector('.al-hd'); let dragging = false, ox = 0, oy = 0; hd.addEventListener('mousedown', e => { if (e.target.tagName === 'BUTTON') return; dragging = true; const r = panel.getBoundingClientRect(); ox = e.clientX - r.left; oy = e.clientY - r.top; e.preventDefault(); }); document.addEventListener('mousemove', e => { if (!dragging) return; panel.style.right = 'auto'; panel.style.top = (e.clientY - oy) + 'px'; panel.style.left = (e.clientX - ox) + 'px'; }); document.addEventListener('mouseup', () => { dragging = false; }); } function setupResize() { const handle = panel.querySelector('#al-resize-handle'); let resizing = false, startW, startH, startX, startY; handle.addEventListener('mousedown', e => { resizing = true; startW = panel.offsetWidth; startH = panel.offsetHeight; startX = e.clientX; startY = e.clientY; e.preventDefault(); e.stopPropagation(); }); document.addEventListener('mousemove', e => { if (!resizing) return; panel.style.width = Math.max(300, startW + (e.clientX - startX)) + 'px'; panel.style.height = Math.max(350, startH + (e.clientY - startY)) + 'px'; }); document.addEventListener('mouseup', () => { resizing = false; }); } function switchTab(tab) { panel.querySelectorAll('.al-tab').forEach(t => t.classList.toggle('active', t.dataset.tab === tab)); panel.querySelectorAll('.al-view').forEach(v => v.classList.remove('active')); panel.querySelector(`#al-view-${tab}`).classList.add('active'); panel.querySelector('#al-batch-bar').style.display = tab === 'queue' ? '' : 'none'; panel.querySelector('#al-footer').style.display = (tab === 'queue' || tab === 'batch-progress') ? '' : 'none'; if (tab === 'done') renderDone(); } function switchToView(name) { panel.querySelectorAll('.al-view').forEach(v => v.classList.remove('active')); panel.querySelector(`#al-view-${name}`).classList.add('active'); panel.querySelectorAll('.al-tab').forEach(t => t.classList.remove('active')); panel.querySelector('#al-batch-bar').style.display = name === 'queue' ? '' : 'none'; panel.querySelector('#al-footer').style.display = name === 'batch-progress' ? '' : 'none'; } function minimize() { panel.remove(); const fab = document.createElement('button'); fab.className = 'al-fab'; fab.id = 'al-fab'; fab.innerHTML = ``; fab.title = 'Vendoo Assistant öffnen'; document.body.appendChild(fab); fab.addEventListener('click', () => { fab.remove(); createPanel(); init(); }); } function close() { panel.remove(); panel = null; } // --- Render --- function renderQueue() { if (!listEl) return; if (!STATE.queue.length) { listEl.innerHTML = '
  • Warteschlange erledigtNeue Vinted-Listings erscheinen automatisch hier.
  • '; return; } listEl.innerHTML = STATE.queue.map(l => { const sel = STATE.selected.has(l.id); const photo = l.photos?.[0] || ''; const cls = ['al-item']; if (sel) cls.push('selected'); if (STATE.batchProgress.currentId === l.id) cls.push('processing'); const statusHtml = STATE.batchProgress.currentId === l.id ? 'Läuft' : 'Bereit'; return `
  • ${extensionPhotoMarkup(photo, 'al-item-img', l.title || 'Produktbild')}
    ${statusHtml}Vinted
    ${esc(l.title || 'Ohne Titel')}
    ${esc(l.brand || 'Ohne Marke')}${l.size ? ' · ' + esc(l.size) : ''}${l.condition ? ' · ' + esc(l.condition) : ''}
    ${esc(l.sku || `VD-${String(l.id).padStart(4, '0')}`)}
    ${l.price ? `${Number(l.price).toFixed(2).replace('.', ',')} €` : 'Preis fehlt'}
  • `; }).join(''); hydrateExtensionImages(listEl); listEl.querySelectorAll('.al-item-cb').forEach(cb => cb.addEventListener('change', e => { e.stopPropagation(); const id = +e.target.dataset.id; if (e.target.checked) STATE.selected.add(id); else STATE.selected.delete(id); renderQueue(); updateFooter(); }) ); listEl.querySelectorAll('.al-quick-fill').forEach(button => button.addEventListener('click', async e => { e.stopPropagation(); const listing = STATE.queue.find(l => l.id === +button.dataset.id); if (!listing) return; const includePhotos = button.classList.contains('al-quick-fill-all'); await fillListingData(listing, { includePhotos }); })); listEl.querySelectorAll('.al-quick-detail,.al-item-open').forEach(button => button.addEventListener('click', e => { e.stopPropagation(); showDetail(+button.dataset.id); })); listEl.querySelectorAll('.al-item').forEach(item => item.addEventListener('click', e => { if (e.target.closest('button,input')) return; showDetail(+item.dataset.id); }) ); panel.querySelector('#al-select-all').checked = STATE.selected.size === STATE.queue.length && STATE.queue.length > 0; } function renderDone() { const dl = panel.querySelector('#al-done-list'); if (!STATE.published.length) { dl.innerHTML = '
  • 📦Noch keine Artikel veröffentlicht.
  • '; return; } dl.innerHTML = STATE.published.map(l => { const photo = l.photos?.[0] || ''; return `
  • ${extensionPhotoMarkup(photo, 'al-item-img', l.title || 'Produktbild')}
    ${esc(l.title || 'Ohne Titel')}
    ${esc(l.brand || '')}${l.price ? ' · ' + Number(l.price).toFixed(2) + ' €' : ''}
    Erledigt
  • `; }).join(''); hydrateExtensionImages(dl); } function updateFooter() { const count = STATE.selected.size; panel.querySelector('#al-sel-count').textContent = `${count} ausgewählt`; const actions = panel.querySelector('#al-footer-actions'); if (STATE.batchRunning) { actions.innerHTML = ''; actions.querySelector('#al-start-batch').addEventListener('click', () => { STATE.batchAbort = true; }); return; } if (count === 1) { const id = [...STATE.selected][0]; actions.innerHTML = ` `; actions.querySelector('#al-selection-fill').addEventListener('click', async () => { const listing = STATE.queue.find(l => l.id === id); if (listing) await fillListingData(listing, { includePhotos: false }); }); actions.querySelector('#al-selection-fill-all').addEventListener('click', async () => { const listing = STATE.queue.find(l => l.id === id); if (listing) await fillListingData(listing, { includePhotos: true }); }); actions.querySelector('#al-selection-detail').addEventListener('click', () => showDetail(id)); actions.querySelector('#al-batch-draft').addEventListener('click', () => { STATE.batchMode = 'draft'; startBatch(); }); actions.querySelector('#al-batch-publish').addEventListener('click', () => { STATE.batchMode = 'publish'; startBatch(); }); } else if (count > 1) { actions.innerHTML = ` `; actions.querySelector('#al-batch-draft').addEventListener('click', () => { STATE.batchMode = 'draft'; startBatch(); }); actions.querySelector('#al-batch-publish').addEventListener('click', () => { STATE.batchMode = 'publish'; startBatch(); }); } else { actions.innerHTML = ''; } } // --- Detail --- function showDetail(id) { const listing = STATE.queue.find(l => l.id === id) || STATE.published.find(l => l.id === id); if (!listing) return; const view = panel.querySelector('#al-view-detail'); const photos = listing.photos || []; view.innerHTML = `
    ${extensionPhotoMarkup(photos[0] || '', 'al-detail-main-photo', `${listing.title || 'Produkt'} · Cover`)}
    Bereit

    ${esc(listing.title || 'Ohne Titel')}

    ${listing.price ? Number(listing.price).toFixed(2).replace('.', ',') + ' €' : 'Preis fehlt'}
    Kategorie
    ${esc(listing.category || 'Nicht gesetzt')}
    Marke
    ${esc(listing.brand || '–')}
    Größe
    ${esc(listing.size || '–')}
    Zustand
    ${esc(listing.condition || '–')}
    ${photos.map((photo, index) => extensionPhotoMarkup(photo, 'al-detail-photo', `${listing.title || 'Produkt'} · Bild ${index + 1}`)).join('')}

    Veröffentlichen auf Vinted

    1. 1Daten prüfenTitel, Preis, Kategorie, Größe
    2. 2Bilder vorbereiten${photos.length}/${photos.length || 0} Bilder verfügbar
    3. 3Beschreibung übernehmenBeschreibung und Attribute
    4. 4Auto-Fill auf VintedFormular automatisch ausfüllen
    5. 5VeröffentlichungListing abschließen
    Auto-Fill · Vinted

    Vendoo übernimmt alle vorhandenen Listing-Daten.

    • ✓ Titel
    • ✓ Preis
    • ✓ Kategorie
    • ✓ Größe & Marke
    • ✓ Beschreibung
    • ✓ Bilder
    ✓ Bereit zum ÜbertragenAlle Pflichtdaten werden vorab geprüft.
    Listing-Daten prüfen & bearbeiten
    Titel
    Beschreibung
    Preis
    Zustand
    Marke
    Größe
    Kategorie (Vinted)
    Farbe
    AI-Kategorie (Original)
    Tags
    ÜBERTRAGEN – OHNE SPEICHERN

    Diese Aktionen füllen das Vinted-Formular nur aus. Es wird weder ein Entwurf gespeichert noch veröffentlicht.

    Entwurf speichern oder veröffentlichen
    `; const v = view; hydrateExtensionImages(view); v.querySelector('.al-detail-back').addEventListener('click', () => switchTab('queue')); // Category picker initCategoryPicker(v, listing.category || ''); v.querySelector('#al-d-fields').addEventListener('click', async () => { await fillListingData({ ...listing, ...readDetailFields() }, { includePhotos: false }); }); v.querySelector('#al-d-fill-all').addEventListener('click', async () => { await fillListingData({ ...listing, ...readDetailFields(), photos: listing.photos || [] }, { includePhotos: true }); }); v.querySelector('#al-d-draft').addEventListener('click', async () => { clickVintedButton('draft'); await markStatus(id, 'draft'); toast('Als Entwurf gespeichert', 'success'); }); v.querySelector('#al-d-publish').addEventListener('click', async () => { clickVintedButton('publish'); await markStatus(id, 'published'); toast('Veröffentlicht!', 'success'); }); v.querySelector('#al-d-all-draft').addEventListener('click', async () => { await doFullListing(listing, 'draft'); await markStatus(id, 'draft'); }); v.querySelector('#al-d-all-publish').addEventListener('click', async () => { await doFullListing(listing, 'publish'); await markStatus(id, 'published'); }); v.querySelector('#al-d-copy').addEventListener('click', () => { const f = readDetailFields(); navigator.clipboard.writeText(`${f.title}\n\n${f.description}\n\nPreis: ${f.price}\nKategorie: ${f.category}\nTags: ${f.tags}`); toast('Kopiert!', 'success'); }); switchToView('detail'); } function readDetailFields() { return { title: panel.querySelector('#al-d-title').value, description: panel.querySelector('#al-d-desc').value, price: panel.querySelector('#al-d-price').value, condition: panel.querySelector('#al-d-condition').value, brand: panel.querySelector('#al-d-brand').value, size: panel.querySelector('#al-d-size').value, category: panel.querySelector('#al-d-category').value, color: panel.querySelector('#al-d-color').value, tags: panel.querySelector('#al-d-tags').value, }; } // --- Sicheres Ausfüllen ohne Speichern oder Veröffentlichen --- async function fillListingData(listing, options = {}) { const includePhotos = options.includePhotos === true; const fields = { title: listing.title || '', description: listing.description || '', price: listing.price ? String(listing.price) : '', condition: listing.condition || '', brand: listing.brand || '', size: listing.size || '', category: listing.category || '', color: listing.color || '', tags: Array.isArray(listing.tags) ? listing.tags.join(' ') : (listing.tags || ''), }; toast(includePhotos ? 'Daten und Bilder werden vorbereitet …' : 'Daten werden in Vinted eingetragen …'); if (includePhotos && Array.isArray(listing.photos) && listing.photos.length) { await uploadPhotos(listing.photos); await sleep(700); } fillVintedForm(fields); await fillVintedChoiceFields(fields); if (fields.category) await selectCategory(fields.category); toast(includePhotos ? 'Daten und Bilder ausgefüllt – noch nicht gespeichert.' : 'Daten ausgefüllt – noch nicht gespeichert.', 'success'); } async function fillVintedChoiceFields(fields) { const jobs = [ [['Marke', 'Brand'], fields.brand], [['Größe', 'Size'], fields.size], [['Zustand', 'Condition'], fields.condition], [['Farbe', 'Color'], fields.color], ]; for (const [labels, value] of jobs) { if (!value) continue; await chooseVintedOption(labels, value); } } async function chooseVintedOption(labels, value) { const normalized = normalizedFieldText(value); if (!normalized) return false; const wantedLabels = labels.map(normalizedFieldText); let trigger = null; let triggerIsExplicitlyAssociated = false; const labelNodes = [...document.querySelectorAll('label')] .filter(label => !isExtensionElement(label)) .sort((a, b) => normalizedFieldText(a.textContent).length - normalizedFieldText(b.textContent).length); for (const label of labelNodes) { const text = normalizedFieldText(label.textContent); if (!wantedLabels.some(item => text === item || text.startsWith(`${item} `) || text.startsWith(`${item}*`))) continue; trigger = getAssociatedControl(label, 'button,[role="button"],select,input'); if (trigger) { triggerIsExplicitlyAssociated = Boolean( (label.htmlFor && trigger.id === label.htmlFor) || label.contains(trigger) || trigger.getAttribute?.('aria-labelledby')?.split(/\s+/).includes(label.id) || label.nextElementSibling?.contains?.(trigger) || label.nextElementSibling === trigger ); break; } } if (!trigger) { for (const el of document.querySelectorAll('button,[role="button"]')) { if (isExtensionElement(el)) continue; const text = normalizedFieldText(el.textContent); if (wantedLabels.some(item => text === item || text.startsWith(`${item} `))) { trigger = el; break; } } } if (!trigger || isLikelyTitleField(trigger)) return false; if (trigger.tagName === 'INPUT') { const role = normalizedFieldText(trigger.getAttribute('role')); const type = normalizedFieldText(trigger.type || 'text'); if (!triggerIsExplicitlyAssociated && role !== 'combobox') return false; if (!['text', 'search', 'button'].includes(type) && role !== 'combobox') return false; setNativeValue(trigger, value); return true; } if (trigger.tagName === 'SELECT') { const option = [...trigger.options].find(o => o.textContent.trim().toLowerCase().includes(normalized)); if (option) { trigger.value = option.value; trigger.dispatchEvent(new Event('change', { bubbles: true })); return true; } return false; } trigger.click(); await sleep(350); for (const option of document.querySelectorAll('[role="option"],li,button,[role="button"]')) { if (option.closest('#al-panel')) continue; const text = option.textContent.trim().toLowerCase(); if (text && (text === normalized || text.includes(normalized) || normalized.includes(text))) { option.click(); await sleep(180); return true; } } return false; } // --- Full listing (photos + fields + category + submit) --- async function doFullListing(listing, mode) { const steps = panel.querySelector('#al-d-fields')?.parentElement; if (!steps) return; toast('Starte Komplett-Eintrag...', 'success'); // 1–3) Bilder, Felder und Kategorie ausfüllen await fillListingData(listing, { includePhotos: true }); await sleep(700); // 4) Submit toast('Abschluss: ' + (mode === 'draft' ? 'Entwurf speichern …' : 'Veröffentlichen …')); await sleep(500); clickVintedButton(mode); toast(mode === 'draft' ? 'Als Entwurf gespeichert!' : 'Veröffentlicht!', 'success'); } // --- Photo Upload --- async function uploadPhotos(photoFilenames) { if (!photoFilenames.length) return { ok: false, count: 0 }; const dropzone = findPhotoDropzone(); const fileInput = findPhotoFileInput(); if (!dropzone && !fileInput) { toast('Foto-Upload Bereich nicht gefunden. Bitte lade Fotos manuell hoch.', 'error'); return { ok: false, count: 0 }; } const files = []; for (const filename of photoFilenames) { try { const url = buildUploadUrl(filename); const imgData = await fetchImage(url); const byteString = atob(imgData.dataUrl.split(',')[1]); const ab = new ArrayBuffer(byteString.length); const ia = new Uint8Array(ab); for (let i = 0; i < byteString.length; i++) ia[i] = byteString.charCodeAt(i); const ext = filename.split('.').pop().toLowerCase(); const mime = imgData.mime || ({ jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', webp: 'image/webp', gif: 'image/gif' }[ext] || 'image/jpeg'); files.push(new File([ab], filename, { type: mime })); } catch (err) { console.warn('Vendoo: Foto laden fehlgeschlagen:', filename, err); } } if (!files.length) { toast('Keine Fotos geladen', 'error'); return { ok: false, count: 0 }; } if (fileInput) { try { const dt = new DataTransfer(); files.forEach(f => dt.items.add(f)); fileInput.files = dt.files; fileInput.dispatchEvent(new Event('change', { bubbles: true })); toast(`${files.length} Fotos hochgeladen`, 'success'); return { ok: true, count: files.length }; } catch (err) { console.warn('Vendoo: FileInput-Methode fehlgeschlagen, versuche Drop:', err); } } if (dropzone) { try { const dt = new DataTransfer(); files.forEach(f => dt.items.add(f)); const dropEvent = new DragEvent('drop', { bubbles: true, cancelable: true, dataTransfer: dt }); dropzone.dispatchEvent(new DragEvent('dragenter', { bubbles: true })); dropzone.dispatchEvent(new DragEvent('dragover', { bubbles: true, cancelable: true, dataTransfer: dt })); dropzone.dispatchEvent(dropEvent); toast(`${files.length} Fotos hochgeladen`, 'success'); return { ok: true, count: files.length }; } catch (err) { console.warn('Vendoo: Drop-Methode fehlgeschlagen:', err); } } toast('Automatischer Upload fehlgeschlagen. Bitte Fotos manuell hochladen.', 'error'); return { ok: false, count: 0 }; } function findPhotoDropzone() { const selectors = [ '[data-testid="photo-upload"]', '[data-testid="file-upload"]', '[class*="photo-upload"]', '[class*="PhotoUpload"]', '[class*="dropzone"]', '[class*="Dropzone"]', '[class*="media-upload"]', '[class*="image-upload"]', '.upload-dropzone', '[class*="drag-and-drop"]', ]; for (const s of selectors) { const el = document.querySelector(s); if (el) return el; } const divs = document.querySelectorAll('div[class]'); for (const d of divs) { if (d.className.match(/photo|upload|drop|media|image/i) && d.offsetHeight > 50) return d; } return null; } function findPhotoFileInput() { const inputs = document.querySelectorAll('input[type="file"]'); for (const inp of inputs) { if (inp.accept && inp.accept.includes('image')) return inp; } if (inputs.length) return inputs[0]; return null; } // --- Category Picker (in-panel dropdown with Vinted categories) --- let selectedVintedCategory = null; function initCategoryPicker(container, aiCategory) { const input = container.querySelector('#al-d-category'); const dropdown = container.querySelector('#al-cat-dropdown'); const display = container.querySelector('#al-cat-selected-display'); if (!input || !dropdown || !display) return; if (aiCategory) { serverMatchCategory(aiCategory).then(matches => { if (matches.length > 0) selectPickerCategory(matches[0], display); renderDropdownResults(matches, dropdown, display); }); } input.addEventListener('focus', () => { if (input.value.trim().length === 0) renderCategoryTree(dropdown, display); else serverMatchCategory(input.value).then(m => renderDropdownResults(m, dropdown, display)); dropdown.classList.add('open'); }); let catSearchTimer; input.addEventListener('input', () => { clearTimeout(catSearchTimer); catSearchTimer = setTimeout(() => { const q = input.value.trim(); if (q.length === 0) renderCategoryTree(dropdown, display); else serverMatchCategory(q).then(m => renderDropdownResults(m, dropdown, display)); dropdown.classList.add('open'); }, 200); }); document.addEventListener('click', e => { if (!container.querySelector('#al-cat-wrap')?.contains(e.target)) dropdown.classList.remove('open'); }); } function renderDropdownResults(matches, dropdown, display) { if (!matches.length) { dropdown.innerHTML = '
    Keine Kategorie gefunden
    '; return; } dropdown.innerHTML = matches.map((m, i) => `
    ${esc(m.name)}
    ${esc(m.path.slice(0, -1).join(' > '))}
    `).join(''); dropdown.querySelectorAll('.al-cat-item').forEach((el, i) => { el.addEventListener('click', e => { e.stopPropagation(); selectPickerCategory(matches[i], display); dropdown.classList.remove('open'); }); }); } function renderCategoryTree(dropdown, display) { let html = '
    '; for (const main of VINTED_CATEGORIES) { html += `
    `; html += `
    ${esc(main.name)}
    `; html += `
    `; if (main.children) { for (const sub of main.children) { html += `
    `; html += `
    ${esc(sub.name)}
    `; html += `
    `; if (sub.children) { for (const leaf of sub.children) { html += `
    ${esc(leaf.name)}
    `; } } html += '
    '; } } html += '
    '; } html += '
    '; dropdown.innerHTML = html; dropdown.querySelectorAll('.al-cat-group-hd').forEach(hd => { hd.addEventListener('click', e => { e.stopPropagation(); const isMain = hd.dataset.main; const isSub = hd.dataset.sub; const sel = isMain ? `[data-main="${isMain}"]` : `[data-sub="${isSub}"]`; const children = hd.nextElementSibling; if (children) { children.classList.toggle('open'); hd.classList.toggle('expanded'); } }); }); dropdown.querySelectorAll('.al-cat-leaf').forEach(leaf => { leaf.addEventListener('click', e => { e.stopPropagation(); selectPickerCategory({ id: +leaf.dataset.catId, name: leaf.dataset.name, path: leaf.dataset.path.split(' > '), pathStr: leaf.dataset.path, }, display); dropdown.classList.remove('open'); }); }); } function selectPickerCategory(cat, display) { selectedVintedCategory = cat; const input = panel.querySelector('#al-d-category'); if (input) input.value = cat.name; if (display) { display.innerHTML = `
    ${esc(cat.name)}
    ${esc(cat.path ? cat.path.join(' > ') : cat.name)}
    `; display.querySelector('.al-cat-clear')?.addEventListener('click', () => { selectedVintedCategory = null; display.innerHTML = ''; if (input) input.value = ''; }); } } // --- Category Selection on Vinted page --- async function selectCategory(category) { if (!category && !selectedVintedCategory) { toast('Keine Kategorie angegeben', 'error'); return; } let catData = selectedVintedCategory; if (!catData && category) { const matches = await serverMatchCategory(category); catData = matches?.[0] || null; } if (!catData || !catData.path?.length) { toast('Kategorie "' + category + '" nicht in Vinted-Kategorien gefunden. Bitte manuell wählen.', 'error'); return; } const catPath = catData.path; const categoryBtn = findCategoryButton(); if (!categoryBtn) { toast('Kategorie-Bereich auf Vinted nicht gefunden. Bitte manuell: ' + catPath.join(' > '), 'error'); return; } categoryBtn.click(); await sleep(1000); for (let i = 0; i < catPath.length; i++) { const found = await clickCategoryLevel(catPath[i]); if (!found) { toast(`Ebene "${catPath[i]}" nicht gefunden. Bisher: ${catPath.slice(0, i).join(' > ')}`, 'error'); return; } await sleep(600); } toast('Kategorie: ' + catPath.join(' > '), 'success'); } function findCategoryButton() { const selectors = [ '[data-testid*="catalog"]', '[data-testid*="category"]', '[class*="catalog"]', '[class*="Category"]', '[class*="category"]', ]; for (const s of selectors) { const el = document.querySelector(s + ' button') || document.querySelector(s); if (el && !el.closest('#al-panel')) return el; } for (const el of document.querySelectorAll('button, [role="button"], a, div[class]')) { if (el.closest('#al-panel')) continue; const text = el.textContent.trim().toLowerCase(); if ((text.includes('kategorie') || text.includes('category') || text.includes('katalog')) && el.offsetHeight > 0 && el.offsetHeight < 200) return el; } return null; } async function clickCategoryLevel(name) { const norm = name.toLowerCase().trim(); const containers = [ ...document.querySelectorAll('[role="dialog"], [class*="modal"], [class*="overlay"], [class*="catalog"], [class*="category"]'), document.body, ]; for (const container of containers) { if (!container || container.closest('#al-panel')) continue; const clickables = container.querySelectorAll('li, [role="option"], [role="button"], button, a, div[class*="item"], div[class*="option"], span[class*="name"]'); for (const el of clickables) { if (el.closest('#al-panel')) continue; const t = el.textContent.trim().toLowerCase(); if (t === norm || t.startsWith(norm) || norm.startsWith(t)) { el.click(); return true; } } for (const el of clickables) { if (el.closest('#al-panel')) continue; const t = el.textContent.trim().toLowerCase(); if ((t.includes(norm) || norm.includes(t)) && t.length > 1 && t.length < 100) { el.click(); return true; } } } return false; } // --- Vinted Form Fill --- function setNativeValue(el, value) { const setter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(el), 'value')?.set || Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set || Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')?.set; if (setter) setter.call(el, value); else el.value = value; el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); } function isExtensionElement(el) { return !el || Boolean(el.closest?.('#al-panel')); } function isUsableFormControl(el, expectedTag = '') { if (!el || isExtensionElement(el) || el.disabled) return false; if (expectedTag && el.tagName?.toLowerCase() !== expectedTag.toLowerCase()) return false; return true; } function normalizedFieldText(value) { return String(value || '').replace(/\s+/g, ' ').trim().toLowerCase(); } function getAssociatedControl(label, selector) { if (!label) return null; if (label.htmlFor) { const byFor = document.getElementById(label.htmlFor); if (byFor?.matches?.(selector) && isUsableFormControl(byFor)) return byFor; } const nested = label.querySelector(selector); if (isUsableFormControl(nested)) return nested; if (label.id && typeof CSS !== 'undefined' && CSS.escape) { const escapedId = CSS.escape(label.id); const labelledSelector = selector.split(',') .map(part => `${part.trim()}[aria-labelledby~="${escapedId}"]`) .join(', '); const labelled = document.querySelector(labelledSelector); if (isUsableFormControl(labelled)) return labelled; } const sibling = label.nextElementSibling; if (sibling) { const direct = sibling.matches?.(selector) ? sibling : sibling.querySelector?.(selector); if (isUsableFormControl(direct)) return direct; } const parent = label.parentElement; if (parent) { const scopedSelector = selector.split(',').map(part => { const item = part.trim(); return `:scope > ${item}, :scope > div > ${item}`; }).join(', '); const controls = [...parent.querySelectorAll(scopedSelector)] .filter(control => isUsableFormControl(control)); if (controls.length === 1) return controls[0]; } return null; } function findField(selectors) { for (const selector of selectors) { for (const el of document.querySelectorAll(selector)) { if (isUsableFormControl(el)) return el; } } return null; } function findByLabel(text, tag) { const wanted = normalizedFieldText(text); const labels = [...document.querySelectorAll('label')] .filter(label => !isExtensionElement(label)) .sort((a, b) => normalizedFieldText(a.textContent).length - normalizedFieldText(b.textContent).length); for (const label of labels) { const labelText = normalizedFieldText(label.textContent); if (!(labelText === wanted || labelText.startsWith(`${wanted} `) || labelText.startsWith(`${wanted}*`))) continue; const control = getAssociatedControl(label, tag); if (isUsableFormControl(control, tag)) return control; } return null; } function isLikelyTitleField(el) { if (!el) return false; const identity = normalizedFieldText([ el.name, el.id, el.getAttribute?.('data-testid'), el.getAttribute?.('aria-label'), el.getAttribute?.('placeholder'), ].filter(Boolean).join(' ')); return /(^|[\s_-])(title|titel)([\s_-]|$)/.test(identity); } function fillVintedForm(fields) { let filled = 0; const titleEl = findField([ '[data-testid="title-input"] input', '[data-testid*="title" i] input', 'input[name="title"]', 'input[id*="title" i]', 'input[aria-label*="Titel" i]', 'input[aria-label*="Title" i]', 'input[placeholder*="Titel" i]', 'input[placeholder*="Title" i]' ]) || findByLabel('Titel', 'input') || findByLabel('Title', 'input'); if (titleEl && fields.title) { setNativeValue(titleEl, fields.title); filled++; } const descEl = findField(['[data-testid="description-input"] textarea', 'textarea[name="description"]', '[class*="Description"] textarea']) || findByLabel('Beschreib', 'textarea') || findByLabel('Describe', 'textarea'); if (descEl && fields.description) { setNativeValue(descEl, fields.description); filled++; } const priceEl = findField(['[data-testid="price-input"] input', 'input[name="price"]', '[class*="Price"] input']) || findByLabel('Preis', 'input') || findByLabel('Price', 'input'); if (priceEl && fields.price) { setNativeValue(priceEl, String(fields.price).replace(/[^\d.,]/g, '').replace(',', '.')); filled++; } if (fields.tags) { const tagEl = findField(['[data-testid="hashtag-input"] input', 'input[name="hashtags"]']) || findByLabel('Hashtag', 'input'); if (tagEl) { const tagList = fields.tags.split(/\s+/).filter(Boolean); tagList.forEach((tag, i) => { setTimeout(() => { setNativeValue(tagEl, tag.replace(/^#/, '')); tagEl.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true })); }, i * 250); }); filled++; } } toast(filled > 0 ? `${filled} Felder ausgefüllt` : 'Keine Felder gefunden — richtige Seite?', filled > 0 ? 'success' : 'error'); return filled; } // --- Submit / Draft --- function clickVintedButton(mode) { const allBtns = document.querySelectorAll('button[type="submit"], button[class*="submit"], button[data-testid]'); if (mode === 'draft') { for (const btn of document.querySelectorAll('button, a, [role="button"]')) { const text = btn.textContent.trim().toLowerCase(); if (text.includes('entwurf') || text.includes('draft') || text.includes('save as draft') || text.includes('als entwurf')) { btn.click(); return true; } } toast('Entwurf-Button nicht gefunden. Bitte manuell als Entwurf speichern.', 'error'); return false; } if (mode === 'publish') { for (const btn of document.querySelectorAll('button, [role="button"]')) { const text = btn.textContent.trim().toLowerCase(); if (text.includes('hochladen') || text.includes('upload') || text.includes('veröffentlichen') || text.includes('publish') || text.includes('anzeige aufgeben') || text.includes('list now')) { btn.click(); return true; } } for (const btn of allBtns) { if (btn.type === 'submit' && !btn.closest('#al-panel')) { btn.click(); return true; } } toast('Veröffentlichen-Button nicht gefunden. Bitte manuell veröffentlichen.', 'error'); return false; } return false; } // --- Status tracking --- async function markStatus(id, status) { try { await api('/api/publish/mark', 'POST', { listing_id: id, platform: 'vinted', status }); } catch {} } // --- Batch --- async function startBatch() { const ids = [...STATE.selected]; if (!ids.length) return; STATE.batchRunning = true; STATE.batchAbort = false; STATE.batchProgress = { current: 0, total: ids.length, currentId: null }; updateFooter(); showBatchProgress(); for (let i = 0; i < ids.length; i++) { if (STATE.batchAbort) break; const id = ids[i]; STATE.batchProgress.current = i + 1; STATE.batchProgress.currentId = id; renderQueue(); updateBatchProgress(); try { const data = await api(`/api/extension/listing/${id}/photos`); const l = data.listing; const photoFiles = l.photos || []; // 1) Upload photos logBatchStep(id, 'Fotos hochladen...'); await uploadPhotos(typeof photoFiles === 'string' ? photoFiles.split(' ').filter(Boolean) : photoFiles); await sleep(1500); // 2) Fill fields logBatchStep(id, 'Felder ausfüllen...'); fillVintedForm({ title: l.title, description: l.description, price: l.price ? String(l.price) : '', condition: l.condition || '', brand: l.brand || '', size: l.size || '', tags: (l.tags || []).join(' '), }); await sleep(1000); // 3) Category if (l.category) { logBatchStep(id, 'Kategorie wählen...'); await selectCategory(l.category); await sleep(1000); } // 4) Submit logBatchStep(id, STATE.batchMode === 'draft' ? 'Als Entwurf speichern...' : 'Veröffentlichen...'); await sleep(500); clickVintedButton(STATE.batchMode); await markStatus(id, STATE.batchMode === 'draft' ? 'draft' : 'published'); logBatchItem(id, 'done'); // Wait for page to process await sleep(3000); } catch (err) { logBatchItem(id, 'error', err.message); } if (i < ids.length - 1 && !STATE.batchAbort) { await countdown(STATE.batchDelay); } } STATE.batchRunning = false; STATE.batchProgress.currentId = null; updateFooter(); toast(STATE.batchAbort ? 'Batch abgebrochen' : `${ids.length} Artikel eingetragen!`, STATE.batchAbort ? 'error' : 'success'); STATE.selected.clear(); await loadQueue(); switchTab('queue'); } function showBatchProgress() { const view = panel.querySelector('#al-view-batch-progress'); const modeText = STATE.batchMode === 'draft' ? 'Entwurf' : 'Veröffentlichen'; view.innerHTML = `
    Modus: ${modeText}
    0 / ${STATE.batchProgress.total} 0%
    `; switchToView('batch-progress'); panel.querySelector('#al-footer').style.display = ''; } function updateBatchProgress() { const { current, total } = STATE.batchProgress; const pct = Math.round((current / total) * 100); const fill = panel.querySelector('#al-bp-fill'); const text = panel.querySelector('#al-bp-text'); const percent = panel.querySelector('#al-bp-percent'); if (fill) fill.style.width = pct + '%'; if (text) text.textContent = `${current} / ${total}`; if (percent) percent.textContent = pct + '%'; } function logBatchStep(id, step) { const log = panel.querySelector('#al-bp-log'); if (!log) return; const listing = STATE.queue.find(l => l.id === id); const title = listing ? listing.title : `#${id}`; log.innerHTML += `
    • ${esc(title.substring(0, 30))}... ${step}
    `; log.scrollTop = log.scrollHeight; } function logBatchItem(id, status, msg) { const log = panel.querySelector('#al-bp-log'); if (!log) return; const listing = STATE.queue.find(l => l.id === id); const title = listing ? listing.title : `#${id}`; const icon = status === 'done' ? '✓' : '✗'; const color = status === 'done' ? '#2c6e49' : '#c0392b'; log.innerHTML += `
    ${icon} ${esc(title)}${msg ? ' — ' + esc(msg) : ''}
    `; log.scrollTop = log.scrollHeight; } function countdown(seconds) { return new Promise(resolve => { const cdEl = panel.querySelector('#al-countdown'); const timeEl = panel.querySelector('#al-cd-time'); if (!cdEl) { setTimeout(resolve, seconds * 1000); return; } cdEl.style.display = ''; let remaining = seconds; timeEl.textContent = remaining; const iv = setInterval(() => { remaining--; timeEl.textContent = remaining; if (remaining <= 0 || STATE.batchAbort) { clearInterval(iv); cdEl.style.display = 'none'; resolve(); } }, 1000); }); } // --- Util --- function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; } function attr(s) { return String(s).replace(/"/g, '"'); } // --- Init --- async function init() { await loadSettings(); if (!panel) createPanel(); await checkConnection(); if (STATE.connected) { await loadQueue(); await sendDiagnosticHeartbeat(); const pending = await storageGet(['pendingFillListingId', 'pendingFillIncludePhotos', 'pendingOpenListingId']); if (pending.pendingFillListingId) { const listing = STATE.queue.find(item => item.id === Number(pending.pendingFillListingId)); if (listing) { const includePhotos = pending.pendingFillIncludePhotos === true; await storageSet({ pendingFillListingId: null, pendingFillIncludePhotos: null }); await fillListingData(listing, { includePhotos }); showDetail(listing.id); } } if (pending.pendingOpenListingId) { const detailListing = STATE.queue.find(item => item.id === Number(pending.pendingOpenListingId)); await storageSet({ pendingOpenListingId: null }); if (detailListing) showDetail(detailListing.id); } } } const runtime = browserApi?.runtime || chromeApi?.runtime; runtime?.onMessage?.addListener((message, _sender, sendResponse) => { if (message?.type !== 'vendoo-fill-listing') return; (async () => { const listing = STATE.queue.find(item => item.id === Number(message.listingId)); if (!listing) throw new Error('Listing nicht in der Warteschlange gefunden'); await fillListingData(listing, { includePhotos: message.includePhotos === true }); showDetail(listing.id); return { ok: true }; })().then(sendResponse).catch(error => sendResponse({ ok: false, error: error.message })); return true; }); setInterval(async () => { if (!STATE.connected) await checkConnection(); if (STATE.connected) await sendDiagnosticHeartbeat(); }, 60 * 1000); if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init(); })();