Move sidebar left, add collapse toggle and remembered sections
- Course outline sidebar now sits on the left of the player instead of the right (mobile stacking order unchanged - video still first). - Collapsible to a slim rail via a toggle button, remembered per device, for a full-width player when wanted. - Which sections a user has manually expanded is now remembered (keyed by the section's path) and shared between the lesson-page sidebar and the dashboard's course view, so re-opening a course doesn't reset everything back to collapsed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
</div>
|
||||
|
||||
{% if node.children or node.lessons %}
|
||||
<div class="tree-content">
|
||||
<div class="tree-content" data-section-path="{{ node.path }}">
|
||||
{% for child_name, child_node in node.children.items() %}
|
||||
{{ render_tree_node(child_node, course_path, current_lesson, depth + 1) }}
|
||||
{% endfor %}
|
||||
|
||||
@@ -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
|
||||
|
||||
+109
-11
@@ -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 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container lesson-sidebar">
|
||||
<h3>Course Outline</h3>
|
||||
<div class="container lesson-sidebar" id="lesson-sidebar">
|
||||
<div class="lesson-sidebar-header">
|
||||
<h3>Course Outline</h3>
|
||||
<button type="button" class="sidebar-toggle-btn" id="sidebar-toggle-btn" onclick="toggleSidebar()" title="Collapse sidebar">◀</button>
|
||||
</div>
|
||||
<div class="tree-container">
|
||||
{{ tree.render_tree_node(course.root_node, course.path, lesson) }}
|
||||
</div>
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user