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:
parent
05c70c1108
commit
d7488c5fa9
384
app/audioexportdialog.cpp
Normal file
384
app/audioexportdialog.cpp
Normal 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
82
app/audioexportdialog.h
Normal 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
467
app/batchexportdialog.cpp
Normal 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
99
app/batchexportdialog.h
Normal 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
158
app/binaryexportdialog.cpp
Normal 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
38
app/binaryexportdialog.h
Normal 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
221
app/exportdialog.cpp
Normal 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
100
app/exportdialog.h
Normal 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
764
app/exportmanager.cpp
Normal 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
101
app/exportmanager.h
Normal 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
282
app/imageexportdialog.cpp
Normal 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
68
app/imageexportdialog.h
Normal 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
|
||||
172
app/settings.cpp
172
app/settings.cpp
@ -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
|
||||
{
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user