diff --git a/extension/_locales/en/messages.json b/extension/_locales/en/messages.json index 1c46cdd8..523cf803 100644 --- a/extension/_locales/en/messages.json +++ b/extension/_locales/en/messages.json @@ -1,201 +1,205 @@ { "store_description": { "description": "The extension description on the extension store", "message": "Multitask efficiently by controlling browser functions from the desktop, even while Chrome is in the background. Manage audio and video playback, check downloads in the notification area, send files to your phone using KDE Connect and more inside the KDE Plasma Desktop!\\n\\nThe plasma-browser-integration package must be installed for this extension to work. It should be available from your distribution's package manager when running Plasma 5.13 or later.\\n\\nNOTE: This extension is not supported on Debian." }, "browseraction_title": { "description": "Title for toolbar popup", "message": "Plasma Browser Integration" }, "browseraction_mpris_title": { "description": "Title for Media controls in popup", "message": "Media Controls" }, "browseraction_mpris_enable_on": { "description": "Heading for list of domains to enable media controls on", "message": "Enable media controls on:" }, "options_title": { "description": "Title for settings page", "message": "Plasma Integration Settings" }, "options_save_failed": { "message": "Saving settings failed" }, "options_save_success": { "message": "Settings successfully saved" }, "options_not_supported_os": { "message": "This extension is not supported on this operating system." }, "options_tab_general": { "description": "The 'General settings' tab in settings", "message": "General" }, "options_tab_about": { "description": "The 'About this plugin' tab in settings", "message": "About" }, "options_plugin_mpris_title": { "description": "Title for Media Controls plugin", "message": "Media Controls" }, "options_plugin_mpris_description": { "description": "Description for Media Controls plugin", "message": "Lets you control video and audio players in websites using the Media Controller plasmoid." }, "options_plugin_mpris_media_sessions_title": { "description": "Title for MediaSessions API Control plugin", "message": "Enhanced Media Controls" }, "options_plugin_mpris_media_sessions_description": { "description": "Description for MediaSessions API Control plugin", "message": "Extract metadata and thumbnails of currently playing content." }, "options_plugin_kdeconnect_title": { "description": "Title for KDE Connect plugin", "message": "Send via KDE Connect" }, "options_plugin_kdeconnect_description": { "description": "Description for KDE Connect plugin", "message": "Adds a context menu entry to links enabling you to send them to your phone and other paired devices using KDE Connect." }, "options_plugin_downloads_title": { "description": "Title for Downloads plugin", "message": "Show downloads in notification area" }, + "options_plugin_downloads_addToRecentDocuments": { + "description": "Option for adding downloaded files to recent documents", + "message": "Add downloaded files to recent documents" + }, "options_plugin_downloads_saveOriginUrl": { "description": "Option for saving download source URL in file metadata", "message": "Save URL a file was downloaded from in the file's attributes" }, "options_plugin_downloads_saveOriginUrl_description": { "message": "Note: The URL may contain sensitive information that could be disclosed when the file is accessible by or shared with others" }, "options_plugin_tabsrunner_title": { "description": "Title for Browser Tabs KRunner plugin", "message": "Find browser tabs in “Run Command” window" }, "options_plugin_tabsrunner_description": { "description": "Description for Browser Tabs KRunner plugin", "message": "Make sure the “Browser Tabs” module is enabled in Plasma Search settings." }, "options_plugin_purpose_title": { "description": "Title for Purpose / Web Share plugin", "message": "Content Sharing" }, "options_plugin_purpose_description": { "description": "Description for Purpose / Web Share plugin", "message": "Adds a \"Share...\" context menu entry and allows websites to open a dialog for sharing contents using the Web Share API." }, "options_plugin_breezeScrollBars_title": { "description": "Title for Breeze style scroll bars plugin", "message": "Use Breeze-style scroll bars" }, "options_plugin_breezeScrollBars_description": { "description": "Description for Breeze style scroll bars plugin", "message": "This may interfere with the appearance of websites that already apply a custom styling to their scroll bars." }, "options_about_host_version": { "description": "Version of extension native host", "message": "Host version: $1" }, "options_about_extension_version": { "description": "Version of browser extension", "message": "Extension version: $1" }, "options_about_copyright": { "message": "© 2017-2019 Kai Uwe Broulik and David Edmundson" }, "options_about_license": { "message": "License: GNU General Public License Version 3" }, "options_about_translated_by": { "message": "Translated by: $1" }, "options_about_translators": { "description": "Name of translators", "message": "Your names" }, "options_about_created_by_kde": { "message": "This browser extension was created by the KDE Community. You can find more information about this project on the KDE Community Wiki." }, "options_about_bugs": { "message": "If you find an issue, please check the list of open bugs and then file a bug report." }, "options_about_kde": { "description": "KDE description taken from kaboutkdedialog_p.h in kmxlgui", "message": "KDE is a world-wide community of software engineers, artists, writers, translators and creators who are committed to Free Software development. KDE produces the Plasma desktop environment, hundreds of applications, and the many software libraries that support them. KDE is a cooperative enterprise: no single entity controls its direction or products. Instead, we work together to achieve the common goal of building the world's finest Free Software. Everyone is welcome to join and contribute to KDE, including you. Visit $3 for more information about the KDE community and the software we produce." }, "options_about_donate": { "message": "If you like what you saw, please consider donating to KDE, so we can continue to make the best free software possible." }, "kdeconnect_open_via": { "description": "Context menu, open link on device whose name we don't (yet) know", "message": "Open via KDE Connect" }, "kdeconnect_open_device": { "description": "Context menu, open link on device $1, similar to 'Open in New Tab'", "message": "Open on '$1'" }, "purpose_share": { "description": "Context menu, share link or page via Purpose framework", "message": "Share..." }, "purpose_share_finished_title": { "description": "Title of share finished notification", "message": "Content Shared" }, "purpose_share_finished_text": { "description": "Text of the share finished notification", "message": "The shared content link ($1) has been copied to the clipboard." }, "purpose_share_failed_title": { "description": "Title of share failed notification", "message": "Sharing Failed" }, "purpose_share_failed_text": { "description": "Text of share failed notification", "message": "Could not share this content: $1" }, "general_error_unknown": { "description": "An unknown error occurred, usually used when an error message by the system is not provided", "message": "Unknown Error" }, "general_error_not_supported_os_title": { "message": "Unsupported operating system" }, "general_error_not_supported_os": { "message": "This extension is only supported on Linux and FreeBSD." }, "general_error_startup_failed_title": { "description": "Title for failure to start plasma-browser-integration-host binary", "message": "Failed to connect to the native host." }, "general_error_startup_failed": { "description": "Description for failure to start plasma-browser-integration-host binary", "message": "Make sure the 'plasma-browser-integration' package is installed correctly and that you are running Plasma 5.13 or later." }, "general_error_startup_failed_wiki_link": { "message": "Visit project wiki page for more information" }, "general_error_host_disconnected_title": { "description": "Title for plasma-browser-integration-host binary unexpectedly closing/crashing", "message": "The native host disconnected unexpectedly." } } diff --git a/extension/constants.js b/extension/constants.js index 798d3eda..614bc9a0 100644 --- a/extension/constants.js +++ b/extension/constants.js @@ -1,54 +1,55 @@ /* Copyright (C) 2017 Kai Uwe Broulik Copyright (C) 2018 David Edmundson This program 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; either version 3 of the License, or (at your option) any later version. This program 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 . */ DEFAULT_EXTENSION_SETTINGS = { mpris: { enabled: true, websiteSettings: {} }, mprisMediaSessions: { enabled: true }, kdeconnect: { enabled: true }, downloads: { enabled: true, + addToRecentDocuments: true, saveOriginUrl: false }, tabsrunner: { enabled: true }, purpose: { enabled: true }, breezeScrollBars: { // this breaks pages in interesting ways, disable by default enabled: false } }; IS_FIREFOX = (typeof InstallTrigger !== "undefined"); // heh. // NOTE if you change this, make sure to adjust the error message shown in action_popup.html SUPPORTED_PLATFORMS = ["linux", "openbsd", "freebsd"]; // Default MPRIS settings for websites const MPRIS_WEBSITE_SETTINGS = { //"https://www.example.com": false }; diff --git a/extension/options.html b/extension/options.html index 541f184f..96dbc1d0 100644 --- a/extension/options.html +++ b/extension/options.html @@ -1,118 +1,124 @@

I18N

I18N

I18N

I18N

I18N
  • I18N

  • I18N

  • I18N

  • +
  • + +

    +
  • I18N

  • I18N

  • I18N

  • I18N

I18N
I18N

I18N
I18N
I18N

I18N

I18N

I18N

I18N

diff --git a/host/CMakeLists.txt b/host/CMakeLists.txt index fbe4345c..3d92c6ab 100644 --- a/host/CMakeLists.txt +++ b/host/CMakeLists.txt @@ -1,37 +1,38 @@ add_definitions(-DTRANSLATION_DOMAIN=\"plasma-browser-integration-host\") configure_file(config-host.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-host.h) set(HOST_SOURCES main.cpp connection.cpp pluginmanager.cpp settings.cpp mprisplugin.cpp abstractbrowserplugin.cpp kdeconnectplugin.cpp downloadplugin.cpp downloadjob.cpp tabsrunnerplugin.cpp purposeplugin.cpp ) qt5_add_dbus_adaptor(HOST_SOURCES ../dbus/org.kde.plasma.browser_integration.TabsRunner.xml tabsrunnerplugin.h TabsRunnerPlugin) qt5_add_dbus_adaptor(HOST_SOURCES ../dbus/org.kde.plasma.browser_integration.Settings.xml settings.h Settings) qt5_add_dbus_adaptor(HOST_SOURCES ../dbus/org.mpris.MediaPlayer2.xml mprisplugin.h MPrisPlugin mprisroot MPrisRoot) qt5_add_dbus_adaptor(HOST_SOURCES ../dbus/org.mpris.MediaPlayer2.Player.xml mprisplugin.h MPrisPlugin mprisplayer MPrisPlayer) add_executable(plasma-browser-integration-host ${HOST_SOURCES}) target_link_libraries( plasma-browser-integration-host Qt5::DBus Qt5::Gui Qt5::Widgets + KF5::Activities KF5::Crash KF5::I18n KF5::KIOCore KF5::PurposeWidgets KF5::FileMetaData ) install(TARGETS plasma-browser-integration-host ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/host/downloadjob.cpp b/host/downloadjob.cpp index e4982a8f..efcb857d 100644 --- a/host/downloadjob.cpp +++ b/host/downloadjob.cpp @@ -1,278 +1,299 @@ /* Copyright (C) 2017 by Kai Uwe Broulik Copyright (C) 2017 by David Edmundson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "downloadjob.h" #include "settings.h" #include +#include #include +#include #include #include #include DownloadJob::DownloadJob(int id) : KJob() , m_id(id) { // the thing with "canResume" in chrome downloads is that it just means // "this download can be resumed right now because it is paused", // it's not a general thing. I think we can always pause/resume downloads // unless they're canceled/interrupted at which point we don't have a DownloadJob // anymore anyway setCapabilities(Killable | Suspendable); // TODO When suspending on Firefox the download job goes away for some reason?! // Until I have the virtue to figure that out just disallow suspending downloads on Firefox :) if (Settings::self().environment() == Settings::Environment::Firefox) { setCapabilities(Killable); } } void DownloadJob::start() { QMetaObject::invokeMethod(this, "doStart", Qt::QueuedConnection); } void DownloadJob::doStart() { } bool DownloadJob::doKill() { emit killRequested(); // TODO what if the user kills us from notification area while the // "Save As" prompt is still open? return true; } bool DownloadJob::doSuspend() { emit suspendRequested(); return true; } bool DownloadJob::doResume() { emit resumeRequested(); return true; } void DownloadJob::update(const QJsonObject &payload) { auto end = payload.constEnd(); bool descriptionDirty = false; auto it = payload.constFind(QStringLiteral("url")); if (it != end) { m_url = QUrl(it->toString()); descriptionDirty = true; // TODO only if actually changed } it = payload.constFind(QStringLiteral("finalUrl")); if (it != end) { m_finalUrl = QUrl(it->toString()); descriptionDirty = true; } it = payload.constFind(QStringLiteral("filename")); if (it != end) { m_fileName = it->toString(); const QUrl destination = QUrl::fromLocalFile(it->toString()); setProperty("destUrl", destination.toString(QUrl::RemoveFilename | QUrl::StripTrailingSlash)); m_destination = destination; descriptionDirty = true; } it = payload.constFind(QStringLiteral("mime")); if (it != end) { m_mimeType = it->toString(); } it = payload.constFind(QStringLiteral("incognito")); if (it != end) { m_incognito = it->toBool(); } it = payload.constFind(QStringLiteral("totalBytes")); if (it != end) { const qlonglong totalAmount = it->toDouble(); if (totalAmount > -1) { setTotalAmount(Bytes, totalAmount); } } it = payload.constFind(QStringLiteral("bytesReceived")); if (it != end) { setProcessedAmount(Bytes, it->toDouble()); } setTotalAmount(Files, 1); it = payload.constFind(QStringLiteral("paused")); if (it != end) { const bool paused = it->toBool(); if (paused) { suspend(); } else { resume(); } } it = payload.constFind(QStringLiteral("estimatedEndTime")); if (it != end) { qulonglong speed = 0; // now calculate the speed from estimated end time and total size // funny how chrome only gives us a time whereas KJob operates on speed // and calculates the time this way :) const QDateTime endTime = QDateTime::fromString(it->toString(), Qt::ISODate); if (endTime.isValid()) { const QDateTime now = QDateTime::currentDateTimeUtc(); qulonglong remainingBytes = totalAmount(Bytes) - processedAmount(Bytes); quint64 remainingTime = now.secsTo(endTime); if (remainingTime > 0) { speed = remainingBytes / remainingTime; } } emitSpeed(speed); } if (descriptionDirty) { updateDescription(); } const QString error = payload.value(QStringLiteral("error")).toString(); if (!error.isEmpty()) { if (error == QLatin1String("USER_CANCELED") || error == QLatin1String("USER_SHUTDOWN")) { setError(KIO::ERR_USER_CANCELED); // will keep Notification applet from showing a "finished"/error message emitResult(); return; } // value is a QVariant so we can be lazy and support both KIO errors and custom test // if QVariant is an int: use that as KIO error // if QVariant is a QString: set UserError and message static const QHash errors { // for a list of these error codes *and their meaning* instead of looking at browser // extension docs, check out Chromium's source code: download_interrupt_reason_values.h {QStringLiteral("FILE_ACCESS_DENIED"), i18n("Access denied.")}, // KIO::ERR_ACCESS_DENIED {QStringLiteral("FILE_NO_SPACE"), i18n("Insufficient free space.")}, // KIO::ERR_DISK_FULL {QStringLiteral("FILE_NAME_TOO_LONG"), i18n("The file name you have chosen is too long.")}, {QStringLiteral("FILE_TOO_LARGE"), i18n("The file is too large to be downloaded.")}, // haha {QStringLiteral("FILE_VIRUS_INFECTED"), i18n("The file possibly contains malicious contents.")}, {QStringLiteral("FILE_TRANSIENT_ERROR"), i18n("A temporary error has occurred. Please try again later.")}, {QStringLiteral("NETWORK_FAILED"), i18n("A network error has occurred.")}, {QStringLiteral("NETWORK_TIMEOUT"), i18n("The network operation timed out.")}, // TODO something less geeky {QStringLiteral("NETWORK_DISCONNECTED"), i18n("The network connection has been lost.")}, {QStringLiteral("NETWORK_SERVER_DOWN"), i18n("The server is no longer reachable.")}, {QStringLiteral("SERVER_FAILED"), i18n("A server error has occurred.")}, // chromium code says "internal use" and this is really not something the user should see // SERVER_NO_RANGE" // SERVER_PRECONDITION {QStringLiteral("SERVER_BAD_CONTENT"), i18n("The server does not have the requested data.")}, {QStringLiteral("CRASH"), i18n("The browser application closed unexpectedly.")} }; const QString &errorValue = errors.value(error); if (errorValue.isEmpty()) { // unknown error setError(KIO::ERR_UNKNOWN); setErrorText(i18n("An unknown error occurred while downloading.")); emitResult(); return; } // KIO::Error doesn't have a UserDefined one, let's just use magic numbers then // TODO at least set the KIO::Errors that we do have setError(1000); setErrorText(errorValue); emitResult(); return; } it = payload.constFind(QStringLiteral("state")); if (it != end) { const QString state = it->toString(); // We ignore "interrupted" state and only cancel if we get supplied an "error" if (state == QLatin1String("complete")) { setError(KJob::NoError); setProcessedAmount(KJob::Files, 1); + // Add to recent document + addToRecentDocuments(); + // Write origin url into extended file attributes saveOriginUrl(); emitResult(); return; } } } void DownloadJob::updateDescription() { description(this, i18nc("Job heading, like 'Copying'", "Downloading"), qMakePair(i18nc("The URL being downloaded", "Source"), (m_finalUrl.isValid() ? m_finalUrl : m_url).toDisplayString()), qMakePair(i18nc("The location being downloaded to", "Destination"), m_destination.toLocalFile()) ); } +void DownloadJob::addToRecentDocuments() +{ + if (m_incognito || m_fileName.isEmpty()) { + return; + } + + const QJsonObject settings = Settings::self().settingsForPlugin(QStringLiteral("downloads")); + + const bool enabled = settings.value(QStringLiteral("addToRecentDocuments")).toBool(); + if (!enabled) { + return; + } + + KActivities::ResourceInstance::notifyAccessed(QUrl::fromLocalFile(m_fileName), qApp->desktopFileName()); +} + void DownloadJob::saveOriginUrl() { if (m_incognito // Blob URLs are dynamically created through JavaScript and cannot be accessed from the outside || m_finalUrl.scheme() == QLatin1String("blob")) { return; } const QJsonObject settings = Settings::self().settingsForPlugin(QStringLiteral("downloads")); const bool saveOriginUrl = settings.value(QStringLiteral("saveOriginUrl")).toBool(); if (!saveOriginUrl) { return; } KFileMetaData::UserMetaData md(m_fileName); QUrl url = m_finalUrl; url.setPassword(QString()); md.setOriginUrl(url); } diff --git a/host/downloadjob.h b/host/downloadjob.h index 957b34e3..412fc059 100644 --- a/host/downloadjob.h +++ b/host/downloadjob.h @@ -1,79 +1,80 @@ /* Copyright (C) 2017 by Kai Uwe Broulik Copyright (C) 2017 by David Edmundson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #pragma once #include #include class DownloadJob : public KJob { Q_OBJECT public: DownloadJob(int id); enum class State { None, InProgress, Interrupted, Complete }; void start() override; void update(const QJsonObject &payload); Q_SIGNALS: void killRequested(); void suspendRequested(); void resumeRequested(); private Q_SLOTS: void doStart(); protected: bool doKill() override; bool doSuspend() override; bool doResume() override; private: void updateDescription(); + void addToRecentDocuments(); void saveOriginUrl(); int m_id = -1; QUrl m_url; QUrl m_finalUrl; QUrl m_destination; QString m_fileName; QString m_mimeType; // In doubt, assume incognito bool m_incognito = true; }; diff --git a/host/downloadplugin.cpp b/host/downloadplugin.cpp index a4e28b00..68f7ba74 100644 --- a/host/downloadplugin.cpp +++ b/host/downloadplugin.cpp @@ -1,116 +1,116 @@ /* Copyright (C) 2017 by Kai Uwe Broulik Copyright (C) 2017 by David Edmundson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "downloadplugin.h" #include "connection.h" #include "downloadjob.h" #include #include DownloadPlugin::DownloadPlugin(QObject* parent) : - AbstractBrowserPlugin(QStringLiteral("downloads"), 2, parent) + AbstractBrowserPlugin(QStringLiteral("downloads"), 3, parent) { } bool DownloadPlugin::onLoad() { // Have extension tell us about all the downloads sendData(QStringLiteral("createAll")); return true; } bool DownloadPlugin::onUnload() { for (auto it = m_jobs.constBegin(), end = m_jobs.constEnd(); it != end; ++it) { it.value()->deleteLater(); // kill() would abort the download } return true; } void DownloadPlugin::handleData(const QString& event, const QJsonObject& payload) { const QJsonObject &download = payload.value(QStringLiteral("download")).toObject(); const int id = download.value(QStringLiteral("id")).toInt(-1); if (id < 0) { qWarning() << "Cannot update download with invalid id" << id; return; } if (event == QLatin1String("created")) { // If we get a created event for an already existing job, update it instead auto *job = m_jobs.value(id); if (job) { job->update(download); return; } job = new DownloadJob(id); // first register and then update, otherwise we miss the initial population.. KIO::getJobTracker()->registerJob(job); job->update(download); m_jobs.insert(id, job); connect(job, &DownloadJob::killRequested, this, [this, id] { sendData(QStringLiteral("cancel"), { {QStringLiteral("downloadId"), id} }); }); connect(job, &DownloadJob::suspendRequested, this, [this, id] { sendData(QStringLiteral("suspend"), { {QStringLiteral("downloadId"), id} }); }); connect(job, &DownloadJob::resumeRequested, this, [this, id] { sendData(QStringLiteral("resume"), { {QStringLiteral("downloadId"), id} }); }); QObject::connect(job, &QObject::destroyed, this, [this, id] { m_jobs.remove(id); }); job->start(); QObject::connect(job, &KJob::finished, this, [this, job, id] { }); } else if (event == QLatin1String("update")) { auto *job = m_jobs.value(id); if (!job) { debug() << "Failed to find download to update with id" << id; return; } job->update(download); } }