Compare commits
2
Commits
2026.09.05.02
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b3b986c0e | ||
|
|
d882e349a9 |
+1
-1
@@ -40,7 +40,7 @@ import type { PluginManifest } from '@@/plugins/manifest';
|
|||||||
*/
|
*/
|
||||||
export const manifest: PluginManifest = {
|
export const manifest: PluginManifest = {
|
||||||
publisher: 'officerdev',
|
publisher: 'officerdev',
|
||||||
version: '2026.09.05.02',
|
version: '2026.09.05.04',
|
||||||
platform: '>=1.0.0',
|
platform: '>=1.0.0',
|
||||||
|
|
||||||
label: 'Karakeep',
|
label: 'Karakeep',
|
||||||
|
|||||||
+37
-2
@@ -79,15 +79,46 @@ export const BookmarksPanel = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
/*
|
||||||
|
`tick` exists to re-ask the same question, which is different from asking a new one.
|
||||||
|
|
||||||
|
Everything else in this effect is a NEW question — a different list, different words. This one is
|
||||||
|
the poll below saying "ask that again", and it has to go through the same path so a refresh cannot
|
||||||
|
drift from a first load.
|
||||||
|
*/
|
||||||
|
const [tick, setTick] = useState(0);
|
||||||
|
|
||||||
// Debounced: long enough that a five-letter word is one query instead of five, short enough that the
|
// Debounced: long enough that a five-letter word is one query instead of five, short enough that the
|
||||||
// grid moves while you type.
|
// grid moves while you type.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setTimeout(() => void load(list, query), 250);
|
const timer = setTimeout(() => void load(list, query), 250);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [list, query, load]);
|
}, [list, query, load, tick]);
|
||||||
|
|
||||||
const waiting = rows?.filter((b) => b.content?.type === 'link' && b.content.crawlStatus === 'pending').length ?? 0;
|
const waiting = rows?.filter((b) => b.content?.type === 'link' && b.content.crawlStatus === 'pending').length ?? 0;
|
||||||
|
|
||||||
|
/*
|
||||||
|
── While anything is still being fetched, ask again ──
|
||||||
|
|
||||||
|
Without this the panel loads once and never moves. That is wrong here in a way it would not be for
|
||||||
|
most lists, because a bookmark is not finished when it is saved: Karakeep crawls it afterwards, and
|
||||||
|
the picture, the description and the favicon all arrive minutes later.
|
||||||
|
|
||||||
|
Worse, the page most likely to be full of unfinished ones is the FIRST page — newest first, and the
|
||||||
|
crawler works through the backlog — so a fresh import shows a grid of "fetching…" that never
|
||||||
|
resolves until somebody reloads by hand. Measured on this instance: 79 of 97 crawled, and all 16
|
||||||
|
pending sitting on page one.
|
||||||
|
|
||||||
|
The poll is self-limiting and needs no switch: it only re-arms while something is actually pending,
|
||||||
|
so a settled library makes no requests at all. Five seconds because a crawl takes a few and nobody
|
||||||
|
is watching the clock.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (waiting === 0) return;
|
||||||
|
const timer = setTimeout(() => setTick((n) => n + 1), 5000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [waiting, tick]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex size-full flex-col">
|
<div className="flex size-full flex-col">
|
||||||
<header className="border-b p-2">
|
<header className="border-b p-2">
|
||||||
@@ -105,7 +136,11 @@ export const BookmarksPanel = () => {
|
|||||||
A bookmark has no picture until Karakeep has crawled it, and on a fresh import that is most
|
A bookmark has no picture until Karakeep has crawled it, and on a fresh import that is most
|
||||||
of them for a while. Without this the panel looks broken at exactly the moment it is working.
|
of them for a while. Without this the panel looks broken at exactly the moment it is working.
|
||||||
*/}
|
*/}
|
||||||
{waiting > 0 && <span>{waiting} still being fetched — pictures appear as they finish.</span>}
|
{waiting > 0 && (
|
||||||
|
<span>
|
||||||
|
{waiting} still being fetched — this updates itself as they finish.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
+25
-10
@@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useSearchParams } from 'react-router';
|
import { useSearchParams } from 'react-router';
|
||||||
import { useClient } from 'hooks/useClient';
|
import { useClient } from 'hooks/useClient';
|
||||||
import { Folder, Settings2, Layers } from 'lucide-react';
|
import { Folder, Settings2, Layers, ChevronUp, ChevronDown } from 'lucide-react';
|
||||||
import { SettingsForm } from './SettingsForm';
|
import { SettingsForm } from './SettingsForm';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -61,16 +61,8 @@ export const ListsPanel = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex size-full flex-col">
|
<div className="flex size-full flex-col">
|
||||||
<header className="flex items-center justify-between border-b px-2 py-1.5">
|
<header className="border-b px-3 py-1.5">
|
||||||
<span className="text-xs font-medium">Lists</span>
|
<span className="text-xs font-medium">Lists</span>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowSettings((was) => !was)}
|
|
||||||
className="p-1 text-muted-foreground hover:text-foreground"
|
|
||||||
aria-label="Settings"
|
|
||||||
>
|
|
||||||
<Settings2 className="size-3.5" />
|
|
||||||
</button>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{error && <p className="px-3 py-2 text-[11px] text-destructive">{error}</p>}
|
{error && <p className="px-3 py-2 text-[11px] text-destructive">{error}</p>}
|
||||||
@@ -105,7 +97,30 @@ export const ListsPanel = () => {
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
{/*
|
||||||
|
Settings, pinned to the bottom.
|
||||||
|
|
||||||
|
It was a gear in the header, which is where a settings control is easy to WRITE and not where
|
||||||
|
anybody looks for one: the owner asked for this after failing to find it. The bottom of a
|
||||||
|
sidebar is the conventional home for the thing you configure once, and being a labelled row
|
||||||
|
rather than an icon means it says what it is.
|
||||||
|
|
||||||
|
The form opens ABOVE the button rather than below it, so the control does not move when you use
|
||||||
|
it. A disclosure that walks away from the pointer as it expands is the small kind of wrong that
|
||||||
|
makes something feel unfinished.
|
||||||
|
*/}
|
||||||
{showSettings && <SettingsForm />}
|
{showSettings && <SettingsForm />}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowSettings((was) => !was)}
|
||||||
|
aria-expanded={showSettings}
|
||||||
|
className="flex w-full shrink-0 items-center gap-2 border-t px-3 py-2 text-left text-xs text-muted-foreground hover:bg-muted/50 hover:text-foreground"
|
||||||
|
>
|
||||||
|
<Settings2 className="size-3.5 shrink-0" />
|
||||||
|
<span className="flex-1">Settings</span>
|
||||||
|
{showSettings ? <ChevronDown className="size-3.5" /> : <ChevronUp className="size-3.5" />}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user