diff --git a/src/app/DownloadDelegate.qml b/src/app/DownloadDelegate.qml
index f05b00b65..9a19b2cca 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: {
-
removed()
+ }
+ },
+ 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..20875e22c 100644
--- a/src/app/DownloadsPage.qml
+++ b/src/app/DownloadsPage.qml
@@ -16,14 +16,16 @@
* 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 +119,7 @@ BrowserPage {
},
Action {
iconName: "edit"
- visible: !selectMode && !pickingMode && !exportPeerPicker.visible
+ visible: !selectMode && !pickingMode
enabled: downloadsListView.count > 0
onTriggered: {
selectMode = true
@@ -207,9 +209,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 +242,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..4f62c9d1e
--- /dev/null
+++ b/src/app/SimpleDownloadDelegate.qml
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2021 UBports Foundation
+ *
+ * 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 f921e82e5..366726f24 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
@@ -737,6 +739,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
@@ -1567,6 +1572,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
@@ -1835,7 +1891,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) {
@@ -1856,6 +1914,10 @@ Common.BrowserView {
{
DownloadsModel.setError(downloadIdDataBase, download.interruptReasonString)
}
+
+ if (!internal.currentDownloadsDialog) {
+ chrome.downloadNotify = true
+ }
}
Connections {
@@ -1878,14 +1940,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 9ad0be601..8db1f7006 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 a53484786..627941c18 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 9f7e0ae60..3235af014 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 "../actions" as Actions
import ".." as Common
@@ -60,6 +61,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()
@@ -175,7 +179,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) {
@@ -196,6 +207,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 {
@@ -306,6 +373,9 @@ Common.BrowserView {
y: webapp.currentWebview ? containerWebView.currentWebview.locationBarController.offset : 0
onChooseAccount: webapp.chooseAccount()
+ onToggleDownloads: {
+ webapp.showDownloadsDialog()
+ }
}
}
@@ -434,14 +504,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)
}
}