fix submodule, performance+, win build scripts

This commit is contained in:
pszsh 2026-01-29 22:36:21 -08:00
parent e2e388eccf
commit fa4257e999
4 changed files with 276 additions and 309 deletions

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "libraries/loop-tempo-estimator"]
path = libraries/loop-tempo-estimator
url = https://github.com/saintmatthieu/loop-tempo-estimator.git

View File

@ -28,10 +28,8 @@
#include <QJniEnvironment>
#include <QtCore/qnativeinterface.h>
// Helper to scan Android Content URIs (Tree)
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;
@ -45,38 +43,28 @@ void scanAndroidTree(const QJniObject& context, const QJniObject& treeUri, const
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
childrenUri.object(), nullptr, nullptr, nullptr, nullptr
);
if (env.checkAndClearExceptions()) {
return;
}
if (!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>());
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 (mime == "vnd.android.document/directory") {
if (recursive) {
scanAndroidTree(context, treeUri, QJniObject::fromString(docId), results, true);
}
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();
}
if (fileUri.isValid()) results << fileUri.toString();
}
}
cursor.callMethod<void>("close");
@ -85,8 +73,7 @@ void scanAndroidTree(const QJniObject& context, const QJniObject& treeUri, const
Utils::Metadata getMetadataAndroid(const QString &path) {
Utils::Metadata meta;
meta.title = QFileInfo(path).fileName(); // Fallback
meta.title = QFileInfo(path).fileName();
QJniObject retriever("android/media/MediaMetadataRetriever");
if (!retriever.isValid()) return meta;
@ -95,71 +82,36 @@ Utils::Metadata getMetadataAndroid(const QString &path) {
try {
if (path.startsWith("content://")) {
QJniObject uri = QJniObject::callStaticObjectMethod(
"android/net/Uri",
"parse",
"(Ljava/lang/String;)Landroid/net/Uri;",
QJniObject::fromString(path).object<jstring>()
);
retriever.callMethod<void>(
"setDataSource",
"(Landroid/content/Context;Landroid/net/Uri;)V",
context.object(),
uri.object()
);
QJniObject uri = QJniObject::callStaticObjectMethod("android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;", QJniObject::fromString(path).object<jstring>());
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;
auto extract = [&](int key) -> QString {
QJniObject val = retriever.callObjectMethod(
"extractMetadata",
"(I)Ljava/lang/String;",
key
);
QJniObject val = retriever.callObjectMethod("extractMetadata", "(I)Ljava/lang/String;", key);
if (env.checkAndClearExceptions()) return QString();
return val.isValid() ? val.toString() : QString();
};
// METADATA_KEY_TITLE = 7
QString t = extract(7);
if (!t.isEmpty()) meta.title = t;
// METADATA_KEY_ARTIST = 2
QString a = extract(2);
if (!a.isEmpty()) meta.artist = a;
// METADATA_KEY_ALBUM = 1
QString al = extract(1);
if (!al.isEmpty()) meta.album = al;
// METADATA_KEY_CD_TRACK_NUMBER = 0
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);
QByteArray ba; ba.resize(len);
env->GetByteArrayRegion(jBa, 0, len, reinterpret_cast<jbyte*>(ba.data()));
meta.art.loadFromData(ba);
}
}
retriever.callMethod<void>("release");
env.checkAndClearExceptions();
return meta;
@ -167,26 +119,18 @@ Utils::Metadata getMetadataAndroid(const QString &path) {
#endif
#ifdef Q_OS_IOS
// Native iOS Metadata Extraction
Utils::Metadata getMetadataIOS(const QString &path) {
Utils::Metadata meta;
meta.title = QFileInfo(path).fileName();
NSURL *url = [NSURL fileURLWithPath:path.toNSString()];
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);
} else if ([item.commonKey isEqualToString:AVMetadataCommonKeyArtwork]) {
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));
@ -196,67 +140,32 @@ Utils::Metadata getMetadataIOS(const QString &path) {
return meta;
}
// Native iOS File Picker Delegate
@interface FilePickerDelegate : NSObject <UIDocumentPickerDelegate>
@property (nonatomic, assign) std::function<void(QString)> callback;
@property (nonatomic, assign) bool isFolder;
@end
@implementation FilePickerDelegate
- (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;
namespace Utils {
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.isFolder = folder;
UIDocumentPickerViewController *picker = nil;
if (folder) {
picker = [[UIDocumentPickerViewController alloc] initForOpeningContentTypes:@[UTTypeFolder] asCopy:NO];
} else {
picker = [[UIDocumentPickerViewController alloc] initForOpeningContentTypes:@[UTTypeAudio] asCopy:YES];
}
UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc] initForOpeningContentTypes:folder ? @[UTTypeFolder] : @[UTTypeAudio] asCopy:!folder];
picker.delegate = g_pickerDelegate;
picker.allowsMultipleSelection = NO;
UIViewController *root = nil;
for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) {
if (scene.activationState == UISceneActivationStateForegroundActive && [scene isKindOfClass:[UIWindowScene class]]) {
root = ((UIWindowScene *)scene).windows.firstObject.rootViewController;
if (root) break;
}
}
if (!root) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
root = [UIApplication sharedApplication].keyWindow.rootViewController;
#pragma clang diagnostic pop
}
if (root) {
[root presentViewController:picker animated:YES completion:nil];
}
UIViewController *root = [UIApplication sharedApplication].keyWindow.rootViewController;
if (root) [root presentViewController:picker animated:YES completion:nil];
}
}
#endif
@ -267,11 +176,8 @@ void configureIOSAudioSession() {
#ifdef Q_OS_IOS
NSError *error = nil;
AVAudioSession *session = [AVAudioSession sharedInstance];
// Critical for background audio playback
[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
}
@ -279,16 +185,8 @@ void configureIOSAudioSession() {
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;
}
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;
}
@ -308,87 +206,55 @@ QString convertToWav(const QString &inputPath) {
#else
QString wavPath = inputPath + ".temp.wav";
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;
}
if (p.waitForFinished() && p.exitCode() == 0) return wavPath;
return QString();
#endif
}
QString resolvePath(const QString& rawPath) {
if (rawPath.startsWith("content://")) return rawPath;
if (rawPath.startsWith("file://")) {
QUrl url(rawPath);
if (url.isLocalFile()) return url.toLocalFile();
// Fallback for malformed file:// URIs
return QUrl::fromPercentEncoding(rawPath.toUtf8()).mid(7);
}
// It's a local path, return as is (spaces are fine in QString paths)
return rawPath;
}
// Global Runtime Cache for Album Art
static QMap<QString, QImage> g_artCache;
static QMutex g_cacheMutex;
Metadata getMetadata(const QString &filePath) {
Metadata meta;
#ifdef Q_OS_ANDROID
meta = getMetadataAndroid(filePath);
#elif defined(Q_OS_IOS)
meta = getMetadataIOS(filePath);
#else
meta.title = QFileInfo(filePath).fileName();
QString ffprobe = getBinary("ffprobe");
QString ffmpeg = getBinary("ffmpeg");
// 1. Get Tags (Fast)
QProcess p;
p.start(ffprobe, {"-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", filePath});
if (p.waitForFinished()) {
QJsonDocument doc = QJsonDocument::fromJson(p.readAllStandardOutput());
QJsonObject root = doc.object();
QJsonObject format = root["format"].toObject();
QJsonObject tags = format["tags"].toObject();
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("track")) meta.trackNumber = tags["track"].toString().split('/').first().toInt();
}
// 2. Check Caches for Album Art
if (!meta.album.isEmpty()) {
QMutexLocker locker(&g_cacheMutex);
// Runtime Cache
if (g_artCache.contains(meta.album)) {
meta.art = g_artCache[meta.album];
} else {
// Disk Cache
if (g_artCache.contains(meta.album)) meta.art = g_artCache[meta.album];
else {
QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/covers";
QDir().mkpath(cacheDir);
QString hash = QString(QCryptographicHash::hash(meta.album.toUtf8(), QCryptographicHash::Md5).toHex());
QString cachePath = cacheDir + "/" + hash + ".png";
if (QFile::exists(cachePath)) {
if (meta.art.load(cachePath)) {
g_artCache.insert(meta.album, meta.art);
}
}
if (QFile::exists(cacheDir + "/" + hash + ".png") && meta.art.load(cacheDir + "/" + hash + ".png")) g_artCache.insert(meta.album, meta.art);
}
}
// 3. Extract Art (Slow) if not found
if (meta.art.isNull()) {
QProcess pArt;
pArt.start(ffmpeg, {"-y", "-v", "quiet", "-i", filePath, "-an", "-vcodec", "png", "-f", "image2pipe", "-"});
@ -396,13 +262,11 @@ Metadata getMetadata(const QString &filePath) {
QByteArray data = pArt.readAllStandardOutput();
if (!data.isEmpty()) {
meta.art.loadFromData(data);
// Update Caches
if (!meta.album.isEmpty() && !meta.art.isNull()) {
if (!meta.album.isEmpty()) {
QMutexLocker locker(&g_cacheMutex);
g_artCache.insert(meta.album, meta.art);
QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/covers";
QDir().mkpath(cacheDir);
QString hash = QString(QCryptographicHash::hash(meta.album.toUtf8(), QCryptographicHash::Md5).toHex());
meta.art.save(cacheDir + "/" + hash + ".png", "PNG");
}
@ -410,19 +274,13 @@ Metadata getMetadata(const QString &filePath) {
}
}
#endif
// Generate Thumbnail for Playlist Performance
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);
for (int x = 0; x < numBins; ++x) {
float maxVibrancy = -1.0f;
@ -431,10 +289,7 @@ 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;
}
@ -445,76 +300,35 @@ bool isContentUriFolder(const QString& path) {
#ifdef Q_OS_ANDROID
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>()
);
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()) return false;
if (!type.isValid()) return false;
QString mime = type.toString();
return mime == "vnd.android.document/directory";
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;
#endif
}
QStringList scanDirectory(const QString &path, bool recursive) {
#ifdef Q_OS_ANDROID
#ifdef Q_OS_ANDROID
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>()
);
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;");
contentResolver.callMethod<void>(
"takePersistableUriPermission",
"(Landroid/net/Uri;I)V",
uri.object(),
1
);
if (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 parentDocUri = QJniObject::callStaticObjectMethod(
"android/provider/DocumentsContract", "buildDocumentUriUsingTree",
"(Landroid/net/Uri;Ljava/lang/String;)Landroid/net/Uri;",
uri.object(), docId.object()
);
if (env.checkAndClearExceptions() || !parentDocUri.isValid()) {
return results;
}
contentResolver.callMethod<void>("takePersistableUriPermission", "(Landroid/net/Uri;I)V", uri.object(), 1);
if (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;
scanAndroidTree(context, uri, docId, results, recursive);
return results;
}
#endif
#endif
QStringList files;
QStringList filters = {"*.mp3", "*.m4a", "*.wav", "*.flac", "*.ogg", "*.aif*", "*.aac"};
QDirIterator::IteratorFlag flag = recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags;
@ -527,32 +341,23 @@ void requestAndroidPermissions(std::function<void(bool)> callback) {
#ifdef Q_OS_ANDROID
QJniObject activity = QNativeInterface::QAndroidApplication::context();
// Check SDK version
QJniObject version = QJniObject::getStaticField<QJniObject>("android/os/Build$VERSION", "SDK_INT", "I");
int sdkInt = version.callMethod<jint>("intValue");
// FIX: Retrieve SDK_INT as a primitive jint, not a QJniObject
jint sdkInt = QJniObject::getStaticField<jint>("android/os/Build$VERSION", "SDK_INT");
QString permission;
if (sdkInt >= 33) { // Android 13+
permission = "android.permission.READ_MEDIA_AUDIO";
} else {
permission = "android.permission.READ_EXTERNAL_STORAGE";
}
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) callback(true);
else {
// FIX: Use QJniEnvironment to find the class, as QJniObject::findClass does not exist
QJniEnvironment env;
jclass stringClass = env.findClass("java/lang/String");
if (result == 0) { // PERMISSION_GRANTED
callback(true);
} else {
// Request permission
QJniObject permissionsArray = QJniObject::callStaticObjectMethod(
"java/lang/reflect/Array",
"newInstance",
"(Ljava/lang/Class;I)Ljava/lang/Object;",
QJniObject::findClass("java/lang/String"),
stringClass,
1
);
@ -565,16 +370,7 @@ void requestAndroidPermissions(std::function<void(bool)> callback) {
QJniObject::fromString(permission).object<jstring>()
);
activity.callMethod<void>(
"requestPermissions",
"([Ljava/lang/String;I)V",
permissionsArray.object(),
101 // Request Code
);
// We can't easily wait for the callback here without a native listener.
// Return false to indicate permission was not immediately available.
// The system dialog will show up. User has to click "Open" again.
activity.callMethod<void>("requestPermissions", "([Ljava/lang/String;I)V", permissionsArray.object(), 101);
callback(false);
}
#else
@ -582,10 +378,7 @@ void requestAndroidPermissions(std::function<void(bool)> callback) {
#endif
}
// --- MetadataLoader Implementation ---
MetadataLoader::MetadataLoader(QObject* parent) : QObject(parent) {}
void MetadataLoader::startLoading(const QStringList& paths) {
m_stop = false;
for (int i = 0; i < paths.size(); ++i) {
@ -595,9 +388,6 @@ void MetadataLoader::startLoading(const QStringList& paths) {
}
emit finished();
}
void MetadataLoader::stop() {
m_stop = true;
}
void MetadataLoader::stop() { m_stop = true; }
}

View File

@ -32,7 +32,22 @@ set "VCPKG_EXE=!VS_INSTALL_DIR!\VC\vcpkg\vcpkg.exe"
set "VCPKG_CMAKE=!VS_INSTALL_DIR!\VC\vcpkg\scripts\buildsystems\vcpkg.cmake"
:: ==============================================================================
:: 1. CHECK VCPKG (Crucial Step Restored)
:: 0. MANIFEST GENERATION
:: ==============================================================================
echo [INFO] Generating vcpkg.json...
(
echo {
echo "name": "yrcrystals",
echo "version-string": "1.0.0",
echo "builtin-baseline": "b1b19307e2d2ec1eefbdb7ea069de7d4bcd31f01",
echo "dependencies": [
echo "fftw3"
echo ]
echo }
) > "%PROJECT_ROOT%\vcpkg.json"
:: ==============================================================================
:: 1. CHECK VCPKG
:: ==============================================================================
echo [INFO] Verifying dependencies...
cd "%PROJECT_ROOT%"

View File

@ -2,64 +2,223 @@
setlocal enabledelayedexpansion
:: ==============================================================================
:: CONFIGURATION
:: PATHS
:: ==============================================================================
set "BUILD_DIR=..\build_windows\x64"
set "QT_PATH=C:\Qt\6.8.3\msvc2022_64"
set "SCRIPT_DIR=%~dp0"
set "PROJECT_ROOT=%SCRIPT_DIR%.."
set "BUILD_DIR=%PROJECT_ROOT%\build_windows\x64"
set "QT_X64_BIN=C:\Qt\6.8.3\msvc2022_64\bin"
set "QT_X64_PLUGINS=C:\Qt\6.8.3\msvc2022_64\plugins"
set "VCPKG_BIN=%PROJECT_ROOT%\vcpkg_installed\x64-windows\bin"
:: ==============================================================================
:: AUTO-DETECT VISUAL STUDIO
:: ENVIRONMENT SETUP (VS Discovery)
:: ==============================================================================
set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe"
if not exist "!VSWHERE!" set "VSWHERE=%ProgramFiles%\Microsoft Visual Studio\Installer\vswhere.exe"
if exist "!VSWHERE!" (
for /f "usebackq tokens=*" %%i in (`"!VSWHERE!" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do (
set "VS_INSTALL_DIR=%%i"
if defined VSINSTALLDIR (
set "VS_INSTALL_DIR=!VSINSTALLDIR!"
) else (
if exist "C:\Program Files\Microsoft Visual Studio\2022\Preview\VC\Auxiliary\Build\vcvarsall.bat" (
set "VS_INSTALL_DIR=C:\Program Files\Microsoft Visual Studio\2022\Preview"
) else if exist "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" (
set "VS_INSTALL_DIR=C:\Program Files\Microsoft Visual Studio\2022\Enterprise"
) else if exist "C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" (
set "VS_INSTALL_DIR=C:\Program Files\Microsoft Visual Studio\18\Enterprise"
)
)
if not defined VS_INSTALL_DIR (
echo [ERROR] Could not find Visual Studio.
pause
exit /b 1
)
if "!VS_INSTALL_DIR:~-1!"=="\" set "VS_INSTALL_DIR=!VS_INSTALL_DIR:~0,-1!"
set "VCVARSALL=!VS_INSTALL_DIR!\VC\Auxiliary\Build\vcvarsall.bat"
set "VCPKG_EXE=!VS_INSTALL_DIR!\VC\vcpkg\vcpkg.exe"
set "VCPKG_CMAKE=!VS_INSTALL_DIR!\VC\vcpkg\scripts\buildsystems\vcpkg.cmake"
echo [INFO] Found Visual Studio at: !VS_INSTALL_DIR!
:: ==============================================================================
:: COMPILER SETUP (Cross-Compile)
:: 0. DYNAMIC BASELINE DISCOVERY
:: ==============================================================================
if /i "%PROCESSOR_ARCHITECTURE%"=="ARM64" (
echo [INFO] Host is ARM64. Using ARM64_x64 cross-compiler.
set "VCVARS_ARCH=arm64_x64"
) else (
echo [INFO] Host is x64. Using Native x64 compiler.
set "VCVARS_ARCH=x64"
echo [INFO] Probing for latest vcpkg baseline...
:: 1. Create temporary manifest with empty baseline to provoke the "suggestion" error
(
echo {
echo "name": "yrcrystals",
echo "version-string": "1.0.0",
echo "dependencies": [ "fftw3" ],
echo "builtin-baseline": ""
echo }
) > "%PROJECT_ROOT%\vcpkg.json"
:: 2. Run dry-run and capture output (stderr included)
"!VCPKG_EXE!" install --triplet x64-windows --dry-run > "%PROJECT_ROOT%\vcpkg_probe.log" 2>&1
:: 3. Extract the hash using PowerShell regex
:: Matches pattern: "builtin-baseline": "HEX_HASH"
set "DETECTED_HASH="
for /f "usebackq tokens=*" %%H in (`powershell -NoProfile -Command "$content = Get-Content '%PROJECT_ROOT%\vcpkg_probe.log' -Raw; if ($content -match 'builtin-baseline.: .([a-f0-9]+).') { $matches[1] }"`) do (
set "DETECTED_HASH=%%H"
)
call "!VCVARSALL!" !VCVARS_ARCH!
if "!DETECTED_HASH!"=="" (
echo [WARNING] Could not auto-detect baseline. Defaulting to standard hash.
set "DETECTED_HASH=b1b19307e2d2ec1eefbdb7ea069de7d4bcd31f01"
) else (
echo [INFO] Detected Baseline: !DETECTED_HASH!
)
:: Clean up log
del "%PROJECT_ROOT%\vcpkg_probe.log" 2>nul
:: 4. Generate Final Manifest with discovered hash
echo [INFO] Writing vcpkg.json...
(
echo {
echo "name": "yrcrystals",
echo "version-string": "1.0.0",
echo "builtin-baseline": "!DETECTED_HASH!",
echo "dependencies": [
echo "fftw3"
echo ]
echo }
) > "%PROJECT_ROOT%\vcpkg.json"
:: ==============================================================================
:: BUILD
:: 1. INSTALL DEPENDENCIES (Targeting x64-windows)
:: ==============================================================================
echo [INFO] Verifying dependencies for x64...
cd "%PROJECT_ROOT%"
"!VCPKG_EXE!" install --triplet x64-windows
if %errorlevel% neq 0 (
echo [ERROR] VCPKG install failed.
pause
exit /b %errorlevel%
)
:: ==============================================================================
:: 2. AUTO-DETECT MAGICK
:: ==============================================================================
set "MAGICK_PATH="
where magick >nul 2>nul
if %errorlevel% equ 0 (
for /f "delims=" %%i in ('where magick') do set "MAGICK_PATH=%%i"
echo [INFO] Found Magick in PATH.
)
if not defined MAGICK_PATH (
for /d %%d in ("C:\Program Files\ImageMagick*") do (
if exist "%%d\magick.exe" set "MAGICK_PATH=%%d\magick.exe"
)
)
if defined MAGICK_PATH (
echo [INFO] Using ImageMagick: !MAGICK_PATH!
set "CMAKE_MAGICK_ARG=-DMAGICK_EXECUTABLE="!MAGICK_PATH!""
) else (
echo [WARNING] ImageMagick not found. Icons will be skipped.
set "CMAKE_MAGICK_ARG="
)
:: ==============================================================================
:: 3. CONFIGURE & BUILD
:: ==============================================================================
echo [INFO] Setting up Environment for x64 Build...
:: CRITICAL: arm64_x64 means "Use ARM64 host to build x64 binaries"
call "!VCVARSALL!" arm64_x64
if not exist "%BUILD_DIR%" mkdir "%BUILD_DIR%"
cd "%BUILD_DIR%"
echo [INFO] Configuring for x64...
cmake -G "Ninja" ^
-DCMAKE_BUILD_TYPE=Release ^
-DCMAKE_PREFIX_PATH="%QT_PATH%" ^
-DCMAKE_TOOLCHAIN_FILE="!VCPKG_CMAKE!" ^
..\..
set "NEED_CONFIG=0"
if not exist "build.ninja" set "NEED_CONFIG=1"
if not exist "CMakeCache.txt" set "NEED_CONFIG=1"
if %errorlevel% neq 0 pause && exit /b %errorlevel%
if "!NEED_CONFIG!"=="1" (
echo [INFO] Build configuration missing. Running CMake Configure...
cmake -G "Ninja" ^
-DCMAKE_BUILD_TYPE=Release ^
-DCMAKE_PREFIX_PATH="%QT_X64_BIN%\..;%PROJECT_ROOT%\vcpkg_installed\x64-windows" ^
-DCMAKE_TOOLCHAIN_FILE="!VCPKG_CMAKE!" ^
-DVCPKG_TARGET_TRIPLET=x64-windows ^
!CMAKE_MAGICK_ARG! ^
"%PROJECT_ROOT%"
if !errorlevel! neq 0 (
echo [ERROR] Configuration Failed.
pause
exit /b !errorlevel!
)
)
echo [INFO] Building...
cmake --build .
if %errorlevel% neq 0 (
echo [ERROR] Build Failed.
pause
exit /b %errorlevel%
)
:: ==============================================================================
:: 4. THE NUKE (Clean Slate)
:: ==============================================================================
echo.
echo [SUCCESS] x64 Build located in build_windows\x64
echo [CLEAN] Removing old DLLs and Plugins...
del /f /q *.dll 2>nul
if exist "platforms" rmdir /s /q "platforms"
if exist "styles" rmdir /s /q "styles"
if exist "multimedia" rmdir /s /q "multimedia"
if exist "audio" rmdir /s /q "audio"
if exist "imageformats" rmdir /s /q "imageformats"
if exist "iconengines" rmdir /s /q "iconengines"
if exist "tls" rmdir /s /q "tls"
:: ==============================================================================
:: 5. COPY DEPENDENCIES (x64 Source)
:: ==============================================================================
echo.
echo [COPY] Copying DLLs...
:: Core & Helpers
copy /Y "%QT_X64_BIN%\Qt6Core.dll" . >nul
copy /Y "%QT_X64_BIN%\Qt6Gui.dll" . >nul
copy /Y "%QT_X64_BIN%\Qt6Widgets.dll" . >nul
copy /Y "%QT_X64_BIN%\Qt6Multimedia.dll" . >nul
copy /Y "%QT_X64_BIN%\Qt6OpenGL.dll" . >nul
copy /Y "%QT_X64_BIN%\Qt6OpenGLWidgets.dll" . >nul
copy /Y "%QT_X64_BIN%\Qt6Network.dll" . >nul
copy /Y "%QT_X64_BIN%\Qt6Svg.dll" . >nul
copy /Y "%QT_X64_BIN%\Qt6ShaderTools.dll" . >nul
copy /Y "%QT_X64_BIN%\Qt6Concurrent.dll" . >nul
copy /Y "%QT_X64_BIN%\d3dcompiler_47.dll" . >nul
copy /Y "%QT_X64_BIN%\opengl32sw.dll" . >nul
:: FFmpeg
copy /Y "%QT_X64_BIN%\avcodec*.dll" . >nul
copy /Y "%QT_X64_BIN%\avformat*.dll" . >nul
copy /Y "%QT_X64_BIN%\avutil*.dll" . >nul
copy /Y "%QT_X64_BIN%\swresample*.dll" . >nul
copy /Y "%QT_X64_BIN%\swscale*.dll" . >nul
:: Plugins
if not exist "platforms" mkdir "platforms"
copy /Y "%QT_X64_PLUGINS%\platforms\qwindows.dll" "platforms\" >nul
if not exist "styles" mkdir "styles"
copy /Y "%QT_X64_PLUGINS%\styles\qwindowsvistastyle.dll" "styles\" >nul
if not exist "imageformats" mkdir "imageformats"
copy /Y "%QT_X64_PLUGINS%\imageformats\*.dll" "imageformats\" >nul
if not exist "multimedia" mkdir "multimedia"
copy /Y "%QT_X64_PLUGINS%\multimedia\*.dll" "multimedia\" >nul
if not exist "iconengines" mkdir "iconengines"
copy /Y "%QT_X64_PLUGINS%\iconengines\*.dll" "iconengines\" >nul
if not exist "tls" mkdir "tls"
copy /Y "%QT_X64_PLUGINS%\tls\*.dll" "tls\" >nul
:: FFTW3 (x64)
if exist "%VCPKG_BIN%\fftw3.dll" (
copy /Y "%VCPKG_BIN%\fftw3.dll" . >nul
)
echo.
echo [SUCCESS] x64 Build Complete.
pause