Replace all OS-rendered emoji with a consistent SVG icon set

Add a shared Jinja icon macro (templates/_icons.html) with 27 Feather-style
stroke SVGs and import it across every live template - tab bars, buttons,
lesson-type/status icons, folder/course icons, and the lesson player's
footer brand mark all now use currentColor SVGs instead of emoji, matching
the app's vector logo instead of rendering inconsistently per platform.
A few icons built dynamically in JS (thumbnail-load fallbacks, toggled
button states) get a small ICON_SVGS constant per file, since JS template
literals can't call the Jinja macro.

Left pure directional glyphs (expand/collapse chevrons, move up/down
arrows, keyboard-shortcut arrows) as plain Unicode - they already render
as monochrome text, matching the existing collapsible-header chevron
pattern rather than the colorful pictographic emoji this was aimed at.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-23 21:49:10 -04:00
co-authored by Claude Sonnet 5
parent e2db5ecef8
commit eae6b09ecd
6 changed files with 154 additions and 90 deletions
+8
View File
@@ -0,0 +1,8 @@
{#
Shared stroke-style SVG icon set (Feather-style, 24x24 viewBox,
currentColor stroke) so every page uses the same hand-picked icons
instead of OS-rendered emoji, which render inconsistently across
platforms and clash with the app's vector logo. Import with
{% import '_icons.html' as icons %} and call icons.icon('name', size).
#}
{% macro icon(name, size=18) %}<svg class="icon" width="{{ size }}" height="{{ size }}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">{% if name == 'home' %}<path d="M3 10.5 12 3l9 7.5"></path><path d="M5 9.5V20a1 1 0 0 0 1 1h4v-6h4v6h4a1 1 0 0 0 1-1V9.5"></path>{% elif name == 'pencil' %}<path d="M12 20h9"></path><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"></path>{% elif name == 'help' %}<circle cx="12" cy="12" r="9"></circle><path d="M9.1 9a2.9 2.9 0 0 1 5.6 1c0 2-2.9 2.5-2.9 4.5"></path><line x1="12" y1="17.5" x2="12" y2="17.51"></line>{% elif name == 'settings' %}<circle cx="12" cy="12" r="3"></circle><path d="M12 2v3M12 19v3M4.2 4.2l2.1 2.1M17.7 17.7l2.1 2.1M2 12h3M19 12h3M4.2 19.8l2.1-2.1M17.7 6.3l2.1-2.1"></path>{% elif name == 'dice' %}<rect x="3" y="3" width="18" height="18" rx="4"></rect><circle cx="8" cy="8" r="1.3" fill="currentColor" stroke="none"></circle><circle cx="16" cy="8" r="1.3" fill="currentColor" stroke="none"></circle><circle cx="8" cy="16" r="1.3" fill="currentColor" stroke="none"></circle><circle cx="16" cy="16" r="1.3" fill="currentColor" stroke="none"></circle><circle cx="12" cy="12" r="1.3" fill="currentColor" stroke="none"></circle>{% elif name == 'folder' %}<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"></path>{% elif name == 'graduation-cap' %}<path d="M22 9 12 4 2 9l10 5 10-5Z"></path><path d="M6 11v5c0 1.4 2.7 2.5 6 2.5s6-1.1 6-2.5v-5"></path>{% elif name == 'check' %}<polyline points="20 6 9 17 4 12"></polyline>{% elif name == 'circle' %}<circle cx="12" cy="12" r="9"></circle>{% elif name == 'x' %}<line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line>{% elif name == 'pin' %}<path d="M12 21s7-6.5 7-11.5A7 7 0 0 0 5 9.5C5 14.5 12 21 12 21Z"></path><circle cx="12" cy="9.5" r="2.3"></circle>{% elif name == 'ban' %}<circle cx="12" cy="12" r="9"></circle><line x1="5.6" y1="5.6" x2="18.4" y2="18.4"></line>{% elif name == 'video' %}<rect x="2" y="5" width="14" height="14" rx="2"></rect><path d="M16 10.5 22 7v10l-6-3.5Z"></path>{% elif name == 'music' %}<path d="M9 18V5l11-2v13"></path><circle cx="6.5" cy="18" r="2.5"></circle><circle cx="17.5" cy="16" r="2.5"></circle>{% elif name == 'clipboard' %}<rect x="6" y="4" width="12" height="17" rx="2"></rect><path d="M9 4V3a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v1"></path><line x1="9" y1="11" x2="15" y2="11"></line><line x1="9" y1="15" x2="13" y2="15"></line>{% elif name == 'package' %}<path d="M21 8 12 3 3 8v8l9 5 9-5Z"></path><path d="M3 8l9 5 9-5M12 13v8"></path>{% elif name == 'file-text' %}<path d="M14 2H7a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8Z"></path><path d="M14 2v6h6"></path><line x1="9" y1="13" x2="15" y2="13"></line><line x1="9" y1="17" x2="15" y2="17"></line>{% elif name == 'trash' %}<path d="M4 7h16"></path><path d="M10 11v6M14 11v6"></path><path d="M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12"></path><path d="M9 7V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3"></path>{% elif name == 'refresh' %}<path d="M21 12a9 9 0 0 1-15.3 6.4"></path><path d="M3 12a9 9 0 0 1 15.3-6.4"></path><polyline points="21 3 21 9 15 9"></polyline><polyline points="3 21 3 15 9 15"></polyline>{% elif name == 'checkbox' %}<path d="M9 11l3 3 7-7"></path><path d="M21 12v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h10"></path>{% elif name == 'list' %}<line x1="4" y1="6" x2="20" y2="6"></line><line x1="4" y1="12" x2="20" y2="12"></line><line x1="4" y1="18" x2="20" y2="18"></line>{% elif name == 'grid' %}<rect x="3" y="3" width="8" height="8" rx="1.5"></rect><rect x="13" y="3" width="8" height="8" rx="1.5"></rect><rect x="3" y="13" width="8" height="8" rx="1.5"></rect><rect x="13" y="13" width="8" height="8" rx="1.5"></rect>{% elif name == 'bar-chart' %}<line x1="6" y1="20" x2="6" y2="14"></line><line x1="12" y1="20" x2="12" y2="8"></line><line x1="18" y1="20" x2="18" y2="11"></line>{% elif name == 'list-dots' %}<line x1="8" y1="6" x2="20" y2="6"></line><line x1="8" y1="12" x2="20" y2="12"></line><line x1="8" y1="18" x2="20" y2="18"></line><circle cx="4" cy="6" r="1"></circle><circle cx="4" cy="12" r="1"></circle><circle cx="4" cy="18" r="1"></circle>{% elif name == 'history' %}<path d="M3 12a9 9 0 1 0 3-6.7"></path><polyline points="3 4 3 9 8 9"></polyline>{% elif name == 'plus-circle' %}<circle cx="12" cy="12" r="9"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line>{% elif name == 'play-circle' %}<circle cx="12" cy="12" r="9"></circle><polygon points="10 8 16 12 10 16 10 8"></polygon>{% elif name == 'play' %}<polygon points="6 3 20 12 6 21 6 3"></polygon>{% endif %}</svg>{% endmacro %}
+63 -53
View File
@@ -1,3 +1,4 @@
{% import '_icons.html' as icons %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
@@ -423,9 +424,14 @@
align-items: center; align-items: center;
gap: 8px; gap: 8px;
} }
.card-icon { .icon {
display: inline-block;
vertical-align: -3px;
flex-shrink: 0; flex-shrink: 0;
color: var(--text-primary); }
.btn .icon, .tab-icon .icon {
vertical-align: -2px;
margin-right: 3px;
} }
.card-body { .card-body {
@@ -1014,13 +1020,13 @@
{% if stats.total_lessons %} {% if stats.total_lessons %}
<div class="course-actions"> <div class="course-actions">
<button class="btn btn-secondary btn-sm" onclick="markCourseWatched()"> <button class="btn btn-secondary btn-sm" onclick="markCourseWatched()">
Mark all as completed {{ icons.icon('check', 14) }} Mark all as completed
</button> </button>
<button class="btn btn-secondary btn-sm" id="queue-toggle-btn" onclick="toggleQueued()"> <button class="btn btn-secondary btn-sm" id="queue-toggle-btn" onclick="toggleQueued()">
{{ '📌 Remove from Next Up' if is_queued else '📌 Add to Next Up' }} {{ icons.icon('pin', 14) }} {{ 'Remove from Next Up' if is_queued else 'Add to Next Up' }}
</button> </button>
{% if has_note %} {% if has_note %}
<a class="btn btn-secondary btn-sm" href="/course/study-guide">📄 Download Study Guide</a> <a class="btn btn-secondary btn-sm" href="/course/study-guide">{{ icons.icon('file-text', 14) }} Download Study Guide</a>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
@@ -1041,7 +1047,7 @@
<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">{{ icons.icon('folder', 16) }}</span>
<span class="tree-name">{{ node.name }}</span> <span class="tree-name">{{ node.name }}</span>
<span class="tree-stats"> <span class="tree-stats">
{% if stats.total_lessons %}{{ stats.completed_lessons }}/{{ stats.total_lessons }} watched{% else %}Empty{% endif %} {% if stats.total_lessons %}{{ stats.completed_lessons }}/{{ stats.total_lessons }} watched{% else %}Empty{% endif %}
@@ -1065,22 +1071,22 @@
onclick="window.location.href='/lesson/{{ lesson_relative_path }}/{{ lesson.title|replace(' ', '_') }}'"> onclick="window.location.href='/lesson/{{ lesson_relative_path }}/{{ lesson.title|replace(' ', '_') }}'">
<div class="lesson-title"> <div class="lesson-title">
<span class="lesson-icon"> <span class="lesson-icon">
{% if lesson.lesson_type == 'video' %}🎥 {% if lesson.lesson_type == 'video' %}{{ icons.icon('video', 16) }}
{% elif lesson.lesson_type == 'audio' %}🎵 {% elif lesson.lesson_type == 'audio' %}{{ icons.icon('music', 16) }}
{% elif lesson.lesson_type == 'quiz' %}📝 {% elif lesson.lesson_type == 'quiz' %}{{ icons.icon('clipboard', 16) }}
{% elif lesson.lesson_type == 'mixed' %}📦 {% elif lesson.lesson_type == 'mixed' %}{{ icons.icon('package', 16) }}
{% else %}📄{% endif %} {% else %}{{ icons.icon('file-text', 16) }}{% endif %}
</span> </span>
<span class="lesson-name">{{ lesson.title }}</span> <span class="lesson-name">{{ lesson.title }}</span>
</div> </div>
<div class="lesson-meta"> <div class="lesson-meta">
<span class="lesson-type {{ lesson.lesson_type }}">{{ lesson.lesson_type|title }}</span> <span class="lesson-type {{ lesson.lesson_type }}">{{ lesson.lesson_type|title }}</span>
{% if lesson.completed %} {% if lesson.completed %}
<span class="status-icon completed"></span> <span class="status-icon completed">{{ icons.icon('check', 14) }}</span>
{% elif percent_watched %} {% elif percent_watched %}
<span class="watched-badge">{{ percent_watched }}% watched</span> <span class="watched-badge">{{ percent_watched }}% watched</span>
{% else %} {% else %}
<span class="status-icon pending"></span> <span class="status-icon pending">{{ icons.icon('circle', 14) }}</span>
{% endif %} {% endif %}
</div> </div>
{% if percent_watched %} {% if percent_watched %}
@@ -1103,15 +1109,15 @@
<div class="lesson-title"> <div class="lesson-title">
{% if view.has_thumbnail %} {% if view.has_thumbnail %}
<img class="lesson-thumb" src="/library/thumbnail?path={{ view.course_path | urlencode }}" alt="" <img class="lesson-thumb" src="/library/thumbnail?path={{ view.course_path | urlencode }}" alt=""
onerror="this.replaceWith(courseFallbackIcon('▶️'))"> onerror="this.replaceWith(courseFallbackIcon('play'))">
{% else %} {% else %}
<span class="lesson-icon">▶️</span> <span class="lesson-icon">{{ icons.icon('play', 16) }}</span>
{% endif %} {% endif %}
<span class="lesson-name">{{ view.lesson_title }}</span> <span class="lesson-name">{{ view.lesson_title }}</span>
</div> </div>
<div class="lesson-meta"> <div class="lesson-meta">
{% if view.completed %} {% if view.completed %}
<span class="status-icon completed"></span> <span class="status-icon completed">{{ icons.icon('check', 14) }}</span>
{% elif view.percent_watched %} {% elif view.percent_watched %}
<span class="watched-badge">{{ view.percent_watched }}% watched</span> <span class="watched-badge">{{ view.percent_watched }}% watched</span>
{% endif %} {% endif %}
@@ -1128,9 +1134,9 @@
<div class="lesson-title"> <div class="lesson-title">
{% if item.has_thumbnail %} {% if item.has_thumbnail %}
<img class="lesson-thumb" src="/library/thumbnail?path={{ item.path | urlencode }}" alt="" <img class="lesson-thumb" src="/library/thumbnail?path={{ item.path | urlencode }}" alt=""
onerror="this.replaceWith(courseFallbackIcon('🎓'))"> onerror="this.replaceWith(courseFallbackIcon('graduation-cap'))">
{% else %} {% else %}
<span class="lesson-icon">🎓</span> <span class="lesson-icon">{{ icons.icon('graduation-cap', 16) }}</span>
{% endif %} {% endif %}
<span class="lesson-name">{{ item.name }}</span> <span class="lesson-name">{{ item.name }}</span>
</div> </div>
@@ -1140,13 +1146,13 @@
{% if library_stats and library_stats.total_courses %} {% if library_stats and library_stats.total_courses %}
<div class="container"> <div class="container">
<a class="btn surprise-me-btn" href="/random-pick">🎲 Surprise Me</a> <a class="btn surprise-me-btn" href="/random-pick">{{ icons.icon('dice', 16) }} Surprise Me</a>
</div> </div>
{% endif %} {% endif %}
<div class="container"> <div class="container">
<div class="card" id="library-card"> <div class="card" id="library-card">
<h2><span class="card-header-label"><svg class="card-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="8" height="8" rx="1.5"></rect><rect x="13" y="3" width="8" height="8" rx="1.5"></rect><rect x="3" y="13" width="8" height="8" rx="1.5"></rect><rect x="13" y="13" width="8" height="8" rx="1.5"></rect></svg>Your Courses</span></h2> <h2><span class="card-header-label">{{ icons.icon('grid') }}Your Courses</span></h2>
<div class="library-toolbar"> <div class="library-toolbar">
<input type="text" id="library-search-input" class="library-search-input" <input type="text" id="library-search-input" class="library-search-input"
placeholder="Search courses…" oninput="handleLibrarySearchInput(this.value)"> placeholder="Search courses…" oninput="handleLibrarySearchInput(this.value)">
@@ -1156,12 +1162,12 @@
</select> </select>
<div class="library-view-toggle"> <div class="library-view-toggle">
<button type="button" class="view-toggle-btn" id="library-view-list-btn" <button type="button" class="view-toggle-btn" id="library-view-list-btn"
onclick="setLibraryViewMode('list')" title="List view"></button> onclick="setLibraryViewMode('list')" title="List view">{{ icons.icon('list', 16) }}</button>
<button type="button" class="view-toggle-btn" id="library-view-grid-btn" <button type="button" class="view-toggle-btn" id="library-view-grid-btn"
onclick="setLibraryViewMode('grid')" title="Grid view"></button> onclick="setLibraryViewMode('grid')" title="Grid view">{{ icons.icon('grid', 16) }}</button>
</div> </div>
<button type="button" class="view-toggle-btn" id="library-select-btn" <button type="button" class="view-toggle-btn" id="library-select-btn"
onclick="toggleSelectionMode()" title="Select multiple"></button> onclick="toggleSelectionMode()" title="Select multiple">{{ icons.icon('checkbox', 16) }}</button>
</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)">
@@ -1169,8 +1175,8 @@
</label> </label>
<div id="bulk-action-bar" class="bulk-action-bar" style="display: none;"> <div id="bulk-action-bar" class="bulk-action-bar" style="display: none;">
<span id="bulk-selected-count" class="bulk-selected-count"></span> <span id="bulk-selected-count" class="bulk-selected-count"></span>
<button type="button" class="btn btn-secondary btn-sm" onclick="bulkHideSelected()">🚫 Hide Selected</button> <button type="button" class="btn btn-secondary btn-sm" onclick="bulkHideSelected()">{{ icons.icon('ban', 14) }} Hide Selected</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="bulkQueueSelected()">📌 Add to Next Up</button> <button type="button" class="btn btn-secondary btn-sm" onclick="bulkQueueSelected()">{{ icons.icon('pin', 14) }} Add to Next Up</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="toggleSelectionMode()">Cancel</button> <button type="button" class="btn btn-secondary btn-sm" onclick="toggleSelectionMode()">Cancel</button>
</div> </div>
<p id="library-path-bar" class="library-breadcrumb"></p> <p id="library-path-bar" class="library-breadcrumb"></p>
@@ -1182,7 +1188,7 @@
{% if library_stats and library_stats.total_courses %} {% if library_stats and library_stats.total_courses %}
<div class="container"> <div class="container">
<div class="card"> <div class="card">
<h2 class="collapsible-header" data-card-id="library-stats"><span class="card-header-label"><svg class="card-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="20" x2="6" y2="14"></line><line x1="12" y1="20" x2="12" y2="8"></line><line x1="18" y1="20" x2="18" y2="11"></line></svg>Library Stats</span></h2> <h2 class="collapsible-header" data-card-id="library-stats"><span class="card-header-label">{{ icons.icon('bar-chart') }}Library Stats</span></h2>
<div class="card-body"> <div class="card-body">
<div class="stats-grid"> <div class="stats-grid">
<div class="stats-tile"> <div class="stats-tile">
@@ -1224,23 +1230,23 @@
{% if next_up %} {% if next_up %}
<div class="container"> <div class="container">
<div class="card" id="next-up-card"> <div class="card" id="next-up-card">
<h2 class="collapsible-header" data-card-id="next-up"><span class="card-header-label"><svg class="card-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="20" y2="6"></line><line x1="8" y1="12" x2="20" y2="12"></line><line x1="8" y1="18" x2="20" y2="18"></line><circle cx="4" cy="6" r="1"></circle><circle cx="4" cy="12" r="1"></circle><circle cx="4" cy="18" r="1"></circle></svg>Next Up</span></h2> <h2 class="collapsible-header" data-card-id="next-up"><span class="card-header-label">{{ icons.icon('list-dots') }}Next Up</span></h2>
<div class="card-body" id="next-up-list"> <div class="card-body" id="next-up-list">
{% for item in next_up %} {% for item in next_up %}
<div class="lesson-item" data-path="{{ item.path }}"> <div class="lesson-item" data-path="{{ item.path }}">
<div class="lesson-title" style="cursor: pointer;" onclick="loadCoursePath('{{ item.path|replace("'", "\\'") }}')"> <div class="lesson-title" style="cursor: pointer;" onclick="loadCoursePath('{{ item.path|replace("'", "\\'") }}')">
{% if item.has_thumbnail %} {% if item.has_thumbnail %}
<img class="lesson-thumb" src="/library/thumbnail?path={{ item.path | urlencode }}" alt="" <img class="lesson-thumb" src="/library/thumbnail?path={{ item.path | urlencode }}" alt=""
onerror="this.replaceWith(courseFallbackIcon('🎓'))"> onerror="this.replaceWith(courseFallbackIcon('graduation-cap'))">
{% else %} {% else %}
<span class="lesson-icon">🎓</span> <span class="lesson-icon">{{ icons.icon('graduation-cap', 16) }}</span>
{% endif %} {% endif %}
<span class="lesson-name">{{ item.name }}</span> <span class="lesson-name">{{ item.name }}</span>
</div> </div>
<div class="lesson-meta"> <div class="lesson-meta">
<button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); moveNextUp(this, -1)" title="Move up"></button> <button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); moveNextUp(this, -1)" title="Move up"></button>
<button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); moveNextUp(this, 1)" title="Move down"></button> <button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); moveNextUp(this, 1)" title="Move down"></button>
<button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); removeNextUp(this)" title="Remove"></button> <button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); removeNextUp(this)" title="Remove">{{ icons.icon('x', 14) }}</button>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
@@ -1251,7 +1257,7 @@
{% if stale_courses %} {% if stale_courses %}
<div class="container"> <div class="container">
<div class="card"> <div class="card">
<h2 class="collapsible-header" data-card-id="pick-back-up"><span class="card-header-label"><svg class="card-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 3-6.7"></path><polyline points="3 4 3 9 8 9"></polyline></svg>Pick Back Up</span></h2> <h2 class="collapsible-header" data-card-id="pick-back-up"><span class="card-header-label">{{ icons.icon('history') }}Pick Back Up</span></h2>
<div class="card-body"> <div class="card-body">
{% for item in stale_courses %} {% for item in stale_courses %}
{{ render_course_row(item, 'Last touched ' + item.last_touched_display) }} {{ render_course_row(item, 'Last touched ' + item.last_touched_display) }}
@@ -1263,7 +1269,7 @@
{% if recently_added %} {% if recently_added %}
<div class="container"> <div class="container">
<div class="card"> <div class="card">
<h2 class="collapsible-header" data-card-id="recently-added"><span class="card-header-label"><svg class="card-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg>Recently Added</span></h2> <h2 class="collapsible-header" data-card-id="recently-added"><span class="card-header-label">{{ icons.icon('plus-circle') }}Recently Added</span></h2>
<div class="card-body"> <div class="card-body">
{% for item in recently_added %} {% for item in recently_added %}
{{ render_course_row(item, item.added_display) }} {{ render_course_row(item, item.added_display) }}
@@ -1275,7 +1281,7 @@
{% if recent_views %} {% if recent_views %}
<div class="container"> <div class="container">
<div class="card"> <div class="card">
<h2 class="collapsible-header" data-card-id="recently-viewed"><span class="card-header-label"><svg class="card-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"></circle><polygon points="10 8 16 12 10 16 10 8"></polygon></svg>Recently Viewed</span></h2> <h2 class="collapsible-header" data-card-id="recently-viewed"><span class="card-header-label">{{ icons.icon('play-circle') }}Recently Viewed</span></h2>
<div class="card-body"> <div class="card-body">
{% for view in recent_views %} {% for view in recent_views %}
{{ render_view_row(view) }} {{ render_view_row(view) }}
@@ -1289,22 +1295,34 @@
<nav class="bottom-tab-bar"> <nav class="bottom-tab-bar">
<a href="/reset_course" class="tab-item {% if active_tab == 'home' %}active{% endif %}"> <a href="/reset_course" class="tab-item {% if active_tab == 'home' %}active{% endif %}">
<span class="tab-icon">🏠</span><span class="tab-label">Home</span> <span class="tab-icon">{{ icons.icon('home', 20) }}</span><span class="tab-label">Home</span>
</a> </a>
<a href="/notes" class="tab-item {% if active_tab == 'notes' %}active{% endif %}"> <a href="/notes" class="tab-item {% if active_tab == 'notes' %}active{% endif %}">
<span class="tab-icon">📝</span><span class="tab-label">Notes</span> <span class="tab-icon">{{ icons.icon('pencil', 20) }}</span><span class="tab-label">Notes</span>
</a> </a>
<a href="/help" class="tab-item {% if active_tab == 'help' %}active{% endif %}"> <a href="/help" class="tab-item {% if active_tab == 'help' %}active{% endif %}">
<span class="tab-icon"></span><span class="tab-label">Help</span> <span class="tab-icon">{{ icons.icon('help', 20) }}</span><span class="tab-label">Help</span>
</a> </a>
<a href="/settings" class="tab-item {% if active_tab == 'settings' %}active{% endif %}"> <a href="/settings" class="tab-item {% if active_tab == 'settings' %}active{% endif %}">
<span class="tab-icon"></span><span class="tab-label">Settings</span> <span class="tab-icon">{{ icons.icon('settings', 20) }}</span><span class="tab-label">Settings</span>
</a> </a>
</nav> </nav>
<div id="toast-container" class="toast-container"></div> <div id="toast-container" class="toast-container"></div>
<script> <script>
// Client-rendered mirror of a few templates/_icons.html entries -
// JS template literals can't call the Jinja macro, so the handful
// of icons built dynamically (thumbnail-load fallbacks, toggled
// button states, JS-rendered library rows) are kept here in sync
// with the same path data by hand.
const ICON_SVGS = {
play: '<svg class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="6 3 20 12 6 21 6 3"></polygon></svg>',
'graduation-cap': '<svg class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 9 12 4 2 9l10 5 10-5Z"></path><path d="M6 11v5c0 1.4 2.7 2.5 6 2.5s6-1.1 6-2.5v-5"></path></svg>',
pin: '<svg class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 21s7-6.5 7-11.5A7 7 0 0 0 5 9.5C5 14.5 12 21 12 21Z"></path><circle cx="12" cy="9.5" r="2.3"></circle></svg>',
folder: '<svg class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"></path></svg>',
};
function showToast(message) { function showToast(message) {
const container = document.getElementById('toast-container'); const container = document.getElementById('toast-container');
if (!container) return; if (!container) return;
@@ -1480,18 +1498,10 @@
// Fallback if a course's thumbnail image 404s/errors after the row // Fallback if a course's thumbnail image 404s/errors after the row
// already rendered (e.g. the file was removed on disk in between) - // already rendered (e.g. the file was removed on disk in between) -
// swaps back to the plain icon rather than showing a broken image. // swaps back to the plain icon rather than showing a broken image.
function courseFallbackIcon(emoji) { function courseFallbackIcon(iconName) {
const span = document.createElement('span'); const span = document.createElement('span');
span.className = 'lesson-icon'; span.className = 'lesson-icon';
span.textContent = emoji; span.innerHTML = ICON_SVGS[iconName] || '';
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; return span;
} }
@@ -1652,7 +1662,7 @@
return ` return `
<div class="library-grid-card" onclick="${clickHandler}"> <div class="library-grid-card" onclick="${clickHandler}">
${selectableAttrs(item)} ${selectableAttrs(item)}
<div class="grid-card-thumb-wrap">📁</div> <div class="grid-card-thumb-wrap"><svg class="icon" width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"></path></svg></div>
<div class="grid-card-name" title="${escapeAttr(item.name)}">${item.name}</div> <div class="grid-card-name" title="${escapeAttr(item.name)}">${item.name}</div>
</div> </div>
`; `;
@@ -1661,8 +1671,8 @@
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
? `<img class="lesson-thumb" src="/library/thumbnail?path=${encodeURIComponent(item.path)}" alt="" onerror="this.replaceWith(courseFallbackIcon('🎓'))">` ? `<img class="lesson-thumb" src="/library/thumbnail?path=${encodeURIComponent(item.path)}" alt="" onerror="this.replaceWith(courseFallbackIcon('graduation-cap'))">`
: `<span class="lesson-icon">🎓</span>`; : `<span class="lesson-icon">${ICON_SVGS['graduation-cap']}</span>`;
const clickHandler = selectionMode ? `toggleSelection('${safePath}')` : `loadCoursePath('${safePath}')`; const clickHandler = selectionMode ? `toggleSelection('${safePath}')` : `loadCoursePath('${safePath}')`;
const pct = item.completion_percentage; const pct = item.completion_percentage;
const progressBadge = pct ? `<span class="watched-badge">${pct}% done</span>` : ''; const progressBadge = pct ? `<span class="watched-badge">${pct}% done</span>` : '';
@@ -1679,7 +1689,7 @@
<div class="lesson-meta"> <div class="lesson-meta">
${progressBadge} ${progressBadge}
<span>${item.media_files} media file${item.media_files === 1 ? '' : 's'}</span> <span>${item.media_files} media file${item.media_files === 1 ? '' : 's'}</span>
<button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); queueCourse('${safePath}', this)" title="Add to Next Up">📌</button> <button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); queueCourse('${safePath}', this)" title="Add to Next Up">${ICON_SVGS.pin}</button>
</div> </div>
${progressBar} ${progressBar}
</div> </div>
@@ -1694,7 +1704,7 @@
<div class="tree-header directory" onclick="${clickHandler}"> <div class="tree-header directory" onclick="${clickHandler}">
<div class="tree-title"> <div class="tree-title">
${selectableAttrs(item)} ${selectableAttrs(item)}
<span class="tree-icon">📁</span> <span class="tree-icon">${ICON_SVGS.folder}</span>
<span class="tree-name">${item.name}</span> <span class="tree-name">${item.name}</span>
</div> </div>
<span class="tree-toggle"></span> <span class="tree-toggle"></span>
@@ -1896,7 +1906,7 @@
.then(r => r.json()) .then(r => r.json())
.then(data => { .then(data => {
if (data.success) { if (data.success) {
btn.textContent = currentlyQueued ? '📌 Add to Next Up' : '📌 Remove from Next Up'; btn.innerHTML = ICON_SVGS.pin + ' ' + (currentlyQueued ? 'Add to Next Up' : 'Remove from Next Up');
} }
}) })
.catch(() => {}); .catch(() => {});
+12 -6
View File
@@ -1,3 +1,4 @@
{% import '_icons.html' as icons %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
@@ -109,6 +110,11 @@
.tab-item.active { .tab-item.active {
color: var(--accent); color: var(--accent);
} }
.icon {
display: inline-block;
vertical-align: -3px;
flex-shrink: 0;
}
.container { .container {
max-width: 800px; max-width: 800px;
margin: 0 auto; margin: 0 auto;
@@ -157,7 +163,7 @@
<li><strong>Browse your library</strong> from the main page, or enter a path manually in Settings</li> <li><strong>Browse your library</strong> from the main page, or enter a path manually in Settings</li>
<li><strong>Click a course</strong> to load it - folders drill down, courses open directly</li> <li><strong>Click a course</strong> to load it - folders drill down, courses open directly</li>
<li><strong>Start learning!</strong> Progress, notes, and completion are all saved automatically</li> <li><strong>Start learning!</strong> Progress, notes, and completion are all saved automatically</li>
<li>Not sure what to watch? <strong>🎲 Surprise Me</strong> at the top of the dashboard picks a random course you haven't finished</li> <li>Not sure what to watch? <strong>{{ icons.icon('dice', 14) }} Surprise Me</strong> at the top of the dashboard picks a random course you haven't finished</li>
</ul> </ul>
</div> </div>
@@ -193,7 +199,7 @@
<li>Check <strong>Also search transcripts</strong> to search inside subtitle files (.srt/.vtt) too, if your courses have them</li> <li>Check <strong>Also search transcripts</strong> to search inside subtitle files (.srt/.vtt) too, if your courses have them</li>
<li>Switch between list and grid (poster) view with the toggle next to the search box - handy for browsing a folder with lots of courses by thumbnail</li> <li>Switch between list and grid (poster) view with the toggle next to the search box - handy for browsing a folder with lots of courses by thumbnail</li>
<li><strong>Hide</strong> a course or whole folder from Settings → Manage Library without touching anything on disk</li> <li><strong>Hide</strong> a course or whole folder from Settings → Manage Library without touching anything on disk</li>
<li>The button in the Library browser turns on <strong>select mode</strong> - pick several courses or folders at once to hide or add to Next Up together</li> <li>The {{ icons.icon('checkbox', 14) }} button in the Library browser turns on <strong>select mode</strong> - pick several courses or folders at once to hide or add to Next Up together</li>
<li><strong>Bulk Rename</strong> (Settings) finds and replaces text across every course/folder name at once - useful for stripping a release-group suffix off a batch of downloads</li> <li><strong>Bulk Rename</strong> (Settings) finds and replaces text across every course/folder name at once - useful for stripping a release-group suffix off a batch of downloads</li>
</ul> </ul>
</div> </div>
@@ -222,16 +228,16 @@
<nav class="bottom-tab-bar"> <nav class="bottom-tab-bar">
<a href="/reset_course" class="tab-item {% if active_tab == 'home' %}active{% endif %}"> <a href="/reset_course" class="tab-item {% if active_tab == 'home' %}active{% endif %}">
<span class="tab-icon">🏠</span><span class="tab-label">Home</span> <span class="tab-icon">{{ icons.icon('home', 20) }}</span><span class="tab-label">Home</span>
</a> </a>
<a href="/notes" class="tab-item {% if active_tab == 'notes' %}active{% endif %}"> <a href="/notes" class="tab-item {% if active_tab == 'notes' %}active{% endif %}">
<span class="tab-icon">📝</span><span class="tab-label">Notes</span> <span class="tab-icon">{{ icons.icon('pencil', 20) }}</span><span class="tab-label">Notes</span>
</a> </a>
<a href="/help" class="tab-item {% if active_tab == 'help' %}active{% endif %}"> <a href="/help" class="tab-item {% if active_tab == 'help' %}active{% endif %}">
<span class="tab-icon"></span><span class="tab-label">Help</span> <span class="tab-icon">{{ icons.icon('help', 20) }}</span><span class="tab-label">Help</span>
</a> </a>
<a href="/settings" class="tab-item {% if active_tab == 'settings' %}active{% endif %}"> <a href="/settings" class="tab-item {% if active_tab == 'settings' %}active{% endif %}">
<span class="tab-icon"></span><span class="tab-label">Settings</span> <span class="tab-icon">{{ icons.icon('settings', 20) }}</span><span class="tab-label">Settings</span>
</a> </a>
</nav> </nav>
+29 -13
View File
@@ -1,3 +1,4 @@
{% import '_icons.html' as icons %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
@@ -128,6 +129,11 @@
.footer-links a:hover { .footer-links a:hover {
color: var(--accent); color: var(--accent);
} }
.icon {
display: inline-block;
vertical-align: -3px;
flex-shrink: 0;
}
.container { .container {
max-width: var(--container-max-width); max-width: var(--container-max-width);
width: 95%; width: 95%;
@@ -558,7 +564,7 @@
<div style="text-align: center; color: var(--text-muted);">Loading content...</div> <div style="text-align: center; color: var(--text-muted);">Loading content...</div>
</div> </div>
<a href="/files/{{ text_file|replace('\\', '/') }}" class="file-link" target="_blank"> <a href="/files/{{ text_file|replace('\\', '/') }}" class="file-link" target="_blank">
📄 Open {{ text_file.split('/')[-1] }} in new tab {{ icons.icon('file-text', 14) }} Open {{ text_file.split('/')[-1] }} in new tab
</a> </a>
</div> </div>
{% endfor %} {% endfor %}
@@ -580,15 +586,15 @@
<a class="section-lesson-row" href="/lesson/{{ item.url }}"> <a class="section-lesson-row" href="/lesson/{{ item.url }}">
{% endif %} {% endif %}
<span class="section-lesson-icon"> <span class="section-lesson-icon">
{% if item.lesson_type == 'video' %}🎥 {% if item.lesson_type == 'video' %}{{ icons.icon('video', 16) }}
{% elif item.lesson_type == 'audio' %}🎵 {% elif item.lesson_type == 'audio' %}{{ icons.icon('music', 16) }}
{% elif item.lesson_type == 'quiz' %}📝 {% elif item.lesson_type == 'quiz' %}{{ icons.icon('clipboard', 16) }}
{% elif item.lesson_type == 'mixed' %}📦 {% elif item.lesson_type == 'mixed' %}{{ icons.icon('package', 16) }}
{% else %}📄{% endif %} {% else %}{{ icons.icon('file-text', 16) }}{% endif %}
</span> </span>
<span class="section-lesson-title">{{ item.title }}</span> <span class="section-lesson-title">{{ item.title }}</span>
{% if item.completed %} {% if item.completed %}
<span class="section-lesson-status completed"></span> <span class="section-lesson-status completed">{{ icons.icon('check', 14) }}</span>
{% elif item.percent_watched %} {% elif item.percent_watched %}
<span class="section-lesson-status">{{ item.percent_watched }}%</span> <span class="section-lesson-status">{{ item.percent_watched }}%</span>
{% endif %} {% endif %}
@@ -650,6 +656,15 @@
</div> </div>
<script> <script>
// Client-rendered mirror of a few templates/_icons.html entries -
// JS template literals can't call the Jinja macro, so the icons
// built dynamically here are kept in sync with the same path
// data by hand.
const ICON_SVGS = {
pencil: '<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"></path><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"></path></svg>',
trash: '<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h16"></path><path d="M10 11v6M14 11v6"></path><path d="M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12"></path><path d="M9 7V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3"></path></svg>',
};
// Load text content // Load text content
{% for text_file in lesson.text_files %} {% for text_file in lesson.text_files %}
console.log('Loading file: {{ text_file }}'); console.log('Loading file: {{ text_file }}');
@@ -852,8 +867,8 @@
return `<div class="note-entry" data-note-id="${n.id}"> return `<div class="note-entry" data-note-id="${n.id}">
${badge} ${badge}
<span class="note-entry-text">${escapeHtml(n.text)}</span> <span class="note-entry-text">${escapeHtml(n.text)}</span>
<button type="button" class="note-entry-edit" onclick="startEditNote('${n.id}')" title="Edit">✏️</button> <button type="button" class="note-entry-edit" onclick="startEditNote('${n.id}')" title="Edit">${ICON_SVGS.pencil}</button>
<button type="button" class="note-entry-delete" onclick="deleteNoteEntry('${n.id}')" title="Delete">🗑</button> <button type="button" class="note-entry-delete" onclick="deleteNoteEntry('${n.id}')" title="Delete">${ICON_SVGS.trash}</button>
</div>`; </div>`;
}).join(''); }).join('');
} }
@@ -1313,12 +1328,13 @@
<footer class="app-footer"> <footer class="app-footer">
<div class="container-inner"> <div class="container-inner">
<div class="footer-brand"> <div class="footer-brand">
📚 <a href="/reset_course" style="color: inherit; text-decoration: none;">OfflineU</a> <svg class="icon" viewBox="0 0 64 64" width="16" height="16" aria-hidden="true"><rect x="2" y="2" width="60" height="60" rx="16" fill="var(--accent)"></rect><path d="M24 20 L24 44 L46 32 Z" fill="var(--bg-secondary)"></path></svg>
<a href="/reset_course" style="color: inherit; text-decoration: none;">OfflineU</a>
</div> </div>
<div class="footer-links"> <div class="footer-links">
<a href="/notes">📝 Notes</a> <a href="/notes">{{ icons.icon('pencil', 14) }} Notes</a>
<a href="/help"> Help</a> <a href="/help">{{ icons.icon('help', 14) }} Help</a>
<a href="/settings"> Settings</a> <a href="/settings">{{ icons.icon('settings', 14) }} Settings</a>
</div> </div>
</div> </div>
</footer> </footer>
+12 -6
View File
@@ -1,3 +1,4 @@
{% import '_icons.html' as icons %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
@@ -111,6 +112,11 @@
.tab-item.active { .tab-item.active {
color: var(--accent); color: var(--accent);
} }
.icon {
display: inline-block;
vertical-align: -3px;
flex-shrink: 0;
}
.container { .container {
max-width: 800px; max-width: 800px;
margin: 0 auto; margin: 0 auto;
@@ -227,7 +233,7 @@
<div class="page-content"> <div class="page-content">
<div class="container"> <div class="container">
<h1>📝 Notes</h1> <h1>{{ icons.icon('pencil', 22) }} Notes</h1>
<p class="subtitle">Every lesson note across your library, newest first.</p> <p class="subtitle">Every lesson note across your library, newest first.</p>
{% if not notes %} {% if not notes %}
@@ -241,7 +247,7 @@
{% for note in notes %} {% for note in notes %}
<div class="note-card" data-search="{{ (note.lesson_title ~ ' ' ~ note.course_name ~ ' ' ~ note.text) | lower }}"> <div class="note-card" data-search="{{ (note.lesson_title ~ ' ' ~ note.course_name ~ ' ' ~ note.text) | lower }}">
<div class="note-card-header"> <div class="note-card-header">
<span class="note-card-icon">📝</span> <span class="note-card-icon">{{ icons.icon('pencil', 20) }}</span>
<div class="note-card-titles"> <div class="note-card-titles">
<span class="note-lesson-title">{{ note.lesson_title }}</span> <span class="note-lesson-title">{{ note.lesson_title }}</span>
<span class="note-course-name">{{ note.course_name }}</span> <span class="note-course-name">{{ note.course_name }}</span>
@@ -265,16 +271,16 @@
<nav class="bottom-tab-bar"> <nav class="bottom-tab-bar">
<a href="/reset_course" class="tab-item {% if active_tab == 'home' %}active{% endif %}"> <a href="/reset_course" class="tab-item {% if active_tab == 'home' %}active{% endif %}">
<span class="tab-icon">🏠</span><span class="tab-label">Home</span> <span class="tab-icon">{{ icons.icon('home', 20) }}</span><span class="tab-label">Home</span>
</a> </a>
<a href="/notes" class="tab-item {% if active_tab == 'notes' %}active{% endif %}"> <a href="/notes" class="tab-item {% if active_tab == 'notes' %}active{% endif %}">
<span class="tab-icon">📝</span><span class="tab-label">Notes</span> <span class="tab-icon">{{ icons.icon('pencil', 20) }}</span><span class="tab-label">Notes</span>
</a> </a>
<a href="/help" class="tab-item {% if active_tab == 'help' %}active{% endif %}"> <a href="/help" class="tab-item {% if active_tab == 'help' %}active{% endif %}">
<span class="tab-icon"></span><span class="tab-label">Help</span> <span class="tab-icon">{{ icons.icon('help', 20) }}</span><span class="tab-label">Help</span>
</a> </a>
<a href="/settings" class="tab-item {% if active_tab == 'settings' %}active{% endif %}"> <a href="/settings" class="tab-item {% if active_tab == 'settings' %}active{% endif %}">
<span class="tab-icon"></span><span class="tab-label">Settings</span> <span class="tab-icon">{{ icons.icon('settings', 20) }}</span><span class="tab-label">Settings</span>
</a> </a>
</nav> </nav>
+30 -12
View File
@@ -1,3 +1,4 @@
{% import '_icons.html' as icons %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
@@ -112,6 +113,11 @@
.tab-item.active { .tab-item.active {
color: var(--accent); color: var(--accent);
} }
.icon {
display: inline-block;
vertical-align: -3px;
flex-shrink: 0;
}
.container { .container {
max-width: 800px; max-width: 800px;
margin: 0 auto; margin: 0 auto;
@@ -439,7 +445,7 @@
</div> </div>
<span id="library-path-status" style="font-size: 0.85em; min-height: 1.2em;"></span> <span id="library-path-status" style="font-size: 0.85em; min-height: 1.2em;"></span>
<div style="display: flex; align-items: center; gap: 10px;"> <div style="display: flex; align-items: center; gap: 10px;">
<button class="btn btn-secondary btn-sm" onclick="refreshLibraryCache()">🔄 Refresh Library</button> <button class="btn btn-secondary btn-sm" onclick="refreshLibraryCache()">{{ icons.icon('refresh', 14) }} Refresh Library</button>
<span id="library-refresh-status" style="font-size: 0.85em; color: var(--text-muted);"></span> <span id="library-refresh-status" style="font-size: 0.85em; color: var(--text-muted);"></span>
</div> </div>
<span class="setting-desc">Courses are cached briefly for speed - use this after adding or removing files directly on disk if you don't want to wait a few minutes for it to notice.</span> <span class="setting-desc">Courses are cached briefly for speed - use this after adding or removing files directly on disk if you don't want to wait a few minutes for it to notice.</span>
@@ -566,28 +572,40 @@
<div class="actions"> <div class="actions">
<button class="btn btn-secondary" onclick="resetSettings()">Reset to Defaults</button> <button class="btn btn-secondary" onclick="resetSettings()">Reset to Defaults</button>
<span id="save-status"> Saved</span> <span id="save-status">{{ icons.icon('check', 14) }} Saved</span>
</div> </div>
</div> </div>
</div> </div>
<nav class="bottom-tab-bar"> <nav class="bottom-tab-bar">
<a href="/reset_course" class="tab-item {% if active_tab == 'home' %}active{% endif %}"> <a href="/reset_course" class="tab-item {% if active_tab == 'home' %}active{% endif %}">
<span class="tab-icon">🏠</span><span class="tab-label">Home</span> <span class="tab-icon">{{ icons.icon('home', 20) }}</span><span class="tab-label">Home</span>
</a> </a>
<a href="/notes" class="tab-item {% if active_tab == 'notes' %}active{% endif %}"> <a href="/notes" class="tab-item {% if active_tab == 'notes' %}active{% endif %}">
<span class="tab-icon">📝</span><span class="tab-label">Notes</span> <span class="tab-icon">{{ icons.icon('pencil', 20) }}</span><span class="tab-label">Notes</span>
</a> </a>
<a href="/help" class="tab-item {% if active_tab == 'help' %}active{% endif %}"> <a href="/help" class="tab-item {% if active_tab == 'help' %}active{% endif %}">
<span class="tab-icon"></span><span class="tab-label">Help</span> <span class="tab-icon">{{ icons.icon('help', 20) }}</span><span class="tab-label">Help</span>
</a> </a>
<a href="/settings" class="tab-item {% if active_tab == 'settings' %}active{% endif %}"> <a href="/settings" class="tab-item {% if active_tab == 'settings' %}active{% endif %}">
<span class="tab-icon"></span><span class="tab-label">Settings</span> <span class="tab-icon">{{ icons.icon('settings', 20) }}</span><span class="tab-label">Settings</span>
</a> </a>
</nav> </nav>
<script src="/static/theme.js"></script> <script src="/static/theme.js"></script>
<script> <script>
// Client-rendered mirror of a few templates/_icons.html entries -
// JS template literals can't call the Jinja macro, so the icons
// built dynamically here are kept in sync with the same path data
// by hand.
const ICON_SVGS = {
ban: '<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"></circle><line x1="5.6" y1="5.6" x2="18.4" y2="18.4"></line></svg>',
'graduation-cap': '<svg class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 9 12 4 2 9l10 5 10-5Z"></path><path d="M6 11v5c0 1.4 2.7 2.5 6 2.5s6-1.1 6-2.5v-5"></path></svg>',
folder: '<svg class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"></path></svg>',
pencil: '<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"></path><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"></path></svg>',
check: '<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>',
};
let saveTimeout = null; let saveTimeout = null;
function showSaved() { function showSaved() {
@@ -652,7 +670,7 @@
list.innerHTML = items.map(item => ` list.innerHTML = items.map(item => `
<div class="curate-row"> <div class="curate-row">
<div class="curate-row-name"> <div class="curate-row-name">
<span>🚫</span> <span>${ICON_SVGS.ban}</span>
<span class="name">${item.name}</span> <span class="name">${item.name}</span>
</div> </div>
<button class="btn btn-secondary btn-sm" onclick="toggleHidden('${item.path.replace(/'/g, "\\'")}', false)">Show</button> <button class="btn btn-secondary btn-sm" onclick="toggleHidden('${item.path.replace(/'/g, "\\'")}', false)">Show</button>
@@ -686,9 +704,9 @@
container.innerHTML = data.items.map(item => { container.innerHTML = data.items.map(item => {
const safePath = item.path.replace(/'/g, "\\'"); const safePath = item.path.replace(/'/g, "\\'");
const safeName = item.name.replace(/'/g, "\\'"); const safeName = item.name.replace(/'/g, "\\'");
const icon = item.type === 'course' ? '🎓' : '📁'; const icon = item.type === 'course' ? ICON_SVGS['graduation-cap'] : ICON_SVGS.folder;
const hiddenClass = item.hidden ? 'is-hidden' : ''; const hiddenClass = item.hidden ? 'is-hidden' : '';
const renameBtn = `<button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); startRename(this, '${safePath}', '${safeName}')" title="Rename">✏️</button>`; const renameBtn = `<button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); startRename(this, '${safePath}', '${safeName}')" title="Rename">${ICON_SVGS.pencil}</button>`;
const toggleBtn = `<button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); toggleHidden('${safePath}', ${!item.hidden}, this)">${item.hidden ? 'Show' : 'Hide'}</button>`; const toggleBtn = `<button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); toggleHidden('${safePath}', ${!item.hidden}, this)">${item.hidden ? 'Show' : 'Hide'}</button>`;
const actions = `<div class="curate-actions">${renameBtn}${toggleBtn}</div>`; const actions = `<div class="curate-actions">${renameBtn}${toggleBtn}</div>`;
@@ -951,7 +969,7 @@
.then(data => { .then(data => {
if (data.success) { if (data.success) {
status.style.color = 'var(--success)'; status.style.color = 'var(--success)';
status.textContent = ' Saved — reload the main page to see it take effect'; status.innerHTML = ICON_SVGS.check + ' Saved — reload the main page to see it take effect';
showSaved(); showSaved();
} else { } else {
status.style.color = 'var(--error)'; status.style.color = 'var(--error)';
@@ -972,7 +990,7 @@
.then(r => r.json()) .then(r => r.json())
.then(data => { .then(data => {
status.style.color = data.success ? 'var(--success)' : 'var(--error)'; status.style.color = data.success ? 'var(--success)' : 'var(--error)';
status.textContent = data.success ? ' Refreshed' : 'Could not refresh'; status.innerHTML = data.success ? (ICON_SVGS.check + ' Refreshed') : 'Could not refresh';
if (data.success) setTimeout(() => { status.textContent = ''; }, 3000); if (data.success) setTimeout(() => { status.textContent = ''; }, 3000);
}) })
.catch(() => { .catch(() => {
@@ -1004,7 +1022,7 @@
.then(data => { .then(data => {
if (data.success) { if (data.success) {
status.style.color = 'var(--success)'; status.style.color = 'var(--success)';
status.textContent = ` Loaded "${data.course_name}" — redirecting...`; status.innerHTML = ICON_SVGS.check + ` Loaded "${data.course_name}" — redirecting...`;
setTimeout(() => { window.location.href = '/'; }, 1000); setTimeout(() => { window.location.href = '/'; }, 1000);
} else { } else {
status.style.color = 'var(--error)'; status.style.color = 'var(--error)';