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:
2026-08-24 19:23:40 -04:00
co-authored by Claude Sonnet 5
parent 1fbf0c9312
commit fc83876abe
10 changed files with 404 additions and 39 deletions
+47
View File
@@ -1291,6 +1291,18 @@
return tag === 'TEXTAREA' || tag === 'INPUT' || tag === 'SELECT' || el.isContentEditable;
}
const PLAYBACK_SPEEDS = ['0.75', '1', '1.25', '1.5', '1.75', '2'];
function stepPlaybackSpeed(direction) {
if (!activeMedia) return;
let idx = PLAYBACK_SPEEDS.findIndex(s => parseFloat(s) === activeMedia.playbackRate);
if (idx === -1) idx = PLAYBACK_SPEEDS.indexOf('1');
idx = Math.max(0, Math.min(PLAYBACK_SPEEDS.length - 1, idx + direction));
const speed = PLAYBACK_SPEEDS[idx];
setPlaybackSpeed(speed);
showNotification(`Speed: ${speed}x`);
}
document.addEventListener('keydown', function(e) {
if (isTypingTarget(e.target)) return;
if ((e.key === 'n' || e.key === 'N') && !e.ctrlKey && !e.altKey && !e.metaKey) {
@@ -1298,6 +1310,33 @@
openQuickCapture();
return;
}
if (!e.ctrlKey && !e.altKey && !e.metaKey) {
switch(e.key) {
case '[':
{% if prev_lesson %}
e.preventDefault();
window.location.href = '/lesson/{{ prev_lesson }}';
{% endif %}
break;
case ']':
{% if next_lesson %}
e.preventDefault();
window.location.href = '/lesson/{{ next_lesson }}';
{% endif %}
break;
case 'f':
case 'F':
if (video) {
e.preventDefault();
if (document.fullscreenElement) {
document.exitFullscreen();
} else {
video.requestFullscreen();
}
}
break;
}
}
if (activeMedia && !e.ctrlKey && !e.altKey && !e.metaKey) {
switch(e.key) {
case ' ':
@@ -1326,6 +1365,14 @@
activeMedia.volume = Math.max(0, activeMedia.volume - 0.1);
showNotification(`Volume: ${Math.round(activeMedia.volume * 100)}%`);
break;
case ',':
e.preventDefault();
stepPlaybackSpeed(-1);
break;
case '.':
e.preventDefault();
stepPlaybackSpeed(1);
break;
}
}
});