(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 `
`;
}
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 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 = `
Vendoo Assistant
...
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.
`;
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) await fillListingData(listing, { includePhotos: false });
}));
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-detail').addEventListener('click', () => showDetail(id));
actions.querySelector('#al-selection-more').addEventListener('click', () => actions.querySelector('#al-selection-menu').classList.toggle('hidden'));
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
- 1Daten prüfenTitel, Preis, Kategorie, Größe✓
- 2Bilder vorbereiten${photos.length}/${photos.length || 0} Bilder verfügbar✓
- 3Beschreibung übernehmenBeschreibung und Attribute✓
- 4Auto-Fill auf VintedFormular automatisch ausfüllen○
- 5VeröffentlichungListing abschließen○
Auto-Fill · VintedVendoo ü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
Ü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();
const pending = await storageGet(['pendingFillListingId']);
if (pending.pendingFillListingId) {
const listing = STATE.queue.find(item => item.id === Number(pending.pendingFillListingId));
if (listing) {
await storageSet({ pendingFillListingId: null });
await fillListingData(listing, { includePhotos: false });
showDetail(listing.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;
});
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();