New main page way of browsing the courses

This commit is contained in:
2026-08-20 18:48:17 -04:00
parent 5c5b068c4c
commit 66dff7e69f
3 changed files with 187 additions and 97 deletions
+68 -39
View File
@@ -680,55 +680,84 @@
function loadLibrary() {
const libraryCard = document.getElementById('library-card');
if (!libraryCard) return;
fetchLibraryLevel(null, document.getElementById('library-groups'), true);
}
fetch('/library')
function fetchLibraryLevel(path, container, isRoot) {
const url = path ? `/library?path=${encodeURIComponent(path)}` : '/library';
fetch(url)
.then(r => r.json())
.then(data => {
document.getElementById('library-path-bar').textContent =
`Scanning ${data.library_path}`;
const container = document.getElementById('library-groups');
const groupNames = Object.keys(data.groups).sort((a, b) => a.localeCompare(b));
if (groupNames.length === 0) {
const reason = (data.errors && data.errors.length)
? data.errors.join(' ')
: `No course folders found under ${data.library_path}.`;
container.innerHTML = `<p style="color:#999;">${reason} You can still load a course by path below.</p>`;
return;
if (isRoot) {
document.getElementById('library-path-bar').textContent =
`Scanning ${data.library_path}`;
}
container.innerHTML = groupNames.map((group, i) => `
<div class="tree-item">
<div class="tree-header directory" onclick="toggleTree(this)">
<div class="tree-title">
<span class="tree-icon">📁</span>
<span class="tree-name">${group}</span>
<span class="tree-stats">${data.groups[group].length} course${data.groups[group].length === 1 ? '' : 's'}</span>
</div>
<button class="tree-toggle">▶</button>
</div>
<div class="tree-content">
${data.groups[group].map(course => `
<div class="lesson-item"
onclick="loadCoursePath('${course.path.replace(/'/g, "\\'")}')">
<div class="lesson-title">
<span class="lesson-icon">🎓</span>
<span>${course.name}</span>
</div>
<span class="lesson-meta">${course.media_files} media file${course.media_files === 1 ? '' : 's'}</span>
</div>
`).join('')}
</div>
</div>
`).join('');
renderLibraryLevel(data, container, isRoot);
})
.catch(() => {
document.getElementById('library-groups').innerHTML =
container.innerHTML =
'<p style="color:#ff6b6b;">Could not reach the library scanner.</p>';
});
}
function renderLibraryLevel(data, container, isRoot) {
if (!data.items || data.items.length === 0) {
const reason = (data.errors && data.errors.length)
? data.errors.join(' ')
: (isRoot ? `No course folders found under ${data.library_path}.` : 'No courses in here.');
container.innerHTML = `<p style="color:#999; padding: 6px 0;">${reason}${isRoot ? ' You can still load a course by path below.' : ''}</p>`;
return;
}
container.innerHTML = data.items.map(item => {
if (item.type === 'course') {
return `
<div class="lesson-item"
onclick="loadCoursePath('${item.path.replace(/'/g, "\\'")}')">
<div class="lesson-title">
<span class="lesson-icon">🎓</span>
<span>${item.name}</span>
</div>
<span class="lesson-meta">${item.media_files} media file${item.media_files === 1 ? '' : 's'}</span>
</div>
`;
}
return `
<div class="tree-item">
<div class="tree-header directory" onclick="toggleLibraryDir(this, '${item.path.replace(/'/g, "\\'")}')">
<div class="tree-title">
<span class="tree-icon">📁</span>
<span class="tree-name">${item.name}</span>
</div>
<button class="tree-toggle">▶</button>
</div>
<div class="tree-content" data-loaded="false"></div>
</div>
`;
}).join('');
}
function toggleLibraryDir(headerEl, path) {
const content = headerEl.nextElementSibling;
const toggle = headerEl.querySelector('.tree-toggle');
if (!content || !content.classList.contains('tree-content')) return;
if (content.classList.contains('expanded')) {
content.classList.remove('expanded');
toggle.textContent = '▶';
return;
}
content.classList.add('expanded');
toggle.textContent = '▼';
if (content.dataset.loaded === 'true') return; // already fetched this branch
content.innerHTML = '<p style="color:#999; padding: 6px 0;">Loading...</p>';
content.dataset.loaded = 'true';
fetchLibraryLevel(path, content, false);
}
function loadCoursePath(path) {
fetch('/load_course', {
method: 'POST',
+40
View File
@@ -312,6 +312,21 @@
</div>
</div>
<div class="card">
<h2>Library</h2>
<div class="setting-row" style="flex-direction: column; align-items: stretch; gap: 8px;">
<label for="library_path">Default courses directory
<span class="setting-desc">Where the Library browser starts. Leave blank to use the server default ({{ default_library_path }}).</span>
</label>
<div style="display: flex; gap: 10px;">
<input type="text" id="library_path" class="hex-input" style="flex: 1; width: auto; font-family: var(--font-family);"
placeholder="{{ default_library_path }}" value="{{ settings.library_path }}">
<button class="btn" onclick="saveLibraryPath()">Save</button>
</div>
<span id="library-path-status" style="font-size: 0.85em; min-height: 1.2em;"></span>
</div>
</div>
<div class="actions">
<button class="btn btn-secondary" onclick="resetSettings()">Reset to Defaults</button>
<span id="save-status">✓ Saved</span>
@@ -380,6 +395,31 @@
}
});
function saveLibraryPath() {
const input = document.getElementById('library_path');
const status = document.getElementById('library-path-status');
fetch('/api/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ library_path: input.value.trim() })
})
.then(r => r.json())
.then(data => {
if (data.success) {
status.style.color = '#28a745';
status.textContent = '✓ Saved — reload the main page to see it take effect';
showSaved();
} else {
status.style.color = '#ff6b6b';
status.textContent = data.error || 'Could not save';
}
})
.catch(err => {
status.style.color = '#ff6b6b';
status.textContent = 'Could not reach the server';
});
}
function resetSettings() {
fetch('/api/settings/reset', { method: 'POST' })
.then(r => r.json())