Adding ability to hide / show courses
This commit is contained in:
@@ -203,6 +203,58 @@
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.btn-secondary:hover { background: var(--bg-tertiary-hover); }
|
||||
.btn-sm {
|
||||
padding: 5px 12px;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
.curate-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 8px 10px;
|
||||
border-radius: var(--radius);
|
||||
background: var(--bg-tertiary);
|
||||
margin-bottom: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.curate-row:hover {
|
||||
background: var(--bg-tertiary-hover);
|
||||
}
|
||||
.curate-row.is-hidden {
|
||||
opacity: 0.55;
|
||||
}
|
||||
.curate-row-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.curate-row-name span.name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.curate-toggle {
|
||||
font-size: 1.1em;
|
||||
width: 18px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.curate-children {
|
||||
margin-left: 22px;
|
||||
margin-top: 4px;
|
||||
display: none;
|
||||
}
|
||||
.curate-children.expanded {
|
||||
display: block;
|
||||
}
|
||||
.curate-empty-hint {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9em;
|
||||
padding: 6px 0;
|
||||
}
|
||||
#save-status {
|
||||
font-size: 0.9em;
|
||||
color: #28a745;
|
||||
@@ -331,6 +383,23 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Curate Library</h2>
|
||||
<p class="setting-desc" style="margin-bottom: 12px;">
|
||||
Hide courses or whole folders from the Library browser without touching anything on disk.
|
||||
Hiding a folder hides everything inside it.
|
||||
</p>
|
||||
|
||||
<div id="hidden-list-wrap" style="margin-bottom: 15px; display: none;">
|
||||
<div style="font-weight: 600; margin-bottom: 8px; font-size: 0.9em; color: var(--text-muted);">Currently hidden</div>
|
||||
<div id="hidden-list"></div>
|
||||
</div>
|
||||
|
||||
<div style="font-weight: 600; margin-bottom: 8px; font-size: 0.9em; color: var(--text-muted);">Browse to hide</div>
|
||||
<div id="curate-path-bar" style="color: var(--text-muted); font-size: 13px; margin-bottom: 8px;"></div>
|
||||
<div id="curate-tree"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Load a Course Manually</h2>
|
||||
<div class="setting-row" style="flex-direction: column; align-items: stretch; gap: 8px;">
|
||||
@@ -431,6 +500,129 @@
|
||||
}
|
||||
});
|
||||
|
||||
// ---- Library curation: hide/show courses & folders ----
|
||||
function loadHiddenList() {
|
||||
fetch('/api/hidden-paths')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
const wrap = document.getElementById('hidden-list-wrap');
|
||||
const list = document.getElementById('hidden-list');
|
||||
const items = data.hidden_paths || [];
|
||||
if (items.length === 0) {
|
||||
wrap.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
wrap.style.display = 'block';
|
||||
list.innerHTML = items.map(item => `
|
||||
<div class="curate-row">
|
||||
<div class="curate-row-name">
|
||||
<span>🚫</span>
|
||||
<span class="name">${item.name}</span>
|
||||
</div>
|
||||
<button class="btn btn-secondary btn-sm" onclick="toggleHidden('${item.path.replace(/'/g, "\\'")}', false)">Show</button>
|
||||
</div>
|
||||
`).join('');
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
function loadCurateLevel(path, container, isRoot) {
|
||||
const url = path ? `/library/manage?path=${encodeURIComponent(path)}` : '/library/manage';
|
||||
fetch(url)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (isRoot) {
|
||||
document.getElementById('curate-path-bar').textContent = `Browsing ${data.library_path}`;
|
||||
}
|
||||
renderCurateLevel(data, container);
|
||||
})
|
||||
.catch(() => {
|
||||
container.innerHTML = '<p style="color:#ff6b6b;">Could not reach the library scanner.</p>';
|
||||
});
|
||||
}
|
||||
|
||||
function renderCurateLevel(data, container) {
|
||||
if (!data.items || data.items.length === 0) {
|
||||
const reason = (data.errors && data.errors.length) ? data.errors.join(' ') : 'Nothing here.';
|
||||
container.innerHTML = `<div class="curate-empty-hint">${reason}</div>`;
|
||||
return;
|
||||
}
|
||||
container.innerHTML = data.items.map(item => {
|
||||
const safePath = item.path.replace(/'/g, "\\'");
|
||||
const icon = item.type === 'course' ? '🎓' : '📁';
|
||||
const hiddenClass = item.hidden ? 'is-hidden' : '';
|
||||
const toggleBtn = `<button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); toggleHidden('${safePath}', ${!item.hidden}, this)">${item.hidden ? 'Show' : 'Hide'}</button>`;
|
||||
|
||||
if (item.type === 'course') {
|
||||
return `
|
||||
<div class="curate-row ${hiddenClass}">
|
||||
<div class="curate-row-name">
|
||||
<span>${icon}</span>
|
||||
<span class="name">${item.name}</span>
|
||||
</div>
|
||||
${toggleBtn}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return `
|
||||
<div class="curate-row ${hiddenClass}" onclick="toggleCurateDir(this, '${safePath}')">
|
||||
<div class="curate-row-name">
|
||||
<span class="curate-toggle">▶</span>
|
||||
<span>${icon}</span>
|
||||
<span class="name">${item.name}</span>
|
||||
</div>
|
||||
${toggleBtn}
|
||||
</div>
|
||||
<div class="curate-children" data-loaded="false"></div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function toggleCurateDir(rowEl, path) {
|
||||
const content = rowEl.nextElementSibling;
|
||||
const toggleIcon = rowEl.querySelector('.curate-toggle');
|
||||
if (!content || !content.classList.contains('curate-children')) return;
|
||||
|
||||
if (content.classList.contains('expanded')) {
|
||||
content.classList.remove('expanded');
|
||||
if (toggleIcon) toggleIcon.textContent = '▶';
|
||||
return;
|
||||
}
|
||||
content.classList.add('expanded');
|
||||
if (toggleIcon) toggleIcon.textContent = '▼';
|
||||
|
||||
if (content.dataset.loaded === 'true') return;
|
||||
content.innerHTML = '<div class="curate-empty-hint">Loading...</div>';
|
||||
content.dataset.loaded = 'true';
|
||||
content.dataset.path = path;
|
||||
loadCurateLevel(path, content, false);
|
||||
}
|
||||
|
||||
function toggleHidden(path, hidden, btnEl) {
|
||||
fetch('/api/hidden-paths', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path: path, hidden: hidden })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
loadHiddenList();
|
||||
// Refresh just the level this toggle happened in, rather
|
||||
// than collapsing the whole tree back to root.
|
||||
const container = btnEl ? (btnEl.closest('.curate-children') || document.getElementById('curate-tree'))
|
||||
: document.getElementById('curate-tree');
|
||||
const isRootContainer = container.id === 'curate-tree';
|
||||
const refreshPath = isRootContainer ? null : container.dataset.path;
|
||||
loadCurateLevel(refreshPath, container, isRootContainer);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
loadHiddenList();
|
||||
loadCurateLevel(null, document.getElementById('curate-tree'), true);
|
||||
|
||||
function saveLibraryPath() {
|
||||
const input = document.getElementById('library_path');
|
||||
const status = document.getElementById('library-path-status');
|
||||
|
||||
Reference in New Issue
Block a user