From a6d270e0ce5c8ada27d10674e2942582af153045 Mon Sep 17 00:00:00 2001 From: Jack Lau Date: Mon, 18 May 2026 20:04:14 +0800 Subject: [PATCH 1/8] doc: add macOS build guide and CLAUDE.md project knowledge Add macOS build instructions to CompileGuide.md and create CLAUDE.md with project structure, build steps, and key patterns for context. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- CLAUDE.md | 63 +++++++++++++++++++++++++++++++++++++++++++++ doc/CompileGuide.md | 57 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..8c25bb25 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,63 @@ +# OpenConverter - Project Knowledge + +## Overview + +OpenConverter is a Qt + FFmpeg-based media converter with GUI and CLI modes. It supports multiple transcoding backends: FFmpeg API, FFmpeg CLI tool (FFTool), and BMF framework. + +## Build (macOS) + +```bash +# Prerequisites: FFmpeg installed via Homebrew (pkg-config), Qt 6.5.1 at /Users/jacklau/Qt +mkdir build && cd build +cmake ../src -DCMAKE_PREFIX_PATH=/Users/jacklau/Qt/6.5.1/macos -DBMF_TRANSCODER=OFF -DCMAKE_BUILD_TYPE=Debug +cmake --build . -j$(sysctl -n hw.ncpu) +# Output: build/OpenConverter.app +``` + +### CMake Options + +| Option | Default | Description | +|--------|---------|-------------| +| `ENABLE_GUI` | ON | Build with Qt GUI | +| `ENABLE_TESTS` | OFF | Build unit tests (uses GTest) | +| `BMF_TRANSCODER` | ON | Enable BMF backend (requires BMF SDK) | +| `FFTOOL_TRANSCODER` | ON | Enable FFmpeg CLI tool backend | +| `FFMPEG_TRANSCODER` | ON | Enable FFmpeg API backend | +| `FFMPEG_ROOT_PATH` | (auto) | Custom FFmpeg install path | + +### Dependencies + +- **Qt**: 6.5.1 at `/Users/jacklau/Qt/6.5.1/macos` (Components: Core, Gui, Widgets, Network) +- **FFmpeg**: 5.1.x from Homebrew (libavcodec, libavformat, libavfilter, libavutil, libswresample, libswscale) +- **BMF** (optional): at `/Users/jacklau/Documents/Programs/Git/Github/bmf/output/bmf` + +## Project Structure + +``` +src/ +├── CMakeLists.txt # Main build file +├── main.cpp # Entry point (GUI or CLI based on ENABLE_GUI) +├── common/ # Shared data structures (encode_parameter, info, stream_context) +├── engine/ # Converter engine orchestration +├── transcoder/ # Backend implementations (ffmpeg, fftool, bmf) +├── builder/ # Qt GUI pages and logic +├── component/ # Reusable Qt widgets +├── resources/ # UI files, translations, icons +└── tests/ # GTest-based unit tests +``` + +## Key Patterns + +- GUI pages inherit from `base_page` and live in `builder/` +- Reusable widgets live in `component/` +- Transcoder backends implement the `transcoder` interface in `transcoder/include/transcoder.h` +- FFmpeg version detection is automatic (supports v4.x through v7.x) +- macOS release builds use `tool/fix_macos_libs.sh` to bundle libraries + +## Testing + +```bash +cmake ../src -DENABLE_TESTS=ON -DENABLE_GUI=OFF -DBMF_TRANSCODER=OFF +cmake --build . +ctest +``` diff --git a/doc/CompileGuide.md b/doc/CompileGuide.md index f1c47953..48d5ff68 100644 --- a/doc/CompileGuide.md +++ b/doc/CompileGuide.md @@ -1,4 +1,59 @@ -# OC Project Compilation Guide for Windows +# OC Project Compilation Guide + +## macOS + +### Prerequisites + +1. **Qt 6.5+** - Install from [qt.io](https://www.qt.io/download) (e.g., to `~/Qt`) +2. **FFmpeg 5.x-7.x** - Install via Homebrew: `brew install ffmpeg` +3. **CMake 3.10+** - Install via Homebrew: `brew install cmake` + +### Build Steps + +```bash +cd OpenConverter +mkdir build && cd build + +# Configure (adjust Qt path to your installation) +cmake ../src \ + -DCMAKE_PREFIX_PATH=~/Qt/6.5.1/macos \ + -DBMF_TRANSCODER=OFF \ + -DCMAKE_BUILD_TYPE=Debug + +# Build +cmake --build . -j$(sysctl -n hw.ncpu) +``` + +The output is `OpenConverter.app` in the build directory. + +### CMake Options + +| Option | Default | Description | +|--------|---------|-------------| +| `ENABLE_GUI` | ON | Build with Qt GUI | +| `ENABLE_TESTS` | OFF | Build unit tests | +| `BMF_TRANSCODER` | ON | Enable BMF backend (requires BMF SDK) | +| `FFTOOL_TRANSCODER` | ON | Enable FFmpeg CLI tool backend | +| `FFMPEG_TRANSCODER` | ON | Enable FFmpeg API backend | +| `FFMPEG_ROOT_PATH` | (auto via pkg-config) | Custom FFmpeg install path | + +### Release Build + +For release builds, after building run the library bundling script: + +```bash +cmake ../src \ + -DCMAKE_PREFIX_PATH=~/Qt/6.5.1/macos \ + -DBMF_TRANSCODER=OFF \ + -DCMAKE_BUILD_TYPE=Release + +cmake --build . -j$(sysctl -n hw.ncpu) +../tool/fix_macos_libs.sh +``` + +--- + +## Windows ## 1. Install Qt 5 or Qt 6 From 368c924e1bfd1193d5f071915a9d1c49610dcbd1 Mon Sep 17 00:00:00 2001 From: Jack Lau Date: Mon, 18 May 2026 20:52:03 +0800 Subject: [PATCH 2/8] feat: add opt-in av_log file logging Add a Logger singleton (common/logger) that installs a custom av_log callback writing timestamped lines to AppData/OpenConverter/openconverter.log in append mode, while forwarding to the original callback so console output is preserved. Toggle via Settings > Enable Log File in the menu bar; state persisted with QSettings (logging/fileLoggingEnabled). Includes 5 GTest unit tests. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- CLAUDE.md | 14 ++++ src/CMakeLists.txt | 2 + src/builder/include/open_converter.h | 2 + src/builder/src/open_converter.cpp | 23 ++++++ src/builder/src/open_converter.ui | 15 ++++ src/common/include/logger.h | 58 +++++++++++++++ src/common/src/logger.cpp | 106 +++++++++++++++++++++++++++ src/tests/transcoder_test.cpp | 57 ++++++++++++++ 8 files changed, 277 insertions(+) create mode 100644 src/common/include/logger.h create mode 100644 src/common/src/logger.cpp diff --git a/CLAUDE.md b/CLAUDE.md index 8c25bb25..f98c7b25 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -61,3 +61,17 @@ cmake ../src -DENABLE_TESTS=ON -DENABLE_GUI=OFF -DBMF_TRANSCODER=OFF cmake --build . ctest ``` + +## Logger + +`common/include/logger.h` / `common/src/logger.cpp` + +- Singleton: `Logger::Instance()` +- `SetLogPath(std::string)` — set before enabling; called from `open_converter.cpp` using Qt-computed path +- `SetEnabled(bool)` — installs/restores `av_log` callback, opens/closes file in append mode +- Log path: `QStandardPaths::GenericDataLocation + "/OpenConverter/openconverter.log"` + - macOS: `~/Library/Application Support/OpenConverter/openconverter.log` +- Persisted via `QSettings` key `logging/fileLoggingEnabled` (default: false) +- Toggle UI: **Settings → Enable Log File** in the menu bar +- Future log level filter: add `SetMinLevel(int avLogLevel)`; check `level <= m_minLevel` in callback +- Note: `av_log_get_default_callback()` not available in FFmpeg 5.1 — use `av_log_default_callback` directly diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1f6ceb64..434ef111 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -25,6 +25,7 @@ set(COMMON_SOURCES ${CMAKE_SOURCE_DIR}/main.cpp ${CMAKE_SOURCE_DIR}/common/src/encode_parameter.cpp ${CMAKE_SOURCE_DIR}/common/src/info.cpp + ${CMAKE_SOURCE_DIR}/common/src/logger.cpp ${CMAKE_SOURCE_DIR}/common/src/process_parameter.cpp ${CMAKE_SOURCE_DIR}/common/src/stream_context.cpp ${CMAKE_SOURCE_DIR}/engine/src/converter.cpp @@ -34,6 +35,7 @@ set(COMMON_SOURCES set(COMMON_HEADERS ${CMAKE_SOURCE_DIR}/common/include/encode_parameter.h ${CMAKE_SOURCE_DIR}/common/include/info.h + ${CMAKE_SOURCE_DIR}/common/include/logger.h ${CMAKE_SOURCE_DIR}/common/include/process_parameter.h ${CMAKE_SOURCE_DIR}/common/include/process_observer.h ${CMAKE_SOURCE_DIR}/common/include/stream_context.h diff --git a/src/builder/include/open_converter.h b/src/builder/include/open_converter.h index 0177a928..9cc3de9f 100644 --- a/src/builder/include/open_converter.h +++ b/src/builder/include/open_converter.h @@ -49,6 +49,7 @@ #include "../../common/include/encode_parameter.h" #include "../../common/include/info.h" +#include "../../common/include/logger.h" #include "../../common/include/process_observer.h" #include "../../common/include/process_parameter.h" @@ -91,6 +92,7 @@ private slots: void SlotTranscoderChanged(QAction *action); void OnNavigationButtonClicked(int pageIndex); void OnQueueButtonClicked(); + void SlotLogToggled(bool checked); private: Ui::OpenConverter *ui; diff --git a/src/builder/src/open_converter.cpp b/src/builder/src/open_converter.cpp index f993c0d6..a6adf68d 100644 --- a/src/builder/src/open_converter.cpp +++ b/src/builder/src/open_converter.cpp @@ -42,6 +42,7 @@ #include #include #include +#include #include "../../common/include/encode_parameter.h" #include "../../common/include/info.h" @@ -79,6 +80,22 @@ OpenConverter::OpenConverter(QWidget *parent) ui->setupUi(this); setAcceptDrops(true); + + // ── File logging ────────────────────────────────────────────────────── + QString logDir = QStandardPaths::writableLocation( + QStandardPaths::GenericDataLocation) + + "/OpenConverter"; + QDir().mkpath(logDir); + Logger::Instance().SetLogPath((logDir + "/openconverter.log").toStdString()); + + QSettings settings; + bool loggingEnabled = settings.value("logging/fileLoggingEnabled", false).toBool(); + ui->action_enableLog->setChecked(loggingEnabled); + Logger::Instance().SetEnabled(loggingEnabled); + + connect(ui->action_enableLog, &QAction::toggled, + this, &OpenConverter::SlotLogToggled); + setWindowTitle("OpenConverter"); setWindowIcon(QIcon(":/OpenConverter-logo.png")); @@ -501,4 +518,10 @@ QString OpenConverter::GetCurrentTranscoderName() const { return "FFMPEG"; } +void OpenConverter::SlotLogToggled(bool checked) { + QSettings settings; + settings.setValue("logging/fileLoggingEnabled", checked); + Logger::Instance().SetEnabled(checked); +} + #include "open_converter.moc" diff --git a/src/builder/src/open_converter.ui b/src/builder/src/open_converter.ui index 1756e9af..8c1388cf 100644 --- a/src/builder/src/open_converter.ui +++ b/src/builder/src/open_converter.ui @@ -146,8 +146,15 @@ QLabel { Transcoder + + + Settings + + + + @@ -166,6 +173,14 @@ QLabel { Chinese + + + true + + + Enable Log File + + diff --git a/src/common/include/logger.h b/src/common/include/logger.h new file mode 100644 index 00000000..bece2715 --- /dev/null +++ b/src/common/include/logger.h @@ -0,0 +1,58 @@ +/* + * Copyright 2025 Jack Lau + * Email: jacklau1222gm@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LOGGER_H +#define LOGGER_H + +#include +#include + +extern "C" { +#include +} + +class Logger { +public: + static Logger& Instance(); + + // Set the full path for the log file (must be called before SetEnabled(true)) + void SetLogPath(const std::string& path); + std::string GetLogPath() const; + + // Enable or disable file logging + void SetEnabled(bool enabled); + bool IsEnabled() const; + + // Non-copyable singleton + Logger(const Logger&) = delete; + Logger& operator=(const Logger&) = delete; + +private: + Logger(); + ~Logger(); + + static void AvLogCallback(void* avcl, int level, const char* fmt, va_list vl); + static const char* LevelToString(int level); + + bool m_enabled = false; + int m_minLevel = AV_LOG_DEBUG; // reserved: future per-level filtering + std::string m_logPath; + std::ofstream m_logFile; + void (*m_originalCallback)(void*, int, const char*, va_list) = nullptr; +}; + +#endif // LOGGER_H diff --git a/src/common/src/logger.cpp b/src/common/src/logger.cpp new file mode 100644 index 00000000..c67624f4 --- /dev/null +++ b/src/common/src/logger.cpp @@ -0,0 +1,106 @@ +/* + * Copyright 2025 Jack Lau + * Email: jacklau1222gm@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "logger.h" + +#include +#include +#include + +extern "C" { +#include +} + +Logger& Logger::Instance() { + static Logger instance; + return instance; +} + +Logger::Logger() { + m_originalCallback = av_log_default_callback; +} + +Logger::~Logger() { + SetEnabled(false); +} + +void Logger::SetLogPath(const std::string& path) { + m_logPath = path; +} + +std::string Logger::GetLogPath() const { + return m_logPath; +} + +bool Logger::IsEnabled() const { + return m_enabled; +} + +void Logger::SetEnabled(bool enabled) { + if (enabled == m_enabled) + return; + + if (enabled) { + m_logFile.open(m_logPath, std::ios::app); + if (!m_logFile.is_open()) + return; // path not set or unwritable — stay disabled + m_enabled = true; + av_log_set_callback(&Logger::AvLogCallback); + } else { + m_enabled = false; + av_log_set_callback(m_originalCallback); + if (m_logFile.is_open()) + m_logFile.close(); + } +} + +const char* Logger::LevelToString(int level) { + if (level <= AV_LOG_FATAL) return "FATAL"; + if (level <= AV_LOG_ERROR) return "ERROR"; + if (level <= AV_LOG_WARNING) return "WARNING"; + if (level <= AV_LOG_INFO) return "INFO"; + return "DEBUG"; +} + +void Logger::AvLogCallback(void* avcl, int level, const char* fmt, va_list vl) { + Logger& logger = Instance(); + + // Copy va_list for our use; forward the original to the saved callback + va_list vl_copy; + va_copy(vl_copy, vl); + + char buf[1024]; + vsnprintf(buf, sizeof(buf), fmt, vl_copy); + va_end(vl_copy); + + // Timestamp + auto now = std::chrono::system_clock::now(); + std::time_t t = std::chrono::system_clock::to_time_t(now); + std::tm* tm_info = std::localtime(&t); + char timeBuf[32]; + std::strftime(timeBuf, sizeof(timeBuf), "%Y-%m-%d %H:%M:%S", tm_info); + + if (logger.m_logFile.is_open()) { + logger.m_logFile << "[" << timeBuf << "] [" + << LevelToString(level) << "] " << buf; + logger.m_logFile.flush(); + } + + // Forward to original callback so console output still works + if (logger.m_originalCallback) + logger.m_originalCallback(avcl, level, fmt, vl); +} diff --git a/src/tests/transcoder_test.cpp b/src/tests/transcoder_test.cpp index b297eb8a..52950c48 100644 --- a/src/tests/transcoder_test.cpp +++ b/src/tests/transcoder_test.cpp @@ -1,4 +1,5 @@ #include "../common/include/encode_parameter.h" +#include "../common/include/logger.h" #include "../engine/include/converter.h" #include #include @@ -7,6 +8,10 @@ #include #include +extern "C" { +#include +} + // Test fixture for transcoder tests class TranscoderTest : public ::testing::Test { protected: @@ -400,3 +405,55 @@ TEST_F(TranscoderTest, AudioVideoStreamOrder) { EXPECT_TRUE(std::filesystem::exists(outputFile)); EXPECT_GT(std::filesystem::file_size(outputFile), 0); } + +// ── Logger Tests ────────────────────────────────────────────────────────────── + +class LoggerTest : public ::testing::Test { +protected: + std::string logPath; + + void SetUp() override { + logPath = (std::filesystem::temp_directory_path() + / "oc_logger_test.log").string(); + std::filesystem::remove(logPath); + Logger::Instance().SetLogPath(logPath); + Logger::Instance().SetEnabled(false); + } + + void TearDown() override { + Logger::Instance().SetEnabled(false); + std::filesystem::remove(logPath); + } +}; + +TEST_F(LoggerTest, DisabledByDefault) { + EXPECT_FALSE(Logger::Instance().IsEnabled()); +} + +TEST_F(LoggerTest, EnableCreatesLogFile) { + Logger::Instance().SetEnabled(true); + EXPECT_TRUE(Logger::Instance().IsEnabled()); + EXPECT_TRUE(std::filesystem::exists(logPath)); +} + +TEST_F(LoggerTest, DisableClosesFile) { + Logger::Instance().SetEnabled(true); + Logger::Instance().SetEnabled(false); + EXPECT_FALSE(Logger::Instance().IsEnabled()); +} + +TEST_F(LoggerTest, AvLogWritesToFile) { + Logger::Instance().SetEnabled(true); + av_log(nullptr, AV_LOG_ERROR, "unit test error line\n"); + Logger::Instance().SetEnabled(false); + + std::ifstream in(logPath); + std::string contents((std::istreambuf_iterator(in)), + std::istreambuf_iterator()); + EXPECT_NE(contents.find("unit test error line"), std::string::npos); + EXPECT_NE(contents.find("[ERROR]"), std::string::npos); +} + +TEST_F(LoggerTest, GetLogPathReturnsSetPath) { + EXPECT_EQ(Logger::Instance().GetLogPath(), logPath); +} From 2a3a50d2ca656550a6e48bfef50e4cae2fecce72 Mon Sep 17 00:00:00 2001 From: Jack Lau Date: Mon, 18 May 2026 21:41:25 +0800 Subject: [PATCH 3/8] feat: add About OpenConverter dialog and Settings preferences Add Help > About OpenConverter showing version, git commit hash, Qt version, and FFmpeg version in a vertical layout with app logo. On macOS, About and Settings are placed in the application menu. Settings dialog contains the Enable Log File checkbox. CMake now detects git commit hash at configure time via git rev-parse. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/CMakeLists.txt | 14 ++++++ src/builder/include/open_converter.h | 1 + src/builder/src/open_converter.cpp | 74 ++++++++++++++++++++++++++++ src/builder/src/open_converter.ui | 12 +++++ 4 files changed, 101 insertions(+) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 434ef111..45c01535 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -191,6 +191,20 @@ list(GET FFMPEG_LIBRARY_DIRS 0 FFMPEG_LIBRARY_DIRS_FIRST) find_ffmpeg_version(${FFMPEG_INCLUDE_DIRS}/libavutil ${FFMPEG_LIBRARY_DIRS_FIRST} OC_FFMPEG_VERSION) add_definitions(-DOC_FFMPEG_VERSION=${OC_FFMPEG_VERSION}) +# Git commit hash +execute_process( + COMMAND git rev-parse --short HEAD + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/.. + OUTPUT_VARIABLE GIT_COMMIT_HASH + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET +) +if(NOT GIT_COMMIT_HASH) + set(GIT_COMMIT_HASH "unknown") +endif() +add_definitions(-DGIT_COMMIT_HASH="${GIT_COMMIT_HASH}") +add_definitions(-DOC_VERSION="${PROJECT_VERSION}") + # Add include directories include_directories( ${CMAKE_SOURCE_DIR} diff --git a/src/builder/include/open_converter.h b/src/builder/include/open_converter.h index 9cc3de9f..10961290 100644 --- a/src/builder/include/open_converter.h +++ b/src/builder/include/open_converter.h @@ -93,6 +93,7 @@ private slots: void OnNavigationButtonClicked(int pageIndex); void OnQueueButtonClicked(); void SlotLogToggled(bool checked); + void SlotAbout(); private: Ui::OpenConverter *ui; diff --git a/src/builder/src/open_converter.cpp b/src/builder/src/open_converter.cpp index a6adf68d..762a2563 100644 --- a/src/builder/src/open_converter.cpp +++ b/src/builder/src/open_converter.cpp @@ -18,7 +18,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -34,6 +36,7 @@ #include #include #include +#include #include #include #include @@ -96,6 +99,35 @@ OpenConverter::OpenConverter(QWidget *parent) connect(ui->action_enableLog, &QAction::toggled, this, &OpenConverter::SlotLogToggled); + connect(ui->action_about, &QAction::triggered, + this, &OpenConverter::SlotAbout); + + // macOS: move Settings and About into the application menu + ui->action_enableLog->setMenuRole(QAction::NoRole); + ui->action_about->setMenuRole(QAction::AboutRole); + + // Create Preferences action for macOS app menu + QAction *prefAction = new QAction(tr("Settings"), this); + prefAction->setMenuRole(QAction::PreferencesRole); + connect(prefAction, &QAction::triggered, this, [this]() { + QDialog *settingsDialog = new QDialog(this); + settingsDialog->setWindowTitle(tr("Settings")); + settingsDialog->setAttribute(Qt::WA_DeleteOnClose); + + QVBoxLayout *layout = new QVBoxLayout(settingsDialog); + QCheckBox *logCheckBox = new QCheckBox(tr("Enable Log File"), settingsDialog); + logCheckBox->setChecked(Logger::Instance().IsEnabled()); + layout->addWidget(logCheckBox); + + connect(logCheckBox, &QCheckBox::toggled, this, [this](bool checked) { + ui->action_enableLog->setChecked(checked); + }); + + settingsDialog->setLayout(layout); + settingsDialog->exec(); + }); + ui->menuSettings->addAction(prefAction); + setWindowTitle("OpenConverter"); setWindowIcon(QIcon(":/OpenConverter-logo.png")); @@ -524,4 +556,46 @@ void OpenConverter::SlotLogToggled(bool checked) { Logger::Instance().SetEnabled(checked); } +void OpenConverter::SlotAbout() { + QString ffmpegVer = QString("%1.%2") + .arg(OC_FFMPEG_VERSION / 10) + .arg(OC_FFMPEG_VERSION % 10); + + QDialog *aboutDialog = new QDialog(this); + aboutDialog->setWindowTitle(tr("About OpenConverter")); + aboutDialog->setAttribute(Qt::WA_DeleteOnClose); + + QVBoxLayout *layout = new QVBoxLayout(aboutDialog); + + QLabel *iconLabel = new QLabel; + QPixmap logo(":/OpenConverter-logo.png"); + if (!logo.isNull()) + iconLabel->setPixmap(logo.scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + iconLabel->setAlignment(Qt::AlignCenter); + layout->addWidget(iconLabel); + + QLabel *textLabel = new QLabel(QString( + "

