Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/extensions/public/store.css
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,15 @@ input:focus, textarea:focus, select:focus { outline: none; border-color: var(--a
font-weight: 800; color: #04211d; background: linear-gradient(135deg, var(--accent), var(--accent-2));
flex: 0 0 auto;
}
/* Real logo (usually a transparent PNG): contain it in a padded, bordered tile
and drop the gradient. The backdrop is chosen from the logo's luminance in
store.js (hydrateIcons); a neutral panel shows until then. */
img.avatar.icon {
background: var(--panel); border: 1px solid var(--border);
padding: 5px; object-fit: contain;
}
img.avatar.icon.icon-dark { background: #0b1018; }
img.avatar.icon.icon-light { background: #ffffff; border-color: rgba(0, 0, 0, 0.12); }
.card h3 { margin: 0; font-size: 16px; }
.card .summary { color: var(--muted); font-size: 14px; margin: 0; min-height: 38px; }
.card .meta { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: auto; }
Expand Down
50 changes: 47 additions & 3 deletions apps/extensions/public/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,52 @@ function qs(name) { return new URLSearchParams(location.search).get(name); }
function initials(name) { return (name || '?').trim().split(/\s+/).slice(0, 2).map((w) => w[0] || '').join('').toUpperCase(); }

// Logo if the listing has one (auto-ingested from the .crx icons), else initials.
// Most logos are transparent PNGs, so we render them CONTAINED (never cropped)
// and pick a light or dark backdrop from the logo's own luminance in
// hydrateIcons() — otherwise a transparent logo shows over the gradient avatar
// and looks muddy.
function avatar(ext, px) {
const style = px ? ` style="width:${px}px;height:${px}px"` : '';
if (ext.iconUrl) return `<img class="avatar" src="${esc(ext.iconUrl)}" alt=""${style} style="object-fit:cover${px ? `;width:${px}px;height:${px}px` : ''}">`;
return `<div class="avatar"${px ? ` style="width:${px}px;height:${px}px;font-size:20px"` : ''}>${esc(initials(ext.name))}</div>`;
const dim = px ? `width:${px}px;height:${px}px` : '';
if (ext.iconUrl) return `<img class="avatar icon" data-icon src="${esc(ext.iconUrl)}" alt="" style="object-fit:contain;${dim}">`;
return `<div class="avatar"${px ? ` style="${dim};font-size:20px"` : ''}>${esc(initials(ext.name))}</div>`;
}

// Choose 'dark' or 'light' backdrop for a (usually transparent) logo by the
// average luminance of its opaque pixels: a bright logo wants a dark tile, a
// dark logo a light tile. Returns null if unreadable (fully transparent / a
// CORS-tainted canvas), leaving the default neutral tile.
function backdropFor(img) {
try {
const s = 24;
const c = document.createElement('canvas');
c.width = s; c.height = s;
const ctx = c.getContext('2d', { willReadFrequently: true });
ctx.drawImage(img, 0, 0, s, s);
const { data } = ctx.getImageData(0, 0, s, s);
let lum = 0, wsum = 0;
for (let i = 0; i < data.length; i += 4) {
const a = data[i + 3] / 255;
if (a < 0.1) continue;
lum += (0.2126 * data[i] + 0.7152 * data[i + 1] + 0.0722 * data[i + 2]) * a;
wsum += a;
}
if (wsum === 0) return null;
return lum / wsum > 140 ? 'dark' : 'light';
} catch (_) {
return 'dark'; // tainted canvas (remote icon) — dark suits the store theme
}
}

function hydrateIcons(root) {
root.querySelectorAll('img.avatar.icon[data-icon]').forEach((img) => {
const apply = () => {
const b = backdropFor(img);
if (b) img.classList.add('icon-' + b);
img.removeAttribute('data-icon');
};
if (img.complete && img.naturalWidth) apply();
else img.addEventListener('load', apply, { once: true });
});
}

// ── AI auto-ingest: paste a .crx URL, we fill the form + scan (no forms) ──
Expand Down Expand Up @@ -115,6 +157,7 @@ async function initBrowse() {
if (!extensions.length) { grid.innerHTML = ''; empty.classList.remove('hidden'); return; }
empty.classList.add('hidden');
grid.innerHTML = extensions.map(card).join('');
hydrateIcons(grid);
} catch (e) {
grid.innerHTML = `<p class="error">Couldn't load extensions: ${esc(e.message)}</p>`;
}
Expand Down Expand Up @@ -162,6 +205,7 @@ async function initDetail() {
<h3>Auto-update</h3>
<p class="hint">Chromium auto-updates this extension from its <code>update_url</code>:</p>
<pre>${esc(ext.updateUrl)}</pre>`;
hydrateIcons(root);

document.getElementById('flagBtn').addEventListener('click', async () => {
const reason = prompt('Reason? (malware, privacy, broken, spam, other)', 'other');
Expand Down
Loading