#include <QApplication>
#include <QActionGroup>
#include <QClipboard>
#include <QCloseEvent>
#include <QCryptographicHash>
#include <cstdio>
#include <QCursor>
#include <QFileDialog>
#include <QColorDialog>
#include <QFontDialog>
#include <QFontDatabase>
#include <QHBoxLayout>
#include <QImage>
#include <QInputDialog>
#include <QLabel>
#include <QLineEdit>
#include <QListWidget>
#include <QMainWindow>
#include <QMessageBox>
#include <QPainter>
#include <QPainterPath>
#include "core/windows_utf8_argv.hpp"
#include "core/build_info.hpp"
#include "core/control_ipc.hpp"
#include "core/preview_shared_memory.hpp"
#include "core/videoredo_joiner.hpp"
#include "gui/license_manager.hpp"
#include <QPaintEvent>
#include <QResizeEvent>
#include <QKeyEvent>
#include <QKeySequenceEdit>
#include <QMouseEvent>
#include <QDragEnterEvent>
#include <QDropEvent>
#include <QMimeData>
#include <QProcess>
#include <QPixmap>
#include <QDir>
#include <QDirIterator>
#include <QDialog>
#include <QDialogButtonBox>
#include <QDateTime>
#include <QDesktopServices>
#include <QElapsedTimer>
#include <QEvent>
#include <QDoubleSpinBox>
#include <QFileInfo>
#include <QFile>
#include <QSaveFile>
#include <QTemporaryDir>
#include <QTimer>
#include <QHash>
#include <QQueue>
#include <QPointer>
#include <QPushButton>
#include <QProgressBar>
#include <QProgressDialog>
#include <QSettings>
#include <QSet>
#include <QShortcut>
#include <QSlider>
#include <QVector>
#include <QComboBox>
#include <QMenuBar>
#include <QMenu>
#include <QTableWidget>
#include <QTabWidget>
#include <QHeaderView>
#include <QGroupBox>
#include <QFormLayout>
#include <QCheckBox>
#include <QSpinBox>
#include <QStandardPaths>
#include <QStyle>
#include <QIcon>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QXmlStreamReader>
#include <QXmlStreamWriter>
#include <QTextEdit>
#include <QTextBrowser>
#include <QTextCursor>
#include <QToolButton>
#include <QTransform>
#include <QUrl>
#include <QUuid>
#include <QVBoxLayout>
#include <QSplitter>
#include <functional>
#include <future>
#include <filesystem>
#include <atomic>
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <vector>
#include <optional>
#include <tuple>
#include <thread>
#include <memory>
#include <utility>
#include "core/frame_navigation.hpp"

#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#include <tlhelp32.h>
#include <mmsystem.h>
#include <dwmapi.h>
#endif

static void apply_native_window_theme(QWidget* window, const QColor& background,
                                      const QColor& text, const QColor& border) {
#ifdef _WIN32
  if (!window) return;
  const HWND handle = reinterpret_cast<HWND>(window->winId());
  const BOOL dark = background.lightness() < 128 ? TRUE : FALSE;
  // Attribute 20 is supported by current Windows 10/11 releases; early Windows
  // 10 builds used 19 for the same immersive-dark-mode setting.
  if (FAILED(DwmSetWindowAttribute(handle, 20, &dark, sizeof(dark))))
    DwmSetWindowAttribute(handle, 19, &dark, sizeof(dark));

  // Windows 11 accepts explicit non-client colours. Older releases simply
  // reject these attributes and retain the immersive dark/light appearance.
  const COLORREF caption_colour = RGB(background.red(), background.green(), background.blue());
  const COLORREF text_colour = RGB(text.red(), text.green(), text.blue());
  const COLORREF border_colour = RGB(border.red(), border.green(), border.blue());
  DwmSetWindowAttribute(handle, 35, &caption_colour, sizeof(caption_colour));
  DwmSetWindowAttribute(handle, 36, &text_colour, sizeof(text_colour));
  DwmSetWindowAttribute(handle, 34, &border_colour, sizeof(border_colour));
  RedrawWindow(handle, nullptr, nullptr, RDW_INVALIDATE | RDW_FRAME | RDW_UPDATENOW);
#else
  (void)window;
  (void)background;
  (void)text;
  (void)border;
#endif
}

struct NavigationReport {
  QString source_uuid;
  std::size_t frame_count{};
  std::vector<std::size_t> keyframes;
  std::vector<std::int64_t> frame_pts;
  std::int64_t time_base_num{1};
  std::int64_t time_base_den{1};
};

using FrameRange = std::pair<std::size_t, std::size_t>;

static QString worker_program() {
#ifdef _WIN32
  constexpr auto executable = "smartcut-worker.exe";
#else
  constexpr auto executable = "smartcut-worker";
#endif
  return QDir(QCoreApplication::applicationDirPath()).filePath(executable);
}

struct ControlThreadEntry {
  std::shared_ptr<std::atomic_bool> finished;
  std::jthread thread;
};

static bool set_process_threads_suspended(qint64 process_id, bool suspended) {
#ifdef _WIN32
  if (process_id <= 0) return false;
  const HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
  if (snapshot == INVALID_HANDLE_VALUE) return false;
  THREADENTRY32 entry{};
  entry.dwSize = sizeof(entry);
  bool changed = false;
  if (Thread32First(snapshot, &entry)) {
    do {
      if (entry.th32OwnerProcessID != static_cast<DWORD>(process_id)) continue;
      const HANDLE thread = OpenThread(THREAD_SUSPEND_RESUME, FALSE, entry.th32ThreadID);
      if (!thread) continue;
      const DWORD result = suspended ? SuspendThread(thread) : ResumeThread(thread);
      changed = changed || result != static_cast<DWORD>(-1);
      CloseHandle(thread);
    } while (Thread32Next(snapshot, &entry));
  }
  CloseHandle(snapshot);
  return changed;
#else
  Q_UNUSED(process_id);
  Q_UNUSED(suspended);
  return false;
#endif
}

struct BatchJob {
  QString input;
  QString output;
  std::size_t frame_count{};
  std::vector<FrameRange> cuts;
  QVector<int> selected_streams;
  bool filter_streams{};
  QString mode{"smart"};
  QString status{"Queued"};
  QString operation{"export"};
  QString watch_key;
  QString audio_codec{"copy"};
  double audio_gain_db{};
  int audio_bitrate{192000};
  int audio_sample_rate{};
  int audio_channels{};
  QString video_encoder{"source"};
  QString hardware_encoder{"auto"};
  QString aspect_ratio{"No Change"};
  int video_width{};
  int video_height{};
  int video_bit_depth{};
  int video_bitrate_kbps{};
  int video_max_bitrate_kbps{};
  QString deinterlace{"None"};
  QString frame_rate{"No Change"};
  int ts_transport_stream_id{1};
  int ts_original_network_id{0xff01};
  int ts_service_id{1};
  int ts_pmt_start_pid{0x1000};
  int ts_start_pid{0x0100};
  int ts_mux_rate{};
  int ts_pcr_period_ms{-1};
  double ts_pat_period{0.1};
  double ts_sdt_period{0.5};
  bool ts_m2ts_mode{};
  bool ts_resend_headers{true};
  bool ts_system_b{};
  bool ts_emit_nit{};
  QString ts_service_name{"SmartCut64"};
  QString ts_service_provider{"SmartCut64"};
};

struct WatchFolder {
  QString id;
  QString input_folder;
  QString output_folder;
  QString patterns{"*.mkv;*.mp4;*.mov;*.ts;*.m2ts;*.mpg;*.mpeg;*.webm;*.vob"};
  QString output_template{"%BASE%-edited.%EXT%"};
  QString profile_id{"intelligent"};
  QString operation{"export"};
  bool recursive{};
  bool enabled{true};
  bool auto_start{true};
  int settle_seconds{5};
};

struct OutputProfile {
  QString id;
  QString name;
  QString mode;
  QString description;
  QString audio_codec{"copy"};
  double audio_gain_db{};
  int audio_bitrate{192000};
  int audio_sample_rate{};
  int audio_channels{};
  QString file_type{"MKV"};
  QString file_extension{"mkv"};
  QString video_encoder{"source"};
  QString hardware_encoder{"auto"};
  QString aspect_ratio{"No Change"};
  int video_width{};
  int video_height{};
  int video_bit_depth{};
  int video_bitrate_kbps{};
  int video_max_bitrate_kbps{};
  QString deinterlace{"None"};
  QString frame_rate{"No Change"};
  int ts_transport_stream_id{1};
  int ts_original_network_id{0xff01};
  int ts_service_id{1};
  int ts_pmt_start_pid{0x1000};
  int ts_start_pid{0x0100};
  int ts_mux_rate{};
  int ts_pcr_period_ms{-1};
  double ts_pat_period{0.1};
  double ts_sdt_period{0.5};
  bool ts_m2ts_mode{};
  bool ts_resend_headers{true};
  bool ts_system_b{};
  bool ts_emit_nit{};
  QString ts_service_name{"SmartCut64"};
  QString ts_service_provider{"SmartCut64"};
  bool enabled{true};
  bool favorite{};
  bool audio_only{};
};

struct ContainerCompatibilityIssue {
  int stream_index{-1};
  int stream_type{-1};
  QString codec;
  QString explanation;
  bool transcodable{};
};

struct ContainerCompatibilityReport {
  QString container;
  QVector<ContainerCompatibilityIssue> issues;
  QStringList warnings;
  bool needs_video_transcode{};
  bool needs_audio_transcode{};
  bool can_transcode{};
};

struct TitleTextObject {
  QString id;
  QString text{"Title"};
  QRect rect{160, 220, 960, 280};
  QFont font{"Arial", 48, QFont::Bold};
  QColor colour{Qt::white};
  QColor outline{Qt::black};
  QColor shadow{0, 0, 0, 190};
  int outline_width{1};
  int shadow_offset{4};
  Qt::Alignment alignment{Qt::AlignCenter};
};

static std::optional<QJsonObject> read_videoredo_title_template(const QString& path) {
  QFile file(path); if (!file.open(QIODevice::ReadOnly)) return std::nullopt;
  QXmlStreamReader xml(&file); QJsonObject result{{"schema", "smartcut-title-template-v1"}}; QJsonArray objects;
  int canvas_width = 1280; int canvas_height = 720; QColor background(Qt::black); double duration = 5.0; double fade_in = 0.5; double fade_out = 0.5;
  while (!xml.atEnd()) {
    xml.readNext(); if (!xml.isStartElement()) continue; const auto name = xml.name();
    if (name == QStringLiteral("VRDTitle")) { duration = xml.attributes().value("duration").toDouble(); fade_in = xml.attributes().value("fade_in").toDouble(); fade_out = xml.attributes().value("fade_out").toDouble(); }
    else if (name == QStringLiteral("Width")) canvas_width = std::max(1, xml.readElementText().toInt());
    else if (name == QStringLiteral("Height")) canvas_height = std::max(1, xml.readElementText().toInt());
    else if (name == QStringLiteral("Background")) { while (xml.readNextStartElement()) { if (xml.name() == QStringLiteral("Color")) background = QColor(xml.readElementText()); else xml.skipCurrentElement(); } }
    else if (name == QStringLiteral("TextBox")) {
      QString text{"Title"}; QString font_name{"Arial"}; QColor colour(Qt::white); int point_size = 32; int align = 1; bool bold = false; bool italics = false; int top = 0; int left = 0; int width = canvas_width; int height = canvas_height;
      while (xml.readNextStartElement()) { const auto field = xml.name(); if (field == QStringLiteral("Text")) text = xml.readElementText(); else if (field == QStringLiteral("PointSize")) point_size = xml.readElementText().toInt(); else if (field == QStringLiteral("FontName")) font_name = xml.readElementText(); else if (field == QStringLiteral("Align")) align = xml.readElementText().toInt(); else if (field == QStringLiteral("Color")) colour = QColor(xml.readElementText()); else if (field == QStringLiteral("Bold")) bold = xml.readElementText().toInt() != 0; else if (field == QStringLiteral("Itallics") || field == QStringLiteral("Italics")) italics = xml.readElementText().toInt() != 0; else if (field == QStringLiteral("Top")) top = xml.readElementText().toInt(); else if (field == QStringLiteral("Left")) left = xml.readElementText().toInt(); else if (field == QStringLiteral("Width")) width = xml.readElementText().toInt(); else if (field == QStringLiteral("Height")) height = xml.readElementText().toInt(); else xml.skipCurrentElement(); }
      QFont font(font_name, std::max(1, point_size)); font.setBold(bold); font.setItalic(italics); const QRect rectangle(qRound(left * 1280.0 / canvas_width), qRound(top * 720.0 / canvas_height), std::max(1, qRound(width * 1280.0 / canvas_width)), std::max(1, qRound(height * 720.0 / canvas_height))); const auto alignment = align == 0 ? "left" : align == 2 ? "right" : "center";
      objects.push_back(QJsonObject{{"id", QString("text-%1").arg(objects.size() + 1)}, {"text", text}, {"rect", QJsonArray{rectangle.x(), rectangle.y(), rectangle.width(), rectangle.height()}}, {"font", font.toString()}, {"colour", colour.name(QColor::HexArgb)}, {"outline_colour", "#ff000000"}, {"shadow_colour", "#00000000"}, {"outline_width", 0}, {"shadow_offset", 0}, {"alignment", alignment}});
    }
  }
  if (xml.hasError() || objects.isEmpty()) return std::nullopt;
  result.insert("objects", objects); result.insert("background_colour", background.name(QColor::HexArgb)); result.insert("background_image", ""); result.insert("canvas_width", canvas_width); result.insert("canvas_height", canvas_height); result.insert("duration_seconds", duration); result.insert("fade_in_seconds", fade_in); result.insert("fade_out_seconds", fade_out); result.insert("description", QFileInfo(path).completeBaseName()); return result;
}

static bool write_videoredo_title_template(const QString& path, const QJsonObject& title) {
  const auto objects = title.value("objects").toArray(); if (objects.isEmpty()) return false; const int canvas_width = std::max(1, title.value("canvas_width").toInt(1280)); const int canvas_height = std::max(1, title.value("canvas_height").toInt(720));
  QSaveFile file(path); if (!file.open(QIODevice::WriteOnly)) return false; QXmlStreamWriter xml(&file); xml.setAutoFormatting(true); xml.writeStartDocument(); xml.writeStartElement("VRDTitle"); xml.writeAttribute("fade_in", QString::number(title.value("fade_in_seconds").toDouble(0.5), 'f', 6)); xml.writeAttribute("fade_out", QString::number(title.value("fade_out_seconds").toDouble(0.5), 'f', 6)); xml.writeAttribute("duration", QString::number(title.value("duration_seconds").toDouble(5.0), 'f', 6)); xml.writeTextElement("Width", QString::number(canvas_width)); xml.writeTextElement("Height", QString::number(canvas_height)); xml.writeStartElement("Aspect"); xml.writeTextElement("Horizontal", QString::number(static_cast<double>(canvas_width), 'f', 6)); xml.writeTextElement("Vertical", QString::number(static_cast<double>(canvas_height), 'f', 6)); xml.writeEndElement(); xml.writeStartElement("Background"); xml.writeTextElement("Color", QColor(title.value("background_colour").toString("#ff000000")).name(QColor::HexRgb).toUpper()); xml.writeEndElement(); xml.writeStartElement("TextBoxes"); xml.writeAttribute("count", QString::number(objects.size()));
  for (int index = 0; index < objects.size(); ++index) { const auto object = objects[index].toObject(); const auto rectangle = object.value("rect").toArray(); if (rectangle.size() != 4) continue; QFont font; font.fromString(object.value("font").toString()); const auto alignment = object.value("alignment").toString("center"); xml.writeStartElement("TextBox"); xml.writeAttribute("sequence", QString::number(index + 1)); xml.writeAttribute("dvdbutton", "0"); xml.writeTextElement("Text", object.value("text").toString()); xml.writeTextElement("Variable", ""); xml.writeTextElement("PointSize", QString::number(std::max(1, font.pointSize()))); xml.writeTextElement("FontName", font.family()); xml.writeTextElement("Align", alignment == "left" ? "0" : alignment == "right" ? "2" : "1"); xml.writeTextElement("VertAlign", "0"); xml.writeTextElement("Color", QColor(object.value("colour").toString("#ffffffff")).name(QColor::HexRgb).toUpper()); xml.writeTextElement("Bold", font.bold() ? "1" : "0"); xml.writeTextElement("Itallics", font.italic() ? "1" : "0"); xml.writeTextElement("Top", QString::number(qRound(rectangle[1].toInt() * canvas_height / 720.0))); xml.writeTextElement("Left", QString::number(qRound(rectangle[0].toInt() * canvas_width / 1280.0))); xml.writeTextElement("Width", QString::number(qRound(rectangle[2].toInt() * canvas_width / 1280.0))); xml.writeTextElement("Height", QString::number(qRound(rectangle[3].toInt() * canvas_height / 720.0))); xml.writeEndElement(); }
  xml.writeEndElement(); const auto first = objects[0].toObject(); QFont default_font; default_font.fromString(first.value("font").toString()); xml.writeStartElement("DefaultTextProperties"); xml.writeTextElement("PointSize", QString::number(std::max(1, default_font.pointSize()))); xml.writeTextElement("FontName", default_font.family()); xml.writeTextElement("Align", "1"); xml.writeTextElement("VertAlign", "0"); xml.writeTextElement("Color", QColor(first.value("colour").toString("#ffffffff")).name(QColor::HexRgb).toUpper()); xml.writeTextElement("Bold", default_font.bold() ? "1" : "0"); xml.writeTextElement("Itallics", default_font.italic() ? "1" : "0"); xml.writeEndElement(); xml.writeEndElement(); xml.writeEndDocument(); return !xml.hasError() && file.commit();
}

class TitleCanvasWidget final : public QWidget {
 public:
  explicit TitleCanvasWidget(QWidget* parent = nullptr) : QWidget(parent) { setMinimumSize(640, 360); setMouseTracking(true); }
  QVector<TitleTextObject> objects;
  qsizetype selected{-1};
  QColor background{Qt::black};
  QString background_image;
  bool safe_area{};
  std::function<void(int)> on_selected;
  std::function<void()> on_changed;
  QImage render_image(bool overlay) const {
    QImage canvas(1280, 720, QImage::Format_ARGB32); canvas.fill(background); QPainter painter(&canvas);
    if (!background_image.isEmpty()) { const QImage image(background_image); if (!image.isNull()) painter.drawImage(canvas.rect(), image); }
    painter.setRenderHint(QPainter::Antialiasing); painter.setRenderHint(QPainter::TextAntialiasing);
    for (int index = 0; index < objects.size(); ++index) { const auto& object = objects[index]; painter.setFont(object.font); const auto flags = object.alignment | Qt::TextWordWrap; if (object.shadow_offset > 0) { painter.setPen(object.shadow); painter.drawText(object.rect.translated(object.shadow_offset, object.shadow_offset), flags, object.text); } if (object.outline_width > 0) { painter.setPen(object.outline); for (int y = -object.outline_width; y <= object.outline_width; ++y) for (int x = -object.outline_width; x <= object.outline_width; ++x) if (x || y) painter.drawText(object.rect.translated(x, y), flags, object.text); } painter.setPen(object.colour); painter.drawText(object.rect, flags, object.text); if (overlay && index == selected) { painter.setPen(QPen(QColor("#19d34a"), 3)); painter.setBrush(Qt::NoBrush); painter.drawRect(object.rect); painter.fillRect(QRect(object.rect.right() - 13, object.rect.bottom() - 13, 14, 14), QColor("#19d34a")); } }
    if (overlay && safe_area) { painter.setPen(QPen(QColor("#e6e600"), 2, Qt::DashLine)); painter.setBrush(Qt::NoBrush); painter.drawRect(QRect(64, 36, 1152, 648)); painter.drawRect(QRect(128, 72, 1024, 576)); }
    return canvas;
  }
 protected:
  void paintEvent(QPaintEvent*) override { QPainter painter(this); painter.fillRect(rect(), Qt::black); const auto image = render_image(true); const auto target = fitted_rect(); painter.drawImage(target, image); }
  void mousePressEvent(QMouseEvent* event) override { if (event->button() != Qt::LeftButton) return; const auto point = canvas_point(event->position()); selected = hit_test(point); if (selected >= 0) { const auto& item = objects[selected]; resizing_ = std::abs(point.x() - item.rect.right()) <= 24 && std::abs(point.y() - item.rect.bottom()) <= 24; drag_offset_ = point - item.rect.topLeft(); } if (on_selected) on_selected(selected); update(); }
  void mouseMoveEvent(QMouseEvent* event) override { if (!(event->buttons() & Qt::LeftButton) || selected < 0 || selected >= objects.size()) return; const auto point = canvas_point(event->position()); auto& item = objects[selected]; if (resizing_) { item.rect.setWidth(std::max(80, point.x() - item.rect.left())); item.rect.setHeight(std::max(40, point.y() - item.rect.top())); } else item.rect.moveTopLeft(point - drag_offset_); item.rect.moveLeft(std::clamp(item.rect.left(), 0, 1280 - item.rect.width())); item.rect.moveTop(std::clamp(item.rect.top(), 0, 720 - item.rect.height())); if (on_changed) on_changed(); update(); }
  void mouseReleaseEvent(QMouseEvent*) override { resizing_ = false; }
 private:
  QRect fitted_rect() const { const double scale = std::min(width() / 1280.0, height() / 720.0); const QSize size(qRound(1280 * scale), qRound(720 * scale)); return QRect((width() - size.width()) / 2, (height() - size.height()) / 2, size.width(), size.height()); }
  QPoint canvas_point(const QPointF& point) const { const auto target = fitted_rect(); return QPoint(std::clamp(qRound((point.x() - target.left()) * 1280.0 / std::max(1, target.width())), 0, 1279), std::clamp(qRound((point.y() - target.top()) * 720.0 / std::max(1, target.height())), 0, 719)); }
  int hit_test(const QPoint& point) const { for (int index = objects.size() - 1; index >= 0; --index) if (objects[index].rect.adjusted(-8, -8, 8, 8).contains(point)) return index; return -1; }
  QPoint drag_offset_;
  bool resizing_{};
};

struct GeneratedChapter {
  std::size_t output_frame{};
  std::uint64_t output_time_us{};
};

class JoinerListWidget final : public QListWidget {
 public:
  explicit JoinerListWidget(QWidget* parent = nullptr) : QListWidget(parent) { setAcceptDrops(true); setDragEnabled(true); setDragDropMode(QAbstractItemView::InternalMove); setDefaultDropAction(Qt::MoveAction); }
  std::function<void(QStringList)> on_files_dropped;
 protected:
  void dragEnterEvent(QDragEnterEvent* event) override { if (event->mimeData()->hasUrls()) event->acceptProposedAction(); else QListWidget::dragEnterEvent(event); }
  void dragMoveEvent(QDragMoveEvent* event) override { if (event->mimeData()->hasUrls()) event->acceptProposedAction(); else QListWidget::dragMoveEvent(event); }
  void dropEvent(QDropEvent* event) override { if (!event->mimeData()->hasUrls()) { QListWidget::dropEvent(event); return; } QStringList paths; for (const auto& url : event->mimeData()->urls()) if (url.isLocalFile() && QFileInfo(url.toLocalFile()).isFile()) paths.push_back(QDir::toNativeSeparators(url.toLocalFile())); paths.sort(Qt::CaseInsensitive); if (on_files_dropped && !paths.isEmpty()) on_files_dropped(paths); event->acceptProposedAction(); }
};

enum class ActionIcon { Open, Save, Join, Dvd, CreateDvd };

static QIcon action_icon(ActionIcon type) {
  QPixmap pixmap(36, 36); pixmap.fill(Qt::transparent);
  QPainter painter(&pixmap); painter.setRenderHint(QPainter::Antialiasing);
  const QColor blue("#1979ca"); painter.setPen(QPen(blue, 2)); painter.setBrush(Qt::NoBrush);
  if (type == ActionIcon::Dvd || type == ActionIcon::CreateDvd) {
    painter.drawEllipse(QRectF(5, 5, 25, 25)); painter.drawEllipse(QRectF(14, 14, 7, 7));
    painter.drawArc(QRectF(9, 9, 17, 17), 35 * 16, 110 * 16);
  } else {
    painter.drawRect(QRectF(6, 5, 23, 26));
    for (int y : {9, 16, 23, 29}) { painter.drawRect(QRectF(3, y, 3, 4)); painter.drawRect(QRectF(29, y, 3, 4)); }
    if (type == ActionIcon::Open) { painter.drawLine(10, 10, 25, 10); painter.drawLine(10, 14, 22, 14); }
    if (type == ActionIcon::Save) { painter.drawRect(QRectF(11, 9, 13, 9)); painter.drawRect(QRectF(11, 22, 13, 7)); }
  }
  if (type == ActionIcon::Join || type == ActionIcon::Dvd) {
    painter.setBrush(blue); painter.drawRect(QRectF(25, 22, 3, 11)); painter.drawRect(QRectF(21, 26, 11, 3));
  } else if (type == ActionIcon::CreateDvd) {
    painter.setBrush(blue); QPolygonF arrow; arrow << QPointF(22, 23) << QPointF(33, 28) << QPointF(22, 33); painter.drawPolygon(arrow);
  }
  return QIcon(pixmap);
}

class SideActionButton final : public QPushButton {
 public:
  explicit SideActionButton(const QString& text, QWidget* parent = nullptr) : QPushButton(text, parent) { setCursor(Qt::PointingHandCursor); }
 protected:
  void paintEvent(QPaintEvent*) override {
    QPainter painter(this); painter.fillRect(rect(), palette().color(QPalette::Button));
    if (underMouse() || isDown()) {
      QColor overlay = palette().color(QPalette::Highlight); overlay.setAlpha(isDown() ? 72 : 38); painter.fillRect(rect(), overlay);
    }
    const auto mode = isEnabled() ? QIcon::Normal : QIcon::Disabled; const auto pixmap = icon().pixmap(iconSize(), mode);
    painter.drawPixmap((width() - pixmap.width()) / 2, 4, pixmap);
    painter.setPen(palette().color(isEnabled() ? QPalette::ButtonText : QPalette::PlaceholderText)); painter.drawText(QRect(2, 40, width() - 4, height() - 41), Qt::AlignHCenter | Qt::AlignTop, text());
  }
};

class FrameGuideWidget final : public QWidget {
 public:
  explicit FrameGuideWidget(QWidget* parent = nullptr) : QWidget(parent) {
    setFixedHeight(76); setAccessibleName("Audio waveform and frame guide");
  }
  void set_position(std::size_t frame, std::size_t count) { frame_ = frame; count_ = count; frame_window_.clear(); update(); }
  void set_frame_window(QVector<std::optional<std::size_t>> frames) { frame_window_ = std::move(frames); update(); }
  void set_waveform(QVector<float> peaks) { waveform_ = std::move(peaks); update(); }
  void clear_waveform() { waveform_.clear(); update(); }
  bool has_waveform() const { return !waveform_.isEmpty(); }
  void set_waveform_style(bool decibels, const QColor& background, const QColor& waveform,
                          const QColor& centre, const QColor& frame_text) {
    decibel_scale_ = decibels; background_ = background; waveform_colour_ = waveform;
    centre_colour_ = centre; frame_text_colour_ = frame_text; update();
  }
 protected:
  void paintEvent(QPaintEvent*) override {
    QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, false); painter.fillRect(rect(), background_);
    painter.setPen(waveform_colour_);
    const int waveform_bottom = 44; const int centre = 22;
    if (waveform_.isEmpty()) {
      painter.drawLine(0, centre, width(), centre);
    } else {
      for (int x = 0; x < width(); ++x) {
        const auto bucket = std::min<qsizetype>(
            static_cast<qsizetype>(static_cast<long long>(x) * waveform_.size() /
                                   std::max(width(), 1)),
            waveform_.size() - 1);
        const auto linear = std::clamp(static_cast<double>(waveform_[bucket]), 0.0, 1.0);
        const auto scaled = decibel_scale_ ? std::clamp((20.0 * std::log10(std::max(linear, 0.001)) + 60.0) / 60.0, 0.0, 1.0) : linear;
        const int amplitude = std::clamp(static_cast<int>(std::lround(scaled * 20.0)), 0, 20);
        painter.drawLine(x, centre - amplitude, x, centre + amplitude);
      }
    }
    painter.setPen(centre_colour_); painter.drawLine(0, waveform_bottom, width(), waveform_bottom);
    const int cells = frame_window_.isEmpty() ? std::clamp(width() / 78, 9, 24)
                                              : static_cast<int>(frame_window_.size());
    const double cell_width = static_cast<double>(width()) / std::max(cells, 1);
    const std::int64_t first = std::max<std::int64_t>(0, static_cast<std::int64_t>(frame_) - cells / 2);
    QFont font = painter.font(); font.setPointSize(8); font.setBold(true); painter.setFont(font);
    for (int cell = 0; cell < cells; ++cell) {
      const int x = static_cast<int>((cell + 0.5) * cell_width);
      if (!frame_window_.isEmpty() && !frame_window_[cell]) continue;
      if (cell % 2 == 0) { painter.setBrush(QColor("#ff2020")); painter.setPen(Qt::NoPen); QPolygon marker; marker << QPoint(x - 3, waveform_bottom + 7) << QPoint(x + 3, waveform_bottom + 7) << QPoint(x, waveform_bottom + 2); painter.drawPolygon(marker); }
      painter.setPen(frame_text_colour_);
      const auto value = frame_window_.isEmpty()
                             ? std::min<std::uint64_t>(static_cast<std::uint64_t>(first + cell), count_ > 0 ? static_cast<std::uint64_t>(count_ - 1) : 0ULL)
                             : static_cast<std::uint64_t>(*frame_window_[cell]);
      painter.drawText(QRectF(cell * cell_width, waveform_bottom + 12, cell_width, 16), Qt::AlignCenter, QString::number(value));
    }
    painter.setBrush(Qt::NoBrush); painter.setPen(QColor("#606060")); painter.drawRect(rect().adjusted(0, 0, -1, -1));
  }
 private:
  std::size_t frame_{};
  std::size_t count_{};
  QVector<float> waveform_;
  QVector<std::optional<std::size_t>> frame_window_;
  bool decibel_scale_{};
  QColor background_{Qt::black};
  QColor waveform_colour_{"#1268b3"};
  QColor centre_colour_{"#083e75"};
  QColor frame_text_colour_{"#00d820"};
};

class InfoGridWidget final : public QWidget {
 public:
  explicit InfoGridWidget(QWidget* parent = nullptr) : QWidget(parent) { setMinimumHeight(132); setAccessibleName("Project information"); }
  void set_values(const QString& cursor_time, std::size_t cursor_frame, const QString& program_time, std::size_t program_frames,
                  const QString& selection_time, std::size_t selection_frames, const QString& output_time, std::size_t output_frames,
                  const QString& bookmark_time = {}, std::optional<std::size_t> bookmark_frame = std::nullopt) {
    cursor_time_ = cursor_time; cursor_frame_ = cursor_frame; program_time_ = program_time; program_frames_ = program_frames;
    selection_time_ = selection_time; selection_frames_ = selection_frames; output_time_ = output_time; output_frames_ = output_frames; bookmark_time_ = bookmark_time; bookmark_frame_ = bookmark_frame; update();
  }
 protected:
  void paintEvent(QPaintEvent*) override {
    QPainter painter(this); painter.fillRect(rect(), palette().color(QPalette::Base)); painter.setPen(palette().color(QPalette::Mid));
    const int label_width = 70; const int time_width = std::max(78, (width() - label_width) * 2 / 3); const int header_height = 21;
    painter.drawLine(label_width, 0, label_width, height()); painter.drawLine(label_width + time_width, 0, label_width + time_width, height());
    const QStringList labels{"Cursor:", "Program:", "Selection:", "Output:", "Joiner:", "Bookmark:"};
    const QStringList times{cursor_time_, program_time_, selection_time_, output_time_, {}, bookmark_time_};
    const QStringList frames{QString::number(cursor_frame_), QString::number(program_frames_), QString::number(selection_frames_), QString::number(output_frames_), {}, bookmark_frame_ ? QString::number(*bookmark_frame_) : QString{}};
    painter.setPen(palette().color(QPalette::Text)); painter.drawText(QRect(label_width, 0, time_width, header_height), Qt::AlignCenter, "Time"); painter.drawText(QRect(label_width + time_width, 0, width() - label_width - time_width, header_height), Qt::AlignCenter, "Frames");
    const int row_height = std::max(18, (height() - header_height) / static_cast<int>(labels.size()));
    for (int row = 0; row < labels.size(); ++row) {
      const int top = header_height + row * row_height; painter.setPen(palette().color(QPalette::Mid)); painter.drawLine(0, top, width(), top); painter.setPen(palette().color(QPalette::Text));
      painter.drawText(QRect(3, top, label_width - 6, row_height), Qt::AlignVCenter | Qt::AlignRight, labels[row]);
      painter.drawText(QRect(label_width + 2, top, time_width - 4, row_height), Qt::AlignCenter, times[row]);
      painter.drawText(QRect(label_width + time_width + 2, top, width() - label_width - time_width - 4, row_height), Qt::AlignCenter, frames[row]);
    }
    painter.setPen(palette().color(QPalette::Mid)); painter.drawRect(rect().adjusted(0, 0, -1, -1));
  }
 private:
  QString cursor_time_{"00:00:00.00"}; QString program_time_{"00:00:00.00"}; QString selection_time_{"00:00:00.00"}; QString output_time_{"00:00:00.00"}; QString bookmark_time_;
  std::optional<std::size_t> bookmark_frame_;
  std::size_t cursor_frame_{}; std::size_t program_frames_{}; std::size_t selection_frames_{}; std::size_t output_frames_{};
};

static std::optional<NavigationReport> parse_navigation_report(const QByteArray& bytes) {
  QJsonParseError error; const auto document = QJsonDocument::fromJson(bytes, &error); if (error.error != QJsonParseError::NoError || !document.isObject()) return std::nullopt;
  const auto root = document.object(); if (root.value("schema").toString() != "worker-navigation-v1" || root.contains("error")) return std::nullopt;
  const auto count_value = root.value("frame_count").toInteger(-1); if (count_value < 0) return std::nullopt;
  NavigationReport report; report.source_uuid = root.value("source_uuid").toString(); report.frame_count = static_cast<std::size_t>(count_value); report.time_base_num = root.value("time_base_num").toInteger(1); report.time_base_den = root.value("time_base_den").toInteger(1);
  const auto keyframes = root.value("keyframes").toArray(); report.keyframes.reserve(static_cast<std::size_t>(keyframes.size())); for (const auto value : keyframes) { const auto frame = value.toInteger(-1); if (frame >= 0 && static_cast<std::size_t>(frame) < report.frame_count) report.keyframes.push_back(static_cast<std::size_t>(frame)); }
  const auto pts = root.value("frame_pts").toArray(); if (static_cast<std::size_t>(pts.size()) != report.frame_count) return std::nullopt; report.frame_pts.reserve(report.frame_count); for (const auto value : pts) report.frame_pts.push_back(value.toInteger());
  if (report.source_uuid.isEmpty() || report.time_base_num <= 0 || report.time_base_den <= 0) return std::nullopt;
  return report;
}

class ThumbnailWidget final : public QWidget {
 public:
  explicit ThumbnailWidget(QWidget* parent = nullptr) : QWidget(parent) { setMinimumHeight(58); QSizePolicy policy(QSizePolicy::Expanding, QSizePolicy::Preferred); policy.setHeightForWidth(true); setSizePolicy(policy); setCursor(Qt::ArrowCursor); setAccessibleName("Video thumbnail"); }
  void set_frame(std::size_t frame, const QPixmap& image, const QString& timecode = {}) { frame_ = frame; image_ = image; timecode_ = timecode; frame_valid_ = true; placeholder_ = false; setCursor(Qt::PointingHandCursor); setToolTip(QString("Frame %1  %2").arg(static_cast<qulonglong>(frame)).arg(timecode)); setAccessibleDescription(QString("Seek to frame %1").arg(static_cast<qulonglong>(frame))); updateGeometry(); update(); }
  void clear_frame() { image_ = {}; timecode_.clear(); frame_valid_ = false; placeholder_ = false; setCursor(Qt::ArrowCursor); setToolTip({}); setAccessibleDescription({}); selected_ = false; cut_ = false; update(); }
  void set_placeholder() { clear_frame(); placeholder_ = true; setAccessibleDescription("No frame at this position"); update(); }
  void set_display_aspect_ratio(double ratio) { display_aspect_ratio_ = std::isfinite(ratio) && ratio > 0.0 ? ratio : 0.0; updateGeometry(); update(); }
  void set_display_options(int brightness, bool show_timecode) { brightness_ = std::clamp(brightness, -100, 100); show_timecode_ = show_timecode; update(); }
  void set_selected(bool selected) { selected_ = selected; update(); }
  void set_cut(bool cut) { cut_ = cut; update(); }
  bool has_image() const { return !image_.isNull(); }
  bool is_placeholder() const { return placeholder_; }
  bool full_frame_geometry_ready() const { const auto target = fitted_image_rect(); if (image_.isNull() || target.isEmpty() || !rect().contains(target)) return false; const auto ratio = effective_aspect_ratio(); return std::abs(static_cast<double>(target.width()) / static_cast<double>(target.height()) - ratio) < 0.03; }
  QSize sizeHint() const override { constexpr int preferred_width = 160; return {preferred_width, heightForWidth(preferred_width)}; }
  bool hasHeightForWidth() const override { return true; }
  int heightForWidth(int width) const override { return std::max(58, static_cast<int>(static_cast<double>(width) / effective_aspect_ratio() + 0.5)); }
  std::function<void(std::size_t)> on_clicked;
  std::function<void(std::size_t)> on_double_clicked;
 protected:
  void paintEvent(QPaintEvent*) override {
    QPainter painter(this); painter.fillRect(rect(), placeholder_ ? Qt::black : QColor("#202020"));
    const auto target = fitted_image_rect();
    if (!image_.isNull()) painter.drawPixmap(target, image_, image_.rect());
    if (brightness_ != 0) { QColor overlay = brightness_ > 0 ? Qt::white : Qt::black; overlay.setAlpha(std::clamp(std::abs(brightness_) * 2, 0, 200)); painter.fillRect(target, overlay); }
    if (show_timecode_ && !timecode_.isEmpty()) { QFont font = painter.font(); font.setPointSize(8); painter.setFont(font); const auto label = QString("%1  F%2").arg(timecode_).arg(static_cast<qulonglong>(frame_)); const auto bounds = painter.fontMetrics().boundingRect(label).adjusted(-4, -1, 4, 1); const QRect box(target.left() + 3, target.bottom() - bounds.height() - 3, std::min(bounds.width(), target.width() - 6), bounds.height()); painter.fillRect(box, QColor(0, 0, 0, 175)); painter.setPen(Qt::white); painter.drawText(box, Qt::AlignCenter, label); }
    if (placeholder_) {
      painter.setPen(QPen(QColor("#505050"), 1));
      painter.drawRect(rect().adjusted(0, 0, -1, -1));
    } else if (selected_) {
      painter.setPen(QPen(QColor("#06b025"), 3));
      painter.drawRect(target.adjusted(1, 1, -2, -2));
    } else {
      painter.setPen(QPen(QColor("#d8d8d8"), 1));
      const auto border = target.adjusted(1, 1, -2, -2);
      painter.drawLine(border.bottomLeft(), border.bottomRight());
      painter.drawLine(border.topLeft(), border.bottomLeft());
      painter.drawLine(border.topRight(), border.bottomRight());
    }
    if (frame_valid_) painter.fillRect(QRect(0, 0, width(), 3), cut_ ? QColor("#c75050") : QColor("#06b025"));
  }
  void mousePressEvent(QMouseEvent* event) override { if (frame_valid_ && event->button() == Qt::LeftButton && on_clicked) { on_clicked(frame_); event->accept(); return; } QWidget::mousePressEvent(event); }
  void mouseDoubleClickEvent(QMouseEvent* event) override { if (frame_valid_ && event->button() == Qt::LeftButton && on_double_clicked) { on_double_clicked(frame_); event->accept(); return; } QWidget::mouseDoubleClickEvent(event); }
 private:
  double effective_aspect_ratio() const { if (display_aspect_ratio_ > 0.0) return display_aspect_ratio_; if (!image_.isNull() && image_.height() > 0) return static_cast<double>(image_.width()) / static_cast<double>(image_.height()); return 16.0 / 9.0; }
  QRect fitted_image_rect() const { const auto ratio = effective_aspect_ratio(); int target_width = width(); int target_height = std::max(1, static_cast<int>(static_cast<double>(target_width) / ratio + 0.5)); if (target_height > height()) { target_height = height(); target_width = std::max(1, static_cast<int>(static_cast<double>(target_height) * ratio + 0.5)); } return {(width() - target_width) / 2, (height() - target_height) / 2, target_width, target_height}; }
  std::size_t frame_{};
  QPixmap image_;
  QString timecode_;
  double display_aspect_ratio_{};
  bool selected_{};
  bool cut_{};
  int brightness_{};
  bool frame_valid_{};
  bool placeholder_{};
  bool show_timecode_{};
};

class FineTuneSlider final : public QSlider {
 public:
  explicit FineTuneSlider(QWidget* parent = nullptr) : QSlider(Qt::Horizontal, parent) {
    setRange(-500, 500);
    setValue(0);
    setMinimumWidth(360);
    setMaximumWidth(600);
    setFixedHeight(22);
    setFocusPolicy(Qt::NoFocus);
    setCursor(Qt::SizeHorCursor);
    setAccessibleName("Fine-tune navigation slider");
    setAccessibleDescription("Spring-centred relative seek: left drag is plus or minus 120 seconds; Shift and Control increase accuracy; right drag is plus or minus one second");
    setToolTip("Fine tune: drag left/right up to +/-120 s; Shift +/-60 s; Ctrl +/-40 s; Shift+Ctrl +/-20 s; right-drag +/-1 s");
    setStyleSheet("QSlider::groove:horizontal{height:2px;background:#b2b2b2;border:1px solid #969696;}"
                  "QSlider::handle:horizontal{width:22px;height:10px;margin:-5px 0;background:#168bd2;border:1px solid #0876b8;}"
                  "QSlider::handle:horizontal:hover{background:#239be1;}"
                  "QSlider::handle:horizontal:pressed{background:#0876b8;}"
                  "QSlider::handle:horizontal:disabled{background:#c4c4c4;border-color:#a0a0a0;}");
  }

  void set_range_seconds(double seconds) { range_seconds_ = std::clamp(seconds, 1.0, 3600.0); }
  double range_seconds() const { return range_seconds_; }
  bool smoke_drag(double fraction, Qt::MouseButton button, Qt::KeyboardModifiers modifiers = Qt::NoModifier) {
    if (!current_time_us || !seek_time_us) return false;
    anchor_time_us_ = current_time_us();
    apply_fraction(std::clamp(fraction, -1.0, 1.0), button, modifiers);
    setValue(0);
    return true;
  }

  std::function<std::uint64_t()> current_time_us;
  std::function<void(std::uint64_t)> seek_time_us;
  std::function<void()> on_drag_started;
  std::function<void()> on_drag_finished;

 protected:
  void mousePressEvent(QMouseEvent* event) override {
    if ((event->button() != Qt::LeftButton && event->button() != Qt::RightButton) || !current_time_us || !seek_time_us) {
      QSlider::mousePressEvent(event);
      return;
    }
    drag_button_ = event->button();
    dragging_ = true;
    anchor_time_us_ = current_time_us();
    setSliderDown(true);
    if (on_drag_started) on_drag_started();
    update_from_x(event->position().x(), event->modifiers());
    event->accept();
  }
  void mouseMoveEvent(QMouseEvent* event) override {
    if (!dragging_ || !(event->buttons() & drag_button_)) {
      QSlider::mouseMoveEvent(event);
      return;
    }
    update_from_x(event->position().x(), event->modifiers());
    event->accept();
  }
  void mouseReleaseEvent(QMouseEvent* event) override {
    if (!dragging_ || event->button() != drag_button_) {
      QSlider::mouseReleaseEvent(event);
      return;
    }
    update_from_x(event->position().x(), event->modifiers());
    dragging_ = false;
    setSliderDown(false);
    setValue(0);
    if (on_drag_finished) on_drag_finished();
    event->accept();
  }

 private:
  void update_from_x(double x, Qt::KeyboardModifiers modifiers) {
    constexpr double handle_width = 22.0;
    const auto half_span = std::max(1.0, (static_cast<double>(width()) - handle_width) / 2.0);
    const auto fraction = std::clamp((x - static_cast<double>(width()) / 2.0) / half_span, -1.0, 1.0);
    setValue(static_cast<int>(std::lround(fraction * 500.0)));
    apply_fraction(fraction, drag_button_, modifiers);
  }
  void apply_fraction(double fraction, Qt::MouseButton button, Qt::KeyboardModifiers modifiers) {
    double effective_range = button == Qt::RightButton ? 1.0 : range_seconds_;
    if (button != Qt::RightButton) {
      if (modifiers.testFlag(Qt::ShiftModifier)) effective_range /= 2.0;
      if (modifiers.testFlag(Qt::ControlModifier)) effective_range /= 3.0;
    }
    const auto offset_us = static_cast<long double>(fraction) * static_cast<long double>(effective_range) * 1000000.0L;
    const auto target = static_cast<long double>(anchor_time_us_) + offset_us;
    const auto clamped = target <= 0.0L ? 0ULL : target >= static_cast<long double>(std::numeric_limits<std::uint64_t>::max())
        ? std::numeric_limits<std::uint64_t>::max() : static_cast<std::uint64_t>(std::llround(target));
    seek_time_us(clamped);
  }

  std::uint64_t anchor_time_us_{};
  double range_seconds_{120.0};
  Qt::MouseButton drag_button_{Qt::NoButton};
  bool dragging_{};
};

class TimelineWidget final : public QWidget {
 public:
  explicit TimelineWidget(QWidget* parent = nullptr) : QWidget(parent), navigation_(100, {0, 25, 50, 75}) { setFocusPolicy(Qt::StrongFocus); setMinimumHeight(52); setMaximumHeight(62); setAccessibleName("Frame timeline"); setAccessibleDescription("Display-order timeline with random-access frames and In/Out marks"); }
  std::size_t current_frame() const { return navigation_.current(); }
  std::size_t frame_count() const { return navigation_.frame_count(); }
  std::size_t output_frame_count() const {
    std::size_t removed = 0;
    for (const auto& range : cut_ranges_) removed += std::min(range.second, frame_count()) - std::min(range.first, frame_count());
    return removed < frame_count() ? frame_count() - removed : 0;
  }
  std::optional<std::size_t> source_frame_for_output_frame(std::size_t output) const {
    if (output >= output_frame_count()) return std::nullopt;
    std::size_t cursor = 0;
    std::size_t output_cursor = 0;
    for (const auto& cut : cut_ranges_) {
      const auto first = std::min(cut.first, frame_count());
      const auto end = std::min(cut.second, frame_count());
      if (cursor < first) {
        const auto length = first - cursor;
        if (output < output_cursor + length) return cursor + output - output_cursor;
        output_cursor += length;
      }
      cursor = std::max(cursor, end);
    }
    if (cursor < frame_count() && output < output_cursor + frame_count() - cursor)
      return cursor + output - output_cursor;
    return std::nullopt;
  }
  bool at_playback_end(bool stop_at_out_mark = false) const {
    if (frame_count() == 0 || (stop_at_out_mark && out_mark_ && navigation_.current() >= *out_mark_)) return true;
    if (!preview_mode_) return navigation_.current() + 1 >= frame_count();
    const auto count = output_frame_count();
    return count == 0 || output_frame(navigation_.current()) + 1 >= count;
  }
  bool range_invalid() const { return in_mark_ && out_mark_ && *out_mark_ < *in_mark_; }
  bool has_valid_range() const { return in_mark_ && out_mark_ && !range_invalid(); }
  std::size_t range_start() const { return in_mark_.value_or(0); }
  std::size_t range_end() const {
    if (!out_mark_) return navigation_.frame_count();
    const auto count = navigation_.frame_count();
    return count > 0 && *out_mark_ == count - 1 ? count : *out_mark_;
  }
  std::uint64_t current_time_us() const { return navigation_.current_time_us(); }
  std::uint64_t display_time_us() const {
    if (!preview_mode_) return current_time_us();
    const auto output = output_frame(navigation_.current());
    if (const auto value = frame_time_us(std::min(output, frame_count() > 0 ? frame_count() - 1 : 0))) return *value;
    return 0;
  }
  std::optional<std::uint64_t> frame_time_us(std::size_t frame) const {
    if (frame >= frame_pts_.size() || frame_pts_[frame] < 0 || time_base_num_ <= 0 || time_base_den_ <= 0) return std::nullopt;
    const auto value = static_cast<long double>(frame_pts_[frame]) * static_cast<long double>(time_base_num_) * 1000000.0L /
                       static_cast<long double>(time_base_den_);
    return value < 0.0L ? std::optional<std::uint64_t>{} : std::optional<std::uint64_t>{static_cast<std::uint64_t>(value)};
  }
  std::optional<std::int64_t> frame_pts(std::size_t frame) const { return frame < frame_pts_.size() ? std::optional<std::int64_t>(frame_pts_[frame]) : std::nullopt; }
  bool set_source_time_us(std::uint64_t time_us) { if (!navigation_.set_time_us(time_us)) return false; update(); if (on_frame_changed) on_frame_changed(navigation_.current()); return true; }
  bool set_time_us(std::uint64_t time_us) {
    if (!preview_mode_) return set_source_time_us(time_us);
    const auto output = frame_at_time_us(time_us);
    const auto source = source_frame_for_output_frame(std::min(output, output_frame_count() > 0 ? output_frame_count() - 1 : 0));
    return source && set_frame(*source);
  }
  bool seek_relative_seconds(double seconds) { const auto offset = static_cast<long double>(seconds) * 1000000.0L; const auto target = static_cast<long double>(display_time_us()) + offset; return set_time_us(target <= 0.0L ? 0ULL : static_cast<std::uint64_t>(target)); }
  bool set_frame(std::size_t frame) {
    if (preview_mode_) {
      const auto kept = kept_frame(frame, true);
      if (!kept) return false;
      frame = *kept;
    }
    if (!navigation_.set_frame(frame)) return false;
    update();
    if (on_frame_changed) on_frame_changed(navigation_.current());
    return true;
  }
  void step_forward() {
    if (!preview_mode_) { navigation_.step(1); notify_frame_changed(); return; }
    const auto source = source_frame_for_output_frame(output_frame(navigation_.current()) + 1);
    if (source) set_frame(*source);
  }
  void step_backward() {
    if (!preview_mode_) { navigation_.step(-1); notify_frame_changed(); return; }
    const auto output = output_frame(navigation_.current());
    if (output > 0) if (const auto source = source_frame_for_output_frame(output - 1)) set_frame(*source);
  }
  void previous_random_access() { do { if (!navigation_.previous_keyframe()) break; } while (preview_mode_ && frame_is_cut(navigation_.current())); if (preview_mode_ && frame_is_cut(navigation_.current())) if (const auto kept = kept_frame(navigation_.current(), false)) navigation_.set_frame(*kept); notify_frame_changed(); }
  void next_random_access() { do { if (!navigation_.next_keyframe()) break; } while (preview_mode_ && frame_is_cut(navigation_.current())); if (preview_mode_ && frame_is_cut(navigation_.current())) if (const auto kept = kept_frame(navigation_.current(), true)) navigation_.set_frame(*kept); notify_frame_changed(); }
  void goto_beginning() { if (preview_mode_) { if (const auto source = source_frame_for_output_frame(0)) set_frame(*source); } else set_frame(0); }
  void goto_end() { if (preview_mode_) { const auto count = output_frame_count(); if (count > 0) if (const auto source = source_frame_for_output_frame(count - 1)) set_frame(*source); } else if (navigation_.frame_count() > 0) set_frame(navigation_.frame_count() - 1); }
  void goto_in_mark() { if (in_mark_) set_frame(*in_mark_); }
  void goto_out_mark() { if (out_mark_) set_frame(*out_mark_); }
  void previous_cut_boundary() { std::vector<std::size_t> boundaries; for (const auto& range : cut_ranges_) { boundaries.push_back(range.first); if (range.second < navigation_.frame_count()) boundaries.push_back(range.second); } std::sort(boundaries.begin(), boundaries.end()); const auto found = std::lower_bound(boundaries.begin(), boundaries.end(), navigation_.current()); if (found != boundaries.begin()) set_frame(*std::prev(found)); }
  void next_cut_boundary() { std::vector<std::size_t> boundaries; for (const auto& range : cut_ranges_) { boundaries.push_back(range.first); if (range.second < navigation_.frame_count()) boundaries.push_back(range.second); } std::sort(boundaries.begin(), boundaries.end()); const auto found = std::upper_bound(boundaries.begin(), boundaries.end(), navigation_.current()); if (found != boundaries.end()) set_frame(*found); }
  void set_chapter_markers(std::vector<std::size_t> markers) { for (auto& marker : markers) if (navigation_.frame_count() > 0) marker = std::min(marker, navigation_.frame_count() - 1); std::sort(markers.begin(), markers.end()); markers.erase(std::unique(markers.begin(), markers.end()), markers.end()); chapter_markers_ = std::move(markers); update(); }
  const std::vector<std::size_t>& chapter_markers() const { return chapter_markers_; }
  void toggle_chapter_marker() { const auto frame = navigation_.current(); const auto found = std::lower_bound(chapter_markers_.begin(), chapter_markers_.end(), frame); if (found != chapter_markers_.end() && *found == frame) chapter_markers_.erase(found); else chapter_markers_.insert(found, frame); update(); if (on_chapters_changed) on_chapters_changed(); }
  void clear_chapter_markers() { chapter_markers_.clear(); update(); if (on_chapters_changed) on_chapters_changed(); }
  void previous_chapter() { auto found = std::lower_bound(chapter_markers_.begin(), chapter_markers_.end(), navigation_.current()); while (found != chapter_markers_.begin()) { --found; if (!preview_mode_ || !frame_is_cut(*found)) { set_frame(*found); return; } } }
  void next_chapter() { auto found = std::upper_bound(chapter_markers_.begin(), chapter_markers_.end(), navigation_.current()); while (found != chapter_markers_.end()) { if (!preview_mode_ || !frame_is_cut(*found)) { set_frame(*found); return; } ++found; } }
  void set_bookmark() { bookmark_ = navigation_.current(); update(); if (on_bookmark_changed) on_bookmark_changed(*bookmark_); }
  void goto_bookmark() { if (bookmark_) set_frame(*bookmark_); }
  std::optional<std::size_t> bookmark() const { return bookmark_; }
  std::size_t frame_at_time_us(std::uint64_t time_us) const { if (frame_pts_.empty()) return 0; const auto target = static_cast<std::int64_t>((static_cast<long double>(time_us) * time_base_den_) / (static_cast<long double>(time_base_num_) * 1000000.0L)); const auto found = std::lower_bound(frame_pts_.begin(), frame_pts_.end(), target); return found == frame_pts_.end() ? frame_pts_.size() - 1 : static_cast<std::size_t>(std::distance(frame_pts_.begin(), found)); }
  void mark_in() { if (before_edit) before_edit(); in_mark_ = navigation_.current(); if (on_mark_changed) on_mark_changed(*in_mark_, true); update(); }
  void mark_out() { if (before_edit) before_edit(); out_mark_ = navigation_.current(); if (on_mark_changed) on_mark_changed(*out_mark_, false); update(); }
  std::optional<std::size_t> in_mark() const { return in_mark_; }
  std::optional<std::size_t> out_mark() const { return out_mark_; }
  void set_marks(std::optional<std::size_t> in_mark, std::optional<std::size_t> out_mark) { in_mark_ = in_mark; out_mark_ = out_mark; update(); }
  void set_cut_ranges(const std::vector<FrameRange>& ranges) { cut_ranges_ = ranges; if (preview_mode_) if (const auto kept = kept_frame(navigation_.current(), true)) navigation_.set_frame(*kept); update(); }
  const std::vector<FrameRange>& cut_ranges() const { return cut_ranges_; }
  bool frame_is_cut(std::size_t frame) const { return std::any_of(cut_ranges_.begin(), cut_ranges_.end(), [frame](const FrameRange& range) { return frame >= range.first && frame < range.second; }); }
  std::optional<std::size_t> cut_end_for_frame(std::size_t frame) const { for (const auto& range : cut_ranges_) if (frame >= range.first && frame < range.second) return range.second; return std::nullopt; }
  std::size_t output_frame(std::size_t source_frame) const { std::size_t removed = 0; for (const auto& range : cut_ranges_) { if (source_frame >= range.second) removed += range.second - range.first; else if (source_frame > range.first) removed += source_frame - range.first; } return source_frame >= removed ? source_frame - removed : 0; }
  void set_preview_mode(bool enabled) { preview_mode_ = enabled; if (preview_mode_) if (const auto kept = kept_frame(navigation_.current(), true)) navigation_.set_frame(*kept); update(); }
  bool preview_mode() const { return preview_mode_; }
  void set_scene_mode(bool enabled) { scene_mode_ = enabled; update(); }
  bool scene_mode() const { return scene_mode_; }
  void toggle_zoom() { zoom_factor_ = zoom_factor_ == 1 ? 8 : 1; update(); }
  void zoom_in() { zoom_factor_ = std::min(zoom_factor_ * 2, 64); update(); }
  void zoom_out() { zoom_factor_ = std::max(zoom_factor_ / 2, 1); update(); }
  void pan_zoom(int direction) { const auto count = preview_mode_ ? output_frame_count() : navigation_.frame_count(); if (zoom_factor_ <= 1 || count == 0) return; const auto span = std::max<std::size_t>(1, count / static_cast<std::size_t>(zoom_factor_)); const auto delta = static_cast<long long>(std::max<std::size_t>(1, span / 4)) * direction; const auto current = preview_mode_ ? output_frame(navigation_.current()) : navigation_.current(); const auto target = std::clamp(static_cast<long long>(current) + delta, 0LL, static_cast<long long>(count - 1)); if (preview_mode_) { if (const auto source = source_frame_for_output_frame(static_cast<std::size_t>(target))) set_frame(*source); } else set_frame(static_cast<std::size_t>(target)); }
  void set_navigation(std::size_t frame_count, const std::vector<std::size_t>& keyframes, const std::vector<std::int64_t>& frame_pts, std::int64_t time_base_num, std::int64_t time_base_den) { frame_pts_ = frame_pts; time_base_num_ = time_base_num; time_base_den_ = time_base_den; navigation_ = smartcut::core::FrameNavigation(frame_count, keyframes, frame_pts, time_base_num, time_base_den); in_mark_.reset(); out_mark_.reset(); bookmark_.reset(); cut_ranges_.clear(); chapter_markers_.clear(); update(); }
  void seek_fraction(double fraction) { const auto count = preview_mode_ ? output_frame_count() : navigation_.frame_count(); if (count == 0) return; const auto clamped = std::clamp(fraction, 0.0, 1.0); const auto display = static_cast<std::size_t>(clamped * static_cast<double>(count - 1)); if (preview_mode_) { if (const auto source = source_frame_for_output_frame(display)) set_frame(*source); } else set_frame(display); }
  std::function<void(std::size_t)> on_frame_changed;
  std::function<void(std::size_t, bool)> on_mark_changed;
  std::function<void()> on_chapters_changed;
  std::function<void(std::size_t)> on_bookmark_changed;
  std::function<void()> on_drag_started;
  std::function<void()> on_drag_finished;
  std::function<void()> before_edit;
 protected:
  void keyPressEvent(QKeyEvent* event) override {
    if (event->key() == Qt::Key_Left) { step_backward(); return; }
    else if (event->key() == Qt::Key_Right) { step_forward(); return; }
    else if (event->key() == Qt::Key_B) { set_bookmark(); return; }
    else if (event->key() == Qt::Key_J) { goto_bookmark(); return; }
    else if (event->key() == Qt::Key_K) { next_random_access(); return; }
    else if (event->key() == Qt::Key_PageUp) { seek_relative_seconds(-120); return; }
    else if (event->key() == Qt::Key_PageDown) { seek_relative_seconds(120); return; }
    else if (event->key() == Qt::Key_I) { mark_in(); return; }
    else if (event->key() == Qt::Key_O) { mark_out(); return; }
    else { QWidget::keyPressEvent(event); return; }
  }
  void mousePressEvent(QMouseEvent* event) override { if (event->button() != Qt::LeftButton) { QWidget::mousePressEvent(event); return; } dragging_ = true; if (on_drag_started) on_drag_started(); seek_from_x(event->position().x()); event->accept(); }
  void mouseMoveEvent(QMouseEvent* event) override { if (!dragging_ || !(event->buttons() & Qt::LeftButton)) { QWidget::mouseMoveEvent(event); return; } seek_from_x(event->position().x()); event->accept(); }
  void mouseReleaseEvent(QMouseEvent* event) override { if (event->button() == Qt::LeftButton && dragging_) { seek_from_x(event->position().x()); dragging_ = false; if (on_drag_finished) on_drag_finished(); event->accept(); return; } QWidget::mouseReleaseEvent(event); }
 protected:
  void paintEvent(QPaintEvent*) override {
    QPainter painter(this); painter.fillRect(rect(), palette().color(QPalette::Window));
    const auto source_count = std::max<std::size_t>(navigation_.frame_count(), 1);
    const auto frame_count = std::max<std::size_t>(preview_mode_ ? output_frame_count() : navigation_.frame_count(), 1);
    const auto current = preview_mode_ ? output_frame(navigation_.current()) : navigation_.current();
    const auto span = zoom_factor_ <= 1 ? frame_count : std::max<std::size_t>(2, frame_count / static_cast<std::size_t>(zoom_factor_)); const auto half = span / 2; const auto start = current > half ? std::min(current - half, frame_count > span ? frame_count - span : 0) : 0; const auto end = std::min(frame_count, start + span); const auto visible = std::max<std::size_t>(end - start, 1);
    const QRect track(12, 13, width() - 24, 17); painter.fillRect(track, preview_mode_ ? QColor("#1479c9") : scene_mode_ ? QColor("#c75050") : QColor("#06b025"));
    const auto frame_x = [&track, start, visible](std::size_t frame) { return track.left() + static_cast<int>(static_cast<long double>(track.width()) * static_cast<long double>(std::clamp(frame, start, start + visible) - start) / static_cast<long double>(visible)); };
    for (const auto& range : cut_ranges_) {
      if (preview_mode_) {
        const auto boundary = output_frame(std::min(range.first, navigation_.frame_count()));
        if (boundary < start || boundary > end) continue;
        const auto x = frame_x(boundary); painter.setBrush(QColor("#2f1e48")); painter.setPen(Qt::NoPen); QPolygon diamond; diamond << QPoint(x, track.center().y() - 5) << QPoint(x + 5, track.center().y()) << QPoint(x, track.center().y() + 5) << QPoint(x - 5, track.center().y()); painter.drawPolygon(diamond);
      } else {
        if (range.second <= start || range.first >= end) continue;
        const auto left = frame_x(std::max(range.first, start));
        const auto right = frame_x(std::min(range.second, end));
        if (!scene_mode_ && right > left) painter.fillRect(QRect(left, track.top(), right - left, track.height()), QColor("#c75050"));
      }
    }
    if (scene_mode_ && !preview_mode_) { std::size_t cursor = start; for (const auto& range : cut_ranges_) { if (range.second <= start) continue; if (range.first >= end) break; const auto kept_end = std::min(range.first, end); if (cursor < kept_end) { const auto left = frame_x(cursor); const auto right = frame_x(kept_end); if (right > left) painter.fillRect(QRect(left, track.top(), right - left, track.height()), QColor("#06b025")); } cursor = std::max(cursor, std::min(range.second, end)); } if (cursor < end) { const auto left = frame_x(cursor); const auto right = frame_x(end); if (right > left) painter.fillRect(QRect(left, track.top(), right - left, track.height()), QColor("#06b025")); } }
    painter.setPen(QColor("#b6b6b6")); painter.drawRect(track);
    for (const auto marker : chapter_markers_) { if (preview_mode_ && frame_is_cut(marker)) continue; const auto displayed = preview_mode_ ? output_frame(marker) : marker; if (displayed < start || displayed >= end) continue; const int x = frame_x(displayed); const QColor marker_colour("#f02020"); painter.setPen(QPen(marker_colour, 1)); painter.drawLine(x, track.top(), x, track.bottom()); painter.setBrush(marker_colour); painter.setPen(Qt::NoPen); QPolygon triangle; triangle << QPoint(x - 5, 9) << QPoint(x + 5, 9) << QPoint(x, 13); painter.drawPolygon(triangle); }
    if (bookmark_ && (!preview_mode_ || !frame_is_cut(*bookmark_))) { const auto displayed = preview_mode_ ? output_frame(*bookmark_) : *bookmark_; if (displayed >= start && displayed < end) { const int x = frame_x(displayed); painter.setBrush(QColor("#ffcc00")); painter.setPen(QPen(QColor("#6b5600"), 1)); QPolygon triangle; triangle << QPoint(x - 6, 36) << QPoint(x + 6, 36) << QPoint(x, 30); painter.drawPolygon(triangle); } }
    const int playhead = frame_x(current);
    painter.save();
    painter.setBrush(QBrush(Qt::white, Qt::SolidPattern));
    painter.setPen(QPen(QColor("#202020"), 1));
    painter.drawRect(QRect(playhead - 5, 10, 10, 24));
    painter.restore();
    const auto draw_range_bracket = [&painter, &frame_x](std::size_t frame, bool opening) {
      const int x = frame_x(frame);
      constexpr int top = 8;
      constexpr int bottom = 36;
      constexpr int cap = 7;
      painter.setPen(QPen(QColor("#111111"), 2));
      painter.drawLine(x, top, x, bottom);
      if (opening) {
        painter.drawLine(x - cap, top, x, top);
        painter.drawLine(x - cap, bottom, x, bottom);
      } else {
        painter.drawLine(x, top, x + cap, top);
        painter.drawLine(x, bottom, x + cap, bottom);
      }
    };
    const auto displayed_in = preview_mode_ ? output_frame(in_mark_.value_or(0)) : in_mark_.value_or(0);
    const auto displayed_out = preview_mode_ ? output_frame(out_mark_.value_or(source_count)) : out_mark_.value_or(frame_count);
    if (displayed_in >= start && displayed_in <= end) draw_range_bracket(displayed_in, true);
    if (displayed_out >= start && displayed_out <= end) draw_range_bracket(displayed_out, false);
  }
 private:
  std::optional<std::size_t> kept_frame(std::size_t frame, bool forward) const {
    if (frame_count() == 0 || output_frame_count() == 0) return std::nullopt;
    frame = std::min(frame, frame_count() - 1);
    if (!frame_is_cut(frame)) return frame;
    for (const auto& range : cut_ranges_) {
      if (frame < range.first || frame >= range.second) continue;
      if (forward && range.second < frame_count()) return range.second;
      if (!forward && range.first > 0) return range.first - 1;
      if (range.second < frame_count()) return range.second;
      if (range.first > 0) return range.first - 1;
    }
    return std::nullopt;
  }
  void notify_frame_changed() { update(); if (on_frame_changed) on_frame_changed(navigation_.current()); }
  void seek_from_x(double x) { const auto fraction = std::clamp((x - 12.0) / std::max(1.0, static_cast<double>(width() - 24)), 0.0, 1.0); const auto count = preview_mode_ ? output_frame_count() : navigation_.frame_count(); if (zoom_factor_ <= 1 || count == 0) { seek_fraction(fraction); return; } const auto span = std::max<std::size_t>(2, count / static_cast<std::size_t>(zoom_factor_)); const auto half = span / 2; const auto current = preview_mode_ ? output_frame(navigation_.current()) : navigation_.current(); const auto start = current > half ? std::min(current - half, count > span ? count - span : 0) : 0; const auto display = std::min(count - 1, start + static_cast<std::size_t>(fraction * static_cast<double>(span - 1))); if (preview_mode_) { if (const auto source = source_frame_for_output_frame(display)) set_frame(*source); } else set_frame(display); }
  smartcut::core::FrameNavigation navigation_;
  std::vector<std::int64_t> frame_pts_;
  std::vector<FrameRange> cut_ranges_;
  std::vector<std::size_t> chapter_markers_;
  std::optional<std::size_t> bookmark_;
  std::int64_t time_base_num_{1};
  std::int64_t time_base_den_{1};
  std::optional<std::size_t> in_mark_;
  std::optional<std::size_t> out_mark_;
  bool dragging_{};
  bool preview_mode_{};
  bool scene_mode_{};
  int zoom_factor_{1};
};

class StartupSplashWidget final : public QWidget {
 public:
  explicit StartupSplashWidget(QWidget* parent = nullptr)
      : QWidget(parent), image_(QStringLiteral(":/smartcut64/startup-splash.png")) {
    setObjectName(QStringLiteral("startupSplash"));
    setAccessibleName(QStringLiteral("SmartCut64 startup screen; activate Open Video to choose media"));
    setMouseTracking(true);
    setFocusPolicy(Qt::StrongFocus);
    setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
  }

  std::function<void()> open_video;
  QSize sizeHint() const override { return {600, 347}; }
  QSize minimumSizeHint() const override { return {420, 243}; }
  bool image_loaded() const { return !image_.isNull(); }
  bool open_hotspot_contains(const QPointF& widget_position) const {
    if (image_.isNull()) return false;
    const auto target = image_target_rect();
    if (!target.contains(widget_position)) return false;
    const double source_x = (widget_position.x() - target.left()) * image_.width() / target.width();
    const double source_y = (widget_position.y() - target.top()) * image_.height() / target.height();
    QPainterPath open_button;
    open_button.addRoundedRect(QRectF(897.0, 530.0, 619.0, 145.0),
                               64.0, 72.0, Qt::AbsoluteSize);
    return open_button.contains(QPointF(source_x, source_y));
  }

 protected:
  void paintEvent(QPaintEvent*) override {
    QPainter painter(this);
    painter.fillRect(rect(), Qt::black);
    painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
    if (!image_.isNull()) painter.drawImage(image_target_rect(), image_);
  }
  void mouseMoveEvent(QMouseEvent* event) override {
    setCursor(open_hotspot_contains(event->position()) ? Qt::PointingHandCursor : Qt::ArrowCursor);
    QWidget::mouseMoveEvent(event);
  }
  void mousePressEvent(QMouseEvent* event) override {
    open_pressed_ = event->button() == Qt::LeftButton && open_hotspot_contains(event->position());
    if (open_pressed_) event->accept(); else QWidget::mousePressEvent(event);
  }
  void mouseReleaseEvent(QMouseEvent* event) override {
    const bool activate = open_pressed_ && event->button() == Qt::LeftButton && open_hotspot_contains(event->position());
    open_pressed_ = false;
    if (activate) {
      event->accept();
      if (open_video) open_video();
      return;
    }
    QWidget::mouseReleaseEvent(event);
  }
  void keyPressEvent(QKeyEvent* event) override {
    if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter || event->key() == Qt::Key_Space) {
      event->accept();
      if (open_video) open_video();
      return;
    }
    QWidget::keyPressEvent(event);
  }
  void leaveEvent(QEvent* event) override {
    setCursor(Qt::ArrowCursor);
    QWidget::leaveEvent(event);
  }

 private:
  QRectF image_target_rect() const {
    if (image_.isNull() || width() <= 0 || height() <= 0) return {};
    const QSizeF scaled = QSizeF(image_.size()).scaled(size(), Qt::KeepAspectRatio);
    return {(width() - scaled.width()) / 2.0, (height() - scaled.height()) / 2.0,
            scaled.width(), scaled.height()};
  }

  QImage image_;
  bool open_pressed_{};
};

class ExportWindow final : public QMainWindow {
 public:
  enum class OverlapChoice { combine, replace, cancel };
  enum class SaveVideoChoice { cancel, save, queue };
  static QVector<OutputProfile> standard_output_profiles() {
    const auto base = [](QString id, QString name, QString mode, QString type, QString extension) {
      OutputProfile profile;
      profile.id = std::move(id);
      profile.name = std::move(name);
      profile.mode = std::move(mode);
      profile.file_type = std::move(type);
      profile.file_extension = std::move(extension);
      profile.description = "Built-in validated output profile.";
      return profile;
    };
    QVector<OutputProfile> profiles;
    auto audio = base("audio-only", "Audio Only", "intelligent", "Elementary", "aac");
    audio.audio_only = true;
    profiles.push_back(audio);
    profiles.push_back(base("elementary", "Elementary Streams", "intelligent", "Elementary", "m2v"));
    auto ipad1080 = base("ipad-1080", "iPad / iPhone (1080p)", "full", "MP4", "mp4");
    ipad1080.video_encoder = "h264"; ipad1080.video_width = 1920; ipad1080.video_height = 1080;
    ipad1080.video_bit_depth = 8; ipad1080.video_bitrate_kbps = 8000; ipad1080.video_max_bitrate_kbps = 12000;
    ipad1080.audio_codec = "aac"; ipad1080.audio_bitrate = 192000; ipad1080.audio_sample_rate = 48000; ipad1080.audio_channels = 2;
    profiles.push_back(ipad1080);
    auto ipad720 = base("ipad-720", "iPad 2 / iPhone 4 (720p)", "full", "MP4", "mp4");
    ipad720.video_encoder = "h264"; ipad720.video_width = 1280; ipad720.video_height = 720;
    ipad720.video_bit_depth = 8; ipad720.video_bitrate_kbps = 5000; ipad720.video_max_bitrate_kbps = 8000;
    ipad720.audio_codec = "aac"; ipad720.audio_bitrate = 160000; ipad720.audio_sample_rate = 48000; ipad720.audio_channels = 2;
    profiles.push_back(ipad720);
    auto m2ts = base("m2ts", "M2TS", "intelligent", "M2TS", "m2ts"); m2ts.ts_m2ts_mode = true; profiles.push_back(m2ts);
    profiles.push_back(base("intelligent", "Matroska MKV", "intelligent", "MKV", "mkv"));
    profiles.push_back(base("mp4", "MP4", "intelligent", "MP4", "mp4"));
    auto ffv1 = base("ffv1-lossless", "FFV1 Lossless", "full", "MKV", "mkv");
    ffv1.video_encoder = "ffv1"; ffv1.hardware_encoder = "auto";
    ffv1.description = "Mathematically lossless FFV1. Uses Vulkan acceleration when enabled and available.";
    profiles.push_back(ffv1);
    auto prores = base("prores", "Apple ProRes", "full", "MOV", "mov");
    prores.video_encoder = "prores"; prores.hardware_encoder = "auto"; prores.video_bit_depth = 10;
    prores.description = "10-bit Apple ProRes. Uses Vulkan acceleration when enabled and available.";
    profiles.push_back(prores);
    profiles.push_back(base("program-stream", "Program Stream", "intelligent", "Program Stream", "mpg"));
    profiles.push_back(base("topfield-rec", "Topfield REC", "intelligent", "Topfield REC", "rec"));
    profiles.push_back(base("transport-stream", "Transport Stream", "intelligent", "TS", "ts"));
    profiles.push_back(base("quicktime-mov", "QuickTime MOV", "intelligent", "MOV", "mov"));
    profiles.push_back(base("windows-tv", "Windows Television", "intelligent", "WTV", "wtv"));
    profiles.push_back(base("dvr-ms", "DVR-MS", "intelligent", "DVRMS", "dvr-ms"));
    profiles.push_back(base("mxf", "Material Exchange Format", "intelligent", "MXF", "mxf"));
    profiles.push_back(base("keyframe", "Keyframe-Only — lossless GOP cuts", "keyframe", "MKV", "mkv"));
    profiles.push_back(base("full", "Full Transcode — match source codec", "full", "MKV", "mkv"));
    profiles.push_back(base("quickstream", "QuickStream Fix — packet copy whole source", "quickstream", "MKV", "mkv"));
    return profiles;
  }
  static ContainerCompatibilityReport transport_compatibility(
      const QJsonArray& streams, const QVector<int>& selected_streams,
      const OutputProfile& profile, const QString& output_suffix) {
    ContainerCompatibilityReport report;
    const auto suffix = output_suffix.toLower();
    const bool m2ts = suffix == "m2ts" || suffix == "mts";
    const bool transport = m2ts || suffix == "ts" || suffix == "rec";
    if (!transport) return report;
    report.container = m2ts ? "Blu-ray M2TS" : suffix == "rec" ? "Topfield transport stream"
                                                              : "MPEG transport stream";
    const QSet<QString> ts_video{"mpeg1video", "mpeg2video", "mpeg4", "h264", "hevc", "vvc",
                                 "cavs", "avs2", "avs3", "dirac", "vc1"};
    const QSet<QString> ts_audio{"mp2", "mp3", "aac", "aac_latm", "ac3", "eac3", "dts",
                                 "truehd", "opus"};
    const QSet<QString> ts_subtitle{"dvb_subtitle", "dvb_teletext", "arib_caption"};
    const QSet<QString> m2ts_video{"mpeg2video", "h264", "hevc", "vc1"};
    const QSet<QString> m2ts_audio{"pcm_bluray", "ac3", "eac3", "dts", "truehd"};
    const QSet<QString> m2ts_subtitle{"hdmv_pgs_subtitle", "hdmv_text_subtitle"};
    int primary_video = -1;
    int primary_audio = -1;
    for (const auto value : streams) {
      const auto stream = value.toObject();
      const int index = stream.value("index").toInt(-1);
      if (!selected_streams.isEmpty() && !selected_streams.contains(index)) continue;
      const int type = stream.value("type").toInt(-1);
      if (type == 0 && primary_video < 0) primary_video = index;
      if (type == 1 && primary_audio < 0) primary_audio = index;
    }
    for (const auto value : streams) {
      const auto stream = value.toObject();
      const int index = stream.value("index").toInt(-1);
      if (!selected_streams.isEmpty() && !selected_streams.contains(index)) continue;
      const int type = stream.value("type").toInt(-1);
      QString codec = stream.value("codec").toString().toLower();
      if (type == 0 && index == primary_video && profile.video_encoder != "source")
        codec = profile.video_encoder == "mpeg2" ? "mpeg2video" : profile.video_encoder.toLower();
      if (type == 1 && index == primary_audio && profile.audio_codec != "copy" &&
          profile.audio_codec != "source")
        codec = profile.audio_codec.toLower();
      if (type == 3 && codec == "dvd_subtitle") {
        report.warnings.push_back(QString("Subtitle stream %1 uses DVD/VobSub. %2 stores it as "
                                         "validated private data; its packet payload is preserved, but "
                                         "some players will not expose it as a selectable subtitle track.")
                                      .arg(index).arg(report.container));
        continue;
      }
      bool native = false;
      if (type == 0) native = (m2ts ? m2ts_video : ts_video).contains(codec);
      else if (type == 1) native = (m2ts ? m2ts_audio : ts_audio).contains(codec);
      else if (type == 3) native = (m2ts ? m2ts_subtitle : ts_subtitle).contains(codec);
      else if (!m2ts && type == 2)
        native = codec == "timed_id3" || codec == "smpte_klv" || codec == "smpte_2038";
      if (native) continue;
      ContainerCompatibilityIssue issue;
      issue.stream_index = index;
      issue.stream_type = type;
      issue.codec = codec;
      const QString kind = type == 0 ? "video" : type == 1 ? "audio" : type == 3 ? "subtitle"
                                                                         : type == 4 ? "attachment" : "data";
      issue.explanation = QString("Stream %1 is %2 (%3). %4 has no native mapping for this codec; "
                                  "it would be written as unidentified private data and would not be "
                                  "reliably playable.")
                              .arg(index).arg(kind, codec, report.container);
      issue.transcodable = (type == 0 && index == primary_video) ||
                           (type == 1 && index == primary_audio);
      report.needs_video_transcode = report.needs_video_transcode ||
                                     (issue.transcodable && type == 0);
      report.needs_audio_transcode = report.needs_audio_transcode ||
                                     (issue.transcodable && type == 1);
      report.issues.push_back(std::move(issue));
    }
    report.can_transcode = !report.issues.isEmpty() &&
                           std::all_of(report.issues.cbegin(), report.issues.cend(),
                                       [](const ContainerCompatibilityIssue& issue) {
                                         return issue.transcodable;
                                       });
    return report;
  }
  static void apply_transport_transcode(OutputProfile& profile, const QString& video_codec,
                                        const QString& audio_codec) {
    if (!video_codec.isEmpty()) {
      profile.video_encoder = video_codec;
      profile.hardware_encoder = "software";
      profile.mode = "full";
      if (video_codec == "h264" || video_codec == "mpeg2") profile.video_bit_depth = 8;
    }
    if (!audio_codec.isEmpty()) {
      profile.audio_codec = audio_codec;
      if (profile.audio_bitrate < 16000 || profile.audio_bitrate > 640000)
        profile.audio_bitrate = 192000;
      if (profile.audio_sample_rate == 0) profile.audio_sample_rate = 48000;
    }
  }
  bool smoke_container_compatibility_ready() const {
    const QJsonArray streams{
        QJsonObject{{"index", 0}, {"type", 0}, {"codec", "av1"}},
        QJsonObject{{"index", 1}, {"type", 1}, {"codec", "flac"}}};
    OutputProfile profile{"m2ts-smoke", "M2TS", "intelligent", "Smoke"};
    profile.file_type = "M2TS";
    profile.file_extension = "m2ts";
    profile.ts_m2ts_mode = true;
    const auto blocked = transport_compatibility(streams, {0, 1}, profile, "m2ts");
    apply_transport_transcode(profile, "h264", "ac3");
    const auto repaired = transport_compatibility(streams, {0, 1}, profile, "m2ts");
    const QJsonArray dvd_streams{
        QJsonObject{{"index", 0}, {"type", 0}, {"codec", "mpeg2video"}},
        QJsonObject{{"index", 1}, {"type", 1}, {"codec", "ac3"}},
        QJsonObject{{"index", 2}, {"type", 3}, {"codec", "dvd_subtitle"}}};
    const auto private_subtitle = transport_compatibility(dvd_streams, {0, 1, 2}, profile, "m2ts");
    return blocked.issues.size() == 2 && blocked.can_transcode && blocked.needs_video_transcode &&
           blocked.needs_audio_transcode && repaired.issues.isEmpty() &&
           private_subtitle.issues.isEmpty() && private_subtitle.warnings.size() == 1 &&
           transport_compatibility(streams, {0, 1}, profile, "mkv").issues.isEmpty();
  }
  static bool save_dialog_profile(const OutputProfile& profile) {
    return profile.id != "keyframe" && profile.id != "full" && profile.id != "quickstream";
  }
  static QString profile_codec_label(const OutputProfile& profile) {
    if (profile.audio_only) return "No Video";
    if (profile.video_encoder == "source") return "Match Source";
    if (profile.video_encoder == "h264") return "H.264";
    if (profile.video_encoder == "hevc") return "H.265/HEVC";
    if (profile.video_encoder == "mpeg2") return "MPEG-2";
    if (profile.video_encoder == "ffv1") return "FFV1 Lossless";
    if (profile.video_encoder == "prores") return "Apple ProRes";
    return profile.video_encoder.toUpper();
  }
  static QString profile_container_label(const OutputProfile& profile) {
    if (profile.file_type == "TS") return "Transport Stream";
    if (profile.file_type == "Program Stream") return "Program Stream";
    if (profile.file_type == "Topfield REC") return "Topfield REC";
    if (profile.file_type == "Elementary") return "Elementary Streams";
    return profile.file_type;
  }
  static QString profile_mode_label(const OutputProfile& profile) {
    if (profile.mode == "full") return "Force Recode";
    if (profile.mode == "keyframe") return "Same as Source";
    return "Intelligent";
  }
  ~ExportWindow() override {
    shutting_down_ = true; ++control_request_generation_; playback_timer_.stop(); audio_delay_timer_.stop(); indexing_status_timer_.stop(); thumbnail_refresh_timer_.stop(); thumbnail_layout_timer_.stop(); project_autosave_timer_.stop(); watch_folder_timer_.stop(); preview_resize_timer_.stop(); export_progress_timer_.stop();
    if (export_paused_ && worker_.state() != QProcess::NotRunning) set_process_threads_suspended(worker_.processId(), false);
    const auto stop = [](QProcess& process) { if (process.state() == QProcess::NotRunning) return; process.kill(); process.waitForFinished(2000); };
    for (auto* process : {&control_worker_, &worker_, &autosave_worker_, &project_interchange_, &journal_snapshot_, &audio_playback_, &video_playback_, &ad_scan_, &join_worker_, &join_item_worker_, &title_worker_, &metadata_worker_, &qsf_worker_, &trim_copy_worker_, &batch_worker_, &source_probe_, &identity_probe_, &metadata_probe_, &preview_probe_, &thumbnail_probe_, &waveform_probe_, &sync_probe_, &evidence_probe_, &history_probe_, &project_validation_, &audio_validation_, &preflight_record_, &project_status_update_, &project_summary_}) stop(*process);
    for (auto& entry : control_threads_) if (entry.thread.joinable()) entry.thread.join();
  }
  void smoke_open_project(const QString& path) { QFile file(path); if (file.open(QIODevice::ReadOnly)) { pending_project_path_ = path; pending_project_json_ = QString::fromUtf8(file.readAll()); project_validation_.start(worker_program(), {"--validate-project", "--project", path}); } }
  void open_command_line_path(const QString& path) { open_startup_path(path); }
  bool smoke_startup_screen_ready() const {
    StartupSplashWidget splash;
    splash.resize(splash.sizeHint());
    const QSizeF image_size(1672.0, 941.0);
    const QSizeF scaled = image_size.scaled(splash.size(), Qt::KeepAspectRatio);
    const QRectF target((splash.width() - scaled.width()) / 2.0,
                        (splash.height() - scaled.height()) / 2.0,
                        scaled.width(), scaled.height());
    const auto source_to_widget = [&target, &image_size](QPointF source) {
      return QPointF(target.left() + source.x() * target.width() / image_size.width(),
                     target.top() + source.y() * target.height() / image_size.height());
    };
    const QPointF open_button = source_to_widget(QPointF(1206.0, 602.0));
    const QPointF rounded_corner = source_to_widget(QPointF(900.0, 532.0));
    bool activated = false;
    splash.open_video = [&activated] { activated = true; };
    QMouseEvent press(QEvent::MouseButtonPress, open_button, open_button, open_button,
                      Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
    QMouseEvent release(QEvent::MouseButtonRelease, open_button, open_button, open_button,
                        Qt::LeftButton, Qt::NoButton, Qt::NoModifier);
    QApplication::sendEvent(&splash, &press);
    QApplication::sendEvent(&splash, &release);
    return splash.image_loaded() && splash.open_hotspot_contains(open_button) && activated &&
           !splash.open_hotspot_contains(rounded_corner) &&
           !splash.open_hotspot_contains(QPointF(50.0, 50.0));
  }
  bool smoke_summary_ready() const { return inspector_ && inspector_->toPlainText().contains("Project summary"); }
  void smoke_force_blocked_export() { project_path_ = "smoke-project"; project_audio_ready_ = false; input_->setText("smoke-input.mkv"); output_->setText("smoke-output.mkv"); start_->click(); }
  bool smoke_blocked_ready() const { return status_ && status_->text().contains("Export blocked"); }
  bool smoke_editing_ready() { set_scene_mode(false, false); timeline_->set_navigation(10, {0, 5}, {}, 1, 25); set_cut_ranges({}); timeline_->seek_fraction(0.75); const bool dragged = timeline_->current_frame() == 6; timeline_->seek_fraction(0.0); timeline_->step_forward(); timeline_->mark_in(); timeline_->step_forward(); timeline_->mark_out(); undo_->click(); const bool mark_undone = !timeline_->out_mark().has_value(); redo_->click(); const bool mark_redone = timeline_->out_mark() == std::optional<std::size_t>(2); trim_->click(); const bool trimmed = cut_ranges_ == std::vector<FrameRange>{{0, 1}, {2, 10}} && cut_list_->rowCount() == 2 && timeline_->frame_is_cut(0) && !timeline_->frame_is_cut(1) && timeline_->frame_is_cut(9) && cut_list_->item(0, 0)->text() == "00:00:00.00" && cut_list_->item(0, 1)->text() == "00:00:00.01"; undo_->click(); const bool cuts_undone = cut_ranges_.empty() && cut_list_->rowCount() == 0; redo_->click(); const bool cuts_redone = cut_ranges_.size() == 2; cut_list_->clearSelection(); cut_list_->selectRow(0); remove_cuts_->click(); const bool removed = cut_ranges_ == std::vector<FrameRange>{{2, 10}}; undo_->click(); invert_cuts_->click(); const bool inverted = cut_ranges_ == std::vector<FrameRange>{{1, 2}} && cut_list_->rowCount() == 1; undo_->click(); return dragged && mark_undone && mark_redone && trimmed && cuts_undone && cuts_redone && removed && inverted && cut_ranges_.size() == 2; }
  bool smoke_end_of_program_selection_ready() {
    set_scene_mode(false, false); timeline_->set_navigation(10, {0, 5}, {}, 1, 25); set_cut_ranges({});
    timeline_->set_frame(7); timeline_->mark_in(); timeline_->goto_end(); timeline_->mark_out(); cut_selected_range();
    const bool final_frame_removed = cut_ranges_ == std::vector<FrameRange>{{7, 10}} && timeline_->frame_is_cut(9) && timeline_->output_frame_count() == 7;
    set_cut_ranges({}); timeline_->set_frame(1); timeline_->mark_in(); timeline_->set_frame(2); timeline_->mark_out(); cut_selected_range();
    const bool ordinary_range_unchanged = cut_ranges_ == std::vector<FrameRange>{{1, 2}} && timeline_->frame_is_cut(1) && !timeline_->frame_is_cut(2);
    return final_frame_removed && ordinary_range_unchanged;
  }
  bool smoke_fine_tune_ready() {
    std::vector<std::int64_t> pts(10000); for (std::size_t frame = 0; frame < pts.size(); ++frame) pts[frame] = static_cast<std::int64_t>(frame);
    timeline_->set_navigation(pts.size(), {0}, pts, 1, 25);
    timeline_->set_frame(5000); const bool normal = fine_tune_->smoke_drag(0.5, Qt::LeftButton) && timeline_->current_frame() == 6500;
    timeline_->set_frame(5000); const bool reverse = fine_tune_->smoke_drag(-0.5, Qt::LeftButton) && timeline_->current_frame() == 3500;
    timeline_->set_frame(5000); const bool shift = fine_tune_->smoke_drag(0.5, Qt::LeftButton, Qt::ShiftModifier) && timeline_->current_frame() == 5750;
    timeline_->set_frame(5000); const bool control = fine_tune_->smoke_drag(0.5, Qt::LeftButton, Qt::ControlModifier) && timeline_->current_frame() == 5500;
    timeline_->set_frame(5000); const bool combined = fine_tune_->smoke_drag(0.5, Qt::LeftButton, Qt::ShiftModifier | Qt::ControlModifier) && timeline_->current_frame() == 5250;
    timeline_->set_frame(5000); const bool precision = fine_tune_->smoke_drag(0.5, Qt::RightButton) && timeline_->current_frame() == 5012;
    return normal && reverse && shift && control && combined && precision && fine_tune_->value() == 0;
  }
  bool smoke_timeline_drag_lifecycle_ready() {
    TimelineWidget timeline;
    timeline.resize(400, 52);
    timeline.set_navigation(100, {0, 25, 50, 75}, {}, 1, 25);
    QString events;
    timeline.on_drag_started = [&events] { events += 'S'; };
    timeline.on_frame_changed = [&events](std::size_t) { events += 'F'; };
    timeline.on_drag_finished = [&events] { events += 'E'; };
    const QPointF press_position(106.0, 21.0);
    const QPointF release_position(294.0, 21.0);
    QMouseEvent press(QEvent::MouseButtonPress, press_position, press_position,
                      press_position, Qt::LeftButton, Qt::LeftButton,
                      Qt::NoModifier);
    QMouseEvent move(QEvent::MouseMove, release_position, release_position,
                     release_position, Qt::NoButton, Qt::LeftButton,
                     Qt::NoModifier);
    QMouseEvent release(QEvent::MouseButtonRelease, release_position,
                        release_position, release_position, Qt::LeftButton,
                        Qt::NoButton, Qt::NoModifier);
    QApplication::sendEvent(&timeline, &press);
    QApplication::sendEvent(&timeline, &move);
    QApplication::sendEvent(&timeline, &release);
    return events.startsWith("SF") && events.endsWith("FE") &&
           events.count('S') == 1 && events.count('E') == 1 &&
           timeline.current_frame() == 74;
  }
  bool smoke_playback_timer_rate_ready() {
    const auto saved_num = frame_rate_num_;
    const auto saved_den = frame_rate_den_;
    frame_rate_num_ = 50;
    frame_rate_den_ = 1;
    const bool fifty = playback_timer_interval_ms() == 20;
    frame_rate_num_ = 30000;
    frame_rate_den_ = 1001;
    const bool ntsc = playback_timer_interval_ms() == 33;
    frame_rate_num_ = 24000;
    frame_rate_den_ = 1001;
    const bool film = playback_timer_interval_ms() == 42;
    frame_rate_num_ = saved_num;
    frame_rate_den_ = saved_den;
    return fifty && ntsc && film && playback_timer_.timerType() == Qt::PreciseTimer;
  }
  bool smoke_chapter_navigation_ready() {
    std::vector<std::int64_t> pts(10); for (std::size_t frame = 0; frame < pts.size(); ++frame) pts[frame] = static_cast<std::int64_t>(frame * 1000);
    timeline_->set_navigation(pts.size(), {0, 5}, pts, 1, 1000);
    timeline_->set_chapter_markers({2, 7}); timeline_->set_frame(5); timeline_->previous_chapter(); const bool previous = timeline_->current_frame() == 2;
    timeline_->next_chapter(); const bool next = timeline_->current_frame() == 7; timeline_->toggle_chapter_marker(); const bool removed = timeline_->chapter_markers() == std::vector<std::size_t>{2};
    timeline_->toggle_chapter_marker(); const bool restored = timeline_->chapter_markers() == std::vector<std::size_t>({2, 7}); timeline_->set_frame(4); timeline_->set_bookmark(); timeline_->set_frame(9); timeline_->goto_bookmark(); const bool bookmark = timeline_->current_frame() == 4 && timeline_->bookmark() == std::optional<std::size_t>(4); timeline_->clear_chapter_markers();
    return previous && next && removed && restored && bookmark && timeline_->chapter_markers().empty();
  }
  bool smoke_timeline_marker_geometry_ready() {
    TimelineWidget timeline;
    timeline.resize(400, 52);
    auto test_palette = timeline.palette();
    test_palette.setColor(QPalette::Window, QColor("#f0f0f0"));
    timeline.setPalette(test_palette);
    timeline.set_navigation(100, {0, 25, 50, 75}, {}, 1, 25);
    timeline.set_chapter_markers({50});
    const auto render = [&timeline] {
      QImage image(timeline.size(), QImage::Format_ARGB32);
      image.fill(Qt::transparent);
      QPainter painter(&image);
      timeline.render(&painter);
      painter.end();
      return image;
    };
    const auto dark = [](const QImage& image, int x, int y) { const auto colour = image.pixelColor(x, y); return colour.red() < 40 && colour.green() < 40 && colour.blue() < 40; };
    const auto light = [](const QImage& image, int x, int y) { const auto colour = image.pixelColor(x, y); return colour.red() > 235 && colour.green() > 235 && colour.blue() > 235; };
    const auto initial = render();
    const auto chapter = initial.pixelColor(200, 21);
    const bool endpoint_pair = dark(initial, 7, 8) && dark(initial, 7, 36) && dark(initial, 392, 8) && dark(initial, 392, 36);
    timeline.set_frame(25); timeline.mark_in(); timeline.set_frame(75); timeline.mark_out();
    timeline.set_frame(30); timeline.mark_in(); timeline.set_frame(70); timeline.mark_out(); timeline.set_frame(40);
    const auto marked = render();
    const bool one_replacement_pair = !dark(marked, 7, 8) && !dark(marked, 392, 8) && !dark(marked, 101, 8) && !dark(marked, 299, 8) &&
                                      dark(marked, 120, 8) && dark(marked, 120, 36) && dark(marked, 279, 8) && dark(marked, 279, 36);
    const bool white_playhead = light(marked, 162, 21);
    return endpoint_pair && chapter.red() > 220 && chapter.green() < 80 && chapter.blue() < 80 && one_replacement_pair && white_playhead;
  }
  bool smoke_legacy_navigation_ready() { std::vector<std::int64_t> pts(20); for (std::size_t frame = 0; frame < pts.size(); ++frame) pts[frame] = static_cast<std::int64_t>(frame * 40); timeline_->set_navigation(20, {0, 5, 10, 15}, pts, 1, 1000); set_cut_ranges({{3, 5}, {10, 12}}); timeline_->goto_beginning(); timeline_->next_cut_boundary(); const bool first_boundary = timeline_->current_frame() == 3; timeline_->next_cut_boundary(); const bool second_boundary = timeline_->current_frame() == 5; timeline_->previous_cut_boundary(); const bool previous_boundary = timeline_->current_frame() == 3; timeline_->set_frame(6); timeline_->next_random_access(); const bool next_keyframe = timeline_->current_frame() == 10; timeline_->previous_random_access(); const bool previous_keyframe = timeline_->current_frame() == 5; timeline_->set_frame(6); timeline_->mark_in(); timeline_->set_frame(9); timeline_->mark_out(); timeline_->goto_in_mark(); const bool selection_start = timeline_->current_frame() == 6; timeline_->goto_out_mark(); const bool selection_end = timeline_->current_frame() == 9; timeline_->goto_end(); const bool end = timeline_->current_frame() == 19; timeline_->goto_beginning(); const bool beginning = timeline_->current_frame() == 0; return first_boundary && second_boundary && previous_boundary && next_keyframe && previous_keyframe && selection_start && selection_end && end && beginning; }
  bool smoke_preview_mode_ready() {
    std::vector<std::int64_t> pts(20); for (std::size_t frame = 0; frame < pts.size(); ++frame) pts[frame] = static_cast<std::int64_t>(frame * 40);
    timeline_->set_navigation(20, {0, 5, 10, 15}, pts, 1, 1000); set_cut_ranges({{3, 5}, {10, 12}}); set_preview_mode(true);
    const bool cut_frames_hidden = !playback_frame_visible(3) && playback_frame_visible(2);
    const bool counts = timeline_->output_frame_count() == 16;
    const bool mappings = timeline_->source_frame_for_output_frame(0) == std::optional<std::size_t>(0) && timeline_->source_frame_for_output_frame(3) == std::optional<std::size_t>(5) && timeline_->source_frame_for_output_frame(8) == std::optional<std::size_t>(12) && timeline_->source_frame_for_output_frame(15) == std::optional<std::size_t>(19);
    timeline_->set_frame(2); timeline_->step_forward(); const bool forward_skip = timeline_->current_frame() == 5;
    timeline_->step_backward(); const bool backward_skip = timeline_->current_frame() == 2;
    timeline_->set_frame(3); const bool direct_skip = timeline_->current_frame() == 5;
    timeline_->seek_fraction(0.5); const bool compressed_seek = timeline_->current_frame() == 9;
    timeline_->goto_end(); const bool end = timeline_->current_frame() == 19; timeline_->goto_beginning(); const bool beginning = timeline_->current_frame() == 0;
    const auto thumbnails = preview_thumbnail_frame_window(*timeline_, 5, 9, 1);
    const QVector<std::optional<std::size_t>> expected_thumbnails{std::nullopt, 0, 1, 2, 5, 6, 7, 8, 9};
    const bool thumbnail_jump = thumbnails == expected_thumbnails &&
                                std::none_of(thumbnails.cbegin(), thumbnails.cend(), [this](const auto& frame) {
                                  return frame && timeline_->frame_is_cut(*frame);
                                });
    timeline_->set_marks(1, 9); timeline_->set_frame(9);
    const bool ordinary_play_ignores_mark_out = !timeline_->at_playback_end();
    const bool repeat_stops_at_mark_out = timeline_->at_playback_end(true);
    timeline_->set_marks(std::nullopt, std::nullopt);
    set_preview_mode(false);
    return counts && mappings && forward_skip && backward_skip && direct_skip && compressed_seek && end && beginning && thumbnail_jump && cut_frames_hidden && ordinary_play_ignores_mark_out && repeat_stops_at_mark_out && !timeline_->preview_mode();
  }
  bool smoke_zoom_thumbnail_ready() {
    timeline_->set_navigation(100, {0}, {}, 1, 25); timeline_->set_frame(50); timeline_->toggle_zoom(); timeline_->zoom_in(); timeline_->pan_zoom(1);
    const bool panned = timeline_->current_frame() > 20;
    active_thumbnail_frames_ = {10, 20, 30, 40, 50, 60, 70, 80, 90, 99};
    jump_thumbnail_digit(5);
    const bool digit = timeline_->current_frame() == 60;
    const auto original_interval = thumbnail_interval_seconds_;
    set_thumbnail_interval_step(6, false);
    const bool interval = std::abs(thumbnail_interval_seconds_ - 2.0) < 0.001;
    const auto window = thumbnail_frame_window(400, 1000, 19, 1);
    const bool contiguous = window.size() == 19 && window[9] == 400 && window.front() == 391 && window.back() == 409 &&
                            std::adjacent_find(window.cbegin(), window.cend(), [](const auto& first, const auto& second) {
                              return !first || !second || *second != *first + 1;
                            }) == window.cend();
    const auto beginning = thumbnail_frame_window(0, 1000, 19, 1);
    const auto ending = thumbnail_frame_window(999, 1000, 19, 1);
    const bool centered_edges = beginning.size() == 19 && beginning[9] == 0 &&
                                std::all_of(beginning.cbegin(), beginning.cbegin() + 9, [](const auto& frame) { return !frame; }) &&
                                ending[9] == 999 &&
                                std::all_of(ending.cbegin() + 10, ending.cend(), [](const auto& frame) { return !frame; });
    const auto short_window = thumbnail_frame_window(2, 5, 19, 1);
    const bool no_repeated_edges = short_window[9] == 2 &&
                                   std::count_if(short_window.cbegin(), short_window.cend(), [](const auto& frame) { return frame.has_value(); }) == 5;
    const auto wide_slots = adaptive_thumbnail_count(1800, 80, 16.0 / 9.0, 19);
    const auto narrow_slots = adaptive_thumbnail_count(900, 80, 16.0 / 9.0, 19);
    const bool adaptive = wide_slots > narrow_slots && narrow_slots >= 3 && wide_slots <= 19 &&
                          wide_slots % 2 == 1 && narrow_slots % 2 == 1;
    thumbnail_interval_seconds_ = original_interval;
    timeline_->toggle_zoom();
    return panned && digit && interval && contiguous && centered_edges && no_repeated_edges && adaptive;
  }
  bool smoke_thumbnail_cut_indicator_ready() {
    QPixmap sample(16, 9); sample.fill(Qt::black);
    ThumbnailWidget kept; kept.resize(80, 58); kept.set_frame(0, sample); kept.set_cut(false);
    ThumbnailWidget cut; cut.resize(80, 58); cut.set_frame(0, sample); cut.set_cut(true);
    QImage kept_image(kept.size(), QImage::Format_ARGB32), cut_image(cut.size(), QImage::Format_ARGB32);
    kept_image.fill(Qt::transparent); cut_image.fill(Qt::transparent);
    QPainter kept_painter(&kept_image); kept.render(&kept_painter); kept_painter.end();
    QPainter cut_painter(&cut_image); cut.render(&cut_painter); cut_painter.end();
    const auto green = kept_image.pixelColor(40, 1); const auto red = cut_image.pixelColor(40, 1);
    return green.green() > 120 && green.red() < 80 && red.red() > 120 && red.green() < 120;
  }
  bool smoke_managed_shortcuts_ready() { if (managed_shortcuts_.size() < 40 || !managed_shortcuts_.contains("Edit/Preview Mode")) return false; auto* shortcut = managed_shortcuts_["Edit/Preview Mode"]; const auto saved = shortcut->key(); shortcut->setKey(QKeySequence(Qt::Key_F2)); const bool changed = shortcut->key() == QKeySequence(Qt::Key_F2); shortcut->setKey(saved); return changed && default_shortcuts_.value("Edit/Preview Mode") == QKeySequence(Qt::Key_P); }
  bool smoke_output_profiles_ready() const { return output_profile_ && output_profile_->count() >= 4 && output_profile_->findData("intelligent") >= 0 && output_profile_->findData("keyframe") >= 0 && output_profile_->findData("full") >= 0 && output_profile_->findData("quickstream") >= 0; }
  bool smoke_save_video_profiles_ready() const {
    const auto profiles = standard_output_profiles();
    const auto find = [&profiles](const QString& id) { return std::find_if(profiles.cbegin(), profiles.cend(), [&id](const OutputProfile& profile) { return profile.id == id; }); };
    const auto audio = find("audio-only"), elementary = find("elementary"), ipad = find("ipad-1080"), mkv = find("intelligent"), mp4 = find("mp4"), m2ts = find("m2ts"), program = find("program-stream"), rec = find("topfield-rec"), transport = find("transport-stream");
    return audio != profiles.cend() && audio->audio_only && output_profile_extension(*audio) == "aac" &&
           elementary != profiles.cend() && elementary->file_type == "Elementary" &&
           ipad != profiles.cend() && ipad->mode == "full" && ipad->video_encoder == "h264" && ipad->video_width == 1920 &&
           mkv != profiles.cend() && mkv->file_extension == "mkv" && mp4 != profiles.cend() && mp4->file_extension == "mp4" &&
           m2ts != profiles.cend() && m2ts->ts_m2ts_mode && program != profiles.cend() && program->file_extension == "mpg" &&
           rec != profiles.cend() && rec->file_extension == "rec" && transport != profiles.cend() && transport->file_extension == "ts";
  }
  bool smoke_custom_profiles_ready() {
    QSettings settings("SmartCut64", "SmartCut64");
    const auto backup = settings.value("output/customProfiles");
    const auto backup_profiles = custom_profiles_;
    OutputProfile smoke{"custom-smoke", "Smoke Smart Copy", "intelligent", "roundtrip"};
    smoke.file_type = "M2TS";
    smoke.file_extension = "m2ts";
    smoke.video_encoder = "mpeg2";
    smoke.hardware_encoder = "software";
    smoke.video_width = 1920;
    smoke.video_height = 1080;
    smoke.video_bit_depth = 8;
    smoke.video_bitrate_kbps = 40000;
    smoke.video_max_bitrate_kbps = 35000;
    smoke.aspect_ratio = "16:9";
    smoke.deinterlace = "None";
    smoke.frame_rate = "25";
    smoke.ts_transport_stream_id = 42;
    smoke.ts_original_network_id = 43;
    smoke.ts_service_id = 44;
    smoke.ts_pmt_start_pid = 4096;
    smoke.ts_start_pid = 512;
    smoke.favorite = true;
    smoke.audio_codec = "aac";
    smoke.audio_gain_db = 3.0;
    smoke.audio_bitrate = 160000;
    smoke.audio_sample_rate = 48000;
    smoke.audio_channels = 2;
    custom_profiles_ = {smoke};
    save_custom_profiles();
    custom_profiles_.clear();
    load_custom_profiles();
    const auto restored_profile = custom_profiles_.isEmpty() ? OutputProfile{} : custom_profiles_.front();
    const bool restored = custom_profiles_.size() == 1 && restored_profile.id == "custom-smoke" &&
                          restored_profile.file_type == "M2TS" && restored_profile.file_extension == "m2ts" &&
                          restored_profile.video_encoder == "mpeg2" && restored_profile.video_width == 1920 &&
                          restored_profile.video_height == 1080 && restored_profile.video_bit_depth == 8 &&
                          restored_profile.video_bitrate_kbps == 40000 &&
                          restored_profile.video_max_bitrate_kbps == 35000 && restored_profile.aspect_ratio == "16:9" &&
                          restored_profile.frame_rate == "25" && restored_profile.ts_transport_stream_id == 42 &&
                          restored_profile.ts_original_network_id == 43 && restored_profile.ts_service_id == 44 &&
                          restored_profile.ts_pmt_start_pid == 4096 && restored_profile.ts_start_pid == 512 &&
                          restored_profile.favorite &&
                          restored_profile.audio_codec == "aac" && qFuzzyCompare(restored_profile.audio_gain_db, 3.0) &&
                          restored_profile.audio_bitrate == 160000 && restored_profile.audio_sample_rate == 48000 &&
                          restored_profile.audio_channels == 2 && output_profile_->findData("custom-smoke") >= 0 &&
                          output_profile_mode("custom-smoke") == "intelligent";

    const auto source_path = preview_dir_.path() + "/videoredo-profile-smoke.OP.xml";
    const auto exported_path = preview_dir_.path() + "/videoredo-profile-roundtrip.OP.xml";
    QFile source(source_path);
    const QByteArray fixture =
        "<?xml version=\"1.0\"?><VRDOutputProfileList version=\"2\"><VRDOutputProfile enabled=\"true\" "
        "favorite=\"true\"><Name>VideoReDo M2TS Smoke</Name><OutputMode>Smart</OutputMode><FileType>M2TS</FileType>"
        "<VideoAttributes><Encoder>MPEG2</Encoder><AspectRatio>16:9</AspectRatio><EncodeDimensions><Width>1920</Width>"
        "<Height>1080</Height></EncodeDimensions><BitDepth>8</BitDepth><EncodeBitrate>40000</EncodeBitrate><MaxBitrate>35000</MaxBitrate>"
        "<DeinterlaceMethod>None</DeinterlaceMethod><FrameRate>25</FrameRate></VideoAttributes><AudioAttributes>"
        "<Encoder>AAC</Encoder><Bitrate>160</Bitrate><SampleRate>48000</SampleRate><ChannelMode>Stereo</ChannelMode>"
        "</AudioAttributes></VRDOutputProfile></VRDOutputProfileList>";
    const bool fixture_written = source.open(QIODevice::WriteOnly | QIODevice::Truncate) && source.write(fixture) == fixture.size();
    source.close();
    const int imported = fixture_written ? import_videoredo_profiles(source_path) : 0;
    const auto imported_profile = std::find_if(custom_profiles_.cbegin(), custom_profiles_.cend(), [](const OutputProfile& value) {
      return value.name == "VideoReDo M2TS Smoke";
    });
    const bool import_valid = imported == 1 && imported_profile != custom_profiles_.cend() &&
                              imported_profile->file_type == "M2TS" && imported_profile->video_encoder == "mpeg2" &&
                              imported_profile->video_width == 1920 && imported_profile->video_height == 1080 &&
                              imported_profile->video_bit_depth == 8 && imported_profile->audio_codec == "aac" &&
                              imported_profile->audio_sample_rate == 48000;
    const bool exported = import_valid && export_videoredo_profiles(exported_path);
    QFile roundtrip(exported_path);
    const auto roundtrip_text = roundtrip.open(QIODevice::ReadOnly) ? roundtrip.readAll() : QByteArray{};
    const bool interchange = exported && roundtrip_text.contains("<VRDOutputProfileList version=\"2\">") &&
                             roundtrip_text.contains("<Name>VideoReDo M2TS Smoke</Name>") &&
                             roundtrip_text.contains("<Encoder>MPEG2</Encoder>") &&
                             roundtrip_text.contains("<Width>1920</Width>");

    if (backup.isValid()) settings.setValue("output/customProfiles", backup);
    else settings.remove("output/customProfiles");
    custom_profiles_ = backup_profiles;
    const int standard_count = standard_output_profiles().size();
    while (output_profile_->count() > standard_count) output_profile_->removeItem(standard_count);
    for (const auto& profile : custom_profiles_) if (profile.enabled) output_profile_->addItem(profile.favorite ? "★ " + profile.name : profile.name, profile.id);
    return restored && interchange;
  }
  bool smoke_multi_title_ready() { TitleCanvasWidget canvas; TitleTextObject first; first.id = "one"; first.text = "First"; first.rect = QRect(50, 50, 500, 180); first.colour = Qt::red; TitleTextObject second; second.id = "two"; second.text = "Second"; second.rect = QRect(650, 430, 500, 180); second.colour = Qt::blue; second.alignment = Qt::AlignRight | Qt::AlignVCenter; second.outline_width = 3; second.shadow_offset = 8; canvas.objects = {first, second}; canvas.selected = 1; canvas.safe_area = true; const auto clean = canvas.render_image(false); const auto overlay = canvas.render_image(true); return canvas.objects.size() == 2 && clean.size() == QSize(1280, 720) && clean != overlay && canvas.objects[1].alignment.testFlag(Qt::AlignRight); }
  bool smoke_export_progress_ready() {
    const auto saved_input = input_->text();
    const auto saved_output = output_->text();
    input_->setText(preview_dir_.path() + "/Smoke Source.mkv");
    output_->setText(preview_dir_.path() + "/Smoke Output.mkv");
    show_export_progress_dialog("Fast Frame Copy", 3);
    const bool ready = export_progress_dialog_ && export_progress_bar_ && export_progress_bar_->format() == "%p%" &&
                       export_progress_dialog_->windowTitle() == "Output: Smoke Output.mkv" &&
                       export_progress_name_->text() == "Smoke Source" && export_progress_mode_->text() == "Fast Frame Copy" &&
                       export_progress_scene_->text() == "Scene 1 of 3" && export_pause_button_->text() == "Pause" &&
                       export_abort_button_->text() == "Abort";
    close_export_progress_dialog(false);
    input_->setText(saved_input);
    output_->setText(saved_output);
    return ready;
  }
  bool smoke_cancel_cleanup(const QString& output) { output_->setText(output); QFile partial(output); if (!partial.open(QIODevice::WriteOnly | QIODevice::Truncate) || partial.write("partial-export") < 0) return false; partial.close(); cancel_->setEnabled(true); cancel_->click(); return !QFileInfo::exists(output) && status_->text().contains("partial output removed"); }
  void smoke_load_source(const QString& path) { input_->setText(path); load_source(path); }
  bool smoke_source_ready() const { return status_ && status_->text().startsWith("Loaded ") && !source_uuid_.isEmpty() && control_navigation_successes_ > 0; }
  void smoke_restart_control_source() { control_worker_.kill(); control_worker_.waitForFinished(2000); reload_source(); }
  bool smoke_control_restart_ready() const { return smoke_source_ready() && control_navigation_successes_ >= 2 && control_worker_.state() != QProcess::NotRunning; }
  void smoke_force_control_fallback() { control_force_failure_ = true; reload_source(); }
  bool smoke_control_fallback_ready() const { return status_ && status_->text().startsWith("Loaded ") && !source_uuid_.isEmpty() && control_navigation_fallbacks_ > 0 && source_probe_.state() == QProcess::NotRunning; }
  void smoke_start_ad_detective() { start_ad_scan(); }
  bool smoke_ad_detective_ready() const { return ad_scan_.state() == QProcess::NotRunning && status_ && status_->text().startsWith("Ad-Detective found ") && !timeline_->chapter_markers().empty(); }
  std::size_t smoke_chapter_marker_count() const { return timeline_->chapter_markers().size(); }
  void smoke_start_quickstream_fix(const QString& input, const QString& output) { start_quickstream_fix(input, output); }
  bool smoke_quickstream_fix_ready() const { return qsf_worker_.state() == QProcess::NotRunning && status_ && status_->text().startsWith("QuickStream Fix completed and validated:"); }
  void smoke_start_elementary_multiplex(const QStringList& inputs, const QString& output) { start_elementary_multiplex(inputs, output); }
  bool smoke_elementary_multiplex_ready() const { return multiplex_worker_.state() == QProcess::NotRunning && status_ && status_->text().startsWith("Elementary streams multiplexed and validated:"); }
  void smoke_start_batch(const QString& input, const QString& output) { batch_jobs_.clear(); BatchJob job; job.input = input; job.output = output; batch_jobs_.push_back(std::move(job)); start_batch(); }
  bool smoke_batch_ready() const { return !batch_running_ && !batch_jobs_.isEmpty() && batch_jobs_.front().status == "Completed" && QFileInfo::exists(batch_jobs_.front().output); }
  void smoke_start_watch_folder(const QString& input, const QString& output) {
    QSettings settings("SmartCut64", "SmartCut64"); smoke_watch_folders_backup_ = settings.value("batch/watchFolders"); smoke_watch_history_backup_ = settings.value("batch/watchHistory"); smoke_batch_queue_backup_ = settings.value("batch/persistentQueue"); smoke_watch_settings_restored_ = false;
    QFile::remove(output); batch_jobs_.clear(); watch_folders_.clear(); watch_history_.clear(); watch_first_seen_.clear(); smoke_watch_output_ = output;
    const QFileInfo source(input); WatchFolder folder; folder.id = "automation-watch"; folder.input_folder = source.absolutePath(); folder.output_folder = QFileInfo(output).absolutePath(); folder.patterns = source.fileName(); folder.output_template = QFileInfo(output).fileName(); folder.profile_id = "intelligent"; folder.settle_seconds = 0; watch_folders_.push_back(folder);
    save_watch_folders(); save_watch_history(); watch_folders_.clear(); load_watch_folders(); scan_watch_folders(true);
  }
  bool smoke_watch_folder_ready() { const bool complete = !batch_running_ && QFileInfo::exists(smoke_watch_output_) && !batch_jobs_.isEmpty() && batch_jobs_.front().status == "Completed" && !batch_jobs_.front().watch_key.isEmpty() && watch_history_.contains(batch_jobs_.front().watch_key); if (!complete) return false; const auto count = batch_jobs_.size(); scan_watch_folders(true); const bool suppressed = batch_jobs_.size() == count; if (suppressed && !smoke_watch_settings_restored_) { QSettings settings("SmartCut64", "SmartCut64"); const auto restore = [&settings](const QString& key, const QVariant& value) { if (value.isValid()) settings.setValue(key, value); else settings.remove(key); }; restore("batch/watchFolders", smoke_watch_folders_backup_); restore("batch/watchHistory", smoke_watch_history_backup_); restore("batch/persistentQueue", smoke_batch_queue_backup_); smoke_watch_settings_restored_ = true; } return suppressed; }
  void smoke_start_batch_ad(const QString& input, const QString& project) { batch_jobs_.clear(); BatchJob job; job.input = input; job.output = project; job.operation = "ad-scan"; batch_jobs_.push_back(std::move(job)); start_batch(); }
  bool smoke_batch_ad_ready() const { return smoke_batch_ready() && batch_jobs_.front().operation == "ad-scan"; }
  bool smoke_streams_ready() const { return !source_streams_.isEmpty() && selected_streams_.size() == source_streams_.size(); }
  void smoke_start_selected_export(const QString& output, QVector<int> streams) { selected_streams_ = std::move(streams); output_->setText(output); output_profile_->setCurrentIndex(output_profile_->findData("quickstream")); start_export(); }
  bool smoke_selected_export_ready(const QString& output) const { return worker_.state() == QProcess::NotRunning && QFileInfo::exists(output) && !status_->text().contains("failed", Qt::CaseInsensitive); }
  void smoke_start_cut_list_export(const QString& output) { if (timeline_->frame_count() < 3) return; set_cut_ranges({{1, 2}}); output_->setText(output); output_profile_->setCurrentIndex(output_profile_->findData("intelligent")); start_export(); }
  bool smoke_cut_list_export_ready(const QString& output) const { const auto report = QJsonDocument::fromJson(last_export_report_.toUtf8()).object(); const auto expected_version = QString::fromLatin1(smartcut::build_info::application_version.data(), static_cast<int>(smartcut::build_info::application_version.size())); return worker_.state() == QProcess::NotRunning && QFileInfo::exists(output) && log_->toPlainText().contains("edit-export-v1") && log_->toPlainText().contains("\"status\":\"PASSED\"") && report.value("output_sha256").toString().size() == 64 && report.value("dependency_versions").toObject().value("smartcut").toString() == expected_version && export_report_action_ && export_report_action_->isEnabled(); }
  void smoke_save_edit_project(const QString& path) { if (timeline_->frame_count() < 3) return; set_scene_mode(false, false); audio_offset_ms_ = 120; QFile::remove(path + ".journal"); set_cut_ranges({{1, 2}}); timeline_->set_chapter_markers({1}); selected_streams_ = {0}; project_path_ = path; save_project_document(worker_, path, false); }
  bool smoke_edit_project_saved() const { QFile file(project_path_); if (worker_.state() != QProcess::NotRunning || !file.open(QIODevice::ReadOnly)) return false; const auto document = QJsonDocument::fromJson(file.readAll()); const auto sources = document.isObject() ? document.object().value("sources").toArray() : QJsonArray{}; return document.isObject() && document.object().value("segments").toArray().size() == 2 && document.object().value("chapter_markers").toArray() == QJsonArray{1} && document.object().value("audio_offset_ms").toInt() == 120 && !sources.isEmpty() && sources.at(0).toObject().value("selected_streams").toArray().size() == 1; }
  void smoke_clear_and_reopen_project() { restoring_project_ = true; set_cut_ranges({}); selected_streams_.clear(); restoring_project_ = false; smoke_open_project(project_path_); }
  bool smoke_edit_project_restored() const { return !source_uuid_.isEmpty() && timeline_->frame_count() > 0 && cut_ranges_ == std::vector<FrameRange>{{1, 2}} && selected_streams_ == QVector<int>{0} && timeline_->chapter_markers() == std::vector<std::size_t>{1} && audio_offset_ms_ == 120; }
  void smoke_modify_project_for_autosave() { set_cut_ranges({{2, 3}}); }
  bool smoke_project_autosaved() const { QFile file(project_path_); if (autosave_worker_.state() != QProcess::NotRunning || !file.open(QIODevice::ReadOnly)) return false; const auto document = QJsonDocument::fromJson(file.readAll()); const auto segments = document.isObject() ? document.object().value("segments").toArray() : QJsonArray{}; return segments.size() == 2 && segments.at(0).toObject().value("end_display_frame_exclusive").toInt() == 2 && segments.at(1).toObject().value("first_display_frame").toInt() == 3; }
  void smoke_modify_project_for_journal_recovery() { set_cut_ranges({{1, 3}}); project_autosave_timer_.stop(); }
  bool smoke_journal_snapshot_idle() const { return journal_snapshot_.state() == QProcess::NotRunning; }
  bool smoke_journal_snapshot_ready() const { if (journal_snapshot_.state() != QProcess::NotRunning) return false; QFile file(project_path_ + ".journal"); return file.open(QIODevice::ReadOnly) && file.readAll().contains("\"cuts\":\"1:3\""); }
  bool smoke_journal_recovered() const { return cut_ranges_ == std::vector<FrameRange>{{1, 3}} && timeline_->chapter_markers() == std::vector<std::size_t>{1} && audio_offset_ms_ == 120; }
  void smoke_start_reload() { if (timeline_->frame_count() < 3) return; set_cut_ranges({{1, 2}}); selected_streams_ = {0}; reload_source(); }
  bool smoke_reload_ready() const { return source_probe_.state() == QProcess::NotRunning && timeline_->frame_count() >= 3 && cut_ranges_ == std::vector<FrameRange>{{1, 2}} && selected_streams_ == QVector<int>{0}; }
  void smoke_start_joiner_snapshot() { if (timeline_->frame_count() < 3) return; set_cut_ranges({{1, 2}}); add_current_to_joiner(); }
  bool smoke_joiner_snapshot_ready() const { return join_item_worker_.state() == QProcess::NotRunning && joiner_inputs_.size() == 1 && joiner_descriptions_.size() == 1 && QFileInfo::exists(joiner_inputs_.front()); }
  bool smoke_joiner_project_roundtrip(const QString& path) { if (!smoke_joiner_snapshot_ready() || !save_joiner_project(path)) return false; const auto expected_path = joiner_inputs_.front(); const auto expected_description = joiner_descriptions_.front(); joiner_inputs_.clear(); joiner_descriptions_.clear(); joiner_title_specs_.clear(); joiner_durations_us_.clear(); return load_joiner_project(path) && joiner_inputs_ == QStringList{expected_path} && joiner_descriptions_ == QStringList{expected_description}; }
  void smoke_open_joiner_selection() { lifecycle_prompt_override_ = QMessageBox::Discard; open_joiner_item(0); lifecycle_prompt_override_.reset(); }
  bool smoke_joiner_selection_ready() const { return smoke_source_ready() && joiner_inputs_.size() == 1 && joiner_descriptions_.size() == 1 && QFileInfo(input_->text()).absoluteFilePath() == QFileInfo(joiner_inputs_.front()).absoluteFilePath(); }
  void smoke_start_title(const QString& reference) { joiner_inputs_.clear(); joiner_descriptions_.clear(); joiner_title_specs_.clear(); QImage image(320, 180, QImage::Format_ARGB32); image.fill(QColor("#17365d")); QPainter painter(&image); painter.setPen(Qt::white); painter.setFont(QFont("Arial", 28, QFont::Bold)); painter.drawText(image.rect(), Qt::AlignCenter, "SmartCut64 Title"); painter.end(); pending_title_bgra_ = preview_dir_.path() + "/title-smoke.bgra"; pending_title_output_ = preview_dir_.path() + "/title-smoke.mkv"; pending_title_spec_ = preview_dir_.path() + "/title-smoke.sctitle.json"; pending_title_description_ = "SmartCut64 Title"; QSaveFile spec(pending_title_spec_); const auto spec_bytes = QJsonDocument(QJsonObject{{"schema", "joiner-title-v1"}, {"reference", reference}, {"text", "SmartCut64 Title"}, {"description", pending_title_description_}}).toJson(); if (!spec.open(QIODevice::WriteOnly) || spec.write(spec_bytes) != spec_bytes.size() || !spec.commit()) return; QSaveFile raw(pending_title_bgra_); if (!raw.open(QIODevice::WriteOnly)) return; for (int row = 0; row < image.height(); ++row) if (raw.write(reinterpret_cast<const char*>(image.constScanLine(row)), image.width() * 4) != image.width() * 4) { raw.cancelWriting(); return; } if (!raw.commit()) return; QFile::remove(pending_title_output_); title_worker_.start(worker_program(), {"--create-title", "--reference", reference, "--bgra", pending_title_bgra_, "--width", QString::number(image.width()), "--height", QString::number(image.height()), "--output", pending_title_output_, "--duration", "1.000", "--fade-in", "0.200", "--fade-out", "0.200", "--description", pending_title_description_}); }
  bool smoke_title_ready() const { return title_worker_.state() == QProcess::NotRunning && joiner_inputs_.size() == 1 && joiner_descriptions_ == QStringList{"SmartCut64 Title"} && joiner_title_specs_.size() == 1 && QFileInfo::exists(joiner_inputs_.front()) && QFileInfo::exists(joiner_title_specs_.front()); }
  bool smoke_title_project_roundtrip(const QString& path) { if (!smoke_title_ready() || !save_joiner_project(path)) return false; const auto expected_media = joiner_inputs_.front(); const auto expected_spec = joiner_title_specs_.front(); joiner_inputs_.clear(); joiner_descriptions_.clear(); joiner_title_specs_.clear(); return load_joiner_project(path) && joiner_inputs_ == QStringList{expected_media} && joiner_descriptions_ == QStringList{"SmartCut64 Title"} && joiner_title_specs_ == QStringList{expected_spec}; }
  void smoke_start_title_join(const QString& reference, const QString& output) { if (!smoke_title_ready()) return; joiner_inputs_.push_back(reference); joiner_descriptions_.push_back(QFileInfo(reference).completeBaseName()); joiner_title_specs_.push_back({}); joiner_list_path_ = preview_dir_.path() + "/title-join-list.txt"; QFile list(joiner_list_path_); if (!list.open(QIODevice::WriteOnly | QIODevice::Truncate)) return; for (const auto& path : joiner_inputs_) { list.write(path.toUtf8()); list.write("\n"); } list.close(); joiner_output_path_ = output; QFile::remove(output); join_worker_.start(worker_program(), {"--join-media", "--list", joiner_list_path_, "--output", output}); }
  bool smoke_title_join_ready() const { return join_worker_.state() == QProcess::NotRunning && joiner_inputs_.size() == 2 && QFileInfo::exists(joiner_output_path_) && status_->text().startsWith("Joined video saved:"); }
  void smoke_start_multi_open(const QString& input) { joiner_inputs_.clear(); joiner_descriptions_.clear(); joiner_title_specs_.clear(); handle_open_files({input, input}, "join"); multi_open_join_verified_ = joiner_inputs_.size() == 2 && joiner_descriptions_.size() == 2 && joiner_title_specs_.size() == 2; joiner_inputs_.clear(); joiner_descriptions_.clear(); joiner_title_specs_.clear(); handle_open_files({input, input}, "combine"); }
  bool smoke_multi_open_ready() const { return multi_open_join_verified_ && !combined_open_pending_ && combined_open_source_count_ == 2 && smoke_source_ready() && timeline_->frame_count() >= 6; }
  void smoke_rapid_preview_requests() { for (int request = 0; request < 40; ++request) timeline_->step_forward(); }
  bool smoke_preview_ready() const { return preview_ && !preview_->pixmap().isNull() && preview_->text() != "Preview decode failed" && preview_probe_.state() == QProcess::NotRunning && !pending_preview_frame_; }
  bool smoke_thumbnails_ready() const {
    const auto visible = std::clamp<qsizetype>(visible_thumbnail_count_, 1, thumbnail_widgets_.size());
    const auto ready = [](const ThumbnailWidget* thumbnail) {
      return thumbnail->is_placeholder() || (thumbnail->has_image() && thumbnail->full_frame_geometry_ready());
    };
    const auto center = visible / 2;
    return thumbnail_probe_.state() == QProcess::NotRunning && !pending_thumbnail_center_ && frame_guide_->has_waveform() && visible >= 3 &&
           thumbnail_widgets_[center]->has_image() && std::all_of(thumbnail_widgets_.cbegin(), thumbnail_widgets_.cbegin() + visible, ready);
  }
  void smoke_start_video_playback() {
    set_scene_mode(false, false);
    if (timeline_->frame_count() > 3) set_cut_ranges({{1, 3}});
    timeline_->set_marks(0, 1);
    set_preview_mode(true);
    timeline_->seek_fraction(0.0);
    displayed_preview_frame_ = 0;
    start_video_playback();
    start_playback_timer();
  }
  bool smoke_playback_preview_ready() const { return timeline_->current_frame() > 2 && displayed_preview_frame_ > 0 && !timeline_->frame_is_cut(timeline_->current_frame()) && !timeline_->frame_is_cut(displayed_preview_frame_); }
  void smoke_stop_video_playback() { request_video_stop(); playback_timer_.stop(); set_preview_mode(false); timeline_->set_marks(std::nullopt, std::nullopt); }
  void smoke_start_navigation_seek_resume(bool fine_tune) {
    if (timeline_->frame_count() < 20) return;
    set_scene_mode(false, false);
    set_preview_mode(false);
    set_cut_ranges({});
    timeline_->set_frame(0);
    edit_without_audio_ = false;
    audio_offset_ms_ = 0;
    volume_->blockSignals(true);
    volume_->setValue(0);
    volume_->blockSignals(false);
    smoke_navigation_seek_target_.reset();
    smoke_navigation_seek_video_generation_before_ = 0;
    smoke_navigation_seek_audio_generation_before_ = 0;
    smoke_navigation_seek_audio_progress_before_ = 0;
    smoke_navigation_seek_resume_count_before_ = 0;
    smoke_navigation_seek_initial_audio_progress_before_ = audio_progress_event_count_;
    smoke_navigation_seek_fine_tune_ = fine_tune;
    smoke_navigation_seek_observing_ = false;
    smoke_navigation_seek_regressed_ = false;
    smoke_navigation_seek_restarted_during_drag_ = false;
    smoke_navigation_seek_last_audio_us_.reset();
    smoke_navigation_seek_drag_stage_ = 0;
    smoke_navigation_seek_drag_step_elapsed_.invalidate();
    smoke_navigation_seek_post_restart_elapsed_.invalidate();
    begin_playback_request();
  }
  bool smoke_navigation_seek_can_drag() const {
    return playback_requested_ &&
           audio_progress_event_count_ > smoke_navigation_seek_initial_audio_progress_before_ &&
           last_audio_progress_generation_ == audio_playback_generation_;
  }
  void smoke_drive_navigation_seek_drag() {
    if (smoke_navigation_seek_target_) return;
    if (smoke_navigation_seek_drag_stage_ == 0) {
      if (!smoke_navigation_seek_can_drag()) return;
      smoke_navigation_seek_video_generation_before_ = video_playback_generation_;
      smoke_navigation_seek_audio_generation_before_ = audio_playback_generation_;
      smoke_navigation_seek_audio_progress_before_ = audio_progress_event_count_;
      smoke_navigation_seek_resume_count_before_ = navigation_seek_resume_count_;
    } else if (!smoke_navigation_seek_drag_step_elapsed_.isValid() ||
               smoke_navigation_seek_drag_step_elapsed_.elapsed() < 60) {
      return;
    }
    QWidget* widget = smoke_navigation_seek_fine_tune_
                          ? static_cast<QWidget*>(fine_tune_)
                          : static_cast<QWidget*>(timeline_);
    const auto width = static_cast<double>(std::max(widget->width(), 48));
    const auto height = widget->height() / 2.0;
    QPointF press_position;
    std::array<QPointF, 3> move_positions;
    auto button = Qt::LeftButton;
    if (smoke_navigation_seek_fine_tune_) {
      button = Qt::RightButton;
      const auto centre = width / 2.0;
      const auto half_span = std::max(1.0, (width - 22.0) / 2.0);
      press_position = QPointF(centre, height);
      move_positions = {QPointF(centre + half_span * 0.15, height),
                        QPointF(centre + half_span * 0.25, height),
                        QPointF(centre + half_span * 0.35, height)};
    } else {
      const auto span = width - 24.0;
      press_position = QPointF(12.0 + span * 0.25, height);
      move_positions = {QPointF(12.0 + span * 0.30, height),
                        QPointF(12.0 + span * 0.35, height),
                        QPointF(12.0 + span * 0.40, height)};
    }
    const auto generations_unchanged = [this] {
      return video_playback_generation_ == smoke_navigation_seek_video_generation_before_ &&
             audio_playback_generation_ == smoke_navigation_seek_audio_generation_before_ &&
             navigation_seek_resume_count_ == smoke_navigation_seek_resume_count_before_;
    };
    if (smoke_navigation_seek_drag_stage_ == 0) {
      QMouseEvent press(QEvent::MouseButtonPress, press_position, press_position,
                        press_position, button, button, Qt::NoModifier);
      QApplication::sendEvent(widget, &press);
      smoke_navigation_seek_restarted_during_drag_ |=
          !generations_unchanged() || !navigation_seek_in_progress_;
      smoke_navigation_seek_drag_stage_ = 1;
      smoke_navigation_seek_drag_step_elapsed_.start();
      return;
    }
    if (smoke_navigation_seek_drag_stage_ <= 3) {
      const auto& position = move_positions[static_cast<std::size_t>(
          smoke_navigation_seek_drag_stage_ - 1)];
      QMouseEvent move(QEvent::MouseMove, position, position, position,
                       Qt::NoButton, button, Qt::NoModifier);
      QApplication::sendEvent(widget, &move);
      smoke_navigation_seek_restarted_during_drag_ |=
          !generations_unchanged() || !navigation_seek_in_progress_;
      ++smoke_navigation_seek_drag_stage_;
      smoke_navigation_seek_drag_step_elapsed_.restart();
      return;
    }
    const auto release_position = move_positions.back();
    QMouseEvent release(QEvent::MouseButtonRelease, release_position,
                        release_position, release_position, button,
                        Qt::NoButton, Qt::NoModifier);
    QApplication::sendEvent(widget, &release);
    smoke_navigation_seek_restarted_during_drag_ |= !generations_unchanged();
    smoke_navigation_seek_target_ = timeline_->current_frame();
    smoke_navigation_seek_observing_ = true;
    ++smoke_navigation_seek_drag_stage_;
    smoke_navigation_seek_elapsed_.start();
  }
  bool smoke_navigation_seek_resume_ready() const {
    return smoke_navigation_seek_target_ && smoke_navigation_seek_elapsed_.isValid() &&
           smoke_navigation_seek_elapsed_.elapsed() >= 300 &&
           smoke_navigation_seek_post_restart_elapsed_.isValid() &&
           smoke_navigation_seek_post_restart_elapsed_.elapsed() >= 300 &&
           video_playback_generation_ == smoke_navigation_seek_video_generation_before_ + 1 &&
           audio_playback_generation_ == smoke_navigation_seek_audio_generation_before_ + 1 &&
           navigation_seek_resume_count_ == smoke_navigation_seek_resume_count_before_ + 1 &&
           audio_progress_event_count_ >= smoke_navigation_seek_audio_progress_before_ + 2 &&
           last_audio_progress_generation_ == audio_playback_generation_ &&
           displayed_video_playback_generation_ == video_playback_generation_ &&
           displayed_preview_frame_ >= *smoke_navigation_seek_target_ &&
           playback_requested_ && playback_active() && !navigation_seek_in_progress_ &&
           !navigation_seek_resume_pending_ &&
           timeline_->current_frame() > *smoke_navigation_seek_target_ &&
           !smoke_navigation_seek_regressed_ &&
           !smoke_navigation_seek_restarted_during_drag_;
  }
  std::size_t smoke_navigation_seek_target() const { return smoke_navigation_seek_target_.value_or(0); }
  std::size_t smoke_navigation_seek_current() const { return timeline_->current_frame(); }
  std::size_t smoke_navigation_seek_displayed() const { return displayed_preview_frame_; }
  bool smoke_navigation_seek_video_restarted_once() const { return smoke_navigation_seek_video_generation_before_ > 0 && video_playback_generation_ == smoke_navigation_seek_video_generation_before_ + 1; }
  bool smoke_navigation_seek_audio_restarted_once() const { return smoke_navigation_seek_audio_generation_before_ > 0 && audio_playback_generation_ == smoke_navigation_seek_audio_generation_before_ + 1; }
  bool smoke_navigation_seek_resumed_once() const { return navigation_seek_resume_count_ == smoke_navigation_seek_resume_count_before_ + 1; }
  bool smoke_navigation_seek_audio_progressed() const { return audio_progress_event_count_ >= smoke_navigation_seek_audio_progress_before_ + 2 && last_audio_progress_generation_ == audio_playback_generation_; }
  bool smoke_navigation_seek_replacement_video_displayed() const { return smoke_navigation_seek_target_ && displayed_video_playback_generation_ == video_playback_generation_ && displayed_preview_frame_ >= *smoke_navigation_seek_target_; }
  bool smoke_navigation_seek_no_regression() const { return !smoke_navigation_seek_regressed_; }
  bool smoke_navigation_seek_no_restart_during_drag() const { return !smoke_navigation_seek_restarted_during_drag_; }
  std::uint64_t smoke_navigation_seek_video_restart_count() const { return video_playback_generation_ >= smoke_navigation_seek_video_generation_before_ ? video_playback_generation_ - smoke_navigation_seek_video_generation_before_ : 0; }
  std::uint64_t smoke_navigation_seek_audio_restart_count() const { return audio_playback_generation_ >= smoke_navigation_seek_audio_generation_before_ ? audio_playback_generation_ - smoke_navigation_seek_audio_generation_before_ : 0; }
  std::uint64_t smoke_navigation_seek_resume_count() const { return navigation_seek_resume_count_ >= smoke_navigation_seek_resume_count_before_ ? navigation_seek_resume_count_ - smoke_navigation_seek_resume_count_before_ : 0; }
  void smoke_stop_all_playback() { stop_playback("Automation complete"); }
  void smoke_start_video_restart_guard() {
    if (timeline_->frame_count() < 40) return;
    set_preview_mode(false);
    timeline_->set_frame(0);
    displayed_preview_frame_ = 0;
    smoke_video_restart_target_ = 32;
    smoke_video_restart_elapsed_.start();
    start_video_playback();
    QTimer::singleShot(100, this, [this] {
      request_video_stop();
      timeline_->set_frame(10);
      start_video_playback();
    });
  }
  bool smoke_video_restart_guard_ready() const {
    return smoke_video_restart_elapsed_.isValid() && smoke_video_restart_elapsed_.elapsed() >= 1100 &&
           displayed_preview_frame_ >= smoke_video_restart_target_ && video_playback_requested_;
  }
  void smoke_start_audio_cut_handoff() {
    const auto count = timeline_->frame_count();
    if (count < 80) return;
    const auto cut_start = count / 3;
    const auto cut_end = count / 2;
    set_scene_mode(false, false);
    set_cut_ranges({{cut_start, cut_end}});
    set_preview_mode(true);
    timeline_->set_frame(cut_start > 8 ? cut_start - 8 : 0);
    volume_->blockSignals(true);
    volume_->setValue(0);
    volume_->blockSignals(false);
    edit_without_audio_ = false;
    smoke_audio_handoff_target_ = std::min(count - 1, cut_end + std::size_t{40});
    smoke_audio_handoff_elapsed_.start();
    begin_playback_request();
  }
  bool smoke_audio_cut_handoff_ready() const {
    return smoke_audio_handoff_elapsed_.isValid() && smoke_audio_handoff_elapsed_.elapsed() >= 2600 &&
           timeline_->current_frame() >= smoke_audio_handoff_target_ &&
           displayed_preview_frame_ >= smoke_audio_handoff_target_ &&
           audio_playback_.state() != QProcess::NotRunning && video_playback_requested_;
  }
  void smoke_prepare_demo_loaded_state() {
    const auto font_id = QFontDatabase::addApplicationFont("C:/Windows/Fonts/segoeui.ttf");
    const auto font_families = QFontDatabase::applicationFontFamilies(font_id);
    ui_font_family_ = font_families.isEmpty() ? QStringLiteral("Arial") : font_families.front();
    ui_font_size_ = 9;
    apply_theme_preset("Professional Dark", false);
    setAttribute(Qt::WA_DontShowOnScreen, true);
    resize(1600, 1000);
    show();
    timeline_->seek_fraction(0.42);
  }
  bool smoke_capture_demo_frame(const QString& path) {
    show();
    qApp->processEvents();
    const auto frame = grab();
    return !frame.isNull() && frame.save(path, "PNG");
  }
  bool smoke_prepare_demo_preview_state() {
    if (timeline_->frame_count() < 4 || cut_ranges_.empty()) return false;
    set_preview_mode(true);
    return timeline_->set_frame(timeline_->frame_count() / 3);
  }
  bool smoke_demo_cut_ready() const {
    return cut_list_->rowCount() == 2 && smoke_preview_ready() && smoke_thumbnails_ready();
  }
  bool smoke_trim_loaded_range() { set_scene_mode(false, false); const auto count = timeline_->frame_count(); if (count < 4) return false; const auto first = count / 4; const auto end = count / 2; timeline_->set_frame(first); timeline_->mark_in(); timeline_->set_frame(end); timeline_->mark_out(); trim_->click(); return cut_ranges_ == std::vector<FrameRange>{{0, first}, {end, count}} && cut_list_->rowCount() == 2 && timeline_->frame_is_cut(0) && !timeline_->frame_is_cut(first) && timeline_->frame_is_cut(count - 1); }
  bool smoke_cut_list_interactions_ready() { set_scene_mode(false, false); timeline_->set_navigation(20, {0, 5, 10, 15}, {}, 1, 25); set_cut_ranges({{2, 5}, {9, 13}}); activate_cut_row(1, true); return timeline_->current_frame() == 9 && timeline_->in_mark() == std::optional<std::size_t>(9) && timeline_->out_mark() == std::optional<std::size_t>(13); }
  bool smoke_export_report_ready() { const QString digest(64, QLatin1Char('a')); const auto version = QString::fromLatin1(smartcut::build_info::application_version.data(), static_cast<int>(smartcut::build_info::application_version.size())); return capture_export_report(QString("{\"schema\":\"worker-edit-export-v1\",\"event\":\"completed\",\"status\":\"PASSED\",\"output_sha256\":\"%1\",\"dependency_versions\":{\"smartcut\":\"%2\"}}\n").arg(digest, version)) && export_report_action_ && export_report_action_->isEnabled() && last_export_report_.contains(digest); }
  bool smoke_output_completion_ready() { output_->setText(preview_dir_.path() + "/completion-smoke.mkv"); QFile file(output_->text()); if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate) || file.write(QByteArray(1024, 'x')) != 1024) return false; file.close(); cut_ranges_.clear(); export_first_frame_ = 0; export_end_frame_ = 25; const auto summary = output_completion_summary(); QFile::remove(output_->text()); return summary.contains("Output Processing Complete") && summary.contains("Video output frames: 25") && summary.contains("Validation status: PASSED") && summary.contains("SHA-256:"); }
  bool smoke_program_options_ready() {
    const auto backup_input = input_->text();
    const auto backup = std::tuple{completion_alert_mode_, completion_alert_wave_, edit_without_audio_, prompt_joiner_descriptions_, completion_alert_requests_, output_default_folder_, project_default_folder_, output_default_extension_, save_cpu_priority_, clear_cuts_after_queue_};
    completion_alert_requests_ = 0; completion_alert_mode_ = 2; completion_alert_wave_.clear(); const bool interactive = play_completion_alert(false); const bool batch = play_completion_alert(true);
    completion_alert_mode_ = 1; const bool batch_blocked = !play_completion_alert(true); edit_without_audio_ = true; prompt_joiner_descriptions_ = true;
    output_default_folder_ = preview_dir_.path(); project_default_folder_ = preview_dir_.path(); output_default_extension_ = "mp4"; save_cpu_priority_ = "below-normal"; clear_cuts_after_queue_ = true; input_->setText(preview_dir_.path() + "/preferences-source.mkv");
    QSettings settings("SmartCut64", "SmartCut64"); const QStringList keys{"audioAlerts/mode", "playback/editWithoutAudio", "output/defaultFolder", "projects/defaultFolder", "output/defaultExtension", "output/cpuPriority", "batch/clearCutsAfterQueue"}; QHash<QString, QVariant> old_values; for (const auto& key : keys) old_values.insert(key, settings.value(key)); settings.setValue("audioAlerts/mode", completion_alert_mode_); settings.setValue("playback/editWithoutAudio", edit_without_audio_); settings.setValue("output/defaultFolder", output_default_folder_); settings.setValue("projects/defaultFolder", project_default_folder_); settings.setValue("output/defaultExtension", output_default_extension_); settings.setValue("output/cpuPriority", save_cpu_priority_); settings.setValue("batch/clearCutsAfterQueue", clear_cuts_after_queue_); const bool persisted = settings.value("audioAlerts/mode").toInt() == 1 && settings.value("playback/editWithoutAudio").toBool() && settings.value("output/defaultExtension").toString() == "mp4" && settings.value("batch/clearCutsAfterQueue").toBool() && default_output_path().endsWith("preferences-source-edited.mp4") && default_project_path().endsWith("preferences-source.smartcut"); for (const auto& key : keys) { if (old_values.value(key).isValid()) settings.setValue(key, old_values.value(key)); else settings.remove(key); }
    const bool ready = interactive && batch && batch_blocked && completion_alert_requests_ == 2 && edit_without_audio_ && prompt_joiner_descriptions_ && persisted;
    std::tie(completion_alert_mode_, completion_alert_wave_, edit_without_audio_, prompt_joiner_descriptions_, completion_alert_requests_, output_default_folder_, project_default_folder_, output_default_extension_, save_cpu_priority_, clear_cuts_after_queue_) = backup; input_->setText(backup_input); return ready;
  }
  bool smoke_chapter_file_ready() {
    const auto backup = std::tuple{chapter_at_cuts_, chapter_at_markers_, chapter_equal_spacing_, chapter_spacing_mode_, chapter_spacing_value_, chapter_minimum_seconds_, chapter_include_first_, chapter_file_template_, chapter_line_format_, chapter_timecode_format_};
    std::vector<std::int64_t> pts(100); for (std::size_t frame = 0; frame < pts.size(); ++frame) pts[frame] = static_cast<std::int64_t>(frame * 40); timeline_->set_navigation(pts.size(), {0}, pts, 1, 1000); set_cut_ranges({{20, 30}}); timeline_->set_chapter_markers({10, 25, 40}); chapter_at_cuts_ = true; chapter_at_markers_ = true; chapter_equal_spacing_ = false; chapter_minimum_seconds_ = 0; chapter_include_first_ = true; chapter_line_format_ = "%C|%T|%N"; chapter_timecode_format_ = "HH:MM:SS:ff";
    const auto chapters = generated_chapters(); const auto path = preview_dir_.path() + "/chapters-smoke.txt"; const bool saved = write_chapter_file(path, false); QFile file(path); const auto text = file.open(QIODevice::ReadOnly) ? QString::fromUtf8(file.readAll()) : QString{}; QFile::remove(path);
    std::tie(chapter_at_cuts_, chapter_at_markers_, chapter_equal_spacing_, chapter_spacing_mode_, chapter_spacing_value_, chapter_minimum_seconds_, chapter_include_first_, chapter_file_template_, chapter_line_format_, chapter_timecode_format_) = backup;
    return saved && chapters.size() == 4 && chapters[0].output_frame == 0 && chapters[1].output_frame == 10 && chapters[2].output_frame == 20 && chapters[3].output_frame == 30 && text.contains("1|00:00:00:00|0") && text.contains("4|00:00:01:05|30");
  }
  bool smoke_display_adjustment_ready() { QImage source(2, 1, QImage::Format_ARGB32); source.setPixelColor(0, 0, QColor(96, 96, 96)); source.setPixelColor(1, 0, QColor(160, 160, 160)); const auto brighter = adjusted_display_image(source, 20, 0); const auto contrasted = adjusted_display_image(source, 0, 50); return brighter.pixelColor(0, 0).red() > 96 && contrasted.pixelColor(0, 0).red() < 96 && contrasted.pixelColor(1, 0).red() > 160 && adjusted_display_image(source, 0, 0) == source; }
  bool smoke_plot_mask_ready() { QImage source(100, 100, QImage::Format_ARGB32); source.fill(Qt::white); const auto masked = plot_masked_image(source, true, 1); return masked.pixelColor(50, 50) == QColor(Qt::black) && masked.pixelColor(2, 2) == QColor(Qt::white) && plot_masked_image(source, false, 3) == source; }
  bool smoke_repeat_selection_ready() { timeline_->set_navigation(20, {0, 10}, {}, 1, 25); timeline_->set_marks(4, 8); timeline_->set_frame(8); repeat_selection_playback_ = true; const bool restarted = restart_repeat_selection(false) && timeline_->current_frame() == 4; repeat_selection_playback_ = false; timeline_->set_marks({}, {}); return restarted && !restart_repeat_selection(false); }
  bool smoke_overlap_choices_ready() {
    set_scene_mode(false, false); timeline_->set_navigation(20, {0, 5, 10, 15}, {}, 1, 25); auto_replace_overlapping_cuts_ = false;
    set_cut_ranges({{2, 6}, {12, 15}}); overlap_choice_override_ = OverlapChoice::combine; timeline_->set_marks(4, 9); cut_selected_range(); const bool combined = cut_ranges_ == std::vector<FrameRange>{{2, 9}, {12, 15}};
    set_cut_ranges({{2, 6}, {12, 15}}); overlap_choice_override_ = OverlapChoice::replace; timeline_->set_marks(4, 9); cut_selected_range(); const bool replaced = cut_ranges_ == std::vector<FrameRange>{{4, 9}, {12, 15}};
    set_cut_ranges({{2, 6}}); overlap_choice_override_ = OverlapChoice::cancel; timeline_->set_marks(4, 9); cut_selected_range(); const bool cancelled = cut_ranges_ == std::vector<FrameRange>{{2, 6}};
    overlap_choice_override_.reset(); return combined && replaced && cancelled;
  }
  bool smoke_modified_lifecycle_ready() {
    restoring_project_ = false; project_path_.clear(); input_->setText("lifecycle-source.mkv"); timeline_->set_navigation(10, {0, 5}, {}, 1, 25); set_project_modified(false);
    timeline_->set_frame(3); delete_current_frame();
    const bool tracked = project_modified_ && windowTitle().contains('*');
    lifecycle_prompt_override_ = QMessageBox::Cancel; const bool cancelled = !confirm_abandon_changes("close the source") && project_modified_;
    lifecycle_prompt_override_ = QMessageBox::Discard; const bool discard_authorized = confirm_abandon_changes("close the source") && project_modified_; close_source_now(); const bool discarded = discard_authorized && !project_modified_ && !windowTitle().contains('*');
    lifecycle_prompt_override_.reset();
    return tracked && cancelled && discarded;
  }
  bool smoke_scene_mode_ready() { set_scene_mode(true, false); timeline_->set_navigation(20, {0, 5, 10, 15}, {}, 1, 25); set_cut_ranges({{0, 20}}); timeline_->set_frame(4); timeline_->mark_in(); timeline_->set_frame(8); timeline_->mark_out(); cut_selected_range(); const bool added = cut_ranges_ == std::vector<FrameRange>{{0, 4}, {8, 20}} && cut_list_->rowCount() == 1 && cut_list_->horizontalHeaderItem(0)->text() == "Scene Start"; cut_list_->selectRow(0); remove_selected_cuts(); const bool removed = cut_ranges_ == std::vector<FrameRange>{{0, 20}} && cut_list_->rowCount() == 0; set_scene_mode(false, false); return added && removed; }
  bool eventFilter(QObject* watched, QEvent* event) override { if (watched == preview_ && event->type() == QEvent::Resize && !current_preview_image_.isNull()) preview_resize_timer_.start(0); if (watched == preview_ && event->type() == QEvent::MouseButtonRelease) { const auto* mouse = static_cast<QMouseEvent*>(event); if (mouse->button() == Qt::LeftButton) { toggle_playback(); return true; } } return QMainWindow::eventFilter(watched, event); }
  void dragEnterEvent(QDragEnterEvent* event) override { if (event->mimeData()->hasUrls()) event->acceptProposedAction(); else QMainWindow::dragEnterEvent(event); }
  void dropEvent(QDropEvent* event) override { if (!event->mimeData()->hasUrls()) { QMainWindow::dropEvent(event); return; } QStringList paths; for (const auto& url : event->mimeData()->urls()) if (url.isLocalFile() && QFileInfo(url.toLocalFile()).isFile()) paths.push_back(QDir::toNativeSeparators(url.toLocalFile())); if (!paths.isEmpty()) handle_open_files(paths); event->acceptProposedAction(); }
  void resizeEvent(QResizeEvent* event) override { QMainWindow::resizeEvent(event); if (!current_preview_image_.isNull()) preview_resize_timer_.start(0); thumbnail_layout_timer_.start(120); }
  void closeEvent(QCloseEvent* event) override { if (!confirm_abandon_changes("exit SmartCut64")) { event->ignore(); return; } invalidate_playback_request(); audio_pause_requested_ = true; request_audio_stop(); request_video_stop(); playback_timer_.stop(); event->accept(); }
  ExportWindow() {
    setAcceptDrops(true);
#ifdef SMARTCUT_INTERNAL_TEST_GUI
    const auto app_arguments = QCoreApplication::arguments();
    automation_mode_ = std::any_of(app_arguments.cbegin(), app_arguments.cend(), [](const QString& argument) { return argument == "--smoke-test" || argument.startsWith("--automation-"); });
#else
    automation_mode_ = false;
#endif
    QSettings ad_settings("SmartCut64", "SmartCut64");
    ad_scene_threshold_ = std::clamp(ad_settings.value("ad/sceneThreshold", 0.30).toDouble(), 0.01, 1.0);
    ad_black_level_ = std::clamp(ad_settings.value("ad/blackLevel", 24).toInt(), 0, 255);
    ad_minimum_seconds_ = std::clamp(ad_settings.value("ad/minimumSeconds", 1.0).toDouble(), 0.0, 60.0);
    ad_maximum_block_seconds_ = std::clamp(ad_settings.value("ad/maximumBlockSeconds", 180).toInt(), 10, 1800);
    ad_automatic_cuts_ = ad_settings.value("ad/automaticCuts", false).toBool();
    thumbnail_interval_seconds_ = std::clamp(ad_settings.value("thumbnails/intervalSecondsV2", 0.0).toDouble(), 0.0, 600.0);
    thumbnail_height_ = ad_settings.value("thumbnails/height", 80).toInt();
    if (!QList<int>{58, 80, 108}.contains(thumbnail_height_)) thumbnail_height_ = 80;
    thumbnail_audio_visible_ = ad_settings.value("thumbnails/audioVisible", true).toBool();
    short_jump_seconds_ = std::clamp(ad_settings.value("navigation/shortJumpSeconds", 10).toInt(), 1, 600);
    long_jump_seconds_ = std::clamp(ad_settings.value("navigation/longJumpSeconds", 120).toInt(), 1, 3600);
    display_brightness_ = std::clamp(ad_settings.value("display/brightness", 0).toInt(), -100, 100);
    display_contrast_ = std::clamp(ad_settings.value("display/contrast", 0).toInt(), -100, 100);
    preview_rotation_degrees_ = ad_settings.value("display/rotationDegrees", -1).toInt(); if (!QList<int>{-1, 0, 90, 180, 270}.contains(preview_rotation_degrees_)) preview_rotation_degrees_ = -1; osd_visible_ = ad_settings.value("display/osdVisible", false).toBool(); frame_number_mode_ = ad_settings.value("display/frameNumberMode", "source").toString(); if (!QStringList{"none", "source", "output"}.contains(frame_number_mode_)) frame_number_mode_ = "source"; auto_replace_overlapping_cuts_ = ad_settings.value("editing/autoReplaceOverlappingCuts", false).toBool(); prompt_joiner_descriptions_ = ad_settings.value("joiner/promptDescriptions", false).toBool(); multiple_file_action_ = ad_settings.value("files/multipleOpenAction", "prompt").toString(); if (multiple_file_action_ != "prompt" && multiple_file_action_ != "join" && multiple_file_action_ != "combine") multiple_file_action_ = "prompt"; edit_without_audio_ = ad_settings.value("playback/editWithoutAudio", false).toBool(); completion_alert_mode_ = std::clamp(ad_settings.value("audioAlerts/mode", 0).toInt(), 0, 2); completion_alert_wave_ = ad_settings.value("audioAlerts/waveFile").toString(); chapter_at_cuts_ = ad_settings.value("chapters/atCutPoints", false).toBool(); chapter_at_markers_ = ad_settings.value("chapters/atSceneMarkers", true).toBool(); chapter_equal_spacing_ = ad_settings.value("chapters/equalSpacing", false).toBool(); chapter_spacing_mode_ = ad_settings.value("chapters/spacingMode", "interval").toString(); if (!QStringList{"interval", "parts"}.contains(chapter_spacing_mode_)) chapter_spacing_mode_ = "interval"; chapter_spacing_value_ = std::clamp(ad_settings.value("chapters/spacingValue", 300).toInt(), 1, chapter_spacing_mode_ == "parts" ? 1000 : 86400); chapter_minimum_seconds_ = std::clamp(ad_settings.value("chapters/minimumSeconds", 0).toInt(), 0, 86400); chapter_include_first_ = ad_settings.value("chapters/includeFirst", false).toBool(); chapter_auto_save_ = ad_settings.value("chapters/autoSave", false).toBool(); chapter_file_template_ = ad_settings.value("chapters/fileTemplate", "%F_chapter.txt").toString(); chapter_line_format_ = ad_settings.value("chapters/lineFormat", "%T").toString(); chapter_timecode_format_ = ad_settings.value("chapters/timecodeFormat", "HH:MM:SS:ff").toString();
    thumbnail_brightness_ = std::clamp(ad_settings.value("thumbnails/brightness", 0).toInt(), -100, 100); thumbnail_timecode_visible_ = ad_settings.value("thumbnails/timecodeVisible", false).toBool(); waveform_decibel_scale_ = ad_settings.value("thumbnails/waveformDecibelScale", true).toBool(); waveform_background_colour_ = QColor(ad_settings.value("thumbnails/waveformBackground", "#000000").toString()); waveform_colour_ = QColor(ad_settings.value("thumbnails/waveformColour", "#1268b3").toString()); waveform_centre_colour_ = QColor(ad_settings.value("thumbnails/waveformCentre", "#083e75").toString()); waveform_text_colour_ = QColor(ad_settings.value("thumbnails/waveformText", "#00d820").toString());
    ad_peak_black_level_ = std::clamp(ad_settings.value("ad/peakBlackLevel", 51).toInt(), 0, 255); ad_contrast_threshold_ = std::clamp(ad_settings.value("ad/contrastThreshold", 38).toInt(), 0, 255); ad_region_left_ = std::clamp(ad_settings.value("ad/regionLeft", 10).toInt(), 0, 45); ad_region_top_ = std::clamp(ad_settings.value("ad/regionTop", 10).toInt(), 0, 45); ad_region_right_ = std::clamp(ad_settings.value("ad/regionRight", 10).toInt(), 0, 45); ad_region_bottom_ = std::clamp(ad_settings.value("ad/regionBottom", 10).toInt(), 0, 45); ad_ip_only_ = ad_settings.value("ad/ipOnly", false).toBool(); ad_black_detection_ = ad_settings.value("ad/blackDetection", true).toBool(); ad_pause_mode_ = std::clamp(ad_settings.value("ad/pauseMode", 0).toInt(), 0, 3); ad_display_during_scan_ = ad_settings.value("ad/displayDuringScan", true).toBool(); ad_cpu_priority_ = ad_settings.value("ad/cpuPriority", "below-normal").toString(); if (!QStringList{"idle", "below-normal", "normal", "above-normal"}.contains(ad_cpu_priority_)) ad_cpu_priority_ = "below-normal";
    plot_mask_visible_ = ad_settings.value("display/plotMaskVisible", false).toBool(); plot_mask_size_ = std::clamp(ad_settings.value("display/plotMaskSize", 1).toInt(), 0, 3); title_duration_seconds_ = std::clamp(ad_settings.value("titles/durationSeconds", 5.0).toDouble(), 0.1, 86400.0); title_fade_in_seconds_ = std::clamp(ad_settings.value("titles/fadeInSeconds", 0.5).toDouble(), 0.0, title_duration_seconds_); title_fade_out_seconds_ = std::clamp(ad_settings.value("titles/fadeOutSeconds", 0.5).toDouble(), 0.0, title_duration_seconds_); output_default_folder_ = ad_settings.value("output/defaultFolder").toString(); project_default_folder_ = ad_settings.value("projects/defaultFolder").toString(); output_default_extension_ = ad_settings.value("output/defaultExtension", "mkv").toString().toLower(); if (!QStringList{"mkv", "mp4", "mov", "mpg", "ts", "wtv", "dvr-ms", "rec", "mxf"}.contains(output_default_extension_)) output_default_extension_ = "mkv"; clear_cuts_after_queue_ = ad_settings.value("batch/clearCutsAfterQueue", false).toBool(); save_cpu_priority_ = ad_settings.value("output/cpuPriority", "below-normal").toString(); if (!QStringList{"idle", "below-normal", "normal", "above-normal"}.contains(save_cpu_priority_)) save_cpu_priority_ = "below-normal"; hardware_decoding_enabled_ = ad_settings.value("playback/hardwareDecodingEnabled", true).toBool(); hardware_transcoding_enabled_ = ad_settings.value("output/hardwareTranscodingEnabled", false).toBool(); hardware_transcoding_backend_ = ad_settings.value("output/hardwareTranscodingBackend", "auto").toString(); if (!QStringList{"auto", "nvenc", "qsv", "amf", "vulkan"}.contains(hardware_transcoding_backend_)) hardware_transcoding_backend_ = "auto"; apply_hardware_decode_preference(); load_batch_queue(); load_watch_folders(); load_watch_history();
    for (const auto& codec : QStringList{"mpeg2", "h264", "hevc", "av1", "vp9", "ffv1", "prores"}) { auto preference = ad_settings.value("output/hardwareByCodec/" + codec, "global").toString(); if (!QStringList{"global", "software", "auto", "nvenc", "qsv", "amf", "vulkan"}.contains(preference)) preference = "global"; hardware_codec_backends_.insert(codec, preference); }
    startup_background_colour_ = QColor(ad_settings.value("startup/backgroundColour", "#000000").toString()); if (!startup_background_colour_.isValid()) startup_background_colour_ = Qt::black; selection_time_colour_ = QColor(ad_settings.value("startup/selectionTimeColour", "#202020").toString()); if (!selection_time_colour_.isValid()) selection_time_colour_ = QColor("#202020"); log_folder_ = ad_settings.value("startup/logFolder", QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/logs").toString(); favorite_places_ = ad_settings.value("files/favoritePlaces").toStringList(); while (favorite_places_.size() > 5) favorite_places_.removeLast(); associated_extensions_ = ad_settings.value("files/associatedExtensions", QStringList{"mkv", "mp4", "mov", "ts", "m2ts", "webm", "VPrj", "BPrj", "VJnr"}).toStringList();
    ui_style_name_ = ad_settings.value("uiStyle/name", "Professional Light").toString(); ui_base_colour_ = QColor(ad_settings.value("uiStyle/base", "#f3f5f7").toString()); ui_surface_colour_ = QColor(ad_settings.value("uiStyle/surface", "#ffffff").toString()); ui_text_colour_ = QColor(ad_settings.value("uiStyle/text", "#1f2933").toString()); ui_border_colour_ = QColor(ad_settings.value("uiStyle/border", "#c8ced6").toString()); ui_accent_colour_ = QColor(ad_settings.value("uiStyle/accent", "#1976c9").toString()); ui_font_family_ = ad_settings.value("uiStyle/fontFamily", QApplication::font().family()).toString(); ui_font_size_ = std::clamp(ad_settings.value("uiStyle/fontSize", QApplication::font().pointSize()).toInt(), 7, 24); if (!ui_base_colour_.isValid() || !ui_surface_colour_.isValid() || !ui_text_colour_.isValid() || !ui_border_colour_.isValid() || !ui_accent_colour_.isValid()) { ui_base_colour_ = QColor("#f3f5f7"); ui_surface_colour_ = Qt::white; ui_text_colour_ = QColor("#1f2933"); ui_border_colour_ = QColor("#c8ced6"); ui_accent_colour_ = QColor("#1976c9"); }
    editor_central_ = new QWidget(this); auto* central = editor_central_; auto* layout = new QVBoxLayout(central); layout->setContentsMargins(10, 7, 10, 3); layout->setSpacing(2);
    input_ = new QLineEdit(central); auto* input_button = new SideActionButton("Open Video", central);
    output_ = new QLineEdit(central); auto* output_button = new QPushButton("Choose Output…", central); output_profile_ = new QComboBox(central); for (const auto& profile : standard_output_profiles()) output_profile_->addItem(profile.favorite ? "★ " + profile.name : profile.name, profile.id); load_custom_profiles(); const auto saved_profile = output_profile_->findData(ad_settings.value("output/defaultProfile", "intelligent")); output_profile_->setCurrentIndex(saved_profile < 0 ? std::max(0, output_profile_->findData("intelligent")) : saved_profile); output_profile_->setAccessibleName("Output profile");
    start_ = new SideActionButton("Save Video", central); play_ = new QPushButton("▶", central); project_save_ = new QPushButton("Save Project…", central); project_open_ = new QPushButton("Open Project…", central); status_ = new QLabel("Ready", central); log_ = new QTextEdit(central); log_->setReadOnly(true);
    cancel_ = new QPushButton("Cancel"); cancel_->setEnabled(false); progress_ = new QProgressBar; progress_->setRange(0, 100); progress_->setValue(0);
    recent_open_ = new QPushButton("Open Recent", central); recent_open_->setAccessibleName("Open most recent source");

    const QStringList menu_titles{"File", "Edit", "Joiner", "Tools", "View", "Ad-Detective", "Help"};
    QHash<QString, QMenu*> menus; for (const auto& title : menu_titles) menus.insert(title, menuBar()->addMenu(title));
    auto* open_action = menus["File"]->addAction("Open Video…"); auto* recent_menu = menus["File"]->addMenu("Open Recent"); auto* favorite_menu = menus["File"]->addMenu("Favorite Places"); menus["File"]->addSeparator(); auto* open_project_action = menus["File"]->addAction("Open Project…"); auto* save_project_action = menus["File"]->addAction("Save Project"); auto* save_project_as_action = menus["File"]->addAction("Save Project As…"); menus["File"]->addSeparator(); auto* save_video_action = menus["File"]->addAction("Save Video As…"); auto* save_chapters_action = menus["File"]->addAction("Save Chapter File…"); auto* queue_batch_action = menus["File"]->addAction("Queue To Batch…"); menus["File"]->addSeparator(); auto* close_action = menus["File"]->addAction("Close Video"); auto* exit_action = menus["File"]->addAction("Exit");
    auto* undo_action = menus["Edit"]->addAction("Undo"); auto* redo_action = menus["Edit"]->addAction("Redo"); menus["Edit"]->addSeparator(); auto* mode_group = new QActionGroup(this); mode_group->setExclusive(true); cut_mode_action_ = menus["Edit"]->addAction("Cut Mode"); scene_mode_action_ = menus["Edit"]->addAction("Scene Mode"); cut_mode_action_->setCheckable(true); scene_mode_action_->setCheckable(true); mode_group->addAction(cut_mode_action_); mode_group->addAction(scene_mode_action_); cut_mode_action_->setChecked(true); menus["Edit"]->addSeparator(); auto* goto_time_action = menus["Edit"]->addAction("Go To Timecode…"); auto* goto_in_action = menus["Edit"]->addAction("Go To Selection Start"); auto* goto_out_action = menus["Edit"]->addAction("Go To Selection End"); auto* goto_start_action = menus["Edit"]->addAction("Go To Program Start"); auto* goto_end_action = menus["Edit"]->addAction("Go To End Of Program"); auto* previous_keyframe_action = menus["Edit"]->addAction("Previous Keyframe"); auto* next_keyframe_action = menus["Edit"]->addAction("Next Keyframe"); auto* set_bookmark_action = menus["Edit"]->addAction("Set Bookmark"); auto* goto_bookmark_action = menus["Edit"]->addAction("Go To Bookmark"); menus["Edit"]->addSeparator(); preview_action_ = menus["Edit"]->addAction("Preview Your Edits"); preview_action_->setCheckable(true); auto* capture_action = menus["Edit"]->addAction("Capture Frame…");
    auto* join_add_action = menus["Joiner"]->addAction("Add Current Project"); menus["Joiner"]->addSeparator(); auto* join_load_project_action = menus["Joiner"]->addAction("Load Joiner Project…"); auto* join_save_project_action = menus["Joiner"]->addAction("Save Joiner Project…"); auto* join_edit_action = menus["Joiner"]->addAction("Edit Joiner List…"); menus["Joiner"]->addSeparator(); auto* join_save_action = menus["Joiner"]->addAction("Create Video From Joiner List…");
    auto* title_editor_action = menus["Tools"]->addAction("Title Editor…"); auto* stream_action = menus["Tools"]->addAction("Select Streams…"); auto* profiles_action = menus["Tools"]->addAction("Edit Output Profiles…"); auto* audio_adjust_action = menus["Tools"]->addAction("Adjust Audio Synchronization…"); auto* display_adjust_action = menus["Tools"]->addAction("Display Adjustment…"); auto* chapter_options_action = menus["Tools"]->addAction("Chapter Marks…"); auto* program_info_action = menus["Tools"]->addAction("Show Program Information…"); auto* hardware_self_test_action = menus["Tools"]->addAction("Hardware Acceleration Self-Test…"); auto* metadata_editor_action = menus["Tools"]->addAction("Edit Media Metadata…"); export_report_action_ = menus["Tools"]->addAction("Show Last Export Report…"); export_report_action_->setEnabled(false); menus["Tools"]->addSeparator(); auto* qsf_action = menus["Tools"]->addAction("QuickStream Fix…"); auto* elementary_mux_action = menus["Tools"]->addAction("Elementary Stream Multiplexer…"); auto* trimmed_copy_action = menus["Tools"]->addAction("Create Trimmed File Copy…"); auto* batch_action = menus["Tools"]->addAction("Start Batch Manager…"); menus["Tools"]->addSeparator(); auto* ui_style_action = menus["Tools"]->addAction("UI Style Editor…"); auto* keyboard_action = menus["Tools"]->addAction("Keyboard Shortcuts…"); auto* options_action = menus["Tools"]->addAction("Options…");
    auto* rotation_menu = menus["View"]->addMenu("Video Rotation"); auto* rotation_group = new QActionGroup(this); rotation_group->setExclusive(true); const QList<QPair<QString, int>> rotations{{"Auto", -1}, {"0°", 0}, {"90° clockwise", 90}, {"180°", 180}, {"270° clockwise", 270}}; for (const auto& rotation : rotations) { auto* action = rotation_menu->addAction(rotation.first); action->setCheckable(true); action->setData(rotation.second); rotation_group->addAction(action); if (rotation.second == preview_rotation_degrees_) action->setChecked(true); connect(action, &QAction::triggered, this, [this, rotation] { set_preview_rotation(rotation.second); }); } auto* osd_action = menus["View"]->addAction("On Screen Display"); osd_action->setCheckable(true); osd_action->setChecked(osd_visible_); auto* frame_display_menu = menus["View"]->addMenu("Frame Number Display"); auto* frame_display_group = new QActionGroup(this); frame_display_group->setExclusive(true); const QList<QPair<QString, QString>> frame_modes{{"None", "none"}, {"Source frame", "source"}, {"Output frame", "output"}}; for (const auto& mode : frame_modes) { auto* action = frame_display_menu->addAction(mode.first); action->setCheckable(true); frame_display_group->addAction(action); if (mode.second == frame_number_mode_) action->setChecked(true); connect(action, &QAction::triggered, this, [this, mode] { frame_number_mode_ = mode.second; QSettings("SmartCut64", "SmartCut64").setValue("display/frameNumberMode", frame_number_mode_); if (!current_preview_image_.isNull()) show_preview_image(current_preview_image_, displayed_preview_frame_); }); } menus["View"]->addSeparator();
    auto* theme_menu = menus["View"]->addMenu("Theme");
    auto* theme_group = new QActionGroup(this);
    theme_group->setExclusive(true);
    light_theme_action_ = theme_menu->addAction("Light Theme");
    dark_theme_action_ = theme_menu->addAction("Dark Theme");
    for (auto* action : {light_theme_action_, dark_theme_action_}) {
      action->setCheckable(true);
      theme_group->addAction(action);
    }
    light_theme_action_->setChecked(ui_style_name_ != "Professional Dark" && ui_style_name_ != "Dark");
    dark_theme_action_->setChecked(!light_theme_action_->isChecked());
    theme_menu->addSeparator();
    auto* customize_theme_action = theme_menu->addAction("Customize.");
    menus["View"]->addSeparator();

    thumbnails_action_ = menus["View"]->addAction("Show Thumbnails"); thumbnails_action_->setCheckable(true); thumbnails_action_->setChecked(true); auto* thumbnail_options_action = menus["View"]->addAction("Thumbnail Options…"); menus["View"]->addSeparator(); auto* zoom_toggle_action = menus["View"]->addAction("Toggle Timeline Zoom"); auto* zoom_out_action = menus["View"]->addAction("Decrease Timeline Zoom"); auto* zoom_in_action = menus["View"]->addAction("Increase Timeline Zoom"); auto* fullscreen_action = menus["View"]->addAction("Full Screen"); menus["View"]->addSeparator(); auto* log_action = menus["View"]->addAction("Show Log"); log_action->setCheckable(true); auto* ad_start_action = menus["Ad-Detective"]->addAction("Start Ad Scan"); auto* ad_batch_action = menus["Ad-Detective"]->addAction("Batch Ad Scan…"); auto* ad_toggle_action = menus["Ad-Detective"]->addAction("Add / Remove Chapter Mark"); auto* ad_previous_action = menus["Ad-Detective"]->addAction("Previous Chapter Mark"); auto* ad_next_action = menus["Ad-Detective"]->addAction("Next Chapter Mark"); menus["Ad-Detective"]->addSeparator(); auto* ad_clear_action = menus["Ad-Detective"]->addAction("Clear All Chapter Marks"); auto* help_contents_action = menus["Help"]->addAction("Contents"); help_contents_action->setShortcut(QKeySequence::HelpContents); auto* help_keyboard_action = menus["Help"]->addAction("Keyboard Shortcuts…"); menus["Help"]->addSeparator(); auto* about_action = menus["Help"]->addAction("About SmartCut64");
    auto* license_action = new QAction("Licence & Activation…", this); menus["Help"]->insertAction(about_action, license_action);
    menus["Ad-Detective"]->addSeparator(); auto* ad_parameters_action = menus["Ad-Detective"]->addAction("Parameters…");

    plot_mask_action_ = new QAction("Show Plot Mask", this); plot_mask_action_->setCheckable(true); plot_mask_action_->setChecked(plot_mask_visible_); menus["View"]->insertAction(zoom_toggle_action, plot_mask_action_);

    auto* workspace = new QHBoxLayout; workspace->setSpacing(8); auto* editor = new QVBoxLayout; editor->setSpacing(4); workspace->addLayout(editor, 1);
    preview_ = new QLabel("Open a video to begin", central); preview_->setAlignment(Qt::AlignCenter); preview_->setMinimumSize(720, 390); preview_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); preview_->setStyleSheet(QString("background:%1;color:#c8c8c8;border:1px solid #777777;").arg(startup_background_colour_.name())); editor->addWidget(preview_, 1);
    thumbnail_panel_ = new QWidget(central); auto* thumbnail_panel_layout = new QVBoxLayout(thumbnail_panel_); thumbnail_panel_layout->setContentsMargins(0, 0, 0, 0); thumbnail_panel_layout->setSpacing(0); auto* thumbnails = new QHBoxLayout; thumbnails->setSpacing(1); thumbnails->setContentsMargins(0, 0, 0, 0); for (int i = 0; i < 19; ++i) { auto* thumbnail = new ThumbnailWidget(thumbnail_panel_); thumbnail->setMinimumHeight(thumbnail_height_); thumbnail->on_clicked = [this](std::size_t frame) { if (timeline_) timeline_->set_frame(frame); }; thumbnail->on_double_clicked = [this](std::size_t frame) { if (timeline_) timeline_->set_frame(frame); adjust_thumbnail_interval(-1); }; thumbnail_widgets_.push_back(thumbnail); thumbnails->addWidget(thumbnail, 1); } thumbnail_panel_layout->addLayout(thumbnails);
    frame_guide_ = new FrameGuideWidget(thumbnail_panel_); thumbnail_panel_layout->addWidget(frame_guide_); editor->addWidget(thumbnail_panel_); for (auto* thumbnail : thumbnail_widgets_) thumbnail->set_display_options(thumbnail_brightness_, thumbnail_timecode_visible_); frame_guide_->set_waveform_style(waveform_decibel_scale_, waveform_background_colour_, waveform_colour_, waveform_centre_colour_, waveform_text_colour_);
    frame_guide_->setVisible(thumbnail_audio_visible_);
    auto* timeline_label = new QLabel("Timeline   |   J/K random access   I/O mark range   Space play", central); timeline_label->hide(); timeline_ = new TimelineWidget(central); editor->addWidget(timeline_);

    auto* navigation_frame = new QFrame(central); navigation_frame->setObjectName("navigationFrame"); auto* navigation = new QVBoxLayout(navigation_frame); navigation->setContentsMargins(10, 8, 10, 8); navigation->setSpacing(6);
    auto* position_row = new QHBoxLayout; selection_start_ = new QLabel("00:00:00.00", navigation_frame); selection_start_->setAlignment(Qt::AlignCenter); selection_start_->setMinimumWidth(105); selection_start_->setStyleSheet(QString("color:%1;").arg(selection_time_colour_.name())); auto* mark_in = new QPushButton("|◀", navigation_frame); mark_in->setObjectName("markButton"); mark_in->setAccessibleName("Set In mark"); frame_readout_ = new QLabel("0", navigation_frame); frame_readout_->setObjectName("frameReadout"); frame_readout_->setAlignment(Qt::AlignCenter); frame_readout_->setFixedWidth(120); auto* mark_out = new QPushButton("▶|", navigation_frame); mark_out->setObjectName("markButton"); mark_out->setAccessibleName("Set Out mark"); program_end_ = new QLabel("00:00:00.00", navigation_frame); program_end_->setAlignment(Qt::AlignCenter); program_end_->setMinimumWidth(105); program_end_->setStyleSheet(QString("color:%1;").arg(selection_time_colour_.name())); position_row->addStretch(); position_row->addWidget(selection_start_); position_row->addWidget(mark_in); position_row->addSpacing(48); position_row->addWidget(frame_readout_); position_row->addSpacing(48); position_row->addWidget(mark_out); position_row->addWidget(program_end_); position_row->addStretch(); navigation->addLayout(position_row);
    fine_tune_ = new FineTuneSlider(navigation_frame); fine_tune_->setEnabled(false); auto* fine_tune_row = new QHBoxLayout; fine_tune_row->addStretch(); fine_tune_row->addWidget(fine_tune_); fine_tune_row->addStretch(); navigation->addLayout(fine_tune_row);
    auto add_transport_button = [navigation_frame](QHBoxLayout* row, const QString& text, const QString& accessible, bool round = false) { auto* button = new QPushButton(text, navigation_frame); button->setAccessibleName(accessible); button->setObjectName(round ? "roundButton" : "transportButton"); button->setFixedSize(round ? QSize(42, 42) : QSize(38, 32)); row->addWidget(button); return button; };
    auto* transport_row = new QHBoxLayout; transport_row->setSpacing(4); transport_row->addStretch();
    auto* previous_key = add_transport_button(transport_row, "◀", "Previous cut boundary"); edit_preview_ = add_transport_button(transport_row, "P", "Toggle edit and preview modes", true); auto* next_key = add_transport_button(transport_row, "▶", "Next cut boundary"); transport_row->addSpacing(28);
    auto* back_three = add_transport_button(transport_row, "◀◀◀", "Shuttle backwards quickly"); auto* back_two = add_transport_button(transport_row, "◀◀", "Shuttle backwards"); auto* previous_frame = add_transport_button(transport_row, "◀", "Previous display frame"); play_->setText("▶"); play_->setObjectName("roundButton"); play_->setFixedSize(42, 42); play_->setAccessibleName("Play or pause preview"); transport_row->addWidget(play_); auto* next_frame = add_transport_button(transport_row, "▶", "Next display frame"); auto* forward_two = add_transport_button(transport_row, "▶▶", "Shuttle forwards"); auto* forward_three = add_transport_button(transport_row, "▶▶▶", "Shuttle forwards quickly"); transport_row->addSpacing(28);
    auto* previous_scene = add_transport_button(transport_row, "◀", "Previous detected scene"); auto* scene_scan = add_transport_button(transport_row, "↻", "Start scene scan", true); auto* next_scene = add_transport_button(transport_row, "▶", "Next detected scene"); transport_row->addStretch(); navigation->addLayout(transport_row);
    auto* action_row = new QHBoxLayout; cut_keep_ = new QPushButton("Cut Selection", navigation_frame); cut_keep_->setAccessibleName("Add the selected range to the cut list"); trim_ = new QPushButton("Trim Unselected", navigation_frame); trim_->setAccessibleName("Cut everything outside the selected range"); auto* save_bottom = new QPushButton("Save Video", navigation_frame); save_bottom->setAccessibleName("Save edited video"); action_row->addStretch(); action_row->addWidget(cut_keep_); action_row->addWidget(trim_); action_row->addWidget(save_bottom); action_row->addStretch(); navigation->addLayout(action_row); editor->addWidget(navigation_frame);
    timecode_ = new QLabel("Source  00:00:00.000       Output  00:00:00.000       Frame  0", central); timecode_->hide();

    auto* side = new QHBoxLayout; side->setSpacing(5); workspace->addLayout(side); auto* data_column = new QVBoxLayout; data_column->setSpacing(5); side->addLayout(data_column); cut_list_ = new QTableWidget(0, 3, central); cut_list_->setHorizontalHeaderLabels({"Cut Start", "Cut End", "Frames"}); cut_list_->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); cut_list_->verticalHeader()->hide(); cut_list_->setSelectionBehavior(QAbstractItemView::SelectRows); cut_list_->setSelectionMode(QAbstractItemView::ExtendedSelection); cut_list_->setEditTriggers(QAbstractItemView::NoEditTriggers); cut_list_->setMinimumWidth(220); cut_list_->setMaximumWidth(245); cut_list_->setAccessibleName("Cut selection list"); data_column->addWidget(cut_list_, 1); invert_cuts_ = new QPushButton("Invert All Cuts", central); invert_cuts_->setAccessibleName("Invert all cut and kept ranges"); remove_cuts_ = new QPushButton("Remove Selected Cuts", central); remove_cuts_->setAccessibleName("Remove selected cuts from the cut list"); data_column->addWidget(invert_cuts_); data_column->addWidget(remove_cuts_);
    auto* info_group = new QGroupBox("Info", central); auto* info_layout = new QVBoxLayout(info_group); info_layout->setContentsMargins(4, 10, 4, 4); info_grid_ = new InfoGridWidget(info_group); info_layout->addWidget(info_grid_); data_column->addWidget(info_group); inspector_ = new QTextEdit(central); inspector_->setReadOnly(true); inspector_->setPlainText("Frame: 0\nVideo: pending\nAudio: pending"); inspector_->hide();
    auto* action_rail = new QVBoxLayout; action_rail->setSpacing(7); side->addLayout(action_rail); auto configure_side_button = [](QPushButton* button, ActionIcon icon) { button->setObjectName("sideAction"); button->setIcon(action_icon(icon)); button->setIconSize(QSize(34, 34)); button->setFixedSize(68, 78); }; configure_side_button(input_button, ActionIcon::Open); configure_side_button(start_, ActionIcon::Save); input_button->setText("Open\nVideo"); start_->setText("Save\nVideo"); action_rail->addWidget(input_button); action_rail->addWidget(start_); auto add_side_button = [action_rail, central, configure_side_button](const QString& text, const QString& accessible, ActionIcon icon) { auto* button = new SideActionButton(text, central); button->setAccessibleName(accessible); configure_side_button(button, icon); action_rail->addWidget(button); return button; };
    auto* add_joiner_button = add_side_button("Add To\nJoiner", "Add current video to joiner", ActionIcon::Join); auto* save_joiner_button = add_side_button("Save\nJoiner", "Save joiner output", ActionIcon::Save); action_rail->addStretch();
    connect(add_joiner_button, &QPushButton::clicked, this, &ExportWindow::add_current_to_joiner); connect(save_joiner_button, &QPushButton::clicked, this, &ExportWindow::create_joiner_video);

    timeline_->on_frame_changed = [this](std::size_t frame) { if (smoke_navigation_seek_observing_ && smoke_navigation_seek_target_ && frame < *smoke_navigation_seek_target_) smoke_navigation_seek_regressed_ = true; const auto source_ms = timeline_->current_time_us() / 1000; const auto output_ms = static_cast<std::uint64_t>(timeline_->output_frame(frame)) * 1000ULL * static_cast<std::uint64_t>(std::max(frame_rate_den_, 1)) / static_cast<std::uint64_t>(std::max(frame_rate_num_, 1)); const auto format_ms = [](std::uint64_t ms) { return QString("%1:%2:%3.%4").arg(ms / 3600000, 2, 10, QLatin1Char('0')).arg((ms / 60000) % 60, 2, 10, QLatin1Char('0')).arg((ms / 1000) % 60, 2, 10, QLatin1Char('0')).arg(ms % 1000, 3, 10, QLatin1Char('0')); }; timecode_->setText(QString("Source %1   Output %2   Frame %3").arg(format_ms(source_ms), format_ms(output_ms)).arg(static_cast<qulonglong>(frame))); frame_readout_->setText(QString::number(static_cast<qulonglong>(frame))); frame_guide_->set_position(frame, timeline_->frame_count()); refresh_info_grid(); status_->setText(timeline_->preview_mode() ? QString("Preview source frame %1 (output frame %2)").arg(static_cast<qulonglong>(frame)).arg(static_cast<qulonglong>(timeline_->output_frame(frame))) : QString("Source frame %1").arg(static_cast<qulonglong>(frame))); request_preview(frame); };
    timeline_->on_drag_started = [this] { begin_navigation_seek(); };
    timeline_->on_drag_finished = [this] { finish_navigation_seek(); };
    timeline_->on_chapters_changed = [this] { set_project_modified(true); status_->setText(QString("%1 chapter markers").arg(static_cast<qulonglong>(timeline_->chapter_markers().size()))); if (!project_path_.isEmpty()) { record_edit_snapshot(); project_autosave_timer_.start(700); } };
    timeline_->on_bookmark_changed = [this](std::size_t frame) { status_->setText(QString("Bookmark set at frame %1").arg(static_cast<qulonglong>(frame))); refresh_info_grid(); };
    fine_tune_->current_time_us = [this] { return timeline_->display_time_us(); };
    fine_tune_->seek_time_us = [this](std::uint64_t time_us) { timeline_->set_time_us(time_us); };
    fine_tune_->on_drag_started = [this] { begin_navigation_seek(); };
    fine_tune_->on_drag_finished = [this] { finish_navigation_seek(); };
    timeline_->on_mark_changed = [this, timeline_label](std::size_t frame, bool is_in) { timeline_label->setText(QString("Timeline   |   %1 mark: frame %2   |   J/K shuttle   I/O mark range   Space play").arg(is_in ? "In" : "Out").arg(static_cast<qulonglong>(frame))); if (timeline_->in_mark()) selection_start_->setText(frame_timecode(*timeline_->in_mark())); if (timeline_->out_mark()) program_end_->setText(frame_timecode(*timeline_->out_mark())); refresh_info_grid(); if (timeline_->range_invalid()) status_->setText("Invalid range: Out precedes In"); else status_->setText("Playback range updated"); };
    undo_ = new QPushButton("Undo", central); undo_->setAccessibleName("Undo edit"); undo_->hide(); redo_ = new QPushButton("Redo", central); redo_->setAccessibleName("Redo edit"); redo_->hide();
    input_->hide(); output_->hide(); output_button->hide(); output_profile_->hide(); project_save_->hide(); project_open_->hide(); recent_open_->hide(); cancel_->hide(); progress_->setTextVisible(false); progress_->setMaximumHeight(7); log_->hide(); log_->setMaximumHeight(150);
    auto* volume_row = new QHBoxLayout; volume_icon_ = new QPushButton("🔊", central); volume_icon_->setFlat(true); volume_icon_->setFixedSize(30, 26); volume_icon_->setAccessibleName("Mute or unmute preview audio"); volume_ = new QSlider(Qt::Horizontal, central); volume_->setRange(0, 100); volume_->setValue(std::clamp(ad_settings.value("audio/volume", 75).toInt(), 0, 100)); volume_->setFixedWidth(100); volume_->setAccessibleName("Preview volume"); muted_ = volume_->value() == 0; if (!muted_) last_volume_ = volume_->value(); volume_icon_->setText(muted_ ? "🔇" : "🔊"); volume_row->addWidget(volume_icon_); volume_row->addWidget(volume_); volume_row->addStretch(); write_audio_volume();
    layout->addLayout(workspace, 1); layout->addLayout(volume_row); layout->addWidget(progress_); layout->addWidget(status_); layout->addWidget(log_);
    startup_splash_ = new StartupSplashWidget(this);
    startup_splash_->open_video = [this] { choose_source_media(); };
    if (automation_mode_) {
      setCentralWidget(editor_central_);
      setWindowTitle("SmartCut64 - Video Editor");
      resize(editor_window_size_);
    } else {
      editor_central_->hide();
      setCentralWidget(startup_splash_);
      setWindowTitle("SmartCut64");
      resize(600, 370);
      startup_splash_->setFocus();
    }
    setStyleSheet("QMainWindow,QWidget{background:#f0f0f0;color:#202020;} QMenuBar,QMenu{background:#f0f0f0;} QMenuBar{padding:0px;} QPushButton{min-height:24px;padding:2px 9px;border:1px solid #868686;border-top-color:#ffffff;border-left-color:#ffffff;border-right-color:#777777;border-bottom-color:#777777;border-radius:1px;background:qlineargradient(x1:0,y1:0,x2:0,y2:1,stop:0 #ffffff,stop:0.46 #f5f5f5,stop:0.54 #e8e8e8,stop:1 #d2d2d2);} QPushButton:hover{border-color:#569de5;background:qlineargradient(x1:0,y1:0,x2:0,y2:1,stop:0 #ffffff,stop:0.48 #eef7ff,stop:0.52 #dceeff,stop:1 #bfdfff);} QPushButton:pressed{padding-left:10px;padding-top:3px;border-top-color:#666666;border-left-color:#666666;border-right-color:#ffffff;border-bottom-color:#ffffff;background:qlineargradient(x1:0,y1:0,x2:0,y2:1,stop:0 #c5c5c5,stop:1 #ededed);} QPushButton:disabled{color:#969696;border-color:#d0d0d0;background:#e8e8e8;} QPushButton#sideAction{min-width:68px;max-width:68px;min-height:78px;max-height:78px;border:0;padding:1px;background:#f0f0f0;} QPushButton#sideAction:hover{background:#dcecfc;} QPushButton#sideAction:pressed{padding-left:2px;padding-top:2px;background:#c4e0fc;} QPushButton#transportButton{min-width:38px;max-width:38px;min-height:32px;max-height:32px;padding:0;font-size:13px;font-weight:700;} QPushButton#transportButton:pressed{padding-left:1px;padding-top:1px;} QPushButton#roundButton{min-width:42px;max-width:42px;min-height:42px;max-height:42px;border-radius:21px;color:white;border:1px solid #095997;border-top-color:#6eb7ec;border-left-color:#52a7e5;border-right-color:#075083;border-bottom-color:#06436f;background:qlineargradient(x1:0,y1:0,x2:0,y2:1,stop:0 #45a4e6,stop:0.45 #258bd3,stop:0.52 #1478c2,stop:1 #075c9d);font-size:18px;font-weight:700;padding:0;} QPushButton#roundButton:hover{background:qlineargradient(x1:0,y1:0,x2:0,y2:1,stop:0 #64b8ef,stop:0.5 #2d98df,stop:1 #0b6db6);} QPushButton#roundButton:pressed{padding-left:1px;padding-top:1px;border-top-color:#06436f;border-left-color:#075083;border-right-color:#6eb7ec;border-bottom-color:#6eb7ec;background:qlineargradient(x1:0,y1:0,x2:0,y2:1,stop:0 #075c9d,stop:1 #258bd3);} QPushButton#markButton{min-width:38px;padding:0;font-weight:700;} QPushButton#markButton:pressed{padding-left:1px;padding-top:1px;} QLabel#frameReadout{background:#f0f0f0;border:1px solid #b6b6b6;font-size:18px;font-weight:700;} QFrame#navigationFrame{border:1px solid #b6b6b6;background:#f0f0f0;} QTableWidget,QTextEdit{background:white;} QHeaderView::section{background:#f0f0f0;padding:3px;border:1px solid #b6b6b6;} QGroupBox{font-weight:400;border:1px solid #b6b6b6;margin-top:7px;} QGroupBox::title{subcontrol-origin:margin;left:5px;padding:0 2px;} QProgressBar::chunk{background:#06b025;}");
    apply_ui_style();
    input_button->setAccessibleName("Choose source media"); output_button->setAccessibleName("Choose export path"); input_->setAccessibleName("Source media path"); output_->setAccessibleName("Export media path"); project_save_->setAccessibleName("Save project"); project_open_->setAccessibleName("Open project"); play_->setAccessibleName("Play or pause preview"); start_->setAccessibleName("Review plan and export"); cancel_->setAccessibleName("Cancel current operation"); progress_->setAccessibleName("Operation progress"); preview_->setAccessibleName("SDR tone-mapped video preview"); inspector_->setAccessibleName("Track HDR and project inspector"); log_->setAccessibleName("Worker and validation log"); status_->setAccessibleName("Operation status"); timecode_->setAccessibleName("Source and output timecode and display frame");
    preview_resize_timer_.setSingleShot(true); connect(&preview_resize_timer_, &QTimer::timeout, this, [this] { if (!current_preview_image_.isNull() && preview_) show_preview_image(current_preview_image_, displayed_preview_frame_); });
    thumbnail_layout_timer_.setSingleShot(true); connect(&thumbnail_layout_timer_, &QTimer::timeout, this, &ExportWindow::update_thumbnail_widget_count); QTimer::singleShot(0, this, &ExportWindow::update_thumbnail_widget_count);
    export_progress_timer_.setSingleShot(false); connect(&export_progress_timer_, &QTimer::timeout, this, &ExportWindow::update_export_progress);
    preview_->installEventFilter(this); preview_->setContextMenuPolicy(Qt::CustomContextMenu); timeline_->setContextMenuPolicy(Qt::CustomContextMenu); thumbnail_panel_->setContextMenuPolicy(Qt::CustomContextMenu); connect(preview_, &QWidget::customContextMenuRequested, this, &ExportWindow::show_editor_context_menu); connect(timeline_, &QWidget::customContextMenuRequested, this, &ExportWindow::show_editor_context_menu); connect(thumbnail_panel_, &QWidget::customContextMenuRequested, this, &ExportWindow::show_thumbnail_context_menu);
    connect(open_action, &QAction::triggered, input_button, &QPushButton::click); connect(recent_menu, &QMenu::aboutToShow, this, [this, recent_menu] { recent_menu->clear(); const auto recent = QSettings("SmartCut64", "SmartCut64").value("recentFiles").toStringList(); for (const auto& path : recent) { auto* action = recent_menu->addAction(QFileInfo(path).fileName()); action->setToolTip(path); action->setEnabled(QFileInfo::exists(path)); connect(action, &QAction::triggered, this, [this, path] { request_load_source(path); }); } if (recent.isEmpty()) { auto* empty = recent_menu->addAction("(Empty)"); empty->setEnabled(false); } }); connect(open_project_action, &QAction::triggered, project_open_, &QPushButton::click); connect(save_project_action, &QAction::triggered, project_save_, &QPushButton::click); connect(save_project_as_action, &QAction::triggered, this, &ExportWindow::save_project_as); connect(save_video_action, &QAction::triggered, start_, &QPushButton::click); connect(save_chapters_action, &QAction::triggered, this, &ExportWindow::save_chapter_file_interactive); connect(close_action, &QAction::triggered, this, &ExportWindow::close_source); connect(exit_action, &QAction::triggered, this, [] { qApp->closeAllWindows(); }); connect(undo_action, &QAction::triggered, undo_, &QPushButton::click); connect(redo_action, &QAction::triggered, redo_, &QPushButton::click); goto_time_action->setShortcut(QKeySequence(QStringLiteral("Ctrl+T"))); connect(goto_time_action, &QAction::triggered, this, &ExportWindow::show_go_to_timecode); connect(goto_in_action, &QAction::triggered, timeline_, &TimelineWidget::goto_in_mark); connect(goto_out_action, &QAction::triggered, timeline_, &TimelineWidget::goto_out_mark); connect(goto_start_action, &QAction::triggered, timeline_, &TimelineWidget::goto_beginning); connect(goto_end_action, &QAction::triggered, timeline_, &TimelineWidget::goto_end); connect(previous_keyframe_action, &QAction::triggered, timeline_, &TimelineWidget::previous_random_access); connect(next_keyframe_action, &QAction::triggered, timeline_, &TimelineWidget::next_random_access); connect(preview_action_, &QAction::toggled, this, [this](bool enabled) { set_preview_mode(enabled); }); connect(log_action, &QAction::toggled, log_, &QWidget::setVisible); connect(save_bottom, &QPushButton::clicked, start_, &QPushButton::click);
    connect(favorite_menu, &QMenu::aboutToShow, this, [this, favorite_menu] { favorite_menu->clear(); for (const auto& folder : favorite_places_) { auto* action = favorite_menu->addAction(QFileInfo(folder).fileName().isEmpty() ? folder : QFileInfo(folder).fileName()); action->setToolTip(QDir::toNativeSeparators(folder)); action->setEnabled(QDir(folder).exists()); connect(action, &QAction::triggered, this, [this, folder] { const auto paths = QFileDialog::getOpenFileNames(this, "Open Video", folder, "Video Files (*.mkv *.mp4 *.mov *.ts *.m2ts *.mpg *.mpeg *.vob *.avi *.webm);;All Files (*)"); if (!paths.isEmpty()) handle_open_files(paths); }); } if (favorite_places_.isEmpty()) { auto* empty = favorite_menu->addAction("Configure in Tools > Options"); empty->setEnabled(false); } });
    connect(cut_mode_action_, &QAction::triggered, this, [this] { set_scene_mode(false); }); connect(scene_mode_action_, &QAction::triggered, this, [this] { set_scene_mode(true); });
    capture_action->setShortcut(QKeySequence::Copy); connect(capture_action, &QAction::triggered, this, &ExportWindow::capture_frame);
    set_bookmark_action->setShortcut(QKeySequence(Qt::Key_B)); goto_bookmark_action->setShortcut(QKeySequence(Qt::Key_J)); connect(set_bookmark_action, &QAction::triggered, timeline_, &TimelineWidget::set_bookmark); connect(goto_bookmark_action, &QAction::triggered, timeline_, &TimelineWidget::goto_bookmark);
    connect(title_editor_action, &QAction::triggered, this, [this] { add_title_to_joiner(); }); connect(qsf_action, &QAction::triggered, this, &ExportWindow::show_quickstream_fix); connect(elementary_mux_action, &QAction::triggered, this, &ExportWindow::show_elementary_multiplexer); connect(trimmed_copy_action, &QAction::triggered, this, &ExportWindow::show_trimmed_file_copy);
    stream_action->setShortcut(QKeySequence(QStringLiteral("Ctrl+Q"))); connect(stream_action, &QAction::triggered, this, &ExportWindow::show_stream_selection);
    connect(profiles_action, &QAction::triggered, this, &ExportWindow::show_output_profiles); connect(audio_adjust_action, &QAction::triggered, this, &ExportWindow::show_audio_adjustment); connect(display_adjust_action, &QAction::triggered, this, &ExportWindow::show_display_adjustment); connect(chapter_options_action, &QAction::triggered, this, &ExportWindow::show_chapter_options); program_info_action->setShortcut(QKeySequence(QStringLiteral("Ctrl+D"))); connect(program_info_action, &QAction::triggered, this, &ExportWindow::show_program_information); connect(hardware_self_test_action, &QAction::triggered, this, &ExportWindow::show_hardware_self_test);
    queue_batch_action->setShortcut(QKeySequence(QStringLiteral("Ctrl+B"))); connect(queue_batch_action, &QAction::triggered, this, &ExportWindow::queue_current_to_batch);
    connect(batch_action, &QAction::triggered, this, &ExportWindow::show_batch_manager); connect(ui_style_action, &QAction::triggered, this, &ExportWindow::show_ui_style_editor);
    connect(light_theme_action_, &QAction::triggered, this, [this] { apply_theme_preset("Professional Light", true); });
    connect(dark_theme_action_, &QAction::triggered, this, [this] { apply_theme_preset("Professional Dark", true); });
    connect(customize_theme_action, &QAction::triggered, this, &ExportWindow::show_ui_style_editor);
    connect(keyboard_action, &QAction::triggered, this, &ExportWindow::show_keyboard_shortcuts); connect(help_keyboard_action, &QAction::triggered, this, &ExportWindow::show_keyboard_shortcuts); connect(help_contents_action, &QAction::triggered, this, &ExportWindow::show_help_contents);
    connect(license_action, &QAction::triggered, this, [this] { license_.show_dialog(this); license_.reload(); refresh_window_title(); status_->setText(license_.status_text()); });
    thumbnails_action_->setShortcut(QKeySequence(Qt::Key_F11)); connect(thumbnails_action_, &QAction::toggled, this, [this](bool visible) { thumbnail_panel_->setVisible(visible); if (visible) { update_thumbnail_widget_count(); if (timeline_->frame_count() > 0) schedule_thumbnail_strip(timeline_->current_frame()); } });
    connect(plot_mask_action_, &QAction::toggled, this, [this](bool visible) { plot_mask_visible_ = visible; QSettings("SmartCut64", "SmartCut64").setValue("display/plotMaskVisible", visible); if (!current_preview_image_.isNull()) show_preview_image(current_preview_image_, displayed_preview_frame_); status_->setText(visible ? "Plot mask shown" : "Plot mask hidden"); });
    connect(osd_action, &QAction::toggled, this, [this](bool visible) { osd_visible_ = visible; QSettings("SmartCut64", "SmartCut64").setValue("display/osdVisible", visible); if (!current_preview_image_.isNull()) show_preview_image(current_preview_image_, displayed_preview_frame_); status_->setText(visible ? "On-screen display shown" : "On-screen display hidden"); });
    connect(zoom_toggle_action, &QAction::triggered, timeline_, &TimelineWidget::toggle_zoom); connect(zoom_out_action, &QAction::triggered, timeline_, &TimelineWidget::zoom_out); connect(zoom_in_action, &QAction::triggered, timeline_, &TimelineWidget::zoom_in); connect(fullscreen_action, &QAction::triggered, this, &ExportWindow::toggle_fullscreen); connect(about_action, &QAction::triggered, this, [this] { const auto version = QString::fromLatin1(smartcut::build_info::application_version.data(), static_cast<int>(smartcut::build_info::application_version.size())); QMessageBox::about(this, "About SmartCut64", QString("SmartCut64 %1\nNative 64-bit frame-accurate video editor\n\nCopyright (C) 2026 SmartCut64 contributors\nLicensed under GNU GPL version 3 or later.\nThis program comes with absolutely no warranty.\n\nComplete corresponding source and third-party notices are distributed with each binary release. Third-party names belong to their respective owners; no sponsorship or endorsement is implied.").arg(version)); });
    thumbnail_options_action->setShortcut(QKeySequence(Qt::SHIFT | Qt::Key_F11));
    connect(thumbnail_options_action, &QAction::triggered, this, &ExportWindow::show_thumbnail_options);
    connect(options_action, &QAction::triggered, this, &ExportWindow::show_program_options);
    timeline_->before_edit = [this] { undo_states_.push_back(edit_state()); redo_states_.clear(); };
    connect(&indexing_status_timer_, &QTimer::timeout, this, [this] { if (!control_navigation_pending_) return; status_->setText(QString("Indexing source navigation in the background… %1 s").arg(++indexing_elapsed_seconds_)); });
    thumbnail_refresh_timer_.setSingleShot(true); connect(&thumbnail_refresh_timer_, &QTimer::timeout, this, [this] { start_pending_thumbnail_strip(); });
    project_autosave_timer_.setSingleShot(true); connect(&project_autosave_timer_, &QTimer::timeout, this, [this] { save_project_document(autosave_worker_, project_path_, true); });
    log_flush_timer_.setSingleShot(true); log_flush_timer_.setInterval(250); connect(&log_flush_timer_, &QTimer::timeout, this, &ExportWindow::flush_application_log); connect(log_, &QTextEdit::textChanged, this, [this] { log_flush_timer_.start(); });
    watch_folder_timer_.setInterval(2000); connect(&watch_folder_timer_, &QTimer::timeout, this, [this] { scan_watch_folders(); }); watch_folder_timer_.start(); QTimer::singleShot(0, this, [this] { scan_watch_folders(); });
    connect(previous_frame, &QPushButton::clicked, timeline_, &TimelineWidget::step_backward); connect(next_frame, &QPushButton::clicked, timeline_, &TimelineWidget::step_forward); connect(previous_key, &QPushButton::clicked, timeline_, &TimelineWidget::previous_cut_boundary); connect(next_key, &QPushButton::clicked, timeline_, &TimelineWidget::next_cut_boundary); connect(edit_preview_, &QPushButton::clicked, this, [this] { set_preview_mode(!timeline_->preview_mode()); }); connect(mark_in, &QPushButton::clicked, this, [this] { set_preview_mode(false); timeline_->mark_in(); }); connect(mark_out, &QPushButton::clicked, this, [this] { set_preview_mode(false); timeline_->mark_out(); }); connect(cut_keep_, &QPushButton::clicked, this, [this] { cut_selected_range(); }); connect(trim_, &QPushButton::clicked, this, [this] { trim_unselected(); }); connect(invert_cuts_, &QPushButton::clicked, this, [this] { invert_all_cuts(); }); connect(remove_cuts_, &QPushButton::clicked, this, [this] { remove_selected_cuts(); }); connect(undo_, &QPushButton::clicked, this, [this] { if (undo_states_.isEmpty()) { status_->setText("Nothing to undo"); return; } redo_states_.push_back(edit_state()); apply_edit_state(undo_states_.takeLast()); set_project_modified(true); status_->setText("Undo: edit state restored"); }); connect(redo_, &QPushButton::clicked, this, [this] { if (redo_states_.isEmpty()) { status_->setText("Nothing to redo"); return; } undo_states_.push_back(edit_state()); apply_edit_state(redo_states_.takeLast()); set_project_modified(true); status_->setText("Redo: edit state restored"); });
    connect(cut_list_, &QTableWidget::itemClicked, this, [this](QTableWidgetItem* item) { if (item) activate_cut_row(item->row(), false); }); connect(cut_list_, &QTableWidget::itemDoubleClicked, this, [this](QTableWidgetItem* item) { if (item) activate_cut_row(item->row(), true); });
    connect(back_two, &QPushButton::clicked, this, [this] { timeline_->seek_relative_seconds(-short_jump_seconds_); }); connect(back_three, &QPushButton::clicked, this, [this] { timeline_->seek_relative_seconds(-long_jump_seconds_); }); connect(forward_two, &QPushButton::clicked, this, [this] { timeline_->seek_relative_seconds(short_jump_seconds_); }); connect(forward_three, &QPushButton::clicked, this, [this] { timeline_->seek_relative_seconds(long_jump_seconds_); }); connect(previous_scene, &QPushButton::clicked, timeline_, &TimelineWidget::previous_chapter); connect(next_scene, &QPushButton::clicked, timeline_, &TimelineWidget::next_chapter); connect(scene_scan, &QPushButton::clicked, this, &ExportWindow::start_ad_scan); connect(ad_start_action, &QAction::triggered, this, &ExportWindow::start_ad_scan); connect(ad_toggle_action, &QAction::triggered, timeline_, &TimelineWidget::toggle_chapter_marker); connect(ad_previous_action, &QAction::triggered, timeline_, &TimelineWidget::previous_chapter); connect(ad_next_action, &QAction::triggered, timeline_, &TimelineWidget::next_chapter); connect(ad_clear_action, &QAction::triggered, timeline_, &TimelineWidget::clear_chapter_markers); ad_previous_action->setShortcut(QKeySequence(Qt::Key_F5)); ad_next_action->setShortcut(QKeySequence(Qt::Key_F6)); ad_toggle_action->setShortcut(QKeySequence(Qt::Key_A));
    connect(ad_batch_action, &QAction::triggered, this, &ExportWindow::show_batch_manager);
    connect(volume_, &QSlider::valueChanged, this, [this](int value) { if (value > 0) last_volume_ = value; muted_ = value == 0; volume_icon_->setText(muted_ ? "🔇" : "🔊"); QSettings("SmartCut64", "SmartCut64").setValue("audio/volume", value); write_audio_volume(); }); connect(volume_icon_, &QPushButton::clicked, this, [this] { if (muted_ || volume_->value() == 0) volume_->setValue(std::max(last_volume_, 1)); else { last_volume_ = volume_->value(); volume_->setValue(0); } });
    connect(ad_parameters_action, &QAction::triggered, this, &ExportWindow::show_ad_parameters); ad_start_action->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_A)); ad_clear_action->setShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_A));
    connect(join_add_action, &QAction::triggered, this, &ExportWindow::add_current_to_joiner);
    connect(join_load_project_action, &QAction::triggered, this, [this] { const auto path = QFileDialog::getOpenFileName(this, "Load Joiner Project", {}, "Joiner Projects (*.scjoin.json *.VJnr);;VideoReDo Joiner Project (*.VJnr);;SmartCut Joiner Project (*.scjoin.json *.json)"); if (!path.isEmpty()) load_joiner_project(path); });
    connect(join_save_project_action, &QAction::triggered, this, [this] { const auto path = QFileDialog::getSaveFileName(this, "Save Joiner Project", {}, "SmartCut Joiner Project (*.scjoin.json);;VideoReDo Joiner Project (*.VJnr)"); if (!path.isEmpty()) save_joiner_project(path); });
    connect(join_edit_action, &QAction::triggered, this, &ExportWindow::edit_joiner_list);
    connect(join_save_action, &QAction::triggered, this, &ExportWindow::create_joiner_video);
    connect(export_report_action_, &QAction::triggered, this, &ExportWindow::show_last_export_report);
    connect(metadata_editor_action, &QAction::triggered, this, &ExportWindow::show_metadata_editor);
    auto shortcut = [this](const QString& id, const QKeySequence& key, QPushButton* button) { bind_shortcut(id, key, [button] { button->click(); }); }; shortcut("Play/Pause", QKeySequence(Qt::Key_Space), play_); shortcut("Save Project", QKeySequence(QStringLiteral("Ctrl+P")), project_save_); shortcut("Undo", QKeySequence::Undo, undo_); shortcut("Redo", QKeySequence::Redo, redo_); shortcut("Save Video", QKeySequence::Save, start_); shortcut("Mark In", QKeySequence(Qt::Key_F3), mark_in); shortcut("Mark Out", QKeySequence(Qt::Key_F4), mark_out); shortcut("Cut Selection", QKeySequence(Qt::Key_Delete), cut_keep_); shortcut("Trim Unselected", QKeySequence(Qt::Key_Insert), trim_); shortcut("Invert Cuts", QKeySequence(QStringLiteral("Ctrl+I")), invert_cuts_); shortcut("Remove Cuts", QKeySequence(QStringLiteral("Ctrl+R")), remove_cuts_);
    auto invoke = [this](const QString& id, const QKeySequence& key, std::function<void()> operation) { bind_shortcut(id, key, std::move(operation)); };
    invoke("Open Video", QKeySequence::Open, [input_button] { input_button->click(); }); invoke("Previous Frame", QKeySequence(Qt::Key_Left), [this] { timeline_->step_backward(); }); invoke("Next Frame", QKeySequence(Qt::Key_Right), [this] { timeline_->step_forward(); }); invoke("Previous Keyframe", QKeySequence(Qt::CTRL | Qt::Key_Left), [this] { timeline_->previous_random_access(); }); invoke("Next Keyframe", QKeySequence(Qt::CTRL | Qt::Key_Right), [this] { timeline_->next_random_access(); }); invoke("Short Jump Back", QKeySequence(Qt::Key_Up), [this] { timeline_->seek_relative_seconds(-short_jump_seconds_); }); invoke("Short Jump Forward", QKeySequence(Qt::Key_Down), [this] { timeline_->seek_relative_seconds(short_jump_seconds_); }); invoke("Long Jump Back", QKeySequence(Qt::Key_PageUp), [this] { timeline_->seek_relative_seconds(-long_jump_seconds_); }); invoke("Long Jump Forward", QKeySequence(Qt::Key_PageDown), [this] { timeline_->seek_relative_seconds(long_jump_seconds_); }); invoke("Program Start", QKeySequence(Qt::Key_Home), [this] { timeline_->goto_beginning(); }); invoke("Program End", QKeySequence(Qt::Key_End), [this] { timeline_->goto_end(); }); invoke("Selection Start", QKeySequence(Qt::Key_S), [this] { timeline_->goto_in_mark(); }); invoke("Selection End", QKeySequence(Qt::Key_E), [this] { timeline_->goto_out_mark(); }); invoke("Next Cut", QKeySequence(Qt::Key_Tab), [this] { timeline_->next_cut_boundary(); }); invoke("Previous Cut", QKeySequence(Qt::SHIFT | Qt::Key_Tab), [this] { timeline_->previous_cut_boundary(); }); invoke("Delete Frame", QKeySequence(Qt::Key_D), [this] { delete_current_frame(); }); invoke("Close Source", QKeySequence(QStringLiteral("Ctrl+F4")), [this] { close_source(); });
    invoke("Edit/Preview Mode", QKeySequence(Qt::Key_P), [this] { set_preview_mode(!timeline_->preview_mode()); }); invoke("Mute", QKeySequence(Qt::Key_M), [this] { volume_icon_->click(); }); invoke("Volume Up", QKeySequence(Qt::SHIFT | Qt::Key_M), [this] { volume_->setValue(std::min(100, volume_->value() + 5)); }); invoke("Volume Down", QKeySequence(Qt::CTRL | Qt::Key_M), [this] { volume_->setValue(std::max(0, volume_->value() - 5)); });
    invoke("Full Screen", QKeySequence(Qt::ALT | Qt::Key_Return), [this] { toggle_fullscreen(); }); invoke("Reload Source", QKeySequence(Qt::Key_F12), [this] { reload_source(); }); invoke("Keyboard Help", QKeySequence(Qt::Key_F1), [this] { show_keyboard_help(); }); invoke("Toggle Zoom", QKeySequence(Qt::Key_Z), [this] { timeline_->toggle_zoom(); }); invoke("Zoom Out", QKeySequence(Qt::Key_F7), [this] { timeline_->zoom_out(); }); invoke("Zoom In", QKeySequence(Qt::Key_F8), [this] { timeline_->zoom_in(); }); invoke("Pan Zoom Left", QKeySequence(Qt::Key_Comma), [this] { timeline_->pan_zoom(-1); }); invoke("Pan Zoom Right", QKeySequence(Qt::Key_Period), [this] { timeline_->pan_zoom(1); }); invoke("Thumbnail Interval Up", QKeySequence(Qt::Key_Plus), [this] { adjust_thumbnail_interval(1); }); invoke("Thumbnail Interval Down", QKeySequence(Qt::Key_Minus), [this] { adjust_thumbnail_interval(-1); });
    for (int digit = 0; digit <= 9; ++digit) { invoke(QString("Thumbnail %1").arg(digit), QKeySequence(Qt::Key_0 + digit), [this, digit] { jump_thumbnail_digit(digit); }); invoke(QString("Thumbnail Interval %1").arg(digit), QKeySequence(Qt::CTRL | (Qt::Key_0 + digit)), [this, digit] { set_thumbnail_interval_step(digit); }); }
    connect(input_button, &QPushButton::clicked, this, &ExportWindow::choose_source_media);
    connect(recent_open_, &QPushButton::clicked, this, [this] { const auto recent = QSettings("SmartCut64", "SmartCut64").value("recentFiles").toStringList(); if (recent.isEmpty() || !QFileInfo::exists(recent.front())) { status_->setText("No recent source is available"); return; } request_load_source(recent.front()); });
    connect(output_button, &QPushButton::clicked, this, [this] { choose_output_profile(); });
    connect(output_button, &QPushButton::clicked, this, [this] { if (!output_->text().isEmpty()) history_probe_.start(worker_program(), {"--export-history", "--journal", output_->text() + ".smartcut.journal"}); });
    connect(start_, &QPushButton::clicked, this, [this] { if (!project_path_.isEmpty() && !project_audio_ready_) { status_->setText("Export blocked: project audio metadata is not validated"); return; } if (input_->text().isEmpty()) { status_->setText("Open a source video first"); return; } const auto choice = choose_output_profile(); if (choice == SaveVideoChoice::cancel) return; if (choice == SaveVideoChoice::queue) { queue_current_to_batch_path(output_->text(), current_output_profile()); return; } request_export_plan(); });
    connect(cancel_, &QPushButton::clicked, this, &ExportWindow::cancel_current_operation);
    connect(project_save_, &QPushButton::clicked, this, [this] { if (input_->text().isEmpty() || timeline_->frame_count() == 0) { status_->setText("Choose a source first"); return; } auto path = project_path_; if (path.isEmpty()) path = QFileDialog::getSaveFileName(this, "Save project", default_project_path(), "SmartCut Project (*.smartcut);;VideoReDo Project (*.VPrj);;VideoReDo Batch Project (*.BPrj);;Legacy JSON Project (*.smartcut.json *.json)"); if (path.isEmpty()) return; project_path_ = path; refresh_window_title(); save_project_document(worker_, project_path_, false); });
    connect(project_open_, &QPushButton::clicked, this, [this] { const auto path = QFileDialog::getOpenFileName(this, "Open project", project_default_folder_, "Editor Projects (*.smartcut *.smartcut.json *.VPrj *.vprj *.BPrj *.bprj *.json);;VideoReDo Projects (*.VPrj *.vprj *.BPrj *.bprj);;All Files (*)"); if (!path.isEmpty()) request_open_project(path); });
    connect(play_, &QPushButton::clicked, this, &ExportWindow::toggle_playback);
    playback_timer_.setTimerType(Qt::PreciseTimer);
    connect(&playback_timer_, &QTimer::timeout, this, [this] { if (timeline_->range_invalid()) { stop_playback("Invalid playback range"); return; } if (timeline_->at_playback_end(repeat_selection_playback_)) { if (restart_repeat_selection(false)) { start_video_playback(); return; } stop_playback("Playback complete"); return; } if (hold_next_frame_) { hold_next_frame_ = false; return; } const auto previous = timeline_->current_frame(); timeline_->step_forward(); const auto jumped_cut = timeline_->preview_mode() && timeline_->current_frame() > previous + 1; if (jumped_cut || skip_preview_cut(false)) start_video_playback(); });
    audio_delay_timer_.setSingleShot(true);
    connect(&audio_delay_timer_, &QTimer::timeout, this, [this] {
      if (!playback_requested_ || audio_pause_requested_ ||
          navigation_seek_in_progress_) return;
      playback_timer_.stop();
      launch_audio_playback(0);
    });
    connect(&audio_playback_, &QProcess::readyReadStandardOutput, this, [this] {
      const auto output = audio_playback_.readAllStandardOutput();
      if (!playback_requested_ || audio_pause_requested_ ||
          navigation_seek_in_progress_) return;
      audio_progress_buffer_ += QString::fromUtf8(output);
      int newline = -1;
      while ((newline = audio_progress_buffer_.indexOf('\n')) >= 0) {
        const auto line = audio_progress_buffer_.left(newline).trimmed();
        audio_progress_buffer_.remove(0, newline + 1);
        if (!line.contains("worker-audio-progress-v1")) continue;
        bool ok = false;
        const auto played_us = json_value(line, "played_us").toULongLong(&ok);
        if (ok) {
          ++audio_progress_event_count_;
          last_audio_progress_generation_ = audio_playback_generation_;
          const auto adjusted = adjusted_audio_time_us(played_us, audio_offset_ms_);
          const auto candidate = timeline_->frame_at_time_us(adjusted);
          if (smoke_navigation_seek_observing_ && smoke_navigation_seek_target_) {
            if (audio_playback_generation_ !=
                    smoke_navigation_seek_audio_generation_before_ + 1 ||
                candidate < *smoke_navigation_seek_target_ ||
                (smoke_navigation_seek_last_audio_us_ &&
                 adjusted < *smoke_navigation_seek_last_audio_us_))
              smoke_navigation_seek_regressed_ = true;
            smoke_navigation_seek_last_audio_us_ = adjusted;
          }
          if (timeline_->preview_mode()) {
            const auto cut_end = timeline_->cut_end_for_frame(candidate);
            if (cut_end) {
              if (*cut_end >= timeline_->frame_count()) {
                stop_playback("Playback complete");
                return;
              }
              timeline_->set_frame(*cut_end);
              preview_audio_resume_pending_ = true;
              audio_pause_requested_ = true;
              request_video_stop();
              request_audio_stop();
              return;
            }
          }
          timeline_->set_source_time_us(adjusted);
        }
        if (timeline_->at_playback_end(repeat_selection_playback_)) {
          if (restart_repeat_selection(true)) {
            request_video_stop();
            return;
          }
          audio_pause_requested_ = false;
          request_video_stop();
          audio_playback_.kill();
        }
      }
    });
    connect(&audio_playback_, &QProcess::errorOccurred, this,
            [this](QProcess::ProcessError error) {
      if (error != QProcess::FailedToStart) return;
      if (navigation_seek_resume_pending_) {
        audio_pause_requested_ = false;
        if (playback_requested_ && !navigation_seek_in_progress_)
          QTimer::singleShot(0, this, &ExportWindow::resume_navigation_seek);
        return;
      }
      if (!playback_requested_ || audio_pause_requested_) return;
      status_->setText("Audio worker unavailable; video-only playback");
      if (!playback_timer_.isActive()) start_playback_timer();
      play_->setText("❚❚");
    });
    connect(&audio_playback_, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this,
            [this](int code, QProcess::ExitStatus) {
      QFile::remove(audio_cancel_path_);
      audio_playback_.readAllStandardOutput();
      audio_playback_.readAllStandardError();
      const auto preview_continuation = preview_audio_next_frame_;
      preview_audio_next_frame_.reset();
      if (navigation_seek_resume_pending_) {
        audio_pause_requested_ = false;
        if (!navigation_seek_in_progress_)
          QTimer::singleShot(0, this, &ExportWindow::resume_navigation_seek);
        return;
      }
      if (preview_audio_resume_pending_) {
        preview_audio_resume_pending_ = false;
        audio_pause_requested_ = false;
        schedule_playback_restart();
        return;
      }
      play_->setText("▶");
      if (audio_pause_requested_ || !playback_requested_) {
        audio_pause_requested_ = false;
        return;
      }
      if (preview_continuation) {
        if (*preview_continuation >= timeline_->frame_count()) {
          stop_playback("Playback complete");
          return;
        }
        timeline_->set_frame(*preview_continuation);
        schedule_playback_restart();
        return;
      }
      if (timeline_->at_playback_end(repeat_selection_playback_) || code == 0) {
        stop_playback("Playback complete");
        return;
      }
      status_->setText("Audio device unavailable; video-only playback");
      if (!playback_timer_.isActive()) start_playback_timer();
      play_->setText("❚❚");
    });
    connect(&video_playback_, &QProcess::readyReadStandardOutput, this, [this] {
      video_playback_output_ += QString::fromUtf8(video_playback_.readAllStandardOutput());
      int newline = -1;
      while ((newline = video_playback_output_.indexOf('\n')) >= 0) {
        const auto line = video_playback_output_.left(newline).trimmed();
        video_playback_output_.remove(0, newline + 1);
        const auto document = QJsonDocument::fromJson(line.toUtf8());
        if (!document.isObject()) continue;
        const auto object = document.object();
        if (object.value("schema").toString() != "worker-video-playback-v1" ||
            object.value("event").toString() != "ready") continue;
        const auto mapping = object.value("mapping").toString();
        if (!video_playback_requested_ || active_video_playback_prefix_.isEmpty() ||
            !mapping.startsWith(active_video_playback_prefix_ + '-')) continue;
        const auto frame = smartcut::core::acquire_shared_preview(mapping.toStdString());
        if (!frame) continue;
        const QImage shared(frame->pixels.data(), frame->width, frame->height,
                            frame->stride, QImage::Format_ARGB32);
        const auto image = shared.copy();
        if (image.isNull() || !smartcut::core::acknowledge_shared_preview(
                                  mapping.toStdString(), frame->sequence)) continue;
        if (video_playback_requested_ && playback_frame_visible(frame->frame_index)) {
          if (smoke_navigation_seek_observing_ && smoke_navigation_seek_target_ &&
              frame->frame_index < *smoke_navigation_seek_target_)
            smoke_navigation_seek_regressed_ = true;
          displayed_video_playback_generation_ = video_playback_generation_;
          show_preview_image(QPixmap::fromImage(image), frame->frame_index);
        }
      }
    });
    connect(&video_playback_, &QProcess::errorOccurred, this, [this](QProcess::ProcessError error) {
      if (error == QProcess::FailedToStart && video_playback_requested_) {
        status_->setText("Streaming video worker unavailable; playback is audio-only");
        log_->append("Persistent video playback worker could not be started");
      }
    });
    connect(&video_playback_, qOverload<int, QProcess::ExitStatus>(&QProcess::finished),
            this, [this](int code, QProcess::ExitStatus exit_status) {
      video_playback_output_ += QString::fromUtf8(video_playback_.readAllStandardOutput());
      QFile::remove(video_cancel_path_);
      if (video_playback_requested_ &&
          (code != 0 || exit_status != QProcess::NormalExit)) {
        status_->setText("Streaming preview stopped; audio playback continues");
        const auto error = QString::fromUtf8(video_playback_.readAllStandardError()).trimmed();
        if (!error.isEmpty()) log_->append(error);
      }
    });
    for (auto* process : {&worker_, &project_interchange_, &batch_worker_, &join_worker_, &title_worker_, &metadata_worker_, &qsf_worker_, &multiplex_worker_}) connect(process, &QProcess::started, this, [this, process] { apply_save_priority(*process); });
    connect(&worker_, &QProcess::started, this, [this] { if (project_operation_active_ && export_pause_button_) { export_pause_button_->setEnabled(true); export_progress_timer_.start(200); } });
    connect(&worker_, &QProcess::errorOccurred, this, [this](QProcess::ProcessError error) { if (error != QProcess::FailedToStart) return; finish_export_progress(false); start_->setEnabled(true); cancel_->setEnabled(false); status_->setText(project_operation_active_ ? "Export worker could not be started" : "Project worker could not be started"); });
    connect(&worker_, &QProcess::readyReadStandardOutput, this, [this] { const auto raw = QString::fromUtf8(worker_.readAllStandardOutput()); worker_output_ += raw; const auto text = raw.trimmed(); if (!text.isEmpty()) log_->append(text); if (text.contains("\"event\":\"started\"")) set_export_progress(2); if (text.contains("\"event\":\"completed\"")) set_export_progress(100); });
    connect(&worker_, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [this](int code, QProcess::ExitStatus exit_status) {
      const auto trailing = QString::fromUtf8(worker_.readAllStandardOutput()); if (!trailing.isEmpty()) { worker_output_ += trailing; log_->append(trailing.trimmed()); }
      const bool cancelled = export_cancelled_;
      finish_export_progress(code == 0 && exit_status == QProcess::NormalExit && !cancelled);
      start_->setEnabled(true); cancel_->setEnabled(false);
      if (cancelled) return;
      if (exit_status == QProcess::CrashExit) { QFile::remove(output_->text()); progress_->setValue(0); status_->setText("Worker crashed; project remains intact and partial output was removed"); return; }
      if (project_operation_active_) capture_export_report(worker_output_);
      if (code == 0 && !project_operation_active_) { record_edit_snapshot(); set_project_modified(false); }
      status_->setText(code == 0 ? (project_operation_active_ ? "Export validated; report available in Tools" : "Project saved") : (project_operation_active_ ? "Export failed validation; report available in Tools" : "Project save failed"));
      if (code == 0 && project_operation_active_ && !last_export_report_.isEmpty()) { const auto summary = output_completion_summary(); log_->append(summary); if (chapter_auto_save_) { const auto chapter_path = default_chapter_path(); const bool saved = write_chapter_file(chapter_path, false); log_->append(saved ? QString("Chapter file: %1").arg(QDir::toNativeSeparators(chapter_path)) : QString("Chapter file creation failed: %1").arg(QDir::toNativeSeparators(chapter_path))); } play_completion_alert(false); if (!automation_mode_) QTimer::singleShot(0, this, &ExportWindow::show_output_processing_complete); }
      if (code != 0 && project_operation_active_ && !automation_mode_) QTimer::singleShot(0, this, &ExportWindow::show_output_processing_failed);
    });
    connect(&autosave_worker_, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [this](int code, QProcess::ExitStatus) { if (code != 0) status_->setText("Project autosave failed"); });
    connect(&metadata_worker_, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [this](int code, QProcess::ExitStatus exit_status) { const auto report = QString::fromUtf8(metadata_worker_.readAllStandardOutput()) + QString::fromUtf8(metadata_worker_.readAllStandardError()); if (!report.trimmed().isEmpty()) log_->append(report.trimmed()); const bool passed = code == 0 && exit_status == QProcess::NormalExit && report.contains("\"status\":\"PASSED\"") && QFileInfo::exists(metadata_output_path_); status_->setText(passed ? QString("Metadata copy saved: %1").arg(QDir::toNativeSeparators(metadata_output_path_)) : "Media metadata update failed validation"); if (!passed) QFile::remove(metadata_output_path_); metadata_output_path_.clear(); });
    connect(&project_interchange_, &QProcess::readyReadStandardOutput, this, [this] { const auto text = QString::fromUtf8(project_interchange_.readAllStandardOutput()).trimmed(); if (!text.isEmpty()) log_->append(text); });
    connect(&project_interchange_, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [this](int code, QProcess::ExitStatus exit_status) {
      const auto trailing = QString::fromUtf8(project_interchange_.readAllStandardOutput()).trimmed(); if (!trailing.isEmpty()) log_->append(trailing);
      const auto mode = project_interchange_mode_;
      if (code != 0 || exit_status != QProcess::NormalExit) { project_interchange_mode_.clear(); status_->setText(mode == "import" ? "VideoReDo project import failed" : "VideoReDo project save failed"); return; }
      if (mode == "import") {
        QFile file(pending_vrd_native_path_); if (!file.open(QIODevice::ReadOnly)) { project_interchange_mode_.clear(); status_->setText("Imported VideoReDo project could not be opened"); return; }
        pending_project_path_ = pending_vrd_native_path_; pending_project_json_ = QString::fromUtf8(file.readAll()); native_project_path_ = pending_vrd_native_path_; project_interchange_mode_.clear(); project_validation_.start(worker_program(), {"--validate-project", "--project", pending_project_path_}); status_->setText("Validating imported VideoReDo project…"); return;
      }
      if (mode == "save-native") {
        project_interchange_mode_ = "export";
        project_interchange_.start(worker_program(), {"--export-videoredo-project", "--project", pending_vrd_native_path_, "--output", pending_vrd_project_path_});
        return;
      }
      if (mode == "save-batch-native") {
        project_interchange_mode_ = "save-batch-project";
        project_interchange_.start(worker_program(), {"--export-videoredo-project", "--project", pending_vrd_native_path_, "--output", pending_vrd_intermediate_path_});
        return;
      }
      if (mode == "save-batch-project") {
        project_interchange_mode_ = "save-batch";
        project_interchange_.start(worker_program(), {"--create-videoredo-batch", "--project", pending_vrd_intermediate_path_, "--batch", pending_vrd_project_path_, "--output", pending_vrd_media_output_});
        return;
      }
      if (mode == "export") { project_interchange_mode_.clear(); native_project_path_ = pending_vrd_native_path_; set_project_modified(false); record_edit_snapshot(); status_->setText("VideoReDo project saved"); }
      if (mode == "save-batch") { project_interchange_mode_.clear(); native_project_path_ = pending_vrd_native_path_; set_project_modified(false); record_edit_snapshot(); status_->setText("VideoReDo batch project saved"); }
    });
    connect(&journal_snapshot_, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [this](int, QProcess::ExitStatus) { if (pending_journal_snapshot_) { pending_journal_snapshot_ = false; record_edit_snapshot(); } });
    connect(&worker_, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [this](int code, QProcess::ExitStatus) { if (code == 0 && project_operation_active_) evidence_probe_.start(worker_program(), {"--record-export", "--input", input_->text(), "--output", output_->text(), "--first-frame", QString::number(static_cast<qulonglong>(export_first_frame_)), "--end-frame", QString::number(static_cast<qulonglong>(export_end_frame_)), "--journal", (project_path_.isEmpty() ? output_->text() + ".smartcut.journal" : project_path_ + ".journal")}); });
    connect(&evidence_probe_, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [this](int code, QProcess::ExitStatus) { status_->setText(code == 0 ? "Export evidence recorded" : "Export evidence failed"); });
    connect(&history_probe_, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [this](int, QProcess::ExitStatus) { const auto history = QString::fromUtf8(history_probe_.readAllStandardOutput()); if (!history.isEmpty() && inspector_) { const auto entries = history.count("\"op\":\"export_evidence\""); const auto passed = history.count("\"status\":\"PASSED\""); const auto audio = history.count("\"op\":\"audio_validation\""); const auto audio_passed = history.lastIndexOf("\"op\":\"audio_validation\"") >= 0 && history.lastIndexOf("\"status\":\"PASSED\"") > history.lastIndexOf("\"op\":\"audio_validation\""); inspector_->setText(inspector_->toPlainText() + QString("\nExport history: %1 entries\nValidated: %2\nAudio preflights: %3\nLatest audio preflight: %4").arg(entries).arg(passed).arg(audio).arg(audio_passed ? "PASSED" : (audio ? "FAILED" : "NONE"))); } });
    connect(&project_validation_, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [this](int code, QProcess::ExitStatus) {
      if (code != 0) { status_->setText("Invalid SmartCut project document"); return; }
      const auto document = QJsonDocument::fromJson(pending_project_json_.toUtf8()); if (!document.isObject()) { status_->setText("Invalid SmartCut project document"); return; }
      const auto root = document.object(); const auto sources = root.value("sources").toArray(); if (sources.isEmpty()) { status_->setText("Project source is missing"); return; }
      const auto source = sources.at(0).toObject(); const auto source_path = source.value("canonical_path").toString(); if (!QFileInfo::exists(source_path)) { status_->setText(QString("Project source is unavailable: %1").arg(source_path)); return; }
      std::vector<FrameRange> kept; for (const auto value : root.value("segments").toArray()) { const auto segment = value.toObject(); kept.emplace_back(static_cast<std::size_t>(segment.value("first_display_frame").toDouble()), static_cast<std::size_t>(segment.value("end_display_frame_exclusive").toDouble())); }
      QVector<int> streams; for (const auto value : source.value("selected_streams").toArray()) streams.push_back(value.toInt()); std::vector<std::size_t> chapters; for (const auto value : root.value("chapter_markers").toArray()) chapters.push_back(static_cast<std::size_t>(value.toDouble()));
      project_path_ = pending_vrd_project_path_.isEmpty() ? pending_project_path_ : pending_vrd_project_path_; if (pending_vrd_project_path_.isEmpty()) native_project_path_.clear(); else native_project_path_ = pending_project_path_; pending_project_kept_ = std::move(kept); pending_project_streams_ = std::move(streams); pending_project_chapters_ = std::move(chapters); restoring_project_ = true; set_scene_mode(root.value("scene_mode").toBool(false), false); audio_offset_ms_ = root.value("audio_offset_ms").toInt(0);
      const auto stored_journal = root.value("journal_path").toString(); const auto journal_path = stored_journal.isEmpty() ? project_path_ + ".journal" : stored_journal; restore_latest_edit_snapshot(journal_path, source.value("source_uuid").toString());
      input_->setText(source_path); load_source(source_path); inspector_->setPlainText("Loading typed project summary…"); history_probe_.start(worker_program(), {"--export-history", "--journal", journal_path}); status_->setText("Restoring project source and edits…");
    });
    connect(&source_probe_, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [this](int code, QProcess::ExitStatus) { indexing_status_timer_.stop(); progress_->setRange(0, 100); progress_->setValue(0); const auto report = parse_navigation_report(source_probe_.readAllStandardOutput()); if (code != 0 || !report) { restoring_project_ = false; status_->setText("Source navigation probe failed"); return; } source_uuid_ = report->source_uuid; QString seed = report->source_uuid + ':' + QString::number(static_cast<qulonglong>(report->frame_count)); for (const auto frame : report->keyframes) seed += ':' + QString::number(static_cast<qulonglong>(frame)); index_fingerprint_ = QString::number(qHash(seed), 16); timeline_->set_navigation(report->frame_count, report->keyframes, report->frame_pts, report->time_base_num, report->time_base_den); apply_pending_chapters(); fine_tune_->setEnabled(report->frame_count > 0); program_end_->setText(frame_timecode(report->frame_count)); selection_start_->setText("00:00:00.00"); frame_readout_->setText("0"); frame_guide_->set_position(0, report->frame_count); if (pending_project_kept_) { auto kept = *pending_project_kept_; std::sort(kept.begin(), kept.end()); std::vector<FrameRange> cuts; std::size_t cursor = 0; for (const auto& range : kept) { const auto first = std::min(range.first, report->frame_count); const auto end = std::min(range.second, report->frame_count); if (cursor < first) cuts.emplace_back(cursor, first); cursor = std::max(cursor, end); } if (cursor < report->frame_count) cuts.emplace_back(cursor, report->frame_count); set_cut_ranges(std::move(cuts)); pending_project_kept_.reset(); } else set_cut_ranges(scene_mode_ ? std::vector<FrameRange>{{0, report->frame_count}} : std::vector<FrameRange>{}); if (pending_project_chapters_) { timeline_->set_chapter_markers(*pending_project_chapters_); pending_project_chapters_.reset(); project_chapters_loaded_ = true; } restoring_project_ = false; undo_states_.clear(); redo_states_.clear(); const bool recovered_snapshot = journal_recovery_applied_; const auto loaded = recovered_snapshot ? "Recovered latest journal snapshot; loaded" : "Loaded"; journal_recovery_applied_ = false; set_project_modified(pending_modified_after_load_.value_or(recovered_snapshot)); pending_modified_after_load_.reset(); status_->setText(QString("%1 %2 frames, %3 keyframes, %4 chapters").arg(loaded).arg(static_cast<qulonglong>(report->frame_count)).arg(static_cast<qulonglong>(report->keyframes.size())).arg(static_cast<qulonglong>(timeline_->chapter_markers().size()))); if (report->frame_count > 0) request_preview(0); });
    connect(&source_probe_, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [this] { control_navigation_pending_ = false; });
    connect(&identity_probe_, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [this](int, QProcess::ExitStatus) { const auto identity = clean_json_value(json_value(QString::fromUtf8(identity_probe_.readAllStandardOutput()), "source_uuid")); if (!identity.isEmpty()) inspector_->setPlainText(QString("Inspector\n\nSource UUID: %1\nCache: %2/8 frames\nPreview: worker-decoded").arg(identity).arg(preview_cache_.size())); });
    connect(&metadata_probe_, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [this](int code, QProcess::ExitStatus) {
      const auto bytes = metadata_probe_.readAllStandardOutput();
      const auto result = QString::fromUtf8(bytes);
      if (code != 0) { inspector_->append("Metadata probe: FAILED"); return; }
      QJsonParseError error;
      const auto document = QJsonDocument::fromJson(bytes, &error);
      if (error.error == QJsonParseError::NoError && document.isObject()) {
        source_streams_ = document.object().value("streams").toArray(); source_metadata_ = document.object().value("metadata").toArray(); selected_streams_.clear(); for (const auto& value : source_streams_) selected_streams_.push_back(value.toObject().value("index").toInt()); if (pending_project_streams_) { QVector<int> restored; for (const auto stream : *pending_project_streams_) if (selected_streams_.contains(stream)) restored.push_back(stream); if (!restored.isEmpty()) selected_streams_ = std::move(restored); pending_project_streams_.reset(); }
        pending_chapter_times_us_.clear();
        for (const auto value : document.object().value("chapters").toArray()) {
          const auto chapter = value.toObject();
          const auto start = static_cast<long double>(chapter.value("start").toDouble());
          const auto num = static_cast<long double>(chapter.value("time_base_num").toDouble(1.0));
          const auto den = static_cast<long double>(chapter.value("time_base_den").toDouble(1.0));
          if (start >= 0.0L && num > 0.0L && den > 0.0L)
            pending_chapter_times_us_.push_back(static_cast<std::uint64_t>(start * num * 1000000.0L / den));
        }
        if (!project_chapters_loaded_) apply_pending_chapters();
        for (const auto value : document.object().value("streams").toArray()) {
          const auto stream = value.toObject();
          if (stream.value("type").toInt(-1) != 0) continue;
          const auto width = stream.value("width").toInt();
          const auto height = stream.value("height").toInt();
          const auto sar_num = stream.value("sar_num").toInt(1);
          const auto sar_den = stream.value("sar_den").toInt(1);
          frame_rate_num_ = std::max(stream.value("frame_rate_num").toInt(25), 1);
          frame_rate_den_ = std::max(stream.value("frame_rate_den").toInt(1), 1);
          if (width > 0 && height > 0 && sar_num > 0 && sar_den > 0) {
            display_aspect_ratio_ = static_cast<double>(width) * static_cast<double>(sar_num) /
                                    (static_cast<double>(height) * static_cast<double>(sar_den));
            for (auto* thumbnail : thumbnail_widgets_) thumbnail->set_display_aspect_ratio(display_aspect_ratio_);
            update_thumbnail_widget_count();
            if (!current_preview_image_.isNull()) show_preview_image(current_preview_image_, displayed_preview_frame_);
          }
          refresh_cut_list();
          break;
        }
      }
      const auto pixel = clean_json_value(json_value(result, "pixel_format"));
      const auto primaries = clean_json_value(json_value(result, "color_primaries"));
      const auto transfer = clean_json_value(json_value(result, "color_transfer"));
      const auto hdr10_plus = clean_json_value(json_value(result, "hdr10_plus_present"));
      const auto dovi_profile = clean_json_value(json_value(result, "dovi_profile"));
      inspector_->append(QString("\nVideo pixel format: %1\nColour primaries/transfer: %2/%3\nHDR10+ metadata: %4\nDolby Vision profile: %5").arg(pixel, primaries, transfer, hdr10_plus, dovi_profile));
    });
    connect(&preview_probe_, &QProcess::readyReadStandardOutput, this, [this] { preview_shared_output_ += QString::fromUtf8(preview_probe_.readAllStandardOutput()); int newline = -1; while ((newline = preview_shared_output_.indexOf('\n')) >= 0) { const auto line = preview_shared_output_.left(newline).trimmed(); preview_shared_output_.remove(0, newline + 1); if (!line.contains("\"schema\":\"worker-preview-shared-v1\"") || !line.contains("\"event\":\"ready\"")) continue; const auto frame = smartcut::core::acquire_shared_preview(active_preview_mapping_.toStdString()); if (!frame) continue; const QImage shared(frame->pixels.data(), frame->width, frame->height, frame->stride, QImage::Format_ARGB32); const auto image = shared.copy(); if (image.isNull() || !smartcut::core::acknowledge_shared_preview(active_preview_mapping_.toStdString(), frame->sequence)) continue; active_preview_received_ = image.save(active_preview_path_); if (active_preview_received_ && active_preview_generation_ == preview_generation_ && active_preview_frame_ == current_preview_frame_) show_preview_image(QPixmap::fromImage(image), active_preview_frame_); } });
    connect(&preview_probe_, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [this](int code, QProcess::ExitStatus) { preview_shared_output_ += QString::fromUtf8(preview_probe_.readAllStandardOutput()); const bool valid = code == 0 && active_preview_received_ && QFileInfo::exists(active_preview_path_); if (valid) { preview_cache_.insert(active_preview_key_, active_preview_path_); cache_order_.enqueue(active_preview_key_); while (cache_order_.size() > 8) { const auto expired = cache_order_.dequeue(); const auto path = preview_cache_.take(expired); QFile::remove(path); } } else { QFile::remove(active_preview_path_); if (!video_playback_requested_ && active_preview_generation_ == preview_generation_ && active_preview_frame_ == current_preview_frame_ && !pending_preview_frame_) { preview_->setText("Preview decode failed"); const auto detail = QString::fromUtf8(preview_probe_.readAllStandardError()).trimmed(); if (!detail.isEmpty()) log_->append(detail); } } QTimer::singleShot(0, this, [this] { start_pending_preview(); }); });
    connect(&thumbnail_probe_, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [this](int code, QProcess::ExitStatus) {
      const auto visible = std::clamp<qsizetype>(visible_thumbnail_count_, 1, thumbnail_widgets_.size());
      if (code == 0 && active_thumbnail_generation_ == preview_generation_ &&
          active_thumbnail_center_ == current_preview_frame_ && active_thumbnail_slots_.size() == visible) {
        for (qsizetype slot = 0; slot < visible; ++slot) {
          if (!active_thumbnail_slots_[slot]) thumbnail_widgets_[slot]->set_placeholder();
        }
        for (qsizetype request = 0; request < active_thumbnail_request_slots_.size(); ++request) {
          const auto slot = active_thumbnail_request_slots_[request];
          if (slot < 0 || slot >= visible || !active_thumbnail_slots_[slot]) continue;
          const auto frame = *active_thumbnail_slots_[slot];
          const auto path = active_thumbnail_directory_ + '/' + QString::number(request) + ".ppm";
          const QPixmap image(path);
          if (!image.isNull()) thumbnail_widgets_[slot]->set_frame(frame, image, frame_timecode(frame));
        }
        for (qsizetype slot = 0; slot < visible; ++slot) {
          thumbnail_widgets_[slot]->set_selected(active_thumbnail_slots_[slot] &&
                                                  *active_thumbnail_slots_[slot] == current_preview_frame_);
        }
      }
      if (pending_thumbnail_center_ && !thumbnail_refresh_timer_.isActive())
        QTimer::singleShot(0, this, [this] { start_pending_thumbnail_strip(); });
    });
    connect(&waveform_probe_, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [this](int code, QProcess::ExitStatus) {
      const auto bytes = waveform_probe_.readAllStandardOutput();
      if (code == 0 && active_waveform_generation_ == preview_generation_) {
        QJsonParseError error;
        const auto document = QJsonDocument::fromJson(bytes, &error);
        if (error.error == QJsonParseError::NoError && document.isObject()) {
          QVector<float> peaks;
          const auto values = document.object().value("peaks").toArray();
          peaks.reserve(values.size());
          for (const auto& value : values) peaks.push_back(static_cast<float>(value.toDouble()));
          frame_guide_->set_waveform(std::move(peaks));
        }
      }
      QTimer::singleShot(0, this, [this] { start_pending_waveform(); });
    });
    connect(&ad_scan_, &QProcess::readyReadStandardOutput, this, [this] {
      ad_scan_output_ += QString::fromUtf8(ad_scan_.readAllStandardOutput());
      const auto progress_position = ad_scan_output_.lastIndexOf("worker-ad-progress-v2");
      if (progress_position >= 0) {
        const auto progress_line = ad_scan_output_.mid(progress_position).section('\n', 0, 0);
        const auto frame = json_value(progress_line, "frame");
        if (!frame.isEmpty()) { status_->setText(QString("Ad-Detective scanning frame %1…").arg(frame)); progress_->setValue(std::min(frame.toInt(), progress_->maximum())); inspector_->setPlainText(QString("Ad-Detective live frame statistics\n\nFrame: %1\nPicture: %2\nAverage: %3\nPeak: %4\nContrast: %5\nScene score: %6\nBlack frame: %7").arg(frame, json_value(progress_line, "picture_type"), json_value(progress_line, "average"), json_value(progress_line, "peak"), json_value(progress_line, "contrast"), json_value(progress_line, "scene_score"), progress_line.contains("\"black\":true") ? "yes" : "no")); if (ad_display_during_scan_) { bool ok = false; const auto scan_frame = frame.toULongLong(&ok); if (ok && scan_frame < timeline_->frame_count()) timeline_->set_frame(static_cast<std::size_t>(scan_frame)); } }
      }
    });
    connect(&ad_scan_, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [this](int code, QProcess::ExitStatus) {
      ad_scan_output_ += QString::fromUtf8(ad_scan_.readAllStandardOutput());
      progress_->setRange(0, 100);
      if (ad_scan_cancel_requested_) { ad_scan_cancel_requested_ = false; progress_->setValue(0); status_->setText("Ad-Detective scan stopped"); if (!ad_display_during_scan_) request_preview(timeline_->current_frame()); return; }
      QJsonObject result;
      const auto lines = ad_scan_output_.split('\n', Qt::SkipEmptyParts);
      for (auto line = lines.crbegin(); line != lines.crend(); ++line) {
        if (!line->contains("\"schema\":\"ad-detective-v2\"")) continue;
        const auto document = QJsonDocument::fromJson(line->toUtf8());
        if (document.isObject()) result = document.object();
        break;
      }
      if (code != 0 || result.value("status").toString() != "PASSED") { progress_->setValue(0); status_->setText("Ad-Detective scan failed"); if (!ad_display_during_scan_) request_preview(timeline_->current_frame()); return; }
      auto markers = timeline_->chapter_markers();
      const auto marker_values = result.value("markers").toArray(); const auto detected = review_ad_markers(marker_values);
      for (const auto frame : detected) markers.push_back(frame);
      if (!detected.empty()) set_project_modified(true);
      timeline_->set_chapter_markers(std::move(markers));
      const auto automatic_cuts = apply_ad_detected_cuts(detected);
      if (!project_path_.isEmpty()) { record_edit_snapshot(); project_autosave_timer_.start(700); }
      progress_->setValue(100);
      status_->setText(QString("Ad-Detective found %1 transitions; %2 chapter markers total; %3 automatic cuts")
                           .arg(static_cast<qulonglong>(detected.size()))
                           .arg(static_cast<qulonglong>(timeline_->chapter_markers().size()))
                            .arg(static_cast<qulonglong>(automatic_cuts)));
      if (!ad_display_during_scan_) request_preview(timeline_->current_frame());
    });
    connect(&join_worker_, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [this](int code, QProcess::ExitStatus) {
      const auto report = QString::fromUtf8(join_worker_.readAllStandardOutput()).trimmed();
      if (!report.isEmpty()) log_->append(report);
      if (combined_open_pending_) { combined_open_pending_ = false; const auto output = pending_combined_output_; if (code == 0 && report.contains("\"status\":\"PASSED\"") && QFileInfo::exists(output)) { input_->setText(output); load_source(output); status_->setText(QString("Opening combined program from %1 files…").arg(combined_open_source_count_)); } else { QFile::remove(output); status_->setText("Combine failed compatibility or output validation"); } return; }
      status_->setText(code == 0 ? QString("Joined video saved: %1").arg(joiner_output_path_) : QString("Joiner failed: %1").arg(clean_json_value(json_value(report, "error"))));
    });
    connect(&qsf_worker_, &QProcess::readyReadStandardOutput, this, [this] { const auto text = QString::fromUtf8(qsf_worker_.readAllStandardOutput()); qsf_output_ += text; if (!text.trimmed().isEmpty()) log_->append(text.trimmed()); });
    connect(&qsf_worker_, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [this](int code, QProcess::ExitStatus exit_status) {
      qsf_output_ += QString::fromUtf8(qsf_worker_.readAllStandardOutput()); progress_->setRange(0, 100); progress_->setValue(code == 0 ? 100 : 0);
      if (exit_status == QProcess::CrashExit || code != 0 || !qsf_output_.contains("\"status\":\"PASSED\"")) { QFile::remove(qsf_output_path_); status_->setText("QuickStream Fix failed validation; partial output removed"); return; }
      status_->setText(QString("QuickStream Fix completed and validated: %1").arg(qsf_output_path_));
    });
    connect(&multiplex_worker_, &QProcess::readyReadStandardOutput, this, [this] { const auto text = QString::fromUtf8(multiplex_worker_.readAllStandardOutput()); multiplex_output_ += text; if (!text.trimmed().isEmpty()) log_->append(text.trimmed()); });
    connect(&multiplex_worker_, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [this](int code, QProcess::ExitStatus exit_status) {
      multiplex_output_ += QString::fromUtf8(multiplex_worker_.readAllStandardOutput()); progress_->setRange(0, 100); const bool passed = code == 0 && exit_status == QProcess::NormalExit && multiplex_output_.contains("\"status\":\"PASSED\"") && QFileInfo::exists(multiplex_output_path_); progress_->setValue(passed ? 100 : 0); if (!passed) QFile::remove(multiplex_output_path_); status_->setText(passed ? QString("Elementary streams multiplexed and validated: %1").arg(multiplex_output_path_) : "Elementary stream multiplex failed validation; partial output removed");
    });
    connect(&trim_copy_worker_, &QProcess::readyReadStandardOutput, this, [this] { trim_copy_output_ += QString::fromUtf8(trim_copy_worker_.readAllStandardOutput()); });
    connect(&trim_copy_worker_, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [this](int code, QProcess::ExitStatus exit_status) {
      trim_copy_output_ += QString::fromUtf8(trim_copy_worker_.readAllStandardOutput()); const bool passed = code == 0 && exit_status == QProcess::NormalExit && trim_copy_output_.contains("\"status\":\"PASSED\"") && QFileInfo::exists(trim_copy_output_path_); progress_->setValue(passed ? 100 : 0); if (!trim_copy_output_.trimmed().isEmpty()) log_->append(trim_copy_output_.trimmed()); status_->setText(passed ? QString("Trimmed file copy completed: %1").arg(trim_copy_output_path_) : "Trimmed file copy failed; temporary output removed");
    });
    connect(&join_item_worker_, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [this](int code, QProcess::ExitStatus) { const auto report = QString::fromUtf8(join_item_worker_.readAllStandardOutput()) + QString::fromUtf8(join_item_worker_.readAllStandardError()); const bool passed = code == 0 && report.contains("\"status\":\"PASSED\"") && QFileInfo::exists(pending_joiner_item_path_); if (passed) { joiner_inputs_.push_back(pending_joiner_item_path_); joiner_descriptions_.push_back(pending_joiner_item_description_); joiner_title_specs_.push_back({}); joiner_durations_us_.push_back(pending_joiner_duration_us_); status_->setText(QString("Edited project added to Joiner (%1 items)").arg(joiner_inputs_.size())); } else { QFile::remove(pending_joiner_item_path_); status_->setText("Could not prepare the edited project for the Joiner"); } pending_joiner_item_path_.clear(); pending_joiner_item_description_.clear(); pending_joiner_duration_us_ = 0; });
    connect(&title_worker_, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [this](int code, QProcess::ExitStatus exit_status) { const auto report = QString::fromUtf8(title_worker_.readAllStandardOutput()) + QString::fromUtf8(title_worker_.readAllStandardError()); const bool passed = code == 0 && exit_status == QProcess::NormalExit && report.contains("\"status\":\"PASSED\"") && QFileInfo::exists(pending_title_output_); if (!report.trimmed().isEmpty()) log_->append(report.trimmed()); if (passed) { const auto duration_us = static_cast<qulonglong>(std::max(0.0, title_duration_seconds_) * 1'000'000.0); if (pending_title_replace_index_ >= 0 && pending_title_replace_index_ < joiner_inputs_.size()) { const auto old_media = joiner_inputs_[pending_title_replace_index_]; const auto old_spec = pending_title_replace_index_ < joiner_title_specs_.size() ? joiner_title_specs_[pending_title_replace_index_] : QString{}; joiner_inputs_[pending_title_replace_index_] = pending_title_output_; joiner_descriptions_[pending_title_replace_index_] = pending_title_description_; while (joiner_title_specs_.size() < joiner_inputs_.size()) joiner_title_specs_.push_back({}); while (joiner_durations_us_.size() < joiner_inputs_.size()) joiner_durations_us_.push_back(0); joiner_title_specs_[pending_title_replace_index_] = pending_title_spec_; joiner_durations_us_[pending_title_replace_index_] = duration_us; if (old_media != pending_title_output_) QFile::remove(old_media); if (!old_spec.isEmpty() && old_spec != pending_title_spec_) QFile::remove(old_spec); status_->setText(QString("Joiner title updated: %1").arg(pending_title_description_)); } else { joiner_inputs_.push_back(pending_title_output_); joiner_descriptions_.push_back(pending_title_description_); joiner_title_specs_.push_back(pending_title_spec_); joiner_durations_us_.push_back(duration_us); status_->setText(QString("Title added to Joiner (%1 items)").arg(joiner_inputs_.size())); } } else { QFile::remove(pending_title_output_); QFile::remove(pending_title_spec_); status_->setText("Title creation failed; no Joiner item was added"); } QFile::remove(pending_title_bgra_); pending_title_output_.clear(); pending_title_bgra_.clear(); pending_title_description_.clear(); pending_title_spec_.clear(); pending_title_replace_index_ = -1; });
    connect(&batch_worker_, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [this](int code, QProcess::ExitStatus exit_status) {
      if (batch_active_index_ >= batch_jobs_.size()) { batch_running_ = false; return; }
      auto& job = batch_jobs_[batch_active_index_]; const auto report = QString::fromUtf8(batch_worker_.readAllStandardOutput()); if (!report.trimmed().isEmpty()) log_->append(report.trimmed());
      const bool passed = code == 0 && exit_status == QProcess::NormalExit && report.contains("\"status\":\"PASSED\""); const bool quick_stream_substituted = report.contains("\"substitution_reason\":\"evaluation_watermark_requires_reencode\""); job.status = passed ? (quick_stream_substituted ? "Completed (watermarked transcode substituted for QuickStream Fix)" : "Completed") : "Failed validation"; if (quick_stream_substituted) status_->setText("Evaluation mode used a watermarked full transcode because QuickStream Fix cannot insert a watermark without decoding video"); if (passed && !job.watch_key.isEmpty()) { watch_history_.insert(job.watch_key); save_watch_history(); } save_batch_queue(); if (!passed) QFile::remove(job.output); else if (job.operation != "ad-scan" && !batch_silent_) play_completion_alert(true);
      ++batch_active_index_; run_next_batch_job();
    });
    connect(&sync_probe_, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [this](int, QProcess::ExitStatus) { const auto text = QString::fromUtf8(sync_probe_.readAllStandardOutput()); const auto drift = json_value(text, "drift_ticks"); const auto action = json_value(text, "action"); hold_next_frame_ = action == "hold_video"; if (!drift.isEmpty()) inspector_->setText(inspector_->toPlainText() + QString("\nSync drift: %1\nSync action: %2").arg(drift, action)); });
    connect(&project_validation_, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [this](int code, QProcess::ExitStatus) { if (code == 0) audio_validation_.start(worker_program(), {"--validate-project-audio", "--project", pending_project_path_}); });
    connect(&project_validation_, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [this](int code, QProcess::ExitStatus) { if (code == 0) project_summary_.start(worker_program(), {"--project-summary", "--project", pending_project_path_}); });
    connect(&project_summary_, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [this](int code, QProcess::ExitStatus) { const auto result = QString::fromUtf8(project_summary_.readAllStandardOutput()).trimmed(); if (!result.isEmpty()) log_->append(result); if (code == 0 && inspector_) { const auto persisted = json_value(result, "audio_validation_status"); const auto stream = json_value(result, "audio_stream"); const auto sample_rate = json_value(result, "audio_sample_rate"); const auto channels = json_value(result, "audio_channels"); inspector_->setPlainText(QString("Project summary\n\nPersisted audio validation: %1\nAudio stream: %2\nSample rate: %3 Hz\nChannels: %4\nSource: worker-project-summary-v1").arg(persisted.isEmpty() ? "NONE" : persisted, stream, sample_rate, channels)); } });
    connect(&audio_validation_, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [this](int code, QProcess::ExitStatus) { project_audio_ready_ = code == 0; const auto result = QString::fromUtf8(audio_validation_.readAllStandardOutput()).trimmed(); if (!result.isEmpty()) log_->append(result); const auto status = project_audio_ready_ ? "PASSED" : "FAILED"; preflight_record_.start(worker_program(), {"--record-audio-validation", "--journal", project_path_ + ".journal", "--status", status}); const auto typed_project = native_project_path_.isEmpty() ? project_path_ : native_project_path_; project_status_update_.start(worker_program(), {"--update-project-audio-validation", "--project", typed_project, "--status", status}); status_->setText(project_audio_ready_ ? "Project audio validated" : "Project audio metadata invalid"); });
    connect(&project_validation_, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [this](int code, QProcess::ExitStatus) { if (code != 0 || !inspector_) return; const auto json = pending_project_json_; const auto audio_stream = json_value(json, "audio_stream"); const auto audio_num = json_value(json, "audio_time_base_num"); const auto audio_den = json_value(json, "audio_time_base_den"); const auto sample_rate = json_value(json, "audio_sample_rate"); const auto channels = json_value(json, "audio_channels"); const auto stored_status = json_value(json, "audio_validation_status"); const bool ready = !audio_stream.isEmpty() && audio_stream != "18446744073709551615" && !audio_num.isEmpty() && !audio_den.isEmpty(); inspector_->setPlainText(QString("Project inspector\n\nPersisted audio validation: %1\nAudio policy: %2\nAudio stream: %3\nTime base: %4/%5\nSample rate: %6 Hz\nChannels: %7").arg(stored_status.isEmpty() ? "NONE" : stored_status, ready ? "copy-complete-source-audio-packets" : "blocked-missing-audio-metadata", audio_stream, audio_num, audio_den, sample_rate, channels)); });
    connect(&plan_probe_, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [this](int code, QProcess::ExitStatus) { const auto plan = QString::fromUtf8(plan_probe_.readAllStandardOutput()).trimmed(); if (code != 0 || !plan.contains("\"schema\":\"render-plan-v1\"")) { start_->setEnabled(true); cancel_->setEnabled(false); status_->setText("Export planning failed"); return; } pending_plan_ = plan; progress_->setValue(5); QMessageBox review(QMessageBox::Information, "Export plan", "Review the copy/encode and metadata policy before export.", QMessageBox::Yes | QMessageBox::No, this); review.setInformativeText(QString("Frames %1 to %2\nAudio: complete overlapping codec frames\nHDR/Dolby: preserve physical per-frame metadata").arg(static_cast<qulonglong>(export_first_frame_)).arg(static_cast<qulonglong>(export_end_frame_))); review.setDetailedText(plan); review.setDefaultButton(QMessageBox::Yes); if (review.exec() == QMessageBox::Yes) begin_export(); else { start_->setEnabled(true); cancel_->setEnabled(false); progress_->setValue(0); status_->setText("Export cancelled at plan review"); } });
    refresh_window_title();
    license_.refresh_online_async(this, [this](bool updated, const QString& message) {
      if (updated) refresh_window_title();
      if (!message.isEmpty()) status_->setText(message);
    });
    set_scene_mode(ad_settings.value("editing/sceneMode", false).toBool(), false);
  }
 private:
  struct EditState { std::optional<std::size_t> in_mark; std::optional<std::size_t> out_mark; std::vector<FrameRange> cuts; };
  EditState edit_state() const { return {timeline_->in_mark(), timeline_->out_mark(), cut_ranges_}; }
  void refresh_window_title() {
    QString subject = input_ && !input_->text().isEmpty() ? QFileInfo(input_->text()).fileName() : QStringLiteral("Video Editor");
    if ((!input_ || input_->text().isEmpty()) && !project_path_.isEmpty()) subject = QFileInfo(project_path_).completeBaseName();
    setWindowTitle(QString("SmartCut64 - %1%2%3").arg(subject).arg(project_modified_ ? "*" : "").arg(license_.edition_suffix()));
  }
  bool require_paid_delivery_tool(const QString& feature) {
    license_.reload();
    if (license_.paid()) return true;
    QMessageBox::information(this, "Paid Licence Required",
      QString("%1 creates a clean copy without decoding every video frame, so an evaluation watermark cannot be inserted safely. This delivery tool requires a paid licence.\n\nUse Save Video for a fully functional evaluation export with a permanent watermark, or install a licence under Help > Licence & Activation.").arg(feature));
    status_->setText(feature + " requires a paid licence for clean output");
    refresh_window_title();
    return false;
  }
  void set_project_modified(bool modified) { project_modified_ = modified; refresh_window_title(); }
  bool save_project_for_lifecycle() {
    if (input_->text().isEmpty() || timeline_->frame_count() == 0) return false;
    auto path = project_path_;
    if (path.isEmpty()) path = QFileDialog::getSaveFileName(this, "Save project", default_project_path(), "SmartCut Project (*.smartcut);;VideoReDo Project (*.VPrj);;VideoReDo Batch Project (*.BPrj);;Legacy JSON Project (*.smartcut.json *.json)");
    if (path.isEmpty()) return false;
    project_autosave_timer_.stop();
    if (autosave_worker_.state() != QProcess::NotRunning && !autosave_worker_.waitForFinished(5000)) return false;
    QProcess saver; const bool video_redo = path.endsWith(".vprj", Qt::CaseInsensitive); const bool video_redo_batch = path.endsWith(".bprj", Qt::CaseInsensitive); const auto typed_path = video_redo || video_redo_batch ? preview_dir_.path() + "/videoredo-lifecycle.smartcut" : path; QFile::remove(typed_path); saver.start(worker_program(), edit_project_arguments(typed_path));
    bool saved = saver.waitForStarted(5000) && saver.waitForFinished(30000) && saver.exitStatus() == QProcess::NormalExit && saver.exitCode() == 0 && QFileInfo::exists(typed_path);
    if (saved && video_redo) { QProcess exporter; exporter.start(worker_program(), {"--export-videoredo-project", "--project", typed_path, "--output", path}); saved = exporter.waitForStarted(5000) && exporter.waitForFinished(30000) && exporter.exitStatus() == QProcess::NormalExit && exporter.exitCode() == 0 && QFileInfo::exists(path); if (saved) native_project_path_ = typed_path; }
    if (saved && video_redo_batch) { const auto intermediate = preview_dir_.path() + "/videoredo-lifecycle.VPrj"; QProcess exporter; exporter.start(worker_program(), {"--export-videoredo-project", "--project", typed_path, "--output", intermediate}); saved = exporter.waitForStarted(5000) && exporter.waitForFinished(30000) && exporter.exitStatus() == QProcess::NormalExit && exporter.exitCode() == 0 && QFileInfo::exists(intermediate); if (saved) { const auto media_output = output_->text().isEmpty() ? default_output_path() : output_->text(); QProcess creator; creator.start(worker_program(), {"--create-videoredo-batch", "--project", intermediate, "--batch", path, "--output", media_output}); saved = creator.waitForStarted(5000) && creator.waitForFinished(30000) && creator.exitStatus() == QProcess::NormalExit && creator.exitCode() == 0 && QFileInfo::exists(path); } if (saved) native_project_path_ = typed_path; }
    if (!saved) {
      status_->setText("Project save failed; the current source remains open");
      return false;
    }
    project_path_ = path; set_project_modified(false); status_->setText("Project saved"); return true;
  }
  void save_project_as() {
    if (input_->text().isEmpty() || timeline_->frame_count() == 0) { status_->setText("Choose a source before saving a project"); return; }
    const auto path = QFileDialog::getSaveFileName(this, "Save Project As", project_path_.isEmpty() ? default_project_path() : project_path_, "SmartCut Project (*.smartcut);;VideoReDo Project (*.VPrj);;VideoReDo Batch Project (*.BPrj);;Legacy JSON Project (*.smartcut.json *.json)");
    if (path.isEmpty()) return;
    project_path_ = path; refresh_window_title(); save_project_document(worker_, project_path_, false);
  }
  bool confirm_abandon_changes(const QString& operation) {
    if (!project_modified_) return true;
    QMessageBox::StandardButton choice{};
    if (lifecycle_prompt_override_) choice = *lifecycle_prompt_override_;
    else choice = QMessageBox::warning(this, "Unsaved Project Changes", QString("Save the changes before you %1?").arg(operation), QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Save);
    if (choice == QMessageBox::Cancel) return false;
    if (choice == QMessageBox::Save) return save_project_for_lifecycle();
    return true;
  }
  void choose_source_media() {
    const auto paths = QFileDialog::getOpenFileNames(this, "Open Video", {}, "Video Files (*.mkv *.mp4 *.mov *.ts *.m2ts *.mpg *.mpeg *.vob *.avi *.webm);;All Files (*)");
    if (!paths.isEmpty()) handle_open_files(paths);
  }
  void show_editor_workspace() {
    if (!editor_central_ || centralWidget() == editor_central_) return;
    if (auto* current = takeCentralWidget()) { current->setParent(this); current->hide(); }
    setCentralWidget(editor_central_);
    editor_central_->show();
    refresh_window_title();
    if (editor_was_maximized_) showMaximized();
    else { showNormal(); resize(editor_window_size_); }
  }
  void show_startup_screen() {
    if (automation_mode_ || !startup_splash_ || centralWidget() == startup_splash_) return;
    editor_was_maximized_ = isMaximized();
    if (!editor_was_maximized_) editor_window_size_ = size();
    if (auto* current = takeCentralWidget()) { current->setParent(this); current->hide(); }
    setCentralWidget(startup_splash_);
    startup_splash_->show();
    showNormal();
    setWindowTitle("SmartCut64" + license_.edition_suffix());
    resize(600, 370);
    startup_splash_->setFocus();
  }
  void request_load_source(const QString& path) { if (path.isEmpty() || !confirm_abandon_changes("open another video")) return; show_editor_workspace(); input_->setText(path); load_source(path); }
  void start_combined_open(const QStringList& files) {
    if (join_worker_.state() != QProcess::NotRunning) { status_->setText("A Joiner or Combine operation is already running"); return; }
    QByteArray fingerprint; for (const auto& path : files) { const QFileInfo info(path); fingerprint += QDir::toNativeSeparators(info.absoluteFilePath()).toUtf8(); fingerprint += ':' + QByteArray::number(info.size()) + ':' + QByteArray::number(info.lastModified().toMSecsSinceEpoch()) + '\n'; }
    const auto digest = QString::fromLatin1(QCryptographicHash::hash(fingerprint, QCryptographicHash::Sha256).toHex().left(16)); QString directory = automation_mode_ ? preview_dir_.path() : QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/combined-sources"; QDir().mkpath(directory); const auto base = QFileInfo(files.front()).completeBaseName(); pending_combined_output_ = directory + '/' + base + "-combined-" + digest + ".mkv"; pending_combined_list_ = pending_combined_output_ + ".sources.txt"; QFile list(pending_combined_list_); if (!list.open(QIODevice::WriteOnly | QIODevice::Truncate)) { status_->setText("Could not create the combined-source list"); return; } for (const auto& path : files) { if (path.contains('\n') || path.contains('\r')) { status_->setText("A source filename cannot be represented in the Combine list"); return; } list.write(QDir::toNativeSeparators(QFileInfo(path).absoluteFilePath()).toUtf8()); list.write("\n"); } list.close(); QFile::remove(pending_combined_output_); combined_open_pending_ = true; combined_open_source_count_ = files.size(); status_->setText(QString("Combining %1 sequential videos for editing…").arg(files.size())); join_worker_.start(worker_program(), {"--join-media", "--list", pending_combined_list_, "--output", pending_combined_output_});
  }
  void handle_open_files(QStringList files, QString action = {}) {
    files.erase(std::remove_if(files.begin(), files.end(), [](const QString& path) { return !QFileInfo(path).isFile(); }), files.end()); files.sort(Qt::CaseInsensitive); if (files.isEmpty()) return; if (files.size() == 1) { request_load_source(files.front()); return; }
    if (action.isEmpty()) action = multiple_file_action_;
    if (action == "prompt") { QMessageBox choice(QMessageBox::Question, "Open Multiple Files", QString("%1 files were selected. Join adds them to the Joiner; Combine opens them as one editable program.").arg(files.size()), QMessageBox::Cancel, this); auto* join = choice.addButton("Join", QMessageBox::AcceptRole); auto* combine = choice.addButton("Combine", QMessageBox::ActionRole); choice.exec(); action = choice.clickedButton() == join ? "join" : choice.clickedButton() == combine ? "combine" : "cancel"; }
    if (action == "join") { add_joiner_files(files, false); status_->setText(QString("%1 sorted files added to the Joiner").arg(files.size())); return; } if (action != "combine") return; if (!confirm_abandon_changes("open the combined videos")) return; start_combined_open(files);
  }
  void request_open_project(const QString& path) {
    if (path.isEmpty() || !confirm_abandon_changes("open another project")) return;
    show_editor_workspace();
    if (path.endsWith(".vprj", Qt::CaseInsensitive) || path.endsWith(".bprj", Qt::CaseInsensitive)) {
      if (project_interchange_.state() != QProcess::NotRunning) { status_->setText("A project interchange operation is already running"); return; }
      pending_vrd_project_path_ = QDir::toNativeSeparators(QFileInfo(path).absoluteFilePath());
      pending_vrd_native_path_ = preview_dir_.path() + "/videoredo-import-" + QString::number(qHash(pending_vrd_project_path_), 16) + ".smartcut";
      QFile::remove(pending_vrd_native_path_);
      project_interchange_mode_ = "import";
      status_->setText("Importing VideoReDo project…");
      project_interchange_.start(worker_program(), {"--import-videoredo-project", "--project", pending_vrd_project_path_, "--output", pending_vrd_native_path_});
      return;
    }
    QFile file(path); if (!file.open(QIODevice::ReadOnly)) { status_->setText("Could not open the selected project"); return; }
    pending_vrd_project_path_.clear(); native_project_path_.clear(); pending_project_path_ = path; pending_project_json_ = QString::fromUtf8(file.readAll()); project_validation_.start(worker_program(), {"--validate-project", "--project", path}); status_->setText("Validating project…");
  }
  void remember_edit() { undo_states_.push_back(edit_state()); redo_states_.clear(); if (!restoring_project_ && timeline_->frame_count() > 0) set_project_modified(true); }
  std::vector<FrameRange> normalize_cut_ranges(std::vector<FrameRange> ranges) const {
    const auto count = timeline_->frame_count();
    for (auto& range : ranges) { range.first = std::min(range.first, count); range.second = std::min(range.second, count); }
    ranges.erase(std::remove_if(ranges.begin(), ranges.end(), [](const FrameRange& range) { return range.second <= range.first; }), ranges.end());
    std::sort(ranges.begin(), ranges.end());
    std::vector<FrameRange> merged;
    for (const auto& range : ranges) { if (!merged.empty() && range.first <= merged.back().second) merged.back().second = std::max(merged.back().second, range.second); else merged.push_back(range); }
    return merged;
  }
  std::vector<FrameRange> kept_ranges() const { std::vector<FrameRange> kept; std::size_t cursor = 0; for (const auto& cut : cut_ranges_) { if (cursor < cut.first) kept.emplace_back(cursor, cut.first); cursor = std::max(cursor, cut.second); } if (cursor < timeline_->frame_count()) kept.emplace_back(cursor, timeline_->frame_count()); return kept; }
  std::vector<FrameRange> displayed_edit_ranges() const { return scene_mode_ ? kept_ranges() : cut_ranges_; }
  std::vector<FrameRange> subtract_range(const std::vector<FrameRange>& ranges, const FrameRange& removed) const { std::vector<FrameRange> result; for (const auto& range : ranges) { if (range.second <= removed.first || range.first >= removed.second) result.push_back(range); else { if (range.first < removed.first) result.emplace_back(range.first, removed.first); if (removed.second < range.second) result.emplace_back(removed.second, range.second); } } return normalize_cut_ranges(std::move(result)); }
  std::vector<FrameRange> intersect_range(const std::vector<FrameRange>& ranges, const FrameRange& retained) const { std::vector<FrameRange> result; for (const auto& range : ranges) { const auto first = std::max(range.first, retained.first); const auto end = std::min(range.second, retained.second); if (first < end) result.emplace_back(first, end); } return normalize_cut_ranges(std::move(result)); }
  void set_scene_mode(bool enabled, bool persist = true) { const bool changed = scene_mode_ != enabled; scene_mode_ = enabled; if (timeline_) timeline_->set_scene_mode(enabled); if (cut_mode_action_) cut_mode_action_->setChecked(!enabled); if (scene_mode_action_) scene_mode_action_->setChecked(enabled); if (cut_keep_) cut_keep_->setText(enabled ? "Add Selection" : "Cut Selection"); if (trim_) trim_->setText(enabled ? "Add Unselected" : "Trim Unselected"); if (cut_list_) cut_list_->setHorizontalHeaderLabels(enabled ? QStringList{"Scene Start", "Scene End", "Frames"} : QStringList{"Cut Start", "Cut End", "Frames"}); if (invert_cuts_) invert_cuts_->setText(enabled ? "Invert All Scenes" : "Invert All Cuts"); if (persist) QSettings("SmartCut64", "SmartCut64").setValue("editing/sceneMode", enabled); refresh_cut_list(); if (changed && persist && !restoring_project_ && timeline_ && timeline_->frame_count() > 0) set_project_modified(true); if (!restoring_project_ && !project_path_.isEmpty() && timeline_ && timeline_->frame_count() > 0) { record_edit_snapshot(); project_autosave_timer_.start(700); } }
  QString frame_timecode(std::size_t frame) const {
    const auto numerator = std::max(frame_rate_num_, 1); const auto denominator = std::max(frame_rate_den_, 1);
    const auto total_seconds = static_cast<std::uint64_t>(frame) * static_cast<std::uint64_t>(denominator) / static_cast<std::uint64_t>(numerator);
    const auto first_frame = (total_seconds * static_cast<std::uint64_t>(numerator) + static_cast<std::uint64_t>(denominator) - 1ULL) / static_cast<std::uint64_t>(denominator);
    const auto frame_in_second = static_cast<std::uint64_t>(frame) >= first_frame ? static_cast<std::uint64_t>(frame) - first_frame : 0ULL;
    return QString("%1:%2:%3.%4").arg(total_seconds / 3600ULL, 2, 10, QLatin1Char('0')).arg((total_seconds / 60ULL) % 60ULL, 2, 10, QLatin1Char('0')).arg(total_seconds % 60ULL, 2, 10, QLatin1Char('0')).arg(frame_in_second, 2, 10, QLatin1Char('0'));
  }
  QString format_time(std::size_t frame) const { return frame_timecode(frame); }
  void refresh_info_grid() {
    if (!info_grid_ || !timeline_) return;
    const auto count = timeline_->frame_count(); const auto selection_frames = timeline_->has_valid_range() ? timeline_->range_end() - timeline_->range_start() : 0;
    std::size_t cut_frames = 0; for (const auto& range : cut_ranges_) cut_frames += range.second - range.first;
    const auto output_frames = cut_frames <= count ? count - cut_frames : 0;
    info_grid_->set_values(frame_timecode(timeline_->current_frame()), timeline_->current_frame(), frame_timecode(count), count,
                           frame_timecode(selection_frames), selection_frames, frame_timecode(output_frames), output_frames,
                           timeline_->bookmark() ? frame_timecode(*timeline_->bookmark()) : QString{}, timeline_->bookmark());
  }
  void refresh_cut_list() {
    if (!cut_list_ || !timeline_) return;
    const auto ranges = displayed_edit_ranges(); cut_list_->setRowCount(static_cast<int>(ranges.size()));
    for (int row = 0; row < static_cast<int>(ranges.size()); ++row) { const auto range = ranges[static_cast<std::size_t>(row)]; const auto displayed_end = range.second == timeline_->frame_count() && range.second > 0 ? range.second - 1 : range.second; const QStringList values{frame_timecode(range.first), frame_timecode(displayed_end), QString::number(static_cast<qulonglong>(range.second - range.first))}; for (int column = 0; column < values.size(); ++column) { auto* item = new QTableWidgetItem(values[column]); item->setTextAlignment(Qt::AlignCenter); cut_list_->setItem(row, column, item); } }
    cut_list_->clearSelection(); const auto cursor = timeline_->current_frame(); for (int row = 0; row < static_cast<int>(ranges.size()); ++row) { const auto range = ranges[static_cast<std::size_t>(row)]; if (cursor >= range.first && cursor < range.second) { cut_list_->selectRow(row); break; } }
  }
  void set_cut_ranges(std::vector<FrameRange> ranges) { cut_ranges_ = normalize_cut_ranges(std::move(ranges)); timeline_->set_cut_ranges(cut_ranges_); refresh_thumbnail_cut_state(); refresh_cut_list(); refresh_info_grid(); if (!restoring_project_ && !project_path_.isEmpty() && timeline_->frame_count() > 0) { record_edit_snapshot(); project_autosave_timer_.start(700); } }
  void apply_edit_state(const EditState& state) { timeline_->set_marks(state.in_mark, state.out_mark); set_cut_ranges(state.cuts); }
  bool selected_range(FrameRange& range) const { if (!timeline_->has_valid_range()) return false; range = {timeline_->range_start(), timeline_->range_end()}; return range.second > range.first; }
  static bool ranges_overlap(const FrameRange& left, const FrameRange& right) { return left.first < right.second && right.first < left.second; }
  bool resolve_overlapping_cut(const FrameRange& selected, std::vector<FrameRange>& cuts) {
    const bool overlaps = std::any_of(cuts.cbegin(), cuts.cend(), [&selected](const FrameRange& range) { return ranges_overlap(range, selected); });
    if (!overlaps) { cuts.push_back(selected); return true; }
    auto choice = auto_replace_overlapping_cuts_ ? OverlapChoice::replace : overlap_choice_override_.value_or(OverlapChoice::cancel);
    if (!auto_replace_overlapping_cuts_ && !overlap_choice_override_) {
      QMessageBox dialog(QMessageBox::Question, "Overlapping Selections", "The new selection overlaps one or more existing cuts.", QMessageBox::NoButton, this); auto* combine = dialog.addButton("Combine", QMessageBox::AcceptRole); auto* replace = dialog.addButton("Replace", QMessageBox::DestructiveRole); auto* cancel = dialog.addButton(QMessageBox::Cancel); dialog.setInformativeText("Combine merges the overlapping ranges. Replace removes the old overlapping cuts and uses the new selection."); dialog.exec(); choice = dialog.clickedButton() == combine ? OverlapChoice::combine : dialog.clickedButton() == replace ? OverlapChoice::replace : OverlapChoice::cancel; (void)cancel;
    }
    if (choice == OverlapChoice::cancel) return false;
    if (choice == OverlapChoice::replace) cuts.erase(std::remove_if(cuts.begin(), cuts.end(), [&selected](const FrameRange& range) { return ranges_overlap(range, selected); }), cuts.end());
    cuts.push_back(selected); return true;
  }
  void cut_selected_range() { FrameRange selected; if (!selected_range(selected)) { status_->setText("Set a valid Mark In and Mark Out first"); return; } if (scene_mode_) { remember_edit(); set_cut_ranges(subtract_range(cut_ranges_, selected)); status_->setText("Selection added to scene list"); return; } auto cuts = cut_ranges_; if (!resolve_overlapping_cut(selected, cuts)) { status_->setText("Overlapping selection cancelled"); return; } remember_edit(); set_cut_ranges(std::move(cuts)); status_->setText("Selection added to cut list"); }
  void trim_unselected() { FrameRange selected; if (!selected_range(selected)) { status_->setText("Set a valid Mark In and Mark Out first"); return; } remember_edit(); if (scene_mode_) set_cut_ranges(intersect_range(cut_ranges_, selected)); else { std::vector<FrameRange> cuts; if (selected.first > 0) cuts.emplace_back(0, selected.first); if (selected.second < timeline_->frame_count()) cuts.emplace_back(selected.second, timeline_->frame_count()); set_cut_ranges(std::move(cuts)); } status_->setText(scene_mode_ ? "Unselected ranges added to scene list" : "Unselected ranges added to cut list"); }
  void invert_all_cuts() { remember_edit(); set_cut_ranges(kept_ranges()); status_->setText(scene_mode_ ? "All scenes inverted" : "All cuts inverted"); }
  void remove_selected_cuts() { const auto selected = cut_list_->selectionModel()->selectedRows(); if (selected.isEmpty()) { status_->setText(scene_mode_ ? "Select one or more scenes to remove" : "Select one or more cuts to remove"); return; } remember_edit(); const auto displayed = displayed_edit_ranges(); if (scene_mode_) { auto cuts = cut_ranges_; for (const auto& index : selected) if (index.row() >= 0 && index.row() < static_cast<int>(displayed.size())) cuts.push_back(displayed[static_cast<std::size_t>(index.row())]); set_cut_ranges(std::move(cuts)); } else { std::vector<FrameRange> remaining; for (int row = 0; row < static_cast<int>(cut_ranges_.size()); ++row) if (std::none_of(selected.begin(), selected.end(), [row](const QModelIndex& index) { return index.row() == row; })) remaining.push_back(cut_ranges_[static_cast<std::size_t>(row)]); set_cut_ranges(std::move(remaining)); } status_->setText(scene_mode_ ? "Selected scenes removed" : "Selected cuts removed"); }
  void activate_cut_row(int row, bool bracket) { const auto ranges = displayed_edit_ranges(); if (row < 0 || row >= static_cast<int>(ranges.size())) return; const auto range = ranges[static_cast<std::size_t>(row)]; set_preview_mode(false); timeline_->set_frame(range.first); if (bracket) { remember_edit(); timeline_->set_marks(range.first, range.second); selection_start_->setText(frame_timecode(range.first)); program_end_->setText(frame_timecode(range.second)); refresh_info_grid(); status_->setText(QString("%1 %2 selected for adjustment").arg(scene_mode_ ? "Scene" : "Cut").arg(row + 1)); } }
  void write_audio_volume() { QFile file(audio_volume_path_); if (file.open(QIODevice::WriteOnly | QIODevice::Truncate)) file.write(QByteArray::number(volume_ ? volume_->value() : 75)); }
  void set_preview_mode(bool enabled) { if (!timeline_ || timeline_->preview_mode() == enabled) return; if (enabled && timeline_->output_frame_count() == 0) { status_->setText("Preview unavailable: every source frame is cut"); if (preview_action_ && preview_action_->isChecked()) preview_action_->setChecked(false); return; } if (playback_requested_ || audio_playback_.state() != QProcess::NotRunning || video_playback_requested_ || playback_timer_.isActive()) stop_playback("Playback stopped while changing preview mode"); ++preview_generation_; preview_probe_.kill(); thumbnail_probe_.kill(); waveform_probe_.kill(); pending_preview_frame_.reset(); pending_thumbnail_center_.reset(); pending_waveform_window_.reset(); active_thumbnail_frames_.clear(); for (auto* thumbnail : thumbnail_widgets_) thumbnail->clear_frame(); frame_guide_->clear_waveform(); timeline_->set_preview_mode(enabled); if (preview_action_ && preview_action_->isChecked() != enabled) preview_action_->setChecked(enabled); edit_preview_->setText(enabled ? "E" : "P"); thumbnail_panel_->setStyleSheet(enabled ? "background:#d6e9ef;" : QString()); status_->setText(enabled ? (scene_mode_ ? "Preview mode: only listed scenes are addressable" : "Preview mode: cut frames are hidden from playback and navigation") : (scene_mode_ ? "Edit mode: source timeline and kept scenes" : "Edit mode: source timeline and cut ranges")); if (timeline_->frame_count() > 0) timeline_->set_frame(timeline_->current_frame()); }
  bool playback_frame_visible(std::size_t frame) const { return !timeline_ || !timeline_->preview_mode() || !timeline_->frame_is_cut(frame); }
  static std::uint64_t adjusted_audio_time_us(std::uint64_t played_us,
                                              int offset_ms) {
    const auto offset_us = static_cast<std::int64_t>(offset_ms) * 1000LL;
    if (offset_us >= 0) {
      const auto increment = static_cast<std::uint64_t>(offset_us);
      return played_us > std::numeric_limits<std::uint64_t>::max() - increment
                 ? std::numeric_limits<std::uint64_t>::max()
                 : played_us + increment;
    }
    const auto decrement = static_cast<std::uint64_t>(-offset_us);
    return played_us > decrement ? played_us - decrement : 0;
  }
  int playback_timer_interval_ms() const {
    const auto numerator = std::max(frame_rate_num_, 1);
    const auto denominator = std::max(frame_rate_den_, 1);
    return std::clamp(static_cast<int>(std::llround(
                          1000.0 * static_cast<double>(denominator) /
                          static_cast<double>(numerator))),
                      1, 1000);
  }
  void start_playback_timer() { playback_timer_.start(playback_timer_interval_ms()); }
  bool playback_active() const { return audio_playback_.state() != QProcess::NotRunning || video_playback_requested_ || playback_timer_.isActive() || audio_delay_timer_.isActive(); }
  void invalidate_playback_request() {
    playback_requested_ = false;
    ++playback_request_generation_;
    audio_delay_timer_.stop();
    navigation_seek_in_progress_ = false;
    navigation_seek_resume_pending_ = false;
    preview_audio_resume_pending_ = false;
    preview_audio_next_frame_.reset();
  }
  void begin_playback_request() {
    if (input_->text().isEmpty()) {
      status_->setText("Choose a source first");
      return;
    }
    playback_requested_ = true;
    ++playback_request_generation_;
    start_audio_playback();
  }
  void schedule_playback_restart() {
    const auto request_generation = playback_request_generation_;
    QTimer::singleShot(0, this, [this, request_generation] {
      if (!playback_requested_ || navigation_seek_in_progress_ ||
          request_generation != playback_request_generation_) return;
      start_audio_playback();
    });
  }
  void begin_navigation_seek() {
    navigation_seek_in_progress_ = true;
    navigation_seek_resume_pending_ = playback_requested_;
    if (!navigation_seek_resume_pending_) return;
    ++playback_request_generation_;
    preview_audio_resume_pending_ = false;
    preview_audio_next_frame_.reset();
    audio_delay_timer_.stop();
    audio_pause_requested_ = true;
    if (audio_playback_.state() != QProcess::NotRunning) request_audio_stop();
    request_video_stop();
    playback_timer_.stop();
    play_->setText("▶");
    status_->setText("Seeking; playback will resume on release");
  }
  void resume_navigation_seek() {
    if (!playback_requested_ || navigation_seek_in_progress_ ||
        !navigation_seek_resume_pending_ ||
        audio_playback_.state() != QProcess::NotRunning) return;
    navigation_seek_resume_pending_ = false;
    audio_pause_requested_ = false;
    ++navigation_seek_resume_count_;
    start_audio_playback();
    if (smoke_navigation_seek_observing_)
      smoke_navigation_seek_post_restart_elapsed_.start();
  }
  void finish_navigation_seek() {
    navigation_seek_in_progress_ = false;
    if (!playback_requested_ || !navigation_seek_resume_pending_) {
      navigation_seek_resume_pending_ = false;
      status_->setText("Navigation seek complete");
      return;
    }
    status_->setText("Resuming playback from the selected position…");
    if (audio_playback_.state() == QProcess::NotRunning)
      QTimer::singleShot(0, this, &ExportWindow::resume_navigation_seek);
  }
  bool skip_preview_cut(bool restart_audio) { if (!timeline_->preview_mode()) return false; const auto cut_end = timeline_->cut_end_for_frame(timeline_->current_frame()); if (!cut_end) return false; if (*cut_end >= timeline_->frame_count()) { stop_playback("Playback complete"); return true; } timeline_->set_frame(*cut_end); if (restart_audio && audio_playback_.state() != QProcess::NotRunning) { preview_audio_resume_pending_ = true; audio_pause_requested_ = true; request_audio_stop(); } return true; }
  void launch_audio_playback(std::uint64_t source_start_us) { if (!playback_requested_ || navigation_seek_in_progress_) return; audio_progress_buffer_.clear(); QFile::remove(audio_cancel_path_); write_audio_volume(); preview_audio_next_frame_.reset(); ++audio_playback_generation_; QStringList arguments{"--play-audio", "--input", input_->text(), "--start-us", QString::number(static_cast<qulonglong>(source_start_us)), "--cancel-file", audio_cancel_path_, "--volume-file", audio_volume_path_}; if (timeline_->preview_mode()) { const auto current = timeline_->current_frame(); for (const auto& cut : timeline_->cut_ranges()) { if (cut.first <= current) continue; const auto boundary = timeline_->frame_time_us(cut.first); if (!boundary) break; const auto audio_boundary = static_cast<std::int64_t>(*boundary) - audio_offset_ms_ * 1000LL; if (audio_boundary > static_cast<std::int64_t>(source_start_us)) { arguments << "--end-us" << QString::number(static_cast<qulonglong>(audio_boundary)); preview_audio_next_frame_ = cut.second; } break; } } audio_playback_.start(worker_program(), arguments); play_->setText("❚❚"); status_->setText(timeline_->preview_mode() ? "Playing edited preview" : "Playing source (audio master)"); }
  void start_audio_playback() { if (!playback_requested_ || navigation_seek_in_progress_) return; if (input_->text().isEmpty()) { invalidate_playback_request(); status_->setText("Choose a source first"); return; } if (skip_preview_cut(false) && timeline_->at_playback_end(repeat_selection_playback_)) return; audio_pause_requested_ = false; start_video_playback(); if (edit_without_audio_) { start_playback_timer(); play_->setText("❚❚"); status_->setText(timeline_->preview_mode() ? "Playing edited preview (video only)" : "Playing source (video only)"); return; } const auto video_us = static_cast<std::int64_t>(timeline_->current_time_us()); const auto offset_us = audio_offset_ms_ * 1000LL; if (offset_us > video_us) { const auto delay_ms = static_cast<int>(std::min<std::int64_t>((offset_us - video_us + 999) / 1000, std::numeric_limits<int>::max())); start_playback_timer(); audio_delay_timer_.start(delay_ms); play_->setText("❚❚"); status_->setText(QString("Playing preview; audio delayed by %1 ms").arg(delay_ms)); return; } launch_audio_playback(static_cast<std::uint64_t>(std::max<std::int64_t>(0, video_us - offset_us))); }
  bool restart_repeat_selection(bool restart_audio) { if (!repeat_selection_playback_ || !timeline_->has_valid_range()) return false; timeline_->goto_in_mark(); if (restart_audio && audio_playback_.state() != QProcess::NotRunning) { preview_audio_resume_pending_ = true; audio_pause_requested_ = true; request_audio_stop(); } return true; }
  void begin_repeat_selection() { if (!timeline_->has_valid_range() || input_->text().isEmpty()) return; repeat_selection_playback_ = true; timeline_->goto_in_mark(); if (audio_playback_.state() != QProcess::NotRunning) { preview_audio_resume_pending_ = true; audio_pause_requested_ = true; request_audio_stop(); } else if (!playback_timer_.isActive()) { if (!playback_requested_) begin_playback_request(); else start_audio_playback(); } status_->setText("Repeating marked selection"); }
  void stop_playback(const QString& message) { invalidate_playback_request(); audio_delay_timer_.stop(); audio_pause_requested_ = true; if (audio_playback_.state() != QProcess::NotRunning) request_audio_stop(); request_video_stop(); playback_timer_.stop(); play_->setText("▶"); status_->setText(message); if (!input_->text().isEmpty() && timeline_->frame_count() > 0) QTimer::singleShot(0, this, [this] { request_preview(timeline_->current_frame()); }); }
  void toggle_playback() { if (playback_requested_ || navigation_seek_resume_pending_ || audio_playback_.state() != QProcess::NotRunning || video_playback_requested_ || playback_timer_.isActive()) stop_playback("Paused"); else begin_playback_request(); }
  void request_audio_stop() { if (audio_playback_.state() == QProcess::NotRunning) return; const auto stopped_generation = audio_playback_generation_; QFile cancel(audio_cancel_path_); if (cancel.open(QIODevice::WriteOnly)) cancel.write("cancel\n"); QTimer::singleShot(2000, &audio_playback_, [this, stopped_generation] { if (audio_playback_.state() != QProcess::NotRunning && audio_playback_generation_ == stopped_generation) audio_playback_.kill(); }); }
  void request_video_stop() {
    video_playback_requested_ = false;
    if (video_playback_.state() == QProcess::NotRunning) return;
    const auto stopped_generation = video_playback_generation_;
    QFile cancel(video_cancel_path_);
    if (cancel.open(QIODevice::WriteOnly | QIODevice::Truncate)) cancel.write("cancel\n");
    QTimer::singleShot(500, &video_playback_, [this, stopped_generation] {
      if (video_playback_.state() != QProcess::NotRunning &&
          video_playback_generation_ == stopped_generation) video_playback_.kill();
    });
  }
  void start_video_playback() {
    if (input_->text().isEmpty() || timeline_->frame_count() == 0) return;
    const auto frame = timeline_->current_frame();
    const auto pts = timeline_->frame_pts(frame);
    if (!pts) return;
    video_playback_requested_ = false;
    active_video_playback_prefix_.clear();
    if (video_playback_.state() != QProcess::NotRunning) {
      QFile cancel(video_cancel_path_);
      if (cancel.open(QIODevice::WriteOnly | QIODevice::Truncate)) cancel.write("cancel\n");
      if (!video_playback_.waitForFinished(250)) {
        video_playback_.kill();
        video_playback_.waitForFinished(250);
      }
    }
    QFile::remove(video_cancel_path_);
    preview_probe_.kill();
    pending_preview_frame_.reset();
    thumbnail_refresh_timer_.stop();
    pending_thumbnail_center_.reset();
    video_playback_output_.clear();
    const auto prefix = QString("SmartCutPlayback-%1-%2")
                            .arg(QCoreApplication::applicationPid())
                            .arg(static_cast<qulonglong>(++video_playback_generation_));
    active_video_playback_prefix_ = prefix;
    video_playback_requested_ = true;
    video_playback_.start(worker_program(),
                          {"--play-video-shared", "--input", input_->text(),
                           "--start-frame", QString::number(static_cast<qulonglong>(frame)),
                           "--start-pts", QString::number(*pts),
                           "--mapping-prefix", prefix,
                           "--cancel-file", video_cancel_path_});
  }
  void apply_pending_chapters() {
    if (timeline_->frame_count() == 0) return;
    std::vector<std::size_t> frames;
    frames.reserve(pending_chapter_times_us_.size());
    for (const auto time_us : pending_chapter_times_us_) frames.push_back(timeline_->frame_at_time_us(time_us));
    timeline_->set_chapter_markers(std::move(frames));
  }
  void capture_frame() {
    if (current_preview_image_.isNull()) { status_->setText("Display a decoded video frame before capturing"); return; }
    QDialog dialog(this); dialog.setWindowTitle("Frame Capture"); auto* layout = new QVBoxLayout(&dialog);
    auto* explanation = new QLabel("Capture the currently displayed full-resolution frame.", &dialog); layout->addWidget(explanation);
    auto* clipboard = new QPushButton("Copy To Clipboard", &dialog); auto* file = new QPushButton("Save To File…", &dialog); auto* cancel = new QPushButton("Cancel", &dialog);
    layout->addWidget(clipboard); layout->addWidget(file); layout->addWidget(cancel);
    connect(clipboard, &QPushButton::clicked, &dialog, [this, &dialog] { QApplication::clipboard()->setPixmap(current_preview_image_); status_->setText("Current frame copied to the clipboard"); dialog.accept(); });
    connect(file, &QPushButton::clicked, &dialog, [this, &dialog] { const auto path = QFileDialog::getSaveFileName(&dialog, "Save Captured Frame", {}, "PNG Image (*.png);;JPEG Image (*.jpg *.jpeg);;Bitmap Image (*.bmp);;TIFF Image (*.tif *.tiff)"); if (path.isEmpty()) return; if (current_preview_image_.save(path)) { status_->setText(QString("Captured frame saved: %1").arg(path)); dialog.accept(); } else status_->setText("Could not save the captured frame"); });
    connect(cancel, &QPushButton::clicked, &dialog, &QDialog::reject); dialog.exec();
  }
  void toggle_fullscreen() { if (isFullScreen()) showNormal(); else showFullScreen(); status_->setText(isFullScreen() ? "Full-screen mode" : "Windowed mode"); }
  void bind_shortcut(const QString& id, const QKeySequence& default_key, std::function<void()> operation) { const auto stored = QSettings("SmartCut64", "SmartCut64").value("keyboard/" + id, default_key.toString(QKeySequence::PortableText)).toString(); auto* shortcut = new QShortcut(QKeySequence::fromString(stored, QKeySequence::PortableText), this); shortcut->setContext(Qt::WindowShortcut); connect(shortcut, &QShortcut::activated, this, std::move(operation)); managed_shortcuts_.insert(id, shortcut); default_shortcuts_.insert(id, default_key); }
  void show_help_contents() {
    QDialog dialog(this); dialog.setWindowTitle("SmartCut64 Help"); dialog.resize(900, 720); auto* layout = new QVBoxLayout(&dialog);
    auto* search_row = new QHBoxLayout; auto* search = new QLineEdit(&dialog); search->setPlaceholderText("Search this guide"); search->setClearButtonEnabled(true); auto* find_next = new QPushButton("Find Next", &dialog); search_row->addWidget(search, 1); search_row->addWidget(find_next); layout->addLayout(search_row);
    auto* help = new QTextBrowser(&dialog); help->setOpenExternalLinks(true); help->setOpenLinks(true); QFile guide(":/smartcut64/help/user-guide.md"); if (guide.open(QIODevice::ReadOnly | QIODevice::Text)) help->setMarkdown(QString::fromUtf8(guide.readAll())); else help->setHtml("<h2>SmartCut64 Help</h2><p>The embedded user guide could not be opened. Reinstall the complete application package or contact support@linekerpandi.com.</p>"); layout->addWidget(help, 1);
    const auto find = [help, search] { if (search->text().isEmpty()) return; if (!help->find(search->text())) { help->moveCursor(QTextCursor::Start); help->find(search->text()); } }; connect(find_next, &QPushButton::clicked, &dialog, find); connect(search, &QLineEdit::returnPressed, &dialog, find);
    auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close, &dialog); layout->addWidget(buttons); connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); dialog.exec();
  }
  void show_keyboard_shortcuts() { QDialog dialog(this); dialog.setWindowTitle("Keyboard Shortcuts"); dialog.resize(620, 720); auto* layout = new QVBoxLayout(&dialog); auto* table = new QTableWidget(managed_shortcuts_.size(), 2, &dialog); table->setHorizontalHeaderLabels({"Command", "Shortcut"}); table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); table->verticalHeader()->hide(); table->setEditTriggers(QAbstractItemView::NoEditTriggers); QStringList ids = managed_shortcuts_.keys(); ids.sort(Qt::CaseInsensitive); QHash<QString, QKeySequenceEdit*> editors; for (int row = 0; row < ids.size(); ++row) { const auto& id = ids[row]; table->setItem(row, 0, new QTableWidgetItem(id)); auto* editor = new QKeySequenceEdit(managed_shortcuts_[id]->key(), table); editor->setClearButtonEnabled(true); table->setCellWidget(row, 1, editor); editors.insert(id, editor); } layout->addWidget(table, 1); auto* reset = new QPushButton("Restore Legacy Defaults", &dialog); layout->addWidget(reset); auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); layout->addWidget(buttons); connect(reset, &QPushButton::clicked, &dialog, [this, editors] { for (auto found = editors.cbegin(); found != editors.cend(); ++found) found.value()->setKeySequence(default_shortcuts_.value(found.key())); }); connect(buttons, &QDialogButtonBox::accepted, &dialog, [&dialog, editors] { QSet<QString> used; for (auto found = editors.cbegin(); found != editors.cend(); ++found) { const auto text = found.value()->keySequence().toString(QKeySequence::PortableText); if (!text.isEmpty() && used.contains(text)) { QMessageBox::warning(&dialog, "Keyboard Shortcuts", QString("The shortcut %1 is assigned more than once.").arg(text)); return; } if (!text.isEmpty()) used.insert(text); } dialog.accept(); }); connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); if (dialog.exec() != QDialog::Accepted) return; QSettings settings("SmartCut64", "SmartCut64"); for (auto found = editors.cbegin(); found != editors.cend(); ++found) { const auto key = found.value()->keySequence(); managed_shortcuts_[found.key()]->setKey(key); settings.setValue("keyboard/" + found.key(), key.toString(QKeySequence::PortableText)); } status_->setText("Keyboard shortcuts updated"); }
  void show_keyboard_help() { QMessageBox::information(this, "SmartCut64 Keyboard Help", "Space: play/pause    P: edit/preview mode    M: mute\nF3/F4: mark in/out    Delete/Insert: cut/trim\nTab/Shift+Tab: next/previous cut    F5/F6: chapter marks\nF7/F8/Z: timeline zoom    ,/.: pan zoom\n0-9: thumbnail frame    Ctrl+0-9 or +/-: thumbnail interval\nF11: thumbnails    F12: reload retaining edits    Alt+Enter: full screen"); }
  void reload_source() { if (input_->text().isEmpty() || timeline_->frame_count() == 0) { status_->setText("Open a source before reloading"); return; } std::vector<FrameRange> kept; std::size_t cursor = 0; for (const auto& cut : cut_ranges_) { if (cursor < cut.first) kept.emplace_back(cursor, cut.first); cursor = std::max(cursor, cut.second); } if (cursor < timeline_->frame_count()) kept.emplace_back(cursor, timeline_->frame_count()); pending_project_kept_ = std::move(kept); pending_project_streams_ = selected_streams_; pending_modified_after_load_ = project_modified_; restoring_project_ = true; const auto path = input_->text(); load_source(path); status_->setText("Reloading source while retaining edits…"); }
  static const QVector<double>& thumbnail_intervals() { static const QVector<double> values{0.0, 0.04, 0.10, 0.25, 0.50, 1.0, 2.0, 5.0, 10.0, 30.0}; return values; }
  void set_thumbnail_interval_step(int step, bool persist = true) { const auto& values = thumbnail_intervals(); step = std::clamp(step, 0, static_cast<int>(values.size()) - 1); thumbnail_interval_seconds_ = values[step]; if (persist) QSettings("SmartCut64", "SmartCut64").setValue("thumbnails/intervalSecondsV2", thumbnail_interval_seconds_); status_->setText(step == 0 ? "Thumbnail interval: single frame" : QString("Thumbnail interval: %1 seconds").arg(thumbnail_interval_seconds_)); if (timeline_->frame_count() > 0) schedule_thumbnail_strip(timeline_->current_frame()); }
  void adjust_thumbnail_interval(int direction) { const auto& values = thumbnail_intervals(); int nearest = 0; double distance = std::numeric_limits<double>::max(); for (int i = 0; i < values.size(); ++i) { const auto candidate = std::abs(values[i] - thumbnail_interval_seconds_); if (candidate < distance) { distance = candidate; nearest = i; } } set_thumbnail_interval_step(nearest + direction); }
  void jump_thumbnail_digit(int digit) { if (active_thumbnail_frames_.isEmpty() || digit < 0 || digit > 9) return; const auto center = static_cast<int>(active_thumbnail_frames_.size() / 2); const auto slot = center + digit - 5; if (slot >= 0 && slot < static_cast<int>(active_thumbnail_frames_.size())) timeline_->set_frame(active_thumbnail_frames_[slot]); }
  QString selected_stream_csv() const { QStringList values; for (const auto index : selected_streams_) values.push_back(QString::number(index)); return values.join(','); }
  QString chapter_csv() const { QStringList values; for (const auto frame : timeline_->chapter_markers()) values.push_back(QString::number(static_cast<qulonglong>(frame))); return values.join(','); }
  static QString stream_csv(const QVector<int>& streams) { QStringList values; for (const auto index : streams) values.push_back(QString::number(index)); return values.join(','); }
  static QString cut_csv(const std::vector<FrameRange>& cuts) { QStringList values; for (const auto& cut : cuts) values.push_back(QString("%1:%2").arg(static_cast<qulonglong>(cut.first)).arg(static_cast<qulonglong>(cut.second))); return values.join(','); }
  QStringList edit_project_arguments(const QString& path) const { return {"--save-edit-project", "--input", input_->text(), "--project", path, "--frame-count", QString::number(static_cast<qulonglong>(timeline_->frame_count())), "--cuts", cut_csv(cut_ranges_), "--journal", path + ".journal", "--streams", selected_stream_csv(), "--chapters", chapter_csv(), "--scene-mode", scene_mode_ ? "1" : "0", "--audio-offset-ms", QString::number(audio_offset_ms_)}; }
  void save_project_document(QProcess& process, const QString& path, bool autosave) {
    if (input_->text().isEmpty() || timeline_->frame_count() == 0 || path.isEmpty()) return;
    const bool video_redo = path.endsWith(".vprj", Qt::CaseInsensitive);
    const bool video_redo_batch = path.endsWith(".bprj", Qt::CaseInsensitive);
    if (video_redo || video_redo_batch) {
      if (project_interchange_.state() != QProcess::NotRunning) { if (!autosave) status_->setText("A project interchange operation is already running"); return; }
      pending_vrd_project_path_ = QDir::toNativeSeparators(QFileInfo(path).absoluteFilePath());
      pending_vrd_native_path_ = preview_dir_.path() + "/videoredo-save-" + QString::number(qHash(pending_vrd_project_path_), 16) + ".smartcut";
      pending_vrd_intermediate_path_ = preview_dir_.path() + "/videoredo-save-" + QString::number(qHash(pending_vrd_project_path_), 16) + ".VPrj";
      pending_vrd_media_output_ = output_->text().isEmpty() ? default_output_path() : output_->text();
      QFile::remove(pending_vrd_native_path_); QFile::remove(pending_vrd_intermediate_path_);
      project_interchange_mode_ = video_redo_batch ? "save-batch-native" : "save-native";
      project_interchange_.start(worker_program(), edit_project_arguments(pending_vrd_native_path_));
      if (!autosave) status_->setText(video_redo_batch ? "Saving VideoReDo-compatible batch project…" : "Saving VideoReDo-compatible project…");
      return;
    }
    if (process.state() != QProcess::NotRunning) return;
    native_project_path_.clear(); pending_vrd_project_path_.clear(); project_operation_active_ = false;
    process.start(worker_program(), edit_project_arguments(path)); if (!autosave) status_->setText("Saving project…");
  }
  void record_edit_snapshot() { if (restoring_project_ || project_path_.isEmpty() || source_uuid_.isEmpty() || timeline_->frame_count() == 0) return; if (journal_snapshot_.state() != QProcess::NotRunning) { pending_journal_snapshot_ = true; return; } journal_snapshot_.start(worker_program(), {"--record-edit-snapshot", "--journal", project_path_ + ".journal", "--source-uuid", source_uuid_, "--frame-count", QString::number(static_cast<qulonglong>(timeline_->frame_count())), "--cuts", cut_csv(cut_ranges_), "--streams", selected_stream_csv(), "--chapters", chapter_csv(), "--scene-mode", scene_mode_ ? "1" : "0", "--audio-offset-ms", QString::number(audio_offset_ms_)}); }
  void restore_latest_edit_snapshot(const QString& journal_path, const QString& expected_source_uuid) {
    QFile file(journal_path); if (!file.open(QIODevice::ReadOnly)) return; QJsonObject latest;
    while (!file.atEnd()) { const auto document = QJsonDocument::fromJson(file.readLine().trimmed()); if (!document.isObject()) continue; const auto object = document.object(); if (object.value("op").toString() == "edit_snapshot" && object.value("source_uuid").toString() == expected_source_uuid) latest = object; }
    if (latest.isEmpty()) return;
    const auto frame_count = static_cast<std::size_t>(latest.value("frame_count").toDouble()); if (frame_count == 0) return;
    const auto cut_values = latest.value("cuts").toString().split(',', Qt::SkipEmptyParts); std::vector<FrameRange> cuts; for (const auto& value : cut_values) { const auto parts = value.split(':'); if (parts.size() != 2) return; bool first_ok = false, end_ok = false; const auto first = parts[0].toULongLong(&first_ok); const auto end = parts[1].toULongLong(&end_ok); if (!first_ok || !end_ok || first >= end || end > frame_count) return; cuts.emplace_back(static_cast<std::size_t>(first), static_cast<std::size_t>(end)); }
    std::sort(cuts.begin(), cuts.end()); std::vector<FrameRange> kept; std::size_t cursor = 0; for (const auto& cut : cuts) { if (cursor < cut.first) kept.emplace_back(cursor, cut.first); cursor = std::max(cursor, cut.second); } if (cursor < frame_count) kept.emplace_back(cursor, frame_count); if (kept.empty()) return;
    const auto stream_values = latest.value("streams").toString().split(',', Qt::SkipEmptyParts); QVector<int> streams; for (const auto& value : stream_values) { bool ok = false; const auto stream = value.toInt(&ok); if (ok) streams.push_back(stream); }
    const auto chapter_values = latest.value("chapters").toString().split(',', Qt::SkipEmptyParts); std::vector<std::size_t> chapters; for (const auto& value : chapter_values) { bool ok = false; const auto frame = value.toULongLong(&ok); if (ok && frame < frame_count) chapters.push_back(static_cast<std::size_t>(frame)); }
    pending_project_kept_ = std::move(kept); pending_project_streams_ = std::move(streams); pending_project_chapters_ = std::move(chapters); set_scene_mode(latest.value("scene_mode").toBool(false), false); audio_offset_ms_ = latest.value("audio_offset_ms").toInt(0); journal_recovery_applied_ = true;
  }
  bool all_streams_selected() const { return !source_streams_.isEmpty() && selected_streams_.size() == source_streams_.size(); }
  void show_stream_selection() {
    if (source_streams_.isEmpty()) { status_->setText("Open and finish probing a source before selecting streams"); return; }
    QDialog dialog(this); dialog.setWindowTitle("Stream Selection"); dialog.resize(760, 430); auto* layout = new QVBoxLayout(&dialog); auto* table = new QTableWidget(source_streams_.size(), 5, &dialog); table->setHorizontalHeaderLabels({"Use", "Stream", "Type", "Codec", "Language / Title"}); table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); table->setSelectionBehavior(QAbstractItemView::SelectRows); table->setEditTriggers(QAbstractItemView::NoEditTriggers);
    const auto metadata_text = [](const QJsonObject& stream) { QString language; QString title; for (const auto& value : stream.value("metadata").toArray()) { const auto item = value.toObject(); if (item.value("key").toString().compare("language", Qt::CaseInsensitive) == 0) language = item.value("value").toString(); if (item.value("key").toString().compare("title", Qt::CaseInsensitive) == 0) title = item.value("value").toString(); } QStringList values; if (!language.isEmpty()) values.push_back(language); if (!title.isEmpty()) values.push_back(title); return values.join(" / "); };
    for (int row = 0; row < source_streams_.size(); ++row) { const auto stream = source_streams_[row].toObject(); const int index = stream.value("index").toInt(); const int type = stream.value("type").toInt(-1); const QString type_name = type == 0 ? "Video" : type == 1 ? "Audio" : type == 3 ? "Subtitle" : type == 4 ? "Attachment" : "Data"; auto* use = new QTableWidgetItem; use->setFlags(Qt::ItemIsEnabled | Qt::ItemIsUserCheckable | Qt::ItemIsSelectable); use->setCheckState(selected_streams_.contains(index) ? Qt::Checked : Qt::Unchecked); table->setItem(row, 0, use); table->setItem(row, 1, new QTableWidgetItem(QString::number(index))); table->setItem(row, 2, new QTableWidgetItem(type_name)); table->setItem(row, 3, new QTableWidgetItem(stream.value("codec").toString())); table->setItem(row, 4, new QTableWidgetItem(metadata_text(stream))); }
    layout->addWidget(table); auto* note = new QLabel("Selected tracks retain their original codec packets, extradata, language, title and disposition. Chapters and container metadata are preserved. At least one video stream is required.", &dialog); note->setWordWrap(true); layout->addWidget(note); auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); layout->addWidget(buttons); connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
    if (dialog.exec() != QDialog::Accepted) return;
    QVector<int> selected; bool has_video = false; for (int row = 0; row < table->rowCount(); ++row) if (table->item(row, 0)->checkState() == Qt::Checked) { const int index = table->item(row, 1)->text().toInt(); selected.push_back(index); if (source_streams_[row].toObject().value("type").toInt(-1) == 0) has_video = true; } if (!has_video) { QMessageBox::warning(this, "Stream Selection", "Select at least one video stream."); return; } const bool changed = selected != selected_streams_; selected_streams_ = std::move(selected); if (changed) set_project_modified(true); if (!project_path_.isEmpty()) { record_edit_snapshot(); project_autosave_timer_.start(700); } status_->setText(QString("%1 of %2 source streams selected").arg(selected_streams_.size()).arg(source_streams_.size()));
  }
  void load_custom_profiles()
  {
    custom_profiles_.clear();
    const auto document = QJsonDocument::fromJson(
        QSettings("SmartCut64", "SmartCut64").value("output/customProfiles").toByteArray());
    if (document.isArray())
      for (const auto &value : document.array())
      {
        const auto object = value.toObject();
        OutputProfile profile{object.value("id").toString(), object.value("name").toString(),
                              object.value("mode").toString(),
                              object.value("description").toString()};
        profile.audio_codec = object.value("audio_codec").toString("copy");
        profile.audio_gain_db = object.value("audio_gain_db").toDouble();
        profile.audio_bitrate = object.value("audio_bitrate").toInt(192000);
        profile.audio_sample_rate = object.value("audio_sample_rate").toInt();
        profile.audio_channels = object.value("audio_channels").toInt();
        profile.file_type = object.value("file_type").toString("MKV");
        profile.file_extension = object.value("file_extension").toString("mkv");
        profile.video_encoder = object.value("video_encoder").toString("source");
        profile.hardware_encoder = object.value("hardware_encoder").toString("auto");
        profile.aspect_ratio = object.value("aspect_ratio").toString("No Change");
        profile.video_width = object.value("video_width").toInt();
        profile.video_height = object.value("video_height").toInt();
        profile.video_bit_depth = object.value("video_bit_depth").toInt();
        profile.video_bitrate_kbps = object.value("video_bitrate_kbps").toInt();
        profile.video_max_bitrate_kbps = object.value("video_max_bitrate_kbps").toInt();
        profile.deinterlace = object.value("deinterlace").toString("None");
        profile.frame_rate = object.value("frame_rate").toString("No Change");
        profile.ts_transport_stream_id = object.value("ts_transport_stream_id").toInt(1);
        profile.ts_original_network_id = object.value("ts_original_network_id").toInt(0xff01);
        profile.ts_service_id = object.value("ts_service_id").toInt(1);
        profile.ts_pmt_start_pid = object.value("ts_pmt_start_pid").toInt(0x1000);
        profile.ts_start_pid = object.value("ts_start_pid").toInt(0x0100);
        profile.ts_mux_rate = object.value("ts_mux_rate").toInt();
        profile.ts_pcr_period_ms = object.value("ts_pcr_period_ms").toInt(-1);
        profile.ts_pat_period = object.value("ts_pat_period").toDouble(0.1);
        profile.ts_sdt_period = object.value("ts_sdt_period").toDouble(0.5);
        profile.ts_m2ts_mode = object.value("ts_m2ts_mode").toBool();
        profile.ts_resend_headers = object.value("ts_resend_headers").toBool(true);
        profile.ts_system_b = object.value("ts_system_b").toBool();
        profile.ts_emit_nit = object.value("ts_emit_nit").toBool();
        profile.ts_service_name = object.value("ts_service_name").toString("SmartCut64");
        profile.ts_service_provider = object.value("ts_service_provider").toString("SmartCut64");
        profile.enabled = object.value("enabled").toBool(true);
        profile.favorite = object.value("favorite").toBool(false);
        const bool audio_valid = profile.audio_codec == "copy" || profile.audio_codec == "source" ||
                                 profile.audio_codec == "aac" || profile.audio_codec == "ac3" ||
                                 profile.audio_codec == "mp2" || profile.audio_codec == "opus" ||
                                 profile.audio_codec == "pcm_s16le";
        if (!profile.id.isEmpty() && !profile.name.isEmpty() && audio_valid &&
            (profile.mode == "intelligent" || profile.mode == "keyframe" ||
             profile.mode == "full" || profile.mode == "quickstream"))
          custom_profiles_.push_back(std::move(profile));
      }
    if (output_profile_)
    {
      const int standard_count = standard_output_profiles().size();
      while (output_profile_->count() > standard_count)
        output_profile_->removeItem(standard_count);
      for (const auto &profile : custom_profiles_)
        if (profile.enabled)
          output_profile_->addItem(profile.favorite ? "★ " + profile.name : profile.name,
                                   profile.id);
    }
  }
  void save_custom_profiles() const
  {
    QJsonArray values;
    for (const auto &profile : custom_profiles_)
      values.push_back(QJsonObject{{"id", profile.id},
                                   {"name", profile.name},
                                   {"mode", profile.mode},
                                   {"description", profile.description},
                                   {"audio_codec", profile.audio_codec},
                                   {"audio_gain_db", profile.audio_gain_db},
                                   {"audio_bitrate", profile.audio_bitrate},
                                   {"audio_sample_rate", profile.audio_sample_rate},
                                   {"audio_channels", profile.audio_channels},
                                   {"file_type", profile.file_type},
                                   {"file_extension", profile.file_extension},
                                   {"video_encoder", profile.video_encoder},
                                   {"hardware_encoder", profile.hardware_encoder},
                                   {"aspect_ratio", profile.aspect_ratio},
                                   {"video_width", profile.video_width},
                                   {"video_height", profile.video_height},
                                   {"video_bit_depth", profile.video_bit_depth},
                                   {"video_bitrate_kbps", profile.video_bitrate_kbps},
                                   {"video_max_bitrate_kbps", profile.video_max_bitrate_kbps},
                                   {"deinterlace", profile.deinterlace},
                                   {"frame_rate", profile.frame_rate},
                                   {"ts_transport_stream_id", profile.ts_transport_stream_id},
                                   {"ts_original_network_id", profile.ts_original_network_id},
                                   {"ts_service_id", profile.ts_service_id},
                                   {"ts_pmt_start_pid", profile.ts_pmt_start_pid},
                                   {"ts_start_pid", profile.ts_start_pid},
                                   {"ts_mux_rate", profile.ts_mux_rate},
                                   {"ts_pcr_period_ms", profile.ts_pcr_period_ms},
                                   {"ts_pat_period", profile.ts_pat_period},
                                   {"ts_sdt_period", profile.ts_sdt_period},
                                   {"ts_m2ts_mode", profile.ts_m2ts_mode},
                                   {"ts_resend_headers", profile.ts_resend_headers},
                                   {"ts_system_b", profile.ts_system_b},
                                   {"ts_emit_nit", profile.ts_emit_nit},
                                   {"ts_service_name", profile.ts_service_name},
                                   {"ts_service_provider", profile.ts_service_provider},
                                   {"enabled", profile.enabled},
                                   {"favorite", profile.favorite}});
    QSettings("SmartCut64", "SmartCut64")
        .setValue("output/customProfiles", QJsonDocument(values).toJson(QJsonDocument::Compact));
  }
  QString output_profile_mode(const QVariant& data = {}) const {
    const auto id = data.isValid() ? data.toString() : output_profile_->currentData().toString();
    if (pending_output_profile_override_ && pending_output_profile_override_->id == id) return pending_output_profile_override_->mode;
    const auto custom = std::find_if(custom_profiles_.cbegin(), custom_profiles_.cend(), [&id](const OutputProfile& profile) { return profile.id == id; });
    if (custom != custom_profiles_.cend()) return custom->mode;
    const auto standards = standard_output_profiles();
    const auto standard = std::find_if(standards.cbegin(), standards.cend(), [&id](const OutputProfile& profile) { return profile.id == id; });
    return standard == standards.cend() ? QString("intelligent") : standard->mode;
  }
  OutputProfile current_output_profile(const QVariant& data = {}) const {
    const auto id = data.isValid() ? data.toString() : output_profile_->currentData().toString();
    if (pending_output_profile_override_ && pending_output_profile_override_->id == id) return *pending_output_profile_override_;
    const auto custom = std::find_if(custom_profiles_.cbegin(), custom_profiles_.cend(), [&id](const OutputProfile& profile) { return profile.id == id; });
    if (custom != custom_profiles_.cend()) return *custom;
    const auto standards = standard_output_profiles();
    const auto standard = std::find_if(standards.cbegin(), standards.cend(), [&id](const OutputProfile& profile) { return profile.id == id; });
    if (standard != standards.cend()) return *standard;
    return OutputProfile{id, output_profile_ ? output_profile_->currentText() : id, "intelligent", "Built-in validated profile"};
  }
  QString output_profile_extension(const OutputProfile& profile) const {
    if (profile.audio_only) {
      QString codec;
      for (const auto value : source_streams_) { const auto stream = value.toObject(); if (stream.value("type").toInt(-1) == 1) { codec = stream.value("codec").toString().toLower(); break; } }
      if (codec == "aac") return "aac";
      if (codec == "ac3") return "ac3";
      if (codec == "eac3") return "eac3";
      if (codec == "mp2") return "mp2";
      if (codec == "mp3") return "mp3";
      if (codec == "opus") return "opus";
      if (codec == "flac") return "flac";
      if (codec == "dts") return "dts";
      if (codec == "truehd") return "thd";
      if (codec.startsWith("pcm")) return "wav";
      return "aac";
    }
    if (profile.file_type != "Elementary") return profile.file_extension;
    QString codec = profile.video_encoder;
    if (codec == "source") for (const auto value : source_streams_) { const auto stream = value.toObject(); if (stream.value("type").toInt(-1) == 0) { codec = stream.value("codec").toString().toLower(); break; } }
    if (codec == "h264" || codec == "avc") return "h264";
    if (codec == "hevc" || codec == "h265") return "hevc";
    if (codec == "av1") return "obu";
    if (codec == "vp9") return "ivf";
    return "m2v";
  }
  static QStringList audio_profile_arguments(const OutputProfile& profile) { if (profile.audio_codec == "copy") return {}; return {"--audio-codec", profile.audio_codec, "--audio-gain-db", QString::number(profile.audio_gain_db, 'f', 2), "--audio-bitrate", QString::number(profile.audio_bitrate), "--audio-sample-rate", QString::number(profile.audio_sample_rate), "--audio-channels", QString::number(profile.audio_channels)}; }
  static bool video_profile_transform_requested(const OutputProfile& profile) { return profile.video_encoder != "source" || profile.video_width > 0 || profile.video_height > 0 || profile.video_bit_depth > 0 || profile.video_bitrate_kbps > 0 || profile.video_max_bitrate_kbps > 0 || profile.aspect_ratio != "No Change" || profile.deinterlace != "None" || profile.frame_rate != "No Change"; }
  QString hardware_codec_key(const OutputProfile& profile) const {
    QString codec = profile.video_encoder.toLower();
    if (codec == "source") {
      for (const auto value : source_streams_) {
        const auto stream = value.toObject();
        if (stream.value("type").toInt(-1) == 0) {
          codec = stream.value("codec").toString().toLower();
          break;
        }
      }
    }
    if (codec == "mpeg1video" || codec == "mpeg2video") return "mpeg2";
    if (codec == "avc") return "h264";
    if (codec == "h265") return "hevc";
    if (codec == "prores_ks") return "prores";
    return codec;
  }
  QStringList video_profile_arguments(const OutputProfile& profile) const {
    const bool evaluation = !license_.paid() && !profile.audio_only;
    if (!video_profile_transform_requested(profile) && !evaluation) return {};
    QString hardware = profile.hardware_encoder;
    if (!hardware_transcoding_enabled_) {
      hardware = "software";
    } else if (hardware == "auto") {
      const auto preference = hardware_codec_backends_.value(hardware_codec_key(profile), "global");
      hardware = preference == "global" ? hardware_transcoding_backend_ : preference;
    }
    const auto codec_key = hardware_codec_key(profile);
    const bool vulkan_codec = codec_key == "ffv1" || codec_key == "prores";
    const bool mpeg2_codec = codec_key == "mpeg2";
    const bool vp9_codec = codec_key == "vp9";
    const bool incompatible_backend =
        (vulkan_codec && !QStringList{"auto", "software", "vulkan"}.contains(hardware)) ||
        (!vulkan_codec && hardware == "vulkan") ||
        (mpeg2_codec && !QStringList{"auto", "software", "qsv"}.contains(hardware)) ||
        (vp9_codec && !QStringList{"auto", "software"}.contains(hardware));
    if (incompatible_backend) hardware = "auto";
    QStringList arguments{"--video-codec", profile.video_encoder, "--video-hardware", hardware, "--video-width", QString::number(profile.video_width), "--video-height", QString::number(profile.video_height), "--video-bit-depth", QString::number(profile.video_bit_depth), "--video-bitrate-kbps", QString::number(profile.video_bitrate_kbps), "--video-max-bitrate-kbps", QString::number(profile.video_max_bitrate_kbps), "--video-aspect", profile.aspect_ratio, "--video-deinterlace", profile.deinterlace, "--video-frame-rate", profile.frame_rate};
    if (evaluation) arguments << "--trial-watermark" << "true";
    return arguments;
  }
  static QStringList transport_stream_profile_arguments(const OutputProfile& profile) { if (profile.file_type != "TS" && profile.file_type != "M2TS") return {}; return {"--ts-transport-stream-id", QString::number(profile.ts_transport_stream_id), "--ts-original-network-id", QString::number(profile.ts_original_network_id), "--ts-service-id", QString::number(profile.ts_service_id), "--ts-pmt-start-pid", QString::number(profile.ts_pmt_start_pid), "--ts-start-pid", QString::number(profile.ts_start_pid), "--ts-mux-rate", QString::number(profile.ts_mux_rate), "--ts-pcr-period-ms", QString::number(profile.ts_pcr_period_ms), "--ts-pat-period", QString::number(profile.ts_pat_period, 'f', 3), "--ts-sdt-period", QString::number(profile.ts_sdt_period, 'f', 3), "--ts-m2ts-mode", profile.ts_m2ts_mode || profile.file_type == "M2TS" ? "true" : "false", "--ts-resend-headers", profile.ts_resend_headers ? "true" : "false", "--ts-system-b", profile.ts_system_b ? "true" : "false", "--ts-emit-nit", profile.ts_emit_nit ? "true" : "false", "--ts-service-name", profile.ts_service_name, "--ts-service-provider", profile.ts_service_provider}; }
  static void apply_output_profile(BatchJob& job, const OutputProfile& profile) { job.mode = profile.mode == "keyframe" ? "keyframe" : profile.mode == "full" || video_profile_transform_requested(profile) ? "full" : "smart"; job.audio_codec = profile.audio_codec; job.audio_gain_db = profile.audio_gain_db; job.audio_bitrate = profile.audio_bitrate; job.audio_sample_rate = profile.audio_sample_rate; job.audio_channels = profile.audio_channels; job.video_encoder = profile.video_encoder; job.hardware_encoder = profile.hardware_encoder; job.aspect_ratio = profile.aspect_ratio; job.video_width = profile.video_width; job.video_height = profile.video_height; job.video_bit_depth = profile.video_bit_depth; job.video_bitrate_kbps = profile.video_bitrate_kbps; job.video_max_bitrate_kbps = profile.video_max_bitrate_kbps; job.deinterlace = profile.deinterlace; job.frame_rate = profile.frame_rate; job.ts_transport_stream_id = profile.ts_transport_stream_id; job.ts_original_network_id = profile.ts_original_network_id; job.ts_service_id = profile.ts_service_id; job.ts_pmt_start_pid = profile.ts_pmt_start_pid; job.ts_start_pid = profile.ts_start_pid; job.ts_mux_rate = profile.ts_mux_rate; job.ts_pcr_period_ms = profile.ts_pcr_period_ms; job.ts_pat_period = profile.ts_pat_period; job.ts_sdt_period = profile.ts_sdt_period; job.ts_m2ts_mode = profile.ts_m2ts_mode || profile.file_type == "M2TS"; job.ts_resend_headers = profile.ts_resend_headers; job.ts_system_b = profile.ts_system_b; job.ts_emit_nit = profile.ts_emit_nit; job.ts_service_name = profile.ts_service_name; job.ts_service_provider = profile.ts_service_provider; }
  static QStringList batch_audio_arguments(const BatchJob& job) { OutputProfile profile; profile.audio_codec = job.audio_codec; profile.audio_gain_db = job.audio_gain_db; profile.audio_bitrate = job.audio_bitrate; profile.audio_sample_rate = job.audio_sample_rate; profile.audio_channels = job.audio_channels; return audio_profile_arguments(profile); }
  QStringList batch_video_arguments(const BatchJob& job) const { OutputProfile profile; profile.video_encoder = job.video_encoder; profile.hardware_encoder = job.hardware_encoder; profile.aspect_ratio = job.aspect_ratio; profile.video_width = job.video_width; profile.video_height = job.video_height; profile.video_bit_depth = job.video_bit_depth; profile.video_bitrate_kbps = job.video_bitrate_kbps; profile.video_max_bitrate_kbps = job.video_max_bitrate_kbps; profile.deinterlace = job.deinterlace; profile.frame_rate = job.frame_rate; profile.audio_only = QStringList{"aac", "ac3", "eac3", "mp2", "mp3", "opus", "flac", "wav", "dts", "thd"}.contains(QFileInfo(job.output).suffix().toLower()); return video_profile_arguments(profile); }
  static QStringList batch_transport_stream_arguments(const BatchJob& job) { if (!QStringList{"ts", "m2ts", "mts"}.contains(QFileInfo(job.output).suffix().toLower())) return {}; OutputProfile profile; profile.file_type = job.ts_m2ts_mode ? "M2TS" : "TS"; profile.ts_transport_stream_id = job.ts_transport_stream_id; profile.ts_original_network_id = job.ts_original_network_id; profile.ts_service_id = job.ts_service_id; profile.ts_pmt_start_pid = job.ts_pmt_start_pid; profile.ts_start_pid = job.ts_start_pid; profile.ts_mux_rate = job.ts_mux_rate; profile.ts_pcr_period_ms = job.ts_pcr_period_ms; profile.ts_pat_period = job.ts_pat_period; profile.ts_sdt_period = job.ts_sdt_period; profile.ts_m2ts_mode = job.ts_m2ts_mode; profile.ts_resend_headers = job.ts_resend_headers; profile.ts_system_b = job.ts_system_b; profile.ts_emit_nit = job.ts_emit_nit; profile.ts_service_name = job.ts_service_name; profile.ts_service_provider = job.ts_service_provider; return transport_stream_profile_arguments(profile); }
  int import_videoredo_profiles(const QString &path)
  {
    QFile file(path);
    if (!file.open(QIODevice::ReadOnly))
      return 0;
    QXmlStreamReader xml(&file);
    std::optional<OutputProfile> profile;
    bool video = false;
    bool audio = false;
    int imported = 0;
    const auto extension_for = [](const QString &type) {
      const auto value = type.toLower();
      if (value == "mpg" || value == "tivo")
        return QString("mpg");
      if (value == "ts" || value == "tivo-ts")
        return QString("ts");
      if (value == "m2ts")
        return QString("m2ts");
      if (value == "mp4")
        return QString("mp4");
      if (value == "mkv")
        return QString("mkv");
      if (value == "rec")
        return QString("rec");
      if (value == "dvrms")
        return QString("dvr-ms");
      if (value == "wtv")
        return QString("wtv");
      if (value == "mxf")
        return QString("mxf");
      return QString("mkv");
    };
    while (!xml.atEnd())
    {
      xml.readNext();
      if (xml.isStartElement())
      {
        const auto name = xml.name().toString();
        if (name == "VRDOutputProfile")
        {
          profile.emplace();
          const auto attributes = xml.attributes();
          const auto source_id = attributes.value("id").toString();
          profile->id =
              QString("vrd-%1-%2")
                  .arg(source_id.isEmpty() ? QString::number(QDateTime::currentMSecsSinceEpoch())
                                           : source_id,
                       QString::number(qHash(path + source_id), 16));
          profile->enabled =
              attributes.value("enabled").toString().compare("false", Qt::CaseInsensitive) != 0;
          profile->favorite =
              attributes.value("favorite").toString().compare("true", Qt::CaseInsensitive) == 0;
          continue;
        }
        if (!profile)
          continue;
        if (name == "VideoAttributes")
        {
          video = true;
          continue;
        }
        if (name == "AudioAttributes")
        {
          audio = true;
          continue;
        }
        if (name == "EncodeDimensions")
          continue;
        auto text = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed();
        if (name == "Name")
          profile->name = text;
        else if (name == "OutputMode")
          profile->mode = text.compare("Smart", Qt::CaseInsensitive) == 0 ? "intelligent" : "full";
        else if (name == "FileType")
        {
          profile->file_type = text;
          profile->file_extension = extension_for(text);
        }
        else if (name == "FileExt" && !text.isEmpty())
          profile->file_extension = text.startsWith('.') ? text.mid(1) : text;
        else if (video && name == "Encoder")
          profile->video_encoder = text.compare("Match", Qt::CaseInsensitive) == 0 ||
                                           text.compare("Auto", Qt::CaseInsensitive) == 0
                                       ? "source"
                                       : text.toLower().remove('-').remove('.');
        else if (video && name == "AspectRatio")
          profile->aspect_ratio = text;
        else if (video && name == "Width")
          profile->video_width = text.toInt();
        else if (video && name == "Height")
          profile->video_height = text.toInt();
        else if (video && name == "BitDepth")
          profile->video_bit_depth = text.toInt();
        else if (video && name == "EncodeBitrate")
          profile->video_bitrate_kbps = text.toInt();
        else if (video && name == "MaxBitrate")
          profile->video_max_bitrate_kbps = text.toInt();
        else if (video && name == "DeinterlaceMethod")
          profile->deinterlace = text;
        else if (video && name == "FrameRate")
          profile->frame_rate = text;
        else if (audio && name == "Encoder")
        {
          const auto codec = text.toLower();
          profile->audio_codec = codec == "auto" || codec == "match"                ? "copy"
                                 : codec.contains("ac3") || codec.contains("dolby") ? "ac3"
                                 : codec.contains("aac")                            ? "aac"
                                 : codec.contains("mp2") || codec.contains("mpeg audio") ? "mp2"
                                 : codec.contains("pcm") || codec.contains("lpcm") ? "pcm_s16le"
                                 : codec.contains("opus")                           ? "opus"
                                                                                    : "source";
        }
        else if (audio && name == "Bitrate")
          profile->audio_bitrate = text.toInt() > 0 ? text.toInt() * 1000 : 192000;
        else if (audio && name == "SampleRate")
          profile->audio_sample_rate =
              text.contains("No Change", Qt::CaseInsensitive)
                  ? 0
                  : text.remove("Hz", Qt::CaseInsensitive).remove(',').trimmed().toInt();
        else if (audio && name == "ChannelMode")
          profile->audio_channels = text.contains("stereo", Qt::CaseInsensitive) ? 2
                                    : text.contains("mono", Qt::CaseInsensitive) ? 1
                                    : text.contains("5.1")                       ? 6
                                    : text.contains("7.1")                       ? 8
                                                                                 : 0;
      }
      else if (xml.isEndElement())
      {
        const auto name = xml.name().toString();
        if (name == "VideoAttributes")
          video = false;
        else if (name == "AudioAttributes")
          audio = false;
        else if (name == "VRDOutputProfile" && profile)
        {
          if (!profile->name.isEmpty())
          {
            profile->description =
                QString("Imported from VideoReDo output profile (%1, %2 encoder).")
                    .arg(profile->file_type, profile->video_encoder);
            const auto duplicate =
                std::find_if(custom_profiles_.begin(), custom_profiles_.end(),
                             [profile = *profile](const OutputProfile &existing) {
                               return existing.name.compare(profile.name, Qt::CaseInsensitive) == 0;
                             });
            if (duplicate == custom_profiles_.end())
              custom_profiles_.push_back(*profile);
            else
              *duplicate = *profile;
            ++imported;
          }
          profile.reset();
          video = false;
          audio = false;
        }
      }
    }
    if (xml.hasError())
      return 0;
    if (imported > 0)
    {
      save_custom_profiles();
      load_custom_profiles();
    }
    return imported;
  }
  bool export_videoredo_profiles(const QString &path) const
  {
    QSaveFile file(path);
    if (!file.open(QIODevice::WriteOnly))
      return false;
    QXmlStreamWriter xml(&file);
    xml.setAutoFormatting(true);
    xml.writeStartDocument();
    xml.writeStartElement("VRDOutputProfileList");
    xml.writeAttribute("version", "2");
    int sequence = 1000;
    const auto encoder_name = [](const QString &value) {
      if (value == "source")
        return QString("Match");
      if (value == "mpeg2")
        return QString("MPEG2");
      if (value == "h264")
        return QString("H264");
      if (value == "hevc")
        return QString("HEVC");
      return value.toUpper();
    };
    const auto audio_name = [](const QString &value) {
      if (value == "copy")
        return QString("Auto");
      if (value == "ac3")
        return QString("AC3");
      if (value == "mp2")
        return QString("MP2");
      if (value == "pcm_s16le")
        return QString("LPCM");
      return value.toUpper();
    };
    for (const auto &profile : custom_profiles_)
    {
      xml.writeStartElement("VRDOutputProfile");
      xml.writeAttribute("id", QString::number(sequence++));
      xml.writeAttribute("enabled", profile.enabled ? "true" : "false");
      xml.writeAttribute("favorite", profile.favorite ? "true" : "false");
      xml.writeTextElement("Name", profile.name);
      xml.writeTextElement("OutputMode", profile.mode == "intelligent" || profile.mode == "keyframe"
                                             ? "Smart"
                                             : "Force Recode");
      xml.writeTextElement("RecodeMode", "Quality");
      xml.writeTextElement("FileType", profile.file_type);
      xml.writeTextElement("FileExt", profile.file_extension);
      xml.writeStartElement("OutputLibrary");
      xml.writeTextElement("Program", "None");
      xml.writeTextElement("CustomPath", "");
      xml.writeEndElement();
      xml.writeTextElement("DefaultOutputDir", "");
      xml.writeStartElement("VideoAttributes");
      xml.writeTextElement("Encoder", encoder_name(profile.video_encoder));
      xml.writeTextElement("AspectRatio", profile.aspect_ratio);
      xml.writeTextElement("AspectCorrection", "None");
      xml.writeStartElement("EncodeDimensions");
      xml.writeTextElement("Width",
                           QString::number(profile.video_width > 0 ? profile.video_width : 3840));
      xml.writeTextElement("Height",
                           QString::number(profile.video_height > 0 ? profile.video_height : 2160));
      xml.writeEndElement();
      if (profile.video_bit_depth > 0)
        xml.writeTextElement("BitDepth", QString::number(profile.video_bit_depth));
      xml.writeTextElement("EncodeBitrate", QString::number(profile.video_bitrate_kbps));
      xml.writeTextElement("HeaderBitrate", QString::number(profile.video_bitrate_kbps));
      xml.writeTextElement("MaxBitrate", QString::number(profile.video_max_bitrate_kbps));
      xml.writeTextElement("EncodingMode", "SP");
      xml.writeTextElement("DeinterlaceMethod", profile.deinterlace);
      xml.writeTextElement("FrameRate", profile.frame_rate);
      xml.writeEndElement();
      xml.writeStartElement("AudioAttributes");
      xml.writeTextElement("Encoder", audio_name(profile.audio_codec));
      xml.writeTextElement("Bitrate", QString::number(profile.audio_bitrate / 1000));
      xml.writeTextElement("SampleRate", profile.audio_sample_rate > 0
                                             ? QString::number(profile.audio_sample_rate)
                                             : "No Change");
      xml.writeTextElement("ChannelMode", profile.audio_channels == 1   ? "Mono"
                                          : profile.audio_channels == 2 ? "Stereo"
                                          : profile.audio_channels == 6 ? "5.1"
                                          : profile.audio_channels == 8 ? "7.1"
                                                                        : "Auto");
      xml.writeTextElement("LPCMEndianess", "Little");
      xml.writeEndElement();
      xml.writeEndElement();
    }
    xml.writeEndElement();
    xml.writeEndDocument();
    return !xml.hasError() && file.commit();
  }
  bool edit_output_profile(OutputProfile &profile, QWidget *parent)
  {
    QDialog dialog(parent);
    dialog.setWindowTitle(profile.name.isEmpty() ? "New Output Profile" : "Edit Output Profile");
    dialog.resize(760, 900);
    auto *layout = new QVBoxLayout(&dialog);
    auto *form = new QFormLayout;
    auto *name = new QLineEdit(profile.name, &dialog);
    auto *mode = new QComboBox(&dialog);
    mode->addItem("Intelligent smart render", "intelligent");
    mode->addItem("Keyframe-only packet copy", "keyframe");
    mode->addItem("Full source-codec transcode", "full");
    mode->addItem("QuickStream Fix", "quickstream");
    const auto mode_index = mode->findData(profile.mode);
    mode->setCurrentIndex(mode_index < 0 ? 0 : mode_index);
    auto *description = new QLineEdit(profile.description, &dialog);
    auto *container = new QComboBox(&dialog);
    for (const auto &value :
         std::vector<std::pair<QString, QString>>{{"Matroska MKV", "MKV"},
                                                  {"MPEG-4", "MP4"},
                                                  {"QuickTime MOV", "MOV"},
                                                  {"Transport Stream", "TS"},
                                                  {"Blu-ray M2TS", "M2TS"},
                                                  {"Program Stream", "MPG"},
                                                  {"Elementary Streams", "Elementary"},
                                                  {"WTV", "WTV"},
                                                  {"DVR-MS", "DVRMS"},
                                                  {"Topfield REC", "REC"},
                                                  {"MXF", "MXF"}})
      container->addItem(value.first, value.second);
    container->setCurrentIndex(std::max(0, container->findData(profile.file_type)));
    auto *video_encoder = new QComboBox(&dialog);
    video_encoder->addItem("Match input codec", "source");
    video_encoder->addItem("MPEG-2", "mpeg2");
    video_encoder->addItem("H.264 / AVC", "h264");
    video_encoder->addItem("H.265 / HEVC", "hevc");
    video_encoder->addItem("AV1", "av1");
    video_encoder->addItem("VP9", "vp9");
    video_encoder->addItem("FFV1 Lossless", "ffv1");
    video_encoder->addItem("Apple ProRes", "prores");
    video_encoder->setCurrentIndex(std::max(0, video_encoder->findData(profile.video_encoder)));
    auto *hardware = new QComboBox(&dialog);
    hardware->addItem("Automatic", "auto");
    hardware->addItem("Software", "software");
    hardware->addItem("Intel Quick Sync", "qsv");
    hardware->addItem("NVIDIA NVENC", "nvenc");
    hardware->addItem("AMD AMF", "amf");
    hardware->addItem("Vulkan (FFV1 / ProRes)", "vulkan");
    hardware->setCurrentIndex(std::max(0, hardware->findData(profile.hardware_encoder)));
    auto *dimensions = new QWidget(&dialog);
    auto *dimensions_layout = new QHBoxLayout(dimensions);
    dimensions_layout->setContentsMargins(0, 0, 0, 0);
    auto *width = new QSpinBox(dimensions);
    auto *height = new QSpinBox(dimensions);
    for (auto *value : {width, height})
    {
      value->setRange(0, 8192);
      value->setSpecialValueText("Match");
    }
    width->setValue(profile.video_width);
    height->setValue(profile.video_height);
    dimensions_layout->addWidget(width);
    dimensions_layout->addWidget(new QLabel("×", dimensions));
    dimensions_layout->addWidget(height);
    auto *bit_depth = new QComboBox(&dialog);
    bit_depth->addItem("Match source", 0);
    bit_depth->addItem("8-bit", 8);
    bit_depth->addItem("10-bit", 10);
    bit_depth->setCurrentIndex(std::max(0, bit_depth->findData(profile.video_bit_depth)));
    auto *video_bitrate = new QSpinBox(&dialog);
    video_bitrate->setRange(0, 200000);
    video_bitrate->setSpecialValueText("Automatic");
    video_bitrate->setSuffix(" kbps");
    video_bitrate->setValue(profile.video_bitrate_kbps);
    auto *video_max_bitrate = new QSpinBox(&dialog);
    video_max_bitrate->setRange(0, 300000);
    video_max_bitrate->setSpecialValueText("Automatic");
    video_max_bitrate->setSuffix(" kbps");
    video_max_bitrate->setValue(profile.video_max_bitrate_kbps);
    auto *aspect = new QComboBox(&dialog);
    for (const auto &value : {"No Change", "1:1", "4:3", "16:9", "2.21:1"})
      aspect->addItem(value);
    aspect->setCurrentText(profile.aspect_ratio);
    auto *deinterlace = new QComboBox(&dialog);
    for (const auto &value : {"None", "Auto", "Bob", "Motion Adaptive"})
      deinterlace->addItem(value);
    deinterlace->setCurrentText(profile.deinterlace);
    auto *frame_rate = new QComboBox(&dialog);
    for (const auto &value :
         {"No Change", "23.976", "24", "25", "29.97", "30", "50", "59.94", "60"})
      frame_rate->addItem(value);
    frame_rate->setCurrentText(profile.frame_rate);
    auto *favorite = new QCheckBox("Favorite", &dialog);
    favorite->setChecked(profile.favorite);
    auto *enabled = new QCheckBox("Enabled", &dialog);
    enabled->setChecked(profile.enabled);
    auto *audio = new QComboBox(&dialog);
    audio->addItem("Copy primary audio unchanged", "copy");
    audio->addItem("Match input audio codec (re-encode)", "source");
    audio->addItem("AAC", "aac");
    audio->addItem("Dolby Digital (AC-3)", "ac3");
    audio->addItem("MPEG Layer II", "mp2");
    audio->addItem("Linear PCM 16-bit", "pcm_s16le");
    audio->addItem("Opus", "opus");
    audio->setCurrentIndex(std::max(0, audio->findData(profile.audio_codec)));
    auto *gain = new QDoubleSpinBox(&dialog);
    gain->setRange(-60.0, 24.0);
    gain->setDecimals(1);
    gain->setSingleStep(0.5);
    gain->setSuffix(" dB");
    gain->setValue(profile.audio_gain_db);
    auto *bitrate = new QSpinBox(&dialog);
    bitrate->setRange(16, 1536);
    bitrate->setSuffix(" kbps");
    bitrate->setValue(std::clamp(profile.audio_bitrate / 1000, 16, 1536));
    auto *sample_rate = new QComboBox(&dialog);
    sample_rate->addItem("Match source", 0);
    sample_rate->addItem("44.1 kHz", 44100);
    sample_rate->addItem("48 kHz", 48000);
    sample_rate->addItem("96 kHz", 96000);
    sample_rate->setCurrentIndex(std::max(0, sample_rate->findData(profile.audio_sample_rate)));
    auto *channels = new QComboBox(&dialog);
    channels->addItem("Match source", 0);
    channels->addItem("Mono", 1);
    channels->addItem("Stereo", 2);
    channels->addItem("5.1 surround", 6);
    channels->addItem("7.1 surround", 8);
    channels->setCurrentIndex(std::max(0, channels->findData(profile.audio_channels)));
    form->addRow("Profile name:", name);
    form->addRow("Processing mode:", mode);
    form->addRow("Description:", description);
    form->addRow("Container:", container);
    form->addRow("Video encoder:", video_encoder);
    form->addRow("Hardware acceleration:", hardware);
    form->addRow("Output dimensions:", dimensions);
    form->addRow("Pixel bit depth:", bit_depth);
    form->addRow("Video bitrate:", video_bitrate);
    form->addRow("Maximum bitrate:", video_max_bitrate);
    form->addRow("Aspect ratio:", aspect);
    form->addRow("Deinterlace:", deinterlace);
    form->addRow("Frame rate:", frame_rate);
    auto *flags = new QWidget(&dialog);
    auto *flags_layout = new QHBoxLayout(flags);
    flags_layout->setContentsMargins(0, 0, 0, 0);
    flags_layout->addWidget(enabled);
    flags_layout->addWidget(favorite);
    flags_layout->addStretch();
    form->addRow("Profile flags:", flags);
    form->addRow("Primary audio:", audio);
    form->addRow("Audio gain:", gain);
    form->addRow("Audio bitrate:", bitrate);
    form->addRow("Sample rate:", sample_rate);
    form->addRow("Channels:", channels);
    layout->addLayout(form);
    auto *transport_group = new QGroupBox("MPEG Transport Stream Multiplexer", &dialog);
    auto *transport_form = new QFormLayout(transport_group);
    const auto integer_control = [transport_group](int minimum, int maximum, int value) {
      auto *control = new QSpinBox(transport_group);
      control->setRange(minimum, maximum);
      control->setValue(value);
      return control;
    };
    auto *ts_transport_id = integer_control(0, 65535, profile.ts_transport_stream_id);
    auto *ts_network_id = integer_control(0, 65535, profile.ts_original_network_id);
    auto *ts_service_id = integer_control(0, 65535, profile.ts_service_id);
    auto *ts_pmt_pid = integer_control(0x20, 0x1ffe, profile.ts_pmt_start_pid);
    auto *ts_start_pid = integer_control(0x20, 0x1ffe, profile.ts_start_pid);
    for (auto *control : {ts_pmt_pid, ts_start_pid}) {
      control->setDisplayIntegerBase(16);
      control->setPrefix("0x");
    }
    auto *ts_mux_rate = integer_control(0, 200000000, profile.ts_mux_rate);
    ts_mux_rate->setSpecialValueText("Variable");
    ts_mux_rate->setSuffix(" bit/s");
    auto *ts_pcr_period = integer_control(-1, 1000, profile.ts_pcr_period_ms);
    ts_pcr_period->setSpecialValueText("Automatic");
    ts_pcr_period->setSuffix(" ms");
    auto *ts_pat_period = new QDoubleSpinBox(transport_group);
    auto *ts_sdt_period = new QDoubleSpinBox(transport_group);
    for (auto *control : {ts_pat_period, ts_sdt_period}) {
      control->setRange(0.01, 10.0);
      control->setDecimals(3);
      control->setSuffix(" s");
    }
    ts_pat_period->setValue(profile.ts_pat_period);
    ts_sdt_period->setValue(profile.ts_sdt_period);
    auto *ts_service_name = new QLineEdit(profile.ts_service_name, transport_group);
    auto *ts_service_provider = new QLineEdit(profile.ts_service_provider, transport_group);
    auto *ts_flags = new QWidget(transport_group);
    auto *ts_flags_layout = new QGridLayout(ts_flags);
    ts_flags_layout->setContentsMargins(0, 0, 0, 0);
    auto *ts_m2ts = new QCheckBox("192-byte M2TS packets", ts_flags);
    auto *ts_resend = new QCheckBox("Resend PAT/PMT at keyframes", ts_flags);
    auto *ts_system_b = new QCheckBox("DVB System B", ts_flags);
    auto *ts_nit = new QCheckBox("Emit NIT", ts_flags);
    ts_m2ts->setChecked(profile.ts_m2ts_mode || profile.file_type == "M2TS");
    ts_resend->setChecked(profile.ts_resend_headers);
    ts_system_b->setChecked(profile.ts_system_b);
    ts_nit->setChecked(profile.ts_emit_nit);
    ts_flags_layout->addWidget(ts_m2ts, 0, 0);
    ts_flags_layout->addWidget(ts_resend, 0, 1);
    ts_flags_layout->addWidget(ts_system_b, 1, 0);
    ts_flags_layout->addWidget(ts_nit, 1, 1);
    auto *ids = new QWidget(transport_group);
    auto *ids_layout = new QHBoxLayout(ids);
    ids_layout->setContentsMargins(0, 0, 0, 0);
    ids_layout->addWidget(new QLabel("TS", ids)); ids_layout->addWidget(ts_transport_id);
    ids_layout->addWidget(new QLabel("Network", ids)); ids_layout->addWidget(ts_network_id);
    ids_layout->addWidget(new QLabel("Service", ids)); ids_layout->addWidget(ts_service_id);
    auto *pids = new QWidget(transport_group);
    auto *pids_layout = new QHBoxLayout(pids);
    pids_layout->setContentsMargins(0, 0, 0, 0);
    pids_layout->addWidget(new QLabel("PMT", pids)); pids_layout->addWidget(ts_pmt_pid);
    pids_layout->addWidget(new QLabel("First elementary", pids)); pids_layout->addWidget(ts_start_pid);
    auto *periods = new QWidget(transport_group);
    auto *periods_layout = new QHBoxLayout(periods);
    periods_layout->setContentsMargins(0, 0, 0, 0);
    periods_layout->addWidget(new QLabel("PCR", periods)); periods_layout->addWidget(ts_pcr_period);
    periods_layout->addWidget(new QLabel("PAT", periods)); periods_layout->addWidget(ts_pat_period);
    periods_layout->addWidget(new QLabel("SDT", periods)); periods_layout->addWidget(ts_sdt_period);
    transport_form->addRow("Identifiers:", ids);
    transport_form->addRow("Packet IDs:", pids);
    transport_form->addRow("Constant mux rate:", ts_mux_rate);
    transport_form->addRow("Table/PCR periods:", periods);
    transport_form->addRow("Service name:", ts_service_name);
    transport_form->addRow("Provider:", ts_service_provider);
    transport_form->addRow("Flags:", ts_flags);
    layout->addWidget(transport_group);
    const auto update_transport = [container, transport_group] {
      const auto type = container->currentData().toString();
      transport_group->setEnabled(type == "TS" || type == "M2TS");
    };
    update_transport();
    connect(container, &QComboBox::currentIndexChanged, &dialog,
            [update_transport](int) { update_transport(); });
    const auto update_audio = [audio, gain, bitrate, sample_rate, channels] {
      const bool encode = audio->currentData().toString() != "copy";
      gain->setEnabled(encode);
      bitrate->setEnabled(encode);
      sample_rate->setEnabled(encode);
      channels->setEnabled(encode);
    };
    update_audio();
    connect(audio, &QComboBox::currentIndexChanged, &dialog,
            [update_audio](int) { update_audio(); });
    auto *note =
        new QLabel("Matching video settings preserve smart-render packet copying outside repaired GOPs. "
                   "Codec, size, bit-depth, frame-rate or deinterlace changes select validated full video "
                   "transcode. Primary-audio transforms affect only that track; other compatible streams "
                   "are copied into the selected delivery container.",
                   &dialog);
    note->setWordWrap(true);
    layout->addWidget(note);
    auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog);
    layout->addWidget(buttons);
    connect(buttons, &QDialogButtonBox::accepted, &dialog, [&dialog, name] {
      if (!name->text().trimmed().isEmpty())
        dialog.accept();
    });
    connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
    if (dialog.exec() != QDialog::Accepted)
      return false;
    profile.name = name->text().trimmed();
    profile.mode = mode->currentData().toString();
    profile.description = description->text().trimmed();
    profile.file_type = container->currentData().toString();
    profile.video_encoder = video_encoder->currentData().toString();
    profile.file_extension = profile.file_type.toLower();
    if (profile.file_extension == "dvrms")
      profile.file_extension = "dvr-ms";
    if (profile.file_extension == "elementary")
      profile.file_extension = profile.video_encoder == "h264" ? "h264" : profile.video_encoder == "hevc" ? "hevc" : profile.video_encoder == "av1" ? "obu" : profile.video_encoder == "vp9" ? "ivf" : "m2v";
    profile.hardware_encoder = hardware->currentData().toString();
    profile.video_width = width->value();
    profile.video_height = height->value();
    profile.video_bit_depth = bit_depth->currentData().toInt();
    profile.video_bitrate_kbps = video_bitrate->value();
    profile.video_max_bitrate_kbps = video_max_bitrate->value();
    profile.aspect_ratio = aspect->currentText();
    profile.deinterlace = deinterlace->currentText();
    profile.frame_rate = frame_rate->currentText();
    profile.ts_transport_stream_id = ts_transport_id->value();
    profile.ts_original_network_id = ts_network_id->value();
    profile.ts_service_id = ts_service_id->value();
    profile.ts_pmt_start_pid = ts_pmt_pid->value();
    profile.ts_start_pid = ts_start_pid->value();
    profile.ts_mux_rate = ts_mux_rate->value();
    profile.ts_pcr_period_ms = ts_pcr_period->value();
    profile.ts_pat_period = ts_pat_period->value();
    profile.ts_sdt_period = ts_sdt_period->value();
    profile.ts_m2ts_mode = ts_m2ts->isChecked();
    profile.ts_resend_headers = ts_resend->isChecked();
    profile.ts_system_b = ts_system_b->isChecked();
    profile.ts_emit_nit = ts_nit->isChecked();
    profile.ts_service_name = ts_service_name->text().trimmed();
    profile.ts_service_provider = ts_service_provider->text().trimmed();
    profile.enabled = enabled->isChecked();
    profile.favorite = favorite->isChecked();
    profile.audio_codec = audio->currentData().toString();
    profile.audio_gain_db = gain->value();
    profile.audio_bitrate = bitrate->value() * 1000;
    profile.audio_sample_rate = sample_rate->currentData().toInt();
    profile.audio_channels = channels->currentData().toInt();
    if (profile.id.isEmpty())
      profile.id = QString("custom-%1").arg(QDateTime::currentMSecsSinceEpoch());
    return true;
  }
  void show_output_profiles()
  {
    QDialog dialog(this);
    dialog.setWindowTitle("Output Profile Manager");
    dialog.resize(720, 430);
    auto *layout = new QVBoxLayout(&dialog);
    auto *profiles = new QListWidget(&dialog);
    auto *description = new QLabel(&dialog);
    description->setWordWrap(true);
    layout->addWidget(profiles, 1);
    layout->addWidget(description);
    const auto refresh = [this, profiles] {
      const auto selected = profiles->currentRow();
      profiles->clear();
      for (int index = 0; index < output_profile_->count(); ++index)
      {
        auto *item = new QListWidgetItem(output_profile_->itemText(index), profiles);
        item->setData(Qt::UserRole, output_profile_->itemData(index));
      }
      profiles->setCurrentRow(std::clamp(selected, 0, std::max(0, profiles->count() - 1)));
    };
    const auto update_description = [this, profiles, description] {
      const auto *item = profiles->currentItem();
      if (!item)
      {
        description->clear();
        return;
      }
      const auto id = item->data(Qt::UserRole).toString();
      const auto found =
          std::find_if(custom_profiles_.cbegin(), custom_profiles_.cend(),
                       [&id](const OutputProfile &profile) { return profile.id == id; });
      if (found == custom_profiles_.cend())
        description->setText("Built-in validated profile. Clone it to create an editable variant.");
      else
        description->setText(
            QString("%1\nPrimary audio: %2%3 at %4 kbps; sample rate %5; channels %6.")
                .arg(found->description, found->audio_codec,
                     found->audio_codec == "copy"
                         ? ""
                         : QString(", gain %1 dB").arg(found->audio_gain_db, 0, 'f', 1))
                .arg(found->audio_bitrate / 1000)
                .arg(found->audio_sample_rate == 0 ? "source"
                                                   : QString::number(found->audio_sample_rate))
                .arg(found->audio_channels == 0 ? "source"
                                                : QString::number(found->audio_channels)));
    };
    refresh();
    profiles->setCurrentRow(output_profile_->currentIndex());
    update_description();
    connect(profiles, &QListWidget::currentRowChanged, &dialog,
            [update_description](int) { update_description(); });
    auto *controls = new QHBoxLayout;
    auto *create = new QPushButton("New…", &dialog);
    auto *clone = new QPushButton("Clone…", &dialog);
    auto *edit = new QPushButton("Edit…", &dialog);
    auto *remove = new QPushButton("Delete", &dialog);
    auto *import_vrd = new QPushButton("Import VideoReDo…", &dialog);
    auto *export_vrd = new QPushButton("Export VideoReDo…", &dialog);
    controls->addWidget(create);
    controls->addWidget(clone);
    controls->addWidget(edit);
    controls->addWidget(remove);
    controls->addStretch();
    controls->addWidget(import_vrd);
    controls->addWidget(export_vrd);
    layout->addLayout(controls);
    connect(create, &QPushButton::clicked, &dialog, [this, refresh, profiles, &dialog] {
      OutputProfile profile;
      if (!edit_output_profile(profile, &dialog))
        return;
      custom_profiles_.push_back(std::move(profile));
      save_custom_profiles();
      load_custom_profiles();
      refresh();
      profiles->setCurrentRow(profiles->count() - 1);
    });
    connect(clone, &QPushButton::clicked, &dialog, [this, refresh, profiles, &dialog] {
      if (!profiles->currentItem())
        return;
      const auto id = profiles->currentItem()->data(Qt::UserRole).toString();
      const auto found = std::find_if(custom_profiles_.cbegin(), custom_profiles_.cend(),
                                      [&id](const OutputProfile &value) { return value.id == id; });
      OutputProfile profile =
          found == custom_profiles_.cend() ? current_output_profile(id) : *found;
      profile.id.clear();
      profile.name =
          (found == custom_profiles_.cend() ? profiles->currentItem()->text() : found->name) +
          " Copy";
      profile.mode = output_profile_mode(id);
      profile.description = found == custom_profiles_.cend()
                                ? "Custom copy of a built-in validated profile."
                                : found->description;
      if (!edit_output_profile(profile, &dialog))
        return;
      custom_profiles_.push_back(std::move(profile));
      save_custom_profiles();
      load_custom_profiles();
      refresh();
      profiles->setCurrentRow(profiles->count() - 1);
    });
    connect(edit, &QPushButton::clicked, &dialog, [this, refresh, profiles, &dialog] { if (!profiles->currentItem()) return; const auto id = profiles->currentItem()->data(Qt::UserRole).toString(); auto found = std::find_if(custom_profiles_.begin(), custom_profiles_.end(), [&id](const OutputProfile& profile) { return profile.id == id; }); if (found == custom_profiles_.end()) { QMessageBox::information(&dialog, "Output Profiles", "Built-in profiles are read-only. Clone this profile to customize it."); return; } const auto row = profiles->currentRow(); if (!edit_output_profile(*found, &dialog)) return; save_custom_profiles(); load_custom_profiles(); refresh(); profiles->setCurrentRow(row); });
    connect(remove, &QPushButton::clicked, &dialog, [this, refresh, profiles] { if (!profiles->currentItem()) return; const auto id = profiles->currentItem()->data(Qt::UserRole).toString(); const auto before = custom_profiles_.size(); custom_profiles_.erase(std::remove_if(custom_profiles_.begin(), custom_profiles_.end(), [&id](const OutputProfile& profile) { return profile.id == id; }), custom_profiles_.end()); if (custom_profiles_.size() == before) return; save_custom_profiles(); load_custom_profiles(); refresh(); });
    connect(import_vrd, &QPushButton::clicked, &dialog, [this, refresh, profiles, &dialog] { const auto path = QFileDialog::getOpenFileName(&dialog, "Import VideoReDo Output Profiles", {}, "VideoReDo Output Profiles (*.OP.xml *.op.xml *.xml)"); if (path.isEmpty()) return; const auto count = import_videoredo_profiles(path); refresh(); if (profiles->count() > 0) profiles->setCurrentRow(profiles->count() - 1); QMessageBox::information(&dialog, "Output Profiles", count > 0 ? QString("Imported %1 VideoReDo output profile(s).").arg(count) : "The file contained no custom profile definitions."); });
    connect(export_vrd, &QPushButton::clicked, &dialog, [this, &dialog] { const auto path = QFileDialog::getSaveFileName(&dialog, "Export VideoReDo Output Profiles", {}, "VideoReDo Output Profiles (*.OP.xml)"); if (!path.isEmpty() && !export_videoredo_profiles(path)) QMessageBox::warning(&dialog, "Output Profiles", "Could not export the VideoReDo profile document."); });
    auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); buttons->button(QDialogButtonBox::Ok)->setText("Set As Default"); layout->addWidget(buttons); connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); if (dialog.exec() != QDialog::Accepted || profiles->currentRow() < 0) return; const auto id = profiles->currentItem()->data(Qt::UserRole); const auto index = output_profile_->findData(id); output_profile_->setCurrentIndex(index < 0 ? 0 : index); QSettings("SmartCut64", "SmartCut64").setValue("output/defaultProfile", output_profile_->currentData()); status_->setText(QString("Default output profile: %1").arg(output_profile_->currentText()));
  }
  static QImage adjusted_display_image(const QImage& source, int brightness, int contrast) {
    if (source.isNull() || (brightness == 0 && contrast == 0)) return source;
    QImage image = source.convertToFormat(QImage::Format_ARGB32); const double gain_base = (100.0 + std::clamp(contrast, -100, 100)) / 100.0; const double gain = gain_base * gain_base; const double offset = std::clamp(brightness, -100, 100) * 2.55;
    const auto channel = [gain, offset](int value) { return std::clamp(static_cast<int>((((value / 255.0 - 0.5) * gain) + 0.5) * 255.0 + offset + 0.5), 0, 255); };
    for (int y = 0; y < image.height(); ++y) { auto* pixels = reinterpret_cast<QRgb*>(image.scanLine(y)); for (int x = 0; x < image.width(); ++x) { const auto pixel = pixels[x]; pixels[x] = qRgba(channel(qRed(pixel)), channel(qGreen(pixel)), channel(qBlue(pixel)), qAlpha(pixel)); } }
    return image;
  }
  static QImage plot_masked_image(const QImage& source, bool visible, int size) { if (source.isNull() || !visible) return source; QImage image = source; const double fraction = std::array{0.35, 0.50, 0.65, 0.80}[static_cast<std::size_t>(std::clamp(size, 0, 3))]; const int width = std::max(1, static_cast<int>(image.width() * fraction)); const int height = std::max(1, static_cast<int>(image.height() * fraction)); QPainter painter(&image); painter.fillRect(QRect((image.width() - width) / 2, (image.height() - height) / 2, width, height), Qt::black); return image; }
  void show_display_adjustment() {
    const int original_brightness = display_brightness_; const int original_contrast = display_contrast_;
    QDialog dialog(this); dialog.setWindowTitle("Display Adjustment"); auto* layout = new QVBoxLayout(&dialog); auto* form = new QFormLayout; auto* brightness = new QSlider(Qt::Horizontal, &dialog); brightness->setRange(-100, 100); brightness->setValue(display_brightness_); brightness->setAccessibleName("Display brightness"); auto* contrast = new QSlider(Qt::Horizontal, &dialog); contrast->setRange(-100, 100); contrast->setValue(display_contrast_); contrast->setAccessibleName("Display contrast"); form->addRow("&Brightness:", brightness); form->addRow("&Contrast:", contrast); layout->addLayout(form);
    auto* note = new QLabel("These controls affect preview display only. Saved video and captured source frames are unchanged.", &dialog); note->setWordWrap(true); layout->addWidget(note); auto* reset = new QPushButton("Reset", &dialog); layout->addWidget(reset); auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); layout->addWidget(buttons);
    const auto preview = [this, brightness, contrast] { display_brightness_ = brightness->value(); display_contrast_ = contrast->value(); if (!current_preview_image_.isNull()) show_preview_image(current_preview_image_, displayed_preview_frame_); };
    connect(brightness, &QSlider::valueChanged, &dialog, [preview](int) { preview(); }); connect(contrast, &QSlider::valueChanged, &dialog, [preview](int) { preview(); }); connect(reset, &QPushButton::clicked, &dialog, [brightness, contrast] { brightness->setValue(0); contrast->setValue(0); }); connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
    if (dialog.exec() != QDialog::Accepted) { display_brightness_ = original_brightness; display_contrast_ = original_contrast; if (!current_preview_image_.isNull()) show_preview_image(current_preview_image_, displayed_preview_frame_); return; }
    QSettings settings("SmartCut64", "SmartCut64"); settings.setValue("display/brightness", display_brightness_); settings.setValue("display/contrast", display_contrast_); status_->setText(QString("Preview display adjusted: brightness %1, contrast %2").arg(display_brightness_).arg(display_contrast_));
  }
  void show_audio_adjustment() {
    const int original_offset = audio_offset_ms_; QDialog dialog(this); dialog.setWindowTitle("Adjust Audio Synchronization and Output Volume"); dialog.resize(440, 250); auto* layout = new QVBoxLayout(&dialog); auto* form = new QFormLayout; auto* offset = new QSlider(Qt::Horizontal, &dialog); offset->setRange(-1000, 1000); offset->setSingleStep(10); offset->setPageStep(50); offset->setValue(std::clamp(audio_offset_ms_, -1000, 1000)); auto* offset_value = new QSpinBox(&dialog); offset_value->setRange(-1000, 1000); offset_value->setSingleStep(10); offset_value->setSuffix(" ms"); offset_value->setValue(offset->value()); auto* offset_row = new QWidget(&dialog); auto* offset_layout = new QHBoxLayout(offset_row); offset_layout->setContentsMargins(0, 0, 0, 0); offset_layout->addWidget(offset, 1); offset_layout->addWidget(offset_value); form->addRow("Audio sync adjustment:", offset_row); layout->addLayout(form);
    auto* repeat = new QCheckBox("Repeat Selection", &dialog); repeat->setEnabled(!input_->text().isEmpty() && timeline_->has_valid_range()); layout->addWidget(repeat); auto* explanation = new QLabel("Positive values delay audio; negative values advance it. Repeat Selection loops the marked range so lip-sync changes can be judged while this dialog remains open. Save Video preserves compressed audio and shifts its timestamps without changing source volume.", &dialog); explanation->setWordWrap(true); layout->addWidget(explanation); auto* passthrough = new QLabel("Output volume: Source level (audio passthrough)", &dialog); passthrough->setEnabled(false); layout->addWidget(passthrough); auto* reset = new QPushButton("Reset To 0 ms", &dialog); layout->addWidget(reset); auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); layout->addWidget(buttons);
    connect(offset, &QSlider::valueChanged, offset_value, &QSpinBox::setValue); connect(offset_value, qOverload<int>(&QSpinBox::valueChanged), offset, &QSlider::setValue); connect(offset_value, qOverload<int>(&QSpinBox::valueChanged), &dialog, [this, repeat](int value) { audio_offset_ms_ = value; if (repeat->isChecked()) begin_repeat_selection(); }); connect(reset, &QPushButton::clicked, offset_value, [offset_value] { offset_value->setValue(0); }); connect(repeat, &QCheckBox::toggled, &dialog, [this](bool enabled) { if (enabled) begin_repeat_selection(); else { repeat_selection_playback_ = false; if (audio_playback_.state() != QProcess::NotRunning || playback_timer_.isActive()) stop_playback("Repeat selection stopped"); } }); connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
    const auto result = dialog.exec(); const bool was_repeating = repeat_selection_playback_; repeat_selection_playback_ = false; if (was_repeating || audio_playback_.state() != QProcess::NotRunning || playback_timer_.isActive()) stop_playback("Audio synchronization preview stopped"); if (result != QDialog::Accepted) { audio_offset_ms_ = original_offset; status_->setText(QString("Audio synchronization unchanged: %1 ms").arg(audio_offset_ms_)); return; } audio_offset_ms_ = offset_value->value(); if (audio_offset_ms_ != original_offset) set_project_modified(true); if (!project_path_.isEmpty() && audio_offset_ms_ != original_offset) { record_edit_snapshot(); project_autosave_timer_.start(700); } status_->setText(QString("Audio synchronization offset: %1 ms").arg(audio_offset_ms_));
  }
  void show_program_information() {
    if (input_->text().isEmpty()) { status_->setText("Open a source to view program information"); return; }
    QDialog dialog(this); dialog.setWindowTitle("Program Information"); dialog.resize(760, 460); auto* layout = new QVBoxLayout(&dialog); const QFileInfo file(input_->text()); auto* summary = new QLabel(QString("%1\n%2 bytes   %3 frames   %4 fps   %5 cut range(s)").arg(file.absoluteFilePath()).arg(static_cast<qulonglong>(file.size())).arg(static_cast<qulonglong>(timeline_->frame_count())).arg(static_cast<double>(frame_rate_num_) / std::max(frame_rate_den_, 1), 0, 'f', 3).arg(static_cast<qulonglong>(cut_ranges_.size())), &dialog); summary->setTextInteractionFlags(Qt::TextSelectableByMouse); layout->addWidget(summary); auto* table = new QTableWidget(source_streams_.size(), 4, &dialog); table->setHorizontalHeaderLabels({"Stream", "Type", "Codec", "Properties"}); table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); table->setEditTriggers(QAbstractItemView::NoEditTriggers); for (int row = 0; row < source_streams_.size(); ++row) { const auto stream = source_streams_[row].toObject(); const auto type = stream.value("type").toInt(-1); const auto type_name = type == 0 ? "Video" : type == 1 ? "Audio" : type == 3 ? "Subtitle" : type == 4 ? "Attachment" : "Data"; QStringList properties; if (type == 0) properties << QString("%1x%2").arg(stream.value("width").toInt()).arg(stream.value("height").toInt()) << stream.value("pixel_format").toString(); if (type == 1) properties << QString("%1 Hz").arg(stream.value("sample_rate").toInt()) << QString("%1 channels").arg(stream.value("channels").toInt()); table->setItem(row, 0, new QTableWidgetItem(QString::number(stream.value("index").toInt()))); table->setItem(row, 1, new QTableWidgetItem(type_name)); table->setItem(row, 2, new QTableWidgetItem(stream.value("codec").toString())); table->setItem(row, 3, new QTableWidgetItem(properties.join(", "))); } layout->addWidget(table, 1); auto* close = new QDialogButtonBox(QDialogButtonBox::Close, &dialog); layout->addWidget(close); connect(close, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); dialog.exec();
  }
  void show_metadata_editor() {
    if (!require_paid_delivery_tool("Media Metadata Copy")) return;
    if (input_->text().isEmpty()) { status_->setText("Open a source before editing media metadata"); return; }
    if (metadata_worker_.state() != QProcess::NotRunning) { status_->setText("A metadata update is already running"); return; }
    QDialog dialog(this); dialog.setWindowTitle("Media Metadata"); dialog.resize(680, 470); auto* layout = new QVBoxLayout(&dialog);
    auto* explanation = new QLabel("Edit container-level tags. SmartCut64 writes a validated packet-copy file: video, audio, subtitles, chapters, timestamps, and codec payloads are not re-encoded.", &dialog); explanation->setWordWrap(true); layout->addWidget(explanation);
    auto* table = new QTableWidget(source_metadata_.size(), 2, &dialog); table->setHorizontalHeaderLabels({"Field", "Value"}); table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents); table->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); table->setSelectionBehavior(QAbstractItemView::SelectRows);
    for (int row = 0; row < source_metadata_.size(); ++row) { const auto item = source_metadata_[row].toObject(); table->setItem(row, 0, new QTableWidgetItem(item.value("key").toString())); table->setItem(row, 1, new QTableWidgetItem(item.value("value").toString())); }
    layout->addWidget(table, 1); auto* row_controls = new QHBoxLayout; auto* add = new QPushButton("Add Field", &dialog); auto* remove = new QPushButton("Remove Field", &dialog); row_controls->addWidget(add); row_controls->addWidget(remove); row_controls->addStretch(); layout->addLayout(row_controls);
    auto* common = new QLabel("Common fields: title, comment, description, genre, date, show, episode_id, network, copyright.", &dialog); common->setWordWrap(true); layout->addWidget(common); auto* buttons = new QDialogButtonBox(QDialogButtonBox::Save | QDialogButtonBox::Cancel, &dialog); buttons->button(QDialogButtonBox::Save)->setText("Save Metadata Copy…"); layout->addWidget(buttons);
    connect(add, &QPushButton::clicked, &dialog, [table] { const int row = table->rowCount(); table->insertRow(row); table->setItem(row, 0, new QTableWidgetItem("title")); table->setItem(row, 1, new QTableWidgetItem); table->setCurrentCell(row, 0); table->editItem(table->item(row, 0)); });
    connect(remove, &QPushButton::clicked, &dialog, [table] { const auto rows = table->selectionModel()->selectedRows(); for (auto it = rows.crbegin(); it != rows.crend(); ++it) table->removeRow(it->row()); });
    connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); if (dialog.exec() != QDialog::Accepted) return;
    QStringList metadata_arguments; QSet<QString> keys;
    for (int row = 0; row < table->rowCount(); ++row) { const auto* key_item = table->item(row, 0); const auto* value_item = table->item(row, 1); const auto key = key_item ? key_item->text().trimmed() : QString{}; if (key.isEmpty()) continue; const auto folded = key.toCaseFolded(); if (keys.contains(folded)) { status_->setText(QString("Duplicate metadata field: %1").arg(key)); return; } keys.insert(folded); metadata_arguments << "--metadata" << key << (value_item ? value_item->text() : QString{}); }
    if (keys.isEmpty()) { status_->setText("Add at least one metadata field"); return; }
    const QFileInfo source(input_->text()); const auto supported = QStringList{"mkv", "mp4", "m4v", "mov", "mpg", "mpeg", "vob", "ts", "m2ts", "mts", "wtv", "dvr-ms", "dvrms", "rec", "mxf"}; const auto suffix = supported.contains(source.suffix().toLower()) ? source.suffix() : QStringLiteral("mkv"); const auto suggested = source.absolutePath() + '/' + source.completeBaseName() + "_metadata." + suffix;
    metadata_output_path_ = QFileDialog::getSaveFileName(this, "Save Metadata Copy", suggested, "Supported Media (*.mkv *.mp4 *.mov *.mpg *.mpeg *.vob *.ts *.m2ts *.wtv *.dvr-ms *.rec *.mxf);;All Files (*)"); if (metadata_output_path_.isEmpty()) return; if (QFileInfo(metadata_output_path_).absoluteFilePath() == source.absoluteFilePath()) { metadata_output_path_.clear(); status_->setText("Choose a different output file; source files are never overwritten"); return; }
    QStringList arguments{"--edit-metadata", "--input", input_->text(), "--output", metadata_output_path_}; arguments.append(metadata_arguments); status_->setText("Writing validated metadata copy…"); metadata_worker_.start(worker_program(), arguments);
  }
  bool capture_export_report(const QString& output) { const auto lines = output.split('\n', Qt::SkipEmptyParts); for (auto line = lines.crbegin(); line != lines.crend(); ++line) { const auto document = QJsonDocument::fromJson(line->trimmed().toUtf8()); if (!document.isObject() || document.object().value("event").toString() != "completed") continue; last_export_report_ = QString::fromUtf8(document.toJson(QJsonDocument::Indented)); if (export_report_action_) export_report_action_->setEnabled(true); return true; } return false; }
  void show_output_processing_failed() {
    const auto document = QJsonDocument::fromJson(last_export_report_.toUtf8());
    const auto report = document.object().value("report").toObject();
    QStringList diagnostics;
    const auto reason = report.value("reason").toString();
    if (!reason.isEmpty()) diagnostics << QString("Reason: %1").arg(reason);
    const auto detail = report.value("detail").toObject();
    const auto phase = detail.value("phase").toString();
    if (!phase.isEmpty()) diagnostics << QString("Phase: %1").arg(phase);
    diagnostics << (QFileInfo::exists(output_->text()) ? "An output file remains; inspect it before replacing your source." : "No incomplete output file was kept.");
    QMessageBox dialog(QMessageBox::Critical, "Output Failed", "SmartCut64 could not complete and validate the output video.", QMessageBox::Ok, this);
    dialog.setInformativeText(diagnostics.join('\n'));
    if (!last_export_report_.isEmpty()) dialog.setDetailedText(last_export_report_);
    dialog.exec();
  }
  QString output_completion_summary() const {
    const auto root = QJsonDocument::fromJson(last_export_report_.toUtf8()).object(); std::size_t frames = export_end_frame_ > export_first_frame_ ? export_end_frame_ - export_first_frame_ : 0;
    for (const auto& cut : cut_ranges_) { const auto first = std::max(cut.first, export_first_frame_); const auto end = std::min(cut.second, export_end_frame_); if (first < end) frames -= std::min(frames, end - first); }
    const double elapsed = export_elapsed_.isValid() ? std::max(0.001, export_elapsed_.elapsed() / 1000.0) : 0.0; const double duration = frame_rate_num_ > 0 ? static_cast<double>(frames) * std::max(frame_rate_den_, 1) / frame_rate_num_ : 0.0; const auto bytes = QFileInfo(output_->text()).size(); const double bitrate = duration > 0.0 && bytes > 0 ? static_cast<double>(bytes) * 8.0 / duration / 1000000.0 : 0.0;
    return QString("Output Processing Complete\n\nOutput file: %1\nVideo output frames: %2\nProcessed frames/sec: %3\nProcessing time (secs): %4\nOutput file size: %5 bytes\nActual overall bitrate (Mbps): %6\nValidation status: %7\nSHA-256: %8")
        .arg(output_->text()).arg(static_cast<qulonglong>(frames)).arg(elapsed > 0.0 ? QString::number(frames / elapsed, 'f', 2) : QStringLiteral("n/a")).arg(elapsed > 0.0 ? QString::number(elapsed, 'f', 3) : QStringLiteral("n/a")).arg(static_cast<qlonglong>(bytes)).arg(bitrate > 0.0 ? QString::number(bitrate, 'f', 3) : QStringLiteral("n/a")).arg(root.value("status").toString("UNKNOWN"), root.value("output_sha256").toString("not available"));
  }
  bool play_completion_alert(bool batch) {
    if (completion_alert_mode_ == 0 || (batch && completion_alert_mode_ < 2)) return false;
    ++completion_alert_requests_;
    if (automation_mode_) return true;
#ifdef _WIN32
    if (!completion_alert_wave_.isEmpty() && QFileInfo::exists(completion_alert_wave_)) {
      const auto path = QDir::toNativeSeparators(completion_alert_wave_).toStdWString();
      if (PlaySoundW(path.c_str(), nullptr, SND_FILENAME | SND_ASYNC | SND_NODEFAULT)) return true;
    }
#endif
    QApplication::beep(); return true;
  }
  void show_output_processing_complete() {
    const auto summary_text = output_completion_summary();
    QDialog dialog(this); dialog.setWindowTitle("Output Processing Complete"); dialog.resize(720, 430); auto* layout = new QVBoxLayout(&dialog); auto* summary = new QTextEdit(&dialog); summary->setReadOnly(true); summary->setPlainText(summary_text); const auto status = QJsonDocument::fromJson(last_export_report_.toUtf8()).object().value("status").toString(); if (status != "PASSED") summary->setStyleSheet("color:#c00000;"); layout->addWidget(summary, 1); auto* controls = new QHBoxLayout; auto* folder = new QPushButton("Open Output Folder", &dialog); auto* report = new QPushButton("Full Validation Report…", &dialog); auto* close = new QPushButton("Close", &dialog); controls->addWidget(folder); controls->addWidget(report); controls->addStretch(); controls->addWidget(close); layout->addLayout(controls); connect(folder, &QPushButton::clicked, &dialog, [this] { QDesktopServices::openUrl(QUrl::fromLocalFile(QFileInfo(output_->text()).absolutePath())); }); connect(report, &QPushButton::clicked, &dialog, [this] { show_last_export_report(); }); connect(close, &QPushButton::clicked, &dialog, &QDialog::accept); dialog.exec();
  }
  void show_last_export_report() { if (last_export_report_.isEmpty()) { status_->setText("No completed export report is available"); return; } const auto document = QJsonDocument::fromJson(last_export_report_.toUtf8()); const auto root = document.object(); QDialog dialog(this); dialog.setWindowTitle("Last Export Report"); dialog.resize(850, 620); auto* layout = new QVBoxLayout(&dialog); auto* summary = new QLabel(QString("Status: %1\nOutput: %2\nSHA-256: %3").arg(root.value("status").toString("UNKNOWN"), output_->text(), root.value("output_sha256").toString("not available")), &dialog); summary->setTextInteractionFlags(Qt::TextSelectableByMouse); layout->addWidget(summary); auto* report = new QTextEdit(&dialog); report->setReadOnly(true); report->setPlainText(last_export_report_); layout->addWidget(report, 1); auto* buttons = new QDialogButtonBox(QDialogButtonBox::Save | QDialogButtonBox::Close, &dialog); layout->addWidget(buttons); connect(buttons->button(QDialogButtonBox::Save), &QPushButton::clicked, &dialog, [this, &dialog] { const auto path = QFileDialog::getSaveFileName(&dialog, "Save Export Report", output_->text() + ".smartcut-report.json", "JSON Report (*.json)"); if (path.isEmpty()) return; QFile file(path); if (file.open(QIODevice::WriteOnly | QIODevice::Truncate)) file.write(last_export_report_.toUtf8()); }); connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); dialog.exec(); }
  std::uint64_t source_boundary_time_us(std::size_t frame) const {
    const auto count = timeline_->frame_count(); if (count == 0) return 0; if (frame < count) { const auto value = timeline_->frame_time_us(frame); if (value) return *value; }
    const auto nominal = static_cast<std::uint64_t>(1000000ULL * static_cast<std::uint64_t>(std::max(frame_rate_den_, 1)) / static_cast<std::uint64_t>(std::max(frame_rate_num_, 1))); if (frame >= count) { const auto last = timeline_->frame_time_us(count - 1); return (last ? *last : nominal * (count - 1)) + nominal; } return nominal * frame;
  }
  std::optional<GeneratedChapter> mapped_source_chapter(std::size_t source_frame) const {
    if (source_frame >= timeline_->frame_count()) return std::nullopt;
    std::size_t removed_frames = 0; std::uint64_t removed_time = 0; for (const auto& cut : cut_ranges_) { if (source_frame >= cut.first && source_frame < cut.second) return std::nullopt; if (cut.second <= source_frame) { removed_frames += cut.second - cut.first; removed_time += source_boundary_time_us(cut.second) - source_boundary_time_us(cut.first); } }
    const auto source_time = source_boundary_time_us(source_frame); return GeneratedChapter{source_frame - std::min(source_frame, removed_frames), source_time - std::min(source_time, removed_time)};
  }
  std::size_t output_frame_count() const { const auto count = timeline_->frame_count(); std::size_t removed = 0; for (const auto& cut : cut_ranges_) removed += cut.second - cut.first; return count - std::min(count, removed); }
  std::uint64_t output_duration_us() const { auto duration = source_boundary_time_us(timeline_->frame_count()); for (const auto& cut : cut_ranges_) { const auto removed = source_boundary_time_us(cut.second) - source_boundary_time_us(cut.first); duration -= std::min(duration, removed); } return duration; }
  std::vector<GeneratedChapter> generated_chapters() const {
    std::vector<GeneratedChapter> chapters; const auto add_source = [this, &chapters](std::size_t frame) { if (const auto mapped = mapped_source_chapter(frame)) chapters.push_back(*mapped); };
    if (chapter_at_cuts_) for (const auto& cut : cut_ranges_) if (cut.second < timeline_->frame_count()) add_source(cut.second);
    if (chapter_at_markers_) for (const auto frame : timeline_->chapter_markers()) add_source(frame);
    const auto duration = output_duration_us(); const auto output_frames = output_frame_count(); if (chapter_equal_spacing_ && duration > 0 && output_frames > 0) { if (chapter_spacing_mode_ == "parts") { const int parts = std::max(chapter_spacing_value_, 2); for (int part = 1; part < parts; ++part) { const auto time = duration * static_cast<std::uint64_t>(part) / static_cast<std::uint64_t>(parts); const auto frame = std::min<std::size_t>(output_frames - 1, static_cast<std::size_t>(static_cast<long double>(time) * std::max(frame_rate_num_, 1) / (1000000.0L * std::max(frame_rate_den_, 1)))); chapters.push_back({frame, time}); } } else { const auto step = static_cast<std::uint64_t>(std::max(chapter_spacing_value_, 1)) * 1000000ULL; for (auto time = step; time < duration; time += step) { const auto frame = std::min<std::size_t>(output_frames - 1, static_cast<std::size_t>(static_cast<long double>(time) * std::max(frame_rate_num_, 1) / (1000000.0L * std::max(frame_rate_den_, 1)))); chapters.push_back({frame, time}); } } }
    if (chapter_include_first_ && output_frames > 0) chapters.push_back({0, 0});
    std::sort(chapters.begin(), chapters.end(), [](const auto& left, const auto& right) { return std::tie(left.output_time_us, left.output_frame) < std::tie(right.output_time_us, right.output_frame); });
    std::vector<GeneratedChapter> filtered; const auto minimum = static_cast<std::uint64_t>(std::max(chapter_minimum_seconds_, 0)) * 1000000ULL; for (const auto& chapter : chapters) { if (!filtered.empty() && (chapter.output_time_us == filtered.back().output_time_us || chapter.output_time_us - filtered.back().output_time_us < minimum)) continue; filtered.push_back(chapter); } return filtered;
  }
  QString chapter_timecode(const GeneratedChapter& chapter) const {
    const auto total_seconds = chapter.output_time_us / 1000000ULL; const auto hours = total_seconds / 3600ULL; const auto minutes = (total_seconds / 60ULL) % 60ULL; const auto seconds = total_seconds % 60ULL; const auto remainder = chapter.output_time_us % 1000000ULL; const auto hundredths = remainder / 10000ULL; const auto fps = std::max<std::uint64_t>(1, static_cast<std::uint64_t>(std::llround(static_cast<double>(std::max(frame_rate_num_, 1)) / std::max(frame_rate_den_, 1)))); const auto frame = std::min<std::uint64_t>(fps - 1, remainder * static_cast<std::uint64_t>(std::max(frame_rate_num_, 1)) / (1000000ULL * static_cast<std::uint64_t>(std::max(frame_rate_den_, 1))));
    const auto hms = QString("%1:%2:%3").arg(hours, 2, 10, QLatin1Char('0')).arg(minutes, 2, 10, QLatin1Char('0')).arg(seconds, 2, 10, QLatin1Char('0')); if (chapter_timecode_format_ == "HH:MM:SS;ff") return hms + QString(";%1").arg(frame, 2, 10, QLatin1Char('0')); if (chapter_timecode_format_ == "HH:MM:SS:hh") return hms + QString(":%1").arg(hundredths, 2, 10, QLatin1Char('0')); if (chapter_timecode_format_ == "HH:MM:SS;hh") return hms + QString(";%1").arg(hundredths, 2, 10, QLatin1Char('0')); if (chapter_timecode_format_ == "HH:MM:SS") return hms; if (chapter_timecode_format_ == "H:MM:SS.ff") return QString("%1:%2:%3.%4").arg(hours).arg(minutes, 2, 10, QLatin1Char('0')).arg(seconds, 2, 10, QLatin1Char('0')).arg(frame, 2, 10, QLatin1Char('0')); return hms + QString(":%1").arg(frame, 2, 10, QLatin1Char('0'));
  }
  QString default_chapter_path() const { const QFileInfo media(output_->text().isEmpty() ? input_->text() : output_->text()); QString name = chapter_file_template_; name.replace("%F", media.completeBaseName()); return media.absoluteDir().filePath(name.isEmpty() ? media.completeBaseName() + "_chapter.txt" : name); }
  bool write_chapter_file(const QString& path, bool announce = true) const {
    if (path.isEmpty() || timeline_->frame_count() == 0) return false;
    QSaveFile file(path); if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { if (announce) status_->setText(QString("Could not create chapter file: %1").arg(file.errorString())); return false; } const auto chapters = generated_chapters(); QByteArray content; for (std::size_t index = 0; index < chapters.size(); ++index) { QString line = chapter_line_format_; line.replace("%T", chapter_timecode(chapters[index])); line.replace("%N", QString::number(static_cast<qulonglong>(chapters[index].output_frame))); line.replace("%C", QString::number(static_cast<qulonglong>(index + 1))); content += line.toUtf8(); content += '\n'; } if (file.write(content) != content.size() || !file.commit()) { if (announce) status_->setText(QString("Could not finish chapter file: %1").arg(file.errorString())); return false; } if (announce) status_->setText(QString("Saved %1 chapter marks to %2").arg(static_cast<qulonglong>(chapters.size())).arg(QDir::toNativeSeparators(path))); return true;
  }
  void save_chapter_file_interactive() { if (timeline_->frame_count() == 0) { status_->setText("Open a video before saving chapter marks"); return; } const auto path = QFileDialog::getSaveFileName(this, "Save Chapter File", default_chapter_path(), "Chapter text (*.txt);;All files (*.*)"); if (!path.isEmpty()) write_chapter_file(path); }
  void show_chapter_options() {
    QDialog dialog(this); dialog.setWindowTitle("Chapter Marks"); dialog.resize(650, 480); auto* layout = new QVBoxLayout(&dialog); auto* locations = new QGroupBox("Chapter locations", &dialog); auto* location_layout = new QVBoxLayout(locations); auto* cuts = new QCheckBox("At cut points", locations); cuts->setChecked(chapter_at_cuts_); auto* markers = new QCheckBox("At scene / Ad-Detective markers", locations); markers->setChecked(chapter_at_markers_); auto* equal = new QCheckBox("Use equal spacing", locations); equal->setChecked(chapter_equal_spacing_); auto* spacing_row = new QHBoxLayout; auto* mode = new QComboBox(locations); mode->addItem("Seconds", "interval"); mode->addItem("Equal parts", "parts"); mode->setCurrentIndex(std::max(0, mode->findData(chapter_spacing_mode_))); auto* value = new QSpinBox(locations); value->setRange(1, chapter_spacing_mode_ == "parts" ? 1000 : 86400); value->setValue(std::max(chapter_spacing_value_, 1)); spacing_row->addWidget(mode); spacing_row->addWidget(value, 1); location_layout->addWidget(cuts); location_layout->addWidget(markers); location_layout->addWidget(equal); location_layout->addLayout(spacing_row); layout->addWidget(locations);
    auto* form = new QFormLayout; auto* minimum = new QSpinBox(&dialog); minimum->setRange(0, 86400); minimum->setSuffix(" seconds"); minimum->setValue(chapter_minimum_seconds_); auto* include_first = new QCheckBox("Include first output frame (00:00:00)", &dialog); include_first->setChecked(chapter_include_first_); auto* automatic = new QCheckBox("Automatically create a chapter file after successful export", &dialog); automatic->setChecked(chapter_auto_save_); auto* file_template = new QLineEdit(chapter_file_template_, &dialog); auto* line_template = new QLineEdit(chapter_line_format_, &dialog); auto* timecode = new QComboBox(&dialog); timecode->addItems({"HH:MM:SS:ff", "HH:MM:SS;ff", "HH:MM:SS:hh", "HH:MM:SS;hh", "HH:MM:SS", "H:MM:SS.ff"}); timecode->setCurrentText(chapter_timecode_format_); form->addRow("Minimum chapter interval:", minimum); form->addRow(QString(), include_first); form->addRow(QString(), automatic); form->addRow("Output filename template:", file_template); form->addRow("Line format:", line_template); form->addRow("Timecode format:", timecode); layout->addLayout(form); auto* help = new QLabel("%F is replaced by the output filename. Each line can use %T for timecode, %N for output frame number, and %C for chapter number. Markers inside removed ranges are omitted and later markers are retimed.", &dialog); help->setWordWrap(true); layout->addWidget(help); auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); layout->addWidget(buttons); connect(mode, &QComboBox::currentIndexChanged, &dialog, [mode, value](int) { const bool parts = mode->currentData().toString() == "parts"; value->setRange(1, parts ? 1000 : 86400); value->setSuffix(parts ? " parts" : " seconds"); }); emit mode->currentIndexChanged(mode->currentIndex()); connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); if (dialog.exec() != QDialog::Accepted) return;
    chapter_at_cuts_ = cuts->isChecked(); chapter_at_markers_ = markers->isChecked(); chapter_equal_spacing_ = equal->isChecked(); chapter_spacing_mode_ = mode->currentData().toString(); chapter_spacing_value_ = value->value(); chapter_minimum_seconds_ = minimum->value(); chapter_include_first_ = include_first->isChecked(); chapter_auto_save_ = automatic->isChecked(); chapter_file_template_ = file_template->text().isEmpty() ? "%F_chapter.txt" : file_template->text(); chapter_line_format_ = line_template->text().isEmpty() ? "%T" : line_template->text(); chapter_timecode_format_ = timecode->currentText(); QSettings settings("SmartCut64", "SmartCut64"); settings.setValue("chapters/atCutPoints", chapter_at_cuts_); settings.setValue("chapters/atSceneMarkers", chapter_at_markers_); settings.setValue("chapters/equalSpacing", chapter_equal_spacing_); settings.setValue("chapters/spacingMode", chapter_spacing_mode_); settings.setValue("chapters/spacingValue", chapter_spacing_value_); settings.setValue("chapters/minimumSeconds", chapter_minimum_seconds_); settings.setValue("chapters/includeFirst", chapter_include_first_); settings.setValue("chapters/autoSave", chapter_auto_save_); settings.setValue("chapters/fileTemplate", chapter_file_template_); settings.setValue("chapters/lineFormat", chapter_line_format_); settings.setValue("chapters/timecodeFormat", chapter_timecode_format_); status_->setText(QString("Chapter generation updated: %1 chapter marks").arg(static_cast<qulonglong>(generated_chapters().size())));
  }
  QStringList known_shell_extensions() const { return {"mkv", "mp4", "mov", "ts", "m2ts", "mts", "mpg", "mpeg", "vob", "avi", "webm", "VPrj", "BPrj", "VJnr", "smartcut", "scjoin"}; }
  bool register_shell_associations(const QStringList& selected) const {
#ifdef _WIN32
    const auto executable = QDir::toNativeSeparators(QCoreApplication::applicationFilePath()); const auto command = QString("\"%1\" \"%2\"").arg(executable, "%1"); bool ok = true;
    { QSettings application("HKEY_CURRENT_USER\\Software\\Classes\\Applications\\smartcut-gui.exe", QSettings::NativeFormat); application.setValue("FriendlyAppName", "SmartCut64"); application.setValue("shell/open/command/.", command); application.sync(); ok = ok && application.status() == QSettings::NoError; }
    for (const auto& extension : known_shell_extensions()) {
      QSettings shell(QString("HKEY_CURRENT_USER\\Software\\Classes\\SystemFileAssociations\\.%1\\shell\\SmartCut64").arg(extension), QSettings::NativeFormat);
      if (selected.contains(extension, Qt::CaseInsensitive)) { shell.setValue(".", "Open with SmartCut64"); shell.setValue("Icon", executable + ",0"); shell.setValue("command/.", command); } else shell.clear();
      shell.sync(); ok = ok && shell.status() == QSettings::NoError;
    }
    return ok;
#else
    (void)selected; return false;
#endif
  }
  void flush_application_log() const { if (!log_ || log_folder_.isEmpty() || !QDir().mkpath(log_folder_)) return; QSaveFile file(QDir(log_folder_).filePath("SmartCut64.log")); const auto bytes = log_->toPlainText().toUtf8(); if (file.open(QIODevice::WriteOnly | QIODevice::Truncate) && file.write(bytes) == bytes.size()) file.commit(); }
  void apply_theme_preset(const QString& name, bool persist) {
    if (name == "Professional Dark" || name == "Dark") {
      ui_style_name_ = "Professional Dark";
      ui_base_colour_ = QColor("#171b21");
      ui_surface_colour_ = QColor("#222831");
      ui_text_colour_ = QColor("#e6ebf1");
      ui_border_colour_ = QColor("#454e5a");
      ui_accent_colour_ = QColor("#2587d9");
    } else {
      ui_style_name_ = "Professional Light";
      ui_base_colour_ = QColor("#f3f5f7");
      ui_surface_colour_ = QColor("#ffffff");
      ui_text_colour_ = QColor("#1f2933");
      ui_border_colour_ = QColor("#c8ced6");
      ui_accent_colour_ = QColor("#1976c9");
    }
    if (light_theme_action_) light_theme_action_->setChecked(ui_style_name_ == "Professional Light");
    if (dark_theme_action_) dark_theme_action_->setChecked(ui_style_name_ == "Professional Dark");
    apply_ui_style();
    if (persist) {
      QSettings settings("SmartCut64", "SmartCut64");
      settings.setValue("uiStyle/name", ui_style_name_);
      settings.setValue("uiStyle/base", ui_base_colour_.name());
      settings.setValue("uiStyle/surface", ui_surface_colour_.name());
      settings.setValue("uiStyle/text", ui_text_colour_.name());
      settings.setValue("uiStyle/border", ui_border_colour_.name());
      settings.setValue("uiStyle/accent", ui_accent_colour_.name());
      status_->setText(ui_style_name_ + " applied");
    }
  }
  QString ui_style_sheet() const {
    QString style = QStringLiteral(
      "QMainWindow,QWidget{background:%BASE%;color:%TEXT%;} QMenuBar,QMenu{background:%BASE%;color:%TEXT%;} QMenuBar{padding:0px;} "
      "QMenuBar::item{background:transparent;padding:2px 5px;} QMenuBar::item:selected{background:%HOVER%;color:%TEXT%;} QMenuBar::item:pressed{background:%PRESSED%;color:%TEXT%;} "
      "QMenu::item{background:transparent;padding:4px 28px 4px 18px;} QMenu::item:selected{background:%HOVER%;color:%TEXT%;} QMenu::item:disabled{color:%DISABLED%;} QMenu::separator{height:1px;background:%BORDER%;margin:3px 5px;} "
      "QLineEdit,QTextEdit,QPlainTextEdit,QTableWidget,QListWidget,QTreeWidget,QComboBox,QSpinBox,QDoubleSpinBox{background:%SURFACE%;color:%TEXT%;border:1px solid %BORDER%;border-radius:3px;padding:3px;selection-background-color:%ACCENT%;selection-color:white;} "
      "QPushButton{min-height:25px;padding:3px 10px;border:1px solid %BORDER%;border-radius:4px;background:%SURFACE%;color:%TEXT%;} QPushButton:hover{background:%HOVER%;border-color:%ACCENT%;} QPushButton:pressed{background:%PRESSED%;} QPushButton:focus{border-color:%ACCENT%;} QPushButton:disabled{color:%DISABLED%;border-color:%DISABLEDBORDER%;background:%BASE%;} "
      "QPushButton#sideAction{min-width:68px;max-width:68px;min-height:78px;max-height:78px;border:0;padding:1px;background:%BASE%;} QPushButton#sideAction:hover{background:%HOVER%;} "
      "QPushButton#transportButton{min-width:38px;max-width:38px;min-height:32px;max-height:32px;border:0;padding:0;font-size:13px;font-weight:700;} QPushButton#roundButton{min-width:42px;max-width:42px;min-height:42px;max-height:42px;border-radius:21px;background:%ACCENT%;color:white;border:1px solid %ACCENTDARK%;font-size:18px;font-weight:700;padding:0;} QPushButton#roundButton:hover{background:%HOVERACCENT%;} QPushButton#markButton{min-width:38px;padding:0;font-weight:700;} "
      "QLabel#frameReadout{background:%SURFACE%;border:1px solid %BORDER%;border-radius:3px;padding:2px;font-size:18px;font-weight:700;} QFrame#navigationFrame{border:1px solid %BORDER%;border-radius:4px;background:%BASE%;} QHeaderView::section{background:%BASE%;color:%TEXT%;padding:5px;border:0;border-right:1px solid %BORDER%;border-bottom:1px solid %BORDER%;} QGroupBox{font-weight:400;border:1px solid %BORDER%;border-radius:4px;margin-top:8px;padding-top:3px;} QGroupBox::title{subcontrol-origin:margin;left:7px;padding:0 4px;} QProgressBar{border:1px solid %BORDER%;border-radius:3px;background:%SURFACE%;text-align:center;} QProgressBar::chunk{background:%ACCENT%;border-radius:2px;} QTabBar::tab{background:%BASE%;color:%TEXT%;padding:6px 11px;border:1px solid %BORDER%;} QTabBar::tab:selected{background:%SURFACE%;border-bottom-color:%SURFACE%;} QToolTip{background:%SURFACE%;color:%TEXT%;border:1px solid %BORDER%;padding:3px;} QStatusBar{border-top:1px solid %BORDER%;}");
    const auto replace = [&style](const QString& key, const QColor& value) { style.replace(key, value.name()); };
    replace("%BASE%", ui_base_colour_); replace("%SURFACE%", ui_surface_colour_); replace("%TEXT%", ui_text_colour_); replace("%BORDER%", ui_border_colour_); replace("%ACCENT%", ui_accent_colour_); replace("%HOVER%", QColor::fromRgbF((ui_base_colour_.redF() + ui_accent_colour_.redF()) / 2.0, (ui_base_colour_.greenF() + ui_accent_colour_.greenF()) / 2.0, (ui_base_colour_.blueF() + ui_accent_colour_.blueF()) / 2.0)); replace("%PRESSED%", ui_accent_colour_.lighter(165)); replace("%DISABLED%", ui_text_colour_.lighter(170)); replace("%DISABLEDBORDER%", ui_border_colour_.lighter(130)); replace("%ACCENTDARK%", ui_accent_colour_.darker(120)); replace("%HOVERACCENT%", ui_accent_colour_.lighter(115));
    return style;
  }
  void apply_ui_style() {
    QFont font(ui_font_family_, ui_font_size_); setFont(font);
    QPalette colours = qApp->palette();
    colours.setColor(QPalette::Window, ui_base_colour_); colours.setColor(QPalette::WindowText, ui_text_colour_);
    colours.setColor(QPalette::Base, ui_surface_colour_); colours.setColor(QPalette::AlternateBase, ui_base_colour_);
    colours.setColor(QPalette::Text, ui_text_colour_); colours.setColor(QPalette::Button, ui_base_colour_);
    colours.setColor(QPalette::ButtonText, ui_text_colour_); colours.setColor(QPalette::Highlight, ui_accent_colour_);
    colours.setColor(QPalette::HighlightedText, Qt::white);
    colours.setColor(QPalette::PlaceholderText, ui_text_colour_.lighter(ui_text_colour_.lightness() < 128 ? 170 : 70));
    qApp->setPalette(colours);
    setStyleSheet(ui_style_sheet());
    apply_native_window_theme(this, ui_base_colour_, ui_text_colour_, ui_border_colour_);
  }
  void show_ui_style_editor() {
    const auto original = std::tuple{ui_style_name_, ui_base_colour_, ui_surface_colour_, ui_text_colour_, ui_border_colour_, ui_accent_colour_, ui_font_family_, ui_font_size_};
    QDialog dialog(this); dialog.setWindowTitle("UI Style Editor"); dialog.resize(590, 480); auto* layout = new QVBoxLayout(&dialog); auto* form = new QFormLayout;
    auto* preset = new QComboBox(&dialog); preset->addItems({"Professional Light", "Professional Dark", "High Contrast", "Custom"}); preset->setCurrentText(preset->findText(ui_style_name_) >= 0 ? ui_style_name_ : "Custom");
    auto colour_button = [&dialog](const QColor& initial) { auto* button = new QPushButton(&dialog); button->setProperty("styleColour", initial); return button; };
    auto* base = colour_button(ui_base_colour_); auto* surface = colour_button(ui_surface_colour_); auto* text = colour_button(ui_text_colour_); auto* border = colour_button(ui_border_colour_); auto* accent = colour_button(ui_accent_colour_); auto* font = new QPushButton(QString("%1, %2 pt").arg(ui_font_family_).arg(ui_font_size_), &dialog);
    const auto refresh_button = [](QPushButton* button) { const auto colour = button->property("styleColour").value<QColor>(); button->setText(colour.name()); button->setStyleSheet(QString("background:%1;color:%2;").arg(colour.name(), colour.lightness() < 128 ? "white" : "black")); };
    for (auto* button : {base, surface, text, border, accent}) refresh_button(button);
    const auto apply_controls = [this, preset, base, surface, text, border, accent] { ui_style_name_ = preset->currentText(); ui_base_colour_ = base->property("styleColour").value<QColor>(); ui_surface_colour_ = surface->property("styleColour").value<QColor>(); ui_text_colour_ = text->property("styleColour").value<QColor>(); ui_border_colour_ = border->property("styleColour").value<QColor>(); ui_accent_colour_ = accent->property("styleColour").value<QColor>(); apply_ui_style(); };
    for (auto* button : {base, surface, text, border, accent}) connect(button, &QPushButton::clicked, &dialog, [button, refresh_button, apply_controls, preset, &dialog] { const auto selected = QColorDialog::getColor(button->property("styleColour").value<QColor>(), &dialog, "Choose UI colour"); if (!selected.isValid()) return; button->setProperty("styleColour", selected); refresh_button(button); preset->setCurrentText("Custom"); apply_controls(); });
    const auto set_colours = [base, surface, text, border, accent, refresh_button, apply_controls](const QString& b, const QString& s, const QString& t, const QString& edge, const QString& a) { const QList<QPair<QPushButton*, QColor>> values{{base, QColor(b)}, {surface, QColor(s)}, {text, QColor(t)}, {border, QColor(edge)}, {accent, QColor(a)}}; for (const auto& value : values) { value.first->setProperty("styleColour", value.second); refresh_button(value.first); } apply_controls(); };
    connect(preset, &QComboBox::currentTextChanged, &dialog, [set_colours](const QString& value) { if (value == "Professional Light") set_colours("#f3f5f7", "#ffffff", "#1f2933", "#c8ced6", "#1976c9"); else if (value == "Professional Dark") set_colours("#171b21", "#222831", "#e6ebf1", "#454e5a", "#2587d9"); else if (value == "High Contrast") set_colours("#000000", "#000000", "#ffffff", "#ffffff", "#ffff00"); });
    connect(font, &QPushButton::clicked, &dialog, [this, font, &dialog] { bool accepted = false; const auto selected = QFontDialog::getFont(&accepted, QFont(ui_font_family_, ui_font_size_), &dialog, "Choose UI font"); if (!accepted) return; ui_font_family_ = selected.family(); ui_font_size_ = std::clamp(selected.pointSize(), 7, 24); font->setText(QString("%1, %2 pt").arg(ui_font_family_).arg(ui_font_size_)); apply_ui_style(); });
    form->addRow("Preset:", preset); form->addRow("Window background:", base); form->addRow("Input and list surface:", surface); form->addRow("Text:", text); form->addRow("Borders:", border); form->addRow("Accent and progress:", accent); form->addRow("Application font:", font); layout->addLayout(form);
    auto* preview = new QGroupBox("Live preview", &dialog); auto* preview_layout = new QHBoxLayout(preview); preview_layout->addWidget(new QLabel("Timeline  00:10:21.12", preview)); preview_layout->addWidget(new QLineEdit("Editable field", preview), 1); preview_layout->addWidget(new QPushButton("Action", preview)); layout->addWidget(preview); layout->addWidget(new QLabel("Styles are stored per user and affect menus, controls, lists, dialogs, transport buttons, and progress displays. Video pixels and edit-range colours are unchanged.", &dialog));
    auto* buttons = new QDialogButtonBox(QDialogButtonBox::Save | QDialogButtonBox::Cancel | QDialogButtonBox::Reset, &dialog); layout->addWidget(buttons); connect(buttons->button(QDialogButtonBox::Reset), &QPushButton::clicked, &dialog, [preset] { preset->setCurrentText("Professional Light"); }); connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
    if (dialog.exec() != QDialog::Accepted) { std::tie(ui_style_name_, ui_base_colour_, ui_surface_colour_, ui_text_colour_, ui_border_colour_, ui_accent_colour_, ui_font_family_, ui_font_size_) = original; apply_ui_style(); return; }
    apply_controls(); QSettings settings("SmartCut64", "SmartCut64"); settings.setValue("uiStyle/name", ui_style_name_); settings.setValue("uiStyle/base", ui_base_colour_.name()); settings.setValue("uiStyle/surface", ui_surface_colour_.name()); settings.setValue("uiStyle/text", ui_text_colour_.name()); settings.setValue("uiStyle/border", ui_border_colour_.name()); settings.setValue("uiStyle/accent", ui_accent_colour_.name()); settings.setValue("uiStyle/fontFamily", ui_font_family_); settings.setValue("uiStyle/fontSize", ui_font_size_); status_->setText("UI style saved and applied");
  }
  void show_hardware_self_test() {
    QString source = input_->text();
    if (source.isEmpty() || !QFileInfo::exists(source)) {
      source = QFileDialog::getOpenFileName(
          this, "Choose Representative Video For Hardware Test", QString(),
          "Video Files (*.mkv *.mp4 *.mov *.ts *.m2ts *.mpg *.mpeg *.vob *.avi *.webm);;All Files (*)");
    }
    if (source.isEmpty()) return;

    constexpr std::size_t requested_frames = 60;
    // Keep the diagnostic bounded even if the editor cursor is hours into a
    // source. Codec, resolution and adapter capability are represented by the
    // opening sample without repeatedly decoding all preceding frames.
    constexpr std::size_t start_frame = 0;
    constexpr std::int64_t start_pts = 0;

    QProgressDialog progress(
        "Testing CPU and GPU preview, MPEG-2/DVD, H.264, HEVC, FFV1 and ProRes paths…",
        "Cancel", 0, 0, this);
    progress.setWindowTitle("Hardware Acceleration Self-Test");
    progress.setWindowModality(Qt::WindowModal);
    progress.setMinimumDuration(0);
    progress.show();
    QElapsedTimer elapsed;
    elapsed.start();
    QProcess worker;
    QByteArray benchmark_stdout;
    QByteArray benchmark_stderr;
    worker.start(worker_program(),
                 {"--hardware-self-test", "--input", source,
                  "--start-frame", QString::number(static_cast<qulonglong>(start_frame)),
                  "--start-pts", QString::number(start_pts),
                  "--frames", QString::number(requested_frames)});
    while (!worker.waitForFinished(100)) {
      // Codec libraries can emit enough diagnostics to fill a redirected pipe.
      // Drain both channels while the modal benchmark is running so a noisy
      // driver cannot deadlock the worker before it publishes the JSON result.
      benchmark_stdout += worker.readAllStandardOutput();
      benchmark_stderr += worker.readAllStandardError();
      QApplication::processEvents();
      progress.setLabelText(QString(
          "Testing CPU and GPU preview, MPEG-2/DVD, H.264, HEVC, FFV1 and ProRes paths…\nElapsed: %1 seconds")
                                .arg(elapsed.elapsed() / 1000));
      if (progress.wasCanceled()) {
        worker.kill();
        worker.waitForFinished(3000);
        status_->setText("Hardware self-test cancelled");
        return;
      }
    }
    benchmark_stdout += worker.readAllStandardOutput();
    benchmark_stderr += worker.readAllStandardError();
    progress.close();
    const auto raw = benchmark_stdout.trimmed();
    const auto document = QJsonDocument::fromJson(raw);
    if (worker.exitStatus() != QProcess::NormalExit || worker.exitCode() != 0 ||
        !document.isObject() ||
        document.object().value("schema").toString() != "hardware-self-test-v1") {
      const auto detail = QString::fromUtf8(benchmark_stderr).trimmed();
      QMessageBox::warning(
          this, "Hardware Acceleration Self-Test",
          "The benchmark could not complete. Try another representative video or leave hardware acceleration off.\n\n" +
              (detail.isEmpty() ? QString::fromUtf8(raw.left(1200)) : detail.left(1200)));
      return;
    }

    const auto root = document.object();
    QDialog dialog(this);
    dialog.setWindowTitle("Hardware Acceleration Self-Test Results");
    dialog.resize(850, 470);
    auto* layout = new QVBoxLayout(&dialog);
    auto* summary = new QLabel(
        QString("Representative source: %1\nThe opening %2 frames were tested at up to %3×%4. GPU is recommended only when it is available, validates correctly, and is at least 10% faster than software.")
            .arg(QFileInfo(source).fileName())
            .arg(root.value("sample_frames").toInt())
            .arg(root.value("benchmark_width").toInt())
            .arg(root.value("benchmark_height").toInt()),
        &dialog);
    summary->setWordWrap(true);
    layout->addWidget(summary);
    auto* table = new QTableWidget(0, 5, &dialog);
    table->setHorizontalHeaderLabels(
        {"Workload", "Software", "GPU path", "Speed-up", "Advice"});
    table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
    table->verticalHeader()->hide();
    table->setEditTriggers(QAbstractItemView::NoEditTriggers);
    const auto result_text = [](const QJsonObject& value) {
      if (value.value("status").toString() != "PASSED") return QString("Failed");
      return QString("%1 fps\n%2 s")
          .arg(value.value("fps").toDouble(), 0, 'f', 1)
          .arg(value.value("seconds").toDouble(), 0, 'f', 2);
    };
    const auto add_comparison = [table, result_text](const QJsonObject& comparison) {
      const int row = table->rowCount();
      table->insertRow(row);
      const auto software = comparison.value("software").toObject();
      const auto hardware = comparison.value("hardware").toObject();
      const bool active = hardware.value("active").toBool();
      const bool recommended = comparison.value("recommend_hardware").toBool();
      const double ratio = comparison.value("speedup").toDouble();
      const QString backend = hardware.value("backend").toString("unavailable");
      const QStringList values{
          comparison.value("name").toString(), result_text(software),
          active ? QString("%1\n%2").arg(backend, result_text(hardware))
                 : QString("Not accelerated\n%1").arg(result_text(hardware)),
          ratio > 0.0 ? QString("%1×").arg(ratio, 0, 'f', 2) : QString("—"),
          recommended ? QString("Use GPU") : QString("Use software")};
      for (int column = 0; column < values.size(); ++column) {
        auto* item = new QTableWidgetItem(values[column]);
        item->setTextAlignment(Qt::AlignCenter);
        table->setItem(row, column, item);
      }
    };
    add_comparison(root.value("decode").toObject());
    for (const auto value : root.value("encoders").toArray())
      add_comparison(value.toObject());
    table->resizeRowsToContents();
    layout->addWidget(table, 1);
    auto* note = new QLabel(
        "Recommendations are codec-specific. Keep GPU options available, but enable full-transcode hardware only for profiles marked ‘Use GPU’. Smart-render boundary GOP encoding remains software-only. You can change preview decoding and the full-transcode backend in Tools > Options.",
        &dialog);
    note->setWordWrap(true);
    layout->addWidget(note);
    auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close, &dialog);
    auto* options = buttons->addButton("Open Hardware Options…", QDialogButtonBox::ActionRole);
    layout->addWidget(buttons);
    connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
    connect(options, &QPushButton::clicked, &dialog, [&dialog] { dialog.accept(); });
    const bool open_options = dialog.exec() == QDialog::Accepted;
    status_->setText("Hardware self-test completed; recommendations shown by workload");
    if (open_options) QTimer::singleShot(0, this, &ExportWindow::show_program_options);
  }
  void show_program_options() {
    QDialog dialog(this); dialog.setWindowTitle("Program Options"); dialog.resize(780, 590); auto* layout = new QVBoxLayout(&dialog); auto* tabs = new QTabWidget(&dialog); layout->addWidget(tabs, 1);

    auto* general = new QWidget(tabs); auto* general_layout = new QVBoxLayout(general); auto* general_form = new QFormLayout; auto* multiple_files = new QComboBox(general); multiple_files->addItem("Prompt to Join or Combine", "prompt"); multiple_files->addItem("Join (sort and add to Joiner)", "join"); multiple_files->addItem("Combine (sort and open as one program)", "combine"); multiple_files->setCurrentIndex(std::max(0, multiple_files->findData(multiple_file_action_))); general_form->addRow("Multiple file open or drag:", multiple_files); general_layout->addLayout(general_form); auto* auto_replace = new QCheckBox("Automatically replace overlapping cuts", general); auto_replace->setChecked(auto_replace_overlapping_cuts_); auto* prompt_joiner = new QCheckBox("Prompt for Joiner descriptions", general); prompt_joiner->setChecked(prompt_joiner_descriptions_); auto* clear_recent = new QPushButton("Clear Recent Files / Folders", general); bool clear_recent_requested = false; general_layout->addWidget(auto_replace); general_layout->addWidget(prompt_joiner); general_layout->addSpacing(12); general_layout->addWidget(clear_recent); general_layout->addStretch(); connect(clear_recent, &QPushButton::clicked, &dialog, [clear_recent, &clear_recent_requested] { clear_recent_requested = true; clear_recent->setText("Recent files will be cleared when OK is pressed"); clear_recent->setEnabled(false); }); tabs->addTab(general, "General");

    auto* navigation = new QWidget(tabs); auto* navigation_layout = new QVBoxLayout(navigation); auto* navigation_form = new QFormLayout; auto* short_jump = new QSpinBox(navigation); short_jump->setRange(1, 600); short_jump->setSuffix(" seconds"); short_jump->setValue(short_jump_seconds_); auto* long_jump = new QSpinBox(navigation); long_jump->setRange(1, 3600); long_jump->setSuffix(" seconds"); long_jump->setValue(long_jump_seconds_); navigation_form->addRow("Up/Down and double-arrow jump:", short_jump); navigation_form->addRow("Page and triple-arrow jump:", long_jump); navigation_layout->addLayout(navigation_form); auto* navigation_note = new QLabel("Left/Right move one display frame. Home/End move to program bounds; Tab and Shift+Tab move between edit boundaries.", navigation); navigation_note->setWordWrap(true); navigation_layout->addWidget(navigation_note); navigation_layout->addStretch(); tabs->addTab(navigation, "Navigation");

    auto* playback = new QWidget(tabs); auto* playback_layout = new QVBoxLayout(playback); auto* no_audio = new QCheckBox("Edit without audio (video-only preview playback)", playback); no_audio->setChecked(edit_without_audio_); auto* hardware_decode = new QCheckBox("Use hardware decoding for preview, thumbnails and Ad-Detective", playback); hardware_decode->setChecked(hardware_decoding_enabled_); playback_layout->addWidget(no_audio); playback_layout->addWidget(hardware_decode); auto* playback_note = new QLabel("Enable video-only mode when no usable audio device is available. Hardware decoding uses the default D3D11VA or Vulkan adapter and falls back to software when unsupported. Tools > Hardware Acceleration Self-Test compares both paths on this PC. Saved streams are not changed.", playback); playback_note->setWordWrap(true); playback_layout->addWidget(playback_note); playback_layout->addStretch(); tabs->addTab(playback, "Playback Devices");

    auto* enhanced = new QWidget(tabs); auto* enhanced_layout = new QVBoxLayout(enhanced); auto* enhanced_form = new QFormLayout; auto* plot_size = new QComboBox(enhanced); plot_size->addItems({"Small", "Medium", "Large", "Extra Large"}); plot_size->setCurrentIndex(plot_mask_size_); enhanced_form->addRow("Plot masking area:", plot_size); enhanced_layout->addLayout(enhanced_form); auto* enhanced_note = new QLabel("View > Show Plot Mask covers the centre of the preview so cuts can be made without revealing most of the program. It never changes exported or captured frames.", enhanced); enhanced_note->setWordWrap(true); enhanced_layout->addWidget(enhanced_note); enhanced_layout->addStretch(); tabs->addTab(enhanced, "Enhanced Features");

    auto* titles = new QWidget(tabs); auto* titles_layout = new QVBoxLayout(titles); auto* titles_form = new QFormLayout; const auto duration_control = [titles](double value) { auto* control = new QDoubleSpinBox(titles); control->setRange(0.0, 3600.0); control->setDecimals(3); control->setSingleStep(0.1); control->setSuffix(" seconds"); control->setValue(value); return control; }; auto* title_fade_in = duration_control(title_fade_in_seconds_); auto* title_duration = duration_control(title_duration_seconds_); title_duration->setMinimum(0.1); auto* title_fade_out = duration_control(title_fade_out_seconds_); titles_form->addRow("Fade-in:", title_fade_in); titles_form->addRow("Title duration:", title_duration); titles_form->addRow("Fade-out:", title_fade_out); titles_layout->addLayout(titles_form); auto* titles_note = new QLabel("These durations apply to every title card added through the Joiner. Titles use the adjacent program's video format and preserve the complete stream layout; non-video streams remain silent during the title.", titles); titles_note->setWordWrap(true); titles_layout->addWidget(titles_note); titles_layout->addStretch(); tabs->addTab(titles, "Titles");

    auto* alerts = new QWidget(tabs); auto* alerts_layout = new QVBoxLayout(alerts); auto* alerts_form = new QFormLayout; auto* alert_mode = new QComboBox(alerts); alert_mode->addItem("Never", 0); alert_mode->addItem("Interactive only", 1); alert_mode->addItem("Interactive and Batch", 2); alert_mode->setCurrentIndex(alert_mode->findData(completion_alert_mode_)); auto* wave = new QLineEdit(completion_alert_wave_, alerts); auto* browse = new QPushButton("Browse…", alerts); auto* wave_row = new QWidget(alerts); auto* wave_layout = new QHBoxLayout(wave_row); wave_layout->setContentsMargins(0, 0, 0, 0); wave_layout->addWidget(wave, 1); wave_layout->addWidget(browse); alerts_form->addRow("Upon output completion play sound:", alert_mode); alerts_form->addRow("External WAV file:", wave_row); alerts_layout->addLayout(alerts_form); auto* test = new QPushButton("Test Sound", alerts); alerts_layout->addWidget(test); alerts_layout->addStretch(); connect(browse, &QPushButton::clicked, &dialog, [wave, &dialog] { const auto path = QFileDialog::getOpenFileName(&dialog, "Select Completion Sound", wave->text(), "Wave Audio (*.wav);;All Files (*)"); if (!path.isEmpty()) wave->setText(path); }); connect(test, &QPushButton::clicked, &dialog, [this, wave] { const auto old_mode = completion_alert_mode_; const auto old_wave = completion_alert_wave_; completion_alert_mode_ = 1; completion_alert_wave_ = wave->text(); play_completion_alert(false); completion_alert_mode_ = old_mode; completion_alert_wave_ = old_wave; }); tabs->addTab(alerts, "Audio Alerts");

    auto* output_page = new QWidget(tabs); auto* output_layout = new QVBoxLayout(output_page); auto* output_form = new QFormLayout; auto folder_row = [&dialog](QLineEdit* edit, const QString& caption) { auto* row = new QWidget(&dialog); auto* row_layout = new QHBoxLayout(row); row_layout->setContentsMargins(0, 0, 0, 0); auto* browse_button = new QPushButton("Browse…", row); row_layout->addWidget(edit, 1); row_layout->addWidget(browse_button); connect(browse_button, &QPushButton::clicked, &dialog, [edit, caption, &dialog] { const auto path = QFileDialog::getExistingDirectory(&dialog, caption, edit->text()); if (!path.isEmpty()) edit->setText(path); }); return row; }; auto* output_folder = new QLineEdit(output_default_folder_, output_page); auto* project_folder = new QLineEdit(project_default_folder_, output_page); auto* output_extension = new QComboBox(output_page); output_extension->addItem("Matroska (.mkv)", "mkv"); output_extension->addItem("MPEG-4 (.mp4)", "mp4"); output_extension->addItem("QuickTime (.mov)", "mov"); output_extension->addItem("MPEG program stream (.mpg)", "mpg"); output_extension->addItem("MPEG transport stream (.ts)", "ts"); output_extension->setCurrentIndex(std::max(0, output_extension->findData(output_default_extension_))); auto* save_priority = new QComboBox(output_page); save_priority->addItem("Idle", "idle"); save_priority->addItem("Below normal", "below-normal"); save_priority->addItem("Normal", "normal"); save_priority->addItem("Above normal", "above-normal"); save_priority->setCurrentIndex(std::max(0, save_priority->findData(save_cpu_priority_))); auto* hardware_encode = new QCheckBox("Use hardware encoding for full transcoding", output_page); hardware_encode->setChecked(hardware_transcoding_enabled_); auto* hardware_backend = new QComboBox(output_page); hardware_backend->addItem("Automatic (available adapter)", "auto"); hardware_backend->addItem("NVIDIA NVENC", "nvenc"); hardware_backend->addItem("Intel Quick Sync", "qsv"); hardware_backend->addItem("AMD AMF", "amf"); hardware_backend->addItem("Vulkan (FFV1 / ProRes)", "vulkan"); hardware_backend->setCurrentIndex(std::max(0, hardware_backend->findData(hardware_transcoding_backend_))); hardware_backend->setEnabled(hardware_encode->isChecked()); connect(hardware_encode, &QCheckBox::toggled, hardware_backend, &QWidget::setEnabled); auto* clear_after_queue = new QCheckBox("Clear current cuts after successfully queueing a Batch job", output_page); clear_after_queue->setChecked(clear_cuts_after_queue_); output_form->addRow("Default output folder (blank = source):", folder_row(output_folder, "Default Output Folder")); output_form->addRow("Default project folder (blank = source):", folder_row(project_folder, "Default Project Folder")); output_form->addRow("Default output container:", output_extension); output_form->addRow("Save-process CPU priority:", save_priority); output_form->addRow("Hardware encoder:", hardware_backend); output_layout->addLayout(output_form); output_layout->addWidget(hardware_encode); output_layout->addWidget(clear_after_queue); auto* output_note = new QLabel("Hardware encoding applies only to full-transcode profiles and is off by default. Vulkan accelerates FFV1 and ProRes when the installed GPU driver supports it; unsupported systems fall back automatically to software. Smart-cut boundary GOPs always use the software encoder. Preview, thumbnail and Ad-Detective decoding automatically use D3D11VA or Vulkan and fall back to software.", output_page); output_note->setWordWrap(true); output_layout->addWidget(output_note); output_layout->addStretch(); tabs->addTab(output_page, "Output & Folders");

    QHash<QString, QComboBox*> codec_hardware_controls;
    auto* codec_hardware_group = new QGroupBox("Per-codec full-transcode preference", output_page);
    auto* codec_hardware_form = new QFormLayout(codec_hardware_group);
    const QList<QPair<QString, QString>> codec_rows{{"mpeg2", "MPEG-2 / DVD"}, {"h264", "H.264 / AVC"}, {"hevc", "HEVC / H.265"}, {"av1", "AV1"}, {"vp9", "VP9"}, {"ffv1", "FFV1"}, {"prores", "Apple ProRes"}};
    for (const auto& [codec, label] : codec_rows) {
      auto* choice = new QComboBox(codec_hardware_group);
      choice->addItem("Use global backend", "global");
      choice->addItem("Software only", "software");
      choice->addItem("Automatic available GPU", "auto");
      if (codec == "mpeg2") choice->addItem("Intel Quick Sync", "qsv");
      else if (codec == "ffv1" || codec == "prores") choice->addItem("Vulkan", "vulkan");
      else if (codec != "vp9") { choice->addItem("NVIDIA NVENC", "nvenc"); choice->addItem("Intel Quick Sync", "qsv"); choice->addItem("AMD AMF", "amf"); }
      const int selected = choice->findData(hardware_codec_backends_.value(codec, "global"));
      choice->setCurrentIndex(selected < 0 ? 0 : selected);
      codec_hardware_form->addRow(label + ':', choice);
      codec_hardware_controls.insert(codec, choice);
    }
    codec_hardware_group->setEnabled(hardware_encode->isChecked());
    connect(hardware_encode, &QCheckBox::toggled, codec_hardware_group, &QWidget::setEnabled);
    output_layout->insertWidget(std::max(0, output_layout->count() - 1), codec_hardware_group);

    auto* startup = new QWidget(tabs); auto* startup_layout = new QVBoxLayout(startup); auto* startup_form = new QFormLayout; QColor startup_background = startup_background_colour_; QColor selection_colour = selection_time_colour_; auto* background_button = new QPushButton(startup_background.name(), startup); auto* selection_button = new QPushButton(selection_colour.name(), startup); auto update_colour_button = [](QPushButton* button, const QColor& colour) { button->setText(colour.name()); button->setStyleSheet(QString("background:%1;color:%2;").arg(colour.name(), colour.lightness() < 128 ? "white" : "black")); }; update_colour_button(background_button, startup_background); update_colour_button(selection_button, selection_colour); auto* log_folder = new QLineEdit(log_folder_, startup); startup_form->addRow("Editor background colour:", background_button); startup_form->addRow("Selection-time colour:", selection_button); startup_form->addRow("Log file folder:", folder_row(log_folder, "SmartCut64 Log Folder")); startup_layout->addLayout(startup_form); auto* startup_note = new QLabel("Appearance settings are applied immediately after OK. Worker and validation messages are mirrored to SmartCut64.log in the selected folder.", startup); startup_note->setWordWrap(true); startup_layout->addWidget(startup_note); startup_layout->addStretch(); connect(background_button, &QPushButton::clicked, &dialog, [&dialog, background_button, &startup_background, update_colour_button] { const auto colour = QColorDialog::getColor(startup_background, &dialog, "Editor Background"); if (colour.isValid()) { startup_background = colour; update_colour_button(background_button, colour); } }); connect(selection_button, &QPushButton::clicked, &dialog, [&dialog, selection_button, &selection_colour, update_colour_button] { const auto colour = QColorDialog::getColor(selection_colour, &dialog, "Selection Time Colour"); if (colour.isValid()) { selection_colour = colour; update_colour_button(selection_button, colour); } }); tabs->addTab(startup, "Start Up");

    auto* favorites = new QWidget(tabs); auto* favorites_layout = new QVBoxLayout(favorites); auto* favorites_form = new QFormLayout; QVector<QLineEdit*> favorite_edits; for (int index = 0; index < 5; ++index) { auto* edit = new QLineEdit(index < favorite_places_.size() ? favorite_places_[index] : QString{}, favorites); favorite_edits.push_back(edit); favorites_form->addRow(QString("Favorite %1:").arg(index + 1), folder_row(edit, QString("Favorite Place %1").arg(index + 1))); } favorites_layout->addLayout(favorites_form); auto* favorites_note = new QLabel("Configured folders appear under File > Favorite Places and open the video picker at that location.", favorites); favorites_note->setWordWrap(true); favorites_layout->addWidget(favorites_note); favorites_layout->addStretch(); tabs->addTab(favorites, "Favorite Places");

    auto* file_types = new QWidget(tabs); auto* file_types_layout = new QVBoxLayout(file_types); auto* extension_list = new QListWidget(file_types); for (const auto& extension : known_shell_extensions()) { auto* item = new QListWidgetItem("." + extension, extension_list); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); item->setCheckState(associated_extensions_.contains(extension, Qt::CaseInsensitive) ? Qt::Checked : Qt::Unchecked); } file_types_layout->addWidget(new QLabel("Show the SmartCut64 command in Explorer for these file types:", file_types)); file_types_layout->addWidget(extension_list, 1); auto* register_now = new QPushButton("Register Checked Associations Now", file_types); file_types_layout->addWidget(register_now); auto* association_note = new QLabel("Registration is per-user and adds an Open with SmartCut64 command without replacing the Windows default application.", file_types); association_note->setWordWrap(true); file_types_layout->addWidget(association_note); const auto checked_extensions = [extension_list] { QStringList values; for (int row = 0; row < extension_list->count(); ++row) if (extension_list->item(row)->checkState() == Qt::Checked) values.push_back(extension_list->item(row)->text().mid(1)); return values; }; connect(register_now, &QPushButton::clicked, &dialog, [this, register_now, checked_extensions] { const bool registered = register_shell_associations(checked_extensions()); register_now->setText(registered ? "Explorer associations registered" : "Association registration failed"); }); tabs->addTab(file_types, "File Types");

    output_extension->addItem("Windows Television (.wtv)", "wtv"); output_extension->addItem("DVR-MS (.dvr-ms)", "dvr-ms"); output_extension->addItem("Topfield recording (.rec)", "rec"); output_extension->addItem("Material Exchange Format (.mxf)", "mxf"); output_extension->setCurrentIndex(std::max(0, output_extension->findData(output_default_extension_)));
    auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); layout->addWidget(buttons); connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); if (dialog.exec() != QDialog::Accepted) return;
    short_jump_seconds_ = short_jump->value(); long_jump_seconds_ = long_jump->value(); multiple_file_action_ = multiple_files->currentData().toString(); auto_replace_overlapping_cuts_ = auto_replace->isChecked(); prompt_joiner_descriptions_ = prompt_joiner->isChecked(); edit_without_audio_ = no_audio->isChecked(); hardware_decoding_enabled_ = hardware_decode->isChecked(); plot_mask_size_ = plot_size->currentIndex(); title_duration_seconds_ = title_duration->value(); title_fade_in_seconds_ = title_fade_in->value(); title_fade_out_seconds_ = title_fade_out->value(); completion_alert_mode_ = alert_mode->currentData().toInt(); completion_alert_wave_ = wave->text().trimmed(); output_default_folder_ = output_folder->text().trimmed(); project_default_folder_ = project_folder->text().trimmed(); output_default_extension_ = output_extension->currentData().toString(); save_cpu_priority_ = save_priority->currentData().toString(); hardware_transcoding_enabled_ = hardware_encode->isChecked(); hardware_transcoding_backend_ = hardware_backend->currentData().toString(); clear_cuts_after_queue_ = clear_after_queue->isChecked(); startup_background_colour_ = startup_background; selection_time_colour_ = selection_colour; log_folder_ = log_folder->text().trimmed(); favorite_places_.clear(); for (auto* edit : favorite_edits) if (!edit->text().trimmed().isEmpty()) favorite_places_.push_back(QDir::toNativeSeparators(edit->text().trimmed())); associated_extensions_ = checked_extensions(); QSettings settings("SmartCut64", "SmartCut64"); settings.setValue("navigation/shortJumpSeconds", short_jump_seconds_); settings.setValue("navigation/longJumpSeconds", long_jump_seconds_); settings.setValue("files/multipleOpenAction", multiple_file_action_); settings.setValue("editing/autoReplaceOverlappingCuts", auto_replace_overlapping_cuts_); settings.setValue("joiner/promptDescriptions", prompt_joiner_descriptions_); settings.setValue("playback/editWithoutAudio", edit_without_audio_); settings.setValue("playback/hardwareDecodingEnabled", hardware_decoding_enabled_); settings.setValue("display/plotMaskSize", plot_mask_size_); settings.setValue("titles/durationSeconds", title_duration_seconds_); settings.setValue("titles/fadeInSeconds", title_fade_in_seconds_); settings.setValue("titles/fadeOutSeconds", title_fade_out_seconds_); settings.setValue("audioAlerts/mode", completion_alert_mode_); settings.setValue("audioAlerts/waveFile", completion_alert_wave_); settings.setValue("output/defaultFolder", output_default_folder_); settings.setValue("projects/defaultFolder", project_default_folder_); settings.setValue("output/defaultExtension", output_default_extension_); settings.setValue("output/cpuPriority", save_cpu_priority_); settings.setValue("output/hardwareTranscodingEnabled", hardware_transcoding_enabled_); settings.setValue("output/hardwareTranscodingBackend", hardware_transcoding_backend_); for (auto it = codec_hardware_controls.cbegin(); it != codec_hardware_controls.cend(); ++it) { const auto preference = it.value()->currentData().toString(); hardware_codec_backends_.insert(it.key(), preference); settings.setValue("output/hardwareByCodec/" + it.key(), preference); } settings.setValue("batch/clearCutsAfterQueue", clear_cuts_after_queue_); settings.setValue("startup/backgroundColour", startup_background_colour_.name()); settings.setValue("startup/selectionTimeColour", selection_time_colour_.name()); settings.setValue("startup/logFolder", log_folder_); settings.setValue("files/favoritePlaces", favorite_places_); settings.setValue("files/associatedExtensions", associated_extensions_); if (clear_recent_requested) settings.remove("recentFiles"); apply_hardware_decode_preference(); preview_->setStyleSheet(QString("background:%1;color:#c8c8c8;border:1px solid #777777;").arg(startup_background_colour_.name())); selection_start_->setStyleSheet(QString("color:%1;").arg(selection_time_colour_.name())); program_end_->setStyleSheet(QString("color:%1;").arg(selection_time_colour_.name())); register_shell_associations(associated_extensions_); flush_application_log(); if (!current_preview_image_.isNull()) show_preview_image(current_preview_image_, displayed_preview_frame_); status_->setText("Program options updated");
  }
  void show_editor_context_menu(const QPoint& point) { auto* source = qobject_cast<QWidget*>(sender()); QMenu menu(this); auto* preview_mode = menu.addAction(timeline_->preview_mode() ? "Switch To Edit Mode" : "Preview Your Edits"); menu.addSeparator(); auto* mark_in = menu.addAction("Set Selection Start"); auto* mark_out = menu.addAction("Set Selection End"); auto* cut = menu.addAction(scene_mode_ ? "Add Selection" : "Cut Selection"); auto* trim = menu.addAction(scene_mode_ ? "Add Unselected" : "Trim Unselected"); menu.addSeparator(); auto* goto_in = menu.addAction("Go To Selection Start"); auto* goto_out = menu.addAction("Go To Selection End"); auto* goto_start = menu.addAction("Go To Program Start"); auto* goto_end = menu.addAction("Go To End Of Program"); auto* previous_keyframe = menu.addAction("Previous Keyframe"); auto* next_keyframe = menu.addAction("Next Keyframe"); auto* goto_time = menu.addAction("Go To Timecode…"); menu.addSeparator(); auto* bookmark = menu.addAction("Set Bookmark"); auto* goto_bookmark = menu.addAction("Go To Bookmark"); auto* thumbnails = menu.addAction(thumbnail_panel_->isVisible() ? "Hide Thumbnails" : "Show Thumbnails"); auto* plot_mask = menu.addAction(plot_mask_visible_ ? "Hide Plot Mask" : "Show Plot Mask"); auto* chapter = menu.addAction("Add / Remove Chapter Mark"); auto* capture = menu.addAction("Capture Frame…"); auto* info = menu.addAction("Program Information…"); const auto chosen = menu.exec(source ? source->mapToGlobal(point) : QCursor::pos()); if (chosen == preview_mode) set_preview_mode(!timeline_->preview_mode()); else if (chosen == mark_in) { set_preview_mode(false); timeline_->mark_in(); } else if (chosen == mark_out) { set_preview_mode(false); timeline_->mark_out(); } else if (chosen == cut) cut_selected_range(); else if (chosen == trim) trim_unselected(); else if (chosen == goto_in) timeline_->goto_in_mark(); else if (chosen == goto_out) timeline_->goto_out_mark(); else if (chosen == goto_start) timeline_->goto_beginning(); else if (chosen == goto_end) timeline_->goto_end(); else if (chosen == previous_keyframe) timeline_->previous_random_access(); else if (chosen == next_keyframe) timeline_->next_random_access(); else if (chosen == goto_time) show_go_to_timecode(); else if (chosen == bookmark) timeline_->set_bookmark(); else if (chosen == goto_bookmark) timeline_->goto_bookmark(); else if (chosen == thumbnails && thumbnails_action_) thumbnails_action_->setChecked(!thumbnail_panel_->isVisible()); else if (chosen == plot_mask && plot_mask_action_) plot_mask_action_->setChecked(!plot_mask_visible_); else if (chosen == chapter) timeline_->toggle_chapter_marker(); else if (chosen == capture) capture_frame(); else if (chosen == info) show_program_information(); }
  void show_thumbnail_context_menu(const QPoint& point) { QMenu menu(this); auto* intervals = menu.addMenu("Thumbnail Interval"); const auto& values = thumbnail_intervals(); for (int step = 0; step < values.size(); ++step) { auto* action = intervals->addAction(step == 0 ? "Single frame" : QString("%1 seconds").arg(values[step])); action->setData(step); } auto* options = menu.addAction("Thumbnail Options…"); const auto chosen = menu.exec(thumbnail_panel_->mapToGlobal(point)); if (chosen == options) show_thumbnail_options(); else if (chosen && chosen->data().isValid()) set_thumbnail_interval_step(chosen->data().toInt()); }
  QString default_output_path() const { if (input_->text().isEmpty()) return {}; const QFileInfo source(input_->text()); const auto folder = !output_default_folder_.isEmpty() && QDir(output_default_folder_).exists() ? output_default_folder_ : source.absolutePath(); return QDir(folder).filePath(source.completeBaseName() + "-edited." + output_default_extension_); }
  QString default_project_path() const { const QFileInfo source(input_->text()); const auto folder = !project_default_folder_.isEmpty() && QDir(project_default_folder_).exists() ? project_default_folder_ : source.absolutePath(); return QDir(folder).filePath(source.completeBaseName() + ".smartcut"); }
  void apply_hardware_decode_preference() const {
    qputenv("SMARTCUT_DISABLE_HARDWARE_DECODE",
            hardware_decoding_enabled_ ? QByteArrayLiteral("0") : QByteArrayLiteral("1"));
  }
  void apply_save_priority(QProcess& process) const {
#ifdef _WIN32
    DWORD priority = BELOW_NORMAL_PRIORITY_CLASS; if (save_cpu_priority_ == "idle") priority = IDLE_PRIORITY_CLASS; else if (save_cpu_priority_ == "normal") priority = NORMAL_PRIORITY_CLASS; else if (save_cpu_priority_ == "above-normal") priority = ABOVE_NORMAL_PRIORITY_CLASS; const auto handle = OpenProcess(PROCESS_SET_INFORMATION, FALSE, static_cast<DWORD>(process.processId())); if (handle) { SetPriorityClass(handle, priority); CloseHandle(handle); }
#else
    (void)process;
#endif
  }
  void open_startup_path(const QString& path) { const auto lower = path.toLower(); if (lower.endsWith(".smartcut") || lower.endsWith(".smartcut.json") || lower.endsWith(".vprj") || lower.endsWith(".bprj")) request_open_project(path); else if (lower.endsWith(".scjoin") || lower.endsWith(".scjoin.json") || lower.endsWith(".vjnr")) { show_editor_workspace(); if (load_joiner_project(path)) status_->setText(QString("Loaded Joiner project: %1 items").arg(joiner_inputs_.size())); else status_->setText("Could not load Joiner project"); } else { show_editor_workspace(); load_source(path); } }
  static QString output_with_extension(const QString& path, const QString& extension) {
    const QFileInfo info(path);
    return QDir::toNativeSeparators(info.dir().filePath(info.completeBaseName() + "." + extension));
  }
  bool choose_transport_transcode(const ContainerCompatibilityReport& report,
                                  OutputProfile& profile, QWidget* parent) {
    QDialog dialog(parent);
    dialog.setWindowTitle("Transport Stream Transcode Options");
    dialog.setMinimumWidth(520);
    auto* layout = new QVBoxLayout(&dialog);
    auto* explanation = new QLabel(
        QString("Choose codecs that %1 can identify reliably. Video conversion requires a full "
                "recode; an audio-only conversion still allows compatible video GOPs to be copied.")
            .arg(report.container), &dialog);
    explanation->setWordWrap(true);
    layout->addWidget(explanation);
    auto* form = new QFormLayout;
    QComboBox* video = nullptr;
    QComboBox* audio = nullptr;
    if (report.needs_video_transcode) {
      video = new QComboBox(&dialog);
      video->addItem("H.264 / AVC (widest compatibility)", "h264");
      video->addItem("H.265 / HEVC", "hevc");
      video->addItem("MPEG-2 Video", "mpeg2");
      form->addRow("Video:", video);
    }
    if (report.needs_audio_transcode) {
      audio = new QComboBox(&dialog);
      audio->addItem("Dolby Digital (AC-3)", "ac3");
      if (report.container != "Blu-ray M2TS") {
        audio->addItem("AAC", "aac");
        audio->addItem("MPEG Layer II", "mp2");
      }
      form->addRow("Audio:", audio);
    }
    layout->addLayout(form);
    auto* note = new QLabel(
        "Software encoders are selected for deterministic availability. The completed file will still "
        "be decoded and structurally validated before SmartCut64 reports success.", &dialog);
    note->setWordWrap(true);
    layout->addWidget(note);
    auto* buttons = new QDialogButtonBox(QDialogButtonBox::Save | QDialogButtonBox::Cancel, &dialog);
    buttons->button(QDialogButtonBox::Save)->setText("Transcode and Save");
    layout->addWidget(buttons);
    connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
    connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
    if (dialog.exec() != QDialog::Accepted) return false;
    const auto video_codec = video ? video->currentData().toString() : QString{};
    const auto audio_codec = audio ? audio->currentData().toString() : QString{};
    apply_transport_transcode(profile, video_codec, audio_codec);
    if (!audio_codec.isEmpty()) {
      int source_channels = 2;
      for (const auto value : source_streams_) {
        const auto stream = value.toObject();
        const int index = stream.value("index").toInt(-1);
        if ((!selected_streams_.isEmpty() && !selected_streams_.contains(index)) ||
            stream.value("type").toInt(-1) != 1) continue;
        source_channels = std::max(1, stream.value("channels").toInt(2));
        break;
      }
      profile.audio_channels = audio_codec == "mp2" ? std::min(source_channels, 2)
                                                     : std::min(source_channels, 6);
      profile.audio_sample_rate = 48000;
    }
    return true;
  }
  bool resolve_transport_compatibility(QString& path, OutputProfile& profile, QWidget* parent) {
    auto report = transport_compatibility(source_streams_, selected_streams_, profile,
                                          QFileInfo(path).suffix());
    if (report.issues.isEmpty() && report.warnings.isEmpty()) return true;
    QMessageBox message(parent);
    message.setWindowTitle("Output Container Compatibility");
    message.setIcon(report.issues.isEmpty() ? QMessageBox::Information : QMessageBox::Warning);
    message.setText(report.issues.isEmpty()
                        ? QString("%1 can save this file, with the limitation described below.")
                              .arg(report.container)
                        : QString("%1 cannot preserve every selected stream as a reliably playable "
                                  "codec.")
                              .arg(report.container));
    QStringList details;
    for (const auto& issue : report.issues) details.push_back(issue.explanation);
    details.append(report.warnings);
    if (!report.can_transcode && !report.issues.isEmpty())
      details.push_back("At least one incompatible secondary, subtitle, attachment, or data stream "
                        "cannot currently be transcoded. Matroska preserves it without conversion; "
                        "alternatively, cancel and deselect that stream.");
    message.setInformativeText(details.join("\n\n"));
    auto* matroska = message.addButton("Use Matroska (.mkv)", QMessageBox::AcceptRole);
    QAbstractButton* transcode = nullptr;
    QAbstractButton* continue_private = nullptr;
    if (report.can_transcode)
      transcode = message.addButton("Choose Transcode Options…", QMessageBox::ActionRole);
    if (report.issues.isEmpty())
      continue_private = message.addButton(QString("Save as %1").arg(QFileInfo(path).suffix().toUpper()),
                                           QMessageBox::ActionRole);
    auto* cancel = message.addButton(QMessageBox::Cancel);
    message.setDefaultButton(qobject_cast<QPushButton*>(matroska));
    message.exec();
    if (message.clickedButton() == matroska) {
      path = output_with_extension(path, "mkv");
      profile.file_type = "MKV";
      profile.file_extension = "mkv";
      profile.ts_m2ts_mode = false;
      return true;
    }
    if (transcode && message.clickedButton() == transcode) {
      if (!choose_transport_transcode(report, profile, parent)) return false;
      const auto repaired = transport_compatibility(source_streams_, selected_streams_, profile,
                                                    QFileInfo(path).suffix());
      if (!repaired.issues.isEmpty()) {
        QMessageBox::critical(parent, "Output Container Compatibility",
                              "The selected transcode settings do not resolve every container conflict.");
        return false;
      }
      return true;
    }
    if (continue_private && message.clickedButton() == continue_private) return true;
    Q_UNUSED(cancel);
    return false;
  }
  SaveVideoChoice choose_output_profile() {
    license_.reload();
    QVector<OutputProfile> profiles;
    QSettings profile_settings("SmartCut64", "SmartCut64");
    const auto favorite_value = profile_settings.value("output/favoriteProfiles");
    const auto favorite_ids = favorite_value.toStringList();
    for (auto profile : standard_output_profiles()) {
      if (!save_dialog_profile(profile)) continue;
      profile.favorite = favorite_value.isValid() && favorite_ids.contains(profile.id);
      profiles.push_back(std::move(profile));
    }
    for (auto profile : custom_profiles_) if (profile.enabled) { if (favorite_value.isValid()) profile.favorite = favorite_ids.contains(profile.id); profiles.push_back(std::move(profile)); }
    if (profiles.isEmpty()) return SaveVideoChoice::cancel;

    QDialog dialog(this); dialog.setWindowTitle("Save Video"); dialog.resize(660, 520);
    auto* layout = new QVBoxLayout(&dialog);
    auto* output_label = new QLabel("Output File", &dialog); output_label->setStyleSheet("font-weight:600;"); layout->addWidget(output_label);
    auto* path = new QLineEdit(output_->text().isEmpty() ? default_output_path() : output_->text(), &dialog);
    path->setAccessibleName("Output file");
    auto* browse = new QPushButton("Select File", &dialog); browse->setAccessibleName("Select output file");
    auto* path_row = new QHBoxLayout; path_row->addWidget(path, 1); path_row->addWidget(browse); layout->addLayout(path_row);
    if (!license_.paid()) { auto* evaluation = new QLabel("Evaluation mode: every video output contains a permanent SmartCut64 watermark and the full video must be re-encoded. Install a paid licence under Help > Licence & Activation for clean smart-render output.", &dialog); evaluation->setWordWrap(true); evaluation->setStyleSheet("color:#a52a2a;font-weight:600;padding:5px;border:1px solid #d7a0a0;background:#fff3f3;"); layout->addWidget(evaluation); }

    auto* profile_label = new QLabel("Profile", &dialog); profile_label->setStyleSheet("font-weight:600;"); layout->addWidget(profile_label);
    auto* selected_name = new QLineEdit(&dialog); selected_name->setReadOnly(true); selected_name->setAccessibleName("Selected output profile");
    auto* profile_options = new QPushButton("Profile Options", &dialog); profile_options->setAccessibleName("Edit selected output profile options");
    auto* profile_row = new QHBoxLayout; profile_row->addWidget(selected_name, 1); profile_row->addWidget(profile_options); layout->addLayout(profile_row);

    auto* table = new QTableWidget(0, 4, &dialog);
    table->setHorizontalHeaderLabels({"Profile", "Codec", "Container", "Output Mode"});
    table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch);
    table->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
    table->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
    table->horizontalHeader()->setSectionResizeMode(3, QHeaderView::ResizeToContents);
    table->verticalHeader()->hide(); table->setSelectionBehavior(QAbstractItemView::SelectRows);
    table->setSelectionMode(QAbstractItemView::SingleSelection); table->setEditTriggers(QAbstractItemView::NoEditTriggers);
    table->setContextMenuPolicy(Qt::CustomContextMenu); table->setAccessibleName("Output profiles"); layout->addWidget(table, 1);
    auto* favorites_only = new QCheckBox("Favorites Only", &dialog); favorites_only->setAccessibleName("Show favorite output profiles only");

    const auto profile_index = [table]() { const auto* item = table->item(table->currentRow(), 0); return item ? item->data(Qt::UserRole).toInt() : -1; };
    const auto apply_extension = [this, path](const OutputProfile& selected) {
      const auto extension = output_profile_extension(selected);
      if (extension.isEmpty() || path->text().trimmed().isEmpty()) return;
      const QFileInfo info(path->text());
      path->setText(info.dir().filePath(info.completeBaseName() + "." + extension));
    };
    const auto update_selection = [&, selected_name] {
      const int index = profile_index();
      if (index < 0 || index >= profiles.size()) { selected_name->clear(); profile_options->setEnabled(false); return; }
      selected_name->setText(profiles[index].name); profile_options->setEnabled(true); apply_extension(profiles[index]);
    };
    const auto refresh = [&, table] {
      const QString selected_id = profile_index() >= 0 && profile_index() < profiles.size() ? profiles[profile_index()].id : output_profile_->currentData().toString();
      table->setRowCount(0);
      int selected_row = -1;
      for (int index = 0; index < profiles.size(); ++index) {
        const auto& profile = profiles[index];
        if (favorites_only->isChecked() && !profile.favorite) continue;
        const int row = table->rowCount(); table->insertRow(row);
        auto* name = new QTableWidgetItem(QString("★  %1").arg(profile.name)); name->setData(Qt::UserRole, index);
        name->setForeground(profile.favorite ? QColor("#d9a400") : QColor("#a5a5a5"));
        table->setItem(row, 0, name); table->setItem(row, 1, new QTableWidgetItem(profile_codec_label(profile)));
        table->setItem(row, 2, new QTableWidgetItem(profile_container_label(profile)));
        table->setItem(row, 3, new QTableWidgetItem(profile_mode_label(profile)));
        if (profile.id == selected_id) selected_row = row;
      }
      if (table->rowCount() > 0) table->selectRow(selected_row >= 0 ? selected_row : 0);
      update_selection();
    };
    refresh();
    connect(table, &QTableWidget::itemSelectionChanged, &dialog, update_selection);
    connect(favorites_only, &QCheckBox::toggled, &dialog, [refresh](bool) { refresh(); });
    connect(table, &QWidget::customContextMenuRequested, &dialog, [&, table, refresh](const QPoint& position) {
      const int index = profile_index(); if (index < 0 || index >= profiles.size()) return;
      QMenu menu(table); auto* favorite = menu.addAction(profiles[index].favorite ? "Remove from Favorites" : "Add to Favorites");
      if (menu.exec(table->viewport()->mapToGlobal(position)) != favorite) return;
      profiles[index].favorite = !profiles[index].favorite;
      QStringList ids; for (const auto& profile : profiles) if (profile.favorite) ids.push_back(profile.id);
      QSettings("SmartCut64", "SmartCut64").setValue("output/favoriteProfiles", ids); refresh();
    });
    connect(profile_options, &QPushButton::clicked, &dialog, [&, refresh] {
      const int index = profile_index(); if (index < 0 || index >= profiles.size()) return;
      OutputProfile edited = profiles[index]; if (!edit_output_profile(edited, &dialog)) return;
      edited.id = profiles[index].id; edited.audio_only = profiles[index].audio_only; profiles[index] = std::move(edited); refresh();
    });
    connect(browse, &QPushButton::clicked, &dialog, [path, &dialog] { const auto selected = QFileDialog::getSaveFileName(&dialog, "Save Video", path->text(), "All Supported Media (*.mkv *.mp4 *.m4v *.mov *.mpg *.mpeg *.vob *.ts *.m2ts *.wtv *.dvr-ms *.rec *.mxf *.m2v *.h264 *.hevc *.obu *.ivf *.aac *.ac3 *.eac3 *.mp2 *.mp3 *.opus *.flac *.wav *.dts *.thd);;Matroska Video (*.mkv);;MPEG-4 Video (*.mp4 *.m4v);;QuickTime Movie (*.mov);;MPEG Program Stream (*.mpg *.mpeg *.vob);;MPEG Transport Stream (*.ts *.m2ts);;Elementary Streams (*.m2v *.h264 *.hevc *.obu *.ivf *.aac *.ac3 *.eac3 *.mp2 *.mp3 *.opus *.flac *.wav *.dts *.thd)"); if (!selected.isEmpty()) path->setText(selected); });

    auto* bottom = new QHBoxLayout; bottom->addWidget(favorites_only); bottom->addStretch();
    auto* save = new QToolButton(&dialog); save->setText("Save"); save->setPopupMode(QToolButton::MenuButtonPopup); save->setAccessibleName("Save video or add to batch");
    auto* save_menu = new QMenu(save); auto* save_now = save_menu->addAction("Save"); auto* queue = save_menu->addAction("Add to Batch Queue"); save->setMenu(save_menu);
    auto* cancel = new QPushButton("Cancel", &dialog); cancel->setAccessibleName("Cancel save video"); bottom->addWidget(save); bottom->addWidget(cancel); layout->addLayout(bottom);
    SaveVideoChoice choice = SaveVideoChoice::cancel;
    connect(save, &QToolButton::clicked, &dialog, [&dialog, &choice] { choice = SaveVideoChoice::save; dialog.accept(); });
    connect(save_now, &QAction::triggered, &dialog, [&dialog, &choice] { choice = SaveVideoChoice::save; dialog.accept(); });
    connect(queue, &QAction::triggered, &dialog, [&dialog, &choice] { choice = SaveVideoChoice::queue; dialog.accept(); });
    connect(cancel, &QPushButton::clicked, &dialog, &QDialog::reject);
    connect(table, &QTableWidget::itemDoubleClicked, &dialog, [&dialog, &choice](QTableWidgetItem*) { choice = SaveVideoChoice::save; dialog.accept(); });
    if (dialog.exec() != QDialog::Accepted || choice == SaveVideoChoice::cancel || path->text().trimmed().isEmpty()) return SaveVideoChoice::cancel;
    const int index = profile_index(); if (index < 0 || index >= profiles.size()) return SaveVideoChoice::cancel;
    auto final_path = QDir::toNativeSeparators(path->text().trimmed());
    auto selected_profile = profiles[index];
    const auto suffix = QFileInfo(final_path).suffix().toLower();
    const QStringList allowed{"mkv", "mp4", "m4v", "mov", "mpg", "mpeg", "vob", "ts", "m2ts", "mts", "wtv", "dvr-ms", "dvrms", "rec", "mxf", "m2v", "mpv", "h264", "264", "avc", "hevc", "h265", "265", "obu", "ivf", "aac", "ac3", "eac3", "mp2", "mp3", "opus", "flac", "wav", "dts", "thd"};
    if (!allowed.contains(suffix)) { QMessageBox::warning(&dialog, "Save Video", "Choose a supported container, video elementary stream, or audio elementary stream extension."); return SaveVideoChoice::cancel; }
    if (QFileInfo(final_path).absoluteFilePath() == QFileInfo(input_->text()).absoluteFilePath()) { QMessageBox::warning(&dialog, "Save Video", "The output file must be different from the source file."); return SaveVideoChoice::cancel; }
    if (!resolve_transport_compatibility(final_path, selected_profile, &dialog)) return SaveVideoChoice::cancel;
    if (QFileInfo::exists(final_path) && QMessageBox::question(&dialog, "Save Video", QString("Replace the existing file?\n%1").arg(final_path), QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) return SaveVideoChoice::cancel;
    output_->setText(final_path); pending_output_profile_override_ = selected_profile;
    int combo_index = output_profile_->findData(selected_profile.id);
    if (combo_index < 0) { output_profile_->addItem(selected_profile.name, selected_profile.id); combo_index = output_profile_->count() - 1; }
    output_profile_->setCurrentIndex(combo_index);
    QSettings("SmartCut64", "SmartCut64").setValue("output/defaultProfile", selected_profile.id);
    return choice;
  }
  void show_thumbnail_options() {
    QDialog dialog(this); dialog.setWindowTitle("Thumbnail Options"); dialog.resize(560, 510); auto* layout = new QVBoxLayout(&dialog); auto* form = new QFormLayout;
    auto* interval = new QDoubleSpinBox(&dialog); interval->setRange(0.0, 600.0); interval->setDecimals(2); interval->setSingleStep(0.25); interval->setSuffix(" seconds"); interval->setSpecialValueText("Single frame"); interval->setValue(thumbnail_interval_seconds_);
    auto* size = new QComboBox(&dialog); size->addItem("Small", 58); size->addItem("Medium", 80); size->addItem("Large", 108); const auto size_index = size->findData(thumbnail_height_); size->setCurrentIndex(size_index < 0 ? 1 : size_index);
    auto* brightness = new QSpinBox(&dialog); brightness->setRange(-100, 100); brightness->setSuffix(" %"); brightness->setValue(thumbnail_brightness_);
    auto* timecode = new QCheckBox("Show source timecode and frame number on thumbnails", &dialog); timecode->setChecked(thumbnail_timecode_visible_);
    auto* audio = new QCheckBox("Display decoded audio graph", &dialog); audio->setChecked(thumbnail_audio_visible_);
    auto* scale = new QComboBox(&dialog); scale->addItem("Linear amplitude", false); scale->addItem("Decibel (dB) amplitude", true); scale->setCurrentIndex(waveform_decibel_scale_ ? 1 : 0);
    QColor wave_background = waveform_background_colour_, wave_line = waveform_colour_, wave_centre = waveform_centre_colour_, wave_text = waveform_text_colour_;
    const auto colour_control = [&dialog](QColor& colour, const QString& caption) { auto* button = new QPushButton(colour.name(), &dialog); const auto refresh = [button, &colour] { button->setText(colour.name()); button->setStyleSheet(QString("background:%1;color:%2;").arg(colour.name(), colour.lightness() < 128 ? "white" : "black")); }; refresh(); connect(button, &QPushButton::clicked, &dialog, [button, &dialog, &colour, caption, refresh] { const auto selected = QColorDialog::getColor(colour, &dialog, caption); if (selected.isValid()) { colour = selected; refresh(); } }); return button; };
    form->addRow("Thumbnail interval:", interval); form->addRow("Thumbnail size:", size); form->addRow("Thumbnail brightness:", brightness); form->addRow("Waveform scale:", scale); form->addRow("Waveform background:", colour_control(wave_background, "Waveform Background")); form->addRow("Waveform line:", colour_control(wave_line, "Waveform Line")); form->addRow("Waveform centre line:", colour_control(wave_centre, "Waveform Centre Line")); form->addRow("Frame-number colour:", colour_control(wave_text, "Frame Number Colour")); layout->addLayout(form); layout->addWidget(timecode); layout->addWidget(audio);
    auto* note = new QLabel("An interval of zero displays adjacent frames. The audio graph is decoded from the selected source audio track; dB scaling expands quiet passages without inventing waveform data.", &dialog); note->setWordWrap(true); layout->addWidget(note);
    auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); layout->addWidget(buttons); connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
    if (dialog.exec() != QDialog::Accepted) return;
    thumbnail_interval_seconds_ = interval->value(); thumbnail_height_ = size->currentData().toInt(); thumbnail_audio_visible_ = audio->isChecked(); thumbnail_brightness_ = brightness->value(); thumbnail_timecode_visible_ = timecode->isChecked(); waveform_decibel_scale_ = scale->currentData().toBool(); waveform_background_colour_ = wave_background; waveform_colour_ = wave_line; waveform_centre_colour_ = wave_centre; waveform_text_colour_ = wave_text; for (auto* thumbnail : thumbnail_widgets_) { thumbnail->setMinimumHeight(thumbnail_height_); thumbnail->set_display_options(thumbnail_brightness_, thumbnail_timecode_visible_); } update_thumbnail_widget_count(); frame_guide_->setVisible(thumbnail_audio_visible_); frame_guide_->set_waveform_style(waveform_decibel_scale_, waveform_background_colour_, waveform_colour_, waveform_centre_colour_, waveform_text_colour_);
    QSettings settings("SmartCut64", "SmartCut64"); settings.setValue("thumbnails/intervalSecondsV2", thumbnail_interval_seconds_); settings.setValue("thumbnails/height", thumbnail_height_); settings.setValue("thumbnails/audioVisible", thumbnail_audio_visible_); settings.setValue("thumbnails/brightness", thumbnail_brightness_); settings.setValue("thumbnails/timecodeVisible", thumbnail_timecode_visible_); settings.setValue("thumbnails/waveformDecibelScale", waveform_decibel_scale_); settings.setValue("thumbnails/waveformBackground", waveform_background_colour_.name()); settings.setValue("thumbnails/waveformColour", waveform_colour_.name()); settings.setValue("thumbnails/waveformCentre", waveform_centre_colour_.name()); settings.setValue("thumbnails/waveformText", waveform_text_colour_.name());
    if (timeline_->frame_count() > 0) schedule_thumbnail_strip(timeline_->current_frame());
    status_->setText("Thumbnail display updated");
  }
  void start_elementary_multiplex(const QStringList& inputs, const QString& destination) {
    if (multiplex_worker_.state() != QProcess::NotRunning) { status_->setText("An elementary-stream multiplex is already running"); return; }
    if (inputs.size() < 2 || destination.isEmpty()) { status_->setText("Select at least two media streams and an output file"); return; }
    for (const auto& input : inputs) {
      if (!QFileInfo(input).isFile() || input.contains('|')) { status_->setText("Every multiplex input must be a readable file"); return; }
    }
    const auto suffix = QFileInfo(destination).suffix().toLower();
    if (!QStringList{"mkv", "mp4", "m4v", "mov", "mpg", "mpeg", "vob", "ts", "m2ts", "mts", "wtv", "dvr-ms", "dvrms", "rec", "mxf"}.contains(suffix)) { status_->setText("Multiplex output must use a supported media container"); return; }
    multiplex_output_.clear(); multiplex_output_path_ = destination; progress_->setRange(0, 0); status_->setText("Multiplexing elementary streams without re-encoding…");
    multiplex_worker_.start(worker_program(), {"--multiplex", "--inputs", inputs.join('|'), "--output", destination});
  }
  void show_elementary_multiplexer() {
    if (!require_paid_delivery_tool("Elementary Stream Multiplexer")) return;
    QDialog dialog(this); dialog.setWindowTitle("Elementary Stream Multiplexer"); dialog.resize(760, 520); auto* layout = new QVBoxLayout(&dialog);
    auto* streams = new QListWidget(&dialog); streams->setSelectionMode(QAbstractItemView::ExtendedSelection); layout->addWidget(new QLabel("Input video, audio, and subtitle streams (mux order):", &dialog)); layout->addWidget(streams, 1);
    auto* controls = new QHBoxLayout; auto* add = new QPushButton("Add Streams…", &dialog); auto* remove = new QPushButton("Remove", &dialog); auto* up = new QPushButton("Move Up", &dialog); auto* down = new QPushButton("Move Down", &dialog); controls->addWidget(add); controls->addWidget(remove); controls->addStretch(); controls->addWidget(up); controls->addWidget(down); layout->addLayout(controls);
    auto* output_row = new QWidget(&dialog); auto* output_layout = new QHBoxLayout(output_row); output_layout->setContentsMargins(0, 0, 0, 0); auto* output = new QLineEdit(output_row); auto* browse = new QPushButton("Browse…", output_row); output_layout->addWidget(output, 1); output_layout->addWidget(browse); auto* form = new QFormLayout; form->addRow("Output file:", output_row); layout->addLayout(form);
    auto* note = new QLabel("Combines independently encoded streams by timestamp and copies their codec packets unchanged. The completed container is reopened to verify stream codecs, packet counts, and monotonic timing. Use MPEG transport stream for the widest raw H.264/HEVC and broadcast-audio compatibility.", &dialog); note->setWordWrap(true); layout->addWidget(note);
    auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); buttons->button(QDialogButtonBox::Ok)->setText("Multiplex"); layout->addWidget(buttons);
    connect(add, &QPushButton::clicked, &dialog, [streams, output, &dialog] { const auto paths = QFileDialog::getOpenFileNames(&dialog, "Add Elementary Streams", {}, "Media Streams (*.h264 *.264 *.avc *.h265 *.265 *.hevc *.m2v *.mpv *.av1 *.ivf *.vp9 *.aac *.ac3 *.eac3 *.mp2 *.mp3 *.opus *.flac *.wav *.srt *.ass *.sup *.mkv *.mp4 *.mov *.ts *.m2ts *.mpg *.mpeg *.webm);;All Files (*)"); for (const auto& path : paths) { streams->addItem(QDir::toNativeSeparators(path)); if (output->text().isEmpty()) { const QFileInfo info(path); output->setText(info.absolutePath() + '/' + info.completeBaseName() + "-muxed.ts"); } } });
    connect(remove, &QPushButton::clicked, &dialog, [streams] { qDeleteAll(streams->selectedItems()); });
    const auto move_item = [streams](int direction) { const int row = streams->currentRow(); const int target = row + direction; if (row < 0 || target < 0 || target >= streams->count()) return; auto* item = streams->takeItem(row); streams->insertItem(target, item); streams->setCurrentRow(target); };
    connect(up, &QPushButton::clicked, &dialog, [move_item] { move_item(-1); }); connect(down, &QPushButton::clicked, &dialog, [move_item] { move_item(1); });
    connect(browse, &QPushButton::clicked, &dialog, [output, &dialog] { const auto path = QFileDialog::getSaveFileName(&dialog, "Multiplex Output", output->text(), "MPEG Transport Stream (*.ts *.m2ts *.mts *.rec);;MPEG Program Stream (*.mpg *.mpeg *.vob);;Matroska Video (*.mkv);;MPEG-4 Video (*.mp4 *.m4v);;QuickTime Movie (*.mov);;Windows Television (*.wtv);;DVR-MS (*.dvr-ms);;MXF (*.mxf)"); if (!path.isEmpty()) output->setText(path); });
    connect(buttons, &QDialogButtonBox::accepted, &dialog, [&dialog, streams, output] { if (streams->count() < 2 || output->text().isEmpty()) { QMessageBox::warning(&dialog, "Elementary Stream Multiplexer", "Add at least two input streams and choose an output file."); return; } dialog.accept(); }); connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
    if (dialog.exec() != QDialog::Accepted) return;
    QStringList inputs;
    for (int row = 0; row < streams->count(); ++row)
      inputs.push_back(streams->item(row)->text());
    start_elementary_multiplex(inputs, output->text());
  }
  void show_trimmed_file_copy() {
    if (!require_paid_delivery_tool("Trimmed File Copy")) return;
    if (trim_copy_worker_.state() != QProcess::NotRunning) { status_->setText("A trimmed file copy is already running"); return; }
    QDialog dialog(this); dialog.setWindowTitle("Create Trimmed File Copy"); dialog.resize(680, 300); auto* layout = new QVBoxLayout(&dialog); auto* form = new QFormLayout;
    auto* source = new QLineEdit(input_->text(), &dialog); auto* destination = new QLineEdit(&dialog);
    if (!source->text().isEmpty()) { const QFileInfo info(source->text()); destination->setText(info.absolutePath() + '/' + info.completeBaseName() + "-trimmed" + (info.suffix().isEmpty() ? QString{} : "." + info.suffix())); }
    auto* source_row = new QWidget(&dialog); auto* source_layout = new QHBoxLayout(source_row); source_layout->setContentsMargins(0, 0, 0, 0); auto* source_browse = new QPushButton("Browse…", source_row); source_layout->addWidget(source, 1); source_layout->addWidget(source_browse);
    auto* destination_row = new QWidget(&dialog); auto* destination_layout = new QHBoxLayout(destination_row); destination_layout->setContentsMargins(0, 0, 0, 0); auto* destination_browse = new QPushButton("Browse…", destination_row); destination_layout->addWidget(destination, 1); destination_layout->addWidget(destination_browse);
    auto* mode = new QComboBox(&dialog); mode->addItem("From beginning", 0); mode->addItem("To end of file", 1); mode->addItem("Start at MByte", 2); mode->addItem("Use selection markers", 3); if (!timeline_->has_valid_range()) mode->setItemData(3, 0, Qt::UserRole - 1);
    auto* start_mib = new QDoubleSpinBox(&dialog); start_mib->setRange(0.0, 1048576.0); start_mib->setDecimals(3); start_mib->setSuffix(" MiB");
    auto* amount_mib = new QDoubleSpinBox(&dialog); amount_mib->setRange(0.001, 1048576.0); amount_mib->setDecimals(3); amount_mib->setValue(10.0); amount_mib->setSuffix(" MiB");
    form->addRow("Source file:", source_row); form->addRow("Output file:", destination_row); form->addRow("Output options:", mode); form->addRow("Start at:", start_mib); form->addRow("MBytes to output:", amount_mib); layout->addLayout(form);
    auto* note = new QLabel("This support utility copies the selected raw byte range exactly; it does not remux or guarantee a playable media file. Selection markers are mapped proportionally to source-file byte positions.", &dialog); note->setWordWrap(true); layout->addWidget(note);
    auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); buttons->button(QDialogButtonBox::Ok)->setText("Start Copy"); layout->addWidget(buttons);
    connect(mode, qOverload<int>(&QComboBox::currentIndexChanged), &dialog, [mode, start_mib, amount_mib](int) { const int selected = mode->currentData().toInt(); start_mib->setEnabled(selected == 2); amount_mib->setEnabled(selected != 3); }); mode->setCurrentIndex(0);
    connect(source_browse, &QPushButton::clicked, &dialog, [source, destination, &dialog] { const auto path = QFileDialog::getOpenFileName(&dialog, "Trimmed Copy Source", {}, "All Files (*)"); if (path.isEmpty()) return; source->setText(path); const QFileInfo info(path); destination->setText(info.absolutePath() + '/' + info.completeBaseName() + "-trimmed" + (info.suffix().isEmpty() ? QString{} : "." + info.suffix())); });
    connect(destination_browse, &QPushButton::clicked, &dialog, [destination, &dialog] { const auto path = QFileDialog::getSaveFileName(&dialog, "Trimmed Copy Output", destination->text(), "All Files (*)"); if (!path.isEmpty()) destination->setText(path); });
    connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
    if (dialog.exec() != QDialog::Accepted) return;
    const QFileInfo source_info(source->text()); const auto size_value = source_info.size(); if (!source_info.exists() || !source_info.isFile() || size_value <= 0 || destination->text().isEmpty()) { status_->setText("Trimmed file copy requires a readable source and output path"); return; }
    const auto size = static_cast<std::uint64_t>(size_value); const auto mib_bytes = [](double value) { return static_cast<std::uint64_t>(std::max(0.0, value) * 1048576.0 + 0.5); };
    std::uint64_t start = 0; std::uint64_t count = std::min(size, mib_bytes(amount_mib->value())); const int selected_mode = mode->currentData().toInt();
    if (selected_mode == 1) start = size - count;
    else if (selected_mode == 2) { start = mib_bytes(start_mib->value()); if (start < size) count = std::min(count, size - start); }
    else if (selected_mode == 3) { const auto frames = timeline_->frame_count(); if (!timeline_->has_valid_range() || frames == 0) { status_->setText("Set valid selection markers before using them for a trimmed copy"); return; } start = static_cast<std::uint64_t>((static_cast<long double>(size) * timeline_->range_start()) / frames); const auto end = static_cast<std::uint64_t>(std::ceil((static_cast<long double>(size) * timeline_->range_end()) / frames)); count = end > start ? end - start : 0; }
    if (start >= size || count == 0) { status_->setText("The requested trimmed byte range is empty or outside the source"); return; }
    trim_copy_output_.clear(); trim_copy_output_path_ = destination->text(); progress_->setRange(0, 100); progress_->setValue(1); status_->setText("Creating byte-for-byte trimmed file copy…");
    trim_copy_worker_.start(worker_program(), {"--trimmed-file-copy", "--input", source->text(), "--output", destination->text(), "--start-byte", QString::number(static_cast<qulonglong>(start)), "--byte-count", QString::number(static_cast<qulonglong>(count))});
  }
  void start_quickstream_fix(const QString& source, const QString& destination, const QSize& dimensions = {}) {
    if (qsf_worker_.state() != QProcess::NotRunning) { status_->setText("QuickStream Fix is already running"); return; }
    if (source.isEmpty() || destination.isEmpty()) { status_->setText("QuickStream Fix requires source and destination files"); return; }
    if (QFileInfo(source).canonicalFilePath().compare(QFileInfo(destination).canonicalFilePath(), Qt::CaseInsensitive) == 0) { status_->setText("QuickStream Fix will not overwrite its source"); return; }
    qsf_output_.clear(); qsf_output_path_ = destination; progress_->setRange(0, 0); status_->setText("QuickStream Fix rebuilding timestamps and validating all copied streams…");
    if (dimensions.isValid()) { status_->setText(QString("QuickStream Fix retaining %1 × %2 frames and rebuilding timestamps…").arg(dimensions.width()).arg(dimensions.height())); qsf_worker_.start(worker_program(), {"--qsf-filter", "--input", source, "--output", destination, "--width", QString::number(dimensions.width()), "--height", QString::number(dimensions.height())}); }
    else qsf_worker_.start(worker_program(), {"--export", "--input", source, "--output", destination});
  }
  void show_quickstream_fix() {
    if (!require_paid_delivery_tool("QuickStream Fix")) return;
    QDialog dialog(this); dialog.setWindowTitle("QuickStream Fix"); dialog.resize(720, 500); auto* layout = new QVBoxLayout(&dialog); auto* form = new QFormLayout;
    auto* source = new QLineEdit(input_->text(), &dialog); auto* destination = new QLineEdit(&dialog); if (!source->text().isEmpty()) destination->setText(QFileInfo(source->text()).absolutePath() + '/' + QFileInfo(source->text()).completeBaseName() + "-fixed.mkv");
    auto* source_row = new QWidget(&dialog); auto* source_layout = new QHBoxLayout(source_row); source_layout->setContentsMargins(0, 0, 0, 0); auto* source_browse = new QPushButton("Browse…", source_row); source_layout->addWidget(source, 1); source_layout->addWidget(source_browse);
    auto* destination_row = new QWidget(&dialog); auto* destination_layout = new QHBoxLayout(destination_row); destination_layout->setContentsMargins(0, 0, 0, 0); auto* destination_browse = new QPushButton("Browse…", destination_row); destination_layout->addWidget(destination, 1); destination_layout->addWidget(destination_browse);
    auto* sample = new QDoubleSpinBox(&dialog); sample->setRange(0.0, 60.0); sample->setDecimals(1); sample->setValue(4.0); sample->setSuffix(" seconds"); sample->setSpecialValueText("Every frame"); form->addRow("Input file:", source_row); form->addRow("Output file:", destination_row); form->addRow("Dimension sample interval:", sample); layout->addLayout(form);
    auto* scan = new QPushButton("Scan / Rescan Dimensions", &dialog); layout->addWidget(scan); auto* dimensions = new QTableWidget(0, 5, &dialog); dimensions->setHorizontalHeaderLabels({"Width", "Height", "Sampled", "Frames", "Frame Range"}); dimensions->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); dimensions->setSelectionBehavior(QAbstractItemView::SelectRows); dimensions->setSelectionMode(QAbstractItemView::SingleSelection); dimensions->setEditTriggers(QAbstractItemView::NoEditTriggers); layout->addWidget(dimensions, 1); auto* filter = new QCheckBox("Filter video to the selected dimensions", &dialog); filter->setChecked(false); layout->addWidget(filter);
    auto* note = new QLabel("Scan samples the source for changing frame dimensions. With filtering enabled, mismatching ranges are removed through the smart-render engine; matching GOPs and all aligned source streams remain packet-copied.", &dialog); note->setWordWrap(true); layout->addWidget(note);
    auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); buttons->button(QDialogButtonBox::Ok)->setText("Start"); layout->addWidget(buttons);
    connect(source_browse, &QPushButton::clicked, &dialog, [source, destination, dimensions, &dialog] { const auto path = QFileDialog::getOpenFileName(&dialog, "QuickStream Fix Input", {}, "Video Files (*.mkv *.mp4 *.mov *.ts *.m2ts *.mpg *.mpeg *.webm);;All Files (*)"); if (!path.isEmpty()) { source->setText(path); destination->setText(QFileInfo(path).absolutePath() + '/' + QFileInfo(path).completeBaseName() + "-fixed.mkv"); dimensions->setRowCount(0); } });
    connect(destination_browse, &QPushButton::clicked, &dialog, [destination, &dialog] { const auto path = QFileDialog::getSaveFileName(&dialog, "QuickStream Fix Output", destination->text(), "Matroska Video (*.mkv);;MPEG-4 Video (*.mp4);;QuickTime Movie (*.mov);;MPEG Program Stream (*.mpg *.mpeg *.vob);;MPEG Transport Stream (*.ts *.m2ts *.rec);;Windows Television (*.wtv);;DVR-MS (*.dvr-ms);;MXF (*.mxf)"); if (!path.isEmpty()) destination->setText(path); });
    connect(scan, &QPushButton::clicked, &dialog, [source, sample, dimensions, &dialog] { if (!QFileInfo::exists(source->text())) return; QProgressDialog progress("Scanning source dimensions…", "Cancel", 0, 0, &dialog); progress.setWindowModality(Qt::WindowModal); progress.show(); QProcess worker; worker.start(worker_program(), {"--qsf-dimensions", "--input", source->text(), "--sample-seconds", QString::number(sample->value(), 'f', 2)}); while (!worker.waitForFinished(100)) { QApplication::processEvents(); if (progress.wasCanceled()) { worker.kill(); worker.waitForFinished(); return; } } const auto document = QJsonDocument::fromJson(worker.readAllStandardOutput().trimmed()); if (worker.exitCode() != 0 || !document.isObject()) { QMessageBox::warning(&dialog, "QuickStream Fix", "Dimension scan failed."); return; } const auto values = document.object().value("dimensions").toArray(); dimensions->setRowCount(values.size()); int dominant = 0; int dominant_frames = -1; for (int row = 0; row < values.size(); ++row) { const auto value = values[row].toObject(); const int frames = value.value("frame_count").toInt(); const QStringList columns{QString::number(value.value("width").toInt()), QString::number(value.value("height").toInt()), QString::number(value.value("sample_count").toInt()), QString::number(frames), QString("%1–%2").arg(value.value("first_frame").toInt()).arg(value.value("last_frame").toInt())}; for (int column = 0; column < columns.size(); ++column) dimensions->setItem(row, column, new QTableWidgetItem(columns[column])); if (frames > dominant_frames) { dominant_frames = frames; dominant = row; } } if (dimensions->rowCount() > 0) dimensions->selectRow(dominant); });
    connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
    if (dialog.exec() == QDialog::Accepted) { QSize selected; if (filter->isChecked()) { const int row = dimensions->currentRow(); if (row < 0) { status_->setText("Scan and select dimensions before enabling QuickStream filtering"); return; } selected = QSize(dimensions->item(row, 0)->text().toInt(), dimensions->item(row, 1)->text().toInt()); } start_quickstream_fix(source->text(), destination->text(), selected); }
  }
  void save_batch_queue() const
  {
    QJsonArray jobs;
    for (const auto &job : batch_jobs_)
    {
      QJsonArray cuts;
      for (const auto &range : job.cuts)
        cuts.push_back(
            QJsonArray{static_cast<qint64>(range.first), static_cast<qint64>(range.second)});
      QJsonArray streams;
      for (const auto stream : job.selected_streams)
        streams.push_back(stream);
      jobs.push_back(QJsonObject{{"input", job.input},
                                 {"output", job.output},
                                 {"frame_count", static_cast<qint64>(job.frame_count)},
                                 {"cuts", cuts},
                                 {"selected_streams", streams},
                                 {"filter_streams", job.filter_streams},
                                 {"mode", job.mode},
                                 {"status", job.status},
                                 {"operation", job.operation},
                                 {"watch_key", job.watch_key},
                                 {"audio_codec", job.audio_codec},
                                 {"audio_gain_db", job.audio_gain_db},
                                 {"audio_bitrate", job.audio_bitrate},
                                 {"audio_sample_rate", job.audio_sample_rate},
                                 {"audio_channels", job.audio_channels},
                                 {"video_encoder", job.video_encoder},
                                 {"hardware_encoder", job.hardware_encoder},
                                 {"aspect_ratio", job.aspect_ratio},
                                 {"video_width", job.video_width},
                                 {"video_height", job.video_height},
                                 {"video_bit_depth", job.video_bit_depth},
                                 {"video_bitrate_kbps", job.video_bitrate_kbps},
                                 {"video_max_bitrate_kbps", job.video_max_bitrate_kbps},
                                 {"deinterlace", job.deinterlace},
                                 {"frame_rate", job.frame_rate},
                                 {"ts_transport_stream_id", job.ts_transport_stream_id},
                                 {"ts_original_network_id", job.ts_original_network_id},
                                 {"ts_service_id", job.ts_service_id},
                                 {"ts_pmt_start_pid", job.ts_pmt_start_pid},
                                 {"ts_start_pid", job.ts_start_pid},
                                 {"ts_mux_rate", job.ts_mux_rate},
                                 {"ts_pcr_period_ms", job.ts_pcr_period_ms},
                                 {"ts_pat_period", job.ts_pat_period},
                                 {"ts_sdt_period", job.ts_sdt_period},
                                 {"ts_m2ts_mode", job.ts_m2ts_mode},
                                 {"ts_resend_headers", job.ts_resend_headers},
                                 {"ts_system_b", job.ts_system_b},
                                 {"ts_emit_nit", job.ts_emit_nit},
                                 {"ts_service_name", job.ts_service_name},
                                 {"ts_service_provider", job.ts_service_provider}});
    }
    QSettings("SmartCut64", "SmartCut64")
        .setValue("batch/persistentQueue", QJsonDocument(jobs).toJson(QJsonDocument::Compact));
  }
  void load_batch_queue()
  {
    batch_jobs_.clear();
    const auto document = QJsonDocument::fromJson(
        QSettings("SmartCut64", "SmartCut64").value("batch/persistentQueue").toByteArray());
    if (!document.isArray())
      return;
    for (const auto &value : document.array())
    {
      const auto object = value.toObject();
      BatchJob job;
      job.input = object.value("input").toString();
      job.output = object.value("output").toString();
      if (job.input.isEmpty() || job.output.isEmpty())
        continue;
      job.frame_count =
          static_cast<std::size_t>(std::max<qint64>(0, object.value("frame_count").toInteger()));
      for (const auto &item : object.value("cuts").toArray())
      {
        const auto range = item.toArray();
        if (range.size() == 2)
        {
          const auto first = std::max<qint64>(0, range[0].toInteger());
          const auto end = std::max<qint64>(0, range[1].toInteger());
          if (first < end)
            job.cuts.emplace_back(static_cast<std::size_t>(first), static_cast<std::size_t>(end));
        }
      }
      for (const auto &stream : object.value("selected_streams").toArray())
        job.selected_streams.push_back(stream.toInt());
      job.filter_streams = object.value("filter_streams").toBool();
      job.mode = object.value("mode").toString("smart");
      job.status = object.value("status").toString("Queued");
      if (job.status == "Running")
        job.status = "Interrupted — queued to retry";
      job.operation = object.value("operation").toString("export");
      job.watch_key = object.value("watch_key").toString();
      job.audio_codec = object.value("audio_codec").toString("copy");
      job.audio_gain_db = object.value("audio_gain_db").toDouble();
      job.audio_bitrate = object.value("audio_bitrate").toInt(192000);
      job.audio_sample_rate = object.value("audio_sample_rate").toInt();
      job.audio_channels = object.value("audio_channels").toInt();
      job.video_encoder = object.value("video_encoder").toString("source");
      job.hardware_encoder = object.value("hardware_encoder").toString("auto");
      job.aspect_ratio = object.value("aspect_ratio").toString("No Change");
      job.video_width = object.value("video_width").toInt();
      job.video_height = object.value("video_height").toInt();
      job.video_bit_depth = object.value("video_bit_depth").toInt();
      job.video_bitrate_kbps = object.value("video_bitrate_kbps").toInt();
      job.video_max_bitrate_kbps = object.value("video_max_bitrate_kbps").toInt();
      job.deinterlace = object.value("deinterlace").toString("None");
      job.frame_rate = object.value("frame_rate").toString("No Change");
      job.ts_transport_stream_id = object.value("ts_transport_stream_id").toInt(1);
      job.ts_original_network_id = object.value("ts_original_network_id").toInt(0xff01);
      job.ts_service_id = object.value("ts_service_id").toInt(1);
      job.ts_pmt_start_pid = object.value("ts_pmt_start_pid").toInt(0x1000);
      job.ts_start_pid = object.value("ts_start_pid").toInt(0x0100);
      job.ts_mux_rate = object.value("ts_mux_rate").toInt();
      job.ts_pcr_period_ms = object.value("ts_pcr_period_ms").toInt(-1);
      job.ts_pat_period = object.value("ts_pat_period").toDouble(0.1);
      job.ts_sdt_period = object.value("ts_sdt_period").toDouble(0.5);
      job.ts_m2ts_mode = object.value("ts_m2ts_mode").toBool();
      job.ts_resend_headers = object.value("ts_resend_headers").toBool(true);
      job.ts_system_b = object.value("ts_system_b").toBool();
      job.ts_emit_nit = object.value("ts_emit_nit").toBool();
      job.ts_service_name = object.value("ts_service_name").toString("SmartCut64");
      job.ts_service_provider = object.value("ts_service_provider").toString("SmartCut64");
      batch_jobs_.push_back(std::move(job));
    }
  }

  void save_watch_folders() const {
    QJsonArray folders;
    for (const auto& folder : watch_folders_) {
      folders.push_back(QJsonObject{{"id", folder.id},
                                    {"input_folder", folder.input_folder},
                                    {"output_folder", folder.output_folder},
                                    {"patterns", folder.patterns},
                                    {"output_template", folder.output_template},
                                    {"profile_id", folder.profile_id},
                                    {"operation", folder.operation},
                                    {"recursive", folder.recursive},
                                    {"enabled", folder.enabled},
                                    {"auto_start", folder.auto_start},
                                    {"settle_seconds", folder.settle_seconds}});
    }
    QSettings("SmartCut64", "SmartCut64")
        .setValue("batch/watchFolders", QJsonDocument(folders).toJson(QJsonDocument::Compact));
  }

  void load_watch_folders() {
    watch_folders_.clear();
    const auto document = QJsonDocument::fromJson(
        QSettings("SmartCut64", "SmartCut64").value("batch/watchFolders").toByteArray());
    if (!document.isArray()) return;
    for (const auto& value : document.array()) {
      const auto object = value.toObject();
      WatchFolder folder;
      folder.id = object.value("id").toString();
      folder.input_folder = object.value("input_folder").toString();
      folder.output_folder = object.value("output_folder").toString();
      folder.patterns = object.value("patterns").toString(folder.patterns);
      folder.output_template = object.value("output_template").toString(folder.output_template);
      folder.profile_id = object.value("profile_id").toString(folder.profile_id);
      folder.operation = object.value("operation").toString(folder.operation);
      folder.recursive = object.value("recursive").toBool();
      folder.enabled = object.value("enabled").toBool(true);
      folder.auto_start = object.value("auto_start").toBool(true);
      folder.settle_seconds = std::clamp(object.value("settle_seconds").toInt(5), 0, 3600);
      if (folder.id.isEmpty()) {
        folder.id = QString::fromLatin1(QCryptographicHash::hash(
            (folder.input_folder + QString::number(QDateTime::currentMSecsSinceEpoch())).toUtf8(),
            QCryptographicHash::Sha256).toHex().left(16));
      }
      if (!folder.input_folder.isEmpty()) watch_folders_.push_back(std::move(folder));
    }
  }

  void save_watch_history() const {
    QStringList history(watch_history_.cbegin(), watch_history_.cend());
    history.sort(Qt::CaseInsensitive);
    if (history.size() > 10000) history = history.sliced(history.size() - 10000);
    QSettings("SmartCut64", "SmartCut64").setValue("batch/watchHistory", history);
  }

  void load_watch_history() {
    watch_history_.clear();
    const auto history = QSettings("SmartCut64", "SmartCut64").value("batch/watchHistory").toStringList();
    for (const auto& item : history) if (!item.isEmpty()) watch_history_.insert(item);
  }

  static QString normalized_watch_path(const QString& path) {
    auto normalized = QDir::cleanPath(QFileInfo(path).absoluteFilePath());
#ifdef _WIN32
    normalized = normalized.toLower();
#endif
    return normalized;
  }

  static QStringList watch_name_filters(QString patterns) {
    patterns.replace(',', ';');
    QStringList filters;
    for (auto pattern : patterns.split(';', Qt::SkipEmptyParts)) {
      pattern = pattern.trimmed();
      if (!pattern.isEmpty()) filters.push_back(pattern);
    }
    return filters.isEmpty() ? QStringList{"*.*"} : filters;
  }

  QString watch_output_path(const WatchFolder& folder, const QFileInfo& source,
                            const OutputProfile& profile) const {
    const auto output_folder = folder.output_folder.trimmed().isEmpty()
        ? source.absolutePath() : QDir(folder.output_folder).absolutePath();
    auto extension = profile.file_extension.trimmed().toLower();
    if (extension.isEmpty()) extension = "mkv";
    if (folder.operation == "ad-scan") extension = "smartcut";
    auto name = folder.output_template.trimmed();
    if (name.isEmpty()) name = folder.operation == "ad-scan"
        ? "%BASE%-adscan.%EXT%" : "%BASE%-edited.%EXT%";
    name.replace("%BASE%", source.completeBaseName(), Qt::CaseInsensitive);
    name.replace("%NAME%", source.fileName(), Qt::CaseInsensitive);
    name.replace("%EXT%", extension, Qt::CaseInsensitive);
    name.replace("%PROFILE%", profile.name, Qt::CaseInsensitive);
    return QDir(output_folder).filePath(name);
  }

  void scan_watch_folders(bool force_settled = false) {
    const auto now = QDateTime::currentMSecsSinceEpoch();
    QSet<QString> active_keys;
    bool queued_any = false;
    int queued_count = 0;
    bool should_start = false;
    for (const auto& folder : watch_folders_) {
      if (!folder.enabled || !QDir(folder.input_folder).exists()) continue;
      const auto profile = current_output_profile(folder.profile_id);
      QDirIterator iterator(folder.input_folder, watch_name_filters(folder.patterns), QDir::Files,
                            folder.recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags);
      while (iterator.hasNext()) {
        const QFileInfo source(iterator.next());
        if (source.size() <= 0) continue;
        const auto normalized = normalized_watch_path(source.absoluteFilePath());
        if (watch_history_.contains("output|" + normalized)) continue;
        const auto key = folder.id + '|' + normalized + '|' + QString::number(source.size()) + '|' +
                         QString::number(source.lastModified().toMSecsSinceEpoch());
        active_keys.insert(key);
        if (watch_history_.contains(key)) continue;
        const bool already_queued = std::any_of(batch_jobs_.cbegin(), batch_jobs_.cend(),
            [&key](const BatchJob& job) { return job.watch_key == key; });
        if (already_queued) continue;
        if (!watch_first_seen_.contains(key)) watch_first_seen_.insert(key, now);
        if (!force_settled && now - watch_first_seen_.value(key) < folder.settle_seconds * 1000LL) continue;
        const auto destination = watch_output_path(folder, source, profile);
        if (normalized_watch_path(destination) == normalized) continue;
        QDir().mkpath(QFileInfo(destination).absolutePath());
        BatchJob job;
        job.input = source.absoluteFilePath();
        job.output = destination;
        job.operation = folder.operation;
        job.watch_key = key;
        apply_output_profile(job, profile);
        batch_jobs_.push_back(std::move(job));
        watch_history_.insert("output|" + normalized_watch_path(destination));
        queued_any = true;
        ++queued_count;
        should_start = should_start || folder.auto_start;
      }
    }
    for (auto found = watch_first_seen_.begin(); found != watch_first_seen_.end();) {
      if (!active_keys.contains(found.key())) found = watch_first_seen_.erase(found);
      else ++found;
    }
    if (queued_any) {
      save_batch_queue();
      save_watch_history();
      status_->setText(QString("Watch folders queued %1 new job%2")
                           .arg(queued_count).arg(queued_count == 1 ? "" : "s"));
      if (should_start && !batch_running_) start_batch(false);
    }
  }
  void queue_current_to_batch_path(const QString& destination, const OutputProfile& profile) {
    QVector<int> streams = selected_streams_;
    if (profile.audio_only) {
      streams.clear();
      for (const auto value : source_streams_) { const auto stream = value.toObject(); if (stream.value("type").toInt(-1) == 1) { streams.push_back(stream.value("index").toInt()); break; } }
      if (streams.isEmpty()) { status_->setText("The source has no audio stream for the Audio Only profile"); return; }
    }
    const bool filter_streams = profile.audio_only || (!streams.isEmpty() && streams.size() != source_streams_.size());
    const auto queued_cut_count = cut_ranges_.size(); BatchJob job; job.input = input_->text(); job.output = destination; job.frame_count = timeline_->frame_count(); job.cuts = cut_ranges_; job.selected_streams = streams; job.filter_streams = filter_streams; apply_output_profile(job, profile); batch_jobs_.push_back(std::move(job)); save_batch_queue(); if (clear_cuts_after_queue_) set_cut_ranges({}); status_->setText(QString("Queued batch job %1 with %2 cuts").arg(batch_jobs_.size()).arg(static_cast<qulonglong>(queued_cut_count)));
  }
  void queue_current_to_batch()
  {
    if (input_->text().isEmpty())
    {
      status_->setText("Open a source before queueing a batch export");
      return;
    }
    const auto destination = QFileDialog::getSaveFileName(this, "Queue Batch Output", default_output_path(), "Matroska Video (*.mkv);;MPEG-4 Video (*.mp4);;QuickTime Movie (*.mov);;MPEG Program Stream (*.mpg *.mpeg *.vob);;MPEG Transport Stream (*.ts *.m2ts *.rec);;Windows Television (*.wtv);;DVR-MS (*.dvr-ms);;MXF (*.mxf)");
    if (destination.isEmpty()) return;
    queue_current_to_batch_path(destination, current_output_profile());
  }
  void show_watch_folder_manager() {
    QDialog dialog(this); dialog.setWindowTitle("Watch Folders"); dialog.resize(1100, 520);
    auto* layout = new QVBoxLayout(&dialog);
    auto* table = new QTableWidget(0, 7, &dialog);
    table->setHorizontalHeaderLabels({"Enabled", "Input Folder", "File Patterns", "Output Folder / Naming", "Profile", "Operation", "Automation"});
    table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
    table->setSelectionBehavior(QAbstractItemView::SelectRows); table->setSelectionMode(QAbstractItemView::SingleSelection); table->setEditTriggers(QAbstractItemView::NoEditTriggers); layout->addWidget(table, 1);
    const auto refresh = [this, table] {
      table->setRowCount(watch_folders_.size());
      for (int row = 0; row < watch_folders_.size(); ++row) {
        const auto& folder = watch_folders_[row];
        const auto profile = current_output_profile(folder.profile_id);
        const QStringList values{folder.enabled ? "Yes" : "No", QDir::toNativeSeparators(folder.input_folder), folder.patterns,
          QString("%1 / %2").arg(QDir::toNativeSeparators(folder.output_folder.isEmpty() ? folder.input_folder : folder.output_folder), folder.output_template),
          profile.name, folder.operation == "ad-scan" ? "Ad-Detective project" : "Video export",
          QString("%1; settle %2 s%3").arg(folder.auto_start ? "Auto-run" : "Queue only").arg(folder.settle_seconds).arg(folder.recursive ? "; recursive" : "")};
        for (int column = 0; column < values.size(); ++column) table->setItem(row, column, new QTableWidgetItem(values[column]));
      }
    };
    const auto edit_folder = [this, &dialog](std::optional<WatchFolder> existing) -> std::optional<WatchFolder> {
      WatchFolder folder = existing.value_or(WatchFolder{});
      QDialog editor(&dialog); editor.setWindowTitle(existing ? "Edit Watch Folder" : "Add Watch Folder"); editor.resize(720, 520); auto* editor_layout = new QVBoxLayout(&editor); auto* form = new QFormLayout;
      auto* input = new QLineEdit(folder.input_folder, &editor); auto* input_browse = new QPushButton("Browse...", &editor); auto* input_row = new QWidget(&editor); auto* input_layout = new QHBoxLayout(input_row); input_layout->setContentsMargins(0, 0, 0, 0); input_layout->addWidget(input, 1); input_layout->addWidget(input_browse);
      auto* output = new QLineEdit(folder.output_folder, &editor); auto* output_browse = new QPushButton("Browse...", &editor); auto* output_row = new QWidget(&editor); auto* output_layout = new QHBoxLayout(output_row); output_layout->setContentsMargins(0, 0, 0, 0); output_layout->addWidget(output, 1); output_layout->addWidget(output_browse);
      auto* patterns = new QLineEdit(folder.patterns, &editor); auto* name_template = new QLineEdit(folder.output_template, &editor);
      auto* profile = new QComboBox(&editor); for (int index = 0; index < output_profile_->count(); ++index) profile->addItem(output_profile_->itemText(index), output_profile_->itemData(index)); profile->setCurrentIndex(std::max(0, profile->findData(folder.profile_id)));
      auto* operation = new QComboBox(&editor); operation->addItem("Export video", "export"); operation->addItem("Run Ad-Detective and save editable project", "ad-scan"); operation->setCurrentIndex(std::max(0, operation->findData(folder.operation)));
      auto* settle = new QSpinBox(&editor); settle->setRange(0, 3600); settle->setSuffix(" seconds"); settle->setValue(folder.settle_seconds);
      auto* enabled = new QCheckBox("Monitor this folder", &editor); enabled->setChecked(folder.enabled); auto* recursive = new QCheckBox("Include subfolders", &editor); recursive->setChecked(folder.recursive); auto* auto_start = new QCheckBox("Start queued jobs automatically", &editor); auto_start->setChecked(folder.auto_start);
      form->addRow("Input folder:", input_row); form->addRow("File patterns (semicolon separated):", patterns); form->addRow("Output folder (blank = input folder):", output_row); form->addRow("Output naming template:", name_template); form->addRow("Output profile:", profile); form->addRow("Operation:", operation); form->addRow("Wait until file is unchanged for:", settle); editor_layout->addLayout(form); editor_layout->addWidget(enabled); editor_layout->addWidget(recursive); editor_layout->addWidget(auto_start);
      auto* note = new QLabel("Naming tokens: %BASE% is the input name without extension, %NAME% is the complete input filename, %EXT% is the profile extension, and %PROFILE% is the profile name. Files are fingerprinted by path, size, and modification time so completed inputs are not queued twice.", &editor); note->setWordWrap(true); editor_layout->addWidget(note); editor_layout->addStretch();
      auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &editor); editor_layout->addWidget(buttons); connect(buttons, &QDialogButtonBox::accepted, &editor, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, &editor, &QDialog::reject);
      connect(input_browse, &QPushButton::clicked, &editor, [input, &editor] { const auto path = QFileDialog::getExistingDirectory(&editor, "Watch Input Folder", input->text()); if (!path.isEmpty()) input->setText(path); });
      connect(output_browse, &QPushButton::clicked, &editor, [output, &editor] { const auto path = QFileDialog::getExistingDirectory(&editor, "Watch Output Folder", output->text()); if (!path.isEmpty()) output->setText(path); });
      if (editor.exec() != QDialog::Accepted || input->text().trimmed().isEmpty()) return std::nullopt;
      folder.input_folder = QDir::cleanPath(input->text().trimmed()); folder.output_folder = output->text().trimmed().isEmpty() ? QString{} : QDir::cleanPath(output->text().trimmed()); folder.patterns = patterns->text().trimmed(); folder.output_template = name_template->text().trimmed(); folder.profile_id = profile->currentData().toString(); folder.operation = operation->currentData().toString(); folder.settle_seconds = settle->value(); folder.enabled = enabled->isChecked(); folder.recursive = recursive->isChecked(); folder.auto_start = auto_start->isChecked();
      if (folder.id.isEmpty()) folder.id = QString::fromLatin1(QCryptographicHash::hash((folder.input_folder + QString::number(QDateTime::currentMSecsSinceEpoch())).toUtf8(), QCryptographicHash::Sha256).toHex().left(16));
      return folder;
    };
    auto* controls = new QHBoxLayout; auto* add = new QPushButton("Add...", &dialog); auto* edit = new QPushButton("Edit...", &dialog); auto* remove = new QPushButton("Remove", &dialog); auto* scan = new QPushButton("Scan Now", &dialog); auto* clear_history = new QPushButton("Clear Processing History", &dialog); controls->addWidget(add); controls->addWidget(edit); controls->addWidget(remove); controls->addStretch(); controls->addWidget(clear_history); controls->addWidget(scan); layout->addLayout(controls);
    connect(add, &QPushButton::clicked, &dialog, [this, edit_folder, refresh] { if (const auto folder = edit_folder(std::nullopt)) { watch_folders_.push_back(*folder); save_watch_folders(); refresh(); } });
    connect(edit, &QPushButton::clicked, &dialog, [this, table, edit_folder, refresh] { const int row = table->currentRow(); if (row < 0 || row >= watch_folders_.size()) return; if (const auto folder = edit_folder(watch_folders_[row])) { watch_folders_[row] = *folder; save_watch_folders(); refresh(); } });
    connect(remove, &QPushButton::clicked, &dialog, [this, table, refresh] { const int row = table->currentRow(); if (row < 0 || row >= watch_folders_.size()) return; watch_folders_.removeAt(row); save_watch_folders(); refresh(); });
    connect(scan, &QPushButton::clicked, &dialog, [this] { scan_watch_folders(true); });
    connect(clear_history, &QPushButton::clicked, &dialog, [this] { watch_history_.clear(); watch_first_seen_.clear(); save_watch_history(); status_->setText("Watch-folder processing history cleared"); });
    refresh(); auto* close = new QDialogButtonBox(QDialogButtonBox::Close, &dialog); layout->addWidget(close); connect(close, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); dialog.exec();
  }
  void show_batch_manager() {
    QDialog dialog(this); dialog.setWindowTitle("Batch Manager"); dialog.resize(1040, 610); auto* layout = new QVBoxLayout(&dialog); auto* naming = new QGroupBox("Automatic Output Naming", &dialog); auto* naming_form = new QFormLayout(naming); auto* destination_folder = new QLineEdit(QSettings("SmartCut64", "SmartCut64").value("batch/destinationFolder").toString(), naming); auto* browse_folder = new QPushButton("Browse…", naming); auto* folder_row = new QWidget(naming); auto* folder_layout = new QHBoxLayout(folder_row); folder_layout->setContentsMargins(0, 0, 0, 0); folder_layout->addWidget(destination_folder, 1); folder_layout->addWidget(browse_folder); auto* modifier = new QLineEdit(QSettings("SmartCut64", "SmartCut64").value("batch/filenameModifier", "-edited").toString(), naming); auto* format = new QComboBox(naming); format->addItem("Matroska (.mkv)", "mkv"); format->addItem("MPEG-4 (.mp4)", "mp4"); format->addItem("QuickTime (.mov)", "mov"); format->addItem("MPEG Program Stream (.mpg)", "mpg"); format->addItem("MPEG Transport Stream (.ts)", "ts"); format->addItem("Blu-ray Transport Stream (.m2ts)", "m2ts"); format->addItem("Windows Recorded TV (.wtv)", "wtv"); format->addItem("Windows DVR-MS (.dvr-ms)", "dvr-ms"); format->addItem("Topfield Recording (.rec)", "rec"); format->addItem("Material Exchange Format (.mxf)", "mxf"); const auto saved_format = format->findData(QSettings("SmartCut64", "SmartCut64").value("batch/outputFormat", "mkv")); format->setCurrentIndex(saved_format < 0 ? 0 : saved_format); auto* profile = new QComboBox(naming); profile->addItem("Intelligent smart render", "smart"); profile->addItem("Keyframe-only copy", "keyframe"); profile->addItem("Full source-codec transcode", "full"); naming_form->addRow("Destination folder (blank = source folder):", folder_row); naming_form->addRow("Filename modifier:", modifier); naming_form->addRow("Output format:", format); naming_form->addRow("Output processing:", profile); layout->addWidget(naming);
    profile->clear(); for (int index = 0; index < output_profile_->count(); ++index) profile->addItem(output_profile_->itemText(index), output_profile_->itemData(index)); const auto batch_profile = profile->findData(QSettings("SmartCut64", "SmartCut64").value("batch/outputProfile", output_profile_->currentData())); profile->setCurrentIndex(batch_profile < 0 ? 0 : batch_profile); naming_form->labelForField(profile)->setProperty("text", "Output profile:");
    auto* table = new QTableWidget(0, 4, &dialog); table->setHorizontalHeaderLabels({"Source / Project", "Destination", "Operation", "Status"}); table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); table->setSelectionBehavior(QAbstractItemView::SelectRows); table->setEditTriggers(QAbstractItemView::NoEditTriggers); layout->addWidget(table, 1);
    const auto refresh = [this, table] { save_batch_queue(); table->setRowCount(batch_jobs_.size()); for (int row = 0; row < batch_jobs_.size(); ++row) { const auto& job = batch_jobs_[row]; const QString profile_name = job.mode == "full" ? "Full transcode" : job.mode == "keyframe" ? "Keyframe-only copy" : "Smart render"; const QString operation = job.operation == "ad-scan" ? "Ad-Detective scan to editable project" : job.operation == "videoredo-batch" ? "VideoReDo BPrj embedded edits and profile" : job.operation == "project" ? profile_name + " saved project" : job.cuts.empty() ? profile_name + " complete source" : QString("%1, %2 cut%3").arg(profile_name).arg(static_cast<qulonglong>(job.cuts.size())).arg(job.cuts.size() == 1 ? "" : "s"); const QStringList values{job.input, job.output, operation + (job.filter_streams ? "; selected streams" : ""), job.status}; for (int column = 0; column < values.size(); ++column) table->setItem(row, column, new QTableWidgetItem(values[column])); } };
    const auto output_for = [destination_folder, modifier, format](const QString& source) { const QFileInfo info(source); const auto folder = destination_folder->text().trimmed().isEmpty() ? info.absolutePath() : destination_folder->text().trimmed(); QString base = info.completeBaseName(); if (base.endsWith(".smartcut", Qt::CaseInsensitive)) base.chop(9); return QDir(folder).filePath(base + modifier->text() + '.' + format->currentData().toString()); };
    refresh(); auto* controls = new QHBoxLayout; auto* add = new QPushButton("Add Videos…", &dialog); auto* add_projects = new QPushButton("Add Projects…", &dialog); auto* add_ad = new QPushButton("Add Ad Scans…", &dialog); auto* watch = new QPushButton("Watch Folders…", &dialog); auto* up = new QPushButton("Move Up", &dialog); auto* down = new QPushButton("Move Down", &dialog); auto* remove = new QPushButton("Remove", &dialog); auto* clear = new QPushButton("Clear", &dialog); controls->addWidget(add); controls->addWidget(add_projects); controls->addWidget(add_ad); controls->addWidget(watch); controls->addWidget(up); controls->addWidget(down); controls->addWidget(remove); controls->addWidget(clear); controls->addStretch(); layout->addLayout(controls);
    auto* execution = new QHBoxLayout; auto* silent = new QCheckBox("Run silently (suppress completion sounds)", &dialog); silent->setChecked(batch_silent_); auto* save_commands = new QPushButton("Save Commands…", &dialog); auto* save_execute = new QPushButton("Save And Execute…", &dialog); auto* run = new QPushButton("Run Batch", &dialog); execution->addWidget(silent); execution->addStretch(); execution->addWidget(save_commands); execution->addWidget(save_execute); execution->addWidget(run); layout->addLayout(execution);
    connect(browse_folder, &QPushButton::clicked, &dialog, [destination_folder, &dialog] { const auto folder = QFileDialog::getExistingDirectory(&dialog, "Batch Destination Folder", destination_folder->text()); if (!folder.isEmpty()) destination_folder->setText(folder); });
    connect(watch, &QPushButton::clicked, &dialog, [this] { show_watch_folder_manager(); });
    connect(add, &QPushButton::clicked, &dialog, [this, &dialog, refresh, output_for, profile] { auto files = QFileDialog::getOpenFileNames(&dialog, "Add Videos To Batch", {}, "Video Files (*.mkv *.mp4 *.mov *.ts *.m2ts *.mpg *.mpeg *.webm);;All Files (*)"); files.sort(Qt::CaseInsensitive); const auto selected_profile = current_output_profile(profile->currentData()); for (const auto& source : files) { BatchJob job; job.input = source; job.output = output_for(source); apply_output_profile(job, selected_profile); batch_jobs_.push_back(std::move(job)); } refresh(); });
    connect(add_projects, &QPushButton::clicked, &dialog, [this, &dialog, refresh, output_for, profile] { auto projects = QFileDialog::getOpenFileNames(&dialog, "Add Editor Projects", {}, "Editor Projects (*.smartcut *.smartcut.json *.json *.BPrj *.bprj);;VideoReDo Batch Project (*.BPrj *.bprj)"); projects.sort(Qt::CaseInsensitive); const auto selected_profile = current_output_profile(profile->currentData()); for (const auto& project : projects) { BatchJob job; job.input = project; job.output = output_for(project); apply_output_profile(job, selected_profile); job.operation = project.endsWith(".bprj", Qt::CaseInsensitive) ? "videoredo-batch" : "project"; batch_jobs_.push_back(std::move(job)); } refresh(); });
    connect(add_ad, &QPushButton::clicked, &dialog, [this, &dialog, refresh, destination_folder, modifier] { auto files = QFileDialog::getOpenFileNames(&dialog, "Add Files For Ad-Detective", {}, "Video Files (*.mkv *.mp4 *.mov *.ts *.m2ts *.mpg *.mpeg *.webm);;All Files (*)"); files.sort(Qt::CaseInsensitive); for (const auto& source : files) { const QFileInfo info(source); const auto folder = destination_folder->text().trimmed().isEmpty() ? info.absolutePath() : destination_folder->text().trimmed(); BatchJob job; job.input = source; job.output = QDir(folder).filePath(info.completeBaseName() + modifier->text() + "-adscan.smartcut.json"); job.operation = "ad-scan"; batch_jobs_.push_back(std::move(job)); } refresh(); });
    connect(remove, &QPushButton::clicked, &dialog, [this, table, refresh] { const int row = table->currentRow(); if (row >= 0 && row < batch_jobs_.size()) batch_jobs_.removeAt(row); refresh(); }); connect(clear, &QPushButton::clicked, &dialog, [this, refresh] { if (!batch_running_) batch_jobs_.clear(); refresh(); }); connect(up, &QPushButton::clicked, &dialog, [this, table, refresh] { const int row = table->currentRow(); if (row > 0) { batch_jobs_.swapItemsAt(row, row - 1); refresh(); table->selectRow(row - 1); } }); connect(down, &QPushButton::clicked, &dialog, [this, table, refresh] { const int row = table->currentRow(); if (row >= 0 && row + 1 < batch_jobs_.size()) { batch_jobs_.swapItemsAt(row, row + 1); refresh(); table->selectRow(row + 1); } });
    const auto save_command_file = [this, &dialog](bool required) {
      const auto path = QFileDialog::getSaveFileName(&dialog, "Save Batch Commands", {}, "Windows Command Script (*.cmd)");
      if (path.isEmpty()) return !required;
      QByteArray content{"@echo off\r\nsetlocal\r\n"};
      const auto quote = [](const QString& value) { return '"' + QDir::toNativeSeparators(value) + '"'; };
      const auto worker = quote(QCoreApplication::applicationDirPath() + "/smartcut-worker.exe");
      for (const auto& job : batch_jobs_) {
        QString command;
        if (job.operation == "project") {
          command = QString("%1 --export-project --project %2 --output %3 --mode %4 %5")
                        .arg(worker, quote(job.input), quote(job.output), job.mode, batch_audio_arguments(job).join(' '));
          command += " " + batch_video_arguments(job).join(' ');
          command += " " + batch_transport_stream_arguments(job).join(' ');
        } else if (job.operation == "videoredo-batch") {
          command = QString("%1 --run-videoredo-batch --project %2 --output %3").arg(worker, quote(job.input), quote(job.output));
          if (!license_.paid()) command += " --trial-watermark true";
        } else if (job.operation == "ad-scan") {
          command = QString("%1 --ad-detect-project --input %2 --project %3 --scene-threshold %4 --minimum-seconds %5 %6")
                        .arg(worker, quote(job.input), quote(job.output), QString::number(ad_scene_threshold_),
                             QString::number(ad_minimum_seconds_), ad_advanced_arguments().join(' '));
        } else if (!job.cuts.empty() || job.mode == "full" || job.audio_codec != "copy" || !batch_video_arguments(job).isEmpty() || !batch_transport_stream_arguments(job).isEmpty()) {
          command = QString("%1 --export-edits --input %2 --output %3 --frame-count %4 --cuts %5 --mode %6")
                        .arg(worker, quote(job.input), quote(job.output),
                             QString::number(static_cast<qulonglong>(job.frame_count)), cut_csv(job.cuts), job.mode);
          if (job.filter_streams) command += " --streams " + stream_csv(job.selected_streams);
          command += " " + batch_audio_arguments(job).join(' ');
          command += " " + batch_video_arguments(job).join(' ');
          command += " " + batch_transport_stream_arguments(job).join(' ');
        } else {
          command = QString("%1 --export --input %2 --output %3").arg(worker, quote(job.input), quote(job.output));
        }
        content += command.toUtf8() + "\r\nif errorlevel 1 exit /b %errorlevel%\r\n";
      }
      QSaveFile file(path);
      return file.open(QIODevice::WriteOnly) && file.write(content) == content.size() && file.commit();
    };
    connect(save_commands, &QPushButton::clicked, &dialog, [save_command_file] { save_command_file(false); }); connect(save_execute, &QPushButton::clicked, &dialog, [this, &dialog, silent, profile, format, save_command_file] { if (!save_command_file(true)) return; batch_silent_ = silent->isChecked(); QSettings settings("SmartCut64", "SmartCut64"); settings.setValue("batch/outputProfile", profile->currentData()); settings.setValue("batch/outputFormat", format->currentData()); dialog.accept(); start_batch(); }); connect(run, &QPushButton::clicked, &dialog, [this, &dialog, silent, profile, format] { batch_silent_ = silent->isChecked(); QSettings settings("SmartCut64", "SmartCut64"); settings.setValue("batch/outputProfile", profile->currentData()); settings.setValue("batch/outputFormat", format->currentData()); dialog.accept(); start_batch(); }); connect(destination_folder, &QLineEdit::editingFinished, &dialog, [destination_folder] { QSettings("SmartCut64", "SmartCut64").setValue("batch/destinationFolder", destination_folder->text()); }); connect(modifier, &QLineEdit::editingFinished, &dialog, [modifier] { QSettings("SmartCut64", "SmartCut64").setValue("batch/filenameModifier", modifier->text()); }); connect(format, &QComboBox::currentIndexChanged, &dialog, [format](int) { QSettings("SmartCut64", "SmartCut64").setValue("batch/outputFormat", format->currentData()); });
    auto* close = new QDialogButtonBox(QDialogButtonBox::Close, &dialog); layout->addWidget(close); connect(close, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); dialog.exec();
  }
  void start_batch(bool include_completed = true) {
    license_.reload();
    if (batch_running_) { status_->setText("Batch processing is already running"); return; }
    if (batch_jobs_.isEmpty()) { status_->setText("The batch queue is empty"); return; }
    batch_skip_completed_ = !include_completed;
    if (include_completed) for (auto& job : batch_jobs_) job.status = "Queued";
    else for (auto& job : batch_jobs_) if (job.status == "Running") job.status = "Queued";
    const bool pending = std::any_of(batch_jobs_.cbegin(), batch_jobs_.cend(), [this](const BatchJob& job) { return !batch_skip_completed_ || job.status != "Completed"; });
    if (!pending) { status_->setText("All batch jobs are already completed"); return; }
    batch_running_ = true; batch_active_index_ = 0; save_batch_queue(); progress_->setRange(0, batch_jobs_.size()); progress_->setValue(0); run_next_batch_job();
  }
  void run_next_batch_job() {
    while (batch_skip_completed_ && batch_active_index_ < batch_jobs_.size() && batch_jobs_[batch_active_index_].status == "Completed") ++batch_active_index_;
    progress_->setValue(batch_active_index_);
    if (batch_active_index_ >= batch_jobs_.size()) { batch_running_ = false; progress_->setRange(0, 100); progress_->setValue(100); save_batch_queue(); const auto failed = std::count_if(batch_jobs_.cbegin(), batch_jobs_.cend(), [](const BatchJob& job) { return job.status != "Completed"; }); status_->setText(QString("Batch complete: %1 completed, %2 failed").arg(batch_jobs_.size() - failed).arg(failed)); return; }
    auto& job = batch_jobs_[batch_active_index_]; job.status = "Running"; save_batch_queue(); status_->setText(QString("Batch job %1 of %2: %3").arg(batch_active_index_ + 1).arg(batch_jobs_.size()).arg(QFileInfo(job.input).fileName()));
    QStringList args{"--export", "--input", job.input, "--output", job.output};
    if (job.operation == "ad-scan") { args = {"--ad-detect-project", "--input", job.input, "--project", job.output, "--scene-threshold", QString::number(ad_scene_threshold_, 'f', 3), "--minimum-seconds", QString::number(ad_minimum_seconds_, 'f', 3)}; args += ad_advanced_arguments(); }
    else if (job.operation == "project") { args = {"--export-project", "--project", job.input, "--output", job.output, "--mode", job.mode}; args += batch_audio_arguments(job); args += batch_video_arguments(job); args += batch_transport_stream_arguments(job); }
    else if (job.operation == "videoredo-batch") { args = {"--run-videoredo-batch", "--project", job.input, "--output", job.output}; if (!license_.paid()) args << "--trial-watermark" << "true"; }
    else if (!job.cuts.empty() || job.mode == "full" || job.audio_codec != "copy" || !batch_video_arguments(job).isEmpty() || !batch_transport_stream_arguments(job).isEmpty()) { args = {"--export-edits", "--input", job.input, "--output", job.output, "--frame-count", QString::number(static_cast<qulonglong>(job.frame_count)), "--cuts", cut_csv(job.cuts), "--mode", job.mode}; if (job.filter_streams) args << "--streams" << stream_csv(job.selected_streams); args += batch_audio_arguments(job); args += batch_video_arguments(job); args += batch_transport_stream_arguments(job); }
    else if (job.filter_streams) args = {"--export-selected", "--input", job.input, "--output", job.output, "--streams", stream_csv(job.selected_streams)};
    batch_worker_.start(worker_program(), args);
  }
  void show_ad_parameters() {
    QDialog dialog(this); dialog.setWindowTitle("Ad-Detective Parameters"); dialog.resize(620, 520); auto* layout = new QVBoxLayout(&dialog); auto* tabs = new QTabWidget(&dialog); layout->addWidget(tabs, 1);
    auto* common = new QWidget(tabs); auto* common_layout = new QVBoxLayout(common); auto* common_form = new QFormLayout; auto* scene = new QDoubleSpinBox(common); scene->setRange(0.01, 1.0); scene->setDecimals(2); scene->setSingleStep(0.01); scene->setValue(ad_scene_threshold_); auto* minimum = new QDoubleSpinBox(common); minimum->setRange(0.0, 60.0); minimum->setDecimals(1); minimum->setSuffix(" seconds"); minimum->setValue(ad_minimum_seconds_); auto* maximum = new QSpinBox(common); maximum->setRange(10, 1800); maximum->setSuffix(" seconds"); maximum->setValue(ad_maximum_block_seconds_); auto* priority = new QComboBox(common); priority->addItem("Idle", "idle"); priority->addItem("Below normal", "below-normal"); priority->addItem("Normal", "normal"); priority->addItem("Above normal", "above-normal"); priority->setCurrentIndex(std::max(0, priority->findData(ad_cpu_priority_))); auto* ip_only = new QCheckBox("Scan I and P pictures only", common); ip_only->setChecked(ad_ip_only_); auto* automatic = new QCheckBox("Automatically add detected commercial blocks to the cut list", common); automatic->setChecked(ad_automatic_cuts_); common_form->addRow("Scene-change threshold:", scene); common_form->addRow("Minimum time between marks:", minimum); common_form->addRow("Maximum commercial block:", maximum); common_form->addRow("Scan CPU priority:", priority); common_layout->addLayout(common_form); common_layout->addWidget(ip_only); common_layout->addWidget(automatic); common_layout->addStretch(); tabs->addTab(common, "Common Parameters");
    auto* black_page = new QWidget(tabs); auto* black_layout = new QVBoxLayout(black_page); auto* black_form = new QFormLayout; const auto level_control = [black_page](int value) { auto* control = new QSpinBox(black_page); control->setRange(0, 255); control->setValue(value); return control; }; auto* average_black = level_control(ad_black_level_); auto* peak_black = level_control(ad_peak_black_level_); auto* contrast = level_control(ad_contrast_threshold_); auto* left = new QSpinBox(black_page); auto* top = new QSpinBox(black_page); auto* right = new QSpinBox(black_page); auto* bottom = new QSpinBox(black_page); for (auto* margin : {left, top, right, bottom}) { margin->setRange(0, 45); margin->setSuffix(" %"); } left->setValue(ad_region_left_); top->setValue(ad_region_top_); right->setValue(ad_region_right_); bottom->setValue(ad_region_bottom_); auto* detect_black = new QCheckBox("Enable black-frame transition detection", black_page); detect_black->setChecked(ad_black_detection_); black_form->addRow("Maximum average luminance:", average_black); black_form->addRow("Maximum peak luminance:", peak_black); black_form->addRow("Maximum contrast:", contrast); black_form->addRow("Left scan margin:", left); black_form->addRow("Top scan margin:", top); black_form->addRow("Right scan margin:", right); black_form->addRow("Bottom scan margin:", bottom); black_layout->addWidget(detect_black); black_layout->addLayout(black_form); auto* calibrate = new QPushButton("Auto Threshold From Marked Selection", black_page); black_layout->addWidget(calibrate); auto* calibration_note = new QLabel("Mark a representative black interval before calibration. The 90th-percentile frame statistics plus a safety margin become the three black-frame thresholds.", black_page); calibration_note->setWordWrap(true); black_layout->addWidget(calibration_note); black_layout->addStretch(); tabs->addTab(black_page, "Black Levels");
    auto* review = new QWidget(tabs); auto* review_layout = new QVBoxLayout(review); auto* review_form = new QFormLayout; auto* pause = new QComboBox(review); pause->addItem("Do not pause", 0); pause->addItem("Pause at start of first detected block", 1); pause->addItem("Pause at end of first detected block", 2); pause->addItem("Review every detected scene", 3); pause->setCurrentIndex(std::max(0, pause->findData(ad_pause_mode_))); auto* display = new QCheckBox("Display analyzed frames while scanning", review); display->setChecked(ad_display_during_scan_); review_form->addRow("After scan:", pause); review_layout->addLayout(review_form); review_layout->addWidget(display); auto* review_note = new QLabel("Review mode presents every detection with frame statistics. Uncheck false positives before accepting chapter marks and automatic cuts.", review); review_note->setWordWrap(true); review_layout->addWidget(review_note); review_layout->addStretch(); tabs->addTab(review, "Review");
    connect(calibrate, &QPushButton::clicked, &dialog, [this, &dialog, average_black, peak_black, contrast, left, top, right, bottom, ip_only] { if (input_->text().isEmpty() || !timeline_->in_mark() || !timeline_->out_mark() || *timeline_->out_mark() <= *timeline_->in_mark()) { QMessageBox::information(&dialog, "Auto Threshold", "Mark a non-empty representative black interval first."); return; } const auto region = QString("%1,%2,%3,%4").arg(left->value() / 100.0, 0, 'f', 3).arg(top->value() / 100.0, 0, 'f', 3).arg(right->value() / 100.0, 0, 'f', 3).arg(bottom->value() / 100.0, 0, 'f', 3); QProcess worker; worker.start(worker_program(), {"--ad-calibrate", "--input", input_->text(), "--first-frame", QString::number(static_cast<qulonglong>(*timeline_->in_mark())), "--end-frame", QString::number(static_cast<qulonglong>(*timeline_->out_mark() + 1)), "--region", region, "--ip-only", ip_only->isChecked() ? "1" : "0"}); if (!worker.waitForFinished(120000) || worker.exitCode() != 0) { QMessageBox::warning(&dialog, "Auto Threshold", "Calibration failed for the marked selection."); return; } const auto document = QJsonDocument::fromJson(worker.readAllStandardOutput().trimmed()); if (!document.isObject() || document.object().value("status").toString() != "PASSED") { QMessageBox::warning(&dialog, "Auto Threshold", "Calibration did not return valid frame statistics."); return; } const auto result = document.object(); average_black->setValue(qRound(result.value("average_black_level").toDouble() * 255.0)); peak_black->setValue(qRound(result.value("peak_black_level").toDouble() * 255.0)); contrast->setValue(qRound(result.value("contrast_threshold").toDouble() * 255.0)); });
    auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); layout->addWidget(buttons); connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); if (dialog.exec() != QDialog::Accepted) return;
    ad_scene_threshold_ = scene->value(); ad_black_level_ = average_black->value(); ad_peak_black_level_ = peak_black->value(); ad_contrast_threshold_ = contrast->value(); ad_region_left_ = left->value(); ad_region_top_ = top->value(); ad_region_right_ = right->value(); ad_region_bottom_ = bottom->value(); ad_ip_only_ = ip_only->isChecked(); ad_black_detection_ = detect_black->isChecked(); ad_pause_mode_ = pause->currentData().toInt(); ad_display_during_scan_ = display->isChecked(); ad_cpu_priority_ = priority->currentData().toString(); ad_minimum_seconds_ = minimum->value(); ad_maximum_block_seconds_ = maximum->value(); ad_automatic_cuts_ = automatic->isChecked();
    QSettings settings("SmartCut64", "SmartCut64"); settings.setValue("ad/sceneThreshold", ad_scene_threshold_); settings.setValue("ad/blackLevel", ad_black_level_); settings.setValue("ad/peakBlackLevel", ad_peak_black_level_); settings.setValue("ad/contrastThreshold", ad_contrast_threshold_); settings.setValue("ad/regionLeft", ad_region_left_); settings.setValue("ad/regionTop", ad_region_top_); settings.setValue("ad/regionRight", ad_region_right_); settings.setValue("ad/regionBottom", ad_region_bottom_); settings.setValue("ad/ipOnly", ad_ip_only_); settings.setValue("ad/blackDetection", ad_black_detection_); settings.setValue("ad/pauseMode", ad_pause_mode_); settings.setValue("ad/displayDuringScan", ad_display_during_scan_); settings.setValue("ad/cpuPriority", ad_cpu_priority_); settings.setValue("ad/minimumSeconds", ad_minimum_seconds_); settings.setValue("ad/maximumBlockSeconds", ad_maximum_block_seconds_); settings.setValue("ad/automaticCuts", ad_automatic_cuts_);
    status_->setText("Ad-Detective parameters updated");
  }
  QString ad_region_csv() const { return QString("%1,%2,%3,%4").arg(ad_region_left_ / 100.0, 0, 'f', 3).arg(ad_region_top_ / 100.0, 0, 'f', 3).arg(ad_region_right_ / 100.0, 0, 'f', 3).arg(ad_region_bottom_ / 100.0, 0, 'f', 3); }
  QStringList ad_advanced_arguments() const { return {"--average-black", QString::number(ad_black_level_ / 255.0, 'f', 5), "--peak-black", QString::number(ad_peak_black_level_ / 255.0, 'f', 5), "--contrast", QString::number(ad_contrast_threshold_ / 255.0, 'f', 5), "--region", ad_region_csv(), "--ip-only", ad_ip_only_ ? "1" : "0", "--black-detection", ad_black_detection_ ? "1" : "0", "--priority", ad_cpu_priority_}; }
  std::vector<std::size_t> review_ad_markers(const QJsonArray& marker_values) {
    last_ad_markers_ = marker_values; std::vector<std::size_t> accepted; if (marker_values.isEmpty()) return accepted;
    if (automation_mode_ || ad_pause_mode_ == 0) { for (const auto& value : marker_values) accepted.push_back(static_cast<std::size_t>(value.toObject().value("frame").toDouble())); return accepted; }
    QDialog dialog(this); dialog.setWindowTitle("Ad-Detective Review"); dialog.resize(850, 430); auto* layout = new QVBoxLayout(&dialog); auto* table = new QTableWidget(marker_values.size(), 8, &dialog); table->setHorizontalHeaderLabels({"Use", "Frame", "Time", "Type", "Scene", "Average", "Peak", "Contrast"}); table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); table->setSelectionBehavior(QAbstractItemView::SelectRows); table->setEditTriggers(QAbstractItemView::NoEditTriggers); for (int row = 0; row < marker_values.size(); ++row) { const auto marker = marker_values[row].toObject(); auto* use = new QTableWidgetItem; use->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable); use->setCheckState(Qt::Checked); table->setItem(row, 0, use); const auto frame = static_cast<std::size_t>(marker.value("frame").toDouble()); table->setItem(row, 1, new QTableWidgetItem(QString::number(static_cast<qulonglong>(frame)))); table->setItem(row, 2, new QTableWidgetItem(format_time(frame))); table->setItem(row, 3, new QTableWidgetItem(marker.value("black").toBool() ? "Black transition" : "Scene change")); table->setItem(row, 4, new QTableWidgetItem(QString::number(marker.value("score").toDouble(), 'f', 4))); table->setItem(row, 5, new QTableWidgetItem(QString::number(marker.value("average").toDouble() * 255.0, 'f', 1))); table->setItem(row, 6, new QTableWidgetItem(QString::number(marker.value("peak").toDouble() * 255.0, 'f', 1))); table->setItem(row, 7, new QTableWidgetItem(QString::number(marker.value("contrast").toDouble() * 255.0, 'f', 1))); } layout->addWidget(table, 1); auto* navigation = new QHBoxLayout; auto* previous = new QPushButton("Previous", &dialog); auto* next = new QPushButton("Next", &dialog); auto* toggle = new QPushButton("Toggle Use", &dialog); navigation->addWidget(previous); navigation->addWidget(next); navigation->addWidget(toggle); navigation->addStretch(); layout->addLayout(navigation); auto seek_row = [this, table](int row) { if (row < 0 || row >= table->rowCount()) return; table->selectRow(row); timeline_->set_frame(static_cast<std::size_t>(table->item(row, 1)->text().toULongLong())); }; connect(table, &QTableWidget::currentCellChanged, &dialog, [seek_row](int row, int, int, int) { seek_row(row); }); connect(previous, &QPushButton::clicked, &dialog, [table, seek_row] { seek_row(std::max(0, table->currentRow() - 1)); }); connect(next, &QPushButton::clicked, &dialog, [table, seek_row] { seek_row(std::min(table->rowCount() - 1, table->currentRow() + 1)); }); connect(toggle, &QPushButton::clicked, &dialog, [table] { if (table->currentRow() >= 0) { auto* item = table->item(table->currentRow(), 0); item->setCheckState(item->checkState() == Qt::Checked ? Qt::Unchecked : Qt::Checked); } }); auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); buttons->button(QDialogButtonBox::Ok)->setText("Accept Markers"); layout->addWidget(buttons); connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); seek_row(ad_pause_mode_ == 2 && table->rowCount() > 1 ? 1 : 0); if (dialog.exec() != QDialog::Accepted) return {}; for (int row = 0; row < table->rowCount(); ++row) if (table->item(row, 0)->checkState() == Qt::Checked) accepted.push_back(static_cast<std::size_t>(table->item(row, 1)->text().toULongLong())); return accepted;
  }
  std::size_t apply_ad_detected_cuts(const std::vector<std::size_t>& markers) {
    if (!ad_automatic_cuts_ || markers.size() < 2) return 0;
    std::vector<FrameRange> additions; std::size_t cluster_start = 0; const auto maximum_us = static_cast<std::uint64_t>(ad_maximum_block_seconds_) * 1000000ULL;
    for (std::size_t index = 1; index <= markers.size(); ++index) {
      const auto start_time = timeline_->frame_time_us(markers[cluster_start]);
      const auto next_time = index < markers.size() ? timeline_->frame_time_us(markers[index]) : std::optional<std::uint64_t>{};
      const bool close_cluster = index == markers.size() || !start_time || !next_time || *next_time > *start_time + maximum_us;
      if (!close_cluster) continue;
      if (index - cluster_start >= 2 && markers[index - 1] > markers[cluster_start]) additions.emplace_back(markers[cluster_start], markers[index - 1]);
      cluster_start = index;
    }
    if (additions.empty()) return 0;
    remember_edit(); auto cuts = cut_ranges_; cuts.insert(cuts.end(), additions.begin(), additions.end()); set_cut_ranges(std::move(cuts)); return additions.size();
  }
  void start_ad_scan() {
    if (ad_scan_.state() != QProcess::NotRunning) { ad_scan_cancel_requested_ = true; ad_scan_.kill(); return; }
    if (input_->text().isEmpty() || timeline_->frame_count() == 0) { status_->setText("Open a video before starting Ad-Detective"); return; }
    ad_scan_cancel_requested_ = false;
    ad_scan_output_.clear();
    status_->setText("Ad-Detective scanning for scene and black-level transitions…");
    if (!ad_display_during_scan_) { current_preview_image_ = {}; preview_->setPixmap({}); preview_->setText("Ad-Detective scanning…"); }
    progress_->setRange(0, static_cast<int>(std::min<std::size_t>(timeline_->frame_count(), static_cast<std::size_t>(std::numeric_limits<int>::max())))); progress_->setValue(static_cast<int>(timeline_->current_frame()));
    const auto minimum_frames = static_cast<std::size_t>(std::max(1.0, std::ceil(ad_minimum_seconds_ * static_cast<double>(frame_rate_num_) / static_cast<double>(frame_rate_den_))));
    QStringList arguments{"--ad-detect", "--input", input_->text(), "--start-frame", QString::number(static_cast<qulonglong>(timeline_->current_frame())), "--scene-threshold", QString::number(ad_scene_threshold_, 'f', 3), "--minimum-frames", QString::number(static_cast<qulonglong>(minimum_frames))}; arguments += ad_advanced_arguments(); ad_scan_.start(worker_program(), arguments);
  }
  std::optional<QString> requested_joiner_description(const QString& proposed) {
    if (!prompt_joiner_descriptions_ || automation_mode_) return proposed;
    bool accepted = false; const auto value = QInputDialog::getText(this, "Joiner Description", "Description:", QLineEdit::Normal, proposed, &accepted).trimmed(); if (!accepted) return std::nullopt; return value.isEmpty() ? proposed : value;
  }
  void add_current_to_joiner() {
    if (input_->text().isEmpty()) { status_->setText("Open a video before adding it to the Joiner"); return; }
    if (join_item_worker_.state() != QProcess::NotRunning) { status_->setText("The current project is already being prepared for the Joiner"); return; }
    const bool filter_streams = !selected_streams_.isEmpty() && !all_streams_selected();
    const auto description = requested_joiner_description(QFileInfo(input_->text()).completeBaseName() + (cut_ranges_.empty() && !filter_streams ? QString{} : " (edited)")); if (!description) { status_->setText("Add to Joiner cancelled"); return; }
    if (cut_ranges_.empty() && !filter_streams) { joiner_inputs_.push_back(QDir::toNativeSeparators(QFileInfo(input_->text()).absoluteFilePath())); joiner_descriptions_.push_back(*description); joiner_title_specs_.push_back({}); joiner_durations_us_.push_back(current_program_duration_us()); status_->setText(QString("Current project added to Joiner (%1 items)").arg(joiner_inputs_.size())); return; }
    pending_joiner_item_path_ = preview_dir_.path() + QString("/joiner-item-%1.mkv").arg(joiner_inputs_.size()); QFile::remove(pending_joiner_item_path_); QStringList args;
    pending_joiner_item_description_ = *description;
    std::size_t removed_frames{}; for (const auto& cut : cut_ranges_) removed_frames += cut.second > cut.first ? cut.second - cut.first : 0; const auto kept_frames = timeline_->frame_count() > removed_frames ? timeline_->frame_count() - removed_frames : 0; pending_joiner_duration_us_ = frame_rate_num_ > 0 ? static_cast<std::uint64_t>((static_cast<long double>(kept_frames) * 1'000'000.0L * frame_rate_den_) / frame_rate_num_) : 0;
    if (!cut_ranges_.empty()) { args = {"--export-edits", "--input", input_->text(), "--output", pending_joiner_item_path_, "--frame-count", QString::number(static_cast<qulonglong>(timeline_->frame_count())), "--cuts", cut_csv(cut_ranges_)}; if (filter_streams) args << "--streams" << selected_stream_csv(); }
    else args = {"--export-selected", "--input", input_->text(), "--output", pending_joiner_item_path_, "--streams", selected_stream_csv()};
    status_->setText("Preparing the edited project for the Joiner…"); join_item_worker_.start(worker_program(), args);
  }
  void add_joiner_files(QStringList files, bool sort_files = true) { if (sort_files) files.sort(Qt::CaseInsensitive); for (const auto& path : files) { const QFileInfo info(path); if (!info.isFile()) continue; const auto description = requested_joiner_description(info.completeBaseName()); if (!description) continue; joiner_inputs_.push_back(QDir::toNativeSeparators(info.absoluteFilePath())); joiner_descriptions_.push_back(*description); joiner_title_specs_.push_back({}); joiner_durations_us_.push_back(0); } }
  void add_title_to_joiner(int edit_index = -1) {
    if (title_worker_.state() != QProcess::NotRunning) { status_->setText("A title is already being encoded"); return; }
    QJsonObject existing; if (edit_index >= 0 && edit_index < joiner_title_specs_.size() && !joiner_title_specs_[edit_index].isEmpty()) { QFile file(joiner_title_specs_[edit_index]); if (file.open(QIODevice::ReadOnly)) existing = QJsonDocument::fromJson(file.readAll()).object(); }
    const QString reference = !existing.value("reference").toString().isEmpty() ? existing.value("reference").toString() : !input_->text().isEmpty() ? input_->text() : (!joiner_inputs_.isEmpty() ? joiner_inputs_.front() : QString{});
    if (reference.isEmpty() || !QFileInfo::exists(reference)) { status_->setText("Open or queue a reference video before adding a title"); return; }
    QDialog dialog(this); dialog.setWindowTitle("Title Editor"); dialog.resize(1080, 790); auto* layout = new QVBoxLayout(&dialog); auto* canvas = new TitleCanvasWidget(&dialog); layout->addWidget(canvas, 1);
    canvas->background = QColor(existing.value("background_colour").toString("#ff000000")); canvas->background_image = existing.value("background_image").toString();
    const auto object_from_json = [](const QJsonObject& value) { TitleTextObject object; object.id = value.value("id").toString(QString("text-%1").arg(QDateTime::currentMSecsSinceEpoch())); object.text = value.value("text").toString("Title"); const auto rectangle = value.value("rect").toArray(); if (rectangle.size() == 4) object.rect = QRect(rectangle[0].toInt(), rectangle[1].toInt(), rectangle[2].toInt(), rectangle[3].toInt()); if (!value.value("font").toString().isEmpty()) object.font.fromString(value.value("font").toString()); object.colour = QColor(value.value("colour").toString("#ffffffff")); object.outline = QColor(value.value("outline_colour").toString("#ff000000")); object.shadow = QColor(value.value("shadow_colour").toString("#be000000")); object.outline_width = value.value("outline_width").toInt(1); object.shadow_offset = value.value("shadow_offset").toInt(4); const auto alignment = value.value("alignment").toString("center"); object.alignment = alignment == "left" ? Qt::AlignLeft | Qt::AlignVCenter : alignment == "right" ? Qt::AlignRight | Qt::AlignVCenter : Qt::AlignCenter; return object; };
    for (const auto& value : existing.value("objects").toArray()) if (value.isObject()) canvas->objects.push_back(object_from_json(value.toObject()));
    if (canvas->objects.isEmpty()) { TitleTextObject object; object.text = existing.value("text").toString("Title"); if (!existing.value("font").toString().isEmpty()) object.font.fromString(existing.value("font").toString()); object.colour = QColor(existing.value("text_colour").toString("#ffffffff")); object.id = "text-1"; canvas->objects.push_back(object); }
    auto* editor_row = new QHBoxLayout; auto* objects = new QListWidget(&dialog); objects->setMinimumWidth(190); editor_row->addWidget(objects); auto* properties = new QWidget(&dialog); auto* properties_layout = new QVBoxLayout(properties); auto* text = new QTextEdit(properties); text->setMaximumHeight(85); properties_layout->addWidget(text); auto* property_form = new QFormLayout; auto* alignment = new QComboBox(properties); alignment->addItem("Left", "left"); alignment->addItem("Center", "center"); alignment->addItem("Right", "right"); auto* outline_width = new QSpinBox(properties); outline_width->setRange(0, 8); auto* shadow_offset = new QSpinBox(properties); shadow_offset->setRange(0, 20); property_form->addRow("Alignment:", alignment); property_form->addRow("Outline width:", outline_width); property_form->addRow("Shadow offset:", shadow_offset); properties_layout->addLayout(property_form); auto* style_controls = new QHBoxLayout; auto* font = new QPushButton("Font…", properties); auto* foreground = new QPushButton("Text Colour…", properties); auto* outline = new QPushButton("Outline…", properties); auto* shadow = new QPushButton("Shadow…", properties); for (auto* button : {font, foreground, outline, shadow}) style_controls->addWidget(button); properties_layout->addLayout(style_controls); editor_row->addWidget(properties, 1); layout->addLayout(editor_row);
    auto* object_controls = new QHBoxLayout; auto* add_object = new QPushButton("Add Text Box", &dialog); auto* duplicate_object = new QPushButton("Duplicate", &dialog); auto* remove_object = new QPushButton("Remove", &dialog); auto* copy_object = new QPushButton("Copy", &dialog); auto* paste_object = new QPushButton("Paste", &dialog); auto* auto_fit = new QPushButton("Auto Fit", &dialog); auto* apply_style_all = new QPushButton("Style All", &dialog); auto* move_up = new QPushButton("Bring Forward", &dialog); auto* move_down = new QPushButton("Send Backward", &dialog); auto* center_h = new QPushButton("Center Horizontally", &dialog); auto* center_v = new QPushButton("Center Vertically", &dialog); for (auto* button : {add_object, duplicate_object, remove_object, copy_object, paste_object, auto_fit, apply_style_all, move_up, move_down, center_h, center_v}) object_controls->addWidget(button); object_controls->addStretch(); layout->addLayout(object_controls);
    auto* project_form = new QFormLayout; auto* description = new QLineEdit(existing.value("description").toString("Title"), &dialog); auto* resolution = new QComboBox(&dialog); resolution->addItem("1280 × 720 (16:9)", QSize(1280, 720)); resolution->addItem("1920 × 1080 (16:9)", QSize(1920, 1080)); resolution->addItem("720 × 576 (PAL 4:3 canvas)", QSize(720, 576)); resolution->addItem("720 × 480 (NTSC 4:3 canvas)", QSize(720, 480)); const auto stored_size = QSize(existing.value("canvas_width").toInt(1280), existing.value("canvas_height").toInt(720)); const auto resolution_index = resolution->findData(stored_size); resolution->setCurrentIndex(resolution_index < 0 ? 0 : resolution_index); project_form->addRow("Joiner description:", description); project_form->addRow("Title canvas:", resolution); layout->addLayout(project_form);
    auto* background_controls = new QHBoxLayout; auto* background = new QPushButton("Background Colour…", &dialog); auto* image = new QPushButton("Background Image…", &dialog); auto* clear_image = new QPushButton("Clear Image", &dialog); auto* safe = new QCheckBox("Show title/action safe areas", &dialog); safe->setChecked(true); auto* load_template = new QPushButton("Load Template…", &dialog); auto* save_template = new QPushButton("Save Template…", &dialog); auto* default_template = new QPushButton("Set As Default", &dialog); auto* clear_default = new QPushButton("Remove Default", &dialog); for (auto* control : {background, image, clear_image, load_template, save_template, default_template, clear_default}) background_controls->addWidget(control); background_controls->addStretch(); background_controls->addWidget(safe); layout->addLayout(background_controls);
    const auto object_json = [](const TitleTextObject& object) { const auto alignment = object.alignment.testFlag(Qt::AlignLeft) ? "left" : object.alignment.testFlag(Qt::AlignRight) ? "right" : "center"; return QJsonObject{{"id", object.id}, {"text", object.text}, {"rect", QJsonArray{object.rect.x(), object.rect.y(), object.rect.width(), object.rect.height()}}, {"font", object.font.toString()}, {"colour", object.colour.name(QColor::HexArgb)}, {"outline_colour", object.outline.name(QColor::HexArgb)}, {"shadow_colour", object.shadow.name(QColor::HexArgb)}, {"outline_width", object.outline_width}, {"shadow_offset", object.shadow_offset}, {"alignment", alignment}}; };
    const auto template_json = [this, canvas, object_json, resolution] { QJsonArray values; for (const auto& object : canvas->objects) values.push_back(object_json(object)); const auto size = resolution->currentData().toSize(); return QJsonObject{{"schema", "smartcut-title-template-v1"}, {"objects", values}, {"background_colour", canvas->background.name(QColor::HexArgb)}, {"background_image", canvas->background_image}, {"canvas_width", size.width()}, {"canvas_height", size.height()}, {"duration_seconds", title_duration_seconds_}, {"fade_in_seconds", title_fade_in_seconds_}, {"fade_out_seconds", title_fade_out_seconds_}}; };
    const auto refresh_list = [canvas, objects] { const int selected = canvas->selected; objects->clear(); for (const auto& object : canvas->objects) objects->addItem(object.text.section('\n', 0, 0).trimmed().left(32).isEmpty() ? "Text Box" : object.text.section('\n', 0, 0).trimmed().left(32)); if (selected >= 0 && selected < objects->count()) objects->setCurrentRow(selected); };
    bool loading = false; const auto load_properties = [canvas, objects, text, alignment, outline_width, shadow_offset, &loading] { const int index = objects->currentRow(); canvas->selected = index; loading = true; if (index >= 0 && index < canvas->objects.size()) { const auto& object = canvas->objects[index]; text->setPlainText(object.text); alignment->setCurrentIndex(std::max(0, alignment->findData(object.alignment.testFlag(Qt::AlignLeft) ? "left" : object.alignment.testFlag(Qt::AlignRight) ? "right" : "center"))); outline_width->setValue(object.outline_width); shadow_offset->setValue(object.shadow_offset); } loading = false; canvas->update(); };
    const auto mutate = [canvas, objects, text, alignment, outline_width, shadow_offset, description, &loading, refresh_list] { const int index = objects->currentRow(); if (loading || index < 0 || index >= canvas->objects.size()) return; auto& object = canvas->objects[index]; object.text = text->toPlainText(); const auto align = alignment->currentData().toString(); object.alignment = align == "left" ? Qt::AlignLeft | Qt::AlignVCenter : align == "right" ? Qt::AlignRight | Qt::AlignVCenter : Qt::AlignCenter; object.outline_width = outline_width->value(); object.shadow_offset = shadow_offset->value(); if (!description->isModified() && index == 0) description->setText(object.text.section('\n', 0, 0).trimmed().left(80)); refresh_list(); canvas->update(); };
    canvas->on_selected = [objects, load_properties](int index) { objects->setCurrentRow(index); load_properties(); }; canvas->on_changed = [canvas] { canvas->update(); }; connect(objects, &QListWidget::currentRowChanged, &dialog, [load_properties](int) { load_properties(); }); connect(text, &QTextEdit::textChanged, &dialog, mutate); connect(alignment, &QComboBox::currentIndexChanged, &dialog, [mutate](int) { mutate(); }); connect(outline_width, &QSpinBox::valueChanged, &dialog, [mutate](int) { mutate(); }); connect(shadow_offset, &QSpinBox::valueChanged, &dialog, [mutate](int) { mutate(); });
    connect(add_object, &QPushButton::clicked, &dialog, [canvas, refresh_list, load_properties] { TitleTextObject object; object.id = QString("text-%1").arg(QDateTime::currentMSecsSinceEpoch()); object.text = "New Text"; object.rect = QRect(320, 280, 640, 160); canvas->objects.push_back(object); canvas->selected = canvas->objects.size() - 1; refresh_list(); load_properties(); }); connect(remove_object, &QPushButton::clicked, &dialog, [canvas, refresh_list, load_properties] { if (canvas->selected < 0 || canvas->objects.size() <= 1) return; canvas->objects.removeAt(canvas->selected); canvas->selected = std::min(canvas->selected, canvas->objects.size() - 1); refresh_list(); load_properties(); }); connect(move_up, &QPushButton::clicked, &dialog, [canvas, refresh_list] { if (canvas->selected >= 0 && canvas->selected + 1 < canvas->objects.size()) { canvas->objects.swapItemsAt(canvas->selected, canvas->selected + 1); ++canvas->selected; refresh_list(); canvas->update(); } }); connect(move_down, &QPushButton::clicked, &dialog, [canvas, refresh_list] { if (canvas->selected > 0) { canvas->objects.swapItemsAt(canvas->selected, canvas->selected - 1); --canvas->selected; refresh_list(); canvas->update(); } }); connect(center_h, &QPushButton::clicked, &dialog, [canvas] { if (canvas->selected >= 0) { auto& rect = canvas->objects[canvas->selected].rect; rect.moveLeft((1280 - rect.width()) / 2); canvas->update(); } }); connect(center_v, &QPushButton::clicked, &dialog, [canvas] { if (canvas->selected >= 0) { auto& rect = canvas->objects[canvas->selected].rect; rect.moveTop((720 - rect.height()) / 2); canvas->update(); } });
    connect(duplicate_object, &QPushButton::clicked, &dialog, [canvas, refresh_list, load_properties] { if (canvas->selected < 0 || canvas->selected >= canvas->objects.size()) return; auto object = canvas->objects[canvas->selected]; object.id = QString("text-%1").arg(QDateTime::currentMSecsSinceEpoch()); object.rect.translate(24, 24); canvas->objects.push_back(object); canvas->selected = canvas->objects.size() - 1; refresh_list(); load_properties(); });
    connect(copy_object, &QPushButton::clicked, &dialog, [canvas, object_json] { if (canvas->selected < 0 || canvas->selected >= canvas->objects.size()) return; QApplication::clipboard()->setText(QJsonDocument(object_json(canvas->objects[canvas->selected])).toJson(QJsonDocument::Compact)); });
    connect(paste_object, &QPushButton::clicked, &dialog, [canvas, object_from_json, refresh_list, load_properties] { QJsonParseError error; const auto document = QJsonDocument::fromJson(QApplication::clipboard()->text().toUtf8(), &error); if (error.error != QJsonParseError::NoError || !document.isObject() || !document.object().contains("text")) return; auto object = object_from_json(document.object()); object.id = QString("text-%1").arg(QDateTime::currentMSecsSinceEpoch()); object.rect.translate(24, 24); canvas->objects.push_back(object); canvas->selected = canvas->objects.size() - 1; refresh_list(); load_properties(); });
    connect(auto_fit, &QPushButton::clicked, &dialog, [canvas, load_properties] { if (canvas->selected < 0 || canvas->selected >= canvas->objects.size()) return; auto& object = canvas->objects[canvas->selected]; const auto original = object.font.pointSize() > 0 ? object.font.pointSize() : 32; for (int size = std::min(240, std::max(original, 8)); size >= 8; --size) { QFont candidate = object.font; candidate.setPointSize(size); const QFontMetrics metrics(candidate); const auto bounds = metrics.boundingRect(QRect(0, 0, object.rect.width(), object.rect.height()), Qt::TextWordWrap | object.alignment, object.text); if (bounds.width() <= object.rect.width() && bounds.height() <= object.rect.height()) { object.font = candidate; break; } } canvas->update(); load_properties(); });
    connect(apply_style_all, &QPushButton::clicked, &dialog, [canvas] { if (canvas->selected < 0 || canvas->selected >= canvas->objects.size()) return; const auto style = canvas->objects[canvas->selected]; for (auto& object : canvas->objects) { object.font = style.font; object.colour = style.colour; object.outline = style.outline; object.shadow = style.shadow; object.outline_width = style.outline_width; object.shadow_offset = style.shadow_offset; object.alignment = style.alignment; } canvas->update(); });
    connect(font, &QPushButton::clicked, &dialog, [canvas, &dialog] { if (canvas->selected < 0) return; bool accepted = false; const auto selected = QFontDialog::getFont(&accepted, canvas->objects[canvas->selected].font, &dialog, "Text Box Font"); if (accepted) { canvas->objects[canvas->selected].font = selected; canvas->update(); } }); const auto choose_colour = [&dialog, canvas](QColor TitleTextObject::* member, const QString& caption) { if (canvas->selected < 0) return; const auto selected = QColorDialog::getColor(canvas->objects[canvas->selected].*member, &dialog, caption); if (selected.isValid()) { canvas->objects[canvas->selected].*member = selected; canvas->update(); } }; connect(foreground, &QPushButton::clicked, &dialog, [choose_colour] { choose_colour(&TitleTextObject::colour, "Text Colour"); }); connect(outline, &QPushButton::clicked, &dialog, [choose_colour] { choose_colour(&TitleTextObject::outline, "Outline Colour"); }); connect(shadow, &QPushButton::clicked, &dialog, [choose_colour] { choose_colour(&TitleTextObject::shadow, "Shadow Colour"); }); connect(background, &QPushButton::clicked, &dialog, [&dialog, canvas] { const auto selected = QColorDialog::getColor(canvas->background, &dialog, "Background Colour"); if (selected.isValid()) { canvas->background = selected; canvas->update(); } }); connect(image, &QPushButton::clicked, &dialog, [&dialog, canvas] { const auto selected = QFileDialog::getOpenFileName(&dialog, "Title Background Image", canvas->background_image, "Images (*.png *.jpg *.jpeg *.bmp *.gif *.tif *.tiff);;All Files (*)"); if (!selected.isEmpty()) { canvas->background_image = selected; canvas->update(); } }); connect(clear_image, &QPushButton::clicked, &dialog, [canvas] { canvas->background_image.clear(); canvas->update(); }); connect(safe, &QCheckBox::toggled, &dialog, [canvas](bool checked) { canvas->safe_area = checked; canvas->update(); }); canvas->safe_area = safe->isChecked();
    const auto apply_template = [canvas, object_from_json, refresh_list, load_properties, resolution](const QJsonObject& value) { const auto values = value.value("objects").toArray(); if (values.isEmpty()) return false; canvas->objects.clear(); for (const auto& item : values) if (item.isObject()) canvas->objects.push_back(object_from_json(item.toObject())); canvas->background = QColor(value.value("background_colour").toString("#ff000000")); canvas->background_image = value.value("background_image").toString(); const auto index = resolution->findData(QSize(value.value("canvas_width").toInt(1280), value.value("canvas_height").toInt(720))); if (index >= 0) resolution->setCurrentIndex(index); canvas->selected = 0; refresh_list(); load_properties(); canvas->update(); return true; };
    if (existing.isEmpty()) { const auto stored = QJsonDocument::fromJson(QSettings("SmartCut64", "SmartCut64").value("titles/defaultTemplate").toByteArray()); if (stored.isObject()) apply_template(stored.object()); }
    connect(load_template, &QPushButton::clicked, &dialog, [this, &dialog, apply_template] { const auto path = QFileDialog::getOpenFileName(&dialog, "Load Title Template", {}, "Title Templates (*.sctitle-template.json *.json *.TT.xml *.tt.xml);;VideoReDo Title Template (*.TT.xml *.tt.xml)"); if (path.isEmpty()) return; QJsonObject title; if (path.endsWith(".tt.xml", Qt::CaseInsensitive)) { const auto imported = read_videoredo_title_template(path); if (!imported) return; title = *imported; title_duration_seconds_ = title.value("duration_seconds").toDouble(title_duration_seconds_); title_fade_in_seconds_ = title.value("fade_in_seconds").toDouble(title_fade_in_seconds_); title_fade_out_seconds_ = title.value("fade_out_seconds").toDouble(title_fade_out_seconds_); } else { QFile file(path); if (!file.open(QIODevice::ReadOnly)) return; title = QJsonDocument::fromJson(file.readAll()).object(); } apply_template(title); }); connect(save_template, &QPushButton::clicked, &dialog, [&dialog, template_json] { const auto path = QFileDialog::getSaveFileName(&dialog, "Save Title Template", {}, "SmartCut Title Template (*.sctitle-template.json);;VideoReDo Title Template (*.TT.xml)"); if (path.isEmpty()) return; const auto title = template_json(); if (path.endsWith(".tt.xml", Qt::CaseInsensitive)) { write_videoredo_title_template(path, title); return; } QSaveFile file(path); const auto bytes = QJsonDocument(title).toJson(QJsonDocument::Indented); if (file.open(QIODevice::WriteOnly) && file.write(bytes) == bytes.size()) file.commit(); }); connect(default_template, &QPushButton::clicked, &dialog, [template_json] { QSettings("SmartCut64", "SmartCut64").setValue("titles/defaultTemplate", QJsonDocument(template_json()).toJson(QJsonDocument::Compact)); }); connect(clear_default, &QPushButton::clicked, &dialog, [] { QSettings("SmartCut64", "SmartCut64").remove("titles/defaultTemplate"); });
    canvas->selected = 0; refresh_list(); load_properties(); auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); buttons->button(QDialogButtonBox::Ok)->setText(edit_index >= 0 ? "Update Title" : "Add Title"); layout->addWidget(buttons); connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
    if (dialog.exec() != QDialog::Accepted) return;
    const auto final_description = description->text().trimmed().isEmpty() ? QStringLiteral("Title") : description->text().trimmed();
    const auto canvas_size = resolution->currentData().toSize(); const auto rendered_canvas = canvas->render_image(false).scaled(canvas_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation).convertToFormat(QImage::Format_ARGB32);
    QString directory = automation_mode_ ? preview_dir_.path() + "/titles" : QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/titles"; if (!QDir().mkpath(directory)) { status_->setText("Could not create the title storage directory"); return; }
    QByteArray identity = reference.toUtf8() + QJsonDocument(template_json()).toJson(QJsonDocument::Compact) + QByteArray::number(QDateTime::currentMSecsSinceEpoch()); const auto name = QString::fromLatin1(QCryptographicHash::hash(identity, QCryptographicHash::Sha256).toHex().left(20)); pending_title_bgra_ = directory + '/' + name + ".bgra"; pending_title_output_ = directory + '/' + name + ".mkv"; pending_title_spec_ = directory + '/' + name + ".sctitle.json"; pending_title_description_ = final_description; pending_title_replace_index_ = edit_index;
    auto specification = template_json(); specification.insert("schema", "joiner-title-v2"); specification.insert("reference", QDir::toNativeSeparators(QFileInfo(reference).absoluteFilePath())); specification.insert("description", final_description); specification.insert("duration_seconds", title_duration_seconds_); specification.insert("fade_in_seconds", title_fade_in_seconds_); specification.insert("fade_out_seconds", title_fade_out_seconds_); QSaveFile spec(pending_title_spec_); if (!spec.open(QIODevice::WriteOnly) || spec.write(QJsonDocument(specification).toJson(QJsonDocument::Indented)) <= 0 || !spec.commit()) { pending_title_bgra_.clear(); pending_title_output_.clear(); pending_title_spec_.clear(); pending_title_description_.clear(); pending_title_replace_index_ = -1; status_->setText("Could not save the title project"); return; }
    QSaveFile raw(pending_title_bgra_); if (!raw.open(QIODevice::WriteOnly)) { QFile::remove(pending_title_spec_); pending_title_bgra_.clear(); pending_title_output_.clear(); pending_title_spec_.clear(); pending_title_description_.clear(); pending_title_replace_index_ = -1; status_->setText("Could not create the title image"); return; } for (int row = 0; row < rendered_canvas.height(); ++row) if (raw.write(reinterpret_cast<const char*>(rendered_canvas.constScanLine(row)), rendered_canvas.width() * 4) != rendered_canvas.width() * 4) { raw.cancelWriting(); QFile::remove(pending_title_spec_); status_->setText("Could not write the title image"); return; } if (!raw.commit()) { QFile::remove(pending_title_spec_); status_->setText("Could not finalize the title image"); return; }
    QFile::remove(pending_title_output_); status_->setText("Encoding title for the Joiner…"); title_worker_.start(worker_program(), {"--create-title", "--reference", reference, "--bgra", pending_title_bgra_, "--width", QString::number(rendered_canvas.width()), "--height", QString::number(rendered_canvas.height()), "--output", pending_title_output_, "--duration", QString::number(title_duration_seconds_, 'f', 3), "--fade-in", QString::number(title_fade_in_seconds_, 'f', 3), "--fade-out", QString::number(title_fade_out_seconds_, 'f', 3), "--description", final_description});
  }
  std::uint64_t current_program_duration_us() const { if (!timeline_ || frame_rate_num_ <= 0 || timeline_->frame_count() == 0) return 0; return static_cast<std::uint64_t>((static_cast<long double>(timeline_->frame_count()) * 1'000'000.0L * frame_rate_den_) / frame_rate_num_); }
  bool save_joiner_project(const QString& path) const {
    if (path.isEmpty() || joiner_inputs_.isEmpty()) return false;
    if (path.endsWith(".vjnr", Qt::CaseInsensitive)) {
      smartcut::core::VideoReDoJoinerProject project;
      project.items.reserve(static_cast<std::size_t>(joiner_inputs_.size()));
      for (int index = 0; index < joiner_inputs_.size(); ++index) {
        smartcut::core::VideoReDoJoinerItem item;
        const QFileInfo source(joiner_inputs_[index]);
        item.project.source_path = QDir::toNativeSeparators(source.absoluteFilePath()).toUtf8().toStdString();
        item.project.description = (index < joiner_descriptions_.size() ? joiner_descriptions_[index] : source.completeBaseName()).toUtf8().toStdString();
        item.project.duration_us = index < joiner_durations_us_.size() ? joiner_durations_us_[index] : 0;
        if (!item.project.duration_us && source.absoluteFilePath() == QFileInfo(input_->text()).absoluteFilePath()) item.project.duration_us = current_program_duration_us();
        if (item.project.duration_us) item.project.selections = {{0, item.project.duration_us}};
        item.original_duration_us = item.project.duration_us; item.cut_duration_us = item.project.duration_us;
        item.output_bytes = source.exists() ? static_cast<std::uint64_t>(source.size()) : 0;
        project.items.push_back(std::move(item));
      }
      std::string error;
      const bool written = smartcut::core::write_videoredo_joiner_project(std::filesystem::path(path.toStdWString()), project, &error);
      if (!written && status_) status_->setText(QString("Could not save VideoReDo Joiner project: %1").arg(QString::fromStdString(error)));
      return written;
    }
    QJsonArray items;
    for (int index = 0; index < joiner_inputs_.size(); ++index) {
      QJsonObject item{{"path", joiner_inputs_[index]}, {"description", index < joiner_descriptions_.size() ? joiner_descriptions_[index] : QFileInfo(joiner_inputs_[index]).completeBaseName()}, {"duration_us", static_cast<qint64>(index < joiner_durations_us_.size() ? joiner_durations_us_[index] : 0)}};
      if (index < joiner_title_specs_.size() && !joiner_title_specs_[index].isEmpty()) { item.insert("type", "title"); item.insert("title_project", joiner_title_specs_[index]); } else item.insert("type", "video");
      items.push_back(item);
    }
    QSaveFile file(path); if (!file.open(QIODevice::WriteOnly)) return false;
    return file.write(QJsonDocument(QJsonObject{{"schema", "joiner-project-v1"}, {"items", items}}).toJson(QJsonDocument::Indented)) > 0 && file.commit();
  }
  bool load_joiner_project(const QString& path) {
    QStringList inputs; QStringList descriptions; QStringList title_specs; QVector<qulonglong> durations;
    if (path.endsWith(".vjnr", Qt::CaseInsensitive)) {
      std::string error; const auto project = smartcut::core::read_videoredo_joiner_project(std::filesystem::path(path.toStdWString()), &error);
      if (!project) { status_->setText(QString("Invalid VideoReDo Joiner project: %1").arg(QString::fromStdString(error))); return false; }
      for (const auto& item : project->items) {
        const auto item_path = QString::fromUtf8(item.project.source_path);
        if (item_path.isEmpty() || !QFileInfo::exists(item_path)) { status_->setText(QString("Joiner project item is unavailable: %1").arg(item_path)); return false; }
        inputs.push_back(QDir::toNativeSeparators(QFileInfo(item_path).absoluteFilePath()));
        const auto description = QString::fromUtf8(item.project.description);
        descriptions.push_back(description.isEmpty() ? QFileInfo(item_path).completeBaseName() : description);
        title_specs.push_back({}); durations.push_back(item.cut_duration_us ? item.cut_duration_us : item.project.duration_us);
      }
    } else {
      QFile file(path); if (!file.open(QIODevice::ReadOnly)) { status_->setText("Could not open Joiner project"); return false; }
      const auto document = QJsonDocument::fromJson(file.readAll());
      if (!document.isObject() || document.object().value("schema").toString() != "joiner-project-v1") { status_->setText("Invalid Joiner project document"); return false; }
      for (const auto& value : document.object().value("items").toArray()) {
        const auto object = value.toObject(); const auto item_path = object.value("path").toString();
        if (item_path.isEmpty() || !QFileInfo::exists(item_path)) { status_->setText(QString("Joiner project item is unavailable: %1").arg(item_path)); return false; }
        const auto title_spec = object.value("title_project").toString();
        if (!title_spec.isEmpty() && !QFileInfo::exists(title_spec)) { status_->setText(QString("Joiner title project is unavailable: %1").arg(title_spec)); return false; }
        inputs.push_back(item_path); const auto description = object.value("description").toString(); descriptions.push_back(description.isEmpty() ? QFileInfo(item_path).completeBaseName() : description); title_specs.push_back(title_spec); durations.push_back(static_cast<qulonglong>(object.value("duration_us").toInteger()));
      }
    }
    joiner_inputs_ = std::move(inputs); joiner_descriptions_ = std::move(descriptions); joiner_title_specs_ = std::move(title_specs); joiner_durations_us_ = std::move(durations);
    status_->setText(QString("Joiner project loaded (%1 items)").arg(joiner_inputs_.size())); return true;
  }
  void open_joiner_item(int index) {
    if (index < 0 || index >= joiner_inputs_.size()) { status_->setText("Select a Joiner item to edit"); return; }
    if (index < joiner_title_specs_.size() && !joiner_title_specs_[index].isEmpty()) { add_title_to_joiner(index); return; }
    const auto path = joiner_inputs_[index];
    if (!QFileInfo::exists(path)) { status_->setText(QString("Joiner item is unavailable: %1").arg(path)); return; }
    request_load_source(path);
    if (QFileInfo(input_->text()).absoluteFilePath() == QFileInfo(path).absoluteFilePath()) status_->setText(QString("Joiner item opened for editing: %1").arg(index < joiner_descriptions_.size() ? joiner_descriptions_[index] : QFileInfo(path).completeBaseName()));
  }
  void edit_joiner_list() {
    QDialog dialog(this); dialog.setWindowTitle("Joiner List"); dialog.resize(720, 420);
    auto* layout = new QVBoxLayout(&dialog); auto* list = new JoinerListWidget(&dialog); for (int index = 0; index < joiner_inputs_.size(); ++index) { auto* item = new QListWidgetItem(index < joiner_descriptions_.size() ? joiner_descriptions_[index] : QFileInfo(joiner_inputs_[index]).completeBaseName(), list); item->setData(Qt::UserRole, joiner_inputs_[index]); item->setData(Qt::UserRole + 1, index < joiner_title_specs_.size() ? joiner_title_specs_[index] : QString{}); item->setData(Qt::UserRole + 2, static_cast<qulonglong>(index < joiner_durations_us_.size() ? joiner_durations_us_[index] : 0)); item->setIcon(style()->standardIcon(item->data(Qt::UserRole + 1).toString().isEmpty() ? QStyle::SP_FileIcon : QStyle::SP_FileDialogDetailedView)); item->setToolTip(joiner_inputs_[index]); item->setFlags(item->flags() | Qt::ItemIsEditable); } layout->addWidget(list, 1);
    auto* hint = new QLabel("Drag video files here to add them in filename order. Double-click a description to rename an item; its source path is shown as a tooltip.", &dialog); hint->setWordWrap(true); layout->addWidget(hint);
    const auto append = [list](const QStringList& files) { for (const auto& path : files) { auto* item = new QListWidgetItem(QFileInfo(path).completeBaseName(), list); item->setData(Qt::UserRole, QDir::toNativeSeparators(QFileInfo(path).absoluteFilePath())); item->setData(Qt::UserRole + 1, QString{}); item->setData(Qt::UserRole + 2, static_cast<qulonglong>(0)); item->setToolTip(item->data(Qt::UserRole).toString()); item->setFlags(item->flags() | Qt::ItemIsEditable); } };
    list->on_files_dropped = append;
    auto* controls = new QHBoxLayout; auto* add = new QPushButton("Add Files…", &dialog); auto* add_title = new QPushButton("Add Title…", &dialog); auto* edit = new QPushButton("Edit Selection", &dialog); auto* load = new QPushButton("Load Project…", &dialog); auto* save = new QPushButton("Save Project…", &dialog); auto* up = new QPushButton("Move Up", &dialog); auto* down = new QPushButton("Move Down", &dialog); auto* remove = new QPushButton("Remove", &dialog); auto* clear = new QPushButton("Clear", &dialog);
    controls->addWidget(add); controls->addWidget(add_title); controls->addWidget(edit); controls->addWidget(load); controls->addWidget(save); controls->addWidget(up); controls->addWidget(down); controls->addWidget(remove); controls->addWidget(clear); controls->addStretch(); layout->addLayout(controls);
    int edit_row = -1; bool add_title_requested = false;
    const auto synchronize = [this, list] { joiner_inputs_.clear(); joiner_descriptions_.clear(); joiner_title_specs_.clear(); joiner_durations_us_.clear(); for (int row = 0; row < list->count(); ++row) { joiner_inputs_.push_back(list->item(row)->data(Qt::UserRole).toString()); joiner_descriptions_.push_back(list->item(row)->text()); joiner_title_specs_.push_back(list->item(row)->data(Qt::UserRole + 1).toString()); joiner_durations_us_.push_back(list->item(row)->data(Qt::UserRole + 2).toULongLong()); } };
    connect(add, &QPushButton::clicked, &dialog, [append, &dialog] { auto files = QFileDialog::getOpenFileNames(&dialog, "Add videos to Joiner", {}, "Video Files (*.mkv *.mp4 *.ts *.m2ts *.mpg *.mpeg *.avi *.webm);;All Files (*)"); files.sort(Qt::CaseInsensitive); append(files); });
    connect(add_title, &QPushButton::clicked, &dialog, [synchronize, &dialog, &add_title_requested] { synchronize(); add_title_requested = true; dialog.accept(); });
    connect(load, &QPushButton::clicked, &dialog, [this, list, &dialog] { const auto path = QFileDialog::getOpenFileName(&dialog, "Load Joiner Project", {}, "Joiner Projects (*.scjoin.json *.VJnr);;VideoReDo Joiner Project (*.VJnr);;SmartCut Joiner Project (*.scjoin.json *.json)"); if (path.isEmpty() || !load_joiner_project(path)) return; list->clear(); for (int index = 0; index < joiner_inputs_.size(); ++index) { auto* item = new QListWidgetItem(joiner_descriptions_[index], list); item->setData(Qt::UserRole, joiner_inputs_[index]); item->setData(Qt::UserRole + 1, index < joiner_title_specs_.size() ? joiner_title_specs_[index] : QString{}); item->setData(Qt::UserRole + 2, static_cast<qulonglong>(index < joiner_durations_us_.size() ? joiner_durations_us_[index] : 0)); item->setIcon(style()->standardIcon(item->data(Qt::UserRole + 1).toString().isEmpty() ? QStyle::SP_FileIcon : QStyle::SP_FileDialogDetailedView)); item->setToolTip(joiner_inputs_[index]); item->setFlags(item->flags() | Qt::ItemIsEditable); } });
    connect(edit, &QPushButton::clicked, &dialog, [list, synchronize, &dialog, &edit_row] { const int row = list->currentRow(); if (row < 0) return; synchronize(); edit_row = row; dialog.accept(); });
    connect(save, &QPushButton::clicked, &dialog, [this, synchronize, &dialog] { const auto path = QFileDialog::getSaveFileName(&dialog, "Save Joiner Project", {}, "SmartCut Joiner Project (*.scjoin.json);;VideoReDo Joiner Project (*.VJnr)"); if (path.isEmpty()) return; synchronize(); save_joiner_project(path); });
    connect(remove, &QPushButton::clicked, &dialog, [list] { delete list->takeItem(list->currentRow()); });
    connect(clear, &QPushButton::clicked, list, &QListWidget::clear);
    connect(up, &QPushButton::clicked, &dialog, [list] { const int row = list->currentRow(); if (row > 0) { auto* item = list->takeItem(row); list->insertItem(row - 1, item); list->setCurrentRow(row - 1); } });
    connect(down, &QPushButton::clicked, &dialog, [list] { const int row = list->currentRow(); if (row >= 0 && row + 1 < list->count()) { auto* item = list->takeItem(row); list->insertItem(row + 1, item); list->setCurrentRow(row + 1); } });
    auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); layout->addWidget(buttons); connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
    if (dialog.exec() != QDialog::Accepted) return;
    synchronize();
    if (add_title_requested) { QTimer::singleShot(0, this, [this] { add_title_to_joiner(); }); return; }
    if (edit_row >= 0) { open_joiner_item(edit_row); return; }
    status_->setText(QString("Joiner list updated (%1 items)").arg(joiner_inputs_.size()));
  }
  void create_joiner_video() {
    if (!require_paid_delivery_tool("Create Video From Joiner List")) return;
    if (join_worker_.state() != QProcess::NotRunning) { status_->setText("Joiner is already running"); return; }
    if (joiner_inputs_.isEmpty() && !input_->text().isEmpty()) add_joiner_files({input_->text()}, false);
    if (joiner_inputs_.isEmpty()) { status_->setText("Add at least one video to the Joiner"); return; }
    for (const auto& path : joiner_inputs_) if (!QFileInfo::exists(path)) { status_->setText(QString("Joiner item is unavailable: %1").arg(path)); return; }
    const auto output = QFileDialog::getSaveFileName(this, "Save Joined Video", {}, "Supported Video (*.mkv *.mp4 *.mov *.mpg *.mpeg *.vob *.ts *.m2ts *.wtv *.dvr-ms *.rec *.mxf);;Matroska Video (*.mkv);;MPEG-4 Video (*.mp4);;QuickTime Video (*.mov);;MPEG Program Stream (*.mpg);;MPEG Transport Stream (*.ts *.m2ts);;Windows Recorded TV (*.wtv *.dvr-ms);;Topfield Recording (*.rec);;Material Exchange Format (*.mxf)");
    if (output.isEmpty()) return;
    joiner_list_path_ = preview_dir_.path() + "/joiner-list.txt";
    QFile list(joiner_list_path_); if (!list.open(QIODevice::WriteOnly | QIODevice::Truncate)) { status_->setText("Could not create Joiner work list"); return; }
    for (const auto& path : joiner_inputs_) { list.write(path.toUtf8()); list.write("\n"); } list.close();
    joiner_output_path_ = output; status_->setText(QString("Joining %1 videos without re-encoding…").arg(joiner_inputs_.size()));
    join_worker_.start(worker_program(), {"--join-media", "--list", joiner_list_path_, "--output", joiner_output_path_});
  }
  void show_go_to_timecode() {
    if (timeline_->frame_count() == 0) { status_->setText("Open a source before entering a timecode"); return; }
    QDialog dialog(this); dialog.setWindowTitle("Go To Timecode"); auto* layout = new QVBoxLayout(&dialog); auto* form = new QFormLayout; auto* value = new QLineEdit(frame_timecode(timeline_->current_frame()), &dialog); value->selectAll(); form->addRow("Timecode (HH:MM:SS.ff):", value); layout->addLayout(form); auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); layout->addWidget(buttons); connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); if (dialog.exec() != QDialog::Accepted) return;
    const auto fields = value->text().trimmed().split(':'); if (fields.size() != 3) { status_->setText("Invalid timecode; use HH:MM:SS.ff"); return; } bool hours_ok = false, minutes_ok = false, seconds_ok = false, frames_ok = true; const auto hours = fields[0].toULongLong(&hours_ok); const auto minutes = fields[1].toUInt(&minutes_ok); const auto second_fields = fields[2].split('.'); const auto seconds = second_fields[0].toUInt(&seconds_ok); unsigned frames = 0; if (second_fields.size() > 1) frames = second_fields[1].toUInt(&frames_ok); if (!hours_ok || !minutes_ok || !seconds_ok || !frames_ok || minutes > 59 || seconds > 59 || frames >= static_cast<unsigned>(std::max(frame_rate_num_ / std::max(frame_rate_den_, 1), 1))) { status_->setText("Invalid timecode; use HH:MM:SS.ff"); return; } const auto whole_seconds = hours * 3600ULL + static_cast<qulonglong>(minutes) * 60ULL + seconds; const auto frame_us = static_cast<std::uint64_t>(frames) * 1000000ULL * static_cast<std::uint64_t>(std::max(frame_rate_den_, 1)) / static_cast<std::uint64_t>(std::max(frame_rate_num_, 1)); timeline_->set_time_us(whole_seconds * 1000000ULL + frame_us);
  }
  void delete_current_frame() { if (timeline_->frame_count() == 0) return; remember_edit(); const auto frame = timeline_->current_frame(); auto cuts = cut_ranges_; cuts.emplace_back(frame, std::min(frame + 1, timeline_->frame_count())); set_cut_ranges(std::move(cuts)); if (frame + 1 < timeline_->frame_count()) timeline_->set_frame(frame + 1); status_->setText(QString(scene_mode_ ? "Frame %1 removed from the scene list" : "Frame %1 added to the cut list").arg(static_cast<qulonglong>(frame))); }
  void close_source() { if (!confirm_abandon_changes("close the source")) return; close_source_now(); }
  void close_source_now() { ++control_request_generation_; control_navigation_pending_ = false; invalidate_playback_request(); audio_pause_requested_ = true; request_audio_stop(); request_video_stop(); playback_timer_.stop(); set_preview_mode(false); source_probe_.kill(); metadata_probe_.kill(); preview_probe_.kill(); thumbnail_probe_.kill(); waveform_probe_.kill(); restoring_project_ = true; input_->clear(); output_->clear(); project_path_.clear(); source_uuid_.clear(); index_fingerprint_.clear(); source_streams_ = {}; source_metadata_ = {}; selected_streams_.clear(); active_thumbnail_frames_.clear(); timeline_->set_navigation(0, {}, {}, 1, 1); set_cut_ranges({}); restoring_project_ = false; set_project_modified(false); preview_->clear(); current_preview_image_ = {}; preview_->setText("Open a video to begin"); status_->setText("Source closed"); show_startup_screen(); }
  void add_current_to_dvd() { status_->setText("DVD authoring is deferred in this build"); }
  void create_dvd() { status_->setText("DVD authoring is deferred in this build"); }
  void handle_navigation_output(const QByteArray& bytes, int code) {
    control_navigation_pending_ = false; indexing_status_timer_.stop(); progress_->setRange(0, 100); progress_->setValue(0); const auto report = parse_navigation_report(bytes); if (code != 0 || !report) { restoring_project_ = false; status_->setText("Source navigation probe failed"); return; }
    source_uuid_ = report->source_uuid; QString seed = report->source_uuid + ':' + QString::number(static_cast<qulonglong>(report->frame_count)); for (const auto frame : report->keyframes) seed += ':' + QString::number(static_cast<qulonglong>(frame)); index_fingerprint_ = QString::number(qHash(seed), 16); timeline_->set_navigation(report->frame_count, report->keyframes, report->frame_pts, report->time_base_num, report->time_base_den); apply_pending_chapters(); fine_tune_->setEnabled(report->frame_count > 0); program_end_->setText(frame_timecode(report->frame_count)); selection_start_->setText("00:00:00.00"); frame_readout_->setText("0"); frame_guide_->set_position(0, report->frame_count);
    if (pending_project_kept_) { auto kept = *pending_project_kept_; std::sort(kept.begin(), kept.end()); std::vector<FrameRange> cuts; std::size_t cursor = 0; for (const auto& range : kept) { const auto first = std::min(range.first, report->frame_count); const auto end = std::min(range.second, report->frame_count); if (cursor < first) cuts.emplace_back(cursor, first); cursor = std::max(cursor, end); } if (cursor < report->frame_count) cuts.emplace_back(cursor, report->frame_count); set_cut_ranges(std::move(cuts)); pending_project_kept_.reset(); } else set_cut_ranges(scene_mode_ ? std::vector<FrameRange>{{0, report->frame_count}} : std::vector<FrameRange>{});
    if (pending_project_chapters_) { timeline_->set_chapter_markers(*pending_project_chapters_); pending_project_chapters_.reset(); project_chapters_loaded_ = true; } restoring_project_ = false; undo_states_.clear(); redo_states_.clear(); const bool recovered_snapshot = journal_recovery_applied_; const auto loaded = recovered_snapshot ? "Recovered latest journal snapshot; loaded" : "Loaded"; journal_recovery_applied_ = false; set_project_modified(pending_modified_after_load_.value_or(recovered_snapshot)); pending_modified_after_load_.reset(); status_->setText(QString("%1 %2 frames, %3 keyframes, %4 chapters").arg(loaded).arg(static_cast<qulonglong>(report->frame_count)).arg(static_cast<qulonglong>(report->keyframes.size())).arg(static_cast<qulonglong>(timeline_->chapter_markers().size()))); if (report->frame_count > 0) request_preview(0);
  }
  void ensure_control_worker() {
    if (control_pipe_.isEmpty()) {
      control_pipe_ = QString("SmartCutControl-%1-%2")
                          .arg(QCoreApplication::applicationPid())
                          .arg(QUuid::createUuid().toString(QUuid::Id128));
    }
    if (control_worker_.state() == QProcess::NotRunning) control_worker_.start(worker_program(), {"--control-server", "--pipe", control_pipe_});
  }
  void request_navigation_control(const QString& path) {
    for (auto iterator = control_threads_.begin(); iterator != control_threads_.end();) {
      if (!iterator->finished->load(std::memory_order_acquire)) { ++iterator; continue; }
      if (iterator->thread.joinable()) iterator->thread.join();
      iterator = control_threads_.erase(iterator);
    }
    ensure_control_worker(); control_navigation_pending_ = true; const auto generation = ++control_request_generation_; const bool force_failure = std::exchange(control_force_failure_, false); const auto pipe = (force_failure ? control_pipe_ + "-missing" : control_pipe_).toStdString(); const unsigned timeout = force_failure ? 100U : 5000U; QJsonObject object{{"schema", "control-request-v1"}, {"id", static_cast<qint64>(generation)}, {"command", "navigation-info"}, {"input", path}}; const auto request = QJsonDocument(object).toJson(QJsonDocument::Compact).toStdString(); QPointer<ExportWindow> window(this);
    auto finished = std::make_shared<std::atomic_bool>(false);
    ControlThreadEntry entry;
    entry.finished = finished;
    entry.thread = std::jthread([window, pipe, request, generation, path, timeout, finished] {
      const auto response = smartcut::core::request_control_pipe(pipe, request, timeout);
      QMetaObject::invokeMethod(qApp, [window, response, generation, path] { if (!window || window->shutting_down_ || generation != window->control_request_generation_ || path != window->input_->text()) return; if (response) { const auto document = QJsonDocument::fromJson(QByteArray::fromStdString(*response)); const auto root = document.object(); if (root.value("ok").toBool() && root.value("payload").isObject()) { ++window->control_navigation_successes_; window->handle_navigation_output(QJsonDocument(root.value("payload").toObject()).toJson(QJsonDocument::Compact), 0); return; } } ++window->control_navigation_fallbacks_; window->source_probe_.start(worker_program(), {"--navigation-info", "--input", path}); }, Qt::QueuedConnection);
      finished->store(true, std::memory_order_release);
    });
    control_threads_.push_back(std::move(entry));
  }
  void load_source(const QString& path) {
    show_editor_workspace();
    if (!restoring_project_) { project_path_.clear(); pending_project_kept_.reset(); pending_project_streams_.reset(); pending_project_chapters_.reset(); audio_offset_ms_ = 0; set_project_modified(false); }
    project_chapters_loaded_ = false;
    refresh_window_title();
    invalidate_playback_request(); audio_pause_requested_ = true; request_audio_stop(); request_video_stop(); playback_timer_.stop(); set_preview_mode(false); thumbnail_refresh_timer_.stop();
    if (ad_scan_.state() != QProcess::NotRunning) { ad_scan_cancel_requested_ = true; ad_scan_.kill(); }
    waveform_probe_.kill(); pending_waveform_window_.reset(); frame_guide_->clear_waveform(); pending_chapter_times_us_.clear(); source_streams_ = {}; source_metadata_ = {}; selected_streams_.clear();
    fine_tune_->setEnabled(false); ++preview_generation_; pending_preview_frame_.reset(); pending_thumbnail_center_.reset(); active_thumbnail_frames_.clear();
    active_thumbnail_slots_.clear(); active_thumbnail_request_slots_.clear();
    source_uuid_.clear(); index_fingerprint_.clear(); display_aspect_ratio_ = 0.0; frame_rate_num_ = 25; frame_rate_den_ = 1; current_preview_image_ = {};
    set_cut_ranges({}); undo_states_.clear(); redo_states_.clear(); for (auto* thumbnail : thumbnail_widgets_) thumbnail->set_display_aspect_ratio(0.0);
    QSettings settings("SmartCut64", "SmartCut64"); auto recent = settings.value("recentFiles").toStringList(); recent.removeAll(path); recent.prepend(path); while (recent.size() > 10) recent.removeLast(); settings.setValue("recentFiles", recent);
    status_->setText("Indexing source navigation in the background…"); indexing_elapsed_seconds_ = 0; progress_->setRange(0, 0); indexing_status_timer_.start(1000);
    request_navigation_control(path); identity_probe_.start(worker_program(), {"--source-identity", "--input", path}); metadata_probe_.start(worker_program(), {"--probe", "--input", path});
  }
  static QString json_value(const QString& text, const QString& key) { const auto marker = QStringLiteral("\"") + key + QStringLiteral("\":"); const auto start = text.indexOf(marker); if (start < 0) return {}; return text.mid(start + marker.size()).split(',').first().split('}').first().trimmed(); }
  static QString clean_json_value(QString value) { if (value.size() >= 2 && value.front() == '"' && value.back() == '"') return value.mid(1, value.size() - 2); return value; }
  void set_preview_rotation(int degrees) { preview_rotation_degrees_ = QList<int>{-1, 0, 90, 180, 270}.contains(degrees) ? degrees : -1; QSettings("SmartCut64", "SmartCut64").setValue("display/rotationDegrees", preview_rotation_degrees_); if (!current_preview_image_.isNull()) show_preview_image(current_preview_image_, displayed_preview_frame_); status_->setText(preview_rotation_degrees_ < 0 ? "Video rotation follows source metadata" : QString("Video display rotated %1 degrees").arg(preview_rotation_degrees_)); }
  void show_preview_image(const QPixmap& image, std::size_t frame) {
    if (image.isNull()) return;
    current_preview_image_ = image;
    auto display = image;
    if (display_aspect_ratio_ > 0.0 && image.height() > 0)
      display = image.scaled(std::max(1, static_cast<int>(static_cast<double>(image.height()) * display_aspect_ratio_ + 0.5)), image.height(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
    const int rotation = preview_rotation_degrees_ < 0 ? 0 : preview_rotation_degrees_;
    if (rotation != 0) { QTransform transform; transform.rotate(rotation); display = display.transformed(transform, Qt::SmoothTransformation); }
    const auto scaled = display.scaled(preview_->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
    const auto adjusted = adjusted_display_image(scaled.toImage(), display_brightness_, display_contrast_);
    auto presented = plot_masked_image(adjusted, plot_mask_visible_, plot_mask_size_);
    if (osd_visible_) {
      const auto display_frame = frame_number_mode_ == "output" && timeline_ ? timeline_->output_frame(frame) : frame;
      QString label = frame_timecode(frame);
      if (frame_number_mode_ != "none") label += QString("   %1 frame %2").arg(frame_number_mode_ == "output" ? "Output" : "Source").arg(static_cast<qulonglong>(display_frame));
      QPainter painter(&presented); painter.setRenderHint(QPainter::TextAntialiasing); const QFontMetrics metrics(painter.font()); const QRect text_rect(14, 12, metrics.horizontalAdvance(label) + 18, metrics.height() + 10); painter.fillRect(text_rect, QColor(0, 0, 0, 175)); painter.setPen(Qt::white); painter.drawText(text_rect.adjusted(9, 5, -9, -5), Qt::AlignLeft | Qt::AlignVCenter, label);
    }
    preview_->setPixmap(QPixmap::fromImage(presented)); displayed_preview_frame_ = frame;
  }
  QString preview_cache_key(std::size_t frame) const { return source_uuid_ + ':' + index_fingerprint_ + ':' + QString::number(static_cast<qulonglong>(frame)); }
  void start_pending_preview() { if (preview_probe_.state() != QProcess::NotRunning || !pending_preview_frame_) return; active_preview_frame_ = *pending_preview_frame_; pending_preview_frame_.reset(); active_preview_generation_ = preview_generation_; active_preview_key_ = preview_cache_key(active_preview_frame_); active_preview_path_ = preview_dir_.path() + QString("/frame-%1-%2-%3.png").arg(index_fingerprint_).arg(static_cast<qulonglong>(active_preview_frame_)).arg(static_cast<qulonglong>(preview_generation_)); active_preview_mapping_ = QString("SmartCutPreview-%1-%2-%3").arg(QCoreApplication::applicationPid()).arg(static_cast<qulonglong>(preview_generation_)).arg(static_cast<qulonglong>(active_preview_frame_)); active_preview_received_ = false; preview_shared_output_.clear(); QStringList args{"--preview-shared", "--input", input_->text(), "--frame", QString::number(static_cast<qulonglong>(active_preview_frame_)), "--mapping", active_preview_mapping_}; if (const auto pts = timeline_->frame_pts(active_preview_frame_)) args << "--pts" << QString::number(*pts); preview_probe_.start(worker_program(), args); }
  static int adaptive_thumbnail_count(int available_width, int thumbnail_height,
                                      double display_aspect_ratio, int maximum) {
    if (available_width <= 0 || maximum <= 0) return std::max(maximum, 0);
    const auto ratio = std::isfinite(display_aspect_ratio) && display_aspect_ratio > 0.0
                           ? display_aspect_ratio : 16.0 / 9.0;
    const auto target_width = std::max(1, static_cast<int>(
        std::ceil(static_cast<double>(std::max(thumbnail_height, 1)) * ratio)));
    const auto minimum = std::min(3, maximum);
    auto count = std::clamp((available_width + 1) / (target_width + 1), minimum, maximum);
    if (count > 1 && count % 2 == 0) --count;
    return count;
  }
  void update_thumbnail_widget_count() {
    if (!thumbnail_panel_ || thumbnail_widgets_.isEmpty()) return;
    const auto count = adaptive_thumbnail_count(thumbnail_panel_->width(),
                                                thumbnail_height_, display_aspect_ratio_,
                                                static_cast<int>(thumbnail_widgets_.size()));
    if (count == visible_thumbnail_count_) return;
    visible_thumbnail_count_ = count;
    for (qsizetype slot = 0; slot < thumbnail_widgets_.size(); ++slot) {
      const bool visible = slot < visible_thumbnail_count_;
      thumbnail_widgets_[slot]->setVisible(visible);
      if (!visible) thumbnail_widgets_[slot]->clear_frame();
    }
    active_thumbnail_frames_.clear();
    active_thumbnail_slots_.clear(); active_thumbnail_request_slots_.clear();
    if (timeline_ && timeline_->frame_count() > 0 && thumbnail_panel_->isVisible())
      schedule_thumbnail_strip(timeline_->current_frame());
  }
  void schedule_thumbnail_strip(std::size_t center) {
    pending_thumbnail_center_ = center;
    const auto visible = std::clamp<qsizetype>(visible_thumbnail_count_, 1, thumbnail_widgets_.size());
    for (qsizetype slot = 0; slot < visible && slot < active_thumbnail_slots_.size(); ++slot)
      thumbnail_widgets_[slot]->set_selected(active_thumbnail_slots_[slot] && *active_thumbnail_slots_[slot] == center);
    thumbnail_refresh_timer_.start(120);
  }
  void refresh_thumbnail_cut_state() {
    for (qsizetype slot = 0; slot < thumbnail_widgets_.size(); ++slot) {
      const bool cut = slot < active_thumbnail_slots_.size() && active_thumbnail_slots_[slot] &&
                       timeline_->frame_is_cut(*active_thumbnail_slots_[slot]);
      thumbnail_widgets_[slot]->set_cut(cut);
    }
  }
  static QVector<std::optional<std::size_t>> thumbnail_frame_window(std::size_t center, std::size_t count, std::size_t slot_count, std::size_t stride) {
    QVector<std::optional<std::size_t>> frames(static_cast<qsizetype>(slot_count));
    if (count == 0 || slot_count == 0) return frames;
    center = std::min(center, count - 1);
    stride = std::max<std::size_t>(stride, 1);
    const auto middle = slot_count / 2;
    for (std::size_t slot = 0; slot < slot_count; ++slot) {
      if (slot <= middle) {
        const auto distance = (middle - slot) * stride;
        if (distance <= center) frames[static_cast<qsizetype>(slot)] = center - distance;
      } else {
        const auto distance = (slot - middle) * stride;
        if (distance < count - center) frames[static_cast<qsizetype>(slot)] = center + distance;
      }
    }
    return frames;
  }
  static QVector<std::optional<std::size_t>> preview_thumbnail_frame_window(const TimelineWidget& timeline, std::size_t center, std::size_t slot_count, std::size_t stride) {
    const auto output_count = timeline.output_frame_count();
    const auto output_center = output_count == 0 ? 0 : std::min(timeline.output_frame(center), output_count - 1);
    const auto outputs = thumbnail_frame_window(output_center, output_count, slot_count, stride);
    QVector<std::optional<std::size_t>> frames(outputs.size());
    for (qsizetype slot = 0; slot < outputs.size(); ++slot)
      if (outputs[slot]) frames[slot] = timeline.source_frame_for_output_frame(*outputs[slot]);
    return frames;
  }
  void start_pending_thumbnail_strip() {
    if (thumbnail_probe_.state() != QProcess::NotRunning || !pending_thumbnail_center_ || timeline_->frame_count() == 0 || thumbnail_panel_->isHidden()) return;
    active_thumbnail_center_ = *pending_thumbnail_center_; pending_thumbnail_center_.reset(); active_thumbnail_generation_ = preview_generation_;
    const auto slot_count = static_cast<std::size_t>(std::clamp<qsizetype>(visible_thumbnail_count_, 1, thumbnail_widgets_.size()));
    const auto stride = thumbnail_interval_seconds_ <= 0.0 ? std::size_t{1} : std::max<std::size_t>(1, static_cast<std::size_t>(std::llround(thumbnail_interval_seconds_ * static_cast<double>(std::max(frame_rate_num_, 1)) / static_cast<double>(std::max(frame_rate_den_, 1)))));
    active_thumbnail_slots_ = timeline_->preview_mode() ? preview_thumbnail_frame_window(*timeline_, active_thumbnail_center_, slot_count, stride) : thumbnail_frame_window(active_thumbnail_center_, timeline_->frame_count(), slot_count, stride);
    if (active_thumbnail_slots_.size() != static_cast<qsizetype>(slot_count)) return;
    frame_guide_->set_frame_window(active_thumbnail_slots_); refresh_thumbnail_cut_state(); active_thumbnail_request_slots_.clear();
    QStringList specifications; active_thumbnail_frames_.clear();
    for (qsizetype slot = 0; slot < active_thumbnail_slots_.size(); ++slot) {
      const auto frame = active_thumbnail_slots_[slot];
      if (!frame) { thumbnail_widgets_[slot]->set_placeholder(); continue; }
      thumbnail_widgets_[slot]->clear_frame();
      const auto pts = timeline_->frame_pts(*frame); if (!pts) return;
      active_thumbnail_request_slots_.push_back(slot);
      active_thumbnail_frames_.push_back(*frame); specifications.push_back(QString("%1:%2").arg(static_cast<qulonglong>(*frame)).arg(*pts));
    }
    if (active_thumbnail_frames_.isEmpty()) return;
    const auto waveform_start = timeline_->frame_time_us(active_thumbnail_frames_.front()); const auto waveform_last = timeline_->frame_time_us(active_thumbnail_frames_.back());
    const bool contiguous_source_window = active_thumbnail_frames_.size() <= 1 || active_thumbnail_frames_.back() - active_thumbnail_frames_.front() == (active_thumbnail_frames_.size() - 1) * stride;
    if (thumbnail_audio_visible_ && waveform_start && waveform_last && (!timeline_->preview_mode() || contiguous_source_window)) { const auto frame_duration = static_cast<std::uint64_t>(1000000ULL * static_cast<std::uint64_t>(std::max(frame_rate_den_, 1)) / static_cast<std::uint64_t>(std::max(frame_rate_num_, 1))); request_waveform(*waveform_start, std::max(*waveform_start + 1, *waveform_last + frame_duration)); }
    else if (timeline_->preview_mode() && !contiguous_source_window) frame_guide_->clear_waveform();
    active_thumbnail_directory_ = preview_dir_.path() + QString("/thumbs-%1-%2").arg(static_cast<qulonglong>(preview_generation_)).arg(static_cast<qulonglong>(active_thumbnail_center_));
    thumbnail_probe_.start(worker_program(), {"--thumbnail-strip-pts", "--input", input_->text(), "--frames", specifications.join(','), "--output-dir", active_thumbnail_directory_});
  }
  void request_waveform(std::uint64_t start_us, std::uint64_t end_us) { pending_waveform_window_ = std::pair{start_us, end_us}; if (waveform_probe_.state() != QProcess::NotRunning) return; start_pending_waveform(); }
  void start_pending_waveform() { if (waveform_probe_.state() != QProcess::NotRunning || !pending_waveform_window_ || input_->text().isEmpty()) return; const auto window = *pending_waveform_window_; pending_waveform_window_.reset(); active_waveform_generation_ = preview_generation_; waveform_probe_.start(worker_program(), {"--audio-waveform", "--input", input_->text(), "--start-us", QString::number(static_cast<qulonglong>(window.first)), "--end-us", QString::number(static_cast<qulonglong>(window.second)), "--buckets", QString::number(std::max(frame_guide_->width(), 256))}); }
  void request_preview(std::size_t frame) { if (input_->text().isEmpty() || source_uuid_.isEmpty() || index_fingerprint_.isEmpty()) return; current_preview_frame_ = frame; if (video_playback_requested_) return; schedule_thumbnail_strip(frame); if (sync_probe_.state() == QProcess::NotRunning) { QStringList sync_args{"--preview-sync", "--input", input_->text(), "--frame", QString::number(static_cast<qulonglong>(frame))}; if (const auto pts = timeline_->frame_pts(frame)) sync_args << "--pts" << QString::number(*pts); sync_probe_.start(worker_program(), sync_args); } const auto key = preview_cache_key(frame); const auto cached = preview_cache_.value(key); if (!cached.isEmpty() && QFileInfo::exists(cached)) { pending_preview_frame_.reset(); show_preview_image(QPixmap(cached), frame); inspector_->setText(inspector_->toPlainText().section("\nCache:", 0, 0) + QString("\nCache: %1/8 frames\nPreview: cached frame %2").arg(preview_cache_.size()).arg(static_cast<qulonglong>(frame))); return; } pending_preview_frame_ = frame; start_pending_preview(); }
  int export_scene_count() const {
    const auto count = timeline_ ? timeline_->frame_count() : 0;
    if (count == 0) return 1;
    int scenes = 0;
    std::size_t cursor = 0;
    for (const auto& range : cut_ranges_) {
      const auto first = std::min(range.first, count);
      const auto end = std::min(range.second, count);
      if (cursor < first) ++scenes;
      cursor = std::max(cursor, end);
    }
    if (cursor < count) ++scenes;
    return std::max(scenes, 1);
  }
  qint64 estimated_export_bytes() const {
    const auto source_bytes = QFileInfo(input_->text()).size();
    const auto count = timeline_ ? timeline_->frame_count() : 0;
    if (source_bytes <= 0 || count == 0) return std::max<qint64>(source_bytes, 1);
    std::size_t removed = 0;
    for (const auto& range : cut_ranges_) {
      const auto first = std::min(range.first, count);
      const auto end = std::min(range.second, count);
      if (end > first) removed += end - first;
    }
    const auto kept = count > removed ? count - removed : std::size_t{1};
    return std::max<qint64>(1, static_cast<qint64>(std::llround(static_cast<long double>(source_bytes) * kept / count)));
  }
  void show_export_progress_dialog(const QString& mode, int scenes) {
    if (!export_progress_dialog_) {
      export_progress_dialog_ = new QDialog(this);
      export_progress_dialog_->setWindowFlag(Qt::WindowContextHelpButtonHint, false);
      export_progress_dialog_->setWindowModality(Qt::WindowModal);
      export_progress_dialog_->setMinimumWidth(520);
      auto* outer = new QHBoxLayout(export_progress_dialog_);
      outer->setContentsMargins(22, 18, 22, 16);
      outer->setSpacing(22);
      auto* icon = new QLabel(export_progress_dialog_);
      icon->setFixedSize(92, 92);
      icon->setAlignment(Qt::AlignCenter);
      icon->setPixmap(action_icon(ActionIcon::Save).pixmap(78, 78));
      outer->addWidget(icon, 0, Qt::AlignTop);
      auto* details = new QVBoxLayout;
      details->setSpacing(12);
      export_progress_bar_ = new QProgressBar(export_progress_dialog_);
      export_progress_bar_->setRange(0, 100);
      export_progress_bar_->setValue(0);
      export_progress_bar_->setFormat("%p%");
      export_progress_bar_->setTextVisible(true);
      export_progress_bar_->setAccessibleName("Output progress");
      details->addWidget(export_progress_bar_);
      export_progress_name_ = new QLabel(export_progress_dialog_);
      export_progress_name_->setAlignment(Qt::AlignCenter);
      details->addWidget(export_progress_name_);
      auto* operation_row = new QHBoxLayout;
      export_progress_mode_ = new QLabel(export_progress_dialog_);
      export_progress_scene_ = new QLabel(export_progress_dialog_);
      export_progress_scene_->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
      operation_row->addWidget(export_progress_mode_, 1);
      operation_row->addWidget(export_progress_scene_, 1);
      details->addLayout(operation_row);
      auto* buttons = new QHBoxLayout;
      buttons->addStretch();
      export_pause_button_ = new QPushButton("Pause", export_progress_dialog_);
      export_pause_button_->setAccessibleName("Pause or resume output");
      export_pause_button_->setEnabled(false);
      export_abort_button_ = new QPushButton("Abort", export_progress_dialog_);
      export_abort_button_->setAccessibleName("Abort output");
      buttons->addWidget(export_pause_button_);
      buttons->addSpacing(24);
      buttons->addWidget(export_abort_button_);
      buttons->addStretch();
      details->addLayout(buttons);
      outer->addLayout(details, 1);
      connect(export_pause_button_, &QPushButton::clicked, this, &ExportWindow::toggle_export_pause);
      connect(export_abort_button_, &QPushButton::clicked, this, &ExportWindow::cancel_current_operation);
      connect(export_progress_dialog_, &QDialog::rejected, this, [this] { if (worker_.state() != QProcess::NotRunning || plan_probe_.state() != QProcess::NotRunning) cancel_current_operation(); });
    }
    export_scene_count_ = std::max(scenes, 1);
    export_expected_bytes_ = estimated_export_bytes();
    export_started_at_ = QDateTime::currentDateTimeUtc();
    export_paused_ = false;
    export_cancelled_ = false;
    export_progress_dialog_->setWindowTitle(QString("Output: %1").arg(QFileInfo(output_->text()).fileName()));
    export_progress_name_->setText(QFileInfo(input_->text()).completeBaseName());
    export_progress_base_mode_ = mode;
    export_progress_mode_->setText(mode);
    export_progress_scene_->setText(QString("Scene 1 of %1").arg(export_scene_count_));
    export_pause_button_->setText("Pause");
    export_pause_button_->setEnabled(false);
    export_abort_button_->setEnabled(true);
    set_export_progress(0);
    export_progress_dialog_->show();
    export_progress_dialog_->raise();
  }
  void set_export_progress(int value) {
    value = std::clamp(value, 0, 100);
    progress_->setValue(value);
    if (export_progress_bar_) export_progress_bar_->setValue(value);
    if (export_progress_scene_) {
      const int scene = std::clamp(1 + value * export_scene_count_ / 100, 1, export_scene_count_);
      export_progress_scene_->setText(QString("Scene %1 of %2").arg(scene).arg(export_scene_count_));
    }
  }
  void update_export_progress() {
    if (worker_.state() == QProcess::NotRunning || export_paused_) return;
    const QFileInfo output_info(output_->text());
    int estimate = std::max(2, export_progress_bar_ ? export_progress_bar_->value() : progress_->value());
    qint64 largest_active_file = 0;
    if (output_info.exists() && output_info.lastModified().toUTC() >= export_started_at_.addMSecs(-1000)) largest_active_file = output_info.size();
    const QDir output_directory(output_info.absolutePath());
    const auto temporary_files = output_directory.entryInfoList({output_info.completeBaseName() + ".smartcut-*.mkv"}, QDir::Files);
    QFileInfo newest_temporary;
    for (const auto& temporary : temporary_files) {
      if (temporary.lastModified().toUTC() < export_started_at_.addMSecs(-1000)) continue;
      largest_active_file = std::max(largest_active_file, temporary.size());
      if (!newest_temporary.exists() || temporary.lastModified() > newest_temporary.lastModified()) newest_temporary = temporary;
    }
    QString phase = export_progress_base_mode_;
    const auto active_name = newest_temporary.fileName().toLower();
    if (active_name.contains(".smartcut-encode-")) phase = "Encoding boundary GOP";
    else if (active_name.contains(".smartcut-copy-")) phase = "Lossless frame copy";
    else if (active_name.contains(".smartcut-join-") || active_name.contains("-joined")) phase = "Joining retained scenes";
    else if (active_name.contains("-delivery")) phase = "Writing final container";
    else if (active_name.contains("-range-")) phase = "Smart-rendering scene";
    else if (output_info.exists() && output_info.lastModified().toUTC() >= export_started_at_.addMSecs(-1000)) phase = "Finalizing output";
    else if (export_progress_base_mode_.contains("Smart", Qt::CaseInsensitive)) phase = "Analyzing cut boundaries";
    if (export_progress_mode_) export_progress_mode_->setText(phase);
    if (largest_active_file > 0 && export_expected_bytes_ > 0)
      estimate = std::max(estimate, std::clamp(static_cast<int>(std::llround(95.0 * largest_active_file / export_expected_bytes_)), 2, 95));
    set_export_progress(estimate);
  }
  void toggle_export_pause() {
    if (worker_.state() == QProcess::NotRunning) return;
    const bool suspend = !export_paused_;
    if (!set_process_threads_suspended(worker_.processId(), suspend)) { status_->setText(suspend ? "Could not pause output processing" : "Could not resume output processing"); return; }
    export_paused_ = suspend;
    export_pause_button_->setText(export_paused_ ? "Resume" : "Pause");
    status_->setText(export_paused_ ? "Output processing paused" : "Export running…");
  }
  void close_export_progress_dialog(bool completed) {
    export_progress_timer_.stop();
    if (completed) set_export_progress(100);
    if (export_progress_dialog_) export_progress_dialog_->hide();
  }
  void finish_export_progress(bool completed) {
    if (export_paused_ && worker_.processId() > 0) set_process_threads_suspended(worker_.processId(), false);
    export_paused_ = false;
    if (export_pause_button_) export_pause_button_->setText("Pause");
    close_export_progress_dialog(completed);
  }
  void cancel_current_operation() {
    export_cancelled_ = true;
    if (export_paused_ && worker_.state() != QProcess::NotRunning) set_process_threads_suspended(worker_.processId(), false);
    export_paused_ = false;
    export_progress_timer_.stop();
    if (export_progress_dialog_) export_progress_dialog_->hide();
    plan_probe_.kill();
    worker_.terminate();
    QTimer::singleShot(1500, &worker_, [this] { if (worker_.state() != QProcess::NotRunning) worker_.kill(); });
    QFile::remove(output_->text());
    progress_->setValue(0);
    cancel_->setEnabled(false);
    start_->setEnabled(true);
    status_->setText("Operation cancelled; partial output removed");
  }
  void start_export() {
    license_.reload();
    if (input_->text().isEmpty() || output_->text().isEmpty()) { status_->setText("Choose both source and export paths"); return; }
    log_->clear(); worker_output_.clear(); status_->setText("Export running…"); export_elapsed_.restart(); start_->setEnabled(false); project_operation_active_ = true; export_first_frame_ = 0; export_end_frame_ = timeline_->frame_count();
    const auto output_options = current_output_profile(); const auto profile = output_options.mode; const auto audio_arguments = audio_profile_arguments(output_options); const auto video_arguments = video_profile_arguments(output_options); const auto transport_stream_arguments = transport_stream_profile_arguments(output_options); const bool audio_transform = !audio_arguments.isEmpty(); const bool video_transform = !video_arguments.isEmpty(); const bool custom_transport_stream = !transport_stream_arguments.isEmpty(); const bool quickstream = profile == "quickstream";
    QVector<int> export_streams = selected_streams_;
    if (output_options.audio_only) { export_streams.clear(); for (const auto value : source_streams_) { const auto stream = value.toObject(); if (stream.value("type").toInt(-1) == 1) { export_streams.push_back(stream.value("index").toInt()); break; } } if (export_streams.isEmpty()) { start_->setEnabled(true); cancel_->setEnabled(false); project_operation_active_ = false; status_->setText("The source has no audio stream for the Audio Only profile"); return; } }
    QStringList export_stream_values; for (const auto index : export_streams) export_stream_values.push_back(QString::number(index)); const auto export_stream_csv = export_stream_values.join(',');
    const bool selected = output_options.audio_only || (!export_streams.isEmpty() && export_streams.size() != source_streams_.size());
    const bool evaluation_video = !license_.paid() && !output_options.audio_only;
    const QString processing_mode = evaluation_video ? "Evaluation Watermark (Full Recode)" : video_transform || profile == "full" ? "Full Recode" : audio_transform ? "Smart Render + Audio Recode" : profile == "intelligent" && !cut_ranges_.empty() ? "Smart Render" : "Fast Frame Copy";
    show_export_progress_dialog(processing_mode, export_scene_count());
    QStringList args{"--export", "--input", input_->text(), "--output", output_->text()};
    if (quickstream && selected && !audio_transform && !video_transform && !custom_transport_stream) args = {"--export-selected", "--input", input_->text(), "--output", output_->text(), "--streams", export_stream_csv};
    else if ((!quickstream || audio_transform || video_transform || custom_transport_stream) && (!cut_ranges_.empty() || profile == "full" || audio_offset_ms_ != 0 || audio_transform || video_transform || custom_transport_stream)) { const auto mode = video_transform ? "full" : profile == "keyframe" ? "keyframe" : profile == "full" ? "full" : "smart"; args = {"--export-edits", "--input", input_->text(), "--output", output_->text(), "--frame-count", QString::number(static_cast<qulonglong>(timeline_->frame_count())), "--cuts", cut_csv(cut_ranges_), "--mode", mode, "--audio-offset-ms", QString::number(audio_offset_ms_)}; if (selected) args << "--streams" << export_stream_csv; args += audio_arguments; args += video_arguments; args += transport_stream_arguments; }
    else if (!quickstream && selected) args = {"--export-selected", "--input", input_->text(), "--output", output_->text(), "--streams", export_stream_csv};
    worker_.start(worker_program(), args);
  }
  void request_export_plan() { if (input_->text().isEmpty() || output_->text().isEmpty()) { status_->setText("Choose both source and export paths"); return; } export_first_frame_ = 0; export_end_frame_ = timeline_->frame_count(); start_->setEnabled(false); cancel_->setEnabled(true); progress_->setValue(1); if (!cut_ranges_.empty()) { pending_plan_ = QString("{\"schema\":\"gui-cut-list-plan-v1\",\"frame_count\":%1,\"cuts\":\"%2\"}").arg(static_cast<qulonglong>(timeline_->frame_count())).arg(cut_csv(cut_ranges_)); status_->setText(QString("Exporting the kept complement of %1 cuts").arg(static_cast<qulonglong>(cut_ranges_.size()))); QTimer::singleShot(0, this, &ExportWindow::begin_export); return; } status_->setText("Building export plan"); plan_probe_.start(worker_program(), {"--plan", "--input", input_->text(), "--first-frame", "0", "--end-frame", QString::number(static_cast<qulonglong>(timeline_->frame_count()))}); }
  void begin_export() { cancel_->setEnabled(true); start_export(); }
  smartcut::gui::LicenseManager license_;
  QJsonArray source_streams_; QJsonArray source_metadata_; QVector<int> selected_streams_; QVector<OutputProfile> custom_profiles_; std::optional<OutputProfile> pending_output_profile_override_; QHash<QString, QShortcut*> managed_shortcuts_; QHash<QString, QKeySequence> default_shortcuts_; QAction* thumbnails_action_{}; QAction* preview_action_{}; QAction* cut_mode_action_{}; QAction* scene_mode_action_{}; QAction* export_report_action_{}; QAction* plot_mask_action_{};
  QAction* light_theme_action_{}; QAction* dark_theme_action_{};
  QPushButton* cancel_{}; QPushButton* cut_keep_{}; QPushButton* trim_{}; QPushButton* invert_cuts_{}; QPushButton* remove_cuts_{}; QPushButton* undo_{}; QPushButton* redo_{}; QPushButton* recent_open_{}; QPushButton* edit_preview_{}; QPushButton* volume_icon_{}; QSlider* volume_{}; QComboBox* output_profile_{}; QProgressBar* progress_{}; QLabel* timecode_{}; QLabel* selection_start_{}; QLabel* program_end_{}; QLabel* frame_readout_{}; FineTuneSlider* fine_tune_{}; FrameGuideWidget* frame_guide_{}; InfoGridWidget* info_grid_{}; QWidget* thumbnail_panel_{}; QProcess plan_probe_; QString pending_plan_; bool dark_theme_{}; bool scene_mode_{}; bool project_modified_{}; bool auto_replace_overlapping_cuts_{}; bool automation_mode_{}; int audio_offset_ms_{}; int display_brightness_{}; int display_contrast_{}; int preview_rotation_degrees_{-1}; bool osd_visible_{}; QString frame_number_mode_{"source"}; int frame_rate_num_{25}; int frame_rate_den_{1}; int last_volume_{75}; int short_jump_seconds_{10}; int long_jump_seconds_{120}; double ad_scene_threshold_{0.30}; int ad_black_level_{24}; int ad_peak_black_level_{51}; int ad_contrast_threshold_{38}; int ad_region_left_{10}; int ad_region_top_{10}; int ad_region_right_{10}; int ad_region_bottom_{10}; int ad_pause_mode_{}; bool ad_ip_only_{}; bool ad_black_detection_{true}; bool ad_display_during_scan_{true}; QString ad_cpu_priority_{"below-normal"}; double ad_minimum_seconds_{1.0}; int ad_maximum_block_seconds_{180}; bool ad_automatic_cuts_{}; bool muted_{}; bool preview_audio_resume_pending_{}; std::optional<std::size_t> preview_audio_next_frame_; bool navigation_seek_in_progress_{}; bool navigation_seek_resume_pending_{}; bool repeat_selection_playback_{}; bool pending_journal_snapshot_{}; bool journal_recovery_applied_{}; bool project_chapters_loaded_{}; double thumbnail_interval_seconds_{}; int thumbnail_height_{80}; bool thumbnail_audio_visible_{true}; std::vector<FrameRange> cut_ranges_; std::vector<std::uint64_t> pending_chapter_times_us_; QJsonArray last_ad_markers_; QVector<EditState> undo_states_; QVector<EditState> redo_states_; std::optional<QMessageBox::StandardButton> lifecycle_prompt_override_; std::optional<OverlapChoice> overlap_choice_override_; std::optional<bool> pending_modified_after_load_;
  int thumbnail_brightness_{};
  int visible_thumbnail_count_{19};
  bool thumbnail_timecode_visible_{};
  bool waveform_decibel_scale_{true};
  QColor waveform_background_colour_{Qt::black};
  QColor waveform_colour_{"#1268b3"};
  QColor waveform_centre_colour_{"#083e75"};
  QColor waveform_text_colour_{"#00d820"};
  bool chapter_at_cuts_{};
  bool chapter_at_markers_{true};
  bool chapter_equal_spacing_{};
  bool chapter_include_first_{};
  bool chapter_auto_save_{};
  int chapter_spacing_value_{300};
  int chapter_minimum_seconds_{};
  QString chapter_spacing_mode_{"interval"};
  QString chapter_file_template_{"%F_chapter.txt"};
  QString chapter_line_format_{"%T"};
  QString chapter_timecode_format_{"HH:MM:SS:ff"};
  QString active_preview_mapping_;
  QString preview_shared_output_;
  bool active_preview_received_{};
  QString control_pipe_;
  std::vector<ControlThreadEntry> control_threads_;
  std::uint64_t control_request_generation_{};
  std::uint64_t control_navigation_successes_{};
  std::uint64_t control_navigation_fallbacks_{};
  bool control_navigation_pending_{};
  bool control_force_failure_{};
  bool shutting_down_{};
  QProcess control_worker_;
  QString multiple_file_action_{"prompt"};
  QString output_default_folder_;
  QString project_default_folder_;
  QString output_default_extension_{"mkv"};
  QString save_cpu_priority_{"below-normal"};
  bool hardware_decoding_enabled_{true};
  bool hardware_transcoding_enabled_{};
  QString hardware_transcoding_backend_{"auto"};
  QHash<QString, QString> hardware_codec_backends_;
  bool clear_cuts_after_queue_{};
  QString pending_combined_output_;
  QString pending_combined_list_;
  int combined_open_source_count_{};
  bool combined_open_pending_{};
  bool multi_open_join_verified_{};
  int plot_mask_size_{1};
  bool plot_mask_visible_{};
  double title_duration_seconds_{5.0};
  double title_fade_in_seconds_{0.5};
  double title_fade_out_seconds_{0.5};
  std::uint64_t pending_joiner_duration_us_{};
  QColor startup_background_colour_{Qt::black};
  QColor selection_time_colour_{"#202020"};
  QString ui_style_name_{"Professional Light"};
  QColor ui_base_colour_{"#f0f0f0"};
  QColor ui_surface_colour_{"#ffffff"};
  QColor ui_text_colour_{"#202020"};
  QColor ui_border_colour_{"#b6b6b6"};
  QColor ui_accent_colour_{"#1979ca"};
  QString ui_font_family_;
  int ui_font_size_{9};
  QString log_folder_;
  QStringList favorite_places_;
  QStringList associated_extensions_;
  QWidget* editor_central_{}; StartupSplashWidget* startup_splash_{}; QSize editor_window_size_{1600, 1000}; bool editor_was_maximized_{};
  bool playback_requested_{};
  std::uint64_t playback_request_generation_{};
  std::uint64_t audio_playback_generation_{};
  std::uint64_t audio_progress_event_count_{};
  std::uint64_t last_audio_progress_generation_{};
  std::uint64_t displayed_video_playback_generation_{};
  std::uint64_t navigation_seek_resume_count_{};
  QString active_video_playback_prefix_;
  QLineEdit* input_{}; QLineEdit* output_{}; QPushButton* project_save_{}; QPushButton* project_open_{}; QPushButton* play_{}; QPushButton* start_{}; QLabel* status_{}; QTextEdit* log_{}; QLabel* preview_{}; QTextEdit* inspector_{}; QTableWidget* cut_list_{}; TimelineWidget* timeline_{}; QVector<ThumbnailWidget*> thumbnail_widgets_; QVector<std::size_t> active_thumbnail_frames_; QVector<BatchJob> batch_jobs_; QVector<WatchFolder> watch_folders_; QSet<QString> watch_history_; QHash<QString, qint64> watch_first_seen_; qsizetype batch_active_index_{}; std::size_t current_preview_frame_{}; std::size_t displayed_preview_frame_{}; std::size_t active_preview_frame_{}; std::size_t active_thumbnail_center_{}; std::size_t export_first_frame_{}; std::size_t export_end_frame_{}; std::size_t completion_alert_requests_{}; std::uint64_t preview_generation_{}; std::uint64_t active_preview_generation_{}; std::uint64_t active_thumbnail_generation_{}; std::uint64_t active_waveform_generation_{}; std::uint64_t video_playback_generation_{}; int indexing_elapsed_seconds_{}; int completion_alert_mode_{}; bool hold_next_frame_{}; bool project_operation_active_{}; bool project_audio_ready_{}; bool audio_pause_requested_{}; bool video_playback_requested_{}; bool ad_scan_cancel_requested_{}; bool batch_running_{}; bool batch_skip_completed_{}; bool restoring_project_{}; bool edit_without_audio_{}; bool prompt_joiner_descriptions_{}; double display_aspect_ratio_{}; QPixmap current_preview_image_; QString project_path_; QString pending_project_path_; QString pending_project_json_; QString active_preview_path_; QString active_preview_key_; QString active_thumbnail_directory_; QString audio_progress_buffer_; QString video_playback_output_; QString ad_scan_output_; QString worker_output_; QString last_export_report_; QString completion_alert_wave_; QString joiner_list_path_; QString joiner_output_path_; QString pending_joiner_item_path_; QString pending_joiner_item_description_; QString qsf_output_; QString qsf_output_path_; QString trim_copy_output_; QString trim_copy_output_path_; QString source_uuid_; QString index_fingerprint_; QString smoke_watch_output_; QString metadata_output_path_; QStringList joiner_inputs_; QStringList joiner_descriptions_; QStringList joiner_title_specs_; QVector<qulonglong> joiner_durations_us_; QTemporaryDir preview_dir_; QString audio_cancel_path_{preview_dir_.path() + "/audio.cancel"}; QString audio_volume_path_{preview_dir_.path() + "/audio.volume"}; QString video_cancel_path_{preview_dir_.path() + "/video.cancel"}; std::optional<std::size_t> pending_preview_frame_; std::optional<std::size_t> pending_thumbnail_center_; std::optional<std::pair<std::uint64_t, std::uint64_t>> pending_waveform_window_; std::optional<std::vector<FrameRange>> pending_project_kept_; std::optional<QVector<int>> pending_project_streams_; std::optional<std::vector<std::size_t>> pending_project_chapters_; QHash<QString, QString> preview_cache_; QQueue<QString> cache_order_; QTimer playback_timer_; QTimer audio_delay_timer_; QTimer indexing_status_timer_; QTimer thumbnail_refresh_timer_; QTimer project_autosave_timer_; QTimer watch_folder_timer_; QTimer log_flush_timer_; QProcess worker_; QProcess autosave_worker_; QProcess journal_snapshot_; QProcess audio_playback_; QProcess video_playback_; QProcess ad_scan_; QProcess join_worker_; QProcess join_item_worker_; QProcess metadata_worker_; QProcess qsf_worker_; QProcess trim_copy_worker_; QProcess batch_worker_; QProcess source_probe_; QProcess identity_probe_; QProcess metadata_probe_; QProcess preview_probe_; QProcess thumbnail_probe_; QProcess waveform_probe_; QProcess sync_probe_; QProcess evidence_probe_; QProcess history_probe_; QProcess project_validation_; QProcess audio_validation_; QProcess preflight_record_; QProcess project_status_update_; QProcess project_summary_;
  QVector<std::optional<std::size_t>> active_thumbnail_slots_; QVector<qsizetype> active_thumbnail_request_slots_;
  QProcess title_worker_; QProcess multiplex_worker_; QProcess project_interchange_; QString project_interchange_mode_; QString pending_vrd_project_path_; QString pending_vrd_native_path_; QString pending_vrd_intermediate_path_; QString pending_vrd_media_output_; QString native_project_path_; QString pending_title_output_; QString pending_title_bgra_; QString pending_title_description_; QString pending_title_spec_; QString multiplex_output_; QString multiplex_output_path_; QVariant smoke_watch_folders_backup_; QVariant smoke_watch_history_backup_; QVariant smoke_batch_queue_backup_; int pending_title_replace_index_{-1}; QElapsedTimer export_elapsed_; bool batch_silent_{}; bool smoke_watch_settings_restored_{};
  QTimer preview_resize_timer_; QTimer thumbnail_layout_timer_; QTimer export_progress_timer_; QDialog* export_progress_dialog_{}; QProgressBar* export_progress_bar_{}; QLabel* export_progress_name_{}; QLabel* export_progress_mode_{}; QLabel* export_progress_scene_{}; QPushButton* export_pause_button_{}; QPushButton* export_abort_button_{}; QDateTime export_started_at_; QString export_progress_base_mode_; qint64 export_expected_bytes_{1}; int export_scene_count_{1}; bool export_paused_{}; bool export_cancelled_{};
  QElapsedTimer smoke_video_restart_elapsed_;
  QElapsedTimer smoke_audio_handoff_elapsed_;
  QElapsedTimer smoke_navigation_seek_elapsed_;
  QElapsedTimer smoke_navigation_seek_drag_step_elapsed_;
  QElapsedTimer smoke_navigation_seek_post_restart_elapsed_;
  std::size_t smoke_video_restart_target_{};
  std::size_t smoke_audio_handoff_target_{};
  std::optional<std::size_t> smoke_navigation_seek_target_;
  std::optional<std::uint64_t> smoke_navigation_seek_last_audio_us_;
  std::uint64_t smoke_navigation_seek_initial_audio_progress_before_{};
  std::uint64_t smoke_navigation_seek_video_generation_before_{};
  std::uint64_t smoke_navigation_seek_audio_generation_before_{};
  std::uint64_t smoke_navigation_seek_audio_progress_before_{};
  std::uint64_t smoke_navigation_seek_resume_count_before_{};
  int smoke_navigation_seek_drag_stage_{};
  bool smoke_navigation_seek_fine_tune_{};
  bool smoke_navigation_seek_observing_{};
  bool smoke_navigation_seek_regressed_{};
  bool smoke_navigation_seek_restarted_during_drag_{};
};

int main(int argc, char** argv) {
  auto utf8_arguments = smartcut::core::windows_utf8_argv();
  if (!utf8_arguments.pointers.empty()) { argc = static_cast<int>(utf8_arguments.pointers.size()); argv = utf8_arguments.pointers.data(); }
  std::setlocale(LC_ALL, ".UTF-8");
  QApplication app(argc, argv);
  if (argc == 2 && QString::fromLocal8Bit(argv[1]) == "--version") {
    std::fprintf(stdout, "SmartCut64 %.*s\n", static_cast<int>(smartcut::build_info::application_version.size()), smartcut::build_info::application_version.data());
    return 0;
  }
#ifdef SMARTCUT_INTERNAL_TEST_GUI
  if (argc == 4 && QString::fromLocal8Bit(argv[1]) == "--automation-title-template") {
    const auto template_path = QString::fromLocal8Bit(argv[2]);
    const auto report_path = QString::fromLocal8Bit(argv[3]);
    QFont heading_font("Arial", 42, QFont::Bold); heading_font.setItalic(true);
    QFont caption_font("Tahoma", 24);
    const QJsonObject title{{"schema", "smartcut-title-template-v1"},
                            {"canvas_width", 1920}, {"canvas_height", 1080},
                            {"background_colour", "#ff102030"},
                            {"duration_seconds", 7.25}, {"fade_in_seconds", 0.75}, {"fade_out_seconds", 1.25},
                            {"objects", QJsonArray{
                              QJsonObject{{"id", "heading"}, {"text", "Clean-room title"}, {"rect", QJsonArray{120, 80, 1040, 240}}, {"font", heading_font.toString()}, {"colour", "#fff0e0d0"}, {"alignment", "center"}},
                              QJsonObject{{"id", "caption"}, {"text", "Second text box"}, {"rect", QJsonArray{180, 500, 920, 120}}, {"font", caption_font.toString()}, {"colour", "#ff80c0ff"}, {"alignment", "left"}}
                            }}};
    const bool written = write_videoredo_title_template(template_path, title);
    const auto imported = written ? read_videoredo_title_template(template_path) : std::nullopt;
    const bool compatible = imported && imported->value("objects").toArray().size() == 2
      && imported->value("canvas_width").toInt() == 1920 && imported->value("canvas_height").toInt() == 1080
      && qAbs(imported->value("duration_seconds").toDouble() - 7.25) < 0.001
      && imported->value("objects").toArray()[0].toObject().value("text").toString() == "Clean-room title"
      && imported->value("objects").toArray()[1].toObject().value("alignment").toString() == "left";
    QFile report(report_path); const bool opened = report.open(QIODevice::WriteOnly | QIODevice::Truncate);
    if (opened) report.write(QString("{\"schema\":\"videoredo-title-template-interchange-v1\",\"written\":%1,\"roundtrip\":%2,\"object_count\":%3}\n")
      .arg(written ? "true" : "false", compatible ? "true" : "false").arg(imported ? imported->value("objects").toArray().size() : 0).toUtf8());
    return written && compatible && opened ? 0 : 5;
  }
  if (argc == 6 && QString::fromLocal8Bit(argv[1]) == "--automation-plan") {
    QProcess worker;
    worker.start(worker_program(), {"--plan", "--input", QString::fromLocal8Bit(argv[2]), "--first-frame", QString::fromLocal8Bit(argv[3]), "--end-frame", QString::fromLocal8Bit(argv[4])});
    if (!worker.waitForFinished(30000) || worker.exitCode() != 0) return 5;
    QFile report(QString::fromLocal8Bit(argv[5]));
    if (!report.open(QIODevice::WriteOnly | QIODevice::Truncate)) return 5;
    report.write(worker.readAllStandardOutput());
    return 0;
  }
  if (argc == 4 && QString::fromLocal8Bit(argv[1]) == "--automation-worker-restart") {
    QProcess worker;
    worker.start("smartcut-worker-intentionally-missing", {});
    const bool initial_failure = !worker.waitForStarted(1000);
    worker.start(worker_program(), {"--source-identity", "--input", QString::fromLocal8Bit(argv[2])});
    const bool restarted = worker.waitForFinished(30000) && worker.exitCode() == 0 && worker.readAllStandardOutput().contains("worker-source-identity-v1");
    QFile report(QString::fromLocal8Bit(argv[3]));
    if (!report.open(QIODevice::WriteOnly | QIODevice::Truncate)) return 5;
    report.write(QString("{\"schema\":\"gui-worker-restart-v1\",\"initial_failure\":%1,\"restarted\":%2}\n").arg(initial_failure ? "true" : "false", restarted ? "true" : "false").toUtf8());
    return initial_failure && restarted ? 0 : 5;
  }
  if (argc == 5 && QString::fromLocal8Bit(argv[1]) == "--automation-export") {
    const auto source = QString::fromLocal8Bit(argv[2]);
    const auto output = QString::fromLocal8Bit(argv[3]);
    QProcess worker;
    worker.start(worker_program(), {"--plan", "--input", source, "--first-frame", "0", "--end-frame", "3"});
    const bool planned = worker.waitForFinished(30000) && worker.exitCode() == 0 && worker.readAllStandardOutput().contains("render-plan-v1");
    worker.start(worker_program(), {"--export-range", "--input", source, "--output", output, "--first-frame", "0", "--end-frame", "3"});
    const bool exported = worker.waitForFinished(30000) && worker.exitCode() == 0 && worker.readAllStandardOutput().contains("\"event\":\"completed\"");
    QFile report(QString::fromLocal8Bit(argv[4]));
    if (!report.open(QIODevice::WriteOnly | QIODevice::Truncate)) return 5;
    report.write(QString("{\"schema\":\"gui-export-automation-v1\",\"planned\":%1,\"exported\":%2}\n").arg(planned ? "true" : "false", exported ? "true" : "false").toUtf8());
    return planned && exported ? 0 : 5;
  }
  if (argc == 2 && QString::fromLocal8Bit(argv[1]) == "--automation-navigation-stress") {
    constexpr int frame_count = 250000; QByteArray report{"{\"schema\":\"worker-navigation-v1\",\"source_uuid\":\"stress-source\",\"time_base_num\":1,\"time_base_den\":90000,\"frame_count\":250000,\"keyframes\":["};
    for (int frame = 0; frame < frame_count; frame += 12) { if (frame) report += ','; report += QByteArray::number(frame); } report += "],\"frame_pts\":[";
    for (int frame = 0; frame < frame_count; ++frame) { if (frame) report += ','; report += QByteArray::number(static_cast<qint64>(frame) * 3600); } report += "]}";
    const auto parsed = parse_navigation_report(report); return parsed && parsed->frame_count == frame_count && parsed->frame_pts.size() == frame_count && parsed->keyframes.size() == 20834 && parsed->frame_pts.back() == 899996400 ? 0 : 5;
  }
#endif
  ExportWindow window;
#ifdef SMARTCUT_INTERNAL_TEST_GUI
  if (argc == 5 && QString::fromLocal8Bit(argv[1]) == "--automation-project-roundtrip") {
    const auto project = QString::fromLocal8Bit(argv[3]); const auto report_path = QString::fromLocal8Bit(argv[4]); window.smoke_load_source(QString::fromLocal8Bit(argv[2])); auto* poll = new QTimer(&window); auto* attempts = new int(0); auto* stage = new int(0);
    QObject::connect(poll, &QTimer::timeout, [&app, &window, poll, attempts, stage, project, report_path] {
      if (*stage == 0 && window.smoke_source_ready() && window.smoke_streams_ready()) { window.smoke_save_edit_project(project); *stage = 1; }
      if (*stage == 1 && window.smoke_edit_project_saved() && window.smoke_journal_snapshot_idle()) { window.smoke_clear_and_reopen_project(); *stage = 2; }
      if (*stage == 2 && window.smoke_edit_project_restored()) { window.smoke_modify_project_for_autosave(); *stage = 3; }
      if (*stage == 3 && window.smoke_project_autosaved()) { window.smoke_modify_project_for_journal_recovery(); *stage = 4; }
      if (*stage == 4 && window.smoke_journal_snapshot_ready()) { window.smoke_clear_and_reopen_project(); *stage = 5; }
      const bool ready = *stage == 5 && window.smoke_journal_recovered(); ++(*attempts);
      if (!ready && (*attempts % 40) == 0) { QFile heartbeat(report_path); if (heartbeat.open(QIODevice::WriteOnly | QIODevice::Truncate)) heartbeat.write(QString("{\"schema\":\"gui-project-roundtrip-v1\",\"stage\":%1,\"source_ready\":%2,\"saved\":%3,\"restored\":%4,\"autosaved\":%5,\"journal_snapshot\":%6}\n").arg(*stage).arg(window.smoke_source_ready() ? "true" : "false", window.smoke_edit_project_saved() ? "true" : "false", window.smoke_edit_project_restored() ? "true" : "false", window.smoke_project_autosaved() ? "true" : "false", window.smoke_journal_snapshot_ready() ? "true" : "false").toUtf8()); }
      if (!ready && *attempts < 2400) return;
      QFile report(report_path); const bool opened = report.open(QIODevice::WriteOnly | QIODevice::Truncate); if (opened) report.write(QString("{\"schema\":\"gui-project-roundtrip-v1\",\"source_reopened\":%1,\"cuts_restored\":%1,\"streams_restored\":%1,\"chapter_markers_restored\":%1,\"autosave_verified\":%1,\"journal_recovery_verified\":%1}\n").arg(ready ? "true" : "false").toUtf8()); poll->stop(); delete attempts; delete stage; app.exit(ready && opened ? 0 : 5);
    });
    poll->start(25); return app.exec();
  }
  if (argc == 5 && QString::fromLocal8Bit(argv[1]) == "--automation-edit-export") {
    const auto output = QString::fromLocal8Bit(argv[3]); const auto report_path = QString::fromLocal8Bit(argv[4]); window.smoke_load_source(QString::fromLocal8Bit(argv[2])); auto* poll = new QTimer(&window); auto* attempts = new int(0); auto* started = new bool(false);
    QObject::connect(poll, &QTimer::timeout, [&app, &window, poll, attempts, started, output, report_path] { if (!*started && window.smoke_source_ready()) { window.smoke_start_cut_list_export(output); *started = true; } const bool ready = *started && window.smoke_cut_list_export_ready(output); if (!ready && ++(*attempts) < 2400) return; QFile report(report_path); const bool opened = report.open(QIODevice::WriteOnly | QIODevice::Truncate); if (opened) report.write(QString("{\"schema\":\"gui-edit-export-v1\",\"cut_list_routed\":%1,\"output_exists\":%2,\"final_report_available\":%1}\n").arg(ready ? "true" : "false", QFileInfo::exists(output) ? "true" : "false").toUtf8()); poll->stop(); delete attempts; delete started; app.exit(ready && opened ? 0 : 5); });
    poll->start(25); return app.exec();
  }
  if (argc == 5 && QString::fromLocal8Bit(argv[1]) == "--automation-stream-selection") {
    const auto output = QString::fromLocal8Bit(argv[3]); const auto report_path = QString::fromLocal8Bit(argv[4]); window.smoke_load_source(QString::fromLocal8Bit(argv[2])); auto* poll = new QTimer(&window); auto* attempts = new int(0); auto* started = new bool(false);
    QObject::connect(poll, &QTimer::timeout, [&app, &window, poll, attempts, started, output, report_path] { if (!*started && window.smoke_source_ready() && window.smoke_streams_ready()) { window.smoke_start_selected_export(output, {0, 2, 3}); *started = true; } const bool ready = *started && window.smoke_selected_export_ready(output); if (!ready && ++(*attempts) < 2400) return; QFile report(report_path); const bool opened = report.open(QIODevice::WriteOnly | QIODevice::Truncate); if (opened) report.write(QString("{\"schema\":\"gui-stream-selection-v1\",\"validated\":%1,\"selected\":[0,2,3]}\n").arg(ready ? "true" : "false").toUtf8()); poll->stop(); delete attempts; delete started; app.exit(ready && opened ? 0 : 5); });
    poll->start(25); return app.exec();
  }
  if (argc == 5 && QString::fromLocal8Bit(argv[1]) == "--automation-batch") {
    const auto output = QString::fromLocal8Bit(argv[3]); const auto report_path = QString::fromLocal8Bit(argv[4]); window.smoke_start_batch(QString::fromLocal8Bit(argv[2]), output); auto* poll = new QTimer(&window); auto* attempts = new int(0);
    QObject::connect(poll, &QTimer::timeout, [&app, &window, poll, attempts, output, report_path] { const bool ready = window.smoke_batch_ready(); if (!ready && ++(*attempts) < 2400) return; QFile report(report_path); const bool opened = report.open(QIODevice::WriteOnly | QIODevice::Truncate); if (opened) report.write(QString("{\"schema\":\"gui-batch-v1\",\"completed\":%1,\"output_exists\":%2}\n").arg(ready ? "true" : "false", QFileInfo::exists(output) ? "true" : "false").toUtf8()); poll->stop(); delete attempts; app.exit(ready && opened ? 0 : 5); });
    poll->start(25); return app.exec();
  }
  if (argc == 5 && QString::fromLocal8Bit(argv[1]) == "--automation-watch-folder") {
    const auto output = QString::fromLocal8Bit(argv[3]); const auto report_path = QString::fromLocal8Bit(argv[4]); window.smoke_start_watch_folder(QString::fromLocal8Bit(argv[2]), output); auto* poll = new QTimer(&window); auto* attempts = new int(0);
    QObject::connect(poll, &QTimer::timeout, [&app, &window, poll, attempts, output, report_path] { const bool ready = window.smoke_watch_folder_ready(); if (!ready && ++(*attempts) < 2400) return; QFile report(report_path); const bool opened = report.open(QIODevice::WriteOnly | QIODevice::Truncate); if (opened) report.write(QString("{\"schema\":\"gui-watch-folder-v1\",\"persistent_configuration\":%1,\"stable_file_queued\":%1,\"duplicate_suppressed\":%1,\"completed\":%1,\"output_exists\":%2}\n").arg(ready ? "true" : "false", QFileInfo::exists(output) ? "true" : "false").toUtf8()); poll->stop(); delete attempts; app.exit(ready && opened ? 0 : 5); });
    poll->start(25); return app.exec();
  }
  if (argc == 6 && QString::fromLocal8Bit(argv[1]) == "--automation-elementary-mux") {
    const QStringList inputs{QString::fromLocal8Bit(argv[2]), QString::fromLocal8Bit(argv[3])}; const auto output = QString::fromLocal8Bit(argv[4]); const auto report_path = QString::fromLocal8Bit(argv[5]); window.smoke_start_elementary_multiplex(inputs, output); auto* poll = new QTimer(&window); auto* attempts = new int(0);
    QObject::connect(poll, &QTimer::timeout, [&app, &window, poll, attempts, output, report_path] { const bool ready = window.smoke_elementary_multiplex_ready(); if (!ready && ++(*attempts) < 2400) return; QFile report(report_path); const bool opened = report.open(QIODevice::WriteOnly | QIODevice::Truncate); if (opened) report.write(QString("{\"schema\":\"gui-elementary-multiplex-v1\",\"validated\":%1,\"output_exists\":%2}\n").arg(ready ? "true" : "false", QFileInfo::exists(output) ? "true" : "false").toUtf8()); poll->stop(); delete attempts; app.exit(ready && opened && QFileInfo::exists(output) ? 0 : 5); });
    poll->start(25); return app.exec();
  }
  if (argc == 5 && QString::fromLocal8Bit(argv[1]) == "--automation-ad-batch") {
    const auto project = QString::fromLocal8Bit(argv[3]); const auto report_path = QString::fromLocal8Bit(argv[4]); window.smoke_start_batch_ad(QString::fromLocal8Bit(argv[2]), project); auto* poll = new QTimer(&window); auto* attempts = new int(0);
    QObject::connect(poll, &QTimer::timeout, [&app, &window, poll, attempts, project, report_path] { const bool ready = window.smoke_batch_ad_ready(); if (!ready && ++(*attempts) < 2400) return; QFile project_file(project); const auto document = project_file.open(QIODevice::ReadOnly) ? QJsonDocument::fromJson(project_file.readAll()) : QJsonDocument{}; const bool editable = document.isObject() && document.object().value("schema").toString() == "project-v1" && document.object().value("chapter_markers").isArray(); QFile report(report_path); const bool opened = report.open(QIODevice::WriteOnly | QIODevice::Truncate); if (opened) report.write(QString("{\"schema\":\"gui-ad-batch-v1\",\"completed\":%1,\"editable_project\":%2}\n").arg(ready ? "true" : "false", editable ? "true" : "false").toUtf8()); poll->stop(); delete attempts; app.exit(ready && editable && opened ? 0 : 5); });
    poll->start(25); return app.exec();
  }
  if (argc == 5 && QString::fromLocal8Bit(argv[1]) == "--automation-quickstream-fix") {
    const auto output = QString::fromLocal8Bit(argv[3]); const auto report_path = QString::fromLocal8Bit(argv[4]); window.smoke_start_quickstream_fix(QString::fromLocal8Bit(argv[2]), output); auto* poll = new QTimer(&window); auto* attempts = new int(0);
    QObject::connect(poll, &QTimer::timeout, [&app, &window, poll, attempts, output, report_path] { const bool ready = window.smoke_quickstream_fix_ready(); if (!ready && ++(*attempts) < 2400) return; QFile report(report_path); const bool opened = report.open(QIODevice::WriteOnly | QIODevice::Truncate); if (opened) report.write(QString("{\"schema\":\"gui-quickstream-fix-v1\",\"validated\":%1,\"output_exists\":%2}\n").arg(ready ? "true" : "false", QFileInfo::exists(output) ? "true" : "false").toUtf8()); poll->stop(); delete attempts; app.exit(ready && opened && QFileInfo::exists(output) ? 0 : 5); });
    poll->start(25); return app.exec();
  }
  if (argc == 4 && QString::fromLocal8Bit(argv[1]) == "--automation-ad-detective") {
    const auto report_path = QString::fromLocal8Bit(argv[3]); window.smoke_load_source(QString::fromLocal8Bit(argv[2])); auto* poll = new QTimer(&window); auto* attempts = new int(0); auto* started = new bool(false);
    QObject::connect(poll, &QTimer::timeout, [&app, &window, poll, attempts, started, report_path] { if (!*started && window.smoke_source_ready()) { window.smoke_start_ad_detective(); *started = true; } const bool ready = *started && window.smoke_ad_detective_ready(); if (!ready && ++(*attempts) < 2400) return; QFile report(report_path); const bool opened = report.open(QIODevice::WriteOnly | QIODevice::Truncate); if (opened) report.write(QString("{\"schema\":\"gui-ad-detective-v1\",\"scan_completed\":%1,\"chapter_markers\":%2}\n").arg(ready ? "true" : "false").arg(static_cast<qulonglong>(window.smoke_chapter_marker_count())).toUtf8()); poll->stop(); delete attempts; delete started; app.exit(ready && opened ? 0 : 5); });
    poll->start(25); return app.exec();
  }
  if (argc == 5 && QString::fromLocal8Bit(argv[1]) == "--automation-navigation-seek-resume") {
    const auto mode = QString::fromLocal8Bit(argv[2]);
    if (mode != "main" && mode != "fine") return 2;
    const auto report_path = QString::fromLocal8Bit(argv[4]);
    window.smoke_load_source(QString::fromLocal8Bit(argv[3]));
    auto* poll = new QTimer(&window);
    auto* attempts = new int(0);
    auto* started = new bool(false);
    QObject::connect(poll, &QTimer::timeout,
                     [&app, &window, poll, attempts, started, mode, report_path] {
      if (!*started && window.smoke_source_ready()) {
        window.smoke_start_navigation_seek_resume(mode == "fine");
        *started = true;
      }
      if (*started) window.smoke_drive_navigation_seek_drag();
      const bool ready = *started && window.smoke_navigation_seek_resume_ready();
      if (!ready && ++(*attempts) < 400) return;
      const auto target = window.smoke_navigation_seek_target();
      const auto current = window.smoke_navigation_seek_current();
      const auto displayed = window.smoke_navigation_seek_displayed();
      const bool video_restarted = window.smoke_navigation_seek_video_restarted_once();
      const bool audio_restarted = window.smoke_navigation_seek_audio_restarted_once();
      const bool resumed_once = window.smoke_navigation_seek_resumed_once();
      const bool audio_progressed = window.smoke_navigation_seek_audio_progressed();
      const bool replacement_video = window.smoke_navigation_seek_replacement_video_displayed();
      const bool no_regression = window.smoke_navigation_seek_no_regression();
      const bool no_drag_restart = window.smoke_navigation_seek_no_restart_during_drag();
      const auto video_restart_count = window.smoke_navigation_seek_video_restart_count();
      const auto audio_restart_count = window.smoke_navigation_seek_audio_restart_count();
      const auto resume_count = window.smoke_navigation_seek_resume_count();
      const bool continued = current > target;
      window.smoke_stop_all_playback();
      QFile report(report_path);
      const bool opened = report.open(QIODevice::WriteOnly | QIODevice::Truncate);
      if (opened) {
        report.write(QString("{\"schema\":\"gui-navigation-seek-resume-v2\",\"bar\":\"%1\",\"video_restarted_once\":%2,\"audio_restarted_once\":%3,\"resumed_once\":%4,\"replacement_audio_progress\":%5,\"replacement_video_displayed\":%6,\"no_position_regression\":%7,\"no_restart_during_drag\":%8,\"playback_continued\":%9,\"video_restart_count\":%10,\"audio_restart_count\":%11,\"resume_count\":%12,\"target_frame\":%13,\"displayed_frame\":%14,\"continued_frame\":%15}\n")
                         .arg(mode, video_restarted ? "true" : "false",
                              audio_restarted ? "true" : "false",
                              resumed_once ? "true" : "false",
                              audio_progressed ? "true" : "false",
                              replacement_video ? "true" : "false",
                              no_regression ? "true" : "false",
                              no_drag_restart ? "true" : "false",
                              continued ? "true" : "false")
                         .arg(static_cast<qulonglong>(video_restart_count))
                         .arg(static_cast<qulonglong>(audio_restart_count))
                         .arg(static_cast<qulonglong>(resume_count))
                         .arg(static_cast<qulonglong>(target))
                         .arg(static_cast<qulonglong>(displayed))
                         .arg(static_cast<qulonglong>(current))
                         .toUtf8());
      }
      poll->stop();
      delete attempts;
      delete started;
      app.exit(ready && opened ? 0 : 5);
    });
    poll->start(25);
    return app.exec();
  }
  if (argc == 4 && QString::fromLocal8Bit(argv[1]) == "--automation-video-restart-guard") {
    const auto report_path = QString::fromLocal8Bit(argv[3]); window.smoke_load_source(QString::fromLocal8Bit(argv[2])); auto* poll = new QTimer(&window); auto* attempts = new int(0); auto* started = new bool(false);
    QObject::connect(poll, &QTimer::timeout, [&app, &window, poll, attempts, started, report_path] { if (!*started && window.smoke_source_ready()) { window.smoke_start_video_restart_guard(); *started = true; } const bool ready = *started && window.smoke_video_restart_guard_ready(); if (!ready && ++(*attempts) < 240) return; window.smoke_stop_video_playback(); QFile report(report_path); const bool opened = report.open(QIODevice::WriteOnly | QIODevice::Truncate); if (opened) report.write(QString("{\"schema\":\"gui-video-restart-guard-v1\",\"replacement_survived_stale_timer\":%1}\n").arg(ready ? "true" : "false").toUtf8()); poll->stop(); delete attempts; delete started; app.exit(ready && opened ? 0 : 5); });
    poll->start(25); return app.exec();
  }
  if (argc == 4 && QString::fromLocal8Bit(argv[1]) == "--automation-preview-cut-handoff") {
    const auto report_path = QString::fromLocal8Bit(argv[3]); window.smoke_load_source(QString::fromLocal8Bit(argv[2])); auto* poll = new QTimer(&window); auto* attempts = new int(0); auto* started = new bool(false);
    QObject::connect(poll, &QTimer::timeout, [&app, &window, poll, attempts, started, report_path] { if (!*started && window.smoke_source_ready()) { window.smoke_start_audio_cut_handoff(); *started = true; } const bool ready = *started && window.smoke_audio_cut_handoff_ready(); if (!ready && ++(*attempts) < 400) return; window.smoke_stop_video_playback(); QFile report(report_path); const bool opened = report.open(QIODevice::WriteOnly | QIODevice::Truncate); if (opened) report.write(QString("{\"schema\":\"gui-preview-cut-handoff-v1\",\"audio_continued\":%1,\"video_continued\":%1}\n").arg(ready ? "true" : "false").toUtf8()); poll->stop(); delete attempts; delete started; app.exit(ready && opened ? 0 : 5); });
    poll->start(25); return app.exec();
  }
  if (argc == 4 && QString::fromLocal8Bit(argv[1]) == "--automation-preview-stress") {
    const auto report_path = QString::fromLocal8Bit(argv[3]); window.smoke_load_source(QString::fromLocal8Bit(argv[2])); auto* poll = new QTimer(&window); auto* attempts = new int(0); auto* stage = new int(0); auto* preview_ready = new bool(false); auto* thumbnails_ready = new bool(false);
    QObject::connect(poll, &QTimer::timeout, [&app, &window, poll, attempts, stage, preview_ready, thumbnails_ready, report_path] { if (*stage == 0 && window.smoke_source_ready()) { window.smoke_rapid_preview_requests(); *stage = 1; } if (*stage == 1) { *preview_ready = window.smoke_preview_ready(); *thumbnails_ready = window.smoke_thumbnails_ready(); if (*preview_ready && *thumbnails_ready) { window.smoke_start_video_playback(); *stage = 2; } } const bool playback_ready = *stage == 2 && window.smoke_playback_preview_ready(); bool trim_ready = false; if (playback_ready) { window.smoke_stop_video_playback(); trim_ready = window.smoke_trim_loaded_range(); } const bool ready = *preview_ready && *thumbnails_ready && playback_ready && trim_ready; if (!ready && ++(*attempts) < 2400) return; window.smoke_stop_video_playback(); QFile report(report_path); const bool opened = report.open(QIODevice::WriteOnly | QIODevice::Truncate); if (opened) report.write(QString("{\"schema\":\"gui-preview-stress-v1\",\"latest_frame_rendered\":%1,\"thumbnail_strip_rendered\":%2,\"thumbnail_aspect_preserved\":%2,\"audio_waveform_rendered\":%2,\"playback_preview_advanced\":%3,\"cut_list_populated\":%4,\"removed_ranges_red\":%4}\n").arg(*preview_ready ? "true" : "false", *thumbnails_ready ? "true" : "false", playback_ready ? "true" : "false", trim_ready ? "true" : "false").toUtf8()); poll->stop(); delete attempts; delete stage; delete preview_ready; delete thumbnails_ready; app.exit(ready && opened ? 0 : 5); });
    poll->start(50); return app.exec();
  }
  if (argc == 5 && QString::fromLocal8Bit(argv[1]) == "--automation-demo-capture") {
    const auto input = QString::fromLocal8Bit(argv[2]);
    const auto output_dir = QString::fromLocal8Bit(argv[3]);
    const auto report_path = QString::fromLocal8Bit(argv[4]);
    const bool output_ready = QDir().mkpath(output_dir);
    window.smoke_load_source(input);
    auto* poll = new QTimer(&window);
    auto* attempts = new int(0);
    auto* stage = new int(0);
    auto* loaded_saved = new bool(false);
    auto* cuts_saved = new bool(false);
    auto* preview_saved = new bool(false);
    QObject::connect(poll, &QTimer::timeout, [&app, &window, poll, attempts, stage, loaded_saved, cuts_saved, preview_saved, output_ready, output_dir, report_path] {
      if (!output_ready) *attempts = 2400;
      if (*stage == 0 && window.smoke_source_ready()) {
        window.smoke_prepare_demo_loaded_state();
        *stage = 1;
      }
      if (*stage == 1 && window.smoke_preview_ready() && window.smoke_thumbnails_ready()) {
        *loaded_saved = window.smoke_capture_demo_frame(QDir(output_dir).filePath("source-loaded.png"));
        if (*loaded_saved && window.smoke_trim_loaded_range()) *stage = 2;
      }
      if (*stage == 2 && window.smoke_demo_cut_ready()) {
        *cuts_saved = window.smoke_capture_demo_frame(QDir(output_dir).filePath("cuts-applied.png"));
        if (*cuts_saved && window.smoke_prepare_demo_preview_state()) *stage = 3;
      }
      if (*stage == 3 && window.smoke_preview_ready() && window.smoke_thumbnails_ready()) {
        *preview_saved = window.smoke_capture_demo_frame(QDir(output_dir).filePath("preview-edits.png"));
        if (*preview_saved) *stage = 4;
      }
      const bool ready = *stage == 4 && *loaded_saved && *cuts_saved && *preview_saved;
      if (!ready && ++(*attempts) < 2400) return;
      QFile report(report_path);
      const bool opened = report.open(QIODevice::WriteOnly | QIODevice::Truncate);
      if (opened) report.write(QString("{\"schema\":\"gui-demo-capture-v1\",\"source_loaded\":%1,\"cuts_applied\":%2,\"preview_edits\":%3}\n")
        .arg(*loaded_saved ? "true" : "false", *cuts_saved ? "true" : "false", *preview_saved ? "true" : "false").toUtf8());
      poll->stop();
      delete attempts; delete stage; delete loaded_saved; delete cuts_saved; delete preview_saved;
      app.exit(ready && opened ? 0 : 5);
    });
    poll->start(50); return app.exec();
  }
  if (argc == 4 && QString::fromLocal8Bit(argv[1]) == "--automation-control-restart") {
    const auto report_path = QString::fromLocal8Bit(argv[3]); window.smoke_load_source(QString::fromLocal8Bit(argv[2])); auto* poll = new QTimer(&window); auto* attempts = new int(0); auto* stage = new int(0);
    QObject::connect(poll, &QTimer::timeout, [&app, &window, poll, attempts, stage, report_path] { if (*stage == 0 && window.smoke_source_ready()) { window.smoke_restart_control_source(); *stage = 1; } if (*stage == 1 && window.smoke_control_restart_ready()) { window.smoke_force_control_fallback(); *stage = 2; } const bool ready = *stage == 2 && window.smoke_control_fallback_ready(); if (!ready && ++(*attempts) < 2400) return; QFile report(report_path); const bool opened = report.open(QIODevice::WriteOnly | QIODevice::Truncate); if (opened) report.write(QString("{\"schema\":\"gui-control-restart-v1\",\"initial_ipc\":%1,\"restart_ipc\":%1,\"one_shot_fallback\":%1}\n").arg(ready ? "true" : "false").toUtf8()); poll->stop(); delete attempts; delete stage; app.exit(ready && opened ? 0 : 5); });
    poll->start(25); return app.exec();
  }
  if (argc == 4 && QString::fromLocal8Bit(argv[1]) == "--automation-multi-open") {
    const auto report_path = QString::fromLocal8Bit(argv[3]); window.smoke_start_multi_open(QString::fromLocal8Bit(argv[2])); auto* poll = new QTimer(&window); auto* attempts = new int(0);
    QObject::connect(poll, &QTimer::timeout, [&app, &window, poll, attempts, report_path] { const bool ready = window.smoke_multi_open_ready(); if (!ready && ++(*attempts) < 2400) return; QFile report(report_path); const bool opened = report.open(QIODevice::WriteOnly | QIODevice::Truncate); if (opened) report.write(QString("{\"schema\":\"gui-multi-open-v1\",\"join_sorted\":%1,\"combine_editable\":%1,\"combined_sources\":2}\n").arg(ready ? "true" : "false").toUtf8()); poll->stop(); delete attempts; app.exit(ready && opened ? 0 : 5); });
    poll->start(25); return app.exec();
  }
  if (argc == 4 && QString::fromLocal8Bit(argv[1]) == "--automation-open-source") {
    const auto report_path = QString::fromLocal8Bit(argv[3]); window.smoke_load_source(QString::fromLocal8Bit(argv[2])); auto* poll = new QTimer(&window); auto* attempts = new int(0);
    QObject::connect(poll, &QTimer::timeout, [&app, &window, poll, attempts, report_path] { const bool ready = window.smoke_source_ready(); if (!ready && ++(*attempts) < 2400) return; QFile report(report_path); const bool opened = report.open(QIODevice::WriteOnly | QIODevice::Truncate); if (opened) report.write(QString("{\"schema\":\"gui-open-source-v1\",\"responsive\":%1}\n").arg(ready ? "true" : "false").toUtf8()); poll->stop(); delete attempts; app.exit(ready && opened ? 0 : 5); });
    poll->start(50); return app.exec();
  }
  if (argc == 4 && QString::fromLocal8Bit(argv[1]) == "--automation-reload") {
    const auto report_path = QString::fromLocal8Bit(argv[3]); window.smoke_load_source(QString::fromLocal8Bit(argv[2])); auto* poll = new QTimer(&window); auto* attempts = new int(0); auto* started = new bool(false);
    QObject::connect(poll, &QTimer::timeout, [&app, &window, poll, attempts, started, report_path] { if (!*started && window.smoke_source_ready()) { window.smoke_start_reload(); *started = true; } const bool ready = *started && window.smoke_reload_ready(); if (!ready && ++(*attempts) < 2400) return; QFile report(report_path); const bool opened = report.open(QIODevice::WriteOnly | QIODevice::Truncate); if (opened) report.write(QString("{\"schema\":\"gui-reload-v1\",\"source_reloaded\":%1,\"edits_retained\":%1,\"streams_retained\":%1}\n").arg(ready ? "true" : "false").toUtf8()); poll->stop(); delete attempts; delete started; app.exit(ready && opened ? 0 : 5); });
    poll->start(25); return app.exec();
  }
  if (argc == 4 && QString::fromLocal8Bit(argv[1]) == "--automation-joiner-project") {
    const auto report_path = QString::fromLocal8Bit(argv[3]); const auto joiner_project = report_path + (report_path.contains("videoredo", Qt::CaseInsensitive) ? ".VJnr" : ".scjoin.json"); window.smoke_load_source(QString::fromLocal8Bit(argv[2])); auto* poll = new QTimer(&window); auto* attempts = new int(0); auto* started = new bool(false); auto* roundtrip = new bool(false); auto* selection_opened = new bool(false);
    QObject::connect(poll, &QTimer::timeout, [&app, &window, poll, attempts, started, roundtrip, selection_opened, report_path, joiner_project] { if (!*started && window.smoke_source_ready()) { window.smoke_start_joiner_snapshot(); *started = true; } if (*started && !*roundtrip && window.smoke_joiner_snapshot_ready()) *roundtrip = window.smoke_joiner_project_roundtrip(joiner_project); if (*roundtrip && !*selection_opened) { window.smoke_open_joiner_selection(); *selection_opened = true; } const bool ready = *selection_opened && window.smoke_joiner_selection_ready(); if (!ready && ++(*attempts) < 2400) return; QFile report(report_path); const bool opened = report.open(QIODevice::WriteOnly | QIODevice::Truncate); if (opened) report.write(QString("{\"schema\":\"gui-joiner-project-v1\",\"edited_snapshot_prepared\":%1,\"queued_for_join\":%1,\"descriptions_persisted\":%1,\"project_roundtrip\":%1,\"edit_selection_reopened\":%1}\n").arg(ready ? "true" : "false").toUtf8()); poll->stop(); delete attempts; delete started; delete roundtrip; delete selection_opened; app.exit(ready && opened ? 0 : 5); });
    poll->start(25); return app.exec();
  }
  if (argc == 5 && QString::fromLocal8Bit(argv[1]) == "--automation-title-join") {
    const auto input = QString::fromLocal8Bit(argv[2]); const auto output = QString::fromLocal8Bit(argv[3]); const auto report_path = QString::fromLocal8Bit(argv[4]); const auto project_path = report_path + ".scjoin.json"; window.smoke_start_title(input); auto* poll = new QTimer(&window); auto* attempts = new int(0); auto* project_roundtrip = new bool(false); auto* join_started = new bool(false);
    QObject::connect(poll, &QTimer::timeout, [&app, &window, poll, attempts, project_roundtrip, join_started, input, output, report_path, project_path] { if (!*project_roundtrip && window.smoke_title_ready()) *project_roundtrip = window.smoke_title_project_roundtrip(project_path); if (*project_roundtrip && !*join_started) { window.smoke_start_title_join(input, output); *join_started = true; } const bool ready = *project_roundtrip && *join_started && window.smoke_title_join_ready(); if (!ready && ++(*attempts) < 4800) return; QFile report(report_path); const bool opened = report.open(QIODevice::WriteOnly | QIODevice::Truncate); if (opened) report.write(QString("{\"schema\":\"gui-title-join-v1\",\"title_encoded\":%1,\"title_project_roundtrip\":%1,\"title_first\":%1,\"stream_layout_preserved\":%1,\"joined_output_valid\":%1}\n").arg(ready ? "true" : "false").toUtf8()); poll->stop(); delete attempts; delete project_roundtrip; delete join_started; app.exit(ready && opened ? 0 : 5); });
    poll->start(25); return app.exec();
  }
  if (argc == 4 && QString::fromLocal8Bit(argv[1]) == "--automation-cancel") {
    const bool cleaned = window.smoke_cancel_cleanup(QString::fromLocal8Bit(argv[2]));
    QFile report(QString::fromLocal8Bit(argv[3]));
    if (!report.open(QIODevice::WriteOnly | QIODevice::Truncate)) return 5;
    report.write(QString("{\"schema\":\"gui-cancel-automation-v1\",\"partial_removed\":%1}\n").arg(cleaned ? "true" : "false").toUtf8());
    return cleaned ? 0 : 5;
  }
  if (argc > 1 && QString::fromLocal8Bit(argv[1]) == "--smoke-test") {
    const auto buttons = window.findChildren<QPushButton*>();
    const auto editors = window.findChildren<QTextEdit*>();
    const bool controls = buttons.size() >= 16 && editors.size() >= 2;
    const bool accessibility = std::all_of(buttons.begin(), buttons.end(), [](const QPushButton* button) { return !button->accessibleName().isEmpty(); });
    const bool inspector = std::any_of(editors.begin(), editors.end(), [](const QTextEdit* editor) { return editor->toPlainText().contains("Audio: pending"); });
    const auto tables = window.findChildren<QTableWidget*>();
    const bool cut_table = tables.size() == 1 && tables.front()->columnCount() == 3 && tables.front()->horizontalHeaderItem(0)->text() == "Cut Start" && tables.front()->horizontalHeaderItem(1)->text() == "Cut End" && tables.front()->horizontalHeaderItem(2)->text() == "Frames";
    const QStringList expected_menus{"File", "Edit", "Joiner", "Tools", "View", "Ad-Detective", "Help"}; QStringList actual_menus; for (const auto* action : window.menuBar()->actions()) actual_menus.push_back(action->text().remove('&'));
    const bool reference_structure = actual_menus == expected_menus && cut_table;
    QFile embedded_help(":/smartcut64/help/user-guide.md"); const bool help_resource = embedded_help.open(QIODevice::ReadOnly | QIODevice::Text) && embedded_help.readAll().contains("# SmartCut64 User Guide");
    if (argc > 2) {
      const bool blocked = argc > 3 && QString::fromLocal8Bit(argv[3]) == "--blocked";
      if (!blocked) window.smoke_open_project(QString::fromLocal8Bit(argv[2]));
      if (blocked) window.smoke_force_blocked_export();
      QTimer::singleShot(1200, [&app, &window, blocked] { app.exit(blocked ? (window.smoke_blocked_ready() ? 0 : 5) : (window.smoke_summary_ready() ? 0 : 5)); });
      return app.exec();
    }
    const auto actions = window.findChildren<QAction*>(); const auto has_action_shortcut = [&actions](const QString& text, const QKeySequence& shortcut) { return std::any_of(actions.begin(), actions.end(), [&text, &shortcut](const QAction* action) { return action->text().remove('&') == text && action->shortcut() == shortcut; }); }; const auto has_action = [&actions](const QString& text) { return std::any_of(actions.begin(), actions.end(), [&text](const QAction* action) { return action->text().remove('&') == text; }); }; const bool legacy_shortcuts = has_action_shortcut("Go To Timecode…", QKeySequence(QStringLiteral("Ctrl+T"))) && has_action_shortcut("Select Streams…", QKeySequence(QStringLiteral("Ctrl+Q"))) && has_action_shortcut("Show Program Information…", QKeySequence(QStringLiteral("Ctrl+D"))) && has_action_shortcut("Contents", QKeySequence::HelpContents); const bool trimmed_copy_action = has_action("Create Trimmed File Copy…"); const bool elementary_mux_action = has_action("Elementary Stream Multiplexer…"); const bool display_parity_actions = has_action("Title Editor…") && has_action("UI Style Editor…") && has_action("On Screen Display") && has_action("Video Rotation") && has_action("Frame Number Display"); const bool file_parity_actions = has_action("Save Project As…") && has_action("Save Video As…");
    const auto smoke_check = [](bool passed, const char* name) {
      if (!passed) {
        std::fprintf(stderr, "GUI smoke failed: %s\n", name);
        std::fflush(stderr);
      }
      return passed;
    };
    if (!smoke_check(controls, "controls")) return 5;
    if (!smoke_check(accessibility, "accessibility")) return 5;
    if (!smoke_check(inspector, "inspector")) return 5;
    if (!smoke_check(reference_structure, "reference_structure")) return 5;
    if (!smoke_check(legacy_shortcuts, "legacy_shortcuts")) return 5;
    if (!smoke_check(trimmed_copy_action, "trimmed_copy_action")) return 5;
    if (!smoke_check(elementary_mux_action, "elementary_mux_action")) return 5;
    if (!smoke_check(display_parity_actions, "display_parity_actions")) return 5;
    if (!smoke_check(file_parity_actions, "file_parity_actions")) return 5;
    if (!smoke_check(help_resource, "help_resource")) return 5;
    if (!smoke_check(window.smoke_startup_screen_ready(), "startup_screen")) return 5;
    if (!smoke_check(window.smoke_fine_tune_ready(), "fine_tune")) return 5;
    if (!smoke_check(window.smoke_timeline_drag_lifecycle_ready(), "timeline_drag_lifecycle")) return 5;
    if (!smoke_check(window.smoke_playback_timer_rate_ready(), "playback_timer_rate")) return 5;
    if (!smoke_check(window.smoke_editing_ready(), "editing")) return 5;
    if (!smoke_check(window.smoke_end_of_program_selection_ready(), "end_of_program_selection")) return 5;
    if (!smoke_check(window.smoke_scene_mode_ready(), "scene_mode")) return 5;
    if (!smoke_check(window.smoke_chapter_navigation_ready(), "chapter_navigation")) return 5;
    if (!smoke_check(window.smoke_timeline_marker_geometry_ready(), "timeline_marker_geometry")) return 5;
    if (!smoke_check(window.smoke_chapter_file_ready(), "chapter_file")) return 5;
    if (!smoke_check(window.smoke_legacy_navigation_ready(), "legacy_navigation")) return 5;
    if (!smoke_check(window.smoke_preview_mode_ready(), "preview_mode")) return 5;
    if (!smoke_check(window.smoke_zoom_thumbnail_ready(), "zoom_thumbnail")) return 5;
    if (!smoke_check(window.smoke_thumbnail_cut_indicator_ready(), "thumbnail_cut_indicator")) return 5;
    if (!smoke_check(window.smoke_managed_shortcuts_ready(), "managed_shortcuts")) return 5;
    if (!smoke_check(window.smoke_output_profiles_ready(), "output_profiles")) return 5;
    if (!smoke_check(window.smoke_save_video_profiles_ready(), "save_video_profiles")) return 5;
    if (!smoke_check(window.smoke_container_compatibility_ready(), "container_compatibility")) return 5;
    if (!smoke_check(window.smoke_custom_profiles_ready(), "custom_profiles")) return 5;
    if (!smoke_check(window.smoke_multi_title_ready(), "multi_title")) return 5;
    if (!smoke_check(window.smoke_export_progress_ready(), "export_progress")) return 5;
    if (!smoke_check(window.smoke_cut_list_interactions_ready(), "cut_list_interactions")) return 5;
    if (!smoke_check(window.smoke_overlap_choices_ready(), "overlap_choices")) return 5;
    if (!smoke_check(window.smoke_export_report_ready(), "export_report")) return 5;
    if (!smoke_check(window.smoke_output_completion_ready(), "output_completion")) return 5;
    if (!smoke_check(window.smoke_program_options_ready(), "program_options")) return 5;
    if (!smoke_check(window.smoke_display_adjustment_ready(), "display_adjustment")) return 5;
    if (!smoke_check(window.smoke_plot_mask_ready(), "plot_mask")) return 5;
    if (!smoke_check(window.smoke_repeat_selection_ready(), "repeat_selection")) return 5;
    if (!smoke_check(window.smoke_modified_lifecycle_ready(), "modified_lifecycle")) return 5;
    return 0;
  }
#endif
  if (argc == 2) {
    const auto source_path = QString::fromLocal8Bit(argv[1]);
    if (QFileInfo::exists(source_path)) { window.show(); QTimer::singleShot(0, [&window, source_path] { window.open_command_line_path(source_path); }); return app.exec(); }
  }
  window.show();
  return app.exec();
}
#include <clocale>
