@@ -135,31 +135,89 @@ const RecentSearches = (() => {
list . unshift ( q ) ;
list . unshift ( q ) ;
const trimmed = list . slice ( 0 , MAX ) ;
const trimmed = list . slice ( 0 , MAX ) ;
save ( trimmed ) ;
save ( trimmed ) ;
pruneResults ( trimmed ) ;
} ,
} ,
remove ( q ) {
remove ( q ) {
const list = load ( ) . filter ( ( x ) => x . toLowerCase ( ) !== ( q || '' ) . toLowerCase ( ) ) ;
const list = load ( ) . filter ( ( x ) => x . toLowerCase ( ) !== ( q || '' ) . toLowerCase ( ) ) ;
save ( list ) ;
save ( list ) ;
pruneResults ( list ) ;
} ,
} ,
clear ( ) { save ( [ ] ) ; saveR esults( { } ) ; } ,
clear ( ) { save ( [ ] ) ; } , // saved r esults stay in SearchLibrary (cleared only from Settings)
// Cache the just-fetched result set for `q` (called on every successful search).
// Cache the just-fetched result set for `q` (called on every successful search).
cacheResults ( q , results ) {
cacheResults ( q , results ) { SearchLibrary . put ( q , results ) ; } ,
q = ( q || '' ) . trim ( ) ;
if ( ! q || ! Array . isArray ( results ) || ! results . length ) return ;
const r = loadResults ( ) ;
r [ norm ( q ) ] = { query : q , results : results . slice ( 0 , RESULTS _PER _QUERY _MAX ) , ts : Date . now ( ) } ;
saveResults ( r ) ;
pruneResults ( load ( ) ) ;
} ,
// Cached {query, results, ts} for `q`, or null.
// Cached {query, results, ts} for `q`, or null.
getResults ( q ) {
getResults ( q ) {
const hit = loadResults ( ) [ norm ( q ) ] ;
return SearchLibrary . get ( q ) ;
return hit && Array . isArray ( hit . results ) && hit . results . length ? hit : null ;
} ,
} ,
// Normalized queries that have a cached result set — one localStorage
// Normalized queries that have a cached result set — one localStorage
// read for the whole dropdown, rather than one per row.
// read for the whole dropdown, rather than one per row.
cachedQueries ( ) { return new Set ( Object . keys ( loadResults ( ) ) ) ; } ,
cachedQueries ( ) { return new Set ( SearchLibrary . list ( ) . map ( ( x ) => x . key ) ) ; } ,
} ;
} ) ( ) ;
// ============================================================================
// Search library — every result set (with thumbnail URLs) is kept on this device
// ============================================================================
// Clearing the search box or the recent-searches dropdown never deletes these;
// they are removed only from Settings → Saved searches (all, or a selection).
// The thumbnails themselves live in the service worker's `ytplayer-thumbs`
// cache (warmed below), so a saved search renders with its pictures offline.
const SearchLibrary = ( ( ) => {
const KEY = 'ytpSearchLibrary' ;
const LEGACY = 'ytpRecentSearchResults' ;
const MAX _QUERIES = 150 ;
const PER _QUERY = 50 ;
const norm = ( q ) => String ( q || '' ) . trim ( ) . toLowerCase ( ) ;
let mem = null ;
function read ( ) {
if ( mem ) return mem ;
let o = { } ;
try { o = JSON . parse ( localStorage . getItem ( KEY ) || '{}' ) || { } ; } catch { o = { } ; }
// One-time import of the old per-recent-search cache.
try {
const old = JSON . parse ( localStorage . getItem ( LEGACY ) || 'null' ) ;
if ( old && typeof old === 'object' ) {
for ( const [ k , v ] of Object . entries ( old ) ) if ( v && Array . isArray ( v . results ) && ! o [ k ] ) o [ k ] = v ;
localStorage . removeItem ( LEGACY ) ;
}
} catch { /* ignore */ }
mem = o ;
return mem ;
}
function write ( ) {
const o = read ( ) ;
for ( let attempt = 0 ; attempt < 4 ; attempt ++ ) {
try { localStorage . setItem ( KEY , JSON . stringify ( o ) ) ; return true ; } catch {
// Quota: drop the oldest fifth and retry.
const keys = Object . keys ( o ) . sort ( ( a , b ) => ( o [ a ] . ts || 0 ) - ( o [ b ] . ts || 0 ) ) ;
keys . slice ( 0 , Math . max ( 1 , Math . ceil ( keys . length / 5 ) ) ) . forEach ( ( k ) => delete o [ k ] ) ;
}
}
return false ;
}
return {
put ( q , results ) {
q = String ( q || '' ) . trim ( ) ;
if ( ! q || ! Array . isArray ( results ) || ! results . length ) return ;
const o = read ( ) ;
o [ norm ( q ) ] = { query : q , results : results . slice ( 0 , PER _QUERY ) , ts : Date . now ( ) } ;
const keys = Object . keys ( o ) ;
if ( keys . length > MAX _QUERIES ) {
keys . sort ( ( a , b ) => ( o [ a ] . ts || 0 ) - ( o [ b ] . ts || 0 ) ) . slice ( 0 , keys . length - MAX _QUERIES ) . forEach ( ( k ) => delete o [ k ] ) ;
}
write ( ) ;
} ,
get ( q ) { const h = read ( ) [ norm ( q ) ] ; return h && Array . isArray ( h . results ) && h . results . length ? h : null ; } ,
has ( q ) { return ! ! read ( ) [ norm ( q ) ] ; } ,
// Newest first: [{ query, count, ts, thumb }]
list ( ) {
const o = read ( ) ;
return Object . keys ( o ) . map ( ( k ) => ( { key : k , query : o [ k ] . query || k , count : ( o [ k ] . results || [ ] ) . length , ts : o [ k ] . ts || 0 , thumb : ( ( o [ k ] . results || [ ] ) [ 0 ] || { } ) . thumbnail || '' } ) )
. sort ( ( a , b ) => b . ts - a . ts ) ;
} ,
remove ( keys ) { const o = read ( ) ; ( keys || [ ] ) . forEach ( ( k ) => delete o [ k ] ) ; write ( ) ; } ,
clear ( ) { mem = { } ; try { localStorage . removeItem ( KEY ) ; } catch { /* blocked */ } } ,
bytes ( ) { try { return ( localStorage . getItem ( KEY ) || '' ) . length ; } catch { return 0 ; } } ,
count ( ) { return Object . keys ( read ( ) ) . length ; } ,
} ;
} ;
} ) ( ) ;
} ) ( ) ;
@@ -1242,6 +1300,14 @@ function startCacheResyncWatch() {
// is only cached once something has actually requested it. Pull it through the
// is only cached once something has actually requested it. Pull it through the
// service worker (which caches thumbnail responses) so a video saved for
// service worker (which caches thumbnail responses) so a video saved for
// offline still has its art when the network is gone.
// offline still has its art when the network is gone.
// Pull a result set's artwork into the service worker's thumbnail cache, a few at
// a time, so a saved search still has pictures offline.
function warmSearchThumbs ( results ) {
const urls = ( results || [ ] ) . map ( ( r ) => r && r . thumbnail ) . filter ( Boolean ) . slice ( 0 , 50 ) ;
let i = 0 ;
const next = ( ) => { if ( i >= urls . length ) return ; warmThumb ( urls [ i ++ ] ) ; setTimeout ( next , 40 ) ; } ;
next ( ) ; next ( ) ; next ( ) ;
}
function warmThumb ( url ) {
function warmThumb ( url ) {
if ( ! WEB || ! url || ! /^https?:/i . test ( url ) ) return ;
if ( ! WEB || ! url || ! /^https?:/i . test ( url ) ) return ;
fetch ( url , { mode : 'cors' , credentials : 'omit' } ) . catch ( ( ) => { } ) ;
fetch ( url , { mode : 'cors' , credentials : 'omit' } ) . catch ( ( ) => { } ) ;
@@ -4482,7 +4548,7 @@ const Remote = (() => {
} catch { found = [ ] ; }
} catch { found = [ ] ; }
results . innerHTML = found . length ? found . slice ( 0 , 20 ) . map ( ( v , i ) => `
results . innerHTML = found . length ? found . slice ( 0 , 20 ) . map ( ( v , i ) => `
<button class="rv-add-item" type="button" data-k=" ${ i } ">
<button class="rv-add-item" type="button" data-k=" ${ i } ">
<img src=" ${ escapeHtml ( v . thumbnail || '' ) } " alt="" loading="lazy" />
<img src=" ${ escapeHtml ( v . thumbnail || '' ) } " alt="" loading="lazy" decoding="async" />
<span><b> ${ escapeHtml ( v . title || '' ) } </b><small> ${ escapeHtml ( v . channel || '' ) } ${ v . duration ? ' · ' + clock ( v . duration ) : '' } </small></span>
<span><b> ${ escapeHtml ( v . title || '' ) } </b><small> ${ escapeHtml ( v . channel || '' ) } ${ v . duration ? ' · ' + clock ( v . duration ) : '' } </small></span>
</button> ` ) . join ( '' ) : '<div class="rv-empty">No results.</div>' ;
</button> ` ) . join ( '' ) : '<div class="rv-empty">No results.</div>' ;
} ) ;
} ) ;
@@ -6905,7 +6971,7 @@ function renderUpNext() {
const item = document . createElement ( 'div' ) ;
const item = document . createElement ( 'div' ) ;
item . className = 'upnext-item' ;
item . className = 'upnext-item' ;
item . innerHTML = `
item . innerHTML = `
<img src=" ${ escapeHtml ( v . thumbnail || '' ) } " alt="" loading="lazy" />
<img src=" ${ escapeHtml ( v . thumbnail || '' ) } " alt="" loading="lazy" decoding="async" />
<div class="upnext-info">
<div class="upnext-info">
<div class="upnext-title"></div>
<div class="upnext-title"></div>
<div class="upnext-channel"></div>
<div class="upnext-channel"></div>
@@ -7333,7 +7399,7 @@ function renderHomePlaylists() {
card . type = 'button' ;
card . type = 'button' ;
card . innerHTML = `
card . innerHTML = `
<span class="home-pl-thumb">
<span class="home-pl-thumb">
${ thumb ? ` <img src=" ${ escapeHtml ( thumb ) } " alt="" loading="lazy" /> `
${ thumb ? ` <img src=" ${ escapeHtml ( thumb ) } " alt="" loading="lazy" decoding="async" /> `
: '<span class="home-pl-thumb-fallback">🎵</span>' }
: '<span class="home-pl-thumb-fallback">🎵</span>' }
${ offlineHtml }
${ offlineHtml }
<span class="home-pl-play" title="Play all">▶</span>
<span class="home-pl-play" title="Play all">▶</span>
@@ -7414,6 +7480,7 @@ function renderList() {
if ( view . type === 'settings' ) { renderSettings ( ) ; return ; }
if ( view . type === 'settings' ) { renderSettings ( ) ; return ; }
if ( view . type === 'saved' ) { renderSaved ( ) ; return ; }
if ( view . type === 'saved' ) { renderSaved ( ) ; return ; }
if ( view . type === 'p2p' ) { renderP2pList ( ) ; return ; }
if ( view . type === 'p2p' ) { renderP2pList ( ) ; return ; }
if ( view . type === 'savedSearches' ) { renderSavedSearches ( ) ; return ; }
if ( view . type === 'downloads' ) { renderDownloads ( ) ; return ; }
if ( view . type === 'downloads' ) { renderDownloads ( ) ; return ; }
if ( view . type === 'stats' ) { renderStats ( ) ; return ; }
if ( view . type === 'stats' ) { renderStats ( ) ; return ; }
if ( view . type === 'notes' ) { renderNotesView ( ) ; return ; }
if ( view . type === 'notes' ) { renderNotesView ( ) ; return ; }
@@ -7776,6 +7843,47 @@ async function renderP2pStrip() {
box . classList . remove ( 'hidden' ) ;
box . classList . remove ( 'hidden' ) ;
}
}
// ============================================================================
// Saved searches — browse (read-only here; deleting happens in Settings)
// ============================================================================
function fmtAgo ( ts ) {
const s = Math . max ( 0 , ( Date . now ( ) - ts ) / 1000 ) ;
if ( s < 90 ) return 'just now' ;
if ( s < 3600 ) return Math . round ( s / 60 ) + ' min ago' ;
if ( s < 86400 ) return Math . round ( s / 3600 ) + ' h ago' ;
return Math . round ( s / 86400 ) + ' d ago' ;
}
function renderSavedSearches ( ) {
els . listTitle . textContent = '📚 Saved searches' ;
els . listActions . innerHTML = '' ;
els . status . classList . add ( 'hidden' ) ;
const c = els . cards ;
c . innerHTML = '' ;
const items = SearchLibrary . list ( ) ;
if ( ! items . length ) {
c . innerHTML = '<div class="empty-state"><div class="empty-icon">📚</div><h3 class="empty-title">No saved searches yet</h3><p class="empty-desc">Every search you run is kept here with its thumbnails, even after you clear the search box. You can remove them in Settings.</p></div>' ;
return ;
}
const sum = document . createElement ( 'div' ) ;
sum . className = 'saved-summary' ;
sum . innerHTML = ` <span class="saved-total"> ${ items . length } </span><span class="saved-sub">saved search ${ items . length === 1 ? '' : 'es' } · ${ fmtBytes ( SearchLibrary . bytes ( ) ) } · manage in Settings</span> ` ;
c . appendChild ( sum ) ;
items . forEach ( ( it ) => {
const row = document . createElement ( 'div' ) ;
row . className = 'card saved-card ss-card' ;
row . innerHTML = `
<div class="thumb"><img loading="lazy" decoding="async" src=" ${ escapeHtml ( it . thumb ) } " alt="" onerror="this.classList.add('no-art')" /></div>
<div class="card-info"><div class="card-title"></div><div class="card-channel"> ${ it . count } result ${ it . count === 1 ? '' : 's' } · ${ fmtAgo ( it . ts ) } </div></div> ` ;
row . querySelector ( '.card-title' ) . textContent = it . query ;
row . addEventListener ( 'click' , ( ) => {
els . searchInput . value = it . query ;
if ( els . runSearch ) els . runSearch ( it . query , { instant : SearchLibrary . get ( it . query ) } ) ;
} ) ;
c . appendChild ( row ) ;
} ) ;
}
async function renderP2pList ( ) {
async function renderP2pList ( ) {
els . listTitle . textContent = '📡 On other devices' ;
els . listTitle . textContent = '📡 On other devices' ;
els . listActions . innerHTML = '' ;
els . listActions . innerHTML = '' ;
@@ -7822,7 +7930,7 @@ async function renderP2pList() {
row . dataset . id = it . id ;
row . dataset . id = it . id ;
const state = it . online ? ` <b class="p2p-on">● ${ it . online } online</b> ` : '<span class="p2p-off">○ none online</span>' ;
const state = it . online ? ` <b class="p2p-on">● ${ it . online } online</b> ` : '<span class="p2p-off">○ none online</span>' ;
row . innerHTML = `
row . innerHTML = `
<div class="thumb"><img loading="lazy" src=" ${ escapeHtml ( v . thumbnail ) } " alt="" onerror="this.classList.add('no-art')" /></div>
<div class="thumb"><img loading="lazy" decoding="async" src=" ${ escapeHtml ( v . thumbnail ) } " alt="" onerror="this.classList.add('no-art')" /></div>
<div class="card-info">
<div class="card-info">
<div class="card-title"></div>
<div class="card-title"></div>
<div class="card-channel"> ${ state } · ${ it . holders } device ${ it . holders === 1 ? '' : 's' } · ${ fmtBytes ( it . size ) } ${ it . height ? ' · ' + it . height + 'p' : '' } ${ it . stale ? ' · <i>not checked lately</i>' : '' } </div>
<div class="card-channel"> ${ state } · ${ it . holders } device ${ it . holders === 1 ? '' : 's' } · ${ fmtBytes ( it . size ) } ${ it . height ? ' · ' + it . height + 'p' : '' } ${ it . stale ? ' · <i>not checked lately</i>' : '' } </div>
@@ -7923,7 +8031,7 @@ async function renderSaved() {
row . className = 'card saved-card' + ( isCustom ? ' custom' : '' ) ;
row . className = 'card saved-card' + ( isCustom ? ' custom' : '' ) ;
row . dataset . id = it . id ;
row . dataset . id = it . id ;
row . innerHTML = `
row . innerHTML = `
<div class="thumb"><img loading="lazy" src=" ${ escapeHtml ( v . thumbnail || thumbUrlFor ( v . id , null ) || '' ) } " alt=""
<div class="thumb"><img loading="lazy" decoding="async" src=" ${ escapeHtml ( v . thumbnail || thumbUrlFor ( v . id , null ) || '' ) } " alt=""
onerror="this.classList.add('no-art')" /> ${ isCustom ? '<span class="edit-badge" title="Edited copy">✂</span>' : '' } ${ isUploadId ( v . id ) ? ` <span class="upload-badge" title="On this server"> ${ v . kind === 'audio' ? '♪' : '📁' } </span> ` : '' } </div>
onerror="this.classList.add('no-art')" /> ${ isCustom ? '<span class="edit-badge" title="Edited copy">✂</span>' : '' } ${ isUploadId ( v . id ) ? ` <span class="upload-badge" title="On this server"> ${ v . kind === 'audio' ? '♪' : '📁' } </span> ` : '' } </div>
<div class="card-info">
<div class="card-info">
<div class="card-title"></div>
<div class="card-title"></div>
@@ -7990,7 +8098,7 @@ function renderDownloads() {
row . dataset . id = v . id ;
row . dataset . id = v . id ;
row . innerHTML = `
row . innerHTML = `
<div class="thumb">
<div class="thumb">
<img loading="lazy" src=" ${ escapeHtml ( v . thumbnail || '' ) } " alt="" />
<img loading="lazy" decoding="async" src=" ${ escapeHtml ( v . thumbnail || '' ) } " alt="" />
<div class="dl-progress"><div class="dl-bar"></div></div>
<div class="dl-progress"><div class="dl-bar"></div></div>
</div>
</div>
<div class="card-info">
<div class="card-info">
@@ -8179,6 +8287,23 @@ async function renderSettings() {
<div id="cacheList" class="cache-list"></div>
<div id="cacheList" class="cache-list"></div>
</div>
</div>
<div class="set-group">
<div class="set-group-title">Saved searches</div>
<div class="set-row">
<span>
Kept on this device
<small>Search results and their thumbnails stay saved after you clear the search box. Only you can remove them, here.</small>
</span>
<span id="ssTotal" class="set-stat">…</span>
</div>
<div class="set-actions">
<button id="ssSelectAll" class="btn">Select all</button>
<button id="ssDeleteSel" class="btn danger" disabled>Delete selected</button>
<button id="ssClearAll" class="btn danger">Clear all</button>
</div>
<div id="ssList" class="ss-list"></div>
</div>
<div class="set-group">
<div class="set-group">
<div class="set-group-title">Playlists</div>
<div class="set-group-title">Playlists</div>
<div class="set-row" style="border:none;gap:8px;flex-wrap:wrap">
<div class="set-row" style="border:none;gap:8px;flex-wrap:wrap">
@@ -8288,6 +8413,40 @@ async function renderSettings() {
$ ( 'setRepeat' ) . addEventListener ( 'change' , ( e ) => { data . settings . repeatMode = e . target . checked ? 'all' : 'off' ; updateLoopRepeatButtons ( ) ; persist ( ) ; } ) ;
$ ( 'setRepeat' ) . addEventListener ( 'change' , ( e ) => { data . settings . repeatMode = e . target . checked ? 'all' : 'off' ; updateLoopRepeatButtons ( ) ; persist ( ) ; } ) ;
$ ( 'setLoopOne' ) . addEventListener ( 'change' , ( e ) => { data . settings . loopOne = e . target . checked ; updateLoopRepeatButtons ( ) ; persist ( ) ; } ) ;
$ ( 'setLoopOne' ) . addEventListener ( 'change' , ( e ) => { data . settings . loopOne = e . target . checked ; updateLoopRepeatButtons ( ) ; persist ( ) ; } ) ;
// ---- Saved searches manager ----
{
const listEl = $ ( 'ssList' ) ;
const sel = new Set ( ) ;
const refresh = ( ) => {
const items = SearchLibrary . list ( ) ;
for ( const k of [ ... sel ] ) if ( ! items . some ( ( i ) => i . key === k ) ) sel . delete ( k ) ;
$ ( 'ssTotal' ) . textContent = items . length ? ` ${ items . length } · ${ fmtBytes ( SearchLibrary . bytes ( ) ) } ` : 'None' ;
listEl . innerHTML = items . length ? items . map ( ( i ) => `
<label class="ss-row"><input type="checkbox" data-k=" ${ escapeHtml ( i . key ) } " ${ sel . has ( i . key ) ? 'checked' : '' } />
<span class="ss-q"> ${ escapeHtml ( i . query ) } </span><small> ${ i . count } · ${ fmtAgo ( i . ts ) } </small></label> ` ) . join ( '' ) : '<div class="muted" style="padding:8px 2px">No saved searches.</div>' ;
$ ( 'ssDeleteSel' ) . disabled = ! sel . size ;
$ ( 'ssDeleteSel' ) . textContent = sel . size ? ` Delete selected ( ${ sel . size } ) ` : 'Delete selected' ;
$ ( 'ssSelectAll' ) . textContent = items . length && sel . size === items . length ? 'Select none' : 'Select all' ;
$ ( 'ssClearAll' ) . disabled = ! items . length ;
} ;
listEl . addEventListener ( 'change' , ( e ) => { const k = e . target . dataset && e . target . dataset . k ; if ( ! k ) return ; e . target . checked ? sel . add ( k ) : sel . delete ( k ) ; refresh ( ) ; } ) ;
$ ( 'ssSelectAll' ) . addEventListener ( 'click' , ( ) => {
const items = SearchLibrary . list ( ) ;
if ( sel . size === items . length ) sel . clear ( ) ; else items . forEach ( ( i ) => sel . add ( i . key ) ) ;
refresh ( ) ;
} ) ;
$ ( 'ssDeleteSel' ) . addEventListener ( 'click' , ( ) => {
if ( ! sel . size || ! confirm ( ` Delete ${ sel . size } saved search ${ sel . size === 1 ? '' : 'es' } ? Their results and thumbnails are removed from this device. ` ) ) return ;
SearchLibrary . remove ( [ ... sel ] ) ; sel . clear ( ) ; refresh ( ) ; toast ( 'Saved searches deleted' ) ;
} ) ;
$ ( 'ssClearAll' ) . addEventListener ( 'click' , ( ) => {
if ( ! confirm ( 'Delete ALL saved searches and their thumbnails from this device?' ) ) return ;
SearchLibrary . clear ( ) ; sel . clear ( ) ; refresh ( ) ; toast ( 'All saved searches deleted' ) ;
} ) ;
refresh ( ) ;
}
// ---- Wire playback controls ----
// ---- Wire playback controls ----
$ ( 'setQuality' ) . addEventListener ( 'change' , ( e ) => {
$ ( 'setQuality' ) . addEventListener ( 'change' , ( e ) => {
data . settings . quality = e . target . value ;
data . settings . quality = e . target . value ;
@@ -8520,7 +8679,7 @@ function renderCard(v, index, list) {
card . innerHTML = `
card . innerHTML = `
<div class="batch-check"></div>
<div class="batch-check"></div>
<div class="thumb">
<div class="thumb">
<img loading="lazy" src=" ${ escapeHtml ( v . thumbnail || '' ) } " alt="" />
<img loading="lazy" decoding="async" src=" ${ escapeHtml ( v . thumbnail || '' ) } " alt="" />
${ v . duration ? ` <span class="dur"> ${ fmtTime ( v . duration ) } </span> ` : '' }
${ v . duration ? ` <span class="dur"> ${ fmtTime ( v . duration ) } </span> ` : '' }
<span class="saved-badge" title=" ${ isDownloading ? 'Downloading…' : 'Saved offline' } "> ${ isDownloading ? '' : '⬇' } </span>
<span class="saved-badge" title=" ${ isDownloading ? 'Downloading…' : 'Saved offline' } "> ${ isDownloading ? '' : '⬇' } </span>
${ isDownloading ? '<div class="dl-progress"><div class="dl-bar"></div></div>' : '' }
${ isDownloading ? '<div class="dl-progress"><div class="dl-bar"></div></div>' : '' }
@@ -9404,6 +9563,7 @@ function wireUI() {
searchAttempted = true ;
searchAttempted = true ;
renderList ( ) ;
renderList ( ) ;
RecentSearches . cacheResults ( q , results ) ;
RecentSearches . cacheResults ( q , results ) ;
warmSearchThumbs ( results ) ;
warmStreams ( results . slice ( 0 , 3 ) . map ( ( r ) => r . id ) ) ;
warmStreams ( results . slice ( 0 , 3 ) . map ( ( r ) => r . id ) ) ;
perfMeasure ( 'ytp:search' , 'ytp:search-start' ) ;
perfMeasure ( 'ytp:search' , 'ytp:search-start' ) ;
} catch ( err ) {
} catch ( err ) {
@@ -9415,6 +9575,7 @@ function wireUI() {
}
}
}
}
els . runSearch = runSearchQuery ;
els . searchForm . addEventListener ( 'submit' , ( e ) => {
els . searchForm . addEventListener ( 'submit' , ( e ) => {
e . preventDefault ( ) ;
e . preventDefault ( ) ;
runSearchQuery ( els . searchInput . value ) ;
runSearchQuery ( els . searchInput . value ) ;
@@ -9423,7 +9584,7 @@ function wireUI() {
// ---- Recent searches dropdown (device-local; see RecentSearches above) ----
// ---- Recent searches dropdown (device-local; see RecentSearches above) ----
function renderRecentSearches ( ) {
function renderRecentSearches ( ) {
const list = RecentSearches . list ( ) ;
const list = RecentSearches . list ( ) ;
if ( ! list . length ) { els . searchRecent . innerHTML = '' ; return ; }
if ( ! list . length && ! SearchLibrary . count ( ) ) { els . searchRecent . innerHTML = '' ; return ; }
const cachedQueries = RecentSearches . cachedQueries ( ) ;
const cachedQueries = RecentSearches . cachedQueries ( ) ;
els . searchRecent . innerHTML = list . map ( ( q ) => {
els . searchRecent . innerHTML = list . map ( ( q ) => {
const isCached = cachedQueries . has ( q . trim ( ) . toLowerCase ( ) ) ;
const isCached = cachedQueries . has ( q . trim ( ) . toLowerCase ( ) ) ;
@@ -9432,11 +9593,12 @@ function wireUI() {
<span class="sr-icon" title=" ${ isCached ? 'Loads instantly, then refreshes' : 'Recent search' } "> ${ isCached ? '⚡' : '🕐' } </span><span class="sr-text"> ${ escapeHtml ( q ) } </span>
<span class="sr-icon" title=" ${ isCached ? 'Loads instantly, then refreshes' : 'Recent search' } "> ${ isCached ? '⚡' : '🕐' } </span><span class="sr-text"> ${ escapeHtml ( q ) } </span>
<button type="button" class="sr-remove" data-remove=" ${ escapeHtml ( q ) } " aria-label="Remove ‘ ${ escapeHtml ( q ) } ’ from recent searches">✕</button>
<button type="button" class="sr-remove" data-remove=" ${ escapeHtml ( q ) } " aria-label="Remove ‘ ${ escapeHtml ( q ) } ’ from recent searches">✕</button>
</div> ` ;
</div> ` ;
} ) . join ( '' ) + ' <button type="button" class="search-recent-clear" data-clear-all>Clear recent searches </button>' ;
} ) . join ( '' ) + ` <button type="button" class="search-recent-clear" data-clear-all>Clear this list <small>(saved results are kept)</small> </button>`
+ ( SearchLibrary . count ( ) ? ` <button type="button" class="search-recent-clear" data-open-saved>📚 Saved searches ( ${ SearchLibrary . count ( ) } )</button> ` : '' ) ;
}
}
function showRecentSearches ( ) {
function showRecentSearches ( ) {
renderRecentSearches ( ) ;
renderRecentSearches ( ) ;
if ( ! RecentSearches . list ( ) . length ) return ;
if ( ! RecentSearches . list ( ) . length && ! SearchLibrary . count ( ) ) return ;
els . searchRecent . classList . remove ( 'hidden' ) ;
els . searchRecent . classList . remove ( 'hidden' ) ;
}
}
function hideRecentSearches ( ) { els . searchRecent . classList . add ( 'hidden' ) ; }
function hideRecentSearches ( ) { els . searchRecent . classList . add ( 'hidden' ) ; }
@@ -9455,6 +9617,13 @@ function wireUI() {
showRecentSearches ( ) ;
showRecentSearches ( ) ;
return ;
return ;
}
}
if ( e . target . closest ( '[data-open-saved]' ) ) {
e . preventDefault ( ) ;
hideRecentSearches ( ) ;
view = { type : 'savedSearches' } ;
render ( ) ;
return ;
}
if ( e . target . closest ( '[data-clear-all]' ) ) {
if ( e . target . closest ( '[data-clear-all]' ) ) {
e . preventDefault ( ) ;
e . preventDefault ( ) ;
RecentSearches . clear ( ) ;
RecentSearches . clear ( ) ;
@@ -10105,7 +10274,7 @@ function renderRelated() {
relatedVideos . forEach ( ( v ) => {
relatedVideos . forEach ( ( v ) => {
const item = document . createElement ( 'div' ) ;
const item = document . createElement ( 'div' ) ;
item . className = 'related-item' ;
item . className = 'related-item' ;
item . innerHTML = ` <img src=" ${ escapeHtml ( v . thumbnail || '' ) } " alt="" loading="lazy" /><div class="ri-info"><div class="ri-title"></div><div class="ri-channel"></div></div> ` ;
item . innerHTML = ` <img src=" ${ escapeHtml ( v . thumbnail || '' ) } " alt="" loading="lazy" decoding="async" /><div class="ri-info"><div class="ri-title"></div><div class="ri-channel"></div></div> ` ;
item . querySelector ( '.ri-title' ) . textContent = v . title ;
item . querySelector ( '.ri-title' ) . textContent = v . title ;
const riChEl = item . querySelector ( '.ri-channel' ) ;
const riChEl = item . querySelector ( '.ri-channel' ) ;
riChEl . textContent = v . channel || '' ;
riChEl . textContent = v . channel || '' ;