Add autoplay, progress rings, duplicate-lesson detection, title-card thumbnails

- Auto-play next lesson on end, with a cancelable countdown and a
  Settings toggle (default on).
- Grid-view course cards show a small progress ring (checkmark at
  100%) instead of a separate bar.
- Duplicate Lesson Files: scans within each course/folder for media
  files that look like the same lesson downloaded twice, with per-file
  delete and a shared ignore list with Duplicate Courses.
- Auto-generated cover art now samples a few early candidate frames
  and keeps the largest JPEG, favoring an intro title card over a
  blank fade-in or a plain presenter frame.
- Settings -> "Regenerate Thumbnails" re-runs that logic for every
  course with an auto-generated thumbnail (never touches manual
  covers), so already-cached thumbnails can pick up the improvement.
- Add .gitignore for __pycache__/.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-24 20:20:36 -04:00
co-authored by Claude Sonnet 5
parent fc83876abe
commit 36d9e7f89b
9 changed files with 531 additions and 40 deletions
+29 -13
View File
@@ -782,6 +782,7 @@
transform: translateY(-2px);
}
.grid-card-thumb-wrap {
position: relative;
width: 100%;
aspect-ratio: 1 / 1;
border-radius: var(--radius);
@@ -822,15 +823,32 @@
font-size: 0.75em;
color: var(--text-muted);
}
.grid-card-progress-track {
height: 3px;
border-radius: 2px;
background: rgba(255, 255, 255, 0.08);
overflow: hidden;
.grid-card-progress-ring {
position: absolute;
bottom: 6px;
right: 6px;
width: 26px;
height: 26px;
border-radius: 50%;
background: conic-gradient(var(--accent) calc(var(--pct) * 1%), rgba(0, 0, 0, 0.4) 0);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}
.grid-card-progress-fill {
height: 100%;
background: var(--accent);
.grid-card-progress-ring::before {
content: '';
position: absolute;
inset: 3px;
border-radius: 50%;
background: var(--bg-secondary);
}
.grid-card-progress-ring-check {
position: relative;
color: var(--accent);
font-size: 12px;
font-weight: 700;
line-height: 1;
}
.transcript-search-toggle {
@@ -1704,19 +1722,17 @@
: courseInitialHtml(item.name);
const clickHandler = selectionMode ? `toggleSelection('${safePath}')` : `loadCoursePath('${safePath}')`;
const pct = item.completion_percentage;
const progressBar = pct
? `<div class="grid-card-progress-track"><div class="grid-card-progress-fill" style="width: ${pct}%;"></div></div>`
const ringHtml = pct
? `<div class="grid-card-progress-ring" style="--pct: ${pct};" title="${pct}% complete">${pct >= 100 ? '<span class="grid-card-progress-ring-check">✓</span>' : ''}</div>`
: '';
const metaBits = [`${item.media_files} media file${item.media_files === 1 ? '' : 's'}`];
if (item.duration_display) metaBits.push(item.duration_display);
if (pct) metaBits.push(`${pct}% done`);
return `
<div class="library-grid-card" onclick="${clickHandler}">
${selectableAttrs(item)}
<div class="grid-card-thumb-wrap">${iconHtml}</div>
<div class="grid-card-thumb-wrap">${iconHtml}${ringHtml}</div>
<div class="grid-card-name" title="${escapeAttr(item.name)}">${item.name}</div>
<div class="grid-card-meta">${metaBits.join(' · ')}</div>
${progressBar}
</div>
`;
}