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:
@@ -359,6 +359,12 @@
|
||||
<span id="prewarm-durations-status" style="font-size: 0.85em; color: var(--text-muted);"></span>
|
||||
</div>
|
||||
<span class="setting-desc">Reads every video/audio file's length and generates cover art for any course without one, up front - so course cards in the Library show their runtime and artwork immediately instead of computing it the first time each course is viewed.</span>
|
||||
|
||||
<div style="display: flex; align-items: center; gap: 10px; margin-top: 10px;">
|
||||
<button class="btn btn-secondary btn-sm" id="regen-thumbnails-btn" onclick="startThumbnailRegen()">{{ icons.icon('refresh', 14) }} Regenerate Thumbnails</button>
|
||||
<span id="regen-thumbnails-status" style="font-size: 0.85em; color: var(--text-muted);"></span>
|
||||
</div>
|
||||
<span class="setting-desc">Re-runs auto-generated cover art from scratch for every course that has one (not manually-placed covers) - useful after an improvement to how thumbnails are picked, since existing ones are otherwise cached forever.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -412,6 +418,12 @@
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label for="autoplay_next">Auto-play next lesson
|
||||
<span class="setting-desc">When a lesson finishes, automatically move on to the next one after a few seconds (cancelable)</span>
|
||||
</label>
|
||||
<input type="checkbox" id="autoplay_next" data-setting="autoplay_next" {% if settings.autoplay_next %}checked{% endif %}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
@@ -541,6 +553,10 @@
|
||||
el.addEventListener('change', () => saveSetting(el.dataset.setting, el.value));
|
||||
});
|
||||
|
||||
document.querySelectorAll('input[type="checkbox"][data-setting]').forEach(el => {
|
||||
el.addEventListener('change', () => saveSetting(el.dataset.setting, el.checked));
|
||||
});
|
||||
|
||||
const accentPicker = document.getElementById('accent-picker');
|
||||
const accentHex = document.getElementById('accent-hex');
|
||||
|
||||
@@ -648,6 +664,70 @@
|
||||
.catch(() => {});
|
||||
})();
|
||||
|
||||
function updateThumbnailRegenStatus(data) {
|
||||
const btn = document.getElementById('regen-thumbnails-btn');
|
||||
const status = document.getElementById('regen-thumbnails-status');
|
||||
if (data.running) {
|
||||
btn.disabled = true;
|
||||
status.style.color = 'var(--text-muted)';
|
||||
status.textContent = `Regenerating… ${data.done}/${data.total}`;
|
||||
return;
|
||||
}
|
||||
btn.disabled = false;
|
||||
if (data.error) {
|
||||
status.style.color = 'var(--error)';
|
||||
status.textContent = `Error: ${data.error}`;
|
||||
} else if (data.total) {
|
||||
status.style.color = 'var(--success)';
|
||||
status.innerHTML = `${ICON_SVGS.check} Regenerated ${data.total} thumbnail${data.total === 1 ? '' : 's'}`;
|
||||
setTimeout(() => { status.textContent = ''; }, 5000);
|
||||
} else {
|
||||
status.style.color = 'var(--text-muted)';
|
||||
status.textContent = 'No auto-generated thumbnails to regenerate';
|
||||
setTimeout(() => { status.textContent = ''; }, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function pollThumbnailRegenStatus() {
|
||||
fetch('/api/library/regenerate-thumbnails/status')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
updateThumbnailRegenStatus(data);
|
||||
if (data.running) setTimeout(pollThumbnailRegenStatus, 1500);
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
function startThumbnailRegen() {
|
||||
const status = document.getElementById('regen-thumbnails-status');
|
||||
document.getElementById('regen-thumbnails-btn').disabled = true;
|
||||
status.style.color = 'var(--text-muted)';
|
||||
status.textContent = 'Starting…';
|
||||
fetch('/api/library/regenerate-thumbnails', { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
updateThumbnailRegenStatus(data);
|
||||
if (data.running) setTimeout(pollThumbnailRegenStatus, 1500);
|
||||
})
|
||||
.catch(() => {
|
||||
status.style.color = 'var(--error)';
|
||||
status.textContent = 'Could not reach the server';
|
||||
document.getElementById('regen-thumbnails-btn').disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
(function resumeThumbnailRegenStatusIfRunning() {
|
||||
fetch('/api/library/regenerate-thumbnails/status')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.running) {
|
||||
updateThumbnailRegenStatus(data);
|
||||
setTimeout(pollThumbnailRegenStatus, 1500);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
})();
|
||||
|
||||
function handleRestoreFileChosen(input) {
|
||||
const file = input.files && input.files[0];
|
||||
input.value = ''; // allow re-choosing the same file later
|
||||
|
||||
Reference in New Issue
Block a user