VulpesOS/gaia/apps/verticalhome/js/stores/item.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

543 lines
15 KiB
JavaScript
Executable file

'use strict';
/* global ApplicationSource */
/* global BookmarkSource */
/* global configurator */
/* global dispatchEvent */
/* global GaiaGrid */
(function(exports) {
const DB_VERSION = 1;
const DB_NAME = 'verticalhome';
const DB_ITEM_STORE = 'items';
const DB_SV_APP_STORE_NAME = 'svAppsInstalled';
const DATASTORE_NAME = 'verticalhome_items_migration';
const PREFS_STORE = 'vertical_preferences_store';
var db;
var datastore;
function sort(entries, order) {
if (!order || !order.length) {
return entries;
}
var newEntries = [];
function isEqual(lookFor, compareWith) {
if (!compareWith.detail || !lookFor) {
return false;
}
if (compareWith instanceof GaiaGrid.Mozapp) {
if (!lookFor.manifestURL || !compareWith.detail.manifestURL) {
return false;
}
if (compareWith.detail.entryPoint) {
return lookFor.manifestURL === compareWith.detail.manifestURL &&
lookFor.entry_point === compareWith.detail.entryPoint;
} else {
return lookFor.manifestURL === compareWith.detail.manifestURL;
}
} else if (compareWith instanceof GaiaGrid.Bookmark) {
if (!lookFor.id || !compareWith.detail.id) {
return false;
}
return lookFor.id === compareWith.detail.id;
}
}
for (var i = 0, iLen = order.length; i < iLen; i++) {
// Add all entries of current section
for (var j = 0, jLen = order[i].length; j < jLen; j++) {
var ind = entries.findIndex(
isEqual.bind(null, order[i][j]));
if (ind >= 0) {
newEntries.push(entries.splice(ind,1)[0]);
}
}
// If we have more sections add a divider
if (i < iLen - 1) {
newEntries.push(new GaiaGrid.Divider());
}
}
// If entries is not empty yet, they could be unordered apps (added at end)
// or SingleVariant apps, installed before vertical started, which we need
// insert it in their correct position (ever if there was a first run with
// SIM).
if (entries.length > 0) {
// We only reorder sv apps if SIM was present on first boot!
if (configurator.isSimPresentOnFirstBoot) {
sortUnclassifiedApps(newEntries, entries);
} else {
newEntries.push(new GaiaGrid.Divider());
newEntries = newEntries.concat(entries);
}
}
for (i = 0, iLen = newEntries.length; i < iLen; i++) {
if (newEntries[i].detail) {
newEntries[i].detail.index = i;
}
}
return newEntries;
}
function sortUnclassifiedApps(dstEntries, orgEntries) {
function compareApps(appA, appB) {
var svAppA = null;
var svAppB = null;
if (appA) {
svAppA = configurator.getSingleVariantApp(appA.detail.manifestURL);
}
if (appB) {
svAppB = configurator.getSingleVariantApp(appB.detail.manifestURL);
}
var locA = svAppA && svAppA.location !== undefined && svAppA.location ||
Number.MAX_VALUE;
var locB = svAppB && svAppB.location !== undefined && svAppB.location ||
Number.MAX_VALUE;
var relPos = locA - locB;
return relPos ? (relPos > 0 ? 1 : -1) : 0;
}
// To simplify the process, we order the apps first (SV apps in relative
// order and at the beginning of the array)
orgEntries.sort(compareApps);
// After the sorting process, if the first element is not a SV app then
// we only need to add them at the end of dstEntries
if (!configurator.getSingleVariantApp(orgEntries[0].detail.manifestURL)) {
dstEntries.push(new GaiaGrid.Divider());
dstEntries = dstEntries.concat(orgEntries);
} else {
var sepAdded = false;
for (var i = 0, iLen = orgEntries.length; i < iLen; i++) {
var app = orgEntries[i];
var svApp = configurator.getSingleVariantApp(app.detail.manifestURL);
if (!svApp) {
if (!sepAdded) {
dstEntries.push(new GaiaGrid.Divider());
sepAdded = true;
}
dstEntries.push(app);
} else {
if (svApp.location !== undefined) {
dstEntries.splice(svApp.location, 0, app);
} else {
dstEntries.push(app);
}
}
}
}
}
function loadTable(table, indexName, iterator, aNext) {
newTxn(table, 'readonly', function(txn, store) {
var index = store.index(indexName);
index.openCursor().onsuccess = function onsuccess(event) {
var cursor = event.target.result;
if (!cursor) {
return;
}
iterator(cursor.value);
cursor.continue();
};
}, aNext);
}
function newTxn(storeName, txnType, withTxnAndStore, successCb) {
var txn = db.transaction([storeName], txnType);
var store = txn.objectStore(storeName);
txn.oncomplete = function(event) {
if (successCb) {
successCb(event);
}
};
txn.onerror = function(event) {
console.warn('Error during transaction.');
};
withTxnAndStore(txn, store);
}
function ItemStore(onsuccess) {
var self = this;
this.applicationSource = new ApplicationSource(this);
this.bookmarkSource = new BookmarkSource(this);
this.sources = [this.applicationSource, this.bookmarkSource];
this.ready = false;
var isEmpty = false;
self.gridOrder = null;
var request = window.indexedDB.open(DB_NAME, DB_VERSION);
request.onupgradeneeded = function _onupgradeneeded(event) {
var db = event.target.result;
var oldVersion = event.oldVersion || 0;
switch (oldVersion) {
case 0:
// Create the item store
var objectStore = db.createObjectStore(DB_ITEM_STORE,
{ keyPath: 'index'});
objectStore.createIndex('index', 'index', { unique: true });
isEmpty = true;
var objectSV = db.createObjectStore(DB_SV_APP_STORE_NAME,
{ keyPath: 'manifestURL' });
objectSV.createIndex('indexSV', 'indexSV', { unique: true });
}
};
request.onsuccess = function _onsuccess() {
onsuccess && onsuccess(isEmpty);
db = request.result;
var cb = self.fetch.bind(self, self.synchronize.bind(self));
if (isEmpty) {
window.addEventListener('configuration-ready', function onReady() {
window.removeEventListener('configuration-ready', onReady);
self.gridOrder = configurator.getGrid();
self.populate(cb);
});
} else {
self.initSources(cb);
}
// Handle migration
navigator.mozSetMessageHandler('connection', self.handleIAC.bind(self));
};
window.addEventListener('gaiagrid-cached-icons-rendered', this);
}
ItemStore.prototype = {
/**
* Whether or not the grid has been rendered. We use this as an
* indicator as to whether or not we should save items to indexedDB.
* If we get items to save before this is true, we wait until the
* cached-icons-rendered event to save.
*/
_renderedOnce: false,
/**
* The saved arguments for the last deferred save call.
*/
_deferredSaveArgs: [],
/**
* A list of all items. These are item objects (App, Bookmark, Divider)
*/
_allItems: [],
/**
* Maintains the current index of the last grid item.
*/
nextPosition: 0,
/**
* Fetches a list of all items in the store.
*/
all: function(success) {
if (!this.ready) {
window.addEventListener('databaseready', this.all.bind(this, success));
return;
}
success(this._allItems);
},
saveTable: function(table, objArr, column, checkPersist, aNext) {
newTxn(table, 'readwrite', function(txn, store) {
store.clear();
for (var i = 0, iLen = objArr.length; i < iLen; i++) {
if (!checkPersist || (checkPersist && objArr[i].persistToDB)) {
store.put(column?objArr[i][column]:objArr[i]);
}
}
if (typeof aNext === 'function') {
aNext();
}
});
},
saveMigrationData: function(objArr, column, checkPersist) {
return datastore.clear().then(() => {
var chain = [];
objArr.forEach((object) => {
if (!checkPersist || object.persistToDB) {
var data = column ? object[column] : object;
chain.push(datastore.put(data, data.index + 1));
}
});
return Promise.all(chain);
});
},
/**
* @param {Object} object a single object to update.
* @param {Function} callback fires when transaction finishes.
*/
saveItem: function(object, callback) {
// intentional use of == meaning null or undefined.
if (object.index == null) {
console.error('Attempting to save object without `index`');
return;
}
newTxn(
DB_ITEM_STORE,
'readwrite',
function(txn, store) {
store.put(object);
},
callback
);
},
/**
* Defers the save of items until after the grid is ready.
*/
deferredSave: function() {
var args = Array.slice(arguments);
if (this._renderedOnce) {
return this.save.apply(this, args);
}
this._deferredSaveArgs = args;
},
/**
* Saves all icons to the database.
*/
save: function(entries, aNext) {
entries = sort(entries, this.gridOrder);
this.gridOrder = null;
// The initial config is simply the list of apps
this.saveTable(DB_ITEM_STORE, entries, 'detail', true, aNext);
},
/**
* Save reference to SingleVariant app previously installed
*/
savePrevInstalledSvApp: function(svApps, aNext) {
this.saveTable(DB_SV_APP_STORE_NAME, svApps, null, false, aNext);
},
/**
* Fetches items from the database.
* @param {Function} callback A function to call after fetching all items.
*/
fetch: function(callback) {
var cached = {};
var collected = [];
function iterator(value) {
collected.push(value);
}
function iteratorSV(value) {
/* jshint validthis: true */
this.applicationSource.addPreviouslyInstalledSvApp(value.manifestURL);
}
loadTable(DB_SV_APP_STORE_NAME, 'indexSV', iteratorSV.bind(this));
loadTable(DB_ITEM_STORE, 'index', iterator, finish.bind(this));
/**
* Add to _allItems if the record is unique by comparing the identifier.
*/
function addIfUnique(item) {
/* jshint validthis: true */
if (!cached[item.identifier]) {
cached[item.identifier] = true;
this._allItems.push(item);
}
}
function finish() {
/* jshint validthis: true */
// Transforms DB results into item classes
for (var i = 0, iLen = collected.length; i < iLen; i++) {
var thisItem = collected[i];
if (thisItem.type === 'app') {
var itemObj = this.applicationSource.mapToApp(thisItem);
addIfUnique.call(this, itemObj);
} else if (thisItem.type === 'divider') {
var divider = new GaiaGrid.Divider(thisItem);
this._allItems.push(divider);
} else if (thisItem.type === 'bookmark') {
var bookmark = new GaiaGrid.Bookmark(thisItem);
addIfUnique.call(this, bookmark);
}
}
this.notifyReady();
if (callback && typeof callback === 'function') {
callback();
}
}
},
/**
* We have fetched data from our local database and displayed it,
* but data inside of our application or bookmark store may be outdated.
* We need to synchronize each source and delete/add records.
*/
synchronize: function() {
this.sources.forEach(function eachSource(source) {
source.synchronize();
});
},
/**
* Initializes all sources.
* @param {Function} callback The callback to fire after all sources init.
*/
initSources: function(callback) {
var pending = this.sources.length;
var allEntries = [];
var self = this;
var current = 0;
function handleSource() {
var source = self.sources[current];
current++;
source.populate(next);
}
handleSource();
function next(entries) {
allEntries = allEntries.concat(entries);
if (!(--pending)) {
callback(allEntries);
} else {
handleSource();
}
}
},
/**
* Populates the database with the initial data.
* @param {Function} callback Callback after database is populated.
*/
populate: function(callback) {
this.initSources(function(entries) {
this.deferredSave(entries, callback);
}.bind(this));
},
/**
* Notifies consumers that the database is ready for queries to be makde.
*/
notifyReady: function() {
this.ready = true;
dispatchEvent(new CustomEvent('databaseready'));
},
/**
* Gets the next available position in the grid
*/
getNextPosition: function() {
var nextPosition = this.nextPosition;
this.nextPosition++;
return nextPosition;
},
handleIAC: function(connectionRequest) {
if (connectionRequest.keyword !== 'verticalhome-migrate') {
return;
}
var port = connectionRequest.port;
port.onmessage = (event) => {
if (event.data.action !== 'migrate') {
console.error('Unhandled migration action requested:',
event.data.action);
return;
}
this.migrate(event.data.manifestURL);
};
port.start();
},
migrate: function(manifestURL) {
this.migratingTo = manifestURL;
console.info('verticalhome migrating to ' + this.migratingTo);
if (this.migrating) {
return;
}
this.migrating = true;
(datastore ? Promise.resolve() :
navigator.getDataStores(DATASTORE_NAME).
then(stores => {
datastore = stores[0];
return Promise.resolve();
},
(e) => {
return Promise.reject(e);
})).
then(() => {
this.all(entries => {
this.saveMigrationData(entries, 'detail', true).then(() => {
this.migrating = false;
window.navigator.mozSettings.createLock().set({
'homescreen.manifestURL': this.migratingTo
});
navigator.getDataStores(PREFS_STORE).then(
stores => {
if (stores.length < 1) {
console.error('Error opening verticalhome prefs datastore');
return;
}
stores[0].put('migrated', true).then(() => {
window.close();
}, e => {
console.error('Error storing migration pref', e);
window.close();
});
});
});
});
},
e => {
console.error('Error retrieving datastore', e);
});
},
handleEvent: function(e) {
switch(e.type) {
case 'gaiagrid-cached-icons-rendered':
this._renderedOnce = true;
window.removeEventListener('gaiagrid-cached-icons-rendered', this);
if (this._deferredSaveArgs.length) {
this.save.apply(this, this._deferredSaveArgs);
}
break;
}
}
};
exports.ItemStore = ItemStore;
}(window));