Add library browser feature
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>OfflineU - Select Course</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 40px; color: #222; }
|
||||
.container { max-width: 900px; margin: 0 auto; }
|
||||
.tabs { margin-bottom: 20px; }
|
||||
.tabs button {
|
||||
padding: 8px 16px; margin-right: 8px; cursor: pointer;
|
||||
border: 1px solid #ccc; background: #f5f5f5; border-radius: 4px;
|
||||
}
|
||||
.tabs button.active { background: #4CAF50; color: white; border-color: #4CAF50; }
|
||||
|
||||
.group { margin: 18px 0; }
|
||||
.group-title {
|
||||
font-size: 1.1em; font-weight: bold; margin-bottom: 6px;
|
||||
padding-bottom: 4px; border-bottom: 2px solid #4CAF50; color: #2e7d32;
|
||||
}
|
||||
.course-card {
|
||||
padding: 10px 14px; border: 1px solid #ddd; margin: 5px 0;
|
||||
cursor: pointer; border-radius: 4px; background: #e8f5e8;
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
}
|
||||
.course-card:hover { background-color: #d7ecd7; }
|
||||
.course-card .meta { color: #666; font-size: 0.85em; }
|
||||
|
||||
.directory { padding: 10px; border: 1px solid #ddd; margin: 5px 0; cursor: pointer; border-radius: 4px; }
|
||||
.directory:hover { background-color: #f0f0f0; }
|
||||
.course-candidate { background-color: #e8f5e8; }
|
||||
|
||||
.empty-state, .error-state {
|
||||
padding: 16px; border: 1px dashed #ccc; border-radius: 4px;
|
||||
color: #666; margin-top: 10px;
|
||||
}
|
||||
.error-state { border-color: #e57373; color: #b71c1c; background: #fff5f5; }
|
||||
.path-bar { font-size: 0.9em; color: #666; margin-bottom: 10px; }
|
||||
code { background: #f0f0f0; padding: 2px 5px; border-radius: 3px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>OfflineU - Course Selection</h1>
|
||||
|
||||
<div class="tabs">
|
||||
<button id="tab-library" class="active" onclick="showTab('library')">📚 Library</button>
|
||||
<button id="tab-filesystem" onclick="showTab('filesystem')">🗂️ Browse Filesystem</button>
|
||||
</div>
|
||||
|
||||
<div id="library-view">
|
||||
<div class="path-bar" id="library-path-bar"></div>
|
||||
<div id="library-groups"></div>
|
||||
</div>
|
||||
|
||||
<div id="filesystem-view" style="display:none;">
|
||||
<div id="browser"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function showTab(tab) {
|
||||
document.getElementById('tab-library').classList.toggle('active', tab === 'library');
|
||||
document.getElementById('tab-filesystem').classList.toggle('active', tab === 'filesystem');
|
||||
document.getElementById('library-view').style.display = tab === 'library' ? '' : 'none';
|
||||
document.getElementById('filesystem-view').style.display = tab === 'filesystem' ? '' : 'none';
|
||||
if (tab === 'filesystem' && !document.getElementById('browser').dataset.loaded) {
|
||||
document.getElementById('browser').dataset.loaded = '1';
|
||||
loadDirectories();
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Library view: grouped, click-to-load, no typed paths ----
|
||||
function loadLibrary() {
|
||||
fetch('/library')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
document.getElementById('library-path-bar').innerHTML =
|
||||
`Scanning: <code>${data.library_path}</code>`;
|
||||
|
||||
const container = document.getElementById('library-groups');
|
||||
const groupNames = Object.keys(data.groups).sort((a, b) => a.localeCompare(b));
|
||||
|
||||
if (data.errors && data.errors.length && groupNames.length === 0) {
|
||||
container.innerHTML = `<div class="error-state">
|
||||
Couldn't load the library at <code>${data.library_path}</code>.<br>
|
||||
${data.errors.map(e => e).join('<br>')}<br><br>
|
||||
Make sure that path is mounted into the container, or use
|
||||
"Browse Filesystem" to pick a course manually.
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
if (groupNames.length === 0) {
|
||||
container.innerHTML = `<div class="empty-state">
|
||||
No courses found under <code>${data.library_path}</code>.
|
||||
Drop course folders in there, or use "Browse Filesystem" instead.
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = groupNames.map(group => `
|
||||
<div class="group">
|
||||
<div class="group-title">📁 ${group}</div>
|
||||
${data.groups[group].map(course => `
|
||||
<div class="course-card" onclick="loadCourse('${course.path.replace(/'/g, "\\'")}')">
|
||||
<span>🎓 ${course.name}</span>
|
||||
<span class="meta">${course.media_files} media file${course.media_files === 1 ? '' : 's'}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`).join('');
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Filesystem view: original full-path browser (fallback) ----
|
||||
function loadDirectories(path = '') {
|
||||
fetch(`/browse?path=${encodeURIComponent(path)}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
const browser = document.getElementById('browser');
|
||||
browser.innerHTML = `
|
||||
<h3>Current: ${data.current_path}</h3>
|
||||
${data.parent_path ? `<div class="directory" onclick="loadDirectories('${data.parent_path}')">📁 .. (Parent)</div>` : ''}
|
||||
${data.directories.map(dir => `
|
||||
<div class="directory ${dir.is_course_candidate ? 'course-candidate' : ''}"
|
||||
onclick="${dir.is_course_candidate ? `loadCourse('${dir.path}')` : `loadDirectories('${dir.path}')`}">
|
||||
📁 ${dir.name} ${dir.media_files > 0 ? `(${dir.media_files} media files)` : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
function loadCourse(path) {
|
||||
fetch('/load_course', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({course_path: path})
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error: ' + data.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadLibrary();
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user