OpenConverter

" + "" + "" + "" + "" + "" + "
Version: %1
Commit: %2
Qt: %3
FFmpeg: %4
" + "
" + "

A media converter built on FFmpeg, Qt, and BMF.

" + "

" + "github.com/OpenConverterLab/OpenConverter

") + .arg(OC_VERSION) + .arg(GIT_COMMIT_HASH) + .arg(QT_VERSION_STR) + .arg(ffmpegVer)); + textLabel->setOpenExternalLinks(true); + textLabel->setTextFormat(Qt::RichText); + layout->addWidget(textLabel); + + aboutDialog->setLayout(layout); + aboutDialog->exec(); +} + #include "open_converter.moc" diff --git a/src/builder/src/open_converter.ui b/src/builder/src/open_converter.ui index 8c1388cf..9773055d 100644 --- a/src/builder/src/open_converter.ui +++ b/src/builder/src/open_converter.ui @@ -152,9 +152,16 @@ QLabel { + + + Help + + + + @@ -181,6 +188,11 @@ QLabel { Enable Log File + + + About OpenConverter + + From 5a210a19873c1229cbb4c4a9ec735bbe63a8dc7c Mon Sep 17 00:00:00 2001 From: Jack Lau Date: Mon, 18 May 2026 22:28:30 +0800 Subject: [PATCH 4/8] feat: persist user settings to AppData/settings.ini Store language, transcoder, enable-log, and last browsed directory in ~/Library/Application Support/OpenConverter/settings.ini. Settings are restored on startup and saved on change (language, transcoder, log toggle) or on exit (last directory). Add a Settings dialog with Reset All Settings button for macOS app menu. SharedData gains a separate lastDirectory field so restoring the saved path doesn't auto-fill page input boxes. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/builder/include/open_converter.h | 1 + src/builder/include/shared_data.h | 5 ++ src/builder/src/info_view_page.cpp | 10 +++- src/builder/src/open_converter.cpp | 82 +++++++++++++++++++++++----- src/builder/src/shared_data.cpp | 12 ++++ 5 files changed, 94 insertions(+), 16 deletions(-) diff --git a/src/builder/include/open_converter.h b/src/builder/include/open_converter.h index 10961290..01857a05 100644 --- a/src/builder/include/open_converter.h +++ b/src/builder/include/open_converter.h @@ -99,6 +99,7 @@ private slots: Ui::OpenConverter *ui; QTranslator m_translator; QString m_currLang; + QString m_settingsPath; QString m_langPath; QString currentInputPath; QString currentOutputPath; diff --git a/src/builder/include/shared_data.h b/src/builder/include/shared_data.h index 6cfb1007..180f067f 100644 --- a/src/builder/include/shared_data.h +++ b/src/builder/include/shared_data.h @@ -47,9 +47,14 @@ class SharedData { // Clear all data void Clear(); + // Last browsed directory (for file dialog starting path only) + QString GetLastDirectory() const; + void SetLastDirectory(const QString &dir); + private: QString inputFilePath; QString outputFilePath; + QString lastDirectory; bool outputPathManuallySet; }; diff --git a/src/builder/src/info_view_page.cpp b/src/builder/src/info_view_page.cpp index f5d49a48..3944f861 100644 --- a/src/builder/src/info_view_page.cpp +++ b/src/builder/src/info_view_page.cpp @@ -155,10 +155,18 @@ void InfoViewPage::SetupUI() { } void InfoViewPage::OnBrowseButtonClicked() { + QString startDir; + if (!filePathLineEdit->text().isEmpty()) { + startDir = filePathLineEdit->text(); + } else { + OpenConverter *mainWindow = qobject_cast(window()); + if (mainWindow && mainWindow->GetSharedData()) + startDir = mainWindow->GetSharedData()->GetLastDirectory(); + } QString fileName = QFileDialog::getOpenFileName( this, "Select Media File", - "", + startDir, "Media Files (*.mp4 *.mkv *.avi *.mov *.flv *.wmv *.mp3 *.wav *.aac *.flac *.jpg *.jpeg *.png *.bmp *.tiff *.webp *.gif);;All Files (*.*)" ); diff --git a/src/builder/src/open_converter.cpp b/src/builder/src/open_converter.cpp index 762a2563..33ca512e 100644 --- a/src/builder/src/open_converter.cpp +++ b/src/builder/src/open_converter.cpp @@ -85,13 +85,15 @@ OpenConverter::OpenConverter(QWidget *parent) setAcceptDrops(true); // ── File logging ────────────────────────────────────────────────────── - QString logDir = QStandardPaths::writableLocation( - QStandardPaths::GenericDataLocation) - + "/OpenConverter"; - QDir().mkpath(logDir); - Logger::Instance().SetLogPath((logDir + "/openconverter.log").toStdString()); - - QSettings settings; + QString appDataDir = QStandardPaths::writableLocation( + QStandardPaths::GenericDataLocation) + + "/OpenConverter"; + QDir().mkpath(appDataDir); + m_settingsPath = appDataDir + "/settings.ini"; + Logger::Instance().SetLogPath((appDataDir + "/openconverter.log").toStdString()); + + // ── Restore user settings ──────────────────────────────────────────── + QSettings settings(m_settingsPath, QSettings::IniFormat); bool loggingEnabled = settings.value("logging/fileLoggingEnabled", false).toBool(); ui->action_enableLog->setChecked(loggingEnabled); Logger::Instance().SetEnabled(loggingEnabled); @@ -123,6 +125,25 @@ OpenConverter::OpenConverter(QWidget *parent) ui->action_enableLog->setChecked(checked); }); + layout->addStretch(); + + QPushButton *clearButton = new QPushButton(tr("Reset All Settings"), settingsDialog); + layout->addWidget(clearButton); + + connect(clearButton, &QPushButton::clicked, this, [this, settingsDialog]() { + QMessageBox::StandardButton reply = QMessageBox::question( + settingsDialog, tr("Reset Settings"), + tr("Reset all settings to defaults? The application will restart."), + QMessageBox::Yes | QMessageBox::No); + if (reply == QMessageBox::Yes) { + QSettings settings(m_settingsPath, QSettings::IniFormat); + settings.clear(); + Logger::Instance().SetEnabled(false); + ui->action_enableLog->setChecked(false); + settingsDialog->close(); + } + }); + settingsDialog->setLayout(layout); settingsDialog->exec(); }); @@ -137,6 +158,11 @@ OpenConverter::OpenConverter(QWidget *parent) // Initialize shared data sharedData = new SharedData(); + // Restore last browsed directory from settings + QString lastDir = settings.value("app/lastFilePath").toString(); + if (!lastDir.isEmpty()) + sharedData->SetLastDirectory(lastDir); + // Initialize batch queue dialog batchQueueDialog = nullptr; @@ -171,8 +197,22 @@ OpenConverter::OpenConverter(QWidget *parent) } if (!transcoderActions.isEmpty()) { - transcoderActions.first()->setChecked(true); - converter->set_transcoder(transcoderActions.first()->objectName().toStdString()); + QString savedTranscoder = settings.value("app/transcoder").toString(); + bool restored = false; + if (!savedTranscoder.isEmpty()) { + for (QAction* action : transcoderActions) { + if (action->objectName() == savedTranscoder) { + action->setChecked(true); + converter->set_transcoder(savedTranscoder.toStdString()); + restored = true; + break; + } + } + } + if (!restored) { + transcoderActions.first()->setChecked(true); + converter->set_transcoder(transcoderActions.first()->objectName().toStdString()); + } } languageGroup->setExclusive(true); @@ -182,14 +222,17 @@ OpenConverter::OpenConverter(QWidget *parent) languageGroup->addAction(action); } - // Initialize language - default to English (no translation file needed) - m_currLang = "english"; + // Initialize language - restore from settings or default to English m_langPath = ":/"; + QString savedLang = settings.value("app/language", "english").toString(); - // Set the English menu item as checked by default for (QAction* action : languageActions) { - if (action->objectName() == "english") { + if (action->objectName() == savedLang) { action->setChecked(true); + if (savedLang != "english") + LoadLanguage(savedLang); + else + m_currLang = savedLang; break; } } @@ -270,6 +313,8 @@ void OpenConverter::SlotTranscoderChanged(QAction *action) { #endif // If the transcoder name is not valid, log an error if (isValid) { + QSettings settings(m_settingsPath, QSettings::IniFormat); + settings.setValue("app/transcoder", action->objectName()); ui->statusBar->showMessage( tr("Current Transcoder changed to %1") .arg(QString::fromStdString(transcoderName))); @@ -283,9 +328,10 @@ void OpenConverter::SlotTranscoderChanged(QAction *action) { // Called every time, when a menu entry of the language menu is called void OpenConverter::SlotLanguageChanged(QAction *action) { if (0 != action) { - // load the language dependent on the action content LoadLanguage(action->objectName()); setWindowIcon(action->icon()); + QSettings settings(m_settingsPath, QSettings::IniFormat); + settings.setValue("app/language", action->objectName()); } } @@ -505,6 +551,12 @@ void OpenConverter::OnNavigationButtonClicked(int pageIndex) { } OpenConverter::~OpenConverter() { + // Save last browsed directory + QSettings settings(m_settingsPath, QSettings::IniFormat); + QString lastDir = sharedData->GetLastDirectory(); + if (!lastDir.isEmpty()) + settings.setValue("app/lastFilePath", lastDir); + // Remove observer before deleting processParameter if (processParameter) { processParameter->remove_observer(this); @@ -551,7 +603,7 @@ QString OpenConverter::GetCurrentTranscoderName() const { } void OpenConverter::SlotLogToggled(bool checked) { - QSettings settings; + QSettings settings(m_settingsPath, QSettings::IniFormat); settings.setValue("logging/fileLoggingEnabled", checked); Logger::Instance().SetEnabled(checked); } diff --git a/src/builder/src/shared_data.cpp b/src/builder/src/shared_data.cpp index 977cda88..4c24d362 100644 --- a/src/builder/src/shared_data.cpp +++ b/src/builder/src/shared_data.cpp @@ -33,6 +33,9 @@ QString SharedData::GetInputFilePath() const { void SharedData::SetInputFilePath(const QString &filePath) { inputFilePath = filePath; + // Track last directory for file dialogs + if (!filePath.isEmpty()) + lastDirectory = QFileInfo(filePath).absolutePath(); // When input path changes, reset output path to auto-generate mode if (!outputPathManuallySet) { outputFilePath = ""; @@ -84,5 +87,14 @@ void SharedData::ResetOutputPath() { void SharedData::Clear() { inputFilePath = ""; outputFilePath = ""; + lastDirectory = ""; outputPathManuallySet = false; } + +QString SharedData::GetLastDirectory() const { + return lastDirectory; +} + +void SharedData::SetLastDirectory(const QString &dir) { + lastDirectory = dir; +} From b9a02f33c0e66d16b554cc7275bd493f7db9444d Mon Sep 17 00:00:00 2001 From: Jack Lau Date: Mon, 18 May 2026 22:37:13 +0800 Subject: [PATCH 5/8] refactor: remove separate Settings/Help menus from menu bar Move About and Settings actions into a programmatic Help menu inserted before Language. On macOS, both actions are moved to the app menu via AboutRole/PreferencesRole and Help disappears. On Windows/Linux, Help menu stays visible at the left of the bar. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/builder/src/open_converter.cpp | 7 ++++++- src/builder/src/open_converter.ui | 14 -------------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/src/builder/src/open_converter.cpp b/src/builder/src/open_converter.cpp index 33ca512e..6ff6b020 100644 --- a/src/builder/src/open_converter.cpp +++ b/src/builder/src/open_converter.cpp @@ -147,7 +147,12 @@ OpenConverter::OpenConverter(QWidget *parent) settingsDialog->setLayout(layout); settingsDialog->exec(); }); - ui->menuSettings->addAction(prefAction); + // Add actions to a menu so macOS can move them to the app menu via roles. + // On Windows/Linux this menu stays visible as "Help" (placed before Language). + QMenu *helpMenu = new QMenu(tr("Help"), this); + helpMenu->addAction(prefAction); + helpMenu->addAction(ui->action_about); + menuBar()->insertMenu(ui->menuLanguage->menuAction(), helpMenu); setWindowTitle("OpenConverter"); setWindowIcon(QIcon(":/OpenConverter-logo.png")); diff --git a/src/builder/src/open_converter.ui b/src/builder/src/open_converter.ui index 9773055d..48aa51b2 100644 --- a/src/builder/src/open_converter.ui +++ b/src/builder/src/open_converter.ui @@ -146,22 +146,8 @@ QLabel { Transcoder - - - Settings - - - - - - Help - - - - - From 70ba6e10f0eeb152f511dd75edf564507cdf6bdf Mon Sep 17 00:00:00 2001 From: Jack Lau Date: Mon, 18 May 2026 22:44:18 +0800 Subject: [PATCH 6/8] config: update ci/cd target branch --- .github/workflows/ci.yml | 4 ++-- .github/workflows/lint.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 064ed009..7b2268d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [ main ] + branches: [ master ] pull_request: - branches: [ main ] + branches: [ master ] workflow_dispatch: jobs: diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index dae5f269..28f6e656 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -2,9 +2,9 @@ name: lint on: push: - branches: [ main ] + branches: [ master ] pull_request: - branches: [ main ] + branches: [ master ] workflow_dispatch: jobs: From b7af9789665711ceec4a02e49aff52a9a9005dc3 Mon Sep 17 00:00:00 2001 From: Jack Lau Date: Mon, 18 May 2026 22:47:32 +0800 Subject: [PATCH 7/8] fix: end of file --- src/resources/macos_launcher.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/src/resources/macos_launcher.sh b/src/resources/macos_launcher.sh index d22a8c39..53fe14e0 100644 --- a/src/resources/macos_launcher.sh +++ b/src/resources/macos_launcher.sh @@ -10,4 +10,3 @@ export DYLD_LIBRARY_PATH="$HOME/Library/Application Support/OpenConverter/Python # Execute the real binary exec "$SCRIPT_DIR/OpenConverter.real" "$@" - From 764cbb9e061d7abedd7436137a1e2beb2475f559 Mon Sep 17 00:00:00 2001 From: Jack Lau Date: Mon, 18 May 2026 22:46:10 +0800 Subject: [PATCH 8/8] release 1.5.5 version --- src/CMakeLists.txt | 2 +- src/resources/linglong.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 45c01535..30a7a9fa 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.10...3.24) -project(OpenConverter VERSION 1.5.4 LANGUAGES CXX) +project(OpenConverter VERSION 1.5.5 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) diff --git a/src/resources/linglong.yaml b/src/resources/linglong.yaml index 4f92792f..3747ccee 100644 --- a/src/resources/linglong.yaml +++ b/src/resources/linglong.yaml @@ -3,7 +3,7 @@ version: "1" package: id: io.github.openconverterlab name: "OpenConverter" - version: 1.5.4.0 + version: 1.5.5.0 kind: app description: | OpenConverter