Add comprehensive export system with format-specific dialogs

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>
This commit is contained in:
njohnson 2026-01-12 20:54:38 -05:00
parent 05c70c1108
commit d7488c5fa9
14 changed files with 2974 additions and 0 deletions

384
app/audioexportdialog.cpp Normal file
View File

@ -0,0 +1,384 @@
#include "audioexportdialog.h"
#include "settings.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QSlider>
#include <QSpinBox>
#include <QComboBox>
#include <QStackedWidget>
#include <QGroupBox>
#include <QPainter>
#include <QtEndian>
AudioExportDialog::AudioExportDialog(QWidget *parent)
: ExportDialog(ContentType::Audio, parent)
, mWaveformLabel(nullptr)
, mDurationLabel(nullptr)
, mSampleRateLabel(nullptr)
, mChannelsLabel(nullptr)
, mBitDepthLabel(nullptr)
, mSampleRate(44100)
, mChannels(2)
, mBitsPerSample(16)
, mDuration(0)
, mOptionsStack(nullptr)
{
// Populate format combo
for (const QString& fmt : supportedFormats()) {
formatCombo()->addItem(fmt.toUpper());
}
// Set default format from settings
QString defaultFormat = Settings::instance().defaultAudioExportFormat().toUpper();
int index = formatCombo()->findText(defaultFormat);
if (index >= 0) {
formatCombo()->setCurrentIndex(index);
}
setupPreview();
}
void AudioExportDialog::setupPreview()
{
// Create waveform label inside preview container
QVBoxLayout* previewLayout = new QVBoxLayout(previewContainer());
previewLayout->setContentsMargins(0, 0, 0, 0);
mWaveformLabel = new QLabel(previewContainer());
mWaveformLabel->setAlignment(Qt::AlignCenter);
mWaveformLabel->setMinimumSize(280, 100);
mWaveformLabel->setText("No audio loaded");
mWaveformLabel->setStyleSheet("color: #808080;");
previewLayout->addWidget(mWaveformLabel);
// Audio info labels
QHBoxLayout* infoLayout = new QHBoxLayout();
mDurationLabel = new QLabel("Duration: --", this);
mSampleRateLabel = new QLabel("Sample Rate: --", this);
mChannelsLabel = new QLabel("Channels: --", this);
infoLayout->addWidget(mDurationLabel);
infoLayout->addWidget(new QLabel("|", this));
infoLayout->addWidget(mSampleRateLabel);
infoLayout->addWidget(new QLabel("|", this));
infoLayout->addWidget(mChannelsLabel);
infoLayout->addStretch();
previewLayout->addLayout(infoLayout);
// Create stacked widget for format-specific options
mOptionsStack = new QStackedWidget(this);
// WAV options (none)
mWavOptionsWidget = new QWidget(this);
QVBoxLayout* wavLayout = new QVBoxLayout(mWavOptionsWidget);
wavLayout->setContentsMargins(0, 0, 0, 0);
QLabel* wavLabel = new QLabel("WAV: Uncompressed PCM audio.", mWavOptionsWidget);
wavLabel->setStyleSheet("color: #808080;");
wavLayout->addWidget(wavLabel);
wavLayout->addStretch();
// MP3 options
mMp3OptionsWidget = new QWidget(this);
QVBoxLayout* mp3Layout = new QVBoxLayout(mMp3OptionsWidget);
mp3Layout->setContentsMargins(0, 0, 0, 0);
QLabel* bitrateLabel = new QLabel("Bitrate:", mMp3OptionsWidget);
mBitrateCombo = new QComboBox(mMp3OptionsWidget);
mBitrateCombo->addItem("128 kbps", 128);
mBitrateCombo->addItem("192 kbps", 192);
mBitrateCombo->addItem("256 kbps", 256);
mBitrateCombo->addItem("320 kbps", 320);
// Set default bitrate from settings
int defaultBitrate = Settings::instance().audioMp3Bitrate();
for (int i = 0; i < mBitrateCombo->count(); ++i) {
if (mBitrateCombo->itemData(i).toInt() == defaultBitrate) {
mBitrateCombo->setCurrentIndex(i);
break;
}
}
mp3Layout->addWidget(bitrateLabel);
mp3Layout->addWidget(mBitrateCombo);
// FFmpeg warning
if (!Settings::instance().ffmpegPath().isEmpty()) {
QLabel* ffmpegOk = new QLabel("FFmpeg available", mMp3OptionsWidget);
ffmpegOk->setStyleSheet("color: #4CAF50;");
mp3Layout->addWidget(ffmpegOk);
} else {
QLabel* ffmpegWarn = new QLabel("FFmpeg required for MP3 export", mMp3OptionsWidget);
ffmpegWarn->setStyleSheet("color: #FF9800;");
mp3Layout->addWidget(ffmpegWarn);
}
mp3Layout->addStretch();
connect(mBitrateCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &AudioExportDialog::onBitrateChanged);
// OGG options
mOggOptionsWidget = new QWidget(this);
QVBoxLayout* oggLayout = new QVBoxLayout(mOggOptionsWidget);
oggLayout->setContentsMargins(0, 0, 0, 0);
QLabel* oggQualityLabel = new QLabel("Quality (-1=lowest, 10=highest):", mOggOptionsWidget);
QHBoxLayout* oggSliderLayout = new QHBoxLayout();
mOggQualitySlider = new QSlider(Qt::Horizontal, mOggOptionsWidget);
mOggQualitySlider->setRange(-1, 10);
mOggQualitySlider->setValue(Settings::instance().audioOggQuality());
mOggQualitySpinBox = new QSpinBox(mOggOptionsWidget);
mOggQualitySpinBox->setRange(-1, 10);
mOggQualitySpinBox->setValue(Settings::instance().audioOggQuality());
oggSliderLayout->addWidget(mOggQualitySlider);
oggSliderLayout->addWidget(mOggQualitySpinBox);
oggLayout->addWidget(oggQualityLabel);
oggLayout->addLayout(oggSliderLayout);
if (Settings::instance().ffmpegPath().isEmpty()) {
QLabel* ffmpegWarn = new QLabel("FFmpeg required for OGG export", mOggOptionsWidget);
ffmpegWarn->setStyleSheet("color: #FF9800;");
oggLayout->addWidget(ffmpegWarn);
}
oggLayout->addStretch();
connect(mOggQualitySlider, &QSlider::valueChanged, this, &AudioExportDialog::onOggQualityChanged);
connect(mOggQualitySpinBox, QOverload<int>::of(&QSpinBox::valueChanged),
mOggQualitySlider, &QSlider::setValue);
// FLAC options
mFlacOptionsWidget = new QWidget(this);
QVBoxLayout* flacLayout = new QVBoxLayout(mFlacOptionsWidget);
flacLayout->setContentsMargins(0, 0, 0, 0);
QLabel* flacCompressionLabel = new QLabel("Compression (0=fast, 8=best):", mFlacOptionsWidget);
QHBoxLayout* flacSliderLayout = new QHBoxLayout();
mFlacCompressionSlider = new QSlider(Qt::Horizontal, mFlacOptionsWidget);
mFlacCompressionSlider->setRange(0, 8);
mFlacCompressionSlider->setValue(Settings::instance().audioFlacCompression());
mFlacCompressionSpinBox = new QSpinBox(mFlacOptionsWidget);
mFlacCompressionSpinBox->setRange(0, 8);
mFlacCompressionSpinBox->setValue(Settings::instance().audioFlacCompression());
flacSliderLayout->addWidget(mFlacCompressionSlider);
flacSliderLayout->addWidget(mFlacCompressionSpinBox);
flacLayout->addWidget(flacCompressionLabel);
flacLayout->addLayout(flacSliderLayout);
if (Settings::instance().ffmpegPath().isEmpty()) {
QLabel* ffmpegWarn = new QLabel("FFmpeg required for FLAC export", mFlacOptionsWidget);
ffmpegWarn->setStyleSheet("color: #FF9800;");
flacLayout->addWidget(ffmpegWarn);
}
flacLayout->addStretch();
connect(mFlacCompressionSlider, &QSlider::valueChanged, this, &AudioExportDialog::onFlacCompressionChanged);
connect(mFlacCompressionSpinBox, QOverload<int>::of(&QSpinBox::valueChanged),
mFlacCompressionSlider, &QSlider::setValue);
// Add to stacked widget
mOptionsStack->addWidget(mWavOptionsWidget); // Index 0
mOptionsStack->addWidget(mMp3OptionsWidget); // Index 1
mOptionsStack->addWidget(mOggOptionsWidget); // Index 2
mOptionsStack->addWidget(mFlacOptionsWidget); // Index 3
// Add stacked widget to options container
QVBoxLayout* optionsLayout = qobject_cast<QVBoxLayout*>(optionsContainer()->layout());
if (optionsLayout) {
optionsLayout->addWidget(mOptionsStack);
}
// Show options for current format
showOptionsForFormat(formatCombo()->currentText());
}
void AudioExportDialog::updatePreview()
{
if (mData.isEmpty()) {
mWaveformLabel->setText("No audio loaded");
return;
}
parseWavInfo();
drawWaveform();
}
QStringList AudioExportDialog::supportedFormats() const
{
return {"wav", "mp3", "ogg", "flac"};
}
void AudioExportDialog::parseWavInfo()
{
if (mData.size() < 44) return;
const char* data = mData.constData();
// Check for RIFF header
bool isRiff = (data[0] == 'R' && data[1] == 'I' && data[2] == 'F' && data[3] == 'F');
bool isRifx = (data[0] == 'R' && data[1] == 'I' && data[2] == 'F' && data[3] == 'X');
if (!isRiff && !isRifx) return;
bool bigEndian = isRifx;
// Parse format chunk
if (bigEndian) {
mChannels = qFromBigEndian<quint16>(reinterpret_cast<const uchar*>(data + 22));
mSampleRate = qFromBigEndian<quint32>(reinterpret_cast<const uchar*>(data + 24));
mBitsPerSample = qFromBigEndian<quint16>(reinterpret_cast<const uchar*>(data + 34));
} else {
mChannels = qFromLittleEndian<quint16>(reinterpret_cast<const uchar*>(data + 22));
mSampleRate = qFromLittleEndian<quint32>(reinterpret_cast<const uchar*>(data + 24));
mBitsPerSample = qFromLittleEndian<quint16>(reinterpret_cast<const uchar*>(data + 34));
}
// Calculate duration
int dataSize = mData.size() - 44;
int bytesPerSample = mBitsPerSample / 8;
if (bytesPerSample > 0 && mChannels > 0 && mSampleRate > 0) {
int numSamples = dataSize / (bytesPerSample * mChannels);
mDuration = static_cast<double>(numSamples) / mSampleRate;
}
// Update labels
int minutes = static_cast<int>(mDuration) / 60;
double seconds = mDuration - (minutes * 60);
mDurationLabel->setText(QString("Duration: %1:%2")
.arg(minutes)
.arg(seconds, 6, 'f', 3, '0'));
mSampleRateLabel->setText(QString("%1 Hz").arg(mSampleRate));
mChannelsLabel->setText(mChannels == 1 ? "Mono" : "Stereo");
}
void AudioExportDialog::drawWaveform()
{
if (mData.size() < 44) return;
int labelWidth = mWaveformLabel->width();
int labelHeight = mWaveformLabel->height();
if (labelWidth < 10 || labelHeight < 10) {
labelWidth = 280;
labelHeight = 100;
}
// Get theme colors
Theme theme = Settings::instance().theme();
QColor bgColor(theme.panelColor);
QColor waveColor(theme.accentColor);
QColor centerLineColor(theme.borderColor);
// Create pixmap
mWaveformPixmap = QPixmap(labelWidth, labelHeight);
mWaveformPixmap.fill(bgColor);
QPainter painter(&mWaveformPixmap);
painter.setRenderHint(QPainter::Antialiasing);
// Draw center line
int centerY = labelHeight / 2;
painter.setPen(centerLineColor);
painter.drawLine(0, centerY, labelWidth, centerY);
// Get audio data (skip 44-byte header)
const qint16* samples = reinterpret_cast<const qint16*>(mData.constData() + 44);
int numSamples = (mData.size() - 44) / (mBitsPerSample / 8);
if (mChannels > 1) {
numSamples /= mChannels;
}
if (numSamples < 2) {
mWaveformLabel->setPixmap(mWaveformPixmap);
return;
}
// Draw waveform
painter.setPen(waveColor);
int samplesPerPixel = numSamples / labelWidth;
if (samplesPerPixel < 1) samplesPerPixel = 1;
int amplitude = labelHeight / 4; // Max amplitude (25% above/below center)
for (int x = 0; x < labelWidth; ++x) {
int startSample = x * samplesPerPixel;
int endSample = qMin(startSample + samplesPerPixel, numSamples);
qint16 minVal = 0, maxVal = 0;
for (int i = startSample; i < endSample; ++i) {
int idx = i * mChannels; // Use first channel
if (idx < (mData.size() - 44) / 2) {
qint16 sample = samples[idx];
if (sample < minVal) minVal = sample;
if (sample > maxVal) maxVal = sample;
}
}
// Scale to display
int yMin = centerY - (maxVal * amplitude / 32768);
int yMax = centerY - (minVal * amplitude / 32768);
painter.drawLine(x, yMin, x, yMax);
}
mWaveformLabel->setPixmap(mWaveformPixmap);
}
void AudioExportDialog::onFormatChanged(const QString& format)
{
ExportDialog::onFormatChanged(format);
showOptionsForFormat(format);
}
void AudioExportDialog::showOptionsForFormat(const QString& format)
{
QString fmt = format.toLower();
if (fmt == "wav") {
mOptionsStack->setCurrentWidget(mWavOptionsWidget);
} else if (fmt == "mp3") {
mOptionsStack->setCurrentWidget(mMp3OptionsWidget);
} else if (fmt == "ogg") {
mOptionsStack->setCurrentWidget(mOggOptionsWidget);
} else if (fmt == "flac") {
mOptionsStack->setCurrentWidget(mFlacOptionsWidget);
}
}
int AudioExportDialog::mp3Bitrate() const
{
return mBitrateCombo ? mBitrateCombo->currentData().toInt() : 256;
}
int AudioExportDialog::oggQuality() const
{
return mOggQualitySlider ? mOggQualitySlider->value() : 5;
}
int AudioExportDialog::flacCompression() const
{
return mFlacCompressionSlider ? mFlacCompressionSlider->value() : 5;
}
void AudioExportDialog::onBitrateChanged(int index)
{
Q_UNUSED(index);
// Could update size estimate here
}
void AudioExportDialog::onOggQualityChanged(int value)
{
if (mOggQualitySpinBox) {
mOggQualitySpinBox->blockSignals(true);
mOggQualitySpinBox->setValue(value);
mOggQualitySpinBox->blockSignals(false);
}
}
void AudioExportDialog::onFlacCompressionChanged(int value)
{
if (mFlacCompressionSpinBox) {
mFlacCompressionSpinBox->blockSignals(true);
mFlacCompressionSpinBox->setValue(value);
mFlacCompressionSpinBox->blockSignals(false);
}
}

