Track your online rabbitholes!

feat: prettier format, timeline slider, dark mode (v ugly commit)

gov 8dced638 76ccee98

+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
··· 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
··· 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
··· 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 + });
-1
src/content/styles.css
··· 1 1 body { 2 - 3 2 }
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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 - import Rabbithole from "src/lib/Rabbithole.svelte" 1 + import Rabbithole from "src/lib/Rabbithole.svelte"; 2 2 3 3 // global styles 4 4 import "./styles.css";
-1
src/newtab/styles.css
··· 1 1 body { 2 - 3 2 } 4 3 5 4 #project-name {
+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
··· 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 }