Skip to content

Commit 58c12f4

Browse files
committed
db improvements and optimizations
1 parent 3d7a70f commit 58c12f4

File tree

17 files changed

+415
-195
lines changed

17 files changed

+415
-195
lines changed

README.md

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,77 @@
1616
</p>
1717

1818

19-
FavBox is a browser extension that enhances and simplifies bookmark management, without depending on external cloud storage or third-party services.
19+
FavBox is a local-first **experimental** browser extension that enhances and simplifies bookmark management without cloud storage or third-party services. It extends your browser's native bookmarking features.
2020

2121
Key features:
2222

2323
🔄 sync with your browser profile;\
2424
🔒 does not send your data to any third-party services;\
25+
🎨 minimalist and clean UI; \
2526
🏷 supports tags for easy organization;\
26-
🔍 offers advanced search, sorting, and filtering by tags, domains, and folders;\
27-
🌁 provides multiple display modes; \
28-
🌗 light and dark theme.\
29-
📖 offers page previews for quick reference;\
30-
🟢 lets you know if a bookmark already exists, to avoid creating duplicates;\
31-
🗑️ provides a feature to detect broken URLs;\
32-
⌨️ provides quick access to search with hotkeys "Ctrl + Shift + K"; \
27+
🔍 provides advanced search, sorting, and filtering capabilities based on tags, domains, folders, and webpage keywords;\
28+
🌁 multiple display modes; \
29+
🌗 light and dark theme;\
30+
🗑️ includes a health check function that detects broken URLs; \
31+
⌨️ provides quick access to search with hotkeys; \
32+
🗒️ includes functionality for creating and managing **local** notes; \
3333
❤️ free and open source;
3434

35+
### Concept
36+
37+
![image](concept.png)
38+
39+
### Implementation
40+
41+
FavBox scans all bookmarks in the browser, then makes requests to the saved pages and extracts data from them such as title, description, image, and meta tags to improve the search. All the data is stored in local storage IndexedDB. The extension also tracks all browser events related to bookmarks and synchronizes the data. It only extends the standard functionality and does not attempt to replace it. You can work with bookmarks both through the extension and the native browser’s built-in bookmark features.
42+
43+
44+
FavBox is a fully local application. To keep tags synced across devices, it uses a trick. Since bookmarks are synchronized between devices, to keep tags synchronized, the app adds them to the page title.
45+
46+
For example, if you have a bookmark titled `Google Chrome — Wikipedia`, to save tags across devices, extension appends them to the title like this:
47+
`Google Chrome — Wikipedia 🏷 #wiki #browser`
48+
49+
This way, your tags become available on other devices without using any cloud services — only through the standard Google Chrome profile sync.
50+
51+
52+
```
53+
├── public # Static assets (icons, etc.)
54+
│   └── icons
55+
├── src # Source code
56+
│   ├── assets # Global assets
57+
│   ├── components # Common reusable app components
58+
│   │   └── app
59+
│   ├── ext # Browser extension includes main app, popup, content script, and service worker
60+
│   │   ├── browser # FavBox app
61+
│   │   │   ├── components # FavBox components
62+
│   │   │   ├── layouts
63+
│   │   │   └── views
64+
│   │   ├── content # Content scripts
65+
│   │   ├── popup # Extension PopUp window
66+
│   │   └── sw # Service Worker of the browser extension
67+
│   ├── helpers # Shared utilities
68+
│   ├── parser # Library to parse HTML content
69+
│   ├── storage # IndexedDB storage
70+
│   │   └── idb
71+
│   └── workers # JS Workers
72+
└── tests
73+
├── integration
74+
└── unit
75+
```
76+
3577
### Building
3678
1. `pnpm run build` to build into `dist`
37-
2. Enable dev mode in `chrome://extensions/` and `Load unpacked` extension
79+
2. Enable dev mode in `chrome://extensions/` and `Load unpacked` extension
80+
81+
### Commands
82+
83+
- **`dev`** Start development server
84+
- **`dev:firefox`** Firefox development build (WIP)
85+
- **`build`** Production build
86+
- **`test:unit`** Run unit tests
87+
- **`test:integration`** Run integration tests
88+
89+
### TODO
90+
- Use SQLite Wasm for storage (ideal for future experiments)
91+
- Improve transaction implementation (ensure reliability & better performance)
92+
- The extension already uses a polyfill to maintain compatibility with other browsers. It would be good to test this in Firefox. (WIP)

app_demo copy.png

3.02 MB
Loading

app_demo.png

-599 KB
Loading

concept.png

113 KB
Loading