82
app/audioexportdialog.h Normal file
View File

@ -0,0 +1,82 @@
#ifndef AUDIOEXPORTDIALOG_H
#define AUDIOEXPORTDIALOG_H
#include "exportdialog.h"
class QSlider;
class QSpinBox;
class QComboBox;
class QStackedWidget;
/**
* @brief Export dialog for audio with waveform preview and format-specific options.
*
* Shows a waveform visualization and provides bitrate/quality settings
* for different output formats (WAV, MP3, OGG, FLAC).
*/
class AudioExportDialog : public ExportDialog
{
Q_OBJECT
public:
explicit AudioExportDialog(QWidget *parent = nullptr);
// Audio-specific settings
int mp3Bitrate() const;
int oggQuality() const; // -1 to 10
int flacCompression() const; // 0-8
protected:
void setupPreview() override;
void updatePreview() override;
QStringList supportedFormats() const override;
void onFormatChanged(const QString& format) override;
private slots:
void onBitrateChanged(int index);
void onOggQualityChanged(int value);
void onFlacCompressionChanged(int value);
private:
void parseWavInfo();
void drawWaveform();
void showOptionsForFormat(const QString& format);
// Waveform display
QLabel* mWaveformLabel;
QPixmap mWaveformPixmap;
// Audio info
QLabel* mDurationLabel;
QLabel* mSampleRateLabel;
QLabel* mChannelsLabel;
QLabel* mBitDepthLabel;
// Parsed audio info
int mSampleRate;
int mChannels;
int mBitsPerSample;
double mDuration;
// Format-specific option widgets
QStackedWidget* mOptionsStack;
// WAV options (none)
QWidget* mWavOptionsWidget;
// MP3 options
QWidget* mMp3OptionsWidget;
QComboBox* mBitrateCombo;
// OGG options
QWidget* mOggOptionsWidget;
QSlider* mOggQualitySlider;
QSpinBox* mOggQualitySpinBox;
// FLAC options
QWidget* mFlacOptionsWidget;
QSlider* mFlacCompressionSlider;
QSpinBox* mFlacCompressionSpinBox;
};
#endif // AUDIOEXPORTDIALOG_H

467
app/batchexportdialog.cpp Normal file
View File

@ -0,0 +1,467 @@
#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;
}

99
app/batchexportdialog.h Normal file
View File

@ -0,0 +1,99 @@
#ifndef BATCHEXPORTDIALOG_H
#define BATCHEXPORTDIALOG_H
#include <QDialog>
#include <QList>
class QTreeWidget;
class QTreeWidgetItem;
class QProgressBar;
class QLabel;
class QLineEdit;
class QPushButton;
class QCheckBox;
class QComboBox;
class QDialogButtonBox;
/**
* @brief Item data for batch export operations.
*/
struct BatchExportItem
{
QString name; // Display name
QString path; // Relative path (for folder structure)
QByteArray data; // Raw data to export
int contentType; // ContentType enum value
bool selected; // Whether to export
};
/**
* @brief Dialog for batch exporting multiple items.
*
* Shows a checkable tree of items with progress tracking,
* folder structure preservation, and conflict resolution options.
*/
class BatchExportDialog : public QDialog
{
Q_OBJECT
public:
explicit BatchExportDialog(QWidget *parent = nullptr);
// Set items to potentially export
void setItems(const QList<BatchExportItem>& items);
// Get export configuration
QString outputDirectory() const;
bool preserveStructure() const;
QString conflictResolution() const; // "number", "overwrite", "skip"
QString imageFormat() const;
QString audioFormat() const;
// Get selected items
QList<BatchExportItem> selectedItems() const;
signals:
void exportProgress(int current, int total, const QString& currentItem);
void exportCompleted(int succeeded, int failed, int skipped);
public slots:
void updateProgress(int current, int total, const QString& currentItem);
void onExportCompleted(int succeeded, int failed, int skipped);
private slots:
void onSelectAll();
void onSelectNone();
void onSelectImages();
void onSelectAudio();
void onBrowseClicked();
void onExportClicked();
void onItemChanged(QTreeWidgetItem* item, int column);
private:
void setupUI();
void updateSelectionCount();
void populateTree();
int countByType(int contentType) const;
// Data
QList<BatchExportItem> mItems;
bool mExporting;
bool mCancelled;
// UI Elements
QTreeWidget* mItemTree;
QLabel* mSelectionLabel;
QLineEdit* mOutputPath;
QPushButton* mBrowseButton;
QCheckBox* mPreserveStructure;
QComboBox* mConflictCombo;
QComboBox* mImageFormatCombo;
QComboBox* mAudioFormatCombo;
QProgressBar* mProgressBar;
QLabel* mProgressLabel;
QDialogButtonBox* mButtonBox;
QPushButton* mExportButton;
QPushButton* mCancelButton;
};
#endif // BATCHEXPORTDIALOG_H

