New mobile layout, new regular screen, added the ability to edit directory names from settings
This commit is contained in:
+144
-41
@@ -248,6 +248,39 @@
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.library-breadcrumb {
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
margin-top: 6px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.library-breadcrumb .crumb {
|
||||
cursor: pointer;
|
||||
max-width: 160px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.library-breadcrumb .crumb:not(.current):hover {
|
||||
color: var(--accent);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.library-breadcrumb .crumb.current {
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.library-breadcrumb .crumb-sep {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Dynamic Directory Tree Styles */
|
||||
.tree-container {
|
||||
margin-top: 20px;
|
||||
@@ -295,22 +328,31 @@
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tree-icon {
|
||||
font-size: 1.2em;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tree-name {
|
||||
font-weight: 500;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tree-stats {
|
||||
font-size: 0.85em;
|
||||
color: var(--text-muted);
|
||||
margin-left: 10px;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tree-toggle {
|
||||
@@ -405,10 +447,20 @@
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.lesson-icon {
|
||||
font-size: 1.1em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.lesson-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.lesson-meta {
|
||||
@@ -417,6 +469,16 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
max-width: 45%;
|
||||
}
|
||||
|
||||
.lesson-meta-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.lesson-type {
|
||||
@@ -473,6 +535,29 @@
|
||||
margin-left: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.lesson-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.lesson-title,
|
||||
.lesson-meta {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.lesson-name,
|
||||
.lesson-meta-text {
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
white-space: normal;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -563,7 +648,7 @@
|
||||
{% elif lesson.lesson_type == 'mixed' %}📦
|
||||
{% else %}📄{% endif %}
|
||||
</span>
|
||||
{{ lesson.title }}
|
||||
<span class="lesson-name">{{ lesson.title }}</span>
|
||||
</div>
|
||||
<div class="lesson-meta">
|
||||
<span class="lesson-type {{ lesson.lesson_type }}">{{ lesson.lesson_type|title }}</span>
|
||||
@@ -599,7 +684,7 @@
|
||||
onclick="window.location.href='/recent/open?course_path={{ view.course_path | urlencode }}&lesson_path={{ view.lesson_path | urlencode }}'">
|
||||
<div class="lesson-title">
|
||||
<span class="lesson-icon">▶️</span>
|
||||
<span>{{ view.lesson_title }}</span>
|
||||
<span class="lesson-name">{{ view.lesson_title }}</span>
|
||||
</div>
|
||||
<div class="lesson-meta">
|
||||
{% if view.completed %}
|
||||
@@ -607,7 +692,7 @@
|
||||
{% elif view.percent_watched %}
|
||||
<span class="watched-badge">{{ view.percent_watched }}% watched</span>
|
||||
{% endif %}
|
||||
<span>{{ view.course_name }}{% if view.viewed_display %} · {{ view.viewed_display }}{% endif %}</span>
|
||||
<span class="lesson-meta-text">{{ view.course_name }}{% if view.viewed_display %} · {{ view.viewed_display }}{% endif %}</span>
|
||||
</div>
|
||||
{% if view.percent_watched %}
|
||||
<div class="lesson-progress-track"><div class="lesson-progress-fill" style="width: {{ view.percent_watched }}%;"></div></div>
|
||||
@@ -621,7 +706,7 @@
|
||||
<div class="container">
|
||||
<div class="card" id="library-card">
|
||||
<h2>Your Courses</h2>
|
||||
<p id="library-path-bar" style="color: #999; font-size: 13px; margin-top: 6px;"></p>
|
||||
<p id="library-path-bar" class="library-breadcrumb"></p>
|
||||
<div id="library-groups" style="margin-top: 15px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -665,22 +750,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Single-level drill-down: the Library browser shows one folder's
|
||||
// contents at a time (replacing the list, not nesting under it) and
|
||||
// tracks how we got here in libraryTrail so a breadcrumb can jump
|
||||
// back up. Much less horizontal space wasted than the old
|
||||
// indent-per-level accordion, especially on narrow screens.
|
||||
let libraryTrail = []; // [{name, path}], root is implicit (not in the array)
|
||||
|
||||
function loadLibrary() {
|
||||
const libraryCard = document.getElementById('library-card');
|
||||
if (!libraryCard) return;
|
||||
fetchLibraryLevel(null, document.getElementById('library-groups'), true);
|
||||
libraryTrail = [];
|
||||
fetchLibraryLevel(null);
|
||||
}
|
||||
|
||||
function fetchLibraryLevel(path, container, isRoot) {
|
||||
function fetchLibraryLevel(path) {
|
||||
const container = document.getElementById('library-groups');
|
||||
container.innerHTML = '<p style="color:#999; padding: 6px 0;">Loading...</p>';
|
||||
const url = path ? `/library?path=${encodeURIComponent(path)}` : '/library';
|
||||
fetch(url)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (isRoot) {
|
||||
document.getElementById('library-path-bar').textContent =
|
||||
`Scanning ${data.library_path}`;
|
||||
}
|
||||
renderLibraryLevel(data, container, isRoot);
|
||||
renderLibraryBreadcrumb();
|
||||
renderLibraryLevel(data);
|
||||
})
|
||||
.catch(() => {
|
||||
container.innerHTML =
|
||||
@@ -688,7 +780,35 @@
|
||||
});
|
||||
}
|
||||
|
||||
function renderLibraryLevel(data, container, isRoot) {
|
||||
function renderLibraryBreadcrumb() {
|
||||
const bar = document.getElementById('library-path-bar');
|
||||
const atRoot = libraryTrail.length === 0;
|
||||
let html = `<span class="crumb ${atRoot ? 'current' : ''}"` +
|
||||
(atRoot ? '' : ` onclick="goToLibraryCrumb(-1)"`) + `>Library</span>`;
|
||||
libraryTrail.forEach((crumb, i) => {
|
||||
const isLast = i === libraryTrail.length - 1;
|
||||
html += ` <span class="crumb-sep">/</span> ` +
|
||||
`<span class="crumb ${isLast ? 'current' : ''}"` +
|
||||
(isLast ? '' : ` onclick="goToLibraryCrumb(${i})"`) +
|
||||
`>${crumb.name}</span>`;
|
||||
});
|
||||
bar.innerHTML = html;
|
||||
}
|
||||
|
||||
function goToLibraryCrumb(index) {
|
||||
if (index < 0) {
|
||||
libraryTrail = [];
|
||||
fetchLibraryLevel(null);
|
||||
return;
|
||||
}
|
||||
const target = libraryTrail[index];
|
||||
libraryTrail = libraryTrail.slice(0, index + 1);
|
||||
fetchLibraryLevel(target.path);
|
||||
}
|
||||
|
||||
function renderLibraryLevel(data) {
|
||||
const container = document.getElementById('library-groups');
|
||||
const isRoot = libraryTrail.length === 0;
|
||||
if (!data.items || data.items.length === 0) {
|
||||
const reason = (data.errors && data.errors.length)
|
||||
? data.errors.join(' ')
|
||||
@@ -698,52 +818,35 @@
|
||||
}
|
||||
|
||||
container.innerHTML = data.items.map(item => {
|
||||
const safePath = item.path.replace(/'/g, "\\'");
|
||||
if (item.type === 'course') {
|
||||
return `
|
||||
<div class="lesson-item"
|
||||
onclick="loadCoursePath('${item.path.replace(/'/g, "\\'")}')">
|
||||
onclick="loadCoursePath('${safePath}')">
|
||||
<div class="lesson-title">
|
||||
<span class="lesson-icon">🎓</span>
|
||||
<span>${item.name}</span>
|
||||
<span class="lesson-name">${item.name}</span>
|
||||
</div>
|
||||
<span class="lesson-meta">${item.media_files} media file${item.media_files === 1 ? '' : 's'}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
const safeName = item.name.replace(/'/g, "\\'");
|
||||
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 class="tree-header directory" onclick="enterLibraryDir('${safePath}', '${safeName}')">
|
||||
<div class="tree-title">
|
||||
<span class="tree-icon">📁</span>
|
||||
<span class="tree-name">${item.name}</span>
|
||||
</div>
|
||||
<div class="tree-content" data-loaded="false"></div>
|
||||
<span class="tree-toggle">›</span>
|
||||
</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 enterLibraryDir(path, name) {
|
||||
libraryTrail.push({ name: name, path: path });
|
||||
fetchLibraryLevel(path);
|
||||
}
|
||||
|
||||
function loadCoursePath(path) {
|
||||
|
||||
Reference in New Issue
Block a user