Add cache cap, playlist export/import, yt-dlp build flag

Cache cap: dropdown setting (no limit / 1/2/5/10 GB) stored in
settings.cacheCap (actual eviction handled by native side).
Export: downloads playlists as JSON via Blob. Import: file reader
merges new playlists by id. yt-dlp: feature gate 'embed-ytdlp'
(default on), skip with --no-default-features for faster debug builds.

Files:
- frontend/app.js: exportPlaylists/importPlaylists, cache cap select
- src-tauri/Cargo.toml: [features] embed-ytdlp
- src-tauri/src/main.rs: #[cfg(feature = embed-ytdlp)] guards
This commit is contained in:
Jonathan Sykes
2026-06-14 15:29:10 +08:00
parent c6b8dfceb8
commit d15d9021b7
7 changed files with 83 additions and 6 deletions

View File

@@ -850,10 +850,32 @@ async function renderSettings() {
<span>Storage used</span>
<span id="cacheTotal" class="set-stat">…</span>
</div>
<label class="set-row">
<span>
Cache size limit
<small>Auto-evict oldest files when cache exceeds this.</small>
</span>
<select id="cacheCapSelect" class="set-select">
<option value="0">No limit</option>
<option value="1073741824">1 GB</option>
<option value="2147483648">2 GB</option>
<option value="5368709120">5 GB</option>
<option value="10737418240">10 GB</option>
</select>
</label>
<div class="set-actions">
<button id="clearCacheBtn" class="btn danger">Clear all cached videos</button>
</div>
<div id="cacheList" class="cache-list"></div>
</div>
<div class="set-group">
<div class="set-group-title">Playlists</div>
<div class="set-row" style="border:none;gap:8px;flex-wrap:wrap">
<button id="exportBtn" class="btn">Export playlists</button>
<button id="importBtn" class="btn">Import playlists</button>
<input id="fileInput" type="file" accept=".json" style="display:none" />
</div>
</div>`;
c.appendChild(wrap);
@@ -930,6 +952,19 @@ async function renderSettings() {
const t = $('cacheTotal');
if (t) t.textContent = 'Offline cache not available in this build';
}
// ---- Cache cap ----
const capSel = $('cacheCapSelect');
capSel.value = String(data.settings.cacheCap || 0);
capSel.addEventListener('change', (e) => {
data.settings.cacheCap = parseInt(e.target.value) || 0;
persist();
});
// ---- Export / Import playlists ----
$('exportBtn').addEventListener('click', exportPlaylists);
$('importBtn').addEventListener('click', () => $('fileInput').click());
$('fileInput').addEventListener('change', importPlaylists);
}
function renderCard(v, index, list) {
@@ -1010,6 +1045,43 @@ function render() {
// ============================================================================
// Playlists
// ============================================================================
function exportPlaylists() {
const json = JSON.stringify(data.playlists, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `ytplayer-playlists-${new Date().toISOString().slice(0, 10)}.json`;
a.click();
URL.revokeObjectURL(url);
toast('Playlists exported');
}
function importPlaylists(e) {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (ev) => {
try {
const imported = JSON.parse(ev.target.result);
if (!Array.isArray(imported)) throw new Error('Invalid format');
// Merge: skip duplicates by id, add new ones
const existingIds = new Set(data.playlists.map((p) => p.id));
for (const pl of imported) {
if (pl.id && pl.name && Array.isArray(pl.videos) && !existingIds.has(pl.id)) {
data.playlists.push(pl);
existingIds.add(pl.id);
}
}
persist();
renderSidebar();
toast(`Imported ${imported.length} playlist(s)`);
} catch (err) {
toast('⚠ Failed to import: ' + err.message);
}
};
reader.readAsText(file);
e.target.value = ''; // allow re-import of same file
}
function openCardMenu(video) {
const inPlaylistView = view.type === 'playlist';
const body = document.createElement('div');