addons/firefox-addons/set-aside

changeset 1:b0827360b8e4

Store favicons and thumbnails in local database
author Guido Berhoerster <guido+set-aside@berhoerster.name>
date Mon Nov 19 17:26:17 2018 +0100 (18 months ago)
parents d13d59494613
children 49ec0da1e698
files background.js sidebar/js/tab-collection-manager.js
line diff
     1.1 --- a/background.js	Sat Nov 17 10:44:16 2018 +0100
     1.2 +++ b/background.js	Mon Nov 19 17:26:17 2018 +0100
     1.3 @@ -58,22 +58,84 @@
     1.4      };
     1.5  }
     1.6  
     1.7 +class ObjectStoreDB {
     1.8 +    constructor(dbName = 'defaultDatabase', objectStoreName = 'objectStore') {
     1.9 +        this.dbName = dbName;
    1.10 +        this.objectStoreName = objectStoreName;
    1.11 +        this._db = undefined;
    1.12 +        this.openingDB = new Promise((resolve, reject) => {
    1.13 +            let request = indexedDB.open(this.dbName);
    1.14 +            request.addEventListener('error', ev => {
    1.15 +                reject(request.error);
    1.16 +            });
    1.17 +            request.addEventListener('success', ev => {
    1.18 +                resolve(request.result);
    1.19 +            });
    1.20 +            request.addEventListener('upgradeneeded', ev => {
    1.21 +                request.result.createObjectStore(this.objectStoreName);
    1.22 +            });
    1.23 +        });
    1.24 +    }
    1.25 +
    1.26 +    async _execTransaction(method, ...methodArguments) {
    1.27 +        if (typeof this._db === 'undefined') {
    1.28 +            this._db = await this.openingDB;
    1.29 +        }
    1.30 +        return new Promise((resolve, reject) => {
    1.31 +            let transaction = this._db.transaction(this.objectStoreName,
    1.32 +                    method.startsWith('get') ? 'readonly' : 'readwrite');
    1.33 +            let objectStore = transaction.objectStore(this.objectStoreName);
    1.34 +            let request = objectStore[method](...methodArguments);
    1.35 +            transaction.addEventListener('complete', ev =>
    1.36 +                    method.startsWith('get') ?
    1.37 +                    resolve(request.result) :
    1.38 +                    resolve());
    1.39 +            transaction.addEventListener('abort', ev =>
    1.40 +                    reject(transaction.error));
    1.41 +            transaction.addEventListener('error', ev =>
    1.42 +                    reject(transaction.error));
    1.43 +        });
    1.44 +    }
    1.45 +
    1.46 +    async get(key) {
    1.47 +        return key === null || typeof key === 'undefined' ?
    1.48 +                this._execTransaction('getAll') :
    1.49 +                this._execTransaction('get', key);
    1.50 +    }
    1.51 +
    1.52 +    async keys() {
    1.53 +        return this._execTransaction('getAllKeys');
    1.54 +    }
    1.55 +
    1.56 +    async set(key, value) {
    1.57 +        return this._execTransaction('put', value, key)
    1.58 +    }
    1.59 +
    1.60 +    async delete(key) {
    1.61 +        return this._execTransaction('delete', key)
    1.62 +    }
    1.63 +
    1.64 +    async clear(key) {
    1.65 +        return this._execTransaction('clear')
    1.66 +    }
    1.67 +}
    1.68 +
    1.69  class Tab {
    1.70      static deserialize(object) {
    1.71          return new Tab(object);
    1.72      }
    1.73  
    1.74 -    constructor({url, title, uuid = generateUuidV4String(), favIconUrl = null,
    1.75 -            thumbnailUrl = null}) {
    1.76 +    constructor({url, title, uuid = generateUuidV4String(), favIcon = null,
    1.77 +            thumbnail = null}) {
    1.78          this.uuid = uuid;
    1.79          this.url = url;
    1.80          this.title = title;
    1.81 -        this.favIconUrl = favIconUrl;
    1.82 -        this.thumbnailUrl = thumbnailUrl;
    1.83 +        this.favIcon = favIcon;
    1.84 +        this.thumbnail = thumbnail;
    1.85      }
    1.86  
    1.87      serialize() {
    1.88 -        return Object.assign({}, this);
    1.89 +        return Object.assign({}, this, {favIcon: null, thumbnail: null});
    1.90      }
    1.91  }
    1.92  
    1.93 @@ -110,6 +172,7 @@
    1.94  class TabCollectionsStorageProxy {
    1.95      constructor() {
    1.96          this.tabCollections = new Map();
    1.97 +        this.objectStoreDB = new ObjectStoreDB('tabCollections');
    1.98          this.ports = new Set();
    1.99          this.browserVersion = undefined;
   1.100          this.messageQueue = [];
   1.101 @@ -133,6 +196,22 @@
   1.102          console.groupEnd();
   1.103          browser.storage.onChanged.addListener(this.onStorageChanged.bind(this));
   1.104  
   1.105 +        // get favicon and thumbnail data from local database
   1.106 +        let updatingTabData = [];
   1.107 +        for (let tabCollectionUuid of this.tabCollections.keys()) {
   1.108 +            updatingTabData.push(this.updateTabData(tabCollectionUuid));
   1.109 +        }
   1.110 +        await Promise.all(updatingTabData);
   1.111 +
   1.112 +        // remove stale data from local database
   1.113 +        for (let tabCollectionUuid of await this.objectStoreDB.keys()) {
   1.114 +            if (!this.tabCollections.has(tabCollectionUuid)) {
   1.115 +                console.log('removing data for stale tab collection',
   1.116 +                    tabCollectionUuid);
   1.117 +                this.objectStoreDB.delete(tabCollectionUuid);
   1.118 +            }
   1.119 +        }
   1.120 +
   1.121          this.isInitialized = true;
   1.122  
   1.123          while (this.messageQueue.length > 0) {
   1.124 @@ -145,7 +224,7 @@
   1.125  
   1.126      async createTabThumbnail(tabId) {
   1.127          let captureUrl = await browser.tabs.captureTab(tabId);
   1.128 -        let thumbnailUrl = await new Promise((resolve, reject) => {
   1.129 +        let thumbnailBlob = await new Promise((resolve, reject) => {
   1.130              let image = new Image();
   1.131              image.addEventListener('load', ev => {
   1.132                  let canvas = document.createElement('canvas');
   1.133 @@ -159,14 +238,14 @@
   1.134                  ctx.fillRect(0, 0, canvas.width, canvas.height);
   1.135                  ctx.drawImage(image, 0, 0, dWidth, dHeight);
   1.136  
   1.137 -                resolve(canvas.toDataURL('image/jpeg', 0.75));
   1.138 +                canvas.toBlob(resolve, 'image/jpeg', 0.75);
   1.139              });
   1.140              image.addEventListener('error', e => {
   1.141                  reject(e);
   1.142              });
   1.143              image.src = captureUrl;
   1.144          });
   1.145 -        return thumbnailUrl;
   1.146 +        return thumbnailBlob;
   1.147      }
   1.148  
   1.149      async createTabCollection(windowId) {
   1.150 @@ -188,14 +267,27 @@
   1.151              return;
   1.152          }
   1.153  
   1.154 -        let tabs = browserTabs.map(browserTab => {
   1.155 +        let tabs = await Promise.all(browserTabs.map(async browserTab => {
   1.156 +            // convert favicon data URI to blob
   1.157 +            let favIcon = null;
   1.158 +            if (!browserTab.discarded) {
   1.159 +                try {
   1.160 +                    let response = await fetch(browserTab.favIconUrl);
   1.161 +                    favIcon = await response.blob();
   1.162 +                } catch (e) {
   1.163 +                    if (!(e instanceof AbortError)) {
   1.164 +                        throw e;
   1.165 +                    }
   1.166 +                }
   1.167 +            }
   1.168 +
   1.169              let tab = new Tab({
   1.170                  url: browserTab.url,
   1.171                  title: browserTab.title,
   1.172 -                favIconUrl: browserTab.favIconUrl
   1.173 +                favIcon
   1.174              });
   1.175              return [tab.uuid, tab];
   1.176 -        });
   1.177 +        }));
   1.178  
   1.179          // create empty tab which becomes the new active tab
   1.180          await browser.tabs.create({active: true});
   1.181 @@ -207,12 +299,25 @@
   1.182                  !browserTab.discarded ?
   1.183                  this.createTabThumbnail(browserTab.id) : null));
   1.184          for (let [, tab] of tabs) {
   1.185 -            tab.thumbnailUrl = thumbnails.shift();
   1.186 +            tab.thumbnail = thumbnails.shift();
   1.187          }
   1.188  
   1.189          let tabCollection = new TabCollection({tabs});
   1.190          console.log('created tab collection:', tabCollection);
   1.191  
   1.192 +        // store tab favicons and thumbnails
   1.193 +        let tabCollectionData = {
   1.194 +            uuid: tabCollection.uuid,
   1.195 +            tabs: new Map()
   1.196 +        };
   1.197 +        for (let [uuid, tab] of tabs) {
   1.198 +            tabCollectionData.tabs.set(uuid, {
   1.199 +                favIcon: tab.favIcon,
   1.200 +                thumbnail: tab.thumbnail
   1.201 +            });
   1.202 +        }
   1.203 +        await this.objectStoreDB.set(tabCollectionData.uuid, tabCollectionData);
   1.204 +
   1.205          // store tab collection
   1.206          console.log('storing tab collection:', tabCollection);
   1.207          await browser.storage.sync.set({
   1.208 @@ -258,6 +363,7 @@
   1.209      async removeTabCollection(tabCollectionUuid) {
   1.210          console.log('removing tab collection %s', tabCollectionUuid);
   1.211          await browser.storage.sync.remove(`collection:${tabCollectionUuid}`);
   1.212 +        this.objectStoreDB.delete(tabCollectionUuid);
   1.213      }
   1.214  
   1.215      async restoreTabCollection(tabCollectionUuid, windowId) {
   1.216 @@ -279,7 +385,35 @@
   1.217          await this.removeTabCollection(tabCollectionUuid);
   1.218      }
   1.219  
   1.220 -    onStorageChanged(changes, areaName) {
   1.221 +    async updateTabData(tabCollectionUuid) {
   1.222 +        let tabCollectionDataObject;
   1.223 +        try {
   1.224 +            tabCollectionDataObject =
   1.225 +                    await this.objectStoreDB.get(tabCollectionUuid);
   1.226 +        } catch (e) {
   1.227 +            console.error(`Failed to get data from database: e.message`);
   1.228 +            return;
   1.229 +        }
   1.230 +        if (typeof tabCollectionDataObject === 'undefined') {
   1.231 +            // does not exist in database
   1.232 +            console.log('no data stored for tab collection', tabCollectionUuid);
   1.233 +            return;
   1.234 +        }
   1.235 +
   1.236 +        console.log(`updating tab collection ${tabCollectionUuid} with data`,
   1.237 +                tabCollectionDataObject);
   1.238 +        let tabCollection = this.tabCollections.get(tabCollectionUuid);
   1.239 +        for (let [tabUuid, tab] of tabCollection.tabs) {
   1.240 +            let tabDataObject = tabCollectionDataObject.tabs.get(tabUuid);
   1.241 +            if (typeof tabDataObject === 'undefined') {
   1.242 +                continue;
   1.243 +            }
   1.244 +            tab.favIcon = tabDataObject.favIcon;
   1.245 +            tab.thumbnail = tabDataObject.thumbnail;
   1.246 +        }
   1.247 +    }
   1.248 +
   1.249 +    async onStorageChanged(changes, areaName) {
   1.250          if (areaName !== 'sync') {
   1.251              return;
   1.252          }
   1.253 @@ -298,6 +432,8 @@
   1.254              // a new collection was created
   1.255              let newTabCollection = TabCollection.deserialize(newValue);
   1.256              this.tabCollections.set(tabCollectionUuid, newTabCollection);
   1.257 +            // try to get tab favicons and thumbnails
   1.258 +            await this.updateTabData(tabCollectionUuid);
   1.259  
   1.260              this.broadcastMessage({
   1.261                  type: 'tabCollectionCreated',
   1.262 @@ -306,6 +442,7 @@
   1.263          } else if (typeof newValue === 'undefined') {
   1.264              // a collection has been removed
   1.265              this.tabCollections.delete(tabCollectionUuid);
   1.266 +            this.objectStoreDB.delete(tabCollectionUuid);
   1.267  
   1.268              this.broadcastMessage({
   1.269                  type: 'tabCollectionRemoved',
   1.270 @@ -315,6 +452,8 @@
   1.271              // a collection has changed
   1.272              let newTabCollection = TabCollection.deserialize(newValue);
   1.273              this.tabCollections.set(tabCollectionUuid, newTabCollection);
   1.274 +            // try to get tab favicons and thumbnails
   1.275 +            await this.updateTabData(tabCollectionUuid);
   1.276  
   1.277              this.broadcastMessage({
   1.278                  type: 'tabCollectionChanged',
     2.1 --- a/sidebar/js/tab-collection-manager.js	Sat Nov 17 10:44:16 2018 +0100
     2.2 +++ b/sidebar/js/tab-collection-manager.js	Mon Nov 19 17:26:17 2018 +0100
     2.3 @@ -78,13 +78,14 @@
     2.4              tabLinkElement.href = tab.url;
     2.5              tabLinkElement.title = tab.title;
     2.6  
     2.7 -            if (tab.thumbnailUrl !== null) {
     2.8 +            if (tab.thumbnail !== null) {
     2.9                  tabItemNode.querySelector('.tab-thumbnail').src =
    2.10 -                        tab.thumbnailUrl;
    2.11 +                        URL.createObjectURL(tab.thumbnail);
    2.12              }
    2.13  
    2.14 -            if (tab.favIconUrl !== null) {
    2.15 -                tabItemNode.querySelector('.tab-favicon').src = tab.favIconUrl;
    2.16 +            if (tab.favIcon !== null) {
    2.17 +                tabItemNode.querySelector('.tab-favicon').src =
    2.18 +                        URL.createObjectURL(tab.favIcon);
    2.19              }
    2.20  
    2.21              tabItemNode.querySelector('.tab-title').textContent = tab.title;