Three visual/structural changes: - Grid cards without a thumbnail (nearly all real courses in this library) showed the same generic graduation-cap icon, making the poster grid hard to visually scan. Replace it with a deterministic color+initial per course name (same idea as Slack/Google Photos default avatars), so cards without real cover art still look distinct from each other. - Notes/Help/Settings only existed as small footer text links, meaning a scroll to the bottom of every page to navigate. Add a persistent bottom tab bar (Home/Notes/Help/Settings, current page highlighted via a new active_tab context var) to the dashboard, Notes Hub, Settings, and Help pages. Deliberately left the lesson page alone - it already has its own fixed-bottom notes panel, and a second fixed bar there would just eat into an already content-dense page. - The no-course dashboard stacked five informational/queue cards (Library Stats, Next Up, Pick Back Up, Recently Added, Continue Watching, Recently Viewed) before "Your Courses," so the actual library browser was the last thing on the page. Move it up to right after the Surprise Me banner - it was already unconditional, so this only changes where it renders, not when. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
297 lines
10 KiB
HTML
297 lines
10 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Notes - OfflineU</title>
|
|
<link rel="manifest" href="/static/manifest.json">
|
|
<meta name="theme-color" content="#007acc">
|
|
<link rel="apple-touch-icon" href="/static/icons/icon-192.png">
|
|
<style>
|
|
:root {
|
|
--bg-primary: #1a1a1a;
|
|
--bg-secondary: #2d2d2d;
|
|
--bg-tertiary: #3d3d3d;
|
|
--bg-tertiary-hover: #404040;
|
|
--text-primary: #e0e0e0;
|
|
--text-muted: #999;
|
|
--border-color: #555;
|
|
--accent: #007acc;
|
|
--accent-hover: #005a9e;
|
|
--font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
--font-size-base: 16px;
|
|
--container-max-width: 1600px;
|
|
--radius: 8px;
|
|
--success: #28a745;
|
|
--error: #ff6b6b;
|
|
}
|
|
[data-theme="light"] {
|
|
--bg-primary: #f2f2f2;
|
|
--bg-secondary: #ffffff;
|
|
--bg-tertiary: #eeeeee;
|
|
--bg-tertiary-hover: #e2e2e2;
|
|
--text-primary: #222222;
|
|
--text-muted: #666666;
|
|
--border-color: #cccccc;
|
|
}
|
|
[data-card-style="elevated"] .card,
|
|
[data-card-style="elevated"] .note-card {
|
|
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
|
|
}
|
|
[data-card-style="bordered"] .card,
|
|
[data-card-style="bordered"] .note-card {
|
|
border: 1px solid var(--border-color);
|
|
}
|
|
* { box-sizing: border-box; }
|
|
html, body { height: 100%; }
|
|
body {
|
|
font-family: var(--font-family);
|
|
font-size: var(--font-size-base);
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
margin: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-height: 100vh;
|
|
}
|
|
.page-content {
|
|
flex: 1;
|
|
padding: 20px;
|
|
padding-bottom: 70px;
|
|
}
|
|
.app-header {
|
|
background: linear-gradient(135deg, var(--bg-secondary), var(--bg-tertiary));
|
|
padding: 18px 0;
|
|
border-bottom: 3px solid var(--accent);
|
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.25);
|
|
}
|
|
.app-header .header-inner {
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
.app-header .brand-icon { font-size: 1.6em; }
|
|
.app-header a.brand-link {
|
|
color: var(--accent);
|
|
text-decoration: none;
|
|
font-size: 1.3em;
|
|
font-weight: 600;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
.bottom-tab-bar {
|
|
position: fixed;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
z-index: 9000;
|
|
display: flex;
|
|
background: var(--bg-secondary);
|
|
border-top: 1px solid var(--border-color);
|
|
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.2);
|
|
padding-bottom: env(safe-area-inset-bottom, 0px);
|
|
}
|
|
.tab-item {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 2px;
|
|
padding: 8px 4px;
|
|
color: var(--text-muted);
|
|
text-decoration: none;
|
|
font-size: 0.72em;
|
|
transition: color 0.2s;
|
|
}
|
|
.tab-item .tab-icon {
|
|
font-size: 1.3em;
|
|
}
|
|
.tab-item:hover,
|
|
.tab-item.active {
|
|
color: var(--accent);
|
|
}
|
|
.container {
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
}
|
|
h1 { color: var(--accent); margin-bottom: 5px; }
|
|
.subtitle { color: var(--text-muted); margin-bottom: 25px; }
|
|
.empty-hint {
|
|
color: var(--text-muted);
|
|
text-align: center;
|
|
padding: 40px 20px;
|
|
}
|
|
.notes-search-input {
|
|
width: 100%;
|
|
background: var(--bg-tertiary);
|
|
color: var(--text-primary);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: var(--radius);
|
|
padding: 10px 14px;
|
|
font-size: 0.95em;
|
|
font-family: var(--font-family);
|
|
margin-bottom: 20px;
|
|
}
|
|
.note-card {
|
|
background: var(--bg-secondary);
|
|
border-radius: var(--radius);
|
|
padding: 18px 20px;
|
|
margin-bottom: 15px;
|
|
border-left: 4px solid var(--accent);
|
|
}
|
|
.note-card-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin-bottom: 10px;
|
|
}
|
|
.note-card-icon {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 4px;
|
|
object-fit: cover;
|
|
flex-shrink: 0;
|
|
font-size: 1.3em;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.note-card-titles {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
.note-lesson-title {
|
|
display: block;
|
|
font-weight: 600;
|
|
}
|
|
.note-course-name {
|
|
display: block;
|
|
font-size: 0.85em;
|
|
color: var(--text-muted);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.outline-badge {
|
|
font-size: 0.75em;
|
|
background: var(--bg-primary);
|
|
color: var(--accent);
|
|
padding: 2px 8px;
|
|
border-radius: 3px;
|
|
border: 1px solid var(--accent);
|
|
white-space: nowrap;
|
|
flex-shrink: 0;
|
|
}
|
|
.timestamp-badge {
|
|
font-size: 0.75em;
|
|
background: var(--bg-primary);
|
|
color: var(--text-muted);
|
|
padding: 2px 8px;
|
|
border-radius: 3px;
|
|
border: 1px solid var(--border-color);
|
|
white-space: nowrap;
|
|
flex-shrink: 0;
|
|
}
|
|
.note-card-body {
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
line-height: 1.5;
|
|
}
|
|
.note-card-link {
|
|
display: inline-block;
|
|
margin-top: 12px;
|
|
font-size: 0.85em;
|
|
color: var(--accent);
|
|
text-decoration: none;
|
|
padding: 4px 10px;
|
|
border-radius: var(--radius);
|
|
transition: background 0.2s ease;
|
|
}
|
|
.note-card-link:hover {
|
|
background: var(--bg-tertiary);
|
|
text-decoration: underline;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="app-header">
|
|
<div class="header-inner">
|
|
<span class="brand-icon">📚</span>
|
|
<a href="/reset_course" class="brand-link">OfflineU</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="page-content">
|
|
<div class="container">
|
|
<h1>📝 Notes</h1>
|
|
<p class="subtitle">Every lesson note across your library, newest first.</p>
|
|
|
|
{% if not notes %}
|
|
<div class="empty-hint">No notes yet - jot something down on a lesson page and it'll show up here.</div>
|
|
{% else %}
|
|
<input type="text" id="notes-search-input" class="notes-search-input"
|
|
placeholder="Search notes, lessons, or courses…" oninput="filterNotes(this.value)">
|
|
<div id="notes-no-results-hint" class="empty-hint" style="display: none;">No notes match your search.</div>
|
|
{% endif %}
|
|
|
|
{% for note in notes %}
|
|
<div class="note-card" data-search="{{ (note.lesson_title ~ ' ' ~ note.course_name ~ ' ' ~ note.text) | lower }}">
|
|
<div class="note-card-header">
|
|
<span class="note-card-icon">📝</span>
|
|
<div class="note-card-titles">
|
|
<span class="note-lesson-title">{{ note.lesson_title }}</span>
|
|
<span class="note-course-name">{{ note.course_name }}</span>
|
|
</div>
|
|
{% if note.timestamp_label %}
|
|
<span class="timestamp-badge">{{ note.timestamp_label }}</span>
|
|
{% endif %}
|
|
{% if note.pushed_to_outline %}
|
|
<span class="outline-badge">Outline</span>
|
|
{% endif %}
|
|
</div>
|
|
<div class="note-card-body">{{ note.text }}</div>
|
|
<a class="note-card-link"
|
|
href="/recent/open?course_path={{ note.course_path | urlencode }}&lesson_path={{ note.lesson_path | urlencode }}{% if note.timestamp_seconds is not none %}&t={{ note.timestamp_seconds }}{% endif %}">
|
|
Open lesson →
|
|
</a>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<nav class="bottom-tab-bar">
|
|
<a href="/reset_course" class="tab-item {% if active_tab == 'home' %}active{% endif %}">
|
|
<span class="tab-icon">🏠</span><span class="tab-label">Home</span>
|
|
</a>
|
|
<a href="/notes" class="tab-item {% if active_tab == 'notes' %}active{% endif %}">
|
|
<span class="tab-icon">📝</span><span class="tab-label">Notes</span>
|
|
</a>
|
|
<a href="/help" class="tab-item {% if active_tab == 'help' %}active{% endif %}">
|
|
<span class="tab-icon">❓</span><span class="tab-label">Help</span>
|
|
</a>
|
|
<a href="/settings" class="tab-item {% if active_tab == 'settings' %}active{% endif %}">
|
|
<span class="tab-icon">⚙</span><span class="tab-label">Settings</span>
|
|
</a>
|
|
</nav>
|
|
|
|
<script>
|
|
function filterNotes(query) {
|
|
const q = query.trim().toLowerCase();
|
|
const cards = document.querySelectorAll('.note-card');
|
|
let visibleCount = 0;
|
|
cards.forEach(function(card) {
|
|
const match = !q || card.dataset.search.includes(q);
|
|
card.style.display = match ? '' : 'none';
|
|
if (match) visibleCount++;
|
|
});
|
|
const noResultsHint = document.getElementById('notes-no-results-hint');
|
|
if (noResultsHint) {
|
|
noResultsHint.style.display = (visibleCount === 0 && cards.length > 0) ? 'block' : 'none';
|
|
}
|
|
}
|
|
</script>
|
|
<script src="/static/theme.js"></script>
|
|
</body>
|
|
</html>
|