File indexing completed on 2024-04-28 04:52:13
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 }