Add Favorites, storage drill-down, keyboard shortcuts, and What's New
- Favorites: star toggle on course rows, the course page, and a new dashboard card; rebased on rename/move, backed up/restored, and caught by stale-reference cleanup (also fixed a pre-existing gap where favorites.json wasn't in the backup file list). - Storage Usage: click a folder row to drill into its contents one level at a time, with a Back button. - Lesson player: added ,/. (speed step), [/] (prev/next lesson), and F (fullscreen) keyboard shortcuts. - Settings: a CHANGELOG.md-backed "What's New" list, since VERSION alone only ever shows the current build. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+35
-5
@@ -325,6 +325,12 @@
|
||||
.storage-row:first-of-type {
|
||||
border-top: none;
|
||||
}
|
||||
.storage-row-clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
.storage-row-clickable:hover {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
.storage-row-name {
|
||||
flex: 0 0 160px;
|
||||
font-weight: 600;
|
||||
@@ -662,6 +668,7 @@
|
||||
<button class="btn btn-secondary" id="storage-scan-btn" onclick="scanStorageUsage()">{{ icons.icon('refresh', 14) }} Scan Disk Usage</button>
|
||||
<span id="storage-status"></span>
|
||||
</div>
|
||||
<div id="storage-breadcrumb"></div>
|
||||
<div id="storage-results"></div>
|
||||
</div>
|
||||
|
||||
@@ -1675,15 +1682,20 @@
|
||||
}
|
||||
|
||||
// ---- Storage Usage ----
|
||||
function scanStorageUsage() {
|
||||
let storagePathStack = [];
|
||||
|
||||
function scanStorageUsage(path) {
|
||||
const btn = document.getElementById('storage-scan-btn');
|
||||
const status = document.getElementById('storage-status');
|
||||
const results = document.getElementById('storage-results');
|
||||
const breadcrumb = document.getElementById('storage-breadcrumb');
|
||||
if (!path) storagePathStack = [];
|
||||
btn.disabled = true;
|
||||
status.style.color = 'var(--text-muted)';
|
||||
status.textContent = 'Scanning… this reads every file, may take a moment.';
|
||||
results.innerHTML = '';
|
||||
fetch('/api/library/storage-usage')
|
||||
const url = path ? `/api/library/storage-usage?path=${encodeURIComponent(path)}` : '/api/library/storage-usage';
|
||||
fetch(url)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
btn.disabled = false;
|
||||
@@ -1692,6 +1704,9 @@
|
||||
status.textContent = data.error;
|
||||
return;
|
||||
}
|
||||
breadcrumb.innerHTML = !data.is_root
|
||||
? `<button class="btn btn-secondary btn-sm" onclick="storageGoBack()">${'←'} Back</button> <span style="color: var(--text-muted); font-size: 0.85em;">${escapeHtml(data.path)}</span>`
|
||||
: '';
|
||||
const entries = data.entries || [];
|
||||
if (!entries.length) {
|
||||
status.style.color = 'var(--text-muted)';
|
||||
@@ -1702,7 +1717,7 @@
|
||||
status.textContent = `Total: ${data.total_human}`;
|
||||
const maxBytes = Math.max(...entries.map(e => e.bytes), 1);
|
||||
results.innerHTML = entries.map(e => `
|
||||
<div class="storage-row">
|
||||
<div class="storage-row${e.is_dir ? ' storage-row-clickable' : ''}" ${e.is_dir ? `onclick="storageDrillDown('${e.path.replace(/'/g, "\\'")}')"` : ''}>
|
||||
<span class="storage-row-name">${escapeHtml(e.name)}</span>
|
||||
<div class="storage-bar-track"><div class="storage-bar-fill" style="width: ${Math.round((e.bytes / maxBytes) * 100)}%;"></div></div>
|
||||
<span class="storage-row-size">${escapeHtml(e.human)}</span>
|
||||
@@ -1716,6 +1731,21 @@
|
||||
});
|
||||
}
|
||||
|
||||
function storageDrillDown(path) {
|
||||
const status = document.getElementById('storage-status');
|
||||
const current = status.dataset.currentPath || '';
|
||||
storagePathStack.push(current);
|
||||
status.dataset.currentPath = path;
|
||||
scanStorageUsage(path);
|
||||
}
|
||||
|
||||
function storageGoBack() {
|
||||
const status = document.getElementById('storage-status');
|
||||
const prev = storagePathStack.pop() || '';
|
||||
status.dataset.currentPath = prev;
|
||||
scanStorageUsage(prev || undefined);
|
||||
}
|
||||
|
||||
// ---- Duplicate Courses ----
|
||||
function scanDuplicates() {
|
||||
const btn = document.getElementById('dup-scan-btn');
|
||||
@@ -1918,9 +1948,9 @@
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
btn.disabled = false;
|
||||
const categoryLabels = { hidden: 'Hidden', next_up: 'Next Up', recent_views: 'Recently Viewed' };
|
||||
const categoryLabels = { hidden: 'Hidden', next_up: 'Next Up', recent_views: 'Recently Viewed', favorites: 'Favorite' };
|
||||
const items = [];
|
||||
for (const category of ['hidden', 'next_up', 'recent_views']) {
|
||||
for (const category of ['hidden', 'next_up', 'recent_views', 'favorites']) {
|
||||
for (const entry of (data[category] || [])) {
|
||||
items.push({ path: entry.path, name: entry.name, category, label: categoryLabels[category] });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user