From bc134c3e83b97b4b82733feaa39e0bd99bb0fcc3 Mon Sep 17 00:00:00 2001 From: Soren Stoutner Date: Tue, 28 Nov 2023 14:10:16 -0700 Subject: [PATCH] Implement saving of MHT web archives. https://redmine.stoutner.com/issues/1089 --- src/ui.rcs/browserwindowui.rc | 2 + src/widgets/TabWidget.cpp | 266 +++++++++++++++++++--------------- src/widgets/TabWidget.h | 4 +- src/windows/BrowserWindow.cpp | 10 +- 4 files changed, 162 insertions(+), 120 deletions(-) diff --git a/src/ui.rcs/browserwindowui.rc b/src/ui.rcs/browserwindowui.rc index adcfa9d..ab3d503 100644 --- a/src/ui.rcs/browserwindowui.rc +++ b/src/ui.rcs/browserwindowui.rc @@ -34,6 +34,8 @@ + + diff --git a/src/widgets/TabWidget.cpp b/src/widgets/TabWidget.cpp index 84cd3ac..4a6b11c 100644 --- a/src/widgets/TabWidget.cpp +++ b/src/widgets/TabWidget.cpp @@ -864,6 +864,31 @@ void TabWidget::reloadAndBypassCache() const currentWebEnginePagePointer->triggerAction(QWebEnginePage::ReloadAndBypassCache); } +void TabWidget::saveArchive() +{ + // Get the suggested file name. + QString suggestedFileName = currentPrivacyWebEngineViewPointer->url().host() + ".mht"; + + // Get the download directory. + QString downloadDirectory = Settings::downloadLocation(); + + // Resolve the system download directory if specified. + if (downloadDirectory == QLatin1String("System Download Directory")) + downloadDirectory = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); + + // Get a file path from the file picker. + QString saveFilePath = QFileDialog::getSaveFileName(this, i18nc("Save file dialog caption", "Save File"), downloadDirectory + QLatin1Char('/') + suggestedFileName); + + // Save the webpage as an archive if the file save path is populated. + if (!saveFilePath.isEmpty()) + { + // Set the saving archive flag. Otherwise, a second download tries to run. + savingArchive = true; + + // Save the archive. + currentWebEnginePagePointer->save(saveFilePath); + } +} void TabWidget::setTabBarVisible(const bool visible) const { @@ -873,163 +898,170 @@ void TabWidget::setTabBarVisible(const bool visible) const void TabWidget::showSaveDialog(QWebEngineDownloadItem *webEngineDownloadItemPointer) { - // Get the download attributes. - QUrl downloadUrl = webEngineDownloadItemPointer->url(); - QString mimeTypeString = webEngineDownloadItemPointer->mimeType(); - QString suggestedFileName = webEngineDownloadItemPointer->suggestedFileName(); - int totalBytes = webEngineDownloadItemPointer->totalBytes(); - - // Check to see if Privacy Browser is not running KDE or if local storage (cookies) is enabled. - if (!isRunningKde || currentPrivacyWebEngineViewPointer->localStorageEnabled) // KDE is not running or local storage (cookies) is enabled. Use WebEngine's downloader. + // Only show the save dialog if an archive is not currently being saved. Otherwise, two save dialogs will be shown. + if (!savingArchive) { - // Instantiate the save dialog. - SaveDialog *saveDialogPointer = new SaveDialog(downloadUrl, mimeTypeString, totalBytes); + // Get the download attributes. + QUrl downloadUrl = webEngineDownloadItemPointer->url(); + QString mimeTypeString = webEngineDownloadItemPointer->mimeType(); + QString suggestedFileName = webEngineDownloadItemPointer->suggestedFileName(); + int totalBytes = webEngineDownloadItemPointer->totalBytes(); + + // Check to see if Privacy Browser is not running KDE or if local storage (cookies) is enabled. + if (!isRunningKde || currentPrivacyWebEngineViewPointer->localStorageEnabled) // KDE is not running or local storage (cookies) is enabled. Use WebEngine's downloader. + { + // Instantiate the save dialog. + SaveDialog *saveDialogPointer = new SaveDialog(downloadUrl, mimeTypeString, totalBytes); - // Display the save dialog. - int saveDialogResult = saveDialogPointer->exec(); + // Display the save dialog. + int saveDialogResult = saveDialogPointer->exec(); - // Process the save dialog results. - if (saveDialogResult == QDialog::Accepted) // Save was selected. - { - // Get the download directory. - QString downloadDirectory = Settings::downloadLocation(); + // Process the save dialog results. + if (saveDialogResult == QDialog::Accepted) // Save was selected. + { + // Get the download directory. + QString downloadDirectory = Settings::downloadLocation(); - // Resolve the system download directory if specified. - if (downloadDirectory == QLatin1String("System Download Directory")) - downloadDirectory = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); + // Resolve the system download directory if specified. + if (downloadDirectory == QLatin1String("System Download Directory")) + downloadDirectory = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); - // Get a file path from the file picker. - QString saveFilePath = QFileDialog::getSaveFileName(this, i18nc("Save file dialog caption", "Save File"), downloadDirectory + QLatin1Char('/') + suggestedFileName); + // Get a file path from the file picker. + QString saveFilePath = QFileDialog::getSaveFileName(this, i18nc("Save file dialog caption", "Save File"), downloadDirectory + QLatin1Char('/') + suggestedFileName); - // Process the save file path. - if (!saveFilePath.isEmpty()) // The file save path is populated. - { - // Create a save file path file info. - QFileInfo saveFilePathFileInfo = QFileInfo(saveFilePath); + // Process the save file path. + if (!saveFilePath.isEmpty()) // The file save path is populated. + { + // Create a save file path file info. + QFileInfo saveFilePathFileInfo = QFileInfo(saveFilePath); - // Get the canonical save path and file name. - QString absoluteSavePath = saveFilePathFileInfo.absolutePath(); - QString saveFileName = saveFilePathFileInfo.fileName(); + // Get the canonical save path and file name. + QString absoluteSavePath = saveFilePathFileInfo.absolutePath(); + QString saveFileName = saveFilePathFileInfo.fileName(); - // Set the download directory and file name. - webEngineDownloadItemPointer->setDownloadDirectory(absoluteSavePath); - webEngineDownloadItemPointer->setDownloadFileName(saveFileName); + // Set the download directory and file name. + webEngineDownloadItemPointer->setDownloadDirectory(absoluteSavePath); + webEngineDownloadItemPointer->setDownloadFileName(saveFileName); - // Create a file download notification. - KNotification *fileDownloadNotificationPointer = new KNotification(QLatin1String("FileDownload")); + // Create a file download notification. + KNotification *fileDownloadNotificationPointer = new KNotification(QLatin1String("FileDownload")); - // Set the notification title. - fileDownloadNotificationPointer->setTitle(i18nc("Download notification title", "Download")); + // Set the notification title. + fileDownloadNotificationPointer->setTitle(i18nc("Download notification title", "Download")); - // Set the notification text. - fileDownloadNotificationPointer->setText(i18nc("Downloading notification text", "Downloading %1", saveFileName)); + // Set the notification text. + fileDownloadNotificationPointer->setText(i18nc("Downloading notification text", "Downloading %1", saveFileName)); - // Get the download icon from the theme. - QIcon downloadIcon = QIcon::fromTheme(QLatin1String("download"), QIcon::fromTheme(QLatin1String("document-save"))); + // Get the download icon from the theme. + QIcon downloadIcon = QIcon::fromTheme(QLatin1String("download"), QIcon::fromTheme(QLatin1String("document-save"))); - // Set the notification icon. - fileDownloadNotificationPointer->setIconName(downloadIcon.name()); + // Set the notification icon. + fileDownloadNotificationPointer->setIconName(downloadIcon.name()); - // Set the action list cancel button. - fileDownloadNotificationPointer->setActions(QStringList({i18nc("Download notification action","Cancel")})); + // Set the action list cancel button. + fileDownloadNotificationPointer->setActions(QStringList({i18nc("Download notification action","Cancel")})); - // Prevent the notification from being autodeleted if it is closed. Otherwise, the updates to the notification below cause a crash. - fileDownloadNotificationPointer->setAutoDelete(false); + // Prevent the notification from being autodeleted if it is closed. Otherwise, the updates to the notification below cause a crash. + fileDownloadNotificationPointer->setAutoDelete(false); - // Handle clicks on the cancel button. - connect(fileDownloadNotificationPointer, &KNotification::action1Activated, [webEngineDownloadItemPointer, saveFileName] () - { - // Cancel the download. - webEngineDownloadItemPointer->cancel(); + // Handle clicks on the cancel button. + connect(fileDownloadNotificationPointer, &KNotification::action1Activated, [webEngineDownloadItemPointer, saveFileName] () + { + // Cancel the download. + webEngineDownloadItemPointer->cancel(); - // Create a file download notification. - KNotification *canceledDownloadNotificationPointer = new KNotification(QLatin1String("FileDownload")); + // Create a file download notification. + KNotification *canceledDownloadNotificationPointer = new KNotification(QLatin1String("FileDownload")); - // Set the notification title. - canceledDownloadNotificationPointer->setTitle(i18nc("Download notification title", "Download")); + // Set the notification title. + canceledDownloadNotificationPointer->setTitle(i18nc("Download notification title", "Download")); - // Set the new text. - canceledDownloadNotificationPointer->setText(i18nc("Download canceled notification", "%1 download canceled", saveFileName)); + // Set the new text. + canceledDownloadNotificationPointer->setText(i18nc("Download canceled notification", "%1 download canceled", saveFileName)); - // Set the notification icon. - canceledDownloadNotificationPointer->setIconName(QLatin1String("download")); + // Set the notification icon. + canceledDownloadNotificationPointer->setIconName(QLatin1String("download")); - // Display the notification. - canceledDownloadNotificationPointer->sendEvent(); - }); + // Display the notification. + canceledDownloadNotificationPointer->sendEvent(); + }); - // Update the notification when the download progresses. - connect(webEngineDownloadItemPointer, &QWebEngineDownloadItem::downloadProgress, [fileDownloadNotificationPointer, saveFileName] (qint64 bytesReceived, qint64 totalBytes) - { - // Set the new text. Total bytes will be 0 if the download size is unknown. - if (totalBytes > 0) + // Update the notification when the download progresses. + connect(webEngineDownloadItemPointer, &QWebEngineDownloadItem::downloadProgress, [fileDownloadNotificationPointer, saveFileName] (qint64 bytesReceived, qint64 totalBytes) { - // Calculate the download percentage. - int downloadPercentage = 100 * bytesReceived / totalBytes; - - // Set the file download notification text. - fileDownloadNotificationPointer->setText(i18nc("Download progress notification text", "%1\% of %2 downloaded (%3 of %4 bytes)", downloadPercentage, saveFileName, - bytesReceived, totalBytes)); - } - else + // Set the new text. Total bytes will be 0 if the download size is unknown. + if (totalBytes > 0) + { + // Calculate the download percentage. + int downloadPercentage = 100 * bytesReceived / totalBytes; + + // Set the file download notification text. + fileDownloadNotificationPointer->setText(i18nc("Download progress notification text", "%1\% of %2 downloaded (%3 of %4 bytes)", downloadPercentage, saveFileName, + bytesReceived, totalBytes)); + } + else + { + // Set the file download notification text. + fileDownloadNotificationPointer->setText(i18nc("Download progress notification text", "%1: %2 bytes downloaded", saveFileName, bytesReceived)); + } + + // Display the updated notification. + fileDownloadNotificationPointer->update(); + }); + + // Update the notification when the download finishes. The save file name must be copied into the lambda or a crash occurs. + connect(webEngineDownloadItemPointer, &QWebEngineDownloadItem::finished, [fileDownloadNotificationPointer, saveFileName, saveFilePath] () { - // Set the file download notification text. - fileDownloadNotificationPointer->setText(i18nc("Download progress notification text", "%1: %2 bytes downloaded", saveFileName, bytesReceived)); - } + // Set the new text. + fileDownloadNotificationPointer->setText(i18nc("Download finished notification text", "%1 download finished", saveFileName)); - // Display the updated notification. - fileDownloadNotificationPointer->update(); - }); - - // Update the notification when the download finishes. The save file name must be copied into the lambda or a crash occurs. - connect(webEngineDownloadItemPointer, &QWebEngineDownloadItem::finished, [fileDownloadNotificationPointer, saveFileName, saveFilePath] () - { - // Set the new text. - fileDownloadNotificationPointer->setText(i18nc("Download finished notification text", "%1 download finished", saveFileName)); + // Set the URL so the file options will be displayed. + fileDownloadNotificationPointer->setUrls(QList {QUrl(saveFilePath)}); - // Set the URL so the file options will be displayed. - fileDownloadNotificationPointer->setUrls(QList {QUrl(saveFilePath)}); + // Remove the actions from the notification. + fileDownloadNotificationPointer->setActions(QStringList()); - // Remove the actions from the notification. - fileDownloadNotificationPointer->setActions(QStringList()); + // Set the notification to disappear after a timeout. + fileDownloadNotificationPointer->setFlags(KNotification::CloseOnTimeout); - // Set the notification to disappear after a timeout. - fileDownloadNotificationPointer->setFlags(KNotification::CloseOnTimeout); + // Display the updated notification. + fileDownloadNotificationPointer->update(); + }); - // Display the updated notification. - fileDownloadNotificationPointer->update(); - }); - - // Display the notification. - fileDownloadNotificationPointer->sendEvent(); + // Display the notification. + fileDownloadNotificationPointer->sendEvent(); - // Start the download. - webEngineDownloadItemPointer->accept(); + // Start the download. + webEngineDownloadItemPointer->accept(); + } + else // The file save path is not populated. + { + // Cancel the download. + webEngineDownloadItemPointer->cancel(); + } } - else // The file save path is not populated. + else // Cancel was selected. { // Cancel the download. webEngineDownloadItemPointer->cancel(); } } - else // Cancel was selected. + else // KDE is running and local storage (cookies) is disabled. Use KDE's native downloader. + // This must use the show command to launch a separate dialog which cancels WebEngine's automatic background download of the file to a temporary location. { - // Cancel the download. - webEngineDownloadItemPointer->cancel(); - } - } - else // KDE is running and local storage (cookies) is disabled. Use KDE's native downloader. - // This must use the show command to launch a separate dialog which cancels WebEngine's automatic background download of the file to a temporary location. - { - // Instantiate the save dialog. `true` instructs it to use the native downloader - SaveDialog *saveDialogPointer = new SaveDialog(downloadUrl, mimeTypeString, totalBytes, suggestedFileName, true); + // Instantiate the save dialog. `true` instructs it to use the native downloader + SaveDialog *saveDialogPointer = new SaveDialog(downloadUrl, mimeTypeString, totalBytes, suggestedFileName, true); - // Connect the save button. - connect(saveDialogPointer, SIGNAL(useNativeKdeDownloader(QUrl &, QString &)), this, SLOT(useNativeKdeDownloader(QUrl &, QString &))); + // Connect the save button. + connect(saveDialogPointer, SIGNAL(useNativeKdeDownloader(QUrl &, QString &)), this, SLOT(useNativeKdeDownloader(QUrl &, QString &))); - // Show the dialog. - saveDialogPointer->show(); + // Show the dialog. + saveDialogPointer->show(); + } } + + // Reset the saving archive flag. + savingArchive = false; } void TabWidget::toggleDomStorage() const diff --git a/src/widgets/TabWidget.h b/src/widgets/TabWidget.h index 41ba64c..ab61671 100644 --- a/src/widgets/TabWidget.h +++ b/src/widgets/TabWidget.h @@ -116,6 +116,7 @@ public Q_SLOTS: void printPreview() const; void refresh() const; void reloadAndBypassCache() const; + void saveArchive(); private Q_SLOTS: // The private slots. @@ -142,8 +143,9 @@ private: QIcon defaultFavoriteIcon = QIcon::fromTheme(QLatin1String("globe"), QIcon::fromTheme(QLatin1String("applications-internet"))); bool isRunningKde = false; QMovie *loadingFavoriteIconMoviePointer; - QString searchEngineUrl; QTabWidget *qTabWidgetPointer; + bool savingArchive; + QString searchEngineUrl; UserAgentHelper *userAgentHelperPointer; bool wipingCurrentFindTextSelection = false; }; diff --git a/src/windows/BrowserWindow.cpp b/src/windows/BrowserWindow.cpp index b90f132..214b0af 100644 --- a/src/windows/BrowserWindow.cpp +++ b/src/windows/BrowserWindow.cpp @@ -91,6 +91,7 @@ BrowserWindow::BrowserWindow(bool firstWindow, QString *initialUrlStringPointer) // Add the custom actions. QAction *newTabActionPointer = actionCollectionPointer->addAction(QLatin1String("new_tab")); QAction *newWindowActionPointer = actionCollectionPointer->addAction(QLatin1String("new_window")); + QAction *saveArchiveActionPointer = actionCollectionPointer->addAction(QLatin1String("save_archive")); zoomDefaultActionPointer = actionCollectionPointer->addAction(QLatin1String("zoom_default")); QAction *reloadAndBypassCacheActionPointer = actionCollectionPointer->addAction(QLatin1String("reload_and_bypass_cache")); viewSourceActionPointer = actionCollectionPointer->addAction(QLatin1String("view_source")); @@ -171,12 +172,13 @@ BrowserWindow::BrowserWindow(bool firstWindow, QString *initialUrlStringPointer) UserAgentHelper *userAgentHelperPointer = new UserAgentHelper(); // Set the action text. - viewSourceActionPointer->setText(i18nc("View source action", "View Source")); - viewSourceInNewTabActionPointer->setText(i18nc("View source in new tab action", "View Source in New Tab")); newTabActionPointer->setText(i18nc("New tab action", "New Tab")); newWindowActionPointer->setText(i18nc("New window action", "New Window")); + saveArchiveActionPointer->setText(i18nc("Save archive action", "Save Archive")); zoomDefaultActionPointer->setText(i18nc("Zoom default action", "Zoom Default")); reloadAndBypassCacheActionPointer->setText(i18nc("Reload and bypass cache action", "Reload and Bypass Cache")); + viewSourceActionPointer->setText(i18nc("View source action", "View Source")); + viewSourceInNewTabActionPointer->setText(i18nc("View source in new tab action", "View Source in New Tab")); javaScriptActionPointer->setText(i18nc("JavaScript action", "JavaScript")); localStorageActionPointer->setText(i18nc("The Local Storage action", "Local Storage")); domStorageActionPointer->setText(i18nc("DOM Storage action", "DOM Storage")); @@ -204,6 +206,7 @@ BrowserWindow::BrowserWindow(bool firstWindow, QString *initialUrlStringPointer) // The toolbar icons don't pick up unless the size is explicit, probably because the toolbar ends up being an intermediate size. newTabActionPointer->setIcon(QIcon::fromTheme(QLatin1String("tab-new"))); newWindowActionPointer->setIcon(QIcon::fromTheme(QLatin1String("window-new"))); + saveArchiveActionPointer->setIcon(QIcon::fromTheme(QLatin1String("document-save"))); zoomDefaultActionPointer->setIcon(QIcon::fromTheme(QLatin1String("zoom-fit-best"))); reloadAndBypassCacheActionPointer->setIcon(QIcon::fromTheme(QLatin1String("view-refresh"))); viewSourceActionPointer->setIcon(QIcon::fromTheme(QLatin1String("view-choose"), QIcon::fromTheme(QLatin1String("accessories-text-editor")))); @@ -238,6 +241,7 @@ BrowserWindow::BrowserWindow(bool firstWindow, QString *initialUrlStringPointer) // Create the key sequences. QKeySequence ctrlTKeySequence = QKeySequence(i18nc("The open new tab key sequence.", "Ctrl+T")); QKeySequence ctrlNKeySequence = QKeySequence(i18nc("The open new window key sequence.", "Ctrl+N")); + QKeySequence ctrlAKeySequence = QKeySequence(i18nc("The save archive key sequence.", "Ctrl+A")); QKeySequence ctrl0KeySequence = QKeySequence(i18nc("The zoom default key sequence.", "Ctrl+0")); QKeySequence ctrlF5KeySequence = QKeySequence(i18nc("The reload and bypass cache key sequence.", "Ctrl+F5")); QKeySequence ctrlUKeySequence = QKeySequence(i18nc("The view source key sequence.", "Ctrl+U")); @@ -272,6 +276,7 @@ BrowserWindow::BrowserWindow(bool firstWindow, QString *initialUrlStringPointer) // Set the action key sequences. actionCollectionPointer->setDefaultShortcut(newTabActionPointer, ctrlTKeySequence); actionCollectionPointer->setDefaultShortcut(newWindowActionPointer, ctrlNKeySequence); + actionCollectionPointer->setDefaultShortcut(saveArchiveActionPointer, ctrlAKeySequence); actionCollectionPointer->setDefaultShortcut(zoomDefaultActionPointer, ctrl0KeySequence); actionCollectionPointer->setDefaultShortcut(reloadAndBypassCacheActionPointer, ctrlF5KeySequence); actionCollectionPointer->setDefaultShortcut(viewSourceActionPointer, ctrlUKeySequence); @@ -306,6 +311,7 @@ BrowserWindow::BrowserWindow(bool firstWindow, QString *initialUrlStringPointer) // Execute the actions. connect(newTabActionPointer, SIGNAL(triggered()), tabWidgetPointer, SLOT(addTab())); connect(newWindowActionPointer, SIGNAL(triggered()), this, SLOT(newWindow())); + connect(saveArchiveActionPointer, SIGNAL(triggered()), tabWidgetPointer, SLOT(saveArchive())); connect(zoomDefaultActionPointer, SIGNAL(triggered()), this, SLOT(zoomDefault())); connect(reloadAndBypassCacheActionPointer, SIGNAL(triggered()), this, SLOT(reloadAndBypassCache())); connect(viewSourceActionPointer, SIGNAL(triggered()), this, SLOT(toggleViewSource())); -- 2.45.2