@@ -8,10 +8,52 @@ function qs(name) { return new URLSearchParams(location.search).get(name); }
88function initials ( name ) { return ( name || '?' ) . trim ( ) . split ( / \s + / ) . slice ( 0 , 2 ) . map ( ( w ) => w [ 0 ] || '' ) . join ( '' ) . toUpperCase ( ) ; }
99
1010// Logo if the listing has one (auto-ingested from the .crx icons), else initials.
11+ // Most logos are transparent PNGs, so we render them CONTAINED (never cropped)
12+ // and pick a light or dark backdrop from the logo's own luminance in
13+ // hydrateIcons() — otherwise a transparent logo shows over the gradient avatar
14+ // and looks muddy.
1115function avatar ( ext , px ) {
12- const style = px ? ` style="width:${ px } px;height:${ px } px"` : '' ;
13- if ( ext . iconUrl ) return `<img class="avatar" src="${ esc ( ext . iconUrl ) } " alt=""${ style } style="object-fit:cover${ px ? `;width:${ px } px;height:${ px } px` : '' } ">` ;
14- return `<div class="avatar"${ px ? ` style="width:${ px } px;height:${ px } px;font-size:20px"` : '' } >${ esc ( initials ( ext . name ) ) } </div>` ;
16+ const dim = px ? `width:${ px } px;height:${ px } px` : '' ;
17+ if ( ext . iconUrl ) return `<img class="avatar icon" data-icon src="${ esc ( ext . iconUrl ) } " alt="" style="object-fit:contain;${ dim } ">` ;
18+ return `<div class="avatar"${ px ? ` style="${ dim } ;font-size:20px"` : '' } >${ esc ( initials ( ext . name ) ) } </div>` ;
19+ }
20+
21+ // Choose 'dark' or 'light' backdrop for a (usually transparent) logo by the
22+ // average luminance of its opaque pixels: a bright logo wants a dark tile, a
23+ // dark logo a light tile. Returns null if unreadable (fully transparent / a
24+ // CORS-tainted canvas), leaving the default neutral tile.
25+ function backdropFor ( img ) {
26+ try {
27+ const s = 24 ;
28+ const c = document . createElement ( 'canvas' ) ;
29+ c . width = s ; c . height = s ;
30+ const ctx = c . getContext ( '2d' , { willReadFrequently : true } ) ;
31+ ctx . drawImage ( img , 0 , 0 , s , s ) ;
32+ const { data } = ctx . getImageData ( 0 , 0 , s , s ) ;
33+ let lum = 0 , wsum = 0 ;
34+ for ( let i = 0 ; i < data . length ; i += 4 ) {
35+ const a = data [ i + 3 ] / 255 ;
36+ if ( a < 0.1 ) continue ;
37+ lum += ( 0.2126 * data [ i ] + 0.7152 * data [ i + 1 ] + 0.0722 * data [ i + 2 ] ) * a ;
38+ wsum += a ;
39+ }
40+ if ( wsum === 0 ) return null ;
41+ return lum / wsum > 140 ? 'dark' : 'light' ;
42+ } catch ( _ ) {
43+ return 'dark' ; // tainted canvas (remote icon) — dark suits the store theme
44+ }
45+ }
46+
47+ function hydrateIcons ( root ) {
48+ root . querySelectorAll ( 'img.avatar.icon[data-icon]' ) . forEach ( ( img ) => {
49+ const apply = ( ) => {
50+ const b = backdropFor ( img ) ;
51+ if ( b ) img . classList . add ( 'icon-' + b ) ;
52+ img . removeAttribute ( 'data-icon' ) ;
53+ } ;
54+ if ( img . complete && img . naturalWidth ) apply ( ) ;
55+ else img . addEventListener ( 'load' , apply , { once : true } ) ;
56+ } ) ;
1557}
1658
1759// ── AI auto-ingest: paste a .crx URL, we fill the form + scan (no forms) ──
@@ -115,6 +157,7 @@ async function initBrowse() {
115157 if ( ! extensions . length ) { grid . innerHTML = '' ; empty . classList . remove ( 'hidden' ) ; return ; }
116158 empty . classList . add ( 'hidden' ) ;
117159 grid . innerHTML = extensions . map ( card ) . join ( '' ) ;
160+ hydrateIcons ( grid ) ;
118161 } catch ( e ) {
119162 grid . innerHTML = `<p class="error">Couldn't load extensions: ${ esc ( e . message ) } </p>` ;
120163 }
@@ -162,6 +205,7 @@ async function initDetail() {
162205 <h3>Auto-update</h3>
163206 <p class="hint">Chromium auto-updates this extension from its <code>update_url</code>:</p>
164207 <pre>${ esc ( ext . updateUrl ) } </pre>` ;
208+ hydrateIcons ( root ) ;
165209
166210 document . getElementById ( 'flagBtn' ) . addEventListener ( 'click' , async ( ) => {
167211 const reason = prompt ( 'Reason? (malware, privacy, broken, spam, other)' , 'other' ) ;
0 commit comments