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: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-26 13:12 UTC — Add course outline sidebar to the lesson page
|
||||||
2026-08-25 12:25 UTC — Remove unreliable duplicate-lesson-file matching
|
2026-08-25 12:25 UTC — Remove unreliable duplicate-lesson-file matching
|
||||||
|
|||||||
@@ -225,12 +225,15 @@ silently stay blank instead of erroring.
|
|||||||
**Playback & progress**
|
**Playback & progress**
|
||||||
- Video/audio player with resize, playback-speed presets, and resume-from-
|
- Video/audio player with resize, playback-speed presets, and resume-from-
|
||||||
last-position
|
last-position
|
||||||
- Course outline sidebar on the lesson page: every section and lesson in
|
- Course outline sidebar on the lesson page (left side): every section and
|
||||||
the course (videos and documents alike - a standalone PDF/doc is its own
|
lesson in the course (videos and documents alike - a standalone PDF/doc
|
||||||
entry, same as a video), one click to jump anywhere without backing out
|
is its own entry, same as a video), one click to jump anywhere without
|
||||||
to the course page first. The current lesson's section opens
|
backing out to the course page first. The current lesson's section
|
||||||
automatically and scrolls into view; sticky and independently
|
opens automatically and scrolls into view; sticky and independently
|
||||||
scrollable on desktop, stacks below the player on narrow viewports.
|
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/
|
Shares its rendering with the loaded-course dashboard view (`templates/
|
||||||
_course_tree.html`), so the two always look and behave the same.
|
_course_tree.html`), so the two always look and behave the same.
|
||||||
- Auto-play next lesson when one ends, with a cancelable few-second
|
- 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>
|
</div>
|
||||||
|
|
||||||
{% if node.children or node.lessons %}
|
{% 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() %}
|
{% for child_name, child_node in node.children.items() %}
|
||||||
{{ render_tree_node(child_node, course_path, current_lesson, depth + 1) }}
|
{{ render_tree_node(child_node, course_path, current_lesson, depth + 1) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -1360,29 +1360,49 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggleTree(element) {
|
function toggleTree(element) {
|
||||||
console.log('Toggle clicked:', element);
|
|
||||||
|
|
||||||
// Find the content div that comes after this header
|
// Find the content div that comes after this header
|
||||||
const content = element.nextElementSibling;
|
const content = element.nextElementSibling;
|
||||||
const toggle = element.querySelector('.tree-toggle');
|
const toggle = element.querySelector('.tree-toggle');
|
||||||
|
|
||||||
console.log('Content element:', content);
|
|
||||||
console.log('Toggle element:', toggle);
|
|
||||||
|
|
||||||
if (content && content.classList.contains('tree-content')) {
|
if (content && content.classList.contains('tree-content')) {
|
||||||
const isExpanded = content.classList.contains('expanded');
|
const isExpanded = content.classList.toggle('expanded');
|
||||||
content.classList.toggle('expanded');
|
if (toggle) toggle.textContent = isExpanded ? '▼' : '▶';
|
||||||
|
if (content.dataset.sectionPath) saveSectionExpanded(content.dataset.sectionPath, isExpanded);
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// Single-level drill-down: the Library browser shows one folder's
|
||||||
// contents at a time (replacing the list, not nesting under it) and
|
// contents at a time (replacing the list, not nesting under it) and
|
||||||
// tracks how we got here in libraryTrail so a breadcrumb can jump
|
// tracks how we got here in libraryTrail so a breadcrumb can jump
|
||||||
|
|||||||
+109
-11
@@ -166,9 +166,43 @@
|
|||||||
top: 20px;
|
top: 20px;
|
||||||
max-height: calc(100vh - 40px);
|
max-height: calc(100vh - 40px);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
order: -1;
|
||||||
}
|
}
|
||||||
.lesson-sidebar h3 {
|
.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) {
|
@media (max-width: 900px) {
|
||||||
.lesson-layout {
|
.lesson-layout {
|
||||||
@@ -179,6 +213,10 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
position: static;
|
position: static;
|
||||||
max-height: 420px;
|
max-height: 420px;
|
||||||
|
order: 0;
|
||||||
|
}
|
||||||
|
.lesson-sidebar.collapsed {
|
||||||
|
max-height: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* Course-outline sidebar tree - scoped under .lesson-sidebar so
|
/* Course-outline sidebar tree - scoped under .lesson-sidebar so
|
||||||
@@ -796,8 +834,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container lesson-sidebar">
|
<div class="container lesson-sidebar" id="lesson-sidebar">
|
||||||
<h3>Course Outline</h3>
|
<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">
|
<div class="tree-container">
|
||||||
{{ tree.render_tree_node(course.root_node, course.path, lesson) }}
|
{{ tree.render_tree_node(course.root_node, course.path, lesson) }}
|
||||||
</div>
|
</div>
|
||||||
@@ -1199,27 +1240,84 @@
|
|||||||
if (content && content.classList.contains('tree-content')) {
|
if (content && content.classList.contains('tree-content')) {
|
||||||
const isExpanded = content.classList.toggle('expanded');
|
const isExpanded = content.classList.toggle('expanded');
|
||||||
if (toggleBtn) toggleBtn.textContent = isExpanded ? '▼' : '▶';
|
if (toggleBtn) toggleBtn.textContent = isExpanded ? '▼' : '▶';
|
||||||
|
if (content.dataset.sectionPath) saveSectionExpanded(content.dataset.sectionPath, isExpanded);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sections default collapsed (same as the dashboard) - expand
|
// Remembers which course sections a user has manually expanded
|
||||||
// just the ancestor chain down to the current lesson so it's
|
// (keyed by the section's absolute path, shared with the
|
||||||
// visible without the user having to hunt for it, and scroll
|
// dashboard's own tree - see _course_tree.html) so re-opening
|
||||||
// it into view within the sidebar's own scroll area.
|
// 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() {
|
(function revealCurrentLessonInSidebar() {
|
||||||
const current = document.querySelector('.lesson-sidebar .lesson-item.current');
|
const current = document.querySelector('.lesson-sidebar .lesson-item.current');
|
||||||
if (!current) return;
|
if (!current) return;
|
||||||
let content = current.closest('.tree-content');
|
let content = current.closest('.tree-content');
|
||||||
while (content) {
|
while (content) {
|
||||||
content.classList.add('expanded');
|
expandSection(content);
|
||||||
const header = content.previousElementSibling;
|
if (content.dataset.sectionPath) saveSectionExpanded(content.dataset.sectionPath, true);
|
||||||
const toggleBtn = header ? header.querySelector('.tree-toggle') : null;
|
|
||||||
if (toggleBtn) toggleBtn.textContent = '▼';
|
|
||||||
content = content.parentElement ? content.parentElement.closest('.tree-content') : null;
|
content = content.parentElement ? content.parentElement.closest('.tree-content') : null;
|
||||||
}
|
}
|
||||||
current.scrollIntoView({ block: 'center' });
|
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();
|
renderNotes();
|
||||||
|
|
||||||
// Seek once metadata is available (activeMedia.duration is NaN
|
// Seek once metadata is available (activeMedia.duration is NaN
|
||||||
|
|||||||
Reference in New Issue
Block a user