# HG changeset patch # User Guido Berhoerster # Date 1542644777 -3600 # Node ID b0827360b8e43655b336b3bb020999f610e52e57 # Parent d13d594946139c8fd955f8cab5cc3713ce7048ba Store favicons and thumbnails in local database diff -r d13d59494613 -r b0827360b8e4 background.js --- a/background.js Sat Nov 17 10:44:16 2018 +0100 +++ b/background.js Mon Nov 19 17:26:17 2018 +0100 @@ -58,22 +58,84 @@ }; } +class ObjectStoreDB { + constructor(dbName = 'defaultDatabase', objectStoreName = 'objectStore') { + this.dbName = dbName; + this.objectStoreName = objectStoreName; + this._db = undefined; + this.openingDB = new Promise((resolve, reject) => { + let request = indexedDB.open(this.dbName); + request.addEventListener('error', ev => { + reject(request.error); + }); + request.addEventListener('success', ev => { + resolve(request.result); + }); + request.addEventListener('upgradeneeded', ev => { + request.result.createObjectStore(this.objectStoreName); + }); + }); + } + + async _execTransaction(method, ...methodArguments) { + if (typeof this._db === 'undefined') { + this._db = await this.openingDB; + } + return new Promise((resolve, reject) => { + let transaction = this._db.transaction(this.objectStoreName, + method.startsWith('get') ? 'readonly' : 'readwrite'); + let objectStore = transaction.objectStore(this.objectStoreName); + let request = objectStore[method](...methodArguments); + transaction.addEventListener('complete', ev => + method.startsWith('get') ? + resolve(request.result) : + resolve()); + transaction.addEventListener('abort', ev => + reject(transaction.error)); + transaction.addEventListener('error', ev => + reject(transaction.error)); + }); + } + + async get(key) { + return key === null || typeof key === 'undefined' ? + this._execTransaction('getAll') : + this._execTransaction('get', key); + } + + async keys() { + return this._execTransaction('getAllKeys'); + } + + async set(key, value) { + return this._execTransaction('put', value, key) + } + + async delete(key) { + return this._execTransaction('delete', key) + } + + async clear(key) { + return this._execTransaction('clear') + } +} + class Tab { static deserialize(object) { return new Tab(object); } - constructor({url, title, uuid = generateUuidV4String(), favIconUrl = null, - thumbnailUrl = null}) { + constructor({url, title, uuid = generateUuidV4String(), favIcon = null, + thumbnail = null}) { this.uuid = uuid; this.url = url; this.title = title; - this.favIconUrl = favIconUrl; - this.thumbnailUrl = thumbnailUrl; + this.favIcon = favIcon; + this.thumbnail = thumbnail; } serialize() { - return Object.assign({}, this); + return Object.assign({}, this, {favIcon: null, thumbnail: null}); } } @@ -110,6 +172,7 @@ class TabCollectionsStorageProxy { constructor() { this.tabCollections = new Map(); + this.objectStoreDB = new ObjectStoreDB('tabCollections'); this.ports = new Set(); this.browserVersion = undefined; this.messageQueue = []; @@ -133,6 +196,22 @@ console.groupEnd(); browser.storage.onChanged.addListener(this.onStorageChanged.bind(this)); + // get favicon and thumbnail data from local database + let updatingTabData = []; + for (let tabCollectionUuid of this.tabCollections.keys()) { + updatingTabData.push(this.updateTabData(tabCollectionUuid)); + } + await Promise.all(updatingTabData); + + // remove stale data from local database + for (let tabCollectionUuid of await this.objectStoreDB.keys()) { + if (!this.tabCollections.has(tabCollectionUuid)) { + console.log('removing data for stale tab collection', + tabCollectionUuid); + this.objectStoreDB.delete(tabCollectionUuid); + } + } + this.isInitialized = true; while (this.messageQueue.length > 0) { @@ -145,7 +224,7 @@ async createTabThumbnail(tabId) { let captureUrl = await browser.tabs.captureTab(tabId); - let thumbnailUrl = await new Promise((resolve, reject) => { + let thumbnailBlob = await new Promise((resolve, reject) => { let image = new Image(); image.addEventListener('load', ev => { let canvas = document.createElement('canvas'); @@ -159,14 +238,14 @@ ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.drawImage(image, 0, 0, dWidth, dHeight); - resolve(canvas.toDataURL('image/jpeg', 0.75)); + canvas.toBlob(resolve, 'image/jpeg', 0.75); }); image.addEventListener('error', e => { reject(e); }); image.src = captureUrl; }); - return thumbnailUrl; + return thumbnailBlob; } async createTabCollection(windowId) { @@ -188,14 +267,27 @@ return; } - let tabs = browserTabs.map(browserTab => { + let tabs = await Promise.all(browserTabs.map(async browserTab => { + // convert favicon data URI to blob + let favIcon = null; + if (!browserTab.discarded) { + try { + let response = await fetch(browserTab.favIconUrl); + favIcon = await response.blob(); + } catch (e) { + if (!(e instanceof AbortError)) { + throw e; + } + } + } + let tab = new Tab({ url: browserTab.url, title: browserTab.title, - favIconUrl: browserTab.favIconUrl + favIcon }); return [tab.uuid, tab]; - }); + })); // create empty tab which becomes the new active tab await browser.tabs.create({active: true}); @@ -207,12 +299,25 @@ !browserTab.discarded ? this.createTabThumbnail(browserTab.id) : null)); for (let [, tab] of tabs) { - tab.thumbnailUrl = thumbnails.shift(); + tab.thumbnail = thumbnails.shift(); } let tabCollection = new TabCollection({tabs}); console.log('created tab collection:', tabCollection); + // store tab favicons and thumbnails + let tabCollectionData = { + uuid: tabCollection.uuid, + tabs: new Map() + }; + for (let [uuid, tab] of tabs) { + tabCollectionData.tabs.set(uuid, { + favIcon: tab.favIcon, + thumbnail: tab.thumbnail + }); + } + await this.objectStoreDB.set(tabCollectionData.uuid, tabCollectionData); + // store tab collection console.log('storing tab collection:', tabCollection); await browser.storage.sync.set({ @@ -258,6 +363,7 @@ async removeTabCollection(tabCollectionUuid) { console.log('removing tab collection %s', tabCollectionUuid); await browser.storage.sync.remove(`collection:${tabCollectionUuid}`); + this.objectStoreDB.delete(tabCollectionUuid); } async restoreTabCollection(tabCollectionUuid, windowId) { @@ -279,7 +385,35 @@ await this.removeTabCollection(tabCollectionUuid); } - onStorageChanged(changes, areaName) { + async updateTabData(tabCollectionUuid) { + let tabCollectionDataObject; + try { + tabCollectionDataObject = + await this.objectStoreDB.get(tabCollectionUuid); + } catch (e) { + console.error(`Failed to get data from database: e.message`); + return; + } + if (typeof tabCollectionDataObject === 'undefined') { + // does not exist in database + console.log('no data stored for tab collection', tabCollectionUuid); + return; + } + + console.log(`updating tab collection ${tabCollectionUuid} with data`, + tabCollectionDataObject); + let tabCollection = this.tabCollections.get(tabCollectionUuid); + for (let [tabUuid, tab] of tabCollection.tabs) { + let tabDataObject = tabCollectionDataObject.tabs.get(tabUuid); + if (typeof tabDataObject === 'undefined') { + continue; + } + tab.favIcon = tabDataObject.favIcon; + tab.thumbnail = tabDataObject.thumbnail; + } + } + + async onStorageChanged(changes, areaName) { if (areaName !== 'sync') { return; } @@ -298,6 +432,8 @@ // a new collection was created let newTabCollection = TabCollection.deserialize(newValue); this.tabCollections.set(tabCollectionUuid, newTabCollection); + // try to get tab favicons and thumbnails + await this.updateTabData(tabCollectionUuid); this.broadcastMessage({ type: 'tabCollectionCreated', @@ -306,6 +442,7 @@ } else if (typeof newValue === 'undefined') { // a collection has been removed this.tabCollections.delete(tabCollectionUuid); + this.objectStoreDB.delete(tabCollectionUuid); this.broadcastMessage({ type: 'tabCollectionRemoved', @@ -315,6 +452,8 @@ // a collection has changed let newTabCollection = TabCollection.deserialize(newValue); this.tabCollections.set(tabCollectionUuid, newTabCollection); + // try to get tab favicons and thumbnails + await this.updateTabData(tabCollectionUuid); this.broadcastMessage({ type: 'tabCollectionChanged', diff -r d13d59494613 -r b0827360b8e4 sidebar/js/tab-collection-manager.js --- a/sidebar/js/tab-collection-manager.js Sat Nov 17 10:44:16 2018 +0100 +++ b/sidebar/js/tab-collection-manager.js Mon Nov 19 17:26:17 2018 +0100 @@ -78,13 +78,14 @@ tabLinkElement.href = tab.url; tabLinkElement.title = tab.title; - if (tab.thumbnailUrl !== null) { + if (tab.thumbnail !== null) { tabItemNode.querySelector('.tab-thumbnail').src = - tab.thumbnailUrl; + URL.createObjectURL(tab.thumbnail); } - if (tab.favIconUrl !== null) { - tabItemNode.querySelector('.tab-favicon').src = tab.favIconUrl; + if (tab.favIcon !== null) { + tabItemNode.querySelector('.tab-favicon').src = + URL.createObjectURL(tab.favIcon); } tabItemNode.querySelector('.tab-title').textContent = tab.title;