158
app/binaryexportdialog.cpp Normal file
View File

@ -0,0 +1,158 @@
#include "binaryexportdialog.h"
#include "settings.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QPlainTextEdit>
#include <QFont>
#include <QComboBox>
#include <QGroupBox>
BinaryExportDialog::BinaryExportDialog(QWidget *parent)
: ExportDialog(ContentType::Binary, parent)
, mHexPreview(nullptr)
, mSizeLabel(nullptr)
, mBytesPerLine(16)
, mPreviewBytes(512) // Show first 512 bytes in preview
{
// Populate format combo
for (const QString& fmt : supportedFormats()) {
formatCombo()->addItem(fmt.toUpper());
}
setupPreview();
}
void BinaryExportDialog::setupPreview()
{
// Create hex preview inside preview container
QVBoxLayout* previewLayout = new QVBoxLayout(previewContainer());
previewLayout->setContentsMargins(0, 0, 0, 0);
mHexPreview = new QPlainTextEdit(previewContainer());
mHexPreview->setReadOnly(true);
mHexPreview->setLineWrapMode(QPlainTextEdit::NoWrap);
// Use monospace font for hex display
QFont monoFont("Consolas", 9);
monoFont.setStyleHint(QFont::Monospace);
mHexPreview->setFont(monoFont);
// Dark theme styling
mHexPreview->setStyleSheet(
"QPlainTextEdit {"
" background-color: #1e1e1e;"
" color: #d4d4d4;"
" border: none;"
" selection-background-color: #264f78;"
"}"
);
mHexPreview->setPlaceholderText("No data loaded");
previewLayout->addWidget(mHexPreview);
// Size info label
QHBoxLayout* infoLayout = new QHBoxLayout();
mSizeLabel = new QLabel("Size: --", this);
infoLayout->addWidget(mSizeLabel);
infoLayout->addStretch();
previewLayout->addLayout(infoLayout);
// Add description to options container
QVBoxLayout* optionsLayout = qobject_cast<QVBoxLayout*>(optionsContainer()->layout());
if (optionsLayout) {
QLabel* descLabel = new QLabel("Raw binary export preserves data exactly as-is.", this);
descLabel->setStyleSheet("color: #808080;");
descLabel->setWordWrap(true);
optionsLayout->addWidget(descLabel);
optionsLayout->addStretch();
}
}
void BinaryExportDialog::updatePreview()
{
if (mData.isEmpty()) {
mHexPreview->setPlainText("");
mSizeLabel->setText("Size: --");
return;
}
// Update size label
qint64 size = mData.size();
QString sizeStr;
if (size >= 1024 * 1024) {
sizeStr = QString("Size: %1 MB (%2 bytes)")
.arg(size / (1024.0 * 1024.0), 0, 'f', 2)
.arg(size);
} else if (size >= 1024) {
sizeStr = QString("Size: %1 KB (%2 bytes)")
.arg(size / 1024.0, 0, 'f', 1)
.arg(size);
} else {
sizeStr = QString("Size: %1 bytes").arg(size);
}
mSizeLabel->setText(sizeStr);
updateHexPreview();
}
QStringList BinaryExportDialog::supportedFormats() const
{
return {"bin", "dat", "raw"};
}
void BinaryExportDialog::updateHexPreview()
{
if (mData.isEmpty()) {
mHexPreview->setPlainText("");
return;
}
QString hexText;
int bytesToShow = qMin(mPreviewBytes, static_cast<int>(mData.size()));
const unsigned char* data = reinterpret_cast<const unsigned char*>(mData.constData());
for (int i = 0; i < bytesToShow; i += mBytesPerLine) {
// Offset
QString line = QString("%1 ").arg(i, 8, 16, QChar('0')).toUpper();
// Hex bytes
QString hexPart;
QString asciiPart;
for (int j = 0; j < mBytesPerLine; ++j) {
int idx = i + j;
if (idx < bytesToShow) {
unsigned char byte = data[idx];
hexPart += QString("%1 ").arg(byte, 2, 16, QChar('0')).toUpper();
// ASCII representation
if (byte >= 32 && byte < 127) {
asciiPart += QChar(byte);
} else {
asciiPart += '.';
}
} else {
hexPart += " ";
asciiPart += ' ';
}
// Add extra space in middle for readability
if (j == 7) {
hexPart += ' ';
}
}
line += hexPart + " |" + asciiPart + "|";
hexText += line + "\n";
}
// Add truncation notice if needed
if (mData.size() > mPreviewBytes) {
hexText += QString("\n... (%1 more bytes not shown)")
.arg(mData.size() - mPreviewBytes);
}
mHexPreview->setPlainText(hexText);
}

38
app/binaryexportdialog.h Normal file
View File

@ -0,0 +1,38 @@
#ifndef BINARYEXPORTDIALOG_H
#define BINARYEXPORTDIALOG_H
#include "exportdialog.h"
class QPlainTextEdit;
class QSpinBox;
/**
* @brief Export dialog for binary/raw data with hex preview.
*
* Shows a hex dump preview of the data and provides options
* for raw binary export.
*/
class BinaryExportDialog : public ExportDialog
{
Q_OBJECT
public:
explicit BinaryExportDialog(QWidget *parent = nullptr);
protected:
void setupPreview() override;
void updatePreview() override;
QStringList supportedFormats() const override;
private:
void updateHexPreview();
QPlainTextEdit* mHexPreview;
QLabel* mSizeLabel;
// Preview settings
int mBytesPerLine;
int mPreviewBytes; // How many bytes to show in preview
};
#endif // BINARYEXPORTDIALOG_H

221
app/exportdialog.cpp Normal file
View File

