Add Recently Added, bulk rename, time remaining, library stats, backup export
Five more usability features on top of the search/thumbnails batch: - Recently Added: dashboard card of courses by folder mtime, separate from Recently Viewed (watched vs. just showed up on disk). - Bulk find/replace rename across every course/folder name at once, with a mandatory preview step before anything touches disk. Refactored the single-item rename route to share the same validate/apply logic. - Estimated time remaining on the loaded course's stats card, computed only from lessons that have actually reported a duration. - Library-wide stats overview: total courses, lessons tracked, time watched, daily streak - read from each course's small progress file rather than re-scanning course contents. - One-click backup/export zip of settings, hidden-paths, recently-viewed, and every course's progress/notes. Also fixes a real performance bug found along the way: search was routing through list_library_directory, which computes a full recursive file count and thumbnail lookup for every course at every level regardless of match - turning a search into an O(every file in the library) scan. Gave search its own lightweight directory-only walk (iter_all_courses), now shared by Recently Added and the stats overview too, so the expensive per-course work only runs for courses that actually match. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -251,6 +251,32 @@
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.stats-tile {
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: var(--radius);
|
||||
padding: 14px 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stats-tile-value {
|
||||
font-size: 1.5em;
|
||||
font-weight: 700;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.stats-tile-label {
|
||||
font-size: 0.8em;
|
||||
color: var(--text-muted);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.library-breadcrumb {
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
@@ -635,8 +661,15 @@
|
||||
<div>
|
||||
<strong>{{ stats.completed_lessons }}/{{ stats.total_lessons }}</strong> lessons completed
|
||||
</div>
|
||||
<div style="color: #007acc; font-size: 1.2em;">
|
||||
{{ "%.1f"|format(stats.completion_percentage) }}%
|
||||
<div style="text-align: right;">
|
||||
<div style="color: #007acc; font-size: 1.2em;">
|
||||
{{ "%.1f"|format(stats.completion_percentage) }}%
|
||||
</div>
|
||||
{% if stats.remaining_display %}
|
||||
<div style="color: var(--text-muted); font-size: 0.85em;">
|
||||
~{{ stats.remaining_display }} remaining
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
@@ -746,6 +779,58 @@
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro render_course_row(item, meta_text) %}
|
||||
<div class="lesson-item" onclick="loadCoursePath('{{ item.path|replace("'", "\\'") }}')">
|
||||
<div class="lesson-title">
|
||||
{% if item.has_thumbnail %}
|
||||
<img class="lesson-thumb" src="/library/thumbnail?path={{ item.path | urlencode }}" alt=""
|
||||
onerror="this.replaceWith(courseFallbackIcon('🎓'))">
|
||||
{% else %}
|
||||
<span class="lesson-icon">🎓</span>
|
||||
{% endif %}
|
||||
<span class="lesson-name">{{ item.name }}</span>
|
||||
</div>
|
||||
<span class="lesson-meta">{{ meta_text }}</span>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% if library_stats and library_stats.total_courses %}
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<h2>📊 Library Stats</h2>
|
||||
<div class="stats-grid">
|
||||
<div class="stats-tile">
|
||||
<div class="stats-tile-value">{{ library_stats.total_courses }}</div>
|
||||
<div class="stats-tile-label">Courses</div>
|
||||
</div>
|
||||
<div class="stats-tile">
|
||||
<div class="stats-tile-value">{{ library_stats.completed_lessons }}/{{ library_stats.lessons_tracked }}</div>
|
||||
<div class="stats-tile-label">Lessons completed</div>
|
||||
</div>
|
||||
<div class="stats-tile">
|
||||
<div class="stats-tile-value">{{ library_stats.watched_display }}</div>
|
||||
<div class="stats-tile-label">Watched</div>
|
||||
</div>
|
||||
<div class="stats-tile">
|
||||
<div class="stats-tile-value">{{ library_stats.streak_days }}</div>
|
||||
<div class="stats-tile-label">Day streak</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if recently_added %}
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<h2>🆕 Recently Added</h2>
|
||||
<div style="margin-top: 12px;">
|
||||
{% for item in recently_added %}
|
||||
{{ render_course_row(item, item.added_display) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if continue_watching %}
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
|
||||
Reference in New Issue
Block a user