diff --git a/PRIVACY_POLICY.md b/PRIVACY_POLICY.md new file mode 100644 index 0000000..50173eb --- /dev/null +++ b/PRIVACY_POLICY.md @@ -0,0 +1,21 @@ +# Privacy Policy + +**Yr Crystals** does not collect, store, or transmit any personal data. + +## Device Permissions + +The app may request access to the following device features solely for local, on-device functionality: + +- **Music Library** — To play audio tracks from your library. + +No data from these sources is recorded, uploaded, or shared. All processing happens entirely on your device. + +## Third-Party Services + +Yr Crystals does not use analytics, advertising, tracking, or any third-party services. + +## Contact + +If you have questions about this policy, contact: **[your email]** + +*Last updated: March 1, 2026* diff --git a/ios/Info.plist b/ios/Info.plist index c17b00a..9f90f8d 100644 --- a/ios/Info.plist +++ b/ios/Info.plist @@ -39,14 +39,8 @@ - NSMicrophoneUsageDescription - This app requires audio access to visualize music. NSAppleMusicUsageDescription - This app requires access to your music library to play tracks. - NSPhotoLibraryUsageDescription - This app requires access to the photo library to load album art. - NSCameraUsageDescription - This app requires camera access for visualizer input. + Yr Crystals needs access to your music library to play tracks. UIFileSharingEnabled diff --git a/src/CommonWidgets.cpp b/src/CommonWidgets.cpp index 6affa07..c86d14f 100644 --- a/src/CommonWidgets.cpp +++ b/src/CommonWidgets.cpp @@ -463,19 +463,21 @@ OverlayWidget::OverlayWidget(QWidget *content, QWidget *parent) 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::setVisible(bool visible) { + if (visible && !isVisible()) { + if constexpr (Theme::FrostedGlass) { + if (auto *p = parentWidget()) { + QPixmap shot = p->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); + } } } + QWidget::setVisible(visible); } void OverlayWidget::mousePressEvent(QMouseEvent *event) { diff --git a/src/CommonWidgets.h b/src/CommonWidgets.h index 09aef76..331f547 100644 --- a/src/CommonWidgets.h +++ b/src/CommonWidgets.h @@ -128,9 +128,9 @@ class OverlayWidget : public QWidget { Q_OBJECT public: OverlayWidget(QWidget *content, QWidget *parent = nullptr); + void setVisible(bool visible) override; protected: - void showEvent(QShowEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void paintEvent(QPaintEvent *event) override; diff --git a/src/PlayerControls.cpp b/src/PlayerControls.cpp index fc78159..353613f 100644 --- a/src/PlayerControls.cpp +++ b/src/PlayerControls.cpp @@ -4,6 +4,7 @@ #include "Theme.h" #include #include +#include #include #include @@ -13,7 +14,7 @@ PlaybackWidget::PlaybackWidget(QWidget *parent) : QWidget(parent) { .arg(Theme::rgba(Theme::Colors::PlaybackSurface), Theme::hex(Theme::Colors::BorderSubtle))); QVBoxLayout *mainLayout = new QVBoxLayout(this); - mainLayout->setContentsMargins(10, 5, 10, 10); + mainLayout->setContentsMargins(10, 5, 10, 2); m_seekSlider = new TouchSlider(this); m_seekSlider->setRange(0, 1000); @@ -42,12 +43,11 @@ PlaybackWidget::PlaybackWidget(QWidget *parent) : QWidget(parent) { QString auxStyle = QString( "QPushButton { background: transparent; color: %1; font-size: %2px; " - "border: none; padding: 10px;%3 } " - "QPushButton:pressed { color: %4; }") + "border: none; padding: 16px 20px; min-width: 48px; min-height: 48px; } " + "QPushButton:pressed { color: %3; }") .arg(Theme::hex(Theme::Colors::TextSecondary)) .arg(Theme::Dims::BtnFontSize) - .arg(minSizeRule, - Theme::hex(Theme::Colors::TextPrimary)); + .arg(Theme::hex(Theme::Colors::TextPrimary)); QPushButton *btnPrev = new QPushButton("<<", this); btnPrev->setStyleSheet(btnStyle); @@ -62,23 +62,23 @@ 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); +#ifdef IS_MOBILE + rowLayout->addStretch(); + rowLayout->addWidget(btnPrev); + rowLayout->addSpacing(10); + rowLayout->addWidget(m_btnPlay); + rowLayout->addSpacing(10); + rowLayout->addWidget(btnNext); + rowLayout->addStretch(); #else QPushButton *btnSettings = new QPushButton("⚙", this); -#endif btnSettings->setStyleSheet(auxStyle); connect(btnSettings, &QPushButton::clicked, this, &PlaybackWidget::settingsClicked); -#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(); rowLayout->addWidget(btnPrev); @@ -88,6 +88,7 @@ PlaybackWidget::PlaybackWidget(QWidget *parent) : QWidget(parent) { rowLayout->addWidget(btnNext); rowLayout->addStretch(); rowLayout->addWidget(btnSettings); +#endif mainLayout->addLayout(rowLayout); } @@ -401,16 +402,70 @@ void SettingsWidget::onSmoothingChanged(int val) { emitParams(); } +// --- SideButton --- + +SideButton::SideButton(Icon icon, QWidget *parent) + : QWidget(parent), m_icon(icon) { + setAttribute(Qt::WA_TranslucentBackground); +} + +void SideButton::paintEvent(QPaintEvent *) { + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + + int cx = width() / 2; + int cy = height() / 2; + float s = std::min(width(), height()) * 0.18f; + + p.setPen(QPen(Theme::Colors::TextSecondary, s * 0.22f, Qt::SolidLine, + Qt::RoundCap, Qt::RoundJoin)); + p.setBrush(Qt::NoBrush); + + if (m_icon == Home) { + float gap = s * 0.35f; + float barH = s * 0.9f; + float barTop = cy; + p.drawLine(QPointF(cx - gap, barTop), QPointF(cx - gap, barTop + barH)); + p.drawLine(QPointF(cx + gap, barTop), QPointF(cx + gap, barTop + barH)); + float peakY = cy - s * 0.5f; + p.drawLine(QPointF(cx - s * 0.55f, cy + s * 0.1f), QPointF(cx, peakY)); + p.drawLine(QPointF(cx, peakY), QPointF(cx + s * 0.55f, cy + s * 0.1f)); + } else { + int rays = 6; + for (int i = 0; i < rays; i++) { + float a = i * M_PI / rays; + float dx = cos(a) * s * 0.7f; + float dy = sin(a) * s * 0.7f; + p.drawLine(QPointF(cx - dx, cy - dy), QPointF(cx + dx, cy + dy)); + } + p.setPen(Qt::NoPen); + p.setBrush(Theme::Colors::TextSecondary); + p.drawEllipse(QPointF(cx, cy), s * 0.25f, s * 0.25f); + } +} + +void SideButton::mousePressEvent(QMouseEvent *) { emit clicked(); } + +// --- PlayerPage --- + PlayerPage::PlayerPage(QWidget *parent) : QWidget(parent) { m_visualizer = new VisualizerWidget(this); m_playback = new PlaybackWidget(this); m_settings = new SettingsWidget(); m_overlay = new OverlayWidget(m_settings, this); +#ifdef IS_MOBILE + m_btnHome = new SideButton(SideButton::Home, this); + connect(m_btnHome, &SideButton::clicked, this, &PlayerPage::homeClicked); + + m_btnSettings = new SideButton(SideButton::Settings, this); + connect(m_btnSettings, &SideButton::clicked, this, &PlayerPage::toggleOverlay); +#else connect(m_playback, &PlaybackWidget::settingsClicked, this, &PlayerPage::toggleOverlay); connect(m_playback, &PlaybackWidget::homeClicked, this, &PlayerPage::homeClicked); +#endif connect(m_settings, &SettingsWidget::closeClicked, this, &PlayerPage::closeOverlay); @@ -449,18 +504,30 @@ PlayerPage::PlayerPage(QWidget *parent) : QWidget(parent) { } } -void PlayerPage::setFullScreen(bool fs) { m_playback->setVisible(!fs); } +void PlayerPage::setFullScreen(bool fs) { + m_playback->setVisible(!fs); + if (m_btnHome) m_btnHome->setVisible(!fs); + if (m_btnSettings) m_btnSettings->setVisible(!fs); +} void PlayerPage::toggleOverlay() { - if (m_overlay->isVisible()) + if (m_overlay->isVisible()) { m_overlay->hide(); - else { + if (m_btnHome) m_btnHome->show(); + if (m_btnSettings) m_btnSettings->show(); + } else { + if (m_btnHome) m_btnHome->hide(); + if (m_btnSettings) m_btnSettings->hide(); m_overlay->raise(); m_overlay->show(); } } -void PlayerPage::closeOverlay() { m_overlay->hide(); } +void PlayerPage::closeOverlay() { + m_overlay->hide(); + if (m_btnHome) m_btnHome->show(); + if (m_btnSettings) m_btnSettings->show(); +} void PlayerPage::resizeEvent(QResizeEvent *event) { int w = event->size().width(); @@ -475,4 +542,19 @@ void PlayerPage::resizeEvent(QResizeEvent *event) { if (m_expandedPad) m_expandedPad->setGeometry(0, 0, w, h); + + if (m_btnHome) { + int bw = w / 5; + int bh = h / 4; + int cy = h / 2 - bh / 2; + m_btnHome->setGeometry(0, cy, bw, bh); + m_btnHome->raise(); + } + if (m_btnSettings) { + int bw = w / 5; + int bh = h / 4; + int cy = h / 2 - bh / 2; + m_btnSettings->setGeometry(w - bw, cy, bw, bh); + m_btnSettings->raise(); + } } diff --git a/src/PlayerControls.h b/src/PlayerControls.h index c359f7c..8bd2bc6 100644 --- a/src/PlayerControls.h +++ b/src/PlayerControls.h @@ -130,6 +130,20 @@ private: std::function m_colorFormatter; }; +class SideButton : public QWidget { + Q_OBJECT +public: + enum Icon { Home, Settings }; + SideButton(Icon icon, QWidget *parent = nullptr); +signals: + void clicked(); +protected: + void paintEvent(QPaintEvent *) override; + void mousePressEvent(QMouseEvent *) override; +private: + Icon m_icon; +}; + class PlayerPage : public QWidget { Q_OBJECT public: @@ -155,4 +169,6 @@ private: OverlayWidget *m_overlay; ExpandedXYPad *m_expandedPad = nullptr; bool m_expandedPadIsDsp = true; + SideButton *m_btnHome = nullptr; + SideButton *m_btnSettings = nullptr; };