const listEl = document.getElementById('list'); const addForm = document.getElementById('addForm'); const titleInput = document.getElementById('title'); const typeInput = document.getElementById('type'); const notesInput = document.getElementById('notes'); const posterInput = document.getElementById('poster'); const refreshBtn = document.getElementById('refresh'); const suggestBtn = document.getElementById('suggest'); const suggestionEl = document.getElementById('suggestion'); const onlyUnwatchedEl = document.getElementById('onlyUnwatched'); const darkToggle = document.getElementById('darkToggle'); const filterInput = document.getElementById('filter'); const searchInput = document.getElementById('search'); const searchResults = document.getElementById('searchResults'); let allItems = []; // cache of fetched watchlist // Dark mode init & persistence function applyDark(isDark) { document.documentElement.classList.toggle('dark', isDark); darkToggle.checked = isDark; try { localStorage.setItem('wtw-dark', isDark ? '1' : '0'); } catch (e) {} } const stored = (() => { try { return localStorage.getItem('wtw-dark'); } catch (e) { return null; } })(); applyDark(stored === '1' || (stored === null && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches)); darkToggle.addEventListener('change', () => applyDark(darkToggle.checked)); // Fetch and cache list async function fetchList() { try { const res = await fetch('/api/watchlist'); const items = await res.json(); allItems = items || []; applyFilter(); } catch (e) { listEl.innerHTML = `
Failed to load list
`; } } function applyFilter() { const q = (filterInput?.value || '').trim().toLowerCase(); let items = allItems.slice(); if (onlyUnwatchedEl.checked) items = items.filter(i => !i.watched); if (q) { items = items.filter(it => { const hay = ((it.title || '') + ' ' + (it.type || '') + ' ' + (it.notes || '') + ' ' + ((it.genres || []).join(' '))).toLowerCase(); return hay.includes(q); }); } renderList(items); } function renderList(items) { listEl.innerHTML = ''; if (!items.length) { listEl.innerHTML = '

No items yet.

'; return; } items.forEach(item => { const card = document.createElement('div'); card.className = 'flex items-center justify-between p-3 rounded border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900'; const left = document.createElement('div'); left.className = 'flex items-center gap-3'; const imgWrap = document.createElement('div'); imgWrap.className = 'w-16 flex-shrink-0'; if (item.poster) { const img = document.createElement('img'); img.src = item.poster; img.alt = item.title || 'poster'; img.className = 'w-16 h-24 object-cover rounded'; imgWrap.appendChild(img); } else { const placeholder = document.createElement('div'); placeholder.className = 'w-16 h-24 bg-gray-200 dark:bg-gray-700 rounded flex items-center justify-center text-sm text-gray-500'; placeholder.textContent = 'No\nImage'; imgWrap.appendChild(placeholder); } const info = document.createElement('div'); info.innerHTML = `
${escapeHtml(item.title)} (${escapeHtml(item.type)})
${item.notes ? escapeHtml(item.notes) : ''}
`; left.appendChild(imgWrap); left.appendChild(info); const right = document.createElement('div'); right.className = 'flex gap-2'; const toggleBtn = document.createElement('button'); toggleBtn.className = 'px-2 py-1 rounded border text-sm'; toggleBtn.textContent = item.watched ? 'Unwatch' : 'Mark watched'; toggleBtn.onclick = async () => { await fetch('/api/watchlist/' + item.id + '/toggle', { method: 'PUT' }); await fetchList(); }; const delBtn = document.createElement('button'); delBtn.className = 'px-2 py-1 rounded border text-sm text-red-600 dark:text-red-400'; delBtn.textContent = 'Remove'; delBtn.onclick = async () => { if (!confirm('Remove this item?')) return; await fetch('/api/watchlist/' + item.id, { method: 'DELETE' }); await fetchList(); }; right.appendChild(toggleBtn); right.appendChild(delBtn); card.appendChild(left); card.appendChild(right); listEl.appendChild(card); }); } addForm.addEventListener('submit', async (e) => { e.preventDefault(); const payload = { title: titleInput.value.trim(), type: typeInput.value, notes: notesInput.value.trim(), poster: posterInput.value.trim() || null }; if (!payload.title) return alert('Title required'); await fetch('/api/watchlist', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); titleInput.value = ''; notesInput.value = ''; posterInput.value = ''; await fetchList(); }); refreshBtn.addEventListener('click', fetchList); onlyUnwatchedEl.addEventListener('change', applyFilter); filterInput?.addEventListener('input', applyFilter); suggestBtn.addEventListener('click', async () => { const onlyUnwatched = onlyUnwatchedEl.checked ? '?unwatched=1' : ''; const res = await fetch('/api/suggest' + onlyUnwatched); const data = await res.json(); suggestionEl.classList.remove('hidden'); if (data.message) { suggestionEl.innerHTML = `
${escapeHtml(data.message)}
`; } else { suggestionEl.innerHTML = `
${escapeHtml(data.title)} (${escapeHtml(data.type)})
`; } }); // --- Search UI + logic (unchanged) --- let searchToken = 0; function debounce(fn, wait = 300) { let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), wait); }; } async function doSearch(q) { searchToken++; const token = searchToken; if (!q) { searchResults.classList.add('hidden'); searchResults.innerHTML = ''; return; } try { const res = await fetch('/api/search?q=' + encodeURIComponent(q)); if (token !== searchToken) return; // stale if (!res.ok) { searchResults.innerHTML = `
Search failed
`; searchResults.classList.remove('hidden'); return; } const items = await res.json(); renderSearchResults(items); } catch (e) { searchResults.innerHTML = `
Search error
`; searchResults.classList.remove('hidden'); } } function renderSearchResults(items) { if (!items || !items.length) { searchResults.innerHTML = `
No results
`; searchResults.classList.remove('hidden'); return; } searchResults.innerHTML = ''; items.forEach(it => { const el = document.createElement('div'); el.className = 'flex gap-3 p-3 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer'; const imgWrap = document.createElement('div'); imgWrap.className = 'w-16 flex-shrink-0'; if (it.poster) { const img = document.createElement('img'); img.src = it.poster; img.alt = it.title || 'poster'; img.className = 'w-16 h-24 object-cover rounded'; imgWrap.appendChild(img); } else { const placeholder = document.createElement('div'); placeholder.className = 'w-16 h-24 bg-gray-200 dark:bg-gray-700 rounded flex items-center justify-center text-sm text-gray-500'; placeholder.textContent = 'No\nImage'; imgWrap.appendChild(placeholder); } const body = document.createElement('div'); body.className = 'flex-1'; const titleRow = document.createElement('div'); titleRow.className = 'flex items-center justify-between'; const titleEl = document.createElement('div'); titleEl.className = 'font-medium'; titleEl.innerHTML = `${escapeHtml(it.title)} (${escapeHtml(it.type)})`; titleRow.appendChild(titleEl); const addBtn = document.createElement('button'); addBtn.className = 'ml-3 text-sm px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700'; addBtn.textContent = 'Add'; addBtn.onclick = async (e) => { e.stopPropagation(); const payload = { title: it.title, type: it.type === 'tv' ? 'series' : 'movie', notes: it.overview || '', poster: it.poster || null }; try { const res = await fetch('/api/watchlist', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (res.ok) { await fetchList(); searchResults.classList.add('hidden'); searchInput.value = ''; } else { alert('Failed to add item'); } } catch (err) { alert('Failed to add item'); } }; titleRow.appendChild(addBtn); const overview = document.createElement('div'); overview.className = 'text-sm text-gray-600 dark:text-gray-300 mt-1'; overview.textContent = it.overview ? (it.overview.length > 220 ? it.overview.slice(0, 220) + '…' : it.overview) : ''; const genres = document.createElement('div'); genres.className = 'text-xs text-gray-500 dark:text-gray-400 mt-1'; genres.textContent = (it.genres || []).join(', '); body.appendChild(titleRow); body.appendChild(overview); body.appendChild(genres); el.appendChild(imgWrap); el.appendChild(body); el.addEventListener('click', () => { titleInput.value = it.title; typeInput.value = it.type === 'tv' ? 'series' : 'movie'; notesInput.value = it.overview || ''; posterInput.value = it.poster || ''; searchResults.classList.add('hidden'); }); searchResults.appendChild(el); }); searchResults.classList.remove('hidden'); } const debouncedSearch = debounce((v) => doSearch(v), 300); searchInput.addEventListener('input', (e) => debouncedSearch(e.target.value.trim())); document.addEventListener('click', (e) => { if (!searchResults.contains(e.target) && e.target !== searchInput) { searchResults.classList.add('hidden'); } }); // --- util --- function escapeHtml(s) { return String(s || '').replace(/[&<>"']/g, function(m) { return ({'&':'&','<':'<','>':'>','"':'"',"'":'''})[m]; }); } // initial load fetchList();