feat: add per-video notes with localStorage, notes tab filter

This commit is contained in:
Gan, Jimmy
2026-06-24 01:42:12 +08:00
parent f74a428521
commit 5bfd72ab3d
2 changed files with 42 additions and 13 deletions
+1 -1
View File
@@ -87,7 +87,7 @@ async def summarize_video(video_id: str) -> str:
async with aiosqlite.connect(DB_PATH) as db: async with aiosqlite.connect(DB_PATH) as db:
await db.execute( await db.execute(
"UPDATE videos SET transcript = ?, summary = ? WHERE video_id = ?", "UPDATE videos SET transcript = ?, summary = ? WHERE video_id = ?",
("No transcript available", "Could not retrieve video transcript.", video_id) ("No transcript available", "📺 Short clip — title only, no description available. Watch to preview.", video_id)
) )
await db.commit() await db.commit()
return None return None
+32 -3
View File
@@ -9,6 +9,8 @@
let syncMessage = $state(''); let syncMessage = $state('');
let theme = $state(localStorage.getItem('theme') || 'dark'); let theme = $state(localStorage.getItem('theme') || 'dark');
let summaryPanel = $state(null); let summaryPanel = $state(null);
let notes = $state(JSON.parse(localStorage.getItem('t-yt-notes') || '{}'));
let activeNote = $state(null);
function toggleTheme() { function toggleTheme() {
theme = theme === 'dark' ? 'light' : 'dark'; theme = theme === 'dark' ? 'light' : 'dark';
@@ -19,6 +21,16 @@
async function loadVideos() { async function loadVideos() {
isLoading = true; isLoading = true;
try { try {
if (activeTab === 'notes') {
// Notes tab: show all pending videos, but only those with notes
const res = await fetch(`/api/videos?status=pending&limit=50`);
if (res.ok) {
const all = await res.json();
videos = all.filter(v => notes[v.video_id]);
if (videos.length > 0) selectedVideo = videos[0];
else selectedVideo = null;
}
} else {
const res = await fetch(`/api/videos?status=${activeTab}&limit=50`); const res = await fetch(`/api/videos?status=${activeTab}&limit=50`);
if (res.ok) { if (res.ok) {
videos = await res.json(); videos = await res.json();
@@ -30,6 +42,7 @@
selectedVideo = null; selectedVideo = null;
} }
} }
}
} catch (err) { } catch (err) {
console.error("Error loading videos:", err); console.error("Error loading videos:", err);
} finally { } finally {
@@ -138,7 +151,7 @@
<!-- Left: video list --> <!-- Left: video list -->
<aside style="width:280px;background:var(--bg-secondary);border-right:1px solid var(--border)" class="flex flex-col flex-shrink-0"> <aside style="width:280px;background:var(--bg-secondary);border-right:1px solid var(--border)" class="flex flex-col flex-shrink-0">
<div class="flex p-1.5" style="border-bottom:1px solid var(--border)"> <div class="flex p-1.5" style="border-bottom:1px solid var(--border)">
{#each ['pending','read','skipped'] as tab} {#each ['pending','read','skipped','notes'] as tab}
<button onclick={() => activeTab = tab} <button onclick={() => activeTab = tab}
class="flex-1 py-1.5 text-center text-[11px] font-bold rounded uppercase transition" class="flex-1 py-1.5 text-center text-[11px] font-bold rounded uppercase transition"
style="color: {activeTab === tab ? 'var(--accent)' : 'var(--text-muted)'}"> style="color: {activeTab === tab ? 'var(--accent)' : 'var(--text-muted)'}">
@@ -158,7 +171,7 @@
<div class="min-w-0"> <div class="min-w-0">
<span class="block text-[10px] font-semibold truncate" style="color:var(--accent)">{video.channel_name}</span> <span class="block text-[10px] font-semibold truncate" style="color:var(--accent)">{video.channel_name}</span>
<h3 class="text-xs font-bold line-clamp-2 leading-snug" style="color:var(--text-primary)">{video.title}</h3> <h3 class="text-xs font-bold line-clamp-2 leading-snug" style="color:var(--text-primary)">{video.title}</h3>
<span class="text-[10px] mt-0.5 block" style="color:var(--text-muted)">{video.publish_date || ''}</span> <span class="text-[10px] mt-0.5 block" style="color:var(--text-muted)">{video.publish_date || ''} {notes[video.video_id] ? '📝' : ''}</span>
</div> </div>
</button> </button>
{/each} {/each}
@@ -173,7 +186,7 @@
<div class="rounded-xl border p-5 space-y-3 shadow-lg" style="background:var(--card-bg);border-color:var(--accent);border-left-width:3px"> <div class="rounded-xl border p-5 space-y-3 shadow-lg" style="background:var(--card-bg);border-color:var(--accent);border-left-width:3px">
<div class="flex items-center justify-between pb-2" style="border-bottom:1px solid var(--border)"> <div class="flex items-center justify-between pb-2" style="border-bottom:1px solid var(--border)">
<h2 class="text-xs font-black tracking-widest uppercase" style="color:var(--accent)">AI Digest</h2> <h2 class="text-xs font-black tracking-widest uppercase" style="color:var(--accent)">AI Digest</h2>
{#if !selectedVideo.summary || selectedVideo.summary.includes("Error") || selectedVideo.summary.includes("Could not")} {#if !selectedVideo.summary || selectedVideo.summary.includes("Error")}
<button onclick={() => generateSummary(selectedVideo.video_id)} <button onclick={() => generateSummary(selectedVideo.video_id)}
class="text-[11px] px-3 py-1 font-semibold rounded border transition" style="color:var(--accent);border-color:var(--accent)"> class="text-[11px] px-3 py-1 font-semibold rounded border transition" style="color:var(--accent);border-color:var(--accent)">
Generate Generate
@@ -194,8 +207,24 @@
<div class="flex space-x-2"> <div class="flex space-x-2">
<button onclick={() => updateStatus(selectedVideo.video_id, 'read')} class="text-[10px] px-2.5 py-1 text-white font-bold rounded transition" style="background:var(--accent)">Read</button> <button onclick={() => updateStatus(selectedVideo.video_id, 'read')} class="text-[10px] px-2.5 py-1 text-white font-bold rounded transition" style="background:var(--accent)">Read</button>
<button onclick={() => updateStatus(selectedVideo.video_id, 'skipped')} class="text-[10px] px-2.5 py-1 font-bold rounded border transition" style="color:var(--text-secondary);border-color:var(--border)">Skip</button> <button onclick={() => updateStatus(selectedVideo.video_id, 'skipped')} class="text-[10px] px-2.5 py-1 font-bold rounded border transition" style="color:var(--text-secondary);border-color:var(--border)">Skip</button>
<button onclick={() => activeNote = activeNote === selectedVideo.video_id ? null : selectedVideo.video_id}
class="text-[10px] px-2.5 py-1 font-bold rounded border transition"
style="color:var(--text-muted);border-color:var(--border)">✎ Note</button>
<a href={selectedVideo.url} target="_blank" rel="noopener noreferrer" class="text-[10px] px-2.5 py-1 text-white font-bold rounded transition" style="background:var(--accent)">▶ Watch</a> <a href={selectedVideo.url} target="_blank" rel="noopener noreferrer" class="text-[10px] px-2.5 py-1 text-white font-bold rounded transition" style="background:var(--accent)">▶ Watch</a>
</div> </div>
{#if activeNote === selectedVideo.video_id}
<div class="space-y-1.5 mt-2">
<textarea rows="2" class="w-full text-xs p-2 rounded border resize-none"
style="background:var(--bg-tertiary);color:var(--text-primary);border-color:var(--border)"
placeholder="Quick note..."
value={notes[selectedVideo.video_id] || ''}
oninput={e => notes[selectedVideo.video_id] = e.target.value}
onkeydown={e => { if (e.key === 'Escape') { localStorage.setItem('t-yt-notes', JSON.stringify(notes)); activeNote = null; } }}
></textarea>
<button onclick={() => { localStorage.setItem('t-yt-notes', JSON.stringify(notes)); activeNote = null; }}
class="text-[10px] px-3 py-1 text-white font-bold rounded transition" style="background:var(--accent)">Save</button>
</div>
{/if}
</div> </div>
</div> </div>
</div> </div>