Snapshot of the working tree exactly as it stood, no edits. This is the predecessor
PRICEGOD was rewritten from ("kept intact, untouched" per pricegod/README.md), and it
is still the only place the DYMO scale is actually implemented —
rfid-daemon/index.js:1068-1265: HID discovery, parseScaleReport, one-shot read, and a
streaming /weight + /scale/start|stop session whose JSON shape PRICEGOD's daemon.js
already speaks.
Preserved verbatim on purpose (hence -og), including the known bug: DYMO_PIDS at
index.js:1082 is [0x8003, 0x8004], so it cannot see the bench M25 (0x8009). Fix that
in whatever daemon inherits the scale, not here.
node_modules stays ignored (22M of the 24M tree). No credentials in the import: the
two PEM markers in utils.js/sheets.js only strip headers off a key read from settings,
and mrpadmin / johnking are an SSH and a Postgres username, both key/trust auth.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4931 lines
212 KiB
JavaScript
4931 lines
212 KiB
JavaScript
// Create placeholder functions for missing API functions
|
||
async function getStoredDiscogsToken() {
|
||
return new Promise((resolve) => {
|
||
if (!chrome || !chrome.storage) {
|
||
resolve(null);
|
||
return;
|
||
}
|
||
chrome.storage.local.get('discogsToken', (data) => {
|
||
resolve((data.discogsToken || '').trim() || null);
|
||
});
|
||
});
|
||
}
|
||
|
||
async function getReleaseData(releaseId) {
|
||
const token = await getStoredDiscogsToken();
|
||
if (!token) return null;
|
||
const idMatch = String(releaseId || '').match(/\d+/);
|
||
const id = idMatch ? idMatch[0] : null;
|
||
if (!id) return null;
|
||
|
||
// Retry up to 3 times — Discogs occasionally rate-limits or drops the first
|
||
// request on a freshly-loaded page, which would otherwise crash the popup.
|
||
for (let attempt = 0; attempt < 3; attempt++) {
|
||
try {
|
||
const response = await fetch(`https://api.discogs.com/releases/${id}`, {
|
||
headers: {
|
||
'Authorization': `Discogs token=${token}`,
|
||
'User-Agent': 'PriceClogs/1.0'
|
||
}
|
||
});
|
||
if (response.status === 429) {
|
||
// Rate limited — back off and retry
|
||
await new Promise(r => setTimeout(r, 1500 * (attempt + 1)));
|
||
continue;
|
||
}
|
||
const data = await response.json();
|
||
if (data && data.artists) return data; // success
|
||
// API returned an error envelope (e.g. { message: "..." }) — retry
|
||
if (attempt < 2) await new Promise(r => setTimeout(r, 1000));
|
||
} catch (_) {
|
||
if (attempt < 2) await new Promise(r => setTimeout(r, 1000));
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
async function getPriceSuggestions(releaseId) {
|
||
try {
|
||
const token = await getStoredDiscogsToken();
|
||
if (!token) {
|
||
throw new Error('Discogs token not configured');
|
||
}
|
||
const idMatch = String(releaseId || '').match(/\d+/);
|
||
const id = idMatch ? idMatch[0] : null;
|
||
if (!id) {
|
||
throw new Error('Invalid release ID');
|
||
}
|
||
const response = await fetch(`https://api.discogs.com/marketplace/price_suggestions/${id}`, {
|
||
headers: {
|
||
'Authorization': `Discogs token=${token}`,
|
||
'User-Agent': 'PriceClogs/1.0'
|
||
}
|
||
});
|
||
return await response.json();
|
||
} catch (error) {
|
||
// console.error('Error fetching price suggestions:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
const state = {
|
||
releaseData: null,
|
||
originalReleaseData: null,
|
||
lastFetchTimestamp: 0,
|
||
sku: null, // 14-digit SKU generated at popup open (e.g. 20260313084637)
|
||
sheetTimestamp: null, // Full ISO timestamp matching the SKU, used for sheet column A
|
||
discogsTabId: null, // Tab ID of the Discogs page, captured at popup open
|
||
fetchCooldown: 1000,
|
||
collectionBoxIndex: 0, // currently selected collection box index
|
||
collectionBoxSkus: [], // SKUs for each box (empty string = no SKU)
|
||
collectionBoxCount: 1, // total number of collection boxes on page
|
||
// MONSTERWIKI background sync results (populated silently at popup open)
|
||
monsterSalesHistory: null,
|
||
monsterCurrentListings: null,
|
||
geminiAutoTriggered: false,
|
||
};
|
||
|
||
// ================================================================
|
||
// MONSTERWIKI — background sync helpers
|
||
// ================================================================
|
||
|
||
// parseSalesHistoryHtml + parseListingsHtml moved to monster_capture.js
|
||
// (loaded before popup.js in popup.html) so background.js can share them.
|
||
|
||
async function getMonsterSettings() {
|
||
return new Promise(resolve =>
|
||
chrome.storage.local.get(['monsterSettings'], r =>
|
||
resolve(r.monsterSettings || { enabled: false, preferLocalhost: false, serverUrl: 'http://100.91.239.7:5002' })
|
||
)
|
||
);
|
||
}
|
||
|
||
function monsterSetIndicator(msg, color, fadeAfterMs = 0) {
|
||
const el = document.getElementById('monster-sync-indicator');
|
||
if (!el) return;
|
||
el.textContent = msg;
|
||
el.style.color = color;
|
||
el.style.opacity = '1';
|
||
el.style.display = 'block';
|
||
if (fadeAfterMs) {
|
||
setTimeout(() => {
|
||
el.style.transition = 'opacity 1.5s';
|
||
el.style.opacity = '0';
|
||
setTimeout(() => { el.style.display = 'none'; }, 1600);
|
||
}, fadeAfterMs);
|
||
}
|
||
}
|
||
|
||
function geminiSetIndicator(msg, color, fadeAfterMs = 0) {
|
||
const el = document.getElementById('gemini-sync-indicator');
|
||
if (!el) return;
|
||
el.textContent = msg;
|
||
el.style.color = color;
|
||
el.style.opacity = '1';
|
||
el.style.display = 'block';
|
||
if (fadeAfterMs) {
|
||
setTimeout(() => {
|
||
el.style.transition = 'opacity 1.5s';
|
||
el.style.opacity = '0';
|
||
setTimeout(() => { el.style.display = 'none'; }, 1600);
|
||
}, fadeAfterMs);
|
||
}
|
||
}
|
||
|
||
async function monsterResolveBase(cfg) {
|
||
// If we're ON ultra, localhost:5002 will answer immediately — use it directly.
|
||
// Otherwise fall back to the configured Tailscale URL.
|
||
try {
|
||
const r = await fetch('http://localhost:5002/plice/health',
|
||
{ signal: AbortSignal.timeout(400) });
|
||
if (r.ok) return 'http://localhost:5002';
|
||
} catch (_) {}
|
||
return (cfg.serverUrl || 'http://100.91.239.7:5002').replace(/\/$/, '');
|
||
}
|
||
|
||
async function monsterBackgroundSync(releaseId) {
|
||
const cfg = await getMonsterSettings();
|
||
if (!cfg.enabled || !releaseId) return;
|
||
|
||
const base = await monsterResolveBase(cfg);
|
||
monsterSetIndicator('⟳ MONSTER syncing…', '#888');
|
||
|
||
const [histHtml, listHtml] = await Promise.all([
|
||
fetch(`https://www.discogs.com/sell/history/${releaseId}`)
|
||
.then(r => r.ok ? r.text() : null).catch(() => null),
|
||
fetch(`https://www.discogs.com/sell/list?release_id=${releaseId}`)
|
||
.then(r => r.ok ? r.text() : null).catch(() => null),
|
||
]);
|
||
|
||
state.monsterSalesHistory = histHtml ? parseSalesHistoryHtml(histHtml) : null;
|
||
state.monsterCurrentListings = listHtml ? parseListingsHtml(listHtml) : null;
|
||
|
||
if (!state.releaseData) return;
|
||
|
||
// Scroll to bottom of page so reviews lazy-load, wait 2s, then scrape
|
||
let liveReviews = state.releaseData.reviews || [];
|
||
if (state.discogsTabId) {
|
||
try {
|
||
const results = await chrome.scripting.executeScript({
|
||
target: { tabId: state.discogsTabId },
|
||
func: async () => {
|
||
window.scrollTo(0, document.body.scrollHeight);
|
||
await new Promise(r => setTimeout(r, 2000));
|
||
const reviews = [];
|
||
document.querySelectorAll('#release-reviews [data-username], #release-reviews [class*="review_card"]')
|
||
.forEach(card => {
|
||
const username = card.getAttribute('data-username')
|
||
|| card.querySelector('[class*="username"]')?.textContent?.trim() || null;
|
||
const dateEl = card.querySelector('time');
|
||
const date = dateEl?.getAttribute('datetime')?.split('T')[0]
|
||
|| dateEl?.textContent?.trim() || null;
|
||
const text = card.querySelector('[class*="review_body"], [class*="reviewBody"]')
|
||
?.textContent?.trim() || null;
|
||
const rating = card.querySelectorAll('[class*="star_full"],[class*="starFull"],[class*="filled"]').length || null;
|
||
const helpful = parseInt(card.querySelector('[class*="helpful"]')?.textContent?.match(/\d+/)?.[0]) || 0;
|
||
if (username && (text || rating))
|
||
reviews.push({ username, date, rating: rating||null, text, helpful, replies: [] });
|
||
});
|
||
return reviews;
|
||
}
|
||
});
|
||
if (results?.[0]?.result?.length) liveReviews = results[0].result;
|
||
} catch (_) {}
|
||
}
|
||
|
||
// Merge live DOM reviews into releaseData for the POST
|
||
const postData = { ...state.releaseData, reviews: liveReviews };
|
||
|
||
try {
|
||
const resp = await fetch(`${base}/plice`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
...buildResearchJson(postData),
|
||
sales_history: state.monsterSalesHistory,
|
||
current_listings: state.monsterCurrentListings,
|
||
}),
|
||
signal: AbortSignal.timeout(12000),
|
||
});
|
||
if (resp.ok) {
|
||
const d = await resp.json();
|
||
const sales = d.sale_rows || 0;
|
||
const listings = d.listing_rows || 0;
|
||
const reviews = d.review_rows || 0;
|
||
monsterSetIndicator(
|
||
`✓ MONSTER — ${sales} sales · ${listings} listings · ${reviews} reviews`,
|
||
'#2a7a2a', 4000
|
||
);
|
||
} else {
|
||
monsterSetIndicator(`✗ MONSTER ${resp.status}`, '#b00', 4000);
|
||
}
|
||
} catch (e) {
|
||
monsterSetIndicator(
|
||
e.name === 'TimeoutError' ? '✗ MONSTER timeout' : '✗ MONSTER unreachable',
|
||
'#b00', 4000
|
||
);
|
||
}
|
||
}
|
||
|
||
// YouTube Cookie Management Functions
|
||
function extractCookies() {
|
||
const button = document.getElementById('getCookies');
|
||
const textarea = document.getElementById('cookieOutput');
|
||
|
||
if (!button || !textarea) return; // Elements don't exist on this page
|
||
|
||
// Check if chrome runtime is available
|
||
if (typeof chrome === 'undefined' || !chrome.runtime) {
|
||
textarea.value = 'Extension functionality not available in preview mode.\nInstall as Chrome extension to use YouTube cookie extraction.';
|
||
return;
|
||
}
|
||
|
||
// Show loading state
|
||
button.disabled = true;
|
||
button.textContent = 'Extracting...';
|
||
textarea.value = 'Extracting cookies...';
|
||
|
||
chrome.runtime.sendMessage({ action: "getCookies" }, async function (response) {
|
||
if (response && response.success && response.cookieText) {
|
||
textarea.value = response.cookieText;
|
||
|
||
// Try to copy to clipboard
|
||
let copied = false;
|
||
try {
|
||
await navigator.clipboard.writeText(response.cookieText);
|
||
copied = true;
|
||
} catch (err) {
|
||
// console.error('Failed to copy:', err);
|
||
}
|
||
|
||
button.textContent = copied ? 'Cookies Saved & Copied!' : 'Cookies Saved!';
|
||
button.style.backgroundColor = '#45a049';
|
||
|
||
// Reset button after delay
|
||
setTimeout(() => {
|
||
button.textContent = 'Extract Cookies';
|
||
button.style.backgroundColor = '#4CAF50';
|
||
button.disabled = false;
|
||
}, 2000);
|
||
} else {
|
||
// Handle error
|
||
console.error('Error response:', response);
|
||
textarea.value = response?.error || 'Failed to extract cookies';
|
||
button.textContent = 'Error - Try Again';
|
||
button.style.backgroundColor = '#f44336';
|
||
button.disabled = false;
|
||
|
||
setTimeout(() => {
|
||
button.textContent = 'Extract Cookies';
|
||
button.style.backgroundColor = '#4CAF50';
|
||
}, 3000);
|
||
}
|
||
});
|
||
}
|
||
|
||
function initializeYouTubeCookieUI() {
|
||
const toggle = document.getElementById('autoUpdate');
|
||
const blagginateToggle = document.getElementById('blagginateBridge');
|
||
const textarea = document.getElementById('cookieOutput');
|
||
const button = document.getElementById('getCookies');
|
||
const container = document.querySelector('.youtube-cookie-container');
|
||
|
||
if (!toggle || !blagginateToggle || !textarea || !button || !container) return; // Elements don't exist
|
||
|
||
// Check if chrome.storage is available (extension context)
|
||
if (typeof chrome !== 'undefined' && chrome.storage) {
|
||
// Load initial state
|
||
chrome.storage.local.get(['autoUpdate', 'blagginateBridge', 'lastCookie', 'lastUpdate'], function (data) {
|
||
// Set toggle states
|
||
toggle.checked = data.autoUpdate === true;
|
||
blagginateToggle.checked = data.blagginateBridge !== false; // Default to true
|
||
|
||
// Show last saved cookie if available
|
||
if (data.lastCookie) {
|
||
const lastUpdateTime = new Date(data.lastUpdate).toLocaleString();
|
||
textarea.value = `Last saved on ${lastUpdateTime}:\n\n${data.lastCookie}`;
|
||
} else {
|
||
textarea.value = 'No cookies saved yet. Click "Extract Cookies" to get YouTube cookies.';
|
||
}
|
||
|
||
// If auto-update is enabled, get fresh cookies
|
||
if (data.autoUpdate === true) {
|
||
extractCookies();
|
||
}
|
||
});
|
||
|
||
// Handle auto-update toggle changes
|
||
toggle.addEventListener('change', function (e) {
|
||
const isEnabled = e.target.checked;
|
||
chrome.storage.local.set({ autoUpdate: isEnabled }, function () {
|
||
if (isEnabled) {
|
||
extractCookies();
|
||
}
|
||
});
|
||
});
|
||
|
||
// Handle BLAGGINATE bridge toggle changes
|
||
blagginateToggle.addEventListener('change', function (e) {
|
||
const isEnabled = e.target.checked;
|
||
chrome.storage.local.set({ blagginateBridge: isEnabled }, function () {
|
||
// Send message to background script to enable/disable bridge
|
||
chrome.runtime.sendMessage({
|
||
type: 'toggle_blagginate_bridge',
|
||
enabled: isEnabled
|
||
});
|
||
|
||
console.log('BLAGGINATE bridge', isEnabled ? 'enabled' : 'disabled');
|
||
});
|
||
});
|
||
} else {
|
||
// Fallback for non-extension context (preview mode)
|
||
textarea.value = 'Extension functionality not available in preview mode.\nInstall as Chrome extension to use YouTube cookie extraction.';
|
||
toggle.disabled = true;
|
||
blagginateToggle.disabled = true;
|
||
button.disabled = true;
|
||
}
|
||
|
||
// Handle button click
|
||
button.addEventListener('click', extractCookies);
|
||
|
||
// JDownloader 2 button: pull fresh YT cookies as Cookie-Editor JSON and copy to clipboard.
|
||
const jdBtn = document.getElementById('getCookiesJd2');
|
||
if (jdBtn) {
|
||
jdBtn.addEventListener('click', () => {
|
||
const orig = jdBtn.textContent;
|
||
jdBtn.disabled = true;
|
||
jdBtn.textContent = 'Fetching...';
|
||
chrome.runtime.sendMessage({ action: 'getCookiesJson' }, async (response) => {
|
||
if (response && response.success && response.cookieJson) {
|
||
try {
|
||
await navigator.clipboard.writeText(response.cookieJson);
|
||
textarea.value = response.cookieJson;
|
||
jdBtn.textContent = 'Copied! Paste into JD2 password field';
|
||
jdBtn.style.background = '#45a049';
|
||
} catch (err) {
|
||
textarea.value = response.cookieJson;
|
||
jdBtn.textContent = 'Shown below — copy manually';
|
||
jdBtn.style.background = '#f39c12';
|
||
}
|
||
} else {
|
||
jdBtn.textContent = 'Failed: ' + ((response && response.error) || 'unknown');
|
||
jdBtn.style.background = '#f44336';
|
||
}
|
||
setTimeout(() => {
|
||
jdBtn.textContent = orig;
|
||
jdBtn.style.background = '#2196F3';
|
||
jdBtn.disabled = false;
|
||
}, 2500);
|
||
});
|
||
});
|
||
}
|
||
}
|
||
|
||
// Tab switching functionality
|
||
function initializeTabs() {
|
||
const tabButtons = document.querySelectorAll('.tab-button');
|
||
const tabContents = document.querySelectorAll('.tab-content');
|
||
|
||
tabButtons.forEach(button => {
|
||
button.addEventListener('click', () => {
|
||
const targetTab = button.getAttribute('data-tab');
|
||
|
||
// Remove active class from all buttons and contents
|
||
tabButtons.forEach(btn => btn.classList.remove('active'));
|
||
tabContents.forEach(content => content.classList.remove('active'));
|
||
|
||
// Add active class to clicked button and corresponding content
|
||
button.classList.add('active');
|
||
document.getElementById(`${targetTab}-tab`).classList.add('active');
|
||
});
|
||
});
|
||
}
|
||
|
||
// Save functions - defined before DOMContentLoaded to ensure availability
|
||
function saveDiscogsToken() {
|
||
if (!chrome || !chrome.storage) {
|
||
alert('Chrome storage not available in preview mode');
|
||
return;
|
||
}
|
||
|
||
const token = document.getElementById('discogs-token').value.trim();
|
||
console.log('Saving Discogs token:', { hasToken: !!token });
|
||
|
||
if (token) {
|
||
chrome.storage.local.set({ discogsToken: token }, () => {
|
||
console.log('Discogs token saved successfully to chrome.storage.local');
|
||
if (chrome.runtime.lastError) {
|
||
console.error('Error saving Discogs token:', chrome.runtime.lastError);
|
||
alert('Error saving Discogs token: ' + chrome.runtime.lastError.message);
|
||
}
|
||
});
|
||
} else {
|
||
console.log('No Discogs token to save (field empty)');
|
||
}
|
||
}
|
||
|
||
function saveGoogleSettings() {
|
||
if (!chrome || !chrome.storage) {
|
||
alert('Chrome storage not available in preview mode');
|
||
return;
|
||
}
|
||
|
||
const spreadsheetId = document.getElementById('spreadsheet-id').value;
|
||
const serviceAccountJson = document.getElementById('google-service-account-json').value;
|
||
|
||
console.log('Saving Google settings:', { spreadsheetId, hasServiceAccount: !!serviceAccountJson });
|
||
|
||
let googleSettings = {};
|
||
|
||
if (spreadsheetId) {
|
||
googleSettings.spreadsheetId = spreadsheetId;
|
||
}
|
||
|
||
if (serviceAccountJson) {
|
||
try {
|
||
const serviceAccount = JSON.parse(serviceAccountJson);
|
||
if (serviceAccount.client_email && serviceAccount.private_key) {
|
||
googleSettings.clientEmail = serviceAccount.client_email;
|
||
googleSettings.privateKey = serviceAccount.private_key;
|
||
}
|
||
} catch (error) {
|
||
console.error('Invalid JSON in service account field:', error);
|
||
alert('Invalid JSON format in Service Account JSON field');
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (Object.keys(googleSettings).length > 0) {
|
||
chrome.storage.local.set({ googleSettings: googleSettings }, () => {
|
||
console.log('Google settings saved successfully to chrome.storage.local');
|
||
if (chrome.runtime.lastError) {
|
||
console.error('Error saving Google settings:', chrome.runtime.lastError);
|
||
alert('Error saving Google settings: ' + chrome.runtime.lastError.message);
|
||
}
|
||
});
|
||
} else {
|
||
console.log('No Google settings to save (all fields empty)');
|
||
}
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
// Initialize tab functionality first
|
||
initializeTabs();
|
||
|
||
// Initialize mapping tab
|
||
initializeMappingTab();
|
||
|
||
// Initialize YouTube cookie functionality
|
||
initializeYouTubeCookieUI();
|
||
|
||
// Load auto-update Discogs price setting
|
||
const autoUpdatePriceCheckbox = document.getElementById('autoUpdateDiscogsPrice');
|
||
if (autoUpdatePriceCheckbox && chrome && chrome.storage) {
|
||
chrome.storage.local.get(['autoUpdateDiscogsPrice'], (result) => {
|
||
// Default to true if not set
|
||
autoUpdatePriceCheckbox.checked = result.autoUpdateDiscogsPrice !== false;
|
||
});
|
||
|
||
// Save setting when changed
|
||
autoUpdatePriceCheckbox.addEventListener('change', () => {
|
||
chrome.storage.local.set({ autoUpdateDiscogsPrice: autoUpdatePriceCheckbox.checked });
|
||
console.log('Auto-update Discogs price setting saved:', autoUpdatePriceCheckbox.checked);
|
||
});
|
||
}
|
||
|
||
// Load and persist QR SKU toggle; show indicator on Discogs tab
|
||
const qrUseSkuCheckbox = document.getElementById('qr-use-sku');
|
||
const skuIndicator = document.getElementById('sku-mode-indicator');
|
||
const skuIndicatorStatus = document.getElementById('sku-mode-status');
|
||
function updateSkuIndicator(on) {
|
||
if (!skuIndicator) return;
|
||
skuIndicator.style.display = 'block';
|
||
skuIndicatorStatus.textContent = on ? 'ON' : 'OFF';
|
||
skuIndicatorStatus.style.color = on ? '#28a745' : '#dc3545';
|
||
}
|
||
if (qrUseSkuCheckbox && chrome && chrome.storage) {
|
||
chrome.storage.local.get(['qrUseSku'], (result) => {
|
||
qrUseSkuCheckbox.checked = result.qrUseSku === true;
|
||
updateSkuIndicator(result.qrUseSku === true);
|
||
});
|
||
qrUseSkuCheckbox.addEventListener('change', () => {
|
||
chrome.storage.local.set({ qrUseSku: qrUseSkuCheckbox.checked });
|
||
updateSkuIndicator(qrUseSkuCheckbox.checked);
|
||
});
|
||
}
|
||
|
||
// Load enable JSON download setting
|
||
const enableJsonDownloadCheckbox = document.getElementById('enableJsonDownload');
|
||
if (enableJsonDownloadCheckbox && chrome && chrome.storage) {
|
||
chrome.storage.local.get(['enableJsonDownload'], (result) => {
|
||
// Default to true if not set
|
||
enableJsonDownloadCheckbox.checked = result.enableJsonDownload !== false;
|
||
});
|
||
|
||
// Save setting when changed
|
||
enableJsonDownloadCheckbox.addEventListener('change', () => {
|
||
chrome.storage.local.set({ enableJsonDownload: enableJsonDownloadCheckbox.checked });
|
||
console.log('Enable JSON download setting saved:', enableJsonDownloadCheckbox.checked);
|
||
});
|
||
}
|
||
|
||
// ── Settings: ALL config + RFID + visibility ─────────────────────────────
|
||
const settingsConfig = [
|
||
// [storageKey, elementId, defaultValue]
|
||
['allDoesLabel', 'allDoesLabel', true],
|
||
['allDoesSheet', 'allDoesSheet', true],
|
||
['allDoesRfid', 'allDoesRfid', true],
|
||
['allDoesJson', 'allDoesJson', true],
|
||
['allDoesPrice', 'allDoesPrice', true],
|
||
['qrUseSku', 'qrUseSku', false],
|
||
['rfidIncludeReleaseId', 'rfidIncludeReleaseId', false],
|
||
['rfidAppendR', 'rfidAppendR', false],
|
||
['btnShowLabel', 'btnShowLabel', true],
|
||
['btnShowRfid', 'btnShowRfid', true],
|
||
['btnShowSheet', 'btnShowSheet', true],
|
||
['btnShowJson', 'btnShowJson', true],
|
||
['btnShowInventory', 'btnShowInventory', true],
|
||
['btnShowLabel203', 'btnShowLabel203', false],
|
||
['enableJsonDownload', 'enableJsonDownload', true],
|
||
['stripSkuSuffix', 'stripSkuSuffix', false],
|
||
];
|
||
const allKeys = settingsConfig.map(c => c[0]);
|
||
chrome.storage.local.get(allKeys, (stored) => {
|
||
for (const [key, id, def] of settingsConfig) {
|
||
const el = document.getElementById(id);
|
||
if (!el) continue;
|
||
el.checked = stored[key] !== undefined ? stored[key] : def;
|
||
el.addEventListener('change', () => {
|
||
chrome.storage.local.set({ [key]: el.checked });
|
||
if (id.startsWith('btnShow')) applyButtonVisibility(stored);
|
||
if (id === 'stripSkuSuffix') {
|
||
window.stripSkuSuffix = el.checked;
|
||
if (state.releaseData) updatePreview(state.releaseData);
|
||
}
|
||
});
|
||
}
|
||
window.stripSkuSuffix = stored.stripSkuSuffix === true;
|
||
applyButtonVisibility(stored);
|
||
});
|
||
|
||
function applyButtonVisibility(stored) {
|
||
// Re-read live from checkboxes (they may have just changed)
|
||
const showLabel = document.getElementById('btnShowLabel')?.checked ?? true;
|
||
const showRfid = document.getElementById('btnShowRfid')?.checked ?? true;
|
||
const showSheet = document.getElementById('btnShowSheet')?.checked ?? true;
|
||
const showJson = document.getElementById('btnShowJson')?.checked ?? true;
|
||
const showInventory = document.getElementById('btnShowInventory')?.checked ?? true;
|
||
const showLabel203 = document.getElementById('btnShowLabel203')?.checked ?? false;
|
||
|
||
const setVis = (id, show) => {
|
||
const el = document.getElementById(id);
|
||
if (el) el.style.display = show ? '' : 'none';
|
||
};
|
||
setVis('printLabel', showLabel);
|
||
setVis('printLabel203', showLabel203);
|
||
setVis('rfidQuickWrite', showRfid);
|
||
setVis('updateSheet', showSheet);
|
||
setVis('writeJSON', showJson);
|
||
setVis('importWordPress', showInventory);
|
||
|
||
// Hide small row entirely if all buttons hidden
|
||
const smallRow = document.getElementById('buttons-small-row');
|
||
if (smallRow) smallRow.style.display = (showSheet || showJson || showInventory) ? '' : 'none';
|
||
}
|
||
|
||
function applyLabelPreviewVisibility() {
|
||
const size = document.getElementById('labelSize')?.value || 'large';
|
||
const previewSmall = document.querySelector('.label-preview');
|
||
const previewLarge = document.querySelector('.label-preview-large');
|
||
const previewXLarge = document.querySelector('.label-preview-xlarge');
|
||
if (previewSmall) previewSmall.style.display = size === 'small' ? '' : 'none';
|
||
if (previewLarge) previewLarge.style.display = size === 'large' ? '' : 'none';
|
||
if (previewXLarge) previewXLarge.style.display = size === 'xlarge' ? '' : 'none';
|
||
}
|
||
|
||
// ── Label settings (font/bold/italic/size + per-field styles) ────────────
|
||
const LFS_FIELDS = ['artist','title','genre','style','format','info','price','condition','comment','notes'];
|
||
|
||
function applyLabelFieldStyles(lfs) {
|
||
window.labelFieldStyles = lfs || {};
|
||
LFS_FIELDS.forEach(f => {
|
||
const s = (lfs || {})[f] || {};
|
||
const bEl = document.getElementById(`lfs-${f}-b`);
|
||
const iEl = document.getElementById(`lfs-${f}-i`);
|
||
const eEl = document.getElementById(`lfs-${f}-e`);
|
||
if (bEl) bEl.checked = !!s.b;
|
||
if (iEl) iEl.checked = !!s.i;
|
||
if (eEl) eEl.checked = !!s.e;
|
||
});
|
||
}
|
||
|
||
function readLabelFieldStyles() {
|
||
const lfs = {};
|
||
LFS_FIELDS.forEach(f => {
|
||
lfs[f] = {
|
||
b: !!(document.getElementById(`lfs-${f}-b`)?.checked),
|
||
i: !!(document.getElementById(`lfs-${f}-i`)?.checked),
|
||
e: !!(document.getElementById(`lfs-${f}-e`)?.checked),
|
||
};
|
||
});
|
||
return lfs;
|
||
}
|
||
|
||
chrome.storage.local.get(['labelFont', 'labelBold', 'labelItalic', 'labelSize', 'labelFieldStyles'], (ls) => {
|
||
const fontSel = document.getElementById('label-font-select');
|
||
const boldCb = document.getElementById('label-bold-toggle');
|
||
const italicCb = document.getElementById('label-italic-toggle');
|
||
const sizeSel = document.getElementById('labelSize');
|
||
if (fontSel && ls.labelFont) fontSel.value = ls.labelFont;
|
||
if (boldCb) boldCb.checked = ls.labelBold === true;
|
||
if (italicCb) italicCb.checked = ls.labelItalic === true;
|
||
if (sizeSel && ls.labelSize) sizeSel.value = ls.labelSize;
|
||
applyLabelPreviewVisibility();
|
||
applyLabelFieldStyles(ls.labelFieldStyles);
|
||
|
||
const onLabelChange = () => {
|
||
const lfs = readLabelFieldStyles();
|
||
window.labelFieldStyles = lfs;
|
||
chrome.storage.local.set({
|
||
labelFont: fontSel?.value,
|
||
labelBold: boldCb?.checked,
|
||
labelItalic: italicCb?.checked,
|
||
labelSize: sizeSel?.value,
|
||
labelFieldStyles: lfs,
|
||
});
|
||
if (state.releaseData) updatePreview(state.releaseData);
|
||
};
|
||
fontSel?.addEventListener('change', onLabelChange);
|
||
boldCb?.addEventListener('change', onLabelChange);
|
||
italicCb?.addEventListener('change', onLabelChange);
|
||
sizeSel?.addEventListener('change', onLabelChange);
|
||
|
||
// Wire per-field checkboxes
|
||
LFS_FIELDS.forEach(f => {
|
||
['b','i','e'].forEach(prop => {
|
||
document.getElementById(`lfs-${f}-${prop}`)?.addEventListener('change', onLabelChange);
|
||
});
|
||
});
|
||
});
|
||
|
||
// Check if chrome.tabs is available (extension context)
|
||
if (typeof chrome !== 'undefined' && chrome.tabs) {
|
||
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
|
||
if (!tabs || !tabs[0] || !tabs[0].url) {
|
||
console.error('No active tab or URL found');
|
||
return;
|
||
}
|
||
|
||
const url = tabs[0].url;
|
||
const releaseMatch = url.match(/\/release\/(\d+)/);
|
||
const masterMatch = url.match(/\/master\/(\d+)/);
|
||
const styleMatch = url.match(/\/style\/([^\/]+)/);
|
||
const isYouTube = url.includes('youtube.com');
|
||
|
||
// Determine which tab to show based on the current page
|
||
if (isYouTube) {
|
||
document.querySelector('.tab-button[data-tab="youtube"]').click();
|
||
} else if (releaseMatch || styleMatch || masterMatch) {
|
||
document.querySelector('.tab-button[data-tab="discogs"]').click();
|
||
} else {
|
||
document.querySelector('.tab-button[data-tab="settings"]').click();
|
||
}
|
||
|
||
if (styleMatch) {
|
||
document.getElementById('stylePageContainer').style.display = 'block';
|
||
} else if (releaseMatch) {
|
||
fetchAndDisplayReleaseData(releaseMatch[1]);
|
||
} else if (masterMatch) {
|
||
showMasterPageStatus(masterMatch[1], tabs[0].id);
|
||
}
|
||
});
|
||
|
||
loadWpSettings();
|
||
loadGoogleSettings();
|
||
loadInventorySettings();
|
||
loadDiscogsToken();
|
||
(function () { var el = document.getElementById('extension-id-display'); if (el) { if (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.id) { el.textContent = chrome.runtime.id; } } })();
|
||
// Copy Extension ID button
|
||
(function () {
|
||
var btn = document.getElementById('copy-extension-id');
|
||
if (btn) {
|
||
btn.addEventListener('click', async function () {
|
||
var id = (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.id) ? chrome.runtime.id : (document.getElementById('extension-id-display').textContent || '').trim();
|
||
try {
|
||
await navigator.clipboard.writeText(id);
|
||
var status = document.getElementById('copy-extension-id-status');
|
||
if (status) {
|
||
status.textContent = 'Copied!';
|
||
status.style.display = 'inline';
|
||
setTimeout(function () { status.style.display = 'none'; }, 1500);
|
||
}
|
||
} catch (e) {
|
||
try {
|
||
var temp = document.createElement('textarea');
|
||
temp.value = id;
|
||
document.body.appendChild(temp);
|
||
temp.select();
|
||
document.execCommand('copy');
|
||
document.body.removeChild(temp);
|
||
var status2 = document.getElementById('copy-extension-id-status');
|
||
if (status2) {
|
||
status2.textContent = 'Copied!';
|
||
status2.style.display = 'inline';
|
||
setTimeout(function () { status2.style.display = 'none'; }, 1500);
|
||
}
|
||
} catch (err2) {
|
||
console.error('Clipboard copy failed:', err2);
|
||
alert('Could not copy extension ID. Please copy manually.');
|
||
}
|
||
}
|
||
});
|
||
}
|
||
})();
|
||
}
|
||
|
||
// Add all event listeners inside DOMContentLoaded to ensure DOM elements exist
|
||
document.getElementById('settings-toggle-connect').addEventListener('click', () => {
|
||
document.getElementById('settings-content-connect').style.display = document.getElementById('settings-content-connect').style.display === 'none' ? 'block' : 'none';
|
||
});
|
||
|
||
document.getElementById('save-connect-settings').addEventListener('click', () => {
|
||
console.log('Save Connect Settings button clicked!');
|
||
|
||
// Save Google Settings
|
||
if (!chrome || !chrome.storage) {
|
||
alert('Chrome storage not available in preview mode');
|
||
return;
|
||
}
|
||
|
||
const spreadsheetId = document.getElementById('spreadsheet-id').value;
|
||
const serviceAccountJson = document.getElementById('google-service-account-json').value;
|
||
|
||
console.log('Saving Google settings:', { spreadsheetId, hasServiceAccount: !!serviceAccountJson });
|
||
|
||
let googleSettings = {};
|
||
|
||
if (spreadsheetId) {
|
||
googleSettings.spreadsheetId = spreadsheetId;
|
||
}
|
||
|
||
if (serviceAccountJson) {
|
||
try {
|
||
const serviceAccount = JSON.parse(serviceAccountJson);
|
||
if (serviceAccount.client_email && serviceAccount.private_key) {
|
||
googleSettings.clientEmail = serviceAccount.client_email;
|
||
googleSettings.privateKey = serviceAccount.private_key;
|
||
}
|
||
} catch (error) {
|
||
console.error('Invalid JSON in service account field:', error);
|
||
alert('Invalid JSON format in Service Account JSON field');
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (Object.keys(googleSettings).length > 0) {
|
||
chrome.storage.local.set({ googleSettings: googleSettings }, () => {
|
||
console.log('Google settings saved successfully to chrome.storage.local');
|
||
if (chrome.runtime.lastError) {
|
||
console.error('Error saving Google settings:', chrome.runtime.lastError);
|
||
alert('Error saving Google settings: ' + chrome.runtime.lastError.message);
|
||
} else {
|
||
alert('Google settings saved successfully!');
|
||
}
|
||
});
|
||
} else {
|
||
console.log('No Google settings to save (all fields empty)');
|
||
}
|
||
|
||
// Save Discogs Token
|
||
const discogsToken = document.getElementById('discogs-token').value.trim();
|
||
if (discogsToken) {
|
||
chrome.storage.local.set({ discogsToken: discogsToken }, () => {
|
||
console.log('Discogs token saved successfully to chrome.storage.local');
|
||
if (chrome.runtime.lastError) {
|
||
console.error('Error saving Discogs token:', chrome.runtime.lastError);
|
||
alert('Error saving Discogs token: ' + chrome.runtime.lastError.message);
|
||
}
|
||
});
|
||
} else {
|
||
console.log('No Discogs token to save (field empty)');
|
||
}
|
||
|
||
// Save Inventory SKU Lookup settings
|
||
const inventorySettings = {
|
||
enabled: document.getElementById('inventorySkuEnabled').checked,
|
||
dbHost: document.getElementById('inventory-db-host').value.trim() || '100.123.123.64',
|
||
dbName: document.getElementById('inventory-db-name').value.trim() || 'wp_rmp_disc_',
|
||
dbUser: document.getElementById('inventory-db-user').value.trim(),
|
||
dbPass: document.getElementById('inventory-db-pass').value.trim(),
|
||
sshKeyPath: document.getElementById('inventory-ssh-key').value.trim() || '~/.ssh/id_rsa'
|
||
};
|
||
chrome.storage.local.set({ inventorySettings }, () => {
|
||
if (chrome.runtime.lastError) {
|
||
console.error('Error saving inventory settings:', chrome.runtime.lastError);
|
||
alert('Error saving inventory settings: ' + chrome.runtime.lastError.message);
|
||
} else {
|
||
console.log('Inventory settings saved successfully.');
|
||
}
|
||
});
|
||
});
|
||
|
||
// MONSTERWIKI settings handlers
|
||
document.getElementById('settings-toggle-monster').addEventListener('click', () => {
|
||
const el = document.getElementById('settings-content-monster');
|
||
el.style.display = el.style.display === 'none' ? 'block' : 'none';
|
||
});
|
||
|
||
document.getElementById('save-monster-settings').addEventListener('click', () => {
|
||
const enabled = document.getElementById('monster-enabled').checked;
|
||
const autoCapture = document.getElementById('monster-autocapture')?.checked === true;
|
||
const preferLocalhost = document.getElementById('monster-prefer-localhost')?.checked === true;
|
||
const serverUrl = document.getElementById('monster-server-url').value.trim()
|
||
|| 'http://100.91.239.7:5002';
|
||
chrome.storage.local.set({ monsterSettings: { enabled, autoCapture, preferLocalhost, serverUrl } }, () => {
|
||
document.getElementById('monster-status').textContent = 'Saved.';
|
||
setTimeout(() => { document.getElementById('monster-status').textContent = ''; }, 2000);
|
||
});
|
||
});
|
||
|
||
document.getElementById('test-monster-connection').addEventListener('click', async () => {
|
||
const statusEl = document.getElementById('monster-status');
|
||
statusEl.textContent = 'Testing…';
|
||
const cfg = await getMonsterSettings();
|
||
const base = (cfg.serverUrl || 'http://100.91.239.7:5002').replace(/\/$/, '');
|
||
try {
|
||
const r = await fetch(`${base}/plice/health`, { signal: AbortSignal.timeout(5000) });
|
||
statusEl.textContent = r.ok ? '✓ Connected' : `✗ HTTP ${r.status}`;
|
||
} catch (e) {
|
||
statusEl.textContent = `✗ ${e.message.includes('abort') ? 'Timeout' : 'Unreachable'}`;
|
||
}
|
||
});
|
||
|
||
// Load saved MONSTERWIKI settings into fields
|
||
chrome.storage.local.get(['monsterSettings'], r => {
|
||
const cfg = r.monsterSettings || {};
|
||
if (document.getElementById('monster-enabled'))
|
||
document.getElementById('monster-enabled').checked = cfg.enabled === true;
|
||
if (document.getElementById('monster-autocapture'))
|
||
document.getElementById('monster-autocapture').checked = cfg.autoCapture === true;
|
||
if (document.getElementById('monster-prefer-localhost'))
|
||
document.getElementById('monster-prefer-localhost').checked = cfg.preferLocalhost === true;
|
||
if (document.getElementById('monster-server-url'))
|
||
document.getElementById('monster-server-url').value = cfg.serverUrl || 'http://100.91.239.7:5002';
|
||
});
|
||
|
||
const geminiToggle = document.getElementById('settings-toggle-gemini');
|
||
if (geminiToggle) {
|
||
geminiToggle.addEventListener('click', () => {
|
||
const el = document.getElementById('settings-content-gemini');
|
||
el.style.display = el.style.display === 'none' ? 'block' : 'none';
|
||
});
|
||
}
|
||
|
||
const geminiTemplateEl = document.getElementById('gemini-prompt-template');
|
||
const geminiStatusEl = document.getElementById('gemini-prompt-status');
|
||
const geminiCloseTabEl = document.getElementById('gemini-close-tab');
|
||
const geminiAutoRunEl = document.getElementById('gemini-auto-run');
|
||
const aiProviderEl = document.getElementById('ai-provider');
|
||
|
||
chrome.storage.local.get(['geminiPromptTemplate', 'geminiCloseTabOnSuccess', 'geminiAutoRunOnPopup', 'aiProvider'], r => {
|
||
if (!geminiTemplateEl) return;
|
||
geminiTemplateEl.value = (r.geminiPromptTemplate && typeof r.geminiPromptTemplate === 'string')
|
||
? r.geminiPromptTemplate
|
||
: defaultGeminiPromptTemplate();
|
||
if (geminiCloseTabEl) geminiCloseTabEl.checked = r.geminiCloseTabOnSuccess !== false;
|
||
if (geminiAutoRunEl) geminiAutoRunEl.checked = r.geminiAutoRunOnPopup === true;
|
||
if (aiProviderEl) aiProviderEl.value = (r.aiProvider === 'deepseek') ? 'deepseek' : 'gemini';
|
||
});
|
||
|
||
if (geminiCloseTabEl) {
|
||
geminiCloseTabEl.addEventListener('change', () => {
|
||
chrome.storage.local.set({ geminiCloseTabOnSuccess: geminiCloseTabEl.checked === true });
|
||
});
|
||
}
|
||
|
||
if (geminiAutoRunEl) {
|
||
geminiAutoRunEl.addEventListener('change', () => {
|
||
chrome.storage.local.set({ geminiAutoRunOnPopup: geminiAutoRunEl.checked === true }, () => {
|
||
if (geminiStatusEl) geminiStatusEl.textContent = geminiAutoRunEl.checked ? 'Auto-run enabled.' : 'Auto-run disabled.';
|
||
setTimeout(() => { if (geminiStatusEl) geminiStatusEl.textContent = ''; }, 1500);
|
||
});
|
||
});
|
||
}
|
||
|
||
if (aiProviderEl) {
|
||
aiProviderEl.addEventListener('change', () => {
|
||
const v = aiProviderEl.value === 'deepseek' ? 'deepseek' : 'gemini';
|
||
chrome.storage.local.set({ aiProvider: v }, () => {
|
||
if (geminiStatusEl) geminiStatusEl.textContent = `Provider: ${v}`;
|
||
setTimeout(() => { if (geminiStatusEl) geminiStatusEl.textContent = ''; }, 1500);
|
||
});
|
||
});
|
||
}
|
||
|
||
const saveGeminiBtn = document.getElementById('save-gemini-prompt');
|
||
if (saveGeminiBtn) {
|
||
saveGeminiBtn.addEventListener('click', () => {
|
||
if (!geminiTemplateEl) return;
|
||
const tpl = geminiTemplateEl.value || '';
|
||
const closeOnSuccess = geminiCloseTabEl ? geminiCloseTabEl.checked === true : true;
|
||
const autoRun = geminiAutoRunEl ? geminiAutoRunEl.checked === true : false;
|
||
chrome.storage.local.set({ geminiPromptTemplate: tpl, geminiCloseTabOnSuccess: closeOnSuccess, geminiAutoRunOnPopup: autoRun }, () => {
|
||
if (geminiStatusEl) geminiStatusEl.textContent = 'Saved.';
|
||
setTimeout(() => { if (geminiStatusEl) geminiStatusEl.textContent = ''; }, 2000);
|
||
});
|
||
});
|
||
}
|
||
|
||
const resetGeminiBtn = document.getElementById('reset-gemini-prompt');
|
||
if (resetGeminiBtn) {
|
||
resetGeminiBtn.addEventListener('click', () => {
|
||
if (!geminiTemplateEl) return;
|
||
geminiTemplateEl.value = defaultGeminiPromptTemplate();
|
||
const closeOnSuccess = geminiCloseTabEl ? geminiCloseTabEl.checked === true : true;
|
||
const autoRun = geminiAutoRunEl ? geminiAutoRunEl.checked === true : false;
|
||
chrome.storage.local.set({ geminiPromptTemplate: geminiTemplateEl.value, geminiCloseTabOnSuccess: closeOnSuccess, geminiAutoRunOnPopup: autoRun }, () => {
|
||
if (geminiStatusEl) geminiStatusEl.textContent = 'Reset.';
|
||
setTimeout(() => { if (geminiStatusEl) geminiStatusEl.textContent = ''; }, 2000);
|
||
});
|
||
});
|
||
}
|
||
|
||
chrome.storage.onChanged.addListener((changes, areaName) => {
|
||
if (areaName !== 'local') return;
|
||
if (!changes.geminiJob) return;
|
||
const job = changes.geminiJob.newValue;
|
||
if (!job) return;
|
||
if (state.releaseData?.id && job.release_id && job.release_id !== state.releaseData.id) return;
|
||
const prov = job.provider === 'deepseek' ? 'DeepSeek' : 'Gemini';
|
||
|
||
if (job.status === 'posting') {
|
||
geminiSetIndicator(`${prov} saving…`, '#888');
|
||
} else if (job.status === 'done') {
|
||
geminiSetIndicator(`✓ ${prov} done`, '#28a745', 2500);
|
||
} else if (job.status === 'error') {
|
||
geminiSetIndicator(`✗ ${prov}: ${job.error || 'error'}`, '#dc3545');
|
||
}
|
||
});
|
||
|
||
document.getElementById('settings-toggle-wowplatter').addEventListener('click', () => {
|
||
document.getElementById('settings-content-wowplatter').style.display = document.getElementById('settings-content-wowplatter').style.display === 'none' ? 'block' : 'none';
|
||
});
|
||
|
||
document.getElementById('save-wp-settings').addEventListener('click', () => {
|
||
saveWpSettings();
|
||
// Note: saveWpSettings() already shows an alert, so we don't need another one
|
||
});
|
||
|
||
document.getElementById('test-wp-connection').addEventListener('click', testWpConnection);
|
||
|
||
// Keep the old save-google-settings for backward compatibility (if needed)
|
||
document.getElementById('save-google-settings')?.addEventListener('click', () => {
|
||
if (!chrome || !chrome.storage) {
|
||
alert('Chrome storage not available in preview mode');
|
||
return;
|
||
}
|
||
|
||
const spreadsheetId = document.getElementById('spreadsheet-id').value;
|
||
const jsonText = document.getElementById('google-service-account-json').value;
|
||
try {
|
||
const serviceAccount = JSON.parse(jsonText);
|
||
chrome.storage.local.set({
|
||
googleSettings: {
|
||
spreadsheetId,
|
||
clientEmail: serviceAccount.client_email,
|
||
privateKey: serviceAccount.private_key
|
||
}
|
||
}, () => alert('Google settings saved!'));
|
||
} catch (error) {
|
||
alert(`Error parsing service account JSON: ${error.message}`);
|
||
}
|
||
});
|
||
|
||
// Mapping tab event listeners
|
||
document.getElementById('save-mappings')?.addEventListener('click', saveMappings);
|
||
document.getElementById('load-mappings')?.addEventListener('click', loadMappings);
|
||
document.getElementById('reset-mappings')?.addEventListener('click', resetMappings);
|
||
document.getElementById('detect-sheet')?.addEventListener('click', detectSheetStructureUI);
|
||
document.getElementById('auto-map-headers')?.addEventListener('click', autoMapHeaders);
|
||
document.getElementById('toggle-manual-range')?.addEventListener('click', toggleManualRange);
|
||
});
|
||
|
||
function loadGoogleSettings() {
|
||
if (!chrome || !chrome.storage) {
|
||
console.log('Chrome storage not available in preview mode');
|
||
return;
|
||
}
|
||
|
||
chrome.storage.local.get('googleSettings', (data) => {
|
||
console.log('Loading Google settings:', data.googleSettings);
|
||
if (data.googleSettings) {
|
||
document.getElementById('spreadsheet-id').value = data.googleSettings.spreadsheetId || '';
|
||
const serviceAccount = {
|
||
client_email: data.googleSettings.clientEmail,
|
||
private_key: data.googleSettings.privateKey
|
||
};
|
||
if (data.googleSettings.clientEmail && data.googleSettings.privateKey) {
|
||
document.getElementById('google-service-account-json').value = JSON.stringify(serviceAccount, null, 2);
|
||
}
|
||
console.log('Google settings loaded successfully');
|
||
} else {
|
||
console.log('No Google settings found in storage');
|
||
}
|
||
});
|
||
}
|
||
|
||
function loadDiscogsToken() {
|
||
if (!chrome || !chrome.storage) {
|
||
console.log('Chrome storage not available in preview mode');
|
||
return;
|
||
}
|
||
|
||
chrome.storage.local.get('discogsToken', (data) => {
|
||
console.log('Loading Discogs token:', { hasToken: !!data.discogsToken });
|
||
if (data.discogsToken) {
|
||
document.getElementById('discogs-token').value = (data.discogsToken || '').trim();
|
||
console.log('Discogs token loaded successfully');
|
||
} else {
|
||
console.log('No Discogs token found in storage');
|
||
}
|
||
});
|
||
}
|
||
|
||
function loadInventorySettings() {
|
||
if (!chrome || !chrome.storage) {
|
||
console.log('Chrome storage not available in preview mode');
|
||
return;
|
||
}
|
||
chrome.storage.local.get(['inventorySettings'], (result) => {
|
||
const s = result.inventorySettings || {};
|
||
const el = (id) => document.getElementById(id);
|
||
if (el('inventorySkuEnabled')) el('inventorySkuEnabled').checked = !!s.enabled;
|
||
if (el('inventory-db-host')) el('inventory-db-host').value = s.dbHost || '100.123.123.64';
|
||
if (el('inventory-db-name')) el('inventory-db-name').value = s.dbName || 'wp_rmp_disc_';
|
||
if (el('inventory-db-user')) el('inventory-db-user').value = s.dbUser || '';
|
||
if (el('inventory-db-pass')) el('inventory-db-pass').value = s.dbPass || '';
|
||
if (el('inventory-ssh-key')) el('inventory-ssh-key').value = s.sshKeyPath || '~/.ssh/id_rsa';
|
||
console.log('Inventory settings loaded successfully');
|
||
});
|
||
}
|
||
|
||
function makeEditable(field) {
|
||
const valueElement = field.querySelector('.value');
|
||
if (valueElement && valueElement.getAttribute('contenteditable') === 'true') {
|
||
valueElement.addEventListener('input', () => {
|
||
const fieldName = field.dataset.field;
|
||
if (state.releaseData) {
|
||
state.releaseData[fieldName] = valueElement.textContent;
|
||
updatePreview(state.releaseData);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
function setupEventListeners() {
|
||
const elements = {
|
||
priceInput: document.getElementById('price'),
|
||
priceCheckboxes: document.querySelectorAll('.price-checkbox'),
|
||
sleeveCondition: document.getElementById('sleeveCondition'),
|
||
mediaCondition: document.getElementById('mediaCondition'),
|
||
commentInput: document.getElementById('commentInput'),
|
||
labelSize: document.getElementById('labelSize'),
|
||
};
|
||
|
||
const updateState = (key, value) => {
|
||
if (state.releaseData) {
|
||
state.releaseData[key] = value;
|
||
updatePreview(state.releaseData);
|
||
}
|
||
};
|
||
|
||
elements.priceInput.addEventListener('input', () => {
|
||
document.querySelectorAll('.price-checkbox').forEach(cb => cb.checked = false);
|
||
updateState('price', elements.priceInput.value);
|
||
});
|
||
|
||
// Also update on focus/blur/keyup/paste to ensure we catch all changes
|
||
elements.priceInput.addEventListener('focus', () => {
|
||
updateState('price', elements.priceInput.value);
|
||
});
|
||
|
||
elements.priceInput.addEventListener('blur', () => {
|
||
updateState('price', elements.priceInput.value);
|
||
});
|
||
|
||
elements.priceInput.addEventListener('keyup', () => {
|
||
updateState('price', elements.priceInput.value);
|
||
});
|
||
|
||
elements.priceInput.addEventListener('paste', () => {
|
||
setTimeout(() => updateState('price', elements.priceInput.value), 10);
|
||
});
|
||
|
||
elements.priceCheckboxes.forEach(checkbox => {
|
||
checkbox.addEventListener('change', () => {
|
||
if (checkbox.checked) {
|
||
elements.priceCheckboxes.forEach(cb => { if (cb !== checkbox) cb.checked = false; });
|
||
elements.priceInput.value = checkbox.value;
|
||
updateState('price', checkbox.value);
|
||
}
|
||
});
|
||
});
|
||
|
||
elements.mediaCondition.addEventListener('change', () => updateState('mediaCondition', elements.mediaCondition.value));
|
||
elements.sleeveCondition.addEventListener('change', () => updateState('sleeveCondition', elements.sleeveCondition.value));
|
||
// Update comment on input, focus, blur, keyup and paste to ensure we catch all changes
|
||
elements.commentInput.addEventListener('input', () => updateState('comment', elements.commentInput.textContent));
|
||
elements.commentInput.addEventListener('focus', () => updateState('comment', elements.commentInput.textContent));
|
||
elements.commentInput.addEventListener('blur', () => updateState('comment', elements.commentInput.textContent));
|
||
elements.commentInput.addEventListener('keyup', () => updateState('comment', elements.commentInput.textContent));
|
||
elements.commentInput.addEventListener('paste', () => {
|
||
setTimeout(() => updateState('comment', elements.commentInput.textContent), 10);
|
||
});
|
||
elements.labelSize.addEventListener('change', () => { applyLabelPreviewVisibility(); updatePreview(state.releaseData); });
|
||
|
||
// QR SKU toggle: switch between release_id and SKU for preview
|
||
const qrSkuToggle = document.getElementById('qr-use-sku');
|
||
if (qrSkuToggle) {
|
||
qrSkuToggle.addEventListener('change', () => {
|
||
if (!state.releaseData) return;
|
||
if (qrSkuToggle.checked) {
|
||
// Prefer existing Discogs SKU (reprint), else use the one generated at popup open
|
||
state.releaseData.sku = state.releaseData.collectionSku || state.sku;
|
||
} else {
|
||
delete state.releaseData.sku;
|
||
}
|
||
updatePreview(state.releaseData);
|
||
});
|
||
}
|
||
|
||
document.querySelectorAll('.info-field').forEach(makeEditable);
|
||
|
||
document.getElementById('printLabel').addEventListener('click', () => generateLabel(state.releaseData));
|
||
document.getElementById('printLabel203').addEventListener('click', () => generateLabel203dpi());
|
||
document.getElementById('writeJSON').addEventListener('click', async (e) => {
|
||
if (e.shiftKey) {
|
||
// Shift+click = upload JSON and merge into state
|
||
const data = await uploadJSON();
|
||
if (data) {
|
||
state.releaseData = { ...state.releaseData, ...data };
|
||
displayReleaseInfo(state.releaseData);
|
||
updatePreview(state.releaseData);
|
||
updateResearchTab();
|
||
showStatusMessage('allStatus', 'JSON loaded from file', true);
|
||
}
|
||
} else {
|
||
writeJSON(state.releaseData);
|
||
}
|
||
});
|
||
|
||
// BP JSON button — copy current Beatport token JSON to clipboard
|
||
const bpJsonBtn = document.getElementById('copyBeatportJson');
|
||
if (bpJsonBtn) {
|
||
bpJsonBtn.addEventListener('click', () => {
|
||
chrome.storage.local.get(['beatportToken'], (result) => {
|
||
const tokenJson = result.beatportToken;
|
||
if (!tokenJson) {
|
||
showStatusMessage('allStatus', 'No Beatport token saved — fetch it in Settings first', false);
|
||
return;
|
||
}
|
||
navigator.clipboard.writeText(tokenJson).then(() => {
|
||
const orig = bpJsonBtn.textContent;
|
||
bpJsonBtn.textContent = 'Copied!';
|
||
setTimeout(() => { bpJsonBtn.textContent = orig; }, 1500);
|
||
}).catch(() => {
|
||
showStatusMessage('allStatus', 'Clipboard write failed', false);
|
||
});
|
||
});
|
||
});
|
||
}
|
||
document.getElementById('updateSheet').addEventListener('click', async () => {
|
||
// console.log('=== GOOGLE SHEETS BUTTON CLICKED ===');
|
||
// console.log('Current state.releaseData:', state.releaseData);
|
||
|
||
if (state.releaseData) {
|
||
const result = await updateSheetOnly(state.releaseData);
|
||
showStatusMessage('sheetStatus', result.message, result.success);
|
||
}
|
||
});
|
||
document.getElementById('bothActions').addEventListener('click', async () => await handleAllActions(state.releaseData));
|
||
|
||
// RFID quick-READ from Discogs tab — shows what's currently on the tag without writing
|
||
const rfidReadBtn = document.getElementById('rfidQuickRead');
|
||
if (rfidReadBtn) {
|
||
rfidReadBtn.addEventListener('click', async () => {
|
||
const rfidEl = document.getElementById('rfidStatus');
|
||
if (rfidEl) {
|
||
rfidEl.style.color = '#888';
|
||
rfidEl.style.whiteSpace = 'pre-line';
|
||
rfidEl.style.fontSize = '11px';
|
||
rfidEl.style.lineHeight = '1.4';
|
||
rfidEl.textContent = 'Reading tag…';
|
||
}
|
||
rfidReadBtn.disabled = true;
|
||
const orig = rfidReadBtn.textContent;
|
||
rfidReadBtn.textContent = '…';
|
||
const tag = await readRfidTagOnce();
|
||
rfidReadBtn.disabled = false;
|
||
rfidReadBtn.textContent = orig;
|
||
if (rfidEl) {
|
||
if (!tag) {
|
||
rfidEl.textContent = '✗ No tag in field (or daemon not running)';
|
||
rfidEl.style.color = '#dc3545';
|
||
} else {
|
||
// Compare against the current Discogs page if available
|
||
const expectedSku = (state.releaseData?.collectionSku || state.sku || '').replace(/-[AR]$/i, '');
|
||
const expectedRel = state.releaseData?.id || null;
|
||
const matchSku = expectedSku && tag.sku === expectedSku;
|
||
const matchRel = expectedRel && tag.releaseId == expectedRel;
|
||
let line2 = '';
|
||
if (expectedSku) {
|
||
line2 = `\nexpect: ${expectedSku}${expectedRel ? ' + rel ' + expectedRel : ''}`;
|
||
if (matchSku && matchRel) line2 += ' ✓ MATCH';
|
||
else if (matchSku) line2 += ' ⚠ sku ok, rel mismatch';
|
||
else line2 += ' ✗ DIFFERENT';
|
||
}
|
||
rfidEl.textContent = `tag: ${formatTag(tag)}${line2}`;
|
||
rfidEl.style.color = (matchSku && matchRel) ? '#28a745' : (matchSku ? '#d29922' : '#888');
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// RFID quick-write from Discogs tab
|
||
const rfidQuickBtn = document.getElementById('rfidQuickWrite');
|
||
if (rfidQuickBtn) {
|
||
rfidQuickBtn.addEventListener('click', async () => {
|
||
const rawSku = (state.releaseData?.collectionSku) || state.sku;
|
||
if (!rawSku) {
|
||
showStatusMessage('allStatus', 'No SKU — open a Discogs release first', false);
|
||
return;
|
||
}
|
||
const sku = rawSku ? rawSku.replace(/-[AR]$/i, '') : rawSku;
|
||
rfidQuickBtn.disabled = true;
|
||
const orig = rfidQuickBtn.textContent;
|
||
rfidQuickBtn.textContent = '…';
|
||
const releaseId = state.releaseData?.id || null;
|
||
const rfidEl = document.getElementById('rfidStatus');
|
||
if (rfidEl) { rfidEl.style.color = '#888'; rfidEl.style.whiteSpace = 'pre-line'; rfidEl.style.fontSize = '11px'; rfidEl.style.lineHeight = '1.4'; }
|
||
|
||
// 1. PRE-SCAN — what's on the tag right now?
|
||
if (rfidEl) rfidEl.textContent = 'Reading current tag…';
|
||
const before = await readRfidTagOnce();
|
||
if (rfidEl && before) {
|
||
rfidEl.textContent = `pre: ${formatTag(before)}\nwriting…`;
|
||
}
|
||
|
||
// 2. WRITE
|
||
const result = await writeRfidTag(sku, releaseId);
|
||
|
||
// 3. POST-SCAN — confirm what's on the tag now
|
||
const after = result?.ok ? await readRfidTagOnce() : null;
|
||
|
||
rfidQuickBtn.disabled = false;
|
||
rfidQuickBtn.textContent = orig;
|
||
if (rfidEl) {
|
||
if (!result) {
|
||
rfidEl.textContent = `pre: ${before ? formatTag(before) : '?'}\n✗ RFID: daemon not running`;
|
||
rfidEl.style.color = '#dc3545';
|
||
} else if (result.ok) {
|
||
rfidEl.textContent = `pre: ${before ? formatTag(before) : '?'}\n✓ wrote ${sku}${releaseId ? ' + rel ' + releaseId : ''}\npost: ${after ? formatTag(after) : '?'}`;
|
||
rfidEl.style.color = '#28a745';
|
||
} else {
|
||
rfidEl.textContent = `pre: ${before ? formatTag(before) : '?'}\n✗ RFID: ${result.error}`;
|
||
rfidEl.style.color = '#dc3545';
|
||
}
|
||
}
|
||
if (result?.ok) {
|
||
const appendR = await new Promise(resolve => chrome.storage.local.get(['rfidAppendR'], r => resolve(r.rfidAppendR === true)));
|
||
if (appendR && state.discogsTabId) {
|
||
try {
|
||
await chrome.tabs.sendMessage(state.discogsTabId, { action: 'updateDiscogsSku', sku: sku + '-R', boxIndex: state.collectionBoxIndex });
|
||
} catch (e) { console.warn('[RFID] -R SKU update failed:', e.message); }
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Website Inventory Sync
|
||
const importBtn = document.getElementById('importWordPress');
|
||
if (importBtn) {
|
||
importBtn.addEventListener('click', handleImportInventory);
|
||
}
|
||
}
|
||
|
||
// Function to update button states based on available settings
|
||
async function updateButtonStates() {
|
||
if (!chrome || !chrome.storage) {
|
||
return; // Skip in preview mode
|
||
}
|
||
|
||
return new Promise((resolve) => {
|
||
chrome.storage.local.get(['discogsToken', 'googleSettings', 'wpSettings'], (result) => {
|
||
const hasDiscogsToken = result.discogsToken && result.discogsToken.trim() !== '';
|
||
const hasGoogleSettings = result.googleSettings &&
|
||
result.googleSettings.spreadsheetId &&
|
||
result.googleSettings.clientEmail &&
|
||
result.googleSettings.privateKey;
|
||
const hasWpSettings = result.wpSettings &&
|
||
result.wpSettings.endpoint && result.wpSettings.endpoint.trim() !== '' &&
|
||
result.wpSettings.key && result.wpSettings.key.trim() !== '' &&
|
||
result.wpSettings.secret && result.wpSettings.secret.trim() !== '';
|
||
|
||
// Update Google Sheets button
|
||
const sheetButton = document.getElementById('updateSheet');
|
||
const bothButton = document.getElementById('bothActions');
|
||
|
||
if (sheetButton) {
|
||
if (hasGoogleSettings) {
|
||
sheetButton.disabled = false;
|
||
sheetButton.style.opacity = '1';
|
||
sheetButton.title = 'Update Google Sheets';
|
||
} else {
|
||
sheetButton.disabled = true;
|
||
sheetButton.style.opacity = '0.5';
|
||
sheetButton.title = 'Google Sheets not configured - go to Settings tab';
|
||
}
|
||
}
|
||
|
||
// Update "Both Actions" button behavior
|
||
if (bothButton) {
|
||
if (hasGoogleSettings) {
|
||
bothButton.title = 'Print Label + Update Google Sheets';
|
||
} else {
|
||
bothButton.title = 'Print Label + Save JSON (Google Sheets not configured)';
|
||
}
|
||
}
|
||
|
||
// console.log('Button states updated:', { hasDiscogsToken, hasGoogleSettings, hasWpSettings });
|
||
resolve();
|
||
});
|
||
});
|
||
}
|
||
|
||
// Settings validation functions
|
||
async function checkSettingsConfigured() {
|
||
// Check if we're in an extension context
|
||
if (!chrome || !chrome.storage) {
|
||
// console.log('Not in extension context, assuming settings not configured');
|
||
return false;
|
||
}
|
||
|
||
return new Promise((resolve) => {
|
||
chrome.storage.local.get(['discogsToken', 'googleSettings', 'wpSettings'], (result) => {
|
||
// console.log('=== DETAILED SETTINGS DEBUG ===');
|
||
// console.log('Raw storage result:', result);
|
||
// console.log('discogsToken value:', result.discogsToken);
|
||
// console.log('googleSettings value:', result.googleSettings);
|
||
// console.log('wpSettings value:', result.wpSettings);
|
||
|
||
const hasDiscogsToken = result.discogsToken && result.discogsToken.trim() !== '';
|
||
// console.log('hasDiscogsToken check:', hasDiscogsToken);
|
||
|
||
const hasGoogleSettings = result.googleSettings &&
|
||
result.googleSettings.spreadsheetId &&
|
||
result.googleSettings.clientEmail &&
|
||
result.googleSettings.privateKey;
|
||
// console.log('hasGoogleSettings check:', hasGoogleSettings);
|
||
if (result.googleSettings) {
|
||
// console.log('Google settings breakdown:', {
|
||
// spreadsheetId: result.googleSettings.spreadsheetId,
|
||
// clientEmail: result.googleSettings.clientEmail,
|
||
// privateKey: result.googleSettings.privateKey ? 'PRESENT' : 'MISSING'
|
||
// });
|
||
}
|
||
|
||
const hasWpSettings = result.wpSettings &&
|
||
result.wpSettings.endpoint && result.wpSettings.endpoint.trim() !== '' &&
|
||
result.wpSettings.key && result.wpSettings.key.trim() !== '' &&
|
||
result.wpSettings.secret && result.wpSettings.secret.trim() !== '';
|
||
// console.log('hasWpSettings check:', hasWpSettings);
|
||
|
||
// Discogs token is essential - that's the minimum requirement
|
||
// Google Sheets and WordPress are optional enhancements
|
||
const hasValidSettings = hasDiscogsToken;
|
||
// console.log('Final validation result:', { hasDiscogsToken, hasGoogleSettings, hasWpSettings, hasValidSettings });
|
||
// console.log('Settings validation: Discogs token is the only requirement');
|
||
// console.log('=== END SETTINGS DEBUG ===');
|
||
resolve(hasValidSettings);
|
||
});
|
||
});
|
||
}
|
||
|
||
function displayStaticInterface() {
|
||
const container = document.querySelector('.container');
|
||
if (container) {
|
||
container.innerHTML = `
|
||
<div style="padding: 20px; text-align: center;">
|
||
<h3>⚙️ Settings Required</h3>
|
||
<p>Please configure your Discogs token to use the extension:</p>
|
||
<ul style="text-align: left; margin: 15px 0;">
|
||
<li><strong>Discogs Token:</strong> Required for API access</li>
|
||
<li><strong>Google Sheets:</strong> Optional - for logging data to spreadsheets</li>
|
||
<li><strong>WordPress:</strong> Optional - for additional integrations</li>
|
||
</ul>
|
||
<p>Click the settings button below to get started.</p>
|
||
<div style="margin-top: 20px;">
|
||
<button id="open-settings-btn" style="padding: 10px 20px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
||
Open Settings
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// Add event listener for the settings button
|
||
document.getElementById('open-settings-btn').addEventListener('click', () => {
|
||
document.getElementById('settings-toggle-connect').click();
|
||
});
|
||
}
|
||
}
|
||
|
||
function displayLimitedInterface() {
|
||
const container = document.querySelector('.container');
|
||
if (container) {
|
||
container.innerHTML = `
|
||
<div style="padding: 20px; text-align: center;">
|
||
<h3>🔧 Extension Ready</h3>
|
||
<p>Your settings are configured! However, some features require being on a Discogs release page.</p>
|
||
<div style="margin: 20px 0; padding: 15px; background: #f0f8ff; border-radius: 8px; text-align: left;">
|
||
<h4>✅ Available Features:</h4>
|
||
<ul>
|
||
<li>View and edit your settings</li>
|
||
<li>YouTube cookie management</li>
|
||
</ul>
|
||
<h4>📋 To use full features:</h4>
|
||
<ul>
|
||
<li>Navigate to a Discogs release page</li>
|
||
<li>Click the extension icon</li>
|
||
<li>Use Print Label, Save JSON, and Update Sheet buttons</li>
|
||
</ul>
|
||
</div>
|
||
<div style="margin-top: 20px;">
|
||
<button id="open-settings-btn" style="padding: 10px 20px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; margin-right: 10px;">
|
||
Open Settings
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// Add event listener for the settings button
|
||
document.getElementById('open-settings-btn').addEventListener('click', () => {
|
||
document.getElementById('settings-toggle-connect').click();
|
||
});
|
||
}
|
||
}
|
||
|
||
async function showMasterPageStatus(masterId, tabId) {
|
||
const container = document.querySelector('.container') || document.getElementById('discogs-tab');
|
||
if (container) {
|
||
container.innerHTML = `
|
||
<div style="padding:16px;">
|
||
<div style="font-size:13px; font-weight:bold; margin-bottom:8px;">Master Release</div>
|
||
<div id="master-status-msg" style="font-size:12px; color:#888;">Checking MONSTERWIKI sync…</div>
|
||
</div>`;
|
||
}
|
||
|
||
const cfg = await getMonsterSettings();
|
||
const statusEl = document.getElementById('master-status-msg');
|
||
|
||
if (!cfg.enabled) {
|
||
if (statusEl) statusEl.textContent = 'MONSTERWIKI sync off — enable in Settings → MONSTERWIKI';
|
||
return;
|
||
}
|
||
|
||
if (statusEl) statusEl.textContent = '⟳ Scraping reviews…';
|
||
|
||
// Inject directly into the master tab — no content script message timing issues
|
||
let reviews = [];
|
||
try {
|
||
const results = await chrome.scripting.executeScript({
|
||
target: { tabId },
|
||
func: async () => {
|
||
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
||
const isVisible = (el) => {
|
||
if (!el) return false;
|
||
const style = window.getComputedStyle(el);
|
||
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
|
||
const rect = el.getBoundingClientRect();
|
||
return rect.width > 0 && rect.height > 0;
|
||
};
|
||
|
||
const expandReplyButtons = async () => {
|
||
const re = /^see\s+\d+\s+repl/i;
|
||
for (let round = 0; round < 6; round++) {
|
||
const btns = [...document.querySelectorAll('button, a, div[role="button"]')]
|
||
.filter(isVisible)
|
||
.filter(el => re.test((el.textContent || '').trim()));
|
||
if (!btns.length) return;
|
||
for (const b of btns) {
|
||
try { b.scrollIntoView({ block: 'center', inline: 'center' }); } catch {}
|
||
try { b.click(); } catch {}
|
||
await sleep(200);
|
||
}
|
||
await sleep(600);
|
||
}
|
||
};
|
||
|
||
// ── 1. Try __NEXT_DATA__ (fast path) ───────────────────────────
|
||
let ndReviews = null;
|
||
try {
|
||
const nd = document.getElementById('__NEXT_DATA__');
|
||
if (nd) {
|
||
const d = JSON.parse(nd.textContent);
|
||
const pp = d?.props?.pageProps;
|
||
|
||
// Deep-search for any array that looks like reviews
|
||
function findReviews(obj, depth = 0) {
|
||
if (!obj || typeof obj !== 'object' || depth > 6) return null;
|
||
if (Array.isArray(obj) && obj.length &&
|
||
(obj[0]?.body || obj[0]?.text || obj[0]?.rating !== undefined) &&
|
||
(obj[0]?.user || obj[0]?.username)) return obj;
|
||
for (const v of Object.values(obj)) {
|
||
const found = findReviews(v, depth + 1);
|
||
if (found) return found;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
const raw = pp?.reviews?.items
|
||
|| (Array.isArray(pp?.reviews) ? pp.reviews : null)
|
||
|| pp?.master?.reviews?.items
|
||
|| pp?.initialState?.reviews?.items
|
||
|| findReviews(pp)
|
||
|| [];
|
||
|
||
console.log('[MONSTER] __NEXT_DATA__ keys:', Object.keys(pp || {}),
|
||
'| reviews found via paths:', raw.length);
|
||
|
||
if (raw.length) {
|
||
const mapped = raw.map(r => ({
|
||
username: r.user?.username || r.username || null,
|
||
date: (r.submitted_at || r.submittedAt || r.date || '').split('T')[0] || null,
|
||
rating: r.rating ?? null,
|
||
text: (r.body || r.text || '').trim() || null,
|
||
helpful: r.helpful_count || r.helpfulCount || 0,
|
||
version_ref: r.version?.description || null,
|
||
version_id: r.version?.id || null,
|
||
replies: (r.replies || []).map(rep => ({
|
||
username: rep.user?.username || rep.username || null,
|
||
date: (rep.submitted_at || rep.date || '').split('T')[0] || null,
|
||
text: (rep.body || rep.text || '').trim() || null,
|
||
})),
|
||
})).filter(r => r.text || r.rating);
|
||
// Discogs rarely embeds replies in __NEXT_DATA__ — only use as fast-path
|
||
// if replies are actually present; otherwise fall through for DOM scrape.
|
||
if (mapped.some(r => r.replies?.length > 0)) return mapped;
|
||
ndReviews = mapped;
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.warn('[MONSTER] __NEXT_DATA__ error:', e.message);
|
||
}
|
||
|
||
// Expand only "See N reply/replies" buttons (no aggressive clicking)
|
||
await expandReplyButtons();
|
||
|
||
// ── 1b. Discogs-specific .review_luKwE scrape (reliable reply nesting) ──
|
||
const reviewEls = document.querySelectorAll('.review_luKwE:not(.replies_r6FWL .review_luKwE)');
|
||
if (reviewEls.length) {
|
||
const classReviews = [];
|
||
reviewEls.forEach(reviewEl => {
|
||
const username = reviewEl.querySelector('.username_N7O6q')?.innerText?.trim() || null;
|
||
const date = reviewEl.querySelector('time')?.getAttribute('datetime')?.split('T')[0] || null;
|
||
const text = reviewEl.querySelector('.markup_Cngxi')?.innerText?.trim() || null;
|
||
const ratingLbl = reviewEl.querySelector('[class*="rating"]')?.getAttribute('aria-label') || '';
|
||
const ratingM = ratingLbl.match(/(\d+)/);
|
||
const rating = ratingM ? parseInt(ratingM[1]) : null;
|
||
const helpful = parseInt([...reviewEl.querySelectorAll('button,a')]
|
||
.map(b => b.textContent || '')
|
||
.find(t => /helpful/i.test(t))?.match(/\d+/)?.[0]) || 0;
|
||
const replies = [];
|
||
const sib = reviewEl.nextElementSibling;
|
||
if (sib?.classList?.contains('replies_r6FWL')) {
|
||
sib.querySelectorAll('.review_luKwE').forEach(replyEl => {
|
||
const ru = replyEl.querySelector('.username_N7O6q')?.innerText?.trim() || null;
|
||
const rd = replyEl.querySelector('time')?.getAttribute('datetime')?.split('T')[0] || null;
|
||
const rt = replyEl.querySelector('.markup_Cngxi')?.innerText?.trim() || null;
|
||
if (ru && rt) replies.push({ username: ru, date: rd, text: rt });
|
||
});
|
||
}
|
||
if (username && (text || rating)) classReviews.push({ username, date, rating, text, helpful, version_ref: null, version_id: null, replies });
|
||
});
|
||
if (classReviews.length) {
|
||
console.log('[MONSTER] .review_luKwE scrape:', classReviews.length, 'reviews,',
|
||
classReviews.reduce((s,r) => s + r.replies.length, 0), 'replies');
|
||
return classReviews;
|
||
}
|
||
}
|
||
|
||
// ── 2. Scroll all scrollable containers + window ───────────────
|
||
// Discogs sometimes uses a scrollable div, not window
|
||
document.querySelectorAll('main, [class*="content"], [class*="Content"], [class*="layout"], [class*="Layout"]')
|
||
.forEach(el => { try { el.scrollTop = el.scrollHeight; } catch(_){} });
|
||
window.scrollTo(0, document.body.scrollHeight);
|
||
await new Promise(r => setTimeout(r, 3000));
|
||
|
||
// ── 3. Structural DOM scrape (class-name agnostic) ─────────────
|
||
// Find every element that has: a /user/ link + a <time> + >80 chars text
|
||
const out = [];
|
||
const seen = new Set();
|
||
|
||
const entryKey = (userLink, timeEl) => {
|
||
const dt = timeEl.getAttribute('datetime') || timeEl.textContent || '';
|
||
return (userLink?.href || '') + '|' + dt;
|
||
};
|
||
|
||
const extractEntryFromTime = (timeEl, root) => {
|
||
let card = timeEl.parentElement;
|
||
for (let i = 0; i < 8 && card && card !== root; i++) {
|
||
const userLink = card.querySelector('a[href*="/user/"]');
|
||
if (!userLink) { card = card.parentElement; continue; }
|
||
const fullText = card.textContent.trim();
|
||
if (fullText.length < 40) { card = card.parentElement; continue; }
|
||
|
||
const username = userLink.textContent.trim()
|
||
|| userLink.href.split('/user/')[1]?.split('/')[0] || null;
|
||
const date = timeEl.getAttribute('datetime')?.split('T')[0]
|
||
|| timeEl.textContent.trim() || null;
|
||
|
||
let text = null;
|
||
const paras = [...card.querySelectorAll('p, [class*="body"], [class*="Body"], [class*="markup"], [class*="Markup"]')];
|
||
if (paras.length) {
|
||
text = paras.sort((a,b) => b.textContent.length - a.textContent.length)[0]
|
||
?.textContent?.trim() || null;
|
||
}
|
||
if (!text) {
|
||
text = fullText.replace(username || '', '').replace(date || '', '').trim().slice(0, 5000) || null;
|
||
}
|
||
|
||
return {
|
||
username,
|
||
date,
|
||
text,
|
||
_userLink: userLink,
|
||
_timeEl: timeEl,
|
||
};
|
||
}
|
||
return null;
|
||
};
|
||
|
||
const collectThreadEntries = (container) => {
|
||
const entries = [];
|
||
const dedupe = new Set();
|
||
const times = [...container.querySelectorAll('time')];
|
||
for (const t of times) {
|
||
const e = extractEntryFromTime(t, container);
|
||
if (!e?.username || !e.text) continue;
|
||
const key = `${e.username}|${e.date || ''}|${e.text.slice(0, 40)}`;
|
||
if (dedupe.has(key)) continue;
|
||
dedupe.add(key);
|
||
entries.push(e);
|
||
}
|
||
return entries;
|
||
};
|
||
|
||
// Walk up from <time> elements to find the review card container
|
||
document.querySelectorAll('time').forEach(timeEl => {
|
||
// Walk up max 8 levels to find a container with a user link
|
||
let card = timeEl.parentElement;
|
||
for (let i = 0; i < 8 && card; i++) {
|
||
const userLink = card.querySelector('a[href*="/user/"]');
|
||
if (!userLink) { card = card.parentElement; continue; }
|
||
|
||
const fullText = card.textContent.trim();
|
||
if (fullText.length < 40) { card = card.parentElement; continue; }
|
||
|
||
const key = entryKey(userLink, timeEl);
|
||
if (seen.has(key)) break;
|
||
seen.add(key);
|
||
|
||
const username = userLink.textContent.trim()
|
||
|| userLink.href.split('/user/')[1]?.split('/')[0] || null;
|
||
const date = timeEl.getAttribute('datetime')?.split('T')[0]
|
||
|| timeEl.textContent.trim() || null;
|
||
|
||
// Get review text: prefer the largest <p> or <div> that isn't the username/date line
|
||
let text = null;
|
||
const paras = [...card.querySelectorAll('p, [class*="body"], [class*="Body"]')];
|
||
if (paras.length) {
|
||
text = paras.sort((a,b) => b.textContent.length - a.textContent.length)[0]
|
||
?.textContent?.trim() || null;
|
||
}
|
||
if (!text) {
|
||
// strip username and date from full text as fallback
|
||
text = fullText.replace(username || '', '').replace(date || '', '').trim().slice(0, 5000) || null;
|
||
}
|
||
|
||
// Stars: count filled star elements or aria-label
|
||
const starAriaEl = card.querySelector('[aria-label*="out of 5"], [aria-label*="/5"]');
|
||
let rating = null;
|
||
if (starAriaEl) {
|
||
const m = starAriaEl.getAttribute('aria-label').match(/(\d+)/);
|
||
if (m) rating = parseInt(m[1]);
|
||
}
|
||
if (!rating) {
|
||
rating = card.querySelectorAll(
|
||
'[class*="star"][class*="full"], [class*="star"][class*="fill"], ' +
|
||
'[class*="starFull"], [class*="starFill"], ' +
|
||
'svg[fill="#000"], svg[fill="#ffb300"]'
|
||
).length || null;
|
||
}
|
||
|
||
const versionEl = [...card.querySelectorAll('p,span,a')]
|
||
.find(el => /referencing/i.test(el.textContent));
|
||
const version_ref = versionEl
|
||
? versionEl.textContent.replace(/.*referencing\s*/i,'').trim().slice(0,200) || null : null;
|
||
|
||
const helpfulEl = card.querySelector('[class*="helpful"], [class*="Helpful"]');
|
||
const helpful = parseInt(helpfulEl?.textContent?.match(/\d+/)?.[0]) || 0;
|
||
|
||
const threadContainers = [card];
|
||
const sib = card.nextElementSibling;
|
||
if (sib && sib.querySelector && (sib.querySelector('time') || /repl/i.test(sib.textContent || ''))) {
|
||
threadContainers.push(sib);
|
||
}
|
||
const threadEntries = threadContainers.flatMap(collectThreadEntries);
|
||
const replies = [];
|
||
if (threadEntries.length > 1) {
|
||
threadEntries.slice(1).forEach(ent => {
|
||
replies.push({ username: ent.username, date: ent.date, text: ent.text });
|
||
if (ent._userLink && ent._timeEl) seen.add(entryKey(ent._userLink, ent._timeEl));
|
||
});
|
||
}
|
||
|
||
if (username && (text || rating)) {
|
||
out.push({ username, date, rating, text, helpful, version_ref, version_id: null, replies });
|
||
}
|
||
break;
|
||
}
|
||
});
|
||
|
||
console.log('[MONSTER] structural DOM scrape:', out.length, 'reviews found',
|
||
'| time elements:', document.querySelectorAll('time').length,
|
||
'| user links:', document.querySelectorAll('a[href*="/user/"]').length);
|
||
|
||
// Prefer DOM results (may have replies); fall back to __NEXT_DATA__ reviews
|
||
if (!out.length && ndReviews?.length) {
|
||
console.log('[MONSTER] DOM scrape empty — using __NEXT_DATA__ reviews as fallback');
|
||
return ndReviews;
|
||
}
|
||
return out;
|
||
}
|
||
});
|
||
reviews = results?.[0]?.result || [];
|
||
} catch (e) {
|
||
if (statusEl) { statusEl.style.color = '#b00'; statusEl.textContent = `✗ Script error: ${e.message}`; }
|
||
return;
|
||
}
|
||
|
||
if (!reviews.length) {
|
||
if (statusEl) { statusEl.style.color = '#888'; statusEl.textContent = 'No reviews found — check DevTools console for [MONSTER] debug output, then reopen popup'; }
|
||
return;
|
||
}
|
||
|
||
const replyCount = reviews.reduce((sum, r) => sum + ((r.replies && r.replies.length) ? r.replies.length : 0), 0);
|
||
if (statusEl) statusEl.textContent = `⟳ Posting ${reviews.length} reviews${replyCount ? ` (+${replyCount} replies)` : ''} to MONSTERWIKI…`;
|
||
|
||
const base = await monsterResolveBase(cfg);
|
||
try {
|
||
const resp = await fetch(`${base}/plice/master`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ master_id: parseInt(masterId), url: `/master/${masterId}`, reviews }),
|
||
signal: AbortSignal.timeout(8000),
|
||
});
|
||
const d = resp.ok ? await resp.json() : {};
|
||
if (statusEl) {
|
||
statusEl.style.color = resp.ok ? '#2a7a2a' : '#b00';
|
||
statusEl.textContent = resp.ok
|
||
? `✓ ${d.review_rows ?? reviews.length} reviews synced to MONSTERWIKI`
|
||
: `✗ Server error ${resp.status}`;
|
||
}
|
||
} catch (e) {
|
||
if (statusEl) { statusEl.style.color = '#b00'; statusEl.textContent = '✗ MONSTERWIKI unreachable — is plice_server.py running?'; }
|
||
}
|
||
}
|
||
|
||
async function fetchAndDisplayReleaseData(releaseId) {
|
||
try {
|
||
console.log('--- Starting Data Fetch ---');
|
||
|
||
// Check if we're in an extension context
|
||
if (!chrome || !chrome.storage) {
|
||
console.log('Not in extension context, showing static interface');
|
||
displayStaticInterface();
|
||
return;
|
||
}
|
||
|
||
// Check if settings are configured before proceeding
|
||
const hasValidSettings = await checkSettingsConfigured();
|
||
if (!hasValidSettings) {
|
||
console.log('Settings not configured, showing static interface');
|
||
displayStaticInterface();
|
||
return;
|
||
}
|
||
|
||
// Check if we have the necessary Chrome APIs for tab interaction
|
||
if (!chrome.tabs || !chrome.scripting) {
|
||
console.log('Chrome APIs not available:', {
|
||
tabs: !!chrome.tabs,
|
||
scripting: !!chrome.scripting,
|
||
url: window.location.href
|
||
});
|
||
displayLimitedInterface();
|
||
return;
|
||
}
|
||
|
||
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
|
||
const tab = tabs[0];
|
||
|
||
// Capture tab ID and generate SKU once at popup open — used for sheet, QR, and Discogs field
|
||
state.discogsTabId = tab.id;
|
||
const now = new Date();
|
||
state.sheetTimestamp = now.toISOString();
|
||
state.sku = now.toISOString().replace(/\D/g, '').slice(0, 14);
|
||
|
||
await chrome.scripting.executeScript({ target: { tabId: tab.id }, files: ['content.js'] });
|
||
|
||
const initialValues = await new Promise((resolve) => {
|
||
chrome.tabs.sendMessage(tab.id, { action: 'getInitialValues' }, response => {
|
||
resolve(chrome.runtime.lastError ? {} : response || {});
|
||
});
|
||
});
|
||
console.log('POPUP: Received initial values from content script:', initialValues);
|
||
|
||
const [releaseData, priceSuggestions] = await Promise.all([
|
||
getReleaseData(releaseId),
|
||
getPriceSuggestions(releaseId)
|
||
]);
|
||
console.log('POPUP: Fetched release data from API:', releaseData);
|
||
console.log('POPUP: Fetched price suggestions from API:', priceSuggestions);
|
||
|
||
if (!releaseData) {
|
||
showStatusMessage('allStatus', 'Discogs API unavailable — try reopening the popup', false);
|
||
return;
|
||
}
|
||
|
||
// Properly combine API data with content script data - preserve API fields!
|
||
state.releaseData = {
|
||
// Start with API data (preserve all API fields like id, genres, styles, tracklist, etc.)
|
||
...releaseData,
|
||
|
||
// Add price suggestions
|
||
priceSuggestions,
|
||
|
||
// Add formatted fields from API data
|
||
artist: (releaseData.artists || []).map(a => a.name.replace(/\(\d+\)/, '')).join(', '),
|
||
genre: (releaseData.genres || []).join(', '),
|
||
style: (releaseData.styles || []).join(', '),
|
||
label: (releaseData.labels ? [...new Set(releaseData.labels.map(l => l.name.replace(/\(\d+\)/, '').replace(/ Records$/, '')))].join(', ') : ''),
|
||
|
||
// Only add specific content script fields (don't spread all initialValues)
|
||
lowPrice: initialValues.lowPrice,
|
||
medianPrice: initialValues.medianPrice,
|
||
highPrice: initialValues.highPrice,
|
||
lastSold: initialValues.lastSold,
|
||
have: initialValues.have,
|
||
want: initialValues.want,
|
||
artistId: initialValues.artistId,
|
||
labelId: initialValues.labelId,
|
||
imageUrl: initialValues.imageUrl,
|
||
sellerPriceRange: initialValues.sellerPriceRange || '',
|
||
mediaCondition: initialValues.mediaCondition || 'Very Good Plus (VG+)',
|
||
sleeveCondition: initialValues.sleeveCondition || 'Very Good Plus (VG+)',
|
||
price: initialValues.price || '',
|
||
comment: initialValues.comment || '',
|
||
collectionFolder: initialValues.collectionFolder || '',
|
||
appleId: initialValues.appleId || '',
|
||
reviews: initialValues.reviews || [],
|
||
recommendations: initialValues.recommendations || [],
|
||
collectionSku: initialValues.collectionSku || ''
|
||
};
|
||
|
||
// Collection box state
|
||
state.collectionBoxCount = initialValues.collectionBoxCount || 1;
|
||
state.collectionBoxSkus = initialValues.collectionBoxSkus || [];
|
||
state.collectionBoxIndex = state.collectionBoxCount - 1; // default to last (newest) box
|
||
|
||
state.originalReleaseData = JSON.parse(JSON.stringify(state.releaseData));
|
||
console.log('POPUP: Final combined state object:', state.releaseData);
|
||
console.log('POPUP: API fields preserved - id:', state.releaseData.id, 'genres:', state.releaseData.genres, 'tracklist length:', state.releaseData.tracklist?.length);
|
||
|
||
document.getElementById('price').value = state.releaseData.price || '';
|
||
document.getElementById('mediaCondition').value = state.releaseData.mediaCondition || 'Very Good Plus (VG+)';
|
||
document.getElementById('sleeveCondition').value = state.releaseData.sleeveCondition || 'Very Good Plus (VG+)';
|
||
document.getElementById('commentInput').textContent = state.releaseData.comment || '';
|
||
|
||
// Pass generated SKU as fallback so SKU field shows even before it's committed to Discogs
|
||
displayReleaseInfo({ ...state.releaseData, collectionSku: state.releaseData.collectionSku || state.sku });
|
||
|
||
// Set the QR code value for the preview:
|
||
// - If item already has a SKU on Discogs, use that (for reprints)
|
||
// - Otherwise if SKU mode is on, use the freshly generated state.sku
|
||
const useSkuOnLoad = document.getElementById('qr-use-sku')?.checked || false;
|
||
if (useSkuOnLoad) {
|
||
state.releaseData.sku = state.releaseData.collectionSku || state.sku;
|
||
}
|
||
updatePreview(state.releaseData);
|
||
|
||
// Disable ALL/SHEET if SKU already present on current box
|
||
updateAllButtonState();
|
||
|
||
// Show collection box selector if multiple copies owned
|
||
renderCollectionBoxSelector(state.collectionBoxCount, state.collectionBoxIndex, state.collectionBoxSkus);
|
||
|
||
setupEventListeners();
|
||
await updateButtonStates(); // Update button states based on available settings
|
||
updateResearchTab(); // Refresh Research tab with all loaded data
|
||
console.log('--- Data Fetch and Display Complete ---');
|
||
|
||
// MONSTERWIKI: kick off silent background sync (sell/history + sell/list)
|
||
monsterBackgroundSync(releaseId);
|
||
inventorySkuLookup(releaseId);
|
||
|
||
} catch (error) {
|
||
console.error('Error in fetchAndDisplayReleaseData:', error);
|
||
document.body.innerHTML = `<div style="padding: 10px; text-align: center;">Error: ${error.message}</div>`;
|
||
}
|
||
}
|
||
|
||
function writeJSON(releaseData) {
|
||
if (!releaseData) return Promise.reject(new Error('No release data'));
|
||
|
||
// Check if JSON download is enabled in settings
|
||
return new Promise((resolve, reject) => {
|
||
chrome.storage.local.get(['enableJsonDownload'], (result) => {
|
||
const isEnabled = result.enableJsonDownload !== false; // Default to true
|
||
|
||
if (!isEnabled) {
|
||
console.log('JSON download is disabled in settings');
|
||
resolve(); // Resolve without downloading
|
||
return;
|
||
}
|
||
|
||
// Proceed with JSON download
|
||
const { marketStats, mediaCondition, sleeveCondition, comment, collectionFolder, appleId, price, priceSuggestions, reviews, recommendations, ...baseData } = releaseData;
|
||
const fullData = { ...baseData, market: { lastSold: marketStats?.lastSold, lowPrice: marketStats?.lowPrice, medianPrice: marketStats?.medianPrice, highPrice: marketStats?.highPrice, suggestedPrices: priceSuggestions }, collection: { mediaCondition, sleeveCondition, comment, collectionFolder, appleId, price }, reviews, recommendations };
|
||
const jsonString = JSON.stringify(fullData, null, 2);
|
||
const blob = new Blob([jsonString], { type: 'application/json' });
|
||
const url = URL.createObjectURL(blob);
|
||
chrome.downloads.download({ url, filename: `JSON/${releaseData.id}.json`, saveAs: false }, () => {
|
||
URL.revokeObjectURL(url);
|
||
resolve();
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
function uploadJSON() {
|
||
return new Promise((resolve) => {
|
||
const input = document.createElement('input');
|
||
input.type = 'file';
|
||
input.accept = 'application/json,.json';
|
||
input.onchange = (e) => {
|
||
const file = e.target.files[0];
|
||
if (!file) { resolve(null); return; }
|
||
const reader = new FileReader();
|
||
reader.onload = (ev) => {
|
||
try {
|
||
const data = JSON.parse(ev.target.result);
|
||
resolve(data);
|
||
} catch (err) {
|
||
showStatusMessage('allStatus', 'Invalid JSON file', false);
|
||
resolve(null);
|
||
}
|
||
};
|
||
reader.readAsText(file);
|
||
};
|
||
input.click();
|
||
});
|
||
}
|
||
|
||
// REMOVED: updateSheetOnly function (now imported from sheets.js)
|
||
|
||
function showStatusMessage(elementId, message, isSuccess) {
|
||
const statusElement = document.getElementById(elementId);
|
||
if (statusElement && message) {
|
||
statusElement.textContent = message;
|
||
statusElement.className = 'status-text show';
|
||
statusElement.style.color = isSuccess ? '#28a745' : '#dc3545';
|
||
|
||
// Hide the message after 3 seconds
|
||
setTimeout(() => {
|
||
statusElement.classList.remove('show');
|
||
}, 3000);
|
||
}
|
||
}
|
||
|
||
// Single-shot tag read for pre/post-write verification.
|
||
// Returns { sku, releaseId, epc } or null if no tag / daemon down.
|
||
async function readRfidTagOnce() {
|
||
try {
|
||
const resp = await fetch('http://127.0.0.1:7790/read-tag', { signal: AbortSignal.timeout(5000) });
|
||
const data = await resp.json();
|
||
if (!data || data.ok === false) return null;
|
||
return {
|
||
sku: data.sku || null,
|
||
releaseId: data.releaseId || data.release || null,
|
||
epc: data.epc || null,
|
||
};
|
||
} catch (e) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function formatTag(t) {
|
||
if (!t) return '(no tag in field)';
|
||
if (t.sku) return `${t.sku}${t.releaseId ? ' + rel ' + t.releaseId : ''}`;
|
||
if (t.epc) return `EPC ${String(t.epc).slice(0, 12)}… (factory tag, not programmed)`;
|
||
return '(unknown)';
|
||
}
|
||
|
||
async function writeRfidTag(sku, releaseId) {
|
||
try {
|
||
const body = { sku };
|
||
if (releaseId) body.releaseId = String(releaseId);
|
||
// Pre-clear any stale SELECT mask before the write so it doesn't
|
||
// compete with a mask left by a previous failed attempt.
|
||
await fetch('http://127.0.0.1:7790/clear-mask', { method: 'POST', signal: AbortSignal.timeout(3000) }).catch(() => {});
|
||
const resp = await fetch('http://127.0.0.1:7790/write-tag', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(body),
|
||
signal: AbortSignal.timeout(120000) // 120s: 5 attempts × ~20s each + headroom
|
||
});
|
||
const data = await resp.json();
|
||
if (!data.ok) console.warn('[RFID] write failed:', data.error);
|
||
else console.log('[RFID] wrote SKU', data.sku, releaseId ? `+ release ${releaseId}` : '');
|
||
return data;
|
||
} catch (e) {
|
||
// Daemon not running, reader not connected, or timed out
|
||
console.warn('[RFID] daemon unavailable:', e.message);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function updateAllButtonState() {
|
||
const hasSku = !!(state.releaseData && state.releaseData.collectionSku);
|
||
for (const id of ['bothActions', 'updateSheet']) {
|
||
const btn = document.getElementById(id);
|
||
if (!btn) continue;
|
||
if (hasSku) {
|
||
btn.disabled = true;
|
||
btn.title = `SKU already present (${state.releaseData.collectionSku}) — item already logged`;
|
||
btn.style.opacity = '0.4';
|
||
btn.style.cursor = 'not-allowed';
|
||
} else {
|
||
btn.disabled = false;
|
||
btn.title = '';
|
||
btn.style.opacity = '';
|
||
btn.style.cursor = '';
|
||
}
|
||
}
|
||
}
|
||
|
||
function renderCollectionBoxSelector(count, activeIdx, skus) {
|
||
const wrapper = document.getElementById('collection-box-selector');
|
||
const container = document.getElementById('collection-box-buttons');
|
||
if (!wrapper || !container) return;
|
||
|
||
if (count <= 1) {
|
||
wrapper.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
wrapper.style.display = 'flex';
|
||
container.innerHTML = '';
|
||
|
||
for (let i = 0; i < count; i++) {
|
||
const hasSku = !!(skus && skus[i]);
|
||
const btn = document.createElement('button');
|
||
btn.textContent = String(i + 1);
|
||
if (hasSku) {
|
||
btn.textContent = String(i + 1) + ' ●';
|
||
btn.title = `Copy ${i + 1} — SKU: ${skus[i]}`;
|
||
} else {
|
||
btn.title = `Copy ${i + 1} — no SKU`;
|
||
}
|
||
btn.style.cssText = [
|
||
'padding: 2px 7px',
|
||
'font-size: 11px',
|
||
'border-radius: 3px',
|
||
'border: 1px solid #aaa',
|
||
'cursor: pointer',
|
||
'background: ' + (i === activeIdx ? '#4a90d9' : '#f0f0f0'),
|
||
'color: ' + (i === activeIdx ? '#fff' : '#333'),
|
||
hasSku ? 'color: ' + (i === activeIdx ? '#9fffb0' : '#2a8a2a') : '',
|
||
].filter(Boolean).join(';');
|
||
btn.addEventListener('click', () => switchCollectionBox(i));
|
||
container.appendChild(btn);
|
||
}
|
||
}
|
||
|
||
async function switchCollectionBox(index) {
|
||
const tabId = state.discogsTabId;
|
||
if (!tabId) return;
|
||
|
||
let boxData;
|
||
try {
|
||
boxData = await new Promise((resolve) => {
|
||
chrome.tabs.sendMessage(tabId, { action: 'selectCollectionBox', index }, response => {
|
||
resolve(chrome.runtime.lastError ? null : response);
|
||
});
|
||
});
|
||
} catch (e) { return; }
|
||
|
||
if (!boxData || !boxData.ok) return;
|
||
|
||
state.collectionBoxIndex = index;
|
||
|
||
// Update state fields from selected box
|
||
state.releaseData.mediaCondition = boxData.mediaCondition || state.releaseData.mediaCondition;
|
||
state.releaseData.sleeveCondition = boxData.sleeveCondition || state.releaseData.sleeveCondition;
|
||
state.releaseData.price = boxData.price || state.releaseData.price;
|
||
state.releaseData.comment = boxData.comment || state.releaseData.comment;
|
||
state.releaseData.collectionSku = boxData.collectionSku || '';
|
||
|
||
// If this box has no SKU, mint a fresh one at click-time so ALL/LABEL/SHEET can use it.
|
||
// If it already has a SKU (e.g. from a previous DB lookup), use that — never show the
|
||
// popup-generated timestamp when a real SKU already exists.
|
||
if (!state.releaseData.collectionSku) {
|
||
const now = new Date();
|
||
state.sheetTimestamp = now.toISOString();
|
||
state.sku = now.toISOString().replace(/\D/g, '').slice(0, 14);
|
||
} else {
|
||
// Use the existing SKU (stripped of -R/-A) as the active SKU
|
||
state.sku = String(state.releaseData.collectionSku).replace(/-[AR]$/i, '');
|
||
}
|
||
|
||
// Update input fields
|
||
const priceEl = document.getElementById('price');
|
||
const mediaEl = document.getElementById('mediaCondition');
|
||
const sleeveEl = document.getElementById('sleeveCondition');
|
||
const commentEl = document.getElementById('commentInput');
|
||
if (priceEl) priceEl.value = state.releaseData.price || '';
|
||
if (mediaEl) mediaEl.value = state.releaseData.mediaCondition || 'Very Good Plus (VG+)';
|
||
if (sleeveEl) sleeveEl.value = state.releaseData.sleeveCondition || 'Very Good Plus (VG+)';
|
||
if (commentEl) commentEl.textContent = state.releaseData.comment || '';
|
||
|
||
// Update SKU field in Release Information panel — show existing SKU or freshly generated one
|
||
const skuFieldEl = document.querySelector('.info-field[data-field="collectionSku"] .value');
|
||
if (skuFieldEl) skuFieldEl.textContent = state.releaseData.collectionSku || state.sku || '';
|
||
|
||
// Re-render selector with updated active index
|
||
renderCollectionBoxSelector(state.collectionBoxCount, index, state.collectionBoxSkus);
|
||
|
||
updateAllButtonState();
|
||
|
||
// Update QR preview: use fresh state.sku for empty boxes if SKU mode is on
|
||
const useSkuForPreview = document.getElementById('qr-use-sku')?.checked || false;
|
||
if (useSkuForPreview) {
|
||
state.releaseData.sku = state.releaseData.collectionSku || state.sku;
|
||
} else {
|
||
delete state.releaseData.sku;
|
||
}
|
||
updatePreview(state.releaseData);
|
||
}
|
||
|
||
async function handleImportInventory() {
|
||
if (!state.releaseData || !state.releaseData.id) {
|
||
showStatusMessage('allStatus', 'No release data — open a Discogs release first', false);
|
||
return;
|
||
}
|
||
|
||
const importBtn = document.getElementById('importWordPress');
|
||
const origText = importBtn.textContent;
|
||
importBtn.textContent = '…';
|
||
importBtn.disabled = true;
|
||
|
||
try {
|
||
const releaseId = state.releaseData.id;
|
||
const cfg = await getMonsterSettings();
|
||
const base = await monsterResolveBase(cfg);
|
||
const url = `${base}/plice/inventory?release_id=${releaseId}`;
|
||
const response = await fetch(url);
|
||
const result = await response.json();
|
||
|
||
if (!result.ok) {
|
||
throw new Error(result.error || 'Failed to fetch inventory');
|
||
}
|
||
|
||
const inventory = result.inventory || [];
|
||
if (inventory.length === 0) {
|
||
showStatusMessage('allStatus', 'No matching records found on website VPS', false);
|
||
return;
|
||
}
|
||
|
||
const boxCount = state.collectionBoxCount || 1;
|
||
const invCount = inventory.length;
|
||
|
||
// "fill oldest first" logic (oldest SKU -> first box)
|
||
// inventory is already sorted by SKU (timestamp) ASC in plice_server.py
|
||
|
||
let updatedCount = 0;
|
||
let priceMismatch = false;
|
||
let lastError = null;
|
||
|
||
for (let i = 0; i < Math.min(boxCount, invCount); i++) {
|
||
const item = inventory[i];
|
||
const sku = item.sku;
|
||
const price = item.price;
|
||
|
||
// Update SKU in Discogs box
|
||
try {
|
||
await chrome.tabs.sendMessage(state.discogsTabId, {
|
||
action: 'updateDiscogsSku',
|
||
sku: sku,
|
||
boxIndex: i
|
||
});
|
||
|
||
// Update price in Discogs box
|
||
// According to prompt: "there is collection box price field which should match the inventory.price field?"
|
||
// We update it to ensure they match.
|
||
await chrome.tabs.sendMessage(state.discogsTabId, {
|
||
action: 'updateDiscogsPrice',
|
||
price: price
|
||
});
|
||
|
||
updatedCount++;
|
||
} catch (e) {
|
||
lastError = e.message;
|
||
}
|
||
}
|
||
|
||
// Handle mismatches as requested: "if they don tmatch up, give me a text msg"
|
||
let msg = `Updated ${updatedCount} box(es) with SKUs from VPS.`;
|
||
if (boxCount !== invCount) {
|
||
msg += ` WARNING: Mismatch! VPS has ${invCount} records, but Discogs has ${boxCount} collection boxes.`;
|
||
}
|
||
if (lastError) {
|
||
msg += ` Error during update: ${lastError}`;
|
||
}
|
||
|
||
showStatusMessage('allStatus', msg, !lastError && boxCount === invCount);
|
||
|
||
// Refresh local state by fetching initial values again
|
||
const newValues = await new Promise((resolve) => {
|
||
chrome.tabs.sendMessage(state.discogsTabId, { action: 'getInitialValues' }, r => resolve(r || {}));
|
||
});
|
||
if (newValues && !newValues.error) {
|
||
state.releaseData = { ...state.releaseData, ...newValues };
|
||
// If the current box now has a real SKU, ditch the popup-generated timestamp
|
||
const currentBoxSku = (newValues.collectionBoxSkus || [])[state.collectionBoxIndex]
|
||
|| newValues.collectionSku || '';
|
||
if (currentBoxSku) {
|
||
const cleanSku = String(currentBoxSku).replace(/-[AR]$/i, '');
|
||
state.sku = cleanSku;
|
||
if (document.getElementById('qr-use-sku')?.checked) {
|
||
state.releaseData.sku = cleanSku;
|
||
}
|
||
}
|
||
displayReleaseInfo(state.releaseData);
|
||
updatePreview(state.releaseData);
|
||
}
|
||
|
||
} catch (e) {
|
||
showStatusMessage('allStatus', `Inventory error: ${e.message}`, false);
|
||
} finally {
|
||
importBtn.textContent = origText;
|
||
importBtn.disabled = false;
|
||
}
|
||
}
|
||
|
||
function refreshPopupSku(sku) {
|
||
if (!state.releaseData) return;
|
||
state.releaseData.collectionSku = sku;
|
||
// Strip -R/-A for label/QR — the print label SKU is always just the integer.
|
||
// Also replace the popup-generated timestamp SKU so the label shows the DB value.
|
||
const cleanSku = String(sku || '').replace(/-[AR]$/i, '');
|
||
state.sku = cleanSku;
|
||
if (document.getElementById('qr-use-sku')?.checked) {
|
||
state.releaseData.sku = cleanSku;
|
||
}
|
||
const skuFieldEl = document.querySelector('.info-field[data-field="collectionSku"] .value');
|
||
if (skuFieldEl) skuFieldEl.textContent = sku;
|
||
updatePreview(state.releaseData);
|
||
}
|
||
|
||
// Shows DB price next to fill status. Returns true if prices differ.
|
||
function showPriceNote(statusEl, panelEl, dbRow, fillLabel) {
|
||
const dbPrice = dbRow.price != null ? parseFloat(dbRow.price) : null;
|
||
const pagePrice = parseFloat(state.releaseData?.price);
|
||
const differ = dbPrice !== null && !isNaN(pagePrice) && Math.abs(dbPrice - pagePrice) >= 0.01;
|
||
const sold = dbRow.instock === 'n' || dbRow.instock === false || dbRow.instock === 0;
|
||
|
||
let html = `<span style="color:#2a7">${fillLabel}</span>`;
|
||
if (sold) {
|
||
html += ` <span style="background:#f8d7da;color:#842029;padding:1px 5px;border-radius:3px;font-size:10px;font-weight:600;">SOLD</span>`;
|
||
}
|
||
if (dbPrice !== null) {
|
||
if (differ) {
|
||
html += ` <span style="background:#fff3cd;color:#856404;padding:1px 5px;border-radius:3px;font-size:10px;font-weight:600;">` +
|
||
`DB $${dbPrice.toFixed(2)} · page $${pagePrice.toFixed(2)}</span>`;
|
||
} else {
|
||
html += ` <span style="color:#888;font-size:10px;">DB $${dbPrice.toFixed(2)}</span>`;
|
||
}
|
||
}
|
||
statusEl.innerHTML = html;
|
||
statusEl.style.color = '';
|
||
if (!differ && !sold) setTimeout(() => { panelEl.style.display = 'none'; }, 3000);
|
||
return differ || sold;
|
||
}
|
||
|
||
async function inventorySkuLookup(releaseId) {
|
||
const settings = await new Promise(resolve =>
|
||
chrome.storage.local.get(['inventorySettings'], r => resolve(r.inventorySettings || {}))
|
||
);
|
||
|
||
if (!settings.enabled) return;
|
||
if (!settings.dbUser || !settings.dbPass || !settings.dbName) return;
|
||
if (!state.discogsTabId) return;
|
||
|
||
const panel = document.getElementById('inventory-sku-panel');
|
||
const status = document.getElementById('inventory-sku-status');
|
||
if (!panel || !status) return;
|
||
|
||
panel.style.display = 'block';
|
||
status.textContent = 'Looking up inventory SKUs…';
|
||
status.style.color = '#888';
|
||
|
||
let rows;
|
||
try {
|
||
const params = new URLSearchParams({
|
||
release_id: releaseId,
|
||
dbHost: settings.dbHost || '100.123.123.64',
|
||
dbName: settings.dbName || 'wp_rmp_disc_',
|
||
dbUser: settings.dbUser,
|
||
dbPass: settings.dbPass,
|
||
sshKeyPath: settings.sshKeyPath || '~/.ssh/id_rsa'
|
||
});
|
||
const resp = await fetch(`http://localhost:7790/inventory-lookup?${params}`);
|
||
if (!resp.ok) throw new Error(`Daemon error: HTTP ${resp.status}`);
|
||
const data = await resp.json();
|
||
if (data.error) throw new Error(data.error);
|
||
rows = data.rows || [];
|
||
} catch (e) {
|
||
status.textContent = `Inventory lookup failed: ${e.message}`;
|
||
status.style.color = '#c00';
|
||
return;
|
||
}
|
||
|
||
if (rows.length === 0) {
|
||
panel.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
const boxCount = state.collectionBoxCount || 1;
|
||
const lastBoxIdx = boxCount - 1;
|
||
|
||
// ── Auto-fill exact matches silently, then always show the row list ───────
|
||
const boxIsLocked = idx => (state.collectionBoxSkus[idx] || '').endsWith('-R');
|
||
|
||
if (rows.length === 1) {
|
||
if (boxIsLocked(lastBoxIdx)) {
|
||
status.innerHTML = `<span style="color:#888">Box ${lastBoxIdx + 1} locked (${state.collectionBoxSkus[lastBoxIdx]})</span>`;
|
||
status.style.color = '';
|
||
} else {
|
||
try {
|
||
await chrome.tabs.sendMessage(state.discogsTabId, {
|
||
action: 'updateDiscogsSku',
|
||
sku: rows[0].sku,
|
||
boxIndex: lastBoxIdx
|
||
});
|
||
refreshPopupSku(rows[0].sku);
|
||
} catch (e) {
|
||
status.textContent = `Failed to fill SKU: ${e.message}`;
|
||
status.style.color = '#c00';
|
||
}
|
||
}
|
||
} else if (rows.length === boxCount) {
|
||
let ok = true;
|
||
for (let i = 0; i < rows.length; i++) {
|
||
if (boxIsLocked(i)) continue;
|
||
try {
|
||
await chrome.tabs.sendMessage(state.discogsTabId, {
|
||
action: 'updateDiscogsSku',
|
||
sku: rows[i].sku,
|
||
boxIndex: i
|
||
});
|
||
} catch (e) {
|
||
ok = false;
|
||
status.textContent = `Failed to fill box ${i + 1}: ${e.message}`;
|
||
status.style.color = '#c00';
|
||
break;
|
||
}
|
||
}
|
||
if (ok) {
|
||
const currentBoxSku = rows[state.collectionBoxIndex]?.sku || rows[0].sku;
|
||
if (!boxIsLocked(state.collectionBoxIndex)) refreshPopupSku(currentBoxSku);
|
||
}
|
||
} else {
|
||
status.innerHTML = `<span style="color:#b60">Mismatch: ${rows.length} DB rows, ${boxCount} box${boxCount !== 1 ? 'es' : ''}</span>`;
|
||
status.style.color = '';
|
||
}
|
||
|
||
// ── Always render the row list ────────────────────────────────────────────
|
||
renderInventoryRows(rows);
|
||
}
|
||
|
||
function renderInventoryRows(rows) {
|
||
const picker = document.getElementById('inventory-sku-picker');
|
||
const cardsEl = document.getElementById('inventory-sku-cards');
|
||
if (!picker || !cardsEl) return;
|
||
|
||
// ── Hide unused box-buttons container (no longer needed) ─────────────────
|
||
const boxButtonsEl = document.getElementById('inventory-sku-box-buttons');
|
||
if (boxButtonsEl) boxButtonsEl.style.display = 'none';
|
||
|
||
cardsEl.innerHTML = '';
|
||
|
||
rows.forEach((row, i) => {
|
||
const rowEl = document.createElement('div');
|
||
rowEl.style.cssText = 'display:flex;align-items:center;gap:6px;margin-bottom:3px;';
|
||
|
||
// ── [n] button writes this row's SKU to collection box n ─────────────
|
||
const btn = document.createElement('button');
|
||
btn.textContent = String(i + 1);
|
||
btn.style.cssText = [
|
||
'padding:2px 7px',
|
||
'font-size:12px',
|
||
'font-weight:600',
|
||
'border-radius:4px',
|
||
'border:1px solid #aaa',
|
||
'cursor:pointer',
|
||
'flex-shrink:0',
|
||
'min-width:24px'
|
||
].join(';');
|
||
|
||
const existingSku = state.collectionBoxSkus[i] || '';
|
||
const locked = existingSku.endsWith('-R');
|
||
if (locked) {
|
||
btn.disabled = true;
|
||
btn.title = `Locked: ${existingSku}`;
|
||
btn.style.opacity = '0.45';
|
||
btn.style.cursor = 'default';
|
||
btn.style.background = '#f0f0f0';
|
||
} else {
|
||
btn.addEventListener('click', async () => {
|
||
try {
|
||
await chrome.tabs.sendMessage(state.discogsTabId, {
|
||
action: 'updateDiscogsSku',
|
||
sku: row.sku,
|
||
boxIndex: i
|
||
});
|
||
refreshPopupSku(row.sku);
|
||
btn.textContent = `✓${i + 1}`;
|
||
btn.style.background = '#e8f8ef';
|
||
btn.style.borderColor = '#2a7';
|
||
btn.disabled = true;
|
||
} catch (e) {
|
||
document.getElementById('inventory-sku-status').textContent = `Write failed: ${e.message}`;
|
||
document.getElementById('inventory-sku-status').style.color = '#c00';
|
||
}
|
||
});
|
||
}
|
||
|
||
// ── Row info text ─────────────────────────────────────────────────────
|
||
const location = (row.crate_id && row.slot_number) ? `${row.crate_id}/${row.slot_number}` : '—';
|
||
const price = row.price ? `$${parseFloat(row.price).toFixed(2)}` : '—';
|
||
const cond = [row.media_condition, row.sleeve_condition].filter(Boolean).join('/') || '—';
|
||
const weight = (row.actual_weight != null && Number.isFinite(Number(row.actual_weight)))
|
||
? `${Number(row.actual_weight)}g`
|
||
: '—';
|
||
const sold = row.instock === 'n' || row.instock === false || row.instock === 0;
|
||
|
||
const dbPrice = row.price != null ? parseFloat(row.price) : null;
|
||
const pagePx = parseFloat(state.releaseData?.price);
|
||
const differ = dbPrice !== null && !isNaN(pagePx) && Math.abs(dbPrice - pagePx) >= 0.01;
|
||
|
||
let infoHtml = `<span style="font-family:monospace;font-size:11px;">`
|
||
+ `${row.sku} ${row.instock || '?'} ${location} ${price} ${cond} ${weight}`
|
||
+ `</span>`;
|
||
if (sold) infoHtml += ` <span style="background:#f8d7da;color:#842029;padding:1px 4px;border-radius:3px;font-size:10px;font-weight:600;">SOLD</span>`;
|
||
if (differ) infoHtml += ` <span style="background:#fff3cd;color:#856404;padding:1px 4px;border-radius:3px;font-size:10px;font-weight:600;">page $${pagePx.toFixed(2)}</span>`;
|
||
|
||
const infoEl = document.createElement('span');
|
||
infoEl.innerHTML = infoHtml;
|
||
|
||
// ── ⚖→DB button: capture current scale weight, UPDATE this SKU in WP ─
|
||
const wtBtn = document.createElement('button');
|
||
wtBtn.textContent = '⚖→DB';
|
||
wtBtn.title = 'Capture current scale reading and update this SKU in wp_rmp_disc_inventory';
|
||
wtBtn.style.cssText = [
|
||
'padding:2px 6px',
|
||
'font-size:11px',
|
||
'border-radius:4px',
|
||
'border:1px solid #aaa',
|
||
'cursor:pointer',
|
||
'background:#eef',
|
||
'flex-shrink:0',
|
||
'margin-left:auto'
|
||
].join(';');
|
||
wtBtn.addEventListener('click', async () => {
|
||
wtBtn.disabled = true;
|
||
const origText = wtBtn.textContent;
|
||
wtBtn.textContent = '…';
|
||
try {
|
||
const cfg = await new Promise(resolve =>
|
||
chrome.storage.local.get(['inventorySettings', 'scaleRequireStable'], resolve)
|
||
);
|
||
const settings = cfg.inventorySettings || {};
|
||
if (!settings.dbUser || !settings.dbPass || !settings.dbName) {
|
||
throw new Error('inventory creds not set in Settings');
|
||
}
|
||
const requireStable = cfg.scaleRequireStable !== false;
|
||
const grams = await getCurrentWeight({ requireStable });
|
||
if (grams == null) {
|
||
throw new Error(requireStable ? 'no stable reading from scale' : 'scale offline');
|
||
}
|
||
const resp = await fetch('http://localhost:7790/inventory-update-weight', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
release_id: state.releaseData?.id || row.release_id,
|
||
sku: row.sku,
|
||
grams: grams,
|
||
dbHost: settings.dbHost || '100.123.123.64',
|
||
dbName: settings.dbName || 'wp_rmp_disc_',
|
||
dbUser: settings.dbUser,
|
||
dbPass: settings.dbPass,
|
||
sshKeyPath: settings.sshKeyPath || '~/.ssh/id_rsa'
|
||
})
|
||
});
|
||
const data = await resp.json();
|
||
if (!resp.ok || data.error) throw new Error(data.error || `HTTP ${resp.status}`);
|
||
wtBtn.textContent = `✓ ${grams}g`;
|
||
wtBtn.style.background = '#e6f4ea';
|
||
wtBtn.style.borderColor = '#2a7';
|
||
// Reflect in the displayed row immediately
|
||
row.actual_weight = grams;
|
||
const wtSpan = infoEl.querySelector('span');
|
||
if (wtSpan) {
|
||
wtSpan.innerHTML = wtSpan.innerHTML.replace(/—(?=(?: )*$)/, `${grams}g`)
|
||
.replace(/\b\d+g(?=(?: )*$)/, `${grams}g`);
|
||
}
|
||
} catch (e) {
|
||
wtBtn.textContent = '✗ ' + (e.message || 'failed');
|
||
wtBtn.style.background = '#fde2e1';
|
||
wtBtn.style.borderColor = '#c33';
|
||
setTimeout(() => {
|
||
wtBtn.textContent = origText;
|
||
wtBtn.style.background = '#eef';
|
||
wtBtn.style.borderColor = '#aaa';
|
||
wtBtn.disabled = false;
|
||
}, 2500);
|
||
}
|
||
});
|
||
|
||
rowEl.appendChild(btn);
|
||
rowEl.appendChild(infoEl);
|
||
rowEl.appendChild(wtBtn);
|
||
cardsEl.appendChild(rowEl);
|
||
});
|
||
|
||
picker.style.display = 'block';
|
||
}
|
||
|
||
async function handleAllActions(releaseData) {
|
||
if (!releaseData) return;
|
||
|
||
const discogsTabId = state.discogsTabId;
|
||
// Read toggle from DOM; also verify against storage to survive popup re-opens
|
||
const skuSettings = await new Promise(resolve => chrome.storage.local.get(['qrUseSku'], resolve));
|
||
const useSkuForQr = skuSettings.qrUseSku === true;
|
||
const skuToUse = (useSkuForQr && releaseData.collectionSku) ? releaseData.collectionSku : state.sku;
|
||
|
||
// Load all action toggles
|
||
const allCfg = await new Promise(resolve =>
|
||
chrome.storage.local.get(['allDoesRfid','allDoesLabel','allDoesSheet','allDoesJson','allDoesPrice','rfidIncludeReleaseId','rfidAppendR'], resolve)
|
||
);
|
||
const doRfid = allCfg.allDoesRfid !== false;
|
||
const doLabel = allCfg.allDoesLabel !== false;
|
||
const doSheet = allCfg.allDoesSheet !== false;
|
||
const doJson = allCfg.allDoesJson !== false;
|
||
const doPrice = allCfg.allDoesPrice !== false;
|
||
|
||
// RFID — fire-and-forget alongside other actions
|
||
if (doRfid) {
|
||
const rawRfidSku = releaseData.collectionSku || skuToUse;
|
||
const rfidSku = rawRfidSku ? rawRfidSku.replace(/-[AR]$/i, '') : rawRfidSku;
|
||
// Always include release_id — see rationale at rfidQuickBtn handler.
|
||
const releaseId = releaseData.id || null;
|
||
const rfidEl = document.getElementById('rfidStatus');
|
||
if (rfidEl) { rfidEl.textContent = 'RFID writing…'; rfidEl.style.color = '#888'; }
|
||
writeRfidTag(rfidSku, releaseId).then(async result => {
|
||
if (!rfidEl) return;
|
||
if (result?.ok) {
|
||
rfidEl.textContent = `✓ RFID wrote ${rfidSku}${releaseId ? ' + rel ' + releaseId : ''}`; rfidEl.style.color = '#28a745';
|
||
if (allCfg.rfidAppendR && tabId) {
|
||
try {
|
||
await chrome.tabs.sendMessage(tabId, { action: 'updateDiscogsSku', sku: rfidSku + '-R', boxIndex: state.collectionBoxIndex });
|
||
} catch (e) { console.warn('[RFID] -R SKU update failed:', e.message); }
|
||
}
|
||
} else if (result) { rfidEl.textContent = `✗ RFID: ${result.error}`; rfidEl.style.color = '#dc3545'; }
|
||
else { rfidEl.textContent = '✗ RFID: daemon not running'; rfidEl.style.color = '#dc3545'; }
|
||
});
|
||
}
|
||
|
||
// Update preview QR before we snapshot it into the print window
|
||
if (useSkuForQr) {
|
||
releaseData.sku = skuToUse;
|
||
updatePreview(releaseData);
|
||
}
|
||
|
||
// Fix the sheet timestamp to match the SKU
|
||
if (useSkuForQr && state.sheetTimestamp) {
|
||
releaseData._fixedTimestamp = state.sheetTimestamp;
|
||
}
|
||
|
||
// ── Discogs page updates (price + SKU) ───────────────────────────────────
|
||
// Resolve tab ID: use value captured at popup-open, fall back to active tab query
|
||
let tabId = discogsTabId;
|
||
if (!tabId) {
|
||
try {
|
||
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
|
||
tabId = tabs[0]?.id || null;
|
||
} catch (e) {}
|
||
}
|
||
|
||
if (tabId) {
|
||
if (doPrice) {
|
||
const price = document.getElementById('price')?.value;
|
||
if (price) {
|
||
try {
|
||
await chrome.tabs.sendMessage(tabId, { action: 'updateDiscogsPrice', price });
|
||
} catch (e) { console.error('[POPUP] price error:', e); }
|
||
}
|
||
}
|
||
|
||
if (useSkuForQr && !releaseData.collectionSku) {
|
||
try {
|
||
const skuForDiscogs = (doRfid && allCfg.rfidAppendR) ? skuToUse + '-R' : skuToUse;
|
||
console.log('[POPUP] sending updateDiscogsSku', skuForDiscogs, 'tabId', tabId, 'boxIndex', state.collectionBoxIndex);
|
||
const r = await chrome.tabs.sendMessage(tabId, { action: 'updateDiscogsSku', sku: skuForDiscogs, boxIndex: state.collectionBoxIndex });
|
||
console.log('[POPUP] SKU response:', JSON.stringify(r));
|
||
} catch (e) { console.error('[POPUP] SKU error:', e); }
|
||
}
|
||
}
|
||
// ─────────────────────────────────────────────────────────────────────────
|
||
|
||
// Label + JSON
|
||
if (doLabel) generateLabel(releaseData);
|
||
if (useSkuForQr) delete releaseData.sku;
|
||
if (doJson) writeJSON(releaseData);
|
||
|
||
// MONSTERWIKI — fire-and-forget POST to ultra Postgres via Tailscale
|
||
getMonsterSettings().then(cfg => {
|
||
if (!cfg.enabled) return;
|
||
const base = (cfg.serverUrl || 'http://100.91.239.7:5002').replace(/\/$/, '');
|
||
fetch(`${base}/plice`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
...buildResearchJson(releaseData),
|
||
sales_history: state.monsterSalesHistory,
|
||
current_listings: state.monsterCurrentListings,
|
||
apple_id: releaseData.appleId || null,
|
||
apple_country: releaseData.appleCountry || null,
|
||
pricing: {
|
||
media_condition: releaseData.mediaCondition || null,
|
||
sleeve_condition: releaseData.sleeveCondition || null,
|
||
price: releaseData.price || null,
|
||
sku: releaseData.collectionSku || null,
|
||
folder: releaseData.collectionFolder || null,
|
||
comment: releaseData.comment || null,
|
||
},
|
||
}),
|
||
}).catch(() => {});
|
||
});
|
||
|
||
// Scale weight → attach to releaseData (Sheets reads it via the actualWeight mapping)
|
||
const scaleCfg = await new Promise(resolve =>
|
||
chrome.storage.local.get(['scaleSaveToSheet', 'scaleRequireStable'], resolve)
|
||
);
|
||
if (scaleCfg.scaleSaveToSheet === true) {
|
||
const requireStable = scaleCfg.scaleRequireStable !== false; // default ON
|
||
const grams = await getCurrentWeight({ requireStable });
|
||
if (grams != null) {
|
||
releaseData.actual_weight = grams;
|
||
} else {
|
||
console.warn('[scale] weight not captured (offline or unstable) — sheet save proceeds without weight');
|
||
}
|
||
}
|
||
|
||
// Google Sheets
|
||
if (doSheet) {
|
||
const googleSettings = await new Promise(resolve =>
|
||
chrome.storage.local.get(['googleSettings'], resolve)
|
||
);
|
||
const hasGoogleSettings = googleSettings.googleSettings?.spreadsheetId &&
|
||
googleSettings.googleSettings?.clientEmail &&
|
||
googleSettings.googleSettings?.privateKey;
|
||
|
||
const parts = [doLabel && 'Label printed', doJson && 'JSON saved'].filter(Boolean);
|
||
const prefix = parts.join(', ');
|
||
if (hasGoogleSettings) {
|
||
const sheetResult = await updateSheetOnly(releaseData);
|
||
showStatusMessage('allStatus', `${prefix ? prefix + '. ' : ''}${sheetResult.message}`, sheetResult.success);
|
||
} else {
|
||
showStatusMessage('allStatus', `${prefix || 'Done'} (Sheets not configured)`, true);
|
||
}
|
||
} else {
|
||
const parts = [doLabel && 'Label printed', doJson && 'JSON saved'].filter(Boolean);
|
||
if (parts.length) showStatusMessage('allStatus', parts.join(', '), true);
|
||
}
|
||
delete releaseData._fixedTimestamp;
|
||
}
|
||
|
||
// Mapping tab functionality
|
||
const DEFAULT_COLUMN_MAPPINGS = {
|
||
'A': 'timestamp',
|
||
'B': 'id',
|
||
'C': 'artist',
|
||
'D': 'title',
|
||
'E': 'genre',
|
||
'F': 'style',
|
||
'G': 'label',
|
||
'H': 'year',
|
||
'I': 'country',
|
||
'J': 'format',
|
||
'K': 'mediaCondition',
|
||
'L': 'sleeveCondition',
|
||
'M': 'price',
|
||
'N': 'have',
|
||
'O': 'want',
|
||
'P': 'numForSale',
|
||
'Q': 'lowPrice',
|
||
'R': 'medianPrice',
|
||
'S': 'highPrice',
|
||
'T': 'lastSold',
|
||
'U': 'tracklist',
|
||
'V': 'extraArtists',
|
||
'W': 'videos',
|
||
'X': 'priceSuggestions',
|
||
'Y': 'labelId',
|
||
'Z': 'artistIds',
|
||
'AA': '',
|
||
'AB': 'imageUrl',
|
||
'AC': 'comment',
|
||
'AD': 'imageUrls',
|
||
'AE': 'appleId',
|
||
'AF': 'catno',
|
||
'AG': 'trackArtists',
|
||
'AH': 'collectionFolder',
|
||
'AI': 'notes',
|
||
'AJ': 'companies',
|
||
'AK': 'identifiers',
|
||
'AL': 'estimatedWeight'
|
||
};
|
||
|
||
const FIELD_OPTIONS = [
|
||
{ value: '', label: 'Skip (empty)' },
|
||
{ value: 'timestamp', label: 'Timestamp' },
|
||
{ value: 'id', label: 'Release ID' },
|
||
{ value: 'artist', label: 'Artist' },
|
||
{ value: 'title', label: 'Title' },
|
||
{ value: 'genre', label: 'Genre' },
|
||
{ value: 'style', label: 'Style' },
|
||
{ value: 'label', label: 'Label' },
|
||
{ value: 'year', label: 'Year' },
|
||
{ value: 'country', label: 'Country' },
|
||
{ value: 'format', label: 'Format' },
|
||
{ value: 'mediaCondition', label: 'Media Condition' },
|
||
{ value: 'sleeveCondition', label: 'Sleeve Condition' },
|
||
{ value: 'price', label: 'Price' },
|
||
{ value: 'have', label: 'Have' },
|
||
{ value: 'want', label: 'Want' },
|
||
{ value: 'numForSale', label: 'Num For Sale' },
|
||
{ value: 'lowPrice', label: 'Low Price' },
|
||
{ value: 'medianPrice', label: 'Median Price' },
|
||
{ value: 'highPrice', label: 'High Price' },
|
||
{ value: 'lastSold', label: 'Last Sold' },
|
||
{ value: 'tracklist', label: 'Tracklist' },
|
||
{ value: 'extraArtists', label: 'Extra Artists' },
|
||
{ value: 'videos', label: 'Videos' },
|
||
{ value: 'priceSuggestions', label: 'Price Suggestions' },
|
||
{ value: 'labelId', label: 'Label ID' },
|
||
{ value: 'artistIds', label: 'Artist IDs' },
|
||
{ value: 'imageUrl', label: 'Image URL' },
|
||
{ value: 'comment', label: 'Comment' },
|
||
{ value: 'imageUrls', label: 'Image URLs' },
|
||
{ value: 'appleId', label: 'Apple ID' },
|
||
{ value: 'catno', label: 'Catalog Number' },
|
||
{ value: 'trackArtists', label: 'Track Artists' },
|
||
{ value: 'collectionFolder', label: 'Collection Folder' },
|
||
{ value: 'notes', label: 'Notes' },
|
||
{ value: 'companies', label: 'Companies' },
|
||
{ value: 'identifiers', label: 'Identifiers' },
|
||
{ value: 'estimatedWeight', label: 'Estimated Weight' },
|
||
{ value: 'actualWeight', label: 'Actual Weight (scale)' }
|
||
];
|
||
|
||
// ── DYMO M10 scale (via rfid-daemon :7790) ────────────────────────────────────
|
||
// While a session is active the daemon keeps the HID device open and the scale
|
||
// stays awake. Click the pill to start/stop a session. With session OFF, /weight
|
||
// still works one-shot (opens, reads, closes) — useful for quick checks without
|
||
// keeping the USB warm.
|
||
const SCALE_DAEMON_BASE = 'http://localhost:7790';
|
||
let lastScale = { online: false, grams: null, stable: false, sessionActive: false, ts: 0 };
|
||
|
||
async function pollScaleOnce() {
|
||
try {
|
||
const r = await fetch(`${SCALE_DAEMON_BASE}/weight`, { cache: 'no-store' });
|
||
if (!r.ok) { lastScale = { online: false, grams: null, stable: false, sessionActive: false, ts: Date.now() }; return; }
|
||
const j = await r.json();
|
||
lastScale = {
|
||
online: !!j.online,
|
||
grams: j.grams != null ? Number(j.grams) : null,
|
||
stable: !!j.stable,
|
||
sessionActive: !!j.sessionActive,
|
||
ts: Date.now()
|
||
};
|
||
} catch (e) {
|
||
lastScale = { online: false, grams: null, stable: false, sessionActive: false, ts: Date.now() };
|
||
}
|
||
renderScalePill();
|
||
}
|
||
|
||
function renderScalePill() {
|
||
const pill = document.getElementById('scale-pill');
|
||
if (!pill) return;
|
||
chrome.storage.local.get(['scaleShowOnDiscogs'], (s) => {
|
||
const show = s.scaleShowOnDiscogs !== false; // default ON
|
||
if (!show) { pill.style.display = 'none'; return; }
|
||
pill.style.display = 'inline-block';
|
||
pill.style.cursor = 'pointer';
|
||
pill.title = lastScale.sessionActive
|
||
? 'Scale session ON — click to stop (lets scale auto-power-off to save batteries)'
|
||
: 'Scale session OFF — click to start (keeps scale awake, reads are instant)';
|
||
|
||
if (lastScale.sessionActive) {
|
||
if (!lastScale.online || lastScale.grams == null) {
|
||
pill.style.background = '#fde2e1';
|
||
pill.style.color = '#c33';
|
||
pill.textContent = '⚖ ON · scale asleep?';
|
||
} else if (lastScale.stable) {
|
||
pill.style.background = '#e6f4ea';
|
||
pill.style.color = '#0a7';
|
||
pill.textContent = `⚖ ON · ${lastScale.grams}g (stable)`;
|
||
} else {
|
||
pill.style.background = '#fff3cd';
|
||
pill.style.color = '#856404';
|
||
pill.textContent = `⚖ ON · ${lastScale.grams}g …settling`;
|
||
}
|
||
} else {
|
||
pill.style.background = '#eee';
|
||
pill.style.color = '#666';
|
||
if (lastScale.online && lastScale.grams != null) {
|
||
pill.textContent = `⚖ OFF · last ${lastScale.grams}g`;
|
||
} else {
|
||
pill.textContent = '⚖ OFF · click to start session';
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
async function scaleSessionToggle() {
|
||
const desiredOn = !lastScale.sessionActive;
|
||
try {
|
||
const r = await fetch(`${SCALE_DAEMON_BASE}/scale/${desiredOn ? 'start' : 'stop'}`, { method: 'POST' });
|
||
const j = await r.json();
|
||
if (!r.ok || j.error) throw new Error(j.error || `HTTP ${r.status}`);
|
||
} catch (e) {
|
||
const pill = document.getElementById('scale-pill');
|
||
if (pill) {
|
||
const prev = pill.textContent;
|
||
pill.textContent = '✗ ' + (e.message || 'toggle failed');
|
||
pill.style.background = '#fde2e1';
|
||
pill.style.color = '#c33';
|
||
setTimeout(() => { pill.textContent = prev; pollScaleOnce(); }, 2000);
|
||
}
|
||
return;
|
||
}
|
||
// Fast follow-up poll so the UI updates within a frame.
|
||
pollScaleOnce();
|
||
}
|
||
|
||
// One-shot fresh read for the save/update flows — returns most-recent grams,
|
||
// or null if scale is offline / would-be-unstable when requireStable=true.
|
||
async function getCurrentWeight({ requireStable = true } = {}) {
|
||
await pollScaleOnce();
|
||
if (!lastScale.online || lastScale.grams == null) return null;
|
||
if (requireStable && !lastScale.stable) return null;
|
||
return lastScale.grams;
|
||
}
|
||
|
||
// Wire the pill click and kick off polling.
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const pill = document.getElementById('scale-pill');
|
||
if (pill) pill.addEventListener('click', scaleSessionToggle);
|
||
});
|
||
setInterval(pollScaleOnce, 700);
|
||
setTimeout(pollScaleOnce, 50);
|
||
|
||
// Global variable to store detected sheet structure
|
||
let detectedSheetStructure = null;
|
||
|
||
function initializeMappingTab() {
|
||
const mappingGrid = document.querySelector('.mapping-grid');
|
||
if (!mappingGrid) return;
|
||
|
||
// Clear existing content
|
||
mappingGrid.innerHTML = '';
|
||
|
||
// Use detected structure if available, otherwise use default A-AL
|
||
const columns = detectedSheetStructure ?
|
||
detectedSheetStructure.columns :
|
||
getDefaultColumns();
|
||
|
||
columns.forEach(column => {
|
||
const item = document.createElement('div');
|
||
item.className = 'mapping-item';
|
||
|
||
const label = document.createElement('label');
|
||
label.textContent = column.header || getColumnLabel(column.letter);
|
||
|
||
const columnLetter = document.createElement('div');
|
||
columnLetter.className = 'column-letter';
|
||
columnLetter.textContent = column.letter;
|
||
|
||
const select = document.createElement('select');
|
||
select.id = `mapping-${column.letter}`;
|
||
|
||
// Add options
|
||
FIELD_OPTIONS.forEach(option => {
|
||
const optionElement = document.createElement('option');
|
||
optionElement.value = option.value;
|
||
optionElement.textContent = option.label;
|
||
select.appendChild(optionElement);
|
||
});
|
||
|
||
// Set default value
|
||
select.value = DEFAULT_COLUMN_MAPPINGS[column.letter] || '';
|
||
|
||
item.appendChild(label);
|
||
item.appendChild(columnLetter);
|
||
item.appendChild(select);
|
||
mappingGrid.appendChild(item);
|
||
});
|
||
|
||
// Load saved mappings
|
||
loadMappings();
|
||
}
|
||
|
||
function getDefaultColumns() {
|
||
const letters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'AA', 'AB', 'AC', 'AD', 'AE', 'AF', 'AG', 'AH', 'AI', 'AJ', 'AK', 'AL'];
|
||
return letters.map(letter => ({ letter, header: '', index: letters.indexOf(letter) }));
|
||
}
|
||
|
||
async function detectSheetStructureUI() {
|
||
try {
|
||
showStatusMessage('mappingStatus', 'Detecting sheet structure...', true);
|
||
|
||
// Get Google Sheets settings
|
||
const settings = await new Promise(resolve => {
|
||
chrome.storage.local.get(['googleSettings'], result => {
|
||
resolve(result.googleSettings || {});
|
||
});
|
||
});
|
||
|
||
if (!settings.spreadsheetId) {
|
||
throw new Error('No spreadsheet ID configured. Please set up Google Sheets in Settings first.');
|
||
}
|
||
|
||
// Call the detection function from sheets.js (different name to avoid conflict)
|
||
const structure = await detectSheetStructure(settings.spreadsheetId, settings.sheetName || 'Sheet1');
|
||
detectedSheetStructure = structure;
|
||
|
||
// Update UI with detected info
|
||
document.getElementById('detected-sheet-name').textContent = structure.sheetName;
|
||
document.getElementById('detected-column-range').textContent = `A:${structure.endColumn} (${structure.columnCount} columns)`;
|
||
|
||
// Regenerate the mapping grid with detected columns
|
||
initializeMappingTab();
|
||
|
||
showStatusMessage('mappingStatus', `Detected ${structure.columnCount} columns from sheet "${structure.sheetName}"`, true);
|
||
|
||
} catch (error) {
|
||
console.error('Error detecting sheet structure:', error);
|
||
showStatusMessage('mappingStatus', `Error: ${error.message}`, false);
|
||
}
|
||
}
|
||
|
||
async function autoMapHeaders() {
|
||
if (!detectedSheetStructure) {
|
||
showStatusMessage('mappingStatus', 'Please detect sheet structure first', false);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
showStatusMessage('mappingStatus', 'Auto-mapping headers...', true);
|
||
|
||
// Use the detection function from sheets.js
|
||
const autoMappings = detectFieldMappings(detectedSheetStructure.columns);
|
||
|
||
// Apply the auto-detected mappings to the UI
|
||
Object.keys(autoMappings).forEach(column => {
|
||
const select = document.getElementById(`mapping-${column}`);
|
||
if (select) {
|
||
select.value = autoMappings[column];
|
||
}
|
||
});
|
||
|
||
showStatusMessage('mappingStatus', 'Headers auto-mapped based on column names!', true);
|
||
|
||
} catch (error) {
|
||
console.error('Error auto-mapping headers:', error);
|
||
showStatusMessage('mappingStatus', `Error: ${error.message}`, false);
|
||
}
|
||
}
|
||
|
||
function toggleManualRange() {
|
||
const manualInput = document.getElementById('manual-range');
|
||
const toggleButton = document.getElementById('toggle-manual-range');
|
||
const detectedRange = document.getElementById('detected-column-range');
|
||
|
||
if (manualInput.style.display === 'none') {
|
||
manualInput.style.display = 'inline-block';
|
||
manualInput.focus();
|
||
toggleButton.textContent = 'Apply';
|
||
detectedRange.style.display = 'none';
|
||
} else {
|
||
const rangeValue = manualInput.value.trim();
|
||
if (rangeValue) {
|
||
applyManualRange(rangeValue);
|
||
}
|
||
manualInput.style.display = 'none';
|
||
toggleButton.textContent = 'Manual';
|
||
detectedRange.style.display = 'inline';
|
||
}
|
||
}
|
||
|
||
function applyManualRange(rangeString) {
|
||
try {
|
||
// Parse range like "A:AL" or "A:Z"
|
||
const match = rangeString.match(/^([A-Z]+):([A-Z]+)$/i);
|
||
if (!match) {
|
||
throw new Error('Invalid range format. Use format like "A:AL"');
|
||
}
|
||
|
||
const startCol = match[1].toUpperCase();
|
||
const endCol = match[2].toUpperCase();
|
||
|
||
// Generate columns for the range
|
||
const columns = [];
|
||
let currentCol = startCol;
|
||
|
||
while (true) {
|
||
columns.push({ letter: currentCol, header: '', index: columns.length });
|
||
if (currentCol === endCol) break;
|
||
currentCol = getNextColumnLetter(currentCol);
|
||
}
|
||
|
||
// Create a manual structure
|
||
detectedSheetStructure = {
|
||
columnCount: columns.length,
|
||
endColumn: endCol,
|
||
columns: columns,
|
||
sheetName: 'Manual Range'
|
||
};
|
||
|
||
// Update UI
|
||
document.getElementById('detected-sheet-name').textContent = 'Manual Range';
|
||
document.getElementById('detected-column-range').textContent = `${startCol}:${endCol} (${columns.length} columns)`;
|
||
|
||
// Regenerate mapping grid
|
||
initializeMappingTab();
|
||
|
||
showStatusMessage('mappingStatus', `Applied manual range: ${rangeString}`, true);
|
||
|
||
} catch (error) {
|
||
showStatusMessage('mappingStatus', `Error: ${error.message}`, false);
|
||
}
|
||
}
|
||
|
||
function getNextColumnLetter(current) {
|
||
let result = '';
|
||
let carry = 1;
|
||
|
||
for (let i = current.length - 1; i >= 0; i--) {
|
||
let charCode = current.charCodeAt(i) - 65 + carry;
|
||
if (charCode > 25) {
|
||
result = 'A' + result;
|
||
carry = 1;
|
||
} else {
|
||
result = String.fromCharCode(65 + charCode) + result;
|
||
carry = 0;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (carry) {
|
||
result = 'A' + result;
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
function getColumnLabel(column) {
|
||
const labels = {
|
||
'A': 'TIMESTAMP', 'B': 'ID', 'C': 'ARTIST', 'D': 'TITLE', 'E': 'GENRE', 'F': 'STYLE',
|
||
'G': 'LABEL', 'H': 'YEAR', 'I': 'COUNTRY', 'J': 'FORMAT', 'K': 'MEDIA COND',
|
||
'L': 'SLEEVE COND', 'M': 'PRICE', 'N': 'HAVE', 'O': 'WANT', 'P': 'NUM FOR SALE',
|
||
'Q': 'LOW PRICE', 'R': 'MEDIAN PRICE', 'S': 'HIGH PRICE', 'T': 'LAST SOLD',
|
||
'U': 'TRACKLIST', 'V': 'EXTRA ARTISTS', 'W': 'VIDEOS', 'X': 'PRICE SUGG',
|
||
'Y': 'LABEL ID', 'Z': 'ARTIST IDS', 'AA': 'EMPTY', 'AB': 'IMAGE',
|
||
'AC': 'COMMENT', 'AD': 'IMAGE URLS', 'AE': 'APPLE ID', 'AF': 'CATNO',
|
||
'AG': 'TRACK ARTISTS', 'AH': 'COLL FOLDER', 'AI': 'NOTES', 'AJ': 'COMPANIES',
|
||
'AK': 'IDENTIFIERS', 'AL': 'EST WT'
|
||
};
|
||
return labels[column] || column;
|
||
}
|
||
|
||
function saveMappings() {
|
||
const mappings = {};
|
||
const columns = detectedSheetStructure ?
|
||
detectedSheetStructure.columns.map(c => c.letter) :
|
||
['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'AA', 'AB', 'AC', 'AD', 'AE', 'AF', 'AG', 'AH', 'AI', 'AJ', 'AK', 'AL'];
|
||
|
||
columns.forEach(column => {
|
||
const select = document.getElementById(`mapping-${column}`);
|
||
if (select) {
|
||
mappings[column] = select.value;
|
||
}
|
||
});
|
||
|
||
// Also save the detected structure for future use
|
||
const saveData = {
|
||
columnMappings: mappings,
|
||
detectedStructure: detectedSheetStructure
|
||
};
|
||
|
||
chrome.storage.local.set(saveData, () => {
|
||
showStatusMessage('mappingStatus', 'Mappings saved successfully!', true);
|
||
});
|
||
}
|
||
|
||
function loadMappings() {
|
||
chrome.storage.local.get(['columnMappings', 'detectedStructure'], (result) => {
|
||
const savedMappings = result.columnMappings || {};
|
||
|
||
// Restore detected structure if available
|
||
if (result.detectedStructure) {
|
||
detectedSheetStructure = result.detectedStructure;
|
||
document.getElementById('detected-sheet-name').textContent = detectedSheetStructure.sheetName;
|
||
document.getElementById('detected-column-range').textContent = `A:${detectedSheetStructure.endColumn} (${detectedSheetStructure.columnCount} columns)`;
|
||
}
|
||
|
||
Object.keys(savedMappings).forEach(column => {
|
||
const select = document.getElementById(`mapping-${column}`);
|
||
if (select) {
|
||
select.value = savedMappings[column];
|
||
}
|
||
});
|
||
|
||
if (Object.keys(savedMappings).length > 0) {
|
||
showStatusMessage('mappingStatus', 'Mappings loaded successfully!', true);
|
||
}
|
||
});
|
||
}
|
||
|
||
function resetMappings() {
|
||
const columns = detectedSheetStructure ?
|
||
detectedSheetStructure.columns.map(c => c.letter) :
|
||
['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'AA', 'AB', 'AC', 'AD', 'AE', 'AF', 'AG', 'AH', 'AI', 'AJ', 'AK', 'AL'];
|
||
|
||
columns.forEach(column => {
|
||
const select = document.getElementById(`mapping-${column}`);
|
||
if (select) {
|
||
select.value = DEFAULT_COLUMN_MAPPINGS[column] || '';
|
||
}
|
||
});
|
||
|
||
showStatusMessage('mappingStatus', 'Mappings reset to defaults!', true);
|
||
}
|
||
|
||
// Export function to get current mappings for use in sheets.js
|
||
function getCurrentMappings() {
|
||
return new Promise((resolve) => {
|
||
chrome.storage.local.get(['columnMappings'], (result) => {
|
||
const savedMappings = result.columnMappings || {};
|
||
const finalMappings = { ...DEFAULT_COLUMN_MAPPINGS, ...savedMappings };
|
||
resolve(finalMappings);
|
||
});
|
||
});
|
||
}
|
||
|
||
// ============================================
|
||
// TEXT LABEL TAB FUNCTIONALITY
|
||
// ============================================
|
||
|
||
// Initialize text label tab
|
||
function initializeTextLabelTab() {
|
||
const textInput = document.getElementById('text-input-area');
|
||
const previewContent = document.getElementById('label-preview-content');
|
||
const previewBox = document.getElementById('label-preview-box');
|
||
const fontFamilySelect = document.getElementById('text-font-family');
|
||
const fontSizeInput = document.getElementById('text-font-size');
|
||
const boldButton = document.getElementById('text-bold');
|
||
const underlineButton = document.getElementById('text-underline');
|
||
const wrapCheckbox = document.getElementById('text-wrap');
|
||
const printButton = document.getElementById('print-text-label');
|
||
const clearButton = document.getElementById('clear-text-label');
|
||
const labelSizeElement = document.getElementById('labelSize');
|
||
const currentLabelSizeDisplay = document.getElementById('current-label-size');
|
||
|
||
if (!textInput || !previewContent) return; // Elements don't exist yet
|
||
|
||
// State management for text label
|
||
const labelState = {
|
||
text: '',
|
||
fontFamily: 'Arial',
|
||
fontSize: 12,
|
||
bold: false,
|
||
underline: false,
|
||
wrap: true,
|
||
horizontalAlign: 'left',
|
||
verticalAlign: 'top'
|
||
};
|
||
|
||
// Update label size display
|
||
function updateLabelSizeDisplay() {
|
||
if (labelSizeElement && currentLabelSizeDisplay && previewBox) {
|
||
const size = labelSizeElement.value;
|
||
const sizeText = size === 'large' ? 'Large (25mm x 54mm)' : 'Small (19mm x 51mm)';
|
||
currentLabelSizeDisplay.textContent = sizeText;
|
||
|
||
// Update preview box size
|
||
if (size === 'small') {
|
||
previewBox.classList.add('small');
|
||
} else {
|
||
previewBox.classList.remove('small');
|
||
}
|
||
}
|
||
}
|
||
|
||
// Update preview based on current state
|
||
function updatePreview() {
|
||
// Update text content
|
||
previewContent.textContent = labelState.text || '';
|
||
|
||
// Apply font family
|
||
previewContent.style.fontFamily = labelState.fontFamily;
|
||
|
||
// Apply font size
|
||
previewContent.style.fontSize = labelState.fontSize + 'pt';
|
||
|
||
// Apply bold
|
||
if (labelState.bold) {
|
||
previewContent.classList.add('bold');
|
||
} else {
|
||
previewContent.classList.remove('bold');
|
||
}
|
||
|
||
// Apply underline
|
||
if (labelState.underline) {
|
||
previewContent.classList.add('underline');
|
||
} else {
|
||
previewContent.classList.remove('underline');
|
||
}
|
||
|
||
// Apply wrap
|
||
if (labelState.wrap) {
|
||
previewContent.classList.remove('no-wrap');
|
||
} else {
|
||
previewContent.classList.add('no-wrap');
|
||
}
|
||
|
||
// Apply horizontal alignment
|
||
previewContent.classList.remove('align-left', 'align-center', 'align-right');
|
||
previewContent.classList.add('align-' + labelState.horizontalAlign);
|
||
|
||
// Apply vertical alignment
|
||
previewContent.classList.remove('valign-top', 'valign-middle', 'valign-bottom');
|
||
previewContent.classList.add('valign-' + labelState.verticalAlign);
|
||
}
|
||
|
||
// Listen for label size changes in main tab
|
||
if (labelSizeElement) {
|
||
labelSizeElement.addEventListener('change', updateLabelSizeDisplay);
|
||
updateLabelSizeDisplay(); // Initial update
|
||
}
|
||
|
||
// Text input change
|
||
if (textInput) {
|
||
textInput.addEventListener('input', () => {
|
||
labelState.text = textInput.value;
|
||
updatePreview();
|
||
});
|
||
}
|
||
|
||
// Font family change
|
||
if (fontFamilySelect) {
|
||
fontFamilySelect.addEventListener('change', () => {
|
||
labelState.fontFamily = fontFamilySelect.value;
|
||
updatePreview();
|
||
});
|
||
}
|
||
|
||
// Font size change
|
||
if (fontSizeInput) {
|
||
fontSizeInput.addEventListener('input', () => {
|
||
const size = parseInt(fontSizeInput.value) || 12;
|
||
labelState.fontSize = size;
|
||
updatePreview();
|
||
});
|
||
}
|
||
|
||
// Bold toggle
|
||
if (boldButton) {
|
||
boldButton.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
labelState.bold = !labelState.bold;
|
||
boldButton.classList.toggle('active');
|
||
updatePreview();
|
||
});
|
||
}
|
||
|
||
// Underline toggle
|
||
if (underlineButton) {
|
||
underlineButton.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
labelState.underline = !labelState.underline;
|
||
underlineButton.classList.toggle('active');
|
||
updatePreview();
|
||
});
|
||
}
|
||
|
||
// Word wrap toggle
|
||
if (wrapCheckbox) {
|
||
wrapCheckbox.addEventListener('change', () => {
|
||
labelState.wrap = wrapCheckbox.checked;
|
||
updatePreview();
|
||
});
|
||
}
|
||
|
||
// Horizontal alignment buttons
|
||
const alignLeftBtn = document.getElementById('align-left');
|
||
const alignCenterBtn = document.getElementById('align-center');
|
||
const alignRightBtn = document.getElementById('align-right');
|
||
|
||
if (alignLeftBtn) {
|
||
alignLeftBtn.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
labelState.horizontalAlign = 'left';
|
||
alignLeftBtn.classList.add('active');
|
||
alignCenterBtn?.classList.remove('active');
|
||
alignRightBtn?.classList.remove('active');
|
||
updatePreview();
|
||
});
|
||
}
|
||
|
||
if (alignCenterBtn) {
|
||
alignCenterBtn.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
labelState.horizontalAlign = 'center';
|
||
alignCenterBtn.classList.add('active');
|
||
alignLeftBtn?.classList.remove('active');
|
||
alignRightBtn?.classList.remove('active');
|
||
updatePreview();
|
||
});
|
||
}
|
||
|
||
if (alignRightBtn) {
|
||
alignRightBtn.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
labelState.horizontalAlign = 'right';
|
||
alignRightBtn.classList.add('active');
|
||
alignLeftBtn?.classList.remove('active');
|
||
alignCenterBtn?.classList.remove('active');
|
||
updatePreview();
|
||
});
|
||
}
|
||
|
||
// Vertical alignment buttons
|
||
const alignTopBtn = document.getElementById('align-top');
|
||
const alignMiddleBtn = document.getElementById('align-middle');
|
||
const alignBottomBtn = document.getElementById('align-bottom');
|
||
|
||
if (alignTopBtn) {
|
||
alignTopBtn.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
labelState.verticalAlign = 'top';
|
||
alignTopBtn.classList.add('active');
|
||
alignMiddleBtn?.classList.remove('active');
|
||
alignBottomBtn?.classList.remove('active');
|
||
updatePreview();
|
||
});
|
||
}
|
||
|
||
if (alignMiddleBtn) {
|
||
alignMiddleBtn.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
labelState.verticalAlign = 'middle';
|
||
alignMiddleBtn.classList.add('active');
|
||
alignTopBtn?.classList.remove('active');
|
||
alignBottomBtn?.classList.remove('active');
|
||
updatePreview();
|
||
});
|
||
}
|
||
|
||
if (alignBottomBtn) {
|
||
alignBottomBtn.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
labelState.verticalAlign = 'bottom';
|
||
alignBottomBtn.classList.add('active');
|
||
alignTopBtn?.classList.remove('active');
|
||
alignMiddleBtn?.classList.remove('active');
|
||
updatePreview();
|
||
});
|
||
}
|
||
|
||
// Clear button
|
||
if (clearButton) {
|
||
clearButton.addEventListener('click', () => {
|
||
// Reset state
|
||
labelState.text = '';
|
||
labelState.fontFamily = 'Arial';
|
||
labelState.fontSize = 12;
|
||
labelState.bold = false;
|
||
labelState.underline = false;
|
||
labelState.wrap = true;
|
||
labelState.horizontalAlign = 'left';
|
||
labelState.verticalAlign = 'top';
|
||
|
||
// Reset UI
|
||
textInput.value = '';
|
||
fontFamilySelect.value = 'Arial';
|
||
fontSizeInput.value = '12';
|
||
boldButton.classList.remove('active');
|
||
underlineButton.classList.remove('active');
|
||
wrapCheckbox.checked = true;
|
||
|
||
// Reset alignment buttons
|
||
const alignLeftBtn = document.getElementById('align-left');
|
||
const alignCenterBtn = document.getElementById('align-center');
|
||
const alignRightBtn = document.getElementById('align-right');
|
||
const alignTopBtn = document.getElementById('align-top');
|
||
const alignMiddleBtn = document.getElementById('align-middle');
|
||
const alignBottomBtn = document.getElementById('align-bottom');
|
||
|
||
alignLeftBtn?.classList.add('active');
|
||
alignCenterBtn?.classList.remove('active');
|
||
alignRightBtn?.classList.remove('active');
|
||
alignTopBtn?.classList.add('active');
|
||
alignMiddleBtn?.classList.remove('active');
|
||
alignBottomBtn?.classList.remove('active');
|
||
|
||
// Update preview
|
||
updatePreview();
|
||
});
|
||
}
|
||
|
||
// Print button
|
||
if (printButton) {
|
||
printButton.addEventListener('click', () => {
|
||
printTextLabel(labelState);
|
||
});
|
||
}
|
||
|
||
// Initial preview update
|
||
updatePreview();
|
||
}
|
||
|
||
// Print text label function
|
||
function printTextLabel(labelState) {
|
||
const labelSizeElement = document.getElementById('labelSize');
|
||
const statusElement = document.getElementById('text-label-status');
|
||
|
||
// Check if there's actual text content
|
||
if (!labelState.text || labelState.text.trim() === '') {
|
||
if (statusElement) {
|
||
statusElement.textContent = 'Please enter some text first';
|
||
statusElement.style.color = '#dc3545';
|
||
setTimeout(() => {
|
||
statusElement.textContent = '';
|
||
}, 3000);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Get label size
|
||
const labelSize = labelSizeElement ? labelSizeElement.value : 'large';
|
||
const dimensions = labelSize === 'large'
|
||
? { width: '54mm', height: '25mm' }
|
||
: { width: '51mm', height: '19mm' };
|
||
|
||
// Build text styles
|
||
const fontWeight = labelState.bold ? 'bold' : 'normal';
|
||
const textDecoration = labelState.underline ? 'underline' : 'none';
|
||
const whiteSpace = labelState.wrap ? 'pre-wrap' : 'pre';
|
||
|
||
// Map horizontal alignment to flexbox align-items
|
||
let alignItems = 'flex-start';
|
||
if (labelState.horizontalAlign === 'center') {
|
||
alignItems = 'center';
|
||
} else if (labelState.horizontalAlign === 'right') {
|
||
alignItems = 'flex-end';
|
||
}
|
||
|
||
// Map vertical alignment to flexbox justify-content
|
||
let justifyContent = 'flex-start';
|
||
if (labelState.verticalAlign === 'middle') {
|
||
justifyContent = 'center';
|
||
} else if (labelState.verticalAlign === 'bottom') {
|
||
justifyContent = 'flex-end';
|
||
}
|
||
|
||
// Create print window content
|
||
const printContent = `
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<title>Print Text Label</title>
|
||
<style>
|
||
@page {
|
||
size: ${dimensions.width} ${dimensions.height};
|
||
margin: 0;
|
||
}
|
||
|
||
body {
|
||
margin: 0;
|
||
padding: 0;
|
||
width: ${dimensions.width};
|
||
height: ${dimensions.height};
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.label-content {
|
||
width: 100%;
|
||
height: 100%;
|
||
padding: 2mm;
|
||
box-sizing: border-box;
|
||
font-family: ${labelState.fontFamily};
|
||
font-size: ${labelState.fontSize}pt;
|
||
font-weight: ${fontWeight};
|
||
text-decoration: ${textDecoration};
|
||
white-space: ${whiteSpace};
|
||
overflow: hidden;
|
||
word-wrap: break-word;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: ${alignItems};
|
||
justify-content: ${justifyContent};
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="label-content">${escapeHtml(labelState.text)}</div>
|
||
<script>
|
||
window.onload = function() {
|
||
window.print();
|
||
setTimeout(function() {
|
||
window.close();
|
||
}, 100);
|
||
};
|
||
</script>
|
||
</body>
|
||
</html>
|
||
`;
|
||
|
||
// Open print window
|
||
const printWindow = window.open('', '_blank', 'width=600,height=400');
|
||
if (printWindow) {
|
||
printWindow.document.open();
|
||
printWindow.document.write(printContent);
|
||
printWindow.document.close();
|
||
|
||
// Show success message
|
||
if (statusElement) {
|
||
statusElement.textContent = 'Label sent to printer!';
|
||
statusElement.style.color = '#28a745';
|
||
setTimeout(() => {
|
||
statusElement.textContent = '';
|
||
}, 3000);
|
||
}
|
||
} else {
|
||
// Show error if popup was blocked
|
||
if (statusElement) {
|
||
statusElement.textContent = 'Print window blocked. Please allow popups.';
|
||
statusElement.style.color = '#dc3545';
|
||
setTimeout(() => {
|
||
statusElement.textContent = '';
|
||
}, 3000);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Helper function to escape HTML
|
||
function escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
// Initialize QR Code tab
|
||
function initializeQRCodeTab() {
|
||
const qrcodeInput = document.getElementById('qrcode-integer-input');
|
||
const qrcodePreviewImg = document.getElementById('qrcode-preview-img');
|
||
const printButton = document.getElementById('print-qrcode-label');
|
||
const clearButton = document.getElementById('clear-qrcode');
|
||
const statusElement = document.getElementById('qrcode-status');
|
||
|
||
if (!qrcodeInput) return; // Elements don't exist yet
|
||
|
||
// Generate QR code URL with maximum size for preview
|
||
function generateQRUrl(integer, size) {
|
||
return `https://api.qrserver.com/v1/create-qr-code/?size=${size}x${size}&data=${encodeURIComponent(String(integer))}`;
|
||
}
|
||
|
||
// Update preview when input changes
|
||
function updatePreview() {
|
||
const value = qrcodeInput.value.trim();
|
||
if (value === '' || isNaN(parseInt(value))) {
|
||
qrcodePreviewImg.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
// Use 200x200 for preview (will scale to fit)
|
||
qrcodePreviewImg.src = generateQRUrl(parseInt(value), 200);
|
||
qrcodePreviewImg.style.display = 'block';
|
||
}
|
||
|
||
// Listen for input changes
|
||
qrcodeInput.addEventListener('input', updatePreview);
|
||
|
||
// Print button click handler
|
||
if (printButton) {
|
||
printButton.addEventListener('click', function() {
|
||
const value = qrcodeInput.value.trim();
|
||
|
||
if (value === '' || isNaN(parseInt(value))) {
|
||
if (statusElement) {
|
||
statusElement.textContent = 'Please enter a valid integer';
|
||
statusElement.style.color = '#dc3545';
|
||
setTimeout(() => { statusElement.textContent = ''; }, 3000);
|
||
}
|
||
return;
|
||
}
|
||
|
||
const integer = parseInt(value);
|
||
|
||
// Label dimensions: 54mm x 25mm (large label)
|
||
// QR code is square, so height is limiting factor (25mm)
|
||
// Use 300x300 for best quality, CSS will scale to fit height
|
||
|
||
const qrSize = 300; // Use 300x300 for best quality
|
||
|
||
const qrUrl = generateQRUrl(integer, qrSize);
|
||
|
||
// Create print content - maximize QR code on label
|
||
const printContent = `
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<title>Print QR Code</title>
|
||
<style>
|
||
@page {
|
||
size: 54mm 25mm;
|
||
margin: 0;
|
||
}
|
||
|
||
body {
|
||
margin: 0;
|
||
padding: 0;
|
||
width: 54mm;
|
||
height: 25mm;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: white;
|
||
}
|
||
|
||
.qr-container {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.qr-container img {
|
||
max-width: 95%;
|
||
max-height: 95%;
|
||
object-fit: contain;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="qr-container">
|
||
<img src="${qrUrl}" alt="${integer}">
|
||
</div>
|
||
<script>
|
||
window.onload = function() {
|
||
window.print();
|
||
setTimeout(function() {
|
||
window.close();
|
||
}, 500);
|
||
};
|
||
</script>
|
||
</body>
|
||
</html>
|
||
`;
|
||
|
||
// Open print window
|
||
const printWindow = window.open('', '_blank', 'width=600,height=400');
|
||
if (printWindow) {
|
||
printWindow.document.open();
|
||
printWindow.document.write(printContent);
|
||
printWindow.document.close();
|
||
|
||
if (statusElement) {
|
||
statusElement.textContent = 'Label sent to printer!';
|
||
statusElement.style.color = '#28a745';
|
||
setTimeout(() => { statusElement.textContent = ''; }, 3000);
|
||
}
|
||
} else {
|
||
if (statusElement) {
|
||
statusElement.textContent = 'Print window blocked. Please allow popups.';
|
||
statusElement.style.color = '#dc3545';
|
||
setTimeout(() => { statusElement.textContent = ''; }, 3000);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Clear button click handler
|
||
if (clearButton) {
|
||
clearButton.addEventListener('click', function() {
|
||
qrcodeInput.value = '';
|
||
qrcodePreviewImg.style.display = 'none';
|
||
if (statusElement) {
|
||
statusElement.textContent = '';
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// Initialize text label tab when DOM is ready
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
// Add a small delay to ensure all elements are loaded
|
||
setTimeout(() => {
|
||
initializeTextLabelTab();
|
||
initializeQRCodeTab();
|
||
}, 100);
|
||
});
|
||
|
||
// ================================================================
|
||
// RESEARCH TAB — LLM Research Export
|
||
// ================================================================
|
||
|
||
/**
|
||
* Build the comprehensive LLM-ready JSON from all available release data.
|
||
*/
|
||
// buildResearchJson moved to monster_capture.js (loaded before popup.js) so the
|
||
// passive background auto-capture path shares identical payload shaping.
|
||
|
||
function buildGeminiInputJson(rd) {
|
||
const have = parseInt(rd.have) || 0;
|
||
const want = parseInt(rd.want) || 0;
|
||
|
||
const priceSugg = {};
|
||
if (rd.priceSuggestions) {
|
||
Object.entries(rd.priceSuggestions).forEach(([cond, data]) => {
|
||
const v = (data && typeof data === 'object') ? data.value : data;
|
||
if (v != null) priceSugg[cond] = v;
|
||
});
|
||
}
|
||
|
||
const tracklist = (rd.tracklist || [])
|
||
.filter(t => t.type_ !== 'heading')
|
||
.map(t => {
|
||
const track = { position: t.position, title: t.title };
|
||
if (t.duration) track.duration = t.duration;
|
||
const artists = (t.artists || [])
|
||
.map(a => a.name.replace(/\s*\(\d+\)$/, '').trim())
|
||
.filter(Boolean);
|
||
if (artists.length) track.artists = artists;
|
||
const extra = (t.extraartists || [])
|
||
.map(a => ({ name: a.name.replace(/\s*\(\d+\)$/, '').trim(), role: a.role }))
|
||
.filter(a => a.name);
|
||
if (extra.length) track.extraartists = extra;
|
||
return track;
|
||
});
|
||
|
||
const cleanReview = r => ({
|
||
...(r.username && { username: r.username }),
|
||
...(r.date && { date: r.date }),
|
||
...(r.rating && { rating: r.rating }),
|
||
...(r.text && { text: r.text.trim() }),
|
||
...(r.helpfulCount != null && r.helpfulCount > 0 && { helpful: r.helpfulCount }),
|
||
...(r.replies?.length && { replies: r.replies.map(cleanReview) }),
|
||
});
|
||
const reviews = (rd.reviews || [])
|
||
.filter(r => r.text || r.rating)
|
||
.map(cleanReview);
|
||
|
||
const formats = (rd.formats || [])
|
||
.map(f => [f.name, ...(f.descriptions || [])].filter(Boolean).join(', '));
|
||
|
||
const companies = (rd.companies || []).map(c => ({
|
||
name: c.name,
|
||
role: c.entity_type_name || c.entity_type,
|
||
})).filter(c => c.name);
|
||
|
||
return {
|
||
release: {
|
||
artist: rd.artist,
|
||
title: rd.title,
|
||
label: rd.label,
|
||
year: rd.year,
|
||
country: rd.country,
|
||
format: formats.join(' / '),
|
||
genre: rd.genres || (rd.genre ? rd.genre.split(', ') : []),
|
||
style: rd.styles || (rd.style ? rd.style.split(', ') : []),
|
||
tracklist,
|
||
companies,
|
||
identifiers: rd.identifiers || [],
|
||
},
|
||
market: {
|
||
have,
|
||
want,
|
||
for_sale: rd.num_for_sale != null ? parseInt(rd.num_for_sale) : null,
|
||
median_price: rd.medianPrice || null,
|
||
price_suggestions: priceSugg,
|
||
},
|
||
community: {
|
||
reviews,
|
||
},
|
||
};
|
||
}
|
||
|
||
function buildGeminiPromptText(inputJson) {
|
||
return buildGeminiPromptTextWithTemplate(defaultGeminiPromptTemplate(), inputJson);
|
||
}
|
||
|
||
function defaultGeminiPromptTemplate() {
|
||
return [
|
||
'If the output is not valid JSON, regenerate it correctly.',
|
||
'',
|
||
'You are a record store assistant writing structured data for a vinyl inventory system.',
|
||
'',
|
||
'INPUT:',
|
||
'A JSON object containing Discogs-style release, market, and community data.',
|
||
'',
|
||
'TASK:',
|
||
'Analyze the input and return ONLY valid JSON. No markdown, no commentary, no extra text.',
|
||
'',
|
||
'OUTPUT SCHEMA:',
|
||
'{',
|
||
' "summary": "Short factual description (1-2 sentences)",',
|
||
'',
|
||
' "store_description": "Punchy, slightly opinionated record store blurb (max 80 words, human tone, not generic AI)",',
|
||
'',
|
||
' "features": {',
|
||
' "special_traits": [],',
|
||
' "pressing_notes": "",',
|
||
' "notable_physical": ""',
|
||
' },',
|
||
'',
|
||
' "sound_profile": {',
|
||
' "genres": [],',
|
||
' "descriptors": [],',
|
||
' "dj_use": []',
|
||
' },',
|
||
'',
|
||
' "highlight_tracks": [',
|
||
' {',
|
||
' "title": "",',
|
||
' "note": ""',
|
||
' }',
|
||
' ],',
|
||
'',
|
||
' "production": {',
|
||
' "mastering": null,',
|
||
' "pressing": null,',
|
||
' "distribution": null,',
|
||
' "notes": ""',
|
||
' },',
|
||
'',
|
||
' "market_insight": {',
|
||
' "rarity": "common | uncommon | scarce",',
|
||
' "collector_notes": "",',
|
||
' "price_commentary": ""',
|
||
' },',
|
||
'',
|
||
' "sales_angles": [],',
|
||
'',
|
||
' "embedding": {',
|
||
' "description": "One clean sentence summarizing the record for similarity search",',
|
||
' "vibe": "One clean sentence describing sound/vibe (no lists)",',
|
||
' "sales": "One clean sentence describing who this is for / why it sells (no lists)"',
|
||
' },',
|
||
' "eom": "__PLICE_EOM__"',
|
||
'}',
|
||
'',
|
||
'RULES:',
|
||
'- Use only information from the input JSON',
|
||
'- Do NOT hallucinate credits or facts not present',
|
||
'- Infer vibe and sound from genre/style/reviews',
|
||
'- Keep language concise and information-dense',
|
||
'- "store_description" should feel like a real record shop, not corporate or overly polished',
|
||
'- Each embedding field must be a single natural sentence, no lists',
|
||
'- Do NOT invent or substitute production credits',
|
||
'- If data is missing, return null instead of guessing',
|
||
'- Only use company names explicitly present in the input',
|
||
'- Map roles exactly (Mastered At → mastering, Pressed By → pressing, Distributed By → distribution)',
|
||
'- Use exact track titles from input tracklist',
|
||
'- Do NOT rename or generalize track names',
|
||
'- The release title is NOT a track name',
|
||
'- Only use track titles found in input.tracklist',
|
||
'- If no track is clearly highlighted in the input, return an empty array',
|
||
'- Do NOT infer or create a “title track”',
|
||
'- If a review references a side (e.g. "B-side") and the tracklist contains a small number of tracks, infer the corresponding track.',
|
||
'- Always include the field "eom" with the exact value "__PLICE_EOM__" at the top level of the output JSON',
|
||
'',
|
||
'INPUT JSON:',
|
||
'{{INPUT_JSON}}',
|
||
].join('\n');
|
||
}
|
||
|
||
function buildGeminiPromptTextWithTemplate(template, inputJson) {
|
||
const inputStr = JSON.stringify(inputJson, null, 2);
|
||
if (!template || typeof template !== 'string') return defaultGeminiPromptTemplate().replace('{{INPUT_JSON}}', inputStr);
|
||
if (template.includes('{{INPUT_JSON}}')) return template.replace('{{INPUT_JSON}}', inputStr);
|
||
return template.trimEnd() + '\n\n' + inputStr;
|
||
}
|
||
|
||
async function startGeminiJob({ openInBackground, silent } = {}) {
|
||
if (!state.releaseData?.id) return { ok: false, error: 'No release data.' };
|
||
|
||
const editor = document.getElementById('gemini-input-editor');
|
||
let inputJson;
|
||
if (editor && editor.value.trim()) {
|
||
try { inputJson = JSON.parse(editor.value); }
|
||
catch { return { ok: false, error: 'Gemini Input JSON is not valid JSON.' }; }
|
||
} else {
|
||
inputJson = buildGeminiInputJson(state.releaseData);
|
||
}
|
||
|
||
const cfg = await new Promise(resolve =>
|
||
chrome.storage.local.get(['geminiPromptTemplate', 'aiProvider'], r => resolve(r || {}))
|
||
);
|
||
const tpl = cfg.geminiPromptTemplate || defaultGeminiPromptTemplate();
|
||
const provider = (cfg.aiProvider === 'deepseek') ? 'deepseek' : 'gemini';
|
||
const promptText = buildGeminiPromptTextWithTemplate(tpl, inputJson);
|
||
const jobId = `ai-${state.releaseData.id}-${Date.now()}`;
|
||
const job = {
|
||
job_id: jobId,
|
||
status: 'pending',
|
||
created_at: new Date().toISOString(),
|
||
model: provider === 'deepseek' ? 'deepseek-web' : 'gemini-web',
|
||
provider,
|
||
release_id: state.releaseData.id,
|
||
discogs_url: `https://www.discogs.com/release/${state.releaseData.id}`,
|
||
input_json: inputJson,
|
||
prompt: promptText,
|
||
};
|
||
|
||
await new Promise(resolve => chrome.storage.local.set({ geminiJob: job }, resolve));
|
||
const url = provider === 'deepseek' ? 'https://chat.deepseek.com/' : 'https://gemini.google.com/app';
|
||
chrome.tabs.create({ url, active: openInBackground !== true });
|
||
if (!silent) {
|
||
setResearchStatus(`${provider === 'deepseek' ? 'DeepSeek' : 'Gemini'} tab opened — running prompt…`, 'ok');
|
||
setTimeout(() => setResearchStatus(''), 4000);
|
||
}
|
||
return { ok: true, job_id: jobId };
|
||
}
|
||
|
||
function maybeAutoRunGemini() {
|
||
const rd = state.releaseData;
|
||
if (!rd?.id) return;
|
||
if (state.geminiAutoTriggered) return;
|
||
state.geminiAutoTriggered = true;
|
||
|
||
(async () => {
|
||
const cfg = await new Promise(resolve =>
|
||
chrome.storage.local.get(['geminiAutoRunOnPopup', 'geminiAutoLast', 'geminiJob'], r => resolve(r || {}))
|
||
);
|
||
if (cfg.geminiAutoRunOnPopup !== true) return;
|
||
|
||
const activeJob = cfg.geminiJob;
|
||
if (activeJob && ['pending', 'running', 'posting'].includes(activeJob.status)) return;
|
||
|
||
const last = cfg.geminiAutoLast || {};
|
||
const lastAt = last.at ? Date.parse(last.at) : 0;
|
||
if (last.release_id === rd.id && lastAt && (Date.now() - lastAt) < 6 * 60 * 60 * 1000) return;
|
||
|
||
await new Promise(resolve =>
|
||
chrome.storage.local.set({ geminiAutoLast: { release_id: rd.id, at: new Date().toISOString() } }, resolve)
|
||
);
|
||
await startGeminiJob({ openInBackground: true, silent: true });
|
||
})();
|
||
}
|
||
|
||
/**
|
||
* Populate the Research tab UI from state.releaseData.
|
||
* Safe to call even if no data yet — it gracefully hides sections.
|
||
*/
|
||
function updateResearchTab() {
|
||
const rd = state.releaseData;
|
||
const hasData = rd && rd.id;
|
||
|
||
// Elements
|
||
const descEl = document.getElementById('research-desc');
|
||
const card = document.getElementById('research-release-card');
|
||
const marketEl = document.getElementById('research-market');
|
||
const trackWrap = document.getElementById('research-tracklist-wrap');
|
||
const actionsEl = document.getElementById('research-actions');
|
||
const previewWrap = document.getElementById('research-preview-wrap');
|
||
|
||
if (!descEl) return; // tab not in DOM yet
|
||
|
||
if (!hasData) {
|
||
descEl.textContent = 'Navigate to a Discogs release page and open the popup to load release data.';
|
||
[card, marketEl, trackWrap, actionsEl, previewWrap].forEach(el => {
|
||
if (el) el.style.display = 'none';
|
||
});
|
||
return;
|
||
}
|
||
|
||
descEl.textContent = 'All available data captured — ready to export.';
|
||
|
||
// --- Release card ---
|
||
card.style.display = 'flex';
|
||
|
||
const img = document.getElementById('research-img');
|
||
const placeholder = document.getElementById('research-cover-placeholder');
|
||
const coverUrl = rd.imageUrl || ((rd.images || [])[0]?.uri) || '';
|
||
if (coverUrl) {
|
||
img.src = coverUrl;
|
||
img.style.display = 'block';
|
||
if (placeholder) placeholder.style.display = 'none';
|
||
} else {
|
||
img.style.display = 'none';
|
||
if (placeholder) placeholder.style.display = 'flex';
|
||
}
|
||
|
||
document.getElementById('research-artist').textContent = rd.artist || '';
|
||
document.getElementById('research-title').textContent = rd.title || '';
|
||
document.getElementById('research-label').textContent = rd.label || '';
|
||
document.getElementById('research-year').textContent = rd.year || '';
|
||
document.getElementById('research-country').textContent = rd.country || '';
|
||
|
||
const catno = (rd.labels || [])[0]?.catno || '';
|
||
document.getElementById('research-catno').textContent = catno;
|
||
const catSep = document.getElementById('research-catno-sep');
|
||
if (catSep) catSep.style.display = catno ? '' : 'none';
|
||
|
||
const formats = (rd.formats || [])
|
||
.map(f => [f.name, ...(f.descriptions || [])].filter(Boolean).join(', '));
|
||
document.getElementById('research-format').textContent = formats.join(' / ');
|
||
|
||
// Genre + style pills
|
||
const genres = rd.genres || (rd.genre ? rd.genre.split(', ') : []);
|
||
const styles = rd.styles || (rd.style ? rd.style.split(', ') : []);
|
||
const tagsEl = document.getElementById('research-tags');
|
||
tagsEl.innerHTML = [
|
||
...genres.map(g => `<span class="research-tag research-tag-genre">${g}</span>`),
|
||
...styles.map(s => `<span class="research-tag research-tag-style">${s}</span>`),
|
||
].join('');
|
||
|
||
// --- Market stats ---
|
||
const have = parseInt(rd.have) || 0;
|
||
const want = parseInt(rd.want) || 0;
|
||
const ratio = have > 0 ? (want / have).toFixed(2) : '—';
|
||
|
||
document.getElementById('rs-have').textContent = have || '—';
|
||
document.getElementById('rs-want').textContent = want || '—';
|
||
document.getElementById('rs-ratio').textContent = ratio;
|
||
document.getElementById('rs-forsale').textContent = rd.num_for_sale != null ? rd.num_for_sale : '—';
|
||
document.getElementById('rs-low').textContent = rd.lowPrice || '—';
|
||
document.getElementById('rs-med').textContent = rd.medianPrice || '—';
|
||
document.getElementById('rs-high').textContent = rd.highPrice || '—';
|
||
document.getElementById('rs-lastsold').textContent = rd.lastSold || '—';
|
||
|
||
const reviews = (rd.reviews || []).filter(r => r.text || r.rating);
|
||
const replyCount = reviews.reduce((n, r) => n + (r.replies?.length || 0), 0);
|
||
const reviewLabel = reviews.length
|
||
? (replyCount ? `${reviews.length}+${replyCount}` : `${reviews.length}`)
|
||
: '—';
|
||
document.getElementById('rs-reviews').textContent = reviewLabel;
|
||
|
||
const tracks = (rd.tracklist || []).filter(t => t.type_ !== 'heading');
|
||
document.getElementById('rs-tracks').textContent = tracks.length || '—';
|
||
|
||
// Ratio signal hint
|
||
const sigEl = document.getElementById('rs-ratio-signal');
|
||
if (sigEl && have > 0) {
|
||
const r = want / have;
|
||
sigEl.textContent = r >= 1 ? 'hot' : r >= 0.5 ? 'sought' : '';
|
||
}
|
||
|
||
marketEl.style.display = 'grid';
|
||
|
||
// --- Tracklist ---
|
||
const trackEl = document.getElementById('research-tracklist');
|
||
if (tracks.length) {
|
||
trackEl.innerHTML = tracks.map(t => {
|
||
const extras = (t.extraartists || [])
|
||
.map(a => a.name.replace(/\s*\(\d+\)$/, ''))
|
||
.join(', ');
|
||
return `<div class="research-track">
|
||
<span class="research-track-pos">${t.position || ''}</span>
|
||
<span class="research-track-title">${t.title || ''}</span>
|
||
${extras ? `<span class="research-track-extra">${extras}</span>` : ''}
|
||
${t.duration ? `<span class="research-track-dur">${t.duration}</span>` : ''}
|
||
</div>`;
|
||
}).join('');
|
||
trackWrap.style.display = 'block';
|
||
} else {
|
||
trackWrap.style.display = 'none';
|
||
}
|
||
|
||
// --- Actions ---
|
||
actionsEl.style.display = 'flex';
|
||
previewWrap.style.display = 'block';
|
||
|
||
// Show Drive button only if service account is configured
|
||
chrome.storage.local.get(['googleSettings'], result => {
|
||
const driveBtn = document.getElementById('research-drive');
|
||
if (!driveBtn) return;
|
||
const hasSA = !!(result.googleSettings && result.googleSettings.serviceAccountJson);
|
||
driveBtn.style.display = hasSA ? '' : 'none';
|
||
});
|
||
|
||
// Clear any stale status/preview
|
||
setResearchStatus('');
|
||
const preview = document.getElementById('research-preview');
|
||
if (preview) preview.textContent = '';
|
||
const geminiInputEditor = document.getElementById('gemini-input-editor');
|
||
if (geminiInputEditor) geminiInputEditor.value = JSON.stringify(buildGeminiInputJson(rd), null, 2);
|
||
const geminiPromptPreview = document.getElementById('gemini-prompt-preview');
|
||
if (geminiPromptPreview) geminiPromptPreview.textContent = '';
|
||
const toggle = document.getElementById('research-toggle-preview');
|
||
if (toggle) {
|
||
toggle.innerHTML = '<span id="research-toggle-icon">▶</span> Preview JSON';
|
||
if (preview) preview.style.display = 'none';
|
||
}
|
||
const giToggle = document.getElementById('gemini-toggle-input');
|
||
if (giToggle) giToggle.innerHTML = '<span id="gemini-input-toggle-icon">▶</span> Preview/Edit Gemini Input';
|
||
if (geminiInputEditor) geminiInputEditor.style.display = 'none';
|
||
const gpToggle = document.getElementById('gemini-toggle-prompt');
|
||
if (gpToggle) gpToggle.innerHTML = '<span id="gemini-prompt-toggle-icon">▶</span> Preview Gemini Prompt';
|
||
if (geminiPromptPreview) geminiPromptPreview.style.display = 'none';
|
||
|
||
maybeAutoRunGemini();
|
||
}
|
||
|
||
/**
|
||
* Set a status message on the research tab.
|
||
* @param {string} msg
|
||
* @param {'ok'|'err'|'inf'|''} type
|
||
*/
|
||
function setResearchStatus(msg, type) {
|
||
const el = document.getElementById('research-status');
|
||
if (!el) return;
|
||
el.textContent = msg;
|
||
el.className = 'research-status' + (type ? ` ${type}` : '');
|
||
}
|
||
|
||
/**
|
||
* Upload a JSON file to Google Drive using the configured service account.
|
||
*/
|
||
async function uploadToDrive(jsonString, filename) {
|
||
const settings = await new Promise(resolve =>
|
||
chrome.storage.local.get(['googleSettings'], r => resolve(r.googleSettings))
|
||
);
|
||
if (!settings || !settings.serviceAccountJson) {
|
||
throw new Error('Google service account not configured — add it in Settings > Connect.');
|
||
}
|
||
|
||
let sa;
|
||
try { sa = JSON.parse(settings.serviceAccountJson); }
|
||
catch (e) { throw new Error('Service account JSON is invalid.'); }
|
||
|
||
// JWT with drive.file scope (createJWT is from sheets.js, now accepts optional scope)
|
||
const jwt = await createJWT(
|
||
sa.client_email,
|
||
sa.private_key,
|
||
'https://www.googleapis.com/auth/drive.file'
|
||
);
|
||
const token = await getAccessToken(jwt);
|
||
|
||
const boundary = 'PliceCogsBoundary' + Date.now();
|
||
const meta = JSON.stringify({ name: filename, mimeType: 'application/json' });
|
||
const body = [
|
||
`--${boundary}`,
|
||
'Content-Type: application/json; charset=UTF-8',
|
||
'',
|
||
meta,
|
||
`--${boundary}`,
|
||
'Content-Type: application/json',
|
||
'',
|
||
jsonString,
|
||
`--${boundary}--`,
|
||
].join('\r\n');
|
||
|
||
const resp = await fetch(
|
||
'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart',
|
||
{
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`,
|
||
'Content-Type': `multipart/related; boundary=${boundary}`,
|
||
},
|
||
body,
|
||
}
|
||
);
|
||
|
||
if (!resp.ok) {
|
||
const err = await resp.json().catch(() => ({}));
|
||
throw new Error(err.error?.message || `Drive upload failed (${resp.status})`);
|
||
}
|
||
|
||
return await resp.json(); // returns { id, name, ... }
|
||
}
|
||
|
||
// ----------------------------------------------------------------
|
||
// Research tab event wiring
|
||
// ----------------------------------------------------------------
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
// PROMPT button in Discogs tab → switch to Research tab
|
||
const promptBtn = document.getElementById('promptResearch');
|
||
if (promptBtn) {
|
||
promptBtn.addEventListener('click', () => {
|
||
const researchTabBtn = document.querySelector('.tab-button[data-tab="research"]');
|
||
if (researchTabBtn) researchTabBtn.click();
|
||
});
|
||
}
|
||
|
||
// Download JSON
|
||
const dlBtn = document.getElementById('research-download');
|
||
if (dlBtn) {
|
||
dlBtn.addEventListener('click', () => {
|
||
if (!state.releaseData?.id) return;
|
||
const json = JSON.stringify(buildResearchJson(state.releaseData), null, 2);
|
||
const filename = `discogs-${state.releaseData.id}-research.json`;
|
||
const blob = new Blob([json], { type: 'application/json' });
|
||
const url = URL.createObjectURL(blob);
|
||
chrome.downloads.download({ url, filename, saveAs: false }, () => {
|
||
URL.revokeObjectURL(url);
|
||
setResearchStatus(`Saved: ${filename}`, 'ok');
|
||
setTimeout(() => setResearchStatus(''), 3000);
|
||
});
|
||
});
|
||
}
|
||
|
||
// Copy JSON to clipboard
|
||
const copyBtn = document.getElementById('research-copy');
|
||
if (copyBtn) {
|
||
copyBtn.addEventListener('click', async () => {
|
||
if (!state.releaseData?.id) return;
|
||
const json = JSON.stringify(buildResearchJson(state.releaseData), null, 2);
|
||
try {
|
||
await navigator.clipboard.writeText(json);
|
||
setResearchStatus('Copied to clipboard — paste straight into your LLM.', 'ok');
|
||
setTimeout(() => setResearchStatus(''), 3000);
|
||
} catch (e) {
|
||
setResearchStatus('Clipboard write failed.', 'err');
|
||
}
|
||
});
|
||
}
|
||
|
||
const geminiBtn = document.getElementById('research-gemini');
|
||
if (geminiBtn) {
|
||
geminiBtn.addEventListener('click', async () => {
|
||
if (!state.releaseData?.id) return;
|
||
geminiBtn.disabled = true;
|
||
const orig = geminiBtn.innerHTML;
|
||
geminiBtn.innerHTML = '<span class="research-btn-icon">⏳</span> Gemini…';
|
||
setResearchStatus('Opening Gemini…', 'inf');
|
||
try {
|
||
const r = await startGeminiJob({ openInBackground: false, silent: true });
|
||
if (!r.ok) throw new Error(r.error || 'Failed to start Gemini.');
|
||
} catch (e) {
|
||
setResearchStatus(e.message || 'Failed to start Gemini.', 'err');
|
||
} finally {
|
||
geminiBtn.disabled = false;
|
||
geminiBtn.innerHTML = orig;
|
||
}
|
||
});
|
||
}
|
||
|
||
// Upload to Google Drive
|
||
const driveBtn = document.getElementById('research-drive');
|
||
if (driveBtn) {
|
||
driveBtn.addEventListener('click', async () => {
|
||
if (!state.releaseData?.id) return;
|
||
driveBtn.disabled = true;
|
||
const orig = driveBtn.innerHTML;
|
||
driveBtn.innerHTML = '<span class="research-btn-icon">⏳</span> Uploading…';
|
||
setResearchStatus('Uploading to Google Drive…', 'inf');
|
||
try {
|
||
const json = JSON.stringify(buildResearchJson(state.releaseData), null, 2);
|
||
const filename = `discogs-${state.releaseData.id}-research.json`;
|
||
const file = await uploadToDrive(json, filename);
|
||
setResearchStatus(`Saved to Drive: ${file.name}`, 'ok');
|
||
setTimeout(() => setResearchStatus(''), 4000);
|
||
} catch (e) {
|
||
setResearchStatus(e.message, 'err');
|
||
} finally {
|
||
driveBtn.disabled = false;
|
||
driveBtn.innerHTML = orig;
|
||
}
|
||
});
|
||
}
|
||
|
||
// JSON preview toggle
|
||
const toggleBtn = document.getElementById('research-toggle-preview');
|
||
if (toggleBtn) {
|
||
toggleBtn.addEventListener('click', () => {
|
||
const preview = document.getElementById('research-preview');
|
||
const icon = document.getElementById('research-toggle-icon');
|
||
if (!preview) return;
|
||
if (preview.style.display === 'none') {
|
||
// Build fresh and show
|
||
if (state.releaseData?.id) {
|
||
preview.textContent = JSON.stringify(buildResearchJson(state.releaseData), null, 2);
|
||
}
|
||
preview.style.display = 'block';
|
||
if (icon) icon.textContent = '▼';
|
||
toggleBtn.querySelector('span + text') // just update the label
|
||
toggleBtn.innerHTML = `<span id="research-toggle-icon">▼</span> Hide JSON`;
|
||
} else {
|
||
preview.style.display = 'none';
|
||
toggleBtn.innerHTML = `<span id="research-toggle-icon">▶</span> Preview JSON`;
|
||
}
|
||
});
|
||
}
|
||
|
||
const geminiInputToggle = document.getElementById('gemini-toggle-input');
|
||
if (geminiInputToggle) {
|
||
geminiInputToggle.addEventListener('click', () => {
|
||
const editor = document.getElementById('gemini-input-editor');
|
||
const icon = document.getElementById('gemini-input-toggle-icon');
|
||
if (!editor) return;
|
||
if (editor.style.display === 'none') {
|
||
if (state.releaseData?.id && !editor.value.trim()) {
|
||
editor.value = JSON.stringify(buildGeminiInputJson(state.releaseData), null, 2);
|
||
}
|
||
editor.style.display = 'block';
|
||
if (icon) icon.textContent = '▼';
|
||
geminiInputToggle.innerHTML = `<span id="gemini-input-toggle-icon">▼</span> Hide Gemini Input`;
|
||
} else {
|
||
editor.style.display = 'none';
|
||
geminiInputToggle.innerHTML = `<span id="gemini-input-toggle-icon">▶</span> Preview/Edit Gemini Input`;
|
||
}
|
||
});
|
||
}
|
||
|
||
const geminiPromptToggle = document.getElementById('gemini-toggle-prompt');
|
||
if (geminiPromptToggle) {
|
||
geminiPromptToggle.addEventListener('click', async () => {
|
||
const preview = document.getElementById('gemini-prompt-preview');
|
||
const icon = document.getElementById('gemini-prompt-toggle-icon');
|
||
if (!preview) return;
|
||
if (preview.style.display === 'none') {
|
||
if (state.releaseData?.id) {
|
||
const promptCfg = await new Promise(resolve => chrome.storage.local.get(['geminiPromptTemplate'], r => resolve(r.geminiPromptTemplate || null)));
|
||
const tpl = promptCfg || defaultGeminiPromptTemplate();
|
||
let inputJson;
|
||
const editor = document.getElementById('gemini-input-editor');
|
||
if (editor && editor.value.trim()) {
|
||
try { inputJson = JSON.parse(editor.value); }
|
||
catch { inputJson = buildGeminiInputJson(state.releaseData); }
|
||
} else {
|
||
inputJson = buildGeminiInputJson(state.releaseData);
|
||
}
|
||
preview.textContent = buildGeminiPromptTextWithTemplate(tpl, inputJson);
|
||
}
|
||
preview.style.display = 'block';
|
||
if (icon) icon.textContent = '▼';
|
||
geminiPromptToggle.innerHTML = `<span id="gemini-prompt-toggle-icon">▼</span> Hide Gemini Prompt`;
|
||
} else {
|
||
preview.style.display = 'none';
|
||
geminiPromptToggle.innerHTML = `<span id="gemini-prompt-toggle-icon">▶</span> Preview Gemini Prompt`;
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
|
||
// Beatport token status UI
|
||
function updateBeatportStatusUI(tokenJson) {
|
||
const dot = document.getElementById('beatport-status-dot');
|
||
const label = document.getElementById('beatport-status-label');
|
||
const info = document.getElementById('beatport-token-info');
|
||
if (!dot) return;
|
||
|
||
if (tokenJson) {
|
||
dot.style.background = '#4CAF50';
|
||
dot.title = 'Token saved';
|
||
try {
|
||
const parsed = JSON.parse(tokenJson);
|
||
const email = parsed.user && parsed.user.email ? parsed.user.email : '';
|
||
const expires = parsed.token && parsed.token.accessTokenExpires
|
||
? new Date(parsed.token.accessTokenExpires).toLocaleString()
|
||
: '';
|
||
label.textContent = email || 'Token saved';
|
||
info.textContent = expires ? `Expires: ${expires}` : 'Token saved';
|
||
} catch(e) {
|
||
label.textContent = 'Token saved';
|
||
info.textContent = '';
|
||
}
|
||
} else {
|
||
dot.style.background = '#ccc';
|
||
dot.title = 'No token saved';
|
||
label.textContent = 'No token';
|
||
info.textContent = '';
|
||
}
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
// Load saved Beatport token state on popup open
|
||
chrome.storage.local.get(['beatportToken'], (result) => {
|
||
updateBeatportStatusUI(result.beatportToken || null);
|
||
});
|
||
|
||
document.getElementById('settings-toggle-beatport').addEventListener('click', () => {
|
||
const content = document.getElementById('settings-content-beatport');
|
||
content.style.display = content.style.display === 'none' ? 'block' : 'none';
|
||
});
|
||
|
||
document.getElementById('beatport-fetch-token').addEventListener('click', () => {
|
||
const statusEl = document.getElementById('beatport-fetch-status');
|
||
const btn = document.getElementById('beatport-fetch-token');
|
||
btn.disabled = true;
|
||
btn.textContent = 'Fetching...';
|
||
statusEl.textContent = '';
|
||
|
||
chrome.runtime.sendMessage({ action: 'getBeatportToken' }, (response) => {
|
||
btn.disabled = false;
|
||
btn.textContent = 'Fetch Token';
|
||
if (response && response.success && response.tokenJson) {
|
||
chrome.storage.local.set({
|
||
beatportToken: response.tokenJson,
|
||
beatportAuth: { data: response.tokenJson, timestamp: Date.now() }
|
||
}, () => {
|
||
updateBeatportStatusUI(response.tokenJson);
|
||
statusEl.style.color = 'green';
|
||
statusEl.textContent = 'Token saved.';
|
||
});
|
||
} else {
|
||
const msg = (response && response.message) ? response.message : 'Failed to fetch token.';
|
||
statusEl.style.color = '#c0392b';
|
||
statusEl.textContent = msg;
|
||
updateBeatportStatusUI(null);
|
||
}
|
||
});
|
||
});
|
||
|
||
document.getElementById('beatport-clear-token').addEventListener('click', () => {
|
||
chrome.storage.local.remove(['beatportToken', 'beatportAuth'], () => {
|
||
updateBeatportStatusUI(null);
|
||
const statusEl = document.getElementById('beatport-fetch-status');
|
||
statusEl.style.color = '#666';
|
||
statusEl.textContent = 'Token cleared.';
|
||
});
|
||
});
|
||
|
||
// Copy JSON button in Beatport settings — copies raw tokenJson to clipboard
|
||
document.getElementById('beatport-copy-json').addEventListener('click', () => {
|
||
const statusEl = document.getElementById('beatport-fetch-status');
|
||
chrome.storage.local.get(['beatportToken'], (result) => {
|
||
const tokenJson = result.beatportToken;
|
||
if (!tokenJson) {
|
||
statusEl.style.color = '#c0392b';
|
||
statusEl.textContent = 'No token saved yet.';
|
||
return;
|
||
}
|
||
navigator.clipboard.writeText(tokenJson).then(() => {
|
||
statusEl.style.color = 'green';
|
||
statusEl.textContent = 'Copied to clipboard!';
|
||
setTimeout(() => { statusEl.textContent = ''; }, 2000);
|
||
}).catch(() => {
|
||
statusEl.style.color = '#c0392b';
|
||
statusEl.textContent = 'Clipboard write failed.';
|
||
});
|
||
});
|
||
});
|
||
});
|
||
|
||
// ── RFID tab ─────────────────────────────────────────────────────────────────
|
||
|
||
async function rfidReadCurrentTag() {
|
||
const skuEl = document.getElementById('rfid-tag-sku');
|
||
const ridEl = document.getElementById('rfid-tag-releaseid');
|
||
const rssiEl = document.getElementById('rfid-tag-rssi');
|
||
const antEl = document.getElementById('rfid-tag-antenna');
|
||
const scanEl = document.getElementById('rfid-tag-scanning');
|
||
if (!skuEl) return;
|
||
scanEl.style.display = '';
|
||
skuEl.style.color = '#555';
|
||
skuEl.textContent = '—';
|
||
ridEl.textContent = '—';
|
||
if (rssiEl) rssiEl.textContent = '—';
|
||
if (antEl) antEl.textContent = '—';
|
||
try {
|
||
const resp = await fetch('http://127.0.0.1:7790/read-tag', { signal: AbortSignal.timeout(15000) });
|
||
const data = await resp.json();
|
||
if (data.ok) {
|
||
skuEl.style.color = '#1976d2';
|
||
skuEl.textContent = data.sku || '—';
|
||
ridEl.textContent = data.releaseId != null ? data.releaseId : '—';
|
||
if (rssiEl) rssiEl.textContent = data.rssi != null ? `${data.rssi} dBm` : '—';
|
||
if (antEl) antEl.textContent = data.antenna != null ? `${data.antenna}` : '—';
|
||
} else {
|
||
skuEl.style.color = '#888';
|
||
skuEl.textContent = 'no tag';
|
||
ridEl.textContent = '—';
|
||
}
|
||
} catch {
|
||
skuEl.style.color = '#aaa';
|
||
skuEl.textContent = 'daemon offline';
|
||
ridEl.textContent = '—';
|
||
} finally {
|
||
scanEl.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
async function rfidCheckConnection() {
|
||
const dot = document.getElementById('rfid-status-dot');
|
||
const label = document.getElementById('rfid-status-label');
|
||
const port = document.getElementById('rfid-status-port');
|
||
const stopBtn = document.getElementById('rfid-stop-btn');
|
||
const maskBtn = document.getElementById('rfid-clear-mask-btn');
|
||
const startCmd = document.getElementById('rfid-start-cmd');
|
||
if (!dot) return;
|
||
label.textContent = 'Checking…';
|
||
dot.style.background = '#ffc107';
|
||
try {
|
||
const resp = await fetch('http://127.0.0.1:7790/status', { signal: AbortSignal.timeout(2000) });
|
||
const data = await resp.json();
|
||
if (data.ok) {
|
||
dot.style.background = '#4CAF50';
|
||
label.textContent = 'H102 connected';
|
||
port.textContent = data.port || '';
|
||
if (stopBtn) { stopBtn.style.display = 'inline-block'; }
|
||
if (maskBtn) { maskBtn.style.display = 'inline-block'; }
|
||
if (startCmd) { startCmd.style.display = 'none'; }
|
||
} else {
|
||
dot.style.background = '#f44336';
|
||
label.textContent = 'Reader not found';
|
||
port.textContent = data.error || '';
|
||
if (stopBtn) { stopBtn.style.display = 'none'; }
|
||
if (maskBtn) { maskBtn.style.display = 'none'; }
|
||
if (startCmd) { startCmd.style.display = 'none'; }
|
||
}
|
||
} catch (e) {
|
||
dot.style.background = '#ccc';
|
||
label.textContent = 'Daemon not running';
|
||
port.textContent = '';
|
||
if (stopBtn) { stopBtn.style.display = 'none'; }
|
||
if (maskBtn) { maskBtn.style.display = 'none'; }
|
||
if (startCmd) { startCmd.style.display = 'flex'; }
|
||
}
|
||
}
|
||
|
||
function rfidUpdateDisplayedData() {
|
||
const skuEl = document.getElementById('rfid-sku-display');
|
||
const ridEl = document.getElementById('rfid-releaseid-display');
|
||
if (!skuEl) return;
|
||
const rawSku = (state.releaseData?.collectionSku) || state.sku || '—';
|
||
const sku = rawSku !== '—' ? rawSku.replace(/-A$/i, '') : rawSku;
|
||
skuEl.textContent = sku;
|
||
ridEl.textContent = state.releaseData?.id || '—';
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
// Show current data when RFID tab is activated
|
||
document.querySelectorAll('.tab-button').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
if (btn.dataset.tab === 'rfid') {
|
||
rfidUpdateDisplayedData();
|
||
rfidCheckConnection();
|
||
rfidReadCurrentTag();
|
||
}
|
||
});
|
||
});
|
||
|
||
const checkBtn = document.getElementById('rfid-check-btn');
|
||
if (checkBtn) checkBtn.addEventListener('click', rfidCheckConnection);
|
||
|
||
// Stop daemon
|
||
const stopBtn = document.getElementById('rfid-stop-btn');
|
||
if (stopBtn) {
|
||
stopBtn.addEventListener('click', async () => {
|
||
stopBtn.disabled = true;
|
||
try {
|
||
await fetch('http://127.0.0.1:7790/shutdown', { method: 'POST', signal: AbortSignal.timeout(3000) });
|
||
} catch (_) {}
|
||
setTimeout(() => { stopBtn.disabled = false; rfidCheckConnection(); }, 600);
|
||
});
|
||
}
|
||
|
||
// Copy start command to clipboard
|
||
const copyCmdBtn = document.getElementById('rfid-copy-cmd-btn');
|
||
if (copyCmdBtn) {
|
||
copyCmdBtn.addEventListener('click', () => {
|
||
const cmd = document.getElementById('rfid-start-cmd-text')?.textContent || 'node rfid-daemon/index.js -q';
|
||
navigator.clipboard.writeText(cmd).then(() => {
|
||
const orig = copyCmdBtn.textContent;
|
||
copyCmdBtn.textContent = 'Copied!';
|
||
setTimeout(() => { copyCmdBtn.textContent = orig; }, 1500);
|
||
});
|
||
});
|
||
}
|
||
|
||
// Clear mask — both the status-area button and the write-area button
|
||
async function doClearMask() {
|
||
const statusEl = document.getElementById('rfid-write-status');
|
||
try {
|
||
const resp = await fetch('http://127.0.0.1:7790/clear-mask', { method: 'POST', signal: AbortSignal.timeout(5000) });
|
||
const result = await resp.json();
|
||
if (statusEl) {
|
||
statusEl.style.color = result.ok ? '#28a745' : '#c0392b';
|
||
statusEl.textContent = result.ok ? 'Mask cleared — reader can see all tags again.' : `Clear failed: ${result.error}`;
|
||
}
|
||
} catch (e) {
|
||
if (statusEl) { statusEl.style.color = '#c0392b'; statusEl.textContent = 'Daemon not running.'; }
|
||
}
|
||
}
|
||
const clearMaskBtn2 = document.getElementById('rfid-clear-mask-btn2');
|
||
if (clearMaskBtn2) clearMaskBtn2.addEventListener('click', doClearMask);
|
||
|
||
const recoverBtn = document.getElementById('rfid-recover-btn');
|
||
if (recoverBtn) {
|
||
recoverBtn.addEventListener('click', async () => {
|
||
const statusEl = document.getElementById('rfid-write-status');
|
||
recoverBtn.disabled = true;
|
||
if (statusEl) { statusEl.style.color = '#555'; statusEl.textContent = 'Recovering reader…'; }
|
||
try {
|
||
const resp = await fetch('http://127.0.0.1:7790/recover', { method: 'POST', signal: AbortSignal.timeout(15000) });
|
||
const result = await resp.json();
|
||
if (statusEl) {
|
||
statusEl.style.color = result.ok ? '#28a745' : '#c0392b';
|
||
statusEl.textContent = result.ok ? 'Reader recovered — try again.' : `Recover failed: ${result.error}`;
|
||
}
|
||
} catch (e) {
|
||
if (statusEl) { statusEl.style.color = '#c0392b'; statusEl.textContent = 'Recover failed — press trigger to wake reader, then try again.'; }
|
||
} finally {
|
||
recoverBtn.disabled = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
const recoverModeBtn = document.getElementById('rfid-recover-mode-btn');
|
||
if (recoverModeBtn) {
|
||
recoverModeBtn.addEventListener('click', async () => {
|
||
const statusEl = document.getElementById('rfid-write-status');
|
||
recoverModeBtn.disabled = true;
|
||
if (statusEl) { statusEl.style.color = '#555'; statusEl.textContent = 'Restoring RFID mode — scanning baud rates, please wait…'; }
|
||
try {
|
||
const resp = await fetch('http://127.0.0.1:7790/recover-mode', { method: 'POST', signal: AbortSignal.timeout(35000) });
|
||
const result = await resp.json();
|
||
if (statusEl) {
|
||
statusEl.style.color = result.ok ? '#28a745' : '#c0392b';
|
||
statusEl.textContent = result.ok ? result.message : `Restore failed: ${result.error}`;
|
||
}
|
||
} catch (e) {
|
||
if (statusEl) { statusEl.style.color = '#c0392b'; statusEl.textContent = 'Restore failed — daemon not running or device not found.'; }
|
||
} finally {
|
||
recoverModeBtn.disabled = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
const unlockBtn = document.getElementById('rfid-unlock-btn');
|
||
if (unlockBtn) {
|
||
unlockBtn.addEventListener('click', async () => {
|
||
const statusEl = document.getElementById('rfid-write-status');
|
||
unlockBtn.disabled = true;
|
||
statusEl.style.color = '#555';
|
||
statusEl.textContent = 'Unlocking EPC bank…';
|
||
try {
|
||
const resp = await fetch('http://127.0.0.1:7790/unlock-tag', { method: 'POST' });
|
||
const result = await resp.json();
|
||
if (result.ok) {
|
||
statusEl.style.color = '#28a745';
|
||
statusEl.textContent = 'Unlocked — tag is now rewritable. Try writing again.';
|
||
} else {
|
||
statusEl.style.color = '#c0392b';
|
||
statusEl.textContent = `Unlock failed: ${result.error}`;
|
||
}
|
||
} catch (e) {
|
||
statusEl.style.color = '#c0392b';
|
||
statusEl.textContent = 'Daemon not running.';
|
||
}
|
||
unlockBtn.disabled = false;
|
||
});
|
||
}
|
||
|
||
const rescanBtn = document.getElementById('rfid-rescan-btn');
|
||
if (rescanBtn) {
|
||
rescanBtn.addEventListener('click', () => rfidReadCurrentTag());
|
||
}
|
||
|
||
const writeBtn = document.getElementById('rfid-write-btn');
|
||
if (writeBtn) {
|
||
writeBtn.addEventListener('click', async () => {
|
||
const statusEl = document.getElementById('rfid-write-status');
|
||
const rawSku = (state.releaseData?.collectionSku) || state.sku;
|
||
const sku = rawSku ? rawSku.replace(/-[AR]$/i, '') : rawSku;
|
||
if (!sku) {
|
||
statusEl.style.color = '#c0392b';
|
||
statusEl.textContent = 'No SKU available — open a Discogs release page first.';
|
||
return;
|
||
}
|
||
writeBtn.disabled = true;
|
||
statusEl.style.color = '#555';
|
||
statusEl.textContent = 'Writing…';
|
||
|
||
const cfg = await new Promise(resolve =>
|
||
chrome.storage.local.get(['rfidAppendR'], resolve)
|
||
);
|
||
// Always include release_id — see rationale at rfidQuickBtn handler.
|
||
const releaseId = state.releaseData?.id || null;
|
||
const result = await writeRfidTag(sku, releaseId);
|
||
|
||
writeBtn.disabled = false;
|
||
if (!result) {
|
||
statusEl.style.color = '#c0392b';
|
||
statusEl.textContent = 'Daemon not running — start node index.js first.';
|
||
} else if (result.ok) {
|
||
statusEl.style.color = '#28a745';
|
||
statusEl.textContent = `Written: ${sku}${releaseId ? ' + release ' + releaseId : ''}`;
|
||
if (cfg.rfidAppendR && state.discogsTabId) {
|
||
try {
|
||
await chrome.tabs.sendMessage(state.discogsTabId, { action: 'updateDiscogsSku', sku: sku + '-R', boxIndex: state.collectionBoxIndex });
|
||
} catch (e) { console.warn('[RFID] -R SKU update failed:', e.message); }
|
||
}
|
||
} else {
|
||
statusEl.style.color = '#c0392b';
|
||
statusEl.textContent = `Failed: ${result.error}`;
|
||
}
|
||
});
|
||
}
|
||
|
||
// Device Info button
|
||
const deviceInfoBtn = document.getElementById('rfid-device-info-btn');
|
||
if (deviceInfoBtn) {
|
||
deviceInfoBtn.addEventListener('click', async () => {
|
||
const display = document.getElementById('rfid-device-info-display');
|
||
deviceInfoBtn.disabled = true;
|
||
display.textContent = 'Fetching…';
|
||
try {
|
||
const resp = await fetch('http://127.0.0.1:7790/device-info', { signal: AbortSignal.timeout(5000) });
|
||
const data = await resp.json();
|
||
if (data.ok) {
|
||
display.textContent = [
|
||
`HW: ${data.hwVer || '—'}`,
|
||
`Firm: ${data.firmVer || '—'}`,
|
||
`SN: ${data.sn || '—'}`,
|
||
`Mod: ${data.moduleName || '—'} v${data.moduleVer || '—'}`,
|
||
`Raw: ${data.raw}`,
|
||
].join('\n');
|
||
} else {
|
||
display.textContent = `Error: ${data.error}`;
|
||
}
|
||
} catch (e) {
|
||
display.textContent = `Daemon offline: ${e.message}`;
|
||
} finally {
|
||
deviceInfoBtn.disabled = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
// Battery button
|
||
const batteryBtn = document.getElementById('rfid-battery-btn');
|
||
if (batteryBtn) {
|
||
batteryBtn.addEventListener('click', async () => {
|
||
const display = document.getElementById('rfid-device-info-display');
|
||
batteryBtn.disabled = true;
|
||
try {
|
||
const resp = await fetch('http://127.0.0.1:7790/battery', { signal: AbortSignal.timeout(5000) });
|
||
const data = await resp.json();
|
||
display.textContent = data.ok ? `Battery: ${data.battery}%` : `Error: ${data.error}`;
|
||
} catch (e) {
|
||
display.textContent = `Daemon offline: ${e.message}`;
|
||
} finally {
|
||
batteryBtn.disabled = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
// Get Config button — fetches and populates fields
|
||
const getConfigBtn = document.getElementById('rfid-get-config-btn');
|
||
if (getConfigBtn) {
|
||
getConfigBtn.addEventListener('click', async () => {
|
||
const display = document.getElementById('rfid-config-display');
|
||
const statusEl = document.getElementById('rfid-config-status');
|
||
getConfigBtn.disabled = true;
|
||
display.textContent = 'Fetching…';
|
||
try {
|
||
const resp = await fetch('http://127.0.0.1:7790/get-config', { signal: AbortSignal.timeout(5000) });
|
||
const d = await resp.json();
|
||
if (d.ok) {
|
||
display.textContent = [
|
||
`WorkMode: ${d.workModeName} Interface: ${d.interfaceName} Baud: ${d.baudRateName}`,
|
||
`Protocol: ${d.rfidProName} Freq: ${d.rfidFreqName || '—'}`,
|
||
`Ant: 0x${(d.ant||0).toString(16).padStart(2,'0')} Q: ${d.qValue} Session: ${d.session}`,
|
||
`FilterTime: ${d.filterTime}s Buzzer: ${d.buzzerTime}×10ms Poll: ${d.pollingInterval}×10ms`,
|
||
`RF Power: ${(d.rfidPower / 10).toFixed(1)} dBm`,
|
||
].join('\n');
|
||
// Populate input fields with current values
|
||
const set = (id, val) => { const el = document.getElementById(id); if (el && val != null) el.value = val; };
|
||
set('rfid-cfg-qvalue', d.qValue);
|
||
set('rfid-cfg-session', d.session);
|
||
set('rfid-cfg-filtertime',d.filterTime);
|
||
set('rfid-cfg-buzzertime',d.buzzerTime);
|
||
set('rfid-cfg-rfpower', (d.rfidPower / 10).toFixed(1));
|
||
set('rfid-cfg-polling', d.pollingInterval);
|
||
statusEl.textContent = '';
|
||
} else {
|
||
display.textContent = `Error: ${d.error}`;
|
||
}
|
||
} catch (e) {
|
||
display.textContent = `Daemon offline: ${e.message}`;
|
||
} finally {
|
||
getConfigBtn.disabled = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
// Set Config button — sends changed fields
|
||
const setConfigBtn = document.getElementById('rfid-set-config-btn');
|
||
if (setConfigBtn) {
|
||
setConfigBtn.addEventListener('click', async () => {
|
||
const statusEl = document.getElementById('rfid-config-status');
|
||
setConfigBtn.disabled = true;
|
||
statusEl.style.color = '#555';
|
||
statusEl.textContent = 'Applying…';
|
||
const body = {};
|
||
const grab = (id, key) => {
|
||
const el = document.getElementById(id);
|
||
if (el && el.value !== '') body[key] = parseInt(el.value, 10);
|
||
};
|
||
grab('rfid-cfg-qvalue', 'qValue');
|
||
grab('rfid-cfg-session', 'session');
|
||
grab('rfid-cfg-filtertime','filterTime');
|
||
grab('rfid-cfg-buzzertime','buzzerTime');
|
||
{ const el = document.getElementById('rfid-cfg-rfpower'); if (el && el.value !== '') body.rfidPower = Math.round(parseFloat(el.value) * 10); }
|
||
grab('rfid-cfg-polling', 'pollingInterval');
|
||
if (Object.keys(body).length === 0) {
|
||
statusEl.style.color = '#c0392b';
|
||
statusEl.textContent = 'No fields to set.';
|
||
setConfigBtn.disabled = false;
|
||
return;
|
||
}
|
||
try {
|
||
const resp = await fetch('http://127.0.0.1:7790/set-config', {
|
||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(body), signal: AbortSignal.timeout(5000)
|
||
});
|
||
const data = await resp.json();
|
||
statusEl.style.color = data.ok ? '#28a745' : '#c0392b';
|
||
statusEl.textContent = data.ok ? `Applied: ${data.changed.join(', ')}` : `Failed: ${data.error}`;
|
||
} catch (e) {
|
||
statusEl.style.color = '#c0392b';
|
||
statusEl.textContent = `Daemon offline: ${e.message}`;
|
||
} finally {
|
||
setConfigBtn.disabled = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
// Inventory Scan button
|
||
const scanBtn = document.getElementById('rfid-scan-btn');
|
||
if (scanBtn) {
|
||
scanBtn.addEventListener('click', async () => {
|
||
const resultEl = document.getElementById('rfid-scan-result');
|
||
const type = document.getElementById('rfid-inv-type')?.value || 'time';
|
||
const param = document.getElementById('rfid-inv-param')?.value || '1';
|
||
scanBtn.disabled = true;
|
||
resultEl.textContent = 'Scanning…';
|
||
try {
|
||
const url = `http://127.0.0.1:7790/inventory?type=${type}¶m=${param}`;
|
||
const resp = await fetch(url, { signal: AbortSignal.timeout(15000) });
|
||
const data = await resp.json();
|
||
if (data.ok && data.status === 0x00) {
|
||
const parts = [`${data.statusText}`];
|
||
if (data.rssi != null) parts.push(`RSSI: ${data.rssi} dBm`);
|
||
if (data.antenna != null) parts.push(`Ant: ${data.antenna}`);
|
||
if (data.epc) parts.push(`EPC: ${data.epc}`);
|
||
resultEl.textContent = parts.join(' | ');
|
||
} else {
|
||
resultEl.textContent = data.statusText || `status 0x${data.status?.toString(16)}`;
|
||
}
|
||
} catch (e) {
|
||
resultEl.textContent = `Daemon offline: ${e.message}`;
|
||
} finally {
|
||
scanBtn.disabled = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
});
|