+34
-30
package.json
+34
-30
package.json
···
1
1
{
2
-
"name": "rabbithole",
3
-
"version": "3.0.0",
4
-
"type": "module",
5
-
"repository": {
6
-
"type": "git",
7
-
"url": "https://github.com/nekitcorp/chrome-extension-svelte-typescript-boilerplate.git"
8
-
},
9
-
"scripts": {
10
-
"dev": "vite",
11
-
"build": "vite build",
12
-
"check": "svelte-check --tsconfig ./tsconfig.json"
13
-
},
14
-
"devDependencies": {
15
-
"@crxjs/vite-plugin": "1.0.14",
16
-
"@sveltejs/vite-plugin-svelte": "1.1.0",
17
-
"@tsconfig/svelte": "3.0.0",
18
-
"@types/chrome": "0.0.200",
19
-
"svelte": "3.52.0",
20
-
"svelte-check": "2.9.2",
21
-
"svelte-preprocess": "4.10.7",
22
-
"tslib": "2.4.0",
23
-
"typescript": "4.8.4",
24
-
"vite": "3.2.0"
25
-
},
26
-
"dependencies": {
27
-
"@svelteuidev/core": "^0.15.3",
28
-
"fuse.js": "^7.0.0",
29
-
"radix-icons-svelte": "^1.2.1",
30
-
"uuid": "^9.0.0"
31
-
}
2
+
"name": "rabbithole",
3
+
"version": "3.0.0",
4
+
"type": "module",
5
+
"repository": {
6
+
"type": "git",
7
+
"url": "https://github.com/nekitcorp/chrome-extension-svelte-typescript-boilerplate.git"
8
+
},
9
+
"scripts": {
10
+
"dev": "vite",
11
+
"build": "vite build",
12
+
"check": "svelte-check --tsconfig ./tsconfig.json"
13
+
},
14
+
"devDependencies": {
15
+
"@crxjs/vite-plugin": "1.0.14",
16
+
"@sveltejs/vite-plugin-svelte": "1.1.0",
17
+
"@tsconfig/svelte": "3.0.0",
18
+
"@types/chrome": "0.0.200",
19
+
"prettier": "^3.6.2",
20
+
"prettier-plugin-svelte": "^3.4.0",
21
+
"svelte": "3.52.0",
22
+
"svelte-check": "2.9.2",
23
+
"svelte-preprocess": "4.10.7",
24
+
"tslib": "2.4.0",
25
+
"typescript": "4.8.4",
26
+
"vite": "3.2.0"
27
+
},
28
+
"dependencies": {
29
+
"@svelteuidev/core": "^0.15.3",
30
+
"d3-scale": "^4.0.2",
31
+
"d3-time": "^3.1.0",
32
+
"fuse.js": "^7.0.0",
33
+
"radix-icons-svelte": "^1.2.1",
34
+
"uuid": "^9.0.0"
35
+
}
32
36
}
+82
pnpm-lock.yaml
+82
pnpm-lock.yaml
···
8
8
'@svelteuidev/core':
9
9
specifier: ^0.15.3
10
10
version: 0.15.3(@svelteuidev/composables@0.15.3)(svelte@3.52.0)
11
+
d3-scale:
12
+
specifier: ^4.0.2
13
+
version: 4.0.2
14
+
d3-time:
15
+
specifier: ^3.1.0
16
+
version: 3.1.0
11
17
fuse.js:
12
18
specifier: ^7.0.0
13
19
version: 7.0.0
···
31
37
'@types/chrome':
32
38
specifier: 0.0.200
33
39
version: 0.0.200
40
+
prettier:
41
+
specifier: ^3.6.2
42
+
version: 3.6.2
43
+
prettier-plugin-svelte:
44
+
specifier: ^3.4.0
45
+
version: 3.4.0(prettier@3.6.2)(svelte@3.52.0)
34
46
svelte:
35
47
specifier: 3.52.0
36
48
version: 3.52.0
···
738
750
engines: {node: '>= 6'}
739
751
dev: true
740
752
753
+
/d3-array@3.2.4:
754
+
resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
755
+
engines: {node: '>=12'}
756
+
dependencies:
757
+
internmap: 2.0.3
758
+
dev: false
759
+
760
+
/d3-color@3.1.0:
761
+
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
762
+
engines: {node: '>=12'}
763
+
dev: false
764
+
765
+
/d3-format@3.1.0:
766
+
resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==}
767
+
engines: {node: '>=12'}
768
+
dev: false
769
+
770
+
/d3-interpolate@3.0.1:
771
+
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
772
+
engines: {node: '>=12'}
773
+
dependencies:
774
+
d3-color: 3.1.0
775
+
dev: false
776
+
777
+
/d3-scale@4.0.2:
778
+
resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
779
+
engines: {node: '>=12'}
780
+
dependencies:
781
+
d3-array: 3.2.4
782
+
d3-format: 3.1.0
783
+
d3-interpolate: 3.0.1
784
+
d3-time: 3.1.0
785
+
d3-time-format: 4.1.0
786
+
dev: false
787
+
788
+
/d3-time-format@4.1.0:
789
+
resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==}
790
+
engines: {node: '>=12'}
791
+
dependencies:
792
+
d3-time: 3.1.0
793
+
dev: false
794
+
795
+
/d3-time@3.1.0:
796
+
resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==}
797
+
engines: {node: '>=12'}
798
+
dependencies:
799
+
d3-array: 3.2.4
800
+
dev: false
801
+
741
802
/debug@2.6.9:
742
803
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
743
804
peerDependencies:
···
1175
1236
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
1176
1237
dev: true
1177
1238
1239
+
/internmap@2.0.3:
1240
+
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
1241
+
engines: {node: '>=12'}
1242
+
dev: false
1243
+
1178
1244
/is-binary-path@2.1.0:
1179
1245
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
1180
1246
engines: {node: '>=8'}
···
1384
1450
nanoid: 3.3.7
1385
1451
picocolors: 1.0.0
1386
1452
source-map-js: 1.0.2
1453
+
dev: true
1454
+
1455
+
/prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@3.52.0):
1456
+
resolution: {integrity: sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ==}
1457
+
peerDependencies:
1458
+
prettier: ^3.0.0
1459
+
svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0
1460
+
dependencies:
1461
+
prettier: 3.6.2
1462
+
svelte: 3.52.0
1463
+
dev: true
1464
+
1465
+
/prettier@3.6.2:
1466
+
resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==}
1467
+
engines: {node: '>=14'}
1468
+
hasBin: true
1387
1469
dev: true
1388
1470
1389
1471
/q@1.5.1:
+256
-247
src/background/index.ts
+256
-247
src/background/index.ts
···
1
-
import { MessageRequest } from "../utils"
2
-
import { WebsiteStore } from "../storage/db"
1
+
import { MessageRequest } from "../utils";
2
+
import { WebsiteStore } from "../storage/db";
3
3
4
4
function sendOverlayUpdate(tabId: number) {
5
5
// ignore errors; receiving end won't exist if it's the newtab page
6
6
try {
7
-
chrome.tabs.sendMessage(tabId, { type: MessageRequest.PING })
8
-
} catch (err) {
9
-
}
7
+
chrome.tabs.sendMessage(tabId, { type: MessageRequest.PING });
8
+
} catch (err) {}
10
9
}
11
10
12
11
// this is meant to be called async
13
-
function storeWebsites(tabs: chrome.tabs.Tab[], db: WebsiteStore, sendResponse: any): Promise<void[]> {
12
+
function storeWebsites(
13
+
tabs: chrome.tabs.Tab[],
14
+
db: WebsiteStore,
15
+
sendResponse: any,
16
+
): Promise<void[]> {
14
17
// delegate this to db?
15
18
// FIXME: add some guarantees that this won't randomly crash
16
-
const promiseArray = tabs.map(tab => fetch(`https://cardyb.bsky.app/v1/extract?url=${encodeURIComponent(tab.url)}`,
17
-
{
18
-
method: 'GET',
19
-
redirect: 'follow'
20
-
})
21
-
.then(response => response.json())
22
-
.then(result => {
23
-
return {
24
-
url: tab.url,
25
-
name: (result.error === "" && result.title !== "") ? result.title : tab.title,
26
-
faviconUrl: tab.favIconUrl,
27
-
savedAt: Date.now(),
28
-
openGraphImageUrl: (result.error === "") ? result.image : null,
29
-
description: (result.error === "") ? result.description : null,
30
-
};
31
-
})
32
-
.then(website => {
33
-
db.saveWebsiteToProject([website])
34
-
.then(res => sendResponse(res))
35
-
.catch(err => {
36
-
console.log(err)
37
-
sendResponse(err)
38
-
});
39
-
})
40
-
.catch(error => {
41
-
// just use info at hand if OG information cannot be retrieved
42
-
// TODO: are there cases when it's preferable to do this?
43
-
db.saveWebsiteToProject([{
44
-
url: tab.url,
45
-
name: tab.title,
46
-
faviconUrl: tab.favIconUrl,
47
-
savedAt: Date.now(),
48
-
openGraphImageUrl: null,
49
-
description: null,
50
-
}])
51
-
.then(res => sendResponse(res))
52
-
.catch(err => {
53
-
console.log(err)
54
-
sendResponse(err)
55
-
});
56
-
}));
19
+
const promiseArray = tabs.map((tab) =>
20
+
fetch(
21
+
`https://cardyb.bsky.app/v1/extract?url=${encodeURIComponent(tab.url)}`,
22
+
{
23
+
method: "GET",
24
+
redirect: "follow",
25
+
},
26
+
)
27
+
.then((response) => response.json())
28
+
.then((result) => {
29
+
return {
30
+
url: tab.url,
31
+
name:
32
+
result.error === "" && result.title !== ""
33
+
? result.title
34
+
: tab.title,
35
+
faviconUrl: tab.favIconUrl,
36
+
savedAt: Date.now(),
37
+
openGraphImageUrl: result.error === "" ? result.image : null,
38
+
description: result.error === "" ? result.description : null,
39
+
};
40
+
})
41
+
.then((website) => {
42
+
db.saveWebsiteToProject([website])
43
+
.then((res) => sendResponse(res))
44
+
.catch((err) => {
45
+
console.log(err);
46
+
sendResponse(err);
47
+
});
48
+
})
49
+
.catch((error) => {
50
+
// just use info at hand if OG information cannot be retrieved
51
+
// TODO: are there cases when it's preferable to do this?
52
+
db.saveWebsiteToProject([
53
+
{
54
+
url: tab.url,
55
+
name: tab.title,
56
+
faviconUrl: tab.favIconUrl,
57
+
savedAt: Date.now(),
58
+
openGraphImageUrl: null,
59
+
description: null,
60
+
},
61
+
])
62
+
.then((res) => sendResponse(res))
63
+
.catch((err) => {
64
+
console.log(err);
65
+
sendResponse(err);
66
+
});
67
+
}),
68
+
);
57
69
58
70
return Promise.all(promiseArray);
59
71
}
60
72
61
73
chrome.runtime.onInstalled.addListener(async () => {
62
-
console.log("just installed!")
74
+
console.log("just installed!");
63
75
WebsiteStore.init(indexedDB);
64
76
});
65
77
66
-
chrome.runtime.onMessage.addListener(
67
-
(request, sender, sendResponse) => {
68
-
if (!("type" in request)) {
69
-
sendResponse({
70
-
error: "request type required"
71
-
});
72
-
}
73
-
const db = new WebsiteStore(indexedDB)
78
+
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
79
+
if (!("type" in request)) {
80
+
sendResponse({
81
+
error: "request type required",
82
+
});
83
+
}
84
+
const db = new WebsiteStore(indexedDB);
74
85
75
-
// ugly callback/.then syntax because of listener function quirks
76
-
// see https://stackoverflow.com/questions/70353944/chrome-runtime-onmessage-returns-undefined-even-when-value-is-known-for-asynch
77
-
switch (request.type) {
78
-
case MessageRequest.SAVE_TAB:
79
-
chrome.tabs.query({ active: true, lastFocusedWindow: true },
80
-
(tabs) => storeWebsites(tabs, db, sendResponse));
86
+
// ugly callback/.then syntax because of listener function quirks
87
+
// see https://stackoverflow.com/questions/70353944/chrome-runtime-onmessage-returns-undefined-even-when-value-is-known-for-asynch
88
+
switch (request.type) {
89
+
case MessageRequest.SAVE_TAB:
90
+
chrome.tabs.query({ active: true, lastFocusedWindow: true }, (tabs) =>
91
+
storeWebsites(tabs, db, sendResponse),
92
+
);
93
+
break;
94
+
case MessageRequest.GET_ALL_ITEMS:
95
+
db.getAllWebsites()
96
+
.then((res) => sendResponse(res))
97
+
.catch((err) => {
98
+
console.log(err);
99
+
sendResponse(err);
100
+
});
101
+
break;
102
+
case MessageRequest.GET_SETTINGS:
103
+
db.getSettings()
104
+
.then((res) => sendResponse(res))
105
+
.catch((err) => {
106
+
console.log(err);
107
+
sendResponse(err);
108
+
});
109
+
break;
110
+
case MessageRequest.UPDATE_SETTINGS:
111
+
if (!("settings" in request)) {
112
+
sendResponse({
113
+
error: "settings required",
114
+
});
81
115
break;
82
-
case MessageRequest.GET_ALL_ITEMS:
83
-
db.getAllWebsites()
84
-
.then((res) => sendResponse(res))
85
-
.catch(err => {
86
-
console.log(err)
87
-
sendResponse(err)
88
-
});
116
+
}
117
+
db.updateSettings(request.settings)
118
+
.then((res) => sendResponse(res))
119
+
.catch((err) => {
120
+
console.log(err);
121
+
sendResponse(err);
122
+
});
123
+
break;
124
+
case MessageRequest.GET_ALL_PROJECTS:
125
+
db.getAllProjects()
126
+
.then((res) => sendResponse(res))
127
+
.catch((err) => {
128
+
console.log(err);
129
+
sendResponse(err);
130
+
});
131
+
break;
132
+
case MessageRequest.GET_PROJECT_SAVED_WEBSITES:
133
+
if (!("projectId" in request)) {
134
+
sendResponse({
135
+
error: "projectId required",
136
+
});
89
137
break;
90
-
case MessageRequest.GET_SETTINGS:
91
-
db.getSettings()
92
-
.then((res) => sendResponse(res))
93
-
.catch(err => {
94
-
console.log(err)
95
-
sendResponse(err)
96
-
});
138
+
}
139
+
db.getAllWebsitesForProject(request.projectId)
140
+
.then((res) => sendResponse(res))
141
+
.catch((err) => {
142
+
console.log(err);
143
+
sendResponse(err);
144
+
});
145
+
break;
146
+
case MessageRequest.CREATE_NEW_PROJECT:
147
+
if (!("newProjectName" in request)) {
148
+
sendResponse({
149
+
error: "newProjectName required",
150
+
});
97
151
break;
98
-
case MessageRequest.UPDATE_SETTINGS:
99
-
if (!("settings" in request)) {
100
-
sendResponse({
101
-
error: "settings required"
102
-
});
103
-
break;
104
-
}
105
-
db.updateSettings(request.settings)
106
-
.then((res) => sendResponse(res))
107
-
.catch(err => {
108
-
console.log(err)
109
-
sendResponse(err)
110
-
});
152
+
}
153
+
db.createNewActiveProject(request.newProjectName)
154
+
.then((res) => sendResponse(res))
155
+
.catch((err) => {
156
+
console.log(err);
157
+
sendResponse(err);
158
+
});
159
+
break;
160
+
case MessageRequest.CHANGE_ACTIVE_PROJECT:
161
+
if (!("projectId" in request)) {
162
+
sendResponse({
163
+
error: "projectId required",
164
+
});
111
165
break;
112
-
case MessageRequest.GET_ALL_PROJECTS:
113
-
db.getAllProjects()
114
-
.then((res) => sendResponse(res))
115
-
.catch(err => {
116
-
console.log(err)
117
-
sendResponse(err)
118
-
});
166
+
}
167
+
db.changeActiveProject(request.projectId)
168
+
.then((res) => sendResponse(res))
169
+
.catch((err) => {
170
+
console.log(err);
171
+
sendResponse(err);
172
+
});
173
+
break;
174
+
case MessageRequest.GET_ACTIVE_PROJECT:
175
+
db.getActiveProject()
176
+
.then((res) => sendResponse(res))
177
+
.catch((err) => {
178
+
console.log(err);
179
+
sendResponse(err);
180
+
});
181
+
break;
182
+
case MessageRequest.GET_PROJECT:
183
+
if (!("projectId" in request)) {
184
+
sendResponse({
185
+
error: "projectId required",
186
+
});
119
187
break;
120
-
case MessageRequest.GET_PROJECT_SAVED_WEBSITES:
121
-
if (!("projectId" in request)) {
122
-
sendResponse({
123
-
error: "projectId required"
124
-
});
125
-
break;
126
-
}
127
-
db.getAllWebsitesForProject(request.projectId)
128
-
.then((res) => sendResponse(res))
129
-
.catch(err => {
130
-
console.log(err)
131
-
sendResponse(err)
132
-
});
133
-
break;
134
-
case MessageRequest.CREATE_NEW_PROJECT:
188
+
}
189
+
db.getProject(request.projectId)
190
+
.then((res) => sendResponse(res))
191
+
.catch((err) => {
192
+
console.log(err);
193
+
sendResponse(err);
194
+
});
195
+
break;
196
+
case MessageRequest.SAVE_WINDOW_TO_NEW_PROJECT:
197
+
chrome.tabs.query({ windowId: sender.tab.windowId }).then((tabs) => {
198
+
let websites: string[] = tabs.map((tab) => tab.url);
199
+
// store websites async
200
+
storeWebsites(tabs, db, sendResponse);
201
+
135
202
if (!("newProjectName" in request)) {
136
203
sendResponse({
137
-
error: "newProjectName required"
138
-
});
139
-
break;
140
-
}
141
-
db.createNewActiveProject(request.newProjectName)
142
-
.then((res) => sendResponse(res))
143
-
.catch(err => {
144
-
console.log(err)
145
-
sendResponse(err)
146
-
});
147
-
break;
148
-
case MessageRequest.CHANGE_ACTIVE_PROJECT:
149
-
if (!("projectId" in request)) {
150
-
sendResponse({
151
-
error: "projectId required"
152
-
});
153
-
break;
154
-
}
155
-
db.changeActiveProject(request.projectId)
156
-
.then((res) => sendResponse(res))
157
-
.catch(err => {
158
-
console.log(err)
159
-
sendResponse(err)
160
-
});
161
-
break;
162
-
case MessageRequest.GET_ACTIVE_PROJECT:
163
-
db.getActiveProject()
164
-
.then((res) => sendResponse(res))
165
-
.catch(err => {
166
-
console.log(err)
167
-
sendResponse(err)
168
-
});
169
-
break;
170
-
case MessageRequest.GET_PROJECT:
171
-
if (!("projectId" in request)) {
172
-
sendResponse({
173
-
error: "projectId required"
204
+
error: "projectName required",
174
205
});
175
-
break;
176
206
}
177
-
db.getProject(request.projectId)
207
+
// FIXME: remove websites to see if that fixes double website store
208
+
db.createNewActiveProject(request.newProjectName, websites)
178
209
.then((res) => sendResponse(res))
179
-
.catch(err => {
180
-
console.log(err)
181
-
sendResponse(err)
210
+
.catch((err) => {
211
+
console.log(err);
212
+
sendResponse(err);
182
213
});
183
-
break;
184
-
case MessageRequest.SAVE_WINDOW_TO_NEW_PROJECT:
185
-
chrome.tabs.query({ windowId: sender.tab.windowId })
186
-
.then(tabs => {
187
-
let websites: string[] = tabs.map(tab => tab.url);
188
-
// store websites async
189
-
storeWebsites(tabs, db, sendResponse);
214
+
});
190
215
191
-
if (!("newProjectName" in request)) {
192
-
sendResponse({
193
-
error: "projectName required"
194
-
});
195
-
}
196
-
// FIXME: remove websites to see if that fixes double website store
197
-
db.createNewActiveProject(request.newProjectName, websites)
198
-
.then((res) => sendResponse(res))
199
-
.catch(err => {
200
-
console.log(err)
201
-
sendResponse(err)
202
-
});
203
-
});
216
+
break;
217
+
case MessageRequest.SAVE_WINDOW_TO_ACTIVE_PROJECT:
218
+
chrome.tabs.query({ windowId: sender.tab.windowId }).then((tabs) => {
219
+
// store websites async
220
+
storeWebsites(tabs, db, sendResponse);
221
+
});
204
222
223
+
break;
224
+
case MessageRequest.RENAME_PROJECT:
225
+
if (!("newName" in request) || !("projectId" in request)) {
226
+
sendResponse({
227
+
error: "projectId and newName required",
228
+
});
205
229
break;
206
-
case MessageRequest.SAVE_WINDOW_TO_ACTIVE_PROJECT:
207
-
chrome.tabs.query({ windowId: sender.tab.windowId })
208
-
.then(tabs => {
209
-
// store websites async
210
-
storeWebsites(tabs, db, sendResponse);
211
-
});
212
-
230
+
}
231
+
db.renameProject(request.projectId, request.newName)
232
+
.then((res) => sendResponse(res))
233
+
.catch((err) => {
234
+
console.log(err);
235
+
sendResponse(err);
236
+
});
237
+
break;
238
+
case MessageRequest.DELETE_PROJECT:
239
+
if (!("projectId" in request)) {
240
+
sendResponse({
241
+
error: "projectId required",
242
+
});
213
243
break;
214
-
case MessageRequest.RENAME_PROJECT:
215
-
if (!("newName" in request) || !("projectId" in request)) {
216
-
sendResponse({
217
-
error: "projectId and newName required"
218
-
});
219
-
break;
220
-
}
221
-
db.renameProject(request.projectId, request.newName)
222
-
.then(res => sendResponse(res))
223
-
.catch(err => {
224
-
console.log(err)
225
-
sendResponse(err)
226
-
});
244
+
}
245
+
db.deleteProject(request.projectId)
246
+
.then((res) => sendResponse(res))
247
+
.catch((err) => {
248
+
console.log(err);
249
+
sendResponse(err);
250
+
});
251
+
break;
252
+
case MessageRequest.DELETE_WEBSITE:
253
+
if (!("url" in request) || !("projectId" in request)) {
254
+
sendResponse({
255
+
error: "projectId and url required",
256
+
});
227
257
break;
228
-
case MessageRequest.DELETE_PROJECT:
229
-
if (!("projectId" in request)) {
230
-
sendResponse({
231
-
error: "projectId required"
232
-
});
233
-
break;
234
-
}
235
-
db.deleteProject(request.projectId)
236
-
.then(res => sendResponse(res))
237
-
.catch(err => {
238
-
console.log(err)
239
-
sendResponse(err)
240
-
});
241
-
break;
242
-
case MessageRequest.DELETE_WEBSITE:
243
-
if (!("url" in request) || !("projectId" in request)) {
244
-
sendResponse({
245
-
error: "projectId and url required"
246
-
});
247
-
break;
248
-
}
249
-
db.deleteWebsiteFromProject(request.projectId, request.url)
250
-
.then(res => sendResponse(res))
251
-
.catch(err => {
252
-
console.log(err)
253
-
sendResponse(err)
254
-
});
255
-
break;
256
-
default:
257
-
}
258
+
}
259
+
db.deleteWebsiteFromProject(request.projectId, request.url)
260
+
.then((res) => sendResponse(res))
261
+
.catch((err) => {
262
+
console.log(err);
263
+
sendResponse(err);
264
+
});
265
+
break;
266
+
default:
267
+
}
258
268
259
-
// arcane incantation required for async `sendResponse`s to work
260
-
// https://developer.chrome.com/docs/extensions/mv3/messaging/#simple
261
-
return true;
262
-
}
263
-
);
269
+
// arcane incantation required for async `sendResponse`s to work
270
+
// https://developer.chrome.com/docs/extensions/mv3/messaging/#simple
271
+
return true;
272
+
});
264
273
265
274
// send updates to tabs when created, changed, updated
266
275
// chrome.tabs.onUpdated.addListener((tabId, info) => {
···
268
277
// sendOverlayUpdate(tabId);
269
278
// }
270
279
// });
271
-
chrome.tabs.onActivated.addListener(tab => {
272
-
chrome.tabs.query({ active: true, lastFocusedWindow: true },
273
-
(tabInfo) => {
274
-
// FIXME: firefox? also maybe should be abstracted
275
-
if ("pendingUrl" in tabInfo[0]) {
276
-
if (!tabInfo[0].pendingUrl.includes("chrome://") &&
277
-
!tabInfo[0].pendingUrl.includes("brave://") &&
278
-
!tabInfo[0].pendingUrl.includes("edge://")
279
-
) {
280
-
sendOverlayUpdate(tab.tabId);
281
-
}
282
-
} else {
283
-
if (!tabInfo[0].url.includes("chrome://") &&
284
-
!tabInfo[0].url.includes("brave://") &&
285
-
!tabInfo[0].url.includes("edge://")
286
-
) {
287
-
sendOverlayUpdate(tab.tabId);
288
-
}
280
+
chrome.tabs.onActivated.addListener((tab) => {
281
+
chrome.tabs.query({ active: true, lastFocusedWindow: true }, (tabInfo) => {
282
+
// FIXME: firefox? also maybe should be abstracted
283
+
if ("pendingUrl" in tabInfo[0]) {
284
+
if (
285
+
!tabInfo[0].pendingUrl.includes("chrome://") &&
286
+
!tabInfo[0].pendingUrl.includes("brave://") &&
287
+
!tabInfo[0].pendingUrl.includes("edge://")
288
+
) {
289
+
sendOverlayUpdate(tab.tabId);
290
+
}
291
+
} else {
292
+
if (
293
+
!tabInfo[0].url.includes("chrome://") &&
294
+
!tabInfo[0].url.includes("brave://") &&
295
+
!tabInfo[0].url.includes("edge://")
296
+
) {
297
+
sendOverlayUpdate(tab.tabId);
289
298
}
290
299
}
291
-
)
300
+
});
292
301
});
+11
-13
src/content/index.ts
+11
-13
src/content/index.ts
···
18
18
}
19
19
20
20
loadOverlay();
21
-
chrome.runtime.onMessage.addListener(
22
-
function(request, sender, sendResponse) {
23
-
if (!("type" in request)) {
24
-
sendResponse({
25
-
error: "request type required"
26
-
});
27
-
}
28
-
if (request.type === MessageRequest.PING) {
29
-
// load floating action overlay
30
-
loadOverlay();
31
-
sendResponse();
32
-
}
21
+
chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
22
+
if (!("type" in request)) {
23
+
sendResponse({
24
+
error: "request type required",
25
+
});
33
26
}
34
-
);
27
+
if (request.type === MessageRequest.PING) {
28
+
// load floating action overlay
29
+
loadOverlay();
30
+
sendResponse();
31
+
}
32
+
});
+10
-6
src/lib/Options.svelte
+10
-6
src/lib/Options.svelte
···
1
1
<script lang="ts">
2
-
import { MessageRequest, NotificationDuration } from "../utils"
3
-
import { Button, Group } from '@svelteuidev/core';
2
+
import { MessageRequest, NotificationDuration } from "../utils";
3
+
import { Button, Group } from "@svelteuidev/core";
4
4
5
5
let successMessage: string = null;
6
6
7
7
async function save() {
8
-
const [savedTab] = await chrome.runtime.sendMessage({type: MessageRequest.SAVE_TAB});
9
-
successMessage = (savedTab.alreadySaved)? "Website already saved!" : "Website saved!";
8
+
const [savedTab] = await chrome.runtime.sendMessage({
9
+
type: MessageRequest.SAVE_TAB,
10
+
});
11
+
successMessage = savedTab.alreadySaved
12
+
? "Website already saved!"
13
+
: "Website saved!";
10
14
setTimeout(() => {
11
15
successMessage = null;
12
16
}, NotificationDuration);
···
15
19
16
20
<div class="container">
17
21
<Group position="center">
18
-
<Button on:click={save} size="md" id="move" color='blue'>
22
+
<Button on:click={save} size="md" id="move" color="blue">
19
23
Save Website
20
24
</Button>
21
-
{#if successMessage}<span class="success">{successMessage}</span>{/if}
25
+
{#if successMessage}<span class="success">{successMessage}</span>{/if}
22
26
</Group>
23
27
</div>
24
28
+26
-15
src/lib/Overlay.svelte
+26
-15
src/lib/Overlay.svelte
···
1
1
<script lang="ts">
2
2
import { onMount } from "svelte";
3
-
import { Button, Group, Tooltip } from '@svelteuidev/core';
3
+
import { Button, Group, Tooltip } from "@svelteuidev/core";
4
4
import Options from "./Options.svelte";
5
-
import ProjectSelector from "src/lib/ProjectSelector.svelte"
6
-
import { MessageRequest, getOrderedProjects } from "../utils.ts"
5
+
import ProjectSelector from "src/lib/ProjectSelector.svelte";
6
+
import { MessageRequest, getOrderedProjects } from "../utils.ts";
7
7
8
8
let settings: Settings = {
9
9
show: false,
10
-
alignment: "right"
10
+
alignment: "right",
11
11
};
12
12
let projects = [];
13
13
let isHovering = false;
14
14
15
15
onMount(async () => {
16
-
settings = await chrome.runtime.sendMessage({type: MessageRequest.GET_SETTINGS});
17
-
projects = await getOrderedProjects()
16
+
settings = await chrome.runtime.sendMessage({
17
+
type: MessageRequest.GET_SETTINGS,
18
+
});
19
+
projects = await getOrderedProjects();
18
20
});
19
21
20
22
function changeAlignment(event) {
···
49
51
<div id="overlay-container" class="overlay {settings.alignment}">
50
52
<div class="buttons">
51
53
<Group position="center" spacing="md">
52
-
<Button on:click={changeAlignment} id="move" variant='light' color='blue'>
54
+
<Button
55
+
on:click={changeAlignment}
56
+
id="move"
57
+
variant="light"
58
+
color="blue"
59
+
>
53
60
Move
54
61
</Button>
55
62
<Tooltip {isHovering} label="You can unhide from the newtab page">
56
63
<Button
57
64
on:click={hideOverlay}
58
-
on:mouseenter={()=>{isHovering=true}}
59
-
on:mouseleave={()=>{isHovering=false}}
65
+
on:mouseenter={() => {
66
+
isHovering = true;
67
+
}}
68
+
on:mouseleave={() => {
69
+
isHovering = false;
70
+
}}
60
71
id="move"
61
-
variant='light'
62
-
color='blue'
63
-
>
72
+
variant="light"
73
+
color="blue"
74
+
>
64
75
Hide
65
76
</Button>
66
77
</Tooltip>
67
78
</Group>
68
79
</div>
69
80
<div class="selector">
70
-
<ProjectSelector projects={projects} handleProjectChange={handleProjectChange} />
81
+
<ProjectSelector {projects} {handleProjectChange} />
71
82
</div>
72
83
<div>
73
-
<Options/>
84
+
<Options />
74
85
</div>
75
86
</div>
76
87
{/if}
···
92
103
height: 200px;
93
104
position: fixed;
94
105
bottom: 16px;
95
-
background-color: rgba(255,255,255,0.8);
106
+
background-color: rgba(255, 255, 255, 0.8);
96
107
border-radius: 20px;
97
108
border: 1px solid black;
98
109
padding: 4px;
+7
-4
src/lib/ProjectSelector.svelte
+7
-4
src/lib/ProjectSelector.svelte
···
1
1
<script>
2
2
import { onMount } from "svelte";
3
-
import { NativeSelect } from '@svelteuidev/core';
4
-
import { MessageRequest } from "../utils.ts"
3
+
import { NativeSelect } from "@svelteuidev/core";
4
+
import { MessageRequest } from "../utils.ts";
5
5
6
6
export let handleProjectChange;
7
7
export let projects = [];
8
8
</script>
9
9
10
10
<div>
11
-
<NativeSelect data={projects.map(p => { return { label: p.name, value: p.id }; })}
11
+
<NativeSelect
12
+
data={projects.map((p) => {
13
+
return { label: p.name, value: p.id };
14
+
})}
12
15
on:change={handleProjectChange}
13
-
/>
16
+
/>
14
17
</div>
+104
-26
src/lib/Rabbithole.svelte
+104
-26
src/lib/Rabbithole.svelte
···
1
1
<script>
2
2
import { onMount } from "svelte";
3
-
import Timeline from "src/lib/Timeline.svelte"
4
-
import Sidebar from "src/lib/Sidebar.svelte"
5
-
import { MessageRequest, getOrderedProjects, NotificationDuration } from "../utils"
6
-
import { SvelteUIProvider, fns, AppShell, Navbar, Title, Divider } from "@svelteuidev/core";
3
+
import Timeline from "src/lib/Timeline.svelte";
4
+
import Sidebar from "src/lib/Sidebar.svelte";
5
+
import {
6
+
MessageRequest,
7
+
getOrderedProjects,
8
+
NotificationDuration,
9
+
} from "../utils";
10
+
import {
11
+
SvelteUIProvider,
12
+
fns,
13
+
AppShell,
14
+
Navbar,
15
+
Title,
16
+
Divider,
17
+
} from "@svelteuidev/core";
7
18
8
19
let activeProject = {};
9
20
let websites = [];
10
21
let projects = [];
11
-
let isDark = true;
22
+
let isDark = false;
12
23
let opened = false;
13
24
14
25
// status for updatingComponents
···
18
29
let syncFail = false;
19
30
20
31
onMount(async () => {
21
-
projects = await getOrderedProjects()
22
-
activeProject = await chrome.runtime.sendMessage({ type: MessageRequest.GET_ACTIVE_PROJECT })
32
+
projects = await getOrderedProjects();
33
+
activeProject = await chrome.runtime.sendMessage({
34
+
type: MessageRequest.GET_ACTIVE_PROJECT,
35
+
});
23
36
updateWebsites();
24
37
});
25
38
···
64
77
setTimeout(() => {
65
78
updateWebsites();
66
79
syncSuccess = true;
67
-
setTimeout(() => { syncSuccess = false }, NotificationDuration);
80
+
setTimeout(() => {
81
+
syncSuccess = false;
82
+
}, NotificationDuration);
68
83
}, 300);
69
84
setTimeout(() => {
70
85
updateWebsites();
···
75
90
activeProject = await chrome.runtime.sendMessage({
76
91
type: MessageRequest.RENAME_PROJECT,
77
92
newName: event.detail.newActiveProjectName,
78
-
projectId: activeProject.id
93
+
projectId: activeProject.id,
79
94
});
80
95
projects = await getOrderedProjects();
81
96
}
···
83
98
async function deleteActiveProject(event) {
84
99
activeProject = await chrome.runtime.sendMessage({
85
100
type: MessageRequest.DELETE_PROJECT,
86
-
projectId: activeProject.id
101
+
projectId: activeProject.id,
87
102
});
88
103
projects = await getOrderedProjects();
89
104
updateWebsites();
···
93
108
await chrome.runtime.sendMessage({
94
109
type: MessageRequest.DELETE_WEBSITE,
95
110
projectId: activeProject.id,
96
-
url: event.detail.url
111
+
url: event.detail.url,
97
112
});
98
113
updateWebsites();
99
114
}
···
105
120
type: MessageRequest.GET_PROJECT_SAVED_WEBSITES,
106
121
projectId: activeProject.id,
107
122
});
108
-
websites = possiblyDuplicatedWebsites.filter((value, index, self) =>
109
-
index === self.findIndex((t) => (
110
-
t.url === value.url
111
-
))
123
+
websites = possiblyDuplicatedWebsites.filter(
124
+
(value, index, self) =>
125
+
index === self.findIndex((t) => t.url === value.url),
112
126
);
113
127
}
114
128
···
116
130
const projects = await chrome.runtime.sendMessage({
117
131
type: MessageRequest.GET_ALL_PROJECTS,
118
132
});
119
-
const blob = new Blob([JSON.stringify(projects)], {type: 'application/json'});
133
+
const blob = new Blob([JSON.stringify(projects)], {
134
+
type: "application/json",
135
+
});
120
136
const url = URL.createObjectURL(blob);
121
-
const link = document.createElement('a');
137
+
const link = document.createElement("a");
122
138
link.href = url;
123
-
link.setAttribute('download', "rabbithole.json");
139
+
link.setAttribute("download", "rabbithole.json");
124
140
link.click();
125
141
}
126
142
127
143
function toggleTheme() {
128
144
isDark = !isDark;
145
+
document.body.classList.toggle("dark-mode", isDark);
129
146
}
147
+
130
148
function toggleOpened() {
131
149
opened = !opened;
132
150
}
···
139
157
width={{
140
158
sm: 300,
141
159
lg: 400,
142
-
base: 100
160
+
base: 100,
143
161
}}
144
162
height={"100%"}
145
163
override={{
146
164
borderRight: "1px solid rgb(233, 236, 239)",
147
-
overflowY: "scroll"
165
+
overflowY: "scroll",
148
166
}}
149
-
hidden={!opened}>
167
+
hidden={!opened}
168
+
>
150
169
<Sidebar
151
-
syncSuccess={syncSuccess}
152
-
projects={projects}
170
+
{syncSuccess}
171
+
{projects}
153
172
on:projectDelete={deleteActiveProject}
154
173
on:projectChange={updateActiveProject}
155
174
on:newProject={createNewProject}
156
175
on:newProjectSync={createNewProjectFromWindow}
157
176
on:projectSync={saveWindowToActiveProject}
158
-
on:exportRabbitholes={exportRabbitholes} />
177
+
on:exportRabbitholes={exportRabbitholes}
178
+
/>
159
179
</Navbar>
160
180
<Timeline
161
181
on:websiteDelete={deleteWebsite}
162
182
on:projectRename={renameActiveProject}
163
-
activeProject={activeProject}
164
-
websites={websites} />
183
+
on:toggleTheme={toggleTheme}
184
+
{activeProject}
185
+
{websites}
186
+
{isDark}
187
+
/>
165
188
</AppShell>
166
189
</SvelteUIProvider>
190
+
191
+
<style>
192
+
:global(body.dark-mode) {
193
+
background-color: #1a1a1a;
194
+
color: #ffffff;
195
+
}
196
+
197
+
:global(body.dark-mode .timeline) {
198
+
background-color: #1a1a1a;
199
+
color: #ffffff;
200
+
}
201
+
202
+
:global(body.dark-mode .mantine-AppShell-root) {
203
+
background-color: #1a1a1a;
204
+
}
205
+
206
+
:global(body.dark-mode .mantine-AppShell-main) {
207
+
background-color: #1a1a1a;
208
+
color: #ffffff;
209
+
}
210
+
211
+
:global(body.dark-mode .mantine-Card-root) {
212
+
background-color: #2c2c2c;
213
+
color: #ffffff;
214
+
}
215
+
216
+
:global(body.dark-mode .mantine-Text-root) {
217
+
color: #ffffff;
218
+
}
219
+
220
+
:global(body.dark-mode .mantine-TextInput-input) {
221
+
background-color: #2c2c2c;
222
+
color: #ffffff;
223
+
border-color: #444;
224
+
}
225
+
226
+
:global(body.dark-mode .mantine-Input-input) {
227
+
background-color: #2c2c2c;
228
+
color: #ffffff;
229
+
}
230
+
231
+
:global(body.dark-mode .mantine-Navbar-root) {
232
+
background-color: #2c2c2c;
233
+
border-right: 1px solid #444 !important;
234
+
}
235
+
236
+
:global(body.dark-mode .mantine-Button-root) {
237
+
background-color: #2c2c2c;
238
+
color: #ffffff;
239
+
}
240
+
241
+
:global(body.dark-mode .mantine-Divider-root) {
242
+
border-color: #444;
243
+
}
244
+
</style>
+16
-10
src/lib/SettingsButtons.svelte
+16
-10
src/lib/SettingsButtons.svelte
···
1
1
<script lang="ts">
2
2
import { onMount } from "svelte";
3
-
import { Button, Group, Tooltip } from '@svelteuidev/core';
3
+
import { Button, Group, Tooltip } from "@svelteuidev/core";
4
4
import Options from "./Options.svelte";
5
-
import { MessageRequest } from "../utils.ts"
5
+
import { MessageRequest } from "../utils.ts";
6
6
7
7
export let settings: Settings = {
8
8
show: false,
9
-
alignment: "right"
9
+
alignment: "right",
10
10
};
11
11
let isHovering = false;
12
12
···
35
35
}
36
36
37
37
onMount(async () => {
38
-
settings = await chrome.runtime.sendMessage({type: MessageRequest.GET_SETTINGS});
38
+
settings = await chrome.runtime.sendMessage({
39
+
type: MessageRequest.GET_SETTINGS,
40
+
});
39
41
});
40
42
</script>
41
43
42
44
<div>
43
45
<Group position="center" spacing="md">
44
-
<Button on:click={changeAlignment} id="move" variant='light' color='blue'>
46
+
<Button on:click={changeAlignment} id="move" variant="light" color="blue">
45
47
{#if settings.alignment === "right"}
46
48
Move button left
47
49
{:else}
···
50
52
</Button>
51
53
<Button
52
54
on:click={hideOverlay}
53
-
on:mouseenter={()=>{isHovering=true}}
54
-
on:mouseleave={()=>{isHovering=false}}
55
+
on:mouseenter={() => {
56
+
isHovering = true;
57
+
}}
58
+
on:mouseleave={() => {
59
+
isHovering = false;
60
+
}}
55
61
id="move"
56
-
variant='light'
57
-
color='blue'
58
-
>
62
+
variant="light"
63
+
color="blue"
64
+
>
59
65
{#if settings.show}
60
66
Hide
61
67
{:else}
+76
-56
src/lib/Sidebar.svelte
+76
-56
src/lib/Sidebar.svelte
···
1
1
<script>
2
2
import { onMount, createEventDispatcher } from "svelte";
3
-
import { Badge, Button, Card, Group, Image, Text, TextInput, Tooltip } from '@svelteuidev/core';
4
-
import SettingsButtons from "src/lib/SettingsButtons.svelte"
5
-
import UpdatingComponent from "src/lib/UpdatingComponent.svelte"
6
-
import { MessageRequest, NotificationDuration } from "../utils"
7
-
import ProjectSelector from "src/lib/ProjectSelector.svelte"
3
+
import {
4
+
Badge,
5
+
Button,
6
+
Card,
7
+
Group,
8
+
Image,
9
+
Text,
10
+
TextInput,
11
+
Tooltip,
12
+
} from "@svelteuidev/core";
13
+
import SettingsButtons from "src/lib/SettingsButtons.svelte";
14
+
import UpdatingComponent from "src/lib/UpdatingComponent.svelte";
15
+
import { MessageRequest, NotificationDuration } from "../utils";
16
+
import ProjectSelector from "src/lib/ProjectSelector.svelte";
8
17
9
18
// FIXME: why aren't types working here?
10
19
const dispatch = createEventDispatcher();
···
19
28
let isHoveringOverSync = false;
20
29
let isHoveringOverDelete = false;
21
30
const textStyleOverride = {
22
-
marginTop: "15px"
23
-
}
31
+
marginTop: "15px",
32
+
};
24
33
25
34
function validateProjectName() {
26
35
let valid = true;
···
28
37
createProjectFailMsg = "Project name required!";
29
38
valid = false;
30
39
}
31
-
if (projects.filter(p => p.name === newRabbitholeName).length > 0) {
40
+
if (projects.filter((p) => p.name === newRabbitholeName).length > 0) {
32
41
createProjectFailMsg = "Project name already used!";
33
42
valid = false;
34
43
}
35
44
if (!valid) {
36
45
createProjectFail = true;
37
-
setTimeout(() => { createProjectFail = false; }, NotificationDuration);
46
+
setTimeout(() => {
47
+
createProjectFail = false;
48
+
}, NotificationDuration);
38
49
return false;
39
50
}
40
51
return valid;
41
52
}
42
53
43
54
async function handleProjectChange(event) {
44
-
dispatch('projectChange', {
55
+
dispatch("projectChange", {
45
56
newProjectId: event.target.value,
46
57
});
47
58
}
48
59
49
60
async function createNewProject() {
50
-
if (validateProjectName()){
51
-
dispatch('newProject', {
52
-
newProjectName: newRabbitholeName
61
+
if (validateProjectName()) {
62
+
dispatch("newProject", {
63
+
newProjectName: newRabbitholeName,
53
64
});
54
65
}
55
66
}
56
67
57
68
async function saveAllTabsToNewProject() {
58
-
if (validateProjectName()){
59
-
dispatch('newProjectSync', {
60
-
newProjectName: newRabbitholeName
69
+
if (validateProjectName()) {
70
+
dispatch("newProjectSync", {
71
+
newProjectName: newRabbitholeName,
61
72
});
62
73
}
63
74
}
64
75
65
76
async function saveAllTabsToActiveProject() {
66
-
dispatch('projectSync');
77
+
dispatch("projectSync");
67
78
}
68
79
69
80
async function deleteProject() {
···
76
87
</script>
77
88
78
89
<div class="sidebar">
79
-
<Group direction="column" position="left" override={{
80
-
alignItems: "left"
81
-
}}>
82
-
<Text weight="bold" size="lg" override={textStyleOverride}>Overlay Settings</Text>
83
-
<SettingsButtons/>
84
-
<Text weight="bold" size="lg" override={textStyleOverride}>Change Project</Text>
85
-
<ProjectSelector
86
-
id="project-selector"
87
-
projects={projects}
88
-
handleProjectChange={handleProjectChange} />
89
-
<Tooltip {isHoveringOverSync} label="Save all tabs in window to current project">
90
-
<UpdatingComponent success={syncSuccess} successMsg="Window synced successfully!">
90
+
<Group
91
+
direction="column"
92
+
position="left"
93
+
override={{
94
+
alignItems: "left",
95
+
}}
96
+
>
97
+
<Text weight="bold" size="lg" override={textStyleOverride}
98
+
>Overlay Settings</Text
99
+
>
100
+
<SettingsButtons />
101
+
<Text weight="bold" size="lg" override={textStyleOverride}
102
+
>Change Project</Text
103
+
>
104
+
<ProjectSelector id="project-selector" {projects} {handleProjectChange} />
105
+
<Tooltip
106
+
{isHoveringOverSync}
107
+
label="Save all tabs in window to current project"
108
+
>
109
+
<UpdatingComponent
110
+
success={syncSuccess}
111
+
successMsg="Window synced successfully!"
112
+
>
91
113
<Button
92
114
on:click={saveAllTabsToActiveProject}
93
-
on:mouseenter={()=>{isHoveringOverSync=true}}
94
-
on:mouseleave={()=>{isHoveringOverSync=false}}
95
-
variant='light'
96
-
color='blue'
97
-
>
115
+
on:mouseenter={() => {
116
+
isHoveringOverSync = true;
117
+
}}
118
+
on:mouseleave={() => {
119
+
isHoveringOverSync = false;
120
+
}}
121
+
variant="light"
122
+
color="blue"
123
+
>
98
124
Sync window
99
125
</Button>
100
126
</UpdatingComponent>
···
102
128
<Tooltip {isHoveringOverDelete} label="This action is irreversible!">
103
129
<Button
104
130
on:click={deleteProject}
105
-
on:mouseenter={()=>{isHoveringOverDelete=true}}
106
-
on:mouseleave={()=>{isHoveringOverDelete=false}}
107
-
variant='filled'
108
-
color='red'
109
-
>
131
+
on:mouseenter={() => {
132
+
isHoveringOverDelete = true;
133
+
}}
134
+
on:mouseleave={() => {
135
+
isHoveringOverDelete = false;
136
+
}}
137
+
variant="filled"
138
+
color="red"
139
+
>
110
140
Delete Project
111
141
</Button>
112
142
</Tooltip>
113
-
<Text weight="bold" size="lg" override={textStyleOverride}>Create Project</Text>
143
+
<Text weight="bold" size="lg" override={textStyleOverride}
144
+
>Create Project</Text
145
+
>
114
146
<UpdatingComponent fail={createProjectFail} failMsg={createProjectFailMsg}>
115
147
<TextInput
116
148
placeholder="My new rabbithole"
117
149
bind:value={newRabbitholeName}
118
-
/>
150
+
/>
119
151
</UpdatingComponent>
120
-
<Button
121
-
on:click={createNewProject}
122
-
variant='light'
123
-
color='blue'
124
-
>
152
+
<Button on:click={createNewProject} variant="light" color="blue">
125
153
Create empty project
126
154
</Button>
127
-
<Button
128
-
on:click={saveAllTabsToNewProject}
129
-
variant='light'
130
-
color='blue'
131
-
>
155
+
<Button on:click={saveAllTabsToNewProject} variant="light" color="blue">
132
156
Create and save all tabs in window
133
157
</Button>
134
-
<Button
135
-
on:click={exportRabbitholes}
136
-
variant='light'
137
-
color='blue'
138
-
>
158
+
<Button on:click={exportRabbitholes} variant="light" color="blue">
139
159
Export rabbitholes
140
160
</Button>
141
161
</Group>
+87
-32
src/lib/Timeline.svelte
+87
-32
src/lib/Timeline.svelte
···
1
1
<script>
2
2
import Fuse from "fuse.js";
3
3
import { onMount, createEventDispatcher } from "svelte";
4
-
import { Badge, Button, Card, Group, Image, Input, Text, TextInput, Tooltip } from '@svelteuidev/core';
5
-
import { MessageRequest } from "../utils"
4
+
import {
5
+
Badge,
6
+
Button,
7
+
Card,
8
+
Group,
9
+
Image,
10
+
Input,
11
+
Text,
12
+
TextInput,
13
+
Tooltip,
14
+
} from "@svelteuidev/core";
15
+
import { MessageRequest } from "../utils";
6
16
import TimelineCard from "src/lib/TimelineCard.svelte";
7
-
import { Pencil1 } from 'radix-icons-svelte';
17
+
import TimelineSlider from "src/lib/TimelineSlider.svelte";
18
+
import { Pencil1, Moon, Sun } from "radix-icons-svelte";
8
19
9
20
// FIXME: why aren't types working here?
10
21
const dispatch = createEventDispatcher();
11
22
12
23
export let activeProject = {};
13
24
export let websites = [];
25
+
export let isDark = false;
14
26
15
27
let searchResults = [];
16
28
let nameClicked = false;
17
29
let isHovering = false;
18
30
let searchQuery = "";
31
+
let filteredWebsites = [];
32
+
let startDate = null;
33
+
let endDate = null;
19
34
35
+
function toggleDarkMode() {
36
+
dispatch("toggleTheme");
37
+
}
20
38
async function renameProject() {
21
39
if (activeProject.name === "") {
22
40
// TODO: error modal
23
41
return;
24
42
}
25
43
dispatch("projectRename", {
26
-
newActiveProjectName: activeProject.name
44
+
newActiveProjectName: activeProject.name,
27
45
});
28
46
}
29
47
···
38
56
39
57
async function deleteWebsite(event) {
40
58
dispatch("websiteDelete", {
41
-
url: event.detail.url
59
+
url: event.detail.url,
42
60
});
43
61
}
44
62
···
57
75
58
76
function applySearchQuery(node) {
59
77
const results = checkWebsiteForQuery(websites);
60
-
searchResults = results.map(res => res.item);
78
+
searchResults = results.map((res) => res.item);
61
79
}
80
+
81
+
function handleDateRangeChange(event) {
82
+
startDate = event.detail.startDate;
83
+
endDate = event.detail.endDate;
84
+
85
+
filteredWebsites = websites.filter((website) => {
86
+
const websiteDate = new Date(website.savedAt);
87
+
return websiteDate >= startDate && websiteDate <= endDate;
88
+
});
89
+
}
90
+
91
+
$: if (websites.length > 0 && filteredWebsites.length === 0) {
92
+
filteredWebsites = websites;
93
+
}
94
+
95
+
$: websitesToDisplay =
96
+
searchQuery.length < 3 ? filteredWebsites : searchResults;
62
97
</script>
63
98
64
99
<div class="timeline">
65
100
<Group position="center">
66
101
<div class="logo-container">
67
-
<img class="logo" alt="Rabbithole logo" src="../assets/icons/logo.png">
102
+
<img class="logo" alt="Rabbithole logo" src="../assets/icons/logo.png" />
68
103
</div>
69
104
<div class="input-div">
70
105
<Tooltip {isHovering} label="Click to rename project">
71
-
<Input id="project-name"
72
-
icon={Pencil1}
73
-
variant="unstyled"
74
-
size="lg"
75
-
on:click={() => { nameClicked = true; }}
76
-
on:mouseenter={()=>{isHovering=true}}
77
-
on:mouseleave={()=>{isHovering=false}}
78
-
bind:value={activeProject.name}
79
-
/>
106
+
<Input
107
+
id="project-name"
108
+
icon={Pencil1}
109
+
variant="unstyled"
110
+
size="lg"
111
+
on:click={() => {
112
+
nameClicked = true;
113
+
}}
114
+
on:mouseenter={() => {
115
+
isHovering = true;
116
+
}}
117
+
on:mouseleave={() => {
118
+
isHovering = false;
119
+
}}
120
+
bind:value={activeProject.name}
121
+
/>
80
122
</Tooltip>
81
123
</div>
82
124
{#if nameClicked}
83
-
<Button
84
-
on:click={renameProject}
85
-
variant='light'
86
-
color='blue'
87
-
>
125
+
<Button on:click={renameProject} variant="light" color="blue">
88
126
Rename
89
127
</Button>
90
128
{/if}
129
+
<Button on:click={toggleDarkMode} variant="light" color="gray">
130
+
{#if isDark}
131
+
<Sun size="16" />
132
+
{:else}
133
+
<Moon size="16" />
134
+
{/if}
135
+
</Button>
91
136
</Group>
137
+
138
+
<TimelineSlider
139
+
{websites}
140
+
{startDate}
141
+
{endDate}
142
+
on:dateRangeChange={handleDateRangeChange}
143
+
/>
144
+
92
145
<div class="feed">
93
146
<div class="search-bar">
94
147
<TextInput
95
148
placeholder="Search"
96
149
bind:value={searchQuery}
97
150
on:input={applySearchQuery}
98
-
/>
151
+
/>
99
152
</div>
100
153
<!-- Following deadgrep's convention of 3 char minimum -->
101
-
{#if searchQuery.length < 3}
102
-
{#each websites as site}
103
-
<TimelineCard website={site} on:websiteDelete={deleteWebsite} />
104
-
{/each}
105
-
{:else}
106
-
{#each searchResults as site}
107
-
<TimelineCard website={site} on:websiteDelete={deleteWebsite} />
108
-
{/each}
109
-
{/if}
154
+
{#each websitesToDisplay as site}
155
+
<TimelineCard website={site} on:websiteDelete={deleteWebsite} />
156
+
{/each}
110
157
</div>
111
158
</div>
112
159
···
115
162
width: 40vw;
116
163
margin: 0 auto;
117
164
margin-top: 50px;
165
+
transition:
166
+
background-color 0.3s ease,
167
+
color 0.3s ease;
168
+
}
169
+
170
+
.timeline.dark {
171
+
background-color: #1a1a1a;
172
+
color: #ffffff;
118
173
}
119
174
120
175
.input-div {
···
133
188
134
189
.search-bar {
135
190
width: inherit;
136
-
margin-x: 20px
191
+
margin-x: 20px;
137
192
}
138
193
</style>
+12
-5
src/lib/TimelineCard.svelte
+12
-5
src/lib/TimelineCard.svelte
···
1
1
<script>
2
2
import { createEventDispatcher } from "svelte";
3
3
import { Button, Card, Group, Image, Text } from "@svelteuidev/core";
4
-
import { Trash } from 'radix-icons-svelte';
4
+
import { Trash } from "radix-icons-svelte";
5
5
6
6
const dispatch = createEventDispatcher();
7
7
···
9
9
10
10
async function deleteWebsite() {
11
11
dispatch("websiteDelete", {
12
-
url: website.url
12
+
url: website.url,
13
13
});
14
14
}
15
15
</script>
···
32
32
</div>
33
33
34
34
<Group spacing="xs">
35
-
<Button variant="light" color="blue" href={website.url} target="_blank" override={{width: "85%"}}>
35
+
<Button
36
+
variant="light"
37
+
color="blue"
38
+
href={website.url}
39
+
target="_blank"
40
+
override={{ width: "85%" }}
41
+
>
36
42
Open
37
43
</Button>
38
44
<Button
39
45
on:click={deleteWebsite}
40
46
variant="light"
41
47
color="red"
42
-
override={{width: "10%"}}>
43
-
<Trash size="30"/>
48
+
override={{ width: "10%" }}
49
+
>
50
+
<Trash size="30" />
44
51
</Button>
45
52
</Group>
46
53
</Card>
+229
src/lib/TimelineSlider.svelte
+229
src/lib/TimelineSlider.svelte
···
1
+
<script>
2
+
import { createEventDispatcher, onMount } from "svelte";
3
+
import { scaleTime } from "d3-scale";
4
+
import { timeDay } from "d3-time";
5
+
6
+
const dispatch = createEventDispatcher();
7
+
8
+
export let websites = [];
9
+
export let startDate = null;
10
+
export let endDate = null;
11
+
12
+
let sliderContainer;
13
+
let isDragging = false;
14
+
let dragHandle = null;
15
+
16
+
$: if (websites.length > 0) {
17
+
initializeSlider();
18
+
}
19
+
20
+
function initializeSlider() {
21
+
if (websites.length === 0) return;
22
+
23
+
const dates = websites.map((w) => new Date(w.savedAt));
24
+
const minDate = new Date(Math.min(...dates));
25
+
const maxDate = new Date(Math.max(...dates));
26
+
27
+
if (!startDate) startDate = minDate;
28
+
if (!endDate) endDate = maxDate;
29
+
30
+
dispatch("dateRangeChange", { startDate, endDate });
31
+
}
32
+
33
+
function handleSliderChange(event) {
34
+
if (websites.length === 0) return;
35
+
36
+
const rect = sliderContainer.getBoundingClientRect();
37
+
const dates = websites.map((w) => new Date(w.savedAt));
38
+
const minDate = new Date(Math.min(...dates));
39
+
const maxDate = new Date(Math.max(...dates));
40
+
41
+
const scale = scaleTime().domain([minDate, maxDate]).range([0, rect.width]);
42
+
43
+
const x = event.clientX - rect.left;
44
+
const selectedDate = scale.invert(x);
45
+
46
+
if (event.target.classList.contains("start-handle")) {
47
+
startDate = selectedDate;
48
+
} else if (event.target.classList.contains("end-handle")) {
49
+
endDate = selectedDate;
50
+
}
51
+
52
+
dispatch("dateRangeChange", { startDate, endDate });
53
+
}
54
+
55
+
function getWebsitePositions() {
56
+
if (websites.length === 0 || !sliderContainer) return [];
57
+
58
+
const rect = sliderContainer.getBoundingClientRect();
59
+
const dates = websites.map((w) => new Date(w.savedAt));
60
+
const minDate = new Date(Math.min(...dates));
61
+
const maxDate = new Date(Math.max(...dates));
62
+
63
+
const scale = scaleTime().domain([minDate, maxDate]).range([0, rect.width]);
64
+
65
+
return websites.map((website) => ({
66
+
...website,
67
+
x: scale(new Date(website.savedAt)),
68
+
}));
69
+
}
70
+
71
+
function getHandlePositions() {
72
+
if (websites.length === 0 || !sliderContainer || !startDate || !endDate)
73
+
return { start: 0, end: 0 };
74
+
75
+
const rect = sliderContainer.getBoundingClientRect();
76
+
const dates = websites.map((w) => new Date(w.savedAt));
77
+
const minDate = new Date(Math.min(...dates));
78
+
const maxDate = new Date(Math.max(...dates));
79
+
80
+
const scale = scaleTime().domain([minDate, maxDate]).range([0, rect.width]);
81
+
82
+
return {
83
+
start: scale(startDate),
84
+
end: scale(endDate),
85
+
};
86
+
}
87
+
88
+
$: websitePositions = getWebsitePositions();
89
+
$: handlePositions = getHandlePositions();
90
+
</script>
91
+
92
+
<div class="timeline-container">
93
+
<div
94
+
class="timeline-slider"
95
+
bind:this={sliderContainer}
96
+
on:click={handleSliderChange}
97
+
>
98
+
<div class="slider-track"></div>
99
+
100
+
<!-- Website icons -->
101
+
{#each websitePositions as website}
102
+
<div
103
+
class="website-icon"
104
+
style="left: {website.x}px"
105
+
title="{website.name} - {new Date(
106
+
website.savedAt,
107
+
).toLocaleDateString()}"
108
+
>
109
+
{#if website.faviconUrl}
110
+
<img src={website.faviconUrl} alt={website.name} />
111
+
{:else}
112
+
<div class="default-icon">•</div>
113
+
{/if}
114
+
</div>
115
+
{/each}
116
+
117
+
<!-- Range selection -->
118
+
<div
119
+
class="range-selection"
120
+
style="left: {handlePositions.start}px; width: {handlePositions.end -
121
+
handlePositions.start}px"
122
+
></div>
123
+
124
+
<!-- Handles -->
125
+
<div
126
+
class="slider-handle start-handle"
127
+
style="left: {handlePositions.start}px"
128
+
on:click={handleSliderChange}
129
+
></div>
130
+
<div
131
+
class="slider-handle end-handle"
132
+
style="left: {handlePositions.end}px"
133
+
on:click={handleSliderChange}
134
+
></div>
135
+
</div>
136
+
137
+
<!-- Date labels -->
138
+
<div class="date-labels">
139
+
<span>{startDate ? startDate.toLocaleDateString() : ""}</span>
140
+
<span>{endDate ? endDate.toLocaleDateString() : ""}</span>
141
+
</div>
142
+
</div>
143
+
144
+
<style>
145
+
.timeline-container {
146
+
margin: 20px 0;
147
+
width: 100%;
148
+
}
149
+
150
+
.timeline-slider {
151
+
position: relative;
152
+
height: 60px;
153
+
width: 100%;
154
+
background: #f5f5f5;
155
+
border-radius: 8px;
156
+
cursor: pointer;
157
+
}
158
+
159
+
.slider-track {
160
+
position: absolute;
161
+
top: 50%;
162
+
left: 0;
163
+
right: 0;
164
+
height: 4px;
165
+
background: #ddd;
166
+
transform: translateY(-50%);
167
+
}
168
+
169
+
.website-icon {
170
+
position: absolute;
171
+
top: 50%;
172
+
transform: translate(-50%, -50%);
173
+
width: 16px;
174
+
height: 16px;
175
+
z-index: 2;
176
+
}
177
+
178
+
.website-icon img {
179
+
width: 100%;
180
+
height: 100%;
181
+
border-radius: 50%;
182
+
}
183
+
184
+
.default-icon {
185
+
width: 100%;
186
+
height: 100%;
187
+
background: #007bff;
188
+
border-radius: 50%;
189
+
display: flex;
190
+
align-items: center;
191
+
justify-content: center;
192
+
color: white;
193
+
font-size: 8px;
194
+
}
195
+
196
+
.range-selection {
197
+
position: absolute;
198
+
top: 50%;
199
+
height: 8px;
200
+
background: rgba(0, 123, 255, 0.3);
201
+
transform: translateY(-50%);
202
+
z-index: 1;
203
+
}
204
+
205
+
.slider-handle {
206
+
position: absolute;
207
+
top: 50%;
208
+
width: 20px;
209
+
height: 20px;
210
+
background: #007bff;
211
+
border: 2px solid white;
212
+
border-radius: 50%;
213
+
transform: translate(-50%, -50%);
214
+
cursor: grab;
215
+
z-index: 3;
216
+
}
217
+
218
+
.slider-handle:active {
219
+
cursor: grabbing;
220
+
}
221
+
222
+
.date-labels {
223
+
display: flex;
224
+
justify-content: space-between;
225
+
margin-top: 10px;
226
+
font-size: 12px;
227
+
color: #666;
228
+
}
229
+
</style>
+5
-5
src/lib/UpdatingComponent.svelte
+5
-5
src/lib/UpdatingComponent.svelte
···
1
1
<script>
2
-
import { Button, Group } from '@svelteuidev/core';
3
-
import { CheckCircled, CrossCircled } from 'radix-icons-svelte';
2
+
import { Button, Group } from "@svelteuidev/core";
3
+
import { CheckCircled, CrossCircled } from "radix-icons-svelte";
4
4
5
5
export let success, fail;
6
6
export let successMsg, failMsg;
···
12
12
</div>
13
13
{#if success}
14
14
<div class="child">
15
-
<span class="success"><CheckCircled size="20"/></span>
15
+
<span class="success"><CheckCircled size="20" /></span>
16
16
</div>
17
17
<div>
18
18
<span class="success">{successMsg}</span>
···
20
20
{/if}
21
21
{#if fail}
22
22
<div class="child">
23
-
<span class="fail"><CrossCircled size="20"/></span>
23
+
<span class="fail"><CrossCircled size="20" /></span>
24
24
</div>
25
25
<div>
26
-
<span class="fail">{ failMsg }</span>
26
+
<span class="fail">{failMsg}</span>
27
27
</div>
28
28
{/if}
29
29
</div>
+1
-1
src/newtab/index.ts
+1
-1
src/newtab/index.ts
+98
-79
src/storage/db.ts
+98
-79
src/storage/db.ts
···
1
-
import { v4 as uuid } from 'uuid';
1
+
import { v4 as uuid } from "uuid";
2
2
3
3
const version = 3;
4
4
···
14
14
faviconUrl: string;
15
15
openGraphImageUrl?: string;
16
16
description?: string;
17
-
};
17
+
}
18
18
19
19
export interface Project {
20
20
id: string; // key
···
42
42
private async getDb(): Promise<IDBDatabase> {
43
43
return new Promise((resolve, reject) => {
44
44
if (this.db !== null) {
45
-
resolve(this.db)
45
+
resolve(this.db);
46
46
return;
47
47
}
48
-
let request = this.factory.open('rabbithole', version);
48
+
let request = this.factory.open("rabbithole", version);
49
49
request.onsuccess = () => {
50
50
const db = request.result;
51
51
db.onerror = (event) => {
···
62
62
request.onerror = () => {
63
63
console.error("Please allow Rabbithole to use storage!");
64
64
reject(new Error("Insufficient permissions"));
65
-
}
65
+
};
66
66
});
67
67
}
68
68
···
70
70
static async init(factory: IDBFactory): Promise<void> {
71
71
await new Promise((resolve, reject) => {
72
72
if (factory === undefined) {
73
-
console.error("This browser doesn't support Rabbithole! You should uninstall it :(");
73
+
console.error(
74
+
"This browser doesn't support Rabbithole! You should uninstall it :(",
75
+
);
74
76
reject(new Error("indexedDB not supported"));
75
77
} else {
76
-
let request = factory.open('rabbithole', version);
78
+
let request = factory.open("rabbithole", version);
77
79
request.onerror = (e) => {
78
80
console.error(e);
79
81
console.error("Please allow Rabbithole to use storage!");
80
82
reject(new Error("Insufficient permissions"));
81
-
}
83
+
};
82
84
83
85
// This event is only implemented in recent browsers
84
86
request.onupgradeneeded = (event) => {
85
87
const db = event.target.result;
86
88
if (event.oldVersion < 1) {
87
-
const objectStore = db.createObjectStore("savedWebsites", { keyPath: "url" });
89
+
const objectStore = db.createObjectStore("savedWebsites", {
90
+
keyPath: "url",
91
+
});
88
92
objectStore.createIndex("name", "name", { unique: false });
89
93
objectStore.createIndex("url", "url", { unique: true });
90
-
objectStore.transaction.oncomplete = () => { };
94
+
objectStore.transaction.oncomplete = () => {};
91
95
}
92
96
if (event.oldVersion < 2) {
93
97
db.createObjectStore("user", { keyPath: "id" });
94
98
}
95
99
if (event.oldVersion < 3) {
96
-
const objectStore = db.createObjectStore("projects", { keyPath: "id" });
100
+
const objectStore = db.createObjectStore("projects", {
101
+
keyPath: "id",
102
+
});
97
103
objectStore.createIndex("name", "name", { unique: true });
98
104
}
99
105
···
111
117
id: uuid(),
112
118
settings: {
113
119
show: true,
114
-
alignment: "right"
120
+
alignment: "right",
115
121
},
116
122
currentProject: null,
117
123
};
118
-
const userRequest = db.transaction(["user"], "readwrite")
124
+
const userRequest = db
125
+
.transaction(["user"], "readwrite")
119
126
.objectStore("user")
120
127
.add(newUser);
121
128
userRequest.onsuccess = async () => {
122
129
await store.createNewActiveProject("Default project");
123
-
}
130
+
};
124
131
return;
125
132
}
126
133
127
-
if (!("currentProject" in user) || user.currentProject === null
128
-
|| user.currentProject === undefined) {
134
+
if (
135
+
!("currentProject" in user) ||
136
+
user.currentProject === null ||
137
+
user.currentProject === undefined
138
+
) {
129
139
await store.createNewActiveProject("Default project");
130
140
}
131
-
}
141
+
};
132
142
}
133
143
});
134
144
}
135
145
136
146
// also adds website to savedWebsites if it isn't there already
137
-
async saveWebsiteToProject(items: Website[]): Promise<Website | { alreadySaved: boolean }[]> {
147
+
async saveWebsiteToProject(
148
+
items: Website[],
149
+
): Promise<Website | { alreadySaved: boolean }[]> {
138
150
return new Promise(async (resolve, reject) => {
139
151
let db: IDBDatabase;
140
152
try {
141
153
db = await this.getDb();
142
154
} catch (err) {
143
-
reject(err)
155
+
reject(err);
144
156
}
145
157
146
158
// update website list of active project
···
154
166
}
155
167
}
156
168
157
-
const projectRequest = db.transaction(["projects"], "readwrite")
169
+
const projectRequest = db
170
+
.transaction(["projects"], "readwrite")
158
171
.objectStore("projects")
159
172
.put(currentProject);
160
173
161
174
projectRequest.onsuccess = (event) => {
162
175
const tx = db.transaction(["savedWebsites"], "readwrite");
163
-
items.forEach(item => tx
164
-
.objectStore("savedWebsites")
165
-
.add(item));
176
+
items.forEach((item) => tx.objectStore("savedWebsites").add(item));
166
177
167
178
tx.oncomplete = async () => {
168
179
console.log(`store item success`);
···
171
182
172
183
tx.onerror = (event) => {
173
184
// ignore error if website is stored already
174
-
if (!(event.target.error.message.indexOf("exists"))) {
185
+
if (!event.target.error.message.indexOf("exists")) {
175
186
reject(new Error(event.target.error));
176
187
}
177
188
};
178
189
179
190
console.log(`store item success`);
180
191
resolve(items);
181
-
}
192
+
};
182
193
183
194
projectRequest.onerror = (event) => {
184
195
// ignore error if website is stored already
185
-
if (!(event.target.error.message.indexOf("exists"))) {
196
+
if (!event.target.error.message.indexOf("exists")) {
186
197
console.log(`store item error`);
187
-
console.log(event.target)
198
+
console.log(event.target);
188
199
reject(new Error(event.target.error));
189
200
}
190
201
};
191
202
});
192
203
}
193
204
194
-
async deleteWebsiteFromProject(projectId: string, url: string): Promise<void> {
205
+
async deleteWebsiteFromProject(
206
+
projectId: string,
207
+
url: string,
208
+
): Promise<void> {
195
209
return new Promise(async (resolve, reject) => {
196
210
let db: IDBDatabase;
197
211
try {
198
212
db = await this.getDb();
199
213
} catch (err) {
200
-
reject(err)
214
+
reject(err);
201
215
}
202
216
203
217
// update website list of active project
···
207
221
return w !== url;
208
222
});
209
223
210
-
const projectRequest = db.transaction(["projects"], "readwrite")
224
+
const projectRequest = db
225
+
.transaction(["projects"], "readwrite")
211
226
.objectStore("projects")
212
227
.put(project);
213
228
214
229
projectRequest.onsuccess = (event) => {
215
230
console.log(`delete item success`);
216
231
resolve();
217
-
}
232
+
};
218
233
219
234
projectRequest.onerror = (event) => {
220
235
// ignore error if website is stored already
221
-
if (!(event.target.error.message.indexOf("exists"))) {
236
+
if (!event.target.error.message.indexOf("exists")) {
222
237
console.log(`store item error`);
223
-
console.log(event.target)
238
+
console.log(event.target);
224
239
reject(new Error(event.target.error));
225
240
}
226
241
};
···
233
248
try {
234
249
db = await this.getDb();
235
250
} catch (err) {
236
-
reject(err)
251
+
reject(err);
237
252
}
238
-
const request = db.transaction(["savedWebsites"])
253
+
const request = db
254
+
.transaction(["savedWebsites"])
239
255
.objectStore("savedWebsites")
240
256
.get(url);
241
257
···
257
273
try {
258
274
db = await this.getDb();
259
275
} catch (err) {
260
-
reject(err)
276
+
reject(err);
261
277
}
262
-
const request = db.transaction(["savedWebsites"])
278
+
const request = db
279
+
.transaction(["savedWebsites"])
263
280
.objectStore("savedWebsites")
264
281
.getAll();
265
282
···
281
298
try {
282
299
db = await this.getDb();
283
300
} catch (err) {
284
-
reject(err)
301
+
reject(err);
285
302
}
286
303
287
304
// update website list of active project
288
305
let project = await this.getProject(projectId);
289
306
project.name = newName;
290
307
291
-
const projectRequest = db.transaction(["projects"], "readwrite")
308
+
const projectRequest = db
309
+
.transaction(["projects"], "readwrite")
292
310
.objectStore("projects")
293
311
.put(project);
294
312
295
313
projectRequest.onsuccess = (event) => {
296
314
console.log(`rename project success`);
297
315
resolve(project);
298
-
}
316
+
};
299
317
300
318
projectRequest.onerror = (event) => {
301
319
console.log(`rename project error`);
302
-
console.log(event.target)
320
+
console.log(event.target);
303
321
reject(new Error(event.target.error));
304
322
};
305
323
});
···
312
330
try {
313
331
db = await this.getDb();
314
332
} catch (err) {
315
-
reject(err)
333
+
reject(err);
316
334
}
317
335
318
-
const projectRequest = db.transaction(["projects"], "readwrite")
336
+
const projectRequest = db
337
+
.transaction(["projects"], "readwrite")
319
338
.objectStore("projects")
320
339
.delete(projectId);
321
340
···
323
342
console.log(`delete project success`);
324
343
// replace active project
325
344
const projects = await this.getAllProjects();
326
-
await this.changeActiveProject(projects[0].id)
345
+
await this.changeActiveProject(projects[0].id);
327
346
resolve(projects[0]);
328
-
}
347
+
};
329
348
330
349
projectRequest.onerror = (event) => {
331
350
console.log(`rename project error`);
332
-
console.log(event.target)
351
+
console.log(event.target);
333
352
reject(new Error(event.target.error));
334
353
};
335
354
});
···
341
360
try {
342
361
db = await this.getDb();
343
362
} catch (err) {
344
-
reject(err)
363
+
reject(err);
345
364
}
346
365
347
366
let user = await this.getUser();
348
367
user.settings = settings;
349
368
350
-
const request = db.transaction(["user"], "readwrite")
369
+
const request = db
370
+
.transaction(["user"], "readwrite")
351
371
.objectStore("user")
352
372
.put(user);
353
373
···
369
389
try {
370
390
db = await this.getDb();
371
391
} catch (err) {
372
-
reject(err)
392
+
reject(err);
373
393
}
374
-
const request = db.transaction(["user"])
375
-
.objectStore("user")
376
-
.getAll();
394
+
const request = db.transaction(["user"]).objectStore("user").getAll();
377
395
378
396
request.onsuccess = (_) => {
379
397
const [user] = request.result;
···
394
412
try {
395
413
db = await this.getDb();
396
414
} catch (err) {
397
-
reject(err)
415
+
reject(err);
398
416
}
399
-
const request = db.transaction(["user"])
400
-
.objectStore("user")
401
-
.getAll();
417
+
const request = db.transaction(["user"]).objectStore("user").getAll();
402
418
403
419
request.onsuccess = (_) => {
404
420
const [user] = request.result;
···
419
435
try {
420
436
db = await this.getDb();
421
437
} catch (err) {
422
-
reject(err)
438
+
reject(err);
423
439
}
424
-
const userRequest = db.transaction(["user"])
425
-
.objectStore("user")
426
-
.getAll();
440
+
const userRequest = db.transaction(["user"]).objectStore("user").getAll();
427
441
428
442
userRequest.onsuccess = (_) => {
429
443
const [user] = userRequest.result;
430
-
const projectRequest = db.transaction(["projects"], "readwrite")
444
+
const projectRequest = db
445
+
.transaction(["projects"], "readwrite")
431
446
.objectStore("projects")
432
447
.get(user.currentProject);
433
448
···
435
450
const project = projectRequest.result;
436
451
console.log("getProject success");
437
452
resolve(project);
438
-
}
453
+
};
439
454
440
455
projectRequest.onerror = (event) => {
441
456
console.log(`getProject error: ${event.target}`);
442
457
reject(new Error("Failed to retrieve settings"));
443
-
}
458
+
};
444
459
};
445
460
446
461
userRequest.onerror = (event) => {
···
456
471
try {
457
472
db = await this.getDb();
458
473
} catch (err) {
459
-
reject(err)
474
+
reject(err);
460
475
}
461
-
const request = db.transaction(["projects"])
476
+
const request = db
477
+
.transaction(["projects"])
462
478
.objectStore("projects")
463
479
.getAll();
464
480
···
480
496
try {
481
497
db = await this.getDb();
482
498
} catch (err) {
483
-
reject(err)
499
+
reject(err);
484
500
}
485
-
const request = db.transaction(["projects"])
501
+
const request = db
502
+
.transaction(["projects"])
486
503
.objectStore("projects")
487
504
.get(projectId);
488
505
···
517
534
try {
518
535
db = await this.getDb();
519
536
} catch (err) {
520
-
reject(err)
537
+
reject(err);
521
538
}
522
-
const userRequest = db.transaction(["user"])
523
-
.objectStore("user")
524
-
.getAll();
539
+
const userRequest = db.transaction(["user"]).objectStore("user").getAll();
525
540
526
541
userRequest.onsuccess = async () => {
527
542
const [user] = userRequest.result;
528
543
user.currentProject = projectId;
529
544
530
-
const userPutRequest = db.transaction(["user"], "readwrite")
545
+
const userPutRequest = db
546
+
.transaction(["user"], "readwrite")
531
547
.objectStore("user")
532
548
.put(user);
533
549
534
550
userPutRequest.onsuccess = () => {
535
551
console.log("changeActiveProject success");
536
552
resolve();
537
-
}
553
+
};
538
554
539
555
userPutRequest.onerror = (event) => {
540
556
console.log(`changeActiveProject error: ${event.target}`);
541
557
reject(new Error("Failed to retrieve settings"));
542
-
}
558
+
};
543
559
};
544
560
545
561
userRequest.onerror = (event) => {
···
550
566
}
551
567
552
568
// create new project and set it as user's active project
553
-
async createNewActiveProject(projectName: string, savedWebsites?: string[])
554
-
: Promise<Project> {
569
+
async createNewActiveProject(
570
+
projectName: string,
571
+
savedWebsites?: string[],
572
+
): Promise<Project> {
555
573
return new Promise(async (resolve, reject) => {
556
574
const db = await this.getDb();
557
575
const user = await this.getUser();
···
564
582
// FIXME: when rabbithole is installed, the first time a session is saved
565
583
// the website list is duplicated, so dedup here for now
566
584
// Also see how else this can be repro'd
567
-
const projectReq = db.transaction(["projects"], "readwrite")
585
+
const projectReq = db
586
+
.transaction(["projects"], "readwrite")
568
587
.objectStore("projects")
569
588
.put(project);
570
589
···
578
597
});
579
598
580
599
resolve(project);
581
-
}
600
+
};
582
601
583
602
projectReq.onerror = (event) => {
584
603
console.log(`getAll error: ${event.target}`);
585
604
reject(new Error("Failed to retrieve items"));
586
-
}
605
+
};
587
606
});
588
607
}
589
608
}
+7
-4
src/utils.ts
+7
-4
src/utils.ts
···
24
24
export const NotificationDuration = 1500;
25
25
26
26
export async function getOrderedProjects(): Promise<Project[]> {
27
-
let projects = await chrome.runtime.sendMessage({ type: MessageRequest.GET_ALL_PROJECTS });
28
-
const activeProject = await chrome.runtime.sendMessage({ type: MessageRequest.GET_ACTIVE_PROJECT });
29
-
27
+
let projects = await chrome.runtime.sendMessage({
28
+
type: MessageRequest.GET_ALL_PROJECTS,
29
+
});
30
+
const activeProject = await chrome.runtime.sendMessage({
31
+
type: MessageRequest.GET_ACTIVE_PROJECT,
32
+
});
30
33
31
34
for (let i = 0; i < projects.length; i++) {
32
35
if (projects[i].name === activeProject.name) {
···
35
38
}
36
39
projects.sort((a, b) => a.name.localeCompare(b.name));
37
40
projects.unshift(activeProject);
38
-
return projects
41
+
return projects;
39
42
}