VulpesOS/gaia/apps/homescreen/js/apps.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

1128 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;
}
// 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));