diff --git a/chrome/js/database.js b/chrome/js/database.js index 9cf3620..c563bd6 100644 --- a/chrome/js/database.js +++ b/chrome/js/database.js @@ -66,7 +66,7 @@ const Database = { */ createApp: function(id, manifestUrl, documentUrl, manifest) { return new Promise((resolve, reject) => { - const transaction = this.db.transaction(["apps"], "readwrite"); + const transaction = this.db.transaction(['apps'], 'readwrite'); transaction.oncomplete = (event) => { console.log('successfully created app with id ' + id); @@ -97,6 +97,39 @@ const Database = { }); }, + /** + * Delete an app from the database. + * + * @param {string} id The ID of the app to delete. + */ + deleteApp: function(id) { + return new Promise((resolve, reject) => { + const transaction = this.db.transaction(['apps'], 'readwrite'); + + transaction.oncomplete = (event) => { + resolve(event); + }; + + transaction.onerror = (event) => { + console.error('Transaction error deleting app with id ' + id); + reject(event); + }; + + const objectStore = transaction.objectStore('apps'); + + const request = objectStore.delete(id); + + request.onsuccess = (event) => { + console.log('Successfully deleted app with id ' + id); + }; + + request.onerror = (event) => { + console.error('Error requesting deletion of app object with id ' + id); + reject(event); + }; + }); + }, + /** * List apps in database. * diff --git a/chrome/js/models/web-apps.js b/chrome/js/models/web-apps.js index 019f857..bb30e08 100644 --- a/chrome/js/models/web-apps.js +++ b/chrome/js/models/web-apps.js @@ -53,10 +53,28 @@ class WebApps { return webApp; } catch (error) { console.error('Error pinning app with id: ' + id); + console.error(error); throw new Error('PinAppFailed'); } } + /** + * Unpin the app with the given ID. + * + * @param {string} id The ID of the app to unpin. + */ + async unpin(id) { + try { + await this.db.deleteApp(id); + await this.refreshAppList(); + return; + } catch(error) { + console.error('Error unpinning app with id: ' + id); + console.error(error); + throw new Error('UnpinAppFailed'); + } + } + /** * Get the current list of pinned apps. * @@ -70,6 +88,7 @@ class WebApps { * Refresh the list of apps in memory. */ async refreshAppList() { + this.apps.clear(); let appRecords = new Map(); try { appRecords = await this.db.listApps(); diff --git a/chrome/js/views/components/browser-window.js b/chrome/js/views/components/browser-window.js index e9d0ffa..25fc6e7 100644 --- a/chrome/js/views/components/browser-window.js +++ b/chrome/js/views/components/browser-window.js @@ -198,6 +198,17 @@ class BrowserWindow extends HTMLElement { width: 100%; flex: 1; } + + site-info-menu { + position: fixed; + left: 9px; + top: 60px; + } + + :host([display-mode='standalone']) site-info-menu { + left: 4px; + top: 28px; + } @@ -262,16 +273,28 @@ class BrowserWindow extends HTMLElement { * * @param {string} name The name of the app. */ - setAppName(name) { + setApplicationName(name) { + this.applicationName = name; this.titleBarText.textContent = name; } + /** + * Set the src of the title bar icon. + * + * @param {string} url The URL of the icon to use. + */ + setApplicationIcon(url) { + this.applicationIcon = url; + this.titleBarIcon.src = url; + } + /** * Set the icon of the app, if any, to which the currently loaded web page belongs. * * @param {string} url The URL of the icon to load. */ - setAppIcon(url) { + set(url) { + this.applicationIcon = url; this.titleBarIcon.src = url; } @@ -295,6 +318,8 @@ class BrowserWindow extends HTMLElement { this.handleIPCMessage.bind(this)); this.webview.addEventListener('did-attach', this.handleWebviewReady.bind(this)); + this.titleBarIcon.addEventListener('click', + this.handleTitleBarIconClick.bind(this)); this.urlBarInput.addEventListener('focus', this.handleUrlBarFocus.bind(this)); this.urlBarInput.addEventListener('blur', @@ -338,10 +363,10 @@ class BrowserWindow extends HTMLElement { case 'display-mode': break; case 'application-name': - this.setAppName(newValue); + this.setApplicationName(newValue || ''); break; case 'application-icon': - this.setAppIcon(newValue); + this.setApplicationIcon(newValue || this.DEFAULT_FAVICON_URL); break; case 'src': if (newValue != this.currentUrl) { @@ -358,6 +383,30 @@ class BrowserWindow extends HTMLElement { this.webview.goBack(); } + /** + * Handle a click on the title bar icon. + * + * @param {Event} event The click event. + */ + handleTitleBarIconClick(event) { + let hostname; + try { + hostname = new URL(this.currentUrl).hostname; + } catch(error) { + hostname = ''; + } + const siteInfoMenu = new SiteInfoMenu( + this.applicationName, + hostname, + // TODO: Figure out how to get a higher resolution icon + this.applicationIcon, + true, + true); + this.shadowRoot.appendChild(siteInfoMenu); + siteInfoMenu.addEventListener('_unpinappbuttonclicked', + this.dispatchUnpinAppRequest.bind(this)); + } + /** * Handle a navigation to a new page. * @@ -669,6 +718,19 @@ class BrowserWindow extends HTMLElement { console.error('Failed to fetch or parse web app manifest: ' + error); }); } + + /** + * Dispatch a request to unpin the app the current page belongs to. + */ + dispatchUnpinAppRequest() { + const documentUrl = this.currentUrl; + this.dispatchEvent(new CustomEvent('_unpinapprequested', { + detail: { + documentUrl: documentUrl + }, + bubbles: true + })); + } } // Register custom element diff --git a/chrome/js/views/components/site-info-menu.js b/chrome/js/views/components/site-info-menu.js index c596965..94803bb 100644 --- a/chrome/js/views/components/site-info-menu.js +++ b/chrome/js/views/components/site-info-menu.js @@ -12,8 +12,9 @@ class SiteInfoMenu extends HTMLElement { * @param {string} hostname - Host name of website or web app. * @param {string} iconUrl - URL of app icon or site icon. * @param {boolean} isApp - True if web app manifest detected. + * @param {boolean} isPinned - True if app is pinned. */ - constructor(name, hostname, iconUrl, isApp) { + constructor(name, hostname, iconUrl, isApp, isPinned) { super(); this.attachShadow({ mode: 'open' }); @@ -32,14 +33,12 @@ class SiteInfoMenu extends HTMLElement { } .site-info { - left: 9px; - top: 60px; + position: fixed; width: 250px; display: block; background-color: #e5e5e5; border-radius: 5px; border: solid 1px #bfbfbf; - position: fixed; padding: 10px; margin: 7.5px 0; z-index: 2; @@ -115,15 +114,23 @@ class SiteInfoMenu extends HTMLElement { `; - let siteInfo; + let pinOrUnpin; + if(isPinned) { + this.isPinned = true; + pinOrUnpin = 'Unpin'; + } else { + this.isPinned = false; + pinOrUnpin = 'Pin'; + } + let siteInfo; if (isApp) { siteInfo = ` -

Pin App

+

${pinOrUnpin} App

${name} from ${hostname} - + `; } else { siteInfo = ` @@ -172,13 +179,19 @@ class SiteInfoMenu extends HTMLElement { } /** - * Handle a click on the pin app button. + * Handle a click on the pin/unpin app button. * * @param {Event} event - The click event. */ handlePinAppButtonClick(event) { - // Dispatch an event to tell the browser window the user wants to pin the current app, then self-destruct - this.dispatchEvent(new CustomEvent('_pinappbuttonclicked')); + if(this.isPinned) { + // Dispatch an event to tell the browser window the user wants to unpin the current app + this.dispatchEvent(new CustomEvent('_unpinappbuttonclicked')); + } else { + // Dispatch an event to tell the browser window the user wants to pin the current app + this.dispatchEvent(new CustomEvent('_pinappbuttonclicked')); + } + // Self-destruct this.remove(); } diff --git a/chrome/js/views/windows-view.js b/chrome/js/views/windows-view.js index 73dd3b8..14cfa93 100644 --- a/chrome/js/views/windows-view.js +++ b/chrome/js/views/windows-view.js @@ -33,6 +33,8 @@ const WindowsView = { this.handleCloseWindowButtonClicked.bind(this)); this.windowsElement.addEventListener('_pinapprequested', this.handlePinAppRequest.bind(this)); + this.windowsElement.addEventListener('_unpinapprequested', + this.handleUnpinAppRequest.bind(this)); this.windowsElement.addEventListener('_locationchanged', this.handleWindowLocationChange.bind(this)); @@ -209,6 +211,39 @@ const WindowsView = { }); }, +/** + * Handle a request to unpin an app. + * + * @param {CustomEvent} event An _unpinapprequested event containing document URL of the requesting page + */ +handleUnpinAppRequest: function(event) { + // Find the app which the provided document URL belongs to + const app = window.webApps.match(event.detail.documentUrl); + + if(!app) { + console.error('Found no app matching the provided document URL'); + window.dispatchEvent(new CustomEvent('_error', { detail: { error: 'Failed to unpin app'}})); + } + + // Delete the app + window.webApps.unpin(app.id).then(() => { + // Unpin all browser windows with a current URL within scope of the pinned app + this.windows.forEach((browserWindow, windowId, windowsMap) => { + const documentUrl = browserWindow.element.getUrl(); + if(app.isWithinScope(documentUrl)) { + // Unapply manifest and revert back to browser display mode + browserWindow.element.setAttribute('display-mode', 'browser'); + browserWindow.element.removeAttribute('application-name'); + browserWindow.element.removeAttribute('application-icon'); + // There may be another overlapping app but the manifest will get applied on the + // next navigation + } + }); + }).catch((error) => { + window.dispatchEvent(new CustomEvent('_error', { detail: { error: 'Failed to unpin app'}})); + }); +}, + /** * Handle a location change of a window. * @@ -228,10 +263,10 @@ const WindowsView = { browserWindow.element.setAttribute('application-name', app.name || app.short_name || ''); browserWindow.element.setAttribute('application-icon', app.getBestIconUrl(this.TITLE_BAR_APP_ICON_SIZE)); } else { - // Reset display mode, application name and application icon + // Unapply manifest and reset display mode to browser browserWindow.element.setAttribute('display-mode', 'browser'); - browserWindow.element.setAttribute('application-name', ''); - browserWindow.element.setAttribute('application-icon', this.DEFAULT_APP_ICON_URL); + browserWindow.element.removeAttribute('application-name'); + browserWindow.element.removeAttribute('application-icon'); } },