performance on ios
This commit is contained in:
parent
762fa82da2
commit
61e220f185
|
|
@ -1,17 +1,21 @@
|
||||||
// src/CommonWidgets.cpp
|
// src/CommonWidgets.cpp
|
||||||
|
|
||||||
#include "CommonWidgets.h"
|
#include "CommonWidgets.h"
|
||||||
|
#include <QFileInfo> // Added for file info extraction
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QVBoxLayout>
|
#include <QListWidget> // Added for WelcomeWidget
|
||||||
#include <QPainter>
|
|
||||||
#include <QMouseEvent>
|
#include <QMouseEvent>
|
||||||
|
#include <QPainter>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
|
#include <QVBoxLayout>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
// --- PlaylistDelegate Implementation ---
|
// --- 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->save();
|
||||||
painter->setRenderHint(QPainter::Antialiasing);
|
painter->setRenderHint(QPainter::Antialiasing);
|
||||||
|
|
||||||
|
|
@ -58,7 +62,8 @@ void PlaylistDelegate::paint(QPainter* painter, const QStyleOptionViewItem& opti
|
||||||
QRect titleRect = textRect;
|
QRect titleRect = textRect;
|
||||||
titleRect.setHeight(titleHeight);
|
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);
|
painter->drawText(titleRect, Qt::AlignLeft | Qt::AlignTop, elidedTitle);
|
||||||
|
|
||||||
// Artist
|
// Artist
|
||||||
|
|
@ -72,7 +77,8 @@ void PlaylistDelegate::paint(QPainter* painter, const QStyleOptionViewItem& opti
|
||||||
artistRect.setTop(titleRect.bottom() + 2);
|
artistRect.setTop(titleRect.bottom() + 2);
|
||||||
artistRect.setHeight(fmArtist.height());
|
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);
|
painter->drawText(artistRect, Qt::AlignLeft | Qt::AlignTop, elidedArtist);
|
||||||
|
|
||||||
// Separator
|
// Separator
|
||||||
|
|
@ -82,13 +88,15 @@ void PlaylistDelegate::paint(QPainter* painter, const QStyleOptionViewItem& opti
|
||||||
painter->restore();
|
painter->restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
QSize PlaylistDelegate::sizeHint(const QStyleOptionViewItem&, const QModelIndex&) const {
|
QSize PlaylistDelegate::sizeHint(const QStyleOptionViewItem &,
|
||||||
|
const QModelIndex &) const {
|
||||||
return QSize(0, 60);
|
return QSize(0, 60);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- XYPad ---
|
// --- 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);
|
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||||
setMinimumHeight(150);
|
setMinimumHeight(150);
|
||||||
setCursor(Qt::CrossCursor);
|
setCursor(Qt::CrossCursor);
|
||||||
|
|
@ -139,7 +147,8 @@ void XYPad::paintEvent(QPaintEvent*) {
|
||||||
if (m_formatter) {
|
if (m_formatter) {
|
||||||
text += "\n" + m_formatter(m_x, m_y);
|
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::mousePressEvent(QMouseEvent *event) { updateFromPos(event->pos()); }
|
||||||
|
|
@ -152,7 +161,8 @@ void XYPad::updateFromPos(const QPoint& pos) {
|
||||||
emit valuesChanged(m_x, m_y);
|
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();
|
QPalette pal = palette();
|
||||||
pal.setColor(QPalette::Window, QColor(0, 0, 0, 100));
|
pal.setColor(QPalette::Window, QColor(0, 0, 0, 100));
|
||||||
setAutoFillBackground(true);
|
setAutoFillBackground(true);
|
||||||
|
|
@ -185,23 +195,86 @@ WelcomeWidget::WelcomeWidget(QWidget* parent) : QWidget(parent) {
|
||||||
QVBoxLayout *layout = new QVBoxLayout(this);
|
QVBoxLayout *layout = new QVBoxLayout(this);
|
||||||
layout->setAlignment(Qt::AlignCenter);
|
layout->setAlignment(Qt::AlignCenter);
|
||||||
layout->setSpacing(20);
|
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->setStyleSheet("color: white; font-size: 32px; font-weight: bold;");
|
||||||
title->setAlignment(Qt::AlignCenter);
|
title->setAlignment(Qt::AlignCenter);
|
||||||
layout->addWidget(title);
|
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; }";
|
||||||
|
|
||||||
|
QHBoxLayout *btnLayout = new QHBoxLayout();
|
||||||
|
btnLayout->setSpacing(20);
|
||||||
|
|
||||||
QPushButton *btnFile = new QPushButton("Open File", this);
|
QPushButton *btnFile = new QPushButton("Open File", this);
|
||||||
btnFile->setStyleSheet(btnStyle);
|
btnFile->setStyleSheet(btnStyle);
|
||||||
btnFile->setFixedWidth(250);
|
btnFile->setCursor(Qt::PointingHandCursor);
|
||||||
connect(btnFile, &QPushButton::clicked, this, &WelcomeWidget::openFileClicked);
|
connect(btnFile, &QPushButton::clicked, this,
|
||||||
layout->addWidget(btnFile);
|
&WelcomeWidget::openFileClicked);
|
||||||
|
btnLayout->addWidget(btnFile);
|
||||||
|
|
||||||
QPushButton *btnFolder = new QPushButton("Open Folder", this);
|
QPushButton *btnFolder = new QPushButton("Open Folder", this);
|
||||||
btnFolder->setStyleSheet(btnStyle);
|
btnFolder->setStyleSheet(btnStyle);
|
||||||
btnFolder->setFixedWidth(250);
|
btnFolder->setCursor(Qt::PointingHandCursor);
|
||||||
connect(btnFolder, &QPushButton::clicked, this, &WelcomeWidget::openFolderClicked);
|
connect(btnFolder, &QPushButton::clicked, this,
|
||||||
layout->addWidget(btnFolder);
|
&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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,19 +1,22 @@
|
||||||
// src/CommonWidgets.h
|
// src/CommonWidgets.h
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <QWidget>
|
|
||||||
#include <QLabel>
|
|
||||||
#include <functional>
|
|
||||||
#include <QStyledItemDelegate>
|
|
||||||
#include "Utils.h"
|
#include "Utils.h"
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QListWidget> // Include directly or fwd declare properly
|
||||||
|
#include <QStyledItemDelegate>
|
||||||
|
#include <QWidget>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
// Replaces PlaylistItemWidget for better performance
|
// Replaces PlaylistItemWidget for better performance
|
||||||
class PlaylistDelegate : public QStyledItemDelegate {
|
class PlaylistDelegate : public QStyledItemDelegate {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
using QStyledItemDelegate::QStyledItemDelegate;
|
using QStyledItemDelegate::QStyledItemDelegate;
|
||||||
void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override;
|
void paint(QPainter *painter, const QStyleOptionViewItem &option,
|
||||||
QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override;
|
const QModelIndex &index) const override;
|
||||||
|
QSize sizeHint(const QStyleOptionViewItem &option,
|
||||||
|
const QModelIndex &index) const override;
|
||||||
};
|
};
|
||||||
|
|
||||||
class XYPad : public QWidget {
|
class XYPad : public QWidget {
|
||||||
|
|
@ -24,10 +27,12 @@ public:
|
||||||
void setValues(float x, float y);
|
void setValues(float x, float y);
|
||||||
signals:
|
signals:
|
||||||
void valuesChanged(float x, float y);
|
void valuesChanged(float x, float y);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void paintEvent(QPaintEvent *event) override;
|
void paintEvent(QPaintEvent *event) override;
|
||||||
void mousePressEvent(QMouseEvent *event) override;
|
void mousePressEvent(QMouseEvent *event) override;
|
||||||
void mouseMoveEvent(QMouseEvent *event) override;
|
void mouseMoveEvent(QMouseEvent *event) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void updateFromPos(const QPoint &pos);
|
void updateFromPos(const QPoint &pos);
|
||||||
QString m_title;
|
QString m_title;
|
||||||
|
|
@ -40,18 +45,30 @@ class OverlayWidget : public QWidget {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
OverlayWidget(QWidget *content, QWidget *parent = nullptr);
|
OverlayWidget(QWidget *content, QWidget *parent = nullptr);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void mousePressEvent(QMouseEvent *event) override;
|
void mousePressEvent(QMouseEvent *event) override;
|
||||||
void paintEvent(QPaintEvent *event) override;
|
void paintEvent(QPaintEvent *event) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QWidget *m_content;
|
QWidget *m_content;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class QListWidget;
|
||||||
|
class QListWidgetItem;
|
||||||
|
|
||||||
class WelcomeWidget : public QWidget {
|
class WelcomeWidget : public QWidget {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
WelcomeWidget(QWidget *parent = nullptr);
|
WelcomeWidget(QWidget *parent = nullptr);
|
||||||
|
void refreshRecents();
|
||||||
signals:
|
signals:
|
||||||
void openFileClicked();
|
void openFileClicked();
|
||||||
void openFolderClicked();
|
void openFolderClicked();
|
||||||
|
void pathSelected(const QString &path);
|
||||||
|
private slots:
|
||||||
|
void onRecentClicked(QListWidgetItem *item);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QListWidget *m_recentList;
|
||||||
};
|
};
|
||||||
|
|
@ -35,6 +35,12 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
|
||||||
&MainWindow::onOpenFile);
|
&MainWindow::onOpenFile);
|
||||||
connect(m_welcome, &WelcomeWidget::openFolderClicked, this,
|
connect(m_welcome, &WelcomeWidget::openFolderClicked, this,
|
||||||
&MainWindow::onOpenFolder);
|
&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);
|
m_stack->addWidget(m_welcome);
|
||||||
|
|
||||||
initUi();
|
initUi();
|
||||||
|
|
@ -202,6 +208,10 @@ void MainWindow::initUi() {
|
||||||
|
|
||||||
connect(m_playerPage, &PlayerPage::toggleFullScreen, this,
|
connect(m_playerPage, &PlayerPage::toggleFullScreen, this,
|
||||||
&MainWindow::onToggleFullScreen);
|
&MainWindow::onToggleFullScreen);
|
||||||
|
connect(m_playerPage, &PlayerPage::homeClicked, this, [this]() {
|
||||||
|
m_welcome->refreshRecents();
|
||||||
|
m_stack->setCurrentWidget(m_welcome);
|
||||||
|
});
|
||||||
|
|
||||||
#ifdef IS_MOBILE
|
#ifdef IS_MOBILE
|
||||||
m_mobileTabs = new QTabWidget();
|
m_mobileTabs = new QTabWidget();
|
||||||
|
|
@ -333,6 +343,7 @@ void MainWindow::loadPath(const QString &rawPath, bool recursive) {
|
||||||
}
|
}
|
||||||
if (!m_tracks.isEmpty()) {
|
if (!m_tracks.isEmpty()) {
|
||||||
loadIndex(0);
|
loadIndex(0);
|
||||||
|
Utils::addRecentFolder(path);
|
||||||
m_metaThread = new QThread(this);
|
m_metaThread = new QThread(this);
|
||||||
m_metaLoader = new Utils::MetadataLoader();
|
m_metaLoader = new Utils::MetadataLoader();
|
||||||
m_metaLoader->moveToThread(m_metaThread);
|
m_metaLoader->moveToThread(m_metaThread);
|
||||||
|
|
@ -355,6 +366,7 @@ void MainWindow::loadPath(const QString &rawPath, bool recursive) {
|
||||||
if (!t.meta.thumbnail.isNull())
|
if (!t.meta.thumbnail.isNull())
|
||||||
item->setData(Qt::DecorationRole, t.meta.thumbnail);
|
item->setData(Qt::DecorationRole, t.meta.thumbnail);
|
||||||
loadIndex(0);
|
loadIndex(0);
|
||||||
|
Utils::addRecentFile(path);
|
||||||
}
|
}
|
||||||
loadSettings();
|
loadSettings();
|
||||||
#ifdef IS_MOBILE
|
#ifdef IS_MOBILE
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,15 @@ PlaybackWidget::PlaybackWidget(QWidget *parent) : QWidget(parent) {
|
||||||
connect(btnSettings, &QPushButton::clicked, this,
|
connect(btnSettings, &QPushButton::clicked, this,
|
||||||
&PlaybackWidget::settingsClicked);
|
&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->addWidget(btnPrev);
|
||||||
rowLayout->addSpacing(10);
|
rowLayout->addSpacing(10);
|
||||||
rowLayout->addWidget(m_btnPlay);
|
rowLayout->addWidget(m_btnPlay);
|
||||||
|
|
@ -338,6 +347,8 @@ PlayerPage::PlayerPage(QWidget *parent) : QWidget(parent) {
|
||||||
|
|
||||||
connect(m_playback, &PlaybackWidget::settingsClicked, this,
|
connect(m_playback, &PlaybackWidget::settingsClicked, this,
|
||||||
&PlayerPage::toggleOverlay);
|
&PlayerPage::toggleOverlay);
|
||||||
|
connect(m_playback, &PlaybackWidget::homeClicked, this,
|
||||||
|
&PlayerPage::homeClicked);
|
||||||
connect(m_settings, &SettingsWidget::closeClicked, this,
|
connect(m_settings, &SettingsWidget::closeClicked, this,
|
||||||
&PlayerPage::closeOverlay);
|
&PlayerPage::closeOverlay);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ signals:
|
||||||
void prevClicked();
|
void prevClicked();
|
||||||
void seekChanged(float pos);
|
void seekChanged(float pos);
|
||||||
void settingsClicked();
|
void settingsClicked();
|
||||||
|
void homeClicked(); // New signal
|
||||||
private slots:
|
private slots:
|
||||||
void onSeekPressed();
|
void onSeekPressed();
|
||||||
void onSeekReleased();
|
void onSeekReleased();
|
||||||
|
|
@ -125,6 +126,7 @@ public:
|
||||||
void setFullScreen(bool fs);
|
void setFullScreen(bool fs);
|
||||||
signals:
|
signals:
|
||||||
void toggleFullScreen();
|
void toggleFullScreen();
|
||||||
|
void homeClicked(); // New signal
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void resizeEvent(QResizeEvent *event) override;
|
void resizeEvent(QResizeEvent *event) override;
|
||||||
|
|
|
||||||
427
src/Utils.cpp
427
src/Utils.cpp
|
|
@ -1,17 +1,18 @@
|
||||||
// src/Utils.cpp
|
// src/Utils.cpp
|
||||||
#include "Utils.h"
|
#include "Utils.h"
|
||||||
#include <QProcess>
|
#include <QCryptographicHash>
|
||||||
#include <QFileInfo>
|
#include <QDebug>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QDirIterator>
|
#include <QDirIterator>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QJsonArray>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QJsonArray>
|
|
||||||
#include <QDebug>
|
|
||||||
#include <QStandardPaths>
|
|
||||||
#include <QCryptographicHash>
|
|
||||||
#include <QMap>
|
#include <QMap>
|
||||||
#include <QMutex>
|
#include <QMutex>
|
||||||
|
#include <QProcess>
|
||||||
|
#include <QSettings>
|
||||||
|
#include <QStandardPaths>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
|
|
@ -23,47 +24,64 @@
|
||||||
|
|
||||||
#ifdef Q_OS_ANDROID
|
#ifdef Q_OS_ANDROID
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
#include <QJniObject>
|
|
||||||
#include <QJniEnvironment>
|
#include <QJniEnvironment>
|
||||||
|
#include <QJniObject>
|
||||||
#include <QtCore/qnativeinterface.h>
|
#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;
|
QJniEnvironment env;
|
||||||
QJniObject contentResolver = context.callObjectMethod("getContentResolver", "()Landroid/content/ContentResolver;");
|
QJniObject contentResolver = context.callObjectMethod(
|
||||||
if (env.checkAndClearExceptions()) return;
|
"getContentResolver", "()Landroid/content/ContentResolver;");
|
||||||
|
if (env.checkAndClearExceptions())
|
||||||
|
return;
|
||||||
|
|
||||||
QJniObject childrenUri = QJniObject::callStaticObjectMethod(
|
QJniObject childrenUri = QJniObject::callStaticObjectMethod(
|
||||||
"android/provider/DocumentsContract", "buildChildDocumentsUriUsingTree",
|
"android/provider/DocumentsContract", "buildChildDocumentsUriUsingTree",
|
||||||
"(Landroid/net/Uri;Ljava/lang/String;)Landroid/net/Uri;",
|
"(Landroid/net/Uri;Ljava/lang/String;)Landroid/net/Uri;",
|
||||||
treeUri.object(), parentDocId.object()
|
treeUri.object(), parentDocId.object());
|
||||||
);
|
if (env.checkAndClearExceptions())
|
||||||
if (env.checkAndClearExceptions()) return;
|
return;
|
||||||
|
|
||||||
QJniObject cursor = contentResolver.callObjectMethod(
|
QJniObject cursor = contentResolver.callObjectMethod(
|
||||||
"query",
|
"query",
|
||||||
"(Landroid/net/Uri;[Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;)Landroid/database/Cursor;",
|
"(Landroid/net/Uri;[Ljava/lang/String;Ljava/lang/String;[Ljava/lang/"
|
||||||
childrenUri.object(), nullptr, nullptr, nullptr, nullptr
|
"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 colDocId = cursor.callMethod<jint>(
|
||||||
jint colMime = cursor.callMethod<jint>("getColumnIndex", "(Ljava/lang/String;)I", QJniObject::fromString("mime_type").object<jstring>());
|
"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")) {
|
while (cursor.callMethod<jboolean>("moveToNext")) {
|
||||||
if (env.checkAndClearExceptions()) break;
|
if (env.checkAndClearExceptions())
|
||||||
QString mime = cursor.callObjectMethod("getString", "(I)Ljava/lang/String;", colMime).toString();
|
break;
|
||||||
QString docId = cursor.callObjectMethod("getString", "(I)Ljava/lang/String;", colDocId).toString();
|
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 (mime == "vnd.android.document/directory") {
|
||||||
if (recursive) scanAndroidTree(context, treeUri, QJniObject::fromString(docId), results, true);
|
if (recursive)
|
||||||
} else if (mime.startsWith("audio/") || mime == "application/ogg" || mime == "audio/x-wav") {
|
scanAndroidTree(context, treeUri, QJniObject::fromString(docId),
|
||||||
|
results, true);
|
||||||
|
} else if (mime.startsWith("audio/") || mime == "application/ogg" ||
|
||||||
|
mime == "audio/x-wav") {
|
||||||
QJniObject fileUri = QJniObject::callStaticObjectMethod(
|
QJniObject fileUri = QJniObject::callStaticObjectMethod(
|
||||||
"android/provider/DocumentsContract", "buildDocumentUriUsingTree",
|
"android/provider/DocumentsContract", "buildDocumentUriUsingTree",
|
||||||
"(Landroid/net/Uri;Ljava/lang/String;)Landroid/net/Uri;",
|
"(Landroid/net/Uri;Ljava/lang/String;)Landroid/net/Uri;",
|
||||||
treeUri.object(), QJniObject::fromString(docId).object<jstring>()
|
treeUri.object(), QJniObject::fromString(docId).object<jstring>());
|
||||||
);
|
if (fileUri.isValid())
|
||||||
if (fileUri.isValid()) results << fileUri.toString();
|
results << fileUri.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cursor.callMethod<void>("close");
|
cursor.callMethod<void>("close");
|
||||||
|
|
@ -74,55 +92,86 @@ Utils::Metadata getMetadataAndroid(const QString &path) {
|
||||||
Utils::Metadata meta;
|
Utils::Metadata meta;
|
||||||
meta.title = QFileInfo(path).fileName();
|
meta.title = QFileInfo(path).fileName();
|
||||||
QJniObject retriever("android/media/MediaMetadataRetriever");
|
QJniObject retriever("android/media/MediaMetadataRetriever");
|
||||||
if (!retriever.isValid()) return meta;
|
if (!retriever.isValid())
|
||||||
|
return meta;
|
||||||
|
|
||||||
QJniObject context = QNativeInterface::QAndroidApplication::context();
|
QJniObject context = QNativeInterface::QAndroidApplication::context();
|
||||||
QJniEnvironment env;
|
QJniEnvironment env;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (path.startsWith("content://")) {
|
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.
|
// FIX: Do NOT use QUrl::toEncoded() here. Android URIs are already
|
||||||
// Passing them through QUrl can double-encode characters (e.g. %20 becomes %2520).
|
// encoded strings. Passing them through QUrl can double-encode characters
|
||||||
QJniObject uri = QJniObject::callStaticObjectMethod("android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;", QJniObject::fromString(path).object<jstring>());
|
// (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()) {
|
if (pfd.isValid() && !env.checkAndClearExceptions()) {
|
||||||
QJniObject fd = pfd.callObjectMethod("getFileDescriptor", "()Ljava/io/FileDescriptor;");
|
QJniObject fd = pfd.callObjectMethod("getFileDescriptor",
|
||||||
|
"()Ljava/io/FileDescriptor;");
|
||||||
if (fd.isValid()) {
|
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");
|
pfd.callMethod<void>("close");
|
||||||
} else {
|
} 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 {
|
} 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 {
|
auto extract = [&](int key) -> QString {
|
||||||
QJniObject val = retriever.callObjectMethod("extractMetadata", "(I)Ljava/lang/String;", key);
|
QJniObject val = retriever.callObjectMethod("extractMetadata",
|
||||||
if (env.checkAndClearExceptions()) return QString();
|
"(I)Ljava/lang/String;", key);
|
||||||
|
if (env.checkAndClearExceptions())
|
||||||
|
return QString();
|
||||||
return val.isValid() ? val.toString() : QString();
|
return val.isValid() ? val.toString() : QString();
|
||||||
};
|
};
|
||||||
|
|
||||||
QString t = extract(7); if (!t.isEmpty()) meta.title = t;
|
QString t = extract(7);
|
||||||
QString a = extract(2); if (!a.isEmpty()) meta.artist = a;
|
if (!t.isEmpty())
|
||||||
QString al = extract(1); if (!al.isEmpty()) meta.album = al;
|
meta.title = t;
|
||||||
QString tr = extract(0); if (!tr.isEmpty()) meta.trackNumber = tr.split('/').first().toInt();
|
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");
|
QJniObject artObj = retriever.callObjectMethod("getEmbeddedPicture", "()[B");
|
||||||
if (!env.checkAndClearExceptions() && artObj.isValid()) {
|
if (!env.checkAndClearExceptions() && artObj.isValid()) {
|
||||||
jbyteArray jBa = artObj.object<jbyteArray>();
|
jbyteArray jBa = artObj.object<jbyteArray>();
|
||||||
if (jBa) {
|
if (jBa) {
|
||||||
int len = env->GetArrayLength(jBa);
|
int len = env->GetArrayLength(jBa);
|
||||||
QByteArray ba; ba.resize(len);
|
QByteArray ba;
|
||||||
env->GetByteArrayRegion(jBa, 0, len, reinterpret_cast<jbyte*>(ba.data()));
|
ba.resize(len);
|
||||||
|
env->GetByteArrayRegion(jBa, 0, len,
|
||||||
|
reinterpret_cast<jbyte *>(ba.data()));
|
||||||
meta.art.loadFromData(ba);
|
meta.art.loadFromData(ba);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -140,14 +189,19 @@ Utils::Metadata getMetadataIOS(const QString &path) {
|
||||||
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:nil];
|
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:nil];
|
||||||
NSArray<AVMetadataItem *> *metadata = [asset commonMetadata];
|
NSArray<AVMetadataItem *> *metadata = [asset commonMetadata];
|
||||||
for (AVMetadataItem *item in metadata) {
|
for (AVMetadataItem *item in metadata) {
|
||||||
if (item.value == nil) continue;
|
if (item.value == nil)
|
||||||
if ([item.commonKey isEqualToString:AVMetadataCommonKeyTitle]) meta.title = QString::fromNSString((NSString *)item.value);
|
continue;
|
||||||
else if ([item.commonKey isEqualToString:AVMetadataCommonKeyArtist]) meta.artist = QString::fromNSString((NSString *)item.value);
|
if ([item.commonKey isEqualToString:AVMetadataCommonKeyTitle])
|
||||||
else if ([item.commonKey isEqualToString:AVMetadataCommonKeyAlbumName]) meta.album = QString::fromNSString((NSString *)item.value);
|
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]) {
|
else if ([item.commonKey isEqualToString:AVMetadataCommonKeyArtwork]) {
|
||||||
if ([item.value isKindOfClass:[NSData class]]) {
|
if ([item.value isKindOfClass:[NSData class]]) {
|
||||||
NSData *data = (NSData *)item.value;
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -159,29 +213,40 @@ Utils::Metadata getMetadataIOS(const QString &path) {
|
||||||
@property(nonatomic, assign) bool isFolder;
|
@property(nonatomic, assign) bool isFolder;
|
||||||
@end
|
@end
|
||||||
@implementation FilePickerDelegate
|
@implementation FilePickerDelegate
|
||||||
- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls {
|
- (void)documentPicker:(UIDocumentPickerViewController *)controller
|
||||||
|
didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls {
|
||||||
if (urls.count > 0) {
|
if (urls.count > 0) {
|
||||||
NSURL *url = urls.firstObject;
|
NSURL *url = urls.firstObject;
|
||||||
if (self.isFolder) [url startAccessingSecurityScopedResource];
|
if (self.isFolder)
|
||||||
if (self.callback) self.callback(QString::fromNSString(url.absoluteString));
|
[url startAccessingSecurityScopedResource];
|
||||||
|
if (self.callback)
|
||||||
|
self.callback(QString::fromNSString(url.absoluteString));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
- (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller {}
|
- (void)documentPickerWasCancelled:
|
||||||
|
(UIDocumentPickerViewController *)controller {
|
||||||
|
}
|
||||||
@end
|
@end
|
||||||
static FilePickerDelegate *g_pickerDelegate = nil;
|
static FilePickerDelegate *g_pickerDelegate = nil;
|
||||||
|
|
||||||
namespace Utils {
|
namespace Utils {
|
||||||
void openIosPicker(bool folder, std::function<void(QString)> callback) {
|
void openIosPicker(bool folder, std::function<void(QString)> callback) {
|
||||||
if (!g_pickerDelegate) g_pickerDelegate = [[FilePickerDelegate alloc] init];
|
if (!g_pickerDelegate)
|
||||||
|
g_pickerDelegate = [[FilePickerDelegate alloc] init];
|
||||||
g_pickerDelegate.callback = callback;
|
g_pickerDelegate.callback = callback;
|
||||||
g_pickerDelegate.isFolder = folder;
|
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.delegate = g_pickerDelegate;
|
||||||
picker.allowsMultipleSelection = NO;
|
picker.allowsMultipleSelection = NO;
|
||||||
|
|
||||||
UIWindow *window = nil;
|
UIWindow *window = nil;
|
||||||
for (UIScene *scene in [UIApplication sharedApplication].connectedScenes) {
|
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) {
|
for (UIWindow *w in ((UIWindowScene *)scene).windows) {
|
||||||
if (w.isKeyWindow) {
|
if (w.isKeyWindow) {
|
||||||
window = w;
|
window = w;
|
||||||
|
|
@ -189,13 +254,15 @@ namespace Utils {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (window) break;
|
if (window)
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
UIViewController *root = window.rootViewController;
|
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
|
#endif
|
||||||
|
|
||||||
namespace Utils {
|
namespace Utils {
|
||||||
|
|
@ -205,16 +272,22 @@ void configureIOSAudioSession() {
|
||||||
NSError *error = nil;
|
NSError *error = nil;
|
||||||
AVAudioSession *session = [AVAudioSession sharedInstance];
|
AVAudioSession *session = [AVAudioSession sharedInstance];
|
||||||
[session setCategory:AVAudioSessionCategoryPlayback error:&error];
|
[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];
|
[session setActive:YES error:&error];
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
static QString getBinary(const QString &name) {
|
static QString getBinary(const QString &name) {
|
||||||
QString bin = QStandardPaths::findExecutable(name);
|
QString bin = QStandardPaths::findExecutable(name);
|
||||||
if (!bin.isEmpty()) return bin;
|
if (!bin.isEmpty())
|
||||||
QStringList paths = { "/opt/homebrew/bin/" + name, "/usr/local/bin/" + name, "/usr/bin/" + name, "/bin/" + name };
|
return bin;
|
||||||
for (const auto& p : paths) if (QFile::exists(p)) return p;
|
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;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -233,19 +306,24 @@ QString convertToWav(const QString &inputPath) {
|
||||||
return inputPath;
|
return inputPath;
|
||||||
#else
|
#else
|
||||||
QString wavPath = inputPath + ".temp.wav";
|
QString wavPath = inputPath + ".temp.wav";
|
||||||
if (QFile::exists(wavPath)) QFile::remove(wavPath);
|
if (QFile::exists(wavPath))
|
||||||
|
QFile::remove(wavPath);
|
||||||
QProcess p;
|
QProcess p;
|
||||||
p.start(getBinary("ffmpeg"), {"-y", "-v", "quiet", "-i", inputPath, "-vn", "-f", "wav", wavPath});
|
p.start(getBinary("ffmpeg"),
|
||||||
if (p.waitForFinished() && p.exitCode() == 0) return wavPath;
|
{"-y", "-v", "quiet", "-i", inputPath, "-vn", "-f", "wav", wavPath});
|
||||||
|
if (p.waitForFinished() && p.exitCode() == 0)
|
||||||
|
return wavPath;
|
||||||
return QString();
|
return QString();
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
QString resolvePath(const QString &rawPath) {
|
QString resolvePath(const QString &rawPath) {
|
||||||
if (rawPath.startsWith("content://")) return rawPath;
|
if (rawPath.startsWith("content://"))
|
||||||
|
return rawPath;
|
||||||
if (rawPath.startsWith("file://")) {
|
if (rawPath.startsWith("file://")) {
|
||||||
QUrl url(rawPath);
|
QUrl url(rawPath);
|
||||||
if (url.isLocalFile()) return url.toLocalFile();
|
if (url.isLocalFile())
|
||||||
|
return url.toLocalFile();
|
||||||
return QUrl::fromPercentEncoding(rawPath.toUtf8()).mid(7);
|
return QUrl::fromPercentEncoding(rawPath.toUtf8()).mid(7);
|
||||||
}
|
}
|
||||||
return rawPath;
|
return rawPath;
|
||||||
|
|
@ -265,27 +343,40 @@ Metadata getMetadata(const QString &filePath) {
|
||||||
QString ffprobe = getBinary("ffprobe");
|
QString ffprobe = getBinary("ffprobe");
|
||||||
QString ffmpeg = getBinary("ffmpeg");
|
QString ffmpeg = getBinary("ffmpeg");
|
||||||
QProcess p;
|
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()) {
|
if (p.waitForFinished()) {
|
||||||
QJsonDocument doc = QJsonDocument::fromJson(p.readAllStandardOutput());
|
QJsonDocument doc = QJsonDocument::fromJson(p.readAllStandardOutput());
|
||||||
QJsonObject tags = doc.object()["format"].toObject()["tags"].toObject();
|
QJsonObject tags = doc.object()["format"].toObject()["tags"].toObject();
|
||||||
if (tags.contains("title")) meta.title = tags["title"].toString();
|
if (tags.contains("title"))
|
||||||
if (tags.contains("artist")) meta.artist = tags["artist"].toString();
|
meta.title = tags["title"].toString();
|
||||||
if (tags.contains("album")) meta.album = tags["album"].toString();
|
if (tags.contains("artist"))
|
||||||
if (tags.contains("track")) meta.trackNumber = tags["track"].toString().split('/').first().toInt();
|
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()) {
|
if (!meta.album.isEmpty()) {
|
||||||
QMutexLocker locker(&g_cacheMutex);
|
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 {
|
else {
|
||||||
QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/covers";
|
QString cacheDir =
|
||||||
QString hash = QString(QCryptographicHash::hash(meta.album.toUtf8(), QCryptographicHash::Md5).toHex());
|
QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
|
||||||
if (QFile::exists(cacheDir + "/" + hash + ".png") && meta.art.load(cacheDir + "/" + hash + ".png")) g_artCache.insert(meta.album, meta.art);
|
"/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()) {
|
if (meta.art.isNull()) {
|
||||||
QProcess pArt;
|
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()) {
|
if (pArt.waitForFinished()) {
|
||||||
QByteArray data = pArt.readAllStandardOutput();
|
QByteArray data = pArt.readAllStandardOutput();
|
||||||
if (!data.isEmpty()) {
|
if (!data.isEmpty()) {
|
||||||
|
|
@ -293,23 +384,32 @@ Metadata getMetadata(const QString &filePath) {
|
||||||
if (!meta.album.isEmpty()) {
|
if (!meta.album.isEmpty()) {
|
||||||
QMutexLocker locker(&g_cacheMutex);
|
QMutexLocker locker(&g_cacheMutex);
|
||||||
g_artCache.insert(meta.album, meta.art);
|
g_artCache.insert(meta.album, meta.art);
|
||||||
QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/covers";
|
QString cacheDir =
|
||||||
|
QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
|
||||||
|
"/covers";
|
||||||
QDir().mkpath(cacheDir);
|
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");
|
meta.art.save(cacheDir + "/" + hash + ".png", "PNG");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#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;
|
return meta;
|
||||||
}
|
}
|
||||||
|
|
||||||
QVector<QColor> extractAlbumColors(const QImage &art, int numBins) {
|
QVector<QColor> extractAlbumColors(const QImage &art, int numBins) {
|
||||||
QVector<QColor> palette(numBins, QColor(127, 127, 127));
|
QVector<QColor> palette(numBins, QColor(127, 127, 127));
|
||||||
if (art.isNull()) return palette;
|
if (art.isNull())
|
||||||
QImage scaled = art.scaled(numBins, 20, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
return palette;
|
||||||
|
QImage scaled =
|
||||||
|
art.scaled(numBins, 20, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||||||
for (int x = 0; x < numBins; ++x) {
|
for (int x = 0; x < numBins; ++x) {
|
||||||
float maxVibrancy = -1.0f;
|
float maxVibrancy = -1.0f;
|
||||||
QColor bestColor = QColor(127, 127, 127);
|
QColor bestColor = QColor(127, 127, 127);
|
||||||
|
|
@ -317,7 +417,10 @@ QVector<QColor> extractAlbumColors(const QImage &art, int numBins) {
|
||||||
QColor c = scaled.pixelColor(x, y);
|
QColor c = scaled.pixelColor(x, y);
|
||||||
float s = c.hsvSaturationF();
|
float s = c.hsvSaturationF();
|
||||||
float v = c.valueF();
|
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;
|
palette[x] = bestColor;
|
||||||
}
|
}
|
||||||
|
|
@ -326,14 +429,21 @@ QVector<QColor> extractAlbumColors(const QImage &art, int numBins) {
|
||||||
|
|
||||||
bool isContentUriFolder(const QString &path) {
|
bool isContentUriFolder(const QString &path) {
|
||||||
#ifdef Q_OS_ANDROID
|
#ifdef Q_OS_ANDROID
|
||||||
if (!path.startsWith("content://")) return false;
|
if (!path.startsWith("content://"))
|
||||||
|
return false;
|
||||||
QJniEnvironment env;
|
QJniEnvironment env;
|
||||||
QJniObject uri = QJniObject::callStaticObjectMethod("android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;", QJniObject::fromString(path).object<jstring>());
|
QJniObject uri = QJniObject::callStaticObjectMethod(
|
||||||
if (!uri.isValid()) return false;
|
"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 context = QNativeInterface::QAndroidApplication::context();
|
||||||
QJniObject contentResolver = context.callObjectMethod("getContentResolver", "()Landroid/content/ContentResolver;");
|
QJniObject contentResolver = context.callObjectMethod(
|
||||||
QJniObject type = contentResolver.callObjectMethod("getType", "(Landroid/net/Uri;)Ljava/lang/String;", uri.object());
|
"getContentResolver", "()Landroid/content/ContentResolver;");
|
||||||
if (env.checkAndClearExceptions() || !type.isValid()) return false;
|
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";
|
return type.toString() == "vnd.android.document/directory";
|
||||||
#else
|
#else
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -345,33 +455,47 @@ QStringList scanDirectory(const QString &path, bool recursive) {
|
||||||
if (path.startsWith("content://")) {
|
if (path.startsWith("content://")) {
|
||||||
QStringList results;
|
QStringList results;
|
||||||
QJniEnvironment env;
|
QJniEnvironment env;
|
||||||
QJniObject uri = QJniObject::callStaticObjectMethod("android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;", QJniObject::fromString(path).object<jstring>());
|
QJniObject uri = QJniObject::callStaticObjectMethod(
|
||||||
if (!uri.isValid()) return results;
|
"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 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)
|
// Try to persist permission, but don't fail if we can't (transient might be
|
||||||
contentResolver.callMethod<void>("takePersistableUriPermission", "(Landroid/net/Uri;I)V", uri.object(), 1);
|
// enough for now)
|
||||||
// FIX: Suppress the SecurityException warning if it fails, as it's not critical for immediate playback
|
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();
|
env.checkAndClearExceptions();
|
||||||
|
|
||||||
QJniObject docId = QJniObject::callStaticObjectMethod("android/provider/DocumentsContract", "getTreeDocumentId", "(Landroid/net/Uri;)Ljava/lang/String;", uri.object());
|
QJniObject docId = QJniObject::callStaticObjectMethod(
|
||||||
if (env.checkAndClearExceptions() || !docId.isValid()) return results;
|
"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);
|
scanAndroidTree(context, uri, docId, results, recursive);
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
QStringList files;
|
QStringList files;
|
||||||
QStringList filters = {"*.mp3", "*.m4a", "*.wav", "*.flac", "*.ogg", "*.aif*", "*.aac"};
|
QStringList filters = {"*.mp3", "*.m4a", "*.wav", "*.flac",
|
||||||
QDirIterator::IteratorFlag flag = recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags;
|
"*.ogg", "*.aif*", "*.aac"};
|
||||||
|
QDirIterator::IteratorFlag flag =
|
||||||
|
recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags;
|
||||||
QDirIterator it(path, filters, QDir::Files, flag);
|
QDirIterator it(path, filters, QDir::Files, flag);
|
||||||
while (it.hasNext()) files << it.next();
|
while (it.hasNext())
|
||||||
|
files << it.next();
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Permission Helper Implementation ---
|
// --- 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) {
|
: QObject(parent), m_callback(cb) {
|
||||||
m_timer = new QTimer(this);
|
m_timer = new QTimer(this);
|
||||||
m_timer->setInterval(500); // Check every 500ms
|
m_timer->setInterval(500); // Check every 500ms
|
||||||
|
|
@ -381,10 +505,15 @@ PermissionHelper::PermissionHelper(std::function<void(bool)> cb, QObject* parent
|
||||||
void PermissionHelper::start() {
|
void PermissionHelper::start() {
|
||||||
#ifdef Q_OS_ANDROID
|
#ifdef Q_OS_ANDROID
|
||||||
QJniObject activity = QNativeInterface::QAndroidApplication::context();
|
QJniObject activity = QNativeInterface::QAndroidApplication::context();
|
||||||
jint sdkInt = QJniObject::getStaticField<jint>("android/os/Build$VERSION", "SDK_INT");
|
jint sdkInt =
|
||||||
QString permission = (sdkInt >= 33) ? "android.permission.READ_MEDIA_AUDIO" : "android.permission.READ_EXTERNAL_STORAGE";
|
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) {
|
if (result == 0) {
|
||||||
m_callback(true);
|
m_callback(true);
|
||||||
|
|
@ -393,12 +522,18 @@ void PermissionHelper::start() {
|
||||||
// Request permission
|
// Request permission
|
||||||
QJniEnvironment env;
|
QJniEnvironment env;
|
||||||
jclass stringClass = env.findClass("java/lang/String");
|
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
|
// 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
|
// Start polling
|
||||||
m_timer->start();
|
m_timer->start();
|
||||||
|
|
@ -412,10 +547,15 @@ void PermissionHelper::start() {
|
||||||
void PermissionHelper::check() {
|
void PermissionHelper::check() {
|
||||||
#ifdef Q_OS_ANDROID
|
#ifdef Q_OS_ANDROID
|
||||||
QJniObject activity = QNativeInterface::QAndroidApplication::context();
|
QJniObject activity = QNativeInterface::QAndroidApplication::context();
|
||||||
jint sdkInt = QJniObject::getStaticField<jint>("android/os/Build$VERSION", "SDK_INT");
|
jint sdkInt =
|
||||||
QString permission = (sdkInt >= 33) ? "android.permission.READ_MEDIA_AUDIO" : "android.permission.READ_EXTERNAL_STORAGE";
|
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) {
|
if (result == 0) {
|
||||||
m_timer->stop();
|
m_timer->stop();
|
||||||
|
|
@ -443,13 +583,19 @@ bool copyContentUriToLocalFile(const QString& uriStr, const QString& destPath) {
|
||||||
#ifdef Q_OS_ANDROID
|
#ifdef Q_OS_ANDROID
|
||||||
QJniEnvironment env;
|
QJniEnvironment env;
|
||||||
QJniObject context = QNativeInterface::QAndroidApplication::context();
|
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.
|
// FIX: Do NOT use QUrl::toEncoded() here. Android URIs are already encoded
|
||||||
// Passing them through QUrl can double-encode characters (e.g. %20 becomes %2520).
|
// strings. Passing them through QUrl can double-encode characters (e.g. %20
|
||||||
QJniObject uri = QJniObject::callStaticObjectMethod("android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;", QJniObject::fromString(uriStr).object<jstring>());
|
// 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()) {
|
if (!inputStream.isValid() || env.checkAndClearExceptions()) {
|
||||||
qWarning() << "Failed to open input stream for URI:" << uriStr;
|
qWarning() << "Failed to open input stream for URI:" << uriStr;
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -463,17 +609,20 @@ bool copyContentUriToLocalFile(const QString& uriStr, const QString& destPath) {
|
||||||
}
|
}
|
||||||
|
|
||||||
jbyteArray buffer = env->NewByteArray(8192);
|
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;
|
bool success = true;
|
||||||
while (true) {
|
while (true) {
|
||||||
jint bytesRead = env->CallIntMethod(inputStream.object(), readMethod, buffer);
|
jint bytesRead =
|
||||||
|
env->CallIntMethod(inputStream.object(), readMethod, buffer);
|
||||||
if (env.checkAndClearExceptions()) {
|
if (env.checkAndClearExceptions()) {
|
||||||
qWarning() << "Exception during read from content URI";
|
qWarning() << "Exception during read from content URI";
|
||||||
success = false;
|
success = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (bytesRead == -1) break;
|
if (bytesRead == -1)
|
||||||
|
break;
|
||||||
|
|
||||||
jbyte *bytes = env->GetByteArrayElements(buffer, nullptr);
|
jbyte *bytes = env->GetByteArrayElements(buffer, nullptr);
|
||||||
dest.write(reinterpret_cast<const char *>(bytes), bytesRead);
|
dest.write(reinterpret_cast<const char *>(bytes), bytesRead);
|
||||||
|
|
@ -493,7 +642,8 @@ MetadataLoader::MetadataLoader(QObject* parent) : QObject(parent) {}
|
||||||
void MetadataLoader::startLoading(const QStringList &paths) {
|
void MetadataLoader::startLoading(const QStringList &paths) {
|
||||||
m_stop = false;
|
m_stop = false;
|
||||||
for (int i = 0; i < paths.size(); ++i) {
|
for (int i = 0; i < paths.size(); ++i) {
|
||||||
if (m_stop) break;
|
if (m_stop)
|
||||||
|
break;
|
||||||
Metadata meta = getMetadata(paths[i]);
|
Metadata meta = getMetadata(paths[i]);
|
||||||
emit metadataReady(i, meta);
|
emit metadataReady(i, meta);
|
||||||
}
|
}
|
||||||
|
|
@ -501,4 +651,35 @@ void MetadataLoader::startLoading(const QStringList& paths) {
|
||||||
}
|
}
|
||||||
void MetadataLoader::stop() { m_stop = true; }
|
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
|
||||||
23
src/Utils.h
23
src/Utils.h
|
|
@ -1,13 +1,13 @@
|
||||||
// src/Utils.h
|
// src/Utils.h
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <QString>
|
|
||||||
#include <QImage>
|
|
||||||
#include <QPixmap>
|
|
||||||
#include <QVector>
|
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QStringList>
|
#include <QImage>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
#include <QPixmap>
|
||||||
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
#include <QVector>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
|
|
@ -40,6 +40,12 @@ namespace Utils {
|
||||||
// Helper to robustly copy content URIs on Android
|
// Helper to robustly copy content URIs on Android
|
||||||
bool copyContentUriToLocalFile(const QString &uriStr, const QString &destPath);
|
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
|
#ifdef Q_OS_IOS
|
||||||
void openIosPicker(bool folder, std::function<void(QString)> callback);
|
void openIosPicker(bool folder, std::function<void(QString)> callback);
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -53,6 +59,7 @@ namespace Utils {
|
||||||
signals:
|
signals:
|
||||||
void metadataReady(int index, const Utils::Metadata &meta);
|
void metadataReady(int index, const Utils::Metadata &meta);
|
||||||
void finished();
|
void finished();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::atomic<bool> m_stop{false};
|
std::atomic<bool> m_stop{false};
|
||||||
};
|
};
|
||||||
|
|
@ -61,13 +68,15 @@ namespace Utils {
|
||||||
class PermissionHelper : public QObject {
|
class PermissionHelper : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit PermissionHelper(std::function<void(bool)> cb, QObject* parent = nullptr);
|
explicit PermissionHelper(std::function<void(bool)> cb,
|
||||||
|
QObject *parent = nullptr);
|
||||||
void start();
|
void start();
|
||||||
private slots:
|
private slots:
|
||||||
void check();
|
void check();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::function<void(bool)> m_callback;
|
std::function<void(bool)> m_callback;
|
||||||
QTimer *m_timer;
|
QTimer *m_timer;
|
||||||
int m_attempts = 0;
|
int m_attempts = 0;
|
||||||
};
|
};
|
||||||
}
|
} // namespace Utils
|
||||||
|
|
@ -16,6 +16,15 @@
|
||||||
VisualizerWidget::VisualizerWidget(QWidget *parent) : QWidget(parent) {
|
VisualizerWidget::VisualizerWidget(QWidget *parent) : QWidget(parent) {
|
||||||
setAttribute(Qt::WA_OpaquePaintEvent);
|
setAttribute(Qt::WA_OpaquePaintEvent);
|
||||||
setNumBins(26);
|
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) {
|
void VisualizerWidget::mouseReleaseEvent(QMouseEvent *event) {
|
||||||
|
|
@ -310,7 +319,13 @@ void VisualizerWidget::paintEvent(QPaintEvent *) {
|
||||||
|
|
||||||
QPainter p(this);
|
QPainter p(this);
|
||||||
p.fillRect(rect(), Qt::black);
|
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);
|
p.setRenderHint(QPainter::Antialiasing);
|
||||||
|
#endif
|
||||||
|
|
||||||
if (m_data.empty())
|
if (m_data.empty())
|
||||||
return;
|
return;
|
||||||
|
|
@ -334,7 +349,9 @@ void VisualizerWidget::paintEvent(QPaintEvent *) {
|
||||||
{
|
{
|
||||||
m_cache.fill(Qt::transparent); // Clear old frame
|
m_cache.fill(Qt::transparent); // Clear old frame
|
||||||
QPainter cachePainter(&m_cache);
|
QPainter cachePainter(&m_cache);
|
||||||
|
#if !defined(Q_OS_IOS)
|
||||||
cachePainter.setRenderHint(QPainter::Antialiasing);
|
cachePainter.setRenderHint(QPainter::Antialiasing);
|
||||||
|
#endif
|
||||||
drawContent(cachePainter, hw, hh);
|
drawContent(cachePainter, hw, hh);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue