Persist web UI state across reloads
Reloading the tab no longer blows away the user's context. The active
view (channels / continue / recent / favourites / bookmarks / waiting /
specific channel / specific playlist), the current filter input, and
the chosen sort all survive a hard refresh via localStorage.
- New `saveUiState()` serializes `{view, search, sort}` after every
interaction that could change them: each setX() view switch, search
oninput, sort onchange.
- `currentViewToken()` collapses the seven `showX` booleans + the two
index integers into a single string. Channel-view tokens reference
the channel by name (`channel:<name>|<playlistIdx>`) instead of by
array index, so a library re-order doesn't strand the user on the
wrong channel.
- `restoreUiState()` runs once after the first library load completes —
the channel resolution needs the `library` array populated, so the
init block was wrapped in an async IIFE to sequence the two steps.
Channels deleted between sessions fall back to "All" cleanly because
`findIndex` returns -1 and we just leave the show* booleans at their
default.
55 unit tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
171553c137
commit
3adcc60b78
1 changed files with 75 additions and 11 deletions
|
|
@ -120,8 +120,8 @@
|
||||||
<header>
|
<header>
|
||||||
<button id="menu-btn" onclick="toggleSidebar()">☰</button>
|
<button id="menu-btn" onclick="toggleSidebar()">☰</button>
|
||||||
<h1>yt-offline</h1>
|
<h1>yt-offline</h1>
|
||||||
<input type="search" id="search" placeholder="Filter…" oninput="renderGrid()">
|
<input type="search" id="search" placeholder="Filter…" oninput="saveUiState();renderGrid()">
|
||||||
<select id="sort" onchange="renderGrid()">
|
<select id="sort" onchange="saveUiState();renderGrid()">
|
||||||
<option value="date-desc">Newest</option>
|
<option value="date-desc">Newest</option>
|
||||||
<option value="date-asc">Oldest</option>
|
<option value="date-asc">Oldest</option>
|
||||||
<option value="title">Title</option>
|
<option value="title">Title</option>
|
||||||
|
|
@ -310,14 +310,69 @@ function resetViewSelectors(){
|
||||||
showFavourites=false;showBookmarks=false;showWaiting=false;
|
showFavourites=false;showBookmarks=false;showWaiting=false;
|
||||||
activeChannelIdx=null;activePlaylistIdx=null;
|
activeChannelIdx=null;activePlaylistIdx=null;
|
||||||
}
|
}
|
||||||
function setContinue(){resetViewSelectors();showContinue=true;selected.clear();closeSidebar();renderSidebar();renderGrid()}
|
|
||||||
function setRecent(){resetViewSelectors();showRecent=true;selected.clear();closeSidebar();renderSidebar();renderGrid()}
|
// Persist the user's current view + search + sort to localStorage so
|
||||||
function setFavourites(){resetViewSelectors();showFavourites=true;selected.clear();closeSidebar();renderSidebar();renderGrid()}
|
// reloading the tab doesn't blow away their context. Cheap to write on
|
||||||
function setBookmarks(){resetViewSelectors();showBookmarks=true;selected.clear();closeSidebar();renderSidebar();renderGrid()}
|
// every interaction — localStorage is synchronous but the payload is tiny.
|
||||||
function setWaiting(){resetViewSelectors();showWaiting=true;selected.clear();closeSidebar();renderSidebar();renderGrid()}
|
function currentViewToken(){
|
||||||
function setChannels(){resetViewSelectors();showChannels=true;selected.clear();closeSidebar();renderSidebar();renderGrid()}
|
if(showContinue)return 'continue';
|
||||||
function setView(ci,pi){resetViewSelectors();activeChannelIdx=ci;activePlaylistIdx=pi;selected.clear();closeSidebar();renderSidebar();renderGrid()}
|
if(showRecent)return 'recent';
|
||||||
function setMusic(){resetViewSelectors();showMusic=true;selected.clear();closeSidebar();renderSidebar();renderGrid()}
|
if(showFavourites)return 'favourites';
|
||||||
|
if(showBookmarks)return 'bookmarks';
|
||||||
|
if(showWaiting)return 'waiting';
|
||||||
|
if(showChannels)return 'channels';
|
||||||
|
if(showMusic)return 'music';
|
||||||
|
if(activeChannelIdx!=null)return `channel:${library[activeChannelIdx]?.name||''}|${activePlaylistIdx??''}`;
|
||||||
|
return 'all';
|
||||||
|
}
|
||||||
|
function saveUiState(){
|
||||||
|
try{
|
||||||
|
localStorage.setItem('ui-state',JSON.stringify({
|
||||||
|
view:currentViewToken(),
|
||||||
|
search:document.getElementById('search')?.value||'',
|
||||||
|
sort:document.getElementById('sort')?.value||'date-desc',
|
||||||
|
}));
|
||||||
|
}catch{}
|
||||||
|
}
|
||||||
|
function restoreUiState(){
|
||||||
|
let raw;try{raw=JSON.parse(localStorage.getItem('ui-state')||'null')}catch{}
|
||||||
|
if(!raw)return;
|
||||||
|
const s=document.getElementById('search'); if(s&&raw.search)s.value=raw.search;
|
||||||
|
const sortEl=document.getElementById('sort'); if(sortEl&&raw.sort)sortEl.value=raw.sort;
|
||||||
|
// Map the persisted view back to the matching show* boolean. Channels
|
||||||
|
// are stored by name (not by index) so a library re-order doesn't land
|
||||||
|
// the user on the wrong channel.
|
||||||
|
resetViewSelectors();
|
||||||
|
switch(raw.view){
|
||||||
|
case 'continue':showContinue=true;break;
|
||||||
|
case 'recent':showRecent=true;break;
|
||||||
|
case 'favourites':showFavourites=true;break;
|
||||||
|
case 'bookmarks':showBookmarks=true;break;
|
||||||
|
case 'waiting':showWaiting=true;break;
|
||||||
|
case 'channels':showChannels=true;break;
|
||||||
|
case 'music':showMusic=true;break;
|
||||||
|
case 'all':break; // default
|
||||||
|
default:
|
||||||
|
if(raw.view&&raw.view.startsWith('channel:')){
|
||||||
|
const rest=raw.view.slice('channel:'.length);
|
||||||
|
const [name,plStr]=rest.split('|');
|
||||||
|
const idx=library.findIndex(c=>c.name===name);
|
||||||
|
if(idx>=0){
|
||||||
|
activeChannelIdx=idx;
|
||||||
|
const pl=parseInt(plStr,10);
|
||||||
|
activePlaylistIdx=Number.isFinite(pl)?pl:null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function setContinue(){resetViewSelectors();showContinue=true;selected.clear();saveUiState();closeSidebar();renderSidebar();renderGrid()}
|
||||||
|
function setRecent(){resetViewSelectors();showRecent=true;selected.clear();saveUiState();closeSidebar();renderSidebar();renderGrid()}
|
||||||
|
function setFavourites(){resetViewSelectors();showFavourites=true;selected.clear();saveUiState();closeSidebar();renderSidebar();renderGrid()}
|
||||||
|
function setBookmarks(){resetViewSelectors();showBookmarks=true;selected.clear();saveUiState();closeSidebar();renderSidebar();renderGrid()}
|
||||||
|
function setWaiting(){resetViewSelectors();showWaiting=true;selected.clear();saveUiState();closeSidebar();renderSidebar();renderGrid()}
|
||||||
|
function setChannels(){resetViewSelectors();showChannels=true;selected.clear();saveUiState();closeSidebar();renderSidebar();renderGrid()}
|
||||||
|
function setView(ci,pi){resetViewSelectors();activeChannelIdx=ci;activePlaylistIdx=pi;selected.clear();saveUiState();closeSidebar();renderSidebar();renderGrid()}
|
||||||
|
function setMusic(){resetViewSelectors();showMusic=true;selected.clear();saveUiState();closeSidebar();renderSidebar();renderGrid()}
|
||||||
|
|
||||||
/* ── Grid ───────────────────────────────────────────────────────── */
|
/* ── Grid ───────────────────────────────────────────────────────── */
|
||||||
function currentVideos(){
|
function currentVideos(){
|
||||||
|
|
@ -1536,7 +1591,16 @@ async function loadSourceNotice(){
|
||||||
|
|
||||||
/* ── Init ───────────────────────────────────────────────────────── */
|
/* ── Init ───────────────────────────────────────────────────────── */
|
||||||
applyTheme(localStorage.getItem('theme')||'dark');
|
applyTheme(localStorage.getItem('theme')||'dark');
|
||||||
loadLibrary();
|
// Library load is async, but the sort + search inputs can be restored
|
||||||
|
// immediately. View selectors need the library array to be populated
|
||||||
|
// before we can resolve `channel:<name>` back to an index — do that
|
||||||
|
// after loadLibrary() resolves.
|
||||||
|
(async()=>{
|
||||||
|
await loadLibrary();
|
||||||
|
restoreUiState();
|
||||||
|
renderSidebar();
|
||||||
|
renderGrid();
|
||||||
|
})();
|
||||||
loadSourceNotice();
|
loadSourceNotice();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue