performance on ios

This commit is contained in:
pszsh 2026-02-13 09:35:20 -08:00
parent 762fa82da2
commit 61e220f185
8 changed files with 922 additions and 600 deletions

View File

@ -1,17 +1,21 @@
// src/CommonWidgets.cpp
#include "CommonWidgets.h"
#include <QFileInfo> // Added for file info extraction
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QPainter>
#include <QListWidget> // Added for WelcomeWidget
#include <QMouseEvent>
#include <QPainter>
#include <QPushButton>
#include <QVBoxLayout>
#include <algorithm>
#include <cmath>
// --- PlaylistDelegate Implementation ---
void PlaylistDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const {
void PlaylistDelegate::paint(QPainter *painter,
const QStyleOptionViewItem &option,
const QModelIndex &index) const {
painter->save();
painter->setRenderHint(QPainter::Antialiasing);
@ -58,7 +62,8 @@ void PlaylistDelegate::paint(QPainter* painter, const QStyleOptionViewItem& opti
QRect titleRect = textRect;
titleRect.setHeight(titleHeight);
QString elidedTitle = fmTitle.elidedText(title, Qt::ElideRight, titleRect.width());
QString elidedTitle =
fmTitle.elidedText(title, Qt::ElideRight, titleRect.width());
painter->drawText(titleRect, Qt::AlignLeft | Qt::AlignTop, elidedTitle);
// Artist
@ -72,7 +77,8 @@ void PlaylistDelegate::paint(QPainter* painter, const QStyleOptionViewItem& opti
artistRect.setTop(titleRect.bottom() + 2);
artistRect.setHeight(fmArtist.height());
QString elidedArtist = fmArtist.elidedText(artist, Qt::ElideRight, artistRect.width());
QString elidedArtist =
fmArtist.elidedText(artist, Qt::ElideRight, artistRect.width());
painter->drawText(artistRect, Qt::AlignLeft | Qt::AlignTop, elidedArtist);
// Separator
@ -82,13 +88,15 @@ void PlaylistDelegate::paint(QPainter* painter, const QStyleOptionViewItem& opti
painter->restore();
}
QSize PlaylistDelegate::sizeHint(const QStyleOptionViewItem&, const QModelIndex&) const {
QSize PlaylistDelegate::sizeHint(const QStyleOptionViewItem &,
const QModelIndex &) const {
return QSize(0, 60);
}
// --- XYPad ---
XYPad::XYPad(const QString& title, QWidget* parent) : QWidget(parent), m_title(title) {
XYPad::XYPad(const QString &title, QWidget *parent)
: QWidget(parent), m_title(title) {
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
setMinimumHeight(150);
setCursor(Qt::CrossCursor);
@ -104,7 +112,7 @@ void XYPad::setValues(float x, float y) {
update();
}
void XYPad::paintEvent(QPaintEvent*) {
void XYPad::paintEvent(QPaintEvent *) {
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
@ -113,8 +121,8 @@ void XYPad::paintEvent(QPaintEvent*) {
p.drawRect(rect().adjusted(0, 0, -1, -1));
p.setPen(QPen(QColor(60, 60, 60), 1, Qt::DotLine));
p.drawLine(width()/2, 0, width()/2, height());
p.drawLine(0, height()/2, width(), height()/2);
p.drawLine(width() / 2, 0, width() / 2, height());
p.drawLine(0, height() / 2, width(), height() / 2);
int px = m_x * width();
int py = (1.0f - m_y) * height();
@ -139,26 +147,28 @@ void XYPad::paintEvent(QPaintEvent*) {
if (m_formatter) {
text += "\n" + m_formatter(m_x, m_y);
}
p.drawText(rect().adjusted(10, 10, -10, -10), Qt::AlignLeft | Qt::AlignTop, text);
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) { updateFromPos(event->pos()); }
void XYPad::mouseMoveEvent(QMouseEvent *event) { updateFromPos(event->pos()); }
void XYPad::updateFromPos(const QPoint& 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);
update();
emit valuesChanged(m_x, m_y);
}
OverlayWidget::OverlayWidget(QWidget* content, QWidget* parent) : QWidget(parent), m_content(content) {
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);
QVBoxLayout* layout = new QVBoxLayout(this);
QVBoxLayout *layout = new QVBoxLayout(this);
layout->setAlignment(Qt::AlignCenter);
layout->setContentsMargins(20, 20, 20, 20);
@ -170,38 +180,101 @@ OverlayWidget::OverlayWidget(QWidget* content, QWidget* parent) : QWidget(parent
hide();
}
void OverlayWidget::mousePressEvent(QMouseEvent* event) {
void OverlayWidget::mousePressEvent(QMouseEvent *event) {
if (!m_content->geometry().contains(event->pos())) {
hide();
}
}
void OverlayWidget::paintEvent(QPaintEvent* event) {
void OverlayWidget::paintEvent(QPaintEvent *event) {
QPainter p(this);
p.fillRect(rect(), QColor(0, 0, 0, 100));
}
WelcomeWidget::WelcomeWidget(QWidget* parent) : QWidget(parent) {
QVBoxLayout* layout = new QVBoxLayout(this);
WelcomeWidget::WelcomeWidget(QWidget *parent) : QWidget(parent) {
QVBoxLayout *layout = new QVBoxLayout(this);
layout->setAlignment(Qt::AlignCenter);
layout->setSpacing(20);
layout->setContentsMargins(40, 40, 40, 40);
QLabel* title = new QLabel("Yr Crystals", this);
QLabel *title = new QLabel("Yr Crystals", this);
title->setStyleSheet("color: white; font-size: 32px; font-weight: bold;");
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 btnStyle =
"QPushButton { background-color: #333; color: white; border: 1px solid "
"#555; border-radius: 8px; padding: 15px; font-size: 18px; } "
"QPushButton:pressed { background-color: #555; }";
QPushButton* btnFile = new QPushButton("Open File", this);
QHBoxLayout *btnLayout = new QHBoxLayout();
btnLayout->setSpacing(20);
QPushButton *btnFile = new QPushButton("Open File", this);
btnFile->setStyleSheet(btnStyle);
btnFile->setFixedWidth(250);
connect(btnFile, &QPushButton::clicked, this, &WelcomeWidget::openFileClicked);
layout->addWidget(btnFile);
btnFile->setCursor(Qt::PointingHandCursor);
connect(btnFile, &QPushButton::clicked, this,
&WelcomeWidget::openFileClicked);
btnLayout->addWidget(btnFile);
QPushButton* btnFolder = new QPushButton("Open Folder", this);
QPushButton *btnFolder = new QPushButton("Open Folder", this);
btnFolder->setStyleSheet(btnStyle);
btnFolder->setFixedWidth(250);
connect(btnFolder, &QPushButton::clicked, this, &WelcomeWidget::openFolderClicked);
layout->addWidget(btnFolder);
btnFolder->setCursor(Qt::PointingHandCursor);
connect(btnFolder, &QPushButton::clicked, this,
&WelcomeWidget::openFolderClicked);
btnLayout->addWidget(btnFolder);
layout->addLayout(btnLayout);
// --- Recents List ---
QLabel *recentLabel = new QLabel("Recent", this);
recentLabel->setStyleSheet("color: #aaa; font-size: 16px; margin-top: 20px;");
layout->addWidget(recentLabel);
m_recentList = new QListWidget(this);
m_recentList->setStyleSheet(
"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; }");
m_recentList->setFocusPolicy(Qt::NoFocus);
m_recentList->setCursor(Qt::PointingHandCursor);
m_recentList->setSelectionMode(QAbstractItemView::SingleSelection);
connect(m_recentList, &QListWidget::itemClicked, this,
&WelcomeWidget::onRecentClicked);
layout->addWidget(m_recentList);
// Refresh on init
refreshRecents();
}
void WelcomeWidget::refreshRecents() {
m_recentList->clear();
QStringList files = Utils::getRecentFiles();
QStringList folders = Utils::getRecentFolders();
// Interleave or section them? Let's just list folders then files.
for (const auto &path : folders) {
QListWidgetItem *item =
new QListWidgetItem("📁 " + QFileInfo(path).fileName());
item->setData(Qt::UserRole, path);
// Tooltip showing full path
item->setToolTip(path);
m_recentList->addItem(item);
}
for (const auto &path : files) {
QListWidgetItem *item =
new QListWidgetItem("🎵 " + QFileInfo(path).fileName());
item->setData(Qt::UserRole, path);
item->setToolTip(path);
m_recentList->addItem(item);
}
}
void WelcomeWidget::onRecentClicked(QListWidgetItem *item) {
if (item) {
emit pathSelected(item->data(Qt::UserRole).toString());
}
}

View File

@ -1,35 +1,40 @@
// src/CommonWidgets.h
#pragma once
#include <QWidget>
#include <QLabel>
#include <functional>
#include <QStyledItemDelegate>
#include "Utils.h"
#include <QLabel>
#include <QListWidget> // Include directly or fwd declare properly
#include <QStyledItemDelegate>
#include <QWidget>
#include <functional>
// Replaces PlaylistItemWidget for better performance
class PlaylistDelegate : public QStyledItemDelegate {
Q_OBJECT
public:
using QStyledItemDelegate::QStyledItemDelegate;
void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override;
QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override;
void paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
QSize sizeHint(const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
};
class XYPad : public QWidget {
Q_OBJECT
public:
XYPad(const QString& title, QWidget* parent = nullptr);
XYPad(const QString &title, QWidget *parent = nullptr);
void setFormatter(std::function<QString(float, float)> formatter);
void setValues(float x, float y);
signals:
void valuesChanged(float x, float y);
protected:
void paintEvent(QPaintEvent* event) override;
void mousePressEvent(QMouseEvent* event) override;
void mouseMoveEvent(QMouseEvent* event) override;
void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
private:
void updateFromPos(const QPoint& pos);
void updateFromPos(const QPoint &pos);
QString m_title;
float m_x = 0.5f;
float m_y = 0.5f;
@ -39,19 +44,31 @@ private:
class OverlayWidget : public QWidget {
Q_OBJECT
public:
OverlayWidget(QWidget* content, QWidget* parent = nullptr);
OverlayWidget(QWidget *content, QWidget *parent = nullptr);
protected:
void mousePressEvent(QMouseEvent* event) override;
void paintEvent(QPaintEvent* event) override;
void mousePressEvent(QMouseEvent *event) override;
void paintEvent(QPaintEvent *event) override;
private:
QWidget* m_content;
QWidget *m_content;
};
class QListWidget;
class QListWidgetItem;
class WelcomeWidget : public QWidget {
Q_OBJECT
public:
WelcomeWidget(QWidget* parent = nullptr);
WelcomeWidget(QWidget *parent = nullptr);
void refreshRecents();
signals:
void openFileClicked();
void openFolderClicked();
void pathSelected(const QString &path);
private slots:
void onRecentClicked(QListWidgetItem *item);
private:
QListWidget *m_recentList;
};

View File

@ -35,6 +35,12 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
&MainWindow::onOpenFile);
connect(m_welcome, &WelcomeWidget::openFolderClicked, this,
&MainWindow::onOpenFolder);
connect(m_welcome, &WelcomeWidget::pathSelected, this,
[this](const QString &path) {
// Determine if folder or file based on path
QFileInfo info(path);
loadPath(path, info.isDir());
});
m_stack->addWidget(m_welcome);
initUi();
@ -202,6 +208,10 @@ void MainWindow::initUi() {
connect(m_playerPage, &PlayerPage::toggleFullScreen, this,
&MainWindow::onToggleFullScreen);
connect(m_playerPage, &PlayerPage::homeClicked, this, [this]() {
m_welcome->refreshRecents();
m_stack->setCurrentWidget(m_welcome);
});
#ifdef IS_MOBILE
m_mobileTabs = new QTabWidget();
@ -333,6 +343,7 @@ void MainWindow::loadPath(const QString &rawPath, bool recursive) {
}
if (!m_tracks.isEmpty()) {
loadIndex(0);
Utils::addRecentFolder(path);
m_metaThread = new QThread(this);
m_metaLoader = new Utils::MetadataLoader();
m_metaLoader->moveToThread(m_metaThread);
@ -355,6 +366,7 @@ void MainWindow::loadPath(const QString &rawPath, bool recursive) {
if (!t.meta.thumbnail.isNull())
item->setData(Qt::DecorationRole, t.meta.thumbnail);
loadIndex(0);
Utils::addRecentFile(path);
}
loadSettings();
#ifdef IS_MOBILE

View File

@ -52,6 +52,15 @@ PlaybackWidget::PlaybackWidget(QWidget *parent) : QWidget(parent) {
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; }");
connect(btnHome, &QPushButton::clicked, this, &PlaybackWidget::homeClicked);
rowLayout->addWidget(btnHome);
rowLayout
->addStretch(); // Add stretch so home is left-aligned, controls center
rowLayout->addWidget(btnPrev);
rowLayout->addSpacing(10);
rowLayout->addWidget(m_btnPlay);
@ -338,6 +347,8 @@ PlayerPage::PlayerPage(QWidget *parent) : QWidget(parent) {
connect(m_playback, &PlaybackWidget::settingsClicked, this,
&PlayerPage::toggleOverlay);
connect(m_playback, &PlaybackWidget::homeClicked, this,
&PlayerPage::homeClicked);
connect(m_settings, &SettingsWidget::closeClicked, this,
&PlayerPage::closeOverlay);

View File

@ -23,6 +23,7 @@ signals:
void prevClicked();
void seekChanged(float pos);
void settingsClicked();
void homeClicked(); // New signal
private slots:
void onSeekPressed();
void onSeekReleased();
@ -125,6 +126,7 @@ public:
void setFullScreen(bool fs);
signals:
void toggleFullScreen();
void homeClicked(); // New signal
protected:
void resizeEvent(QResizeEvent *event) override;

View File

@ -1,17 +1,18 @@
// src/Utils.cpp
#include "Utils.h"
#include <QProcess>
#include <QFileInfo>
#include <QCryptographicHash>
#include <QDebug>
#include <QDir>
#include <QDirIterator>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QDebug>
#include <QStandardPaths>
#include <QCryptographicHash>
#include <QMap>
#include <QMutex>
#include <QProcess>
#include <QSettings>
#include <QStandardPaths>
#include <QUrl>
#include <cmath>
@ -23,47 +24,64 @@
#ifdef Q_OS_ANDROID
#include <QCoreApplication>
#include <QJniObject>
#include <QJniEnvironment>
#include <QJniObject>
#include <QtCore/qnativeinterface.h>
void scanAndroidTree(const QJniObject& context, const QJniObject& treeUri, const QJniObject& parentDocId, QStringList& results, bool recursive) {
void scanAndroidTree(const QJniObject &context, const QJniObject &treeUri,
const QJniObject &parentDocId, QStringList &results,
bool recursive) {
QJniEnvironment env;
QJniObject contentResolver = context.callObjectMethod("getContentResolver", "()Landroid/content/ContentResolver;");
if (env.checkAndClearExceptions()) return;
QJniObject contentResolver = context.callObjectMethod(
"getContentResolver", "()Landroid/content/ContentResolver;");
if (env.checkAndClearExceptions())
return;
QJniObject childrenUri = QJniObject::callStaticObjectMethod(
"android/provider/DocumentsContract", "buildChildDocumentsUriUsingTree",
"(Landroid/net/Uri;Ljava/lang/String;)Landroid/net/Uri;",
treeUri.object(), parentDocId.object()
);
if (env.checkAndClearExceptions()) return;
treeUri.object(), parentDocId.object());
if (env.checkAndClearExceptions())
return;
QJniObject cursor = contentResolver.callObjectMethod(
"query",
"(Landroid/net/Uri;[Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;)Landroid/database/Cursor;",
childrenUri.object(), nullptr, nullptr, nullptr, nullptr
);
"(Landroid/net/Uri;[Ljava/lang/String;Ljava/lang/String;[Ljava/lang/"
"String;Ljava/lang/String;)Landroid/database/Cursor;",
childrenUri.object(), nullptr, nullptr, nullptr, nullptr);
if (env.checkAndClearExceptions() || !cursor.isValid()) return;
if (env.checkAndClearExceptions() || !cursor.isValid())
return;
jint colDocId = cursor.callMethod<jint>("getColumnIndex", "(Ljava/lang/String;)I", QJniObject::fromString("document_id").object<jstring>());
jint colMime = cursor.callMethod<jint>("getColumnIndex", "(Ljava/lang/String;)I", QJniObject::fromString("mime_type").object<jstring>());
jint colDocId = cursor.callMethod<jint>(
"getColumnIndex", "(Ljava/lang/String;)I",
QJniObject::fromString("document_id").object<jstring>());
jint colMime = cursor.callMethod<jint>(
"getColumnIndex", "(Ljava/lang/String;)I",
QJniObject::fromString("mime_type").object<jstring>());
while (cursor.callMethod<jboolean>("moveToNext")) {
if (env.checkAndClearExceptions()) break;
QString mime = cursor.callObjectMethod("getString", "(I)Ljava/lang/String;", colMime).toString();
QString docId = cursor.callObjectMethod("getString", "(I)Ljava/lang/String;", colDocId).toString();
if (env.checkAndClearExceptions())
break;
QString mime =
cursor.callObjectMethod("getString", "(I)Ljava/lang/String;", colMime)
.toString();
QString docId =
cursor.callObjectMethod("getString", "(I)Ljava/lang/String;", colDocId)
.toString();
if (mime == "vnd.android.document/directory") {
if (recursive) scanAndroidTree(context, treeUri, QJniObject::fromString(docId), results, true);
} else if (mime.startsWith("audio/") || mime == "application/ogg" || mime == "audio/x-wav") {
if (recursive)
scanAndroidTree(context, treeUri, QJniObject::fromString(docId),
results, true);
} else if (mime.startsWith("audio/") || mime == "application/ogg" ||
mime == "audio/x-wav") {
QJniObject fileUri = QJniObject::callStaticObjectMethod(
"android/provider/DocumentsContract", "buildDocumentUriUsingTree",
"(Landroid/net/Uri;Ljava/lang/String;)Landroid/net/Uri;",
treeUri.object(), QJniObject::fromString(docId).object<jstring>()
);
if (fileUri.isValid()) results << fileUri.toString();
treeUri.object(), QJniObject::fromString(docId).object<jstring>());
if (fileUri.isValid())
results << fileUri.toString();
}
}
cursor.callMethod<void>("close");
@ -74,55 +92,86 @@ Utils::Metadata getMetadataAndroid(const QString &path) {
Utils::Metadata meta;
meta.title = QFileInfo(path).fileName();
QJniObject retriever("android/media/MediaMetadataRetriever");
if (!retriever.isValid()) return meta;
if (!retriever.isValid())
return meta;
QJniObject context = QNativeInterface::QAndroidApplication::context();
QJniEnvironment env;
try {
if (path.startsWith("content://")) {
QJniObject contentResolver = context.callObjectMethod("getContentResolver", "()Landroid/content/ContentResolver;");
QJniObject contentResolver = context.callObjectMethod(
"getContentResolver", "()Landroid/content/ContentResolver;");
// FIX: Do NOT use QUrl::toEncoded() here. Android URIs are already encoded strings.
// Passing them through QUrl can double-encode characters (e.g. %20 becomes %2520).
QJniObject uri = QJniObject::callStaticObjectMethod("android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;", QJniObject::fromString(path).object<jstring>());
// FIX: Do NOT use QUrl::toEncoded() here. Android URIs are already
// encoded strings. Passing them through QUrl can double-encode characters
// (e.g. %20 becomes %2520).
QJniObject uri = QJniObject::callStaticObjectMethod(
"android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;",
QJniObject::fromString(path).object<jstring>());
QJniObject pfd = contentResolver.callObjectMethod("openFileDescriptor", "(Landroid/net/Uri;Ljava/lang/String;)Landroid/os/ParcelFileDescriptor;", uri.object(), QJniObject::fromString("r").object<jstring>());
QJniObject pfd = contentResolver.callObjectMethod(
"openFileDescriptor",
"(Landroid/net/Uri;Ljava/lang/String;)Landroid/os/"
"ParcelFileDescriptor;",
uri.object(), QJniObject::fromString("r").object<jstring>());
if (pfd.isValid() && !env.checkAndClearExceptions()) {
QJniObject fd = pfd.callObjectMethod("getFileDescriptor", "()Ljava/io/FileDescriptor;");
QJniObject fd = pfd.callObjectMethod("getFileDescriptor",
"()Ljava/io/FileDescriptor;");
if (fd.isValid()) {
retriever.callMethod<void>("setDataSource", "(Ljava/io/FileDescriptor;)V", fd.object());
retriever.callMethod<void>(
"setDataSource", "(Ljava/io/FileDescriptor;)V", fd.object());
}
pfd.callMethod<void>("close");
} else {
retriever.callMethod<void>("setDataSource", "(Landroid/content/Context;Landroid/net/Uri;)V", context.object(), uri.object());
retriever.callMethod<void>(
"setDataSource", "(Landroid/content/Context;Landroid/net/Uri;)V",
context.object(), uri.object());
}
} else {
retriever.callMethod<void>("setDataSource", "(Ljava/lang/String;)V", QJniObject::fromString(path).object<jstring>());
retriever.callMethod<void>(
"setDataSource", "(Ljava/lang/String;)V",
QJniObject::fromString(path).object<jstring>());
}
} catch (...) {
env.checkAndClearExceptions();
return meta;
}
} catch (...) { env.checkAndClearExceptions(); return meta; }
if (env.checkAndClearExceptions()) return meta;
if (env.checkAndClearExceptions())
return meta;
auto extract = [&](int key) -> QString {
QJniObject val = retriever.callObjectMethod("extractMetadata", "(I)Ljava/lang/String;", key);
if (env.checkAndClearExceptions()) return QString();
QJniObject val = retriever.callObjectMethod("extractMetadata",
"(I)Ljava/lang/String;", key);
if (env.checkAndClearExceptions())
return QString();
return val.isValid() ? val.toString() : QString();
};
QString t = extract(7); if (!t.isEmpty()) meta.title = t;
QString a = extract(2); if (!a.isEmpty()) meta.artist = a;
QString al = extract(1); if (!al.isEmpty()) meta.album = al;
QString tr = extract(0); if (!tr.isEmpty()) meta.trackNumber = tr.split('/').first().toInt();
QString t = extract(7);
if (!t.isEmpty())
meta.title = t;
QString a = extract(2);
if (!a.isEmpty())
meta.artist = a;
QString al = extract(1);
if (!al.isEmpty())
meta.album = al;
QString tr = extract(0);
if (!tr.isEmpty())
meta.trackNumber = tr.split('/').first().toInt();
QJniObject artObj = retriever.callObjectMethod("getEmbeddedPicture", "()[B");
if (!env.checkAndClearExceptions() && artObj.isValid()) {
jbyteArray jBa = artObj.object<jbyteArray>();
if (jBa) {
int len = env->GetArrayLength(jBa);
QByteArray ba; ba.resize(len);
env->GetByteArrayRegion(jBa, 0, len, reinterpret_cast<jbyte*>(ba.data()));
QByteArray ba;
ba.resize(len);
env->GetByteArrayRegion(jBa, 0, len,
reinterpret_cast<jbyte *>(ba.data()));
meta.art.loadFromData(ba);
}
}
@ -140,14 +189,19 @@ Utils::Metadata getMetadataIOS(const QString &path) {
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:nil];
NSArray<AVMetadataItem *> *metadata = [asset commonMetadata];
for (AVMetadataItem *item in metadata) {
if (item.value == nil) continue;
if ([item.commonKey isEqualToString:AVMetadataCommonKeyTitle]) meta.title = QString::fromNSString((NSString *)item.value);
else if ([item.commonKey isEqualToString:AVMetadataCommonKeyArtist]) meta.artist = QString::fromNSString((NSString *)item.value);
else if ([item.commonKey isEqualToString:AVMetadataCommonKeyAlbumName]) meta.album = QString::fromNSString((NSString *)item.value);
if (item.value == nil)
continue;
if ([item.commonKey isEqualToString:AVMetadataCommonKeyTitle])
meta.title = QString::fromNSString((NSString *)item.value);
else if ([item.commonKey isEqualToString:AVMetadataCommonKeyArtist])
meta.artist = QString::fromNSString((NSString *)item.value);
else if ([item.commonKey isEqualToString:AVMetadataCommonKeyAlbumName])
meta.album = QString::fromNSString((NSString *)item.value);
else if ([item.commonKey isEqualToString:AVMetadataCommonKeyArtwork]) {
if ([item.value isKindOfClass:[NSData class]]) {
NSData *data = (NSData *)item.value;
meta.art.loadFromData(QByteArray::fromRawData((const char *)data.bytes, data.length));
meta.art.loadFromData(
QByteArray::fromRawData((const char *)data.bytes, data.length));
}
}
}
@ -155,33 +209,44 @@ Utils::Metadata getMetadataIOS(const QString &path) {
}
@interface FilePickerDelegate : NSObject <UIDocumentPickerDelegate>
@property (nonatomic, assign) std::function<void(QString)> callback;
@property (nonatomic, assign) bool isFolder;
@property(nonatomic, assign) std::function<void(QString)> callback;
@property(nonatomic, assign) bool isFolder;
@end
@implementation FilePickerDelegate
- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls {
- (void)documentPicker:(UIDocumentPickerViewController *)controller
didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls {
if (urls.count > 0) {
NSURL *url = urls.firstObject;
if (self.isFolder) [url startAccessingSecurityScopedResource];
if (self.callback) self.callback(QString::fromNSString(url.absoluteString));
if (self.isFolder)
[url startAccessingSecurityScopedResource];
if (self.callback)
self.callback(QString::fromNSString(url.absoluteString));
}
}
- (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller {}
- (void)documentPickerWasCancelled:
(UIDocumentPickerViewController *)controller {
}
@end
static FilePickerDelegate* g_pickerDelegate = nil;
static FilePickerDelegate *g_pickerDelegate = nil;
namespace Utils {
void openIosPicker(bool folder, std::function<void(QString)> callback) {
if (!g_pickerDelegate) g_pickerDelegate = [[FilePickerDelegate alloc] init];
void openIosPicker(bool folder, std::function<void(QString)> callback) {
if (!g_pickerDelegate)
g_pickerDelegate = [[FilePickerDelegate alloc] init];
g_pickerDelegate.callback = callback;
g_pickerDelegate.isFolder = folder;
UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc] initForOpeningContentTypes:folder ? @[UTTypeFolder] : @[UTTypeAudio] asCopy:!folder];
UIDocumentPickerViewController *picker =
[[UIDocumentPickerViewController alloc]
initForOpeningContentTypes:folder ? @[ UTTypeFolder ]
: @[ UTTypeAudio ]
asCopy:!folder];
picker.delegate = g_pickerDelegate;
picker.allowsMultipleSelection = NO;
UIWindow *window = nil;
for (UIScene *scene in [UIApplication sharedApplication].connectedScenes) {
if (scene.activationState == UISceneActivationStateForegroundActive && [scene isKindOfClass:[UIWindowScene class]]) {
if (scene.activationState == UISceneActivationStateForegroundActive &&
[scene isKindOfClass:[UIWindowScene class]]) {
for (UIWindow *w in ((UIWindowScene *)scene).windows) {
if (w.isKeyWindow) {
window = w;
@ -189,13 +254,15 @@ namespace Utils {
}
}
}
if (window) break;
if (window)
break;
}
UIViewController *root = window.rootViewController;
if (root) [root presentViewController:picker animated:YES completion:nil];
}
if (root)
[root presentViewController:picker animated:YES completion:nil];
}
} // namespace Utils
#endif
namespace Utils {
@ -205,16 +272,22 @@ void configureIOSAudioSession() {
NSError *error = nil;
AVAudioSession *session = [AVAudioSession sharedInstance];
[session setCategory:AVAudioSessionCategoryPlayback error:&error];
if (error) qWarning() << "Failed to set audio session category:" << QString::fromNSString(error.localizedDescription);
if (error)
qWarning() << "Failed to set audio session category:"
<< QString::fromNSString(error.localizedDescription);
[session setActive:YES error:&error];
#endif
}
static QString getBinary(const QString& name) {
static QString getBinary(const QString &name) {
QString bin = QStandardPaths::findExecutable(name);
if (!bin.isEmpty()) return bin;
QStringList paths = { "/opt/homebrew/bin/" + name, "/usr/local/bin/" + name, "/usr/bin/" + name, "/bin/" + name };
for (const auto& p : paths) if (QFile::exists(p)) return p;
if (!bin.isEmpty())
return bin;
QStringList paths = {"/opt/homebrew/bin/" + name, "/usr/local/bin/" + name,
"/usr/bin/" + name, "/bin/" + name};
for (const auto &p : paths)
if (QFile::exists(p))
return p;
return name;
}
@ -233,19 +306,24 @@ QString convertToWav(const QString &inputPath) {
return inputPath;
#else
QString wavPath = inputPath + ".temp.wav";
if (QFile::exists(wavPath)) QFile::remove(wavPath);
if (QFile::exists(wavPath))
QFile::remove(wavPath);
QProcess p;
p.start(getBinary("ffmpeg"), {"-y", "-v", "quiet", "-i", inputPath, "-vn", "-f", "wav", wavPath});
if (p.waitForFinished() && p.exitCode() == 0) return wavPath;
p.start(getBinary("ffmpeg"),
{"-y", "-v", "quiet", "-i", inputPath, "-vn", "-f", "wav", wavPath});
if (p.waitForFinished() && p.exitCode() == 0)
return wavPath;
return QString();
#endif
}
QString resolvePath(const QString& rawPath) {
if (rawPath.startsWith("content://")) return rawPath;
QString resolvePath(const QString &rawPath) {
if (rawPath.startsWith("content://"))
return rawPath;
if (rawPath.startsWith("file://")) {
QUrl url(rawPath);
if (url.isLocalFile()) return url.toLocalFile();
if (url.isLocalFile())
return url.toLocalFile();
return QUrl::fromPercentEncoding(rawPath.toUtf8()).mid(7);
}
return rawPath;
@ -265,27 +343,40 @@ Metadata getMetadata(const QString &filePath) {
QString ffprobe = getBinary("ffprobe");
QString ffmpeg = getBinary("ffmpeg");
QProcess p;
p.start(ffprobe, {"-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", filePath});
p.start(ffprobe, {"-v", "quiet", "-print_format", "json", "-show_format",
"-show_streams", filePath});
if (p.waitForFinished()) {
QJsonDocument doc = QJsonDocument::fromJson(p.readAllStandardOutput());
QJsonObject tags = doc.object()["format"].toObject()["tags"].toObject();
if (tags.contains("title")) meta.title = tags["title"].toString();
if (tags.contains("artist")) meta.artist = tags["artist"].toString();
if (tags.contains("album")) meta.album = tags["album"].toString();
if (tags.contains("track")) meta.trackNumber = tags["track"].toString().split('/').first().toInt();
if (tags.contains("title"))
meta.title = tags["title"].toString();
if (tags.contains("artist"))
meta.artist = tags["artist"].toString();
if (tags.contains("album"))
meta.album = tags["album"].toString();
if (tags.contains("track"))
meta.trackNumber = tags["track"].toString().split('/').first().toInt();
}
if (!meta.album.isEmpty()) {
QMutexLocker locker(&g_cacheMutex);
if (g_artCache.contains(meta.album)) meta.art = g_artCache[meta.album];
if (g_artCache.contains(meta.album))
meta.art = g_artCache[meta.album];
else {
QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/covers";
QString hash = QString(QCryptographicHash::hash(meta.album.toUtf8(), QCryptographicHash::Md5).toHex());
if (QFile::exists(cacheDir + "/" + hash + ".png") && meta.art.load(cacheDir + "/" + hash + ".png")) g_artCache.insert(meta.album, meta.art);
QString cacheDir =
QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
"/covers";
QString hash = QString(
QCryptographicHash::hash(meta.album.toUtf8(), QCryptographicHash::Md5)
.toHex());
if (QFile::exists(cacheDir + "/" + hash + ".png") &&
meta.art.load(cacheDir + "/" + hash + ".png"))
g_artCache.insert(meta.album, meta.art);
}
}
if (meta.art.isNull()) {
QProcess pArt;
pArt.start(ffmpeg, {"-y", "-v", "quiet", "-i", filePath, "-an", "-vcodec", "png", "-f", "image2pipe", "-"});
pArt.start(ffmpeg, {"-y", "-v", "quiet", "-i", filePath, "-an", "-vcodec",
"png", "-f", "image2pipe", "-"});
if (pArt.waitForFinished()) {
QByteArray data = pArt.readAllStandardOutput();
if (!data.isEmpty()) {
@ -293,23 +384,32 @@ Metadata getMetadata(const QString &filePath) {
if (!meta.album.isEmpty()) {
QMutexLocker locker(&g_cacheMutex);
g_artCache.insert(meta.album, meta.art);
QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/covers";
QString cacheDir =
QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
"/covers";
QDir().mkpath(cacheDir);
QString hash = QString(QCryptographicHash::hash(meta.album.toUtf8(), QCryptographicHash::Md5).toHex());
QString hash =
QString(QCryptographicHash::hash(meta.album.toUtf8(),
QCryptographicHash::Md5)
.toHex());
meta.art.save(cacheDir + "/" + hash + ".png", "PNG");
}
}
}
}
#endif
if (!meta.art.isNull()) meta.thumbnail = QPixmap::fromImage(meta.art.scaled(60, 60, Qt::KeepAspectRatio, Qt::SmoothTransformation));
if (!meta.art.isNull())
meta.thumbnail = QPixmap::fromImage(
meta.art.scaled(60, 60, Qt::KeepAspectRatio, Qt::SmoothTransformation));
return meta;
}
QVector<QColor> extractAlbumColors(const QImage &art, int numBins) {
QVector<QColor> palette(numBins, QColor(127, 127, 127));
if (art.isNull()) return palette;
QImage scaled = art.scaled(numBins, 20, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
if (art.isNull())
return palette;
QImage scaled =
art.scaled(numBins, 20, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
for (int x = 0; x < numBins; ++x) {
float maxVibrancy = -1.0f;
QColor bestColor = QColor(127, 127, 127);
@ -317,23 +417,33 @@ QVector<QColor> extractAlbumColors(const QImage &art, int numBins) {
QColor c = scaled.pixelColor(x, y);
float s = c.hsvSaturationF();
float v = c.valueF();
if (s * v > maxVibrancy) { maxVibrancy = s * v; bestColor = c; }
if (s * v > maxVibrancy) {
maxVibrancy = s * v;
bestColor = c;
}
}
palette[x] = bestColor;
}
return palette;
}
bool isContentUriFolder(const QString& path) {
bool isContentUriFolder(const QString &path) {
#ifdef Q_OS_ANDROID
if (!path.startsWith("content://")) return false;
if (!path.startsWith("content://"))
return false;
QJniEnvironment env;
QJniObject uri = QJniObject::callStaticObjectMethod("android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;", QJniObject::fromString(path).object<jstring>());
if (!uri.isValid()) return false;
QJniObject uri = QJniObject::callStaticObjectMethod(
"android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;",
QJniObject::fromString(path).object<jstring>());
if (!uri.isValid())
return false;
QJniObject context = QNativeInterface::QAndroidApplication::context();
QJniObject contentResolver = context.callObjectMethod("getContentResolver", "()Landroid/content/ContentResolver;");
QJniObject type = contentResolver.callObjectMethod("getType", "(Landroid/net/Uri;)Ljava/lang/String;", uri.object());
if (env.checkAndClearExceptions() || !type.isValid()) return false;
QJniObject contentResolver = context.callObjectMethod(
"getContentResolver", "()Landroid/content/ContentResolver;");
QJniObject type = contentResolver.callObjectMethod(
"getType", "(Landroid/net/Uri;)Ljava/lang/String;", uri.object());
if (env.checkAndClearExceptions() || !type.isValid())
return false;
return type.toString() == "vnd.android.document/directory";
#else
return false;
@ -345,33 +455,47 @@ QStringList scanDirectory(const QString &path, bool recursive) {
if (path.startsWith("content://")) {
QStringList results;
QJniEnvironment env;
QJniObject uri = QJniObject::callStaticObjectMethod("android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;", QJniObject::fromString(path).object<jstring>());
if (!uri.isValid()) return results;
QJniObject uri = QJniObject::callStaticObjectMethod(
"android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;",
QJniObject::fromString(path).object<jstring>());
if (!uri.isValid())
return results;
QJniObject context = QNativeInterface::QAndroidApplication::context();
QJniObject contentResolver = context.callObjectMethod("getContentResolver", "()Landroid/content/ContentResolver;");
QJniObject contentResolver = context.callObjectMethod(
"getContentResolver", "()Landroid/content/ContentResolver;");
// Try to persist permission, but don't fail if we can't (transient might be enough for now)
contentResolver.callMethod<void>("takePersistableUriPermission", "(Landroid/net/Uri;I)V", uri.object(), 1);
// FIX: Suppress the SecurityException warning if it fails, as it's not critical for immediate playback
// Try to persist permission, but don't fail if we can't (transient might be
// enough for now)
contentResolver.callMethod<void>("takePersistableUriPermission",
"(Landroid/net/Uri;I)V", uri.object(), 1);
// FIX: Suppress the SecurityException warning if it fails, as it's not
// critical for immediate playback
env.checkAndClearExceptions();
QJniObject docId = QJniObject::callStaticObjectMethod("android/provider/DocumentsContract", "getTreeDocumentId", "(Landroid/net/Uri;)Ljava/lang/String;", uri.object());
if (env.checkAndClearExceptions() || !docId.isValid()) return results;
QJniObject docId = QJniObject::callStaticObjectMethod(
"android/provider/DocumentsContract", "getTreeDocumentId",
"(Landroid/net/Uri;)Ljava/lang/String;", uri.object());
if (env.checkAndClearExceptions() || !docId.isValid())
return results;
scanAndroidTree(context, uri, docId, results, recursive);
return results;
}
#endif
QStringList files;
QStringList filters = {"*.mp3", "*.m4a", "*.wav", "*.flac", "*.ogg", "*.aif*", "*.aac"};
QDirIterator::IteratorFlag flag = recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags;
QStringList filters = {"*.mp3", "*.m4a", "*.wav", "*.flac",
"*.ogg", "*.aif*", "*.aac"};
QDirIterator::IteratorFlag flag =
recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags;
QDirIterator it(path, filters, QDir::Files, flag);
while (it.hasNext()) files << it.next();
while (it.hasNext())
files << it.next();
return files;
}
// --- Permission Helper Implementation ---
PermissionHelper::PermissionHelper(std::function<void(bool)> cb, QObject* parent)
PermissionHelper::PermissionHelper(std::function<void(bool)> cb,
QObject *parent)
: QObject(parent), m_callback(cb) {
m_timer = new QTimer(this);
m_timer->setInterval(500); // Check every 500ms
@ -381,10 +505,15 @@ PermissionHelper::PermissionHelper(std::function<void(bool)> cb, QObject* parent
void PermissionHelper::start() {
#ifdef Q_OS_ANDROID
QJniObject activity = QNativeInterface::QAndroidApplication::context();
jint sdkInt = QJniObject::getStaticField<jint>("android/os/Build$VERSION", "SDK_INT");
QString permission = (sdkInt >= 33) ? "android.permission.READ_MEDIA_AUDIO" : "android.permission.READ_EXTERNAL_STORAGE";
jint sdkInt =
QJniObject::getStaticField<jint>("android/os/Build$VERSION", "SDK_INT");
QString permission = (sdkInt >= 33)
? "android.permission.READ_MEDIA_AUDIO"
: "android.permission.READ_EXTERNAL_STORAGE";
jint result = activity.callMethod<jint>("checkSelfPermission", "(Ljava/lang/String;)I", QJniObject::fromString(permission).object<jstring>());
jint result = activity.callMethod<jint>(
"checkSelfPermission", "(Ljava/lang/String;)I",
QJniObject::fromString(permission).object<jstring>());
if (result == 0) {
m_callback(true);
@ -393,12 +522,18 @@ void PermissionHelper::start() {
// Request permission
QJniEnvironment env;
jclass stringClass = env.findClass("java/lang/String");
QJniObject permissionsArray = QJniObject::callStaticObjectMethod("java/lang/reflect/Array", "newInstance", "(Ljava/lang/Class;I)Ljava/lang/Object;", stringClass, 1);
QJniObject permissionsArray = QJniObject::callStaticObjectMethod(
"java/lang/reflect/Array", "newInstance",
"(Ljava/lang/Class;I)Ljava/lang/Object;", stringClass, 1);
// FIX: Use callStaticMethod<void> because Array.set returns void
QJniObject::callStaticMethod<void>("java/lang/reflect/Array", "set", "(Ljava/lang/Object;ILjava/lang/Object;)V", permissionsArray.object(), 0, QJniObject::fromString(permission).object<jstring>());
QJniObject::callStaticMethod<void>(
"java/lang/reflect/Array", "set",
"(Ljava/lang/Object;ILjava/lang/Object;)V", permissionsArray.object(),
0, QJniObject::fromString(permission).object<jstring>());
activity.callMethod<void>("requestPermissions", "([Ljava/lang/String;I)V", permissionsArray.object(), 101);
activity.callMethod<void>("requestPermissions", "([Ljava/lang/String;I)V",
permissionsArray.object(), 101);
// Start polling
m_timer->start();
@ -412,10 +547,15 @@ void PermissionHelper::start() {
void PermissionHelper::check() {
#ifdef Q_OS_ANDROID
QJniObject activity = QNativeInterface::QAndroidApplication::context();
jint sdkInt = QJniObject::getStaticField<jint>("android/os/Build$VERSION", "SDK_INT");
QString permission = (sdkInt >= 33) ? "android.permission.READ_MEDIA_AUDIO" : "android.permission.READ_EXTERNAL_STORAGE";
jint sdkInt =
QJniObject::getStaticField<jint>("android/os/Build$VERSION", "SDK_INT");
QString permission = (sdkInt >= 33)
? "android.permission.READ_MEDIA_AUDIO"
: "android.permission.READ_EXTERNAL_STORAGE";
jint result = activity.callMethod<jint>("checkSelfPermission", "(Ljava/lang/String;)I", QJniObject::fromString(permission).object<jstring>());
jint result = activity.callMethod<jint>(
"checkSelfPermission", "(Ljava/lang/String;)I",
QJniObject::fromString(permission).object<jstring>());
if (result == 0) {
m_timer->stop();
@ -435,21 +575,27 @@ void PermissionHelper::check() {
void requestAndroidPermissions(std::function<void(bool)> callback) {
// Create a self-managed helper that deletes itself when done
PermissionHelper* helper = new PermissionHelper(callback);
PermissionHelper *helper = new PermissionHelper(callback);
helper->start();
}
bool copyContentUriToLocalFile(const QString& uriStr, const QString& destPath) {
bool copyContentUriToLocalFile(const QString &uriStr, const QString &destPath) {
#ifdef Q_OS_ANDROID
QJniEnvironment env;
QJniObject context = QNativeInterface::QAndroidApplication::context();
QJniObject contentResolver = context.callObjectMethod("getContentResolver", "()Landroid/content/ContentResolver;");
QJniObject contentResolver = context.callObjectMethod(
"getContentResolver", "()Landroid/content/ContentResolver;");
// FIX: Do NOT use QUrl::toEncoded() here. Android URIs are already encoded strings.
// Passing them through QUrl can double-encode characters (e.g. %20 becomes %2520).
QJniObject uri = QJniObject::callStaticObjectMethod("android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;", QJniObject::fromString(uriStr).object<jstring>());
// FIX: Do NOT use QUrl::toEncoded() here. Android URIs are already encoded
// strings. Passing them through QUrl can double-encode characters (e.g. %20
// becomes %2520).
QJniObject uri = QJniObject::callStaticObjectMethod(
"android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;",
QJniObject::fromString(uriStr).object<jstring>());
QJniObject inputStream = contentResolver.callObjectMethod("openInputStream", "(Landroid/net/Uri;)Ljava/io/InputStream;", uri.object());
QJniObject inputStream = contentResolver.callObjectMethod(
"openInputStream", "(Landroid/net/Uri;)Ljava/io/InputStream;",
uri.object());
if (!inputStream.isValid() || env.checkAndClearExceptions()) {
qWarning() << "Failed to open input stream for URI:" << uriStr;
return false;
@ -463,20 +609,23 @@ bool copyContentUriToLocalFile(const QString& uriStr, const QString& destPath) {
}
jbyteArray buffer = env->NewByteArray(8192);
jmethodID readMethod = env->GetMethodID(env->FindClass("java/io/InputStream"), "read", "([B)I");
jmethodID readMethod =
env->GetMethodID(env->FindClass("java/io/InputStream"), "read", "([B)I");
bool success = true;
while (true) {
jint bytesRead = env->CallIntMethod(inputStream.object(), readMethod, buffer);
jint bytesRead =
env->CallIntMethod(inputStream.object(), readMethod, buffer);
if (env.checkAndClearExceptions()) {
qWarning() << "Exception during read from content URI";
success = false;
break;
}
if (bytesRead == -1) break;
if (bytesRead == -1)
break;
jbyte* bytes = env->GetByteArrayElements(buffer, nullptr);
dest.write(reinterpret_cast<const char*>(bytes), bytesRead);
jbyte *bytes = env->GetByteArrayElements(buffer, nullptr);
dest.write(reinterpret_cast<const char *>(bytes), bytesRead);
env->ReleaseByteArrayElements(buffer, bytes, JNI_ABORT);
}
@ -489,11 +638,12 @@ bool copyContentUriToLocalFile(const QString& uriStr, const QString& destPath) {
#endif
}
MetadataLoader::MetadataLoader(QObject* parent) : QObject(parent) {}
void MetadataLoader::startLoading(const QStringList& paths) {
MetadataLoader::MetadataLoader(QObject *parent) : QObject(parent) {}
void MetadataLoader::startLoading(const QStringList &paths) {
m_stop = false;
for (int i = 0; i < paths.size(); ++i) {
if (m_stop) break;
if (m_stop)
break;
Metadata meta = getMetadata(paths[i]);
emit metadataReady(i, meta);
}
@ -501,4 +651,35 @@ void MetadataLoader::startLoading(const QStringList& paths) {
}
void MetadataLoader::stop() { m_stop = true; }
// --- Recent Files Implementation ---
void addRecentFile(const QString &path) {
QSettings settings("YrCrystals", "App");
QStringList files = settings.value("recentFiles").toStringList();
files.removeAll(path);
files.prepend(path);
while (files.size() > 10)
files.removeLast();
settings.setValue("recentFiles", files);
}
void addRecentFolder(const QString &path) {
QSettings settings("YrCrystals", "App");
QStringList folders = settings.value("recentFolders").toStringList();
folders.removeAll(path);
folders.prepend(path);
while (folders.size() > 10)
folders.removeLast();
settings.setValue("recentFolders", folders);
}
QStringList getRecentFiles() {
QSettings settings("YrCrystals", "App");
return settings.value("recentFiles").toStringList();
}
QStringList getRecentFolders() {
QSettings settings("YrCrystals", "App");
return settings.value("recentFolders").toStringList();
}
} // namespace Utils

View File

@ -1,73 +1,82 @@
// src/Utils.h
#pragma once
#include <QString>
#include <QImage>
#include <QPixmap>
#include <QVector>
#include <QColor>
#include <QStringList>
#include <QImage>
#include <QObject>
#include <QPixmap>
#include <QString>
#include <QStringList>
#include <QTimer>
#include <QVector>
#include <atomic>
#include <functional>
namespace Utils {
bool checkDependencies();
QString convertToWav(const QString &inputPath);
QString resolvePath(const QString& rawPath);
bool checkDependencies();
QString convertToWav(const QString &inputPath);
QString resolvePath(const QString &rawPath);
// Configure platform-specific audio sessions (iOS)
void configureIOSAudioSession();
// Configure platform-specific audio sessions (iOS)
void configureIOSAudioSession();
struct Metadata {
struct Metadata {
QString title;
QString artist;
QString album;
int trackNumber = 0;
QImage art;
QPixmap thumbnail;
};
};
Metadata getMetadata(const QString &filePath);
QVector<QColor> extractAlbumColors(const QImage &art, int numBins);
QStringList scanDirectory(const QString &path, bool recursive);
Metadata getMetadata(const QString &filePath);
QVector<QColor> extractAlbumColors(const QImage &art, int numBins);
QStringList scanDirectory(const QString &path, bool recursive);
bool isContentUriFolder(const QString& path);
bool isContentUriFolder(const QString &path);
// Updated to use a helper object for async polling
void requestAndroidPermissions(std::function<void(bool)> callback);
// Updated to use a helper object for async polling
void requestAndroidPermissions(std::function<void(bool)> callback);
// Helper to robustly copy content URIs on Android
bool copyContentUriToLocalFile(const QString& uriStr, const QString& destPath);
// Helper to robustly copy content URIs on Android
bool copyContentUriToLocalFile(const QString &uriStr, const QString &destPath);
// Recent Files Management
void addRecentFile(const QString &path);
void addRecentFolder(const QString &path);
QStringList getRecentFiles();
QStringList getRecentFolders();
#ifdef Q_OS_IOS
void openIosPicker(bool folder, std::function<void(QString)> callback);
void openIosPicker(bool folder, std::function<void(QString)> callback);
#endif
class MetadataLoader : public QObject {
class MetadataLoader : public QObject {
Q_OBJECT
public:
explicit MetadataLoader(QObject* parent = nullptr);
void startLoading(const QStringList& paths);
public:
explicit MetadataLoader(QObject *parent = nullptr);
void startLoading(const QStringList &paths);
void stop();
signals:
void metadataReady(int index, const Utils::Metadata& meta);
signals:
void metadataReady(int index, const Utils::Metadata &meta);
void finished();
private:
std::atomic<bool> m_stop{false};
};
// Helper class to poll for permission results on Android
class PermissionHelper : public QObject {
private:
std::atomic<bool> m_stop{false};
};
// Helper class to poll for permission results on Android
class PermissionHelper : public QObject {
Q_OBJECT
public:
explicit PermissionHelper(std::function<void(bool)> cb, QObject* parent = nullptr);
public:
explicit PermissionHelper(std::function<void(bool)> cb,
QObject *parent = nullptr);
void start();
private slots:
private slots:
void check();
private:
private:
std::function<void(bool)> m_callback;
QTimer* m_timer;
QTimer *m_timer;
int m_attempts = 0;
};
}
};
} // namespace Utils

View File

@ -16,6 +16,15 @@
VisualizerWidget::VisualizerWidget(QWidget *parent) : QWidget(parent) {
setAttribute(Qt::WA_OpaquePaintEvent);
setNumBins(26);
#if defined(Q_OS_IOS)
// IOS Optimization: Cap internal rendering resolution
// Native retina (3.0) is overkill for this visualizer and kills fill-rate.
// 2.0 is visually indistinguishable for moving graphics but much faster.
// Note: We cannot easily change the widget's DPR directly without affecting
// layout, but we can scale the painter or use a target pixmap. For now,
// simpler optimization: rely on NO Antialiasing.
#endif
}
void VisualizerWidget::mouseReleaseEvent(QMouseEvent *event) {
@ -310,7 +319,13 @@ void VisualizerWidget::paintEvent(QPaintEvent *) {
QPainter p(this);
p.fillRect(rect(), Qt::black);
#if defined(Q_OS_IOS)
// iOS Optimization: Disable Antialiasing for performance
// Retina screens are high density enough that AA is often not needed
#else
p.setRenderHint(QPainter::Antialiasing);
#endif
if (m_data.empty())
return;
@ -334,7 +349,9 @@ void VisualizerWidget::paintEvent(QPaintEvent *) {
{
m_cache.fill(Qt::transparent); // Clear old frame
QPainter cachePainter(&m_cache);
#if !defined(Q_OS_IOS)
cachePainter.setRenderHint(QPainter::Antialiasing);
#endif
drawContent(cachePainter, hw, hh);
}