pliceclogs-og/ui.js
type-two e5b8504bc6 Import pliceclogs as-is — the original Discogs seller extension
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>
2026-07-24 13:47:09 +10:00

444 lines
21 KiB
JavaScript

function autoFitText(element, text, maxFontSize = 10, minFontSize = 3.5, isMultiline = false, useEllipsis = false) {
if (!element) return;
element.textContent = text;
// Add flex styling for consistent single-line alignment as in original script
if (!isMultiline) {
element.style.display = 'flex';
element.style.alignItems = 'center';
element.style.lineHeight = '1';
element.style.whiteSpace = 'nowrap';
} else {
element.style.whiteSpace = 'normal';
element.style.wordBreak = 'break-word';
}
element.style.fontSize = maxFontSize + 'pt';
element.style.overflow = 'hidden';
if (useEllipsis) {
element.style.textOverflow = 'ellipsis';
element.classList.add('dynamic-text');
return;
}
element.style.textOverflow = 'clip';
// Temporarily force flex-start so scrollWidth measures properly (bypasses right-align overflow bugs)
const originalJustify = element.style.justifyContent;
if (!isMultiline) {
element.style.justifyContent = 'flex-start';
}
let fontSize = maxFontSize;
while (fontSize >= minFontSize) {
if (element.scrollWidth <= element.clientWidth + 1 && element.scrollHeight <= element.clientHeight + 1) {
break;
}
fontSize -= 0.25;
element.style.fontSize = fontSize + 'pt';
}
// Restore original justification
if (!isMultiline) {
element.style.justifyContent = originalJustify;
}
element.classList.add('dynamic-text');
}
function updatePreview(data) {
if (!data) return;
// Apply label font settings before measuring (affects autoFitText width calculations)
const labelFont = document.getElementById('label-font-select')?.value || 'Arial';
const labelBold = document.getElementById('label-bold-toggle')?.checked ? 'bold' : 'normal';
const labelItalic = document.getElementById('label-italic-toggle')?.checked ? 'italic' : 'normal';
['.label-preview', '.label-preview-large', '.label-preview-xlarge'].forEach(sel => {
const el = document.querySelector(sel);
if (el) { el.style.fontFamily = labelFont; el.style.fontWeight = labelBold; el.style.fontStyle = labelItalic; }
});
// Un-hide parent containers to allow element dimensions to be calculated correctly
const hiddenParents = [];
const elementsToCheck = [document.querySelector('.label-preview'), document.querySelector('.label-preview-large'), document.querySelector('.label-preview-xlarge')];
elementsToCheck.forEach(el => {
let current = el;
while (current && current !== document.body) {
const style = window.getComputedStyle(current);
if (style.display === 'none') {
hiddenParents.push({
el: current,
display: current.style.display,
visibility: current.style.visibility,
position: current.style.position
});
current.style.display = 'block';
current.style.visibility = 'hidden';
current.style.position = 'absolute';
}
current = current.parentElement;
}
});
// Per-field style overrides from settings
const lfs = window.labelFieldStyles || {};
const applyFieldStyle = (selector, fieldKey) => {
const s = lfs[fieldKey] || {};
[selector, selector + '-large', selector + '-xlarge'].forEach(sel => {
const el = document.querySelector(sel);
if (!el) return;
if (s.b) el.style.fontWeight = 'bold';
else el.style.fontWeight = '';
if (s.i) el.style.fontStyle = 'italic';
else el.style.fontStyle = '';
});
};
['artist','title','genre','style','format','price','condition','comment','notes'].forEach(f => applyFieldStyle('.print-' + f, f));
applyFieldStyle('.print-bottom-info', 'info');
const updateAllSizes = (selector, text, maxFontSize = 10, minFontSize = 3.5, isMultiline = false, fieldKey = null) => {
const elSmall = document.querySelector(selector);
const elLarge = document.querySelector(selector + '-large');
const elXLarge = document.querySelector(selector + '-xlarge');
const useEllipsis = fieldKey ? !!(lfs[fieldKey] && lfs[fieldKey].e) : false;
autoFitText(elSmall, text, maxFontSize, minFontSize, isMultiline, useEllipsis);
autoFitText(elLarge, text, maxFontSize + 1.5, minFontSize, isMultiline, useEllipsis);
autoFitText(elXLarge, text, maxFontSize + 3, minFontSize, isMultiline, useEllipsis);
};
updateAllSizes('.print-artist', data.artist || '', 8.5, 3.5, false, 'artist');
updateAllSizes('.print-title', data.title || '', 8.5, 3.5, false, 'title');
updateAllSizes('.print-genre', data.genre || '', 8.5, 3.5, false, 'genre');
// Shorten 'Progressive' to 'Prog' for label preview
const styleText = (data.style || '').replace(/Progressive/g, 'Prog');
updateAllSizes('.print-style', styleText, 8.5, 3.5, false, 'style');
let formatText;
if (typeof data.format === 'string' && data.format.trim()) {
formatText = data.format;
} else {
const format = (data.formats || [])[0] || {};
const formatDesc = format.descriptions ? format.descriptions.join(' ') : '';
formatText = `${data.format_quantity || 1}x ${format.name || ''} ${formatDesc}`.trim();
}
// Use smaller base for size logic: description uses 6pt / 8pt max usually
updateAllSizes('.print-description', formatText, 6.5, 3.5, true, 'format');
updateAllSizes('.print-comment', data.comment || '', 7, 3.5, false, 'comment');
updateAllSizes('.print-notes', data.notes || '', 6, 3.5, false, 'notes');
['', '-large', '-xlarge'].forEach(suffix => {
const bottomInfoDiv = document.querySelector(`.print-bottom-info${suffix}`);
if (bottomInfoDiv) {
const yearElement = bottomInfoDiv.querySelector('.year');
const countryElement = bottomInfoDiv.querySelector('.country');
const labelElement = bottomInfoDiv.querySelector('.label');
const hasYear = data.year && data.year !== 0;
// Toggle no-year class to hide year span and its separator
bottomInfoDiv.classList.toggle('no-year', !hasYear);
if (yearElement) {
yearElement.textContent = hasYear ? data.year.toString() : '';
}
if (countryElement && labelElement) {
const countryText = data.country || '';
// Remove ' Records' from label text for print preview only
const labelText = (data.label || '').replace(/ Records$/, '');
countryElement.textContent = countryText;
labelElement.textContent = labelText;
// Dynamically fit the entire row by shrinking only the label
bottomInfoDiv.style.whiteSpace = 'nowrap';
bottomInfoDiv.style.overflow = 'hidden';
let maxFont = suffix === '-xlarge' ? 9 : (suffix ? 8 : 7);
let labelFontSize = maxFont;
countryElement.style.fontSize = maxFont + 'pt';
labelElement.style.fontSize = labelFontSize + 'pt';
while (labelFontSize >= 3) {
if (bottomInfoDiv.scrollWidth <= bottomInfoDiv.clientWidth + 1) {
break;
}
labelFontSize -= 0.25;
labelElement.style.fontSize = labelFontSize + 'pt';
}
}
}
});
['', '-large', '-xlarge'].forEach(suffix => {
const conditionElement = document.querySelector(`.print-condition${suffix}`);
if (conditionElement) {
const mediaMatch = data.mediaCondition?.match(/\((.*?)\)/);
const sleeveMatch = data.sleeveCondition?.match(/\((.*?)\)/);
const mediaCondition = mediaMatch ? mediaMatch[1] : data.mediaCondition;
const sleeveCondition = sleeveMatch ? sleeveMatch[1] : data.sleeveCondition;
const condText = `${mediaCondition} | ${sleeveCondition}`;
const condS = lfs['condition'] || {};
if (condS.b) conditionElement.style.fontWeight = 'bold'; else conditionElement.style.fontWeight = '';
if (condS.i) conditionElement.style.fontStyle = 'italic'; else conditionElement.style.fontStyle = '';
autoFitText(conditionElement, condText, suffix === '-xlarge' ? 8.5 : (suffix ? 7.5 : 6), 3.5, false, !!condS.e);
}
});
['', '-large', '-xlarge'].forEach(suffix => {
const priceElement = document.querySelector(`.print-price${suffix}`);
if (priceElement && data.price) {
const priceText = String(data.price).startsWith('$') ? data.price : `$${data.price}`;
const priceS = lfs['price'] || {};
if (priceS.b) priceElement.style.fontWeight = 'bold'; else priceElement.style.fontWeight = '';
if (priceS.i) priceElement.style.fontStyle = 'italic'; else priceElement.style.fontStyle = '';
autoFitText(priceElement, priceText, suffix === '-xlarge' ? 15 : (suffix ? 13 : 10), 5, false, !!priceS.e);
}
});
['', '-large', '-xlarge'].forEach(suffix => {
const barcodeElement = document.querySelector(`.print-barcode${suffix}`);
if (barcodeElement) {
let qrData = data.sku != null ? data.sku : data.id;
// Labels always show the bare integer SKU — strip -R and -A suffixes unconditionally
if (typeof qrData === 'string') qrData = qrData.replace(/-[AR]$/i, '');
const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=100x100&data=${qrData}`;
barcodeElement.innerHTML = `<img src="${qrUrl}" alt="${qrData}">`;
}
});
// Restore hidden parents
for (let i = hiddenParents.length - 1; i >= 0; i--) {
const p = hiddenParents[i];
p.el.style.display = p.display;
p.el.style.visibility = p.visibility;
p.el.style.position = p.position;
}
}
function displayReleaseInfo(data) {
const releaseDetailsContainer = document.getElementById('releaseDetails');
if (!releaseDetailsContainer) return;
const fields = [
{ key: 'artist', value: data.artist },
{ key: 'title', value: data.title },
{ key: 'genre', value: data.genre },
{ key: 'style', value: data.style },
{ key: 'label', value: (data.label || '').replace(/ Records$/, '') },
{ key: 'year', value: data.year === 0 ? '' : data.year },
{ key: 'country', value: data.country || 'N/A' },
{ key: 'format', value: `${data.format_quantity || 1} x ${data.formats.map(f => `${f.name} (${f.descriptions?.join(', ') || ''})`).join(', ')}` },
{ key: 'catno', value: data.labels ? data.labels[0].catno : 'N/A' },
{ key: 'releaseId', value: data.id },
{ key: 'comment', value: data.comment },
{ key: 'appleId', value: data.appleId || 'N/A' },
{ key: 'collectionSku', value: data.collectionSku || '' },
];
fields.forEach(field => {
const fieldElement = releaseDetailsContainer.querySelector(`.info-field[data-field="${field.key}"] .value`);
if (fieldElement) {
fieldElement.textContent = field.value;
}
});
const marketInfoContainer = document.querySelector('.market-info');
if (marketInfoContainer) {
marketInfoContainer.innerHTML = `
<h3><b>Market Information:</b></h3>
<div class="data-row"><span class="data-label"><b>Have:</b></span> ${data.community?.have || 0}</div>
<div class="data-row"><span class="data-label"><b>Want:</b></span> ${data.community?.want || 0}</div>
<div class="data-row"><span class="data-label"><b>For Sale:</b></span> ${data.num_for_sale || 0}</div>
<div class="data-row"><span class="data-label"><b>Price Range:</b></span> ${data.sellerPriceRange || 'N/A'}</div>
<div class="data-row"><span class="data-label"><b>Lowest Price:</b></span> ${data.lowPrice || 'N/A'}</div>
<div class="data-row"><span class="data-label"><b>Last Sold:</b></span> ${data.lastSold || 'N/A'}</div>
<div class="data-row"><span class="data-label"><b>Low Price:</b></span> ${data.lowPrice || 'N/A'}</div>
<div class="data-row"><span class="data-label"><b>Median Price:</b></span> ${data.medianPrice || 'N/A'}</div>
<div class="data-row"><span class="data-label"><b>High Price:</b></span> ${data.highPrice || 'N/A'}</div>
<h3><b>Suggested Prices:</b></h3>
<div class="data-row"><span class="data-label"><b>Mint (M):</b></span> ${data.priceSuggestions?.['Mint (M)']?.value?.toFixed(2) || 'N/A'}</div>
<div class="data-row"><span class="data-label"><b>Near Mint (NM or M-):</b></span> ${data.priceSuggestions?.['Near Mint (NM or M-)']?.value?.toFixed(2) || 'N/A'}</div>
<div class="data-row"><span class="data-label"><b>Very Good Plus (VG+):</b></span> ${data.priceSuggestions?.['Very Good Plus (VG+)']?.value?.toFixed(2) || 'N/A'}</div>
<div class="data-row"><span class="data-label"><b>Very Good (VG):</b></span> ${data.priceSuggestions?.['Very Good (VG)']?.value?.toFixed(2) || 'N/A'}</div>
<div class="data-row"><span class="data-label"><b>Good Plus (G+):</b></span> ${data.priceSuggestions?.['Good Plus (G+)']?.value?.toFixed(2) || 'N/A'}</div>
<div class="data-row"><span class="data-label"><b>Good (G):</b></span> ${data.priceSuggestions?.['Good (G)']?.value?.toFixed(2) || 'N/A'}</div>
<div class="data-row"><span class="data-label"><b>Fair (F):</b></span> ${data.priceSuggestions?.['Fair (F)']?.value?.toFixed(2) || 'N/A'}</div>
<div class="data-row"><span class="data-label"><b>Poor (P):</b></span> ${data.priceSuggestions?.['Poor (P)']?.value?.toFixed(2) || 'N/A'}</div>
`;
}
const tracklistDiv = document.querySelector('.tracklist');
if (tracklistDiv) {
const tracklistHTML = Array.isArray(data.tracklist) ? data.tracklist.map(entry => {
if (entry.type_ === "heading") {
return `<div class="data-row"><span class="data-label"><b>${entry.title}</b></span></div>`;
} else if (entry.type_ === "track") {
const artists = Array.isArray(entry.artists) ? entry.artists.map(artist => artist.name.replace(/\(\d+\)/, '')).join(', ') : '';
return `<div class="data-row">
<span class="data-label"><b>Track ${entry.position}:</b></span> ${entry.title}${artists ? ' - ' + artists : ''}
</div>`;
}
return '';
}).join('') : '';
tracklistDiv.innerHTML = `
<h3><b>Tracklist:</b></h3>
${tracklistHTML}
`;
}
}
function generateLabel() {
const labelWindow = window.open('', '_blank', 'width=600,height=600');
if (labelWindow) {
const labelSize = document.getElementById('labelSize')?.value || 'large';
const cssFile = labelSize === 'xlarge' ? 'xlarge.css' : (labelSize === 'large' ? 'large.css' : 'small.css');
const previewSel = labelSize === 'xlarge' ? '.label-preview-xlarge' : (labelSize === 'large' ? '.label-preview-large' : '.label-preview');
const pageDims = labelSize === 'xlarge' ? '64mm 34mm' : (labelSize === 'large' ? '54mm 25mm' : '51mm 19mm');
const labelClass = labelSize === 'xlarge' ? 'print-label-xlarge' : (labelSize === 'large' ? 'print-label-large' : 'print-label');
const labelW = labelSize === 'xlarge' ? '64mm' : (labelSize === 'large' ? '54mm' : '51mm');
const labelH = labelSize === 'xlarge' ? '34mm' : (labelSize === 'large' ? '25mm' : '19mm');
const printFont = document.getElementById('label-font-select')?.value || '"MS Gothic","Hiragino Kaku Gothic Pro",Arial,sans-serif';
const printWeight = document.getElementById('label-bold-toggle')?.checked ? 'bold' : 'normal';
const printStyle = document.getElementById('label-italic-toggle')?.checked ? 'italic' : 'normal';
const labelContent = document.querySelector(previewSel).innerHTML;
labelWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="${chrome.runtime.getURL(cssFile)}">
<link rel="stylesheet" href="${chrome.runtime.getURL('label.css')}">
<style>
@page {
size: ${pageDims};
margin: 0;
}
html, body {
margin: 0;
padding: 0;
width: ${labelW};
height: ${labelH};
overflow: hidden;
display: flex;
justify-content: flex-start;
align-items: flex-start;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
.${labelClass} {
width: ${labelW};
height: ${labelH};
position: relative;
font-family: ${printFont};
font-weight: ${printWeight};
font-style: ${printStyle};
background: white;
margin: 0;
padding: 0;
overflow: hidden;
}
</style>
</head>
<body>
<div class="${labelClass}">
${labelContent}
</div>
</body>
</html>
`);
labelWindow.document.close();
}
}
function generateLabel203dpi() {
const labelWindow = window.open('', '_blank', 'width=600,height=600');
if (!labelWindow) return;
const labelSize = document.getElementById('labelSize')?.value || 'large';
const cssFile = labelSize === 'xlarge' ? 'xlarge.css' : (labelSize === 'large' ? 'large.css' : 'small.css');
const previewSel = labelSize === 'xlarge' ? '.label-preview-xlarge' : (labelSize === 'large' ? '.label-preview-large' : '.label-preview');
const pageDims = labelSize === 'xlarge' ? '64mm 34mm' : (labelSize === 'large' ? '54mm 25mm' : '51mm 19mm');
const labelClass = labelSize === 'xlarge' ? 'print-label-xlarge' : (labelSize === 'large' ? 'print-label-large' : 'print-label');
const labelW = labelSize === 'xlarge' ? '64mm' : (labelSize === 'large' ? '54mm' : '51mm');
const labelH = labelSize === 'xlarge' ? '34mm' : (labelSize === 'large' ? '25mm' : '19mm');
const printFont = document.getElementById('label-font-select')?.value || '"MS Gothic","Hiragino Kaku Gothic Pro",Arial,sans-serif';
const printWeight = document.getElementById('label-bold-toggle')?.checked ? 'bold' : 'normal';
const printStyle = document.getElementById('label-italic-toggle')?.checked ? 'italic' : 'normal';
const labelContent = document.querySelector(previewSel).innerHTML;
labelWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="${chrome.runtime.getURL(cssFile)}">
<link rel="stylesheet" href="${chrome.runtime.getURL('label.css')}">
<style>
@page {
size: ${pageDims};
margin: 0;
/* Request 203dpi from the print engine */
resolution: 203dpi;
}
html, body {
margin: 0;
padding: 0;
width: ${labelW};
height: ${labelH};
overflow: hidden;
display: flex;
justify-content: flex-start;
align-items: flex-start;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
.${labelClass} {
width: ${labelW};
height: ${labelH};
position: relative;
font-family: ${printFont};
font-weight: ${printWeight};
font-style: ${printStyle};
background: white;
margin: 0;
padding: 0;
overflow: hidden;
}
/* 203dpi test watermark — screen only, does not print */
@media screen {
body::after {
content: '203 DPI TEST';
position: fixed;
bottom: 4px;
right: 6px;
font-size: 10px;
color: #aaa;
font-family: monospace;
pointer-events: none;
}
}
</style>
</head>
<body>
<div class="${labelClass}">
${labelContent}
</div>
</body>
</html>
`);
labelWindow.document.close();
}