Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,19 @@

> **How long?** Main pages request up to 100 bookmarks with 2.5–3.75 seconds between pages. Folder pages, network time, and retries add to this. The progress panel shows collected counts and elapsed time; the total is not known in advance.

## Full-page bookmark library

Click the extension toolbar icon to open `library.html`, a full-tab library that reads the extension's existing cumulative archives directly. The original collection/export page remains available through **同步与备份** (Sync and backup), with a link back to the library.

- Table and card layouts with a separate full-text, quote, image, and video preview pane.
- Case-insensitive search across text, authors, quoted text, folders, and URLs; quoted phrases and `-excluded` terms are supported. Press `/` to focus search.
- Folder, media-type, and availability filters; chronological and like-count sorting; 50 results per page.
- Switch between saved accounts or refresh the local archive after a scan.
- Open an xarchive JSON file as a read-only preview without replacing the saved archive. Imported previews are not persisted across reloads.
- Download the filtered results as a JSON snapshot explicitly marked as partial coverage.

The initial library UI is in Simplified Chinese. No new extension permissions, hosted services, or build step are required. Remote media loads from the URLs already present in the archive; unavailable media cannot be recovered. The existing standalone viewer remains available.

## Updating the extension

```bash
Expand Down
4 changes: 2 additions & 2 deletions background.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ chrome.webRequest.onSendHeaders.addListener(
['requestHeaders', 'extraHeaders']
);

/** Open the options page when the extension icon is clicked. */
/** Open the full-page bookmark library when the extension icon is clicked. */
chrome.action.onClicked.addListener(() => {
chrome.runtime.openOptionsPage();
chrome.tabs.create({ url: chrome.runtime.getURL('library.html') });
});

/**
Expand Down
68 changes: 68 additions & 0 deletions lib/library-search.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
export const escapeHTML = (s) =>
String(s ?? "").replace(
/[&<>"']/g,
(c) =>
({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[
c
],
);
export function safeURL(s) {
try {
const u = new URL(s);
return ["https:", "http:"].includes(u.protocol) ? u.href : "";
} catch {
return "";
}
}
export function filterBookmarks(
all,
{ query = "", folder = "", media = "", status = "", sort = "new" } = {},
) {
const terms = (query.match(/-?"[^"]+"|\S+/g) || [])
.map((t) => ({
negative: t.startsWith("-"),
text: (t.startsWith("-") ? t.slice(1) : t)
.replaceAll('"', "")
.toLowerCase(),
}))
.filter((t) => t.text);
return all
.filter((b) => {
if (folder === "__unsorted" && b.folders?.length) return false;
if (folder && folder !== "__unsorted" && !b.folders?.includes(folder))
return false;
if (status && b.status !== status) return false;
const m = b.media || [];
if (media === "text" && m.length) return false;
if (media === "photo" && !m.some((x) => x.type === "photo")) return false;
if (
media === "video" &&
!m.some((x) => ["video", "animated_gif"].includes(x.type))
)
return false;
const text = [
b.full_text,
b.author?.name,
b.author?.screen_name,
b.quoted_tweet?.full_text,
...(b.folders || []),
...(b.entities?.urls || []).flatMap((u) => [
u.url,
u.expanded_url,
u.display_url,
]),
]
.join(" ")
.toLowerCase();
return terms.every((t) =>
t.negative ? !text.includes(t.text) : text.includes(t.text),
);
})
.sort((a, b) =>
sort === "likes"
? (b.metrics?.likes || 0) - (a.metrics?.likes || 0)
: sort === "old"
? (Date.parse(a.created_at) || 0) - (Date.parse(b.created_at) || 0)
: (Date.parse(b.created_at) || 0) - (Date.parse(a.created_at) || 0),
);
}
Loading