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.
This commit is contained in:
parent
93e3a48004
commit
b38dea0597
25901 changed files with 9982925 additions and 369 deletions
1128
gaia/apps/homescreen/js/apps.js
Executable file
1128
gaia/apps/homescreen/js/apps.js
Executable file
File diff suppressed because it is too large
Load diff
110
gaia/apps/homescreen/js/appsmetadata.js
Executable file
110
gaia/apps/homescreen/js/appsmetadata.js
Executable file
|
|
@ -0,0 +1,110 @@
|
|||
'use strict';
|
||||
|
||||
(function(exports) {
|
||||
const DB_NAME = 'home-metadata';
|
||||
const DB_ORDER_STORE = 'order';
|
||||
const DB_ICON_STORE = 'icon';
|
||||
const DB_VERSION = 1;
|
||||
|
||||
function AppsMetadata() {}
|
||||
|
||||
AppsMetadata.prototype = {
|
||||
db: null,
|
||||
|
||||
init: function() {
|
||||
return new Promise((resolve, reject) => {
|
||||
var req = window.indexedDB.open(DB_NAME, DB_VERSION);
|
||||
req.onupgradeneeded = this.upgradeSchema;
|
||||
req.onsuccess = (e) => {
|
||||
this.db = e.target.result;
|
||||
resolve();
|
||||
};
|
||||
req.onerror = (e) => {
|
||||
console.error('Error opening homescreen metadata db', e);
|
||||
reject(e);
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
upgradeSchema: function(e) {
|
||||
var db = e.target.result;
|
||||
var fromVersion = e.oldVersion;
|
||||
if (fromVersion < 1) {
|
||||
var store = db.createObjectStore(DB_ORDER_STORE, { keyPath: 'id' });
|
||||
store.createIndex('order', 'order', { unique: false });
|
||||
store = db.createObjectStore(DB_ICON_STORE, { keyPath: 'id' });
|
||||
store.createIndex('icon', 'icon', { unique: false });
|
||||
}
|
||||
},
|
||||
|
||||
set: function(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var txn = this.db.transaction([DB_ORDER_STORE, DB_ICON_STORE],
|
||||
'readwrite');
|
||||
for (var entry of data) {
|
||||
if (!entry.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof entry.order !== 'undefined') {
|
||||
txn.objectStore(DB_ORDER_STORE).
|
||||
put({ id: entry.id, order: entry.order });
|
||||
}
|
||||
if (typeof entry.icon !== 'undefined') {
|
||||
txn.objectStore(DB_ICON_STORE).
|
||||
put({ id: entry.id, icon: entry.icon });
|
||||
}
|
||||
}
|
||||
txn.oncomplete = resolve;
|
||||
txn.onerror = reject;
|
||||
});
|
||||
},
|
||||
|
||||
remove: function(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var txn = this.db.transaction([DB_ORDER_STORE, DB_ICON_STORE],
|
||||
'readwrite');
|
||||
txn.objectStore(DB_ORDER_STORE).delete(id);
|
||||
txn.objectStore(DB_ICON_STORE).delete(id);
|
||||
txn.oncomplete = resolve;
|
||||
txn.onerror = reject;
|
||||
});
|
||||
},
|
||||
|
||||
getAll: function(onResult) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var txn = this.db.transaction([DB_ORDER_STORE, DB_ICON_STORE],
|
||||
'readonly');
|
||||
var orderStore = txn.objectStore(DB_ORDER_STORE);
|
||||
var iconStore = txn.objectStore(DB_ICON_STORE);
|
||||
var cursor = orderStore.index('order').openCursor();
|
||||
var results = [];
|
||||
|
||||
cursor.onsuccess = e => {
|
||||
var cursor = e.target.result;
|
||||
if (cursor) {
|
||||
var result = cursor.value;
|
||||
var iconRequest = iconStore.get(result.id);
|
||||
iconRequest.onsuccess = function(result, e) {
|
||||
if (e.target.result) {
|
||||
result.icon = e.target.result.icon;
|
||||
}
|
||||
results.push(result);
|
||||
if (onResult) {
|
||||
onResult(result);
|
||||
}
|
||||
}.bind(this, result);
|
||||
cursor.continue();
|
||||
}
|
||||
};
|
||||
|
||||
txn.oncomplete = () => {
|
||||
resolve(results);
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
exports.AppsMetadata = AppsMetadata;
|
||||
|
||||
}(window));
|
||||
5
gaia/apps/homescreen/js/bootstrap.js
vendored
Executable file
5
gaia/apps/homescreen/js/bootstrap.js
vendored
Executable file
|
|
@ -0,0 +1,5 @@
|
|||
/* global AppWindow */
|
||||
/* exported appWindow */
|
||||
'use strict';
|
||||
|
||||
var appWindow = new AppWindow();
|
||||
234
gaia/apps/homescreen/js/datastore.js
Executable file
234
gaia/apps/homescreen/js/datastore.js
Executable file
|
|
@ -0,0 +1,234 @@
|
|||
'use strict';
|
||||
|
||||
(function(exports) {
|
||||
function Datastore(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
Datastore.prototype = {
|
||||
/**
|
||||
* The name of the datastore
|
||||
*/
|
||||
name: '',
|
||||
|
||||
/**
|
||||
* The version of the indexed database
|
||||
*/
|
||||
DB_VERSION: 1,
|
||||
|
||||
/**
|
||||
* The datastore
|
||||
*/
|
||||
datastore: null,
|
||||
|
||||
/**
|
||||
* The last datastore revision that was synced
|
||||
*/
|
||||
lastRevision: null,
|
||||
|
||||
/**
|
||||
* Our local indexed db where we store our copy of bookmarks
|
||||
*/
|
||||
db: null,
|
||||
|
||||
get revisionName() {
|
||||
return this.name + '_revision';
|
||||
},
|
||||
|
||||
get idbName() {
|
||||
return this.name + '_mirror';
|
||||
},
|
||||
|
||||
get storeName() {
|
||||
return this.name + '_store';
|
||||
},
|
||||
|
||||
init: function() {
|
||||
var revisionString = localStorage.getItem(this.revisionName);
|
||||
if (revisionString) {
|
||||
this.lastRevision = JSON.parse(revisionString);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
var req = window.indexedDB.open(this.idbName, this.DB_VERSION);
|
||||
req.onupgradeneeded = this.upgradeSchema.bind(this);
|
||||
req.onsuccess = (e) => {
|
||||
this.db = e.target.result;
|
||||
resolve();
|
||||
|
||||
// Open up the shared datastore
|
||||
if (!navigator.getDataStores) {
|
||||
console.error('DataStore API is unavailable');
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.getDataStores(this.name).then((stores) => {
|
||||
if (stores.length < 1) {
|
||||
console.error(this.name + ' inaccessible');
|
||||
return;
|
||||
}
|
||||
|
||||
this.datastore = stores[0];
|
||||
this.datastore.addEventListener('change',
|
||||
this.onChange.bind(this));
|
||||
this.synchronise();
|
||||
}, (e) => {
|
||||
console.error('Error getting datastore', e);
|
||||
});
|
||||
};
|
||||
req.onerror = (e) => {
|
||||
console.error('Error opening datastore mirror database', e);
|
||||
reject(e);
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
upgradeSchema: function(e) {
|
||||
var db = e.target.result;
|
||||
var fromVersion = e.oldVersion;
|
||||
if (fromVersion < 1) {
|
||||
var store = db.createObjectStore(this.storeName, { keyPath: 'id' });
|
||||
store.createIndex('data', 'data', { unique: false });
|
||||
}
|
||||
},
|
||||
|
||||
synchronise: function() {
|
||||
// Serialise mirror refreshes: overlapping clear/add snapshots erase
|
||||
// rows while listeners are reading them.
|
||||
this._syncQueue = (this._syncQueue || Promise.resolve()).then(
|
||||
() => this._synchronise());
|
||||
return this._syncQueue;
|
||||
},
|
||||
_synchronise: function() {
|
||||
return new Promise((resolve, reject) => {
|
||||
var cursor = this.datastore.sync(this.lastRevision);
|
||||
|
||||
var self = this;
|
||||
function cursorResolve(task) {
|
||||
var promises = [];
|
||||
switch (task.operation) {
|
||||
case 'update':
|
||||
case 'add':
|
||||
promises.push(self.set(task.data));
|
||||
break;
|
||||
|
||||
case 'remove':
|
||||
promises.push(self.remove(task.id));
|
||||
break;
|
||||
|
||||
case 'clear':
|
||||
promises.push(self.clear());
|
||||
break;
|
||||
|
||||
case 'done':
|
||||
self.updateRevision();
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
promises.push(cursor.next());
|
||||
Promise.all(promises).then(
|
||||
(results) => {
|
||||
cursorResolve(results.pop());
|
||||
}, reject);
|
||||
}
|
||||
|
||||
cursor.next().then(cursorResolve, reject);
|
||||
});
|
||||
},
|
||||
|
||||
set: function(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var txn = this.db.transaction([this.storeName], 'readwrite');
|
||||
txn.oncomplete = function onComplete(data, resolve) {
|
||||
resolve();
|
||||
document.dispatchEvent(new CustomEvent(this.name + '-set',
|
||||
{ detail: { id: data.id }}));
|
||||
}.bind(this, data, resolve);
|
||||
txn.onerror = reject;
|
||||
try {
|
||||
txn.objectStore(this.storeName).put({ id: data.id, data: data });
|
||||
} catch(e) {
|
||||
console.error('Error putting data in ' + this.idbName + ':', e);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
remove: function(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var txn = this.db.transaction([this.storeName], 'readwrite');
|
||||
txn.oncomplete = function onComplete(id, resolve) {
|
||||
resolve();
|
||||
document.dispatchEvent(new CustomEvent(this.name + '-removed',
|
||||
{ detail: { id: id }}));
|
||||
}.bind(this, id, resolve);
|
||||
txn.onerror = reject;
|
||||
try {
|
||||
txn.objectStore(this.storeName).delete(id);
|
||||
} catch(e) {
|
||||
console.error('Error deleting data from ' + this.idbName + ':', e);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
clear: function() {
|
||||
return new Promise((resolve, reject) => {
|
||||
var txn = this.db.transaction([this.storeName], 'readwrite');
|
||||
txn.oncomplete = function onComplete(resolve) {
|
||||
resolve();
|
||||
document.dispatchEvent(new CustomEvent(this.name + '-cleared'));
|
||||
}.bind(this, resolve);
|
||||
txn.onerror = reject;
|
||||
txn.objectStore(this.storeName).clear();
|
||||
});
|
||||
},
|
||||
|
||||
updateRevision: function() {
|
||||
this.lastRevision = this.datastore.revisionId;
|
||||
localStorage.setItem(this.revisionName,
|
||||
JSON.stringify(this.lastRevision));
|
||||
},
|
||||
|
||||
onChange: function(e) {
|
||||
this.synchronise().then(() => {},
|
||||
(error) => {
|
||||
console.error('Failed to handle ' + this.name + ' change', error);
|
||||
});
|
||||
},
|
||||
|
||||
get: function(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var txn = this.db.transaction([this.storeName], 'readonly');
|
||||
txn.onerror = reject;
|
||||
txn.objectStore(this.storeName).get(id).onsuccess =
|
||||
(event) => {
|
||||
resolve(event.target.result);
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
getAll: function(filter = () => true) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var results = [];
|
||||
var txn = this.db.transaction([this.storeName], 'readonly');
|
||||
txn.onerror = reject;
|
||||
txn.oncomplete = () => { resolve(results); };
|
||||
txn.objectStore(this.storeName).openCursor().onsuccess =
|
||||
(event) => {
|
||||
var cursor = event.target.result;
|
||||
if (cursor) {
|
||||
if (filter(cursor.value)) {
|
||||
results.push(cursor.value);
|
||||
}
|
||||
cursor.continue();
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
exports.Datastore = Datastore;
|
||||
|
||||
}(window));
|
||||
180
gaia/apps/homescreen/js/firstrun.js
Executable file
180
gaia/apps/homescreen/js/firstrun.js
Executable file
|
|
@ -0,0 +1,180 @@
|
|||
/* global LazyLoader */
|
||||
'use strict';
|
||||
|
||||
(function(exports) {
|
||||
|
||||
const COLS_PREF = 'grid.cols';
|
||||
|
||||
const VERTICALHOME_APPS = 'verticalhome_items_migration';
|
||||
|
||||
const VERTICALHOME_SETTINGS = 'vertical_preferences_store';
|
||||
|
||||
const HOMESCREEN_SETTINGS = 'homescreen_settings';
|
||||
|
||||
function FirstRun() {
|
||||
return importVerticalHome().then(Promise.resolve(),
|
||||
(e) => {
|
||||
console.warn('Loading default settings, import failed:', e);
|
||||
return loadDefault();
|
||||
});
|
||||
}
|
||||
|
||||
function importVerticalHome() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!navigator.getDataStores) {
|
||||
reject('DataStore API is unavailable');
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.getDataStores(VERTICALHOME_APPS).then(
|
||||
stores => {
|
||||
if (stores.length < 1) {
|
||||
reject('verticalhome datastores inaccessible');
|
||||
return;
|
||||
}
|
||||
|
||||
stores[0].getLength().then(length => {
|
||||
if (length === 0) {
|
||||
reject('No verticalhome app order to import');
|
||||
return;
|
||||
}
|
||||
|
||||
var cursor = stores[0].sync();
|
||||
|
||||
var items = {};
|
||||
var iterateItems = () => {
|
||||
cursor.next().then(task => {
|
||||
switch (task.operation) {
|
||||
case 'done':
|
||||
// Convert from verticalhome storage format to something
|
||||
// homescreen can use, skipping over features that aren't
|
||||
// supported.
|
||||
var id;
|
||||
var order = [];
|
||||
for (var prop in items) {
|
||||
var item = items[prop];
|
||||
switch (item.type) {
|
||||
case 'app':
|
||||
id = item.manifestURL + '/' +
|
||||
(item.entryPoint ? item.entryPoint : '');
|
||||
break;
|
||||
|
||||
case 'bookmark':
|
||||
id = item.id;
|
||||
break;
|
||||
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
|
||||
order.push({ id: id, order: item.index });
|
||||
}
|
||||
|
||||
// Sort by order
|
||||
order.sort((a, b) => { return a.order - b.order; });
|
||||
|
||||
// Rewrite order so there are no gaps
|
||||
order.forEach((entry, index) => {
|
||||
entry.order = index;
|
||||
});
|
||||
|
||||
// Load columns setting and resolve
|
||||
navigator.getDataStores(VERTICALHOME_SETTINGS).then(
|
||||
stores => {
|
||||
stores[0].get(COLS_PREF).then(cols => {
|
||||
resolve({ order: order, small: cols >= 4 });
|
||||
|
||||
// If 4-column is set, we should synchronise the
|
||||
// homescreen settings datastore so that Settings
|
||||
// reflects the correct value. (3 is the default)
|
||||
if (cols >= 4) {
|
||||
navigator.getDataStores(HOMESCREEN_SETTINGS).then(
|
||||
stores => {
|
||||
stores[0].put(cols, COLS_PREF);
|
||||
}).catch(
|
||||
e => {
|
||||
console.error(
|
||||
'Error storing homescreen cols setting', e);
|
||||
});
|
||||
}
|
||||
}, () => {
|
||||
resolve({ order: order, small: false });
|
||||
});
|
||||
},
|
||||
(e) => {
|
||||
console.error('Error retrieving columns preference', e);
|
||||
resolve({ order: order, small: false });
|
||||
});
|
||||
return;
|
||||
|
||||
case 'clear':
|
||||
items = {};
|
||||
break;
|
||||
|
||||
case 'put':
|
||||
case 'add':
|
||||
items[task.id] = task.data;
|
||||
break;
|
||||
|
||||
case 'remove':
|
||||
delete items[task.id];
|
||||
break;
|
||||
|
||||
default:
|
||||
reject('Unhandled datastore operation: ' + task.operation);
|
||||
return;
|
||||
}
|
||||
|
||||
iterateItems();
|
||||
}, reject);
|
||||
};
|
||||
|
||||
iterateItems();
|
||||
}, reject);
|
||||
}, reject);
|
||||
});
|
||||
}
|
||||
|
||||
function loadDefault() {
|
||||
return new Promise((resolve, reject) => {
|
||||
LazyLoader.getJSON('js/init.json').then(
|
||||
json => {
|
||||
if (!json) {
|
||||
resolve({ order: [], small: false});
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert the initialisation JSON format to something more useful
|
||||
// for homescreen.
|
||||
var small = !!(json.preferences && json.preferences[COLS_PREF] &&
|
||||
(json.preferences[COLS_PREF] > 3));
|
||||
|
||||
var order = [];
|
||||
if (json.grid) {
|
||||
var index = 0;
|
||||
json.grid.forEach((section) => {
|
||||
section.forEach((entry) => {
|
||||
if (!entry || !entry.manifestURL) {
|
||||
return;
|
||||
}
|
||||
|
||||
var id = entry.manifestURL + '/' +
|
||||
(entry.entry_point ? entry.entry_point : '');
|
||||
order.push({ id: id, order: index });
|
||||
index ++;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
resolve({ order: order, small: small});
|
||||
},
|
||||
e => {
|
||||
reject(e);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
exports.FirstRun = FirstRun;
|
||||
|
||||
}(window));
|
||||
272
gaia/apps/homescreen/js/pages.js
Executable file
272
gaia/apps/homescreen/js/pages.js
Executable file
|
|
@ -0,0 +1,272 @@
|
|||
/* global PagesStore, IconsHelper */
|
||||
'use strict';
|
||||
|
||||
(function(exports) {
|
||||
|
||||
/**
|
||||
* The size in pixels of the icons in the pinned pages.
|
||||
*/
|
||||
const PAGES_ICON_SIZE = 30;
|
||||
|
||||
function Pages() {
|
||||
// Element references
|
||||
this.panel = document.getElementById('pages-panel');
|
||||
this.panels = document.getElementById('panels');
|
||||
this.pages = document.getElementById('pages');
|
||||
this.scrollable = document.querySelector('#pages-panel > .scrollable');
|
||||
this.bottombar = document.getElementById('bottombar');
|
||||
this.remove = document.getElementById('remove');
|
||||
this.done = document.getElementById('done');
|
||||
|
||||
// Tracking if the list is empty
|
||||
this.empty = true;
|
||||
|
||||
// Edit mode
|
||||
this.editMode = false;
|
||||
|
||||
// Dialogs
|
||||
this.dialogs = [];
|
||||
|
||||
// Signal handlers
|
||||
this.pages.addEventListener('click', this);
|
||||
this.pages.addEventListener('contextmenu', this);
|
||||
this.pages.addEventListener('keydown', this);
|
||||
window.addEventListener('resize', this);
|
||||
|
||||
this.done.addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
this.exitEditMode();
|
||||
});
|
||||
|
||||
this.remove.addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
this.unpinSelectedCard();
|
||||
});
|
||||
|
||||
// Initialise and populate pinned pages
|
||||
this.pagesStore = new PagesStore('places');
|
||||
this.pagesStore.init().then(() => {
|
||||
// Triggered when pages are added and updated.
|
||||
document.addEventListener('places-set', (e) => {
|
||||
var id = e.detail.id;
|
||||
this.pagesStore.get(id).then((page) => {
|
||||
for (var child of this.pages.children) {
|
||||
if (child.dataset.id === id) {
|
||||
if (!page.data.pinned) {
|
||||
this.removeCard(child);
|
||||
} else {
|
||||
this.updatePinnedPage(child, page.data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!page.data.pinned) {
|
||||
return;
|
||||
}
|
||||
|
||||
// A new page was pinned.
|
||||
this.addPinnedPage(page.data);
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('places-removed', (e) => {
|
||||
var id = e.detail.id;
|
||||
for (var child of this.pages.children) {
|
||||
if (child.dataset.id === id) {
|
||||
this.removeCard(child);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('places-cleared', () => {
|
||||
this.exitEditMode();
|
||||
|
||||
for (var child of this.pages.children) {
|
||||
this.pages.removeChild(child);
|
||||
}
|
||||
|
||||
if (!this.empty) {
|
||||
this.empty = true;
|
||||
this.panel.classList.add('empty');
|
||||
}
|
||||
});
|
||||
},
|
||||
(e) => {
|
||||
console.error('Error initialising pinned pages', e);
|
||||
}).then(() => {
|
||||
return this.pagesStore.getAll().then((pages) => {
|
||||
for (var page of pages) {
|
||||
this.addPinnedPage(page.data);
|
||||
}
|
||||
|
||||
// If there are no pinned pages, display helpful information.
|
||||
if (this.empty) {
|
||||
this.panel.classList.add('empty');
|
||||
}
|
||||
}, (e) => {
|
||||
console.error('Error getting pinned pages', e);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Pages.prototype = {
|
||||
updatePinnedPage: function(card, page) {
|
||||
card.title = page.title;
|
||||
card.dataset.id = page.url;
|
||||
card.style.order = -Math.round(page.pinTime / 1000);
|
||||
page.meta = page.meta || {};
|
||||
|
||||
if (page.screenshot) {
|
||||
page.meta.screenshot = page.screenshot;
|
||||
}
|
||||
|
||||
setTimeout(function(card, page) {
|
||||
card.meta = page.meta;
|
||||
IconsHelper.getIconBlob(page.url, PAGES_ICON_SIZE, page)
|
||||
.then((iconObj) => {
|
||||
if (iconObj.blob) {
|
||||
var iconURL = URL.createObjectURL(iconObj.blob);
|
||||
card.icon = `url(${iconURL})`;
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error('Failed to fetch icon', e);
|
||||
});
|
||||
}.bind(this, card, page));
|
||||
},
|
||||
|
||||
addPinnedPage: function(page) {
|
||||
var pinCard = document.createElement('gaia-pin-card');
|
||||
|
||||
// Make the card accessible/allow activation
|
||||
pinCard.tabIndex = 0;
|
||||
pinCard.setAttribute('role', 'link');
|
||||
|
||||
this.updatePinnedPage(pinCard, page);
|
||||
this.pages.appendChild(pinCard);
|
||||
|
||||
if (this.empty) {
|
||||
this.empty = false;
|
||||
this.panel.classList.remove('empty');
|
||||
}
|
||||
},
|
||||
|
||||
launchCard: function(card) {
|
||||
var features = {
|
||||
name: card.title,
|
||||
icon: card.icon,
|
||||
remote: true
|
||||
};
|
||||
|
||||
window.open(card.dataset.id, '_blank', Object.keys(features).map(
|
||||
key => encodeURIComponent(key) + '=' + encodeURIComponent(features[key])
|
||||
).join(','));
|
||||
},
|
||||
|
||||
enterEditMode: function(card) {
|
||||
if (this.selectedCard) {
|
||||
this.selectedCard.classList.remove('selected');
|
||||
}
|
||||
if (card) {
|
||||
this.selectedCard = card;
|
||||
card.classList.add('selected');
|
||||
}
|
||||
|
||||
this.editMode = true;
|
||||
document.body.classList.add('edit-mode');
|
||||
this.remove.classList.add('active');
|
||||
},
|
||||
|
||||
exitEditMode: function() {
|
||||
if (!this.editMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.editMode = false;
|
||||
document.body.classList.remove('edit-mode');
|
||||
this.remove.classList.remove('active');
|
||||
if (this.selectedCard) {
|
||||
this.selectedCard.classList.remove('selected');
|
||||
this.selectedCard = null;
|
||||
}
|
||||
},
|
||||
|
||||
removeCard: function(card) {
|
||||
if (this.selectedCard === card) {
|
||||
this.selectedCard = null;
|
||||
}
|
||||
this.pages.removeChild(card, () => {
|
||||
if (!this.empty && this.pages.children.length === 0) {
|
||||
this.exitEditMode();
|
||||
this.empty = true;
|
||||
this.panel.classList.add('empty');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
unpinSelectedCard: function() {
|
||||
if (!this.selectedCard) {
|
||||
return;
|
||||
}
|
||||
|
||||
var id = this.selectedCard.dataset.id;
|
||||
|
||||
// Remove child immediately, datastore operations can be quite slow
|
||||
this.removeCard(this.selectedCard);
|
||||
|
||||
this.pagesStore.get(id).then(entry => {
|
||||
entry.data.pinned = false;
|
||||
this.pagesStore.datastore.put(entry.data, id).then(() => {},
|
||||
e => {
|
||||
console.error('Error unpinning page:', e);
|
||||
});
|
||||
}, e => {
|
||||
console.error('Error retrieving page to unpin:', e);
|
||||
});
|
||||
},
|
||||
|
||||
handleEvent: function(e) {
|
||||
switch (e.type) {
|
||||
case 'click':
|
||||
if (e.target.nodeName !== 'GAIA-PIN-CARD') {
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.editMode) {
|
||||
this.enterEditMode(e.target);
|
||||
} else {
|
||||
this.launchCard(e.target);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'contextmenu':
|
||||
if (e.target.nodeName !== 'GAIA-PIN-CARD') {
|
||||
break;
|
||||
}
|
||||
this.enterEditMode(e.target);
|
||||
break;
|
||||
|
||||
case 'keydown':
|
||||
if (e.target.nodeName !== 'GAIA-PIN-CARD') {
|
||||
break;
|
||||
}
|
||||
|
||||
switch (e.keyCode) {
|
||||
case 32: // Space
|
||||
case 13: // Enter
|
||||
this.launchCard(e.target);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'resize':
|
||||
this.pages.synchronise();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
exports.Pages = Pages;
|
||||
|
||||
}(window));
|
||||
25
gaia/apps/homescreen/js/pagesstore.js
Executable file
25
gaia/apps/homescreen/js/pagesstore.js
Executable file
|
|
@ -0,0 +1,25 @@
|
|||
/* global Datastore */
|
||||
'use strict';
|
||||
|
||||
(function(exports) {
|
||||
function PagesStore(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
PagesStore.prototype = Object.create(Datastore.prototype);
|
||||
|
||||
PagesStore.prototype.DB_VERSION = 1;
|
||||
|
||||
PagesStore.prototype.set = function(data) {
|
||||
data.id = data.url;
|
||||
return Datastore.prototype.set.call(this, data);
|
||||
};
|
||||
|
||||
PagesStore.prototype.getAll = function() {
|
||||
var filter = (value) => value.data && value.data.pinned;
|
||||
return Datastore.prototype.getAll.call(this, filter);
|
||||
};
|
||||
|
||||
exports.PagesStore = PagesStore;
|
||||
|
||||
}(window));
|
||||
118
gaia/apps/homescreen/js/settings.js
Executable file
118
gaia/apps/homescreen/js/settings.js
Executable file
|
|
@ -0,0 +1,118 @@
|
|||
'use strict';
|
||||
|
||||
(function(exports) {
|
||||
|
||||
/**
|
||||
* Name of the global home screen settings datastore.
|
||||
*/
|
||||
const SETTINGS_STORE = 'homescreen_settings';
|
||||
|
||||
/**
|
||||
* Stored settings version, for use when changing/refactoring settings
|
||||
* storage.
|
||||
*/
|
||||
const SETTINGS_VERSION = 0;
|
||||
|
||||
/**
|
||||
* Name of the global home screen columns setting.
|
||||
*/
|
||||
const COLUMNS_SETTING = 'grid.cols';
|
||||
|
||||
/**
|
||||
* Name of the global home screen paging setting.
|
||||
*/
|
||||
const PAGING_SETTING = 'grid.paging';
|
||||
|
||||
function Settings() {
|
||||
// Initialise default settings
|
||||
this.small = false;
|
||||
this.scrollSnapping = false;
|
||||
this.firstRun = false;
|
||||
|
||||
// Listen to external settings changes
|
||||
if (navigator.getDataStores) {
|
||||
navigator.getDataStores(SETTINGS_STORE).then(stores => {
|
||||
if (stores.length < 1) {
|
||||
return Promise.reject('Settings datastore inaccessible');
|
||||
}
|
||||
|
||||
var signalChange = () => {
|
||||
this.save();
|
||||
window.dispatchEvent(new CustomEvent('settings-changed'));
|
||||
};
|
||||
|
||||
var syncSmallSetting = (signal) => {
|
||||
stores[0].get(COLUMNS_SETTING).then(cols => {
|
||||
var oldSmall = this.small;
|
||||
this.small = cols > 3;
|
||||
if (this.small !== oldSmall) {
|
||||
signalChange();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var syncPagingSetting = () => {
|
||||
stores[0].get(PAGING_SETTING).then(paging => {
|
||||
var oldSnapping = this.scrollSnapping;
|
||||
this.scrollSnapping = paging || false;
|
||||
if (this.scrollSnapping !== oldSnapping) {
|
||||
signalChange();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
stores[0].addEventListener('change', e => {
|
||||
switch (e.id) {
|
||||
case COLUMNS_SETTING:
|
||||
syncSmallSetting();
|
||||
break;
|
||||
case PAGING_SETTING:
|
||||
syncPagingSetting();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
syncSmallSetting();
|
||||
syncPagingSetting();
|
||||
}, e => {
|
||||
console.error('Error retrieving home screen settings datastore:', e);
|
||||
});
|
||||
} else {
|
||||
console.error('Datastore API unavailable');
|
||||
}
|
||||
|
||||
// Restore existing settings
|
||||
var settingsString = localStorage.getItem('settings');
|
||||
if (!settingsString) {
|
||||
this.firstRun = true;
|
||||
return;
|
||||
}
|
||||
|
||||
var settings = JSON.parse(settingsString);
|
||||
if (settings.version !== SETTINGS_VERSION) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.small = settings.small || false;
|
||||
this.scrollSnapping = settings.scrollSnapping || false;
|
||||
|
||||
// Monitor global homescreen settings
|
||||
if (!navigator.getDataStores) {
|
||||
console.error('Datastore API unavailable');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Settings.prototype = {
|
||||
save: function() {
|
||||
localStorage.setItem('settings', JSON.stringify({
|
||||
version: SETTINGS_VERSION,
|
||||
small: this.small,
|
||||
scrollSnapping: this.scrollSnapping
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
exports.Settings = Settings;
|
||||
|
||||
}(window));
|
||||
1
gaia/apps/homescreen/js/setup.js
Executable file
1
gaia/apps/homescreen/js/setup.js
Executable file
|
|
@ -0,0 +1 @@
|
|||
window.GaiaAppIconBaseurl = 'shared/elements/gaia-site-icon/';
|
||||
175
gaia/apps/homescreen/js/window.js
Executable file
175
gaia/apps/homescreen/js/window.js
Executable file
|
|
@ -0,0 +1,175 @@
|
|||
/* global Apps, Pages */
|
||||
'use strict';
|
||||
|
||||
(function(exports) {
|
||||
|
||||
/**
|
||||
* Time after receiving a handled hashchange that we ignore hashchange
|
||||
* events for.
|
||||
*/
|
||||
const HASH_CHANGE_DEBOUNCE = 100;
|
||||
|
||||
function AppWindow() {
|
||||
this.header = document.getElementById('page-indicator-header');
|
||||
this.indicator = document.getElementById('page-indicator');
|
||||
this.panels = document.getElementById('panels');
|
||||
this.shadow = document.getElementById('shadow');
|
||||
|
||||
this.apps = new Apps();
|
||||
this.pages = new Pages();
|
||||
|
||||
// Panel visibility state
|
||||
this.appsVisible = undefined;
|
||||
|
||||
// Home-button handling
|
||||
this.ignoreHashChangeTimeout = null;
|
||||
|
||||
// Signal handlers
|
||||
this.indicator.addEventListener('keypress', this);
|
||||
this.panels.addEventListener('scroll', this);
|
||||
window.addEventListener('hashchange', this, true);
|
||||
window.addEventListener('localized', this);
|
||||
document.addEventListener('visibilitychange', this);
|
||||
|
||||
// Update the panel indicator
|
||||
this.updatePanelIndicator();
|
||||
|
||||
// Application has finished initialisation
|
||||
window.performance.mark('navigationInteractive');
|
||||
}
|
||||
|
||||
AppWindow.prototype = {
|
||||
updatePanelIndicator: function() {
|
||||
var appsVisible =
|
||||
Math.abs(this.panels.scrollLeft) <= this.panels.scrollWidth / 4;
|
||||
if (this.appsVisible !== appsVisible) {
|
||||
this.appsVisible = appsVisible;
|
||||
|
||||
this.header.setAttribute('data-l10n-id', appsVisible ?
|
||||
'apps-panel' : 'pages-panel');
|
||||
|
||||
this.indicator.children[0].classList.toggle('active', appsVisible);
|
||||
this.indicator.children[1].classList.toggle('active', !appsVisible);
|
||||
this.indicator.setAttribute('aria-valuenow', appsVisible ? 1 : 2);
|
||||
this.indicator.setAttribute('data-l10n-args', JSON.stringify({
|
||||
currentPage: appsVisible ? 1 : 2,
|
||||
totalPages: 2
|
||||
}));
|
||||
|
||||
this.apps.panel.setAttribute('aria-hidden', !appsVisible);
|
||||
this.pages.panel.setAttribute('aria-hidden', appsVisible);
|
||||
|
||||
if (appsVisible) {
|
||||
this.pages.scrollable.removeEventListener('scroll', this);
|
||||
this.apps.scrollable.addEventListener('scroll', this);
|
||||
} else {
|
||||
this.apps.scrollable.removeEventListener('scroll', this);
|
||||
this.pages.scrollable.addEventListener('scroll', this);
|
||||
}
|
||||
|
||||
this.updateShadowState();
|
||||
}
|
||||
},
|
||||
|
||||
updateShadowState: function() {
|
||||
var position = this.appsVisible ?
|
||||
this.apps.scrollable.scrollTop : this.pages.scrollable.scrollTop;
|
||||
var scrolled = position > 1;
|
||||
if (this.shadow.classList.contains('visible') !== scrolled) {
|
||||
this.shadow.classList.toggle('visible', scrolled);
|
||||
}
|
||||
},
|
||||
|
||||
handleEvent: function(e) {
|
||||
switch (e.type) {
|
||||
// Switch between panels
|
||||
case 'keypress':
|
||||
if (!e.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (e.keyCode) {
|
||||
case e.DOM_VK_RIGHT:
|
||||
this.panels.scrollTo(
|
||||
{ left: this.panels.scrollLeftMax, top: 0, behavior: 'smooth' });
|
||||
return;
|
||||
case e.DOM_VK_LEFT:
|
||||
this.panels.scrollTo(
|
||||
{ left: 0, top: 0, behavior: 'smooth' });
|
||||
return;
|
||||
}
|
||||
return;
|
||||
|
||||
// Display the top shadow when scrolling down
|
||||
case 'scroll':
|
||||
if (e.target === this.panels) {
|
||||
this.updatePanelIndicator();
|
||||
} else {
|
||||
this.updateShadowState();
|
||||
}
|
||||
return;
|
||||
|
||||
// Cancel edit modes/scroll to the top/scroll to apps on home-button
|
||||
case 'hashchange':
|
||||
if (document.hidden || this.ignoreHashChangeTimeout !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
this.ignoreHashChangeTimeout = setTimeout(() => {
|
||||
this.ignoreHashChangeTimeout = null;
|
||||
}, HASH_CHANGE_DEBOUNCE);
|
||||
|
||||
// If a dialog is showing, cancel the dialog
|
||||
for (var dialog of this.apps.dialogs.concat(this.pages.dialogs)) {
|
||||
if (!dialog.opened) {
|
||||
continue;
|
||||
}
|
||||
|
||||
dialog.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// If we're in edit mode, cancel edit mode
|
||||
if (this.apps.editMode) {
|
||||
this.apps.exitEditMode();
|
||||
return;
|
||||
}
|
||||
if (this.pages.editMode) {
|
||||
this.pages.exitEditMode();
|
||||
return;
|
||||
}
|
||||
|
||||
// If we're on the pages panel and scrolled to the top, show apps
|
||||
if (!this.appsVisible && this.pages.scrollable.scrollTop === 0) {
|
||||
this.panels.scrollTo({ left: 0, top: 0, behavior: 'smooth' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, scroll the visible panel to the top
|
||||
var visiblePanel = this.appsVisible ?
|
||||
this.apps.scrollable : this.pages.scrollable;
|
||||
visiblePanel.scrollTo({ left: 0, top: 0, behavior: 'smooth' });
|
||||
return;
|
||||
|
||||
case 'localized':
|
||||
this.updatePanelIndicator();
|
||||
return;
|
||||
|
||||
// Exit edit mode if the user switches away from the home screen (e.g.
|
||||
// by locking the phone).
|
||||
case 'visibilitychange':
|
||||
if (document.hidden) {
|
||||
this.apps.exitEditMode();
|
||||
this.pages.exitEditMode();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
exports.AppWindow = AppWindow;
|
||||
|
||||
}(window));
|
||||
Loading…
Reference in a new issue