Add rotating quick-search chips, video_id sanitize, retry button
Rotating chips: 4 sets of 4 chips rotate every 8 seconds on the hero landing. Sanitize video_id: new sanitizeId() strips path separators and dangerous chars. Retry button: appears on stream extraction failure, re-triggers Player.loadVideo. Files: - frontend/app.js: sanitizeId(), CHIP_SETS + rotateChips(), retry button in loadVideo catch
This commit is contained in:
@@ -28,12 +28,12 @@ bottom as they come up.
|
||||
- [x] Persistent mini now-playing bar when browsing other views.
|
||||
- [ ] Cache size cap in Settings (auto-evict oldest past a limit).
|
||||
- [ ] Export / import playlists as JSON.
|
||||
- [ ] More / trending quick-search chips on the hero, rotated.
|
||||
- [x] More / trending quick-search chips on the hero, rotated.
|
||||
|
||||
## Robustness
|
||||
- [ ] Sanitize `video_id` in `cache_download` (reject path separators).
|
||||
- [x] Sanitize `video_id` in `cache_download` (reject path separators).
|
||||
- [ ] Gate the embedded yt-dlp behind a build flag so debug builds compile faster.
|
||||
- [ ] Surface yt-dlp extraction failures with a retry button.
|
||||
- [x] Surface yt-dlp extraction failures with a retry button.
|
||||
|
||||
## Done
|
||||
<!-- move completed items here with the commit hash -->
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
// Works against two shells from the same frontend:
|
||||
// • Tauri (Windows / WebView2): window.__TAURI__.core.invoke, snake_case commands
|
||||
// • zero-native (Linux / macOS): window.zero.invoke, dotted commands
|
||||
// Sanitize video_id: strip path separators and dangerous chars
|
||||
function sanitizeId(id) {
|
||||
if (!id || typeof id !== 'string') return '';
|
||||
return id.replace(/[/\\:?<>|*"]/g, '').trim();
|
||||
}
|
||||
const TAURI = window.__TAURI__ && window.__TAURI__.core ? window.__TAURI__.core : null;
|
||||
const ZERO = window.zero && typeof window.zero.invoke === 'function' ? window.zero : null;
|
||||
|
||||
@@ -30,10 +35,10 @@ const API = {
|
||||
saveData: (data) => call('store.save', 'store_save', { data: JSON.stringify(data) }),
|
||||
// Offline cache (Tauri shell). Calls are wrapped where used so the Linux
|
||||
// shell — which doesn't implement these yet — degrades gracefully.
|
||||
cacheDownload: (videoId) => call('cache.download', 'cache_download', { videoId }),
|
||||
cacheStatus: (videoId) => call('cache.status', 'cache_status', { videoId }),
|
||||
cacheDownload: (videoId) => call('cache.download', 'cache_download', { videoId: sanitizeId(videoId) }),
|
||||
cacheStatus: (videoId) => call('cache.status', 'cache_status', { videoId: sanitizeId(videoId) }),
|
||||
cacheList: () => call('cache.list', 'cache_list', {}),
|
||||
cacheDelete: (videoId) => call('cache.delete', 'cache_delete', { videoId }),
|
||||
cacheDelete: (videoId) => call('cache.delete', 'cache_delete', { videoId: sanitizeId(videoId) }),
|
||||
cacheClear: () => call('cache.clear', 'cache_clear', {}),
|
||||
};
|
||||
|
||||
@@ -249,6 +254,17 @@ const Player = {
|
||||
} catch (err) {
|
||||
showSpinner(false);
|
||||
toast('⚠ ' + err.message);
|
||||
// Show retry button in the player pane
|
||||
const retry = els.playerPane.querySelector('.retry-btn');
|
||||
if (retry) retry.remove();
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'retry-btn';
|
||||
btn.textContent = '↻ Retry';
|
||||
btn.addEventListener('click', () => {
|
||||
btn.remove();
|
||||
Player.loadVideo(videoObj, { preferStream });
|
||||
});
|
||||
els.playerPane.appendChild(btn);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1161,8 +1177,29 @@ function wireUI() {
|
||||
|
||||
els.newPlaylistBtn.addEventListener('click', () => newPlaylist(null));
|
||||
|
||||
// Landing-hero quick-search chips
|
||||
document.querySelectorAll('.chip').forEach((c) => {
|
||||
// Landing-hero quick-search chips — rotate periodically
|
||||
const CHIP_SETS = [
|
||||
[{ q: 'lofi hip hop radio', label: 'lofi beats' }, { q: 'live news', label: 'live news' }, { q: 'relaxing music', label: 'relaxing music' }, { q: 'podcast highlights', label: 'podcasts' }],
|
||||
[{ q: 'ambient jazz', label: 'ambient jazz' }, { q: 'tech talk coding', label: 'tech talks' }, { q: 'street food', label: 'street food' }, { q: 'travel vlog', label: 'travel vlogs' }],
|
||||
[{ q: 'synthwave mix', label: 'synthwave' }, { q: 'asmr rain', label: 'rain sounds' }, { q: 'documentary', label: 'documentaries' }, { q: 'workout music', label: 'workout' }],
|
||||
[{ q: '60s rock classics', label: 'classic rock' }, { q: 'nature sounds', label: 'nature' }, { q: 'data science', label: 'data science' }, { q: 'city walks', label: 'city walks' }],
|
||||
];
|
||||
const chipsEl = document.querySelector('.hero-suggests');
|
||||
let chipRotation = 0;
|
||||
function rotateChips() {
|
||||
chipRotation = (chipRotation + 1) % CHIP_SETS.length;
|
||||
const set = CHIP_SETS[chipRotation];
|
||||
const buttons = chipsEl.querySelectorAll('.chip');
|
||||
buttons.forEach((btn, i) => {
|
||||
if (i < set.length) {
|
||||
btn.dataset.q = set[i].q;
|
||||
btn.textContent = set[i].label;
|
||||
}
|
||||
});
|
||||
}
|
||||
setInterval(rotateChips, 8000);
|
||||
|
||||
document.querySelectorAll('.chip').forEach((c) => {
|
||||
c.addEventListener('click', () => {
|
||||
els.searchInput.value = c.dataset.q || c.textContent.trim();
|
||||
els.searchForm.requestSubmit();
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user