manifest.chrome.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"manifest_version": 3,
33
"name": "FavBox",
44
"description": "Clear and modern bookmarking tool.",
5-
"version": "2.0",
5+
"version": "2.0.1",
66
"permissions": [
77
"bookmarks",
88
"activeTab",

manifest.firefox.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"manifest_version": 3,
33
"name": "FavBox",
44
"description": "Clear and modern bookmarking tool.",
5-
"version": "2.0",
5+
"version": "2.0.1",
66
"permissions": [
77
"bookmarks",
88
"activeTab",

src/ext/browser/app.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { createApp } from 'vue';
22
import '@/assets/app.css';
33
import masonry from 'vue-next-masonry';
44
import Notifications from 'notiwind';
5-
import { MotionPlugin } from '@vueuse/motion';
65
import FloatingVue from 'floating-vue';
76
import router from './router';
87
import AppLayout from './layouts/AppLayout.vue';
@@ -11,7 +10,6 @@ import '@zanmato/vue3-treeselect/dist/vue3-treeselect.min.css';
1110
import '@fontsource-variable/inter';
1211

1312
const app = createApp(AppLayout).use(router).use(masonry).use(Notifications)
14-
.use(MotionPlugin)
1513
.use(FloatingVue);
1614

1715
app.mount('#app');

src/ext/browser/components/CommandPalette.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ const commands = [
161161
const paginate = async (skip) => {
162162
try {
163163
items.value.push(
164-
...(await attributeStorage.filterAttributesByKeyAndValue(command.value.value, searchTerm.value, skip, 100)),
164+
...(await attributeStorage.filterByKeyAndValue(command.value.value, searchTerm.value, skip, 100)),
165165
);
166166
} catch (e) {
167167
console.error(e);
@@ -245,7 +245,7 @@ const arraySearch = () => {
245245
246246
const dbSearch = async () => {
247247
try {
248-
items.value = await attributeStorage.filterAttributesByKeyAndValue(command.value.value, searchTerm.value, 0, 50);
248+
items.value = await attributeStorage.filterByKeyAndValue(command.value.value, searchTerm.value, 0, 50);
249249
} catch (e) {
250250
console.error(e);
251251
}

src/ext/sw/index.js

Lines changed: 70 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -67,29 +67,42 @@ browser.contextMenus.onClicked.addListener((info) => {
6767

6868
// https:// developer.browser.com/docs/extensions/reference/bookmarks/#event-onCreated
6969
browser.bookmarks.onCreated.addListener(async (id, bookmark) => {
70+
console.time(`bookmark-created-${id}`);
7071
console.warn('🎉 Handle bookmark create..', id, bookmark);
7172
if (bookmark.url === undefined) {
7273
console.warn('bad bookmark data', bookmark);
7374
return;
7475
}
7576
let response = null;
76-
const [activeTab] = await browser.tabs.query({ active: true });
77-
try {
78-
console.warn('activeTab', activeTab);
79-
console.warn('requesting html from tab', activeTab);
80-
const content = await browser.tabs.sendMessage(activeTab.id, { action: 'getHTML' });
81-
response = { html: content?.html, error: 0 };
82-
console.warn('response from tab', response);
83-
} catch (e) {
84-
console.log('No tabs. It is weird. Fetching data from internet.. 🌎');
77+
let activeTab = null;
78+
const { nativeImport } = await browser.storage.session.get('nativeImport');
79+
console.log('native import', nativeImport);
80+
81+
if (nativeImport === true) {
82+
console.log('Native browser import. Fetching data from internet.. 🌎');
8583
response = await fetchHelper.fetch(bookmark.url, 15000);
84+
} else {
85+
// fetch HTML from active tab (content script)
86+
activeTab = await browser.tabs.query({ active: true });
87+
try {
88+
console.warn('activeTab', activeTab);
89+
console.warn('requesting html from tab', activeTab);
90+
const content = await browser.tabs.sendMessage(activeTab.id, { action: 'getHTML' });
91+
response = { html: content?.html, error: 0 };
92+
console.warn('response from tab', response);
93+
} catch (e) {
94+
console.log('No tabs. It is weird. Fetching data from internet.. 🌎');
95+
response = await fetchHelper.fetch(bookmark.url, 15000);
96+
}
8697
}
98+
8799
try {
88100
if (response === null) {
89101
throw new Error('No page data: response is null');
90102
}
91-
const entity = await (new MetadataParser(bookmark, response)).getFavboxBookmark();
92-
if (entity.image === null && activeTab) {
103+
const foldersMap = await bookmarkHelper.buildFoldersMap();
104+
const entity = await (new MetadataParser(bookmark, response, foldersMap)).getFavboxBookmark();
105+
if (!nativeImport && entity.image === null && activeTab) {
93106
try {
94107
console.warn('📸 No image, take a screenshot', activeTab);
95108
const screenshot = await browser.tabs.captureVisibleTab(activeTab.windowId, { format: 'jpeg', quality: 10 });
@@ -98,48 +111,55 @@ browser.bookmarks.onCreated.addListener(async (id, bookmark) => {
98111
console.error('📸', e);
99112
}
100113
}
101-
console.warn('Entity', entity);
102-
const r = await bookmarkStorage.create(entity);
103-
console.log('🎉 Bookmark has been created..', r);
114+
console.log('🔖 Entity', entity);
115+
await bookmarkStorage.create(entity);
116+
await attributeStorage.create(entity);
104117
refreshUserInterface();
118+
console.log('🎉 Bookmark has been created..');
105119
} catch (e) {
106120
console.error('🎉', e, id, bookmark);
107121
}
122+
console.timeEnd(`bookmark-created-${id}`);
108123
});
109124

110-
// https://developer.browser.com/docs/extensions/reference/bookmarks/#event-onChanged
125+
// https://developer.chrome.com/docs/extensions/reference/bookmarks/#event-onChanged
111126
browser.bookmarks.onChanged.addListener(async (id, changeInfo) => {
127+
console.time(`bookmark-changed-${id}`);
112128
try {
113-
console.log('🔄 Bookmark has been updated..', id, changeInfo);
114129
const [bookmark] = await browser.bookmarks.get(id);
115-
116-
const folderTree = await bookmarkHelper.getFoldersTreeByBookmark(id);
130+
// folder
117131
if (!bookmark.url) {
118132
console.warn('changeInfo', changeInfo, bookmark);
119-
await bookmarkStorage.updateFolders(bookmark, folderTree);
120-
} else {
121-
const toUpdate = {
133+
console.warn('Folder', bookmark);
134+
await bookmarkStorage.updateFolderNameByFolderId(bookmark.id, bookmark.title);
135+
console.log('🔄 Folder has been updated..', id, changeInfo);
136+
}
137+
// bookmark
138+
if (bookmark.url) {
139+
await bookmarkStorage.update(id, {
122140
title: tagHelper.getTitle(changeInfo.title),
123141
tags: tagHelper.getTags(changeInfo.title),
124142
url: bookmark.url,
125143
updatedAt: new Date().toISOString(),
126-
};
127-
await bookmarkStorage.update(id, toUpdate);
128-
console.log('🔀', bookmark, toUpdate, toUpdate.tags);
129-
await attributeStorage.refreshTags();
144+
});
145+
console.log('🔄 Bookmark has been updated..', id, changeInfo);
130146
}
131147
} catch (e) {
132148
console.error('🔄', e, id, changeInfo);
133149
}
134150
try {
135-
// browser.runtime.sendMessage({ action: 'refresh' });
151+
await attributeStorage.refreshTags();
152+
await attributeStorage.refreshFolders();
153+
refreshUserInterface();
136154
} catch (e) {
137-
console.error('Refresh app UI on change', e);
155+
console.error('🔄 Error updating attributes', e);
138156
}
157+
console.timeEnd(`bookmark-changed-${id}`);
139158
});
140159

141-
// https://developer.browser.com/docs/extensions/reference/bookmarks/#event-onMoved
160+
// https://developer.chrome.com/docs/extensions/reference/bookmarks/#event-onMoved
142161
browser.bookmarks.onMoved.addListener(async (id, moveInfo) => {
162+
console.time(`bookmark-moved-${id}`);
143163
try {
144164
const [folder] = await browser.bookmarks.get(moveInfo.parentId);
145165
console.log('🗂 Bookmark has been moved..', id, moveInfo, folder);
@@ -149,21 +169,26 @@ browser.bookmarks.onMoved.addListener(async (id, moveInfo) => {
149169
folderId: folder.id,
150170
updatedAt: new Date().toISOString(),
151171
});
172+
await attributeStorage.refreshFolders();
173+
refreshUserInterface();
152174
} catch (e) {
153175
console.error('🗂', e, id, moveInfo);
154176
}
155-
refreshUserInterface();
177+
console.timeEnd(`bookmark-moved-${id}`);
156178
});
157179

158-
// https://developer.browser.com/docs/extensions/reference/bookmarks/#event-onRemoved
180+
// https://developer.chrome.com/docs/extensions/reference/bookmarks/#event-onRemoved
159181
browser.bookmarks.onRemoved.addListener(async (id, removeInfo) => {
182+
console.time(`bookmark-removed-${id}`);
160183
console.log('🗑️ Handle remove bookmark..', id, removeInfo);
184+
// folder has been deleted..
161185
if (removeInfo.node.children !== undefined) {
162186
try {
163187
const items = bookmarkHelper.getAllBookmarksFromNode(removeInfo.node);
164188
const bookmarksToRemove = items.map((bookmark) => bookmark.id);
165189
if (bookmarksToRemove.length) {
166190
const total = await bookmarkStorage.removeByIds(bookmarksToRemove);
191+
await attributeStorage.refresh();
167192
console.log('🗑️ Folder has been removed..', total, id, removeInfo);
168193
}
169194
refreshUserInterface();
@@ -172,17 +197,20 @@ browser.bookmarks.onRemoved.addListener(async (id, removeInfo) => {
172197
}
173198
return;
174199
}
200+
// single bookmark has been deleted..
175201
try {
176202
const bookmark = await bookmarkStorage.getById(id);
177203
if (!bookmark) {
178204
throw new Error(`Bookmark with ID ${id} not found in storage.`);
179205
}
180206
await bookmarkStorage.remove(id);
207+
await attributeStorage.remove(bookmark);
181208
refreshUserInterface();
182209
console.log('🗑️ Bookmark has been removed..', id, removeInfo);
183210
} catch (e) {
184211
console.error('🗑️', e);
185212
}
213+
console.timeEnd(`bookmark-removed-${id}`);
186214
});
187215

188216
// https://bugs.chromium.org/p/chromium/issues/detail?id=1185241
@@ -207,6 +235,18 @@ browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
207235
return true;
208236
});
209237

238+
// https://developer.chrome.com/docs/extensions/reference/api/bookmarks#event-onImportBegan
239+
browser.bookmarks.onImportBegan.addListener(() => {
240+
console.log('📄 Import bookmarks started');
241+
browser.storage.session.set({ nativeImport: true });
242+
});
243+
244+
// https://developer.chrome.com/docs/extensions/reference/api/bookmarks#event-onImportEnded
245+
chrome.bookmarks.onImportEnded.addListener(() => {
246+
console.log('📄 Import bookmarks ended');
247+
browser.storage.session.set({ nativeImport: false });
248+
});
249+
210250
function refreshUserInterface() {
211251
try {
212252
browser.runtime.sendMessage({ action: 'refresh' });

src/ext/sw/sync.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import MetadataParser from '@/parser/metadata';
77
import bookmarkHelper from '@/helpers/bookmark';
88

99
const sync = async () => {
10-
console.time('Execution time');
10+
console.time('Sync time');
1111
await initStorage();
1212
const bookmarkStorage = new BookmarkStorage();
1313
const attributeStorage = new AttributeStorage();
@@ -23,13 +23,15 @@ const sync = async () => {
2323
return;
2424
}
2525
await browser.storage.session.set({ status: false });
26+
const foldersMap = await bookmarkHelper.buildFoldersMap();
27+
console.log('Folders map:', foldersMap);
2628
const bookmarksIterator = await bookmarkHelper.iterateBookmarks();
2729
let batch = [];
2830
let processed = 0;
2931
for await (const b of bookmarksIterator) {
3032
batch.push(b);
3133
processed += 1;
32-
if (batch.length % 100 === 0 || processed === browserTotal) {
34+
if (batch.length % 150 === 0 || processed === browserTotal) {
3335
try {
3436
const browserBookmarkKeyList = batch.map((i) => i.id);
3537
const extBookmarksKeyList = await bookmarkStorage.getIds(browserBookmarkKeyList);
@@ -44,7 +46,7 @@ const sync = async () => {
4446
return { bookmark, response };
4547
}));
4648
const parseResult = await Promise.all(
47-
httpResults.map(({ bookmark, response }) => (new MetadataParser(bookmark, response)).getFavboxBookmark()),
49+
httpResults.map(({ bookmark, response }) => (new MetadataParser(bookmark, response, foldersMap)).getFavboxBookmark()),
4850
);
4951
console.time('DB execution time');
5052
console.warn(parseResult);
@@ -66,7 +68,7 @@ const sync = async () => {
6668
}
6769
}
6870
await browser.storage.session.set({ status: true });
69-
console.timeEnd('Execution time');
71+
console.timeEnd('Sync time');
7072
};
7173

7274
export default sync;

0 commit comments

Comments
 (0)