Add Favorites, storage drill-down, keyboard shortcuts, and What's New
- Favorites: star toggle on course rows, the course page, and a new dashboard card; rebased on rename/move, backed up/restored, and caught by stale-reference cleanup (also fixed a pre-existing gap where favorites.json wasn't in the backup file list). - Storage Usage: click a folder row to drill into its contents one level at a time, with a Back button. - Lesson player: added ,/. (speed step), [/] (prev/next lesson), and F (fullscreen) keyboard shortcuts. - Settings: a CHANGELOG.md-backed "What's New" list, since VERSION alone only ever shows the current build. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -728,6 +728,13 @@
|
||||
color: white;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.favorite-toggle-btn.active {
|
||||
color: #e0a825;
|
||||
border-color: #e0a825;
|
||||
}
|
||||
.favorite-toggle-btn.active svg {
|
||||
fill: currentColor;
|
||||
}
|
||||
#library-select-btn {
|
||||
border-radius: var(--radius);
|
||||
margin-left: 4px;
|
||||
@@ -1044,6 +1051,9 @@
|
||||
<button class="btn btn-secondary btn-sm" id="queue-toggle-btn" onclick="toggleQueued()">
|
||||
{{ icons.icon('pin', 14) }} {{ 'Remove from Next Up' if is_queued else 'Add to Next Up' }}
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-sm{{ ' favorite-toggle-btn active' if is_favorited else ' favorite-toggle-btn' }}" id="favorite-toggle-btn" onclick="toggleFavorited()">
|
||||
{{ icons.icon('star', 14) }} {{ 'Remove from Favorites' if is_favorited else 'Add to Favorites' }}
|
||||
</button>
|
||||
{% if has_note %}
|
||||
<a class="btn btn-secondary btn-sm" href="/course/study-guide">{{ icons.icon('file-text', 14) }} Download Study Guide</a>
|
||||
{% endif %}
|
||||
@@ -1249,6 +1259,31 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if favorites %}
|
||||
<div class="container">
|
||||
<div class="card" id="favorites-card">
|
||||
<h2 class="collapsible-header" data-card-id="favorites"><span class="card-header-label">{{ icons.icon('star') }}Favorites</span></h2>
|
||||
<div class="card-body" id="favorites-list">
|
||||
{% for item in favorites %}
|
||||
<div class="lesson-item" data-path="{{ item.path }}">
|
||||
<div class="lesson-title" style="cursor: pointer;" onclick="loadCoursePath('{{ item.path|replace("'", "\\'") }}')">
|
||||
{% if item.has_thumbnail %}
|
||||
<img class="lesson-thumb" src="/library/thumbnail?path={{ item.path | urlencode }}" alt=""
|
||||
onerror="this.replaceWith(courseFallbackIcon('graduation-cap'))">
|
||||
{% else %}
|
||||
<span class="lesson-icon">{{ icons.icon('graduation-cap', 16) }}</span>
|
||||
{% endif %}
|
||||
<span class="lesson-name">{{ item.name }}</span>
|
||||
</div>
|
||||
<div class="lesson-meta">
|
||||
<button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); removeFavorite(this)" title="Remove from Favorites">{{ icons.icon('x', 14) }}</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if next_up %}
|
||||
<div class="container">
|
||||
<div class="card" id="next-up-card">
|
||||
@@ -1346,6 +1381,8 @@
|
||||
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>',
|
||||
star: '<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="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg>',
|
||||
'star-filled': '<svg class="icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></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>',
|
||||
};
|
||||
|
||||
@@ -1708,6 +1745,7 @@
|
||||
const progressBar = pct
|
||||
? `<div class="lesson-progress-track"><div class="lesson-progress-fill" style="width: ${pct}%;"></div></div>`
|
||||
: '';
|
||||
const favorited = !!item.favorited;
|
||||
return `
|
||||
<div class="lesson-item" onclick="${clickHandler}">
|
||||
<div class="lesson-title">
|
||||
@@ -1718,6 +1756,7 @@
|
||||
<div class="lesson-meta">
|
||||
${progressBadge}
|
||||
<span>${item.media_files} media file${item.media_files === 1 ? '' : 's'}${item.duration_display ? ` · ${item.duration_display}` : ''}</span>
|
||||
<button class="btn btn-secondary btn-sm favorite-toggle-btn${favorited ? ' active' : ''}" data-path="${escapeAttr(item.path)}" onclick="event.stopPropagation(); favoriteCourse('${safePath}', this)" title="${favorited ? 'Remove from Favorites' : 'Add to Favorites'}">${favorited ? ICON_SVGS['star-filled'] : ICON_SVGS.star}</button>
|
||||
<button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); queueCourse('${safePath}', this)" title="Add to Next Up">${ICON_SVGS.pin}</button>
|
||||
</div>
|
||||
${progressBar}
|
||||
@@ -1889,6 +1928,50 @@
|
||||
saveNextUpOrder();
|
||||
}
|
||||
|
||||
function removeFavorite(btnEl) {
|
||||
const row = btnEl.closest('.lesson-item');
|
||||
const path = row.dataset.path;
|
||||
fetch('/api/favorites', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path: path, favorited: false })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
row.remove();
|
||||
const list = document.getElementById('favorites-list');
|
||||
if (list && list.children.length === 0) {
|
||||
document.getElementById('favorites-card').closest('.container').remove();
|
||||
}
|
||||
document.querySelectorAll(`.favorite-toggle-btn[data-path="${CSS.escape(path)}"]`).forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
btn.innerHTML = ICON_SVGS.star;
|
||||
btn.title = 'Add to Favorites';
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
function favoriteCourse(path, btnEl) {
|
||||
const nowFavorited = !btnEl.classList.contains('active');
|
||||
fetch('/api/favorites', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path: path, favorited: nowFavorited })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
btnEl.classList.toggle('active', nowFavorited);
|
||||
btnEl.innerHTML = nowFavorited ? ICON_SVGS['star-filled'] : ICON_SVGS.star;
|
||||
btnEl.title = nowFavorited ? 'Remove from Favorites' : 'Add to Favorites';
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
function removeNextUp(btnEl) {
|
||||
const row = btnEl.closest('.lesson-item');
|
||||
const path = row.dataset.path;
|
||||
@@ -1940,6 +2023,24 @@
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
function toggleFavorited() {
|
||||
const btn = document.getElementById('favorite-toggle-btn');
|
||||
const currentlyFavorited = btn.classList.contains('active');
|
||||
fetch('/api/favorites', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path: {{ course.path|tojson }}, favorited: !currentlyFavorited })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
btn.classList.toggle('active', !currentlyFavorited);
|
||||
btn.innerHTML = ICON_SVGS.star + ' ' + (currentlyFavorited ? 'Add to Favorites' : 'Remove from Favorites');
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
// ---- Collapsible dashboard cards (client-only, remembered per device) ----
|
||||
|
||||
Reference in New Issue
Block a user