Mercurial > addons > firefox-addons > set-aside
comparison background.js @ 1:b0827360b8e4
Store favicons and thumbnails in local database
author | Guido Berhoerster <guido+set-aside@berhoerster.name> |
---|---|
date | Mon, 19 Nov 2018 17:26:17 +0100 |
parents | d13d59494613 |
children | 9d699dc7823d |
comparison
equal
deleted
inserted
replaced
0:d13d59494613 | 1:b0827360b8e4 |
---|---|
56 releaseType, | 56 releaseType, |
57 releaseNumber, | 57 releaseNumber, |
58 }; | 58 }; |
59 } | 59 } |
60 | 60 |
61 class ObjectStoreDB { | |
62 constructor(dbName = 'defaultDatabase', objectStoreName = 'objectStore') { | |
63 this.dbName = dbName; | |
64 this.objectStoreName = objectStoreName; | |
65 this._db = undefined; | |
66 this.openingDB = new Promise((resolve, reject) => { | |
67 let request = indexedDB.open(this.dbName); | |
68 request.addEventListener('error', ev => { | |
69 reject(request.error); | |
70 }); | |
71 request.addEventListener('success', ev => { | |
72 resolve(request.result); | |
73 }); | |
74 request.addEventListener('upgradeneeded', ev => { | |
75 request.result.createObjectStore(this.objectStoreName); | |
76 }); | |
77 }); | |
78 } | |
79 | |
80 async _execTransaction(method, ...methodArguments) { | |
81 if (typeof this._db === 'undefined') { | |
82 this._db = await this.openingDB; | |
83 } | |
84 return new Promise((resolve, reject) => { | |
85 let transaction = this._db.transaction(this.objectStoreName, | |
86 method.startsWith('get') ? 'readonly' : 'readwrite'); | |
87 let objectStore = transaction.objectStore(this.objectStoreName); | |
88 let request = objectStore[method](...methodArguments); | |
89 transaction.addEventListener('complete', ev => | |
90 method.startsWith('get') ? | |
91 resolve(request.result) : | |
92 resolve()); | |
93 transaction.addEventListener('abort', ev => | |
94 reject(transaction.error)); | |
95 transaction.addEventListener('error', ev => | |
96 reject(transaction.error)); | |
97 }); | |
98 } | |
99 | |
100 async get(key) { | |
101 return key === null || typeof key === 'undefined' ? | |
102 this._execTransaction('getAll') : | |
103 this._execTransaction('get', key); | |
104 } | |
105 | |
106 async keys() { | |
107 return this._execTransaction('getAllKeys'); | |
108 } | |
109 | |
110 async set(key, value) { | |
111 return this._execTransaction('put', value, key) | |
112 } | |
113 | |
114 async delete(key) { | |
115 return this._execTransaction('delete', key) | |
116 } | |
117 | |
118 async clear(key) { | |
119 return this._execTransaction('clear') | |
120 } | |
121 } | |
122 | |
61 class Tab { | 123 class Tab { |
62 static deserialize(object) { | 124 static deserialize(object) { |
63 return new Tab(object); | 125 return new Tab(object); |
64 } | 126 } |
65 | 127 |
66 constructor({url, title, uuid = generateUuidV4String(), favIconUrl = null, | 128 constructor({url, title, uuid = generateUuidV4String(), favIcon = null, |
67 thumbnailUrl = null}) { | 129 thumbnail = null}) { |
68 this.uuid = uuid; | 130 this.uuid = uuid; |
69 this.url = url; | 131 this.url = url; |
70 this.title = title; | 132 this.title = title; |
71 this.favIconUrl = favIconUrl; | 133 this.favIcon = favIcon; |
72 this.thumbnailUrl = thumbnailUrl; | 134 this.thumbnail = thumbnail; |
73 } | 135 } |
74 | 136 |
75 serialize() { | 137 serialize() { |
76 return Object.assign({}, this); | 138 return Object.assign({}, this, {favIcon: null, thumbnail: null}); |
77 } | 139 } |
78 } | 140 } |
79 | 141 |
80 class TabCollection { | 142 class TabCollection { |
81 static deserialize(object) { | 143 static deserialize(object) { |
108 } | 170 } |
109 | 171 |
110 class TabCollectionsStorageProxy { | 172 class TabCollectionsStorageProxy { |
111 constructor() { | 173 constructor() { |
112 this.tabCollections = new Map(); | 174 this.tabCollections = new Map(); |
175 this.objectStoreDB = new ObjectStoreDB('tabCollections'); | |
113 this.ports = new Set(); | 176 this.ports = new Set(); |
114 this.browserVersion = undefined; | 177 this.browserVersion = undefined; |
115 this.messageQueue = []; | 178 this.messageQueue = []; |
116 this.isInitialized = false; | 179 this.isInitialized = false; |
117 | 180 |
131 console.log('tab collections from storage'); | 194 console.log('tab collections from storage'); |
132 console.table(this.tabCollections); | 195 console.table(this.tabCollections); |
133 console.groupEnd(); | 196 console.groupEnd(); |
134 browser.storage.onChanged.addListener(this.onStorageChanged.bind(this)); | 197 browser.storage.onChanged.addListener(this.onStorageChanged.bind(this)); |
135 | 198 |
199 // get favicon and thumbnail data from local database | |
200 let updatingTabData = []; | |
201 for (let tabCollectionUuid of this.tabCollections.keys()) { | |
202 updatingTabData.push(this.updateTabData(tabCollectionUuid)); | |
203 } | |
204 await Promise.all(updatingTabData); | |
205 | |
206 // remove stale data from local database | |
207 for (let tabCollectionUuid of await this.objectStoreDB.keys()) { | |
208 if (!this.tabCollections.has(tabCollectionUuid)) { | |
209 console.log('removing data for stale tab collection', | |
210 tabCollectionUuid); | |
211 this.objectStoreDB.delete(tabCollectionUuid); | |
212 } | |
213 } | |
214 | |
136 this.isInitialized = true; | 215 this.isInitialized = true; |
137 | 216 |
138 while (this.messageQueue.length > 0) { | 217 while (this.messageQueue.length > 0) { |
139 let [message, port] = this.messageQueue.pop(); | 218 let [message, port] = this.messageQueue.pop(); |
140 if (this.ports.has(port)) { | 219 if (this.ports.has(port)) { |
143 } | 222 } |
144 } | 223 } |
145 | 224 |
146 async createTabThumbnail(tabId) { | 225 async createTabThumbnail(tabId) { |
147 let captureUrl = await browser.tabs.captureTab(tabId); | 226 let captureUrl = await browser.tabs.captureTab(tabId); |
148 let thumbnailUrl = await new Promise((resolve, reject) => { | 227 let thumbnailBlob = await new Promise((resolve, reject) => { |
149 let image = new Image(); | 228 let image = new Image(); |
150 image.addEventListener('load', ev => { | 229 image.addEventListener('load', ev => { |
151 let canvas = document.createElement('canvas'); | 230 let canvas = document.createElement('canvas'); |
152 canvas.width = THUMBNAIL_WIDTH; | 231 canvas.width = THUMBNAIL_WIDTH; |
153 canvas.height = THUMBNAIL_HEIGHT; | 232 canvas.height = THUMBNAIL_HEIGHT; |
157 let ctx = canvas.getContext('2d'); | 236 let ctx = canvas.getContext('2d'); |
158 ctx.fillStyle = '#fff'; | 237 ctx.fillStyle = '#fff'; |
159 ctx.fillRect(0, 0, canvas.width, canvas.height); | 238 ctx.fillRect(0, 0, canvas.width, canvas.height); |
160 ctx.drawImage(image, 0, 0, dWidth, dHeight); | 239 ctx.drawImage(image, 0, 0, dWidth, dHeight); |
161 | 240 |
162 resolve(canvas.toDataURL('image/jpeg', 0.75)); | 241 canvas.toBlob(resolve, 'image/jpeg', 0.75); |
163 }); | 242 }); |
164 image.addEventListener('error', e => { | 243 image.addEventListener('error', e => { |
165 reject(e); | 244 reject(e); |
166 }); | 245 }); |
167 image.src = captureUrl; | 246 image.src = captureUrl; |
168 }); | 247 }); |
169 return thumbnailUrl; | 248 return thumbnailBlob; |
170 } | 249 } |
171 | 250 |
172 async createTabCollection(windowId) { | 251 async createTabCollection(windowId) { |
173 let browserTabs = await browser.tabs.query({ | 252 let browserTabs = await browser.tabs.query({ |
174 windowId, | 253 windowId, |
186 SUPPORTED_PROTOCOLS.includes(new URL(browserTab.url).protocol)); | 265 SUPPORTED_PROTOCOLS.includes(new URL(browserTab.url).protocol)); |
187 if (browserTabs.length === 0) { | 266 if (browserTabs.length === 0) { |
188 return; | 267 return; |
189 } | 268 } |
190 | 269 |
191 let tabs = browserTabs.map(browserTab => { | 270 let tabs = await Promise.all(browserTabs.map(async browserTab => { |
271 // convert favicon data URI to blob | |
272 let favIcon = null; | |
273 if (!browserTab.discarded) { | |
274 try { | |
275 let response = await fetch(browserTab.favIconUrl); | |
276 favIcon = await response.blob(); | |
277 } catch (e) { | |
278 if (!(e instanceof AbortError)) { | |
279 throw e; | |
280 } | |
281 } | |
282 } | |
283 | |
192 let tab = new Tab({ | 284 let tab = new Tab({ |
193 url: browserTab.url, | 285 url: browserTab.url, |
194 title: browserTab.title, | 286 title: browserTab.title, |
195 favIconUrl: browserTab.favIconUrl | 287 favIcon |
196 }); | 288 }); |
197 return [tab.uuid, tab]; | 289 return [tab.uuid, tab]; |
198 }); | 290 })); |
199 | 291 |
200 // create empty tab which becomes the new active tab | 292 // create empty tab which becomes the new active tab |
201 await browser.tabs.create({active: true}); | 293 await browser.tabs.create({active: true}); |
202 | 294 |
203 // capture tabs, return null for discarded tabs since they can only be | 295 // capture tabs, return null for discarded tabs since they can only be |
205 // interaction, and thus might hang the capture process indefinetly | 297 // interaction, and thus might hang the capture process indefinetly |
206 let thumbnails = await Promise.all(browserTabs.map(browserTab => | 298 let thumbnails = await Promise.all(browserTabs.map(browserTab => |
207 !browserTab.discarded ? | 299 !browserTab.discarded ? |
208 this.createTabThumbnail(browserTab.id) : null)); | 300 this.createTabThumbnail(browserTab.id) : null)); |
209 for (let [, tab] of tabs) { | 301 for (let [, tab] of tabs) { |
210 tab.thumbnailUrl = thumbnails.shift(); | 302 tab.thumbnail = thumbnails.shift(); |
211 } | 303 } |
212 | 304 |
213 let tabCollection = new TabCollection({tabs}); | 305 let tabCollection = new TabCollection({tabs}); |
214 console.log('created tab collection:', tabCollection); | 306 console.log('created tab collection:', tabCollection); |
307 | |
308 // store tab favicons and thumbnails | |
309 let tabCollectionData = { | |
310 uuid: tabCollection.uuid, | |
311 tabs: new Map() | |
312 }; | |
313 for (let [uuid, tab] of tabs) { | |
314 tabCollectionData.tabs.set(uuid, { | |
315 favIcon: tab.favIcon, | |
316 thumbnail: tab.thumbnail | |
317 }); | |
318 } | |
319 await this.objectStoreDB.set(tabCollectionData.uuid, tabCollectionData); | |
215 | 320 |
216 // store tab collection | 321 // store tab collection |
217 console.log('storing tab collection:', tabCollection); | 322 console.log('storing tab collection:', tabCollection); |
218 await browser.storage.sync.set({ | 323 await browser.storage.sync.set({ |
219 [`collection:${tabCollection.uuid}`]: tabCollection.serialize() | 324 [`collection:${tabCollection.uuid}`]: tabCollection.serialize() |
256 } | 361 } |
257 | 362 |
258 async removeTabCollection(tabCollectionUuid) { | 363 async removeTabCollection(tabCollectionUuid) { |
259 console.log('removing tab collection %s', tabCollectionUuid); | 364 console.log('removing tab collection %s', tabCollectionUuid); |
260 await browser.storage.sync.remove(`collection:${tabCollectionUuid}`); | 365 await browser.storage.sync.remove(`collection:${tabCollectionUuid}`); |
366 this.objectStoreDB.delete(tabCollectionUuid); | |
261 } | 367 } |
262 | 368 |
263 async restoreTabCollection(tabCollectionUuid, windowId) { | 369 async restoreTabCollection(tabCollectionUuid, windowId) { |
264 console.log('restoring tab collection %s in window %s', | 370 console.log('restoring tab collection %s in window %s', |
265 tabCollectionUuid, windowId); | 371 tabCollectionUuid, windowId); |
277 }, tabProperties))); | 383 }, tabProperties))); |
278 await Promise.all(creatingTabs); | 384 await Promise.all(creatingTabs); |
279 await this.removeTabCollection(tabCollectionUuid); | 385 await this.removeTabCollection(tabCollectionUuid); |
280 } | 386 } |
281 | 387 |
282 onStorageChanged(changes, areaName) { | 388 async updateTabData(tabCollectionUuid) { |
389 let tabCollectionDataObject; | |
390 try { | |
391 tabCollectionDataObject = | |
392 await this.objectStoreDB.get(tabCollectionUuid); | |
393 } catch (e) { | |
394 console.error(`Failed to get data from database: e.message`); | |
395 return; | |
396 } | |
397 if (typeof tabCollectionDataObject === 'undefined') { | |
398 // does not exist in database | |
399 console.log('no data stored for tab collection', tabCollectionUuid); | |
400 return; | |
401 } | |
402 | |
403 console.log(`updating tab collection ${tabCollectionUuid} with data`, | |
404 tabCollectionDataObject); | |
405 let tabCollection = this.tabCollections.get(tabCollectionUuid); | |
406 for (let [tabUuid, tab] of tabCollection.tabs) { | |
407 let tabDataObject = tabCollectionDataObject.tabs.get(tabUuid); | |
408 if (typeof tabDataObject === 'undefined') { | |
409 continue; | |
410 } | |
411 tab.favIcon = tabDataObject.favIcon; | |
412 tab.thumbnail = tabDataObject.thumbnail; | |
413 } | |
414 } | |
415 | |
416 async onStorageChanged(changes, areaName) { | |
283 if (areaName !== 'sync') { | 417 if (areaName !== 'sync') { |
284 return; | 418 return; |
285 } | 419 } |
286 | 420 |
287 console.group('sync storage area changed:', changes); | 421 console.group('sync storage area changed:', changes); |
296 let tabCollectionUuid = key.replace('collection:', ''); | 430 let tabCollectionUuid = key.replace('collection:', ''); |
297 if (typeof oldValue === 'undefined') { | 431 if (typeof oldValue === 'undefined') { |
298 // a new collection was created | 432 // a new collection was created |
299 let newTabCollection = TabCollection.deserialize(newValue); | 433 let newTabCollection = TabCollection.deserialize(newValue); |
300 this.tabCollections.set(tabCollectionUuid, newTabCollection); | 434 this.tabCollections.set(tabCollectionUuid, newTabCollection); |
435 // try to get tab favicons and thumbnails | |
436 await this.updateTabData(tabCollectionUuid); | |
301 | 437 |
302 this.broadcastMessage({ | 438 this.broadcastMessage({ |
303 type: 'tabCollectionCreated', | 439 type: 'tabCollectionCreated', |
304 tabCollection: newTabCollection | 440 tabCollection: newTabCollection |
305 }); | 441 }); |
306 } else if (typeof newValue === 'undefined') { | 442 } else if (typeof newValue === 'undefined') { |
307 // a collection has been removed | 443 // a collection has been removed |
308 this.tabCollections.delete(tabCollectionUuid); | 444 this.tabCollections.delete(tabCollectionUuid); |
445 this.objectStoreDB.delete(tabCollectionUuid); | |
309 | 446 |
310 this.broadcastMessage({ | 447 this.broadcastMessage({ |
311 type: 'tabCollectionRemoved', | 448 type: 'tabCollectionRemoved', |
312 tabCollectionUuid | 449 tabCollectionUuid |
313 }); | 450 }); |
314 } else { | 451 } else { |
315 // a collection has changed | 452 // a collection has changed |
316 let newTabCollection = TabCollection.deserialize(newValue); | 453 let newTabCollection = TabCollection.deserialize(newValue); |
317 this.tabCollections.set(tabCollectionUuid, newTabCollection); | 454 this.tabCollections.set(tabCollectionUuid, newTabCollection); |
455 // try to get tab favicons and thumbnails | |
456 await this.updateTabData(tabCollectionUuid); | |
318 | 457 |
319 this.broadcastMessage({ | 458 this.broadcastMessage({ |
320 type: 'tabCollectionChanged', | 459 type: 'tabCollectionChanged', |
321 tabCollection: newTabCollection | 460 tabCollection: newTabCollection |
322 }); | 461 }); |