Files
vendoo/extensions/popup.js
Masterluke77andGitHub 6f48827f4d Vendoo 1.41.2 – Git Ignore Boundary & Module Tracking Hotfix (#18)
* docs: prepare Vendoo 1.41.2 git-ignore boundary hotfix

* fix: track native source modules with root-anchored runtime ignores
2026-07-09 17:09:00 +02:00

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 => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[ch]));
}
setInterval(sendDiagnosticHeartbeat, 60 * 1000);
init();