Stop resetting lesson progress on view, and add Notes Hub search

view_lesson() called update_lesson_progress() with no arguments on
every page load, which defaults completed=False and progress_seconds=0
and silently overwrote whatever was already saved - just opening an
already-watched lesson reset its progress/completed state unless the
client happened to report real values within the next 15 seconds.
Replaced with touch_lesson_accessed(), which only bumps last_accessed.

Also fixes the "Resume from Xs" feature this fed into: it read a value
that was never populated on this route, and even when given a real
one, checked activeMedia.duration synchronously before metadata had
loaded, so it silently never applied. Now waits for loadedmetadata (or
resolves immediately if already available) and defers to an explicit
note-timestamp jump (?t=) when both are present.

Notes Hub gets a live search box filtering by note text, lesson title,
or course name (client-side, no reload), with a "no notes match" state
- useful now that a single category folder can hold dozens of courses'
worth of timestamped notes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-22 22:00:35 -04:00
co-authored by Claude Sonnet 5
parent 393f78206a
commit 3fedf849d3
3 changed files with 75 additions and 17 deletions
+19 -11
View File
@@ -912,12 +912,27 @@
renderNotes();
if (activeMedia && INITIAL_SEEK_SECONDS !== null && INITIAL_SEEK_SECONDS !== undefined) {
const doInitialSeek = function() { activeMedia.currentTime = INITIAL_SEEK_SECONDS; };
// Seek once metadata is available (activeMedia.duration is NaN
// before that, so doing this synchronously on script load - as
// this used to - silently never applied). An explicit jump to
// a note's timestamp (?t=, see INITIAL_SEEK_SECONDS) takes
// priority over resuming general playback progress.
const savedProgressSeconds = {{ lesson_progress_seconds|default(0) }};
function applyInitialSeek() {
if (INITIAL_SEEK_SECONDS !== null && INITIAL_SEEK_SECONDS !== undefined) {
activeMedia.currentTime = INITIAL_SEEK_SECONDS;
} else if (savedProgressSeconds > 0 && savedProgressSeconds < activeMedia.duration - 30) {
activeMedia.currentTime = savedProgressSeconds;
showNotification(`Resumed from ${Math.floor(savedProgressSeconds / 60)}:${String(Math.floor(savedProgressSeconds % 60)).padStart(2, '0')}`);
}
}
if (activeMedia) {
if (activeMedia.readyState >= 1) {
doInitialSeek();
applyInitialSeek();
} else {
activeMedia.addEventListener('loadedmetadata', doInitialSeek, { once: true });
activeMedia.addEventListener('loadedmetadata', applyInitialSeek, { once: true });
}
}
@@ -1047,13 +1062,6 @@
saveProgress(activeMedia.currentTime, true);
markAsCompleted();
});
// Resume from saved position
const savedProgress = {{ lesson.progress_seconds|default(0) }};
if (savedProgress > 0 && savedProgress < activeMedia.duration - 30) {
activeMedia.currentTime = savedProgress;
showNotification(`Resumed from ${Math.floor(savedProgress / 60)}:${String(Math.floor(savedProgress % 60)).padStart(2, '0')}`);
}
}
function saveProgress(progressSeconds, completed = false) {
+32 -1
View File
@@ -117,6 +117,17 @@
text-align: center;
padding: 40px 20px;
}
.notes-search-input {
width: 100%;
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius);
padding: 10px 14px;
font-size: 0.95em;
font-family: var(--font-family);
margin-bottom: 20px;
}
.note-card {
background: var(--bg-secondary);
border-radius: var(--radius);
@@ -213,10 +224,14 @@
{% if not notes %}
<div class="empty-hint">No notes yet - jot something down on a lesson page and it'll show up here.</div>
{% else %}
<input type="text" id="notes-search-input" class="notes-search-input"
placeholder="Search notes, lessons, or courses…" oninput="filterNotes(this.value)">
<div id="notes-no-results-hint" class="empty-hint" style="display: none;">No notes match your search.</div>
{% endif %}
{% for note in notes %}
<div class="note-card">
<div class="note-card" data-search="{{ (note.lesson_title ~ ' ' ~ note.course_name ~ ' ' ~ note.text) | lower }}">
<div class="note-card-header">
<span class="note-card-icon">📝</span>
<div class="note-card-titles">
@@ -251,6 +266,22 @@
</div>
</footer>
<script>
function filterNotes(query) {
const q = query.trim().toLowerCase();
const cards = document.querySelectorAll('.note-card');
let visibleCount = 0;
cards.forEach(function(card) {
const match = !q || card.dataset.search.includes(q);
card.style.display = match ? '' : 'none';
if (match) visibleCount++;
});
const noResultsHint = document.getElementById('notes-no-results-hint');
if (noResultsHint) {
noResultsHint.style.display = (visibleCount === 0 && cards.length > 0) ? 'block' : 'none';
}
}
</script>
<script src="/static/theme.js"></script>
</body>
</html>