aboutsummaryrefslogtreecommitdiffstats
path: root/frontend-vanilla
diff options
context:
space:
mode:
authorAdam Mathes <adam@adammathes.com>2026-02-18 19:07:37 -0800
committerAdam Mathes <adam@adammathes.com>2026-02-18 19:25:30 -0800
commitd466cf8a4717e09cef6d8b3c39c06e0935dafba7 (patch)
tree79d28e0076a302786e2f592798f8a8752af0815a /frontend-vanilla
parent082089e427df8e34a366684f71f35ed700ec5d04 (diff)
downloadneko-d466cf8a4717e09cef6d8b3c39c06e0935dafba7.tar.gz
neko-d466cf8a4717e09cef6d8b3c39c06e0935dafba7.tar.bz2
neko-d466cf8a4717e09cef6d8b3c39c06e0935dafba7.zip
Fix mark-as-read regression and add debugging tools
Diffstat (limited to 'frontend-vanilla')
-rw-r--r--frontend-vanilla/src/main.ts96
1 files changed, 52 insertions, 44 deletions
diff --git a/frontend-vanilla/src/main.ts b/frontend-vanilla/src/main.ts
index e3d3a71..0a67dfe 100644
--- a/frontend-vanilla/src/main.ts
+++ b/frontend-vanilla/src/main.ts
@@ -13,6 +13,25 @@ declare global {
}
}
+const urlParams = new URLSearchParams(window.location.search);
+const DEBUG = urlParams.has('debug');
+
+function debugLog(...args: any[]) {
+ if (DEBUG) {
+ console.log('[NEKO-DEBUG]', ...args);
+ }
+}
+
+// Add to window for console debugging
+if (typeof window !== 'undefined') {
+ window.app = window.app || {};
+ window.app.debug = () => {
+ const url = new URL(window.location.href);
+ url.searchParams.set('debug', '1');
+ window.location.href = url.toString();
+ };
+}
+
// Style theme management: load/unload CSS files
const STYLE_THEMES = ['default', 'refined', 'terminal', 'codex', 'sakura'] as const;
@@ -239,8 +258,6 @@ export function attachLayoutListeners() {
const id = parseInt(itemRow.getAttribute('data-id')!);
activeItemId = id;
- activeItemId = id;
-
const item = store.items.find(i => i._id === id);
if (item && !item.read) {
updateItem(id, { read: true });
@@ -265,18 +282,7 @@ export function renderFeeds() {
}
export function renderTags() {
- /* Soft deprecated.
- const tagList = document.getElementById('tag-list');
- if (!tagList) return;
-
- const { tags, activeTagName } = store;
- tagList.innerHTML = tags.map(tag => {
- const isActive = activeTagName === tag.title;
- return `<li class="tag-item ${isActive ? 'active' : ''}">
- <a href="/tag/${encodeURIComponent(tag.title)}" data-nav="tag" data-value="${tag.title}">${tag.title}</a>
- </li>`;
- }).join('');
- */
+ /* Soft deprecated. */
}
export function renderFilters() {
@@ -330,6 +336,7 @@ export function renderItems() {
// Mark-as-read: debounced
if (readTimeoutId === null) {
readTimeoutId = window.setTimeout(() => {
+ debugLog('onscroll trigger checkReadItems');
checkReadItems(scrollRoot);
readTimeoutId = null;
}, 250);
@@ -340,18 +347,34 @@ export function renderItems() {
function checkReadItems(scrollRoot: HTMLElement) {
const containerRect = scrollRoot.getBoundingClientRect();
+ debugLog('checkReadItems start', { containerTop: containerRect.top });
+
// Batch DOM query: select all feed items at once instead of O(n) individual
// querySelector calls with attribute selectors per scroll tick.
const allItems = scrollRoot.querySelectorAll('.feed-item');
for (const el of allItems) {
- const id = parseInt(el.getAttribute('data-id')!);
+ const idAttr = el.getAttribute('data-id');
+ if (!idAttr) continue;
+ const id = parseInt(idAttr);
const item = store.items.find(i => i._id === id);
if (!item || item.read) continue;
const rect = el.getBoundingClientRect();
- // Mark as read if the bottom of the item is above the top of the container
- if (rect.bottom < containerRect.top) {
- updateItem(item._id, { read: true });
+ // Use a small buffer (5px) to be more robust
+ const isPast = rect.bottom < (containerRect.top + 5);
+
+ if (DEBUG) {
+ debugLog(`Item ${id} check`, {
+ rectTop: rect.top,
+ rectBottom: rect.bottom,
+ containerTop: containerRect.top,
+ isPast
+ });
+ }
+
+ if (isPast) {
+ debugLog(`Marking as read (scrolled past): ${id}`);
+ updateItem(id, { read: true });
}
}
}
@@ -369,12 +392,15 @@ if (typeof window !== 'undefined') {
const scrollRoot = document.getElementById('main-content');
// console.log('Polling...', { scrollRoot: !!scrollRoot, loading: store.loading, hasMore: store.hasMore });
- if (store.loading || !store.hasMore) return;
-
if (scrollRoot) {
// Check for read items periodically (robustness fallback)
+ // This MUST run even if we are not loading more items
checkReadItems(scrollRoot);
+ }
+
+ if (store.loading || !store.hasMore) return;
+ if (scrollRoot) {
// Check container scroll (if container is scrollable)
if (scrollRoot.scrollHeight > scrollRoot.clientHeight) {
if (scrollRoot.scrollHeight - scrollRoot.scrollTop - scrollRoot.clientHeight < 200) {
@@ -400,10 +426,6 @@ if (typeof window !== 'undefined') {
}, 1000);
}
-// ... (add this variable at module level or inside renderSettings if possible, but module level is safer for persistence across clicks if renderSettings re-runs? No, event flow is synchronous: click button -> click file input. User selects file. Change event fires.
-// Actually, file input click is async in terms of user action. renderSettings won't run in between unless something else triggers it.
-// But to be safe, I'll update the function signature of importOPML to importData.
-
export function renderSettings() {
const contentArea = document.getElementById('content-area');
if (!contentArea) return;
@@ -581,20 +603,6 @@ export function renderSettings() {
}
});
});
-
- /* Soft deprecated.
- document.querySelectorAll('.update-feed-tag-btn').forEach(btn => {
- btn.addEventListener('click', async (e) => {
- const id = parseInt((e.target as HTMLElement).getAttribute('data-id')!);
- const input = document.querySelector(`.feed-tag-input[data-id="${id}"]`) as HTMLInputElement;
- await updateFeed(id, { category: input.value.trim() });
- await fetchFeeds();
- // await fetchTags();
- renderSettings();
- alert('Feed updated');
- });
- });
- */
}
async function addFeed(url: string): Promise<boolean> {
@@ -689,15 +697,18 @@ export async function scrapeItem(id: number) {
}
}
-export async function updateItem(id: number, updates: Partial<Item>) {
+export async function updateItem(id: number | string, updates: Partial<Item>) {
+ const idStr = String(id);
+ debugLog('updateItem called', idStr, updates);
+
try {
- const res = await apiFetch(`/api/item/${id}`, {
+ const res = await apiFetch(`/api/item/${idStr}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates)
});
if (res.ok) {
- const item = store.items.find(i => i._id === id);
+ const item = store.items.find(i => String(i._id) === idStr);
if (item) {
Object.assign(item, updates);
// Selective DOM update to avoid full re-render
@@ -761,9 +772,6 @@ export async function fetchItems(feedId?: string, tagName?: string, append: bool
const res = await apiFetch(`/api/stream?${params.toString()}`);
if (res.ok) {
const items = await res.json();
- // V1 logic: keep loading as long as we get results.
- // Backend limit is currently 15, so checking >= 50 caused premature stop.
- // We accept one extra empty fetch at the end to be robust against page size changes.
store.setHasMore(items.length > 0);
store.setItems(items, append);
}