Recently Written · git

sbm-extension

sbm bookmarks for Firefox and Chrome: fuzzy search, add the page, sync with bm and the sbm app

git clone https://github.com/equwal/sbm-extension

Log | Files | Refs


popup.js (4418 bytes)

1 // The popup: search as you type, Enter or a click opens, "Add this page" adds.
2 "use strict";
3 
4 const api = globalThis.browser ?? globalThis.chrome;
5 const $ = (id) => document.getElementById(id);
6 const MAX_ROWS = 50;
7 let all = []; // newest first, as bm adds at the end
8 let shown = [];
9 
10 function send(msg) {
11   return api.runtime.sendMessage(msg).then((r) => {
12     if (r && r.error) throw new Error(r.error);
13     return r && r.value;
14   });
15 }
16 
17 function ago(ms) {
18   const min = Math.round((Date.now() - ms) / 60000);
19   if (min < 1) return "just now";
20   if (min < 60) return min + " min ago";
21   return Math.round(min / 60) + " h ago";
22 }
23 
24 function showStatus(s) {
25   const n = all.length + (all.length === 1 ? " bookmark" : " bookmarks");
26   let text = n + " in this browser. Sign in to sync in the options.";
27   if (s.error) text = n + ". Sync: " + s.error;
28   else if (s.token) text = n + (s.synced ? ", synced " + ago(s.synced) : "") + ".";
29   $("status").textContent = text;
30 }
31 
32 function render() {
33   shown = Fuzzy.filter(all, $("search").value, (b) => b.desc + " " + b.tags.join(" ") + " " + b.url);
34   const list = $("list");
35   list.replaceChildren();
36   for (const b of shown.slice(0, MAX_ROWS)) {
37     const li = document.createElement("li");
38     const a = document.createElement("a");
39     a.href = b.url;
40     a.textContent = b.desc || b.url;
41     const small = document.createElement("small");
42     small.textContent = [Tsv.host(b.url), b.tags.join(" ")].filter((x) => x).join("  ยท  ");
43     a.append(document.createElement("br"), small);
44     a.addEventListener("click", (e) => {
45       e.preventDefault();
46       open(b.url, e.ctrlKey || e.metaKey || e.button === 1);
47     });
48     li.append(a);
49     list.append(li);
50   }
51   const empty = $("empty");
52   empty.hidden = shown.length > 0;
53   empty.textContent = all.length === 0 ? "No bookmarks yet. Sign in to sync in the options, or add this page."
54     : "No bookmark matches. Press Enter to search the web.";
55 }
56 
57 async function load() {
58   const s = await api.storage.local.get(["text", "token", "synced", "error"]);
59   all = Tsv.parse(s.text || "").reverse();
60   render();
61   showStatus(s);
62   return s;
63 }
64 
65 /** Open in this tab, or in a new tab with Ctrl. */
66 function open(url, newTab) {
67   if (newTab) api.tabs.create({ url });
68   else api.tabs.update({ url });
69   window.close();
70 }
71 
72 $("search").addEventListener("input", render);
73 $("search").addEventListener("keydown", (e) => {
74   if (e.key !== "Enter") return;
75   const text = $("search").value;
76   if (shown.length > 0) open(shown[0].url, e.ctrlKey || e.metaKey);
77   else if (text.trim() !== "") open(Tsv.target(text), e.ctrlKey || e.metaKey);
78 });
79 
80 $("options").addEventListener("click", (e) => {
81   e.preventDefault();
82   api.runtime.openOptionsPage();
83   window.close();
84 });
85 
86 /** The form takes the place of the list while it is open. */
87 function showForm(on) {
88   for (const id of ["search", "list", "add-page"]) $(id).hidden = on;
89   $("add").hidden = !on;
90   if (on) $("empty").hidden = true;
91   else render();
92 }
93 
94 $("add-page").addEventListener("click", async () => {
95   const [tab] = await api.tabs.query({ active: true, currentWindow: true });
96   $("url").value = tab && tab.url ? tab.url : "";
97   $("desc").value = tab && tab.title ? tab.title : "";
98   $("tags").value = "";
99   const known = [...new Set(all.flatMap((b) => b.tags))].sort();
100   $("known").textContent = known.length ? "Tags in your file: " + known.join(" ") : "";
101   showForm(true);
102   $("tags").focus();
103 });
104 
105 $("cancel").addEventListener("click", () => showForm(false));
106 
107 $("add").addEventListener("submit", async (e) => {
108   e.preventDefault();
109   const bookmark = {
110     url: $("url").value.replace(/\s/g, ""),
111     desc: Tsv.clean($("desc").value).trim(),
112     tags: $("tags").value.split(/\s+/).filter((t) => t !== ""),
113   };
114   try {
115     const r = await send({ type: "add", bookmark });
116     showForm(false);
117     await load();
118     $("status").textContent = r.existing ? "Already a bookmark: " + (r.existing.desc || r.existing.url) : "Bookmark added.";
119   } catch (err) {
120     $("status").textContent = "Cannot add: " + err.message;
121   }
122 });
123 
124 api.storage.onChanged.addListener((changes, area) => {
125   if (area === "local" && (changes.text || changes.synced || changes.error)) load();
126 });
127 
128 load().then((s) => {
129   // Sync when the popup opens, unless a sync ran in the last minute.
130   if (s.token && (!s.synced || Date.now() - s.synced > 60000)) send({ type: "sync" }).catch(() => {});
131 });