diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d5e6e0..329b097 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ +2026-08-26 13:41 UTC — Move sidebar left, add collapse toggle and remembered sections 2026-08-26 13:17 UTC — Add hover tooltips for truncated sidebar titles 2026-08-26 13:12 UTC — Add course outline sidebar to the lesson page 2026-08-25 12:25 UTC — Remove unreliable duplicate-lesson-file matching diff --git a/README.md b/README.md index 59d3b55..0584de0 100644 --- a/README.md +++ b/README.md @@ -225,12 +225,15 @@ silently stay blank instead of erroring. **Playback & progress** - Video/audio player with resize, playback-speed presets, and resume-from- last-position -- Course outline sidebar on the lesson page: every section and lesson in - the course (videos and documents alike - a standalone PDF/doc is its own - entry, same as a video), one click to jump anywhere without backing out - to the course page first. The current lesson's section opens - automatically and scrolls into view; sticky and independently +- Course outline sidebar on the lesson page (left side): every section and + lesson in the course (videos and documents alike - a standalone PDF/doc + is its own entry, same as a video), one click to jump anywhere without + backing out to the course page first. The current lesson's section + opens automatically and scrolls into view; sticky and independently scrollable on desktop, stacks below the player on narrow viewports. + Collapsible to a slim rail (remembered per device) for a full-width + player, and which sections you've manually expanded is remembered too, + so re-opening a course doesn't reset everything back to collapsed. Shares its rendering with the loaded-course dashboard view (`templates/ _course_tree.html`), so the two always look and behave the same. - Auto-play next lesson when one ends, with a cancelable few-second diff --git a/VERSION b/VERSION index 43dccd4..1f3250b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2026-08-26 13:17 UTC — add hover tooltips for truncated sidebar titles +2026-08-26 13:41 UTC — move sidebar left, add collapse toggle and remembered sections diff --git a/templates/_course_tree.html b/templates/_course_tree.html index d87372c..f5abeee 100644 --- a/templates/_course_tree.html +++ b/templates/_course_tree.html @@ -33,7 +33,7 @@ {% if node.children or node.lessons %} -
+
{% for child_name, child_node in node.children.items() %} {{ render_tree_node(child_node, course_path, current_lesson, depth + 1) }} {% endfor %} diff --git a/templates/course_dashboard.html b/templates/course_dashboard.html index b44909c..e7ec574 100644 --- a/templates/course_dashboard.html +++ b/templates/course_dashboard.html @@ -1360,29 +1360,49 @@ } function toggleTree(element) { - console.log('Toggle clicked:', element); - // Find the content div that comes after this header const content = element.nextElementSibling; const toggle = element.querySelector('.tree-toggle'); - - console.log('Content element:', content); - console.log('Toggle element:', toggle); - + if (content && content.classList.contains('tree-content')) { - const isExpanded = content.classList.contains('expanded'); - content.classList.toggle('expanded'); - - if (toggle) { - toggle.textContent = content.classList.contains('expanded') ? '▼' : '▶'; - } - - console.log('Toggled tree, expanded:', content.classList.contains('expanded')); - } else { - console.log('No content element found or not tree-content'); + const isExpanded = content.classList.toggle('expanded'); + if (toggle) toggle.textContent = isExpanded ? '▼' : '▶'; + if (content.dataset.sectionPath) saveSectionExpanded(content.dataset.sectionPath, isExpanded); } } + // Remembers which course sections a user has manually expanded + // (keyed by the section's absolute path, so it's stable across + // page loads and shared between this tree and the lesson page's + // sidebar - see _course_tree.html) so re-opening a course doesn't + // reset every section back to collapsed. + function getExpandedSectionPaths() { + try { + return new Set(JSON.parse(localStorage.getItem('offlineu-expanded-sections') || '[]')); + } catch (e) { + return new Set(); + } + } + + function saveSectionExpanded(path, expanded) { + const paths = getExpandedSectionPaths(); + if (expanded) paths.add(path); else paths.delete(path); + localStorage.setItem('offlineu-expanded-sections', JSON.stringify([...paths])); + } + + function restoreExpandedSections() { + const saved = getExpandedSectionPaths(); + if (!saved.size) return; + document.querySelectorAll('.tree-content[data-section-path]').forEach(function(content) { + if (!saved.has(content.dataset.sectionPath)) return; + content.classList.add('expanded'); + const header = content.previousElementSibling; + const toggle = header ? header.querySelector('.tree-toggle') : null; + if (toggle) toggle.textContent = '▼'; + }); + } + restoreExpandedSections(); + // Single-level drill-down: the Library browser shows one folder's // contents at a time (replacing the list, not nesting under it) and // tracks how we got here in libraryTrail so a breadcrumb can jump diff --git a/templates/lesson_view.html b/templates/lesson_view.html index 03fd7c2..a98ff3d 100644 --- a/templates/lesson_view.html +++ b/templates/lesson_view.html @@ -166,9 +166,43 @@ top: 20px; max-height: calc(100vh - 40px); overflow-y: auto; + order: -1; } .lesson-sidebar h3 { - margin-top: 0; + margin: 0; + } + .lesson-sidebar-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + } + .sidebar-toggle-btn { + background: var(--bg-tertiary); + border: none; + color: var(--text-primary); + border-radius: var(--radius); + width: 28px; + height: 28px; + flex-shrink: 0; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.9em; + transition: background 0.2s; + } + .sidebar-toggle-btn:hover { + background: var(--bg-tertiary-hover); + } + .lesson-sidebar.collapsed { + flex: 0 0 auto; + padding: 20px 8px; + overflow: hidden; + } + .lesson-sidebar.collapsed .lesson-sidebar-header h3, + .lesson-sidebar.collapsed .tree-container { + display: none; } @media (max-width: 900px) { .lesson-layout { @@ -179,6 +213,10 @@ width: 100%; position: static; max-height: 420px; + order: 0; + } + .lesson-sidebar.collapsed { + max-height: none; } } /* Course-outline sidebar tree - scoped under .lesson-sidebar so @@ -796,8 +834,11 @@
-
-

Course Outline

+
+
+

Course Outline

+ +
{{ tree.render_tree_node(course.root_node, course.path, lesson) }}
@@ -1199,27 +1240,84 @@ if (content && content.classList.contains('tree-content')) { const isExpanded = content.classList.toggle('expanded'); if (toggleBtn) toggleBtn.textContent = isExpanded ? '▼' : '▶'; + if (content.dataset.sectionPath) saveSectionExpanded(content.dataset.sectionPath, isExpanded); } } - // Sections default collapsed (same as the dashboard) - expand - // just the ancestor chain down to the current lesson so it's - // visible without the user having to hunt for it, and scroll - // it into view within the sidebar's own scroll area. + // Remembers which course sections a user has manually expanded + // (keyed by the section's absolute path, shared with the + // dashboard's own tree - see _course_tree.html) so re-opening + // a course doesn't reset every section back to collapsed. + function getExpandedSectionPaths() { + try { + return new Set(JSON.parse(localStorage.getItem('offlineu-expanded-sections') || '[]')); + } catch (e) { + return new Set(); + } + } + + function saveSectionExpanded(path, expanded) { + const paths = getExpandedSectionPaths(); + if (expanded) paths.add(path); else paths.delete(path); + localStorage.setItem('offlineu-expanded-sections', JSON.stringify([...paths])); + } + + function expandSection(content) { + content.classList.add('expanded'); + const header = content.previousElementSibling; + const toggleBtn = header ? header.querySelector('.tree-toggle') : null; + if (toggleBtn) toggleBtn.textContent = '▼'; + } + + (function restoreExpandedSections() { + const saved = getExpandedSectionPaths(); + if (!saved.size) return; + document.querySelectorAll('.lesson-sidebar .tree-content[data-section-path]').forEach(function(content) { + if (saved.has(content.dataset.sectionPath)) expandSection(content); + }); + })(); + + // Sections default collapsed - additionally expand the ancestor + // chain down to the current lesson (even if not previously + // remembered) so it's visible without hunting for it, and + // scroll it into view within the sidebar's own scroll area. + // Persisted the same as a manual expand, so it stays open on + // later visits too. (function revealCurrentLessonInSidebar() { const current = document.querySelector('.lesson-sidebar .lesson-item.current'); if (!current) return; let content = current.closest('.tree-content'); while (content) { - content.classList.add('expanded'); - const header = content.previousElementSibling; - const toggleBtn = header ? header.querySelector('.tree-toggle') : null; - if (toggleBtn) toggleBtn.textContent = '▼'; + expandSection(content); + if (content.dataset.sectionPath) saveSectionExpanded(content.dataset.sectionPath, true); content = content.parentElement ? content.parentElement.closest('.tree-content') : null; } current.scrollIntoView({ block: 'center' }); })(); + // ---- Sidebar collapse (client-only, remembered per device) ---- + function toggleSidebar() { + const sidebar = document.getElementById('lesson-sidebar'); + const btn = document.getElementById('sidebar-toggle-btn'); + const collapsed = sidebar.classList.toggle('collapsed'); + if (btn) { + btn.textContent = collapsed ? '▶' : '◀'; + btn.title = collapsed ? 'Expand sidebar' : 'Collapse sidebar'; + } + localStorage.setItem('offlineu-lesson-sidebar-collapsed', collapsed ? '1' : '0'); + } + + (function restoreSidebarCollapsed() { + if (localStorage.getItem('offlineu-lesson-sidebar-collapsed') !== '1') return; + const sidebar = document.getElementById('lesson-sidebar'); + const btn = document.getElementById('sidebar-toggle-btn'); + if (sidebar) sidebar.classList.add('collapsed'); + if (btn) { + btn.textContent = '▶'; + btn.title = 'Expand sidebar'; + } + })(); + renderNotes(); // Seek once metadata is available (activeMedia.duration is NaN