From 4e86256f1b96f11fca63be1e5ca53df555c16cee Mon Sep 17 00:00:00 2001 From: pszsh Date: Sun, 1 Mar 2026 02:07:35 -0800 Subject: [PATCH] okay, release will happen now. i cant find any more bugs, so it must be time. --- .gitignore | 4 +- .sdkmanrc | 2 +- CMakeLists.txt | 9 +- Makefile | 25 ++- src/CommonWidgets.cpp | 467 ++++++++++++++++++++++++++++++++++------- src/CommonWidgets.h | 93 +++++++- src/MainWindow.cpp | 19 +- src/PlayerControls.cpp | 283 +++++++++++++++++-------- src/PlayerControls.h | 50 +++-- src/Theme.h | 155 ++++++++++++++ 10 files changed, 904 insertions(+), 203 deletions(-) create mode 100644 src/Theme.h diff --git a/.gitignore b/.gitignore index dab6bcf..a0332e8 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,6 @@ ADC-2024/ *.json LICENCE readme.md -vamp-plugin-sdk.cmake \ No newline at end of file +vamp-plugin-sdk.cmake +*.keystore +*.jks \ No newline at end of file diff --git a/.sdkmanrc b/.sdkmanrc index ff24131..1517f48 100644 --- a/.sdkmanrc +++ b/.sdkmanrc @@ -1,4 +1,4 @@ # Enable auto-env through the sdkman_auto_env config # Add key=value pairs of SDKs to use below -java=17-homebrew +java=17.0.13-tem gradle=9.2.1 \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index cb1011f..564582b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -173,6 +173,7 @@ if(EXISTS "${ICON_SOURCE}") endif() set(PROJECT_HEADERS + src/Theme.h src/Utils.h src/Processor.h src/AudioEngine.h @@ -213,10 +214,16 @@ if(EXISTS "${ICON_SOURCE}" AND MAGICK_EXECUTABLE) add_dependencies(YrCrystals GenerateIcons) endif() -# --- Mobile Definitions --- +# --- Platform Definitions --- if(BUILD_ANDROID OR BUILD_IOS) target_compile_definitions(YrCrystals PRIVATE IS_MOBILE) endif() +if(BUILD_ANDROID) + target_compile_definitions(YrCrystals PRIVATE PLATFORM_ANDROID) +endif() +if(BUILD_IOS) + target_compile_definitions(YrCrystals PRIVATE PLATFORM_IOS) +endif() # --- Linking --- diff --git a/Makefile b/Makefile index 242b143..65d0cb3 100644 --- a/Makefile +++ b/Makefile @@ -13,8 +13,11 @@ TARGET = YrCrystals # Android Specifics PKG_NAME = org.qtproject.example.YrCrystals -# CRITICA: Qt6 generates 'android-build-debug.apk' by default APK_PATH = $(BUILD_DIR_ANDROID)/android-build/build/outputs/apk/debug/android-build-debug.apk +AAB_PATH = $(BUILD_DIR_ANDROID)/android-build/build/outputs/bundle/release/android-build-release.aab +KEYSTORE_PATH = $(CURDIR)/android/release.keystore +KEYSTORE_ALIAS = yrcrystals +KEYSTORE_PASS = yrcrystals2026 all: macos @@ -51,6 +54,24 @@ android: @cmake --build $(BUILD_DIR_ANDROID) --target apk @echo "APK generated at $(APK_PATH)" +android-release: + @if [ ! -d "$(QT_ANDROID_KIT)" ]; then echo "Error: QT_ANDROID_KIT not found at $(QT_ANDROID_KIT)"; exit 1; fi + @if [ ! -f "$(KEYSTORE_PATH)" ]; then echo "Error: Release keystore not found at $(KEYSTORE_PATH)"; exit 1; fi + @mkdir -p $(BUILD_DIR_ANDROID) + @cd $(BUILD_DIR_ANDROID) && cmake .. \ + -DCMAKE_TOOLCHAIN_FILE=$(QT_ANDROID_KIT)/lib/cmake/Qt6/qt.toolchain.cmake \ + -DQT_ANDROID_ABIS="arm64-v8a" \ + -DANDROID_PLATFORM=android-24 \ + -DQT_ANDROID_BUILD_ALL_ABIS=OFF \ + -DBUILD_ANDROID=ON \ + -DCMAKE_BUILD_TYPE=Release + @QT_ANDROID_KEYSTORE_PATH=$(KEYSTORE_PATH) \ + QT_ANDROID_KEYSTORE_ALIAS=$(KEYSTORE_ALIAS) \ + QT_ANDROID_KEYSTORE_STORE_PASS=$(KEYSTORE_PASS) \ + QT_ANDROID_KEYSTORE_KEY_PASS=$(KEYSTORE_PASS) \ + cmake --build $(BUILD_DIR_ANDROID) --target aab + @echo "Signed AAB at $(AAB_PATH)" + ios: macos @if [ ! -d "$(QT_IOS_KIT)" ]; then echo "Error: QT_IOS_KIT not found at $(QT_IOS_KIT)"; exit 1; fi @mkdir -p $(BUILD_DIR_IOS) @@ -93,4 +114,4 @@ distclean: @rm -rf $(BUILD_DIR_MACOS) $(BUILD_DIR_WIN) $(BUILD_DIR_ANDROID) $(BUILD_DIR_IOS) @echo "Removed all build directories." -.PHONY: all desktop macos windows android ios run install_android run_android debug_android clean distclean \ No newline at end of file +.PHONY: all desktop macos windows android android-release ios run install_android run_android debug_android clean distclean \ No newline at end of file diff --git a/src/CommonWidgets.cpp b/src/CommonWidgets.cpp index d35c965..6affa07 100644 --- a/src/CommonWidgets.cpp +++ b/src/CommonWidgets.cpp @@ -1,19 +1,187 @@ // src/CommonWidgets.cpp #include "CommonWidgets.h" +#include "Theme.h" #include #include #include #include #include #include +#include #include #include +#include +#include #include #include #include -// --- PlaylistDelegate Implementation --- +// --- TouchSlider --- + +TouchSlider::TouchSlider(QWidget *parent) : QWidget(parent) { + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + setFixedHeight(Theme::Dims::SliderHeight); +} + +void TouchSlider::setRange(int min, int max) { + m_min = min; + m_max = max; + m_value = std::clamp(m_value, m_min, m_max); + update(); +} + +void TouchSlider::setValue(int val) { + val = std::clamp(val, m_min, m_max); + if (val == m_value) return; + m_value = val; + update(); + emit valueChanged(m_value); +} + +QSize TouchSlider::sizeHint() const { + return QSize(200, Theme::Dims::SliderHeight); +} + +int TouchSlider::trackLeft() const { return Theme::Dims::TrackPad; } +int TouchSlider::trackRight() const { return width() - Theme::Dims::TrackPad; } + +int TouchSlider::handleX() const { + if (m_max == m_min) return trackLeft(); + float frac = float(m_value - m_min) / float(m_max - m_min); + return trackLeft() + frac * (trackRight() - trackLeft()); +} + +void TouchSlider::paintEvent(QPaintEvent *) { + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + + int cy = height() / 2; + int tl = trackLeft(); + int tr = trackRight(); + int hx = handleX(); + int hh = Theme::Dims::TrackH / 2; + + p.setPen(Qt::NoPen); + p.setBrush(Theme::Colors::TrackBg); + p.drawRoundedRect(QRectF(tl, cy - hh, tr - tl, Theme::Dims::TrackH), hh, hh); + + p.setBrush(Theme::Colors::Accent); + if (hx > tl) + p.drawRoundedRect(QRectF(tl, cy - hh, hx - tl, Theme::Dims::TrackH), hh, hh); + + p.setBrush(Qt::white); + p.drawEllipse(QPointF(hx, cy), Theme::Dims::HandleRadius, + Theme::Dims::HandleRadius); +} + +void TouchSlider::mousePressEvent(QMouseEvent *event) { + int hx = handleX(); + int dx = event->pos().x() - hx; + int dy = event->pos().y() - height() / 2; + int hr = Theme::Dims::HitRadius; + if (dx * dx + dy * dy <= hr * hr) { + m_dragging = true; + emit sliderPressed(); + } +} + +void TouchSlider::mouseMoveEvent(QMouseEvent *event) { + if (!m_dragging) return; + int tl = trackLeft(); + int tr = trackRight(); + float frac = float(event->pos().x() - tl) / float(tr - tl); + frac = std::clamp(frac, 0.0f, 1.0f); + int newVal = m_min + frac * (m_max - m_min); + if (newVal != m_value) { + m_value = newVal; + update(); + emit valueChanged(m_value); + } +} + +void TouchSlider::mouseReleaseEvent(QMouseEvent *) { + if (m_dragging) { + m_dragging = false; + emit sliderReleased(); + } +} + +// --- ToggleSwitch --- + +ToggleSwitch::ToggleSwitch(const QString &label, QWidget *parent) + : QWidget(parent), m_label(label) { + setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); + setFixedHeight(Theme::Dims::ToggleRow); + setCursor(Qt::PointingHandCursor); +} + +void ToggleSwitch::setChecked(bool checked) { + if (m_checked == checked) return; + m_checked = checked; + + if (m_anim) m_anim->stop(); + m_anim = new QPropertyAnimation(this, "knobX", this); + connect(m_anim, &QObject::destroyed, this, [this]() { m_anim = nullptr; }); + m_anim->setDuration(150); + m_anim->setStartValue(m_knobX); + m_anim->setEndValue(checked ? 1.0f : 0.0f); + m_anim->start(QAbstractAnimation::DeleteWhenStopped); + + emit toggled(checked); +} + +void ToggleSwitch::setKnobX(float x) { + m_knobX = x; + update(); +} + +QSize ToggleSwitch::sizeHint() const { + QFontMetrics fm(font()); + int textW = m_label.isEmpty() ? 0 : fm.horizontalAdvance(m_label) + 8; + return QSize(Theme::Dims::ToggleW + textW, Theme::Dims::ToggleRow); +} + +void ToggleSwitch::paintEvent(QPaintEvent *) { + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + + constexpr int tw = Theme::Dims::ToggleW; + constexpr int th = Theme::Dims::ToggleH; + constexpr int kd = Theme::Dims::ToggleKnob; + constexpr int pad = 2; + + int ty = (height() - th) / 2; + + auto off = Theme::Colors::TrackOff; + auto on = Theme::Colors::Accent; + QColor trackColor = QColor::fromRgbF( + off.redF() + (on.redF() - off.redF()) * m_knobX, + off.greenF() + (on.greenF() - off.greenF()) * m_knobX, + off.blueF() + (on.blueF() - off.blueF()) * m_knobX); + p.setPen(Qt::NoPen); + p.setBrush(trackColor); + p.drawRoundedRect(QRectF(0, ty, tw, th), th / 2.0, th / 2.0); + + float knobLeft = pad + m_knobX * (tw - kd - 2 * pad); + float knobCX = knobLeft + kd / 2.0; + float knobCY = ty + th / 2.0; + p.setBrush(Qt::white); + p.drawEllipse(QPointF(knobCX, knobCY), kd / 2.0, kd / 2.0); + + if (!m_label.isEmpty()) { + p.setPen(Qt::white); + QFont f = font(); + f.setPointSize(13); + p.setFont(f); + p.drawText(QRectF(tw + 8, 0, width() - tw - 8, height()), + Qt::AlignLeft | Qt::AlignVCenter, m_label); + } +} + +void ToggleSwitch::mousePressEvent(QMouseEvent *) { setChecked(!m_checked); } + +// --- PlaylistDelegate --- void PlaylistDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, @@ -21,57 +189,47 @@ void PlaylistDelegate::paint(QPainter *painter, painter->save(); painter->setRenderHint(QPainter::Antialiasing); - // Background - if (option.state & QStyle::State_Selected) { - painter->fillRect(option.rect, QColor(50, 50, 50)); - } else { - painter->fillRect(option.rect, QColor(17, 17, 17)); // Match list background - } + if (option.state & QStyle::State_Selected) + painter->fillRect(option.rect, Theme::Colors::ListSelected); + else + painter->fillRect(option.rect, Theme::Colors::SurfaceDark); - QRect r = option.rect.adjusted(5, 5, -5, -5); + QRect r = option.rect.adjusted(Theme::Dims::ListPad, Theme::Dims::ListPad, + -Theme::Dims::ListPad, -Theme::Dims::ListPad); - // Icon / Art - // CRITICAL OPTIMIZATION: Use pre-scaled thumbnail from DecorationRole + constexpr int iconSize = Theme::Dims::ListIcon; QPixmap art = index.data(Qt::DecorationRole).value(); - QRect iconRect(r.left(), r.top(), 50, 50); + QRect iconRect(r.left(), r.top(), iconSize, iconSize); if (!art.isNull()) { - // Draw pre-scaled art directly. No scaling in paint loop. - // Center it if aspect ratio differs slightly int x = iconRect.x() + (iconRect.width() - art.width()) / 2; int y = iconRect.y() + (iconRect.height() - art.height()) / 2; painter->drawPixmap(x, y, art); } else { - // Placeholder - painter->fillRect(iconRect, QColor(40, 40, 40)); + painter->fillRect(iconRect, Theme::Colors::AlbumArtPlaceholder); } - // Text - QRect textRect = r.adjusted(60, 0, 0, 0); + QRect textRect = r.adjusted(iconSize + 10, 0, 0, 0); QString title = index.data(Qt::DisplayRole).toString(); QString artist = index.data(Qt::UserRole + 1).toString(); - // Title - painter->setPen(Qt::white); + painter->setPen(Theme::Colors::TextPrimary); QFont f = option.font; f.setBold(true); - f.setPointSize(14); + f.setPointSize(Theme::Dims::ListTitlePt); painter->setFont(f); - // Calculate height for title QFontMetrics fmTitle(f); - int titleHeight = fmTitle.height(); QRect titleRect = textRect; - titleRect.setHeight(titleHeight); + titleRect.setHeight(fmTitle.height()); QString elidedTitle = fmTitle.elidedText(title, Qt::ElideRight, titleRect.width()); painter->drawText(titleRect, Qt::AlignLeft | Qt::AlignTop, elidedTitle); - // Artist - painter->setPen(QColor(170, 170, 170)); + painter->setPen(Theme::Colors::TextSecondary); f.setBold(false); - f.setPointSize(12); + f.setPointSize(Theme::Dims::ListArtistPt); painter->setFont(f); QFontMetrics fmArtist(f); @@ -83,8 +241,7 @@ void PlaylistDelegate::paint(QPainter *painter, fmArtist.elidedText(artist, Qt::ElideRight, artistRect.width()); painter->drawText(artistRect, Qt::AlignLeft | Qt::AlignTop, elidedArtist); - // Separator - painter->setPen(QColor(34, 34, 34)); + painter->setPen(Theme::Colors::ListSeparator); painter->drawLine(option.rect.bottomLeft(), option.rect.bottomRight()); painter->restore(); @@ -92,7 +249,7 @@ void PlaylistDelegate::paint(QPainter *painter, QSize PlaylistDelegate::sizeHint(const QStyleOptionViewItem &, const QModelIndex &) const { - return QSize(0, 60); + return QSize(0, Theme::Dims::ListRow); } // --- XYPad --- @@ -100,7 +257,7 @@ QSize PlaylistDelegate::sizeHint(const QStyleOptionViewItem &, XYPad::XYPad(const QString &title, QWidget *parent) : QWidget(parent), m_title(title) { setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - setMinimumHeight(150); + setMinimumHeight(Theme::Dims::XYMinH); setCursor(Qt::CrossCursor); } @@ -118,11 +275,11 @@ void XYPad::paintEvent(QPaintEvent *) { QPainter p(this); p.setRenderHint(QPainter::Antialiasing); - p.fillRect(rect(), QColor(40, 40, 40, 200)); - p.setPen(QPen(QColor(80, 80, 80), 1)); + p.fillRect(rect(), Theme::Colors::XYBg); + p.setPen(QPen(Theme::Colors::XYBorder, 1)); p.drawRect(rect().adjusted(0, 0, -1, -1)); - p.setPen(QPen(QColor(60, 60, 60), 1, Qt::DotLine)); + p.setPen(QPen(Theme::Colors::XYGrid, 1, Qt::DotLine)); p.drawLine(width() / 2, 0, width() / 2, height()); p.drawLine(0, height() / 2, width(), height() / 2); @@ -130,12 +287,13 @@ void XYPad::paintEvent(QPaintEvent *) { int py = (1.0f - m_y) * height(); p.setPen(Qt::NoPen); - p.setBrush(QColor(0, 212, 255, 180)); - p.drawEllipse(QPoint(px, py), 12, 12); + p.setBrush(Theme::Colors::XYAccentDim); + p.drawEllipse(QPoint(px, py), Theme::Dims::XYIndicator, + Theme::Dims::XYIndicator); p.setBrush(Qt::white); - p.drawEllipse(QPoint(px, py), 4, 4); + p.drawEllipse(QPoint(px, py), Theme::Dims::XYInner, Theme::Dims::XYInner); - p.setPen(QPen(QColor(0, 212, 255, 100), 1)); + p.setPen(QPen(Theme::Colors::XYAccentFaint, 1)); p.drawLine(px, 0, px, height()); p.drawLine(0, py, width(), py); @@ -146,53 +304,196 @@ void XYPad::paintEvent(QPaintEvent *) { p.setFont(f); QString text = m_title; - if (m_formatter) { + if (m_formatter) text += "\n" + m_formatter(m_x, m_y); - } p.drawText(rect().adjusted(10, 10, -10, -10), Qt::AlignLeft | Qt::AlignTop, text); } -void XYPad::mousePressEvent(QMouseEvent *event) { updateFromPos(event->pos()); } -void XYPad::mouseMoveEvent(QMouseEvent *event) { updateFromPos(event->pos()); } +void XYPad::mousePressEvent(QMouseEvent *event) { + int px = m_x * width(); + int py = (1.0f - m_y) * height(); + QPoint diff = event->pos() - QPoint(px, py); + int hr = Theme::Dims::HitRadius; + if (diff.x() * diff.x() + diff.y() * diff.y() <= hr * hr) { + m_dragging = true; + m_lastPos = event->pos(); + } +} -void XYPad::updateFromPos(const QPoint &pos) { - m_x = std::clamp((float)pos.x() / width(), 0.0f, 1.0f); - m_y = std::clamp(1.0f - (float)pos.y() / height(), 0.0f, 1.0f); +void XYPad::mouseMoveEvent(QMouseEvent *event) { + if (!m_dragging) return; + QPoint delta = event->pos() - m_lastPos; + m_lastPos = event->pos(); + m_x = std::clamp(m_x + delta.x() * 0.7f / width(), 0.0f, 1.0f); + m_y = std::clamp(m_y - delta.y() * 0.7f / height(), 0.0f, 1.0f); update(); emit valuesChanged(m_x, m_y); } -OverlayWidget::OverlayWidget(QWidget *content, QWidget *parent) - : QWidget(parent), m_content(content) { - QPalette pal = palette(); - pal.setColor(QPalette::Window, QColor(0, 0, 0, 100)); - setAutoFillBackground(true); - setPalette(pal); +void XYPad::mouseReleaseEvent(QMouseEvent *) { m_dragging = false; } +// --- ExpandedXYPad --- + +ExpandedXYPad::ExpandedXYPad(QWidget *parent) : QWidget(parent) { + hide(); +} + +void ExpandedXYPad::open(const QString &title, + std::function formatter, + float x, float y) { + m_title = title; + m_formatter = formatter; + m_x = x; + m_y = y; + m_dragging = false; + raise(); + show(); + update(); +} + +QRect ExpandedXYPad::padRect() const { + int side = std::min(width(), height()) * 85 / 100; + int cx = (width() - side) / 2; + int cy = (height() - side) / 2; + return QRect(cx, cy, side, side); +} + +void ExpandedXYPad::paintEvent(QPaintEvent *) { + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + + p.fillRect(rect(), m_dragging ? Theme::Colors::ExpandedXYBgDragging + : Theme::Colors::ExpandedXYBg); + + QRect pr = padRect(); + + p.fillRect(pr, Theme::Colors::XYBg); + p.setPen(QPen(Theme::Colors::XYBorder, 1)); + p.drawRect(pr.adjusted(0, 0, -1, -1)); + + p.setPen(QPen(Theme::Colors::XYGrid, 1, Qt::DotLine)); + p.drawLine(pr.center().x(), pr.top(), pr.center().x(), pr.bottom()); + p.drawLine(pr.left(), pr.center().y(), pr.right(), pr.center().y()); + + int px = pr.left() + m_x * pr.width(); + int py = pr.top() + (1.0f - m_y) * pr.height(); + + p.setPen(Qt::NoPen); + p.setBrush(Theme::Colors::XYAccentDim); + p.drawEllipse(QPoint(px, py), Theme::Dims::XYIndicator, + Theme::Dims::XYIndicator); + p.setBrush(Qt::white); + p.drawEllipse(QPoint(px, py), Theme::Dims::XYInner, Theme::Dims::XYInner); + + p.setPen(QPen(Theme::Colors::XYAccentFaint, 1)); + p.drawLine(px, pr.top(), px, pr.bottom()); + p.drawLine(pr.left(), py, pr.right(), py); + + p.setPen(Qt::white); + QFont f = font(); + f.setBold(true); + f.setPointSize(12); + p.setFont(f); + + QString text = m_title; + if (m_formatter) + text += "\n" + m_formatter(m_x, m_y); + p.drawText(pr.adjusted(10, 10, -10, -10), Qt::AlignLeft | Qt::AlignTop, text); +} + +void ExpandedXYPad::mousePressEvent(QMouseEvent *event) { + QRect pr = padRect(); + int hr = Theme::Dims::HitRadius; + + int px = pr.left() + m_x * pr.width(); + int py = pr.top() + (1.0f - m_y) * pr.height(); + QPoint diff = event->pos() - QPoint(px, py); + if (diff.x() * diff.x() + diff.y() * diff.y() <= hr * hr) { + m_dragging = true; + m_lastPos = event->pos(); + update(); + return; + } + + QRect cushion = pr.adjusted(-hr, -hr, hr, hr); + if (!cushion.contains(event->pos())) { + hide(); + emit closed(); + } +} + +void ExpandedXYPad::mouseMoveEvent(QMouseEvent *event) { + if (!m_dragging) return; + QRect pr = padRect(); + QPoint delta = event->pos() - m_lastPos; + m_lastPos = event->pos(); + m_x = std::clamp(m_x + delta.x() * 0.7f / pr.width(), 0.0f, 1.0f); + m_y = std::clamp(m_y - delta.y() * 0.7f / pr.height(), 0.0f, 1.0f); + update(); + emit valuesChanged(m_x, m_y); +} + +void ExpandedXYPad::mouseReleaseEvent(QMouseEvent *) { + m_dragging = false; + update(); +} + +// --- OverlayWidget --- + +OverlayWidget::OverlayWidget(QWidget *content, QWidget *parent) + : QWidget(parent) { QVBoxLayout *layout = new QVBoxLayout(this); layout->setAlignment(Qt::AlignCenter); layout->setContentsMargins(20, 20, 20, 20); - content->setParent(this); - content->setMaximumWidth(500); - content->setMaximumHeight(600); + QScrollArea *scroll = new QScrollArea(this); + scroll->setWidget(content); + scroll->setWidgetResizable(true); + scroll->setFrameShape(QFrame::NoFrame); + scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + scroll->setMaximumWidth(500); + scroll->setStyleSheet( + "QScrollArea { background: transparent; border: none; }" + "QScrollArea > QWidget > QWidget { background: transparent; }"); + scroll->viewport()->setStyleSheet("background: transparent;"); - layout->addWidget(content); + m_content = scroll; + layout->addWidget(scroll); hide(); } -void OverlayWidget::mousePressEvent(QMouseEvent *event) { - if (!m_content->geometry().contains(event->pos())) { - hide(); +void OverlayWidget::showEvent(QShowEvent *event) { + QWidget::showEvent(event); + if constexpr (Theme::FrostedGlass) { + if (auto *w = window()) { + QPixmap shot = w->grab(); + int sw = shot.width() / 10; + int sh = shot.height() / 10; + m_blurredBg = shot.scaled(sw, sh, Qt::IgnoreAspectRatio, + Qt::SmoothTransformation) + .scaled(shot.size(), Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); + } } } -void OverlayWidget::paintEvent(QPaintEvent *event) { - QPainter p(this); - p.fillRect(rect(), QColor(0, 0, 0, 100)); +void OverlayWidget::mousePressEvent(QMouseEvent *event) { + if (!m_content->geometry().contains(event->pos())) + hide(); } +void OverlayWidget::paintEvent(QPaintEvent *) { + QPainter p(this); + if constexpr (Theme::FrostedGlass) { + if (!m_blurredBg.isNull()) + p.drawPixmap(0, 0, m_blurredBg); + } + p.fillRect(rect(), Theme::Colors::OverlayDim); +} + +// --- WelcomeWidget --- + WelcomeWidget::WelcomeWidget(QWidget *parent) : QWidget(parent) { QVBoxLayout *layout = new QVBoxLayout(this); layout->setAlignment(Qt::AlignCenter); @@ -204,10 +505,31 @@ WelcomeWidget::WelcomeWidget(QWidget *parent) : QWidget(parent) { title->setAlignment(Qt::AlignCenter); layout->addWidget(title); - QString btnStyle = - "QPushButton { background-color: #333; color: white; border: 1px solid " - "#555; border-radius: 8px; padding: 15px; font-size: 18px; } " - "QPushButton:pressed { background-color: #555; }"; + QString minSizeRule = Theme::Dims::BtnMinSize > 0 + ? QString(" min-height: %1px;").arg(Theme::Dims::BtnMinSize) + : QString(); + + QString btnStyle = QString( + "QPushButton { background-color: %1; color: %2; border: 1px solid " + "%3; border-radius: 8px; padding: 15px; font-size: 18px;%4 } " + "QPushButton:pressed { background-color: %5; }") + .arg(Theme::hex(Theme::Colors::SurfaceLight), + Theme::hex(Theme::Colors::TextPrimary), + Theme::hex(Theme::Colors::BorderMid), + minSizeRule, + Theme::hex(Theme::Colors::BorderMid)); + + QString listStyle = QString( + "QListWidget { background: transparent; border: none; color: %1; " + "font-size: 16px; }" + "QListWidget::item { padding: %2px; border-bottom: 1px solid %3; }" + "QListWidget::item:hover { background: %4; }" + "QListWidget::item:selected { background: %5; }") + .arg(Theme::hex(Theme::Colors::TextMuted)) + .arg(Theme::Dims::ListItemPad) + .arg(Theme::hex(Theme::Colors::SurfaceLight), + Theme::hex(Theme::Colors::SurfaceMid), + Theme::hex(Theme::Colors::SurfaceLight)); QHBoxLayout *btnLayout = new QHBoxLayout(); btnLayout->setSpacing(20); @@ -228,18 +550,11 @@ WelcomeWidget::WelcomeWidget(QWidget *parent) : QWidget(parent) { layout->addLayout(btnLayout); - // --- Lists Container (Recent + Frequents) --- - QString listStyle = - "QListWidget { background: transparent; border: none; color: #ddd; " - "font-size: 16px; }" - "QListWidget::item { padding: 10px; border-bottom: 1px solid #333; }" - "QListWidget::item:hover { background: #222; }" - "QListWidget::item:selected { background: #333; }"; - QString labelStyle = "color: #aaa; font-size: 16px; margin-top: 20px;"; + QString labelStyle = QString("color: %1; font-size: 16px; margin-top: 20px;") + .arg(Theme::hex(Theme::Colors::TextSecondary)); m_listsContainer = new QWidget(this); - // Recent column QWidget *recentCol = new QWidget(m_listsContainer); QVBoxLayout *recentLayout = new QVBoxLayout(recentCol); recentLayout->setContentsMargins(0, 0, 0, 0); @@ -255,7 +570,6 @@ WelcomeWidget::WelcomeWidget(QWidget *parent) : QWidget(parent) { &WelcomeWidget::onRecentClicked); recentLayout->addWidget(m_recentList); - // Frequents column QWidget *freqCol = new QWidget(m_listsContainer); QVBoxLayout *freqLayout = new QVBoxLayout(freqCol); freqLayout->setContentsMargins(0, 0, 0, 0); @@ -271,7 +585,6 @@ WelcomeWidget::WelcomeWidget(QWidget *parent) : QWidget(parent) { &WelcomeWidget::onRecentClicked); freqLayout->addWidget(m_frequentList); - // Default to vertical layout m_listsLayout = new QVBoxLayout(m_listsContainer); m_listsLayout->setContentsMargins(0, 0, 0, 0); m_listsLayout->addWidget(recentCol); @@ -303,7 +616,6 @@ void WelcomeWidget::refreshRecents() { m_recentList->addItem(item); } - // Refresh frequents m_frequentList->clear(); auto freqs = Utils::getFrequentPaths(10); for (const auto &pair : freqs) { @@ -327,7 +639,6 @@ void WelcomeWidget::updateListsLayout() { return; m_isHorizontal = wantHorizontal; - // Reparent children out of old layout QList children; while (m_listsLayout->count() > 0) { QLayoutItem *item = m_listsLayout->takeAt(0); @@ -350,4 +661,4 @@ void WelcomeWidget::onRecentClicked(QListWidgetItem *item) { if (item) { emit pathSelected(item->data(Qt::UserRole).toString()); } -} \ No newline at end of file +} diff --git a/src/CommonWidgets.h b/src/CommonWidgets.h index 570df66..09aef76 100644 --- a/src/CommonWidgets.h +++ b/src/CommonWidgets.h @@ -3,12 +3,67 @@ #pragma once #include "Utils.h" #include -#include // Include directly or fwd declare properly +#include #include #include #include -// Replaces PlaylistItemWidget for better performance +class QPropertyAnimation; + +class TouchSlider : public QWidget { + Q_OBJECT +public: + explicit TouchSlider(QWidget *parent = nullptr); + void setRange(int min, int max); + void setValue(int val); + int value() const { return m_value; } + QSize sizeHint() const override; +signals: + void valueChanged(int value); + void sliderPressed(); + void sliderReleased(); + +protected: + void paintEvent(QPaintEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + +private: + int handleX() const; + int trackLeft() const; + int trackRight() const; + int m_min = 0; + int m_max = 100; + int m_value = 0; + bool m_dragging = false; +}; + +class ToggleSwitch : public QWidget { + Q_OBJECT + Q_PROPERTY(float knobX READ knobX WRITE setKnobX) +public: + explicit ToggleSwitch(const QString &label = QString(), + QWidget *parent = nullptr); + bool isChecked() const { return m_checked; } + void setChecked(bool checked); + float knobX() const { return m_knobX; } + void setKnobX(float x); + QSize sizeHint() const override; +signals: + void toggled(bool checked); + +protected: + void paintEvent(QPaintEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + +private: + bool m_checked = false; + float m_knobX = 0.0f; + QString m_label; + QPropertyAnimation *m_anim = nullptr; +}; + class PlaylistDelegate : public QStyledItemDelegate { Q_OBJECT public: @@ -25,6 +80,8 @@ public: XYPad(const QString &title, QWidget *parent = nullptr); void setFormatter(std::function formatter); void setValues(float x, float y); + float x() const { return m_x; } + float y() const { return m_y; } signals: void valuesChanged(float x, float y); @@ -32,13 +89,39 @@ protected: void paintEvent(QPaintEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; private: - void updateFromPos(const QPoint &pos); QString m_title; float m_x = 0.5f; float m_y = 0.5f; std::function m_formatter; + bool m_dragging = false; + QPoint m_lastPos; +}; + +class ExpandedXYPad : public QWidget { + Q_OBJECT +public: + ExpandedXYPad(QWidget *parent = nullptr); + void open(const QString &title, + std::function formatter, + float x, float y); +signals: + void valuesChanged(float x, float y); + void closed(); +protected: + void paintEvent(QPaintEvent *) override; + void mousePressEvent(QMouseEvent *) override; + void mouseMoveEvent(QMouseEvent *) override; + void mouseReleaseEvent(QMouseEvent *) override; +private: + QRect padRect() const; + QString m_title; + std::function m_formatter; + float m_x = 0.5f, m_y = 0.5f; + bool m_dragging = false; + QPoint m_lastPos; }; class OverlayWidget : public QWidget { @@ -47,11 +130,13 @@ public: OverlayWidget(QWidget *content, QWidget *parent = nullptr); protected: + void showEvent(QShowEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void paintEvent(QPaintEvent *event) override; private: QWidget *m_content; + QPixmap m_blurredBg; }; class QBoxLayout; @@ -79,4 +164,4 @@ private: QWidget *m_listsContainer; QBoxLayout *m_listsLayout = nullptr; bool m_isHorizontal = false; -}; \ No newline at end of file +}; diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 679d9a2..a385dea 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -1,5 +1,6 @@ // src/MainWindow.cpp #include "MainWindow.h" +#include "Theme.h" #include #include #include @@ -166,9 +167,12 @@ void MainWindow::initUi() { m_playlist = new QListWidget(); m_playlist->setStyleSheet( - "QListWidget { background-color: #111; border: none; } QListWidget::item " - "{ border-bottom: 1px solid #222; padding: 0px; } " - "QListWidget::item:selected { background-color: #333; }"); + QString("QListWidget { background-color: %1; border: none; } " + "QListWidget::item { border-bottom: 1px solid %2; padding: 0px; } " + "QListWidget::item:selected { background-color: %3; }") + .arg(Theme::hex(Theme::Colors::SurfaceDark), + Theme::hex(Theme::Colors::SurfaceMid), + Theme::hex(Theme::Colors::SurfaceLight))); m_playlist->setSelectionMode(QAbstractItemView::SingleSelection); m_playlist->setItemDelegate(new PlaylistDelegate(m_playlist)); m_playlist->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); @@ -209,9 +213,12 @@ void MainWindow::initUi() { #ifdef IS_MOBILE m_mobileTabs = new QTabWidget(); m_mobileTabs->setStyleSheet( - "QTabWidget::pane { border: 0; } QTabBar::tab { background: #222; color: " - "white; padding: 15px; min-width: 100px; } QTabBar::tab:selected { " - "background: #444; }"); + QString("QTabWidget::pane { border: 0; } QTabBar::tab { background: %1; " + "color: %2; padding: 15px; min-width: 100px; } " + "QTabBar::tab:selected { background: %3; }") + .arg(Theme::hex(Theme::Colors::SurfaceMid), + Theme::hex(Theme::Colors::TextPrimary), + Theme::hex(Theme::Colors::SurfaceElevated))); m_mobileTabs->addTab(m_playerPage, "Visualizer"); m_mobileTabs->addTab(m_playlist, "Playlist"); m_stack->addWidget(m_mobileTabs); diff --git a/src/PlayerControls.cpp b/src/PlayerControls.cpp index 44c0d0b..fc78159 100644 --- a/src/PlayerControls.cpp +++ b/src/PlayerControls.cpp @@ -1,6 +1,7 @@ // src/PlayerControls.cpp #include "PlayerControls.h" +#include "Theme.h" #include #include #include @@ -8,29 +9,45 @@ PlaybackWidget::PlaybackWidget(QWidget *parent) : QWidget(parent) { setStyleSheet( - "background-color: rgba(0, 0, 0, 150); border-top: 1px solid #444;"); + QString("background-color: %1; border-top: 1px solid %2;") + .arg(Theme::rgba(Theme::Colors::PlaybackSurface), + Theme::hex(Theme::Colors::BorderSubtle))); QVBoxLayout *mainLayout = new QVBoxLayout(this); mainLayout->setContentsMargins(10, 5, 10, 10); - m_seekSlider = new QSlider(Qt::Horizontal, this); + m_seekSlider = new TouchSlider(this); m_seekSlider->setRange(0, 1000); - m_seekSlider->setFixedHeight(30); - m_seekSlider->setStyleSheet( - "QSlider::handle:horizontal { background: white; width: 20px; margin: " - "-8px 0; border-radius: 10px; }" - "QSlider::groove:horizontal { background: #444; height: 4px; }" - "QSlider::sub-page:horizontal { background: #00d4ff; }"); - connect(m_seekSlider, &QSlider::sliderPressed, this, + connect(m_seekSlider, &TouchSlider::sliderPressed, this, &PlaybackWidget::onSeekPressed); - connect(m_seekSlider, &QSlider::sliderReleased, this, + connect(m_seekSlider, &TouchSlider::sliderReleased, this, &PlaybackWidget::onSeekReleased); mainLayout->addWidget(m_seekSlider); QHBoxLayout *rowLayout = new QHBoxLayout(); - QString btnStyle = - "QPushButton { background: transparent; color: white; font-size: 24px; " - "border: 1px solid #444; border-radius: 8px; padding: 10px 20px; } " - "QPushButton:pressed { background: #333; }"; + + QString minSizeRule = Theme::Dims::BtnMinSize > 0 + ? QString(" min-width: %1px; min-height: %1px;").arg(Theme::Dims::BtnMinSize) + : QString(); + + QString btnPad = (Theme::Dims::BtnMinSize > 0) ? "6px 10px" : "10px 20px"; + QString btnStyle = QString( + "QPushButton { background: transparent; color: %1; font-size: %2px; " + "border: 1px solid %3; border-radius: 8px; padding: %4;%5 } " + "QPushButton:pressed { background: %6; }") + .arg(Theme::hex(Theme::Colors::TextPrimary)) + .arg(Theme::Dims::BtnFontSize) + .arg(Theme::hex(Theme::Colors::BorderSubtle), + btnPad, minSizeRule, + Theme::hex(Theme::Colors::SurfaceLight)); + + QString auxStyle = QString( + "QPushButton { background: transparent; color: %1; font-size: %2px; " + "border: none; padding: 10px;%3 } " + "QPushButton:pressed { color: %4; }") + .arg(Theme::hex(Theme::Colors::TextSecondary)) + .arg(Theme::Dims::BtnFontSize) + .arg(minSizeRule, + Theme::hex(Theme::Colors::TextPrimary)); QPushButton *btnPrev = new QPushButton("<<", this); btnPrev->setStyleSheet(btnStyle); @@ -45,22 +62,25 @@ PlaybackWidget::PlaybackWidget(QWidget *parent) : QWidget(parent) { btnNext->setStyleSheet(btnStyle); connect(btnNext, &QPushButton::clicked, this, &PlaybackWidget::nextClicked); +#if defined(PLATFORM_ANDROID) + QPushButton *btnSettings = new QPushButton("...", this); +#else QPushButton *btnSettings = new QPushButton("⚙", this); - btnSettings->setStyleSheet( - "QPushButton { background: transparent; color: #aaa; font-size: 24px; " - "border: none; padding: 10px; } QPushButton:pressed { color: white; }"); +#endif + btnSettings->setStyleSheet(auxStyle); connect(btnSettings, &QPushButton::clicked, this, &PlaybackWidget::settingsClicked); - QPushButton *btnHome = new QPushButton("⌂", this); // House icon or similar - btnHome->setStyleSheet( - "QPushButton { background: transparent; color: #aaa; font-size: 24px; " - "border: none; padding: 10px; } QPushButton:pressed { color: white; }"); +#if defined(PLATFORM_ANDROID) + QPushButton *btnHome = new QPushButton("<", this); +#else + QPushButton *btnHome = new QPushButton("⌂", this); +#endif + btnHome->setStyleSheet(auxStyle); connect(btnHome, &QPushButton::clicked, this, &PlaybackWidget::homeClicked); rowLayout->addWidget(btnHome); - rowLayout - ->addStretch(); // Add stretch so home is left-aligned, controls center + rowLayout->addStretch(); rowLayout->addWidget(btnPrev); rowLayout->addSpacing(10); rowLayout->addWidget(m_btnPlay); @@ -95,23 +115,37 @@ void PlaybackWidget::onPlayToggle() { } SettingsWidget::SettingsWidget(QWidget *parent) : QWidget(parent) { - setStyleSheet("background-color: rgba(30, 30, 30, 230); border-radius: 12px; " - "border: 1px solid #666;"); + setStyleSheet( + QString("background-color: %1; border-radius: 12px; " + "border: 1px solid %2;") + .arg(Theme::rgba(Theme::Colors::SettingsSurface), + Theme::hex(Theme::Colors::BorderBright))); QVBoxLayout *layout = new QVBoxLayout(this); - layout->setContentsMargins(15, 15, 15, 15); - layout->setSpacing(10); + layout->setContentsMargins(Theme::Dims::SettingsPad, Theme::Dims::SettingsPad, + Theme::Dims::SettingsPad, Theme::Dims::SettingsPad); + layout->setSpacing(Theme::Dims::SettingsSpace); QHBoxLayout *header = new QHBoxLayout(); QLabel *title = new QLabel("Settings", this); - title->setStyleSheet("color: white; font-size: 18px; font-weight: bold; " - "border: none; background: transparent;"); + title->setStyleSheet( + QString("color: %1; font-size: 18px; font-weight: bold; " + "border: none; background: transparent;") + .arg(Theme::hex(Theme::Colors::TextPrimary))); +#if defined(PLATFORM_ANDROID) + QPushButton *btnClose = new QPushButton("X", this); +#else QPushButton *btnClose = new QPushButton("✕", this); +#endif btnClose->setFixedSize(30, 30); - btnClose->setStyleSheet("QPushButton { background: #444; color: white; " - "border-radius: 15px; border: none; font-weight: " - "bold; } QPushButton:pressed { background: #666; }"); + btnClose->setStyleSheet( + QString("QPushButton { background: %1; color: %2; " + "border-radius: 15px; border: none; font-weight: " + "bold; } QPushButton:pressed { background: %3; }") + .arg(Theme::hex(Theme::Colors::SurfaceElevated), + Theme::hex(Theme::Colors::TextPrimary), + Theme::hex(Theme::Colors::BorderBright))); connect(btnClose, &QPushButton::clicked, this, &SettingsWidget::closeClicked); header->addWidget(title); @@ -120,129 +154,145 @@ SettingsWidget::SettingsWidget(QWidget *parent) : QWidget(parent) { layout->addLayout(header); QGridLayout *grid = new QGridLayout(); - auto createCheck = [&](const QString &text, bool checked, int r, int c) { - QCheckBox *cb = new QCheckBox(text, this); - cb->setChecked(checked); - cb->setStyleSheet("QCheckBox { color: white; font-size: 14px; padding: " - "5px; border: none; background: transparent; } " - "QCheckBox::indicator { width: 20px; height: 20px; }"); - connect(cb, &QCheckBox::toggled, this, &SettingsWidget::emitParams); - grid->addWidget(cb, r, c); - return cb; + auto createToggle = [&](const QString &text, bool checked, int r, int c) { + ToggleSwitch *ts = new ToggleSwitch(text, this); + ts->setChecked(checked); + connect(ts, &ToggleSwitch::toggled, this, &SettingsWidget::emitParams); + grid->addWidget(ts, r, c); + return ts; }; - // Updated Defaults based on user request - m_checkGlass = createCheck("Glass", true, 0, 0); - m_checkEntropy = createCheck("Entropy", false, 0, 1); - m_checkAlbumColors = createCheck("Album Colors", false, 1, 0); - m_checkMirrored = createCheck("Mirrored", true, 1, 1); - m_checkInverted = createCheck("Invert", false, 2, 0); + m_checkGlass = createToggle("Glass", true, 0, 0); + m_checkEntropy = createToggle("Entropy", false, 0, 1); + m_checkAlbumColors = createToggle("Album Colors", false, 1, 0); + m_checkMirrored = createToggle("Mirrored", true, 1, 1); + m_checkInverted = createToggle("Invert", false, 2, 0); layout->addLayout(grid); - // Helper for sliders + QString lblStyle = + QString("color: %1; font-weight: bold; border: none; " + "background: transparent; min-width: 80px;") + .arg(Theme::hex(Theme::Colors::TextPrimary)); + auto addSlider = [&](const QString &label, int min, int max, int val, - QSlider *&slider, QLabel *&lbl) { + TouchSlider *&slider, QLabel *&lbl) { QHBoxLayout *h = new QHBoxLayout(); lbl = new QLabel(label, this); - lbl->setStyleSheet("color: white; font-weight: bold; border: none; " - "background: transparent; min-width: 80px;"); - slider = new QSlider(Qt::Horizontal, this); + lbl->setStyleSheet(lblStyle); + slider = new TouchSlider(this); slider->setRange(min, max); slider->setValue(val); - slider->setStyleSheet( - "QSlider::handle:horizontal { background: #aaa; width: 24px; margin: " - "-10px 0; border-radius: 12px; } QSlider::groove:horizontal { " - "background: #444; height: 4px; }"); h->addWidget(lbl); h->addWidget(slider); layout->addLayout(h); }; - // Updated Slider Defaults addSlider("Bins: 21", 10, 64, 21, m_sliderBins, m_lblBins); - connect(m_sliderBins, &QSlider::valueChanged, this, + connect(m_sliderBins, &TouchSlider::valueChanged, this, &SettingsWidget::onBinsChanged); addSlider("FPS: 60", 15, 120, 60, m_sliderFps, m_lblFps); - connect(m_sliderFps, &QSlider::valueChanged, this, [this](int val) { + connect(m_sliderFps, &TouchSlider::valueChanged, this, [this](int val) { m_lblFps->setText(QString("FPS: %1").arg(val)); emit fpsChanged(val); }); addSlider("Bright: 66%", 10, 200, 66, m_sliderBrightness, m_lblBrightness); - connect(m_sliderBrightness, &QSlider::valueChanged, this, + connect(m_sliderBrightness, &TouchSlider::valueChanged, this, &SettingsWidget::onBrightnessChanged); addSlider("Granularity", 0, 100, 95, m_sliderGranularity, m_lblGranularity); - connect(m_sliderGranularity, &QSlider::valueChanged, this, + connect(m_sliderGranularity, &TouchSlider::valueChanged, this, &SettingsWidget::onSmoothingChanged); addSlider("Detail", 0, 100, 5, m_sliderDetail, m_lblDetail); - connect(m_sliderDetail, &QSlider::valueChanged, this, + connect(m_sliderDetail, &TouchSlider::valueChanged, this, &SettingsWidget::onSmoothingChanged); addSlider("Strength", 0, 100, 95, m_sliderStrength, m_lblStrength); - connect(m_sliderStrength, &QSlider::valueChanged, this, + connect(m_sliderStrength, &TouchSlider::valueChanged, this, &SettingsWidget::onSmoothingChanged); - // Entropy slider — shown only when Entropy checkbox is checked { m_entropyContainer = new QWidget(this); m_entropyContainer->setStyleSheet("border: none; background: transparent;"); QHBoxLayout *h = new QHBoxLayout(m_entropyContainer); h->setContentsMargins(0, 0, 0, 0); m_lblEntropy = new QLabel("Entropy: 0.0", this); - m_lblEntropy->setStyleSheet("color: white; font-weight: bold; border: none; " - "background: transparent; min-width: 80px;"); - m_sliderEntropy = new QSlider(Qt::Horizontal, this); - m_sliderEntropy->setRange(-150, 150); // -1.5 to 1.5, center detent at 0 + m_lblEntropy->setStyleSheet(lblStyle); + m_sliderEntropy = new TouchSlider(this); + m_sliderEntropy->setRange(-150, 150); m_sliderEntropy->setValue(0); - m_sliderEntropy->setStyleSheet( - "QSlider::handle:horizontal { background: #aaa; width: 24px; margin: " - "-10px 0; border-radius: 12px; } QSlider::groove:horizontal { " - "background: #444; height: 4px; }"); h->addWidget(m_lblEntropy); h->addWidget(m_sliderEntropy); layout->addWidget(m_entropyContainer); m_entropyContainer->setVisible(false); - connect(m_sliderEntropy, &QSlider::valueChanged, this, + connect(m_sliderEntropy, &TouchSlider::valueChanged, this, &SettingsWidget::onEntropyChanged); - connect(m_checkEntropy, &QCheckBox::toggled, m_entropyContainer, + connect(m_checkEntropy, &ToggleSwitch::toggled, m_entropyContainer, &QWidget::setVisible); } - QHBoxLayout *padsLayout = new QHBoxLayout(); - - m_padDsp = new XYPad("DSP", this); - m_padDsp->setFormatter([](float x, float y) { + m_dspFormatter = [](float x, float y) { int power = 6 + (int)(x * 7.0f + 0.5f); int window = std::pow(2, power); int hop = 64 + y * (8192 - 64); if (hop > window) hop = window; return QString("Window: %1\nHop: %2").arg(window).arg(hop); - }); - // Default to FFT 8192 (x=1.0), Hop 64 (y=0.0) - m_padDsp->setValues(1.0f, 0.0f); - connect(m_padDsp, &XYPad::valuesChanged, this, - &SettingsWidget::onDspPadChanged); - padsLayout->addWidget(m_padDsp); - - m_padColor = new XYPad("Color", this); - m_padColor->setFormatter([](float x, float y) { + }; + m_colorFormatter = [](float x, float y) { float hue = x * 2.0f; float cont = 0.1f + y * 2.9f; return QString("Hue: %1\nCont: %2") .arg(hue, 0, 'f', 2) .arg(cont, 0, 'f', 2); - }); - // Default to Hue 0.35 (x=0.175), Cont 0.10 (y=0.0) + }; + + m_padDsp = new XYPad("DSP", this); + m_padDsp->setFormatter(m_dspFormatter); + m_padDsp->setValues(1.0f, 0.0f); + connect(m_padDsp, &XYPad::valuesChanged, this, + &SettingsWidget::onDspPadChanged); + + m_padColor = new XYPad("Color", this); + m_padColor->setFormatter(m_colorFormatter); m_padColor->setValues(0.175f, 0.0f); connect(m_padColor, &XYPad::valuesChanged, this, &SettingsWidget::onColorPadChanged); - padsLayout->addWidget(m_padColor); - layout->addLayout(padsLayout); + if constexpr (Theme::MobileXYPad) { + m_padDsp->hide(); + m_padColor->hide(); + + QString expandBtnStyle = QString( + "QPushButton { background: %1; color: %2; border: 1px solid %3; " + "border-radius: 8px; font-weight: bold; min-height: %4px; } " + "QPushButton:pressed { background: %5; }") + .arg(Theme::hex(Theme::Colors::SurfaceLight), + Theme::hex(Theme::Colors::TextPrimary), + Theme::hex(Theme::Colors::BorderMid)) + .arg(Theme::Dims::XYExpandBtnH) + .arg(Theme::hex(Theme::Colors::SurfaceElevated)); + + QHBoxLayout *padsLayout = new QHBoxLayout(); + QPushButton *btnDsp = new QPushButton("DSP", this); + btnDsp->setStyleSheet(expandBtnStyle); + connect(btnDsp, &QPushButton::clicked, this, &SettingsWidget::expandDspPad); + padsLayout->addWidget(btnDsp); + + QPushButton *btnColor = new QPushButton("Color", this); + btnColor->setStyleSheet(expandBtnStyle); + connect(btnColor, &QPushButton::clicked, this, &SettingsWidget::expandColorPad); + padsLayout->addWidget(btnColor); + + layout->addLayout(padsLayout); + } else { + QHBoxLayout *padsLayout = new QHBoxLayout(); + padsLayout->addWidget(m_padDsp); + padsLayout->addWidget(m_padColor); + layout->addLayout(padsLayout); + } } void SettingsWidget::setParams(bool glass, bool albumColors, @@ -329,6 +379,21 @@ void SettingsWidget::onBrightnessChanged(int val) { emitParams(); } +void SettingsWidget::updateDspPad(float x, float y) { + m_padDsp->setValues(x, y); + onDspPadChanged(x, y); +} + +void SettingsWidget::updateColorPad(float x, float y) { + m_padColor->setValues(x, y); + onColorPadChanged(x, y); +} + +float SettingsWidget::dspPadX() const { return m_padDsp->x(); } +float SettingsWidget::dspPadY() const { return m_padDsp->y(); } +float SettingsWidget::colorPadX() const { return m_padColor->x(); } +float SettingsWidget::colorPadY() const { return m_padColor->y(); } + void SettingsWidget::onSmoothingChanged(int val) { m_granularity = m_sliderGranularity->value(); m_detail = m_sliderDetail->value(); @@ -351,6 +416,37 @@ PlayerPage::PlayerPage(QWidget *parent) : QWidget(parent) { connect(m_visualizer, &VisualizerWidget::tapDetected, this, &PlayerPage::toggleFullScreen); + + if constexpr (Theme::MobileXYPad) { + m_expandedPad = new ExpandedXYPad(this); + + connect(m_settings, &SettingsWidget::expandDspPad, this, [this]() { + m_overlay->hide(); + m_expandedPadIsDsp = true; + m_expandedPad->open("DSP", m_settings->dspFormatter(), + m_settings->dspPadX(), m_settings->dspPadY()); + }); + + connect(m_settings, &SettingsWidget::expandColorPad, this, [this]() { + m_overlay->hide(); + m_expandedPadIsDsp = false; + m_expandedPad->open("Color", m_settings->colorFormatter(), + m_settings->colorPadX(), m_settings->colorPadY()); + }); + + connect(m_expandedPad, &ExpandedXYPad::valuesChanged, this, + [this](float x, float y) { + if (m_expandedPadIsDsp) + m_settings->updateDspPad(x, y); + else + m_settings->updateColorPad(x, y); + }); + + connect(m_expandedPad, &ExpandedXYPad::closed, this, [this]() { + m_overlay->raise(); + m_overlay->show(); + }); + } } void PlayerPage::setFullScreen(bool fs) { m_playback->setVisible(!fs); } @@ -376,4 +472,7 @@ void PlayerPage::resizeEvent(QResizeEvent *event) { m_playback->setGeometry(0, h - pbHeight, w, pbHeight); m_overlay->setGeometry(0, 0, w, h); -} \ No newline at end of file + + if (m_expandedPad) + m_expandedPad->setGeometry(0, 0, w, h); +} diff --git a/src/PlayerControls.h b/src/PlayerControls.h index 0047591..c359f7c 100644 --- a/src/PlayerControls.h +++ b/src/PlayerControls.h @@ -3,10 +3,8 @@ #pragma once #include "CommonWidgets.h" #include "VisualizerWidget.h" -#include #include #include -#include #include class PlaybackWidget : public QWidget { @@ -22,14 +20,14 @@ signals: void prevClicked(); void seekChanged(float pos); void settingsClicked(); - void homeClicked(); // New signal + void homeClicked(); private slots: void onSeekPressed(); void onSeekReleased(); void onPlayToggle(); private: - QSlider *m_seekSlider; + TouchSlider *m_seekSlider; bool m_seeking = false; bool m_isPlaying = false; QPushButton *m_btnPlay; @@ -40,6 +38,15 @@ class SettingsWidget : public QWidget { public: SettingsWidget(QWidget *parent = nullptr); + void updateDspPad(float x, float y); + void updateColorPad(float x, float y); + float dspPadX() const; + float dspPadY() const; + float colorPadX() const; + float colorPadY() const; + std::function dspFormatter() const { return m_dspFormatter; } + std::function colorFormatter() const { return m_colorFormatter; } + bool isGlass() const { return m_checkGlass->isChecked(); } bool isEntropy() const { return m_checkEntropy->isChecked(); } float getEntropy() const { return m_entropy; } @@ -68,6 +75,8 @@ signals: void dspParamsChanged(int fft, int hop); void binsChanged(int n); void closeClicked(); + void expandDspPad(); + void expandColorPad(); private slots: void emitParams(); @@ -79,28 +88,28 @@ private slots: void onEntropyChanged(int val); private: - QCheckBox *m_checkGlass; - QCheckBox *m_checkEntropy; - QCheckBox *m_checkAlbumColors; - QCheckBox *m_checkMirrored; - QCheckBox *m_checkInverted; + ToggleSwitch *m_checkGlass; + ToggleSwitch *m_checkEntropy; + ToggleSwitch *m_checkAlbumColors; + ToggleSwitch *m_checkMirrored; + ToggleSwitch *m_checkInverted; XYPad *m_padDsp; XYPad *m_padColor; - QSlider *m_sliderBins; + TouchSlider *m_sliderBins; QLabel *m_lblBins; - QSlider *m_sliderFps; + TouchSlider *m_sliderFps; QLabel *m_lblFps; - QSlider *m_sliderBrightness; + TouchSlider *m_sliderBrightness; QLabel *m_lblBrightness; - QSlider *m_sliderGranularity; + TouchSlider *m_sliderGranularity; QLabel *m_lblGranularity; - QSlider *m_sliderDetail; + TouchSlider *m_sliderDetail; QLabel *m_lblDetail; - QSlider *m_sliderStrength; + TouchSlider *m_sliderStrength; QLabel *m_lblStrength; - QSlider *m_sliderEntropy; + TouchSlider *m_sliderEntropy; QLabel *m_lblEntropy; QWidget *m_entropyContainer; float m_entropy = 0.0f; @@ -116,6 +125,9 @@ private: int m_fft = 4096; int m_hop = 1024; int m_bins = 26; + + std::function m_dspFormatter; + std::function m_colorFormatter; }; class PlayerPage : public QWidget { @@ -128,7 +140,7 @@ public: void setFullScreen(bool fs); signals: void toggleFullScreen(); - void homeClicked(); // New signal + void homeClicked(); protected: void resizeEvent(QResizeEvent *event) override; @@ -141,4 +153,6 @@ private: PlaybackWidget *m_playback; SettingsWidget *m_settings; OverlayWidget *m_overlay; -}; \ No newline at end of file + ExpandedXYPad *m_expandedPad = nullptr; + bool m_expandedPadIsDsp = true; +}; diff --git a/src/Theme.h b/src/Theme.h new file mode 100644 index 0000000..3dd25b2 --- /dev/null +++ b/src/Theme.h @@ -0,0 +1,155 @@ +// src/Theme.h + +#pragma once +#include +#include + +namespace Theme { + +#if defined(PLATFORM_IOS) +inline constexpr bool FrostedGlass = true; +#else +inline constexpr bool FrostedGlass = false; +#endif + +#if defined(PLATFORM_IOS) || defined(PLATFORM_ANDROID) +inline constexpr bool MobileXYPad = true; +#else +inline constexpr bool MobileXYPad = false; +#endif + +namespace Colors { + +// Accent & palette (shared across all skins) +inline const QColor Accent(0x00, 0xd4, 0xff); +inline const QColor TrackBg(0x44, 0x44, 0x44); +inline const QColor TrackOff(0x55, 0x55, 0x55); +inline const QColor TextPrimary(Qt::white); +inline const QColor TextSecondary(0xaa, 0xaa, 0xaa); +inline const QColor TextMuted(0xdd, 0xdd, 0xdd); + +// Surface tiers +inline const QColor SurfaceDark(0x11, 0x11, 0x11); +inline const QColor SurfaceMid(0x22, 0x22, 0x22); +inline const QColor SurfaceLight(0x33, 0x33, 0x33); +inline const QColor SurfaceElevated(0x44, 0x44, 0x44); + +// Borders +inline const QColor BorderSubtle(0x44, 0x44, 0x44); +inline const QColor BorderMid(0x55, 0x55, 0x55); +inline const QColor BorderBright(0x66, 0x66, 0x66); + +// XYPad +inline const QColor XYBg(40, 40, 40, 200); +inline const QColor XYBorder(80, 80, 80); +inline const QColor XYGrid(60, 60, 60); +inline const QColor XYAccentDim(0, 212, 255, 180); +inline const QColor XYAccentFaint(0, 212, 255, 100); + +// Expanded XYPad overlay +inline const QColor ExpandedXYBg(0, 0, 0, 160); +inline const QColor ExpandedXYBgDragging(0, 0, 0, 20); + +// List +inline const QColor ListSelected(50, 50, 50); +inline const QColor ListSeparator(34, 34, 34); +inline const QColor AlbumArtPlaceholder(40, 40, 40); + +// Platform-specific alpha levels +#if defined(PLATFORM_IOS) +inline const QColor OverlayDim(0, 0, 0, 60); +inline const QColor SettingsSurface(30, 30, 30, 160); +inline const QColor PlaybackSurface(0, 0, 0, 100); +#elif defined(PLATFORM_ANDROID) +inline const QColor OverlayDim(0, 0, 0, 100); +inline const QColor SettingsSurface(28, 28, 28, 240); +inline const QColor PlaybackSurface(0, 0, 0, 150); +#else +inline const QColor OverlayDim(0, 0, 0, 100); +inline const QColor SettingsSurface(28, 28, 28, 240); +inline const QColor PlaybackSurface(0, 0, 0, 150); +#endif + +} // namespace Colors + +namespace Dims { + +#if defined(PLATFORM_IOS) || defined(PLATFORM_ANDROID) + +// Touch-sized +inline constexpr int SliderHeight = 48; +inline constexpr int HandleRadius = 20; +inline constexpr int TrackH = 6; +inline constexpr int HitRadius = 40; +inline constexpr int TrackPad = 20; + +inline constexpr int ToggleW = 52; +inline constexpr int ToggleH = 28; +inline constexpr int ToggleKnob = 24; +inline constexpr int ToggleRow = 36; + +inline constexpr int XYIndicator = 20; +inline constexpr int XYInner = 6; +inline constexpr int XYMinH = 200; + +inline constexpr int ListIcon = 60; +inline constexpr int ListRow = 80; +inline constexpr int ListTitlePt = 16; +inline constexpr int ListArtistPt = 14; +inline constexpr int ListPad = 7; +inline constexpr int ListItemPad = 14; + +inline constexpr int BtnFontSize = 28; +inline constexpr int BtnMinSize = 48; + +inline constexpr int SettingsPad = 12; +inline constexpr int SettingsSpace = 8; +inline constexpr int XYExpandBtnH = 48; + +#else + +// Desktop: mouse-optimized, compact +inline constexpr int SliderHeight = 36; +inline constexpr int HandleRadius = 12; +inline constexpr int TrackH = 4; +inline constexpr int HitRadius = 28; +inline constexpr int TrackPad = 14; + +inline constexpr int ToggleW = 44; +inline constexpr int ToggleH = 22; +inline constexpr int ToggleKnob = 18; +inline constexpr int ToggleRow = 30; + +inline constexpr int XYIndicator = 12; +inline constexpr int XYInner = 4; +inline constexpr int XYMinH = 150; + +inline constexpr int ListIcon = 50; +inline constexpr int ListRow = 60; +inline constexpr int ListTitlePt = 14; +inline constexpr int ListArtistPt = 12; +inline constexpr int ListPad = 5; +inline constexpr int ListItemPad = 10; + +inline constexpr int BtnFontSize = 24; +inline constexpr int BtnMinSize = 0; + +inline constexpr int SettingsPad = 12; +inline constexpr int SettingsSpace = 10; +inline constexpr int XYExpandBtnH = 0; + +#endif + +} // namespace Dims + +inline QString hex(const QColor &c) { return c.name(QColor::HexRgb); } + +inline QString rgba(const QColor &c) { + return QString("rgba(%1,%2,%3,%4)") + .arg(c.red()) + .arg(c.green()) + .arg(c.blue()) + .arg(c.alpha()); +} + +} // namespace Theme