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;
};