File indexing completed on 2024-04-21 04:51:19

0001 /*
0002     SPDX-FileCopyrightText: 2022 Gary Wang <wzc782970009@gmail.com>
0003 
0004     SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 #include "exportguidesdialog.h"
0008 
0009 #include "bin/model/markerlistmodel.hpp"
0010 #include "core.h"
0011 #include "doc/kdenlivedoc.h"
0012 #include "kdenlivesettings.h"
0013 #include "profiles/profilemodel.hpp"
0014 #include "project/projectmanager.h"
0015 
0016 #include "kdenlive_debug.h"
0017 #include <KMessageWidget>
0018 #include <QAction>
0019 #include <QButtonGroup>
0020 #include <QClipboard>
0021 #include <QDateTimeEdit>
0022 #include <QFileDialog>
0023 #include <QFontDatabase>
0024 #include <QPushButton>
0025 #include <QTime>
0026 
0027 #include "klocalizedstring.h"
0028 
0029 #define YT_FORMAT "{{timecode}} {{comment}}"
0030 
0031 ExportGuidesDialog::ExportGuidesDialog(const MarkerListModel *model, const GenTime duration, QWidget *parent)
0032     : QDialog(parent)
0033     , m_markerListModel(model)
0034     , m_projectDuration(duration)
0035 {
0036     //    setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
0037     setupUi(this);
0038     setWindowTitle(i18n("Export guides"));
0039 
0040     // We should setup TimecodeDisplay since it requires a proper Timecode
0041     offsetTimeSpinbox->setTimecode(Timecode(Timecode::HH_MM_SS_FF, pCore->getCurrentFps()));
0042 
0043     const QString defaultFormat(YT_FORMAT);
0044     formatEdit->setText(KdenliveSettings::exportGuidesFormat().isEmpty() ? defaultFormat : KdenliveSettings::exportGuidesFormat());
0045     categoryChooser->setMarkerModel(m_markerListModel);
0046     messageWidget->setText(i18n("If you are using the exported text for YouTube, you might want to check:\n"
0047                                 "1. The start time of 00:00 must have a chapter.\n"
0048                                 "2. There must be at least three timestamps in ascending order.\n"
0049                                 "3. The minimum length for video chapters is 10 seconds."));
0050     messageWidget->setVisible(false);
0051 
0052     updateContentByModel();
0053 
0054     QPushButton *btn = buttonBox->addButton(i18n("Copy to Clipboard"), QDialogButtonBox::ActionRole);
0055     btn->setIcon(QIcon::fromTheme("edit-copy"));
0056     QPushButton *btn2 = buttonBox->addButton(i18n("Save"), QDialogButtonBox::ActionRole);
0057     btn2->setIcon(QIcon::fromTheme("document-save"));
0058 
0059     connect(categoryChooser, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this]() { updateContentByModel(); });
0060 
0061     connect(offsetTimeComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int newIndex) {
0062         offsetTimeSpinbox->setEnabled(newIndex != 0);
0063         updateContentByModel();
0064     });
0065 
0066     connect(offsetTimeSpinbox, &TimecodeDisplay::timeCodeUpdated, this, [this]() { updateContentByModel(); });
0067 
0068     connect(formatEdit, &QLineEdit::textEdited, this, [this]() { updateContentByModel(); });
0069 
0070     connect(btn, &QAbstractButton::clicked, this, [this]() {
0071         QClipboard *clipboard = QGuiApplication::clipboard();
0072         clipboard->setText(this->generatedContent->toPlainText());
0073     });
0074 
0075     connect(btn2, &QAbstractButton::clicked, this, [this]() {
0076         QString filter = format_text->isChecked() ? QString("%1 (*.txt)").arg(i18n("Text File")) : QString("%1 (*.json)").arg(i18n("JSON File"));
0077         const QString startFolder = pCore->projectManager()->current()->projectDataFolder();
0078         QString filename = QFileDialog::getSaveFileName(this, i18nc("@title:window", "Export Guides Data"), startFolder, filter);
0079         QFile file(filename);
0080         if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
0081             messageWidget->setText(i18n("Cannot write to file %1", QUrl::fromLocalFile(filename).fileName()));
0082             messageWidget->setMessageType(KMessageWidget::Warning);
0083             messageWidget->animatedShow();
0084             return;
0085         }
0086         file.write(generatedContent->toPlainText().toUtf8());
0087         file.close();
0088         messageWidget->setText(i18n("Guides saved to %1", QUrl::fromLocalFile(filename).fileName()));
0089         messageWidget->setMessageType(KMessageWidget::Positive);
0090         messageWidget->animatedShow();
0091     });
0092     QButtonGroup *exportType = new QButtonGroup(this);
0093     exportType->addButton(format_text, 1);
0094     exportType->addButton(format_ffmpeg, 2);
0095     exportType->addButton(format_json, 3);
0096 
0097     connect(exportType, QOverload<QAbstractButton *, bool>::of(&QButtonGroup::buttonToggled), this, [this, exportType](QAbstractButton *button, bool checked) {
0098         if (checked) {
0099             textOptions->setEnabled(exportType->id(button) == 1);
0100             updateContentByModel();
0101         }
0102     });
0103 
0104     connect(buttonReset, &QAbstractButton::clicked, [this, defaultFormat]() {
0105         formatEdit->setText(defaultFormat);
0106         updateContentByModel();
0107     });
0108 
0109     // fill info button menu
0110     QMap<QString, QString> infoMenu;
0111     infoMenu.insert(QStringLiteral("{{category}}"), i18n("Category name"));
0112     infoMenu.insert(QStringLiteral("{{index}}"), i18n("Guide number"));
0113     infoMenu.insert(QStringLiteral("{{realtimecode}}"), i18n("Guide position in HH:MM:SS:FF"));
0114     infoMenu.insert(QStringLiteral("{{timecode}}"), i18n("Guide position in (HH:)MM.SS"));
0115     infoMenu.insert(QStringLiteral("{{nexttimecode}}"), i18n("Next guide position in (HH:)MM.SS"));
0116     infoMenu.insert(QStringLiteral("{{frame}}"), i18n("Guide position in frames"));
0117     infoMenu.insert(QStringLiteral("{{nextframe}}"), i18n("Next guide position in frames"));
0118     infoMenu.insert(QStringLiteral("{{comment}}"), i18n("Guide comment"));
0119     QMapIterator<QString, QString> i(infoMenu);
0120     QAction *a;
0121     while (i.hasNext()) {
0122         i.next();
0123         a = new QAction(this);
0124         a->setText(QString("%1 - %2").arg(i.value(), i.key()));
0125         a->setData(i.key());
0126         infoButton->addAction(a);
0127     }
0128     connect(infoButton, &QToolButton::triggered, [this](QAction *a) {
0129         formatEdit->insert(a->data().toString());
0130         updateContentByModel();
0131     });
0132     adjustSize();
0133 }
0134 
0135 ExportGuidesDialog::~ExportGuidesDialog()
0136 {
0137     KdenliveSettings::setExportGuidesFormat(formatEdit->text());
0138 }
0139 
0140 GenTime ExportGuidesDialog::offsetTime() const
0141 {
0142     switch (offsetTimeComboBox->currentIndex()) {
0143     case 1: // Add
0144         return offsetTimeSpinbox->gentime();
0145     case 2: // Subtract
0146         return -offsetTimeSpinbox->gentime();
0147     case 0: // Disabled
0148     default:
0149         return GenTime(0);
0150     }
0151 }
0152 
0153 QString chapterTimeStringFromMs(double timeMs)
0154 {
0155     int totalSec = qAbs(timeMs / 1000);
0156     bool negative = timeMs < 0 && totalSec > 0; // since our minimal unit is second.
0157     int hour = totalSec / 3600;
0158     int min = totalSec % 3600 / 60;
0159     int sec = totalSec % 3600 % 60;
0160     if (hour == 0) {
0161         return QString::asprintf("%s%d:%02d", negative ? "-" : "", min, sec);
0162     } else {
0163         return QString::asprintf("%s%d:%02d:%02d", negative ? "-" : "", hour, min, sec);
0164     }
0165 }
0166 
0167 void ExportGuidesDialog::updateContentByModel() const
0168 {
0169     const int markerIndex = categoryChooser->currentCategory();
0170     if (format_json->isChecked()) {
0171         messageWidget->setVisible(false);
0172         generatedContent->setPlainText(m_markerListModel->toJson({markerIndex}));
0173         return;
0174     }
0175     if (format_ffmpeg->isChecked()) {
0176         messageWidget->setVisible(false);
0177         generatedContent->setPlainText(getFFmpegChaptersData());
0178         return;
0179     }
0180     const QString format(formatEdit->text());
0181     const GenTime offset(offsetTime());
0182 
0183     QStringList chapterTexts;
0184     QList<CommentedTime> markers(m_markerListModel->getAllMarkers(markerIndex));
0185     bool needCheck = format == YT_FORMAT;
0186     bool needShowInfoMsg = false;
0187 
0188     const int markerCount = markers.length();
0189     const double currentFps = pCore->getCurrentFps();
0190     for (int i = 0; i < markers.length(); i++) {
0191         const CommentedTime &currentMarker = markers.at(i);
0192         const GenTime &nextGenTime = markerCount - 1 == i ? m_projectDuration : markers.at(i + 1).time();
0193 
0194         QString line(format);
0195         GenTime currentTime = currentMarker.time() + offset;
0196         GenTime nextTime = nextGenTime + offset;
0197 
0198         if (i == 0 && needCheck && !qFuzzyCompare(currentTime.seconds(), 0)) {
0199             needShowInfoMsg = true;
0200         }
0201 
0202         if (needCheck && std::abs(nextTime.seconds() - currentTime.seconds()) < 10) {
0203             needShowInfoMsg = true;
0204         }
0205 
0206         line.replace("{{index}}", QString::number(i + 1));
0207         line.replace("{{realtimecode}}", pCore->timecode().getDisplayTimecode(currentTime, false));
0208         line.replace("{{timecode}}", chapterTimeStringFromMs(currentTime.ms()));
0209         line.replace("{{nexttimecode}}", chapterTimeStringFromMs(nextTime.ms()));
0210         line.replace("{{frame}}", QString::number(currentTime.frames(currentFps)));
0211         line.replace("{{nextframe}}", QString::number(nextTime.frames(currentFps)));
0212         line.replace("{{comment}}", currentMarker.comment());
0213         line.replace("{{category}}", pCore->markerTypes[currentMarker.markerType()].displayName);
0214         chapterTexts.append(line);
0215     }
0216 
0217     generatedContent->setPlainText(chapterTexts.join('\n'));
0218 
0219     if (needCheck && markerCount < 3) {
0220         needShowInfoMsg = true;
0221     }
0222 
0223     messageWidget->setVisible(needShowInfoMsg);
0224 }
0225 
0226 const QString ExportGuidesDialog::getFFmpegChaptersData() const
0227 {
0228     QString result = QStringLiteral(";FFMETADATA1\n\n");
0229     int frame_rate_num = pCore->getCurrentProfile()->frame_rate_num();
0230     int frame_rate_den = pCore->getCurrentProfile()->frame_rate_den();
0231     const double currentFps = pCore->getCurrentFps();
0232     const GenTime offset(offsetTime());
0233     const QString frameRate = QStringLiteral("[CHAPTER]\nTIMEBASE=%1/%2\n").arg(frame_rate_num).arg(frame_rate_den);
0234     const int markerIndex = categoryChooser->currentCategory();
0235     QList<CommentedTime> markers(m_markerListModel->getAllMarkers(markerIndex));
0236     const int markerCount = markers.length();
0237     for (int i = 0; i < markers.length(); i++) {
0238         const CommentedTime &currentMarker = markers.at(i);
0239         const GenTime &nextGenTime = markerCount - 1 == i ? m_projectDuration : markers.at(i + 1).time();
0240         GenTime currentTime = currentMarker.time() + offset;
0241         GenTime nextTime = nextGenTime + offset;
0242         result.append(frameRate);
0243         result.append(QStringLiteral("START=%1\n").arg(currentTime.frames(currentFps)));
0244         result.append(QStringLiteral("END=%1\n").arg(nextTime.frames(currentFps)));
0245         result.append(QStringLiteral("title=%1\n\n").arg(currentMarker.comment()));
0246     }
0247     return result;
0248 }
0249 
0250 #undef YT_FORMAT