File indexing completed on 2024-04-14 08:26:19

0001 /*
0002     SPDX-FileCopyrightText: 2011 Jean-Baptiste Mardelle <jb@kdenlive.org>
0003     SPDX-FileCopyrightText: 2021 Julius Künzel <jk.kdedev@smartalb.uber.space>
0004 
0005 SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0006 */
0007 
0008 #include "archivewidget.h"
0009 #include "bin/bin.h"
0010 #include "bin/projectclip.h"
0011 #include "bin/projectfolder.h"
0012 #include "bin/projectitemmodel.h"
0013 #include "core.h"
0014 #include "projectsettings.h"
0015 #include "titler/titlewidget.h"
0016 #include "utils/qstringutils.h"
0017 #include "xml/xml.hpp"
0018 
0019 #include "doc/kdenlivedoc.h"
0020 #include "kdenlive_debug.h"
0021 #include "utils/KMessageBox_KdenliveCompat.h"
0022 #include <KGuiItem>
0023 #include <KLocalizedString>
0024 #include <KMessageBox>
0025 #include <KMessageWidget>
0026 #include <KTar>
0027 #include <KZip>
0028 #include <kio/directorysizejob.h>
0029 
0030 #include <QTreeWidget>
0031 #include <QtConcurrent>
0032 #include <utility>
0033 ArchiveWidget::ArchiveWidget(const QString &projectName, const QString &xmlData, const QStringList &luma_list, const QStringList &other_list, QWidget *parent)
0034     : QDialog(parent)
0035     , m_requestedSize(0)
0036     , m_copyJob(nullptr)
0037     , m_name(projectName.section(QLatin1Char('.'), 0, -2))
0038     , m_temp(nullptr)
0039     , m_abortArchive(false)
0040     , m_extractMode(false)
0041     , m_progressTimer(nullptr)
0042     , m_archive(nullptr)
0043     , m_missingClips(0)
0044 {
0045     setAttribute(Qt::WA_DeleteOnClose);
0046     setupUi(this);
0047     setWindowTitle(i18nc("@title:window", "Archive Project"));
0048     archive_url->setUrl(QUrl::fromLocalFile(QDir::homePath()));
0049     connect(archive_url, &KUrlRequester::textChanged, this, &ArchiveWidget::slotCheckSpace);
0050     connect(this, &ArchiveWidget::archivingFinished, this, &ArchiveWidget::slotArchivingBoolFinished);
0051     connect(this, &ArchiveWidget::archiveProgress, this, &ArchiveWidget::slotArchivingIntProgress);
0052     connect(proxy_only, &QCheckBox::stateChanged, this, &ArchiveWidget::slotProxyOnly);
0053     connect(timeline_archive, &QCheckBox::stateChanged, this, &ArchiveWidget::onlyTimelineItems);
0054 
0055     // Prepare xml
0056     m_doc.setContent(xmlData);
0057 
0058     // Setup categories
0059     QTreeWidgetItem *videos = new QTreeWidgetItem(files_list, QStringList() << i18n("Video Clips"));
0060     videos->setIcon(0, QIcon::fromTheme(QStringLiteral("video-x-generic")));
0061     videos->setData(0, Qt::UserRole, QStringLiteral("videos"));
0062     videos->setExpanded(false);
0063     QTreeWidgetItem *sounds = new QTreeWidgetItem(files_list, QStringList() << i18n("Audio Clips"));
0064     sounds->setIcon(0, QIcon::fromTheme(QStringLiteral("audio-x-generic")));
0065     sounds->setData(0, Qt::UserRole, QStringLiteral("sounds"));
0066     sounds->setExpanded(false);
0067     QTreeWidgetItem *images = new QTreeWidgetItem(files_list, QStringList() << i18n("Image Clips"));
0068     images->setIcon(0, QIcon::fromTheme(QStringLiteral("image-x-generic")));
0069     images->setData(0, Qt::UserRole, QStringLiteral("images"));
0070     images->setExpanded(false);
0071     QTreeWidgetItem *slideshows = new QTreeWidgetItem(files_list, QStringList() << i18n("Slideshow Clips"));
0072     slideshows->setIcon(0, QIcon::fromTheme(QStringLiteral("image-x-generic")));
0073     slideshows->setData(0, Qt::UserRole, QStringLiteral("slideshows"));
0074     slideshows->setExpanded(false);
0075     QTreeWidgetItem *texts = new QTreeWidgetItem(files_list, QStringList() << i18n("Text Clips"));
0076     texts->setIcon(0, QIcon::fromTheme(QStringLiteral("text-plain")));
0077     texts->setData(0, Qt::UserRole, QStringLiteral("texts"));
0078     texts->setExpanded(false);
0079     QTreeWidgetItem *playlists = new QTreeWidgetItem(files_list, QStringList() << i18n("Playlist Clips"));
0080     playlists->setIcon(0, QIcon::fromTheme(QStringLiteral("video-mlt-playlist")));
0081     playlists->setData(0, Qt::UserRole, QStringLiteral("playlist"));
0082     playlists->setExpanded(false);
0083     QTreeWidgetItem *others = new QTreeWidgetItem(files_list, QStringList() << i18n("Other Clips"));
0084     others->setIcon(0, QIcon::fromTheme(QStringLiteral("unknown")));
0085     others->setData(0, Qt::UserRole, QStringLiteral("others"));
0086     others->setExpanded(false);
0087     QTreeWidgetItem *lumas = new QTreeWidgetItem(files_list, QStringList() << i18n("Luma Files"));
0088     lumas->setIcon(0, QIcon::fromTheme(QStringLiteral("image-x-generic")));
0089     lumas->setData(0, Qt::UserRole, QStringLiteral("lumas"));
0090     lumas->setExpanded(false);
0091 
0092     QTreeWidgetItem *proxies = new QTreeWidgetItem(files_list, QStringList() << i18n("Proxy Clips"));
0093     proxies->setIcon(0, QIcon::fromTheme(QStringLiteral("video-x-generic")));
0094     proxies->setData(0, Qt::UserRole, QStringLiteral("proxy"));
0095     proxies->setExpanded(false);
0096 
0097     QTreeWidgetItem *subtitles = new QTreeWidgetItem(files_list, QStringList() << i18n("Subtitles"));
0098     subtitles->setIcon(0, QIcon::fromTheme(QStringLiteral("text-plain")));
0099     // subtitles->setData(0, Qt::UserRole, QStringLiteral("subtitles"));
0100     subtitles->setExpanded(false);
0101 
0102     QStringList subtitlePath = pCore->currentDoc()->getAllSubtitlesPath(true);
0103     for (auto &path : subtitlePath) {
0104         QFileInfo info(path);
0105         m_requestedSize += static_cast<KIO::filesize_t>(info.size());
0106         new QTreeWidgetItem(subtitles, QStringList() << path);
0107     }
0108 
0109     // process all files
0110     QStringList allFonts;
0111     QStringList extraImageUrls;
0112     QStringList otherUrls;
0113     Qt::CaseSensitivity sensitivity = Qt::CaseSensitive;
0114 #ifdef Q_OS_WIN
0115     // File names in Windows are not case sensitive. So "C:\my_file.mp4" and "c:\my_file.mp4" point to the same file, ensure we handle this
0116     sensitivity = Qt::CaseInsensitive;
0117     for (auto &u : other_list) {
0118         if (!otherUrls.contains(u, sensitivity)) {
0119             otherUrls << u;
0120         }
0121     }
0122 #else
0123     otherUrls << other_list;
0124 #endif
0125     generateItems(lumas, luma_list);
0126 
0127     QMap<QString, QString> slideUrls;
0128     QMap<QString, QString> audioUrls;
0129     QMap<QString, QString> videoUrls;
0130     QMap<QString, QString> imageUrls;
0131     QMap<QString, QString> playlistUrls;
0132     QMap<QString, QString> proxyUrls;
0133     QList<std::shared_ptr<ProjectClip>> clipList = pCore->projectItemModel()->getRootFolder()->childClips();
0134     QStringList handledUrls;
0135     for (const std::shared_ptr<ProjectClip> &clip : qAsConst(clipList)) {
0136         ClipType::ProducerType t = clip->clipType();
0137         if (t == ClipType::Color || t == ClipType::Timeline) {
0138             continue;
0139         }
0140         const QString id = clip->binId();
0141         const QString url = clip->clipUrl();
0142         if (t == ClipType::SlideShow) {
0143             // TODO: Slideshow files
0144             slideUrls.insert(id, url);
0145             handledUrls << url;
0146         } else if (t == ClipType::Image) {
0147             imageUrls.insert(id, url);
0148             handledUrls << url;
0149         } else if (t == ClipType::QText) {
0150             allFonts << clip->getProducerProperty(QStringLiteral("family"));
0151         } else if (t == ClipType::Text) {
0152             QString titleData = clip->getProducerProperty(QStringLiteral("xmldata"));
0153             QStringList imagefiles = TitleWidget::extractImageList(titleData, pCore->currentDoc()->documentRoot());
0154             QStringList fonts = TitleWidget::extractFontList(clip->getProducerProperty(QStringLiteral("xmldata")));
0155             extraImageUrls << imagefiles;
0156             allFonts << fonts;
0157         } else if (t == ClipType::Playlist) {
0158             playlistUrls.insert(id, url);
0159             handledUrls << url;
0160             const QStringList files = ProjectSettings::extractPlaylistUrls(clip->clipUrl());
0161             for (auto &f : files) {
0162                 if (handledUrls.contains(f, sensitivity)) {
0163                     continue;
0164                 }
0165                 otherUrls << f;
0166             }
0167         } else if (!clip->clipUrl().isEmpty()) {
0168             handledUrls << url;
0169             if (t == ClipType::Audio) {
0170                 audioUrls.insert(id, url);
0171             } else {
0172                 videoUrls.insert(id, url);
0173                 // Check if we have a proxy
0174                 QString proxy = clip->getProducerProperty(QStringLiteral("kdenlive:proxy"));
0175                 if (!proxy.isEmpty() && proxy != QLatin1String("-") && QFile::exists(proxy)) {
0176                     proxyUrls.insert(id, proxy);
0177                 }
0178             }
0179         }
0180         const QStringList files = clip->filesUsedByEffects();
0181         for (auto &f : files) {
0182             if (handledUrls.contains(f, sensitivity)) {
0183                 continue;
0184             }
0185             otherUrls << f;
0186             handledUrls << f;
0187         }
0188     }
0189 
0190     generateItems(images, extraImageUrls);
0191     generateItems(sounds, audioUrls);
0192     generateItems(videos, videoUrls);
0193     generateItems(images, imageUrls);
0194     generateItems(slideshows, slideUrls);
0195     generateItems(playlists, playlistUrls);
0196     otherUrls.removeDuplicates();
0197     generateItems(others, otherUrls);
0198     generateItems(proxies, proxyUrls);
0199 
0200     allFonts.removeDuplicates();
0201 
0202     m_infoMessage = new KMessageWidget(this);
0203     auto *s = static_cast<QVBoxLayout *>(layout());
0204     s->insertWidget(5, m_infoMessage);
0205     m_infoMessage->setCloseButtonVisible(false);
0206     m_infoMessage->setWordWrap(true);
0207     m_infoMessage->hide();
0208 
0209     // missing clips, warn user
0210     if (m_missingClips > 0) {
0211         QString infoText = i18np("You have %1 missing clip in your project.", "You have %1 missing clips in your project.", m_missingClips);
0212         m_infoMessage->setMessageType(KMessageWidget::Warning);
0213         m_infoMessage->setText(infoText);
0214         m_infoMessage->animatedShow();
0215     }
0216 
0217     // TODO: fonts
0218 
0219     // Hide unused categories, add item count
0220     int total = 0;
0221     for (int i = 0; i < files_list->topLevelItemCount(); ++i) {
0222         QTreeWidgetItem *parentItem = files_list->topLevelItem(i);
0223         int items = parentItem->childCount();
0224         if (items == 0) {
0225             files_list->topLevelItem(i)->setHidden(true);
0226         } else {
0227             if (parentItem->data(0, Qt::UserRole).toString() == QLatin1String("slideshows")) {
0228                 // Special case: slideshows contain several files
0229                 for (int j = 0; j < items; ++j) {
0230                     total += parentItem->child(j)->data(0, SlideshowImagesRole).toStringList().count();
0231                 }
0232             } else {
0233                 total += items;
0234             }
0235             parentItem->setText(0, files_list->topLevelItem(i)->text(0) + QLatin1Char(' ') + i18np("(%1 item)", "(%1 items)", items));
0236         }
0237     }
0238     if (m_name.isEmpty()) {
0239         m_name = i18n("Untitled");
0240     }
0241     project_files->setText(i18np("%1 file to archive, requires %2", "%1 files to archive, requires %2", total, KIO::convertSize(m_requestedSize)));
0242     buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Archive"));
0243     connect(buttonBox->button(QDialogButtonBox::Apply), &QAbstractButton::clicked, this, &ArchiveWidget::slotStartArchiving);
0244     buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false);
0245 
0246     slotCheckSpace();
0247 
0248     // Validate some basic project properties
0249     QDomElement mlt = m_doc.documentElement();
0250     QDomNodeList tracks = mlt.elementsByTagName(QStringLiteral("track"));
0251     if (tracks.size() == 0 || !xmlData.contains(QStringLiteral("kdenlive:docproperties.version"))) {
0252         m_infoMessage->setMessageType(KMessageWidget::Warning);
0253         m_infoMessage->setText(i18n("There was an error processing project file"));
0254         m_infoMessage->animatedShow();
0255         buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false);
0256     } else {
0257         // Don't allow archiving a modified or unsaved project
0258         if (pCore->currentDoc()->isModified()) {
0259             m_infoMessage->setMessageType(KMessageWidget::Warning);
0260             m_infoMessage->setText(i18n("Please save your project before archiving"));
0261             m_infoMessage->animatedShow();
0262             buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false);
0263         }
0264     }
0265 }
0266 
0267 // Constructor for extract widget
0268 ArchiveWidget::ArchiveWidget(QUrl url, QWidget *parent)
0269     : QDialog(parent)
0270     , m_requestedSize(0)
0271     , m_copyJob(nullptr)
0272     , m_temp(nullptr)
0273     , m_abortArchive(false)
0274     , m_extractMode(true)
0275     , m_extractUrl(std::move(url))
0276     , m_archive(nullptr)
0277     , m_missingClips(0)
0278     , m_infoMessage(nullptr)
0279 {
0280     // setAttribute(Qt::WA_DeleteOnClose);
0281 
0282     setupUi(this);
0283     m_progressTimer = new QTimer;
0284     m_progressTimer->setInterval(800);
0285     m_progressTimer->setSingleShot(false);
0286     connect(m_progressTimer, &QTimer::timeout, this, &ArchiveWidget::slotExtractProgress);
0287     connect(this, &ArchiveWidget::extractingFinished, this, &ArchiveWidget::slotExtractingFinished);
0288     connect(this, &ArchiveWidget::showMessage, this, &ArchiveWidget::slotDisplayMessage);
0289 
0290     compressed_archive->setHidden(true);
0291     proxy_only->setHidden(true);
0292     project_files->setHidden(true);
0293     files_list->setHidden(true);
0294     timeline_archive->setHidden(true);
0295     compression_type->setHidden(true);
0296     label->setText(i18n("Extract to"));
0297     setWindowTitle(i18nc("@title:window", "Open Archived Project"));
0298     archive_url->setUrl(QUrl::fromLocalFile(QDir::homePath()));
0299     buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Extract"));
0300     connect(buttonBox->button(QDialogButtonBox::Apply), &QAbstractButton::clicked, this, &ArchiveWidget::slotStartExtracting);
0301     buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true);
0302     adjustSize();
0303 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0304     m_archiveThread = QtConcurrent::run(this, &ArchiveWidget::openArchiveForExtraction);
0305 #else
0306     m_archiveThread = QtConcurrent::run(&ArchiveWidget::openArchiveForExtraction, this);
0307 #endif
0308 }
0309 
0310 ArchiveWidget::~ArchiveWidget()
0311 {
0312     delete m_archive;
0313     delete m_progressTimer;
0314 }
0315 
0316 void ArchiveWidget::slotDisplayMessage(const QString &icon, const QString &text)
0317 {
0318     icon_info->setPixmap(QIcon::fromTheme(icon).pixmap(16, 16));
0319     text_info->setText(text);
0320 }
0321 
0322 void ArchiveWidget::slotJobResult(bool success, const QString &text)
0323 {
0324     m_infoMessage->setMessageType(success ? KMessageWidget::Positive : KMessageWidget::Warning);
0325     m_infoMessage->setText(text);
0326     m_infoMessage->animatedShow();
0327     archive_url->setEnabled(true);
0328     compressed_archive->setEnabled(true);
0329     compression_type->setEnabled(true);
0330     proxy_only->setEnabled(true);
0331     timeline_archive->setEnabled(true);
0332     buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true);
0333     buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Archive"));
0334 }
0335 
0336 void ArchiveWidget::openArchiveForExtraction()
0337 {
0338     Q_EMIT showMessage(QStringLiteral("system-run"), i18n("Opening archive…"));
0339     QMimeDatabase db;
0340     QMimeType mime = db.mimeTypeForUrl(m_extractUrl);
0341     if (mime.inherits(QStringLiteral("application/x-compressed-tar"))) {
0342         m_archive = new KTar(m_extractUrl.toLocalFile());
0343     } else {
0344         m_archive = new KZip(m_extractUrl.toLocalFile());
0345     }
0346 
0347     if (!m_archive->isOpen() && !m_archive->open(QIODevice::ReadOnly)) {
0348         Q_EMIT showMessage(QStringLiteral("dialog-close"), i18n("Cannot open archive file:\n %1", m_extractUrl.toLocalFile()));
0349         groupBox->setEnabled(false);
0350         return;
0351     }
0352 
0353     // Check that it is a kdenlive project archive
0354     bool isProjectArchive = false;
0355     QStringList files = m_archive->directory()->entries();
0356     for (int i = 0; i < files.count(); ++i) {
0357         if (files.at(i).endsWith(QLatin1String(".kdenlive"))) {
0358             m_projectName = files.at(i);
0359             isProjectArchive = true;
0360             break;
0361         }
0362     }
0363 
0364     if (!isProjectArchive) {
0365         Q_EMIT showMessage(QStringLiteral("dialog-close"), i18n("File %1\n is not an archived Kdenlive project", m_extractUrl.toLocalFile()));
0366         groupBox->setEnabled(false);
0367         buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false);
0368         return;
0369     }
0370     buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true);
0371     Q_EMIT showMessage(QStringLiteral("dialog-ok"), i18n("Ready"));
0372 }
0373 
0374 void ArchiveWidget::done(int r)
0375 {
0376     if (closeAccepted()) {
0377         QDialog::done(r);
0378     }
0379 }
0380 
0381 void ArchiveWidget::closeEvent(QCloseEvent *e)
0382 {
0383 
0384     if (closeAccepted()) {
0385         e->accept();
0386     } else {
0387         e->ignore();
0388     }
0389 }
0390 
0391 bool ArchiveWidget::closeAccepted()
0392 {
0393     if (!m_extractMode && !archive_url->isEnabled()) {
0394         // Archiving in progress, should we stop?
0395         if (KMessageBox::warningContinueCancel(this, i18n("Archiving in progress, do you want to stop it?"), i18n("Stop Archiving"),
0396                                                KGuiItem(i18n("Stop Archiving"))) != KMessageBox::Continue) {
0397             return false;
0398         }
0399         m_infoMessage->setMessageType(KMessageWidget::Information);
0400         m_infoMessage->setText(i18n("Abort processing"));
0401         m_infoMessage->animatedShow();
0402         m_abortArchive = true;
0403         if (m_copyJob) {
0404             m_copyJob->kill();
0405         }
0406         m_archiveThread.waitForFinished();
0407     }
0408     return true;
0409 }
0410 
0411 void ArchiveWidget::generateItems(QTreeWidgetItem *parentItem, const QStringList &items)
0412 {
0413     QStringList filesList;
0414     QStringList filesPath;
0415     QString fileName;
0416     int ix = 0;
0417     bool isSlideshow = parentItem->data(0, Qt::UserRole).toString() == QLatin1String("slideshows");
0418     for (const QString &file : items) {
0419         fileName = QUrl::fromLocalFile(file).fileName();
0420         if (file.isEmpty() || fileName.isEmpty()) {
0421             continue;
0422         }
0423         auto *item = new QTreeWidgetItem(parentItem, QStringList() << file);
0424         if (isSlideshow) {
0425             // we store each slideshow in a separate subdirectory
0426             item->setData(0, Qt::UserRole, ix);
0427             ix++;
0428             QUrl slideUrl = QUrl::fromLocalFile(file);
0429             QDir dir(slideUrl.adjusted(QUrl::RemoveFilename).toLocalFile());
0430             if (slideUrl.fileName().startsWith(QLatin1String(".all."))) {
0431                 // MIME type slideshow (for example *.png)
0432                 QStringList filters;
0433                 // TODO: improve jpeg image detection with extension like jpeg, requires change in MLT image producers
0434                 filters << QStringLiteral("*.") + slideUrl.fileName().section(QLatin1Char('.'), -1);
0435                 dir.setNameFilters(filters);
0436                 QFileInfoList resultList = dir.entryInfoList(QDir::Files);
0437                 QStringList slideImages;
0438                 qint64 totalSize = 0;
0439                 for (int i = 0; i < resultList.count(); ++i) {
0440                     totalSize += resultList.at(i).size();
0441                     slideImages << resultList.at(i).absoluteFilePath();
0442                 }
0443                 item->setData(0, SlideshowImagesRole, slideImages);
0444                 item->setData(0, SlideshowSizeRole, totalSize);
0445                 m_requestedSize += static_cast<KIO::filesize_t>(totalSize);
0446             } else {
0447                 // pattern url (like clip%.3d.png)
0448                 QStringList result = dir.entryList(QDir::Files);
0449                 QString filter = slideUrl.fileName();
0450                 QString ext = filter.section(QLatin1Char('.'), -1);
0451                 filter = filter.section(QLatin1Char('%'), 0, -2);
0452                 QString regexp = QLatin1Char('^') + filter + QStringLiteral("\\d+\\.") + ext + QLatin1Char('$');
0453                 static const QRegularExpression rx(QRegularExpression::anchoredPattern(regexp));
0454                 QStringList slideImages;
0455                 QString directory = dir.absolutePath();
0456                 if (!directory.endsWith(QLatin1Char('/'))) {
0457                     directory.append(QLatin1Char('/'));
0458                 }
0459                 qint64 totalSize = 0;
0460                 for (const QString &path : qAsConst(result)) {
0461                     if (rx.match(path).hasMatch()) {
0462                         totalSize += QFileInfo(directory + path).size();
0463                         slideImages << directory + path;
0464                     }
0465                 }
0466                 item->setData(0, SlideshowImagesRole, slideImages);
0467                 item->setData(0, SlideshowSizeRole, totalSize);
0468                 m_requestedSize += static_cast<KIO::filesize_t>(totalSize);
0469             }
0470         } else if (filesList.contains(fileName) && !filesPath.contains(file)) {
0471             // we have 2 different files with same name
0472             const QString previousName = fileName;
0473             fileName = QStringUtils::getUniqueFileName(filesList, previousName);
0474             item->setData(0, Qt::UserRole, fileName);
0475         }
0476         if (!isSlideshow) {
0477             item->setData(0, IsInTimelineRole, 1);
0478             qint64 fileSize = QFileInfo(file).size();
0479             if (fileSize <= 0) {
0480                 item->setIcon(0, QIcon::fromTheme(QStringLiteral("edit-delete")));
0481                 m_missingClips++;
0482             } else {
0483                 m_requestedSize += static_cast<KIO::filesize_t>(fileSize);
0484                 item->setData(0, SlideshowSizeRole, fileSize);
0485             }
0486             filesList << fileName;
0487         }
0488         filesPath << file;
0489     }
0490 }
0491 
0492 void ArchiveWidget::generateItems(QTreeWidgetItem *parentItem, const QMap<QString, QString> &items)
0493 {
0494     QStringList filesList;
0495     QStringList filesPath;
0496     QString fileName;
0497     int ix = 0;
0498     bool isSlideshow = parentItem->data(0, Qt::UserRole).toString() == QLatin1String("slideshows");
0499     QMap<QString, QString>::const_iterator it = items.constBegin();
0500     const auto timelineBinId = pCore->bin()->getUsedClipIds();
0501     while (it != items.constEnd()) {
0502         QString file = it.value();
0503         QTreeWidgetItem *item = new QTreeWidgetItem(parentItem, QStringList() << file);
0504         item->setData(0, IsInTimelineRole, 0);
0505         for (int id : timelineBinId) {
0506             if (id == it.key().toInt()) {
0507                 m_timelineSize = static_cast<KIO::filesize_t>(QFileInfo(it.value()).size());
0508                 item->setData(0, IsInTimelineRole, 1);
0509             }
0510         }
0511         // Store the clip's id
0512         item->setData(0, ClipIdRole, it.key());
0513         fileName = QUrl::fromLocalFile(file).fileName();
0514         if (isSlideshow) {
0515             // we store each slideshow in a separate subdirectory
0516             item->setData(0, Qt::UserRole, ix);
0517             ix++;
0518             QUrl slideUrl = QUrl::fromLocalFile(file);
0519             QDir dir(slideUrl.adjusted(QUrl::RemoveFilename).toLocalFile());
0520             if (slideUrl.fileName().startsWith(QLatin1String(".all."))) {
0521                 // MIME type slideshow (for example *.png)
0522                 QStringList filters;
0523                 // TODO: improve jpeg image detection with extension like jpeg, requires change in MLT image producers
0524                 filters << QStringLiteral("*.") + slideUrl.fileName().section(QLatin1Char('.'), -1);
0525                 dir.setNameFilters(filters);
0526                 QFileInfoList resultList = dir.entryInfoList(QDir::Files);
0527                 QStringList slideImages;
0528                 qint64 totalSize = 0;
0529                 for (int i = 0; i < resultList.count(); ++i) {
0530                     totalSize += resultList.at(i).size();
0531                     slideImages << resultList.at(i).absoluteFilePath();
0532                 }
0533                 item->setData(0, SlideshowImagesRole, slideImages);
0534                 item->setData(0, SlideshowSizeRole, totalSize);
0535                 m_requestedSize += static_cast<KIO::filesize_t>(totalSize);
0536             } else {
0537                 // pattern url (like clip%.3d.png)
0538                 QStringList result = dir.entryList(QDir::Files);
0539                 QString filter = slideUrl.fileName();
0540                 QString ext = filter.section(QLatin1Char('.'), -1).section(QLatin1Char('?'), 0, 0);
0541                 filter = filter.section(QLatin1Char('%'), 0, -2);
0542                 QString regexp = QLatin1Char('^') + filter + QStringLiteral("\\d+\\.") + ext + QLatin1Char('$');
0543                 static const QRegularExpression rx(QRegularExpression::anchoredPattern(regexp));
0544                 QStringList slideImages;
0545                 qint64 totalSize = 0;
0546                 for (const QString &path : qAsConst(result)) {
0547                     if (rx.match(path).hasMatch()) {
0548                         totalSize += QFileInfo(dir.absoluteFilePath(path)).size();
0549                         slideImages << dir.absoluteFilePath(path);
0550                     }
0551                 }
0552                 item->setData(0, SlideshowImagesRole, slideImages);
0553                 item->setData(0, SlideshowSizeRole, totalSize);
0554                 m_requestedSize += static_cast<KIO::filesize_t>(totalSize);
0555             }
0556         } else if (filesList.contains(fileName) && !filesPath.contains(file)) {
0557             // we have 2 different files with same name
0558             const QString previousName = fileName;
0559             fileName = QStringUtils::getUniqueFileName(filesList, previousName);
0560             item->setData(0, Qt::UserRole, fileName);
0561         }
0562         if (!isSlideshow) {
0563             qint64 fileSize = QFileInfo(file).size();
0564             if (fileSize <= 0) {
0565                 item->setIcon(0, QIcon::fromTheme(QStringLiteral("edit-delete")));
0566                 m_missingClips++;
0567             } else {
0568                 m_requestedSize += static_cast<KIO::filesize_t>(fileSize);
0569                 item->setData(0, SlideshowSizeRole, fileSize);
0570             }
0571             filesList << fileName;
0572         }
0573         filesPath << file;
0574         ++it;
0575     }
0576 }
0577 
0578 void ArchiveWidget::slotCheckSpace()
0579 {
0580     QStorageInfo info(archive_url->url().toLocalFile());
0581     auto freeSize = static_cast<KIO::filesize_t>(info.bytesAvailable());
0582     if (freeSize > m_requestedSize) {
0583         // everything is ok
0584         buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true);
0585         slotDisplayMessage(QStringLiteral("dialog-ok"), i18n("Available space on drive: %1", KIO::convertSize(freeSize)));
0586     } else {
0587         buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false);
0588         slotDisplayMessage(QStringLiteral("dialog-close"), i18n("Not enough space on drive, free space: %1", KIO::convertSize(freeSize)));
0589     }
0590 }
0591 
0592 bool ArchiveWidget::slotStartArchiving(bool firstPass)
0593 {
0594     if (firstPass && ((m_copyJob != nullptr) || m_archiveThread.isRunning())) {
0595         // archiving in progress, abort
0596         if (m_copyJob) {
0597             m_copyJob->kill(KJob::EmitResult);
0598         }
0599         m_abortArchive = true;
0600         return true;
0601     }
0602     m_infoMessage->setMessageType(KMessageWidget::Information);
0603     m_infoMessage->setText(i18n("Starting archive job"));
0604     m_infoMessage->animatedShow();
0605     archive_url->setEnabled(false);
0606     compressed_archive->setEnabled(false);
0607     compression_type->setEnabled(false);
0608     proxy_only->setEnabled(false);
0609     timeline_archive->setEnabled(false);
0610     buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false);
0611     buttonBox->button(QDialogButtonBox::Close)->setText(i18n("Abort"));
0612 
0613     bool isArchive = compressed_archive->isChecked();
0614     if (!firstPass) {
0615         m_copyJob = nullptr;
0616     } else {
0617         // starting archiving
0618         m_abortArchive = false;
0619         m_duplicateFiles.clear();
0620         m_replacementList.clear();
0621         m_foldersList.clear();
0622         m_filesList.clear();
0623         m_processedFiles.clear();
0624         slotDisplayMessage(QStringLiteral("system-run"), i18n("Archiving…"));
0625         repaint();
0626     }
0627     QList<QUrl> files;
0628     QDir destUrl;
0629     QString destPath;
0630     QTreeWidgetItem *parentItem;
0631     bool isSlideshow = false;
0632     int items = 0;
0633     bool isLastCategory = false;
0634 
0635     // We parse all files going into one folder, then start the copy job
0636     for (int i = 0; i < files_list->topLevelItemCount(); ++i) {
0637         parentItem = files_list->topLevelItem(i);
0638         if (parentItem->isDisabled() || parentItem->childCount() == 0) {
0639             parentItem->setDisabled(true);
0640             parentItem->setExpanded(false);
0641             if (i == files_list->topLevelItemCount() - 1) {
0642                 isLastCategory = true;
0643                 break;
0644             }
0645             continue;
0646         }
0647 
0648         if (parentItem->data(0, Qt::UserRole).toString() == QLatin1String("slideshows")) {
0649             QUrl slideFolder = QUrl::fromLocalFile(archive_url->url().toLocalFile() + QStringLiteral("/slideshows"));
0650             if (isArchive) {
0651                 m_foldersList.append(QStringLiteral("slideshows"));
0652             } else {
0653                 QDir dir(slideFolder.toLocalFile());
0654                 if (!dir.mkpath(QStringLiteral("."))) {
0655                     KMessageBox::error(this, i18n("Cannot create directory %1", slideFolder.toLocalFile()));
0656                 }
0657             }
0658             isSlideshow = true;
0659         } else {
0660             isSlideshow = false;
0661         }
0662         files_list->setCurrentItem(parentItem);
0663         parentItem->setExpanded(true);
0664         destPath = parentItem->data(0, Qt::UserRole).toString() + QLatin1Char('/');
0665         destUrl = QDir(archive_url->url().toLocalFile() + QLatin1Char('/') + destPath);
0666         QTreeWidgetItem *item;
0667         for (int j = 0; j < parentItem->childCount(); ++j) {
0668             item = parentItem->child(j);
0669             if (item->isDisabled() || item->isHidden()) {
0670                 continue;
0671             }
0672             if (m_processedFiles.contains(item->text(0))) {
0673                 // File was already processed, rename
0674                 continue;
0675             }
0676             m_processedFiles << item->text(0);
0677             items++;
0678             if (parentItem->data(0, Qt::UserRole).toString() == QLatin1String("playlist")) {
0679                 // Special case: playlists (mlt files) may contain urls that need to be replaced too
0680                 QString filename(QUrl::fromLocalFile(item->text(0)).fileName());
0681                 m_infoMessage->setText(i18n("Copying %1", filename));
0682                 const QString playList = processPlaylistFile(item->text(0));
0683                 if (isArchive) {
0684                     m_temp = new QTemporaryFile();
0685                     if (!m_temp->open()) {
0686                         KMessageBox::error(this, i18n("Cannot create temporary file"));
0687                     }
0688                     m_temp->write(playList.toUtf8());
0689                     m_temp->close();
0690                     m_filesList.insert(m_temp->fileName(), destPath + filename);
0691                 } else {
0692                     if (!destUrl.mkpath(QStringLiteral("."))) {
0693                         KMessageBox::error(this, i18n("Cannot create directory %1", destUrl.absolutePath()));
0694                     }
0695                     QFile file(destUrl.absoluteFilePath(filename));
0696                     if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
0697                         qCWarning(KDENLIVE_LOG) << "//////  ERROR writing to file: " << file.fileName();
0698                         KMessageBox::error(this, i18n("Cannot write to file %1", file.fileName()));
0699                     }
0700                     file.write(playList.toUtf8());
0701                     if (file.error() != QFile::NoError) {
0702                         KMessageBox::error(this, i18n("Cannot write to file %1", file.fileName()));
0703                         file.close();
0704                         return false;
0705                     }
0706                     file.close();
0707                 }
0708             } else if (isSlideshow) {
0709                 // Special case: slideshows
0710                 destPath += item->data(0, Qt::UserRole).toString() + QLatin1Char('/');
0711                 destUrl = QDir(archive_url->url().toLocalFile() + QDir::separator() + destPath);
0712                 QStringList srcFiles = item->data(0, SlideshowImagesRole).toStringList();
0713                 for (int k = 0; k < srcFiles.count(); ++k) {
0714                     files << QUrl::fromLocalFile(srcFiles.at(k));
0715                 }
0716                 item->setDisabled(true);
0717                 if (parentItem->indexOfChild(item) == parentItem->childCount() - 1) {
0718                     // We have processed all slideshows
0719                     parentItem->setDisabled(true);
0720                 }
0721                 // Slideshows are processed one by one, we call slotStartArchiving after each item
0722                 break;
0723             } else if (item->data(0, Qt::UserRole).isNull()) {
0724                 files << QUrl::fromLocalFile(item->text(0));
0725             } else {
0726                 // We must rename the destination file, since another file with same name exists
0727                 // TODO: monitor progress
0728                 if (isArchive) {
0729                     m_filesList.insert(item->text(0), destPath + item->data(0, Qt::UserRole).toString());
0730                 } else {
0731                     m_duplicateFiles.insert(QUrl::fromLocalFile(item->text(0)),
0732                                             QUrl::fromLocalFile(destUrl.absoluteFilePath(item->data(0, Qt::UserRole).toString())));
0733                 }
0734             }
0735         }
0736         if (!isSlideshow) {
0737             // Slideshow is processed one by one and parent is disabled only once all items are done
0738             parentItem->setDisabled(true);
0739         }
0740         // We process each clip category one by one and call slotStartArchiving recursively
0741         break;
0742     }
0743 
0744     if (items == 0 && isLastCategory && m_duplicateFiles.isEmpty()) {
0745         // No clips to archive
0746         slotArchivingFinished(nullptr, true);
0747         return true;
0748     }
0749 
0750     if (destPath.isEmpty()) {
0751         if (m_duplicateFiles.isEmpty()) {
0752             return false;
0753         }
0754         QMapIterator<QUrl, QUrl> i(m_duplicateFiles);
0755         if (i.hasNext()) {
0756             i.next();
0757             const QUrl startJobSrc = i.key();
0758             const QUrl startJobDst = i.value();
0759             m_duplicateFiles.remove(startJobSrc);
0760             m_infoMessage->setText(i18n("Copying %1", startJobSrc.fileName()));
0761             KIO::CopyJob *job = KIO::copyAs(startJobSrc, startJobDst, KIO::HideProgressInfo);
0762             connect(job, &KJob::result, this, [this](KJob *jb) { slotArchivingFinished(jb, false); });
0763             connect(job, &KJob::processedSize, this, &ArchiveWidget::slotArchivingProgress);
0764         }
0765         return true;
0766     }
0767 
0768     if (isArchive) {
0769         m_foldersList.append(destPath);
0770         for (int i = 0; i < files.count(); ++i) {
0771             if (!m_filesList.contains(files.at(i).toLocalFile())) {
0772                 m_filesList.insert(files.at(i).toLocalFile(), destPath + files.at(i).fileName());
0773             }
0774         }
0775         slotArchivingFinished();
0776     } else if (files.isEmpty()) {
0777         slotStartArchiving(false);
0778     } else {
0779         if (!destUrl.mkpath(QStringLiteral("."))) {
0780             KMessageBox::error(this, i18n("Cannot create directory %1", destUrl.absolutePath()));
0781         }
0782         m_copyJob = KIO::copy(files, QUrl::fromLocalFile(destUrl.absolutePath()), KIO::HideProgressInfo);
0783         connect(m_copyJob, &KJob::result, this, [this](KJob *jb) { slotArchivingFinished(jb, false); });
0784         connect(m_copyJob, &KJob::processedSize, this, &ArchiveWidget::slotArchivingProgress);
0785     }
0786     if (firstPass) {
0787         progressBar->setValue(0);
0788         buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Abort"));
0789         buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true);
0790     }
0791     return true;
0792 }
0793 
0794 void ArchiveWidget::slotArchivingFinished(KJob *job, bool finished)
0795 {
0796     if (job == nullptr || job->error() == 0) {
0797         if (!finished && slotStartArchiving(false)) {
0798             // We still have files to archive
0799             return;
0800         }
0801         if (!compressed_archive->isChecked()) {
0802             // Archiving finished
0803             progressBar->setValue(100);
0804             if (processProjectFile()) {
0805                 slotJobResult(true, i18n("Project was successfully archived."));
0806             } else {
0807                 slotJobResult(false, i18n("There was an error processing project file"));
0808             }
0809             buttonBox->button(QDialogButtonBox::Close)->setText(i18n("Close"));
0810         } else {
0811             processProjectFile();
0812         }
0813     } else {
0814         m_copyJob = nullptr;
0815         slotJobResult(false, i18n("There was an error while copying the files: %1", job->errorString()));
0816     }
0817     if (!compressed_archive->isChecked()) {
0818         for (int i = 0; i < files_list->topLevelItemCount(); ++i) {
0819             files_list->topLevelItem(i)->setDisabled(false);
0820             for (int j = 0; j < files_list->topLevelItem(i)->childCount(); ++j) {
0821                 files_list->topLevelItem(i)->child(j)->setDisabled(false);
0822             }
0823         }
0824     }
0825 }
0826 
0827 void ArchiveWidget::slotArchivingProgress(KJob *, qulonglong size)
0828 {
0829     if (m_requestedSize == 0) {
0830         progressBar->setValue(100);
0831     } else {
0832         progressBar->setValue(static_cast<int>(100 * size / m_requestedSize));
0833     }
0834 }
0835 
0836 QString ArchiveWidget::processPlaylistFile(const QString &filename)
0837 {
0838     QDomDocument doc;
0839     if (!Xml::docContentFromFile(doc, filename, false)) {
0840         return QString();
0841     }
0842     return processMltFile(doc, QStringLiteral("../"));
0843 }
0844 
0845 bool ArchiveWidget::processProjectFile()
0846 {
0847     bool isArchive = compressed_archive->isChecked();
0848 
0849     QString playList = processMltFile(m_doc);
0850 
0851     m_archiveName.clear();
0852     if (isArchive) {
0853         m_temp = new QTemporaryFile;
0854         if (!m_temp->open()) {
0855             KMessageBox::error(this, i18n("Cannot create temporary file"));
0856         }
0857         m_temp->write(playList.toUtf8());
0858         m_temp->close();
0859         m_archiveName = QString(archive_url->url().toLocalFile() + QDir::separator() + m_name);
0860         if (compression_type->currentIndex() == 1) {
0861             m_archiveName.append(QStringLiteral(".zip"));
0862         } else {
0863             m_archiveName.append(QStringLiteral(".tar.gz"));
0864         };
0865         if (QFile::exists(m_archiveName) &&
0866             KMessageBox::questionTwoActions(nullptr, i18n("File %1 already exists.\nDo you want to overwrite it?", m_archiveName), {},
0867                                             KStandardGuiItem::overwrite(), KStandardGuiItem::cancel()) != KMessageBox::PrimaryAction) {
0868             return false;
0869         }
0870 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0871         m_archiveThread = QtConcurrent::run(this, &ArchiveWidget::createArchive);
0872 #else
0873         m_archiveThread = QtConcurrent::run(&ArchiveWidget::createArchive, this);
0874 #endif
0875         return true;
0876     }
0877 
0878     // Make a copy of original project file for extra safety
0879     QString path = archive_url->url().toLocalFile() + QDir::separator() + m_name + QStringLiteral("-backup.kdenlive");
0880     if (QFile::exists(path) && KMessageBox::warningTwoActions(this, i18n("File %1 already exists.\nDo you want to overwrite it?", path), {},
0881                                                               KStandardGuiItem::overwrite(), KStandardGuiItem::cancel()) != KMessageBox::PrimaryAction) {
0882         return false;
0883     }
0884     QFile::remove(path);
0885     QFile source(pCore->currentDoc()->url().toLocalFile());
0886     if (!source.copy(path)) {
0887         // Error
0888         KMessageBox::error(this, i18n("Cannot write to file %1", path));
0889         return false;
0890     }
0891 
0892     path = archive_url->url().toLocalFile() + QDir::separator() + m_name + QStringLiteral(".kdenlive");
0893     QFile file(path);
0894     if (file.exists() && KMessageBox::warningTwoActions(this, i18n("Output file already exists. Do you want to overwrite it?"), {},
0895                                                         KStandardGuiItem::overwrite(), KStandardGuiItem::cancel()) != KMessageBox::PrimaryAction) {
0896         return false;
0897     }
0898     if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
0899         qCWarning(KDENLIVE_LOG) << "//////  ERROR writing to file: " << path;
0900         KMessageBox::error(this, i18n("Cannot write to file %1", path));
0901         return false;
0902     }
0903 
0904     file.write(playList.toUtf8());
0905     if (file.error() != QFile::NoError) {
0906         KMessageBox::error(this, i18n("Cannot write to file %1", path));
0907         file.close();
0908         return false;
0909     }
0910     file.close();
0911     return true;
0912 }
0913 
0914 void ArchiveWidget::processElement(QDomElement e, const QString root)
0915 {
0916     if (e.isNull()) {
0917         return;
0918     }
0919     bool isTimewarp = Xml::getXmlProperty(e, QStringLiteral("mlt_service")) == QLatin1String("timewarp");
0920     QString src = Xml::getXmlProperty(e, QStringLiteral("resource"));
0921     if (!src.isEmpty()) {
0922         if (isTimewarp) {
0923             // Timewarp needs to be handled separately.
0924             src = Xml::getXmlProperty(e, QStringLiteral("warp_resource"));
0925         }
0926         if (QFileInfo(src).isRelative()) {
0927             src.prepend(root);
0928         }
0929         QUrl srcUrl = QUrl::fromLocalFile(src);
0930         QUrl dest = m_replacementList.value(srcUrl);
0931         if (!dest.isEmpty()) {
0932             if (isTimewarp) {
0933                 Xml::setXmlProperty(e, QStringLiteral("warp_resource"), dest.toLocalFile());
0934                 Xml::setXmlProperty(e, QStringLiteral("resource"),
0935                                     QString("%1:%2").arg(Xml::getXmlProperty(e, QStringLiteral("warp_speed")), dest.toLocalFile()));
0936             } else {
0937                 Xml::setXmlProperty(e, QStringLiteral("resource"), dest.toLocalFile());
0938             }
0939         }
0940     }
0941     src = Xml::getXmlProperty(e, QStringLiteral("kdenlive:proxy"));
0942     if (src.size() > 2) {
0943         if (QFileInfo(src).isRelative()) {
0944             src.prepend(root);
0945         }
0946         QUrl srcUrl = QUrl::fromLocalFile(src);
0947         QUrl dest = m_replacementList.value(srcUrl);
0948         if (!dest.isEmpty()) {
0949             Xml::setXmlProperty(e, QStringLiteral("kdenlive:proxy"), dest.toLocalFile());
0950         }
0951     }
0952     propertyProcessUrl(e, QStringLiteral("kdenlive:originalurl"), root);
0953     src = Xml::getXmlProperty(e, QStringLiteral("xmldata"));
0954     if (!src.isEmpty() && (src.contains(QLatin1String("QGraphicsPixmapItem")) || src.contains(QLatin1String("QGraphicsSvgItem")))) {
0955         bool found = false;
0956         // Title with images, replace paths
0957         QDomDocument titleXML;
0958         titleXML.setContent(src);
0959         QDomNodeList images = titleXML.documentElement().elementsByTagName(QLatin1String("item"));
0960         for (int j = 0; j < images.count(); ++j) {
0961             QDomNode n = images.at(j);
0962             QDomElement url = n.firstChildElement(QLatin1String("content"));
0963             if (!url.isNull() && url.hasAttribute(QLatin1String("url"))) {
0964                 QUrl srcUrl = QUrl::fromLocalFile(url.attribute(QLatin1String("url")));
0965                 QUrl dest = m_replacementList.value(srcUrl);
0966                 if (dest.isValid()) {
0967                     url.setAttribute(QLatin1String("url"), dest.toLocalFile());
0968                     found = true;
0969                 }
0970             }
0971         }
0972         if (found) {
0973             // replace content
0974             Xml::setXmlProperty(e, QStringLiteral("xmldata"), titleXML.toString());
0975         }
0976     }
0977     propertyProcessUrl(e, QStringLiteral("luma_file"), root);
0978 }
0979 
0980 QString ArchiveWidget::processMltFile(const QDomDocument &doc, const QString &destPrefix)
0981 {
0982     QTreeWidgetItem *item;
0983     bool isArchive = compressed_archive->isChecked();
0984 
0985     m_replacementList.clear();
0986     for (int i = 0; i < files_list->topLevelItemCount(); ++i) {
0987         QTreeWidgetItem *parentItem = files_list->topLevelItem(i);
0988         if (parentItem->childCount() > 0) {
0989             // QDir destFolder(archive_url->url().toLocalFile() + QDir::separator() + parentItem->data(0, Qt::UserRole).toString());
0990             bool isSlideshow = parentItem->data(0, Qt::UserRole).toString() == QLatin1String("slideshows");
0991             for (int j = 0; j < parentItem->childCount(); ++j) {
0992                 item = parentItem->child(j);
0993                 QUrl src = QUrl::fromLocalFile(item->text(0));
0994                 QUrl dest =
0995                     QUrl::fromLocalFile(destPrefix + parentItem->data(0, Qt::UserRole).toString() + QLatin1Char('/') + item->data(0, Qt::UserRole).toString());
0996                 if (isSlideshow) {
0997                     dest = QUrl::fromLocalFile(destPrefix + parentItem->data(0, Qt::UserRole).toString() + QLatin1Char('/') +
0998                                                item->data(0, Qt::UserRole).toString() + QLatin1Char('/') + src.fileName());
0999                 } else if (item->data(0, Qt::UserRole).isNull()) {
1000                     dest = QUrl::fromLocalFile(destPrefix + parentItem->data(0, Qt::UserRole).toString() + QLatin1Char('/') + src.fileName());
1001                 }
1002                 m_replacementList.insert(src, dest);
1003             }
1004         }
1005     }
1006 
1007     QDomElement mlt = doc.documentElement();
1008     QString root = mlt.attribute(QStringLiteral("root"));
1009     if (!root.isEmpty() && !root.endsWith(QLatin1Char('/'))) {
1010         root.append(QLatin1Char('/'));
1011     }
1012 
1013     // Adjust global settings
1014     QString basePath;
1015     if (isArchive) {
1016         basePath = QStringLiteral("$CURRENTPATH");
1017     } else {
1018         basePath = archive_url->url().adjusted(QUrl::StripTrailingSlash).toLocalFile();
1019     }
1020     // Switch to relative path
1021     mlt.removeAttribute(QStringLiteral("root"));
1022 
1023     // process mlt producers
1024     QDomNodeList prods = mlt.elementsByTagName(QStringLiteral("producer"));
1025     for (int i = 0; i < prods.count(); ++i) {
1026         processElement(prods.item(i).toElement(), root);
1027     }
1028     QDomNodeList chains = mlt.elementsByTagName(QStringLiteral("chain"));
1029     for (int i = 0; i < chains.count(); ++i) {
1030         processElement(chains.item(i).toElement(), root);
1031     }
1032 
1033     // process mlt transitions (for luma files)
1034     prods = mlt.elementsByTagName(QStringLiteral("transition"));
1035     for (int i = 0; i < prods.count(); ++i) {
1036         QDomElement e = prods.item(i).toElement();
1037         if (e.isNull()) {
1038             continue;
1039         }
1040         propertyProcessUrl(e, QStringLiteral("resource"), root);
1041         propertyProcessUrl(e, QStringLiteral("luma"), root);
1042         propertyProcessUrl(e, QStringLiteral("luma.resource"), root);
1043     }
1044 
1045     // process mlt filters
1046     prods = mlt.elementsByTagName(QStringLiteral("filter"));
1047     for (int i = 0; i < prods.count(); ++i) {
1048         QDomElement e = prods.item(i).toElement();
1049         if (e.isNull()) {
1050             continue;
1051         }
1052         // properties for vidstab files
1053         propertyProcessUrl(e, QStringLiteral("filename"), root);
1054         propertyProcessUrl(e, QStringLiteral("results"), root);
1055         // properties for LUT files
1056         propertyProcessUrl(e, QStringLiteral("av.file"), root);
1057     }
1058 
1059     QString playList = doc.toString();
1060     if (isArchive) {
1061         QString startString(QStringLiteral("\""));
1062         startString.append(archive_url->url().adjusted(QUrl::StripTrailingSlash).toLocalFile());
1063         QString endString(QStringLiteral("\""));
1064         endString.append(basePath);
1065         playList.replace(startString, endString);
1066         startString = QLatin1Char('>') + archive_url->url().adjusted(QUrl::StripTrailingSlash).toLocalFile();
1067         endString = QLatin1Char('>') + basePath;
1068         playList.replace(startString, endString);
1069     }
1070     return playList;
1071 }
1072 
1073 void ArchiveWidget::propertyProcessUrl(const QDomElement &e, const QString &propertyName, const QString &root)
1074 {
1075     QString src = Xml::getXmlProperty(e, propertyName);
1076     if (!src.isEmpty()) {
1077         qDebug() << "Found property " << propertyName << " with content: " << src;
1078         if (QFileInfo(src).isRelative()) {
1079             src.prepend(root);
1080         }
1081         QUrl srcUrl = QUrl::fromLocalFile(src);
1082         QUrl dest = m_replacementList.value(srcUrl);
1083         if (!dest.isEmpty()) {
1084             qDebug() << "-> hast replacement entry " << dest;
1085             Xml::setXmlProperty(e, propertyName, dest.toLocalFile());
1086         }
1087     }
1088 }
1089 
1090 void ArchiveWidget::createArchive()
1091 {
1092     QFileInfo dirInfo(archive_url->url().toLocalFile());
1093     QString user = dirInfo.owner();
1094     QString group = dirInfo.group();
1095     if (compression_type->currentIndex() == 1) {
1096         m_archive = new KZip(m_archiveName);
1097     } else {
1098         m_archive = new KTar(m_archiveName, QStringLiteral("application/x-gzip"));
1099     }
1100 
1101     QString errorString;
1102     bool success = true;
1103 
1104     if (!m_archive->isOpen() && !m_archive->open(QIODevice::WriteOnly)) {
1105         success = false;
1106         errorString = i18n("Cannot open archive file %1", m_archiveName);
1107     }
1108 
1109     // Create folders
1110     if (success) {
1111         for (const QString &path : qAsConst(m_foldersList)) {
1112             success = success && m_archive->writeDir(path, user, group);
1113         }
1114     }
1115 
1116     // Add files
1117     if (success) {
1118         int ix = 0;
1119         QMapIterator<QString, QString> i(m_filesList);
1120         int max = m_filesList.count();
1121         while (i.hasNext()) {
1122             i.next();
1123             m_infoMessage->setText(i18n("Archiving %1", i.key()));
1124             success = m_archive->addLocalFile(i.key(), i.value());
1125             Q_EMIT archiveProgress(100 * ix / max);
1126             ix++;
1127             if (!success || m_abortArchive) {
1128                 if (!success) {
1129                     errorString.append(i18n("Cannot copy file %1 to %2.", i.key(), i.value()));
1130                 }
1131                 break;
1132             }
1133         }
1134     }
1135 
1136     if (m_abortArchive) {
1137         return;
1138     }
1139 
1140     // Add project file
1141     if (!m_temp) {
1142         success = false;
1143     }
1144     if (success) {
1145         success = m_archive->addLocalFile(m_temp->fileName(), m_name + QStringLiteral(".kdenlive"));
1146         delete m_temp;
1147         m_temp = nullptr;
1148     }
1149 
1150     errorString.append(m_archive->errorString());
1151     success = success && m_archive->close();
1152 
1153     Q_EMIT archivingFinished(success, errorString);
1154 }
1155 
1156 void ArchiveWidget::slotArchivingBoolFinished(bool result, const QString &errorString)
1157 {
1158     if (result) {
1159         slotJobResult(true, i18n("Project was successfully archived.\n%1", m_archiveName));
1160         // buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false);
1161     } else {
1162         slotJobResult(false, i18n("There was an error while archiving the project: %1", errorString.isEmpty() ? i18n("Unknown Error") : errorString));
1163     }
1164     progressBar->setValue(100);
1165     for (int i = 0; i < files_list->topLevelItemCount(); ++i) {
1166         files_list->topLevelItem(i)->setDisabled(false);
1167         for (int j = 0; j < files_list->topLevelItem(i)->childCount(); ++j) {
1168             files_list->topLevelItem(i)->child(j)->setDisabled(false);
1169         }
1170     }
1171     buttonBox->button(QDialogButtonBox::Close)->setText(i18n("Close"));
1172 }
1173 
1174 void ArchiveWidget::slotArchivingIntProgress(int p)
1175 {
1176     progressBar->setValue(p);
1177 }
1178 
1179 void ArchiveWidget::slotStartExtracting()
1180 {
1181     if (m_archiveThread.isRunning()) {
1182         // TODO: abort extracting
1183         return;
1184     }
1185     QFileInfo f(m_extractUrl.toLocalFile());
1186     m_requestedSize = static_cast<KIO::filesize_t>(f.size());
1187     QDir dir(archive_url->url().toLocalFile());
1188     if (!dir.mkpath(QStringLiteral("."))) {
1189         KMessageBox::error(this, i18n("Cannot create directory %1", archive_url->url().toLocalFile()));
1190     }
1191     slotDisplayMessage(QStringLiteral("system-run"), i18n("Extracting…"));
1192     buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Abort"));
1193     buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true);
1194 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
1195     m_archiveThread = QtConcurrent::run(this, &ArchiveWidget::doExtracting);
1196 #else
1197     m_archiveThread = QtConcurrent::run(&ArchiveWidget::doExtracting, this);
1198 #endif
1199     m_progressTimer->start();
1200 }
1201 
1202 void ArchiveWidget::slotExtractProgress()
1203 {
1204     KIO::DirectorySizeJob *job = KIO::directorySize(archive_url->url());
1205     connect(job, &KJob::result, this, &ArchiveWidget::slotGotProgress);
1206 }
1207 
1208 void ArchiveWidget::slotGotProgress(KJob *job)
1209 {
1210     if (!job->error()) {
1211         auto *j = static_cast<KIO::DirectorySizeJob *>(job);
1212         progressBar->setValue(static_cast<int>(100 * j->totalSize() / m_requestedSize));
1213     }
1214     job->deleteLater();
1215 }
1216 
1217 void ArchiveWidget::doExtracting()
1218 {
1219     m_archive->directory()->copyTo(archive_url->url().toLocalFile() + QDir::separator());
1220     m_archive->close();
1221     Q_EMIT extractingFinished();
1222 }
1223 
1224 QString ArchiveWidget::extractedProjectFile() const
1225 {
1226     return archive_url->url().toLocalFile() + QDir::separator() + m_projectName;
1227 }
1228 
1229 void ArchiveWidget::slotExtractingFinished()
1230 {
1231     m_progressTimer->stop();
1232     // Process project file
1233     QFile file(extractedProjectFile());
1234     bool error = false;
1235     if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
1236         error = true;
1237     } else {
1238         QString playList = QString::fromUtf8(file.readAll());
1239         file.close();
1240         if (playList.isEmpty()) {
1241             error = true;
1242         } else {
1243             playList.replace(QLatin1String("$CURRENTPATH"), archive_url->url().adjusted(QUrl::StripTrailingSlash).toLocalFile());
1244             if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
1245                 qCWarning(KDENLIVE_LOG) << "//////  ERROR writing to file: ";
1246                 error = true;
1247             } else {
1248                 file.write(playList.toUtf8());
1249                 if (file.error() != QFile::NoError) {
1250                     error = true;
1251                 }
1252                 file.close();
1253             }
1254         }
1255     }
1256     if (error) {
1257         KMessageBox::error(QApplication::activeWindow(), i18n("Cannot open project file %1", extractedProjectFile()), i18n("Cannot open file"));
1258         reject();
1259     } else {
1260         accept();
1261     }
1262 }
1263 
1264 void ArchiveWidget::slotProxyOnly(int onlyProxy)
1265 {
1266     m_requestedSize = 0;
1267     if (onlyProxy == Qt::Checked) {
1268         // Archive proxy clips
1269         QStringList proxyIdList;
1270         QTreeWidgetItem *parentItem = nullptr;
1271 
1272         // Build list of existing proxy ids
1273         for (int i = 0; i < files_list->topLevelItemCount(); ++i) {
1274             parentItem = files_list->topLevelItem(i);
1275             if (parentItem->data(0, Qt::UserRole).toString() == QLatin1String("proxy")) {
1276                 break;
1277             }
1278         }
1279         if (!parentItem) {
1280             return;
1281         }
1282         int items = parentItem->childCount();
1283         for (int j = 0; j < items; ++j) {
1284             proxyIdList << parentItem->child(j)->data(0, ClipIdRole).toString();
1285         }
1286 
1287         // Parse all items to disable original clips for existing proxies
1288         for (int i = 0; i < proxyIdList.count(); ++i) {
1289             const QString &id = proxyIdList.at(i);
1290             if (id.isEmpty()) {
1291                 continue;
1292             }
1293             for (int j = 0; j < files_list->topLevelItemCount(); ++j) {
1294                 parentItem = files_list->topLevelItem(j);
1295                 if (parentItem->data(0, Qt::UserRole).toString() == QLatin1String("proxy")) {
1296                     continue;
1297                 }
1298                 items = parentItem->childCount();
1299                 for (int k = 0; k < items; ++k) {
1300                     if (parentItem->child(k)->data(0, ClipIdRole).toString() == id) {
1301                         // This item has a proxy, do not archive it
1302                         parentItem->child(k)->setFlags(Qt::ItemIsSelectable);
1303                         break;
1304                     }
1305                 }
1306             }
1307         }
1308     } else {
1309         // Archive all clips
1310         for (int i = 0; i < files_list->topLevelItemCount(); ++i) {
1311             QTreeWidgetItem *parentItem = files_list->topLevelItem(i);
1312             int items = parentItem->childCount();
1313             for (int j = 0; j < items; ++j) {
1314                 parentItem->child(j)->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
1315             }
1316         }
1317     }
1318 
1319     // Calculate requested size
1320     int total = 0;
1321     for (int i = 0; i < files_list->topLevelItemCount(); ++i) {
1322         QTreeWidgetItem *parentItem = files_list->topLevelItem(i);
1323         int items = parentItem->childCount();
1324         int itemsCount = 0;
1325         bool isSlideshow = parentItem->data(0, Qt::UserRole).toString() == QLatin1String("slideshows");
1326 
1327         for (int j = 0; j < items; ++j) {
1328             if (!parentItem->child(j)->isDisabled()) {
1329                 m_requestedSize += static_cast<KIO::filesize_t>(parentItem->child(j)->data(0, SlideshowSizeRole).toInt());
1330                 if (isSlideshow) {
1331                     total += parentItem->child(j)->data(0, SlideshowImagesRole).toStringList().count();
1332                 } else {
1333                     total++;
1334                 }
1335                 itemsCount++;
1336             }
1337         }
1338         parentItem->setText(0, parentItem->text(0).section(QLatin1Char('('), 0, 0) + i18np("(%1 item)", "(%1 items)", itemsCount));
1339     }
1340     project_files->setText(i18np("%1 file to archive, requires %2", "%1 files to archive, requires %2", total, KIO::convertSize(m_requestedSize)));
1341     slotCheckSpace();
1342 }
1343 
1344 void ArchiveWidget::onlyTimelineItems(int onlyTimeline)
1345 {
1346     int count = files_list->topLevelItemCount();
1347     for (int idx = 0; idx < count; ++idx) {
1348         QTreeWidgetItem *parent = files_list->topLevelItem(idx);
1349         int childCount = parent->childCount();
1350         for (int cidx = 0; cidx < childCount; ++cidx) {
1351             parent->child(cidx)->setHidden(true);
1352             if (onlyTimeline == Qt::Checked) {
1353                 if (parent->child(cidx)->data(0, IsInTimelineRole).toInt() > 0) {
1354                     parent->child(cidx)->setHidden(false);
1355                 }
1356             } else {
1357                 parent->child(cidx)->setHidden(false);
1358             }
1359         }
1360     }
1361 
1362     // calculating total number of files
1363     int total = 0;
1364     for (int i = 0; i < files_list->topLevelItemCount(); ++i) {
1365         QTreeWidgetItem *parentItem = files_list->topLevelItem(i);
1366         int items = parentItem->childCount();
1367         int itemsCount = 0;
1368         bool isSlideshow = parentItem->data(0, Qt::UserRole).toString() == QLatin1String("slideshows");
1369 
1370         for (int j = 0; j < items; ++j) {
1371             if (!parentItem->child(j)->isHidden() && !parentItem->child(j)->isDisabled()) {
1372                 if (isSlideshow) {
1373                     total += parentItem->child(j)->data(0, IsInTimelineRole).toStringList().count();
1374                 } else {
1375                     total++;
1376                 }
1377                 itemsCount++;
1378             }
1379         }
1380         parentItem->setText(0, parentItem->text(0).section(QLatin1Char('('), 0, 0) + i18np("(%1 item)", "(%1 items)", itemsCount));
1381     }
1382     project_files->setText(i18np("%1 file to archive, requires %2", "%1 files to archive, requires %2", total,
1383                                  KIO::convertSize((onlyTimeline == Qt::Checked) ? m_timelineSize : m_requestedSize)));
1384     slotCheckSpace();
1385 }