Implement a unified export system for extracting data from parsed files: ExportManager (singleton): - Centralized export handling for all content types - Content type detection (image, audio, video, text, binary) - Batch export support with progress tracking Format-specific export dialogs: - ImageExportDialog: PNG, JPEG, BMP, TGA with quality options - AudioExportDialog: WAV, MP3, OGG with FFmpeg integration - BinaryExportDialog: Raw data export with optional decompression - BatchExportDialog: Recursive export with filtering options - Base ExportDialog class for common functionality Settings additions: - FFmpeg path configuration with auto-detection - Search common install locations and PATH Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
468 lines
15 KiB
C++
468 lines
15 KiB
C++
#include "batchexportdialog.h"
|
|
#include "settings.h"
|
|
#include "exportdialog.h"
|
|
|
|
#include <QVBoxLayout>
|
|
#include <QHBoxLayout>
|
|
#include <QGridLayout>
|
|
#include <QTreeWidget>
|
|
#include <QTreeWidgetItem>
|
|
#include <QHeaderView>
|
|
#include <QProgressBar>
|
|
#include <QLabel>
|
|
#include <QLineEdit>
|
|
#include <QPushButton>
|
|
#include <QCheckBox>
|
|
#include <QComboBox>
|
|
#include <QDialogButtonBox>
|
|
#include <QGroupBox>
|
|
#include <QFileDialog>
|
|
#include <QStandardPaths>
|
|
|
|
BatchExportDialog::BatchExportDialog(QWidget *parent)
|
|
: QDialog(parent)
|
|
, mExporting(false)
|
|
, mCancelled(false)
|
|
, mItemTree(nullptr)
|
|
, mSelectionLabel(nullptr)
|
|
, mOutputPath(nullptr)
|
|
, mBrowseButton(nullptr)
|
|
, mPreserveStructure(nullptr)
|
|
, mConflictCombo(nullptr)
|
|
, mImageFormatCombo(nullptr)
|
|
, mAudioFormatCombo(nullptr)
|
|
, mProgressBar(nullptr)
|
|
, mProgressLabel(nullptr)
|
|
, mButtonBox(nullptr)
|
|
, mExportButton(nullptr)
|
|
, mCancelButton(nullptr)
|
|
{
|
|
setWindowTitle("Batch Export");
|
|
setMinimumSize(600, 500);
|
|
setModal(true);
|
|
|
|
setupUI();
|
|
}
|
|
|
|
void BatchExportDialog::setupUI()
|
|
{
|
|
QVBoxLayout* mainLayout = new QVBoxLayout(this);
|
|
|
|
// Item tree with checkboxes
|
|
mItemTree = new QTreeWidget(this);
|
|
mItemTree->setHeaderLabels({"Name", "Type", "Size"});
|
|
mItemTree->setRootIsDecorated(true);
|
|
mItemTree->setAlternatingRowColors(true);
|
|
mItemTree->header()->setStretchLastSection(false);
|
|
mItemTree->header()->setSectionResizeMode(0, QHeaderView::Stretch);
|
|
mItemTree->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
|
|
mItemTree->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
|
|
mainLayout->addWidget(mItemTree, 1);
|
|
|
|
connect(mItemTree, &QTreeWidget::itemChanged, this, &BatchExportDialog::onItemChanged);
|
|
|
|
// Selection buttons
|
|
QHBoxLayout* selectionLayout = new QHBoxLayout();
|
|
QPushButton* selectAllBtn = new QPushButton("Select All", this);
|
|
QPushButton* selectNoneBtn = new QPushButton("Select None", this);
|
|
QPushButton* selectImagesBtn = new QPushButton("Images Only", this);
|
|
QPushButton* selectAudioBtn = new QPushButton("Audio Only", this);
|
|
mSelectionLabel = new QLabel("Selected: 0 of 0", this);
|
|
|
|
selectionLayout->addWidget(selectAllBtn);
|
|
selectionLayout->addWidget(selectNoneBtn);
|
|
selectionLayout->addWidget(selectImagesBtn);
|
|
selectionLayout->addWidget(selectAudioBtn);
|
|
selectionLayout->addStretch();
|
|
selectionLayout->addWidget(mSelectionLabel);
|
|
mainLayout->addLayout(selectionLayout);
|
|
|
|
connect(selectAllBtn, &QPushButton::clicked, this, &BatchExportDialog::onSelectAll);
|
|
connect(selectNoneBtn, &QPushButton::clicked, this, &BatchExportDialog::onSelectNone);
|
|
connect(selectImagesBtn, &QPushButton::clicked, this, &BatchExportDialog::onSelectImages);
|
|
connect(selectAudioBtn, &QPushButton::clicked, this, &BatchExportDialog::onSelectAudio);
|
|
|
|
// Output options group
|
|
QGroupBox* optionsGroup = new QGroupBox("Export Options", this);
|
|
QGridLayout* optionsLayout = new QGridLayout(optionsGroup);
|
|
|
|
// Output directory
|
|
optionsLayout->addWidget(new QLabel("Output:", this), 0, 0);
|
|
mOutputPath = new QLineEdit(this);
|
|
mOutputPath->setText(Settings::instance().batchExportDirectory());
|
|
if (mOutputPath->text().isEmpty()) {
|
|
mOutputPath->setText(QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation));
|
|
}
|
|
optionsLayout->addWidget(mOutputPath, 0, 1);
|
|
mBrowseButton = new QPushButton("Browse...", this);
|
|
optionsLayout->addWidget(mBrowseButton, 0, 2);
|
|
connect(mBrowseButton, &QPushButton::clicked, this, &BatchExportDialog::onBrowseClicked);
|
|
|
|
// Preserve structure
|
|
mPreserveStructure = new QCheckBox("Preserve folder structure", this);
|
|
mPreserveStructure->setChecked(Settings::instance().batchExportPreserveStructure());
|
|
optionsLayout->addWidget(mPreserveStructure, 1, 0, 1, 3);
|
|
|
|
// Conflict resolution
|
|
optionsLayout->addWidget(new QLabel("Conflicts:", this), 2, 0);
|
|
mConflictCombo = new QComboBox(this);
|
|
mConflictCombo->addItem("Append number", "number");
|
|
mConflictCombo->addItem("Overwrite", "overwrite");
|
|
mConflictCombo->addItem("Skip", "skip");
|
|
QString savedResolution = Settings::instance().batchExportConflictResolution();
|
|
for (int i = 0; i < mConflictCombo->count(); ++i) {
|
|
if (mConflictCombo->itemData(i).toString() == savedResolution) {
|
|
mConflictCombo->setCurrentIndex(i);
|
|
break;
|
|
}
|
|
}
|
|
optionsLayout->addWidget(mConflictCombo, 2, 1, 1, 2);
|
|
|
|
// Format options
|
|
optionsLayout->addWidget(new QLabel("Image format:", this), 3, 0);
|
|
mImageFormatCombo = new QComboBox(this);
|
|
mImageFormatCombo->addItem("PNG", "png");
|
|
mImageFormatCombo->addItem("JPG", "jpg");
|
|
mImageFormatCombo->addItem("BMP", "bmp");
|
|
mImageFormatCombo->addItem("TGA", "tga");
|
|
mImageFormatCombo->addItem("TIFF", "tiff");
|
|
QString savedImageFormat = Settings::instance().defaultImageExportFormat();
|
|
for (int i = 0; i < mImageFormatCombo->count(); ++i) {
|
|
if (mImageFormatCombo->itemData(i).toString() == savedImageFormat) {
|
|
mImageFormatCombo->setCurrentIndex(i);
|
|
break;
|
|
}
|
|
}
|
|
optionsLayout->addWidget(mImageFormatCombo, 3, 1, 1, 2);
|
|
|
|
optionsLayout->addWidget(new QLabel("Audio format:", this), 4, 0);
|
|
mAudioFormatCombo = new QComboBox(this);
|
|
mAudioFormatCombo->addItem("WAV", "wav");
|
|
mAudioFormatCombo->addItem("MP3", "mp3");
|
|
mAudioFormatCombo->addItem("OGG", "ogg");
|
|
mAudioFormatCombo->addItem("FLAC", "flac");
|
|
QString savedAudioFormat = Settings::instance().defaultAudioExportFormat();
|
|
for (int i = 0; i < mAudioFormatCombo->count(); ++i) {
|
|
if (mAudioFormatCombo->itemData(i).toString() == savedAudioFormat) {
|
|
mAudioFormatCombo->setCurrentIndex(i);
|
|
break;
|
|
}
|
|
}
|
|
optionsLayout->addWidget(mAudioFormatCombo, 4, 1, 1, 2);
|
|
|
|
mainLayout->addWidget(optionsGroup);
|
|
|
|
// Progress section
|
|
mProgressBar = new QProgressBar(this);
|
|
mProgressBar->setVisible(false);
|
|
mainLayout->addWidget(mProgressBar);
|
|
|
|
mProgressLabel = new QLabel(this);
|
|
mProgressLabel->setVisible(false);
|
|
mainLayout->addWidget(mProgressLabel);
|
|
|
|
// Dialog buttons
|
|
mButtonBox = new QDialogButtonBox(this);
|
|
mExportButton = mButtonBox->addButton("Export All", QDialogButtonBox::AcceptRole);
|
|
mCancelButton = mButtonBox->addButton(QDialogButtonBox::Cancel);
|
|
mainLayout->addWidget(mButtonBox);
|
|
|
|
connect(mExportButton, &QPushButton::clicked, this, &BatchExportDialog::onExportClicked);
|
|
connect(mCancelButton, &QPushButton::clicked, this, &QDialog::reject);
|
|
}
|
|
|
|
void BatchExportDialog::setItems(const QList<BatchExportItem>& items)
|
|
{
|
|
mItems = items;
|
|
populateTree();
|
|
updateSelectionCount();
|
|
|
|
// Update title with item count
|
|
setWindowTitle(QString("Batch Export (%1 items)").arg(items.size()));
|
|
}
|
|
|
|
void BatchExportDialog::populateTree()
|
|
{
|
|
mItemTree->clear();
|
|
mItemTree->blockSignals(true);
|
|
|
|
// Group items by path
|
|
QMap<QString, QTreeWidgetItem*> folderItems;
|
|
|
|
for (int i = 0; i < mItems.size(); ++i) {
|
|
const BatchExportItem& item = mItems[i];
|
|
|
|
// Determine parent folder
|
|
QString folder;
|
|
QString name = item.name;
|
|
int lastSlash = item.path.lastIndexOf('/');
|
|
if (lastSlash > 0) {
|
|
folder = item.path.left(lastSlash);
|
|
}
|
|
|
|
QTreeWidgetItem* parent = nullptr;
|
|
if (!folder.isEmpty()) {
|
|
if (!folderItems.contains(folder)) {
|
|
// Create folder item
|
|
QTreeWidgetItem* folderItem = new QTreeWidgetItem(mItemTree);
|
|
folderItem->setText(0, folder);
|
|
folderItem->setCheckState(0, Qt::Checked);
|
|
folderItem->setFlags(folderItem->flags() | Qt::ItemIsAutoTristate);
|
|
folderItem->setData(0, Qt::UserRole, -1); // -1 indicates folder
|
|
folderItems[folder] = folderItem;
|
|
}
|
|
parent = folderItems[folder];
|
|
}
|
|
|
|
// Create item
|
|
QTreeWidgetItem* treeItem = parent ? new QTreeWidgetItem(parent) : new QTreeWidgetItem(mItemTree);
|
|
treeItem->setText(0, name);
|
|
treeItem->setCheckState(0, item.selected ? Qt::Checked : Qt::Unchecked);
|
|
treeItem->setData(0, Qt::UserRole, i); // Store index
|
|
|
|
// Type column
|
|
QString typeStr;
|
|
switch (item.contentType) {
|
|
case ExportDialog::Image: typeStr = "Image"; break;
|
|
case ExportDialog::Audio: typeStr = "Audio"; break;
|
|
case ExportDialog::Video: typeStr = "Video"; break;
|
|
case ExportDialog::Text: typeStr = "Text"; break;
|
|
default: typeStr = "Binary"; break;
|
|
}
|
|
treeItem->setText(1, typeStr);
|
|
|
|
// Size column
|
|
qint64 size = item.data.size();
|
|
QString sizeStr;
|
|
if (size >= 1024 * 1024) {
|
|
sizeStr = QString("%1 MB").arg(size / (1024.0 * 1024.0), 0, 'f', 1);
|
|
} else if (size >= 1024) {
|
|
sizeStr = QString("%1 KB").arg(size / 1024.0, 0, 'f', 0);
|
|
} else {
|
|
sizeStr = QString("%1 B").arg(size);
|
|
}
|
|
treeItem->setText(2, sizeStr);
|
|
}
|
|
|
|
mItemTree->expandAll();
|
|
mItemTree->blockSignals(false);
|
|
}
|
|
|
|
QString BatchExportDialog::outputDirectory() const
|
|
{
|
|
return mOutputPath->text();
|
|
}
|
|
|
|
bool BatchExportDialog::preserveStructure() const
|
|
{
|
|
return mPreserveStructure->isChecked();
|
|
}
|
|
|
|
QString BatchExportDialog::conflictResolution() const
|
|
{
|
|
return mConflictCombo->currentData().toString();
|
|
}
|
|
|
|
QString BatchExportDialog::imageFormat() const
|
|
{
|
|
return mImageFormatCombo->currentData().toString();
|
|
}
|
|
|
|
QString BatchExportDialog::audioFormat() const
|
|
{
|
|
return mAudioFormatCombo->currentData().toString();
|
|
}
|
|
|
|
QList<BatchExportItem> BatchExportDialog::selectedItems() const
|
|
{
|
|
QList<BatchExportItem> selected;
|
|
|
|
std::function<void(QTreeWidgetItem*)> collectSelected = [&](QTreeWidgetItem* item) {
|
|
int idx = item->data(0, Qt::UserRole).toInt();
|
|
if (idx >= 0 && idx < mItems.size()) {
|
|
if (item->checkState(0) == Qt::Checked) {
|
|
selected.append(mItems[idx]);
|
|
}
|
|
}
|
|
for (int i = 0; i < item->childCount(); ++i) {
|
|
collectSelected(item->child(i));
|
|
}
|
|
};
|
|
|
|
for (int i = 0; i < mItemTree->topLevelItemCount(); ++i) {
|
|
collectSelected(mItemTree->topLevelItem(i));
|
|
}
|
|
|
|
return selected;
|
|
}
|
|
|
|
void BatchExportDialog::updateProgress(int current, int total, const QString& currentItem)
|
|
{
|
|
mProgressBar->setMaximum(total);
|
|
mProgressBar->setValue(current);
|
|
mProgressLabel->setText(QString("Exporting: %1").arg(currentItem));
|
|
}
|
|
|
|
void BatchExportDialog::onExportCompleted(int succeeded, int failed, int skipped)
|
|
{
|
|
mExporting = false;
|
|
mProgressBar->setVisible(false);
|
|
mProgressLabel->setVisible(false);
|
|
mExportButton->setEnabled(true);
|
|
mExportButton->setText("Export All");
|
|
|
|
// Show results
|
|
mProgressLabel->setText(QString("Completed: %1 succeeded, %2 failed, %3 skipped")
|
|
.arg(succeeded).arg(failed).arg(skipped));
|
|
mProgressLabel->setVisible(true);
|
|
|
|
if (failed == 0) {
|
|
accept();
|
|
}
|
|
}
|
|
|
|
void BatchExportDialog::onSelectAll()
|
|
{
|
|
mItemTree->blockSignals(true);
|
|
for (int i = 0; i < mItemTree->topLevelItemCount(); ++i) {
|
|
QTreeWidgetItem* item = mItemTree->topLevelItem(i);
|
|
item->setCheckState(0, Qt::Checked);
|
|
}
|
|
mItemTree->blockSignals(false);
|
|
updateSelectionCount();
|
|
}
|
|
|
|
void BatchExportDialog::onSelectNone()
|
|
{
|
|
mItemTree->blockSignals(true);
|
|
for (int i = 0; i < mItemTree->topLevelItemCount(); ++i) {
|
|
QTreeWidgetItem* item = mItemTree->topLevelItem(i);
|
|
item->setCheckState(0, Qt::Unchecked);
|
|
}
|
|
mItemTree->blockSignals(false);
|
|
updateSelectionCount();
|
|
}
|
|
|
|
void BatchExportDialog::onSelectImages()
|
|
{
|
|
mItemTree->blockSignals(true);
|
|
|
|
std::function<void(QTreeWidgetItem*)> setByType = [&](QTreeWidgetItem* item) {
|
|
int idx = item->data(0, Qt::UserRole).toInt();
|
|
if (idx >= 0 && idx < mItems.size()) {
|
|
bool isImage = mItems[idx].contentType == ExportDialog::Image;
|
|
item->setCheckState(0, isImage ? Qt::Checked : Qt::Unchecked);
|
|
}
|
|
for (int i = 0; i < item->childCount(); ++i) {
|
|
setByType(item->child(i));
|
|
}
|
|
};
|
|
|
|
for (int i = 0; i < mItemTree->topLevelItemCount(); ++i) {
|
|
setByType(mItemTree->topLevelItem(i));
|
|
}
|
|
|
|
mItemTree->blockSignals(false);
|
|
updateSelectionCount();
|
|
}
|
|
|
|
void BatchExportDialog::onSelectAudio()
|
|
{
|
|
mItemTree->blockSignals(true);
|
|
|
|
std::function<void(QTreeWidgetItem*)> setByType = [&](QTreeWidgetItem* item) {
|
|
int idx = item->data(0, Qt::UserRole).toInt();
|
|
if (idx >= 0 && idx < mItems.size()) {
|
|
bool isAudio = mItems[idx].contentType == ExportDialog::Audio;
|
|
item->setCheckState(0, isAudio ? Qt::Checked : Qt::Unchecked);
|
|
}
|
|
for (int i = 0; i < item->childCount(); ++i) {
|
|
setByType(item->child(i));
|
|
}
|
|
};
|
|
|
|
for (int i = 0; i < mItemTree->topLevelItemCount(); ++i) {
|
|
setByType(mItemTree->topLevelItem(i));
|
|
}
|
|
|
|
mItemTree->blockSignals(false);
|
|
updateSelectionCount();
|
|
}
|
|
|
|
void BatchExportDialog::onBrowseClicked()
|
|
{
|
|
QString dir = QFileDialog::getExistingDirectory(this, "Select Output Directory",
|
|
mOutputPath->text());
|
|
if (!dir.isEmpty()) {
|
|
mOutputPath->setText(dir);
|
|
}
|
|
}
|
|
|
|
void BatchExportDialog::onExportClicked()
|
|
{
|
|
if (mExporting) {
|
|
mCancelled = true;
|
|
return;
|
|
}
|
|
|
|
// Save settings
|
|
Settings::instance().setBatchExportDirectory(mOutputPath->text());
|
|
Settings::instance().setBatchExportPreserveStructure(mPreserveStructure->isChecked());
|
|
Settings::instance().setBatchExportConflictResolution(mConflictCombo->currentData().toString());
|
|
|
|
// Start export
|
|
mExporting = true;
|
|
mCancelled = false;
|
|
mExportButton->setText("Cancel");
|
|
mProgressBar->setVisible(true);
|
|
mProgressBar->setValue(0);
|
|
mProgressLabel->setVisible(true);
|
|
|
|
// Signal acceptance - MainWindow will handle actual export
|
|
accept();
|
|
}
|
|
|
|
void BatchExportDialog::onItemChanged(QTreeWidgetItem* item, int column)
|
|
{
|
|
Q_UNUSED(item);
|
|
Q_UNUSED(column);
|
|
updateSelectionCount();
|
|
}
|
|
|
|
void BatchExportDialog::updateSelectionCount()
|
|
{
|
|
int selected = 0;
|
|
int total = 0;
|
|
|
|
std::function<void(QTreeWidgetItem*)> count = [&](QTreeWidgetItem* item) {
|
|
int idx = item->data(0, Qt::UserRole).toInt();
|
|
if (idx >= 0) {
|
|
total++;
|
|
if (item->checkState(0) == Qt::Checked) {
|
|
selected++;
|
|
}
|
|
}
|
|
for (int i = 0; i < item->childCount(); ++i) {
|
|
count(item->child(i));
|
|
}
|
|
};
|
|
|
|
for (int i = 0; i < mItemTree->topLevelItemCount(); ++i) {
|
|
count(mItemTree->topLevelItem(i));
|
|
}
|
|
|
|
mSelectionLabel->setText(QString("Selected: %1 of %2").arg(selected).arg(total));
|
|
mExportButton->setEnabled(selected > 0);
|
|
}
|
|
|
|
int BatchExportDialog::countByType(int contentType) const
|
|
{
|
|
int count = 0;
|
|
for (const BatchExportItem& item : mItems) {
|
|
if (item.contentType == contentType) {
|
|
count++;
|
|
}
|
|
}
|
|
return count;
|
|
}
|