From 4bade901eccf6890a16b922fbd114bf42b6985bc Mon Sep 17 00:00:00 2001 From: Kugi Eusebio Date: Fri, 4 Dec 2020 02:54:15 +0800 Subject: [PATCH] download ux changes --- src/app/DownloadDelegate.qml | 204 ++++++++++++++++++--------- src/app/DownloadsDialog.qml | 196 +++++++++++++++++++++++++ src/app/DownloadsPage.qml | 35 +++-- src/app/FileUtils.js | 38 +++++ src/app/SimpleDownloadDelegate.qml | 164 +++++++++++++++++++++ src/app/webbrowser/Browser.qml | 66 ++++++++- src/app/webbrowser/Chrome.qml | 7 + src/app/webbrowser/NavigationBar.qml | 64 +++++++++ src/app/webcontainer/Chrome.qml | 70 ++++++++- src/app/webcontainer/WebApp.qml | 74 +++++++++- 10 files changed, 830 insertions(+), 88 deletions(-) create mode 100644 src/app/DownloadsDialog.qml create mode 100644 src/app/FileUtils.js create mode 100644 src/app/SimpleDownloadDelegate.qml diff --git a/src/app/DownloadDelegate.qml b/src/app/DownloadDelegate.qml index 2cf952102..6fe78b5ae 100644 --- a/src/app/DownloadDelegate.qml +++ b/src/app/DownloadDelegate.qml @@ -18,7 +18,9 @@ import QtQuick 2.4 import Ubuntu.Components 1.3 +import QtQuick.Layouts 1.3 import ".." +import "FileUtils.js" as FileUtils ListItem { id: downloadDelegate @@ -31,6 +33,7 @@ ListItem { property string downloadId property var download readonly property int progress: download ? 100 * (download.receivedBytes / download.totalBytes) : -1 + property real speed: 0 property bool paused: download.isPaused property alias incognito: incognitoIcon.visible @@ -40,31 +43,67 @@ ListItem { signal cancelled() height: visible ? layout.height : 0 + + Timer { + id: speedTimer + + property real prevBytes: 0 + + interval: 1000 + running: download && !paused? true : false + repeat: true + onTriggered: { + if (download) { + speed = download.receivedBytes - prevBytes + prevBytes = download.receivedBytes + } + } + } + + MimeData { + id: linkMimeData + + text: model ? model.url : "" + } SlotsLayout { id: layout - Item { + ColumnLayout { SlotsLayout.position: SlotsLayout.Leading - width: units.gu(3) - height: units.gu(3) - - Image { - id: thumbimage - asynchronous: true - anchors.fill: parent - fillMode: Image.PreserveAspectFit - sourceSize.width: width - sourceSize.height: height + spacing: units.gu(1) + + Item { + Layout.alignment: Qt.AlignHCenter + implicitWidth: units.gu(3) + implicitHeight: units.gu(3) + + Image { + id: thumbimage + asynchronous: true + anchors.fill: parent + fillMode: Image.PreserveAspectFit + sourceSize.width: width + sourceSize.height: height + } + + Image { + asynchronous: true + anchors.fill: parent + anchors.margins: units.gu(0.2) + source: "image://theme/%1".arg(downloadDelegate.icon || "save") + visible: thumbimage.status !== Image.Ready + cache: true + } } - Image { - asynchronous: true - anchors.fill: parent - anchors.margins: units.gu(0.2) - source: "image://theme/%1".arg(downloadDelegate.icon || "save") - visible: thumbimage.status !== Image.Ready - cache: true + Label { + Layout.alignment: Qt.AlignHCenter + visible: !progressBar.indeterminateProgress && incomplete + horizontalAlignment: Text.AlignHCenter + // TRANSLATORS: %1 is the percentage of the download completed so far + text: i18n.tr("%1%").arg(progressBar.progress) + opacity: paused ? 0.5 : 1 } } @@ -133,70 +172,55 @@ ListItem { } } - IndeterminateProgressBar { - id: progressBar + ColumnLayout { + visible: incomplete && !error.visible anchors { left: parent.left right: parent.right } - height: units.gu(0.5) - visible: incomplete && !error.visible - progress: downloadDelegate.progress - // Work around UDM bug #1450144 - indeterminateProgress: progress < 0 || progress > 100 - opacity: paused ? 0.5 : 1 - } - } - - Column { - SlotsLayout.position: SlotsLayout.Trailing - spacing: units.gu(1) - width: (incomplete && !error.visible) ? cancelButton.width : 0 - Button { - id: cancelButton - visible: incomplete && !error.visible - text: i18n.tr("Cancel") - onClicked: { - if (download) { - download.cancel() - cancelled() - } + IndeterminateProgressBar { + id: progressBar + Layout.fillWidth: true + implicitHeight: units.gu(0.5) + progress: downloadDelegate.progress + // Work around UDM bug #1450144 + indeterminateProgress: progress < 0 || progress > 100 + opacity: paused ? 0.5 : 1 } - } - Label { - visible: !progressBar.indeterminateProgress && incomplete && !error.visible - width: cancelButton.width - horizontalAlignment: Text.AlignHCenter - textSize: Label.Small - // TRANSLATORS: %1 is the percentage of the download completed so far - text: i18n.tr("%1%").arg(progressBar.progress) - opacity: paused ? 0.5 : 1 - } + RowLayout { + Layout.fillWidth: true + implicitHeight: units.gu(4) - Button { - visible: incomplete && ! paused && ! error.visible - text: i18n.tr("Pause") - width: cancelButton.width - onClicked: { - if (download) { - download.pause() + Label { + horizontalAlignment: Text.AlignHCenter + textSize: Label.Small + text: download ? FileUtils.formatBytes(download.receivedBytes) + " / " + FileUtils.formatBytes(download.totalBytes) : i18n.tr("Unknown") + opacity: paused ? 0.5 : 1 } - } - } - Button { - visible: incomplete && paused && ! error.visible - text: i18n.tr("Resume") - width: cancelButton.width - onClicked: { - if (download) { - download.resume() + Label { + horizontalAlignment: Text.AlignHCenter + textSize: Label.Small + // TRANSLATORS: %1 is the number of bytes i.e. 2bytes, 5KB, 1MB + text: "(" + i18n.tr("%1/s").arg(FileUtils.formatBytes(downloadDelegate.speed)) + ")" + opacity: paused ? 0.5 : 1 } } } } + Icon { + id: iconPauseResume + + implicitWidth: units.gu(4) + implicitHeight: implicitWidth + SlotsLayout.position: SlotsLayout.Trailing + asynchronous: true + name: paused ? "media-preview-start" : "media-preview-pause" + visible: incomplete && !error.visible + color: theme.palette.normal.overlayText + } } Icon { @@ -213,7 +237,8 @@ ListItem { name: "private-browsing" } - leadingActions: error.visible || !incomplete ? deleteActionList : null + leadingActions: deleteActionList + trailingActions: trailingActionList ListItemActions { id: deleteActionList @@ -222,10 +247,49 @@ ListItem { objectName: "leadingAction.delete" iconName: "delete" enabled: error.visible || !incomplete + visible: enabled + text: i18n.tr("Delete File") + onTriggered: { + removed() + } + }, + Action { + objectName: "leadingAction.remove" + iconName: "list-remove" + enabled: !incomplete && !error.visible + visible: enabled + text: i18n.tr("Remove from History") onTriggered: { - cancelled() + } + }, + Action { + objectName: "leadingAction.cancel" + iconName: "cancel" + enabled: incomplete && !error.visible + visible: enabled + text: i18n.tr("Cancel") + onTriggered: { + if (download) { + download.cancel() + cancelled() + } + } + } + ] + } + ListItemActions { + id: trailingActionList + actions: [ + Action { + objectName: "trailingAction.CopyLink" + iconName: "edit-copy" + enabled: model.url != "" ? true : false + visible: enabled + text: i18n.tr("Copy Download Link") + onTriggered: { + Clipboard.push(linkMimeData) } } ] diff --git a/src/app/DownloadsDialog.qml b/src/app/DownloadsDialog.qml new file mode 100644 index 000000000..9cbec6d44 --- /dev/null +++ b/src/app/DownloadsDialog.qml @@ -0,0 +1,196 @@ +/* + * Copyright 2015-2016 Canonical Ltd. + * + * This file is part of morph-browser. + * + * morph-browser is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * morph-browser is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import QtQuick 2.9 +import QtWebEngine 1.10 +import Ubuntu.Components 1.3 +import Ubuntu.Components.Popups 1.3 +import QtQuick.Layouts 1.3 +import Ubuntu.Content 1.3 +import webbrowsercommon.private 0.1 + +import "MimeTypeMapper.js" as MimeTypeMapper +import "FileUtils.js" as FileUtils + +Popover { + id: downloadsDialog + + property var downloadsList: [] + property bool isEmpty: downloadsListView.count === 0 + readonly property real controlsHeight: (downloadsDialogColumn.spacing * (downloadsDialogColumn.children.length - 1)) + + (downloadsDialogColumn.anchors.margins * 2) + buttonsBar.height + titleLabel.height + property real maximumWidth: units.gu(60) + property real preferredWidth: browser.width - units.gu(6) + + property real maximumHeight: browser.height - units.gu(6) + property real preferredHeight: downloadsDialogColumn.height + units.gu(2) + + signal showDownloadsPage() + signal preview(string url) + + contentHeight: preferredHeight > maximumHeight ? maximumHeight : preferredHeight + contentWidth: preferredWidth > maximumWidth ? maximumWidth : preferredWidth + + grabDismissAreaEvents: true + + ColumnLayout { + id: downloadsDialogColumn + + anchors { + top: parent.top + left: parent.left + right: parent.right + margins: units.gu(1) + } + + spacing: units.gu(2) + + Label { + id: titleLabel + + Layout.fillWidth: true + font.bold: true + text: i18n.tr("Recent Downloads") + } + + Rectangle { + id: emptyLabel + + visible: isEmpty + color: "transparent" + + Layout.preferredHeight: units.gu(10) + Layout.minimumHeight: label.height + Layout.fillWidth: true + + Label { + id: label + text: i18n.tr("No Recent Downloads") + anchors.centerIn: parent + } + } + + ListView { + id: downloadsListView + + visible: !isEmpty + Layout.fillWidth: true + Layout.preferredHeight: downloadsListView.count * units.gu(7) + Layout.maximumHeight: browser.height - controlsHeight - units.gu(7) + Layout.minimumHeight: units.gu(7) + clip: true + + model: downloadsList + + property int selectedIndex: -1 + + delegate: SimpleDownloadDelegate { + download: modelData + title.text: FileUtils.getFilename(modelData.path) + subtitle.text: if (cancelled) { + i18n.tr("Cancelled") + } else { + // TRANSLATORS: %1 is the percentage of the download completed so far + (error ? modelData.interruptReasonString : (incomplete ? i18n.tr("%1%").arg(progress) : i18n.tr("Completed"))) + + " - " + FileUtils.formatBytes(download.receivedBytes) + } + + image: !incomplete && thumbnailLoader.status == Loader.Ready + && (modelData.mimeType.indexOf("image") === 0 + || modelData.mimeType.indexOf("video") === 0) + ? "image://thumbnailer/file://" + modelData.path : "" + icon: MimeDatabase.iconForMimetype(modelData.mimeType) + + onClicked: { + /* TODO: Enable once content picker in a popover is merged */ + /*if (!incomplete && !error) { + var properties = {"path": download.path, "contentType": MimeTypeMapper.mimeTypeToContentType(download.mimeType), "mimeType": download.mimeType, "downloadUrl": download.url} + var exportDialog = PopupUtils.open(Qt.resolvedUrl("ContentExportDialog.qml"), downloadsDialog.parent, properties) + exportDialog.preview.connect(downloadsDialog.preview) + } else {*/ + if (download) { + if (paused) { + download.resume() + } else { + download.pause() + } + } + //} + } + + onRemove: downloadsListView.removeItem(index) + onCancel: DownloadsModel.cancelDownload(download.id) + onDeleted: { + if (!incomplete) { + DownloadsModel.deleteDownload(download.path) + } + } + } + + Keys.onDeletePressed: { + currentItem.removeItem(currentItem.download) + } + + function removeItem(index) { + downloadsList.splice(index, 1); + model = downloadsList + forceLayout() + } + + function clear() { + downloadsList.splice(0, downloadsList.length); + model = downloadsList + forceLayout() + } + } + + Item { + id: buttonsBar + + Layout.fillWidth: true + implicitHeight: clearButton.height + + Button { + id: clearButton + visible: !isEmpty + objectName: "downloadsDialog.clearButton" + text: i18n.tr("Clear") + onClicked: { + downloadsListView.clear() + } + } + + Button { + id: viewButton + objectName: "downloadsDialog.viewButton" + anchors.right: parent.right + text: i18n.tr("View All") + color: theme.palette.normal.activity + onClicked: { + showDownloadsPage() + downloadsDialog.destroy() + } + } + } + } + + Loader { + id: thumbnailLoader + source: "Thumbnailer.qml" + } +} diff --git a/src/app/DownloadsPage.qml b/src/app/DownloadsPage.qml index 900d8cfe7..b749bbd9a 100644 --- a/src/app/DownloadsPage.qml +++ b/src/app/DownloadsPage.qml @@ -16,14 +16,15 @@ * along with this program. If not, see . */ -import QtQuick 2.4 -import QtWebEngine 1.5 +import QtQuick 2.9 +import QtWebEngine 1.10 import Ubuntu.Components 1.3 +import Ubuntu.Components.Popups 1.3 import Ubuntu.Content 1.3 import webbrowsercommon.private 0.1 import "MimeTypeMapper.js" as MimeTypeMapper -import "UrlUtils.js" as UrlUtils +import "FileUtils.js" as FileUtils BrowserPage { id: downloadsItem @@ -117,7 +118,7 @@ BrowserPage { }, Action { iconName: "edit" - visible: !selectMode && !pickingMode && !exportPeerPicker.visible + visible: !selectMode && !pickingMode enabled: downloadsListView.count > 0 onTriggered: { selectMode = true @@ -207,9 +208,11 @@ BrowserPage { } delegate: DownloadDelegate { + id: downloadDelegate + download: ActiveDownloadsSingleton.currentDownloads[model.downloadId] downloadId: model.downloadId - title: getDisplayPath(model.path) + title: FileUtils.getFilename(model.path) url: model.url image: model.complete && thumbnailLoader.status == Loader.Ready && (model.mimetype.indexOf("image") === 0 @@ -238,12 +241,22 @@ BrowserPage { } onClicked: { - if (model.complete && !selectMode) { - exportPeerPicker.contentType = MimeTypeMapper.mimeTypeToContentType(model.mimetype); - exportPeerPicker.visible = true; - exportPeerPicker.path = model.path; - exportPeerPicker.mimeType = model.mimetype; - exportPeerPicker.downloadUrl = model.url; + if (!selectMode) { + if (model.complete) { + exportPeerPicker.contentType = MimeTypeMapper.mimeTypeToContentType(model.mimetype); + exportPeerPicker.visible = true; + exportPeerPicker.path = model.path; + exportPeerPicker.mimeType = model.mimetype; + exportPeerPicker.downloadUrl = model.url; + } else { + if (download) { + if (paused) { + download.resume() + } else { + download.pause() + } + } + } } } diff --git a/src/app/FileUtils.js b/src/app/FileUtils.js new file mode 100644 index 000000000..bea44d930 --- /dev/null +++ b/src/app/FileUtils.js @@ -0,0 +1,38 @@ +/* + * Copyright 2014 Canonical Ltd. + * + * This file is part of morph-browser. + * + * morph-browser is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * morph-browser is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ +/*jslint node: true */ +.pragma library +'use strict'; + +function formatBytes(bytes, decimals) { + decimals = decimals ? decimals : 2 + if (bytes === 0) return '0 B'; + + var k = 1000; + var dm = decimals < 0 ? 0 : decimals; + var sizes = ["B", "KB", "MB", "GB", "TB"]; + + var i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; +} + +function getFilename(path) { + return path.replace(/^.*[\\\/]/, '') +} + diff --git a/src/app/SimpleDownloadDelegate.qml b/src/app/SimpleDownloadDelegate.qml new file mode 100644 index 000000000..5155e925d --- /dev/null +++ b/src/app/SimpleDownloadDelegate.qml @@ -0,0 +1,164 @@ +/* + * Copyright 2015-2016 Canonical Ltd. + * + * This file is part of morph-browser. + * + * morph-browser is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * morph-browser is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import QtQuick 2.9 +import QtWebEngine 1.10 +import Ubuntu.Components 1.3 + +ListItem { + id: simpleDownloadDelegate + + property var download + property alias title: listItemlayout.title + property alias subtitle: listItemlayout.subtitle + property string icon + property alias image: thumbimage.source + + readonly property bool incomplete: !download.isFinished + readonly property bool error: download.interruptReason > WebEngineDownloadItem.NoReason + readonly property bool cancelled: download.interruptReason == WebEngineDownloadItem.UserCanceled + readonly property bool paused: download.isPaused + readonly property int progress: download ? 100 * (download.receivedBytes / download.totalBytes) : -1 + + signal cancel() + signal remove() + signal deleted() + + divider.visible: false + + MimeData { + id: linkMimeData + + text: download.url + } + + ListItemLayout { + id: listItemlayout + + subtitle.opacity: paused && !cancelled && !error ? 0.5 : 1 + + Item { + + SlotsLayout.position: SlotsLayout.Leading + implicitWidth: units.gu(3) + implicitHeight: implicitWidth + + Image { + id: thumbimage + + asynchronous: true + anchors.fill: parent + fillMode: Image.PreserveAspectFit + sourceSize.width: width + sourceSize.height: height + } + + Image { + asynchronous: true + anchors.fill: parent + anchors.margins: units.gu(0.2) + source: "image://theme/%1".arg(simpleDownloadDelegate.icon || "save") + visible: thumbimage.status !== Image.Ready + cache: true + } + } + + Icon { + id: iconError + + implicitWidth: units.gu(4) + implicitHeight: implicitWidth + SlotsLayout.position: SlotsLayout.Last + asynchronous: true + name: "dialog-warning-symbolic" + visible: error && !cancelled + color: theme.palette.normal.negative + } + + Icon { + id: iconPauseResume + + implicitWidth: units.gu(4) + implicitHeight: implicitWidth + SlotsLayout.position: SlotsLayout.Trailing + asynchronous: true + name: paused ? "media-preview-start" : "media-preview-pause" + visible: incomplete && !error + color: theme.palette.normal.backgroundText + } + } + + leadingActions: deleteActionList + trailingActions: trailingActionList + + ListItemActions { + id: deleteActionList + actions: [ + Action { + objectName: "leadingAction.delete" + iconName: "delete" + enabled: error || !incomplete + visible: enabled + text: i18n.tr("Delete File") + onTriggered: { + deleted() + remove() + } + }, + Action { + objectName: "leadingAction.remove" + iconName: "list-remove" + enabled: !incomplete && !error + visible: enabled + text: i18n.tr("Remove from List") + onTriggered: { + remove() + } + }, + Action { + objectName: "leadingAction.cancel" + iconName: "cancel" + text: i18n.tr("Cancel") + enabled: incomplete && !error + visible: enabled + onTriggered: { + if (download) { + download.cancel() + cancel() + } + } + } + ] + } + + ListItemActions { + id: trailingActionList + actions: [ + Action { + objectName: "trailingAction.CopyLink" + iconName: "edit-copy" + enabled: download.url != "" ? true : false + visible: enabled + text: i18n.tr("Copy Download Link") + onTriggered: { + Clipboard.push(linkMimeData) + } + } + ] + } +} diff --git a/src/app/webbrowser/Browser.qml b/src/app/webbrowser/Browser.qml index 26fa7fc42..3e475aa47 100644 --- a/src/app/webbrowser/Browser.qml +++ b/src/app/webbrowser/Browser.qml @@ -25,6 +25,8 @@ import Qt.labs.settings 1.0 import Morph.Web 0.1 import Ubuntu.Components 1.3 import Ubuntu.Components.Popups 1.3 +import Ubuntu.Content 1.3 +import QtQuick.Controls 2.2 as QQC2 import webbrowserapp.private 0.1 import webbrowsercommon.private 0.1 import "../actions" as Actions @@ -707,6 +709,9 @@ Common.BrowserView { // QtWebEngine icons are provided as e.g. image://favicon/https://duckduckgo.com/favicon.ico else internal.addBookmark(tab.url, tab.title, (UrlUtils.schemeIs(tab.icon, "image") && UrlUtils.hostIs(tab.icon, "favicon")) ? tab.icon.toString().substring(("image://favicon/").length) : tab.icon) } + onToggleDownloads: { + internal.showDownloadsDialog() + } onWebviewChanged: bookmarked = isCurrentUrlBookmarked() Connections { target: chrome.tab @@ -1530,6 +1535,57 @@ Common.BrowserView { internal.currentBookmarkOptionsDialog = PopupUtils.open(Qt.resolvedUrl("BookmarkOptions.qml"), location, properties) } + + property var recentDownloads: [] + property var currentDownloadsDialog: null + function showDownloadsDialog(caller) { + if (!internal.currentDownloadsDialog) { + chrome.downloadNotify = false + if (caller === undefined) caller = chrome.downloadsButtonPlaceHolder + var properties = {"downloadsList": recentDownloads} + + internal.currentDownloadsDialog = PopupUtils.open(Qt.resolvedUrl("../DownloadsDialog.qml"), + caller, properties) + } + } + + function addNewDownload(download) { + recentDownloads.unshift(download) + chrome.showDownloadButton = Qt.binding( + function(){ + if (browser.wide) { + return true; + } else { + if (internal.currentDownloadsDialog) { + if (internal.currentDownloadsDialog.isEmpty) { + return false; + } else { + return true; + } + } else { + if (recentDownloads.length > 0) { + return true; + } else { + return false; + } + } + } + }) + if (internal.currentDownloadsDialog) { + internal.currentDownloadsDialog.downloadsList = recentDownloads + } + } + } + + Connections { + target: internal.currentDownloadsDialog + + onShowDownloadsPage: showDownloadsPage() + + onPreview: { + PopupUtils.close(internal.currentDownloadsDialog); + currentWebview.url = url; + } } // Work around https://launchpad.net/bugs/1502675 by delaying the switch to @@ -1798,7 +1854,9 @@ Common.BrowserView { console.log("adding download with id " + downloadIdDataBase) Common.ActiveDownloadsSingleton.currentDownloads[downloadIdDataBase] = download DownloadsModel.add(downloadIdDataBase, (download.url.toString().indexOf("file://%1/pdf_tmp".arg(cacheLocation)) === 0) ? "" : download.url, download.path, download.mimeType, incognito) - downloadsViewLoader.active = true + + internal.addNewDownload(download) + internal.showDownloadsDialog() } function setDownloadComplete(download) { @@ -1819,6 +1877,10 @@ Common.BrowserView { { DownloadsModel.setError(downloadIdDataBase, download.interruptReasonString) } + + if (!internal.currentDownloadsDialog) { + chrome.downloadNotify = true + } } Connections { @@ -1841,14 +1903,12 @@ Common.BrowserView { console.log("a download was requested with path %1".arg(download.path)) download.accept(); - browser.showDownloadsPage(); browser.startDownload(download); } onDownloadFinished: { console.log(incognito? "download finished" : "a download was finished with path %1.".arg(download.path)); - browser.showDownloadsPage(); browser.setDownloadComplete(download); // delete pdf in cache / close the tab diff --git a/src/app/webbrowser/Chrome.qml b/src/app/webbrowser/Chrome.qml index 97c6136cb..7da7b6f6a 100644 --- a/src/app/webbrowser/Chrome.qml +++ b/src/app/webbrowser/Chrome.qml @@ -32,6 +32,9 @@ ChromeBase { property alias bookmarked: navigationBar.bookmarked signal closeTabRequested() signal toggleBookmark() + signal toggleDownloads() + property bool showDownloadButton: false + property bool downloadNotify: false property alias drawerActions: navigationBar.drawerActions property alias drawerOpen: navigationBar.drawerOpen property alias requestedUrl: navigationBar.requestedUrl @@ -45,6 +48,7 @@ ChromeBase { property alias showFaviconInAddressBar: navigationBar.showFaviconInAddressBar property alias availableHeight: navigationBar.availableHeight readonly property alias bookmarkTogglePlaceHolder: navigationBar.bookmarkTogglePlaceHolder + readonly property alias downloadsButtonPlaceHolder: navigationBar.downloadsButtonPlaceHolder property bool touchEnabled: true readonly property real tabsBarHeight: tabsBar.height + tabsBar.anchors.topMargin + content.anchors.topMargin property BrowserWindow thisWindow @@ -112,6 +116,8 @@ ChromeBase { loading: chrome.loading fgColor: theme.palette.normal.backgroundText iconColor: (incognito && !showTabsBar) ? theme.palette.normal.baseText : fgColor + showDownloadButton: chrome.showDownloadButton + downloadNotify: chrome.downloadNotify focus: true @@ -124,6 +130,7 @@ ChromeBase { onCloseTabRequested: chrome.closeTabRequested() onToggleBookmark: chrome.toggleBookmark() + onToggleDownloads: chrome.toggleDownloads() } } diff --git a/src/app/webbrowser/NavigationBar.qml b/src/app/webbrowser/NavigationBar.qml index 7b31d4000..e9ae04fb7 100644 --- a/src/app/webbrowser/NavigationBar.qml +++ b/src/app/webbrowser/NavigationBar.qml @@ -31,6 +31,9 @@ FocusScope { property alias bookmarked: addressbar.bookmarked signal closeTabRequested() signal toggleBookmark() + signal toggleDownloads() + property bool showDownloadButton: false + property bool downloadNotify: false property list drawerActions readonly property bool drawerOpen: internal.openDrawer property alias requestedUrl: addressbar.requestedUrl @@ -42,6 +45,7 @@ FocusScope { property alias incognito: addressbar.incognito property alias showFaviconInAddressBar: addressbar.showFavicon readonly property alias bookmarkTogglePlaceHolder: addressbar.bookmarkTogglePlaceHolder + readonly property alias downloadsButtonPlaceHolder: downloadsButton property color fgColor: theme.palette.normal.baseText property color iconColor: theme.palette.normal.baseText property real availableHeight @@ -237,6 +241,66 @@ FocusScope { onTriggered: closeTabRequested() } + + ChromeButton { + id: downloadsButton + objectName: "downloadsButton" + + visible: showDownloadButton && !tabListMode + iconName: "save" + iconSize: 0.5 * height + iconColor: downloadNotify ? theme.palette.normal.focus : root.iconColor + + height: root.height + width: height * 0.8 + + anchors.verticalCenter: parent.verticalCenter + + Connections { + target: root + + onDownloadNotifyChanged: { + if (downloadNotify) { + shakeAnimation.start() + } + } + } + + Behavior on iconColor { + ColorAnimation { duration: UbuntuAnimation.BriskDuration } + } + + SequentialAnimation { + id: shakeAnimation + + loops: 4 + + RotationAnimation { + target: downloadsButton + direction: RotationAnimation.Counterclockwise + to: 350 + duration: 50 + } + + RotationAnimation { + target: downloadsButton + direction: RotationAnimation.Clockwise + to: 10 + duration: 50 + } + + RotationAnimation { + target: downloadsButton + direction: RotationAnimation.Counterclockwise + to: 0 + duration: 50 + } + } + + onTriggered: { + toggleDownloads() + } + } ChromeButton { id: drawerButton diff --git a/src/app/webcontainer/Chrome.qml b/src/app/webcontainer/Chrome.qml index 83100a697..07d2ddc7a 100644 --- a/src/app/webcontainer/Chrome.qml +++ b/src/app/webcontainer/Chrome.qml @@ -26,6 +26,10 @@ ChromeBase { property var webview: null property bool navigationButtonsVisible: false property bool accountSwitcher: false + signal toggleDownloads() + property bool showDownloadButton: false + property bool downloadNotify: false + readonly property alias downloadsButtonPlaceHolder: downloadsButton loading: webview && webview.loading && webview.loadProgress !== 100 loadProgress: loading ? webview.loadProgress : 0 @@ -39,6 +43,8 @@ ChromeBase { reloadButton.iconColor = color; settingsButton.iconColor = color; accountsButton.iconColor = color; + + downloadsButton.iconColor = Qt.binding(function(){ return downloadNotify ? theme.palette.normal.focus : color}) } signal chooseAccount() @@ -141,7 +147,7 @@ ChromeBase { width: visible ? height : 0 anchors { - right: settingsButton.left + right: downloadsButton.left verticalCenter: parent.verticalCenter } @@ -149,6 +155,68 @@ ChromeBase { onTriggered: chrome.webview.reload() } + ChromeButton { + id: downloadsButton + objectName: "downloadsButton" + + visible: chrome.navigationButtonsVisible && showDownloadButton + iconName: "save" + iconSize: 0.6 * height + + height: parent.height + width: visible ? height : 0 + + anchors { + right: settingsButton.left + verticalCenter: parent.verticalCenter + } + + Connections { + target: root + + onDownloadNotifyChanged: { + if (downloadNotify) { + shakeAnimation.start() + } + } + } + + Behavior on iconColor { + ColorAnimation { duration: UbuntuAnimation.BriskDuration } + } + + SequentialAnimation { + id: shakeAnimation + + loops: 4 + + RotationAnimation { + target: downloadsButton + direction: RotationAnimation.Counterclockwise + to: 350 + duration: 50 + } + + RotationAnimation { + target: downloadsButton + direction: RotationAnimation.Clockwise + to: 10 + duration: 50 + } + + RotationAnimation { + target: downloadsButton + direction: RotationAnimation.Counterclockwise + to: 0 + duration: 50 + } + } + + onTriggered: { + toggleDownloads() + } + } + ChromeButton { id: settingsButton objectName: "settingsButton" diff --git a/src/app/webcontainer/WebApp.qml b/src/app/webcontainer/WebApp.qml index 0dfa792a8..781be0b41 100644 --- a/src/app/webcontainer/WebApp.qml +++ b/src/app/webcontainer/WebApp.qml @@ -22,6 +22,7 @@ import Qt.labs.settings 1.0 import webbrowsercommon.private 0.1 import Morph.Web 0.1 import Ubuntu.Components 1.3 +import Ubuntu.Components.Popups 1.3 import Ubuntu.Unity.Action 1.1 as UnityActions import Ubuntu.UnityWebApps 0.1 as UnityWebApps import "../actions" as Actions @@ -61,6 +62,9 @@ Common.BrowserView { property bool chromeVisible: false readonly property bool chromeless: !chromeVisible && !backForwardButtonsVisible && !accountSwitcher readonly property real themeColorTextContrastFactor: 3.0 + + property var recentDownloads: [] + property var currentDownloadsDialog: null signal chooseAccount() @@ -176,7 +180,14 @@ Common.BrowserView { console.log("adding download with id " + downloadIdDataBase) Common.ActiveDownloadsSingleton.currentDownloads[downloadIdDataBase] = download DownloadsModel.add(downloadIdDataBase, download.url, download.path, download.mimeType, false) - downloadsViewLoader.active = true + + addNewDownload(download) + + if (webapp.chromeless) { + showDownloadsDialog(anchorItem) + } else { + showDownloadsDialog() + } } function setDownloadComplete(download) { @@ -197,6 +208,62 @@ Common.BrowserView { { DownloadsModel.setError(downloadIdDataBase, download.interruptReasonString) } + + if (!currentDownloadsDialog && chromeLoader.item) { + if (!webapp.chromeless) { + chromeLoader.item.downloadNotify = true + + if (!chromeLoader.item.navigationButtonsVisible) { + showDownloadsDialog() + } + } else { + showDownloadsDialog(anchorItem) + } + } + } + + function showDownloadsDialog(caller) { + if (!currentDownloadsDialog && chromeLoader.item) { + if (!webapp.chromeless) { + chromeLoader.item.downloadNotify = false + if (caller === undefined) caller = chromeLoader.item.downloadsButtonPlaceHolder + } else { + if (caller === undefined) caller = webapp + } + var properties = {"downloadsList": recentDownloads} + currentDownloadsDialog = PopupUtils.open(Qt.resolvedUrl("../DownloadsDialog.qml"), + caller, properties) + } + } + + function addNewDownload(download) { + recentDownloads.unshift(download) + if (chromeLoader.item && !webapp.chromeless) { + chromeLoader.item.showDownloadButton = true + } + if (currentDownloadsDialog) { + currentDownloadsDialog.downloadsList = recentDownloads + } + } + + Connections { + target: currentDownloadsDialog + onShowDownloadsPage: showDownloadsPage() + onPreview: { + PopupUtils.close(currentDownloadsDialog); + webapp.currentWebview.url = url; + } + } + + /* Only used for anchoring the downloads dialog to the top when chromeless */ + Item { + id: anchorItem + anchors { + top: parent.top + right: parent.right + } + height: units.gu(2) + width: height } Item { @@ -307,6 +374,9 @@ Common.BrowserView { y: webapp.currentWebview ? containerWebView.currentWebview.locationBarController.offset : 0 onChooseAccount: webapp.chooseAccount() + onToggleDownloads: { + webapp.showDownloadsDialog() + } } } @@ -435,14 +505,12 @@ Common.BrowserView { console.log("a download was requested with path %1".arg(download.path)) download.accept(); - webapp.showDownloadsPage(); webapp.startDownload(download); } onDownloadFinished: { console.log("a download was finished with path %1.".arg(download.path)) - webapp.showDownloadsPage() webapp.setDownloadComplete(download) } }