diff --git a/importer/dialogpage.cpp b/importer/dialogpage.cpp index 2ae6c5d7..f8992194 100644 --- a/importer/dialogpage.cpp +++ b/importer/dialogpage.cpp @@ -1,92 +1,177 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2009 Aurélien Gâteau 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 2 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA. */ // Self #include "dialogpage.h" // Qt #include #include #include #include +#include // KDE #include +#include // Local #include +#include "importdialog.h" namespace Gwenview { struct DialogPagePrivate : public Ui_DialogPage { QVBoxLayout* mLayout; QList mButtons; QEventLoop* mEventLoop; + DialogPage* q; + QStringList failedFileList; + QStringList failedDirList; + QAction* fileDetails; + QAction* dirDetails; + + void setupFailedListActions() + { + fileDetails = new QAction(i18n("Show failed files...")); + mErrorMessageWidget->addAction(fileDetails); + QObject::connect(fileDetails, &QAction::triggered, + q, &DialogPage::slotShowFailedFileDetails); + fileDetails->setVisible(false); + + dirDetails = new QAction(i18n("Show failed subfolders...")); + mErrorMessageWidget->addAction(dirDetails); + QObject::connect(dirDetails, &QAction::triggered, + q, &DialogPage::slotShowFailedDirDetails); + dirDetails->setVisible(false); + } + + void showErrors(const QStringList& files, const QStringList& dirs) + { + mErrorMessageWidget->setVisible(true); + failedFileList.clear(); + failedDirList.clear(); + QStringList message; + if (files.count() > 0) { + failedFileList = files; + message << i18np("Failed to import %1 document.", + "Failed to import %1 documents.", + files.count()); + fileDetails->setVisible(true); + } else fileDetails->setVisible(false); + + if (dirs.count() > 0) { + failedDirList = dirs; + message << i18np("Failed to create %1 destination subfolder.", + "Failed to create %1 destination subfolders.", + dirs.count()); + dirDetails->setVisible(true); + } else dirDetails->setVisible(false); + + mErrorMessageWidget->setText(message.join("
")); + mErrorMessageWidget->animatedShow(); + } + + void showFailedFileDetails() + { + QString message = i18n("Failed to import documents:"); + KMessageBox::errorList(q, + message, + failedFileList + ); + } + + void showFailedDirDetails() + { + QString message = i18n("Failed to create subfolders:"); + KMessageBox::errorList(q, + message, + failedDirList + ); + } }; DialogPage::DialogPage(QWidget* parent) : QWidget(parent) , d(new DialogPagePrivate) { + d->q = this; d->setupUi(this); d->mLayout = new QVBoxLayout(d->mButtonContainer); + d->setupFailedListActions(); + d->mErrorMessageWidget->hide(); } DialogPage::~DialogPage() { delete d; } void DialogPage::removeButtons() { qDeleteAll(d->mButtons); d->mButtons.clear(); } void DialogPage::setText(const QString& text) { d->mLabel->setText(text); } int DialogPage::addButton(const KGuiItem& item) { int id = d->mButtons.size(); QPushButton* button = new QPushButton; KGuiItem::assign(button, item); button->setFixedHeight(button->sizeHint().height() * 2); connect(button, &QAbstractButton::clicked, this, [this, id]() { d->mEventLoop->exit(id); }); d->mLayout->addWidget(button); d->mButtons << button; return id; } +void DialogPage::slotShowErrors(const QStringList& files, const QStringList& dirs) +{ + d->showErrors(files, dirs); +} + +void DialogPage::slotShowFailedFileDetails() +{ + d->showFailedFileDetails(); +} + +void DialogPage::slotShowFailedDirDetails() +{ + d->showFailedDirDetails(); +} + int DialogPage::exec() { QEventLoop loop; d->mEventLoop = &loop; return loop.exec(); } } // namespace diff --git a/importer/dialogpage.h b/importer/dialogpage.h index f9f58430..49454ca4 100644 --- a/importer/dialogpage.h +++ b/importer/dialogpage.h @@ -1,55 +1,60 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2009 Aurélien Gâteau 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 2 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA. */ #ifndef DIALOGPAGE_H #define DIALOGPAGE_H // Qt #include // KDE // Local class KGuiItem; namespace Gwenview { struct DialogPagePrivate; class DialogPage : public QWidget { Q_OBJECT public: explicit DialogPage(QWidget* parent = nullptr); ~DialogPage() override; void removeButtons(); void setText(const QString&); int addButton(const KGuiItem&); int exec(); +public Q_SLOTS: + void slotShowErrors(const QStringList&, const QStringList&); + void slotShowFailedFileDetails(); + void slotShowFailedDirDetails(); + private: DialogPagePrivate* const d; }; } // namespace #endif /* DIALOGPAGE_H */ diff --git a/importer/dialogpage.ui b/importer/dialogpage.ui index 7d90f287..9032c5b9 100644 --- a/importer/dialogpage.ui +++ b/importer/dialogpage.ui @@ -1,116 +1,136 @@ DialogPage 0 0 - 400 - 239 + 355 + 287 - - + + Qt::Vertical - - QSizePolicy::Expanding + + + 20 + 55 + + + + + + + + Qt::Vertical 20 40 - + Qt::Horizontal 178 20 - + Qt::Vertical QSizePolicy::Fixed 20 12 - + Qt::Horizontal 177 20 - - + + Qt::Vertical + + QSizePolicy::Expanding + 20 - 55 + 40 - - - - Qt::Vertical + + + + + 0 + 0 + - - - 20 - 40 - + + KMessageWidget::Error - + + + + KMessageWidget + QFrame +
kmessagewidget.h
+
+
diff --git a/importer/importdialog.cpp b/importer/importdialog.cpp index 4a96a373..3ee19066 100644 --- a/importer/importdialog.cpp +++ b/importer/importdialog.cpp @@ -1,267 +1,289 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2009 Aurélien Gâteau 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 2 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA. */ // Self #include "importdialog.h" // Qt #include #include #include // KDE #include #include #include #include #include #include #include #include #include #include // Local #include "dialogpage.h" #include "importer.h" #include "importerconfig.h" #include "progresspage.h" #include "thumbnailpage.h" namespace Gwenview { class ImportDialogPrivate { public: ImportDialog* q; QStackedWidget* mCentralWidget; ThumbnailPage* mThumbnailPage; ProgressPage* mProgressPage; DialogPage* mDialogPage; Importer* mImporter; + void checkForFailedUrls() + { + // First check for errors on file imports or subfolder creation + QList failedUrls = mImporter->failedUrlList(); + QList failedSubFolders = mImporter->failedSubFolderList(); + int failedUrlCount = failedUrls.count(); + int failedSubFolderCount = failedSubFolders.count(); + if (failedUrlCount + failedSubFolderCount > 0) { + QStringList files, dirs; + for (int i=0; ishowErrors(files, dirs); + } + } + void deleteImportedUrls() { QList importedUrls = mImporter->importedUrlList(); QList skippedUrls = mImporter->skippedUrlList(); int importedCount = importedUrls.count(); int skippedCount = skippedUrls.count(); if (importedCount == 0 && skippedCount == 0) { return; } QStringList message; message << i18np( "One document has been imported.", "%1 documents have been imported.", importedCount); if (skippedCount > 0) { message << i18np( "One document has been skipped because it had already been imported.", "%1 documents have been skipped because they had already been imported.", skippedCount); } if (mImporter->renamedCount() > 0) { message[0].append("*"); message << "* " + i18np( "One of them has been renamed because another document with the same name had already been imported.", "%1 of them have been renamed because other documents with the same name had already been imported.", mImporter->renamedCount()) + ""; } message << QString(); if (skippedCount == 0) { message << i18np( "Delete the imported document from the device?", "Delete the %1 imported documents from the device?", importedCount); } else if (importedCount == 0) { message << i18np( "Delete the skipped document from the device?", "Delete the %1 skipped documents from the device?", skippedCount); } else { message << i18ncp( "Singular sentence is actually never used.", "Delete the imported or skipped document from the device?", "Delete the %1 imported and skipped documents from the device?", importedCount + skippedCount); } int answer = KMessageBox::questionYesNo(mCentralWidget, "" + message.join("
") + "
", i18nc("@title:window", "Import Finished"), KGuiItem(i18n("Keep")), KStandardGuiItem::del() ); if (answer == KMessageBox::Yes) { return; } QList urls = importedUrls + skippedUrls; while (true) { KIO::Job* job = KIO::del(urls); if (job->exec()) { break; } // Deleting failed int answer = KMessageBox::warningYesNo(mCentralWidget, i18np("Failed to delete the document:\n%2", "Failed to delete documents:\n%2", urls.count(), job->errorString()), QString(), KGuiItem(i18n("Retry")), KGuiItem(i18n("Ignore")) ); if (answer != KMessageBox::Yes) { // Ignore break; } } } void startGwenview() { KService::Ptr service = KService::serviceByDesktopName("org.kde.gwenview"); if (!service) { qCritical() << "Could not find gwenview"; } else { KRun::runService(*service, {mThumbnailPage->destinationUrl()}, nullptr /* window */); } } void showWhatNext() { mCentralWidget->setCurrentWidget(mDialogPage); mDialogPage->setText(i18n("What do you want to do now?")); mDialogPage->removeButtons(); int gwenview = mDialogPage->addButton(KGuiItem(i18n("View Imported Documents with Gwenview"), "gwenview")); int importMore = mDialogPage->addButton(KGuiItem(i18n("Import more Documents"))); mDialogPage->addButton(KGuiItem(i18n("Quit"), "dialog-cancel")); int answer = mDialogPage->exec(); if (answer == gwenview) { startGwenview(); qApp->quit(); } else if (answer == importMore) { mCentralWidget->setCurrentWidget(mThumbnailPage); } else { /* quit */ qApp->quit(); } } }; ImportDialog::ImportDialog() : d(new ImportDialogPrivate) { d->q = this; d->mImporter = new Importer(this); connect(d->mImporter, &Importer::error, this, &ImportDialog::showImportError); d->mThumbnailPage = new ThumbnailPage; QUrl url = ImporterConfig::destinationUrl(); if (!url.isValid()) { url = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::PicturesLocation)); int year = QDate::currentDate().year(); url.setPath(url.path() + '/' + QString::number(year)); } d->mThumbnailPage->setDestinationUrl(url); d->mProgressPage = new ProgressPage(d->mImporter); d->mDialogPage = new DialogPage; d->mCentralWidget = new QStackedWidget; setCentralWidget(d->mCentralWidget); d->mCentralWidget->addWidget(d->mThumbnailPage); d->mCentralWidget->addWidget(d->mProgressPage); d->mCentralWidget->addWidget(d->mDialogPage); connect(d->mThumbnailPage, &ThumbnailPage::importRequested, this, &ImportDialog::startImport); connect(d->mThumbnailPage, &ThumbnailPage::rejected, this, &QWidget::close); connect(d->mImporter, &Importer::importFinished, this, &ImportDialog::slotImportFinished); + connect(this, &ImportDialog::showErrors, + d->mDialogPage, &DialogPage::slotShowErrors); d->mCentralWidget->setCurrentWidget(d->mThumbnailPage); setWindowIcon(QIcon::fromTheme("gwenview")); setAutoSaveSettings(); } ImportDialog::~ImportDialog() { delete d; } QSize ImportDialog::sizeHint() const { return QSize(700, 500); } void ImportDialog::setSourceUrl(const QUrl& url, const QString& deviceUdi) { QString name, iconName; if (deviceUdi.isEmpty()) { name = url.url(QUrl::PreferLocalFile); iconName = KProtocolInfo::icon(url.scheme()); if (iconName.isEmpty()) { iconName = "folder"; } } else { Solid::Device device(deviceUdi); name = device.vendor() + ' ' + device.product(); iconName = device.icon(); } d->mThumbnailPage->setSourceUrl(url, iconName, name); } void ImportDialog::startImport() { QUrl url = d->mThumbnailPage->destinationUrl(); ImporterConfig::setDestinationUrl(url); ImporterConfig::self()->save(); d->mCentralWidget->setCurrentWidget(d->mProgressPage); d->mImporter->setAutoRenameFormat( ImporterConfig::autoRename() ? ImporterConfig::autoRenameFormat() : QString()); d->mImporter->start(d->mThumbnailPage->urlList(), url); } void ImportDialog::slotImportFinished() { + d->checkForFailedUrls(); d->deleteImportedUrls(); d->showWhatNext(); } void ImportDialog::showImportError(const QString& message) { KMessageBox::sorry(this, message); d->mCentralWidget->setCurrentWidget(d->mThumbnailPage); } } // namespace diff --git a/importer/importdialog.h b/importer/importdialog.h index 3e435c61..850f543d 100644 --- a/importer/importdialog.h +++ b/importer/importdialog.h @@ -1,59 +1,62 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2009 Aurélien Gâteau 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 2 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA. */ #ifndef IMPORTDIALOG_H #define IMPORTDIALOG_H // Qt #include // KDE #include // Local namespace Gwenview { class ImportDialogPrivate; class ImportDialog : public KMainWindow { Q_OBJECT public: ImportDialog(); ~ImportDialog() override; QSize sizeHint() const override; public Q_SLOTS: void setSourceUrl(const QUrl&, const QString& deviceUdi); private Q_SLOTS: void startImport(); void slotImportFinished(); void showImportError(const QString&); +Q_SIGNALS: + void showErrors(const QStringList&, const QStringList&); + private: ImportDialogPrivate* const d; }; } // namespace #endif /* IMPORTDIALOG_H */ diff --git a/importer/importer.cpp b/importer/importer.cpp index 4648704b..3b10e69e 100644 --- a/importer/importer.cpp +++ b/importer/importer.cpp @@ -1,259 +1,278 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2009 Aurélien Gâteau 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 2 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA. */ // Self #include "importer.h" // Qt #include #include #include #include // KDE #include #include #include #include #include #include #include #include // stdc++ #include // Local #include #include #include #include namespace Gwenview { struct ImporterPrivate { Importer* q; QWidget* mAuthWindow; std::unique_ptr mFileNameFormater; QUrl mTempImportDirUrl; QTemporaryDir* mTempImportDir; /* @defgroup reset Should be reset in start() * @{ */ QList mUrlList; QList mImportedUrlList; QList mSkippedUrlList; + QList mFailedUrlList; + QList mFailedSubFolderList; int mRenamedCount; int mProgress; int mJobProgress; /* @} */ QUrl mCurrentUrl; bool createImportDir(const QUrl& url) { Q_ASSERT(url.isLocalFile()); // FIXME: Support remote urls if (!QDir().mkpath(url.toLocalFile())) { emit q->error(i18n("Could not create destination folder.")); return false; } QString tempDirPath = url.toLocalFile() + "/.gwenview_importer-XXXXXX"; mTempImportDir = new QTemporaryDir(tempDirPath); if (!mTempImportDir->isValid()) { emit q->error(i18n("Could not create temporary upload folder.")); return false; } mTempImportDirUrl = QUrl::fromLocalFile(mTempImportDir->path() + '/'); if (!mTempImportDirUrl.isValid()) { emit q->error(i18n("Could not create temporary upload folder.")); return false; } return true; } void importNext() { if (mUrlList.empty()) { q->finalizeImport(); return; } mCurrentUrl = mUrlList.takeFirst(); QUrl dst = mTempImportDirUrl; dst.setPath(dst.path() + mCurrentUrl.fileName()); KIO::Job* job = KIO::copy(mCurrentUrl, dst, KIO::HideProgressInfo); KJobWidgets::setWindow(job, mAuthWindow); QObject::connect(job, &KJob::result, q, &Importer::slotCopyDone); QObject::connect(job, SIGNAL(percent(KJob*,ulong)), q, SLOT(slotPercent(KJob*,ulong))); } void renameImportedUrl(const QUrl& src) { QUrl dst = src.resolved(QUrl("..")); QString fileName; if (mFileNameFormater.get()) { KFileItem item(src); item.setDelayedMimeTypes(true); // Get the document time, but do not cache the result because the // 'src' url is temporary: if we import "foo/image.jpg" and // "bar/image.jpg", both images will be temporarily saved in the // 'src' url. QDateTime dateTime = TimeUtils::dateTimeForFileItem(item, TimeUtils::SkipCache); fileName = mFileNameFormater->format(src, dateTime); } else { fileName = src.fileName(); } dst.setPath(dst.path() + fileName); FileUtils::RenameResult result; // Create additional subfolders if needed (e.g. when extra slashes in FileNameFormater) QUrl subFolder = dst.adjusted(QUrl::RemoveFilename); KIO::Job* job = KIO::mkpath(subFolder, QUrl(), KIO::HideProgressInfo); KJobWidgets::setWindow(job,mAuthWindow); if (!job->exec()) { // if subfolder creation fails qWarning() << "Could not create subfolder:" << subFolder; + if (!mFailedSubFolderList.contains(subFolder)) { + mFailedSubFolderList << subFolder; + } result = FileUtils::RenameFailed; } else { // if subfolder creation succeeds result = FileUtils::rename(src, dst, mAuthWindow); } switch (result) { case FileUtils::RenamedOK: mImportedUrlList << mCurrentUrl; break; case FileUtils::RenamedUnderNewName: mRenamedCount++; mImportedUrlList << mCurrentUrl; break; case FileUtils::Skipped: mSkippedUrlList << mCurrentUrl; break; case FileUtils::RenameFailed: + mFailedUrlList << mCurrentUrl; qWarning() << "Rename failed for" << mCurrentUrl; } q->advance(); importNext(); } }; Importer::Importer(QWidget* parent) : QObject(parent) , d(new ImporterPrivate) { d->q = this; d->mAuthWindow = parent; } Importer::~Importer() { delete d; } void Importer::setAutoRenameFormat(const QString& format) { if (format.isEmpty()) { d->mFileNameFormater.reset(nullptr); } else { d->mFileNameFormater.reset(new FileNameFormater(format)); } } void Importer::start(const QList& list, const QUrl& destination) { d->mUrlList = list; d->mImportedUrlList.clear(); d->mSkippedUrlList.clear(); + d->mFailedUrlList.clear(); + d->mFailedSubFolderList.clear(); d->mRenamedCount = 0; d->mProgress = 0; d->mJobProgress = 0; emitProgressChanged(); emit maximumChanged(d->mUrlList.count() * 100); if (!d->createImportDir(destination)) { qWarning() << "Could not create import dir"; return; } d->importNext(); } void Importer::slotCopyDone(KJob* _job) { KIO::CopyJob* job = static_cast(_job); QUrl url = job->destUrl(); if (job->error()) { - qWarning() << "FIXME: What do we do with failed urls?"; + // Add document to failed url list and proceed with next one + d->mFailedUrlList << d->mCurrentUrl; advance(); d->importNext(); return; } d->renameImportedUrl(url); } void Importer::finalizeImport() { delete d->mTempImportDir; emit importFinished(); } void Importer::advance() { ++d->mProgress; d->mJobProgress = 0; emitProgressChanged(); } void Importer::slotPercent(KJob*, unsigned long percent) { d->mJobProgress = percent; emitProgressChanged(); } void Importer::emitProgressChanged() { emit progressChanged(d->mProgress * 100 + d->mJobProgress); } QList Importer::importedUrlList() const { return d->mImportedUrlList; } QList Importer::skippedUrlList() const { return d->mSkippedUrlList; } +QList Importer::failedUrlList() const +{ + return d->mFailedUrlList; +} + +QList Importer::failedSubFolderList() const +{ + return d->mFailedSubFolderList; +} + int Importer::renamedCount() const { return d->mRenamedCount; } } // namespace diff --git a/importer/importer.h b/importer/importer.h index 0ac4c288..b5b9bc9c 100644 --- a/importer/importer.h +++ b/importer/importer.h @@ -1,92 +1,102 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2009 Aurélien Gâteau 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 2 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA. */ #ifndef IMPORTER_H #define IMPORTER_H // Qt #include #include // KDE // Local class KJob; namespace Gwenview { struct ImporterPrivate; class Importer : public QObject { Q_OBJECT public: explicit Importer(QWidget* authWindow); ~Importer() override; /** * Defines the auto-rename format applied to imported documents * Set to QString() to reset */ void setAutoRenameFormat(const QString&); void start(const QList& list, const QUrl& destUrl); QList importedUrlList() const; /** * Documents which have been skipped during import */ QList skippedUrlList() const; + /** + * Documents which have failed during import + */ + QList failedUrlList() const; + + /** + * Subfolders which failed to create during import + */ + QList failedSubFolderList() const; + /** * How many documents have been renamed during import */ int renamedCount() const; Q_SIGNALS: void importFinished(); void progressChanged(int); void maximumChanged(int); /** * An error has occurred and caused the whole process to stop without * importing anything */ void error(const QString& message); private Q_SLOTS: void slotCopyDone(KJob*); void slotPercent(KJob*, unsigned long); void emitProgressChanged(); private: friend struct ImporterPrivate; ImporterPrivate* const d; void advance(); void finalizeImport(); }; } // namespace #endif /* IMPORTER_H */