* docs: prepare Vendoo 1.41.2 git-ignore boundary hotfix * fix: track native source modules with root-anchored runtime ignores
205 lines
8.6 KiB
JavaScript
205 lines
8.6 KiB
JavaScript
const browserApi = globalThis.browser || null;
|
|
const chromeApi = globalThis.chrome || null;
|
|
const $ = selector => document.querySelector(selector);
|
|
const state = { queue: [], serverUrl: 'http://localhost:8124' };
|
|
|
|
function sendMessage(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) {
|
|
if (browserApi) return browserApi.storage.local.get(keys);
|
|
return new Promise(resolve => chromeApi.storage.local.get(keys, resolve));
|
|
}
|
|
function storageSet(values) {
|
|
if (browserApi) return browserApi.storage.local.set(values);
|
|
return new Promise(resolve => chromeApi.storage.local.set(values, resolve));
|
|
}
|
|
function openTab(url) {
|
|
if (browserApi) return browserApi.tabs.create({ url });
|
|
return chromeApi.tabs.create({ url });
|
|
}
|
|
async function ensureServerPermission(url) {
|
|
const parsed = new URL(url);
|
|
const origin = `${parsed.protocol}//${parsed.host}/*`;
|
|
const permissions = browserApi?.permissions || chromeApi?.permissions;
|
|
if (!permissions) return true;
|
|
if (browserApi) {
|
|
if (await permissions.contains({ origins: [origin] })) return true;
|
|
return permissions.request({ origins: [origin] });
|
|
}
|
|
return new Promise(resolve => permissions.contains({ origins: [origin] }, has => has ? resolve(true) : permissions.request({ origins: [origin] }, resolve)));
|
|
}
|
|
|
|
async function api(path, method = 'GET', body) {
|
|
const response = await sendMessage({ type: 'api', serverUrl: state.serverUrl, path, method, body });
|
|
if (!response?.ok) throw new Error(response?.data?.error || response?.error || 'Server nicht erreichbar');
|
|
return response.data;
|
|
}
|
|
|
|
function setConnection(ok, text) {
|
|
const chip = $('#connection-chip');
|
|
chip.className = `connection-chip ${ok ? 'ok' : 'error'}`;
|
|
chip.querySelector('span').textContent = text;
|
|
}
|
|
|
|
async function imageData(filename) {
|
|
if (!filename) return '';
|
|
const url = `${state.serverUrl}/uploads/${String(filename).split('/').map(encodeURIComponent).join('/')}`;
|
|
const response = await sendMessage({ type: 'fetch-image', url });
|
|
return response?.ok ? response.dataUrl : '';
|
|
}
|
|
|
|
function formatPrice(value) {
|
|
const num = Number(value);
|
|
return Number.isFinite(num) ? `${num.toFixed(2).replace('.', ',')} €` : 'Preis fehlt';
|
|
}
|
|
|
|
async function renderQueue() {
|
|
const list = $('#queue-list');
|
|
const empty = $('#empty-card');
|
|
$('#list-count').textContent = `${state.queue.length} Einträge`;
|
|
if (!state.queue.length) {
|
|
list.innerHTML = '';
|
|
empty.classList.remove('hidden');
|
|
return;
|
|
}
|
|
empty.classList.add('hidden');
|
|
list.innerHTML = state.queue.map(item => `
|
|
<article class="queue-item" data-id="${item.id}">
|
|
<div class="queue-item-image"><img data-photo="${item.photos?.[0] || ''}" alt="${escapeHtml(item.title || 'Produktbild')}"></div>
|
|
<div class="queue-item-main">
|
|
<div class="queue-item-top">
|
|
<div class="queue-item-title">${escapeHtml(item.title || 'Ohne Titel')}</div>
|
|
<div class="queue-item-price">${formatPrice(item.price)}</div>
|
|
</div>
|
|
<div class="queue-item-meta">${escapeHtml([item.brand, item.size, item.condition].filter(Boolean).join(' · ') || 'Noch keine Eigenschaften')}</div>
|
|
<div class="queue-item-sku">${escapeHtml(item.sku || `VD-${String(item.id).padStart(4, '0')}`)}</div>
|
|
<div class="queue-item-actions">
|
|
<button class="item-action primary" data-action="fill" data-id="${item.id}">Daten</button>
|
|
<button class="item-action" data-action="fill-all" data-id="${item.id}">Alles</button>
|
|
<button class="item-action" data-action="detail" data-id="${item.id}">Prüfen</button>
|
|
<button class="item-action" data-action="app" data-id="${item.id}">Vendoo</button>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
`).join('');
|
|
|
|
const imgNodes = [...list.querySelectorAll('img[data-photo]')];
|
|
for (const img of imgNodes) {
|
|
const photo = img.dataset.photo;
|
|
if (!photo) { img.removeAttribute('src'); continue; }
|
|
const src = await imageData(photo);
|
|
if (src) img.src = src;
|
|
}
|
|
|
|
list.querySelectorAll('[data-action]').forEach(button => button.addEventListener('click', async event => {
|
|
const { action, id } = event.currentTarget.dataset;
|
|
const listing = state.queue.find(item => item.id === Number(id));
|
|
if (!listing) return;
|
|
if (action === 'fill') {
|
|
await storageSet({ pendingFillListingId: listing.id, pendingFillIncludePhotos: false });
|
|
openTab('https://www.vinted.de/items/new');
|
|
window.close();
|
|
return;
|
|
}
|
|
if (action === 'fill-all') {
|
|
await storageSet({ pendingFillListingId: listing.id, pendingFillIncludePhotos: true });
|
|
openTab('https://www.vinted.de/items/new');
|
|
window.close();
|
|
return;
|
|
}
|
|
if (action === 'detail') {
|
|
await storageSet({ pendingOpenListingId: listing.id });
|
|
openTab('https://www.vinted.de/items/new');
|
|
window.close();
|
|
return;
|
|
}
|
|
if (action === 'app') {
|
|
openTab(`${state.serverUrl}/#publish`);
|
|
window.close();
|
|
}
|
|
}));
|
|
}
|
|
|
|
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() {
|
|
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: 'popup',
|
|
server_url: state.serverUrl,
|
|
capabilities: { popup: true, queue: true, publishing_queue: true, diagnostics: true },
|
|
});
|
|
} catch {}
|
|
}
|
|
|
|
async function loadQueue() {
|
|
try {
|
|
const data = await api('/api/extension/queue?platform=vinted');
|
|
state.queue = data.queue || [];
|
|
setConnection(true, 'Verbunden');
|
|
await sendDiagnosticHeartbeat();
|
|
$('#queue-state').textContent = state.queue.length ? 'Bereit zum Ausfüllen' : 'Queue leer';
|
|
$('#queue-summary-text').textContent = state.queue.length
|
|
? 'Sichere Autofill-Aktionen ohne versteckte Veröffentlichung.'
|
|
: 'Neue Vinted-Listings erscheinen automatisch hier.';
|
|
$('#queue-count').textContent = String(state.queue.length);
|
|
await renderQueue();
|
|
} catch (error) {
|
|
setConnection(false, 'Offline');
|
|
$('#queue-state').textContent = 'Server nicht erreichbar';
|
|
$('#queue-summary-text').textContent = error.message;
|
|
state.queue = [];
|
|
await renderQueue();
|
|
}
|
|
}
|
|
|
|
async function init() {
|
|
const settings = await storageGet(['serverUrl', 'provider', 'language']);
|
|
state.serverUrl = String(settings.serverUrl || 'http://localhost:8124').replace(/\/$/, '');
|
|
$('#server-url').value = state.serverUrl;
|
|
if (settings.provider) $('#provider').value = settings.provider;
|
|
if (settings.language) $('#language').value = settings.language;
|
|
await loadQueue();
|
|
}
|
|
|
|
$('#settings-toggle').addEventListener('click', () => { $('#dashboard-view').classList.add('hidden'); $('#settings-view').classList.remove('hidden'); });
|
|
$('#back-dashboard').addEventListener('click', () => { $('#settings-view').classList.add('hidden'); $('#dashboard-view').classList.remove('hidden'); });
|
|
$('#save-settings').addEventListener('click', async () => {
|
|
state.serverUrl = $('#server-url').value.trim().replace(/\/$/, '');
|
|
if (!await ensureServerPermission(state.serverUrl)) throw new Error('Zugriff auf die Serveradresse wurde nicht erlaubt.');
|
|
await storageSet({ serverUrl: state.serverUrl, provider: $('#provider').value, language: $('#language').value });
|
|
$('#settings-view').classList.add('hidden');
|
|
$('#dashboard-view').classList.remove('hidden');
|
|
await loadQueue();
|
|
});
|
|
$('#reload-list').addEventListener('click', loadQueue);
|
|
$('#open-queue').addEventListener('click', () => openTab(`${state.serverUrl}/#publish`));
|
|
$('#open-app').addEventListener('click', () => openTab(state.serverUrl));
|
|
$('#empty-open-app').addEventListener('click', () => openTab(state.serverUrl));
|
|
|
|
function escapeHtml(input) {
|
|
return String(input ?? '').replace(/[&<>"']/g, ch => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[ch]));
|
|
}
|
|
|
|
setInterval(sendDiagnosticHeartbeat, 60 * 1000);
|
|
init();
|