feat: add per-video notes with localStorage, notes tab filter
This commit is contained in:
@@ -87,7 +87,7 @@ async def summarize_video(video_id: str) -> str:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
await db.execute(
|
||||
"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()
|
||||
return None
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
let syncMessage = $state('');
|
||||
let theme = $state(localStorage.getItem('theme') || 'dark');
|
||||
let summaryPanel = $state(null);
|
||||
let notes = $state(JSON.parse(localStorage.getItem('t-yt-notes') || '{}'));
|
||||
let activeNote = $state(null);
|
||||
|
||||
function toggleTheme() {
|
||||
theme = theme === 'dark' ? 'light' : 'dark';
|
||||
@@ -19,15 +21,26 @@
|
||||
async function loadVideos() {
|
||||
isLoading = true;
|
||||
try {
|
||||
const res = await fetch(`/api/videos?status=${activeTab}&limit=50`);
|
||||
if (res.ok) {
|
||||
videos = await res.json();
|
||||
if (videos.length > 0) {
|
||||
const currentId = selectedVideo?.video_id;
|
||||
const found = videos.find(v => v.video_id === currentId);
|
||||
selectedVideo = found || videos[0];
|
||||
} else {
|
||||
selectedVideo = null;
|
||||
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`);
|
||||
if (res.ok) {
|
||||
videos = await res.json();
|
||||
if (videos.length > 0) {
|
||||
const currentId = selectedVideo?.video_id;
|
||||
const found = videos.find(v => v.video_id === currentId);
|
||||
selectedVideo = found || videos[0];
|
||||
} else {
|
||||
selectedVideo = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -138,7 +151,7 @@
|
||||
<!-- Left: video list -->
|
||||
<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)">
|
||||
{#each ['pending','read','skipped'] as tab}
|
||||
{#each ['pending','read','skipped','notes'] as tab}
|
||||
<button onclick={() => activeTab = tab}
|
||||
class="flex-1 py-1.5 text-center text-[11px] font-bold rounded uppercase transition"
|
||||
style="color: {activeTab === tab ? 'var(--accent)' : 'var(--text-muted)'}">
|
||||
@@ -158,7 +171,7 @@
|
||||
<div class="min-w-0">
|
||||
<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>
|
||||
<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>
|
||||
</button>
|
||||
{/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="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>
|
||||
{#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)}
|
||||
class="text-[11px] px-3 py-1 font-semibold rounded border transition" style="color:var(--accent);border-color:var(--accent)">
|
||||
Generate
|
||||
@@ -194,8 +207,24 @@
|
||||
<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, '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>
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user