@ -0,0 +1,221 @@
#include "exportdialog.h"
#include "exportmanager.h"
#include "settings.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGridLayout>
#include <QComboBox>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QCheckBox>
#include <QDialogButtonBox>
#include <QGroupBox>
#include <QFileDialog>
#include <QFileInfo>
#include <QStandardPaths>
ExportDialog::ExportDialog(ContentType type, QWidget *parent)
: QDialog(parent)
, mContentType(type)
, mPreviewContainer(nullptr)
, mOptionsContainer(nullptr)
, mFormatCombo(nullptr)
, mOutputPath(nullptr)
, mBrowseButton(nullptr)
, mButtonBox(nullptr)
, mRememberSettings(nullptr)
, mInfoLabel(nullptr)
{
// Set dialog title based on content type
QString title;
switch (type) {
case Image: title = "Export Image"; break;
case Audio: title = "Export Audio"; break;
case Video: title = "Export Video"; break;
case Text: title = "Export Text"; break;
case Binary: title = "Export Data"; break;
}
setWindowTitle(title);
setMinimumWidth(500);
setModal(true);
setupCommonUI();
}
void ExportDialog::setupCommonUI()
{
mMainLayout = new QVBoxLayout(this);
// Content area: preview on left, options on right
mContentLayout = new QHBoxLayout();
// Left side: preview container
mLeftLayout = new QVBoxLayout();
mPreviewContainer = new QWidget(this);
mPreviewContainer->setMinimumSize(256, 256);
mPreviewContainer->setMaximumSize(300, 300);
mPreviewContainer->setStyleSheet("background-color: #1e1e1e; border: 1px solid #3e3e3e;");
mLeftLayout->addWidget(mPreviewContainer);
// Info label below preview
mInfoLabel = new QLabel(this);
mInfoLabel->setAlignment(Qt::AlignCenter);
mLeftLayout->addWidget(mInfoLabel);
mLeftLayout->addStretch();
mContentLayout->addLayout(mLeftLayout);
// Right side: format and options
mRightLayout = new QVBoxLayout();
// Format selection
QHBoxLayout* formatLayout = new QHBoxLayout();
QLabel* formatLabel = new QLabel("Format:", this);
mFormatCombo = new QComboBox(this);
formatLayout->addWidget(formatLabel);
formatLayout->addWidget(mFormatCombo, 1);
mRightLayout->addLayout(formatLayout);
// Options container (subclasses add format-specific options here)
mOptionsContainer = new QGroupBox("Options", this);
QVBoxLayout* optionsLayout = new QVBoxLayout(mOptionsContainer);
optionsLayout->setContentsMargins(8, 8, 8, 8);
mRightLayout->addWidget(mOptionsContainer);
mRightLayout->addStretch();
mContentLayout->addLayout(mRightLayout, 1);
mMainLayout->addLayout(mContentLayout);
// Output path
QHBoxLayout* pathLayout = new QHBoxLayout();
QLabel* outputLabel = new QLabel("Output:", this);
mOutputPath = new QLineEdit(this);
mBrowseButton = new QPushButton("Browse...", this);
pathLayout->addWidget(outputLabel);
pathLayout->addWidget(mOutputPath, 1);
pathLayout->addWidget(mBrowseButton);
mMainLayout->addLayout(pathLayout);
// Remember settings checkbox
mRememberSettings = new QCheckBox("Remember these settings", this);
mRememberSettings->setChecked(Settings::instance().exportRememberSettings());
mMainLayout->addWidget(mRememberSettings);
// Dialog buttons
mButtonBox = new QDialogButtonBox(QDialogButtonBox::Cancel | QDialogButtonBox::Ok, this);
mButtonBox->button(QDialogButtonBox::Ok)->setText("Export");
mMainLayout->addWidget(mButtonBox);
// Connect signals
connect(mBrowseButton, &QPushButton::clicked, this, &ExportDialog::onBrowseClicked);
connect(mFormatCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &ExportDialog::onFormatComboChanged);
connect(mButtonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(mButtonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
}
void ExportDialog::setData(const QByteArray& data, const QString& suggestedName)
{
mData = data;
mSuggestedName = suggestedName;
// Set default output path
QString dir;
switch (mContentType) {
case Image:
dir = ExportManager::instance().lastExportDir("image");
if (dir.isEmpty()) dir = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
break;
case Audio:
dir = ExportManager::instance().lastExportDir("audio");
if (dir.isEmpty()) dir = QStandardPaths::writableLocation(QStandardPaths::MusicLocation);
break;
default:
dir = ExportManager::instance().lastExportDir("raw");
if (dir.isEmpty()) dir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
break;
}
// Get base name without extension
QString baseName = QFileInfo(suggestedName).baseName();
if (baseName.isEmpty()) baseName = "export";
// Get default format
QString format = mFormatCombo->currentText().toLower();
if (format.isEmpty() && mFormatCombo->count() > 0) {
format = mFormatCombo->itemText(0).toLower();
}
mOutputPath->setText(dir + "/" + baseName + "." + format);
// Update preview (subclass implementation)
updatePreview();
}
void ExportDialog::setMetadata(const QVariantMap& metadata)
{
mMetadata = metadata;
}
QString ExportDialog::selectedFormat() const
{
return mFormatCombo->currentText().toLower();
}
QString ExportDialog::outputPath() const
{
return mOutputPath->text();
}
bool ExportDialog::rememberSettings() const
{
return mRememberSettings->isChecked();
}
void ExportDialog::onFormatChanged(const QString& format)
{
// Update output path extension
QString path = mOutputPath->text();
QFileInfo fi(path);
QString newPath = fi.path() + "/" + fi.baseName() + "." + format.toLower();
mOutputPath->setText(newPath);
}
void ExportDialog::onBrowseClicked()
{
QString filter;
QString format = selectedFormat();
// Build filter based on content type
switch (mContentType) {
case Image:
filter = QString("%1 Files (*.%2)").arg(format.toUpper()).arg(format);
break;
case Audio:
filter = QString("%1 Files (*.%2)").arg(format.toUpper()).arg(format);
break;
case Text:
filter = "Text Files (*.txt);;All Files (*)";
break;
case Binary:
default:
filter = "Binary Files (*.bin *.dat);;All Files (*)";
break;
}
QString path = QFileDialog::getSaveFileName(this, "Export", mOutputPath->text(), filter);
if (!path.isEmpty()) {
mOutputPath->setText(path);
}
}
void ExportDialog::onFormatComboChanged(int index)
{
Q_UNUSED(index);
QString format = mFormatCombo->currentText();
onFormatChanged(format);
}

100
app/exportdialog.h Normal file
View File

@ -0,0 +1,100 @@
#ifndef EXPORTDIALOG_H
#define EXPORTDIALOG_H
#include <QDialog>
#include <QVariantMap>
#include <QByteArray>
class QComboBox;
class QLabel;
class QDialogButtonBox;
class QPushButton;
class QCheckBox;
class QLineEdit;
class QVBoxLayout;
class QHBoxLayout;
class QGroupBox;
/**
* @brief Base class for media-specific export dialogs.
*
* Provides common UI elements and functionality for exporting different
* content types (images, audio, binary, etc.)
*/
class ExportDialog : public QDialog
{
Q_OBJECT
public:
enum ContentType {
Image,
Audio,
Video,
Text,
Binary
};
explicit ExportDialog(ContentType type, QWidget *parent = nullptr);
virtual ~ExportDialog() = default;
// Set data and metadata to export
void setData(const QByteArray& data, const QString& suggestedName);
void setMetadata(const QVariantMap& metadata);
// Get export settings
QString selectedFormat() const;
QString outputPath() const;
bool rememberSettings() const;
// Content type
ContentType contentType() const { return mContentType; }
protected:
// Subclasses implement these
virtual void setupPreview() = 0;
virtual void updatePreview() = 0;
virtual QStringList supportedFormats() const = 0;
virtual void onFormatChanged(const QString& format);
// Setup common UI elements
void setupCommonUI();
// Get the preview area (subclasses add their preview widget here)
QWidget* previewContainer() const { return mPreviewContainer; }
// Get the options area (subclasses add format-specific options here)
QGroupBox* optionsContainer() const { return mOptionsContainer; }
// Access common widgets
QComboBox* formatCombo() const { return mFormatCombo; }
QLineEdit* outputPathEdit() const { return mOutputPath; }
// Member data
ContentType mContentType;
QByteArray mData;
QString mSuggestedName;
QVariantMap mMetadata;
private slots:
void onBrowseClicked();
void onFormatComboChanged(int index);
private:
// Common UI elements
QWidget* mPreviewContainer;
QGroupBox* mOptionsContainer;
QComboBox* mFormatCombo;
QLineEdit* mOutputPath;
QPushButton* mBrowseButton;
QDialogButtonBox* mButtonBox;
QCheckBox* mRememberSettings;
QLabel* mInfoLabel;
// Layouts
QVBoxLayout* mMainLayout;
QHBoxLayout* mContentLayout;
QVBoxLayout* mLeftLayout;
QVBoxLayout* mRightLayout;
};
#endif // EXPORTDIALOG_H

764
app/exportmanager.cpp Normal file
View File

@ -0,0 +1,764 @@
#include "exportmanager.h"
#include "settings.h"
#include "imageexportdialog.h"
#include "audioexportdialog.h"
#include "binaryexportdialog.h"
#include "batchexportdialog.h"
#include "../libs/dsl/dslkeys.h"
#include <QFileDialog>
#include <QMessageBox>
#include <QClipboard>
#include <QApplication>
#include <QMimeData>
#include <QProcess>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QStandardPaths>
#include <QTemporaryFile>
#include <QImageReader>
ExportManager& ExportManager::instance() {
static ExportManager instance;
return instance;
}
ExportManager::ExportManager(QObject* parent)
: QObject(parent)
{
}
ExportManager::ContentType ExportManager::detectContentType(const QVariantMap& vars) const {
// Check for viewer type in vars
if (DslKeys::contains(vars, DslKey::Viewer)) {
QString viewer = DslKeys::get(vars, DslKey::Viewer).toString();
if (viewer == "image") return Image;
if (viewer == "audio") return Audio;
if (viewer == "text") return Text;
if (viewer == "hex") return Binary;
}
return Unknown;
}
ExportManager::ContentType ExportManager::detectContentType(const QByteArray& data, const QString& filename) const {
QString ext = QFileInfo(filename).suffix().toLower();
// Check by extension
QStringList imageExts = {"png", "jpg", "jpeg", "bmp", "tga", "dds", "tiff", "tif", "webp", "gif"};
QStringList audioExts = {"wav", "mp3", "ogg", "flac", "aiff", "wma", "m4a"};
QStringList textExts = {"txt", "xml", "json", "csv", "ini", "cfg", "log", "md", "htm", "html"};
if (imageExts.contains(ext)) return Image;
if (audioExts.contains(ext)) return Audio;
if (textExts.contains(ext)) return Text;
// Check by magic bytes
if (data.size() >= 4) {
// PNG
if (data.startsWith("\x89PNG")) return Image;
// JPEG
if (data.startsWith("\xFF\xD8\xFF")) return Image;
// BMP
if (data.startsWith("BM")) return Image;
// GIF
if (data.startsWith("GIF8")) return Image;
// RIFF (WAV)
if (data.startsWith("RIFF") && data.size() >= 12 && data.mid(8, 4) == "WAVE") return Audio;
// OGG
if (data.startsWith("OggS")) return Audio;
// FLAC
if (data.startsWith("fLaC")) return Audio;
// ID3 (MP3)
if (data.startsWith("ID3") || (data.size() >= 2 && (uchar)data[0] == 0xFF && ((uchar)data[1] & 0xE0) == 0xE0)) return Audio;
}
// Check if it looks like text
bool looksLikeText = true;
int checkLen = qMin(data.size(), 1024);
for (int i = 0; i < checkLen && looksLikeText; ++i) {
char c = data[i];
if (c < 0x09 || (c > 0x0D && c < 0x20 && c != 0x1B)) {
looksLikeText = false;
}
}
if (looksLikeText && !data.isEmpty()) return Text;
return Binary;
}
bool ExportManager::exportRawData(const QByteArray& data, const QString& suggestedName, QWidget* parent) {
QString baseName = QFileInfo(suggestedName).baseName();
if (baseName.isEmpty()) baseName = "export";
QString dir = lastExportDir("raw");
if (dir.isEmpty()) dir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
QString savePath = QFileDialog::getSaveFileName(parent, "Export Raw Data",
dir + "/" + baseName + ".bin",
"Binary Files (*.bin *.dat);;All Files (*)");
if (savePath.isEmpty()) return false;
setLastExportDir("raw", QFileInfo(savePath).path());
QFile file(savePath);
if (!file.open(QIODevice::WriteOnly)) {
emit exportError(QString("Failed to open file for writing: %1").arg(file.errorString()));
return false;
}
file.write(data);
file.close();
emit exportCompleted(savePath, true);
return true;
}
bool ExportManager::exportImage(const QImage& image, const QString& format, const QString& suggestedName, QWidget* parent) {
if (image.isNull()) {
emit exportError("Cannot export null image");
return false;
}
QString ext = format.toLower();
QString filter = getFilterForFormat(ext);
QString baseName = QFileInfo(suggestedName).baseName();
if (baseName.isEmpty()) baseName = "image";
QString dir = lastExportDir("image");
if (dir.isEmpty()) dir = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
QString savePath = QFileDialog::getSaveFileName(parent, "Export Image",
dir + "/" + baseName + "." + ext, filter);
if (savePath.isEmpty()) return false;
setLastExportDir("image", QFileInfo(savePath).path());
bool success = false;
if (ext == "tga") {
success = saveTGA(image, savePath);
} else {
// Qt handles png, jpg, bmp, tiff natively
success = image.save(savePath, ext.toUpper().toUtf8().constData());
}
if (!success) {
emit exportError(QString("Failed to save image as %1").arg(ext.toUpper()));
return false;
}
emit exportCompleted(savePath, true);
return true;
}
bool ExportManager::exportAudio(const QByteArray& wavData, const QString& format, const QString& suggestedName, QWidget* parent) {
if (wavData.isEmpty()) {
emit exportError("No audio data to export");
return false;
}
QString ext = format.toLower();
QString baseName = QFileInfo(suggestedName).baseName();
if (baseName.isEmpty()) baseName = "audio";
QString dir = lastExportDir("audio");
if (dir.isEmpty()) dir = QStandardPaths::writableLocation(QStandardPaths::MusicLocation);
QString filter;
if (ext == "wav") filter = "WAV Files (*.wav)";
else if (ext == "mp3") filter = "MP3 Files (*.mp3)";
else if (ext == "ogg") filter = "OGG Files (*.ogg)";
else if (ext == "flac") filter = "FLAC Files (*.flac)";
else filter = "All Files (*)";
QString savePath = QFileDialog::getSaveFileName(parent, "Export Audio",
dir + "/" + baseName + "." + ext, filter);
if (savePath.isEmpty()) return false;
setLastExportDir("audio", QFileInfo(savePath).path());
if (ext == "wav") {
// Direct write for WAV
QFile file(savePath);
if (!file.open(QIODevice::WriteOnly)) {
emit exportError(QString("Failed to open file: %1").arg(file.errorString()));
return false;
}
file.write(wavData);
file.close();
emit exportCompleted(savePath, true);
return true;
}
// Need FFmpeg for MP3/OGG/FLAC conversion
if (!hasFFmpeg()) {
QMessageBox::warning(parent, "FFmpeg Required",
QString("FFmpeg is required to export audio as %1.\n\n"
"Install FFmpeg and ensure it's in your system PATH,\n"
"or configure the path in Edit > Preferences > Tools.").arg(ext.toUpper()));
return false;
}
// Write temp WAV file
QTemporaryFile tempWav;
tempWav.setFileTemplate(QDir::temp().filePath("xplor_XXXXXX.wav"));
if (!tempWav.open()) {
emit exportError("Failed to create temporary file");
return false;
}
tempWav.write(wavData);
QString tempPath = tempWav.fileName();
tempWav.close();
bool success = convertWithFFmpeg(tempPath, savePath, ext);
// Cleanup temp file
QFile::remove(tempPath);
if (success) {
emit exportCompleted(savePath, true);
}
return success;
}
bool ExportManager::exportText(const QByteArray& data, const QString& suggestedName, QWidget* parent) {
QString baseName = QFileInfo(suggestedName).baseName();
if (baseName.isEmpty()) baseName = "text";
QString dir = lastExportDir("text");
if (dir.isEmpty()) dir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
QString savePath = QFileDialog::getSaveFileName(parent, "Export Text",
dir + "/" + baseName + ".txt",
"Text Files (*.txt);;All Files (*)");
if (savePath.isEmpty()) return false;
setLastExportDir("text", QFileInfo(savePath).path());
QFile file(savePath);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
emit exportError(QString("Failed to open file: %1").arg(file.errorString()));
return false;
}
file.write(data);
file.close();
emit exportCompleted(savePath, true);
return true;
}
bool ExportManager::exportWithDialog(const QByteArray& data, const QString& name,
ContentType type, QWidget* parent) {
switch (type) {
case Image: {
QImage image;
if (image.loadFromData(data)) {
return exportImageWithDialog(image, name, parent);
}
// Fall back to binary export
break;
}
case Audio: {
AudioExportDialog dialog(parent);
dialog.setData(data, name);
if (dialog.exec() == QDialog::Accepted) {
QString format = dialog.selectedFormat();
QString path = dialog.outputPath();
if (dialog.rememberSettings()) {
Settings::instance().setDefaultAudioExportFormat(format);
Settings::instance().setAudioMp3Bitrate(dialog.mp3Bitrate());
Settings::instance().setAudioOggQuality(dialog.oggQuality());
Settings::instance().setAudioFlacCompression(dialog.flacCompression());
}
// Export based on format
if (format == "wav") {
QFile file(path);
if (file.open(QIODevice::WriteOnly)) {
file.write(data);
file.close();
emit exportCompleted(path, true);
return true;
}
} else {
// Need FFmpeg conversion
QTemporaryFile tempWav;
tempWav.setFileTemplate(QDir::temp().filePath("xplor_XXXXXX.wav"));
if (tempWav.open()) {
tempWav.write(data);
QString tempPath = tempWav.fileName();
tempWav.close();
bool success = convertWithFFmpeg(tempPath, path, format);
QFile::remove(tempPath);
if (success) {
emit exportCompleted(path, true);
return true;
}
}
}
}
return false;
}
case Text:
return exportText(data, name, parent);
case Binary:
case Unknown:
default:
break;
}
// Binary export dialog
BinaryExportDialog dialog(parent);
dialog.setData(data, name);
if (dialog.exec() == QDialog::Accepted) {
QString path = dialog.outputPath();
QFile file(path);
if (file.open(QIODevice::WriteOnly)) {
file.write(data);
file.close();
emit exportCompleted(path, true);
return true;
}
}
return false;
}
bool ExportManager::exportImageWithDialog(const QImage& image, const QString& name, QWidget* parent) {
if (image.isNull()) {
emit exportError("Cannot export null image");
return false;
}
ImageExportDialog dialog(parent);
dialog.setImage(image, name);
if (dialog.exec() == QDialog::Accepted) {
QString format = dialog.selectedFormat();
QString path = dialog.outputPath();
if (dialog.rememberSettings()) {
Settings::instance().setDefaultImageExportFormat(format);
Settings::instance().setImageJpegQuality(dialog.jpegQuality());
Settings::instance().setImagePngCompression(dialog.pngCompression());
}
setLastExportDir("image", QFileInfo(path).path());
bool success = false;
if (format == "tga") {
success = saveTGA(image, path);
} else if (format == "jpg" || format == "jpeg") {
success = image.save(path, "JPEG", dialog.jpegQuality());
} else if (format == "png") {
// Qt doesn't support PNG compression level directly in save()
// We'd need to use QImageWriter for that
success = image.save(path, "PNG");
} else {
success = image.save(path, format.toUpper().toUtf8().constData());
}
if (success) {
emit exportCompleted(path, true);
return true;
} else {
emit exportError(QString("Failed to save image as %1").arg(format.toUpper()));
}
}
return false;
}
bool ExportManager::quickExport(const QByteArray& data, const QString& name,
ContentType type, QWidget* parent) {
QString baseName = QFileInfo(name).baseName();
if (baseName.isEmpty()) baseName = "export";
QString dir, format, filter;
switch (type) {
case Image:
dir = lastExportDir("image");
if (dir.isEmpty()) dir = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
format = Settings::instance().defaultImageExportFormat();
filter = getFilterForFormat(format);
break;
case Audio:
dir = lastExportDir("audio");
if (dir.isEmpty()) dir = QStandardPaths::writableLocation(QStandardPaths::MusicLocation);
format = Settings::instance().defaultAudioExportFormat();
filter = QString("%1 Files (*.%2)").arg(format.toUpper()).arg(format);
break;
case Text:
dir = lastExportDir("text");
if (dir.isEmpty()) dir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
format = "txt";
filter = "Text Files (*.txt)";
break;
default:
dir = lastExportDir("raw");
if (dir.isEmpty()) dir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
format = "bin";
filter = "Binary Files (*.bin)";
break;
}
QString savePath = QFileDialog::getSaveFileName(parent, "Quick Export",
dir + "/" + baseName + "." + format, filter);
if (savePath.isEmpty()) return false;
// Handle type-specific export
switch (type) {
case Image: {
QImage image;
if (image.loadFromData(data)) {
setLastExportDir("image", QFileInfo(savePath).path());
QString ext = QFileInfo(savePath).suffix().toLower();
if (ext == "tga") {
return saveTGA(image, savePath);
}
int quality = (ext == "jpg" || ext == "jpeg") ?
Settings::instance().imageJpegQuality() : -1;
if (image.save(savePath, ext.toUpper().toUtf8().constData(), quality)) {
emit exportCompleted(savePath, true);
return true;
}
}
break;
}
case Audio: {
setLastExportDir("audio", QFileInfo(savePath).path());
QString ext = QFileInfo(savePath).suffix().toLower();
if (ext == "wav") {
QFile file(savePath);
if (file.open(QIODevice::WriteOnly)) {
file.write(data);
file.close();
emit exportCompleted(savePath, true);
return true;
}
} else {
// FFmpeg conversion
QTemporaryFile tempWav;
tempWav.setFileTemplate(QDir::temp().filePath("xplor_XXXXXX.wav"));
if (tempWav.open()) {
tempWav.write(data);
QString tempPath = tempWav.fileName();
tempWav.close();
bool success = convertWithFFmpeg(tempPath, savePath, ext);
QFile::remove(tempPath);
if (success) {
emit exportCompleted(savePath, true);
return true;
}
}
}
break;
}
default: {
setLastExportDir("raw", QFileInfo(savePath).path());
QFile file(savePath);
if (file.open(QIODevice::WriteOnly)) {
file.write(data);
file.close();
emit exportCompleted(savePath, true);
return true;
}
break;
}
}
return false;
}
bool ExportManager::quickExportImage(const QImage& image, const QString& name, QWidget* parent) {
if (image.isNull()) return false;
QString baseName = QFileInfo(name).baseName();
if (baseName.isEmpty()) baseName = "image";
QString dir = lastExportDir("image");
if (dir.isEmpty()) dir = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
QString format = Settings::instance().defaultImageExportFormat();
QString filter = getFilterForFormat(format);
QString savePath = QFileDialog::getSaveFileName(parent, "Quick Export Image",
dir + "/" + baseName + "." + format, filter);
if (savePath.isEmpty()) return false;
setLastExportDir("image", QFileInfo(savePath).path());
QString ext = QFileInfo(savePath).suffix().toLower();
if (ext == "tga") {
if (saveTGA(image, savePath)) {
emit exportCompleted(savePath, true);
return true;
}
return false;
}
int quality = (ext == "jpg" || ext == "jpeg") ?
Settings::instance().imageJpegQuality() : -1;
if (image.save(savePath, ext.toUpper().toUtf8().constData(), quality)) {
emit exportCompleted(savePath, true);
return true;
}
return false;
}
bool ExportManager::batchExport(const QList<BatchExportItem>& items, QWidget* parent) {
if (items.isEmpty()) return false;
BatchExportDialog dialog(parent);
dialog.setItems(items);
if (dialog.exec() != QDialog::Accepted) return false;
QList<BatchExportItem> selected = dialog.selectedItems();
if (selected.isEmpty()) return false;
QString outputDir = dialog.outputDirectory();
bool preserveStructure = dialog.preserveStructure();
QString conflictResolution = dialog.conflictResolution();
QString imageFormat = dialog.imageFormat();
QString audioFormat = dialog.audioFormat();
int succeeded = 0, failed = 0, skipped = 0;
for (int i = 0; i < selected.size(); ++i) {
const BatchExportItem& item = selected[i];
emit batchExportProgress(i + 1, selected.size(), item.name);
// Determine output path
QString relativePath = preserveStructure ? item.path : item.name;
QString ext;
switch (item.contentType) {
case Image: ext = imageFormat; break;
case Audio: ext = audioFormat; break;
case Text: ext = "txt"; break;
default: ext = "bin"; break;
}
QString baseName = QFileInfo(relativePath).baseName();
QString subDir = preserveStructure ? QFileInfo(relativePath).path() : "";
QString targetDir = subDir.isEmpty() ? outputDir : outputDir + "/" + subDir;
QString targetPath = targetDir + "/" + baseName + "." + ext;
// Ensure directory exists
QDir().mkpath(targetDir);
// Handle conflicts
if (QFile::exists(targetPath)) {
if (conflictResolution == "skip") {
skipped++;
continue;
} else if (conflictResolution == "number") {
int num = 1;
QString newPath;
do {
newPath = targetDir + "/" + baseName + QString("_%1.").arg(num++) + ext;
} while (QFile::exists(newPath) && num < 1000);
targetPath = newPath;
}
// "overwrite" - just proceed
}
// Export based on type
bool success = false;
switch (item.contentType) {
case Image: {
QImage image;
if (image.loadFromData(item.data)) {
if (ext == "tga") {
success = saveTGA(image, targetPath);
} else {
int quality = (ext == "jpg") ? Settings::instance().imageJpegQuality() : -1;
success = image.save(targetPath, ext.toUpper().toUtf8().constData(), quality);
}
}
break;
}
case Audio:
if (ext == "wav") {
QFile file(targetPath);
if (file.open(QIODevice::WriteOnly)) {
file.write(item.data);
file.close();
success = true;
}
} else {
QTemporaryFile tempWav;
tempWav.setFileTemplate(QDir::temp().filePath("xplor_XXXXXX.wav"));
if (tempWav.open()) {
tempWav.write(item.data);
QString tempPath = tempWav.fileName();
tempWav.close();
success = convertWithFFmpeg(tempPath, targetPath, ext);
QFile::remove(tempPath);
}
}
break;
default: {
QFile file(targetPath);
if (file.open(QIODevice::WriteOnly)) {
file.write(item.data);
file.close();
success = true;
}
break;
}
}
if (success) {
succeeded++;
} else {
failed++;
}
}
emit batchExportCompleted(succeeded, failed, skipped);
return failed == 0;
}
QStringList ExportManager::supportedImageFormats() const {
return {"png", "jpg", "bmp", "tiff", "tga"};
}
QStringList ExportManager::supportedAudioFormats() const {
return {"wav", "mp3", "ogg", "flac"};
}
bool ExportManager::hasFFmpeg() const {
if (!m_ffmpegChecked) {
m_cachedFFmpegPath = findFFmpegPath();
m_ffmpegChecked = true;
}
return !m_cachedFFmpegPath.isEmpty();
}
void ExportManager::copyImageToClipboard(const QImage& image) {
if (image.isNull()) return;
QApplication::clipboard()->setImage(image);
}
void ExportManager::copyTextToClipboard(const QString& text) {
QApplication::clipboard()->setText(text);
}
void ExportManager::copyBinaryAsHex(const QByteArray& data) {
QString hex = data.toHex(' ').toUpper();
QApplication::clipboard()->setText(hex);
}
QString ExportManager::lastExportDir(const QString& category) const {
return m_lastDirs.value(category);
}
void ExportManager::setLastExportDir(const QString& category, const QString& dir) {
m_lastDirs[category] = dir;
}
bool ExportManager::saveTGA(const QImage& image, const QString& path) {
QFile file(path);
if (!file.open(QIODevice::WriteOnly)) return false;
QImage img = image.convertToFormat(QImage::Format_ARGB32);
// TGA Header (18 bytes)
QByteArray header(18, 0);
header[2] = 2; // Uncompressed true-color
header[12] = img.width() & 0xFF;
header[13] = (img.width() >> 8) & 0xFF;
header[14] = img.height() & 0xFF;
header[15] = (img.height() >> 8) & 0xFF;
header[16] = 32; // 32 bits per pixel
header[17] = 0x20; // Top-left origin
file.write(header);
// Write BGRA pixels (TGA uses BGRA order)
for (int y = 0; y < img.height(); ++y) {
for (int x = 0; x < img.width(); ++x) {
QRgb pixel = img.pixel(x, y);
char bgra[4] = {
static_cast<char>(qBlue(pixel)),
static_cast<char>(qGreen(pixel)),
static_cast<char>(qRed(pixel)),
static_cast<char>(qAlpha(pixel))
};
file.write(bgra, 4);
}
}
return true;
}
QString ExportManager::getFilterForFormat(const QString& format) const {
QString ext = format.toLower();
if (ext == "png") return "PNG Images (*.png)";
if (ext == "jpg" || ext == "jpeg") return "JPEG Images (*.jpg *.jpeg)";
if (ext == "bmp") return "BMP Images (*.bmp)";
if (ext == "tiff" || ext == "tif") return "TIFF Images (*.tiff *.tif)";
if (ext == "tga") return "TGA Images (*.tga)";
if (ext == "webp") return "WebP Images (*.webp)";
return "All Files (*)";
}
bool ExportManager::convertWithFFmpeg(const QString& inputPath, const QString& outputPath, const QString& format) {
QString ffmpegPath = m_cachedFFmpegPath;
if (ffmpegPath.isEmpty()) {
ffmpegPath = findFFmpegPath();
}
if (ffmpegPath.isEmpty()) {
emit exportError("FFmpeg not found");
return false;
}
QProcess ffmpeg;
QStringList args;
args << "-y"; // Overwrite output
args << "-i" << inputPath;
// Format-specific encoding settings
if (format == "mp3") {
args << "-codec:a" << "libmp3lame" << "-qscale:a" << "2";
} else if (format == "ogg") {
args << "-codec:a" << "libvorbis" << "-qscale:a" << "5";
} else if (format == "flac") {
args << "-codec:a" << "flac";
}
args << outputPath;
ffmpeg.start(ffmpegPath, args);
if (!ffmpeg.waitForFinished(30000)) {
emit exportError("FFmpeg conversion timed out");
return false;
}
if (ffmpeg.exitCode() != 0) {
QString errorOutput = QString::fromUtf8(ffmpeg.readAllStandardError());
emit exportError(QString("FFmpeg conversion failed: %1").arg(errorOutput.left(200)));
return false;
}
return true;
}
QString ExportManager::findFFmpegPath() const {
// Use Settings' ffmpegPath which handles auto-detection
return Settings::instance().ffmpegPath();
}

101
app/exportmanager.h Normal file
View File

@ -0,0 +1,101 @@
#ifndef EXPORTMANAGER_H
#define EXPORTMANAGER_H
#include <QObject>
#include <QImage>
#include <QVariantMap>
#include <QStringList>
class QWidget;
class QTreeWidgetItem;
struct BatchExportItem;
/**
* @brief The ExportManager class handles all export operations for tree items.
*
* This singleton provides centralized export functionality with format-specific
* options for images, audio, text, and raw binary data.
*/
class ExportManager : public QObject {
Q_OBJECT
public:
enum ContentType {
Unknown,
Image,
Audio,
Video,
Text,
Binary
};
static ExportManager& instance();
// Content type detection
ContentType detectContentType(const QVariantMap& vars) const;
ContentType detectContentType(const QByteArray& data, const QString& filename) const;
// Dialog-based export (shows full options dialog)
bool exportWithDialog(const QByteArray& data, const QString& name,
ContentType type, QWidget* parent);
bool exportImageWithDialog(const QImage& image, const QString& name, QWidget* parent);
// Quick export (uses saved defaults, minimal UI - just file picker)
bool quickExport(const QByteArray& data, const QString& name,
ContentType type, QWidget* parent);
bool quickExportImage(const QImage& image, const QString& name, QWidget* parent);
// Batch export (shows batch dialog for multiple items)
bool batchExport(const QList<BatchExportItem>& items, QWidget* parent);
// Legacy export methods (direct export with file dialog)
bool exportRawData(const QByteArray& data, const QString& suggestedName, QWidget* parent);
bool exportImage(const QImage& image, const QString& format, const QString& suggestedName, QWidget* parent);
bool exportAudio(const QByteArray& wavData, const QString& format, const QString& suggestedName, QWidget* parent);
bool exportText(const QByteArray& data, const QString& suggestedName, QWidget* parent);
// Format support
QStringList supportedImageFormats() const;
QStringList supportedAudioFormats() const;
bool hasFFmpeg() const;
// Clipboard operations
void copyImageToClipboard(const QImage& image);
void copyTextToClipboard(const QString& text);
void copyBinaryAsHex(const QByteArray& data);
// Last directory tracking
QString lastExportDir(const QString& category) const;
void setLastExportDir(const QString& category, const QString& dir);
signals:
void exportCompleted(const QString& path, bool success);
void exportError(const QString& error);
// Batch export signals
void batchExportProgress(int current, int total, const QString& currentItem);
void batchExportCompleted(int succeeded, int failed, int skipped);
private:
explicit ExportManager(QObject* parent = nullptr);
~ExportManager() = default;
// Disable copy
ExportManager(const ExportManager&) = delete;
ExportManager& operator=(const ExportManager&) = delete;
// Image format helpers
bool saveTGA(const QImage& image, const QString& path);
QString getFilterForFormat(const QString& format) const;
// Audio conversion
bool convertWithFFmpeg(const QString& inputPath, const QString& outputPath, const QString& format);
QString findFFmpegPath() const;
// State
QMap<QString, QString> m_lastDirs;
mutable QString m_cachedFFmpegPath;
mutable bool m_ffmpegChecked = false;
};
#endif // EXPORTMANAGER_H

282
app/imageexportdialog.cpp Normal file
View File

@ -0,0 +1,282 @@
#include "imageexportdialog.h"
#include "settings.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QSlider>
#include <QSpinBox>
#include <QComboBox>
#include <QStackedWidget>
#include <QGroupBox>
#include <QFileInfo>
ImageExportDialog::ImageExportDialog(QWidget *parent)
: ExportDialog(ContentType::Image, parent)
, mPreviewLabel(nullptr)
, mOptionsStack(nullptr)
, mJpegOptionsWidget(nullptr)
, mJpegQualitySlider(nullptr)
, mJpegQualitySpinBox(nullptr)
, mPngOptionsWidget(nullptr)
, mPngCompressionSlider(nullptr)
, mPngCompressionSpinBox(nullptr)
, mNoOptionsWidget(nullptr)
{
// Populate format combo
for (const QString& fmt : supportedFormats()) {
formatCombo()->addItem(fmt.toUpper());
}
// Set default format from settings
QString defaultFormat = Settings::instance().defaultImageExportFormat().toUpper();
int index = formatCombo()->findText(defaultFormat);
if (index >= 0) {
formatCombo()->setCurrentIndex(index);
}
setupPreview();
}
void ImageExportDialog::setupPreview()
{
// Create preview label inside preview container
QVBoxLayout* previewLayout = new QVBoxLayout(previewContainer());
previewLayout->setContentsMargins(0, 0, 0, 0);
mPreviewLabel = new QLabel(previewContainer());
mPreviewLabel->setAlignment(Qt::AlignCenter);
mPreviewLabel->setMinimumSize(256, 256);
mPreviewLabel->setText("No image loaded");
mPreviewLabel->setStyleSheet("color: #808080;");
previewLayout->addWidget(mPreviewLabel);
// Create stacked widget for format-specific options
mOptionsStack = new QStackedWidget(this);
// JPEG options
mJpegOptionsWidget = new QWidget(this);
QVBoxLayout* jpegLayout = new QVBoxLayout(mJpegOptionsWidget);
jpegLayout->setContentsMargins(0, 0, 0, 0);
QLabel* jpegQualityLabel = new QLabel("Quality:", mJpegOptionsWidget);
QHBoxLayout* jpegSliderLayout = new QHBoxLayout();
mJpegQualitySlider = new QSlider(Qt::Horizontal, mJpegOptionsWidget);
mJpegQualitySlider->setRange(1, 100);
mJpegQualitySlider->setValue(Settings::instance().imageJpegQuality());
mJpegQualitySpinBox = new QSpinBox(mJpegOptionsWidget);
mJpegQualitySpinBox->setRange(1, 100);
mJpegQualitySpinBox->setValue(Settings::instance().imageJpegQuality());
jpegSliderLayout->addWidget(mJpegQualitySlider);
jpegSliderLayout->addWidget(mJpegQualitySpinBox);
jpegLayout->addWidget(jpegQualityLabel);
jpegLayout->addLayout(jpegSliderLayout);
jpegLayout->addStretch();
connect(mJpegQualitySlider, &QSlider::valueChanged, this, &ImageExportDialog::onJpegQualityChanged);
connect(mJpegQualitySpinBox, QOverload<int>::of(&QSpinBox::valueChanged),
mJpegQualitySlider, &QSlider::setValue);
// PNG options
mPngOptionsWidget = new QWidget(this);
QVBoxLayout* pngLayout = new QVBoxLayout(mPngOptionsWidget);
pngLayout->setContentsMargins(0, 0, 0, 0);
QLabel* pngCompressionLabel = new QLabel("Compression (0=fast, 9=best):", mPngOptionsWidget);
QHBoxLayout* pngSliderLayout = new QHBoxLayout();
mPngCompressionSlider = new QSlider(Qt::Horizontal, mPngOptionsWidget);
mPngCompressionSlider->setRange(0, 9);
mPngCompressionSlider->setValue(Settings::instance().imagePngCompression());
mPngCompressionSpinBox = new QSpinBox(mPngOptionsWidget);
mPngCompressionSpinBox->setRange(0, 9);
mPngCompressionSpinBox->setValue(Settings::instance().imagePngCompression());
pngSliderLayout->addWidget(mPngCompressionSlider);
pngSliderLayout->addWidget(mPngCompressionSpinBox);
pngLayout->addWidget(pngCompressionLabel);
pngLayout->addLayout(pngSliderLayout);
pngLayout->addStretch();
connect(mPngCompressionSlider, &QSlider::valueChanged, this, &ImageExportDialog::onPngCompressionChanged);
connect(mPngCompressionSpinBox, QOverload<int>::of(&QSpinBox::valueChanged),
mPngCompressionSlider, &QSlider::setValue);
// No options widget (for formats without settings)
mNoOptionsWidget = new QWidget(this);
QVBoxLayout* noOptionsLayout = new QVBoxLayout(mNoOptionsWidget);
noOptionsLayout->setContentsMargins(0, 0, 0, 0);
QLabel* noOptionsLabel = new QLabel("No additional options for this format.", mNoOptionsWidget);
noOptionsLabel->setStyleSheet("color: #808080;");
noOptionsLayout->addWidget(noOptionsLabel);
noOptionsLayout->addStretch();
// Add to stacked widget
mOptionsStack->addWidget(mJpegOptionsWidget); // Index 0
mOptionsStack->addWidget(mPngOptionsWidget); // Index 1
mOptionsStack->addWidget(mNoOptionsWidget); // Index 2
// Add stacked widget to options container
QVBoxLayout* optionsLayout = qobject_cast<QVBoxLayout*>(optionsContainer()->layout());
if (optionsLayout) {
optionsLayout->addWidget(mOptionsStack);
}
// Show options for current format
showOptionsForFormat(formatCombo()->currentText());
}
void ImageExportDialog::updatePreview()
{
if (mImage.isNull()) {
mPreviewLabel->setText("No image loaded");
return;
}
// Scale image to fit preview area while maintaining aspect ratio
QSize previewSize = mPreviewLabel->size();
QPixmap pixmap = QPixmap::fromImage(mImage.scaled(
previewSize, Qt::KeepAspectRatio, Qt::SmoothTransformation));
mPreviewLabel->setPixmap(pixmap);
updateFileSizeEstimate();
}
QStringList ImageExportDialog::supportedFormats() const
{
return {"png", "jpg", "bmp", "tiff", "tga"};
}
void ImageExportDialog::setImage(const QImage& image, const QString& suggestedName)
{
mImage = image;
mSuggestedName = suggestedName;
// Update info label with image details
QString info = QString("%1 x %2, %3-bit")
.arg(image.width())
.arg(image.height())
.arg(image.depth());
// Find the info label (it's in the base class)
QList<QLabel*> labels = findChildren<QLabel*>();
for (QLabel* label : labels) {
if (label->objectName().isEmpty() && label != mPreviewLabel) {
// This is likely the info label
label->setText(info);
break;
}
}
// Call base class to set output path
setData(QByteArray(), suggestedName);
updatePreview();
}
void ImageExportDialog::onFormatChanged(const QString& format)
{
ExportDialog::onFormatChanged(format);
showOptionsForFormat(format);
updateFileSizeEstimate();
}
void ImageExportDialog::showOptionsForFormat(const QString& format)
{
QString fmt = format.toLower();
if (fmt == "jpg" || fmt == "jpeg") {
mOptionsStack->setCurrentWidget(mJpegOptionsWidget);
} else if (fmt == "png") {
mOptionsStack->setCurrentWidget(mPngOptionsWidget);
} else {
mOptionsStack->setCurrentWidget(mNoOptionsWidget);
}
}
int ImageExportDialog::jpegQuality() const
{
return mJpegQualitySlider ? mJpegQualitySlider->value() : 90;
}
int ImageExportDialog::pngCompression() const
{
return mPngCompressionSlider ? mPngCompressionSlider->value() : 6;
}
void ImageExportDialog::onJpegQualityChanged(int value)
{
if (mJpegQualitySpinBox) {
mJpegQualitySpinBox->blockSignals(true);
mJpegQualitySpinBox->setValue(value);
mJpegQualitySpinBox->blockSignals(false);
}
updateFileSizeEstimate();
}
void ImageExportDialog::onPngCompressionChanged(int value)
{
if (mPngCompressionSpinBox) {
mPngCompressionSpinBox->blockSignals(true);
mPngCompressionSpinBox->setValue(value);
mPngCompressionSpinBox->blockSignals(false);
}
updateFileSizeEstimate();
}
void ImageExportDialog::updateFileSizeEstimate()
{
if (mImage.isNull()) return;
QString format = selectedFormat();
qint64 estimatedSize = 0;
// Rough size estimates
int pixelCount = mImage.width() * mImage.height();
if (format == "jpg" || format == "jpeg") {
// JPEG: roughly quality/100 * pixelCount * 0.3 bytes
double qualityFactor = jpegQuality() / 100.0;
estimatedSize = static_cast<qint64>(pixelCount * 0.3 * qualityFactor);
} else if (format == "png") {
// PNG: depends on compression and image content, rough estimate
double compressionFactor = 1.0 - (pngCompression() * 0.08);
estimatedSize = static_cast<qint64>(pixelCount * 3 * compressionFactor * 0.5);
} else if (format == "bmp") {
// BMP: uncompressed, 3-4 bytes per pixel + header
estimatedSize = pixelCount * 4 + 54;
} else if (format == "tga") {
// TGA: uncompressed, 4 bytes per pixel + header
estimatedSize = pixelCount * 4 + 18;
} else if (format == "tiff") {
// TIFF: similar to PNG
estimatedSize = static_cast<qint64>(pixelCount * 3 * 0.5);
}
// Format size string
QString sizeStr;
if (estimatedSize > 1024 * 1024) {
sizeStr = QString("Est: ~%1 MB").arg(estimatedSize / (1024.0 * 1024.0), 0, 'f', 1);
} else if (estimatedSize > 1024) {
sizeStr = QString("Est: ~%1 KB").arg(estimatedSize / 1024.0, 0, 'f', 0);
} else {
sizeStr = QString("Est: ~%1 bytes").arg(estimatedSize);
}
// Update info label
QString info = QString("%1 x %2, %3-bit\n%4")
.arg(mImage.width())
.arg(mImage.height())
.arg(mImage.depth())
.arg(sizeStr);
QList<QLabel*> labels = findChildren<QLabel*>();
for (QLabel* label : labels) {
if (label->objectName().isEmpty() && label != mPreviewLabel &&
!label->text().contains("Quality") && !label->text().contains("Compression") &&
!label->text().contains("Format") && !label->text().contains("Output") &&
!label->text().contains("No additional")) {
label->setText(info);
break;
}
}
}

68
app/imageexportdialog.h Normal file
View File

@ -0,0 +1,68 @@
#ifndef IMAGEEXPORTDIALOG_H
#define IMAGEEXPORTDIALOG_H
#include "exportdialog.h"
#include <QImage>
class QSlider;
class QSpinBox;
class QStackedWidget;
/**
* @brief Export dialog for images with preview and format-specific options.
*
* Shows a thumbnail preview of the image and provides quality/compression
* settings for different output formats (PNG, JPEG, TGA, BMP, TIFF).
*/
class ImageExportDialog : public ExportDialog
{
Q_OBJECT
public:
explicit ImageExportDialog(QWidget *parent = nullptr);
// Set image directly (already decoded)
void setImage(const QImage& image, const QString& suggestedName);
// Get configured image for export
QImage exportImage() const { return mImage; }
// Format-specific options
int jpegQuality() const;
int pngCompression() const;
protected:
void setupPreview() override;
void updatePreview() override;
QStringList supportedFormats() const override;
void onFormatChanged(const QString& format) override;
private slots:
void onJpegQualityChanged(int value);
void onPngCompressionChanged(int value);
private:
void updateFileSizeEstimate();
void showOptionsForFormat(const QString& format);
QImage mImage;
QLabel* mPreviewLabel;
// Format-specific option widgets
QStackedWidget* mOptionsStack;
// JPEG options
QWidget* mJpegOptionsWidget;
QSlider* mJpegQualitySlider;
QSpinBox* mJpegQualitySpinBox;
// PNG options
QWidget* mPngOptionsWidget;
QSlider* mPngCompressionSlider;
QSpinBox* mPngCompressionSpinBox;
// No options widget (for TGA, BMP, TIFF)
QWidget* mNoOptionsWidget;
};
#endif // IMAGEEXPORTDIALOG_H

View File

@ -259,6 +259,55 @@ QString Settings::findPython()
return QString();
}
QString Settings::ffmpegPath() const
{
QString path = m_settings.value("Tools/FFmpegPath").toString();
if (path.isEmpty() || !QFileInfo::exists(path)) {
path = findFFmpeg();
}
return path;
}
void Settings::setFFmpegPath(const QString& path)
{
m_settings.setValue("Tools/FFmpegPath", path);
emit settingsChanged();
}
QString Settings::findFFmpeg()
{
// Common locations to search for FFmpeg
QStringList searchPaths = {
"ffmpeg",
"ffmpeg.exe",
"C:/ffmpeg/bin/ffmpeg.exe",
"C:/Program Files/ffmpeg/bin/ffmpeg.exe",
"C:/Program Files (x86)/ffmpeg/bin/ffmpeg.exe",
QDir::homePath() + "/ffmpeg/bin/ffmpeg.exe",
"/usr/bin/ffmpeg",
"/usr/local/bin/ffmpeg",
};
QString pathEnv = qEnvironmentVariable("PATH");
#ifdef Q_OS_WIN
QStringList pathDirs = pathEnv.split(';', Qt::SkipEmptyParts);
#else
QStringList pathDirs = pathEnv.split(':', Qt::SkipEmptyParts);
#endif
for (const QString& dir : pathDirs) {
searchPaths.append(dir + "/ffmpeg.exe");
searchPaths.append(dir + "/ffmpeg");
}
for (const QString& path : searchPaths) {
if (QFileInfo::exists(path)) {
return QDir::cleanPath(path);
}
}
return QString();
}
QString Settings::scriptsDirectory() const
{
QString defaultPath = QCoreApplication::applicationDirPath() + "/scripts";
@ -603,6 +652,129 @@ void Settings::setViewerForExtension(const QString& extension, const QString& vi
setListFileExtensions(listExts);
}
// Export Settings
QString Settings::defaultImageExportFormat() const
{
return m_settings.value("Export/ImageFormat", "png").toString();
}
void Settings::setDefaultImageExportFormat(const QString& format)
{
m_settings.setValue("Export/ImageFormat", format);
emit settingsChanged();
}
QString Settings::defaultAudioExportFormat() const
{
return m_settings.value("Export/AudioFormat", "wav").toString();
}
void Settings::setDefaultAudioExportFormat(const QString& format)
{
m_settings.setValue("Export/AudioFormat", format);
emit settingsChanged();
}
int Settings::imageJpegQuality() const
{
return m_settings.value("Export/JpegQuality", 90).toInt();
}
void Settings::setImageJpegQuality(int quality)
{
m_settings.setValue("Export/JpegQuality", qBound(1, quality, 100));
emit settingsChanged();
}
int Settings::imagePngCompression() const
{
return m_settings.value("Export/PngCompression", 6).toInt();
}
void Settings::setImagePngCompression(int level)
{
m_settings.setValue("Export/PngCompression", qBound(0, level, 9));
emit settingsChanged();
}
int Settings::audioMp3Bitrate() const
{
return m_settings.value("Export/Mp3Bitrate", 256).toInt();
}
void Settings::setAudioMp3Bitrate(int bitrate)
{
m_settings.setValue("Export/Mp3Bitrate", bitrate);
emit settingsChanged();
}
int Settings::audioOggQuality() const
{
return m_settings.value("Export/OggQuality", 5).toInt();
}
void Settings::setAudioOggQuality(int quality)
{
m_settings.setValue("Export/OggQuality", qBound(-1, quality, 10));
emit settingsChanged();
}
int Settings::audioFlacCompression() const
{
return m_settings.value("Export/FlacCompression", 5).toInt();
}
void Settings::setAudioFlacCompression(int level)
{
m_settings.setValue("Export/FlacCompression", qBound(0, level, 8));
emit settingsChanged();
}
bool Settings::exportRememberSettings() const
{
return m_settings.value("Export/RememberSettings", true).toBool();
}
void Settings::setExportRememberSettings(bool remember)
{
m_settings.setValue("Export/RememberSettings", remember);
emit settingsChanged();
}
QString Settings::batchExportDirectory() const
{
return m_settings.value("Export/BatchDirectory",
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)).toString();
}
void Settings::setBatchExportDirectory(const QString& path)
{
m_settings.setValue("Export/BatchDirectory", path);
emit settingsChanged();
}
bool Settings::batchExportPreserveStructure() const
{
return m_settings.value("Export/PreserveStructure", true).toBool();
}
void Settings::setBatchExportPreserveStructure(bool preserve)
{
m_settings.setValue("Export/PreserveStructure", preserve);
emit settingsChanged();
}
QString Settings::batchExportConflictResolution() const
{
return m_settings.value("Export/ConflictResolution", "number").toString();
}
void Settings::setBatchExportConflictResolution(const QString& resolution)
{
m_settings.setValue("Export/ConflictResolution", resolution);
emit settingsChanged();
}
// Window State
QByteArray Settings::windowGeometry() const
{

View File

@ -50,6 +50,10 @@ public:
void setPythonPath(const QString& path);
static QString findPython(); // Auto-detect Python
QString ffmpegPath() const;
void setFFmpegPath(const QString& path);
static QString findFFmpeg(); // Auto-detect FFmpeg
QString scriptsDirectory() const;
void setScriptsDirectory(const QString& path);
@ -117,6 +121,40 @@ public:
QStringList listFileExtensions() const;
void setListFileExtensions(const QStringList& extensions);
// Export Settings
QString defaultImageExportFormat() const;
void setDefaultImageExportFormat(const QString& format);
QString defaultAudioExportFormat() const;
void setDefaultAudioExportFormat(const QString& format);
int imageJpegQuality() const; // 1-100, default 90
void setImageJpegQuality(int quality);
int imagePngCompression() const; // 0-9, default 6
void setImagePngCompression(int level);
int audioMp3Bitrate() const; // 128, 192, 256, 320 kbps
void setAudioMp3Bitrate(int bitrate);
int audioOggQuality() const; // -1 to 10, default 5
void setAudioOggQuality(int quality);
int audioFlacCompression() const; // 0-8, default 5
void setAudioFlacCompression(int level);
bool exportRememberSettings() const;
void setExportRememberSettings(bool remember);
QString batchExportDirectory() const;
void setBatchExportDirectory(const QString& path);
bool batchExportPreserveStructure() const;
void setBatchExportPreserveStructure(bool preserve);
QString batchExportConflictResolution() const; // "number", "overwrite", "skip"
void setBatchExportConflictResolution(const QString& resolution);
// Window State
QByteArray windowGeometry() const;
void setWindowGeometry(const QByteArray& geometry);