Add section lesson list, section progress, and library grid view
Three comparison-driven upgrades against typical course/media platforms: - Lesson page gets a collapsible "Lessons in this section" list so you can jump between siblings without leaving the page, instead of only global Prev/Next buttons. find_lesson_in_tree() now also returns the DirectoryNode that owns the lesson (its .lessons list is the sibling set) since DirectoryNode has no parent pointer; a new get_section_lessons() resolves each sibling's progress the same way view_lesson() already does for the current lesson. - Course tree section headers show "X/Y watched" instead of a flat item count, via DynamicCourseParser._calculate_completion_stats (already computed recursive completion for any node, just never called per-section) exposed as a Jinja global. - Library browser gets a list/grid toggle for browsing large category folders by poster-style thumbnail instead of a 32px-icon list. Client-side only, persisted via localStorage, defaults to the existing list view; both folder browsing and search results already funnel through the same render function so grid mode covers both. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+53
-9
@@ -496,6 +496,13 @@ class DynamicCourseParser:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Exposed to templates so a section header can show "X/Y watched" for any
|
||||||
|
# DirectoryNode without a separate per-node data-loading pass - the course
|
||||||
|
# tree is walked once per render regardless, and this method already
|
||||||
|
# recurses on whatever node it's given.
|
||||||
|
app.jinja_env.globals['section_stats'] = DynamicCourseParser._calculate_completion_stats
|
||||||
|
|
||||||
|
|
||||||
def _has_direct_media(directory: Path) -> bool:
|
def _has_direct_media(directory: Path) -> bool:
|
||||||
"""Check whether a directory contains media files directly (not recursively)"""
|
"""Check whether a directory contains media files directly (not recursively)"""
|
||||||
try:
|
try:
|
||||||
@@ -2331,8 +2338,8 @@ def view_lesson(lesson_path: str):
|
|||||||
if not current_course:
|
if not current_course:
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
# Find the lesson in the tree
|
# Find the lesson in the tree, and the section (DirectoryNode) it belongs to
|
||||||
lesson = find_lesson_in_tree(current_course.root_node, lesson_path)
|
lesson, section_node = find_lesson_in_tree(current_course.root_node, lesson_path)
|
||||||
|
|
||||||
if not lesson:
|
if not lesson:
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for('index'))
|
||||||
@@ -2380,6 +2387,7 @@ def view_lesson(lesson_path: str):
|
|||||||
initial_seek_seconds=seek_seconds,
|
initial_seek_seconds=seek_seconds,
|
||||||
outline_topic_id=lesson_progress.get('outline_topic_id', ''),
|
outline_topic_id=lesson_progress.get('outline_topic_id', ''),
|
||||||
outline_topic_name=lesson_progress.get('outline_topic_name', ''),
|
outline_topic_name=lesson_progress.get('outline_topic_name', ''),
|
||||||
|
section_lessons=get_section_lessons(current_course, section_node, lesson),
|
||||||
prev_lesson=prev_lesson,
|
prev_lesson=prev_lesson,
|
||||||
next_lesson=next_lesson)
|
next_lesson=next_lesson)
|
||||||
|
|
||||||
@@ -2397,8 +2405,14 @@ def get_lesson_url(lesson: Lesson, course_path: str) -> str:
|
|||||||
return lesson_url
|
return lesson_url
|
||||||
|
|
||||||
|
|
||||||
def find_lesson_in_tree(node: DirectoryNode, target_path: str) -> Optional[Lesson]:
|
def find_lesson_in_tree(node: DirectoryNode, target_path: str) -> Tuple[Optional[Lesson], Optional[DirectoryNode]]:
|
||||||
"""Find a lesson in the tree by path"""
|
"""
|
||||||
|
Find a lesson in the tree by path, along with the DirectoryNode that
|
||||||
|
directly owns it - DirectoryNode has no parent pointer, so this is the
|
||||||
|
only way to get "the section this lesson belongs to" (its `.lessons`
|
||||||
|
list is exactly that section's sibling set, used for the lesson page's
|
||||||
|
"up next in this section" list).
|
||||||
|
"""
|
||||||
# Check lessons in current node
|
# Check lessons in current node
|
||||||
for lesson in node.lessons:
|
for lesson in node.lessons:
|
||||||
lesson_url = get_lesson_url(lesson, current_course.path)
|
lesson_url = get_lesson_url(lesson, current_course.path)
|
||||||
@@ -2415,15 +2429,45 @@ def find_lesson_in_tree(node: DirectoryNode, target_path: str) -> Optional[Lesso
|
|||||||
if (lesson_url == target_path or
|
if (lesson_url == target_path or
|
||||||
lesson_file_path == target_path or
|
lesson_file_path == target_path or
|
||||||
lesson_path_with_title == target_path):
|
lesson_path_with_title == target_path):
|
||||||
return lesson
|
return lesson, node
|
||||||
|
|
||||||
# Recursively search children
|
# Recursively search children
|
||||||
for child in node.children.values():
|
for child in node.children.values():
|
||||||
result = find_lesson_in_tree(child, target_path)
|
result_lesson, result_node = find_lesson_in_tree(child, target_path)
|
||||||
if result:
|
if result_lesson:
|
||||||
return result
|
return result_lesson, result_node
|
||||||
|
|
||||||
return None
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def get_section_lessons(course: Course, section_node: DirectoryNode, current_lesson: Lesson) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
The sibling lessons in current_lesson's own section, for the lesson
|
||||||
|
page's "up next in this section" list. view_lesson() isn't on the
|
||||||
|
apply_progress_to_tree() code path, so - like that route already does
|
||||||
|
for the current lesson's own notes/progress - this reads progress
|
||||||
|
directly from the progress file rather than relying on Lesson fields.
|
||||||
|
"""
|
||||||
|
progress = ProgressTracker.load_progress(course)
|
||||||
|
siblings = []
|
||||||
|
for sibling in section_node.lessons:
|
||||||
|
key = _resolve_lesson_progress_key(course, sibling, progress)
|
||||||
|
entry = progress.get(key, {}) if key else {}
|
||||||
|
completed = entry.get('completed', False)
|
||||||
|
progress_seconds = entry.get('progress_seconds', 0)
|
||||||
|
duration_seconds = entry.get('duration_seconds', 0)
|
||||||
|
percent_watched = 100 if completed else (
|
||||||
|
round(100 * progress_seconds / duration_seconds) if duration_seconds and progress_seconds else 0
|
||||||
|
)
|
||||||
|
siblings.append({
|
||||||
|
'title': sibling.title,
|
||||||
|
'url': get_lesson_url(sibling, course.path),
|
||||||
|
'lesson_type': sibling.lesson_type,
|
||||||
|
'completed': completed,
|
||||||
|
'percent_watched': percent_watched,
|
||||||
|
'is_current': sibling is current_lesson,
|
||||||
|
})
|
||||||
|
return siblings
|
||||||
|
|
||||||
|
|
||||||
def get_all_lessons(node: DirectoryNode) -> List[Tuple[str, Lesson]]:
|
def get_all_lessons(node: DirectoryNode) -> List[Tuple[str, Lesson]]:
|
||||||
|
|||||||
@@ -618,6 +618,82 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.library-view-toggle {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.view-toggle-btn {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
color: var(--text-muted);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.view-toggle-btn:first-child {
|
||||||
|
border-radius: var(--radius) 0 0 var(--radius);
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
.view-toggle-btn:last-child {
|
||||||
|
border-radius: 0 var(--radius) var(--radius) 0;
|
||||||
|
}
|
||||||
|
.view-toggle-btn.active {
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
.library-grid-card {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s, transform 0.2s;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.library-grid-card:hover {
|
||||||
|
background: var(--bg-tertiary-hover);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
.grid-card-thumb-wrap {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 2.4em;
|
||||||
|
}
|
||||||
|
.grid-card-thumb {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.grid-card-name {
|
||||||
|
font-size: 0.9em;
|
||||||
|
font-weight: 500;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
.grid-card-meta {
|
||||||
|
font-size: 0.75em;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
.transcript-search-toggle {
|
.transcript-search-toggle {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -840,13 +916,14 @@
|
|||||||
|
|
||||||
<div class="tree-container">
|
<div class="tree-container">
|
||||||
{% macro render_tree_node(node, depth=0) %}
|
{% macro render_tree_node(node, depth=0) %}
|
||||||
|
{% set stats = section_stats(node) %}
|
||||||
<div class="tree-item">
|
<div class="tree-item">
|
||||||
<div class="tree-header directory" onclick="toggleTree(this)">
|
<div class="tree-header directory" onclick="toggleTree(this)">
|
||||||
<div class="tree-title">
|
<div class="tree-title">
|
||||||
<span class="tree-icon">📁</span>
|
<span class="tree-icon">📁</span>
|
||||||
<span class="tree-name">{{ node.name }}</span>
|
<span class="tree-name">{{ node.name }}</span>
|
||||||
<span class="tree-stats">
|
<span class="tree-stats">
|
||||||
{{ (node.children|length + node.lessons|length) }} items
|
{% if stats.total_lessons %}{{ stats.completed_lessons }}/{{ stats.total_lessons }} watched{% else %}Empty{% endif %}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{% if node.children or node.lessons %}
|
{% if node.children or node.lessons %}
|
||||||
@@ -1067,6 +1144,12 @@
|
|||||||
<option value="name-asc">Name (A→Z)</option>
|
<option value="name-asc">Name (A→Z)</option>
|
||||||
<option value="name-desc">Name (Z→A)</option>
|
<option value="name-desc">Name (Z→A)</option>
|
||||||
</select>
|
</select>
|
||||||
|
<div class="library-view-toggle">
|
||||||
|
<button type="button" class="view-toggle-btn" id="library-view-list-btn"
|
||||||
|
onclick="setLibraryViewMode('list')" title="List view">☰</button>
|
||||||
|
<button type="button" class="view-toggle-btn" id="library-view-grid-btn"
|
||||||
|
onclick="setLibraryViewMode('grid')" title="Grid view">▦</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<label class="transcript-search-toggle">
|
<label class="transcript-search-toggle">
|
||||||
<input type="checkbox" id="search-transcripts-toggle" onchange="handleLibrarySearchInput(document.getElementById('library-search-input').value)">
|
<input type="checkbox" id="search-transcripts-toggle" onchange="handleLibrarySearchInput(document.getElementById('library-search-input').value)">
|
||||||
@@ -1261,6 +1344,56 @@
|
|||||||
return span;
|
return span;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Same idea as courseFallbackIcon, but sized for a grid card's
|
||||||
|
// square thumbnail area rather than an inline list-row icon.
|
||||||
|
function gridFallbackIcon(emoji) {
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.textContent = emoji;
|
||||||
|
return span;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Library list/grid view toggle (client-only, remembered per device) ----
|
||||||
|
let libraryViewMode = localStorage.getItem('offlineu-library-view-mode') || 'list';
|
||||||
|
|
||||||
|
function updateViewModeButtons() {
|
||||||
|
const listBtn = document.getElementById('library-view-list-btn');
|
||||||
|
const gridBtn = document.getElementById('library-view-grid-btn');
|
||||||
|
if (listBtn) listBtn.classList.toggle('active', libraryViewMode === 'list');
|
||||||
|
if (gridBtn) gridBtn.classList.toggle('active', libraryViewMode === 'grid');
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLibraryViewMode(mode) {
|
||||||
|
libraryViewMode = mode;
|
||||||
|
localStorage.setItem('offlineu-library-view-mode', mode);
|
||||||
|
updateViewModeButtons();
|
||||||
|
if (lastLibraryItems) renderItemRows(sortLibraryItems(lastLibraryItems));
|
||||||
|
}
|
||||||
|
|
||||||
|
function courseCardHtml(item) {
|
||||||
|
const safePath = item.path.replace(/'/g, "\\'");
|
||||||
|
const iconHtml = item.has_thumbnail
|
||||||
|
? `<img class="grid-card-thumb" src="/library/thumbnail?path=${encodeURIComponent(item.path)}" alt="" onerror="this.replaceWith(gridFallbackIcon('🎓'))">`
|
||||||
|
: `<span>🎓</span>`;
|
||||||
|
return `
|
||||||
|
<div class="library-grid-card" onclick="loadCoursePath('${safePath}')">
|
||||||
|
<div class="grid-card-thumb-wrap">${iconHtml}</div>
|
||||||
|
<div class="grid-card-name">${item.name}</div>
|
||||||
|
<div class="grid-card-meta">${item.media_files} media file${item.media_files === 1 ? '' : 's'}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function directoryCardHtml(item) {
|
||||||
|
const safePath = item.path.replace(/'/g, "\\'");
|
||||||
|
const safeName = item.name.replace(/'/g, "\\'");
|
||||||
|
return `
|
||||||
|
<div class="library-grid-card" onclick="enterLibraryDir('${safePath}', '${safeName}')">
|
||||||
|
<div class="grid-card-thumb-wrap">📁</div>
|
||||||
|
<div class="grid-card-name">${item.name}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
function courseRowHtml(item) {
|
function courseRowHtml(item) {
|
||||||
const safePath = item.path.replace(/'/g, "\\'");
|
const safePath = item.path.replace(/'/g, "\\'");
|
||||||
const iconHtml = item.has_thumbnail
|
const iconHtml = item.has_thumbnail
|
||||||
@@ -1295,8 +1428,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderItemRows(items) {
|
function renderItemRows(items) {
|
||||||
document.getElementById('library-groups').innerHTML =
|
const container = document.getElementById('library-groups');
|
||||||
items.map(item => item.type === 'course' ? courseRowHtml(item) : directoryRowHtml(item)).join('');
|
container.classList.toggle('library-grid', libraryViewMode === 'grid');
|
||||||
|
container.innerHTML = libraryViewMode === 'grid'
|
||||||
|
? items.map(item => item.type === 'course' ? courseCardHtml(item) : directoryCardHtml(item)).join('')
|
||||||
|
: items.map(item => item.type === 'course' ? courseRowHtml(item) : directoryRowHtml(item)).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortLibraryItems(items) {
|
function sortLibraryItems(items) {
|
||||||
@@ -1512,6 +1648,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', loadLibrary);
|
document.addEventListener('DOMContentLoaded', loadLibrary);
|
||||||
|
document.addEventListener('DOMContentLoaded', updateViewModeButtons);
|
||||||
document.addEventListener('DOMContentLoaded', initCollapsibleCards);
|
document.addEventListener('DOMContentLoaded', initCollapsibleCards);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -188,6 +188,65 @@
|
|||||||
.outline-section {
|
.outline-section {
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
}
|
}
|
||||||
|
.section-lessons {
|
||||||
|
margin: 20px 0;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
}
|
||||||
|
.section-lessons-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.section-lessons-count {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
.section-lessons-list {
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 0.25s ease;
|
||||||
|
}
|
||||||
|
.section-lessons-list.expanded {
|
||||||
|
max-height: 2000px;
|
||||||
|
}
|
||||||
|
.section-lesson-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 8px 14px;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--text-primary);
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
a.section-lesson-row:hover {
|
||||||
|
background: var(--bg-tertiary-hover);
|
||||||
|
}
|
||||||
|
.section-lesson-row.current {
|
||||||
|
background: var(--bg-tertiary-hover);
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.section-lesson-title {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.section-lesson-status {
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: var(--text-muted);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.section-lesson-status.completed {
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
.note-status {
|
.note-status {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
@@ -502,6 +561,43 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if section_lessons and section_lessons|length > 1 %}
|
||||||
|
<div class="section-lessons">
|
||||||
|
<div class="section-lessons-header" onclick="toggleSectionLessons()">
|
||||||
|
<span id="section-lessons-arrow">▾</span>
|
||||||
|
<span>Lessons in this section</span>
|
||||||
|
<span class="section-lessons-count">({{ section_lessons|length }})</span>
|
||||||
|
</div>
|
||||||
|
<div class="section-lessons-list expanded" id="section-lessons-list-wrap">
|
||||||
|
{% for item in section_lessons %}
|
||||||
|
{% if item.is_current %}
|
||||||
|
<span class="section-lesson-row current">
|
||||||
|
{% else %}
|
||||||
|
<a class="section-lesson-row" href="/lesson/{{ item.url }}">
|
||||||
|
{% endif %}
|
||||||
|
<span class="section-lesson-icon">
|
||||||
|
{% if item.lesson_type == 'video' %}🎥
|
||||||
|
{% elif item.lesson_type == 'audio' %}🎵
|
||||||
|
{% elif item.lesson_type == 'quiz' %}📝
|
||||||
|
{% elif item.lesson_type == 'mixed' %}📦
|
||||||
|
{% else %}📄{% endif %}
|
||||||
|
</span>
|
||||||
|
<span class="section-lesson-title">{{ item.title }}</span>
|
||||||
|
{% if item.completed %}
|
||||||
|
<span class="section-lesson-status completed">✓</span>
|
||||||
|
{% elif item.percent_watched %}
|
||||||
|
<span class="section-lesson-status">{{ item.percent_watched }}%</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if item.is_current %}
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="outline-section">
|
<div class="outline-section">
|
||||||
<h3>Push to Outline</h3>
|
<h3>Push to Outline</h3>
|
||||||
<div class="outline-topic-row">
|
<div class="outline-topic-row">
|
||||||
@@ -904,6 +1000,27 @@
|
|||||||
if (notesPanelToggleArrow) notesPanelToggleArrow.textContent = '▴';
|
if (notesPanelToggleArrow) notesPanelToggleArrow.textContent = '▴';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// "Lessons in this section" defaults open (unlike notes, this
|
||||||
|
// is primary navigation) - only collapse it if the user
|
||||||
|
// explicitly did so on a previous visit.
|
||||||
|
function toggleSectionLessons() {
|
||||||
|
const listWrap = document.getElementById('section-lessons-list-wrap');
|
||||||
|
const arrow = document.getElementById('section-lessons-arrow');
|
||||||
|
if (!listWrap) return;
|
||||||
|
const expanded = listWrap.classList.toggle('expanded');
|
||||||
|
if (arrow) arrow.textContent = expanded ? '▾' : '▸';
|
||||||
|
localStorage.setItem('offlineu-section-lessons-expanded', expanded ? '1' : '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
const listWrap = document.getElementById('section-lessons-list-wrap');
|
||||||
|
const arrow = document.getElementById('section-lessons-arrow');
|
||||||
|
if (listWrap && localStorage.getItem('offlineu-section-lessons-expanded') === '0') {
|
||||||
|
listWrap.classList.remove('expanded');
|
||||||
|
if (arrow) arrow.textContent = '▸';
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
function openQuickCapture() {
|
function openQuickCapture() {
|
||||||
if (activeMedia && !activeMedia.paused) activeMedia.pause();
|
if (activeMedia && !activeMedia.paused) activeMedia.pause();
|
||||||
updateQuickCaptureBadge();
|
updateQuickCaptureBadge();
|
||||||
|
|||||||
Reference in New Issue
Block a user