#include "xmbmodel.h" #include "apppaths.h" #include "util.h" #include "xmbentry.h" #include #include #include #include #include #include static QString pickIcon(const QString& desired, bool category) { if (!desired.isEmpty() && QFileInfo::exists(desired)) return QUrl::fromLocalFile(desired).toString(); return Util::fallbackIconQrc(category); } static XmbEntry parseDesktop(const QString& filePath) { // Minimal .desktop parsing via QSettings INI QSettings s(filePath, QSettings::IniFormat); s.setIniCodec("UTF-8"); XmbEntry e; e.type = EntryType::Desktop; e.path = filePath; e.title = s.value("Desktop Entry/Name", QFileInfo(filePath).baseName()).toString(); e.subtitle = s.value("Desktop Entry/Comment", "").toString(); e.exec = s.value("Desktop Entry/Exec", "").toString(); // Icon field kann Theme-Icon sein; wir lassen es erstmal als String stehen (Fallback greift). const auto iconField = s.value("Desktop Entry/Icon", "").toString(); e.icon = iconField.isEmpty() ? Util::fallbackIconQrc(false) : iconField; return e; } static XmbEntry parseLinkFile(const QString& filePath) { // Einfaches INI: [Link] title=..., icon=..., url=..., exec=..., args=... QSettings s(filePath, QSettings::IniFormat); s.setIniCodec("UTF-8"); XmbEntry e; e.type = EntryType::Link; e.path = filePath; e.title = s.value("Link/title", QFileInfo(filePath).baseName()).toString(); e.subtitle = s.value("Link/subtitle", "").toString(); e.url = s.value("Link/url", "").toString(); e.exec = s.value("Link/exec", "").toString(); e.args = s.value("Link/args", "").toString().split(' ', Qt::SkipEmptyParts); const auto icon = s.value("Link/icon", "").toString(); e.icon = icon.isEmpty() ? Util::fallbackIconQrc(false) : icon; e.gameId = s.value("Link/gameId", "").toString(); // optional return e; } static XmbEntry parseJsonAction(const QString& filePath) { QFile f(filePath); XmbEntry e; e.type = EntryType::JsonAction; e.path = filePath; if (!f.open(QIODevice::ReadOnly)) { e.title = QFileInfo(filePath).baseName(); e.icon = Util::fallbackIconQrc(false); return e; } const auto doc = QJsonDocument::fromJson(f.readAll()); const auto o = doc.object(); e.title = o.value("title").toString(QFileInfo(filePath).baseName()); e.subtitle = o.value("subtitle").toString(); e.exec = o.value("exec").toString(); e.url = o.value("url").toString(); e.gameId = o.value("gameId").toString(); const auto icon = o.value("icon").toString(); e.icon = icon.isEmpty() ? Util::fallbackIconQrc(false) : icon; if (o.value("args").isArray()) { for (const auto& v : o.value("args").toArray()) e.args << v.toString(); } return e; } XmbModel::XmbModel(QObject* parent) : QObject(parent) { m_cfg.load(); build(); } QString XmbModel::assetUrl(const QString& rel) const { // rel: "sounds/move.wav" etc. const QString base = AppPaths::assetsRoot(); const QString abs = QDir(base).filePath(rel); if (QFileInfo::exists(abs)) return QUrl::fromLocalFile(abs).toString(); // Fallback: resources/ return QString("qrc:/%1").arg(rel); } QString XmbModel::bootQml() const { // 1) Config boot animation const QString p = m_cfg.bootAnimation(); if (!p.isEmpty() && QFileInfo::exists(p)) return QUrl::fromLocalFile(p).toString(); // 2) bundled fallback return "qrc:/qml/FallbackBoot.qml"; } QVariantList XmbModel::itemsForCategory(int idx) const { if (idx < 0 || idx >= m_categories.size()) return {}; const auto cat = m_categories[idx].toMap(); return cat.value("items").toList(); } void XmbModel::reload() { m_cfg.load(); build(); } void XmbModel::build() { m_categories.clear(); const QDir xmbDir(AppPaths::xmbRoot()); xmbDir.mkpath("."); // ensure exists // Order aus config; wenn Ordner fehlen: trotzdem als Kategorie anlegen const auto order = m_cfg.order(); for (const auto& catNameRaw : order) { const QString catName = catNameRaw.trimmed(); QVariantMap cat; cat["title"] = catName; cat["icon"] = Util::fallbackIconQrc(true); const QString catPath = xmbDir.filePath(catName); QDir catDir(catPath); QVariantList items; if (catDir.exists()) { const auto entries = catDir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot, QDir::Name | QDir::IgnoreCase); for (const auto& fi : entries) { XmbEntry e; if (fi.isDir()) { e.type = EntryType::Folder; e.title = Util::sanitizeTitle(fi.fileName()); e.subtitle = fi.absoluteFilePath(); e.icon = Util::fallbackIconQrc(false); e.path = fi.absoluteFilePath(); items << e.toVariant(); continue; } const QString ext = fi.suffix().toLower(); if (ext == "desktop") { e = parseDesktop(fi.absoluteFilePath()); items << e.toVariant(); } else if (ext == "link") { e = parseLinkFile(fi.absoluteFilePath()); items << e.toVariant(); } else if (ext == "json") { e = parseJsonAction(fi.absoluteFilePath()); items << e.toVariant(); } else { // ignorieren oder als Unknown anzeigen continue; } } } cat["items"] = items; m_categories << cat; } emit categoriesChanged(); }