2026-01-18 12:13:07 +00:00
|
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
|
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
|
|
|
|
|
|
|
|
|
#include "desktopimporter.h"
|
|
|
|
|
|
2026-01-19 23:12:12 +00:00
|
|
|
#include <KConfigGroup>
|
|
|
|
|
#include <KDesktopFile>
|
2026-01-18 12:13:07 +00:00
|
|
|
#include <QDir>
|
|
|
|
|
#include <QFile>
|
|
|
|
|
#include <QRegularExpression>
|
|
|
|
|
#include <QStandardPaths>
|
|
|
|
|
|
|
|
|
|
DesktopImporter::DesktopImporter(QObject *parent)
|
|
|
|
|
: PlatformImporter(parent)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QString DesktopImporter::platformName() const
|
|
|
|
|
{
|
|
|
|
|
return QStringLiteral("Desktop");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QString DesktopImporter::platformId() const
|
|
|
|
|
{
|
|
|
|
|
return QStringLiteral("desktop");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool DesktopImporter::isAvailable() const
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QStringList DesktopImporter::getDesktopFilePaths() const
|
|
|
|
|
{
|
|
|
|
|
QStringList paths;
|
|
|
|
|
|
|
|
|
|
// Standard XDG application directories
|
|
|
|
|
QStringList appDirs = QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation);
|
|
|
|
|
|
|
|
|
|
// Add common system locations
|
|
|
|
|
appDirs << QStringLiteral("/usr/share/applications");
|
|
|
|
|
appDirs << QStringLiteral("/usr/local/share/applications");
|
|
|
|
|
appDirs << expandPath(QStringLiteral("~/.local/share/applications"));
|
|
|
|
|
|
|
|
|
|
// Flatpak export directories
|
|
|
|
|
appDirs << expandPath(QStringLiteral("~/.local/share/flatpak/exports/share/applications"));
|
|
|
|
|
appDirs << QStringLiteral("/var/lib/flatpak/exports/share/applications");
|
|
|
|
|
|
|
|
|
|
for (const QString &dir : appDirs) {
|
|
|
|
|
QDir appDir(dir);
|
|
|
|
|
if (appDir.exists()) {
|
|
|
|
|
QStringList files = appDir.entryList({QStringLiteral("*.desktop")}, QDir::Files);
|
|
|
|
|
for (const QString &file : files) {
|
|
|
|
|
QString fullPath = appDir.absoluteFilePath(file);
|
|
|
|
|
if (!paths.contains(fullPath)) {
|
|
|
|
|
paths << fullPath;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return paths;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool DesktopImporter::isGameDesktopFile(const QString &filePath) const
|
|
|
|
|
{
|
2026-01-19 23:12:12 +00:00
|
|
|
KDesktopFile desktopFile(filePath);
|
|
|
|
|
KConfigGroup desktop = desktopFile.desktopGroup();
|
2026-01-18 12:13:07 +00:00
|
|
|
|
2026-01-19 23:12:12 +00:00
|
|
|
const QString type = desktop.readEntry(QStringLiteral("Type"));
|
2026-01-18 12:13:07 +00:00
|
|
|
if (type != QStringLiteral("Application")) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if hidden or not shown
|
2026-01-19 23:12:12 +00:00
|
|
|
if (desktop.readEntry(QStringLiteral("Hidden"), false)) {
|
2026-01-18 12:13:07 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
2026-01-19 23:12:12 +00:00
|
|
|
if (desktop.readEntry(QStringLiteral("NoDisplay"), false)) {
|
2026-01-18 12:13:07 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check categories for game-related entries
|
2026-01-19 23:12:12 +00:00
|
|
|
const QStringList categories = desktop.readEntry(QStringLiteral("Categories")).split(QLatin1Char(';'), Qt::SkipEmptyParts);
|
2026-01-18 12:13:07 +00:00
|
|
|
|
2026-01-19 23:12:12 +00:00
|
|
|
return hasGameCategory(categories);
|
2026-01-18 12:13:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Game *DesktopImporter::parseDesktopFile(const QString &filePath) const
|
|
|
|
|
{
|
2026-01-19 23:12:12 +00:00
|
|
|
KDesktopFile desktopFile(filePath);
|
|
|
|
|
KConfigGroup desktop = desktopFile.desktopGroup();
|
2026-01-18 12:13:07 +00:00
|
|
|
|
2026-01-19 23:12:12 +00:00
|
|
|
const QString name = desktop.readEntry(QStringLiteral("Name"));
|
|
|
|
|
QString exec = desktop.readEntry(QStringLiteral("Exec"));
|
|
|
|
|
const QString icon = desktop.readEntry(QStringLiteral("Icon"));
|
|
|
|
|
const QString comment = desktop.readEntry(QStringLiteral("Comment"));
|
|
|
|
|
const QString genericName = desktop.readEntry(QStringLiteral("GenericName"));
|
2026-01-18 12:13:07 +00:00
|
|
|
|
|
|
|
|
if (name.isEmpty() || exec.isEmpty()) {
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clean up the exec command - remove field codes like %f, %u, %F, %U
|
|
|
|
|
static QRegularExpression fieldCodeRegex(QStringLiteral("%[fFuUdDnNickvm]"));
|
|
|
|
|
exec = exec.remove(fieldCodeRegex).trimmed();
|
|
|
|
|
|
|
|
|
|
// Create unique ID from the desktop file name
|
|
|
|
|
QFileInfo fileInfo(filePath);
|
2026-01-19 23:12:12 +00:00
|
|
|
const QString desktopId = fileInfo.completeBaseName();
|
|
|
|
|
QString gameId = QStringLiteral("desktop_") + desktopId;
|
2026-01-18 12:13:07 +00:00
|
|
|
|
|
|
|
|
Game *game = new Game(gameId, name);
|
|
|
|
|
game->setLaunchCommand(exec);
|
|
|
|
|
game->setPlatform(platformName());
|
2026-01-19 23:12:12 +00:00
|
|
|
game->setPlatformId(desktopId);
|
2026-01-18 12:13:07 +00:00
|
|
|
|
|
|
|
|
if (!comment.isEmpty()) {
|
|
|
|
|
game->setDescription(comment);
|
|
|
|
|
} else if (!genericName.isEmpty()) {
|
|
|
|
|
game->setDescription(genericName);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try to find icon
|
|
|
|
|
if (!icon.isEmpty()) {
|
|
|
|
|
// If it's an absolute path, use it directly
|
|
|
|
|
if (QFile::exists(icon)) {
|
|
|
|
|
game->setIconUrl(QUrl::fromLocalFile(icon));
|
|
|
|
|
} else {
|
|
|
|
|
// Try to find in standard icon locations
|
|
|
|
|
QStringList iconPaths = {
|
|
|
|
|
expandPath(QStringLiteral("~/.local/share/icons/hicolor/256x256/apps/") + icon + QStringLiteral(".png")),
|
|
|
|
|
QStringLiteral("/usr/share/icons/hicolor/256x256/apps/") + icon + QStringLiteral(".png"),
|
|
|
|
|
QStringLiteral("/usr/share/pixmaps/") + icon + QStringLiteral(".png"),
|
|
|
|
|
expandPath(QStringLiteral("~/.local/share/icons/hicolor/128x128/apps/") + icon + QStringLiteral(".png")),
|
|
|
|
|
QStringLiteral("/usr/share/icons/hicolor/128x128/apps/") + icon + QStringLiteral(".png"),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (const QString &iconPath : iconPaths) {
|
|
|
|
|
if (QFile::exists(iconPath)) {
|
|
|
|
|
const QUrl url = QUrl::fromLocalFile(iconPath);
|
|
|
|
|
game->setIconUrl(url);
|
|
|
|
|
if (!game->coverUrl().isValid() || game->coverUrl().isEmpty()) {
|
|
|
|
|
game->setCoverUrl(url);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return game;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QList<Game *> DesktopImporter::importGames()
|
|
|
|
|
{
|
|
|
|
|
QList<Game *> games;
|
|
|
|
|
|
|
|
|
|
QStringList desktopFiles = getDesktopFilePaths();
|
|
|
|
|
int total = desktopFiles.count();
|
|
|
|
|
int current = 0;
|
|
|
|
|
|
|
|
|
|
for (const QString &filePath : desktopFiles) {
|
|
|
|
|
current++;
|
|
|
|
|
Q_EMIT importProgress(current, total);
|
|
|
|
|
|
|
|
|
|
if (!isGameDesktopFile(filePath)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Game *game = parseDesktopFile(filePath);
|
|
|
|
|
if (game) {
|
|
|
|
|
games.append(game);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return games;
|
|
|
|
|
}
|