From 4efe1ffdf15a66edb2036d75a33d17b1470bcf96 Mon Sep 17 00:00:00 2001 From: Yanhui Shen Date: Wed, 11 Jan 2017 21:56:53 +0800 Subject: [PATCH] Migrate to Qt5 Note: to build and run this commit, some hacks are needed. --- frontend/qt5/AllRes.qrc | 8 + frontend/qt5/AppEnv.cpp | 125 ++++ frontend/qt5/AppEnv.h | 41 ++ frontend/qt5/CustomHeadTabWidget.cpp | 22 + frontend/qt5/CustomHeadTabWidget.hpp | 22 + frontend/qt5/DlgConvertOption.cpp | 221 +++++++ frontend/qt5/DlgConvertOption.h | 43 ++ frontend/qt5/DlgConvertOption.ui | 152 +++++ frontend/qt5/DlgConvertTask.cpp | 86 +++ frontend/qt5/DlgConvertTask.h | 35 ++ frontend/qt5/DlgConvertTask.ui | 34 + frontend/qt5/DlgListSelect.cpp | 45 ++ frontend/qt5/DlgListSelect.h | 26 + frontend/qt5/DlgListSelect.ui | 105 ++++ frontend/qt5/DlgLoadingMedia.cpp | 31 + frontend/qt5/DlgLoadingMedia.h | 26 + frontend/qt5/DlgLoadingMedia.ui | 71 +++ frontend/qt5/FoobarStyle.h | 68 ++ frontend/qt5/FrmProgressBar.cpp | 65 ++ frontend/qt5/FrmProgressBar.h | 51 ++ frontend/qt5/FrmProgressBar.ui | 70 +++ frontend/qt5/FrmTagEditor.cpp | 388 ++++++++++++ frontend/qt5/FrmTagEditor.h | 75 +++ frontend/qt5/FrmTagEditor.ui | 91 +++ frontend/qt5/FrmToolBar.cpp | 39 ++ frontend/qt5/FrmToolBar.h | 27 + frontend/qt5/FrmToolBar.ui | 291 +++++++++ frontend/qt5/IPlaylistView.h | 30 + frontend/qt5/MainWindow.cpp | 480 +++++++++++++++ frontend/qt5/MainWindow.h | 111 ++++ frontend/qt5/MainWindow.ui | 53 ++ frontend/qt5/MidClickTabBar.cpp | 18 + frontend/qt5/MidClickTabBar.hpp | 25 + frontend/qt5/PlaylistActionHistory.h | 96 +++ frontend/qt5/PlaylistClipboard.h | 42 ++ frontend/qt5/SimplePlaylistView.cpp | 887 +++++++++++++++++++++++++++ frontend/qt5/SimplePlaylistView.h | 119 ++++ frontend/qt5/UiHelper.hpp | 50 ++ frontend/qt5/main.cpp | 30 + frontend/qt5/mous-qt.pro | 69 +++ frontend/qt5/mous-qt_zh_CN.ts | 382 ++++++++++++ frontend/qt5/resource/next.png | Bin 0 -> 1209 bytes frontend/qt5/resource/pause.png | Bin 0 -> 1145 bytes frontend/qt5/resource/play.png | Bin 0 -> 1177 bytes frontend/qt5/resource/previous.png | Bin 0 -> 1211 bytes frontend/qt5/resource/stop.png | Bin 0 -> 1165 bytes 46 files changed, 4650 insertions(+) create mode 100644 frontend/qt5/AllRes.qrc create mode 100644 frontend/qt5/AppEnv.cpp create mode 100644 frontend/qt5/AppEnv.h create mode 100644 frontend/qt5/CustomHeadTabWidget.cpp create mode 100644 frontend/qt5/CustomHeadTabWidget.hpp create mode 100644 frontend/qt5/DlgConvertOption.cpp create mode 100644 frontend/qt5/DlgConvertOption.h create mode 100644 frontend/qt5/DlgConvertOption.ui create mode 100644 frontend/qt5/DlgConvertTask.cpp create mode 100644 frontend/qt5/DlgConvertTask.h create mode 100644 frontend/qt5/DlgConvertTask.ui create mode 100644 frontend/qt5/DlgListSelect.cpp create mode 100644 frontend/qt5/DlgListSelect.h create mode 100644 frontend/qt5/DlgListSelect.ui create mode 100644 frontend/qt5/DlgLoadingMedia.cpp create mode 100644 frontend/qt5/DlgLoadingMedia.h create mode 100644 frontend/qt5/DlgLoadingMedia.ui create mode 100644 frontend/qt5/FoobarStyle.h create mode 100644 frontend/qt5/FrmProgressBar.cpp create mode 100644 frontend/qt5/FrmProgressBar.h create mode 100644 frontend/qt5/FrmProgressBar.ui create mode 100644 frontend/qt5/FrmTagEditor.cpp create mode 100644 frontend/qt5/FrmTagEditor.h create mode 100644 frontend/qt5/FrmTagEditor.ui create mode 100644 frontend/qt5/FrmToolBar.cpp create mode 100644 frontend/qt5/FrmToolBar.h create mode 100644 frontend/qt5/FrmToolBar.ui create mode 100644 frontend/qt5/IPlaylistView.h create mode 100644 frontend/qt5/MainWindow.cpp create mode 100644 frontend/qt5/MainWindow.h create mode 100644 frontend/qt5/MainWindow.ui create mode 100644 frontend/qt5/MidClickTabBar.cpp create mode 100644 frontend/qt5/MidClickTabBar.hpp create mode 100644 frontend/qt5/PlaylistActionHistory.h create mode 100644 frontend/qt5/PlaylistClipboard.h create mode 100644 frontend/qt5/SimplePlaylistView.cpp create mode 100644 frontend/qt5/SimplePlaylistView.h create mode 100644 frontend/qt5/UiHelper.hpp create mode 100644 frontend/qt5/main.cpp create mode 100644 frontend/qt5/mous-qt.pro create mode 100644 frontend/qt5/mous-qt_zh_CN.ts create mode 100644 frontend/qt5/resource/next.png create mode 100644 frontend/qt5/resource/pause.png create mode 100644 frontend/qt5/resource/play.png create mode 100644 frontend/qt5/resource/previous.png create mode 100644 frontend/qt5/resource/stop.png diff --git a/frontend/qt5/AllRes.qrc b/frontend/qt5/AllRes.qrc new file mode 100644 index 0000000..14800f3 --- /dev/null +++ b/frontend/qt5/AllRes.qrc @@ -0,0 +1,8 @@ + + + resource/next.png + resource/pause.png + resource/play.png + resource/previous.png + + diff --git a/frontend/qt5/AppEnv.cpp b/frontend/qt5/AppEnv.cpp new file mode 100644 index 0000000..d07bb16 --- /dev/null +++ b/frontend/qt5/AppEnv.cpp @@ -0,0 +1,125 @@ +#include "AppEnv.h" +#include + +namespace Path { + const QString Config = "/.config/mous"; + const QString Plugin = "/lib/mous"; + const QString Resource = "/share/mous"; +} + +namespace Key { + const QString IfNotUtf8 = "IfNotUtf8"; + const QString TagEditorSplitterState = "TagEditorSplitterState"; + const QString WindowGeometry = "WindowGeometry"; + const QString WindowState = "WindowState"; + const QString TabCount = "TabCount"; + const QString TabIndex = "TabIndex"; + const QString Volume = "Volume"; +} + +bool AppEnv::Init() +{ +#ifdef CMAKE_INSTALL_PREFIX + pluginDir = QString(CMAKE_INSTALL_PREFIX) + Path::Plugin; + resourceDir = QString(CMAKE_INSTALL_PREFIX) + Path::Resource; +#else + QStringList libPathes = + (QStringList() << ("/usr/local" + Path::Plugin) << ("/usr" + Path::Plugin)); + foreach (QString path, libPathes) { + if (QFileInfo(path).isDir()) { + pluginDir = path; + break; + } + } + QStringList resPathes = + (QStringList() << ("/usr/local" + Path::Resource) << ("/usr" + Path::Resource)); + foreach (QString path, resPathes) { + if (QFileInfo(path).isDir()) { + resourceDir = path; + break; + } + } +#endif + if (!pluginDir.isEmpty() && !resourceDir.isEmpty()) { + InitFilePath(); + return LoadConfig(); + } else { + return false; + } +} + +void AppEnv::Save() +{ + QSettings settings(configFile, QSettings::IniFormat); + settings.setValue(Key::IfNotUtf8, ifNotUtf8); + settings.setValue(Key::WindowGeometry, windowGeometry); + settings.setValue(Key::WindowState, windowState); + settings.setValue(Key::TagEditorSplitterState, tagEditorSplitterState); + settings.setValue(Key::TabCount, tabCount); + settings.setValue(Key::TabIndex, tabIndex); + settings.setValue(Key::Volume, volume); + settings.sync(); +} + +void AppEnv::InitFilePath() +{ + configDir = QDir::homePath() + Path::Config + "/qt"; + + configFile = configDir + "/config"; + qDebug() << "configFile" << configFile; + + QString locale = QLocale::system().name(); + translationFile = resourceDir + "/qt/mous-qt_" + locale; + qDebug() << "locale:" << locale; + qDebug() << "translationFile:" << translationFile; +} + +bool AppEnv::LoadConfig() +{ + if (!CheckDefaultConfig()) + return false; + + QSettings settings(configFile, QSettings::IniFormat); + ifNotUtf8 = settings.value(Key::IfNotUtf8).toString(); + windowGeometry = settings.value(Key::WindowGeometry).toByteArray(); + windowState = settings.value(Key::WindowState).toByteArray(); + tagEditorSplitterState = settings.value(Key::TagEditorSplitterState).toByteArray(); + tabCount = settings.value(Key::TabCount).toInt(); + tabIndex = settings.value(Key::TabIndex).toInt(); + volume = settings.value(Key::Volume).toInt(); + return true; +} + +bool AppEnv::CheckDefaultConfig() +{ + QString configRoot = QDir::homePath() + Path::Config + "/qt"; + QFileInfo configRootInfo(configRoot); + if (configRootInfo.exists()) { + if (!configRootInfo.isDir() || !configRootInfo.isWritable()) + return false; + } else { + QDir().mkpath(configRoot); + } + + QFileInfo configFileInfo(configFile); + if (configFileInfo.exists()) { + if (!configFileInfo.isFile() || !configFileInfo.isWritable()) + return false; + else + return true; + } else { + QFile(configFile).open(QIODevice::WriteOnly); + } + + QSettings settings(configFile, QSettings::IniFormat); + settings.setValue(Key::IfNotUtf8, "GBK"); + settings.setValue(Key::WindowGeometry, QByteArray()); + settings.setValue(Key::WindowState, QByteArray()); + settings.setValue(Key::TagEditorSplitterState, QByteArray()); + settings.setValue(Key::TabCount, 1); + settings.setValue(Key::TabIndex, 0); + settings.setValue(Key::Volume, -1); + settings.sync(); + + return true; +} diff --git a/frontend/qt5/AppEnv.h b/frontend/qt5/AppEnv.h new file mode 100644 index 0000000..6ffae29 --- /dev/null +++ b/frontend/qt5/AppEnv.h @@ -0,0 +1,41 @@ +#ifndef APPENV_H +#define APPENV_H + +#include +#include + +struct AppEnv +{ +public: + bool Init(); + void Save(); + +public: + // path + QString configDir; + QString pluginDir; + QString resourceDir; + + QString configFile; + QString translationFile; + + // config + QString ifNotUtf8; + + // ui && status + QByteArray windowGeometry; + QByteArray windowState; + QByteArray tagEditorSplitterState; + int tabCount; + int tabIndex; + int volume; + +private: + void InitFilePath(); + bool LoadConfig(); + bool CheckDefaultConfig(); +}; + +typedef scx::Singleton GlobalAppEnv; + +#endif // APPENV_H diff --git a/frontend/qt5/CustomHeadTabWidget.cpp b/frontend/qt5/CustomHeadTabWidget.cpp new file mode 100644 index 0000000..13df957 --- /dev/null +++ b/frontend/qt5/CustomHeadTabWidget.cpp @@ -0,0 +1,22 @@ +#include "CustomHeadTabWidget.hpp" +using namespace sqt; + +CustomHeadTabWidget::CustomHeadTabWidget(QWidget *parent): + QTabWidget(parent) +{ + setMouseTracking(true); +} + +void CustomHeadTabWidget::SetTabBar(QTabBar *tb) +{ + QTabWidget::setTabBar(tb); +} + +void CustomHeadTabWidget::mouseDoubleClickEvent(QMouseEvent* event) +{ + QTabWidget::mouseDoubleClickEvent(event); + + if (!tabBar()->underMouse()) + emit SigDoubleClick(); +} + diff --git a/frontend/qt5/CustomHeadTabWidget.hpp b/frontend/qt5/CustomHeadTabWidget.hpp new file mode 100644 index 0000000..4d1d18d --- /dev/null +++ b/frontend/qt5/CustomHeadTabWidget.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include + +namespace sqt { + +class CustomHeadTabWidget: public QTabWidget +{ + Q_OBJECT + +public: + CustomHeadTabWidget(QWidget * parent = 0); + void SetTabBar(QTabBar* tb); + +signals: + void SigDoubleClick(); + +private: + void mouseDoubleClickEvent(QMouseEvent* event); +}; + +} diff --git a/frontend/qt5/DlgConvertOption.cpp b/frontend/qt5/DlgConvertOption.cpp new file mode 100644 index 0000000..da5c3c8 --- /dev/null +++ b/frontend/qt5/DlgConvertOption.cpp @@ -0,0 +1,221 @@ +#include "DlgConvertOption.h" +#include "ui_DlgConvertOption.h" +#include "UiHelper.hpp" + +#include + +#include +using namespace mous; + +#define DEF_FROM_CAST(type, to, from)\ + type to = static_cast(from) + +DlgConvertOption::DlgConvertOption(QWidget *parent) : + QDialog(parent), + ui(new Ui::DlgConvertOption) +{ + ui->setupUi(this); + + setWindowFlags(windowFlags() + & ~Qt::WindowContextHelpButtonHint); + + connect(ui->btnOk, SIGNAL(clicked()), this, SLOT(accept())); + connect(ui->btnCancel, SIGNAL(clicked()), this, SLOT(reject())); +} + +DlgConvertOption::~DlgConvertOption() +{ + delete ui; +} + +QString DlgConvertOption::Dir() const +{ + return ui->editDir->text(); +} + +void DlgConvertOption::SetDir(const QString &dir) +{ + ui->editDir->setText(dir); +} + +QString DlgConvertOption::FileName() const +{ + return ui->editFile->text(); +} + +void DlgConvertOption::SetFileName(const QString &name) +{ + ui->editFile->setText(name); +} + +void DlgConvertOption::BindWidgetAndOption(const std::vector& opts) +{ + if (opts.empty()) { + ui->boxOptions->hide(); + return; + } + + QBoxLayout* layout = new QBoxLayout(QBoxLayout::TopToBottom); + for (size_t i = 0; i < opts.size(); ++i) { + const BaseOption* baseOpt = opts[i]; + + switch (baseOpt->type) { + case OptionType::Grouped: + { + DEF_FROM_CAST(const GroupedOption*, opt, baseOpt); + + QLabel* desc = new QLabel(QString::fromUtf8(baseOpt->desc.c_str())); + QComboBox* com = new QComboBox(); + QStackedWidget* stack = new QStackedWidget(); + for (size_t i = 0; i < opt->groups.size(); ++i) { + com->addItem(QString::fromUtf8(opt->groups[i].first.c_str())); + QWidget* page = new QWidget(); + QBoxLayout* pageLayout = new QBoxLayout(QBoxLayout::TopToBottom); + for (size_t j = 0; j < opt->groups[i].second.size(); ++j) { + BuildWidgetAndOption(pageLayout, opt->groups[i].second[j]); + } + page->setLayout(pageLayout); + stack->addWidget(page); + } + com->setCurrentIndex(opt->defaultUse); + stack->setCurrentIndex(opt->defaultUse); + QBoxLayout* title = new QBoxLayout(QBoxLayout::LeftToRight); + title->addWidget(desc); + title->addWidget(com, 1); + + layout->addLayout(title); + layout->addWidget(stack); + + m_ComboxWidgetHash[com] = QPair(stack, opt); + connect(com, SIGNAL(currentIndexChanged(int)), this, SLOT(SlotGroupChanged(int))); + } + break; + + default: + { + QLabel* desc = new QLabel(QString::fromUtf8(baseOpt->desc.c_str())); + layout->addWidget(desc); + } + break; + } + + BuildWidgetAndOption(layout, baseOpt); + } + + ui->boxOptions->setLayout(layout); + ui->btnOk->setFocus(); +} + +void DlgConvertOption::BuildWidgetAndOption(QBoxLayout* layout, const mous::BaseOption* baseOpt) +{ + switch (baseOpt->type) { + case OptionType::Boolean: + { + DEF_FROM_CAST(const BooleanOption*, opt, baseOpt); + QCheckBox* box = new QCheckBox(QString::fromUtf8(opt->detail.c_str())); + box->setChecked(opt->defaultChoice); + layout->addWidget(box); + + m_WidgetOptionHash[box] = opt; + connect(box, SIGNAL(stateChanged(int)), this, SLOT(SlotIntValChanged(int))); + } + break; + + case OptionType::RangedInt: + { + DEF_FROM_CAST(const RangedIntOption*, opt, baseOpt); + QSlider* slider = new QSlider(Qt::Horizontal); + slider->setMinimum(opt->min); + slider->setMaximum(opt->max); + slider->setValue(opt->defaultVal); + QSpinBox* box = new QSpinBox(); + box->setMinimum(opt->min); + box->setMaximum(opt->max); + box->setValue(opt->defaultVal); + QBoxLayout* row = new QBoxLayout(QBoxLayout::LeftToRight); + row->addWidget(slider); + row->addWidget(box); + connect(slider, SIGNAL(valueChanged(int)), box, SLOT(setValue(int))); + connect(box, SIGNAL(valueChanged(int)), slider, SLOT(setValue(int))); + layout->addLayout(row); + + m_WidgetOptionHash[box] = opt; + connect(box, SIGNAL(valueChanged(int)), this, SLOT(SlotIntValChanged(int))); + } + break; + + case OptionType::RangedFloat: + { + } + break; + + case OptionType::EnumedInt: + { + DEF_FROM_CAST(const EnumedIntOption*, opt, baseOpt); + QComboBox* box = new QComboBox(); + for (size_t i = 0; i < opt->enumedVal.size(); ++i) { + box->addItem(QString::number(opt->enumedVal[i])); + } + box->setCurrentIndex(opt->defaultChoice); + layout->addWidget(box); + + m_WidgetOptionHash[box] = opt; + connect(box, SIGNAL(currentIndexChanged(int)), this, SLOT(SlotIntValChanged(int))); + } + break; + + default: + break; + } +} + +void DlgConvertOption::SlotGroupChanged(int index) +{ + QObject* combox = sender(); + assert(combox != nullptr); + + QStackedWidget* stack = m_ComboxWidgetHash[combox].first; + const GroupedOption* opt = m_ComboxWidgetHash[combox].second; + if (index >= 0 && index < stack->count()) { + sqt::SwitchStackPage(stack, index); + opt->userUse = index; + } +} + +void DlgConvertOption::SlotIntValChanged(int val) +{ + QObject* widget = sender(); + assert(widget != nullptr); + + const BaseOption* baseOpt = m_WidgetOptionHash[widget]; + if (baseOpt == nullptr) + return; + + qDebug() << mous::ToString(baseOpt->type) << val; + + switch (baseOpt->type) { + case OptionType::Boolean: + { + DEF_FROM_CAST(const BooleanOption*, opt, baseOpt); + opt->userChoice = val == Qt::Checked ? true : false; + } + break; + + case OptionType::RangedInt: + { + DEF_FROM_CAST(const RangedIntOption*, opt, baseOpt); + opt->userVal = val; + } + break; + + case OptionType::EnumedInt: + { + DEF_FROM_CAST(const EnumedIntOption*, opt, baseOpt); + opt->userChoice = val; + } + break; + + default: + break; + } +} diff --git a/frontend/qt5/DlgConvertOption.h b/frontend/qt5/DlgConvertOption.h new file mode 100644 index 0000000..aab3375 --- /dev/null +++ b/frontend/qt5/DlgConvertOption.h @@ -0,0 +1,43 @@ +#pragma once + +#include + +#include + +namespace Ui { + class DlgConvertOption; +} + +namespace mous { + struct BaseOption; + struct GroupedOption; +} + +class DlgConvertOption : public QDialog +{ + Q_OBJECT + +public: + explicit DlgConvertOption(QWidget *parent = 0); + ~DlgConvertOption(); + + QString Dir() const; + void SetDir(const QString& dir); + QString FileName() const; + void SetFileName(const QString& name); + + void BindWidgetAndOption(const std::vector& opts); + +private: + void BuildWidgetAndOption(QBoxLayout* layout, const mous::BaseOption* option); + +private slots: + void SlotGroupChanged(int index); + void SlotIntValChanged(int val); + +private: + Ui::DlgConvertOption *ui; + QHash > m_ComboxWidgetHash; + QHash m_WidgetOptionHash; +}; + diff --git a/frontend/qt5/DlgConvertOption.ui b/frontend/qt5/DlgConvertOption.ui new file mode 100644 index 0000000..366a12f --- /dev/null +++ b/frontend/qt5/DlgConvertOption.ui @@ -0,0 +1,152 @@ + + + DlgConvertOption + + + + 0 + 0 + 344 + 149 + + + + Dialog + + + + 2 + + + + + + 0 + 0 + + + + Encoder Optoin + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + Output File + + + + 2 + + + + + + + Directory: + + + + + + + File Name: + + + + + + + + + + + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + OK + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + + 0 + 0 + + + + Cancel + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + diff --git a/frontend/qt5/DlgConvertTask.cpp b/frontend/qt5/DlgConvertTask.cpp new file mode 100644 index 0000000..0b0c7d3 --- /dev/null +++ b/frontend/qt5/DlgConvertTask.cpp @@ -0,0 +1,86 @@ +#include "DlgConvertTask.h" +#include "ui_DlgConvertTask.h" +#include "FrmProgressBar.h" +#include "core/IConvTask.h" +using namespace mous; + +Q_DECLARE_METATYPE(IConvTask*) + +DlgConvertTask::DlgConvertTask(QWidget *parent) : + QDialog(parent), + ui(new Ui::DlgConvertTask) +{ + ui->setupUi(this); + ui->listAllTask->setAlternatingRowColors(true); + + m_ProgressTimer.setInterval(20); + connect(&m_ProgressTimer, SIGNAL(timeout()), this, SLOT(SlotUpdateProgress())); +} + +DlgConvertTask::~DlgConvertTask() +{ + disconnect(&m_ProgressTimer, 0, this, 0); + + delete ui; +} + +void DlgConvertTask::AddTask(IConvTask * newTask, const QString &output) +{ + QListWidgetItem* item = new QListWidgetItem(); + FrmProgressBar* bar = new FrmProgressBar(); + item->setData(Qt::UserRole, QVariant::fromValue(newTask)); + item->setSizeHint(QSize(-1, bar->height())); + bar->SetKey(item); + bar->SetFileName(output); + connect(bar, SIGNAL(SigCanceled(void*)), this, SLOT(SlotCancelTask(void*))); + + ui->listAllTask->insertItem(0, item); + ui->listAllTask->setItemWidget(item, bar); + + newTask->Run(output.toUtf8().data()); + + m_ProgressTimer.start(); +} + +void DlgConvertTask::SlotUpdateProgress() +{ + for (int i = 0; i < ui->listAllTask->count(); ++i) { + QListWidgetItem* item = ui->listAllTask->item(i); + FrmProgressBar* bar = (FrmProgressBar*)ui->listAllTask->itemWidget(item); + IConvTask* task = item->data(Qt::UserRole).value(); + + bar->SetProgress(task->Progress()*100); + + if (task->IsFinished()) { + disconnect(bar, 0, this, 0); + ui->listAllTask->removeItemWidget(item); + ui->listAllTask->takeItem(ui->listAllTask->row(item)); + delete item; + delete bar; + IConvTask::Free(task); + } + } + + if (ui->listAllTask->count() == 0) { + m_ProgressTimer.stop(); + if (ui->boxAutoClose->isChecked()) + close(); + } +} + +void DlgConvertTask::SlotCancelTask(void* key) +{ + QListWidgetItem* item = (QListWidgetItem*)key; + FrmProgressBar* bar = (FrmProgressBar*)ui->listAllTask->itemWidget(item); + disconnect(bar, 0, this, 0); + + QVariant var(item->data(Qt::UserRole)); + IConvTask* task = var.value(); + task->Cancel(); + + ui->listAllTask->removeItemWidget(item); + ui->listAllTask->takeItem(ui->listAllTask->row(item)); + delete item; + delete bar; + IConvTask::Free(task); +} diff --git a/frontend/qt5/DlgConvertTask.h b/frontend/qt5/DlgConvertTask.h new file mode 100644 index 0000000..398aaab --- /dev/null +++ b/frontend/qt5/DlgConvertTask.h @@ -0,0 +1,35 @@ +#ifndef DLGCONVERTTASK_H +#define DLGCONVERTTASK_H + +#include +#include +#include "FrmProgressBar.h" + +namespace Ui { +class DlgConvertTask; +} + +namespace mous { +class IConvTask; +} + +class DlgConvertTask : public QDialog +{ + Q_OBJECT + +public: + explicit DlgConvertTask(QWidget *parent = 0); + ~DlgConvertTask(); + + void AddTask(mous::IConvTask* newTask, const QString& output); + +private slots: + void SlotUpdateProgress(); + void SlotCancelTask(void *key); + +private: + Ui::DlgConvertTask *ui; + QTimer m_ProgressTimer; +}; + +#endif // DLGCONVERTTASK_H diff --git a/frontend/qt5/DlgConvertTask.ui b/frontend/qt5/DlgConvertTask.ui new file mode 100644 index 0000000..337c264 --- /dev/null +++ b/frontend/qt5/DlgConvertTask.ui @@ -0,0 +1,34 @@ + + + DlgConvertTask + + + + 0 + 0 + 400 + 300 + + + + Dialog + + + + 2 + + + + + + + + Auto Close This Dialog Once Finished. + + + + + + + + diff --git a/frontend/qt5/DlgListSelect.cpp b/frontend/qt5/DlgListSelect.cpp new file mode 100644 index 0000000..57f1a3f --- /dev/null +++ b/frontend/qt5/DlgListSelect.cpp @@ -0,0 +1,45 @@ +#include "DlgListSelect.h" +#include "ui_DlgListSelect.h" + +DlgListSelect::DlgListSelect(QWidget *parent) : + QDialog(parent), + ui(new Ui::DlgListSelect) +{ + ui->setupUi(this); + ui->listWidget->setAlternatingRowColors(true); + + connect(ui->btnOk, SIGNAL(clicked()), this, SLOT(accept())); + connect(ui->btnCancel, SIGNAL(clicked()), this, SLOT(reject())); + connect(ui->listWidget, SIGNAL(itemDoubleClicked(QListWidgetItem*)), this, SLOT(accept())); + + setWindowFlags((Qt::CustomizeWindowHint + | Qt::WindowTitleHint + | Qt::WindowCloseButtonHint + | Qt::Tool) + & ~Qt::WindowMaximizeButtonHint); + setFixedSize(width(), height()); +} + +DlgListSelect::~DlgListSelect() +{ + delete ui; +} + +void DlgListSelect::SetItems(const QStringList &items) +{ + for (int i = 0; i < items.size(); ++i) { + QListWidgetItem* widgetItem = new QListWidgetItem(items.at(i)); + widgetItem->setSizeHint(QSize(-1, 22)); + ui->listWidget->addItem(widgetItem); + } +} + +void DlgListSelect::SetSelectedIndex(int index) +{ + ui->listWidget->setCurrentRow(index); +} + +int DlgListSelect::GetSelectedIndex() const +{ + return ui->listWidget->currentIndex().row(); +} diff --git a/frontend/qt5/DlgListSelect.h b/frontend/qt5/DlgListSelect.h new file mode 100644 index 0000000..df4ee58 --- /dev/null +++ b/frontend/qt5/DlgListSelect.h @@ -0,0 +1,26 @@ +#ifndef DLGLISTSELECT_H +#define DLGLISTSELECT_H + +#include + +namespace Ui { +class DlgListSelect; +} + +class DlgListSelect : public QDialog +{ + Q_OBJECT + +public: + explicit DlgListSelect(QWidget *parent = 0); + ~DlgListSelect(); + + void SetItems(const QStringList& items); + void SetSelectedIndex(int index); + int GetSelectedIndex() const; + +private: + Ui::DlgListSelect *ui; +}; + +#endif // DLGLISTSELECT_H diff --git a/frontend/qt5/DlgListSelect.ui b/frontend/qt5/DlgListSelect.ui new file mode 100644 index 0000000..1d01595 --- /dev/null +++ b/frontend/qt5/DlgListSelect.ui @@ -0,0 +1,105 @@ + + + DlgListSelect + + + Qt::ApplicationModal + + + + 0 + 0 + 263 + 233 + + + + Dialog + + + true + + + + 2 + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + OK + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + + 0 + 0 + + + + Cancel + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + diff --git a/frontend/qt5/DlgLoadingMedia.cpp b/frontend/qt5/DlgLoadingMedia.cpp new file mode 100644 index 0000000..23a87b5 --- /dev/null +++ b/frontend/qt5/DlgLoadingMedia.cpp @@ -0,0 +1,31 @@ +#include "DlgLoadingMedia.h" +#include "ui_DlgLoadingMedia.h" + +const char PROGRESS_CHARS[] = "-\\|/-\\|/"; +//const char* PROGRESS_CHARS[] = { ".", "..", "...", "...." }; + +DlgLoadingMedia::DlgLoadingMedia(QWidget *parent) : + QDialog(parent), + ui(new Ui::DlgLoadingMedia), + m_ProgressCharIndex(0) +{ + ui->setupUi(this); + + setWindowFlags((Qt::CustomizeWindowHint + | Qt::Tool | Qt::WindowTitleHint) + & ~Qt::WindowMaximizeButtonHint + & ~Qt::WindowCloseButtonHint); + setFixedSize(width(), height()); +} + +DlgLoadingMedia::~DlgLoadingMedia() +{ + delete ui; +} + +void DlgLoadingMedia::SetFileName(const QString &fileName) +{ + ui->labelFileName->setText(fileName); + ui->labelHintChar->setText(QChar(PROGRESS_CHARS[m_ProgressCharIndex])); + m_ProgressCharIndex = (m_ProgressCharIndex+1) % (sizeof(PROGRESS_CHARS)-1); +} diff --git a/frontend/qt5/DlgLoadingMedia.h b/frontend/qt5/DlgLoadingMedia.h new file mode 100644 index 0000000..d013b5f --- /dev/null +++ b/frontend/qt5/DlgLoadingMedia.h @@ -0,0 +1,26 @@ +#ifndef DLGLOADINGMEDIA_H +#define DLGLOADINGMEDIA_H + +#include + +namespace Ui { +class DlgLoadingMedia; +} + +class DlgLoadingMedia : public QDialog +{ + Q_OBJECT + +public: + explicit DlgLoadingMedia(QWidget *parent = 0); + ~DlgLoadingMedia(); + +public: + void SetFileName(const QString& fileName); + +private: + Ui::DlgLoadingMedia *ui; + int m_ProgressCharIndex; +}; + +#endif // DLGLOADINGMEDIA_H diff --git a/frontend/qt5/DlgLoadingMedia.ui b/frontend/qt5/DlgLoadingMedia.ui new file mode 100644 index 0000000..4cc56c6 --- /dev/null +++ b/frontend/qt5/DlgLoadingMedia.ui @@ -0,0 +1,71 @@ + + + DlgLoadingMedia + + + + 0 + 0 + 282 + 42 + + + + Dialog + + + 100.000000000000000 + + + + + + + 0 + 0 + + + + + Monospace + + + + hint + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + 0 + 0 + + + + foo + + + + + + + + diff --git a/frontend/qt5/FoobarStyle.h b/frontend/qt5/FoobarStyle.h new file mode 100644 index 0000000..cb2917e --- /dev/null +++ b/frontend/qt5/FoobarStyle.h @@ -0,0 +1,68 @@ +#pragma once + +#include + +class FoobarStyle: public QProxyStyle +{ +public: + FoobarStyle(QStyle *baseStyle = 0): + QProxyStyle(baseStyle) + { + } + + void drawPrimitive(PrimitiveElement element, const QStyleOption *option, QPainter *painter, const QWidget *widget) const + { + if (element == QStyle::PE_IndicatorItemViewItemDrop) { + qDebug() << option->rect; + int y = 0; + if (!option->rect.isNull()) { + y = option->rect.y(); + } else { + const QAbstractItemView* view = qobject_cast(widget); + if (view == nullptr) + return; + int rows = view->model()->rowCount(); + QModelIndex last = view->model()->index(rows-1, 0); + QRect lastRect = view->visualRect(last); + y = lastRect.bottom(); + } + + if (!option->rect.isNull()) { + m_BelowIndicator.setX(widget->rect().width()/2); + m_BelowIndicator.setY(y + option->rect.height()/2); + } else { + m_BelowIndicator.setX(-1); + m_BelowIndicator.setY(-1); + } + + painter->setRenderHint(QPainter::Antialiasing, true); + QColor c(Qt::black); + QPen pen(c); + pen.setWidth(2); + QBrush brush(c); + painter->setPen(pen); + painter->setBrush(brush); + + QPoint a(0, y); + QPoint b(widget->rect().width(), y); + painter->drawLine(a, b); + + painter->drawPoint(m_BelowIndicator); + + } else { + m_BelowIndicator.setX(-1); + m_BelowIndicator.setY(-1); + + QProxyStyle::drawPrimitive(element, option, painter, widget); + } + } + + QPoint BelowIndicator() const + { + return m_BelowIndicator; + } + +private: + mutable QPoint m_BelowIndicator; +}; + diff --git a/frontend/qt5/FrmProgressBar.cpp b/frontend/qt5/FrmProgressBar.cpp new file mode 100644 index 0000000..7e8493e --- /dev/null +++ b/frontend/qt5/FrmProgressBar.cpp @@ -0,0 +1,65 @@ +#include "FrmProgressBar.h" +#include "ui_FrmProgressBar.h" +#include + +FrmProgressBar::FrmProgressBar(QWidget *parent) : + QWidget(parent), + ui(new Ui::FrmProgressBar) +{ + ui->setupUi(this); + + connect(ui->btnCancel, SIGNAL(clicked()), this, SLOT(SlotBtnCancel())); +} + +FrmProgressBar::~FrmProgressBar() +{ + disconnect(ui->btnCancel, 0, this, 0); + + delete ui; +} + +void FrmProgressBar::SetKey(void *_key) +{ + key = _key; +} + +void FrmProgressBar::SetFileName(const QString &fileName) +{ + ui->labelFileName->setText(fileName); +} + +void FrmProgressBar::SetProgress(int progress) +{ + ui->barProgress->setValue(progress); + + UpdatePassedTime(); +} + +void FrmProgressBar::SlotBtnCancel() +{ + emit SigCanceled(key); +} + +void FrmProgressBar::UpdatePassedTime() +{ + m_SpeedRecord.time[m_SpeedRecord.time[0] != -1 ? 1 : 0] = QDateTime::currentMSecsSinceEpoch(); + qint64 passedSec = (m_SpeedRecord.time[1] - m_SpeedRecord.time[0]) / 1000; + if (m_SpeedRecord.time[1] > 0) + ui->labelTime->setText(QString("%1 : %2").arg(int(passedSec/60), 2, 10, QChar('0')) + .arg(int(passedSec%60), 2, 10, QChar('0'))); + + /* rest time estimate + m_SpeedRecord.time[m_SpeedRecord.time[0] != -1 ? 1 : 0] = QDateTime::currentMSecsSinceEpoch(); + m_SpeedRecord.progress[m_SpeedRecord.progress[0] != -1 ? 1 : 0] = ui->barProgress->value(); + + if (m_SpeedRecord.time[1] > 0 && m_SpeedRecord.progress[1] > 0) { + qint64 deltaTime = m_SpeedRecord.time[1] - m_SpeedRecord.time[0]; + int deltaProgress = m_SpeedRecord.progress[1] - m_SpeedRecord.progress[0]; + double secSpeed = (double)deltaProgress / (deltaTime / 1000); + qint64 restSec = (ui->barProgress->maximum() - ui->barProgress->value()) / secSpeed; + QString restSecSrc; + restSecSrc.sprintf("%.2d : %.2d", int(restSec/60), int(restSec%60)); + ui->labelRestTime->setText(restSecSrc); + } + */ +} diff --git a/frontend/qt5/FrmProgressBar.h b/frontend/qt5/FrmProgressBar.h new file mode 100644 index 0000000..8af21b0 --- /dev/null +++ b/frontend/qt5/FrmProgressBar.h @@ -0,0 +1,51 @@ +#ifndef FRMPROGRESSBAR_H +#define FRMPROGRESSBAR_H + +#include + +namespace Ui { +class FrmProgressBar; +} + +class FrmProgressBar : public QWidget +{ + Q_OBJECT + +public: + explicit FrmProgressBar(QWidget *parent = 0); + ~FrmProgressBar(); + + void SetKey(void* key); + + void SetProgress(int progress); + void SetFileName(const QString& fileName); + +signals: + void SigCanceled(void* key); + +private slots: + void SlotBtnCancel(); + +private: + void UpdatePassedTime(); + +private: + Ui::FrmProgressBar *ui; + void* key; + + struct SpeedRecord + { + qint64 time[2]; + int progress[2]; + + SpeedRecord() + { + progress[0] = time[0] = -1; + progress[1] = time[1] = -1; + } + }; + + SpeedRecord m_SpeedRecord; +}; + +#endif // FRMPROGRESSBAR_H diff --git a/frontend/qt5/FrmProgressBar.ui b/frontend/qt5/FrmProgressBar.ui new file mode 100644 index 0000000..a52078a --- /dev/null +++ b/frontend/qt5/FrmProgressBar.ui @@ -0,0 +1,70 @@ + + + FrmProgressBar + + + + 0 + 0 + 443 + 57 + + + + Form + + + + + + 0 + + + + + + + FileName + + + + + + + 0 + + + %p% + + + + + + + + + + + -- : -- + + + Qt::AlignCenter + + + + + + + Cancel + + + + + + + + + + + + diff --git a/frontend/qt5/FrmTagEditor.cpp b/frontend/qt5/FrmTagEditor.cpp new file mode 100644 index 0000000..5716b35 --- /dev/null +++ b/frontend/qt5/FrmTagEditor.cpp @@ -0,0 +1,388 @@ +#include "FrmTagEditor.h" +#include "ui_FrmTagEditor.h" +#include "AppEnv.h" + +#include +#include +using namespace std; + +FrmTagEditor::FrmTagEditor(QWidget *parent) : + QWidget(parent), + ui(new Ui::FrmTagEditor), + m_Player(nullptr), + m_ParserFactory(nullptr), + m_CurrentParser(nullptr), + m_LabelImage(nullptr), + m_OldImagePath(QDir::homePath()), + m_SemLoadFinished(1) +{ + ui->setupUi(this); + + m_LabelImage = new QLabel(); + m_LabelImage->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter); + ui->scrollAreaCover->setWidget(m_LabelImage); + ui->scrollAreaCover->setWidgetResizable(true); + + ui->scrollAreaCover->setContextMenuPolicy(Qt::ActionsContextMenu); + QAction* actionSaveImageAs = new QAction(tr("Save Image As"), ui->scrollAreaCover); + ui->scrollAreaCover->addAction(actionSaveImageAs); + QAction* actionChangeCoverArt = new QAction(tr("Change Cover Art"), ui->scrollAreaCover); + ui->scrollAreaCover->addAction(actionChangeCoverArt); + connect(actionSaveImageAs, SIGNAL(triggered()), this, SLOT(SlotSaveImageAs())); + connect(actionChangeCoverArt, SIGNAL(triggered()), this, SLOT(SlotChangeCoverArt())); + + ui->tagTable->setAlternatingRowColors(true); + ui->tagTable->setShowGrid(false); + ui->tagTable->setColumnCount(2); + ui->tagTable->setHorizontalHeaderLabels(QStringList(tr("Name")) << tr("Value")); + ui->tagTable->horizontalHeader()->setVisible(true); + ui->tagTable->horizontalHeader()->setStretchLastSection(true); + ui->tagTable->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); + ui->tagTable->setEnabled(false); + + ShowBottomBtns(false); + + ui->labelFailed->clear(); + ui->labelFailed->hide(); + + QList names; + names << tr("Album") << tr("Title") << tr("Artist") + << tr("Genre") << tr("Year") << tr("Track") << tr("Comment"); + ui->tagTable->setRowCount(names.size()); + + for (int i = 0; i < names.size(); ++i) { + QTableWidgetItem* key = new QTableWidgetItem(names[i]); + ui->tagTable->setItem(i, 0, key); + key->setFlags(Qt::NoItemFlags | Qt::ItemIsEnabled); + + QTableWidgetItem* val = new QTableWidgetItem(""); + ui->tagTable->setItem(i, 1, val); + val->setFlags(val->flags() | Qt::ItemIsEditable); + + ui->tagTable->setRowHeight(i, 22); + } + + connect(ui->btnSave, SIGNAL(clicked()), this, SLOT(SlotBtnSave())); + connect(ui->btnCancel, SIGNAL(clicked()), this, SLOT(SlotBtnCancel())); + connect(ui->splitter, SIGNAL(splitterMoved(int,int)), this, SLOT(SlotSplitterMoved(int,int))); + connect(ui->tagTable, SIGNAL(cellChanged(int,int)), this, SLOT(SlotCellChanged(int,int))); +} + +FrmTagEditor::~FrmTagEditor() +{ + delete ui; + delete m_LabelImage; +} + +void FrmTagEditor::SaveUiStatus() +{ + auto env = GlobalAppEnv::Instance(); + env->tagEditorSplitterState = ui->splitter->saveState(); +} + +void FrmTagEditor::RestoreUiStatus() +{ + auto env = GlobalAppEnv::Instance(); + ui->splitter->restoreState(env->tagEditorSplitterState); +} + +void FrmTagEditor::SetPlayer(IPlayer *player) +{ + m_Player = player; +} + +void FrmTagEditor::SetTagParserFactory(const ITagParserFactory *factory) +{ + if (m_ParserFactory == nullptr && m_ParserFactory != nullptr && m_CurrentParser != nullptr) { + m_CurrentParser->Close(); + m_ParserFactory->FreeParser(m_CurrentParser); + } + m_ParserFactory = factory; +} + +void FrmTagEditor::LoadMediaItem(const mous::MediaItem& item) +{ + if (!m_SemLoadFinished.tryAcquire()) + return; + + m_CurrentItem = item; + DoLoadFileTag(item.url); + + m_UnsavedFields.clear(); + ShowBottomBtns(false); + + m_SemLoadFinished.release(); +} + +void FrmTagEditor::DoLoadFileTag(const std::string &fileName) +{ + if (m_ParserFactory == nullptr) + return; + + if (m_CurrentParser != nullptr) { + m_CurrentParser->Close(); + m_ParserFactory->FreeParser(m_CurrentParser); + } + if ((m_CurrentParser = m_ParserFactory->CreateParser(fileName)) == nullptr) { + return; + } + m_CurrentParser->Open(fileName); + if (m_CurrentParser->CanEdit()) + ui->tagTable->setEnabled(true); + UpdateTag(); + + { + vector buf; + m_CurrentImgFmt = m_CurrentParser->DumpCoverArt(buf); + m_CurrentImgData.swap(buf); + qDebug() << fileName.c_str(); + qDebug() << "cover art size:" << m_CurrentImgData.size(); + } + + if (!m_CurrentImgData.empty()) { + const uchar* data = (const uchar*)m_CurrentImgData.data(); + const uint size = m_CurrentImgData.size(); + if (m_CurrentImage.loadFromData(data, size)) { + UpdateCoverArt(); + ui->scrollAreaCover->show(); + } + } else { + m_CurrentImage.detach(); + m_CurrentImage = QPixmap(); + UpdateCoverArt(); + //ui->scrollAreaCover->hide(); + } +} + +void FrmTagEditor::ShowBottomBtns(bool show) +{ + ui->btnSave->setVisible(show); + ui->btnCancel->setVisible(show); +} + +void FrmTagEditor::SlotBtnSave() +{ + if (m_CurrentParser == nullptr) + return; + + MediaItem tmpItem = m_CurrentItem; + + typedef QPair Cell; + foreach (const Cell& cell, m_UnsavedFields) { + qDebug() << cell; + QString qtext = ui->tagTable->item(cell.first, cell.second)->text(); + string text = qtext.toUtf8().data(); + switch (cell.first) { + case 0: + tmpItem.tag.album = text; + m_CurrentParser->SetAlbum(text); + break; + + case 1: + tmpItem.tag.title = text; + m_CurrentParser->SetTitle(text); + break; + + case 2: + tmpItem.tag.artist = text; + m_CurrentParser->SetArtist(text); + break; + + case 3: + tmpItem.tag.genre = text; + m_CurrentParser->SetGenre(text); + break; + + case 4: + tmpItem.tag.year = qtext.toInt(); + m_CurrentParser->SetYear(qtext.toInt()); + break; + + case 5: + tmpItem.tag.track = qtext.toInt(); + m_CurrentParser->SetTrack(qtext.toInt()); + break; + + case 6: + tmpItem.tag.comment = text; + m_CurrentParser->SetComment(text); + break; + } + } + + m_Player->PauseDecoder(); + bool saveOk = m_CurrentParser->Save(); + m_Player->ResumeDecoder(); + + if (saveOk) { + m_CurrentItem = tmpItem; + emit SigMediaItemChanged(m_CurrentItem); + } else { + ui->labelFailed->setText(tr("Failed to save!")); + ui->labelFailed->show(); + connect(&m_DelayTimer, SIGNAL(timeout()), this, SLOT(SlotHideLabelFailed())); + m_DelayTimer.setSingleShot(true); + m_DelayTimer.start(2.5*1000); + UpdateTag(); + } + + m_UnsavedFields.clear(); + ShowBottomBtns(false); +} + +void FrmTagEditor::SlotBtnCancel() +{ + UpdateTag(); + m_UnsavedFields.clear(); + ShowBottomBtns(false); +} + +void FrmTagEditor::SlotSplitterMoved(int pos, int index) +{ + Q_UNUSED(pos); + Q_UNUSED(index); + + UpdateCoverArt(); +} + +void FrmTagEditor::SlotCellChanged(int row, int column) +{ + m_UnsavedFields.insert(QPair(row, column)); + ShowBottomBtns(true); +} + +void FrmTagEditor::SlotHideLabelFailed() +{ + m_DelayTimer.disconnect(this, SLOT(SlotHideLabelFailed())); + ui->labelFailed->hide(); +} + +void FrmTagEditor::SlotSaveImageAs() +{ + qDebug() << m_CurrentImgFmt; + qDebug() << m_CurrentImgData.size(); + + // check format & has data + QString fmt; + switch (m_CurrentImgFmt) { + case CoverFormat::JPEG: + fmt = "(*.jpg)"; + break; + + case CoverFormat::PNG: + fmt = "(*.png)"; + break; + + default: + fmt.clear(); + } + if (fmt.isEmpty() || m_CurrentImgData.empty()) + return; + + // pick file name + QString fileName = + QFileDialog::getSaveFileName(this, tr("Save Image As"), m_OldImagePath, fmt); + if (fileName.isEmpty()) + return; + m_OldImagePath = QFileInfo(fileName).absolutePath(); + + // write it + QFile outfile(fileName); + outfile.open(QIODevice::WriteOnly); + if (outfile.isOpen()) { + outfile.write(m_CurrentImgData.data(), m_CurrentImgData.size()); + } + outfile.close(); +} + +void FrmTagEditor::SlotChangeCoverArt() +{ + if (m_CurrentParser == nullptr) + return; + + QString fileName = + QFileDialog::getOpenFileName(this, tr("Select Image File"), m_OldImagePath, tr("Images (*.jpg *.png)")); + + // check format + QString suffix = QFileInfo(fileName).suffix(); + EmCoverFormat fmt = CoverFormat::None; + if (suffix == "jpg") { + fmt = CoverFormat::JPEG; + } else if (suffix == "png") { + fmt = CoverFormat::PNG; + } else { + return; + } + + // read data + QByteArray bytes; + QFile imgFile(fileName); + imgFile.open(QIODevice::ReadOnly); + if (imgFile.isOpen()) { + bytes = imgFile.readAll(); + } + imgFile.close(); + if (bytes.isEmpty()) + return; + + // modify media file & show it + const char* data = bytes.data(); + const size_t size = bytes.size(); + m_Player->PauseDecoder(); + bool storeOk = m_CurrentParser->StoreCoverArt(fmt, data, size); + m_Player->ResumeDecoder(); + if (storeOk) { + m_CurrentImgFmt = fmt; + m_CurrentImgData.resize(size); + memcpy(m_CurrentImgData.data(), data, size); + if (m_CurrentImage.loadFromData((uchar*)data, size)) { + UpdateCoverArt(); + ui->scrollAreaCover->show(); + } + } +} + +void FrmTagEditor::UpdateTag() +{ + if (m_CurrentParser == nullptr) + return; + + QList valList; + for (int i = 0; i < ui->tagTable->rowCount(); ++i) { + QTableWidgetItem* val = ui->tagTable->item(i, 1); + valList << val; + } + + valList[0]->setText(QString::fromUtf8(m_CurrentItem.tag.album.c_str())); + valList[1]->setText(QString::fromUtf8(m_CurrentItem.tag.title.c_str())); + valList[2]->setText(QString::fromUtf8(m_CurrentItem.tag.artist.c_str())); + valList[3]->setText(QString::fromUtf8(m_CurrentItem.tag.genre.c_str())); + valList[4]->setText(QString::number(m_CurrentItem.tag.year)); + valList[5]->setText(QString::number(m_CurrentItem.tag.track)); + valList[6]->setText(QString::fromUtf8(m_CurrentItem.tag.comment.c_str())); +} + +void FrmTagEditor::UpdateCoverArt() +{ + if (m_CurrentImage.isNull() || m_LabelImage == nullptr) { + m_LabelImage->setPixmap(QPixmap()); + } else { + QSize size = m_CurrentImage.size(); + size.scale(ui->scrollAreaCover->viewport()->size(), Qt::KeepAspectRatio); + const QPixmap& img = m_CurrentImage.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation); + m_LabelImage->setPixmap(img); + } +} + +void FrmTagEditor::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + + if (event->size() != event->oldSize()) + UpdateCoverArt(); +} + +void FrmTagEditor::WaitForLoadFinished() +{ + m_SemLoadFinished.acquire(); +} diff --git a/frontend/qt5/FrmTagEditor.h b/frontend/qt5/FrmTagEditor.h new file mode 100644 index 0000000..f7d1b38 --- /dev/null +++ b/frontend/qt5/FrmTagEditor.h @@ -0,0 +1,75 @@ +#pragma once + +#include + +#include +#include +#include +using namespace mous; + +#include +using namespace std; + +namespace Ui { +class FrmTagEditor; +} + +class FrmTagEditor : public QWidget +{ + Q_OBJECT + +public: + explicit FrmTagEditor(QWidget *parent = 0); + ~FrmTagEditor(); + + void SaveUiStatus(); + void RestoreUiStatus(); + + void SetPlayer(mous::IPlayer* player); + void SetTagParserFactory(const mous::ITagParserFactory* factory); + void WaitForLoadFinished(); + void LoadMediaItem(const mous::MediaItem& item); + +signals: + void SigMediaItemChanged(const MediaItem& item); + +private: + void DoLoadFileTag(const std::string& fileName); + void ShowBottomBtns(bool show); + void UpdateTag(); + void UpdateCoverArt(); + +private: + void resizeEvent(QResizeEvent * event); + +private slots: + void SlotBtnSave(); + void SlotBtnCancel(); + void SlotSplitterMoved(int pos, int index); + void SlotCellChanged(int row, int column); + void SlotHideLabelFailed(); + + void SlotSaveImageAs(); + void SlotChangeCoverArt(); + +private: + Ui::FrmTagEditor *ui; + + mous::IPlayer* m_Player; + const mous::ITagParserFactory* m_ParserFactory; + mous::ITagParser* m_CurrentParser; + mous::MediaItem m_CurrentItem; + + QPixmap m_CurrentImage; + QLabel* m_LabelImage; + EmCoverFormat m_CurrentImgFmt; + vector m_CurrentImgData; + QString m_OldImagePath; + + QSemaphore m_SemLoadFinished; + + QSet > m_UnsavedFields; + + QTimer m_DelayTimer; +}; + diff --git a/frontend/qt5/FrmTagEditor.ui b/frontend/qt5/FrmTagEditor.ui new file mode 100644 index 0000000..d5c1af4 --- /dev/null +++ b/frontend/qt5/FrmTagEditor.ui @@ -0,0 +1,91 @@ + + + FrmTagEditor + + + + 0 + 0 + 210 + 296 + + + + Form + + + + 2 + + + + + Qt::Vertical + + + + true + + + + + 0 + 0 + 200 + 70 + + + + + + + + + + false + + + false + + + + + + + QLabel { color : red; } + + + TextLabel + + + Qt::AlignCenter + + + + + + + + + Save + + + + + + + Cancel + + + + + + + + + + + + + + diff --git a/frontend/qt5/FrmToolBar.cpp b/frontend/qt5/FrmToolBar.cpp new file mode 100644 index 0000000..b3878e8 --- /dev/null +++ b/frontend/qt5/FrmToolBar.cpp @@ -0,0 +1,39 @@ +#include "FrmToolBar.h" +#include "ui_FrmToolBar.h" + +FrmToolBar::FrmToolBar(QWidget *parent) : + QWidget(parent), + ui(new Ui::FrmToolBar) +{ + ui->setupUi(this); +} + +FrmToolBar::~FrmToolBar() +{ + delete ui; +} + +QToolButton* FrmToolBar::BtnPlay() +{ + return ui->btnPlay; +} + +QToolButton* FrmToolBar::BtnPrev() +{ + return ui->btnPrev; +} + +QToolButton* FrmToolBar::BtnNext() +{ + return ui->btnNext; +} + +QSlider* FrmToolBar::SliderVolume() +{ + return ui->sliderVolume; +} + +QSlider* FrmToolBar::SliderPlaying() +{ + return ui->sliderPlaying; +} diff --git a/frontend/qt5/FrmToolBar.h b/frontend/qt5/FrmToolBar.h new file mode 100644 index 0000000..39b4932 --- /dev/null +++ b/frontend/qt5/FrmToolBar.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +namespace Ui { +class FrmToolBar; +} + +class FrmToolBar : public QWidget +{ + Q_OBJECT + +public: + explicit FrmToolBar(QWidget *parent = 0); + ~FrmToolBar(); + + QToolButton* BtnPlay(); + QToolButton* BtnNext(); + QToolButton* BtnPrev(); + + QSlider* SliderVolume(); + QSlider* SliderPlaying(); + +private: + Ui::FrmToolBar *ui; +}; + diff --git a/frontend/qt5/FrmToolBar.ui b/frontend/qt5/FrmToolBar.ui new file mode 100644 index 0000000..823fb9e --- /dev/null +++ b/frontend/qt5/FrmToolBar.ui @@ -0,0 +1,291 @@ + + + FrmToolBar + + + + 0 + 0 + 471 + 42 + + + + Form + + + + 0 + + + 0 + + + + + + + + + :/img/resource/play.png:/img/resource/play.png + + + + 20 + 20 + + + + QToolButton::DelayedPopup + + + true + + + Qt::NoArrow + + + + + + + + + + + :/img/resource/previous.png:/img/resource/previous.png + + + + 20 + 20 + + + + true + + + + + + + + + + + :/img/resource/next.png:/img/resource/next.png + + + + 20 + 20 + + + + true + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 0 + + + + + + + + + 0 + 0 + + + + QSlider::groove:horizontal { +border: 1px solid #bbb; +background: white; +height: 8px; +border-radius: 4px; +} + +QSlider::sub-page:horizontal { +background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #87CEEB, stop: 1 #4682B4); +background: qlineargradient(x1: 0, y1: 0.2, x2: 1, y2: 1, + stop: 0 #87CEEB, stop: 1 #4682B4); +border: 1px solid #777; +height: 8px; +border-radius: 4px; +} + +QSlider::add-page:horizontal { +background: #fff; +border: 1px solid #777; +height: 8px; +border-radius: 4px; +} + +QSlider::handle:horizontal { +background: qlineargradient(x1:0, y1:0, x2:1, y2:1, + stop:0 #eee, stop:1 #ccc); +border: 1px solid #777; +width: 14px; + +margin-top: -4px; +margin-bottom: -4px; +border-radius: 8px; +} + +QSlider::handle:horizontal:hover { +background: qlineargradient(x1:0, y1:0, x2:1, y2:1, + stop:0 #fff, stop:1 #ddd); +border: 1px solid #444; +} + +QSlider::sub-page:horizontal:disabled { +background: #bbb; +border-color: #999; +} + +QSlider::add-page:horizontal:disabled { +background: #eee; +border-color: #999; +} + +QSlider::handle:horizontal:disabled { +background: #eee; +border: 1px solid #aaa; +} + + + + 100 + + + Qt::Horizontal + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 0 + + + + + + + + QSlider::groove:horizontal { +border: 1px solid #bbb; +background: white; +height: 8px; +border-radius: 4px; +} + +QSlider::sub-page:horizontal { +background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #87CEEB, stop: 1 #4682B4); +background: qlineargradient(x1: 0, y1: 0.2, x2: 1, y2: 1, + stop: 0 #87CEEB, stop: 1 #4682B4); +border: 1px solid #777; +height: 8px; +border-radius: 4px; +} + +QSlider::add-page:horizontal { +background: #fff; +border: 1px solid #777; +height: 8px; +border-radius: 4px; +} + +QSlider::handle:horizontal { +background: qlineargradient(x1:0, y1:0, x2:1, y2:1, + stop:0 #eee, stop:1 #ccc); +border: 1px solid #777; +width: 14px; + +margin-top: -4px; +margin-bottom: -4px; +border-radius: 8px; +} + +QSlider::handle:horizontal:hover { +background: qlineargradient(x1:0, y1:0, x2:1, y2:1, + stop:0 #fff, stop:1 #ddd); +border: 1px solid #444; +} + +QSlider::sub-page:horizontal:disabled { +background: #bbb; +border-color: #999; +} + +QSlider::add-page:horizontal:disabled { +background: #eee; +border-color: #999; +} + +QSlider::handle:horizontal:disabled { +background: #eee; +border: 1px solid #aaa; +} + + + + 999 + + + Qt::Horizontal + + + false + + + false + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 0 + + + + + + + + + + + diff --git a/frontend/qt5/IPlaylistView.h b/frontend/qt5/IPlaylistView.h new file mode 100644 index 0000000..c25fd88 --- /dev/null +++ b/frontend/qt5/IPlaylistView.h @@ -0,0 +1,30 @@ +#ifndef IPLAYLISTVIEW_H +#define IPLAYLISTVIEW_H + +namespace mous { + struct MediaItem; + class IMediaLoader; +} + +#include "PlaylistClipboard.h" + +class IPlaylistView +{ +public: + virtual ~IPlaylistView() { } + + virtual void SetMediaLoader(const mous::IMediaLoader* loader) = 0; + virtual void SetClipboard(PlaylistClipboard* clipboard) = 0; + + virtual const mous::MediaItem* PrevItem() const = 0; + virtual const mous::MediaItem* NextItem() const = 0; + virtual int ItemCount() const = 0; + virtual const char* PlayMode() const = 0; + virtual void SetPlayMode(int mode) = 0; + virtual void Save(const char* filename) const = 0; + virtual void Load(const char* filename) = 0; + + virtual void OnMediaItemUpdated(const mous::MediaItem& item) = 0; +}; + +#endif // IPLAYLISTVIEW_H diff --git a/frontend/qt5/MainWindow.cpp b/frontend/qt5/MainWindow.cpp new file mode 100644 index 0000000..0b5a521 --- /dev/null +++ b/frontend/qt5/MainWindow.cpp @@ -0,0 +1,480 @@ +#include "MainWindow.h" +#include "ui_MainWindow.h" + +#include "AppEnv.h" +#include "DlgListSelect.h" +#include "DlgConvertOption.h" +#include "SimplePlaylistView.h" + +#include "MidClickTabBar.hpp" +#include "CustomHeadTabWidget.hpp" +using namespace sqt; + +#include // for usleep() + +#include +using namespace scx; + +#include +using namespace mous; + +using namespace std; + +MainWindow::MainWindow(QWidget *parent) : + QMainWindow(parent), + ui(new Ui::MainWindow) +{ + ui->setupUi(this); + InitMousCore(); + InitMyUi(); + InitQtSlots(); + + m_FrmTagEditor.RestoreUiStatus(); + auto env = GlobalAppEnv::Instance(); + restoreGeometry(env->windowGeometry); + restoreState(env->windowState); +} + +MainWindow::~MainWindow() +{ + m_FrmTagEditor.WaitForLoadFinished(); + + QMutexLocker locker(&m_PlayerMutex); + + m_Player->SigFinished()->Disconnect(this); + + if (m_Player->Status() == PlayerStatus::Playing) { + m_Player->Close(); + } + if (m_TimerUpdateUi.isActive()) { + m_TimerUpdateUi.stop(); + } + + delete ui; + + ClearMousCore(); +} + +void MainWindow::closeEvent(QCloseEvent*) +{ + auto env = GlobalAppEnv::Instance(); + env->tagEditorSplitterState = m_FrmTagEditor.saveGeometry(); + env->windowGeometry = saveGeometry(); + env->windowState = saveState(); + env->tabCount = m_TabWidgetPlaylist->count(); + env->tabIndex = m_TabWidgetPlaylist->currentIndex(); + QMutexLocker locker(&m_PlayerMutex); + env->volume = m_Player->Volume(); + locker.unlock(); + m_FrmTagEditor.SaveUiStatus(); + + for (int i = 0 ; i < m_TabWidgetPlaylist->count(); ++i) { + SimplePlaylistView* view = qobject_cast(m_TabWidgetPlaylist->widget(i)); + QString filePath = env->configDir + QString("/playlist%1.dat").arg(i); + view->Save(filePath.toLatin1()); + } +} + +void MainWindow::InitMousCore() +{ + m_PluginManager = IPluginManager::Create(); + m_MediaLoader = IMediaLoader::Create(); + m_Player = IPlayer::Create(); + m_ConvFactory = IConvTaskFactory::Create(); + m_ParserFactory = ITagParserFactory::Create(); + + m_PluginManager->LoadPluginDir(GlobalAppEnv::Instance()->pluginDir.toLocal8Bit().data()); + //const vector& pathList = m_PluginManager->PluginPaths(); + + vector packAgentList = + m_PluginManager->PluginAgents(PluginType::MediaPack); + vector tagAgentList = + m_PluginManager->PluginAgents(PluginType::TagParser); + + m_MediaLoader->RegisterMediaPackPlugin(packAgentList); + m_MediaLoader->RegisterTagParserPlugin(tagAgentList); + + vector decoderAgentList = + m_PluginManager->PluginAgents(PluginType::Decoder); + vector encoderAgentList = + m_PluginManager->PluginAgents(PluginType::Encoder); + vector rendererAgentList = + m_PluginManager->PluginAgents(PluginType::Renderer); + + m_Player->RegisterRendererPlugin(rendererAgentList[0]); + m_Player->RegisterDecoderPlugin(decoderAgentList); + m_Player->SetBufferCount(102); + m_Player->SigFinished()->Connect(&MainWindow::SlotPlayerFinished, this); + + m_ConvFactory->RegisterDecoderPlugin(decoderAgentList); + m_ConvFactory->RegisterEncoderPlugin(encoderAgentList); + + m_ParserFactory->RegisterTagParserPlugin(tagAgentList); + + m_FrmTagEditor.SetPlayer(m_Player); + m_FrmTagEditor.SetTagParserFactory(m_ParserFactory); + + qDebug() << ">> MediaPack count:" << packAgentList.size(); + qDebug() << ">> TagParser count:" << tagAgentList.size(); + qDebug() << ">> Decoder count:" << decoderAgentList.size(); + qDebug() << ">> Encoder count:" << encoderAgentList.size(); + qDebug() << ">> Renderer count:" << rendererAgentList.size(); +} + +void MainWindow::ClearMousCore() +{ + m_Player->SigFinished()->Disconnect(this); + m_FrmTagEditor.SetPlayer(nullptr); + m_FrmTagEditor.SetTagParserFactory(nullptr); + + m_Player->UnregisterAll(); + m_MediaLoader->UnregisterAll(); + m_ConvFactory->UnregisterAll(); + m_ParserFactory->UnregisterAll(); + m_PluginManager->UnloadAll(); + + IPluginManager::Free(m_PluginManager); + IMediaLoader::Free(m_MediaLoader); + IPlayer::Free(m_Player); + IConvTaskFactory::Free(m_ConvFactory); + ITagParserFactory::Free(m_ParserFactory); +} + +void MainWindow::InitMyUi() +{ + auto env = GlobalAppEnv::Instance(); + + // Playing & Paused icon + m_IconPlaying.addFile(QString::fromUtf8(":/img/resource/play.png"), QSize(), QIcon::Normal, QIcon::On); + m_IconPaused.addFile(QString::fromUtf8(":/img/resource/pause.png"), QSize(), QIcon::Normal, QIcon::On); + + // Volume + if (env->volume < 0) { + env->volume = m_Player->Volume(); + } else { + m_Player->SetVolume(env->volume); + } + m_FrmToolBar.SliderVolume()->setValue(env->volume); + + // PlayList View + m_TabBarPlaylist = new MidClickTabBar(this); + m_TabWidgetPlaylist = new CustomHeadTabWidget(this); + m_TabWidgetPlaylist->SetTabBar(m_TabBarPlaylist); + m_TabWidgetPlaylist->setMovable(true); + ui->layoutPlaylist->addWidget(m_TabWidgetPlaylist); + + // Status bar buttons + m_BtnPreference = new QToolButton(ui->statusBar); + m_BtnPreference->setAutoRaise(true); + m_BtnPreference->setText(QChar((int)0x263A)); + m_BtnPreference->setToolTip(tr("Preference")); + + ui->statusBar->addPermanentWidget(m_BtnPreference, 0); + + // Recover previous playlist + for (int i = 0; i < env->tabCount; ++i) { + SlotWidgetPlayListDoubleClick(); + SimplePlaylistView* view = qobject_cast(m_TabWidgetPlaylist->widget(i)); + QString filePath = env->configDir + QString("/playlist%1.dat").arg(i); + if (QFileInfo(filePath).isFile()) + view->Load(filePath.toLatin1()); + else + qDebug() << filePath << "not exist!"; + } + m_TabWidgetPlaylist->setCurrentIndex(env->tabIndex); + + // Show left-side Dock + m_Dock = new QDockWidget(tr("Metadata")); + m_Dock->setObjectName("Dock"); + m_Dock->setWidget(&m_FrmTagEditor); + addDockWidget(Qt::LeftDockWidgetArea, m_Dock); + m_Dock->setFeatures(QDockWidget::NoDockWidgetFeatures | QDockWidget::DockWidgetMovable); + + // Show top slider + ui->toolBar->addWidget(&m_FrmToolBar); + ui->toolBar->setMovable(false); + setContextMenuPolicy(Qt::NoContextMenu); +} + +void MainWindow::InitQtSlots() +{ + connect(&m_TimerUpdateUi, SIGNAL(timeout()), this, SLOT(SlotUpdateUi())); + + connect(m_FrmToolBar.BtnPlay(), SIGNAL(clicked()), this, SLOT(SlotBtnPlay())); + connect(m_FrmToolBar.BtnPrev(), SIGNAL(clicked()), this, SLOT(SlotBtnPrev())); + connect(m_FrmToolBar.BtnNext(), SIGNAL(clicked()), this, SLOT(SlotBtnNext())); + + connect(m_FrmToolBar.SliderVolume(), SIGNAL(valueChanged(int)), this, SLOT(SlotSliderVolumeValueChanged(int))); + + connect(m_FrmToolBar.SliderPlaying(), SIGNAL(sliderPressed()), this, SLOT(SlotSliderPlayingPressed())); + connect(m_FrmToolBar.SliderPlaying(), SIGNAL(sliderReleased()), this, SLOT(SlotSliderPlayingReleased())); + connect(m_FrmToolBar.SliderPlaying(), SIGNAL(valueChanged(int)), this, SLOT(SlotSliderPlayingValueChanged(int))); + + connect(m_TabBarPlaylist, SIGNAL(SigMidClick(int)), this, SLOT(SlotBarPlayListMidClick(int))); + connect(m_TabWidgetPlaylist, SIGNAL(SigDoubleClick()), this, SLOT(SlotWidgetPlayListDoubleClick())); + + connect(&m_FrmTagEditor, SIGNAL(SigMediaItemChanged(const MediaItem&)), this, SLOT(SlotTagUpdated(const MediaItem&))); +} + +/* MousCore slots */ +void MainWindow::SlotPlayerFinished() +{ + QMetaObject::invokeMethod(this, "SlotUiPlayerFinished", Qt::QueuedConnection); +} + +void MainWindow::SlotUiPlayerFinished() +{ + qDebug() << "SlotUiPlayerFinished()"; + + if (m_UsedPlaylistView != nullptr) { + const MediaItem* item = m_UsedPlaylistView->NextItem(); + if (item != nullptr) { + SlotPlayMediaItem(m_UsedPlaylistView, *item); + } + } +} + +/* Qt slots */ +void MainWindow::SlotUpdateUi() +{ + // Update statusbar & progress slider + if (!m_PlayerMutex.tryLock()) + return; + + if (m_Player->Status() == PlayerStatus::Playing) { + long total = m_Player->RangeDuration(); + long ms = m_Player->OffsetMs(); + long hz = m_Player->SamleRate(); + long kbps = m_Player->BitRate(); + m_PlayerMutex.unlock(); + + const QString& status = QString("%1 Hz | %2 Kbps | %3:%4/%5:%6").arg(hz).arg(kbps, 4). + arg(ms/1000/60, 2, 10, QChar('0')).arg(ms/1000%60, 2, 10, QChar('0')). + arg(total/1000/60, 2, 10, QChar('0')).arg(total/1000%60, 2, 10, QChar('0')); + + ui->statusBar->showMessage(status); + + if (!m_SliderPlayingPreempted) { + int val = (double)ms / total * m_FrmToolBar.SliderPlaying()->maximum(); + m_FrmToolBar.SliderPlaying()->setSliderPosition(val); + } + + } else { + m_PlayerMutex.unlock(); + ui->statusBar->showMessage(""); + m_FrmToolBar.SliderPlaying()->setSliderPosition(0); + setWindowTitle("Mous"); + } +} + +void MainWindow::SlotBtnPlay() +{ + QMutexLocker locker(&m_PlayerMutex); + + qDebug() << m_Player->Status(); + + switch (m_Player->Status()) { + case PlayerStatus::Closed: + if (m_UsedMediaItem != nullptr) { + if (m_Player->Open(m_UsedMediaItem->url) == ErrorCode::Ok) + SlotBtnPlay(); + } + break; + + case PlayerStatus::Playing: + m_Player->Pause(); + m_TimerUpdateUi.stop(); + m_FrmToolBar.BtnPlay()->setIcon(m_IconPlaying); + break; + + case PlayerStatus::Paused: + m_TimerUpdateUi.start(m_UpdateInterval); + m_Player->Resume(); + m_FrmToolBar.BtnPlay()->setIcon(m_IconPaused); + break; + + case PlayerStatus::Stopped: + m_TimerUpdateUi.start(m_UpdateInterval); + if (m_UsedMediaItem->hasRange) + m_Player->Play(m_UsedMediaItem->msBeg, m_UsedMediaItem->msEnd); + else + m_Player->Play(); + m_FrmToolBar.BtnPlay()->setIcon(m_IconPaused); + break; + } +} + +void MainWindow::SlotBtnPrev() +{ + if (m_UsedPlaylistView == nullptr) + return; + + const mous::MediaItem* item = m_UsedPlaylistView->PrevItem(); + if (item != nullptr) + SlotPlayMediaItem(m_UsedPlaylistView, *item); +} + +void MainWindow::SlotBtnNext() +{ + if (m_UsedPlaylistView == nullptr) { + return; + } + + const mous::MediaItem* item = m_UsedPlaylistView->NextItem(); + if (item != nullptr) + SlotPlayMediaItem(m_UsedPlaylistView, *item); +} + +void MainWindow::SlotSliderVolumeValueChanged(int val) +{ + QMutexLocker locker(&m_PlayerMutex); + m_Player->SetVolume(val); +} + +void MainWindow::SlotSliderPlayingPressed() +{ + m_SliderPlayingPreempted = true; +} + +void MainWindow::SlotSliderPlayingReleased() +{ + m_SliderPlayingPreempted = false; +} + +void MainWindow::SlotSliderPlayingValueChanged(int val) +{ + if (!m_SliderPlayingPreempted) + return; + + const double& percent = (double)val / m_FrmToolBar.SliderPlaying()->maximum(); + + QMutexLocker locker(&m_PlayerMutex); + if (m_Player->Status() != PlayerStatus::Closed) + m_Player->SeekPercent(percent); +} + +void MainWindow::SlotBarPlayListMidClick(int index) +{ + if (m_TabWidgetPlaylist->count() <= 1) + return; + + SimplePlaylistView* view = (SimplePlaylistView*)m_TabWidgetPlaylist->widget(index); + m_TabWidgetPlaylist->removeTab(index); + + disconnect(view, 0, this, 0); + + delete view; + + m_TabBarPlaylist->setFocus(); +} + +void MainWindow::SlotWidgetPlayListDoubleClick() +{ + SimplePlaylistView* view = new SimplePlaylistView(this); + view->SetMediaLoader(m_MediaLoader); + view->SetClipboard(&m_Clipboard); + + connect(view, SIGNAL(SigPlayMediaItem(IPlaylistView*, const MediaItem&)), + this, SLOT(SlotPlayMediaItem(IPlaylistView*, const MediaItem&))); + connect(view, SIGNAL(SigConvertMediaItem(const MediaItem&)), + this, SLOT(SlotConvertMediaItem(const MediaItem&))); + connect(view, SIGNAL(SigConvertMediaItems(const QList&)), + this, SLOT(SlotConvertMediaItems(const QList&))); + + m_TabWidgetPlaylist->addTab(view, tr("List") + " " + QString::number(m_TabWidgetPlaylist->count())); + m_TabWidgetPlaylist->setCurrentIndex(m_TabWidgetPlaylist->count()-1); +} + +void MainWindow::SlotPlayMediaItem(IPlaylistView *view, const MediaItem& item) +{ + m_UsedPlaylistView = view; + + QMutexLocker locker(&m_PlayerMutex); + + if (m_Player->Status() == PlayerStatus::Playing) { + m_Player->Close(); + } + if (m_Player->Status() != PlayerStatus::Closed) { + m_Player->Close(); + m_TimerUpdateUi.stop(); + } + + m_UsedMediaItem = &item; + + if (m_Player->Open(item.url) != ErrorCode::Ok) { + setWindowTitle("Mous ( " + tr("Failed to open!") + " )"); + usleep(100*1000); + return SlotBtnNext(); + } + + m_TimerUpdateUi.start(m_UpdateInterval); + if (item.hasRange) + m_Player->Play(item.msBeg, item.msEnd); + else + m_Player->Play(); + m_FrmToolBar.BtnPlay()->setIcon(m_IconPaused); + + setWindowTitle("Mous ( " + QString::fromUtf8(item.tag.title.c_str()) + " )"); + + m_FrmTagEditor.LoadMediaItem(item); +} + +void MainWindow::SlotConvertMediaItem(const MediaItem& item) +{ + //==== show encoders + vector encoderNames = m_ConvFactory->EncoderNames(); + if (encoderNames.empty()) + return; + QStringList list; + for (size_t i = 0; i < encoderNames.size(); ++i) { + list << QString::fromUtf8(encoderNames[i].c_str()); + qDebug() << ">> encoder:" << i+1 << encoderNames[i].c_str(); + } + + DlgListSelect dlgEncoders(this); + dlgEncoders.setWindowTitle(tr("Available Encoders")); + dlgEncoders.SetItems(list); + dlgEncoders.SetSelectedIndex(0); + dlgEncoders.exec(); + + if (dlgEncoders.result() != QDialog::Accepted) + return; + + int encoderIndex = dlgEncoders.GetSelectedIndex(); + IConvTask* newTask = m_ConvFactory->CreateTask(item, encoderNames[encoderIndex]); + if (newTask == nullptr) + return; + + //==== show options + vector opts = newTask->EncoderOptions(); + + QString fileName = + QString::fromUtf8((item.tag.artist + " - " + item.tag.title + "." + newTask->EncoderFileSuffix()).c_str()); + DlgConvertOption dlgOption(this); + dlgOption.SetDir(QDir::homePath()); + dlgOption.SetFileName(fileName); + dlgOption.BindWidgetAndOption(opts); + dlgOption.setWindowTitle(tr("Config")); + dlgOption.exec(); + + if (dlgOption.result() != QDialog::Accepted) { + IConvTask::Free(newTask); + return; + } + + //==== do work + QString filePath = QFileInfo(dlgOption.Dir(), dlgOption.FileName()).absoluteFilePath(); + m_DlgConvertTask.show(); + m_DlgConvertTask.AddTask(newTask, filePath); +} + +void MainWindow::SlotConvertMediaItems(const QList& items) +{ + +} + +void MainWindow::SlotTagUpdated(const MediaItem& item) +{ + if (m_UsedPlaylistView != nullptr) + m_UsedPlaylistView->OnMediaItemUpdated(item); +} diff --git a/frontend/qt5/MainWindow.h b/frontend/qt5/MainWindow.h new file mode 100644 index 0000000..00cc7d9 --- /dev/null +++ b/frontend/qt5/MainWindow.h @@ -0,0 +1,111 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +using namespace mous; + +#include +using namespace std; + +#include "FrmToolBar.h" +#include "FrmTagEditor.h" +#include "IPlaylistView.h" +#include "DlgConvertTask.h" +#include "PlaylistClipboard.h" + +namespace Ui { + class MainWindow; +} + +namespace sqt { + class MidClickTabBar; + class CustomHeadTabWidget; +} + +namespace mous { + struct MediaItem; +} + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = 0); + ~MainWindow(); + +private: + void closeEvent(QCloseEvent *); + +private: + void InitMyUi(); + void InitMousCore(); + void ClearMousCore(); + void InitQtSlots(); + +private: + void SlotPlayerFinished(); +private slots: + void SlotUiPlayerFinished(); + +private slots: + void SlotUpdateUi(); + + void SlotBtnPlay(); + void SlotBtnPrev(); + void SlotBtnNext(); + + void SlotSliderVolumeValueChanged(int); + + void SlotSliderPlayingPressed(); + void SlotSliderPlayingReleased(); + void SlotSliderPlayingValueChanged(int); + + void SlotBarPlayListMidClick(int index); + void SlotWidgetPlayListDoubleClick(); + + void SlotPlayMediaItem(IPlaylistView* view, const MediaItem& item); + void SlotConvertMediaItem(const MediaItem& item); + void SlotConvertMediaItems(const QList& items); + + void SlotTagUpdated(const MediaItem& item); + +private: + Ui::MainWindow *ui = nullptr; + QDockWidget* m_Dock = nullptr; + FrmToolBar m_FrmToolBar; + FrmTagEditor m_FrmTagEditor; + sqt::MidClickTabBar* m_TabBarPlaylist = nullptr; + sqt::CustomHeadTabWidget* m_TabWidgetPlaylist = nullptr; + + QIcon m_IconPlaying; + QIcon m_IconPaused; + + QToolButton* m_BtnPreference = nullptr; + + QTimer m_TimerUpdateUi; + const int m_UpdateInterval = 500; + + IPluginManager* m_PluginManager = nullptr; + IMediaLoader* m_MediaLoader = nullptr; + IPlayer* m_Player = nullptr; + IConvTaskFactory* m_ConvFactory = nullptr; + ITagParserFactory* m_ParserFactory = nullptr; + QMutex m_PlayerMutex { QMutex::Recursive }; + + IPlaylistView* m_UsedPlaylistView = nullptr; + const MediaItem* m_UsedMediaItem = nullptr; + PlaylistClipboard m_Clipboard; + + bool m_SliderPlayingPreempted = false; + + DlgConvertTask m_DlgConvertTask; +}; + diff --git a/frontend/qt5/MainWindow.ui b/frontend/qt5/MainWindow.ui new file mode 100644 index 0000000..16fb1a7 --- /dev/null +++ b/frontend/qt5/MainWindow.ui @@ -0,0 +1,53 @@ + + + MainWindow + + + + 0 + 0 + 700 + 500 + + + + Mous + + + + + 2 + + + + + QLayout::SetDefaultConstraint + + + + + + + + + 0 + 0 + + + + + + toolBar + + + TopToolBarArea + + + false + + + + + + + diff --git a/frontend/qt5/MidClickTabBar.cpp b/frontend/qt5/MidClickTabBar.cpp new file mode 100644 index 0000000..feba09b --- /dev/null +++ b/frontend/qt5/MidClickTabBar.cpp @@ -0,0 +1,18 @@ +#include "MidClickTabBar.hpp" +#include +#include +using namespace sqt; + +MidClickTabBar::MidClickTabBar(QWidget * parent): + QTabBar(parent) +{ + +} + +void MidClickTabBar::mouseReleaseEvent(QMouseEvent *event) +{ + QTabBar::mouseReleaseEvent(event); + + if (event->button() == Qt::MiddleButton) + emit SigMidClick(tabAt(event->pos())); +} diff --git a/frontend/qt5/MidClickTabBar.hpp b/frontend/qt5/MidClickTabBar.hpp new file mode 100644 index 0000000..6ca544b --- /dev/null +++ b/frontend/qt5/MidClickTabBar.hpp @@ -0,0 +1,25 @@ +#ifndef MIDCLICKTABBAR_H +#define MIDCLICKTABBAR_H + +#include + +namespace sqt { + +class MidClickTabBar : public QTabBar +{ + Q_OBJECT + +public: + MidClickTabBar(QWidget * parent = 0); + +signals: + void SigMidClick(int id); + +private: + virtual void mouseReleaseEvent(QMouseEvent* event); + +}; + +} + +#endif // DCLICKTABBAR_H diff --git a/frontend/qt5/PlaylistActionHistory.h b/frontend/qt5/PlaylistActionHistory.h new file mode 100644 index 0000000..409737a --- /dev/null +++ b/frontend/qt5/PlaylistActionHistory.h @@ -0,0 +1,96 @@ +#ifndef PLAYLISTACTIONHISTORY_H +#define PLAYLISTACTIONHISTORY_H + +#include +#include + +template +class PlaylistActionHistory +{ +public: + enum ActionType + { + Move, + Insert, + Remove + }; + + typedef std::deque > ActionItemList; + + struct Action + { + ActionType type; + ActionItemList srcItemList; + int insertPos; + int moveVisualPos; + int moveInsertPos; + }; + +public: + PlaylistActionHistory(): + m_MaxHistory(10) + { + + } + + int MaxHistory() const + { + return m_MaxHistory; + } + + void SetMaxHistory(int n) + { + m_MaxHistory = n; + } + + void PushUndoAction(const Action& action) + { + m_UndoStack.push_back(action); + + int uselessCount = m_UndoStack.size() - m_MaxHistory; + for (; uselessCount >= 0; --uselessCount) { + m_UndoStack.pop_front(); + } + + m_RedoStack.clear(); + } + + bool HasUndoAction() + { + return !m_UndoStack.empty(); + } + + Action PopUndoAction() + { + Action action = m_UndoStack.back(); + m_UndoStack.pop_back(); + m_RedoStack.push_back(action); + return action; + } + + bool HasRedoAction() + { + return !m_RedoStack.empty(); + } + + Action TakeRedoAction() + { + Action action = m_RedoStack.back(); + m_RedoStack.pop_back(); + m_UndoStack.push_back(action); + return action; + } + + void ClearHistory() + { + m_UndoStack.clear(); + m_RedoStack.clear(); + } + +public: + int m_MaxHistory; + std::deque m_UndoStack; + std::deque m_RedoStack; +}; + +#endif // PLAYLISTACTIONHISTORY_H diff --git a/frontend/qt5/PlaylistClipboard.h b/frontend/qt5/PlaylistClipboard.h new file mode 100644 index 0000000..f761c55 --- /dev/null +++ b/frontend/qt5/PlaylistClipboard.h @@ -0,0 +1,42 @@ +#ifndef PLAYLISTCLIPBOARD_H +#define PLAYLISTCLIPBOARD_H + +#include + +template +class PlaylistClipboard +{ +public: + PlaylistClipboard(): + m_Empty(true) + { + + } + + bool Empty() const + { + return m_Empty; + } + + std::deque Content() const + { + return m_Content; + } + + void SetContent(const std::deque& content) + { + m_Content = content; + m_Empty = false; + } + + void Clear() + { + m_Empty = true; + } + +private: + bool m_Empty; + std::deque m_Content; +}; + +#endif // PLAYLISTCLIPBOARD_H diff --git a/frontend/qt5/SimplePlaylistView.cpp b/frontend/qt5/SimplePlaylistView.cpp new file mode 100644 index 0000000..baf71de --- /dev/null +++ b/frontend/qt5/SimplePlaylistView.cpp @@ -0,0 +1,887 @@ +#include "SimplePlaylistView.h" + +#include +using namespace std; + +#include +#include +#include +using namespace mous; + +#include +#include +using namespace scx; + +#include "UiHelper.hpp" +using namespace sqt; + +#include "AppEnv.h" +#include "FoobarStyle.h" + +const QString FILE_MIME = "file://"; + +typedef PlaylistActionHistory ActionHistory; + +SimplePlaylistView::SimplePlaylistView(QWidget *parent) : + QTreeView(parent), + m_MediaLoader(nullptr), + m_Clipboard(nullptr), + m_PlaylistMutex(QMutex::Recursive), + m_PlayModeGroup(this), + m_ShortcutCopy(qobject_cast(this)), + m_ShortcutCut(qobject_cast(this)), + m_ShortcutPaste(qobject_cast(this)), + m_ShortcutDelete(qobject_cast(this)), + m_ShortcutUndo(qobject_cast(this)), + m_ShortcutRedo(qobject_cast(this)) +{ + m_FoobarStyle = new FoobarStyle(style()); + setStyle(m_FoobarStyle); + + setContextMenuPolicy(Qt::ActionsContextMenu); + + QList actionList; + QAction* action = nullptr; + QMenu* menu = nullptr; + + // Action append + action = new QAction(tr("Append"), this); + connect(action, SIGNAL(triggered()), this, SLOT(SlotAppend())); + actionList << action; + + actionList << new QAction(this); + + // Action remove + action = new QAction(tr("Remove"), this); + connect(action, SIGNAL(triggered()), this, SLOT(SlotShortcutDelete())); + actionList << action; + + // Action copy + action = new QAction(tr("Copy"), this); + connect(action, SIGNAL(triggered()), this, SLOT(SlotShortcutCopy())); + actionList << action; + + // Action cut + action = new QAction(tr("Cut"), this); + connect(action, SIGNAL(triggered()), this, SLOT(SlotShortcutCut())); + actionList << action; + + // Action paste + action = new QAction(tr("Paste"), this); + connect(action, SIGNAL(triggered()), this, SLOT(SlotShortcutPaste())); + actionList << action; + + actionList << new QAction(this); + + // Action tagging + /* + action = new QAction(tr("Tagging"), this); + connect(action, SIGNAL(triggered()), this, SLOT(SlotTagging())); + actionList << action; + */ + + // Action convert + action = new QAction(tr("Convert"), this); + connect(action, SIGNAL(triggered()), this, SLOT(SlotConvert())); + actionList << action; + + // Action properties + /* + action = new QAction(tr("Properties"), this); + connect(action, SIGNAL(triggered()), this, SLOT(SlotProperties())); + actionList << action; + */ + + /* + actionList << new QAction(this); + + // Action playlist menu + action = new QAction(tr("Playlist"), this); + actionList << action; + menu = new QMenu(this); + action->setMenu(menu); + + action = new QAction(tr("Load"), this); + menu->addAction(action); + connect(action, SIGNAL(triggered()), this, SLOT(SlotPlaylistLoad())); + + action = new QAction(tr("Rename"), this); + menu->addAction(action); + connect(action, SIGNAL(triggered()), this, SLOT(SlotPlaylistRename())); + + action = new QAction(tr("Save As"), this); + menu->addAction(action); + connect(action, SIGNAL(triggered()), this, SLOT(SlotPlaylistSaveAs())); + */ + + // Action play mode menu + action = new QAction(tr("Play Mode"), this); + actionList << action; + menu = new QMenu(this); + action->setMenu(menu); + + action = new QAction(tr("Normal"), this); + action->setCheckable(true); + m_PlayModeGroup.addAction(action); + action = new QAction(tr("Repeat"), this); + action->setCheckable(true); + m_PlayModeGroup.addAction(action); + action->setChecked(true); + action = new QAction(tr("Shuffle"), this); + action->setCheckable(true); + m_PlayModeGroup.addAction(action); + action = new QAction(tr("Shuffle Repeat"), this); + action->setCheckable(true); + m_PlayModeGroup.addAction(action); + action = new QAction(tr("Repeat One"), this); + action->setCheckable(true); + m_PlayModeGroup.addAction(action); + m_PlayModeGroup.setExclusive(true); + menu->addActions(m_PlayModeGroup.actions()); + + // Style + sqt::SetActionSeparator(actionList); + addActions(actionList); + + setDragEnabled(true); + setAcceptDrops(true); + setDropIndicatorShown(true); + setDragDropMode(QAbstractItemView::DragDrop); + setDefaultDropAction(Qt::IgnoreAction); + + setRootIsDecorated(false); + setItemsExpandable(false); + setAlternatingRowColors(true); + setUniformRowHeights(true); + + setSelectionMode(QAbstractItemView::ExtendedSelection); + setSelectionBehavior(QAbstractItemView::SelectRows); + + setModel(&m_ItemModel); + header()->setSectionResizeMode(QHeaderView::Stretch); + + // Header + QStringList headList; + headList << tr("Album") << tr("Artist") << tr("Title") << tr("Track") << tr("Duration"); + m_ItemModel.setHorizontalHeaderLabels(headList); + m_ItemModel.setColumnCount(headList.size()); + + // Test + /* + m_ItemModel.setRowCount(0); + for (int row = 0; row < m_ItemModel.rowCount(); ++row) { + for (int column = 0; column < m_ItemModel.columnCount(); ++column) { + QStandardItem *item = new QStandardItem(QString("row %0, column %1").arg(row).arg(column)); + item.setEditable(false); + item.setSizeHint(QSize(-1, 25)); + m_ItemModel.setItem(row, column, item); + } + } + */ + + m_Playlist.SetMode(PlaylistMode::Repeat); + + // connect + connect(this, SIGNAL(SigReadyToLoad()), + this, SLOT(SlotReadyToLoad()), Qt::BlockingQueuedConnection); + connect(this, SIGNAL(SigLoadFinished()), + this, SLOT(SlotLoadFinished()), Qt::BlockingQueuedConnection); + connect(this, SIGNAL(SigListRowGot(const ListRow&)), + this, SLOT(SlotListRowGot(const ListRow&)), Qt::BlockingQueuedConnection); + + connect(&m_PlayModeGroup, SIGNAL(triggered(QAction*)), this, SLOT(SlotPlayModeMenu(QAction*))); + + SetupShortcuts(); +} + +SimplePlaylistView::~SimplePlaylistView() +{ + QList actionList = actions(); + while (!actionList.isEmpty()) { + QAction* action = actionList.takeFirst(); + removeAction(action); + if (action->menu() != nullptr) + delete action->menu(); + delete action; + } + + while (m_ItemModel.rowCount() > 0) { + QList rowList = m_ItemModel.takeRow(0); + for(int i = 0; i < rowList.size(); ++i) + delete rowList[i]; + } + + m_Playlist.Clear(); + +} + +/* IPlayListView interfaces */ +void SimplePlaylistView::SetMediaLoader(const IMediaLoader* loader) +{ + m_MediaLoader = loader; +} + +void SimplePlaylistView::SetClipboard(PlaylistClipboard* clipboard) +{ + m_Clipboard = clipboard; +} + +const MediaItem* SimplePlaylistView::NextItem() const +{ + QMutexLocker locker(&m_PlaylistMutex); + return m_Playlist.HasNext(1) ? &m_Playlist.NextItem(1, true) : nullptr; +} + +const MediaItem* SimplePlaylistView::PrevItem() const +{ + QMutexLocker locker(&m_PlaylistMutex); + return m_Playlist.HasNext(-1) ? &m_Playlist.NextItem(-1, true) : nullptr; +} + +int SimplePlaylistView::ItemCount() const +{ + QMutexLocker locker(&m_PlaylistMutex); + return m_Playlist.Count(); +} + +const char* SimplePlaylistView::PlayMode() const +{ + QMutexLocker locker(&m_PlaylistMutex); + int mode = (int)m_Playlist.Mode(); + locker.unlock(); + + if (mode < 0 || mode > m_PlayModeGroup.actions().size()) + return ""; + else + return m_PlayModeGroup.actions()[mode]->text().toLocal8Bit().data(); +} + +void SimplePlaylistView::SetPlayMode(int mode) +{ + m_PlayModeGroup.actions()[mode]->setChecked(true); +} + +void SimplePlaylistView::Save(const char* filename) const +{ + QMutexLocker locker(&m_PlaylistMutex); + + typedef PlaylistSerializer Serializer; + Serializer::Store(m_Playlist, filename); +} + +void SimplePlaylistView::Load(const char* filename) +{ + QMutexLocker locker(&m_PlaylistMutex); + + typedef PlaylistSerializer Serializer; + Serializer::Load(m_Playlist, filename); + + for (int i = 0; i < m_Playlist.Count(); ++i) { + ListRow listRow = BuildListRow(m_Playlist[i]); + m_ItemModel.appendRow(listRow.fields); + } + + int index = (int)m_Playlist.Mode(); + m_PlayModeGroup.actions()[index]->setChecked(true); +} + +void SimplePlaylistView::OnMediaItemUpdated(const mous::MediaItem& item) +{ + QMutexLocker locker(&m_PlaylistMutex); + + if (m_Playlist.Empty()) + return; + + // we SHOULD check all items here!!! + for (int row = 0; row < m_Playlist.Count(); ++row) { + MediaItem& destItem = m_Playlist[row]; + if (destItem.url == item.url && + destItem.duration == item.duration && + destItem.hasRange == item.hasRange && + destItem.msBeg == item.msBeg && + destItem.msEnd == item.msEnd) { + destItem = item; + m_ItemModel.item(row, 0)->setText(QString::fromUtf8(item.tag.album.c_str())); + m_ItemModel.item(row, 1)->setText(QString::fromUtf8(item.tag.artist.c_str())); + m_ItemModel.item(row, 2)->setText(QString::fromUtf8(item.tag.title.c_str())); + m_ItemModel.item(row, 3)->setText(QString::number(item.tag.track)); + } + } +} + +void SimplePlaylistView::SetupShortcuts() +{ + m_ShortcutCopy.setKey(QKeySequence::Copy); + m_ShortcutCut.setKey(QKeySequence::Cut); + m_ShortcutPaste.setKey(QKeySequence::Paste); + m_ShortcutDelete.setKey(QKeySequence::Delete); + m_ShortcutUndo.setKey(QKeySequence::Undo); + m_ShortcutRedo.setKey(QKeySequence::Redo); + + connect(&m_ShortcutCopy, SIGNAL(activated()), this, SLOT(SlotShortcutCopy())); + connect(&m_ShortcutCut, SIGNAL(activated()), this, SLOT(SlotShortcutCut())); + connect(&m_ShortcutPaste, SIGNAL(activated()), this, SLOT(SlotShortcutPaste())); + connect(&m_ShortcutDelete, SIGNAL(activated()), this, SLOT(SlotShortcutDelete())); + connect(&m_ShortcutUndo, SIGNAL(activated()), this, SLOT(SlotShortcutUndo())); + connect(&m_ShortcutRedo, SIGNAL(activated()), this, SLOT(SlotShortcutRedo())); +} + +/* Override qt methods */ +void SimplePlaylistView::mouseDoubleClickEvent(QMouseEvent * event) +{ + QTreeView::mouseDoubleClickEvent(event); + + QMutexLocker locker(&m_PlaylistMutex); + + if (m_Playlist.Empty()) + return; + + QModelIndex index(selectedIndexes()[0]); + qDebug() << index.row(); + + m_Playlist.JumpTo(index.row()); + const MediaItem& item = m_Playlist.NextItem(0, false); + + emit SigPlayMediaItem(this, item); +} + +void SimplePlaylistView::dragEnterEvent(QDragEnterEvent *event) +{ + QTreeView::dragEnterEvent(event); + event->accept(); +} + +void SimplePlaylistView::dragMoveEvent(QDragMoveEvent *event) +{ + QTreeView::dragMoveEvent(event); + event->accept(); +} + +void SimplePlaylistView::dropEvent(QDropEvent *event) +{ + event->accept(); + + const QString& text = event->mimeData()->text(); + + QMutexLocker locker(&m_PlaylistMutex); + + if (text.startsWith(FILE_MIME)) { + qDebug() << "!drop:append file"; + + QStringList files = text.split(FILE_MIME, QString::SkipEmptyParts); + for (int i = 0; i < files.size(); ++i) { + QString file = QUrl(files[i].trimmed()).toLocalFile(); + // try parse + if (!QFileInfo(file).isFile()) { + file = QUrl::fromEncoded(file.toLocal8Bit()).toLocalFile(); + } + files[i] = file; + + qDebug() << files[i]; + } + + std::thread([this, files] { + LoadMediaItem(files); + }).detach(); + } else if (text.isEmpty()) { + QList rowList = PickSelectedRows(); + qSort(rowList); + if (!rowList.empty()) { + // calc insert pos + int visualInsertPos = indexAt(m_FoobarStyle->BelowIndicator()).row(); + if (visualInsertPos == -1) + visualInsertPos = m_Playlist.Count(); + + int realInsertPos = visualInsertPos; + for (int i = 0; i < rowList.size(); ++i) { + if (rowList[i] < visualInsertPos) + --realInsertPos; + } + + qDebug() << "visualInsertPos" << visualInsertPos; + qDebug() << "realInsertPos" << realInsertPos; + + // copy & remove + deque content(rowList.count()); + for (int i = 0; i < rowList.size(); ++i) { + content[i] = m_Playlist[rowList[i]]; + } + for (int i = rowList.size()-1; i >= 0; --i) { + int delPos = rowList[i]; + m_ItemModel.removeRow(delPos); + } + + // insert or append(optimized?) + ActionHistory::Action action; + action.type = ActionHistory::Move; + action.moveVisualPos = visualInsertPos; + action.moveInsertPos = realInsertPos; + + for (size_t i = 0; i < content.size(); ++i) { + const ListRow& listRow = BuildListRow(content[i]); + m_ItemModel.insertRow(realInsertPos+i, listRow.fields); + action.srcItemList.push_back(std::pair(rowList[i], listRow.item)); + } + + // as for playlist, we already have "move" + m_Playlist.Move(rowList.toVector().toStdVector(), visualInsertPos); + + // Record operation + if (!action.srcItemList.empty()) + m_History.PushUndoAction(action); + } + } + + QTreeView::dropEvent(event); +} + +/* Action menus */ +void SimplePlaylistView::SlotAppend() +{ + // Pick media files + QString oldPath("~"); + if (!m_PrevMediaFilePath.isEmpty()) { + QFileInfo info(m_PrevMediaFilePath); + oldPath = info.dir().dirName(); + } + QStringList pathList = QFileDialog::getOpenFileNames( + this, tr("Open Media"), oldPath, "*"); + if (pathList.isEmpty()) + return; + + m_PrevMediaFilePath = pathList.first(); + + // Async load + std::thread([this, pathList] { + LoadMediaItem(pathList); + }).detach(); +} + +void SimplePlaylistView::SlotTagging() +{ + +} + +void SimplePlaylistView::SlotConvert() +{ + QMutexLocker locker(&m_PlaylistMutex); + + if (m_Playlist.Empty()) + return; + + QModelIndexList list = selectedIndexes(); + if (list.empty()) + return; + + QModelIndex index(list[0]); + qDebug() << index.row(); + + MediaItem& item = m_Playlist[index.row()]; + + emit SigConvertMediaItem(item); +} + +void SimplePlaylistView::SlotProperties() +{ + +} + +void SimplePlaylistView::SlotPlaylistLoad() +{ + +} + +void SimplePlaylistView::SlotPlaylistRename() +{ + +} + +void SimplePlaylistView::SlotPlaylistSaveAs() +{ + +} + +void SimplePlaylistView::SlotReadyToLoad() +{ + setUpdatesEnabled(false); + + m_DlgLoadingMedia.setWindowTitle(tr("Loading")); + m_DlgLoadingMedia.show(); +} + + +void SimplePlaylistView::SlotLoadFinished() +{ + setUpdatesEnabled(true); + m_DlgLoadingMedia.hide(); +} + +void SimplePlaylistView::SlotListRowGot(const ListRow& listRow) +{ + QFileInfo info(QString::fromUtf8(listRow.item.url.c_str())); + QString fileName(info.fileName()); + + m_DlgLoadingMedia.SetFileName(fileName); + + QMutexLocker locker(&m_PlaylistMutex); + + m_Playlist.Append(listRow.item); + m_ItemModel.appendRow(listRow.fields); +} + +void SimplePlaylistView::SlotPlayModeMenu(QAction* action) +{ + QMutexLocker locker(&m_PlaylistMutex); + + int index = m_PlayModeGroup.actions().indexOf(action); + if (index < 0) + return; + m_Playlist.SetMode((EmPlaylistMode)index); +} + +void SimplePlaylistView::SlotShortcutCopy() +{ + QMutexLocker locker(&m_PlaylistMutex); + + qDebug() << "copy"; + + if (m_Clipboard == nullptr) + return; + + QList selectedRows = PickSelectedRows(); + if (selectedRows.empty()) { + m_Clipboard->Clear(); + return; + } + + deque contents; + for (int i = 0; i < selectedRows.size(); ++i) { + contents.push_back(m_Playlist[selectedRows[i]]); + } + m_Clipboard->SetContent(contents); +} + +void SimplePlaylistView::SlotShortcutCut() +{ + qDebug() << "cut"; + + SlotShortcutCopy(); + SlotShortcutDelete(); +} + +void SimplePlaylistView::SlotShortcutPaste() +{ + QMutexLocker locker(&m_PlaylistMutex); + + qDebug() << "paste"; + + if (m_Clipboard == nullptr || m_Clipboard->Empty()) + return; + + deque content = m_Clipboard->Content(); + + // insert or append(optimized?) + QList selectedRows = PickSelectedRows(); + int insertPos = selectedRows.count() == 1 ? selectedRows[0] : -1; + + ActionHistory::Action action; + action.type = ActionHistory::Insert; + action.insertPos = insertPos; + + for (size_t i = 0; i < content.size(); ++i) { + const ListRow& listRow = BuildListRow(content[i]); + if (insertPos != -1) { + m_Playlist.Insert(insertPos+i, listRow.item); + m_ItemModel.insertRow(insertPos+i, listRow.fields); + } else { + m_Playlist.Append(listRow.item); + m_ItemModel.appendRow(listRow.fields); + } + action.srcItemList.push_back(std::pair(-1, listRow.item)); + } + + // Record operation + if (!action.srcItemList.empty()) + m_History.PushUndoAction(action); +} + +void SimplePlaylistView::SlotShortcutDelete() +{ + QMutexLocker locker(&m_PlaylistMutex); + + qDebug() << "delete"; + + QList selectedRows = PickSelectedRows(); + if (selectedRows.empty()) + return; + qSort(selectedRows); + + ActionHistory::Action action; + action.type = ActionHistory::Remove; + + for (int i = selectedRows.size()-1; i >= 0; --i) { + int delPos = selectedRows[i]; + + // push at front to ensure asc seq + action.srcItemList.push_front(std::pair(delPos, m_Playlist[delPos])); + + m_Playlist.Remove(delPos); + m_ItemModel.removeRow(delPos); + } + + // Record operation + if (!action.srcItemList.empty()) + m_History.PushUndoAction(action); +} + +void SimplePlaylistView::SlotShortcutUndo() +{ + QMutexLocker locker(&m_PlaylistMutex); + + qDebug() << "undo"; + + if (m_History.HasUndoAction()) { + ActionHistory::Action action = m_History.PopUndoAction(); + const int n = action.srcItemList.size(); + switch (action.type) { + case ActionHistory::Insert: + { + assert(!m_Playlist.Empty() && m_ItemModel.rowCount() != 0); + + // previously inserted or appended ? + const int rmSince = action.insertPos != -1 ? action.insertPos : m_Playlist.Count() - n; + vector indexes(n); + for (int i = 0; i < n; ++i) { + indexes[i] = rmSince + i; + } + m_Playlist.Remove(indexes); + m_ItemModel.removeRows(rmSince, n); + } + break; + + case ActionHistory::Remove: + { + // the seq should already be asc here + for (int i = 0; i < n; ++i) { + int insertPos = action.srcItemList[i].first; + MediaItem& item = action.srcItemList[i].second; + const ListRow& listRow = BuildListRow(item); + m_Playlist.Insert(insertPos, listRow.item); + m_ItemModel.insertRow(insertPos, listRow.fields); + } + } + break; + + case ActionHistory::Move: + { + int moveInsertPos = action.moveInsertPos; + for (int i = 0; i < n; ++i) { + MediaItem& item = action.srcItemList[i].second; + const ListRow& listRow = BuildListRow(item); + m_ItemModel.removeRow(moveInsertPos + i); + m_ItemModel.insertRow(action.srcItemList[i].first, listRow.fields); + m_Playlist.Move(vector(1, moveInsertPos + i), action.srcItemList[i].first); + } + } + break; + + default: + break; + } + } +} + +void SimplePlaylistView::SlotShortcutRedo() +{ + QMutexLocker locker(&m_PlaylistMutex); + + qDebug() << "redo"; + + if (m_History.HasRedoAction()) { + ActionHistory::Action action = m_History.TakeRedoAction(); + const int n = action.srcItemList.size(); + switch (action.type) { + case ActionHistory::Insert: + { + // insert or append + for (int i = 0; i < n; ++i) { + ListRow listRow = BuildListRow(action.srcItemList[i].second); + if (action.insertPos != -1) { + m_Playlist.Insert(action.insertPos+i, listRow.item); + m_ItemModel.insertRow(action.insertPos+i, listRow.fields); + } else { + m_Playlist.Append(listRow.item); + m_ItemModel.appendRow(listRow.fields); + } + } + } + break; + + case ActionHistory::Remove: + { + // original seq is asc + for (int i = n-1; i >= 0; --i) { + int delPos = action.srcItemList[i].first; + m_Playlist.Remove(delPos); + m_ItemModel.removeRow(delPos); + } + } + break; + + case ActionHistory::Move: + { + // remove + for (int i = n-1; i >= 0; --i) { + int delPos = action.srcItemList[i].first; + m_ItemModel.removeRow(delPos); + } + + vector oldPos(n); + int begPos = action.moveInsertPos; + for (int i = 0; i < n; ++i) { + oldPos[i] = action.srcItemList[i].first; + const ListRow& listRow = BuildListRow(action.srcItemList[i].second); + m_ItemModel.insertRow(begPos+i, listRow.fields); + } + + // as for playlist, we already have "move" + m_Playlist.Move(oldPos, action.moveVisualPos); + } + break; + + default: + break; + } + } +} + +void SimplePlaylistView::LoadMediaItem(const QStringList& pathList) +{ + if (pathList.empty()) + return; + + //emit SigReadyToLoad(); + + // Prepare for history + ActionHistory::Action action; + action.type = ActionHistory::Insert; + action.insertPos = -1; + + string ifNotUtf8 = GlobalAppEnv::Instance()->ifNotUtf8.toStdString(); + + for (int i = 0; i < pathList.size(); ++i) { + if (pathList.at(i).isEmpty()) + continue; + + // Although load ok, + // the item may still invaild(player won't be able to play it) + deque mediaItemList; + const char* filePath = pathList.at(i).toUtf8().data(); + + if (m_MediaLoader->LoadMedia(filePath, mediaItemList) != ErrorCode::Ok) + continue; + + for(size_t j = 0; j < mediaItemList.size(); ++j) { + MediaItem& item = mediaItemList[j]; + CorrectTagCharset(item.tag, ifNotUtf8); + ListRow listRow = BuildListRow(item); + action.srcItemList.push_back(std::pair(-1, item)); + emit SigListRowGot(listRow); + } + } + + // Record operation + if (!action.srcItemList.empty()) + m_History.PushUndoAction(action); + + //emit SigLoadFinished(); +} + +QList SimplePlaylistView::PickSelectedRows() const +{ + QList rowList; + QModelIndexList indexList = selectedIndexes(); + QSet rowSet; + for (int i = 0; i < indexList.size(); ++i) { + int row = indexList[i].row(); + if (!rowSet.contains(row)) { + rowSet.insert(row); + rowList.append(row); + } + } + return rowList; +} + +// TODO: this function should be seprated in to a head file, +// then FrmTagEditor::LoadMediaItem() can be LoadMediaFile() +void SimplePlaylistView::CorrectTagCharset(MediaTag& tag, const string& ifNotUtf8) const +{ + string tmp; + // artist + if (!CharsetHelper::IsUtf8(tag.artist.c_str()) + && IconvHelper::ConvFromTo(ifNotUtf8, "UTF-8", tag.artist.data(), tag.artist.size(), tmp)) + tag.artist = tmp; + //else + // qDebug() << "no touch:" << QString::fromUtf8(tag.artist.c_str()); + + // album + if (!CharsetHelper::IsUtf8(tag.album.c_str()) + && IconvHelper::ConvFromTo(ifNotUtf8, "UTF-8", tag.album.data(), tag.album.size(), tmp)) + tag.album = tmp; + //else + // qDebug() << "no touch:" << QString::fromUtf8(tag.album.c_str()); + + // title + if (!CharsetHelper::IsUtf8(tag.title.c_str()) + && IconvHelper::ConvFromTo(ifNotUtf8, "UTF-8", tag.title.data(), tag.title.size(), tmp)) + tag.title = tmp; + //else + // qDebug() << "no touch:" << QString::fromUtf8(tag.title.c_str()); + + // comment + if (!CharsetHelper::IsUtf8(tag.comment.c_str()) + && IconvHelper::ConvFromTo(ifNotUtf8, "UTF-8", tag.comment.data(), tag.comment.size(), tmp)) + tag.comment = tmp; + //else + // qDebug() << "no touch:" << QString::fromUtf8(tag.comment.c_str()); + + // genre + if (!CharsetHelper::IsUtf8(tag.genre.c_str()) + && IconvHelper::ConvFromTo(ifNotUtf8, "UTF-8", tag.genre.data(), tag.genre.size(), tmp)) + tag.genre = tmp; + //else + // qDebug() << "no touch:" << QString::fromUtf8(tag.genre.c_str()); +} + +SimplePlaylistView::ListRow SimplePlaylistView::BuildListRow(MediaItem& item) const +{ + ListRow listRow; + listRow.item = item; + + // Check sec duration + int secDuration = 0; + if (item.hasRange) { + if (item.msEnd != (uint64_t)-1) + secDuration = (item.msEnd - item.msBeg)/1000; + else + secDuration = (item.duration - item.msBeg)/1000; + } else { + secDuration = item.duration/1000; + } + QString strDuration; + strDuration.sprintf("%.2d:%.2d", secDuration/60, secDuration%60); + + // Build fields + listRow.fields << new QStandardItem(QString::fromUtf8(item.tag.album.c_str())); + listRow.fields << new QStandardItem(QString::fromUtf8(item.tag.artist.c_str())); + listRow.fields << new QStandardItem(QString::fromUtf8(item.tag.title.c_str())); + listRow.fields << new QStandardItem(QString::number(item.tag.track)); + listRow.fields << new QStandardItem(strDuration); + for (int i = 0; i < listRow.fields.size(); ++i) { + QStandardItem* item = listRow.fields[i]; + item->setEditable(false); + item->setSizeHint(QSize(-1, 22)); + } + + return listRow; +} diff --git a/frontend/qt5/SimplePlaylistView.h b/frontend/qt5/SimplePlaylistView.h new file mode 100644 index 0000000..b3c9421 --- /dev/null +++ b/frontend/qt5/SimplePlaylistView.h @@ -0,0 +1,119 @@ +#pragma once + +#include + +#include + +#include +#include +using namespace mous; + +#include "IPlaylistView.h" +#include "DlgLoadingMedia.h" +#include "PlaylistActionHistory.h" + +class FoobarStyle; + +class SimplePlaylistView : public QTreeView, public IPlaylistView +{ + Q_OBJECT + +public: + explicit SimplePlaylistView(QWidget *parent = 0); + +public: + virtual ~SimplePlaylistView(); + + virtual void SetMediaLoader(const IMediaLoader* loader); + virtual void SetClipboard(PlaylistClipboard* clipboard); + + virtual const MediaItem* NextItem() const; + virtual const MediaItem* PrevItem() const; + virtual int ItemCount() const; + virtual const char* PlayMode() const; + virtual void SetPlayMode(int mode); + virtual void Save(const char* filename) const; + virtual void Load(const char* filename); + + virtual void OnMediaItemUpdated(const mous::MediaItem& item); + +signals: + void SigPlayMediaItem(IPlaylistView *view, const MediaItem& item); + void SigConvertMediaItem(const MediaItem& item); + void SigConvertMediaItems(const QList& items); + +private: + struct ListRow + { + MediaItem item; + QList fields; + }; + +private: + void SetupShortcuts(); + + void mouseDoubleClickEvent(QMouseEvent * event); + void dragEnterEvent(QDragEnterEvent *event); + void dragMoveEvent(QDragMoveEvent *event); + void dropEvent(QDropEvent *event); + +private slots: + void SlotAppend(); + + void SlotTagging(); + void SlotConvert(); + void SlotProperties(); + + void SlotPlaylistLoad(); + void SlotPlaylistRename(); + void SlotPlaylistSaveAs(); + + void SlotReadyToLoad(); + void SlotLoadFinished(); + void SlotListRowGot(const ListRow& listRow); + + void SlotPlayModeMenu(QAction*); + + void SlotShortcutCopy(); + void SlotShortcutCut(); + void SlotShortcutPaste(); + void SlotShortcutDelete(); + void SlotShortcutUndo(); + void SlotShortcutRedo(); + +signals: + void SigReadyToLoad(); + void SigListRowGot(const ListRow& listRow); + void SigLoadFinished(); + +private: + void LoadMediaItem(const QStringList& pathList); + QList PickSelectedRows() const; + void CorrectTagCharset(MediaTag& tag, const std::string& ifNotUtf8) const; + ListRow BuildListRow(MediaItem &item) const; + +private: + const IMediaLoader* m_MediaLoader; + PlaylistClipboard* m_Clipboard; + + QString m_PrevMediaFilePath; + + QStandardItemModel m_ItemModel; + Playlist m_Playlist; + mutable QMutex m_PlaylistMutex; + + DlgLoadingMedia m_DlgLoadingMedia; + + PlaylistActionHistory m_History; + + FoobarStyle* m_FoobarStyle; + + QActionGroup m_PlayModeGroup; + QShortcut m_ShortcutCopy; + QShortcut m_ShortcutCut; + QShortcut m_ShortcutPaste; + QShortcut m_ShortcutDelete; + QShortcut m_ShortcutUndo; + QShortcut m_ShortcutRedo; +}; + diff --git a/frontend/qt5/UiHelper.hpp b/frontend/qt5/UiHelper.hpp new file mode 100644 index 0000000..84f9cc7 --- /dev/null +++ b/frontend/qt5/UiHelper.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include + +namespace sqt { + +static inline void SetActionSeparator(QList list) +{ + for (int i = 0; i < list.size(); ++i) { + QAction* action = list[i]; + if (action->text().isEmpty()) + action->setSeparator(true); + } +} + +static inline void AdjustAllStackPages(QStackedWidget* stack, QSizePolicy plicy) +{ + for (int i = 0; i < stack->count(); ++i) + { + stack->widget(i)->setSizePolicy(plicy); + } +} + +static inline void SwitchStackPage(QStackedWidget* stack, int index) +{ + const int pageCount = stack->count(); + if (pageCount != 0) + { + QSizePolicy policyMin(QSizePolicy::Ignored, QSizePolicy::Ignored); + QSizePolicy policyMax(QSizePolicy::Expanding, QSizePolicy::Expanding); + + // minimal previous page + if (stack->currentIndex() != index) + { + stack->currentWidget()->setSizePolicy(policyMin); + } + else + { + // minimal all page + AdjustAllStackPages(stack, policyMin); + } + // show and maximal new page + stack->setCurrentIndex(index); + stack->currentWidget()->setSizePolicy(policyMax); + stack->adjustSize(); + } +} + +} + diff --git a/frontend/qt5/main.cpp b/frontend/qt5/main.cpp new file mode 100644 index 0000000..3c0563c --- /dev/null +++ b/frontend/qt5/main.cpp @@ -0,0 +1,30 @@ +#include +#include + +#include "AppEnv.h" +#include "MainWindow.h" + +int main(int argc, char *argv[]) +{ + auto env = GlobalAppEnv::Instance(); + auto flag = env->Init(); + if (!flag) { + qDebug() << "bad"; + } + + + QApplication app(argc, argv); + + QTranslator translator; + translator.load(env->translationFile); + app.installTranslator(&translator); + + MainWindow win; + win.show(); + + int ret = app.exec(); + + env->Save(); + + return ret; +} diff --git a/frontend/qt5/mous-qt.pro b/frontend/qt5/mous-qt.pro new file mode 100644 index 0000000..a8c26e9 --- /dev/null +++ b/frontend/qt5/mous-qt.pro @@ -0,0 +1,69 @@ +#------------------------------------------------- +# +# Project created by QtCreator 2012-02-20T22:12:47 +# +#------------------------------------------------- + +QT += widgets +CONFIG += c++14 + +TARGET = mous-qt +TEMPLATE = app + +INCLUDEPATH += ../../sdk +LIBS += -liconv -L /Users/bsdelf/myapp/lib/ -lMousCore + +macx { + #QMAKE_CXXFLAGS_RELEASE += -fvisibility=hidden + #QMAKE_CXXFLAGS_DEBUG += -fvisibility=hidden + #LIBS += -framework + CONFIG += x86_64 + QMAKE_MACOSX_DEPLOYMENT_TARGET = 10.12 +} + + +SOURCES += main.cpp\ + MainWindow.cpp \ + CustomHeadTabWidget.cpp \ + MidClickTabBar.cpp \ + DlgListSelect.cpp \ + DlgLoadingMedia.cpp \ + FrmProgressBar.cpp \ + DlgConvertTask.cpp \ + DlgConvertOption.cpp \ + FrmToolBar.cpp \ + FrmTagEditor.cpp \ + SimplePlaylistView.cpp \ + AppEnv.cpp + +HEADERS += MainWindow.h \ + CustomHeadTabWidget.hpp \ + MidClickTabBar.hpp \ + UiHelper.hpp \ + DlgListSelect.h \ + DlgLoadingMedia.h \ + FrmProgressBar.h \ + DlgConvertTask.h \ + DlgConvertOption.h \ + FrmToolBar.h \ + FrmTagEditor.h \ + IPlaylistView.h \ + SimplePlaylistView.h \ + PlaylistActionHistory.h \ + PlaylistClipboard.h \ + FoobarStyle.h \ + AppEnv.h + +FORMS += MainWindow.ui \ + DlgListSelect.ui \ + DlgLoadingMedia.ui \ + FrmProgressBar.ui \ + DlgConvertTask.ui \ + DlgConvertOption.ui \ + FrmToolBar.ui \ + FrmTagEditor.ui + +TRANSLATIONS = mous-qt_zh_CN.ts + +RESOURCES += \ + AllRes.qrc diff --git a/frontend/qt5/mous-qt_zh_CN.ts b/frontend/qt5/mous-qt_zh_CN.ts new file mode 100644 index 0000000..ca9db47 --- /dev/null +++ b/frontend/qt5/mous-qt_zh_CN.ts @@ -0,0 +1,382 @@ + + + + + DlgConvertOption + + + Dialog + + + + + Encoder Optoin + 编码器选项 + + + + Output File + 输出文件 + + + + Directory: + 目录: + + + + File Name: + 文件名: + + + + OK + 确定 + + + + Cancel + 取消 + + + + DlgConvertTask + + + Dialog + + + + + Auto Close This Dialog Once Finished. + 任务结束时自动关闭窗口 + + + + DlgListSelect + + + Dialog + + + + + OK + 确定 + + + + Cancel + 取消 + + + + DlgLoadingMedia + + + Dialog + + + + + hint + + + + + foo + + + + + FrmProgressBar + + + Form + + + + + FileName + 文件名 + + + + %p% + + + + + -- : -- + + + + + Cancel + 取消 + + + + FrmTagEditor + + + Form + + + + + TextLabel + + + + + Save + 保存 + + + + Cancel + 取消 + + + + + Save Image As + 图片另存为 + + + + Change Cover Art + 更换封面图片 + + + + Name + 属性 + + + + Value + + + + + Album + 专辑 + + + + Title + 标题 + + + + Artist + 艺术家 + + + + Genre + 风格 + + + + Year + 年代 + + + + Track + 轨道 + + + + Comment + 备注 + + + + Failed to save! + 保存失败! + + + + Select Image File + 选择图片 + + + + Images (*.jpg *.png) + 图片格式 (*.jpg *png) + + + + FrmToolBar + + + Form + + + + + MainWindow + + + Mous + + + + + toolBar + + + + + Preference + 设置 + + + + Metadata + 元信息 + + + + List + 列表 + + + + Failed to open! + 打开失败! + + + + Available Encoders + 可用的编码器 + + + + Config + 配置 + + + + SimplePlaylistView + + + Append + 添加 + + + + Remove + 删除 + + + + Copy + 复制 + + + + Cut + 剪切 + + + + Paste + 粘贴 + + + + Convert + 格式转换 + + + Properties + 文件属性 + + + Playlist + 播放列表 + + + Load + 载入 + + + Rename + 重命名 + + + Save As + 另存为 + + + + Play Mode + 播放模式 + + + + Normal + 常规播放 + + + + Repeat + 重复播放 + + + + Shuffle + 随机播放 + + + + Shuffle Repeat + 随机重复播放 + + + + Repeat One + 单曲 && 重复播放 + + + + Artist + 艺术家 + + + + Album + 专辑 + + + + Title + 标题 + + + + Track + 轨道 + + + + Duration + 时间 + + + + Open Media + 打开媒体文件 + + + + Loading + + + + diff --git a/frontend/qt5/resource/next.png b/frontend/qt5/resource/next.png new file mode 100644 index 0000000000000000000000000000000000000000..8436606795dacf48df2519bc988de9d9bd8785a8 GIT binary patch literal 1209 zcmV;q1V;ObP)kdg00002b3#c}2nbc| zMg#x=010qNS#tmY19kua19ky@)q>0b000?uMObuGZ)S9NVRB^vcXxL#X>MzCV_|S* zE^l&Yo9;Xs000CyNklF*k_h zeSj(z(62g|sNwaiB$%6W2R@iz2N=1}PWyp2O#z0un;uHxOh{=^S_R4Y-U2=QJ zFRTH)*JsmyAZC$Xc5*A5DB^;^NTozh<~hw(x`h#9=%e`+z&j;7=Z9sT2N7?L0=GET3SBm^*l!GXGs~(O^k?pX&1lXE{R; zg}S>r^1IS%Tl7JC!#OQtrTot4Tv!9}Tw`7;&>=mnzira0-xa0%Qx$CuFD_4{?KmK= z@Fic-SOK(kZbQmT9hBvtekN_v4@U)eyRY8__oq(w2eE?i1!Al~lxCEF!JJ`ZOb^wHD->opoOP2${*CLxm+<#;+#Vd6C zsZfNWsmN%m;!%jwa zmU@mQrfJe9QA&j_p5*wNasjkkRU}fIUug8=(?b|JtZ8E)z;`t!aw1;j z91WhLKnp$6Adi%NSErVibz_K!+4AHvnEzIhQ%iT4A*DFaJ8yokO5FgVYnXKV7RVAing15ZqTyv?rJv6DR<^ri*@T? XW>@+Kxuv$c00000NkvXXu0mjfp&%!d literal 0 HcmV?d00001 diff --git a/frontend/qt5/resource/pause.png b/frontend/qt5/resource/pause.png new file mode 100644 index 0000000000000000000000000000000000000000..5c7d128ee8728707a6193eb41af32b4e28890533 GIT binary patch literal 1145 zcmV-<1cv*GP)kdg00002b3#c}2nbc| zMg#x=010qNS#tmY19kua19ky@)q>0b000?uMObuGZ)S9NVRB^vcXxL#X>MzCV_|S* zE^l&Yo9;Xs000B{Nkl^Bw_qq@)F2oWehM>5RxKr%Hm6}!%+eJ5;P+bTi3lS*+iMTL0p`m}$nM`IT^X~QY zaG4NCay|}k4)2`reDBVC=eR(>eVF$r-4qj)1*i$D;c~M0PMbdKBbY0P8D8 zNRZe0(#=lS4j`Bv4AY$T#V)(F>5$@qgbX?I43H&hGVkVQTK`M%-dOl9R8v}PlTfBY zi6Kx_XPp{e(V)oOvODwsMjyfSW8sRgbZCm;7>{s>LF)-A9bt!GSz(YOIg)jEYVNmv z1n0)Wim$Y|!zjl&!d>KX1_-1=S4uXx!c9g*BZ^}mZ1f1;9t>A~RnmD}-Nz`9B_lW? z0AARoLt71gX2l*QS$4-iYC!^!PWx(`EtOXpXIQuMLe5;j%?k_(ck>KSD6@^PrqfZu zTjj9UEjB3e5=Y{bXGp9+X2H-k6+2RKUOr5==^p;LmOwhwEw*SeNk!$kNFa(HGKK-u zRZ!`3w1jRkodNeoK5Xumws?wX8B=*yiC?HsOV_htc8-SPJfF~|t$W)nvB)6?>h9je zpGZp`B~LIKJ2`?1ZgB&mdA7LD28rp&2~X$v@KS(GB*rNxdAP4PAj zxg#Mfs#4O^iJuSw4-k|P?C3C|O?k9NW=@#Snlh&M2Y27mKA zEr?d9!3_>$)>9G;NvH)J4GS>GWR~PWe;o5}V-&T-|4X2-ix-T>&Ur*`wm-HEFKilt zuO(p>asSD#JyU84ISQ2I(R#6!$GlZj6jqaPDd^{#{VCl@kf+FqJX$X{f|z$rRf;Yp zVTpQ%9ad=4p(}V53{#Rv>&23686AG2DFW(Y$pv2iG`yw>$eTRG5QX?gI{s@akKaO# zckR=1pMCXu0%0~Z#l`}6XweZQ5l`4(Y|?dCIiC~I12n@dB=B{O`HcAf;wlaCw*?)k z!5dP8Cbjkdfj`KLfO%Dt1z?WJOtrxGI3}@zeM2oyMbus8-%>#3%c|NNsxNA&2Aj$E z^C}f`g1y`TNQISea-KDt%T7aeVQ;7!g~ocQ=#cPC@ifIam82kQ`jKyNcEl<_wXkoj zo4qiG7K=BbLmcH%?&C0yTk3pGXxP9Lkdg00002b3#c}2nbc| zMg#x=010qNS#tmY19kua19ky@)q>0b000?uMObuGZ)S9NVRB^vcXxL#X>MzCV_|S* zE^l&Yo9;Xs000CSNkl%Nv2I}kqk^*6{QX0LZL=0)zW5F zG%W~zP+W)@OF>cx&K0dd`VED0c(U%)kWw4u_Y$9)GLP|r__>~#*6vz_Yb_d6P zTV?QWUzqXbIty%JAKU3Ahch6MiYCj%fIgz!k-)(o%EjIHUPbxCO52VAb z4Bjq<8_mKJMP6lltMZsjJSzdVg3+`rYAkSCyN68GJ$$kfK|0bb)TuK_S@LYl;06=? zL1e_n2Ft5U(if?VW+5Gc_)gA$x{_Pq85OWu@{9swiGE~>U-*lM$O5hnKpyyAac<|= zry{>UEjIMg&lY0C8Fgw@`GI5n-04TlBP*WLPc>T6?~l?$DGA%`AD6L^2nQs{K>W=q zK4PJ5kr7!6JV-)HdMNV!GT;FM%i1Qk)vz3vjc<92pD+s%lhNK~2EIS?B{c~fQAMN# z3o-+t93-!ac|PD{7BK_MbE{M`C}n)flhR}9cg}}DNvK=%cw%eeYuw}rkK-(I3z8#) zfM_KL9fMok%H~?+Aw}=MU z6dl82xDrhyVKO4eH_XuxRtiA|C6059-c|C{Zsb3FAw@)(oJb-FqoEoDQ!G$NCYIvU zoMaEH#X4Km`GkN9sD@F9;PVROu^ex4l_hEZ!kQZZFC=H4)7-GR>@-xT zIz!bgG}c48X}v$hUWLW2t8Mj*?>L9EOE3Assnug0;N?E_uy{+fn_WE0{oIG+U*@?k zmaHdbInCiSYabS~u%|5)>`&J22q%#ZI^pbTJ5yihaec`i*HG+SE@xZV<9c?T$F*aU r3kMi-{h{@|yGoelg1dY+Va@sv%NF!G!=KTL00000NkvXXu0mjfS=|gw literal 0 HcmV?d00001 diff --git a/frontend/qt5/resource/previous.png b/frontend/qt5/resource/previous.png new file mode 100644 index 0000000000000000000000000000000000000000..bcf2376d6033990d06433a3763e097d2fcbf5e90 GIT binary patch literal 1211 zcmV;s1VsCZP)kdg00002b3#c}2nbc| zMg#x=010qNS#tmY19kua19ky@)q>0b000?uMObuGZ)S9NVRB^vcXxL#X>MzCV_|S* zE^l&Yo9;Xs000C!Nkl1v7XOj6zCec48xRV% z6+x@wzAgky7ox;eLr_FZR^m^(60B_%v0Y@LwzNfr7&oPsAf&ozWhz12WRl7J+ z50?-f>GyCZ=gfTX``vSL`HnL>VEQa2`x^D1l-p=@8JvtAn`Tzy8WP}q2 zc+sXsQw4rtE^H;5b=%%;Km?GC`Qau@k`Hq`z2wP>?l3eDqdc#T80C$Ch;G86un@gau-*@g+~O%(O-?*}A*uP&I;NJT5k9FiJ)8?$kgQ>tq5K zc%PpsNcuDl5f_tja4+XFZ_Omf+e5&1hS%1J9tDNQ({dCpbty#a_ zms+Yi$fi)IspJ*?-55V{m_^!nx#WGY;e$b>af?(5t}FwuNiTYQ;rNALS>j%u_l#t1QP=uU zmPyEm3>PvtY3=dltRVm=&@ftDmSK}WRDDV4GoacMk>)8g5Lwl5tfV#;(n=Z+@g_s- z$q_pLP@gpKI>DkE`F{+wjPb(48TXL`+3e#0o5MGGdPQC)CcIb-2EH2EJY@KdcGy#* zl!By12rN22ar=9Wqhqg~0kXN`{35E2Hbo@8I z=dcP*YgBoIF6Ft8j=U6sO;}xS9A%jXEfI%(+6J-V8Q~C*(o)VR7#XZ?6A;1YRi-lX zyUdyJ&NqdQG~f~GysfHx_4{{RA}0f;q$CT#B%_((0^i~oO$8g8+Im-1b;-Y?faJ4G zZfL4|)l@C?)qMDTc`j4002ovPDHLkV1mmrHkdg00002b3#c}2nbc| zMg#x=010qNS#tmY19kua19ky@)q>0b000?uMObuGZ)S9NVRB^vcXxL#X>MzCV_|S* zE^l&Yo9;Xs000CGNklrkmC6Jy9WesR^^K8P&yuG_6BOq91w;O z0FIbYMal#}U|XkRx6FwT6Tku#*>#;1HyPf+y_n^2g($1BtyaSfaR;8k6ZA~sI(gSC zc%vyhSu{W$FX5Pt^Vi%%4psu)qlr?gpwL-XKAv1R_HEJ~1I0WF4?Jpm~W5GYEj ztH#jhkPumvF9GJYitJ75H}MpnMT6lX9Y15XR54~N2ULy4d;0$f``E+=_o>u3cZcqz ztB)Df#=OsD0F^qVjg&%)$pL|--KDEznj~;+%&Pn%Kky?%Ctk&nW;j|!4e%6}nAfZ3nxTO315g7c|Bk-L zfaG<;pbU5l8X*cRv9L2JXw=|y1iD;xnjS1dKkxTfK@nT`F9Ew5D9F>4bh~(rnSG8{ z9IwEKi+b)Ufs3v5z@c9-(K+Snz+2!?KjqVA*sc&7?)`d7C_O6|h0Pt^l*|)UbjczO zpHqs6{c1tr{jM{Ly^B^h(GM`fHiqD-BOIzeI|&rCUIitq#~4@XO$B|~3_nj_UbE*a z_zsUE(wSO(Of&e;r{TG7?{D~uDlKGfEwz9w%aCkX=Os-r_7})Orh+paP4EeXiDW3t zT2Q`-u^N!^S6smWBcueERN)jwd!i#FYW(a`Q(s>b2 zBl1;=3cRN8@g?$5rZwm&>RT`nX;=;&DABsmLiTnzs1o-Ne$v fC9`oMo%a0)Lk#jiOZi|800000NkvXXu0mjfdhQk9 literal 0 HcmV?d00001