Fix camera switching and Contacts/SMS activity routing across shared hosts. Integrate Marketplace installation and native standalone recovery tools, Bluetooth runtime packages and trusted certificates. Update English/French READMEs and the shared changelog with tested scope and remaining radio/audio/Bluetooth limitations. Preview17 remains the existing APK; no new native image is released. Validation: shared JavaScript and native Python tests; Android packaging and payload parity from an isolated publication checkout.
1132 lines
36 KiB
JavaScript
Executable file
1132 lines
36 KiB
JavaScript
Executable file
/* global MozActivity, AppsMetadata, Datastore, LazyLoader, FirstRun,
|
|
IconsHelper, Settings */
|
|
'use strict';
|
|
|
|
(function(exports) {
|
|
/**
|
|
* Timeout before resizing the apps grid after apps change.
|
|
*/
|
|
const RESIZE_TIMEOUT = 500;
|
|
|
|
/**
|
|
* Timeout before showing a dialog. Without this, the click that comes through
|
|
* after an activate event from gaia-container will close the dialog.
|
|
*/
|
|
const DIALOG_SHOW_TIMEOUT = 50;
|
|
|
|
/**
|
|
* The distance at the top and bottom of the icon container that when hovering
|
|
* an icon in will cause scrolling.
|
|
*/
|
|
const AUTOSCROLL_DISTANCE = 40;
|
|
|
|
/**
|
|
* The timeout before auto-scrolling a page when hovering at the edges
|
|
* of the grid.
|
|
*/
|
|
const AUTOSCROLL_DELAY = 750;
|
|
|
|
/**
|
|
* The time to wait after setting a scroll-position before disabling
|
|
* overflow during drag-and-drop.
|
|
*/
|
|
const AUTOSCROLL_OVERFLOW_DELAY = 500;
|
|
|
|
/**
|
|
* App roles that will be skipped on the homescreen.
|
|
*/
|
|
const HIDDEN_ROLES = [
|
|
'system', 'input', 'homescreen', 'theme', 'addon', 'langpack'
|
|
];
|
|
|
|
/**
|
|
* Strings that are matched against to black-list app origins.
|
|
* TODO: This should not be hard-coded.
|
|
*/
|
|
const BLACKLIST = [];
|
|
|
|
function Apps() {
|
|
// Chrome is displayed
|
|
window.performance.mark('navigationLoaded');
|
|
|
|
// Element references
|
|
this.panel = document.getElementById('apps-panel');
|
|
this.meta = document.head.querySelector('meta[name="theme-color"]');
|
|
this.scrollable = document.querySelector('#apps-panel > .scrollable');
|
|
this.icons = document.getElementById('apps');
|
|
this.remove = document.getElementById('remove');
|
|
this.rename = document.getElementById('rename');
|
|
this.done = document.getElementById('done');
|
|
this.cancelDownload = document.getElementById('cancel-download');
|
|
this.resumeDownload = document.getElementById('resume-download');
|
|
this.dialogs = [this.cancelDownload, this.resumeDownload];
|
|
|
|
// XXX Working around gaia-components issue #8
|
|
var dialog;
|
|
for (dialog of this.dialogs) {
|
|
dialog.hide();
|
|
}
|
|
|
|
// Change the colour of the statusbar when showing dialogs
|
|
var dialogVisibilityCallback = () => {
|
|
for (var dialog of this.dialogs) {
|
|
if (dialog.opened) {
|
|
this.meta.content = 'white';
|
|
document.body.classList.add('dialog-active');
|
|
return;
|
|
}
|
|
}
|
|
this.meta.content = 'transparent';
|
|
document.body.classList.remove('dialog-active');
|
|
};
|
|
for (dialog of this.dialogs) {
|
|
var observer = new MutationObserver(dialogVisibilityCallback);
|
|
observer.observe(dialog,
|
|
{ attributes: true, attributeFilter: ['style'] });
|
|
}
|
|
|
|
// Paging
|
|
this.resizeTimeout = null;
|
|
this.pageHeight = 1;
|
|
this.gridHeight = 1;
|
|
this.pendingGridHeight = 1;
|
|
this.iconsPerPage = 0;
|
|
this.iconsLeft = 0;
|
|
this.iconsRight = 0;
|
|
|
|
// Scroll behaviour
|
|
this.appsVisible = false;
|
|
|
|
// Drag-and-drop
|
|
this.dragging = false;
|
|
this.draggedIndex = -1;
|
|
this.autoScrollInterval = null;
|
|
this.autoScrollOverflowTimeout = null;
|
|
this.hoverIcon = null;
|
|
|
|
// Edit mode
|
|
this.editMode = false;
|
|
this.shouldEnterEditMode = false;
|
|
this.selectedIcon = null;
|
|
this.rename.addEventListener('click', e => {
|
|
e.preventDefault();
|
|
this.renameSelectedIcon();
|
|
});
|
|
this.remove.addEventListener('click', e => {
|
|
e.preventDefault();
|
|
this.removeSelectedIcon();
|
|
});
|
|
this.done.addEventListener('click', e => {
|
|
e.preventDefault();
|
|
this.exitEditMode();
|
|
});
|
|
|
|
// Icon and grid sizing behaviour
|
|
this._iconSize = 0;
|
|
this.lastWindowWidth = window.innerWidth;
|
|
this.lastWindowHeight = window.innerHeight;
|
|
|
|
// Signal handlers
|
|
this.icons.addEventListener('activate', this);
|
|
this.icons.addEventListener('drag-start', this);
|
|
this.icons.addEventListener('drag-move', this);
|
|
this.icons.addEventListener('drag-end', this);
|
|
this.icons.addEventListener('drag-rearrange', this);
|
|
this.icons.addEventListener('drag-finish', this);
|
|
navigator.mozApps.mgmt.addEventListener('install', this);
|
|
navigator.mozApps.mgmt.addEventListener('uninstall', this);
|
|
window.addEventListener('localized', this);
|
|
window.addEventListener('online', this);
|
|
window.addEventListener('resize', this);
|
|
window.addEventListener('settings-changed', this);
|
|
|
|
// Settings
|
|
this.settings = new Settings();
|
|
this.icons.classList.toggle('small', this.settings.small);
|
|
this.scrollable.classList.toggle('snapping', this.settings.scrollSnapping);
|
|
|
|
// Populate apps and bookmarks asynchronously
|
|
this.metadataLoaded = 0;
|
|
this.startupMetadata = [];
|
|
this.iconsToRetry = [];
|
|
this.pendingIcons = {};
|
|
this.metadata = new AppsMetadata();
|
|
this.bookmarks = new Datastore('bookmarks_store');
|
|
|
|
// Make sure icons isn't doing lots of unnecessary work while we're
|
|
// loading the first screen of apps.
|
|
this.visualLoadComplete = false;
|
|
this.icons.freeze();
|
|
this.icons.classList.add('loading');
|
|
|
|
Promise.all([
|
|
// Load app metadata. If metadata loading fails, continue populating apps
|
|
// anyway - it means they'll be in the default order and their order
|
|
// won't save, but it's better than showing a blank screen.
|
|
// If this is the first run, get the app order from the first-run script
|
|
// after initialising the metadata database.
|
|
this.metadata.init().then(this.settings.firstRun ? () => {
|
|
return LazyLoader.load('js/firstrun.js').then(
|
|
() => {
|
|
return FirstRun().then((results) => {
|
|
this.toggleSmall(results.small);
|
|
this.startupMetadata = results.order;
|
|
this.settings.small = results.small;
|
|
this.settings.save();
|
|
return Promise.resolve();
|
|
}, (e) => {
|
|
console.error('Error running first-run script', e);
|
|
return Promise.resolve();
|
|
});
|
|
},
|
|
(e) => {
|
|
console.error('Failed to load first-run script');
|
|
return Promise.resolve();
|
|
});
|
|
} :
|
|
() => {
|
|
return this.metadata.getAll(result => {
|
|
this.startupMetadata.push(result);
|
|
this.metadataLoaded++;
|
|
|
|
// Process results in batches as they come in
|
|
var processResult = data => {
|
|
if (this.pendingIcons[data.id]) {
|
|
this.addAppIcon.apply(this, this.pendingIcons[data.id]);
|
|
delete this.pendingIcons[data.id];
|
|
}
|
|
};
|
|
|
|
if (!this.visualLoadComplete) {
|
|
processResult(result);
|
|
this.refreshGridSize();
|
|
} else if ((this.metadataLoaded % this.iconsPerPage) === 0) {
|
|
this.icons.freeze();
|
|
for (var entry of this.startupMetadata) {
|
|
processResult(entry);
|
|
}
|
|
this.refreshGridSize();
|
|
this.icons.thaw();
|
|
}
|
|
}).then(Promise.resolve(),
|
|
e => {
|
|
console.error('Failed to retrieve metadata entries', e);
|
|
return Promise.resolve();
|
|
});
|
|
},
|
|
(e) => {
|
|
console.error('Failed to initialise metadata db', e);
|
|
return Promise.resolve();
|
|
}),
|
|
|
|
// Populate apps
|
|
new Promise((resolve, reject) => {
|
|
var request = navigator.mozApps.mgmt.getAll();
|
|
request.onsuccess = (e) => {
|
|
for (var app of request.result) {
|
|
this.addApp(app);
|
|
}
|
|
resolve();
|
|
};
|
|
request.onerror = (e) => {
|
|
console.error('Error calling getAll: ' + request.error.name);
|
|
resolve();
|
|
};
|
|
}),
|
|
|
|
// Initialise and populate bookmarks
|
|
this.bookmarks.init().then(() => {
|
|
document.addEventListener('bookmarks_store-set', (e) => {
|
|
var id = e.detail.id;
|
|
this.bookmarks.get(id).then((bookmark) => {
|
|
for (var child of this.icons.children) {
|
|
var icon = child.firstElementChild;
|
|
if (icon.bookmark && icon.bookmark.id === id) {
|
|
icon.bookmark = bookmark.data;
|
|
icon.refresh();
|
|
return;
|
|
}
|
|
}
|
|
this.addAppIcon(bookmark.data);
|
|
this.storeAppOrder();
|
|
});
|
|
});
|
|
|
|
document.addEventListener('bookmarks_store-removed', (e) => {
|
|
var id = e.detail.id;
|
|
for (var child of this.icons.children) {
|
|
var icon = child.firstElementChild;
|
|
if (icon.bookmark && icon.bookmark.id === id) {
|
|
this.icons.removeChild(child, () => {
|
|
this.storeAppOrder();
|
|
this.refreshGridSize();
|
|
this.snapScrollPosition();
|
|
});
|
|
this.metadata.remove(id);
|
|
|
|
if (this.selectedIcon === icon) {
|
|
this.updateSelectedIcon(null);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
});
|
|
|
|
document.addEventListener('bookmarks_store-cleared', () => {
|
|
for (var child of this.icons.children) {
|
|
var icon = child.firstElementChild;
|
|
if (icon.bookmark) {
|
|
this.icons.removeChild(child);
|
|
}
|
|
}
|
|
this.storeAppOrder();
|
|
this.refreshGridSize();
|
|
this.snapScrollPosition();
|
|
});
|
|
}, (e) => {
|
|
console.error('Error initialising bookmarks', e);
|
|
return Promise.resolve();
|
|
}).then(() => {
|
|
return this.bookmarks.getAll().then((bookmarks) => {
|
|
for (var bookmark of bookmarks) {
|
|
this.addAppIcon(bookmark.data);
|
|
}
|
|
}, (e) => {
|
|
console.error('Error getting bookmarks', e);
|
|
return Promise.resolve();
|
|
});
|
|
})
|
|
]).then(() => {
|
|
// Add any applications that were missed and are in the startup metadata
|
|
var id;
|
|
var pendingIcons = this.pendingIcons;
|
|
this.pendingIcons = {};
|
|
for (id in pendingIcons) {
|
|
this.addAppIcon.apply(this, pendingIcons[id]);
|
|
}
|
|
|
|
// Make sure icons has been thawed
|
|
if (!this.visualLoadComplete) {
|
|
this.onVisualLoad();
|
|
}
|
|
|
|
// Remove unknown entries from the startup metadata
|
|
if (!this.settings.firstRun) {
|
|
for (var data of this.startupMetadata) {
|
|
console.log('Removing unknown app metadata entry', data.id);
|
|
this.metadata.remove(data.id).then(
|
|
() => {},
|
|
(e) => {
|
|
console.error('Error removing unknown app metadata entry', e);
|
|
});
|
|
}
|
|
}
|
|
this.startupMetadata = null;
|
|
|
|
// Update icons that we've added from the startup metadata in case their
|
|
// icons have updated or the icon size has changed.
|
|
for (var child of this.icons.children) {
|
|
var icon = child.firstElementChild;
|
|
this.refreshIcon(icon);
|
|
}
|
|
|
|
// Add any applications that aren't in the startup metadata
|
|
var newIcons = false;
|
|
pendingIcons = this.pendingIcons;
|
|
for (id in pendingIcons) {
|
|
this.addAppIcon.apply(this, pendingIcons[id]);
|
|
newIcons = true;
|
|
}
|
|
this.pendingIcons = null;
|
|
|
|
// Store app order of new icons
|
|
if (newIcons) {
|
|
this.storeAppOrder();
|
|
} else {
|
|
// Grid size is only refreshed in the non-startup path, so unless we
|
|
// added new icons post-startup, refresh here to make sure it's the
|
|
// correct size.
|
|
this.refreshGridSize();
|
|
}
|
|
|
|
// All asynchronous loading has finished
|
|
window.performance.mark('fullyLoaded');
|
|
});
|
|
}
|
|
|
|
Apps.prototype = {
|
|
get iconSize() {
|
|
// If this._iconSize is 0, let's refresh the value.
|
|
if (!this._iconSize) {
|
|
var children = this.icons.children;
|
|
for (var container of children) {
|
|
if (container.style.display !== 'none') {
|
|
this._iconSize = container.firstElementChild.size;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return this._iconSize;
|
|
},
|
|
|
|
toggleSmall: function(small) {
|
|
if (this.icons.classList.contains('small') === small) {
|
|
return;
|
|
}
|
|
|
|
this.icons.classList.toggle('small', small);
|
|
this.icons.synchronise();
|
|
this.refreshGridSize();
|
|
this.snapScrollPosition();
|
|
},
|
|
|
|
toggleScrollSnapping: function(scrollSnapping) {
|
|
if (this.scrollable.classList.contains('snapping') === scrollSnapping) {
|
|
return;
|
|
}
|
|
|
|
this.scrollable.classList.toggle('snapping', scrollSnapping);
|
|
this.snapScrollPosition();
|
|
},
|
|
|
|
onVisualLoad: function() {
|
|
this.visualLoadComplete = true;
|
|
this.icons.thaw();
|
|
this.icons.classList.remove('loading');
|
|
|
|
// App/bookmark loading happens asynchronously and we call this function
|
|
// when we've loaded as many apps as necessary to fill the screen.
|
|
window.performance.mark('visuallyLoaded');
|
|
window.performance.mark('contentInteractive');
|
|
},
|
|
|
|
addApp: function(app) {
|
|
var manifest = app.manifest || app.updateManifest;
|
|
if (!manifest) {
|
|
//console.log('Skipping app with no manifest', app);
|
|
return;
|
|
}
|
|
|
|
if (HIDDEN_ROLES.includes(manifest.role)) {
|
|
return;
|
|
}
|
|
|
|
// Do not add blacklisted apps
|
|
if (BLACKLIST.includes(app.origin)) {
|
|
return;
|
|
}
|
|
|
|
if (manifest.entry_points) {
|
|
for (var entryPoint in manifest.entry_points) {
|
|
this.addAppIcon(app, entryPoint);
|
|
}
|
|
} else {
|
|
this.addAppIcon(app);
|
|
}
|
|
},
|
|
|
|
addIconContainer: function(icon, entry) {
|
|
var container = document.createElement('div');
|
|
container.classList.add('icon-container');
|
|
container.order = -1;
|
|
container.appendChild(icon);
|
|
|
|
// Try to insert the container in the right order
|
|
if (entry !== -1 && this.startupMetadata[entry].order >= 0) {
|
|
container.order = this.startupMetadata[entry].order;
|
|
var children = this.icons.children;
|
|
for (var i = 0, iLen = children.length; i < iLen; i++) {
|
|
var child = children[i];
|
|
if (child.order !== -1 && child.order < container.order) {
|
|
continue;
|
|
}
|
|
this.icons.insertBefore(container, child);
|
|
if (this.startupMetadata === null) {
|
|
this.iconAdded(container);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!container.parentNode) {
|
|
this.icons.appendChild(container);
|
|
if (this.startupMetadata === null) {
|
|
this.iconAdded(container);
|
|
}
|
|
}
|
|
|
|
return container;
|
|
},
|
|
|
|
getIconId: function(appOrBookmark, entryPoint) {
|
|
if (appOrBookmark.id) {
|
|
return appOrBookmark.id;
|
|
} else {
|
|
return appOrBookmark.manifestURL + '/' + (entryPoint ? entryPoint : '');
|
|
}
|
|
},
|
|
|
|
addAppIcon: function(appOrBookmark, entryPoint) {
|
|
var id = this.getIconId(appOrBookmark, entryPoint);
|
|
var entry = -1;
|
|
|
|
if (this.startupMetadata !== null) {
|
|
entry = this.startupMetadata.findIndex(data => {
|
|
return data.id === id;
|
|
});
|
|
if (entry === -1) {
|
|
this.pendingIcons[id] = Array.slice(arguments);
|
|
return;
|
|
}
|
|
}
|
|
|
|
var icon = document.createElement('gaia-app-icon');
|
|
if (entryPoint) {
|
|
icon.entryPoint = entryPoint;
|
|
}
|
|
var container = this.addIconContainer(icon, entry);
|
|
|
|
if (appOrBookmark.id) {
|
|
icon.bookmark = appOrBookmark;
|
|
this.refreshIcon(icon);
|
|
} else {
|
|
icon.app = appOrBookmark;
|
|
|
|
// Hide/show the icon if the role changes to/from a hidden role
|
|
var handleRoleChange = function(app, container) {
|
|
var manifest = app.manifest || app.updateManifest;
|
|
var hidden = (manifest && manifest.role &&
|
|
HIDDEN_ROLES.includes(manifest.role));
|
|
container.style.display = hidden ? 'none' : '';
|
|
};
|
|
|
|
icon.app.addEventListener('downloadapplied',
|
|
function(app, container) {
|
|
handleRoleChange(app, container);
|
|
this.icons.synchronise();
|
|
}.bind(this, icon.app, container));
|
|
|
|
handleRoleChange(icon.app, container);
|
|
}
|
|
|
|
// Save the new icon if it gets refreshed.
|
|
this.iconsToRetry.push(id);
|
|
icon.addEventListener('icon-loaded', function(icon, id) {
|
|
if (icon.isUserSet) {
|
|
// If the icon was set manually, we don't cache it.
|
|
return;
|
|
}
|
|
|
|
icon.icon.then((blob) => {
|
|
// Remove icon from list of icons to retry when we go online
|
|
var retryIndex = this.iconsToRetry.indexOf(id);
|
|
if (retryIndex !== -1) {
|
|
this.iconsToRetry.splice(retryIndex, 1);
|
|
}
|
|
|
|
this.metadata.set([{ id: id, icon: blob }]).then(
|
|
() => {},
|
|
(e) => {
|
|
console.error('Error saving icon', e);
|
|
});
|
|
});
|
|
}.bind(this, icon, id));
|
|
|
|
// Refresh icon image and title
|
|
icon.size = this.iconSize ? this.iconSize : icon.size;
|
|
if (entry !== -1) {
|
|
// Load the cached icon and update name
|
|
icon.icon = this.startupMetadata[entry].icon;
|
|
this.startupMetadata.splice(entry, 1);
|
|
icon.updateName();
|
|
if (window.VulpesCompat && icon.app) icon.refresh(); // Replace legacy rasterized cache with original app assets.
|
|
} else {
|
|
// Start loading a new icon and update name
|
|
icon.refresh();
|
|
}
|
|
|
|
// Override default launch behaviour
|
|
icon.addEventListener('activated', e => {
|
|
e.preventDefault();
|
|
this.handleEvent({ type: 'activate',
|
|
detail: { target: e.target.parentNode },
|
|
preventDefault: () => {}});
|
|
});
|
|
},
|
|
|
|
refreshIcon: function(icon) {
|
|
icon.size = this.iconSize ? this.iconSize : icon.size;
|
|
if (icon.bookmark) {
|
|
IconsHelper.setElementIcon(icon, this.iconSize).then(() => {},
|
|
e => {
|
|
console.error('Error refreshing bookmark icon', e);
|
|
|
|
// Refreshing a bookmark icon will set the default icon image if
|
|
// no user icon has been set.
|
|
if (!icon.isUserSet) {
|
|
icon.refresh();
|
|
}
|
|
});
|
|
} else {
|
|
icon.refresh();
|
|
}
|
|
},
|
|
|
|
storeAppOrder: function() {
|
|
var storedOrders = [];
|
|
var children = this.icons.children;
|
|
for (var i = 0, iLen = children.length; i < iLen; i++) {
|
|
var appIcon = children[i].firstElementChild;
|
|
var id = this.getIconId(appIcon.app ? appIcon.app : appIcon.bookmark,
|
|
appIcon.entryPoint);
|
|
storedOrders.push({ id: id, order: i });
|
|
}
|
|
this.metadata.set(storedOrders).then(
|
|
() => {},
|
|
(e) => {
|
|
console.error('Error storing app order', e);
|
|
});
|
|
},
|
|
|
|
iconAdded: function(container) {
|
|
// Refresh the grid size if this child is visible
|
|
if (container.style.display === 'none') {
|
|
return;
|
|
}
|
|
|
|
this.refreshGridSize();
|
|
},
|
|
|
|
refreshGridSize: function() {
|
|
var children = this.icons.children;
|
|
var cols = this.settings.small ? 4 : 3;
|
|
|
|
var visibleChildren = 0;
|
|
var firstVisibleChild = -1;
|
|
for (var i = 0, iLen = children.length; i < iLen; i++) {
|
|
if (children[i].style.display !== 'none') {
|
|
visibleChildren ++;
|
|
if (firstVisibleChild === -1) {
|
|
firstVisibleChild = i;
|
|
this.iconsLeft = this.icons.getChildOffsetRect(children[i]).left;
|
|
} else if (visibleChildren === cols) {
|
|
this.iconsRight = this.icons.getChildOffsetRect(children[i]).right;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (visibleChildren < 1) {
|
|
// Reset these to default values when all children have been removed
|
|
this.pendingGridHeight = this.gridHeight = 0;
|
|
this.pageHeight = this.scrollable.clientHeight;
|
|
} else {
|
|
var iconHeight = Math.round(children[firstVisibleChild].offsetHeight);
|
|
var scrollHeight = this.scrollable.clientHeight;
|
|
var rowsPerPage = Math.floor(scrollHeight / iconHeight);
|
|
var pageHeight = rowsPerPage * iconHeight;
|
|
var gridHeight;
|
|
|
|
if (this.settings.scrollSnapping) {
|
|
gridHeight = (Math.ceil((iconHeight *
|
|
Math.ceil(visibleChildren / cols)) / pageHeight) *
|
|
pageHeight) + (scrollHeight - pageHeight);
|
|
} else {
|
|
gridHeight = (Math.ceil(visibleChildren / cols) + 1) * iconHeight;
|
|
}
|
|
|
|
this.pageHeight = pageHeight;
|
|
this.pendingGridHeight = gridHeight;
|
|
this.iconsPerPage = rowsPerPage * cols;
|
|
|
|
// When a full screen of apps is visible, we mark that as visual loading
|
|
// being complete.
|
|
if (!this.visualLoadComplete &&
|
|
Math.floor(visibleChildren / cols) * iconHeight >= scrollHeight) {
|
|
this.onVisualLoad();
|
|
}
|
|
}
|
|
|
|
// Reset scroll-snap points
|
|
this.scrollable.style.scrollSnapPointsY = `repeat(${this.pageHeight}px)`;
|
|
|
|
// Set page border background
|
|
this.icons.style.backgroundSize = '100% ' + (this.pageHeight * 2) + 'px';
|
|
|
|
// Make sure the grid is a multiple of the page size. If the size has
|
|
// shrunk we do this in a timeout so that the page scrolls has time
|
|
// to scroll into place before we shrink the container.
|
|
if (this.resizeTimeout !== null) {
|
|
clearTimeout(this.resizeTimeout);
|
|
}
|
|
var setGridHeight = () => {
|
|
this.resizeTimeout = null;
|
|
this.icons.style.height = gridHeight + 'px';
|
|
this.gridHeight = this.pendingGridHeight;
|
|
};
|
|
if (this.pendingGridHeight > this.gridHeight) {
|
|
setGridHeight();
|
|
} else if (this.pendingGridHeight !== this.gridHeight) {
|
|
this.resizeTimeout = setTimeout(setGridHeight, RESIZE_TIMEOUT);
|
|
}
|
|
},
|
|
|
|
snapScrollPosition: function(bias) {
|
|
bias = bias || 0;
|
|
var gridHeight = this.pendingGridHeight;
|
|
var currentScroll = this.scrollable.scrollTop;
|
|
var scrollHeight = this.scrollable.clientHeight;
|
|
|
|
var destination;
|
|
if (this.settings.scrollSnapping) {
|
|
destination = Math.min(gridHeight - scrollHeight,
|
|
Math.round(currentScroll / this.pageHeight + bias) * this.pageHeight);
|
|
} else {
|
|
destination = Math.min(gridHeight - scrollHeight,
|
|
currentScroll + (this.pageHeight * bias));
|
|
}
|
|
|
|
if (Math.abs(destination - currentScroll) > 1) {
|
|
this.scrollable.style.overflow = '';
|
|
this.scrollable.scrollTo(
|
|
{ left: 0, top: destination, behavior: 'smooth' });
|
|
|
|
if (this.autoScrollOverflowTimeout !== null) {
|
|
clearTimeout(this.autoScrollOverflowTimeout);
|
|
this.autoScrollOverflowTimeout = null;
|
|
}
|
|
|
|
if (this.dragging) {
|
|
document.body.classList.add('autoscroll');
|
|
this.autoScrollOverflowTimeout = setTimeout(() => {
|
|
this.autoScrollOverflowTimeout = null;
|
|
this.scrollable.style.overflow = 'hidden';
|
|
document.body.classList.remove('autoscroll');
|
|
this.scrollable.scrollTop = destination;
|
|
}, AUTOSCROLL_OVERFLOW_DELAY);
|
|
}
|
|
}
|
|
},
|
|
|
|
showActionDialog: function(dialog, args, callbacks) {
|
|
// XXX Working around gaia-components issue #8.
|
|
if (dialog.style.display !== 'none') {
|
|
return;
|
|
}
|
|
|
|
function executeCallback(dialog, callback) {
|
|
callback();
|
|
dialog.close();
|
|
}
|
|
|
|
var actions = dialog.getElementsByClassName('action');
|
|
for (var i = 0, iLen = Math.min(actions.length, callbacks.length);
|
|
i < iLen; i++) {
|
|
actions[i].onclick = executeCallback.bind(this, dialog, callbacks[i]);
|
|
}
|
|
if (args) {
|
|
dialog.querySelector('.body').setAttribute('data-l10n-args', args);
|
|
}
|
|
setTimeout(() => { dialog.open(); }, DIALOG_SHOW_TIMEOUT);
|
|
},
|
|
|
|
getChildIndex: function(child) {
|
|
// XXX Note, we're taking advantage of gaia-container using
|
|
// Array instead of HTMLCollection here.
|
|
return this.icons.children.indexOf(child);
|
|
},
|
|
|
|
removeSelectedIcon: function() {
|
|
if (!this.selectedIcon) {
|
|
return;
|
|
}
|
|
|
|
if (this.selectedIcon.app) {
|
|
if (!this.selectedIcon.app.removable) {
|
|
return;
|
|
}
|
|
navigator.mozApps.mgmt.uninstall(this.selectedIcon.app);
|
|
} else if (this.selectedIcon.bookmark) {
|
|
var remove = new MozActivity({
|
|
name: 'remove-bookmark',
|
|
data: { type: 'url', url: this.selectedIcon.bookmark.id }
|
|
});
|
|
|
|
// Re-enter edit mode because the activity will hide the document,
|
|
// which exits edit mode
|
|
var icon = this.selectedIcon;
|
|
remove.onsuccess = () => {
|
|
this.enterEditMode(null);
|
|
};
|
|
remove.onerror = () => {
|
|
this.enterEditMode(icon);
|
|
};
|
|
}
|
|
},
|
|
|
|
renameSelectedIcon: function() {
|
|
if (!this.selectedIcon || !this.selectedIcon.bookmark) {
|
|
return;
|
|
}
|
|
|
|
var rename = new MozActivity({
|
|
name: 'save-bookmark',
|
|
data: { type: 'url', url: this.selectedIcon.bookmark.id }
|
|
});
|
|
|
|
// Re-enter edit mode because the activity will hide the document,
|
|
// which exits edit mode
|
|
var icon = this.selectedIcon;
|
|
rename.onsuccess = rename.onerror = () => {
|
|
this.enterEditMode(icon);
|
|
};
|
|
},
|
|
|
|
iconIsEditable: function(icon) {
|
|
return (icon.bookmark || (icon.app && icon.app.removable)) ? true : false;
|
|
},
|
|
|
|
updateSelectedIcon: function(icon) {
|
|
if (this.selectedIcon === icon) {
|
|
return;
|
|
}
|
|
|
|
if (this.selectedIcon && (!icon || this.iconIsEditable(icon))) {
|
|
this.selectedIcon.classList.remove('selected');
|
|
this.selectedIcon.removeEventListener('touchstart', this);
|
|
this.selectedIcon = null;
|
|
}
|
|
|
|
var selectedRenameable = false;
|
|
var selectedRemovable = false;
|
|
|
|
if (icon) {
|
|
selectedRenameable = !!icon.bookmark;
|
|
selectedRemovable = selectedRenameable || icon.app.removable;
|
|
|
|
if (selectedRenameable || selectedRemovable) {
|
|
this.selectedIcon = icon;
|
|
icon.classList.add('selected');
|
|
icon.addEventListener('touchstart', this);
|
|
this.rename.classList.toggle('active', selectedRenameable);
|
|
this.remove.classList.toggle('active', selectedRemovable);
|
|
} else if (!icon.classList.contains('uneditable')) {
|
|
icon.classList.add('uneditable');
|
|
icon.addEventListener('animationend', function animEnd() {
|
|
icon.removeEventListener('animationend', animEnd);
|
|
icon.classList.remove('uneditable');
|
|
});
|
|
}
|
|
} else {
|
|
this.rename.classList.remove('active');
|
|
this.remove.classList.remove('active');
|
|
}
|
|
},
|
|
|
|
enterEditMode: function(icon) {
|
|
console.debug('Entering edit mode on ' + (icon ? icon.name : 'no icon'));
|
|
this.updateSelectedIcon(icon);
|
|
|
|
if (this.editMode) {
|
|
return;
|
|
}
|
|
|
|
this.editMode = true;
|
|
document.body.classList.add('edit-mode');
|
|
},
|
|
|
|
exitEditMode: function() {
|
|
if (!this.editMode) {
|
|
return;
|
|
}
|
|
console.debug('Exiting edit mode');
|
|
|
|
this.editMode = false;
|
|
|
|
document.body.classList.remove('edit-mode');
|
|
this.rename.classList.remove('active');
|
|
this.remove.classList.remove('active');
|
|
this.updateSelectedIcon(null);
|
|
},
|
|
|
|
handleEvent: function(e) {
|
|
var icon, child, id;
|
|
|
|
switch (e.type) {
|
|
// App launching
|
|
case 'activate':
|
|
e.preventDefault();
|
|
icon = e.detail.target.firstElementChild;
|
|
|
|
// If we're in edit mode, remap taps to selection
|
|
if (this.editMode) {
|
|
this.enterEditMode(icon);
|
|
return;
|
|
}
|
|
|
|
switch (icon.state) {
|
|
case 'unrecoverable':
|
|
navigator.mozApps.mgmt.uninstall(icon.app);
|
|
break;
|
|
|
|
case 'installing':
|
|
this.showActionDialog(this.cancelDownload,
|
|
JSON.stringify({ name: icon.name }),
|
|
[() => {
|
|
icon.app.cancelDownload();
|
|
}]);
|
|
break;
|
|
|
|
case 'error':
|
|
case 'paused':
|
|
this.showActionDialog(this.resumeDownload,
|
|
JSON.stringify({ name: icon.name }),
|
|
[() => {
|
|
icon.app.download();
|
|
}]);
|
|
break;
|
|
|
|
default:
|
|
icon.launch();
|
|
break;
|
|
}
|
|
break;
|
|
|
|
// Activate drag-and-drop immediately for selected icons
|
|
case 'touchstart':
|
|
this.icons.dragAndDropTimeout = 0;
|
|
break;
|
|
|
|
// Disable scrolling during dragging, and display bottom-bar
|
|
case 'drag-start':
|
|
console.debug('Drag-start on ' +
|
|
e.detail.target.firstElementChild.name);
|
|
this.dragging = true;
|
|
this.shouldEnterEditMode = true;
|
|
document.body.classList.add('dragging');
|
|
this.scrollable.style.overflow = 'hidden';
|
|
this.draggedIndex = this.getChildIndex(e.detail.target);
|
|
break;
|
|
|
|
case 'drag-finish':
|
|
console.debug('Drag-finish');
|
|
this.dragging = false;
|
|
document.body.classList.remove('dragging');
|
|
document.body.classList.remove('autoscroll');
|
|
this.scrollable.style.overflow = '';
|
|
|
|
if (this.autoScrollInterval !== null) {
|
|
clearInterval(this.autoScrollInterval);
|
|
this.autoScrollInterval = null;
|
|
}
|
|
|
|
if (this.autoScrollOverflowTimeout !== null) {
|
|
clearTimeout(this.autoScrollOverflowTimeout);
|
|
this.autoScrollOverflowTimeout = null;
|
|
}
|
|
|
|
if (this.hoverIcon) {
|
|
this.hoverIcon.classList.remove('hover-before', 'hover-after');
|
|
this.hoverIcon = null;
|
|
}
|
|
|
|
// Restore normal drag-and-drop after dragging selected icons
|
|
this.icons.dragAndDropTimeout = -1;
|
|
break;
|
|
|
|
// Handle app/site editing and dragging to the end of the icon grid.
|
|
case 'drag-end':
|
|
console.debug('Drag-end, target: ' + (e.detail.dropTarget ?
|
|
e.detail.dropTarget.firstElementChild.name : 'none'));
|
|
if (e.detail.dropTarget === null &&
|
|
e.detail.clientX >= this.iconsLeft &&
|
|
e.detail.clientX < this.iconsRight) {
|
|
// If the drop target is null, and the client coordinates are
|
|
// within the panel, we must be dropping over the start or end of
|
|
// the container.
|
|
e.preventDefault();
|
|
var bottom = e.detail.clientY < this.lastWindowHeight / 2;
|
|
console.debug('Reordering dragged icon to ' +
|
|
(bottom ? 'bottom' : 'top'));
|
|
this.icons.reorderChild(e.detail.target,
|
|
bottom ? this.icons.firstChild : null,
|
|
this.storeAppOrder.bind(this));
|
|
break;
|
|
}
|
|
|
|
if (e.detail.dropTarget === e.detail.target) {
|
|
icon = e.detail.target.firstElementChild;
|
|
if (this.editMode || this.shouldEnterEditMode) {
|
|
e.preventDefault();
|
|
this.enterEditMode(icon);
|
|
}
|
|
}
|
|
break;
|
|
|
|
// Save the app grid after rearrangement
|
|
case 'drag-rearrange':
|
|
console.debug('Drag rearrange');
|
|
this.storeAppOrder();
|
|
break;
|
|
|
|
// Handle app-uninstall bar highlight and auto-scroll
|
|
case 'drag-move':
|
|
var inAutoscroll = false;
|
|
|
|
if (e.detail.clientY > this.lastWindowHeight - AUTOSCROLL_DISTANCE) {
|
|
// User is dragging in the lower auto-scroll area
|
|
inAutoscroll = true;
|
|
if (this.autoScrollInterval === null) {
|
|
this.autoScrollInterval = setInterval(() => {
|
|
this.shouldEnterEditMode = false;
|
|
this.snapScrollPosition(1);
|
|
return true;
|
|
}, AUTOSCROLL_DELAY);
|
|
}
|
|
} else if (e.detail.clientY < AUTOSCROLL_DISTANCE) {
|
|
// User is dragging in the upper auto-scroll area
|
|
inAutoscroll = true;
|
|
if (this.autoScrollInterval === null) {
|
|
this.autoScrollInterval = setInterval(() => {
|
|
this.shouldEnterEditMode = false;
|
|
this.snapScrollPosition(-1);
|
|
return true;
|
|
}, AUTOSCROLL_DELAY);
|
|
}
|
|
} else {
|
|
// User is dragging in the grid, provide some visual feedback
|
|
var hoverIcon = this.icons.getChildFromPoint(e.detail.clientX,
|
|
e.detail.clientY);
|
|
if (this.hoverIcon !== hoverIcon) {
|
|
if (this.hoverIcon) {
|
|
this.shouldEnterEditMode = false;
|
|
this.hoverIcon.classList.remove('hover-before', 'hover-after');
|
|
}
|
|
this.hoverIcon = (hoverIcon !== e.detail.target) ? hoverIcon : null;
|
|
|
|
if (this.hoverIcon) {
|
|
var offset = this.draggedIndex -
|
|
this.getChildIndex(this.hoverIcon);
|
|
this.hoverIcon.classList.add((offset >= 0) ?
|
|
'hover-before' : 'hover-after');
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!inAutoscroll && this.autoScrollInterval !== null) {
|
|
clearInterval(this.autoScrollInterval);
|
|
this.autoScrollInterval = null;
|
|
}
|
|
break;
|
|
|
|
// Add apps installed after startup
|
|
case 'install':
|
|
// Check if the app already exists, and if so, update it.
|
|
// This happens when reinstalling an app via WebIDE.
|
|
var existing = false;
|
|
for (child of this.icons.children) {
|
|
icon = child.firstElementChild;
|
|
if (icon.app && icon.app.manifestURL === e.application.manifestURL) {
|
|
icon.app = e.application;
|
|
icon.refresh();
|
|
existing = true;
|
|
}
|
|
}
|
|
if (existing) {
|
|
return;
|
|
}
|
|
|
|
this.addApp(e.application);
|
|
this.storeAppOrder();
|
|
break;
|
|
|
|
// Remove apps uninstalled after startup
|
|
case 'uninstall':
|
|
var callback = () => {
|
|
this.storeAppOrder();
|
|
this.refreshGridSize();
|
|
this.snapScrollPosition();
|
|
};
|
|
|
|
for (child of this.icons.children) {
|
|
icon = child.firstElementChild;
|
|
if (icon.app && icon.app.manifestURL === e.application.manifestURL) {
|
|
id = this.getIconId(e.application, icon.entryPoint);
|
|
this.metadata.remove(id).then(() => {},
|
|
(e) => {
|
|
console.error('Error removing uninstalled app', e);
|
|
});
|
|
|
|
this.icons.removeChild(child, callback);
|
|
|
|
// We only want to store the app order once, so clear the callback
|
|
callback = null;
|
|
|
|
if (this.selectedIcon === icon) {
|
|
this.updateSelectedIcon(null);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'localized':
|
|
for (icon of this.icons.children) {
|
|
icon.firstElementChild.updateName();
|
|
}
|
|
this.icons.synchronise();
|
|
break;
|
|
|
|
case 'online':
|
|
for (var i = 0, iLen = this.iconsToRetry.length; i < iLen; i++) {
|
|
for (child of this.icons.children) {
|
|
icon = child.firstElementChild;
|
|
id = this.getIconId(icon.app ? icon.app : icon.bookmark,
|
|
icon.entryPoint);
|
|
if (id === this.iconsToRetry[i]) {
|
|
this.refreshIcon(icon);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'resize':
|
|
if (this.lastWindowWidth === window.innerWidth &&
|
|
this.lastWindowHeight === window.innerHeight) {
|
|
break;
|
|
}
|
|
|
|
this.lastWindowWidth = window.innerWidth;
|
|
this.lastWindowHeight = window.innerHeight;
|
|
|
|
// Reset icon-size
|
|
var oldIconSize = this.iconSize;
|
|
this._iconSize = 0;
|
|
|
|
// If the icon size has changed, refresh icons
|
|
if (oldIconSize !== this.iconSize) {
|
|
for (child of this.icons.children) {
|
|
icon = child.firstElementChild;
|
|
this.refreshIcon(icon);
|
|
}
|
|
}
|
|
|
|
// Re-synchronise icon position
|
|
this.icons.synchronise();
|
|
|
|
// Recalculate grid size/snap points
|
|
this.refreshGridSize();
|
|
this.snapScrollPosition();
|
|
break;
|
|
|
|
case 'settings-changed':
|
|
this.toggleSmall(this.settings.small);
|
|
this.toggleScrollSnapping(this.settings.scrollSnapping);
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
exports.Apps = Apps;
|
|
|
|
}(window));
|