mirror of
https://invent.kde.org/marcoa/a-la-karte.git
synced 2026-02-09 21:13:08 +00:00
Compare commits
23 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9db6e4cd6a | |||
| 0aa7c4727a | |||
| ecb7eb66d6 | |||
| 55d5a70656 | |||
| 5f0978dd0e | |||
| 245944fca9 | |||
| 365c3b31cb | |||
| ec7d61e5b9 | |||
| 1889f9cf81 | |||
| aabb8d0f4b | |||
| 8ecc1e3a74 | |||
| f02b26f681 | |||
| 5b993cff6b | |||
| 444ef65a78 | |||
| ed9367fd71 | |||
| 3ec2badcd7 | |||
| 27f5a31561 | |||
| 99406a1989 | |||
| 49997ed0cb | |||
| a0b95fbf9a | |||
| 71c1b047b8 | |||
| e754d88eb0 | |||
| b961a8cc8f |
36 changed files with 4515 additions and 311 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -8,3 +8,6 @@
|
||||||
/.idea/
|
/.idea/
|
||||||
|
|
||||||
/FEATURE_GAP_ANALYSIS.md
|
/FEATURE_GAP_ANALYSIS.md
|
||||||
|
|
||||||
|
/third_party/SDL/
|
||||||
|
/kde-builder-persistent-data.json-kde-builder.yaml
|
||||||
|
|
|
||||||
58
CLEANUP_NOTES.md
Normal file
58
CLEANUP_NOTES.md
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
# Cleanup Notes
|
||||||
|
|
||||||
|
## 2026-01-25
|
||||||
|
|
||||||
|
This file tracks cleanup/refactor steps taken during a “deep cleanup” pass, with the goal of reducing dead/duplicate code **without changing application functionality**.
|
||||||
|
|
||||||
|
### Applied changes
|
||||||
|
|
||||||
|
#### 1) Remove duplicate QML type registration in `src/main.cpp`
|
||||||
|
|
||||||
|
- **What changed**
|
||||||
|
- Removed manual `qmlRegisterSingletonType` / `qmlRegisterType` / `qmlRegisterUncreatableType` calls for types already marked with `QML_ELEMENT` / `QML_SINGLETON`.
|
||||||
|
- **Why**
|
||||||
|
- The project uses `ecm_add_qml_module(...)` and the generated QML type registration (based on `QML_ELEMENT` / `QML_SINGLETON`) already registers these types.
|
||||||
|
- Keeping both mechanisms is redundant and risks divergence.
|
||||||
|
- **Files**
|
||||||
|
- `src/main.cpp`
|
||||||
|
|
||||||
|
#### 2) Remove dead QML state in `src/qml/SettingsPage.qml`
|
||||||
|
|
||||||
|
- **What changed**
|
||||||
|
- Removed unused `pendingDisableImportDelegate` state.
|
||||||
|
- Simplified `requestDisableImport(...)` to no longer accept an unused `delegate` parameter.
|
||||||
|
- **Why**
|
||||||
|
- The variable was assigned/cleared but never read.
|
||||||
|
- **Files**
|
||||||
|
- `src/qml/SettingsPage.qml`
|
||||||
|
|
||||||
|
#### 3) Stop shipping `FocusableCard.qml` in the QML module (but keep the file)
|
||||||
|
|
||||||
|
- **What changed**
|
||||||
|
- Removed `qml/components/FocusableCard.qml` from the `QML_FILES` list in `ecm_add_qml_module(...)`.
|
||||||
|
- **Why**
|
||||||
|
- The component is not referenced anywhere in QML currently.
|
||||||
|
- We want to keep the implementation around because it may be useful later (e.g. Desktop/Couch mode), but avoid shipping unused module contents.
|
||||||
|
- **Files**
|
||||||
|
- `src/CMakeLists.txt`
|
||||||
|
- Note: `src/qml/components/FocusableCard.qml` still exists in the repository.
|
||||||
|
|
||||||
|
### Verification performed
|
||||||
|
|
||||||
|
- Built successfully:
|
||||||
|
- `cmake --build build-debug`
|
||||||
|
- Quick run smoke test:
|
||||||
|
- Launches without QML type registration errors.
|
||||||
|
- Only the existing Kirigami `StackView has detected conflicting anchors` warning was observed.
|
||||||
|
|
||||||
|
### Intentional non-changes (paused items)
|
||||||
|
|
||||||
|
- Do **not** delete `src/qml/components/FocusableCard.qml` yet.
|
||||||
|
- Do **not** remove or migrate any persisted `Config` settings or public QML API fields yet.
|
||||||
|
|
||||||
|
### How to revert / resume later
|
||||||
|
|
||||||
|
- To re-ship `FocusableCard.qml`:
|
||||||
|
- Add it back to `ecm_add_qml_module(... QML_FILES ...)` in `src/CMakeLists.txt`.
|
||||||
|
- To fully remove it:
|
||||||
|
- `git rm src/qml/components/FocusableCard.qml` (only after confirming it is not needed for future UI modes).
|
||||||
18
README.md
18
README.md
|
|
@ -6,10 +6,25 @@
|
||||||
|
|
||||||
A unified game launcher for KDE Plasma 6, inspired by [KDE Apps Initiative issue#26](https://invent.kde.org/teams/apps-initiative/tasks/-/issues/26).
|
A unified game launcher for KDE Plasma 6, inspired by [KDE Apps Initiative issue#26](https://invent.kde.org/teams/apps-initiative/tasks/-/issues/26).
|
||||||
|
|
||||||
|
## Project tracks
|
||||||
|
|
||||||
|
This repository currently has two development tracks:
|
||||||
|
|
||||||
|
- **Legacy (launcher-first)**
|
||||||
|
- The current `master`/`legacy` baseline: a traditional game launcher UI.
|
||||||
|
- See `docs/LEGACY.md`.
|
||||||
|
- **Gamecenter (launcher + daemons)**
|
||||||
|
- The `gamecenter` branch: an experimental direction to evolve A-La-Karte into a launcher + game center suitable for a console-like KDE experience.
|
||||||
|
- See `docs/GAMECENTER.md`.
|
||||||
|
|
||||||
|
For documentation index, see `docs/README.md`.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Import games from Steam, Lutris, Heroic, Bottles, itch.io, Legendary, RetroArch playlists, Flatpak applications, and desktop entries
|
- Import games from Steam, Lutris, Heroic, Bottles, itch.io, Legendary, RetroArch playlists, Flatpak applications, and desktop entries
|
||||||
- Manual game entries with editable metadata (name, developer/publisher, executable, working directory)
|
- Manual game entries with editable metadata (name, developer/publisher, executable, working directory)
|
||||||
|
- Per-game launch overrides (environment variables, compatibility runner selection, Wine/Proton prefix path)
|
||||||
|
- Per-game screenshots gallery with import and centralized storage
|
||||||
- Grid and list views
|
- Grid and list views
|
||||||
- Search by name/developer/publisher and filter by source
|
- Search by name/developer/publisher and filter by source
|
||||||
- Favorites and hidden games views
|
- Favorites and hidden games views
|
||||||
|
|
@ -194,6 +209,8 @@ Settings are stored in `~/.config/alakarterc` and include:
|
||||||
|
|
||||||
Game library is stored in `~/.local/share/alakarte/library.json`.
|
Game library is stored in `~/.local/share/alakarte/library.json`.
|
||||||
|
|
||||||
|
Imported screenshots are stored under `~/.local/share/alakarte/media/screenshots/<gameId>/`.
|
||||||
|
|
||||||
## Code Overview
|
## Code Overview
|
||||||
|
|
||||||
- **Application entry point**: `src/main.cpp`
|
- **Application entry point**: `src/main.cpp`
|
||||||
|
|
@ -201,6 +218,7 @@ Game library is stored in `~/.local/share/alakarte/library.json`.
|
||||||
- **Data model**:
|
- **Data model**:
|
||||||
- `src/game.*` stores per-game metadata
|
- `src/game.*` stores per-game metadata
|
||||||
- `src/gamemodel.*` and `src/gamesortfiltermodel.*` provide sorting/filtering for the UI
|
- `src/gamemodel.*` and `src/gamesortfiltermodel.*` provide sorting/filtering for the UI
|
||||||
|
- **Media**: `src/mediamanager.*` + `src/screenshotmodel.*` provide centralized per-game screenshot management
|
||||||
- **Importers**: `src/*importer.*` (Steam/Lutris/Heroic/etc.)
|
- **Importers**: `src/*importer.*` (Steam/Lutris/Heroic/etc.)
|
||||||
- **Launcher**: `src/gamelauncher.*` launches processes/URLs and records last-played on launch
|
- **Launcher**: `src/gamelauncher.*` launches processes/URLs and records last-played on launch
|
||||||
- **UI**: `src/qml/` (Kirigami + Qt Quick)
|
- **UI**: `src/qml/` (Kirigami + Qt Quick)
|
||||||
|
|
|
||||||
18
changelog.md
18
changelog.md
|
|
@ -1,5 +1,23 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.1.1rc2] - 2026-01-25
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Per-game compatibility settings: Native/Wine/Proton/custom runner selection
|
||||||
|
- Proton discovery (Steam/Flatpak) and per-game prefix path support
|
||||||
|
- Per-game launch environment variables
|
||||||
|
- Centralized screenshot management with per-game gallery and import action
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Game JSON now supports a nested `launch` object for launch overrides (backwards compatible)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- More robust `library.json` parsing with better logging and legacy entry handling
|
||||||
|
- Runner-aware launching no longer bypasses overrides for Steam/Lutris URL entries
|
||||||
|
|
||||||
## [0.1.1rc1] - 2026-01-24
|
## [0.1.1rc1] - 2026-01-24
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
52
docs/GAMECENTER.md
Normal file
52
docs/GAMECENTER.md
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Gamecenter track (Launcher + Game Center)
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
The gamecenter track evolves A-La-Karte from “a launcher” into “a launcher + game center” suitable for a **fully integrated, console-like experience on KDE**.
|
||||||
|
|
||||||
|
Key ideas:
|
||||||
|
|
||||||
|
- The UI becomes a client of background services.
|
||||||
|
- Launching and monitoring games is handled by a daemon.
|
||||||
|
- Runner resolution (Wine/Proton) is handled by a runner manager service.
|
||||||
|
|
||||||
|
## Architecture (high level)
|
||||||
|
|
||||||
|
### UI (A-La-Karte)
|
||||||
|
|
||||||
|
- Displays the library and settings.
|
||||||
|
- Requests launches via the Game Center daemon instead of launching processes directly.
|
||||||
|
- Surfaces daemon/service errors to the user.
|
||||||
|
|
||||||
|
### Game Center daemon
|
||||||
|
|
||||||
|
- Manages game sessions (launch/stop, running state, concurrency policy).
|
||||||
|
- Tracks/monitors sessions so Steam/Lutris URL-based launches can still be managed.
|
||||||
|
- Intended to be activatable via DBus/systemd.
|
||||||
|
|
||||||
|
DBus service (planned/implemented on the gamecenter branch):
|
||||||
|
|
||||||
|
- `org.kde.GameCenter1`
|
||||||
|
|
||||||
|
### Runner manager daemon
|
||||||
|
|
||||||
|
- Manages “runners” (e.g. Proton, Wine, GE builds):
|
||||||
|
- install/uninstall
|
||||||
|
- inventory listing
|
||||||
|
- resolve launch specs deterministically
|
||||||
|
- prefix lifecycle helpers
|
||||||
|
|
||||||
|
DBus service (planned/implemented on the gamecenter branch):
|
||||||
|
|
||||||
|
- `org.kde.ALaKarte.Runner1`
|
||||||
|
|
||||||
|
## Branches
|
||||||
|
|
||||||
|
- `master` / `legacy`
|
||||||
|
- Launcher-focused baseline.
|
||||||
|
- `gamecenter`
|
||||||
|
- Experimental daemon-based direction.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
This document is intentionally a living overview. Detailed API docs / roadmaps should live in separate documents as they solidify.
|
||||||
18
docs/LEGACY.md
Normal file
18
docs/LEGACY.md
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Legacy track (Launcher)
|
||||||
|
|
||||||
|
## What it is
|
||||||
|
|
||||||
|
The legacy track is a traditional desktop application:
|
||||||
|
|
||||||
|
- The UI directly owns the game library.
|
||||||
|
- The UI launches games.
|
||||||
|
- Focus is on being a high-quality KDE Plasma launcher with good keyboard/gamepad navigation.
|
||||||
|
|
||||||
|
## What it is not
|
||||||
|
|
||||||
|
- A system-level game session manager.
|
||||||
|
- A daemon that monitors and manages sessions across launchers.
|
||||||
|
|
||||||
|
## Why it exists
|
||||||
|
|
||||||
|
This track is the stable baseline and the fastest way to iterate on UX, importers, and per-game configuration.
|
||||||
14
docs/README.md
Normal file
14
docs/README.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Documentation
|
||||||
|
|
||||||
|
This folder contains higher-level project documentation.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
- `LEGACY.md`
|
||||||
|
- Notes on the current launcher-focused architecture (historically the "legacy" track).
|
||||||
|
- `GAMECENTER.md`
|
||||||
|
- Notes on the "gamecenter" track: a launcher + Game Center daemon-based architecture for a more console-like KDE experience.
|
||||||
|
|
||||||
|
## Contributing docs
|
||||||
|
|
||||||
|
If you add a new document, link it from this index so it stays discoverable.
|
||||||
22
docs/ROADMAP.md
Normal file
22
docs/ROADMAP.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
# Roadmap
|
||||||
|
|
||||||
|
This document captures the high-level direction of the project.
|
||||||
|
|
||||||
|
## Legacy track (launcher-first)
|
||||||
|
|
||||||
|
- Maintain a stable, polished KDE Plasma launcher.
|
||||||
|
- Keep importers and library UX solid and reliable.
|
||||||
|
|
||||||
|
## Gamecenter track (launcher + daemons)
|
||||||
|
|
||||||
|
- Evolve into a fully integrated, console-like experience on KDE.
|
||||||
|
- Move launching + session monitoring into a daemon.
|
||||||
|
- Add runner management (Wine/Proton) as a service.
|
||||||
|
|
||||||
|
## Near-term goals
|
||||||
|
|
||||||
|
- (Fill in)
|
||||||
|
|
||||||
|
## Longer-term goals
|
||||||
|
|
||||||
|
- (Fill in)
|
||||||
|
|
@ -75,6 +75,16 @@
|
||||||
</keywords>
|
</keywords>
|
||||||
|
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="0.1.1rc2" date="2026-01-25">
|
||||||
|
<description>
|
||||||
|
<p>Preview release with per-game compatibility and screenshots</p>
|
||||||
|
<ul>
|
||||||
|
<li>Per-game compatibility settings (Native/Wine/Proton/custom runner)</li>
|
||||||
|
<li>Per-game launch environment variables</li>
|
||||||
|
<li>Centralized per-game screenshots gallery with import action</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
<release version="0.1.1rc1" date="2026-01-24">
|
<release version="0.1.1rc1" date="2026-01-24">
|
||||||
<description>
|
<description>
|
||||||
<p>Preview release with improved navigation and importing</p>
|
<p>Preview release with improved navigation and importing</p>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,9 @@ add_executable(alakarte
|
||||||
gamesortfiltermodel.cpp
|
gamesortfiltermodel.cpp
|
||||||
game.cpp
|
game.cpp
|
||||||
gamepadmanager.cpp
|
gamepadmanager.cpp
|
||||||
|
inputmanager.cpp
|
||||||
|
mediamanager.cpp
|
||||||
|
screenshotmodel.cpp
|
||||||
platformimporter.cpp
|
platformimporter.cpp
|
||||||
steamimporter.cpp
|
steamimporter.cpp
|
||||||
lutrisimporter.cpp
|
lutrisimporter.cpp
|
||||||
|
|
@ -61,10 +64,10 @@ ecm_add_qml_module(alakarte URI org.kde.alakarte
|
||||||
qml/LibraryView.qml
|
qml/LibraryView.qml
|
||||||
qml/GameCard.qml
|
qml/GameCard.qml
|
||||||
qml/GameDetailsSheet.qml
|
qml/GameDetailsSheet.qml
|
||||||
|
qml/DiagnosticsSheet.qml
|
||||||
qml/SettingsPage.qml
|
qml/SettingsPage.qml
|
||||||
qml/SidebarView.qml
|
qml/SidebarView.qml
|
||||||
qml/GameEditDialog.qml
|
qml/GameEditDialog.qml
|
||||||
qml/components/FocusableCard.qml
|
|
||||||
qml/components/GameGridView.qml
|
qml/components/GameGridView.qml
|
||||||
qml/components/SearchHeader.qml
|
qml/components/SearchHeader.qml
|
||||||
qml/components/IconWithResourceFallback.qml
|
qml/components/IconWithResourceFallback.qml
|
||||||
|
|
@ -117,6 +120,7 @@ ecm_add_qml_module(alakarte URI org.kde.alakarte
|
||||||
gamesortfiltermodel.h
|
gamesortfiltermodel.h
|
||||||
game.h
|
game.h
|
||||||
gamepadmanager.h
|
gamepadmanager.h
|
||||||
|
inputmanager.h
|
||||||
platformimporter.h
|
platformimporter.h
|
||||||
steamimporter.h
|
steamimporter.h
|
||||||
lutrisimporter.h
|
lutrisimporter.h
|
||||||
|
|
|
||||||
503
src/app.cpp
503
src/app.cpp
|
|
@ -17,7 +17,10 @@
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
|
#include <QJsonParseError>
|
||||||
|
#include <QSet>
|
||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
|
#include <QTimer>
|
||||||
#include <QtConcurrent>
|
#include <QtConcurrent>
|
||||||
|
|
||||||
App *App::s_instance = nullptr;
|
App *App::s_instance = nullptr;
|
||||||
|
|
@ -27,6 +30,7 @@ App::App(QObject *parent)
|
||||||
, m_gameModel(new GameModel(this))
|
, m_gameModel(new GameModel(this))
|
||||||
, m_launcher(new GameLauncher(this))
|
, m_launcher(new GameLauncher(this))
|
||||||
, m_steamGridDB(new SteamGridDB(this))
|
, m_steamGridDB(new SteamGridDB(this))
|
||||||
|
, m_mediaManager(new MediaManager(this))
|
||||||
, m_config(new Config(this))
|
, m_config(new Config(this))
|
||||||
{
|
{
|
||||||
loadLibrary();
|
loadLibrary();
|
||||||
|
|
@ -140,6 +144,12 @@ App::App(QObject *parent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (m_config->autoImportOnStartup()) {
|
||||||
|
QTimer::singleShot(0, this, [this]() {
|
||||||
|
importAllGames();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
App *App::instance()
|
App *App::instance()
|
||||||
|
|
@ -172,6 +182,11 @@ SteamGridDB *App::steamGridDB() const
|
||||||
return m_steamGridDB;
|
return m_steamGridDB;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MediaManager *App::mediaManager() const
|
||||||
|
{
|
||||||
|
return m_mediaManager;
|
||||||
|
}
|
||||||
|
|
||||||
Config *App::config() const
|
Config *App::config() const
|
||||||
{
|
{
|
||||||
return m_config;
|
return m_config;
|
||||||
|
|
@ -208,18 +223,17 @@ void App::importAllGames()
|
||||||
if (m_importing)
|
if (m_importing)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const bool anyEnabled = m_config->importSteam() || m_config->importLutris() || m_config->importHeroic() || m_config->importDesktop()
|
const bool doSteam = m_config->importSteam();
|
||||||
|| m_config->importBottles() || m_config->importFlatpak() || m_config->importItch() || m_config->importLegendary() || m_config->importRetroArch();
|
const bool doLutris = m_config->importLutris();
|
||||||
|
const bool doHeroic = m_config->importHeroic();
|
||||||
|
const bool doDesktop = m_config->importDesktop();
|
||||||
|
const bool doBottles = m_config->importBottles();
|
||||||
|
const bool doFlatpak = m_config->importFlatpak();
|
||||||
|
const bool doItch = m_config->importItch();
|
||||||
|
const bool doLegendary = m_config->importLegendary();
|
||||||
|
const bool doRetroArch = m_config->importRetroArch();
|
||||||
|
|
||||||
const bool doSteam = m_config->importSteam() && !m_gameModel->hasPlatformPrefix(QStringLiteral("Steam"));
|
const bool anyEnabled = doSteam || doLutris || doHeroic || doDesktop || doBottles || doFlatpak || doItch || doLegendary || doRetroArch;
|
||||||
const bool doLutris = m_config->importLutris() && !m_gameModel->hasPlatformPrefix(QStringLiteral("Lutris"));
|
|
||||||
const bool doHeroic = m_config->importHeroic() && !m_gameModel->hasPlatformPrefix(QStringLiteral("Heroic"));
|
|
||||||
const bool doDesktop = m_config->importDesktop() && !m_gameModel->hasPlatformPrefix(QStringLiteral("Desktop"));
|
|
||||||
const bool doBottles = m_config->importBottles() && !m_gameModel->hasPlatformPrefix(QStringLiteral("Bottles"));
|
|
||||||
const bool doFlatpak = m_config->importFlatpak() && !m_gameModel->hasPlatformPrefix(QStringLiteral("Flatpak"));
|
|
||||||
const bool doItch = m_config->importItch() && !m_gameModel->hasPlatformPrefix(QStringLiteral("itch.io"));
|
|
||||||
const bool doLegendary = m_config->importLegendary() && !m_gameModel->hasPlatformPrefix(QStringLiteral("Legendary"));
|
|
||||||
const bool doRetroArch = m_config->importRetroArch() && !m_gameModel->hasPlatformPrefix(QStringLiteral("RetroArch"));
|
|
||||||
|
|
||||||
if (!anyEnabled) {
|
if (!anyEnabled) {
|
||||||
setImportStatus(tr("No import sources enabled"));
|
setImportStatus(tr("No import sources enabled"));
|
||||||
|
|
@ -227,17 +241,55 @@ void App::importAllGames()
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(doSteam || doLutris || doHeroic || doDesktop || doBottles || doFlatpak || doItch || doLegendary || doRetroArch)) {
|
const QSet<QString> existingIds = [this]() {
|
||||||
setImportStatus(tr("All enabled sources already imported"));
|
QSet<QString> ids;
|
||||||
Q_EMIT importCompleted(0);
|
const QList<Game *> games = m_gameModel->allGames();
|
||||||
return;
|
for (Game *game : games) {
|
||||||
}
|
if (game) {
|
||||||
|
ids.insert(game->id());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}();
|
||||||
|
|
||||||
setImporting(true);
|
setImporting(true);
|
||||||
setImportStatus(tr("Importing games..."));
|
setImportStatus(tr("Importing games..."));
|
||||||
|
|
||||||
[[maybe_unused]] auto future = QtConcurrent::run([this, doSteam, doLutris, doHeroic, doDesktop, doBottles, doFlatpak, doItch, doLegendary, doRetroArch]() {
|
[[maybe_unused]] auto future = QtConcurrent::run([this,
|
||||||
|
doSteam,
|
||||||
|
doLutris,
|
||||||
|
doHeroic,
|
||||||
|
doDesktop,
|
||||||
|
doBottles,
|
||||||
|
doFlatpak,
|
||||||
|
doItch,
|
||||||
|
doLegendary,
|
||||||
|
doRetroArch,
|
||||||
|
existingIds]() {
|
||||||
int totalCount = 0;
|
int totalCount = 0;
|
||||||
|
QSet<QString> seenIds = existingIds;
|
||||||
|
|
||||||
|
const auto keepNewGames = [&seenIds](const QList<Game *> &games) {
|
||||||
|
QList<Game *> result;
|
||||||
|
result.reserve(games.size());
|
||||||
|
|
||||||
|
for (Game *game : games) {
|
||||||
|
if (!game) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString id = game->id();
|
||||||
|
if (id.isEmpty() || seenIds.contains(id)) {
|
||||||
|
delete game;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
seenIds.insert(id);
|
||||||
|
result.append(game);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
// Import from Steam
|
// Import from Steam
|
||||||
if (doSteam) {
|
if (doSteam) {
|
||||||
|
|
@ -249,7 +301,7 @@ void App::importAllGames()
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
|
|
||||||
SteamImporter steamImporter;
|
SteamImporter steamImporter;
|
||||||
QList<Game *> steamGames = steamImporter.importGames();
|
QList<Game *> steamGames = keepNewGames(steamImporter.importGames());
|
||||||
for (Game *game : steamGames) {
|
for (Game *game : steamGames) {
|
||||||
game->moveToThread(this->thread());
|
game->moveToThread(this->thread());
|
||||||
game->setParent(nullptr);
|
game->setParent(nullptr);
|
||||||
|
|
@ -284,7 +336,7 @@ void App::importAllGames()
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
|
|
||||||
LutrisImporter lutrisImporter;
|
LutrisImporter lutrisImporter;
|
||||||
QList<Game *> lutrisGames = lutrisImporter.importGames();
|
QList<Game *> lutrisGames = keepNewGames(lutrisImporter.importGames());
|
||||||
for (Game *game : lutrisGames) {
|
for (Game *game : lutrisGames) {
|
||||||
game->moveToThread(this->thread());
|
game->moveToThread(this->thread());
|
||||||
game->setParent(nullptr);
|
game->setParent(nullptr);
|
||||||
|
|
@ -319,7 +371,7 @@ void App::importAllGames()
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
|
|
||||||
HeroicImporter heroicImporter;
|
HeroicImporter heroicImporter;
|
||||||
QList<Game *> heroicGames = heroicImporter.importGames();
|
QList<Game *> heroicGames = keepNewGames(heroicImporter.importGames());
|
||||||
for (Game *game : heroicGames) {
|
for (Game *game : heroicGames) {
|
||||||
game->moveToThread(this->thread());
|
game->moveToThread(this->thread());
|
||||||
game->setParent(nullptr);
|
game->setParent(nullptr);
|
||||||
|
|
@ -354,7 +406,7 @@ void App::importAllGames()
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
|
|
||||||
DesktopImporter desktopImporter;
|
DesktopImporter desktopImporter;
|
||||||
QList<Game *> desktopGames = desktopImporter.importGames();
|
QList<Game *> desktopGames = keepNewGames(desktopImporter.importGames());
|
||||||
for (Game *game : desktopGames) {
|
for (Game *game : desktopGames) {
|
||||||
game->moveToThread(this->thread());
|
game->moveToThread(this->thread());
|
||||||
game->setParent(nullptr);
|
game->setParent(nullptr);
|
||||||
|
|
@ -389,7 +441,7 @@ void App::importAllGames()
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
|
|
||||||
BottlesImporter bottlesImporter;
|
BottlesImporter bottlesImporter;
|
||||||
QList<Game *> bottlesGames = bottlesImporter.importGames();
|
QList<Game *> bottlesGames = keepNewGames(bottlesImporter.importGames());
|
||||||
for (Game *game : bottlesGames) {
|
for (Game *game : bottlesGames) {
|
||||||
game->moveToThread(this->thread());
|
game->moveToThread(this->thread());
|
||||||
game->setParent(nullptr);
|
game->setParent(nullptr);
|
||||||
|
|
@ -424,7 +476,7 @@ void App::importAllGames()
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
|
|
||||||
FlatpakImporter flatpakImporter;
|
FlatpakImporter flatpakImporter;
|
||||||
QList<Game *> flatpakGames = flatpakImporter.importGames();
|
QList<Game *> flatpakGames = keepNewGames(flatpakImporter.importGames());
|
||||||
for (Game *game : flatpakGames) {
|
for (Game *game : flatpakGames) {
|
||||||
game->moveToThread(this->thread());
|
game->moveToThread(this->thread());
|
||||||
game->setParent(nullptr);
|
game->setParent(nullptr);
|
||||||
|
|
@ -459,7 +511,7 @@ void App::importAllGames()
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
|
|
||||||
ItchImporter itchImporter;
|
ItchImporter itchImporter;
|
||||||
QList<Game *> itchGames = itchImporter.importGames();
|
QList<Game *> itchGames = keepNewGames(itchImporter.importGames());
|
||||||
for (Game *game : itchGames) {
|
for (Game *game : itchGames) {
|
||||||
game->moveToThread(this->thread());
|
game->moveToThread(this->thread());
|
||||||
game->setParent(nullptr);
|
game->setParent(nullptr);
|
||||||
|
|
@ -494,7 +546,7 @@ void App::importAllGames()
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
|
|
||||||
LegendaryImporter legendaryImporter;
|
LegendaryImporter legendaryImporter;
|
||||||
QList<Game *> legendaryGames = legendaryImporter.importGames();
|
QList<Game *> legendaryGames = keepNewGames(legendaryImporter.importGames());
|
||||||
for (Game *game : legendaryGames) {
|
for (Game *game : legendaryGames) {
|
||||||
game->moveToThread(this->thread());
|
game->moveToThread(this->thread());
|
||||||
game->setParent(nullptr);
|
game->setParent(nullptr);
|
||||||
|
|
@ -529,7 +581,7 @@ void App::importAllGames()
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
|
|
||||||
RetroArchImporter retroArchImporter;
|
RetroArchImporter retroArchImporter;
|
||||||
QList<Game *> retroArchGames = retroArchImporter.importGames();
|
QList<Game *> retroArchGames = keepNewGames(retroArchImporter.importGames());
|
||||||
for (Game *game : retroArchGames) {
|
for (Game *game : retroArchGames) {
|
||||||
game->moveToThread(this->thread());
|
game->moveToThread(this->thread());
|
||||||
game->setParent(nullptr);
|
game->setParent(nullptr);
|
||||||
|
|
@ -558,7 +610,7 @@ void App::importAllGames()
|
||||||
QMetaObject::invokeMethod(
|
QMetaObject::invokeMethod(
|
||||||
this,
|
this,
|
||||||
[this, totalCount]() {
|
[this, totalCount]() {
|
||||||
setImportStatus(tr("Import complete: %1 games found").arg(totalCount));
|
setImportStatus(tr("Import complete: %1 new games found").arg(totalCount));
|
||||||
setImporting(false);
|
setImporting(false);
|
||||||
saveLibrary();
|
saveLibrary();
|
||||||
Q_EMIT importCompleted(totalCount);
|
Q_EMIT importCompleted(totalCount);
|
||||||
|
|
@ -572,28 +624,57 @@ void App::importFromSteam()
|
||||||
if (m_importing)
|
if (m_importing)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
const QSet<QString> existingIds = [this]() {
|
||||||
|
QSet<QString> ids;
|
||||||
|
const QList<Game *> games = m_gameModel->allGames();
|
||||||
|
for (Game *game : games) {
|
||||||
|
if (game) {
|
||||||
|
ids.insert(game->id());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}();
|
||||||
|
|
||||||
setImporting(true);
|
setImporting(true);
|
||||||
setImportStatus(tr("Scanning Steam library..."));
|
setImportStatus(tr("Scanning Steam library..."));
|
||||||
|
|
||||||
[[maybe_unused]] auto future = QtConcurrent::run([this]() {
|
[[maybe_unused]] auto future = QtConcurrent::run([this, existingIds]() {
|
||||||
SteamImporter importer;
|
SteamImporter importer;
|
||||||
QList<Game *> games = importer.importGames();
|
QList<Game *> games = importer.importGames();
|
||||||
|
QSet<QString> seenIds = existingIds;
|
||||||
|
QList<Game *> newGames;
|
||||||
|
newGames.reserve(games.size());
|
||||||
|
|
||||||
for (Game *game : games) {
|
for (Game *game : games) {
|
||||||
|
if (!game) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString id = game->id();
|
||||||
|
if (id.isEmpty() || seenIds.contains(id)) {
|
||||||
|
delete game;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
seenIds.insert(id);
|
||||||
|
newGames.append(game);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Game *game : newGames) {
|
||||||
game->moveToThread(this->thread());
|
game->moveToThread(this->thread());
|
||||||
game->setParent(nullptr);
|
game->setParent(nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
QMetaObject::invokeMethod(
|
QMetaObject::invokeMethod(
|
||||||
this,
|
this,
|
||||||
[this, games]() {
|
[this, newGames]() {
|
||||||
for (Game *game : games) {
|
for (Game *game : newGames) {
|
||||||
m_gameModel->addGame(game);
|
m_gameModel->addGame(game);
|
||||||
}
|
}
|
||||||
setImportStatus(tr("Steam import complete: %1 games found").arg(games.count()));
|
setImportStatus(tr("Steam import complete: %1 new games found").arg(newGames.count()));
|
||||||
setImporting(false);
|
setImporting(false);
|
||||||
saveLibrary();
|
saveLibrary();
|
||||||
Q_EMIT importCompleted(games.count());
|
Q_EMIT importCompleted(newGames.count());
|
||||||
},
|
},
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
});
|
});
|
||||||
|
|
@ -604,28 +685,57 @@ void App::importFromLutris()
|
||||||
if (m_importing)
|
if (m_importing)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
const QSet<QString> existingIds = [this]() {
|
||||||
|
QSet<QString> ids;
|
||||||
|
const QList<Game *> games = m_gameModel->allGames();
|
||||||
|
for (Game *game : games) {
|
||||||
|
if (game) {
|
||||||
|
ids.insert(game->id());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}();
|
||||||
|
|
||||||
setImporting(true);
|
setImporting(true);
|
||||||
setImportStatus(tr("Scanning Lutris library..."));
|
setImportStatus(tr("Scanning Lutris library..."));
|
||||||
|
|
||||||
[[maybe_unused]] auto future = QtConcurrent::run([this]() {
|
[[maybe_unused]] auto future = QtConcurrent::run([this, existingIds]() {
|
||||||
LutrisImporter importer;
|
LutrisImporter importer;
|
||||||
QList<Game *> games = importer.importGames();
|
QList<Game *> games = importer.importGames();
|
||||||
|
QSet<QString> seenIds = existingIds;
|
||||||
|
QList<Game *> newGames;
|
||||||
|
newGames.reserve(games.size());
|
||||||
|
|
||||||
for (Game *game : games) {
|
for (Game *game : games) {
|
||||||
|
if (!game) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString id = game->id();
|
||||||
|
if (id.isEmpty() || seenIds.contains(id)) {
|
||||||
|
delete game;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
seenIds.insert(id);
|
||||||
|
newGames.append(game);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Game *game : newGames) {
|
||||||
game->moveToThread(this->thread());
|
game->moveToThread(this->thread());
|
||||||
game->setParent(nullptr);
|
game->setParent(nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
QMetaObject::invokeMethod(
|
QMetaObject::invokeMethod(
|
||||||
this,
|
this,
|
||||||
[this, games]() {
|
[this, newGames]() {
|
||||||
for (Game *game : games) {
|
for (Game *game : newGames) {
|
||||||
m_gameModel->addGame(game);
|
m_gameModel->addGame(game);
|
||||||
}
|
}
|
||||||
setImportStatus(tr("Lutris import complete: %1 games found").arg(games.count()));
|
setImportStatus(tr("Lutris import complete: %1 new games found").arg(newGames.count()));
|
||||||
setImporting(false);
|
setImporting(false);
|
||||||
saveLibrary();
|
saveLibrary();
|
||||||
Q_EMIT importCompleted(games.count());
|
Q_EMIT importCompleted(newGames.count());
|
||||||
},
|
},
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
});
|
});
|
||||||
|
|
@ -636,28 +746,57 @@ void App::importFromHeroic()
|
||||||
if (m_importing)
|
if (m_importing)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
const QSet<QString> existingIds = [this]() {
|
||||||
|
QSet<QString> ids;
|
||||||
|
const QList<Game *> games = m_gameModel->allGames();
|
||||||
|
for (Game *game : games) {
|
||||||
|
if (game) {
|
||||||
|
ids.insert(game->id());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}();
|
||||||
|
|
||||||
setImporting(true);
|
setImporting(true);
|
||||||
setImportStatus(tr("Scanning Heroic library..."));
|
setImportStatus(tr("Scanning Heroic library..."));
|
||||||
|
|
||||||
[[maybe_unused]] auto future = QtConcurrent::run([this]() {
|
[[maybe_unused]] auto future = QtConcurrent::run([this, existingIds]() {
|
||||||
HeroicImporter importer;
|
HeroicImporter importer;
|
||||||
QList<Game *> games = importer.importGames();
|
QList<Game *> games = importer.importGames();
|
||||||
|
QSet<QString> seenIds = existingIds;
|
||||||
|
QList<Game *> newGames;
|
||||||
|
newGames.reserve(games.size());
|
||||||
|
|
||||||
for (Game *game : games) {
|
for (Game *game : games) {
|
||||||
|
if (!game) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString id = game->id();
|
||||||
|
if (id.isEmpty() || seenIds.contains(id)) {
|
||||||
|
delete game;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
seenIds.insert(id);
|
||||||
|
newGames.append(game);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Game *game : newGames) {
|
||||||
game->moveToThread(this->thread());
|
game->moveToThread(this->thread());
|
||||||
game->setParent(nullptr);
|
game->setParent(nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
QMetaObject::invokeMethod(
|
QMetaObject::invokeMethod(
|
||||||
this,
|
this,
|
||||||
[this, games]() {
|
[this, newGames]() {
|
||||||
for (Game *game : games) {
|
for (Game *game : newGames) {
|
||||||
m_gameModel->addGame(game);
|
m_gameModel->addGame(game);
|
||||||
}
|
}
|
||||||
setImportStatus(tr("Heroic import complete: %1 games found").arg(games.count()));
|
setImportStatus(tr("Heroic import complete: %1 new games found").arg(newGames.count()));
|
||||||
setImporting(false);
|
setImporting(false);
|
||||||
saveLibrary();
|
saveLibrary();
|
||||||
Q_EMIT importCompleted(games.count());
|
Q_EMIT importCompleted(newGames.count());
|
||||||
},
|
},
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
});
|
});
|
||||||
|
|
@ -668,28 +807,57 @@ void App::importFromDesktop()
|
||||||
if (m_importing)
|
if (m_importing)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
const QSet<QString> existingIds = [this]() {
|
||||||
|
QSet<QString> ids;
|
||||||
|
const QList<Game *> games = m_gameModel->allGames();
|
||||||
|
for (Game *game : games) {
|
||||||
|
if (game) {
|
||||||
|
ids.insert(game->id());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}();
|
||||||
|
|
||||||
setImporting(true);
|
setImporting(true);
|
||||||
setImportStatus(tr("Scanning desktop entries..."));
|
setImportStatus(tr("Scanning desktop entries..."));
|
||||||
|
|
||||||
[[maybe_unused]] auto future = QtConcurrent::run([this]() {
|
[[maybe_unused]] auto future = QtConcurrent::run([this, existingIds]() {
|
||||||
DesktopImporter importer;
|
DesktopImporter importer;
|
||||||
QList<Game *> games = importer.importGames();
|
QList<Game *> games = importer.importGames();
|
||||||
|
QSet<QString> seenIds = existingIds;
|
||||||
|
QList<Game *> newGames;
|
||||||
|
newGames.reserve(games.size());
|
||||||
|
|
||||||
for (Game *game : games) {
|
for (Game *game : games) {
|
||||||
|
if (!game) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString id = game->id();
|
||||||
|
if (id.isEmpty() || seenIds.contains(id)) {
|
||||||
|
delete game;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
seenIds.insert(id);
|
||||||
|
newGames.append(game);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Game *game : newGames) {
|
||||||
game->moveToThread(this->thread());
|
game->moveToThread(this->thread());
|
||||||
game->setParent(nullptr);
|
game->setParent(nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
QMetaObject::invokeMethod(
|
QMetaObject::invokeMethod(
|
||||||
this,
|
this,
|
||||||
[this, games]() {
|
[this, newGames]() {
|
||||||
for (Game *game : games) {
|
for (Game *game : newGames) {
|
||||||
m_gameModel->addGame(game);
|
m_gameModel->addGame(game);
|
||||||
}
|
}
|
||||||
setImportStatus(tr("Desktop import complete: %1 games found").arg(games.count()));
|
setImportStatus(tr("Desktop import complete: %1 new games found").arg(newGames.count()));
|
||||||
setImporting(false);
|
setImporting(false);
|
||||||
saveLibrary();
|
saveLibrary();
|
||||||
Q_EMIT importCompleted(games.count());
|
Q_EMIT importCompleted(newGames.count());
|
||||||
},
|
},
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
});
|
});
|
||||||
|
|
@ -700,28 +868,57 @@ void App::importFromBottles()
|
||||||
if (m_importing)
|
if (m_importing)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
const QSet<QString> existingIds = [this]() {
|
||||||
|
QSet<QString> ids;
|
||||||
|
const QList<Game *> games = m_gameModel->allGames();
|
||||||
|
for (Game *game : games) {
|
||||||
|
if (game) {
|
||||||
|
ids.insert(game->id());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}();
|
||||||
|
|
||||||
setImporting(true);
|
setImporting(true);
|
||||||
setImportStatus(tr("Scanning Bottles..."));
|
setImportStatus(tr("Scanning Bottles..."));
|
||||||
|
|
||||||
[[maybe_unused]] auto future = QtConcurrent::run([this]() {
|
[[maybe_unused]] auto future = QtConcurrent::run([this, existingIds]() {
|
||||||
BottlesImporter importer;
|
BottlesImporter importer;
|
||||||
QList<Game *> games = importer.importGames();
|
QList<Game *> games = importer.importGames();
|
||||||
|
QSet<QString> seenIds = existingIds;
|
||||||
|
QList<Game *> newGames;
|
||||||
|
newGames.reserve(games.size());
|
||||||
|
|
||||||
for (Game *game : games) {
|
for (Game *game : games) {
|
||||||
|
if (!game) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString id = game->id();
|
||||||
|
if (id.isEmpty() || seenIds.contains(id)) {
|
||||||
|
delete game;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
seenIds.insert(id);
|
||||||
|
newGames.append(game);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Game *game : newGames) {
|
||||||
game->moveToThread(this->thread());
|
game->moveToThread(this->thread());
|
||||||
game->setParent(nullptr);
|
game->setParent(nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
QMetaObject::invokeMethod(
|
QMetaObject::invokeMethod(
|
||||||
this,
|
this,
|
||||||
[this, games]() {
|
[this, newGames]() {
|
||||||
for (Game *game : games) {
|
for (Game *game : newGames) {
|
||||||
m_gameModel->addGame(game);
|
m_gameModel->addGame(game);
|
||||||
}
|
}
|
||||||
setImportStatus(tr("Bottles import complete: %1 games found").arg(games.count()));
|
setImportStatus(tr("Bottles import complete: %1 new games found").arg(newGames.count()));
|
||||||
setImporting(false);
|
setImporting(false);
|
||||||
saveLibrary();
|
saveLibrary();
|
||||||
Q_EMIT importCompleted(games.count());
|
Q_EMIT importCompleted(newGames.count());
|
||||||
},
|
},
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
});
|
});
|
||||||
|
|
@ -732,28 +929,57 @@ void App::importFromFlatpak()
|
||||||
if (m_importing)
|
if (m_importing)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
const QSet<QString> existingIds = [this]() {
|
||||||
|
QSet<QString> ids;
|
||||||
|
const QList<Game *> games = m_gameModel->allGames();
|
||||||
|
for (Game *game : games) {
|
||||||
|
if (game) {
|
||||||
|
ids.insert(game->id());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}();
|
||||||
|
|
||||||
setImporting(true);
|
setImporting(true);
|
||||||
setImportStatus(tr("Scanning Flatpak games..."));
|
setImportStatus(tr("Scanning Flatpak games..."));
|
||||||
|
|
||||||
[[maybe_unused]] auto future = QtConcurrent::run([this]() {
|
[[maybe_unused]] auto future = QtConcurrent::run([this, existingIds]() {
|
||||||
FlatpakImporter importer;
|
FlatpakImporter importer;
|
||||||
QList<Game *> games = importer.importGames();
|
QList<Game *> games = importer.importGames();
|
||||||
|
QSet<QString> seenIds = existingIds;
|
||||||
|
QList<Game *> newGames;
|
||||||
|
newGames.reserve(games.size());
|
||||||
|
|
||||||
for (Game *game : games) {
|
for (Game *game : games) {
|
||||||
|
if (!game) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString id = game->id();
|
||||||
|
if (id.isEmpty() || seenIds.contains(id)) {
|
||||||
|
delete game;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
seenIds.insert(id);
|
||||||
|
newGames.append(game);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Game *game : newGames) {
|
||||||
game->moveToThread(this->thread());
|
game->moveToThread(this->thread());
|
||||||
game->setParent(nullptr);
|
game->setParent(nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
QMetaObject::invokeMethod(
|
QMetaObject::invokeMethod(
|
||||||
this,
|
this,
|
||||||
[this, games]() {
|
[this, newGames]() {
|
||||||
for (Game *game : games) {
|
for (Game *game : newGames) {
|
||||||
m_gameModel->addGame(game);
|
m_gameModel->addGame(game);
|
||||||
}
|
}
|
||||||
setImportStatus(tr("Flatpak import complete: %1 games found").arg(games.count()));
|
setImportStatus(tr("Flatpak import complete: %1 new games found").arg(newGames.count()));
|
||||||
setImporting(false);
|
setImporting(false);
|
||||||
saveLibrary();
|
saveLibrary();
|
||||||
Q_EMIT importCompleted(games.count());
|
Q_EMIT importCompleted(newGames.count());
|
||||||
},
|
},
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
});
|
});
|
||||||
|
|
@ -764,28 +990,57 @@ void App::importFromItch()
|
||||||
if (m_importing)
|
if (m_importing)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
const QSet<QString> existingIds = [this]() {
|
||||||
|
QSet<QString> ids;
|
||||||
|
const QList<Game *> games = m_gameModel->allGames();
|
||||||
|
for (Game *game : games) {
|
||||||
|
if (game) {
|
||||||
|
ids.insert(game->id());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}();
|
||||||
|
|
||||||
setImporting(true);
|
setImporting(true);
|
||||||
setImportStatus(tr("Scanning itch.io library..."));
|
setImportStatus(tr("Scanning itch.io library..."));
|
||||||
|
|
||||||
[[maybe_unused]] auto future = QtConcurrent::run([this]() {
|
[[maybe_unused]] auto future = QtConcurrent::run([this, existingIds]() {
|
||||||
ItchImporter importer;
|
ItchImporter importer;
|
||||||
QList<Game *> games = importer.importGames();
|
QList<Game *> games = importer.importGames();
|
||||||
|
QSet<QString> seenIds = existingIds;
|
||||||
|
QList<Game *> newGames;
|
||||||
|
newGames.reserve(games.size());
|
||||||
|
|
||||||
for (Game *game : games) {
|
for (Game *game : games) {
|
||||||
|
if (!game) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString id = game->id();
|
||||||
|
if (id.isEmpty() || seenIds.contains(id)) {
|
||||||
|
delete game;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
seenIds.insert(id);
|
||||||
|
newGames.append(game);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Game *game : newGames) {
|
||||||
game->moveToThread(this->thread());
|
game->moveToThread(this->thread());
|
||||||
game->setParent(nullptr);
|
game->setParent(nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
QMetaObject::invokeMethod(
|
QMetaObject::invokeMethod(
|
||||||
this,
|
this,
|
||||||
[this, games]() {
|
[this, newGames]() {
|
||||||
for (Game *game : games) {
|
for (Game *game : newGames) {
|
||||||
m_gameModel->addGame(game);
|
m_gameModel->addGame(game);
|
||||||
}
|
}
|
||||||
setImportStatus(tr("itch.io import complete: %1 games found").arg(games.count()));
|
setImportStatus(tr("itch.io import complete: %1 new games found").arg(newGames.count()));
|
||||||
setImporting(false);
|
setImporting(false);
|
||||||
saveLibrary();
|
saveLibrary();
|
||||||
Q_EMIT importCompleted(games.count());
|
Q_EMIT importCompleted(newGames.count());
|
||||||
},
|
},
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
});
|
});
|
||||||
|
|
@ -796,28 +1051,57 @@ void App::importFromLegendary()
|
||||||
if (m_importing)
|
if (m_importing)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
const QSet<QString> existingIds = [this]() {
|
||||||
|
QSet<QString> ids;
|
||||||
|
const QList<Game *> games = m_gameModel->allGames();
|
||||||
|
for (Game *game : games) {
|
||||||
|
if (game) {
|
||||||
|
ids.insert(game->id());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}();
|
||||||
|
|
||||||
setImporting(true);
|
setImporting(true);
|
||||||
setImportStatus(tr("Scanning Legendary library..."));
|
setImportStatus(tr("Scanning Legendary library..."));
|
||||||
|
|
||||||
[[maybe_unused]] auto future = QtConcurrent::run([this]() {
|
[[maybe_unused]] auto future = QtConcurrent::run([this, existingIds]() {
|
||||||
LegendaryImporter importer;
|
LegendaryImporter importer;
|
||||||
QList<Game *> games = importer.importGames();
|
QList<Game *> games = importer.importGames();
|
||||||
|
QSet<QString> seenIds = existingIds;
|
||||||
|
QList<Game *> newGames;
|
||||||
|
newGames.reserve(games.size());
|
||||||
|
|
||||||
for (Game *game : games) {
|
for (Game *game : games) {
|
||||||
|
if (!game) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString id = game->id();
|
||||||
|
if (id.isEmpty() || seenIds.contains(id)) {
|
||||||
|
delete game;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
seenIds.insert(id);
|
||||||
|
newGames.append(game);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Game *game : newGames) {
|
||||||
game->moveToThread(this->thread());
|
game->moveToThread(this->thread());
|
||||||
game->setParent(nullptr);
|
game->setParent(nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
QMetaObject::invokeMethod(
|
QMetaObject::invokeMethod(
|
||||||
this,
|
this,
|
||||||
[this, games]() {
|
[this, newGames]() {
|
||||||
for (Game *game : games) {
|
for (Game *game : newGames) {
|
||||||
m_gameModel->addGame(game);
|
m_gameModel->addGame(game);
|
||||||
}
|
}
|
||||||
setImportStatus(tr("Legendary import complete: %1 games found").arg(games.count()));
|
setImportStatus(tr("Legendary import complete: %1 new games found").arg(newGames.count()));
|
||||||
setImporting(false);
|
setImporting(false);
|
||||||
saveLibrary();
|
saveLibrary();
|
||||||
Q_EMIT importCompleted(games.count());
|
Q_EMIT importCompleted(newGames.count());
|
||||||
},
|
},
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
});
|
});
|
||||||
|
|
@ -828,28 +1112,57 @@ void App::importFromRetroArch()
|
||||||
if (m_importing)
|
if (m_importing)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
const QSet<QString> existingIds = [this]() {
|
||||||
|
QSet<QString> ids;
|
||||||
|
const QList<Game *> games = m_gameModel->allGames();
|
||||||
|
for (Game *game : games) {
|
||||||
|
if (game) {
|
||||||
|
ids.insert(game->id());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}();
|
||||||
|
|
||||||
setImporting(true);
|
setImporting(true);
|
||||||
setImportStatus(tr("Scanning RetroArch playlists..."));
|
setImportStatus(tr("Scanning RetroArch playlists..."));
|
||||||
|
|
||||||
[[maybe_unused]] auto future = QtConcurrent::run([this]() {
|
[[maybe_unused]] auto future = QtConcurrent::run([this, existingIds]() {
|
||||||
RetroArchImporter importer;
|
RetroArchImporter importer;
|
||||||
QList<Game *> games = importer.importGames();
|
QList<Game *> games = importer.importGames();
|
||||||
|
QSet<QString> seenIds = existingIds;
|
||||||
|
QList<Game *> newGames;
|
||||||
|
newGames.reserve(games.size());
|
||||||
|
|
||||||
for (Game *game : games) {
|
for (Game *game : games) {
|
||||||
|
if (!game) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString id = game->id();
|
||||||
|
if (id.isEmpty() || seenIds.contains(id)) {
|
||||||
|
delete game;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
seenIds.insert(id);
|
||||||
|
newGames.append(game);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Game *game : newGames) {
|
||||||
game->moveToThread(this->thread());
|
game->moveToThread(this->thread());
|
||||||
game->setParent(nullptr);
|
game->setParent(nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
QMetaObject::invokeMethod(
|
QMetaObject::invokeMethod(
|
||||||
this,
|
this,
|
||||||
[this, games]() {
|
[this, newGames]() {
|
||||||
for (Game *game : games) {
|
for (Game *game : newGames) {
|
||||||
m_gameModel->addGame(game);
|
m_gameModel->addGame(game);
|
||||||
}
|
}
|
||||||
setImportStatus(tr("RetroArch import complete: %1 games found").arg(games.count()));
|
setImportStatus(tr("RetroArch import complete: %1 new games found").arg(newGames.count()));
|
||||||
setImporting(false);
|
setImporting(false);
|
||||||
saveLibrary();
|
saveLibrary();
|
||||||
Q_EMIT importCompleted(games.count());
|
Q_EMIT importCompleted(newGames.count());
|
||||||
},
|
},
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
});
|
});
|
||||||
|
|
@ -952,19 +1265,47 @@ void App::loadLibrary()
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
|
const QByteArray data = file.readAll();
|
||||||
if (!doc.isArray()) {
|
QJsonParseError parseError;
|
||||||
|
QJsonDocument doc = QJsonDocument::fromJson(data, &parseError);
|
||||||
|
if (parseError.error != QJsonParseError::NoError) {
|
||||||
|
qWarning() << "Failed to parse library.json:" << parseError.errorString() << "at offset" << parseError.offset;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonArray gamesArray = doc.array();
|
QJsonArray gamesArray;
|
||||||
for (const QJsonValue &value : gamesArray) {
|
if (doc.isArray()) {
|
||||||
if (value.isObject()) {
|
gamesArray = doc.array();
|
||||||
Game *game = Game::fromJson(value.toObject(), this);
|
} else if (doc.isObject()) {
|
||||||
if (game) {
|
const QJsonObject rootObj = doc.object();
|
||||||
m_gameModel->addGame(game);
|
const QJsonValue gamesValue = rootObj.value(QStringLiteral("games"));
|
||||||
}
|
if (gamesValue.isArray()) {
|
||||||
|
gamesArray = gamesValue.toArray();
|
||||||
|
} else {
|
||||||
|
qWarning() << "Invalid library.json format: expected a JSON array (or an object with a 'games' array)";
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
qWarning() << "Invalid library.json format: expected a JSON array (or an object with a 'games' array)";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_gameModel->clear();
|
||||||
|
|
||||||
|
for (int i = 0; i < gamesArray.size(); ++i) {
|
||||||
|
const QJsonValue value = gamesArray.at(i);
|
||||||
|
if (!value.isObject()) {
|
||||||
|
qWarning() << "Skipping invalid library entry (not an object) at index" << i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Game *game = Game::fromJson(value.toObject(), this);
|
||||||
|
if (!game) {
|
||||||
|
qWarning() << "Skipping invalid game entry at index" << i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_gameModel->addGame(game);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "gamelauncher.h"
|
#include "gamelauncher.h"
|
||||||
#include "gamemodel.h"
|
#include "gamemodel.h"
|
||||||
|
#include "mediamanager.h"
|
||||||
#include "steamgriddb.h"
|
#include "steamgriddb.h"
|
||||||
|
|
||||||
class App : public QObject
|
class App : public QObject
|
||||||
|
|
@ -22,6 +23,7 @@ class App : public QObject
|
||||||
Q_PROPERTY(GameModel *gameModel READ gameModel CONSTANT)
|
Q_PROPERTY(GameModel *gameModel READ gameModel CONSTANT)
|
||||||
Q_PROPERTY(GameLauncher *launcher READ launcher CONSTANT)
|
Q_PROPERTY(GameLauncher *launcher READ launcher CONSTANT)
|
||||||
Q_PROPERTY(SteamGridDB *steamGridDB READ steamGridDB CONSTANT)
|
Q_PROPERTY(SteamGridDB *steamGridDB READ steamGridDB CONSTANT)
|
||||||
|
Q_PROPERTY(MediaManager *mediaManager READ mediaManager CONSTANT)
|
||||||
Q_PROPERTY(Config *config READ config CONSTANT)
|
Q_PROPERTY(Config *config READ config CONSTANT)
|
||||||
Q_PROPERTY(bool importing READ importing NOTIFY importingChanged)
|
Q_PROPERTY(bool importing READ importing NOTIFY importingChanged)
|
||||||
Q_PROPERTY(QString importStatus READ importStatus NOTIFY importStatusChanged)
|
Q_PROPERTY(QString importStatus READ importStatus NOTIFY importStatusChanged)
|
||||||
|
|
@ -33,6 +35,7 @@ public:
|
||||||
GameModel *gameModel() const;
|
GameModel *gameModel() const;
|
||||||
GameLauncher *launcher() const;
|
GameLauncher *launcher() const;
|
||||||
SteamGridDB *steamGridDB() const;
|
SteamGridDB *steamGridDB() const;
|
||||||
|
MediaManager *mediaManager() const;
|
||||||
Config *config() const;
|
Config *config() const;
|
||||||
|
|
||||||
bool importing() const;
|
bool importing() const;
|
||||||
|
|
@ -72,6 +75,7 @@ private:
|
||||||
GameModel *m_gameModel;
|
GameModel *m_gameModel;
|
||||||
GameLauncher *m_launcher;
|
GameLauncher *m_launcher;
|
||||||
SteamGridDB *m_steamGridDB;
|
SteamGridDB *m_steamGridDB;
|
||||||
|
MediaManager *m_mediaManager;
|
||||||
Config *m_config;
|
Config *m_config;
|
||||||
|
|
||||||
bool m_importing = false;
|
bool m_importing = false;
|
||||||
|
|
|
||||||
|
|
@ -279,6 +279,74 @@ void Config::setTheme(const QString &theme)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int Config::windowWidth() const
|
||||||
|
{
|
||||||
|
return m_windowWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Config::setWindowWidth(int width)
|
||||||
|
{
|
||||||
|
width = qBound(0, width, 10000);
|
||||||
|
if (m_windowWidth != width) {
|
||||||
|
m_windowWidth = width;
|
||||||
|
save();
|
||||||
|
Q_EMIT windowWidthChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int Config::windowHeight() const
|
||||||
|
{
|
||||||
|
return m_windowHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Config::setWindowHeight(int height)
|
||||||
|
{
|
||||||
|
height = qBound(0, height, 10000);
|
||||||
|
if (m_windowHeight != height) {
|
||||||
|
m_windowHeight = height;
|
||||||
|
save();
|
||||||
|
Q_EMIT windowHeightChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Config::windowMaximized() const
|
||||||
|
{
|
||||||
|
return m_windowMaximized;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Config::setWindowMaximized(bool maximized)
|
||||||
|
{
|
||||||
|
if (m_windowMaximized != maximized) {
|
||||||
|
m_windowMaximized = maximized;
|
||||||
|
save();
|
||||||
|
Q_EMIT windowMaximizedChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Config::setWindowSize(int width, int height)
|
||||||
|
{
|
||||||
|
width = qBound(0, width, 10000);
|
||||||
|
height = qBound(0, height, 10000);
|
||||||
|
|
||||||
|
const bool widthChanged = m_windowWidth != width;
|
||||||
|
const bool heightChanged = m_windowHeight != height;
|
||||||
|
|
||||||
|
if (!widthChanged && !heightChanged) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_windowWidth = width;
|
||||||
|
m_windowHeight = height;
|
||||||
|
save();
|
||||||
|
|
||||||
|
if (widthChanged) {
|
||||||
|
Q_EMIT windowWidthChanged();
|
||||||
|
}
|
||||||
|
if (heightChanged) {
|
||||||
|
Q_EMIT windowHeightChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Config::save()
|
void Config::save()
|
||||||
{
|
{
|
||||||
const auto config = KSharedConfig::openConfig(QStringLiteral("alakarterc"));
|
const auto config = KSharedConfig::openConfig(QStringLiteral("alakarterc"));
|
||||||
|
|
@ -308,6 +376,11 @@ void Config::save()
|
||||||
behaviorGroup.writeEntry(QStringLiteral("exitAfterLaunch"), m_exitAfterLaunch);
|
behaviorGroup.writeEntry(QStringLiteral("exitAfterLaunch"), m_exitAfterLaunch);
|
||||||
behaviorGroup.writeEntry(QStringLiteral("coverLaunchesGame"), m_coverLaunchesGame);
|
behaviorGroup.writeEntry(QStringLiteral("coverLaunchesGame"), m_coverLaunchesGame);
|
||||||
|
|
||||||
|
KConfigGroup windowGroup(config, QStringLiteral("Window"));
|
||||||
|
windowGroup.writeEntry(QStringLiteral("width"), m_windowWidth);
|
||||||
|
windowGroup.writeEntry(QStringLiteral("height"), m_windowHeight);
|
||||||
|
windowGroup.writeEntry(QStringLiteral("maximized"), m_windowMaximized);
|
||||||
|
|
||||||
config->sync();
|
config->sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -339,6 +412,11 @@ void Config::load()
|
||||||
m_animatedCovers = behaviorGroup.readEntry(QStringLiteral("animatedCovers"), false);
|
m_animatedCovers = behaviorGroup.readEntry(QStringLiteral("animatedCovers"), false);
|
||||||
m_exitAfterLaunch = behaviorGroup.readEntry(QStringLiteral("exitAfterLaunch"), false);
|
m_exitAfterLaunch = behaviorGroup.readEntry(QStringLiteral("exitAfterLaunch"), false);
|
||||||
m_coverLaunchesGame = behaviorGroup.readEntry(QStringLiteral("coverLaunchesGame"), false);
|
m_coverLaunchesGame = behaviorGroup.readEntry(QStringLiteral("coverLaunchesGame"), false);
|
||||||
|
|
||||||
|
const KConfigGroup windowGroup(config, QStringLiteral("Window"));
|
||||||
|
m_windowWidth = windowGroup.readEntry(QStringLiteral("width"), 0);
|
||||||
|
m_windowHeight = windowGroup.readEntry(QStringLiteral("height"), 0);
|
||||||
|
m_windowMaximized = windowGroup.readEntry(QStringLiteral("maximized"), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Config::resetToDefaults()
|
void Config::resetToDefaults()
|
||||||
|
|
@ -363,6 +441,10 @@ void Config::resetToDefaults()
|
||||||
m_animatedCovers = false;
|
m_animatedCovers = false;
|
||||||
m_theme.clear();
|
m_theme.clear();
|
||||||
|
|
||||||
|
m_windowWidth = 0;
|
||||||
|
m_windowHeight = 0;
|
||||||
|
m_windowMaximized = false;
|
||||||
|
|
||||||
save();
|
save();
|
||||||
|
|
||||||
Q_EMIT viewModeChanged();
|
Q_EMIT viewModeChanged();
|
||||||
|
|
@ -384,4 +466,7 @@ void Config::resetToDefaults()
|
||||||
Q_EMIT highQualityImagesChanged();
|
Q_EMIT highQualityImagesChanged();
|
||||||
Q_EMIT animatedCoversChanged();
|
Q_EMIT animatedCoversChanged();
|
||||||
Q_EMIT themeChanged();
|
Q_EMIT themeChanged();
|
||||||
|
Q_EMIT windowWidthChanged();
|
||||||
|
Q_EMIT windowHeightChanged();
|
||||||
|
Q_EMIT windowMaximizedChanged();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
23
src/config.h
23
src/config.h
|
|
@ -30,6 +30,9 @@ class Config : public QObject
|
||||||
Q_PROPERTY(bool animatedCovers READ animatedCovers WRITE setAnimatedCovers NOTIFY animatedCoversChanged)
|
Q_PROPERTY(bool animatedCovers READ animatedCovers WRITE setAnimatedCovers NOTIFY animatedCoversChanged)
|
||||||
Q_PROPERTY(bool coverLaunchesGame READ coverLaunchesGame WRITE setCoverLaunchesGame NOTIFY coverLaunchesGameChanged)
|
Q_PROPERTY(bool coverLaunchesGame READ coverLaunchesGame WRITE setCoverLaunchesGame NOTIFY coverLaunchesGameChanged)
|
||||||
Q_PROPERTY(QString theme READ theme WRITE setTheme NOTIFY themeChanged)
|
Q_PROPERTY(QString theme READ theme WRITE setTheme NOTIFY themeChanged)
|
||||||
|
Q_PROPERTY(int windowWidth READ windowWidth WRITE setWindowWidth NOTIFY windowWidthChanged)
|
||||||
|
Q_PROPERTY(int windowHeight READ windowHeight WRITE setWindowHeight NOTIFY windowHeightChanged)
|
||||||
|
Q_PROPERTY(bool windowMaximized READ windowMaximized WRITE setWindowMaximized NOTIFY windowMaximizedChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
enum ViewMode {
|
enum ViewMode {
|
||||||
|
|
@ -42,7 +45,7 @@ public:
|
||||||
enum UiMode {
|
enum UiMode {
|
||||||
Auto,
|
Auto,
|
||||||
Desktop,
|
Desktop,
|
||||||
Handheld
|
Couch
|
||||||
};
|
};
|
||||||
Q_ENUM(UiMode)
|
Q_ENUM(UiMode)
|
||||||
|
|
||||||
|
|
@ -105,10 +108,21 @@ public:
|
||||||
QString theme() const;
|
QString theme() const;
|
||||||
void setTheme(const QString &theme);
|
void setTheme(const QString &theme);
|
||||||
|
|
||||||
|
int windowWidth() const;
|
||||||
|
void setWindowWidth(int width);
|
||||||
|
|
||||||
|
int windowHeight() const;
|
||||||
|
void setWindowHeight(int height);
|
||||||
|
|
||||||
|
bool windowMaximized() const;
|
||||||
|
void setWindowMaximized(bool maximized);
|
||||||
|
|
||||||
Q_INVOKABLE void save();
|
Q_INVOKABLE void save();
|
||||||
Q_INVOKABLE void load();
|
Q_INVOKABLE void load();
|
||||||
Q_INVOKABLE void resetToDefaults();
|
Q_INVOKABLE void resetToDefaults();
|
||||||
|
|
||||||
|
Q_INVOKABLE void setWindowSize(int width, int height);
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void viewModeChanged();
|
void viewModeChanged();
|
||||||
void uiModeChanged();
|
void uiModeChanged();
|
||||||
|
|
@ -129,6 +143,9 @@ Q_SIGNALS:
|
||||||
void animatedCoversChanged();
|
void animatedCoversChanged();
|
||||||
void coverLaunchesGameChanged();
|
void coverLaunchesGameChanged();
|
||||||
void themeChanged();
|
void themeChanged();
|
||||||
|
void windowWidthChanged();
|
||||||
|
void windowHeightChanged();
|
||||||
|
void windowMaximizedChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
ViewMode m_viewMode = GridView;
|
ViewMode m_viewMode = GridView;
|
||||||
|
|
@ -150,4 +167,8 @@ private:
|
||||||
bool m_highQualityImages = true;
|
bool m_highQualityImages = true;
|
||||||
bool m_animatedCovers = false;
|
bool m_animatedCovers = false;
|
||||||
QString m_theme;
|
QString m_theme;
|
||||||
|
|
||||||
|
int m_windowWidth = 0;
|
||||||
|
int m_windowHeight = 0;
|
||||||
|
bool m_windowMaximized = false;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,13 @@ QStringList DesktopImporter::getDesktopFilePaths() const
|
||||||
// Add common system locations
|
// Add common system locations
|
||||||
appDirs << QStringLiteral("/usr/share/applications");
|
appDirs << QStringLiteral("/usr/share/applications");
|
||||||
appDirs << QStringLiteral("/usr/local/share/applications");
|
appDirs << QStringLiteral("/usr/local/share/applications");
|
||||||
appDirs << expandPath(QStringLiteral("~/.local/share/applications"));
|
|
||||||
|
const QString xdgDataHome = qEnvironmentVariable("XDG_DATA_HOME");
|
||||||
|
if (!xdgDataHome.isEmpty()) {
|
||||||
|
appDirs << QDir(xdgDataHome).absoluteFilePath(QStringLiteral("applications"));
|
||||||
|
} else {
|
||||||
|
appDirs << expandPath(QStringLiteral("~/.local/share/applications"));
|
||||||
|
}
|
||||||
|
|
||||||
// Flatpak export directories
|
// Flatpak export directories
|
||||||
appDirs << expandPath(QStringLiteral("~/.local/share/flatpak/exports/share/applications"));
|
appDirs << expandPath(QStringLiteral("~/.local/share/flatpak/exports/share/applications"));
|
||||||
|
|
|
||||||
379
src/game.cpp
379
src/game.cpp
|
|
@ -4,6 +4,8 @@
|
||||||
#include "game.h"
|
#include "game.h"
|
||||||
|
|
||||||
#include <KLocalizedString>
|
#include <KLocalizedString>
|
||||||
|
#include <QCryptographicHash>
|
||||||
|
#include <QJsonValue>
|
||||||
#include <QLocale>
|
#include <QLocale>
|
||||||
#include <QUuid>
|
#include <QUuid>
|
||||||
|
|
||||||
|
|
@ -131,6 +133,186 @@ void Game::setWorkingDirectory(const QString &dir)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QVariantMap Game::launchEnv() const
|
||||||
|
{
|
||||||
|
return m_launchEnv;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::setLaunchEnv(const QVariantMap &env)
|
||||||
|
{
|
||||||
|
if (m_launchEnv != env) {
|
||||||
|
m_launchEnv = env;
|
||||||
|
Q_EMIT launchEnvChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Game::launchRunner() const
|
||||||
|
{
|
||||||
|
return m_launchRunner;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::setLaunchRunner(const QString &runner)
|
||||||
|
{
|
||||||
|
if (m_launchRunner != runner) {
|
||||||
|
m_launchRunner = runner;
|
||||||
|
Q_EMIT launchRunnerChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Game::launchRunnerPath() const
|
||||||
|
{
|
||||||
|
return m_launchRunnerPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::setLaunchRunnerPath(const QString &path)
|
||||||
|
{
|
||||||
|
if (m_launchRunnerPath != path) {
|
||||||
|
m_launchRunnerPath = path;
|
||||||
|
Q_EMIT launchRunnerPathChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Game::launchPrefixPath() const
|
||||||
|
{
|
||||||
|
return m_launchPrefixPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::setLaunchPrefixPath(const QString &path)
|
||||||
|
{
|
||||||
|
if (m_launchPrefixPath != path) {
|
||||||
|
m_launchPrefixPath = path;
|
||||||
|
Q_EMIT launchPrefixPathChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Game::activeLaunchProfile() const
|
||||||
|
{
|
||||||
|
return m_activeLaunchProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::setActiveLaunchProfile(const QString &profileId)
|
||||||
|
{
|
||||||
|
const QString normalized = profileId.trimmed().isEmpty() ? QStringLiteral("default") : profileId.trimmed();
|
||||||
|
if (m_activeLaunchProfile != normalized) {
|
||||||
|
m_activeLaunchProfile = normalized;
|
||||||
|
Q_EMIT activeLaunchProfileChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap Game::launchProfileConfig(const QString &profileId) const
|
||||||
|
{
|
||||||
|
const QString id = profileId.trimmed();
|
||||||
|
if (id.isEmpty() || id == QLatin1String("default")) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const QVariant value = m_launchProfiles.value(id);
|
||||||
|
if (!value.isValid()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.toMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::setLaunchProfileConfig(const QString &profileId, const QVariantMap &config)
|
||||||
|
{
|
||||||
|
const QString id = profileId.trimmed();
|
||||||
|
if (id.isEmpty() || id == QLatin1String("default")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap cleaned;
|
||||||
|
|
||||||
|
const QString runner = config.value(QStringLiteral("runner")).toString().trimmed();
|
||||||
|
if (!runner.isEmpty()) {
|
||||||
|
cleaned.insert(QStringLiteral("runner"), runner);
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString runnerPath = config.value(QStringLiteral("runnerPath")).toString().trimmed();
|
||||||
|
if (!runnerPath.isEmpty()) {
|
||||||
|
cleaned.insert(QStringLiteral("runnerPath"), runnerPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString prefixPath = config.value(QStringLiteral("prefixPath")).toString().trimmed();
|
||||||
|
if (!prefixPath.isEmpty()) {
|
||||||
|
cleaned.insert(QStringLiteral("prefixPath"), prefixPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const QVariantMap env = config.value(QStringLiteral("env")).toMap();
|
||||||
|
if (!env.isEmpty()) {
|
||||||
|
cleaned.insert(QStringLiteral("env"), env);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cleaned.isEmpty()) {
|
||||||
|
m_launchProfiles.remove(id);
|
||||||
|
} else {
|
||||||
|
m_launchProfiles.insert(id, cleaned);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap Game::effectiveLaunchConfig() const
|
||||||
|
{
|
||||||
|
return effectiveLaunchConfigForProfile(m_activeLaunchProfile);
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap Game::effectiveLaunchConfigForProfile(const QString &profileId) const
|
||||||
|
{
|
||||||
|
QVariantMap result;
|
||||||
|
|
||||||
|
if (!m_launchRunner.isEmpty()) {
|
||||||
|
result.insert(QStringLiteral("runner"), m_launchRunner);
|
||||||
|
}
|
||||||
|
if (!m_launchRunnerPath.isEmpty()) {
|
||||||
|
result.insert(QStringLiteral("runnerPath"), m_launchRunnerPath);
|
||||||
|
}
|
||||||
|
if (!m_launchPrefixPath.isEmpty()) {
|
||||||
|
result.insert(QStringLiteral("prefixPath"), m_launchPrefixPath);
|
||||||
|
}
|
||||||
|
if (!m_launchEnv.isEmpty()) {
|
||||||
|
result.insert(QStringLiteral("env"), m_launchEnv);
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString id = profileId.trimmed();
|
||||||
|
if (id.isEmpty() || id == QLatin1String("default")) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QVariantMap profileConfig = launchProfileConfig(id);
|
||||||
|
if (profileConfig.isEmpty()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString runner = profileConfig.value(QStringLiteral("runner")).toString().trimmed();
|
||||||
|
if (!runner.isEmpty()) {
|
||||||
|
result.insert(QStringLiteral("runner"), runner);
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString runnerPath = profileConfig.value(QStringLiteral("runnerPath")).toString().trimmed();
|
||||||
|
if (!runnerPath.isEmpty()) {
|
||||||
|
result.insert(QStringLiteral("runnerPath"), runnerPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString prefixPath = profileConfig.value(QStringLiteral("prefixPath")).toString().trimmed();
|
||||||
|
if (!prefixPath.isEmpty()) {
|
||||||
|
result.insert(QStringLiteral("prefixPath"), prefixPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const QVariantMap profileEnv = profileConfig.value(QStringLiteral("env")).toMap();
|
||||||
|
if (!profileEnv.isEmpty()) {
|
||||||
|
QVariantMap mergedEnv;
|
||||||
|
const QVariantMap baseEnv = result.value(QStringLiteral("env")).toMap();
|
||||||
|
for (auto it = baseEnv.constBegin(); it != baseEnv.constEnd(); ++it) {
|
||||||
|
mergedEnv.insert(it.key(), it.value());
|
||||||
|
}
|
||||||
|
for (auto it = profileEnv.constBegin(); it != profileEnv.constEnd(); ++it) {
|
||||||
|
mergedEnv.insert(it.key(), it.value());
|
||||||
|
}
|
||||||
|
result.insert(QStringLiteral("env"), mergedEnv);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
QString Game::platform() const
|
QString Game::platform() const
|
||||||
{
|
{
|
||||||
return m_platform;
|
return m_platform;
|
||||||
|
|
@ -268,6 +450,84 @@ QJsonObject Game::toJson() const
|
||||||
obj[QStringLiteral("favorite")] = m_favorite;
|
obj[QStringLiteral("favorite")] = m_favorite;
|
||||||
obj[QStringLiteral("hidden")] = m_hidden;
|
obj[QStringLiteral("hidden")] = m_hidden;
|
||||||
obj[QStringLiteral("installed")] = m_installed;
|
obj[QStringLiteral("installed")] = m_installed;
|
||||||
|
|
||||||
|
const bool hasLaunchConfig = !m_launchEnv.isEmpty() || !m_launchRunner.isEmpty() || !m_launchRunnerPath.isEmpty() || !m_launchPrefixPath.isEmpty()
|
||||||
|
|| !m_launchProfiles.isEmpty() || (!m_activeLaunchProfile.isEmpty() && m_activeLaunchProfile != QLatin1String("default"));
|
||||||
|
if (hasLaunchConfig) {
|
||||||
|
QJsonObject launchObj;
|
||||||
|
|
||||||
|
if (!m_launchEnv.isEmpty()) {
|
||||||
|
QJsonObject envObj;
|
||||||
|
for (auto it = m_launchEnv.constBegin(); it != m_launchEnv.constEnd(); ++it) {
|
||||||
|
envObj.insert(it.key(), QJsonValue::fromVariant(it.value()));
|
||||||
|
}
|
||||||
|
launchObj.insert(QStringLiteral("env"), envObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_launchRunner.isEmpty()) {
|
||||||
|
launchObj.insert(QStringLiteral("runner"), m_launchRunner);
|
||||||
|
}
|
||||||
|
if (!m_launchRunnerPath.isEmpty()) {
|
||||||
|
launchObj.insert(QStringLiteral("runnerPath"), m_launchRunnerPath);
|
||||||
|
}
|
||||||
|
if (!m_launchPrefixPath.isEmpty()) {
|
||||||
|
launchObj.insert(QStringLiteral("prefixPath"), m_launchPrefixPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_activeLaunchProfile.isEmpty() && m_activeLaunchProfile != QLatin1String("default")) {
|
||||||
|
launchObj.insert(QStringLiteral("activeProfile"), m_activeLaunchProfile);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_launchProfiles.isEmpty()) {
|
||||||
|
QJsonObject profilesObj;
|
||||||
|
for (auto it = m_launchProfiles.constBegin(); it != m_launchProfiles.constEnd(); ++it) {
|
||||||
|
const QString profileId = it.key();
|
||||||
|
if (profileId.isEmpty() || profileId == QLatin1String("default")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QVariantMap profileConfig = it.value().toMap();
|
||||||
|
if (profileConfig.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject profileObj;
|
||||||
|
|
||||||
|
const QString runner = profileConfig.value(QStringLiteral("runner")).toString();
|
||||||
|
if (!runner.isEmpty()) {
|
||||||
|
profileObj.insert(QStringLiteral("runner"), runner);
|
||||||
|
}
|
||||||
|
const QString runnerPath = profileConfig.value(QStringLiteral("runnerPath")).toString();
|
||||||
|
if (!runnerPath.isEmpty()) {
|
||||||
|
profileObj.insert(QStringLiteral("runnerPath"), runnerPath);
|
||||||
|
}
|
||||||
|
const QString prefixPath = profileConfig.value(QStringLiteral("prefixPath")).toString();
|
||||||
|
if (!prefixPath.isEmpty()) {
|
||||||
|
profileObj.insert(QStringLiteral("prefixPath"), prefixPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const QVariantMap env = profileConfig.value(QStringLiteral("env")).toMap();
|
||||||
|
if (!env.isEmpty()) {
|
||||||
|
QJsonObject envObj;
|
||||||
|
for (auto envIt = env.constBegin(); envIt != env.constEnd(); ++envIt) {
|
||||||
|
envObj.insert(envIt.key(), QJsonValue::fromVariant(envIt.value()));
|
||||||
|
}
|
||||||
|
profileObj.insert(QStringLiteral("env"), envObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!profileObj.isEmpty()) {
|
||||||
|
profilesObj.insert(profileId, profileObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!profilesObj.isEmpty()) {
|
||||||
|
launchObj.insert(QStringLiteral("profiles"), profilesObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
obj.insert(QStringLiteral("launch"), launchObj);
|
||||||
|
}
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -276,26 +536,135 @@ Game *Game::fromJson(const QJsonObject &json, QObject *parent)
|
||||||
QString id = json[QStringLiteral("id")].toString();
|
QString id = json[QStringLiteral("id")].toString();
|
||||||
QString name = json[QStringLiteral("name")].toString();
|
QString name = json[QStringLiteral("name")].toString();
|
||||||
|
|
||||||
if (id.isEmpty() || name.isEmpty()) {
|
if (name.isEmpty()) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const QString platformId = json[QStringLiteral("platformId")].toString();
|
||||||
|
const QString platform = json[QStringLiteral("platform")].toString();
|
||||||
|
const QString launchCommand = json[QStringLiteral("launchCommand")].toString();
|
||||||
|
const QString workingDirectory = json[QStringLiteral("workingDirectory")].toString();
|
||||||
|
if (id.isEmpty()) {
|
||||||
|
const QByteArray fingerprint =
|
||||||
|
(platformId + QLatin1Char('|') + platform + QLatin1Char('|') + name + QLatin1Char('|') + launchCommand + QLatin1Char('|') + workingDirectory)
|
||||||
|
.toUtf8();
|
||||||
|
const QByteArray hash = QCryptographicHash::hash(fingerprint, QCryptographicHash::Sha1).toHex();
|
||||||
|
id = QStringLiteral("legacy-") + QString::fromLatin1(hash);
|
||||||
|
}
|
||||||
|
|
||||||
Game *game = new Game(id, name, parent);
|
Game *game = new Game(id, name, parent);
|
||||||
game->setDescription(json[QStringLiteral("description")].toString());
|
game->setDescription(json[QStringLiteral("description")].toString());
|
||||||
game->setDeveloper(json[QStringLiteral("developer")].toString());
|
game->setDeveloper(json[QStringLiteral("developer")].toString());
|
||||||
game->setPublisher(json[QStringLiteral("publisher")].toString());
|
game->setPublisher(json[QStringLiteral("publisher")].toString());
|
||||||
game->setCoverUrl(QUrl(json[QStringLiteral("coverUrl")].toString()));
|
game->setCoverUrl(QUrl(json[QStringLiteral("coverUrl")].toString()));
|
||||||
game->setIconUrl(QUrl(json[QStringLiteral("iconUrl")].toString()));
|
game->setIconUrl(QUrl(json[QStringLiteral("iconUrl")].toString()));
|
||||||
game->setLaunchCommand(json[QStringLiteral("launchCommand")].toString());
|
game->setLaunchCommand(launchCommand);
|
||||||
game->setWorkingDirectory(json[QStringLiteral("workingDirectory")].toString());
|
game->setWorkingDirectory(workingDirectory);
|
||||||
game->setPlatform(json[QStringLiteral("platform")].toString());
|
game->setPlatform(platform);
|
||||||
game->setPlatformId(json[QStringLiteral("platformId")].toString());
|
game->setPlatformId(platformId);
|
||||||
game->setDateAdded(QDateTime::fromString(json[QStringLiteral("dateAdded")].toString(), Qt::ISODate));
|
game->setDateAdded(QDateTime::fromString(json[QStringLiteral("dateAdded")].toString(), Qt::ISODate));
|
||||||
game->setLastPlayed(QDateTime::fromString(json[QStringLiteral("lastPlayed")].toString(), Qt::ISODate));
|
game->setLastPlayed(QDateTime::fromString(json[QStringLiteral("lastPlayed")].toString(), Qt::ISODate));
|
||||||
game->setPlayTime(json[QStringLiteral("playTime")].toInteger());
|
game->setPlayTime(json[QStringLiteral("playTime")].toInteger());
|
||||||
game->setFavorite(json[QStringLiteral("favorite")].toBool());
|
game->setFavorite(json[QStringLiteral("favorite")].toBool());
|
||||||
game->setHidden(json[QStringLiteral("hidden")].toBool());
|
game->setHidden(json[QStringLiteral("hidden")].toBool());
|
||||||
game->setInstalled(json[QStringLiteral("installed")].toBool(true));
|
game->setInstalled(json[QStringLiteral("installed")].toBool(true));
|
||||||
|
|
||||||
|
QVariantMap env;
|
||||||
|
QString runner;
|
||||||
|
QString runnerPath;
|
||||||
|
QString prefixPath;
|
||||||
|
QString activeProfile;
|
||||||
|
const QJsonValue launchValue = json.value(QStringLiteral("launch"));
|
||||||
|
if (launchValue.isObject()) {
|
||||||
|
const QJsonObject launchObj = launchValue.toObject();
|
||||||
|
const QJsonValue envValue = launchObj.value(QStringLiteral("env"));
|
||||||
|
if (envValue.isObject()) {
|
||||||
|
env = envValue.toObject().toVariantMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
runner = launchObj.value(QStringLiteral("runner")).toString();
|
||||||
|
runnerPath = launchObj.value(QStringLiteral("runnerPath")).toString();
|
||||||
|
prefixPath = launchObj.value(QStringLiteral("prefixPath")).toString();
|
||||||
|
|
||||||
|
activeProfile = launchObj.value(QStringLiteral("activeProfile")).toString();
|
||||||
|
|
||||||
|
const QJsonValue profilesValue = launchObj.value(QStringLiteral("profiles"));
|
||||||
|
if (profilesValue.isObject()) {
|
||||||
|
const QJsonObject profilesObj = profilesValue.toObject();
|
||||||
|
for (auto it = profilesObj.constBegin(); it != profilesObj.constEnd(); ++it) {
|
||||||
|
if (!it.value().isObject()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString profileId = it.key();
|
||||||
|
if (profileId.isEmpty() || profileId == QLatin1String("default")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonObject profileObj = it.value().toObject();
|
||||||
|
QVariantMap profileConfig;
|
||||||
|
|
||||||
|
const QString pRunner = profileObj.value(QStringLiteral("runner")).toString();
|
||||||
|
if (!pRunner.isEmpty()) {
|
||||||
|
profileConfig.insert(QStringLiteral("runner"), pRunner);
|
||||||
|
}
|
||||||
|
const QString pRunnerPath = profileObj.value(QStringLiteral("runnerPath")).toString();
|
||||||
|
if (!pRunnerPath.isEmpty()) {
|
||||||
|
profileConfig.insert(QStringLiteral("runnerPath"), pRunnerPath);
|
||||||
|
}
|
||||||
|
const QString pPrefixPath = profileObj.value(QStringLiteral("prefixPath")).toString();
|
||||||
|
if (!pPrefixPath.isEmpty()) {
|
||||||
|
profileConfig.insert(QStringLiteral("prefixPath"), pPrefixPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonValue pEnvValue = profileObj.value(QStringLiteral("env"));
|
||||||
|
if (pEnvValue.isObject()) {
|
||||||
|
const QVariantMap pEnv = pEnvValue.toObject().toVariantMap();
|
||||||
|
if (!pEnv.isEmpty()) {
|
||||||
|
profileConfig.insert(QStringLiteral("env"), pEnv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!profileConfig.isEmpty()) {
|
||||||
|
game->setLaunchProfileConfig(profileId, profileConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonValue legacyEnvValue = json.value(QStringLiteral("launchEnv"));
|
||||||
|
if (env.isEmpty() && legacyEnvValue.isObject()) {
|
||||||
|
env = legacyEnvValue.toObject().toVariantMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runner.isEmpty()) {
|
||||||
|
runner = json.value(QStringLiteral("launchRunner")).toString();
|
||||||
|
}
|
||||||
|
if (runnerPath.isEmpty()) {
|
||||||
|
runnerPath = json.value(QStringLiteral("launchRunnerPath")).toString();
|
||||||
|
}
|
||||||
|
if (prefixPath.isEmpty()) {
|
||||||
|
prefixPath = json.value(QStringLiteral("launchPrefixPath")).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!env.isEmpty()) {
|
||||||
|
game->setLaunchEnv(env);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!runner.isEmpty()) {
|
||||||
|
game->setLaunchRunner(runner);
|
||||||
|
}
|
||||||
|
if (!runnerPath.isEmpty()) {
|
||||||
|
game->setLaunchRunnerPath(runnerPath);
|
||||||
|
}
|
||||||
|
if (!prefixPath.isEmpty()) {
|
||||||
|
game->setLaunchPrefixPath(prefixPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!activeProfile.isEmpty()) {
|
||||||
|
game->setActiveLaunchProfile(activeProfile);
|
||||||
|
}
|
||||||
|
|
||||||
return game;
|
return game;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
37
src/game.h
37
src/game.h
|
|
@ -9,6 +9,7 @@
|
||||||
#include <QQmlEngine>
|
#include <QQmlEngine>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
|
#include <QVariantMap>
|
||||||
|
|
||||||
class Game : public QObject
|
class Game : public QObject
|
||||||
{
|
{
|
||||||
|
|
@ -25,6 +26,11 @@ class Game : public QObject
|
||||||
Q_PROPERTY(QUrl iconUrl READ iconUrl WRITE setIconUrl NOTIFY iconUrlChanged)
|
Q_PROPERTY(QUrl iconUrl READ iconUrl WRITE setIconUrl NOTIFY iconUrlChanged)
|
||||||
Q_PROPERTY(QString launchCommand READ launchCommand WRITE setLaunchCommand NOTIFY launchCommandChanged)
|
Q_PROPERTY(QString launchCommand READ launchCommand WRITE setLaunchCommand NOTIFY launchCommandChanged)
|
||||||
Q_PROPERTY(QString workingDirectory READ workingDirectory WRITE setWorkingDirectory NOTIFY workingDirectoryChanged)
|
Q_PROPERTY(QString workingDirectory READ workingDirectory WRITE setWorkingDirectory NOTIFY workingDirectoryChanged)
|
||||||
|
Q_PROPERTY(QVariantMap launchEnv READ launchEnv WRITE setLaunchEnv NOTIFY launchEnvChanged)
|
||||||
|
Q_PROPERTY(QString launchRunner READ launchRunner WRITE setLaunchRunner NOTIFY launchRunnerChanged)
|
||||||
|
Q_PROPERTY(QString launchRunnerPath READ launchRunnerPath WRITE setLaunchRunnerPath NOTIFY launchRunnerPathChanged)
|
||||||
|
Q_PROPERTY(QString launchPrefixPath READ launchPrefixPath WRITE setLaunchPrefixPath NOTIFY launchPrefixPathChanged)
|
||||||
|
Q_PROPERTY(QString activeLaunchProfile READ activeLaunchProfile WRITE setActiveLaunchProfile NOTIFY activeLaunchProfileChanged)
|
||||||
Q_PROPERTY(QString platform READ platform WRITE setPlatform NOTIFY platformChanged)
|
Q_PROPERTY(QString platform READ platform WRITE setPlatform NOTIFY platformChanged)
|
||||||
Q_PROPERTY(QString platformId READ platformId WRITE setPlatformId NOTIFY platformIdChanged)
|
Q_PROPERTY(QString platformId READ platformId WRITE setPlatformId NOTIFY platformIdChanged)
|
||||||
Q_PROPERTY(QDateTime dateAdded READ dateAdded WRITE setDateAdded NOTIFY dateAddedChanged)
|
Q_PROPERTY(QDateTime dateAdded READ dateAdded WRITE setDateAdded NOTIFY dateAddedChanged)
|
||||||
|
|
@ -65,6 +71,26 @@ public:
|
||||||
QString workingDirectory() const;
|
QString workingDirectory() const;
|
||||||
void setWorkingDirectory(const QString &dir);
|
void setWorkingDirectory(const QString &dir);
|
||||||
|
|
||||||
|
QVariantMap launchEnv() const;
|
||||||
|
void setLaunchEnv(const QVariantMap &env);
|
||||||
|
|
||||||
|
QString launchRunner() const;
|
||||||
|
void setLaunchRunner(const QString &runner);
|
||||||
|
|
||||||
|
QString launchRunnerPath() const;
|
||||||
|
void setLaunchRunnerPath(const QString &path);
|
||||||
|
|
||||||
|
QString launchPrefixPath() const;
|
||||||
|
void setLaunchPrefixPath(const QString &path);
|
||||||
|
|
||||||
|
QString activeLaunchProfile() const;
|
||||||
|
void setActiveLaunchProfile(const QString &profileId);
|
||||||
|
|
||||||
|
Q_INVOKABLE QVariantMap launchProfileConfig(const QString &profileId) const;
|
||||||
|
Q_INVOKABLE void setLaunchProfileConfig(const QString &profileId, const QVariantMap &config);
|
||||||
|
QVariantMap effectiveLaunchConfigForProfile(const QString &profileId) const;
|
||||||
|
QVariantMap effectiveLaunchConfig() const;
|
||||||
|
|
||||||
QString platform() const;
|
QString platform() const;
|
||||||
void setPlatform(const QString &platform);
|
void setPlatform(const QString &platform);
|
||||||
|
|
||||||
|
|
@ -108,6 +134,11 @@ Q_SIGNALS:
|
||||||
void iconUrlChanged();
|
void iconUrlChanged();
|
||||||
void launchCommandChanged();
|
void launchCommandChanged();
|
||||||
void workingDirectoryChanged();
|
void workingDirectoryChanged();
|
||||||
|
void launchEnvChanged();
|
||||||
|
void launchRunnerChanged();
|
||||||
|
void launchRunnerPathChanged();
|
||||||
|
void launchPrefixPathChanged();
|
||||||
|
void activeLaunchProfileChanged();
|
||||||
void platformChanged();
|
void platformChanged();
|
||||||
void platformIdChanged();
|
void platformIdChanged();
|
||||||
void dateAddedChanged();
|
void dateAddedChanged();
|
||||||
|
|
@ -128,6 +159,12 @@ private:
|
||||||
QUrl m_iconUrl;
|
QUrl m_iconUrl;
|
||||||
QString m_launchCommand;
|
QString m_launchCommand;
|
||||||
QString m_workingDirectory;
|
QString m_workingDirectory;
|
||||||
|
QVariantMap m_launchEnv;
|
||||||
|
QString m_launchRunner;
|
||||||
|
QString m_launchRunnerPath;
|
||||||
|
QString m_launchPrefixPath;
|
||||||
|
QString m_activeLaunchProfile = QStringLiteral("default");
|
||||||
|
QVariantMap m_launchProfiles;
|
||||||
QString m_platform;
|
QString m_platform;
|
||||||
QString m_platformId;
|
QString m_platformId;
|
||||||
QDateTime m_dateAdded;
|
QDateTime m_dateAdded;
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,140 @@
|
||||||
|
|
||||||
#include "gamelauncher.h"
|
#include "gamelauncher.h"
|
||||||
#include "app.h"
|
#include "app.h"
|
||||||
|
#include "gamepadmanager.h"
|
||||||
|
#include "inputmanager.h"
|
||||||
|
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QDesktopServices>
|
#include <QDesktopServices>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QProcessEnvironment>
|
||||||
|
#include <QStandardPaths>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
|
|
||||||
|
static QStringList steamCandidateRoots()
|
||||||
|
{
|
||||||
|
const QString home = QDir::homePath();
|
||||||
|
return {
|
||||||
|
home + QStringLiteral("/.steam/root"),
|
||||||
|
home + QStringLiteral("/.steam/steam"),
|
||||||
|
home + QStringLiteral("/.local/share/Steam"),
|
||||||
|
home + QStringLiteral("/.var/app/com.valvesoftware.Steam/data/Steam"),
|
||||||
|
home + QStringLiteral("/.var/app/com.valvesoftware.Steam/.local/share/Steam"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString findSteamClientInstallPathFromProton(const QString &protonExe)
|
||||||
|
{
|
||||||
|
if (protonExe.isEmpty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QDir dir(QFileInfo(protonExe).absoluteDir());
|
||||||
|
for (int i = 0; i < 10; ++i) {
|
||||||
|
if (dir.exists(QStringLiteral("steamapps"))) {
|
||||||
|
return dir.absolutePath();
|
||||||
|
}
|
||||||
|
if (!dir.cdUp()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const QString &root : steamCandidateRoots()) {
|
||||||
|
if (QDir(root).exists(QStringLiteral("steamapps"))) {
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString discoverDefaultProtonExecutable()
|
||||||
|
{
|
||||||
|
static bool done = false;
|
||||||
|
static QString cached;
|
||||||
|
|
||||||
|
if (done) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
done = true;
|
||||||
|
|
||||||
|
QStringList candidates;
|
||||||
|
for (const QString &root : steamCandidateRoots()) {
|
||||||
|
const QString compatTools = root + QStringLiteral("/compatibilitytools.d");
|
||||||
|
QDir compatDir(compatTools);
|
||||||
|
if (compatDir.exists()) {
|
||||||
|
const QStringList toolDirs = compatDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
|
||||||
|
for (const QString &tool : toolDirs) {
|
||||||
|
const QString protonPath = compatTools + QLatin1Char('/') + tool + QStringLiteral("/proton");
|
||||||
|
if (QFileInfo::exists(protonPath)) {
|
||||||
|
candidates.append(protonPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString common = root + QStringLiteral("/steamapps/common");
|
||||||
|
QDir commonDir(common);
|
||||||
|
if (commonDir.exists()) {
|
||||||
|
const QStringList dirs = commonDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
|
||||||
|
for (const QString &d : dirs) {
|
||||||
|
if (!d.contains(QStringLiteral("Proton"), Qt::CaseInsensitive)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const QString protonPath = common + QLatin1Char('/') + d + QStringLiteral("/proton");
|
||||||
|
if (QFileInfo::exists(protonPath)) {
|
||||||
|
candidates.append(protonPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates.removeDuplicates();
|
||||||
|
candidates.sort();
|
||||||
|
|
||||||
|
for (const QString &c : candidates) {
|
||||||
|
if (c.contains(QStringLiteral("Proton - Experimental"))) {
|
||||||
|
cached = c;
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!candidates.isEmpty()) {
|
||||||
|
cached = candidates.constLast();
|
||||||
|
}
|
||||||
|
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString profileIdForCurrentUiMode()
|
||||||
|
{
|
||||||
|
const Config *config = App::instance() ? App::instance()->config() : nullptr;
|
||||||
|
if (!config) {
|
||||||
|
return QStringLiteral("default");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config->uiMode() == Config::Couch) {
|
||||||
|
return QStringLiteral("couch");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config->uiMode() == Config::Auto) {
|
||||||
|
GamepadManager *pad = GamepadManager::instance();
|
||||||
|
InputManager *input = InputManager::instance();
|
||||||
|
|
||||||
|
if (pad && pad->connected()) {
|
||||||
|
const bool activeGamepad = input && input->activeInput() == InputManager::Gamepad;
|
||||||
|
const bool noKeyboardMouse = input && !input->hasSeenKeyboardMouse();
|
||||||
|
if (activeGamepad || noKeyboardMouse) {
|
||||||
|
return QStringLiteral("couch");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return QStringLiteral("default");
|
||||||
|
}
|
||||||
|
|
||||||
GameLauncher::GameLauncher(QObject *parent)
|
GameLauncher::GameLauncher(QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
{
|
{
|
||||||
|
|
@ -34,6 +161,161 @@ bool GameLauncher::hasRunningGames() const
|
||||||
return !m_runningGames.isEmpty();
|
return !m_runningGames.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QVariantMap GameLauncher::resolveLaunchInfo(Game *game) const
|
||||||
|
{
|
||||||
|
QVariantMap info;
|
||||||
|
info.insert(QStringLiteral("ok"), false);
|
||||||
|
|
||||||
|
if (!game) {
|
||||||
|
info.insert(QStringLiteral("error"), tr("No game selected"));
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
info.insert(QStringLiteral("gameId"), game->id());
|
||||||
|
info.insert(QStringLiteral("gameName"), game->name());
|
||||||
|
info.insert(QStringLiteral("isRunning"), m_runningGames.contains(game->id()));
|
||||||
|
|
||||||
|
const QString command = game->launchCommand();
|
||||||
|
info.insert(QStringLiteral("launchCommand"), command);
|
||||||
|
if (command.isEmpty()) {
|
||||||
|
info.insert(QStringLiteral("error"), tr("No launch command configured"));
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString profileId = profileIdForCurrentUiMode();
|
||||||
|
const QVariantMap effectiveLaunchConfig = game->effectiveLaunchConfigForProfile(profileId);
|
||||||
|
const QString runner = effectiveLaunchConfig.value(QStringLiteral("runner")).toString().trimmed();
|
||||||
|
const QString runnerPath = effectiveLaunchConfig.value(QStringLiteral("runnerPath")).toString().trimmed();
|
||||||
|
const QString prefixPath = effectiveLaunchConfig.value(QStringLiteral("prefixPath")).toString().trimmed();
|
||||||
|
const QVariantMap launchEnv = effectiveLaunchConfig.value(QStringLiteral("env")).toMap();
|
||||||
|
const bool hasLaunchOverrides = !runner.isEmpty() || !runnerPath.isEmpty() || !prefixPath.isEmpty() || !launchEnv.isEmpty();
|
||||||
|
|
||||||
|
info.insert(QStringLiteral("runner"), runner);
|
||||||
|
info.insert(QStringLiteral("runnerPath"), runnerPath);
|
||||||
|
info.insert(QStringLiteral("prefixPath"), prefixPath);
|
||||||
|
info.insert(QStringLiteral("workingDirectory"), game->workingDirectory());
|
||||||
|
info.insert(QStringLiteral("hasLaunchOverrides"), hasLaunchOverrides);
|
||||||
|
|
||||||
|
QVariantMap envOverrides;
|
||||||
|
for (auto it = launchEnv.constBegin(); it != launchEnv.constEnd(); ++it) {
|
||||||
|
const QString key = it.key();
|
||||||
|
if (key.isEmpty() || key.contains(QLatin1Char('='))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
envOverrides.insert(key, it.value().toString());
|
||||||
|
}
|
||||||
|
info.insert(QStringLiteral("envOverrides"), envOverrides);
|
||||||
|
|
||||||
|
if (!hasLaunchOverrides && command.startsWith(QLatin1String("steam://"))) {
|
||||||
|
info.insert(QStringLiteral("ok"), true);
|
||||||
|
info.insert(QStringLiteral("launchType"), QStringLiteral("url"));
|
||||||
|
info.insert(QStringLiteral("url"), command);
|
||||||
|
info.insert(QStringLiteral("usesDesktopServices"), true);
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasLaunchOverrides && command.startsWith(QLatin1String("lutris "))) {
|
||||||
|
info.insert(QStringLiteral("ok"), true);
|
||||||
|
info.insert(QStringLiteral("launchType"), QStringLiteral("url"));
|
||||||
|
info.insert(QStringLiteral("url"), command.mid(7));
|
||||||
|
info.insert(QStringLiteral("usesDesktopServices"), true);
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList parts;
|
||||||
|
if (command.startsWith(QLatin1String("steam://"))) {
|
||||||
|
parts = {QStringLiteral("xdg-open"), command};
|
||||||
|
} else if (command.startsWith(QLatin1String("lutris "))) {
|
||||||
|
parts = {QStringLiteral("xdg-open"), command.mid(7)};
|
||||||
|
} else {
|
||||||
|
parts = QProcess::splitCommand(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parts.isEmpty()) {
|
||||||
|
info.insert(QStringLiteral("error"), tr("Invalid launch command"));
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString program = parts.takeFirst();
|
||||||
|
info.insert(QStringLiteral("program"), program);
|
||||||
|
info.insert(QStringLiteral("args"), parts);
|
||||||
|
|
||||||
|
QString finalProgram = program;
|
||||||
|
QStringList finalArgs = parts;
|
||||||
|
|
||||||
|
QVariantMap effectiveEnv = envOverrides;
|
||||||
|
|
||||||
|
if ((runner == QLatin1String("wine") || runner == QLatin1String("proton")) && (program == QLatin1String("xdg-open"))) {
|
||||||
|
info.insert(QStringLiteral("error"), tr("This runner cannot be used with URL-based launch commands"));
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runner == QLatin1String("custom")) {
|
||||||
|
if (runnerPath.isEmpty()) {
|
||||||
|
info.insert(QStringLiteral("error"), tr("Custom runner is enabled but no runner path is configured"));
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
finalProgram = runnerPath;
|
||||||
|
finalArgs = {program};
|
||||||
|
finalArgs.append(parts);
|
||||||
|
} else if (runner == QLatin1String("wine")) {
|
||||||
|
QString wineExe = runnerPath;
|
||||||
|
if (wineExe.isEmpty()) {
|
||||||
|
wineExe = QStandardPaths::findExecutable(QStringLiteral("wine"));
|
||||||
|
}
|
||||||
|
if (wineExe.isEmpty()) {
|
||||||
|
info.insert(QStringLiteral("error"), tr("Wine runner is enabled but Wine was not found"));
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString winePrefix = prefixPath;
|
||||||
|
if (winePrefix.isEmpty()) {
|
||||||
|
winePrefix = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/prefixes/") + game->id();
|
||||||
|
}
|
||||||
|
|
||||||
|
effectiveEnv.insert(QStringLiteral("WINEPREFIX"), winePrefix);
|
||||||
|
info.insert(QStringLiteral("resolvedPrefixPath"), winePrefix);
|
||||||
|
|
||||||
|
finalProgram = wineExe;
|
||||||
|
finalArgs = {program};
|
||||||
|
finalArgs.append(parts);
|
||||||
|
} else if (runner == QLatin1String("proton")) {
|
||||||
|
QString protonExe = runnerPath;
|
||||||
|
if (protonExe.isEmpty()) {
|
||||||
|
protonExe = discoverDefaultProtonExecutable();
|
||||||
|
}
|
||||||
|
if (protonExe.isEmpty()) {
|
||||||
|
info.insert(QStringLiteral("error"), tr("Proton runner is enabled but no Proton installation was found"));
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString steamInstallPath = findSteamClientInstallPathFromProton(protonExe);
|
||||||
|
info.insert(QStringLiteral("resolvedSteamInstallPath"), steamInstallPath);
|
||||||
|
|
||||||
|
QString compatDataPath = prefixPath;
|
||||||
|
if (compatDataPath.isEmpty()) {
|
||||||
|
compatDataPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/prefixes/") + game->id();
|
||||||
|
}
|
||||||
|
|
||||||
|
effectiveEnv.insert(QStringLiteral("STEAM_COMPAT_DATA_PATH"), compatDataPath);
|
||||||
|
info.insert(QStringLiteral("resolvedPrefixPath"), compatDataPath);
|
||||||
|
|
||||||
|
if (!steamInstallPath.isEmpty()) {
|
||||||
|
effectiveEnv.insert(QStringLiteral("STEAM_COMPAT_CLIENT_INSTALL_PATH"), steamInstallPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
finalProgram = protonExe;
|
||||||
|
finalArgs = {QStringLiteral("run"), program};
|
||||||
|
finalArgs.append(parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
info.insert(QStringLiteral("finalProgram"), finalProgram);
|
||||||
|
info.insert(QStringLiteral("finalArgs"), finalArgs);
|
||||||
|
info.insert(QStringLiteral("effectiveEnv"), effectiveEnv);
|
||||||
|
info.insert(QStringLiteral("ok"), true);
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
void GameLauncher::launchGame(Game *game)
|
void GameLauncher::launchGame(Game *game)
|
||||||
{
|
{
|
||||||
if (!game) {
|
if (!game) {
|
||||||
|
|
@ -46,6 +328,14 @@ void GameLauncher::launchGame(Game *game)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const QString profileId = profileIdForCurrentUiMode();
|
||||||
|
const QVariantMap effectiveLaunchConfig = game->effectiveLaunchConfigForProfile(profileId);
|
||||||
|
const QString runner = effectiveLaunchConfig.value(QStringLiteral("runner")).toString().trimmed();
|
||||||
|
const QString runnerPath = effectiveLaunchConfig.value(QStringLiteral("runnerPath")).toString().trimmed();
|
||||||
|
const QString prefixPath = effectiveLaunchConfig.value(QStringLiteral("prefixPath")).toString().trimmed();
|
||||||
|
const QVariantMap launchEnv = effectiveLaunchConfig.value(QStringLiteral("env")).toMap();
|
||||||
|
const bool hasLaunchOverrides = !runner.isEmpty() || !runnerPath.isEmpty() || !prefixPath.isEmpty() || !launchEnv.isEmpty();
|
||||||
|
|
||||||
// Check if already running
|
// Check if already running
|
||||||
if (m_runningGames.contains(game->id())) {
|
if (m_runningGames.contains(game->id())) {
|
||||||
Q_EMIT gameError(game, tr("Game is already running"));
|
Q_EMIT gameError(game, tr("Game is already running"));
|
||||||
|
|
@ -53,7 +343,7 @@ void GameLauncher::launchGame(Game *game)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle Steam URLs
|
// Handle Steam URLs
|
||||||
if (command.startsWith(QLatin1String("steam://"))) {
|
if (!hasLaunchOverrides && command.startsWith(QLatin1String("steam://"))) {
|
||||||
QDesktopServices::openUrl(QUrl(command));
|
QDesktopServices::openUrl(QUrl(command));
|
||||||
game->setLastPlayed(QDateTime::currentDateTime());
|
game->setLastPlayed(QDateTime::currentDateTime());
|
||||||
Q_EMIT gameStarted(game);
|
Q_EMIT gameStarted(game);
|
||||||
|
|
@ -61,7 +351,7 @@ void GameLauncher::launchGame(Game *game)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle Lutris URLs
|
// Handle Lutris URLs
|
||||||
if (command.startsWith(QLatin1String("lutris "))) {
|
if (!hasLaunchOverrides && command.startsWith(QLatin1String("lutris "))) {
|
||||||
QString lutrisCommand = command.mid(7); // Remove "lutris "
|
QString lutrisCommand = command.mid(7); // Remove "lutris "
|
||||||
QDesktopServices::openUrl(QUrl(lutrisCommand));
|
QDesktopServices::openUrl(QUrl(lutrisCommand));
|
||||||
game->setLastPlayed(QDateTime::currentDateTime());
|
game->setLastPlayed(QDateTime::currentDateTime());
|
||||||
|
|
@ -72,6 +362,21 @@ void GameLauncher::launchGame(Game *game)
|
||||||
// Start process for other commands
|
// Start process for other commands
|
||||||
QProcess *process = new QProcess(this);
|
QProcess *process = new QProcess(this);
|
||||||
|
|
||||||
|
// Apply per-game environment variables
|
||||||
|
QProcessEnvironment env;
|
||||||
|
bool envChanged = false;
|
||||||
|
if (!launchEnv.isEmpty()) {
|
||||||
|
env = QProcessEnvironment::systemEnvironment();
|
||||||
|
envChanged = true;
|
||||||
|
for (auto it = launchEnv.constBegin(); it != launchEnv.constEnd(); ++it) {
|
||||||
|
const QString key = it.key();
|
||||||
|
if (key.isEmpty() || key.contains(QLatin1Char('='))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
env.insert(key, it.value().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Set working directory if available
|
// Set working directory if available
|
||||||
if (!game->workingDirectory().isEmpty()) {
|
if (!game->workingDirectory().isEmpty()) {
|
||||||
process->setWorkingDirectory(game->workingDirectory());
|
process->setWorkingDirectory(game->workingDirectory());
|
||||||
|
|
@ -84,7 +389,14 @@ void GameLauncher::launchGame(Game *game)
|
||||||
m_processToGame.insert(process, game);
|
m_processToGame.insert(process, game);
|
||||||
|
|
||||||
// Parse command and arguments
|
// Parse command and arguments
|
||||||
QStringList parts = QProcess::splitCommand(command);
|
QStringList parts;
|
||||||
|
if (command.startsWith(QLatin1String("steam://"))) {
|
||||||
|
parts = {QStringLiteral("xdg-open"), command};
|
||||||
|
} else if (command.startsWith(QLatin1String("lutris "))) {
|
||||||
|
parts = {QStringLiteral("xdg-open"), command.mid(7)};
|
||||||
|
} else {
|
||||||
|
parts = QProcess::splitCommand(command);
|
||||||
|
}
|
||||||
if (parts.isEmpty()) {
|
if (parts.isEmpty()) {
|
||||||
cleanupProcess(process);
|
cleanupProcess(process);
|
||||||
Q_EMIT gameError(game, tr("Invalid launch command"));
|
Q_EMIT gameError(game, tr("Invalid launch command"));
|
||||||
|
|
@ -92,7 +404,89 @@ void GameLauncher::launchGame(Game *game)
|
||||||
}
|
}
|
||||||
|
|
||||||
QString program = parts.takeFirst();
|
QString program = parts.takeFirst();
|
||||||
process->start(program, parts);
|
|
||||||
|
QString finalProgram = program;
|
||||||
|
QStringList finalArgs = parts;
|
||||||
|
|
||||||
|
if ((runner == QLatin1String("wine") || runner == QLatin1String("proton")) && (program == QLatin1String("xdg-open"))) {
|
||||||
|
cleanupProcess(process);
|
||||||
|
Q_EMIT gameError(game, tr("This runner cannot be used with URL-based launch commands"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runner == QLatin1String("custom")) {
|
||||||
|
if (runnerPath.isEmpty()) {
|
||||||
|
cleanupProcess(process);
|
||||||
|
Q_EMIT gameError(game, tr("Custom runner is enabled but no runner path is configured"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
finalProgram = runnerPath;
|
||||||
|
finalArgs = {program};
|
||||||
|
finalArgs.append(parts);
|
||||||
|
} else if (runner == QLatin1String("wine")) {
|
||||||
|
QString wineExe = runnerPath;
|
||||||
|
if (wineExe.isEmpty()) {
|
||||||
|
wineExe = QStandardPaths::findExecutable(QStringLiteral("wine"));
|
||||||
|
}
|
||||||
|
if (wineExe.isEmpty()) {
|
||||||
|
cleanupProcess(process);
|
||||||
|
Q_EMIT gameError(game, tr("Wine runner is enabled but Wine was not found"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString winePrefix = prefixPath;
|
||||||
|
if (winePrefix.isEmpty()) {
|
||||||
|
winePrefix = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/prefixes/") + game->id();
|
||||||
|
}
|
||||||
|
QDir().mkpath(winePrefix);
|
||||||
|
|
||||||
|
if (!envChanged) {
|
||||||
|
env = QProcessEnvironment::systemEnvironment();
|
||||||
|
envChanged = true;
|
||||||
|
}
|
||||||
|
env.insert(QStringLiteral("WINEPREFIX"), winePrefix);
|
||||||
|
|
||||||
|
finalProgram = wineExe;
|
||||||
|
finalArgs = {program};
|
||||||
|
finalArgs.append(parts);
|
||||||
|
} else if (runner == QLatin1String("proton")) {
|
||||||
|
QString protonExe = runnerPath;
|
||||||
|
if (protonExe.isEmpty()) {
|
||||||
|
protonExe = discoverDefaultProtonExecutable();
|
||||||
|
}
|
||||||
|
if (protonExe.isEmpty()) {
|
||||||
|
cleanupProcess(process);
|
||||||
|
Q_EMIT gameError(game, tr("Proton runner is enabled but no Proton installation was found"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString steamInstallPath = findSteamClientInstallPathFromProton(protonExe);
|
||||||
|
|
||||||
|
QString compatDataPath = prefixPath;
|
||||||
|
if (compatDataPath.isEmpty()) {
|
||||||
|
compatDataPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/prefixes/") + game->id();
|
||||||
|
}
|
||||||
|
QDir().mkpath(compatDataPath);
|
||||||
|
|
||||||
|
if (!envChanged) {
|
||||||
|
env = QProcessEnvironment::systemEnvironment();
|
||||||
|
envChanged = true;
|
||||||
|
}
|
||||||
|
env.insert(QStringLiteral("STEAM_COMPAT_DATA_PATH"), compatDataPath);
|
||||||
|
if (!steamInstallPath.isEmpty()) {
|
||||||
|
env.insert(QStringLiteral("STEAM_COMPAT_CLIENT_INSTALL_PATH"), steamInstallPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
finalProgram = protonExe;
|
||||||
|
finalArgs = {QStringLiteral("run"), program};
|
||||||
|
finalArgs.append(parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (envChanged) {
|
||||||
|
process->setProcessEnvironment(env);
|
||||||
|
}
|
||||||
|
|
||||||
|
process->start(finalProgram, finalArgs);
|
||||||
|
|
||||||
if (!process->waitForStarted(5000)) {
|
if (!process->waitForStarted(5000)) {
|
||||||
QString error = process->errorString();
|
QString error = process->errorString();
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QProcess>
|
#include <QProcess>
|
||||||
#include <QQmlEngine>
|
#include <QQmlEngine>
|
||||||
|
#include <QVariantMap>
|
||||||
|
|
||||||
#include "game.h"
|
#include "game.h"
|
||||||
|
|
||||||
|
|
@ -24,6 +25,7 @@ public:
|
||||||
bool hasRunningGames() const;
|
bool hasRunningGames() const;
|
||||||
|
|
||||||
Q_INVOKABLE void launchGame(Game *game);
|
Q_INVOKABLE void launchGame(Game *game);
|
||||||
|
Q_INVOKABLE QVariantMap resolveLaunchInfo(Game *game) const;
|
||||||
Q_INVOKABLE void stopGame(Game *game);
|
Q_INVOKABLE void stopGame(Game *game);
|
||||||
Q_INVOKABLE bool isGameRunning(Game *game) const;
|
Q_INVOKABLE bool isGameRunning(Game *game) const;
|
||||||
|
|
||||||
|
|
|
||||||
104
src/inputmanager.cpp
Normal file
104
src/inputmanager.cpp
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
||||||
|
|
||||||
|
#include "inputmanager.h"
|
||||||
|
|
||||||
|
#include "gamepadmanager.h"
|
||||||
|
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QEvent>
|
||||||
|
#include <QGuiApplication>
|
||||||
|
|
||||||
|
InputManager *InputManager::s_instance = nullptr;
|
||||||
|
|
||||||
|
InputManager::InputManager(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
{
|
||||||
|
if (QCoreApplication::instance()) {
|
||||||
|
QCoreApplication::instance()->installEventFilter(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *pad = GamepadManager::instance();
|
||||||
|
connect(pad, &GamepadManager::activeChanged, this, [this, pad]() {
|
||||||
|
if (pad->active()) {
|
||||||
|
if (!m_hasSeenGamepad) {
|
||||||
|
m_hasSeenGamepad = true;
|
||||||
|
Q_EMIT inputHistoryChanged();
|
||||||
|
}
|
||||||
|
setActiveInput(Gamepad);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(pad, &GamepadManager::connectedChanged, this, [this, pad]() {
|
||||||
|
if (!pad->connected() && m_activeInput == Gamepad) {
|
||||||
|
setActiveInput(KeyboardMouse);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
InputManager *InputManager::instance()
|
||||||
|
{
|
||||||
|
if (!s_instance) {
|
||||||
|
s_instance = new InputManager();
|
||||||
|
}
|
||||||
|
return s_instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputManager *InputManager::create(QQmlEngine *engine, QJSEngine *scriptEngine)
|
||||||
|
{
|
||||||
|
Q_UNUSED(engine)
|
||||||
|
Q_UNUSED(scriptEngine)
|
||||||
|
return instance();
|
||||||
|
}
|
||||||
|
|
||||||
|
InputManager::ActiveInput InputManager::activeInput() const
|
||||||
|
{
|
||||||
|
return m_activeInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputManager::hasSeenKeyboardMouse() const
|
||||||
|
{
|
||||||
|
return m_hasSeenKeyboardMouse;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputManager::hasSeenGamepad() const
|
||||||
|
{
|
||||||
|
return m_hasSeenGamepad;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputManager::setActiveInput(ActiveInput input)
|
||||||
|
{
|
||||||
|
if (m_activeInput == input) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_activeInput = input;
|
||||||
|
Q_EMIT activeInputChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputManager::eventFilter(QObject *watched, QEvent *event)
|
||||||
|
{
|
||||||
|
Q_UNUSED(watched)
|
||||||
|
|
||||||
|
if (!event) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event->type()) {
|
||||||
|
case QEvent::KeyPress:
|
||||||
|
case QEvent::KeyRelease:
|
||||||
|
case QEvent::MouseButtonPress:
|
||||||
|
case QEvent::MouseButtonRelease:
|
||||||
|
case QEvent::Wheel:
|
||||||
|
if (!m_hasSeenKeyboardMouse) {
|
||||||
|
m_hasSeenKeyboardMouse = true;
|
||||||
|
Q_EMIT inputHistoryChanged();
|
||||||
|
}
|
||||||
|
setActiveInput(KeyboardMouse);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
54
src/inputmanager.h
Normal file
54
src/inputmanager.h
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QQmlEngine>
|
||||||
|
|
||||||
|
class QEvent;
|
||||||
|
class QJSEngine;
|
||||||
|
|
||||||
|
class InputManager : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
QML_SINGLETON
|
||||||
|
|
||||||
|
Q_PROPERTY(ActiveInput activeInput READ activeInput NOTIFY activeInputChanged)
|
||||||
|
Q_PROPERTY(bool hasSeenKeyboardMouse READ hasSeenKeyboardMouse NOTIFY inputHistoryChanged)
|
||||||
|
Q_PROPERTY(bool hasSeenGamepad READ hasSeenGamepad NOTIFY inputHistoryChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
enum ActiveInput {
|
||||||
|
KeyboardMouse = 0,
|
||||||
|
Gamepad = 1,
|
||||||
|
};
|
||||||
|
Q_ENUM(ActiveInput)
|
||||||
|
|
||||||
|
static InputManager *instance();
|
||||||
|
static InputManager *create(QQmlEngine *engine, QJSEngine *scriptEngine);
|
||||||
|
|
||||||
|
ActiveInput activeInput() const;
|
||||||
|
|
||||||
|
bool hasSeenKeyboardMouse() const;
|
||||||
|
bool hasSeenGamepad() const;
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void activeInputChanged();
|
||||||
|
void inputHistoryChanged();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit InputManager(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
static InputManager *s_instance;
|
||||||
|
|
||||||
|
ActiveInput m_activeInput = KeyboardMouse;
|
||||||
|
bool m_hasSeenKeyboardMouse = false;
|
||||||
|
bool m_hasSeenGamepad = false;
|
||||||
|
|
||||||
|
void setActiveInput(ActiveInput input);
|
||||||
|
};
|
||||||
66
src/main.cpp
66
src/main.cpp
|
|
@ -1,12 +1,15 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
#include <QCommandLineParser>
|
#include <QCommandLineParser>
|
||||||
#include <QGuiApplication>
|
#include <QGuiApplication>
|
||||||
#include <QIcon>
|
#include <QIcon>
|
||||||
#include <QQmlApplicationEngine>
|
#include <QQmlApplicationEngine>
|
||||||
#include <QQmlContext>
|
#include <QQmlContext>
|
||||||
#include <QQuickStyle>
|
#include <QQuickStyle>
|
||||||
|
#include <QTextStream>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
#include <KAboutData>
|
#include <KAboutData>
|
||||||
#include <KCrash>
|
#include <KCrash>
|
||||||
|
|
@ -16,15 +19,10 @@
|
||||||
|
|
||||||
#include "alakarte-version.h"
|
#include "alakarte-version.h"
|
||||||
#include "app.h"
|
#include "app.h"
|
||||||
#include "config.h"
|
|
||||||
#include "game.h"
|
|
||||||
#include "gamepadmanager.h"
|
|
||||||
#include "gamelauncher.h"
|
|
||||||
#include "gamemodel.h"
|
|
||||||
#include "gamesortfiltermodel.h"
|
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
|
qputenv("QT_DESKTOP_FILE_NAME", QByteArray("org.kde.alakarte"));
|
||||||
QGuiApplication app(argc, argv);
|
QGuiApplication app(argc, argv);
|
||||||
QGuiApplication::setDesktopFileName(QStringLiteral("org.kde.alakarte"));
|
QGuiApplication::setDesktopFileName(QStringLiteral("org.kde.alakarte"));
|
||||||
|
|
||||||
|
|
@ -52,30 +50,56 @@ int main(int argc, char *argv[])
|
||||||
|
|
||||||
QCommandLineParser parser;
|
QCommandLineParser parser;
|
||||||
aboutData.setupCommandLine(&parser);
|
aboutData.setupCommandLine(&parser);
|
||||||
|
|
||||||
|
QCommandLineOption importAllAndExitOption(QStringLiteral("import-all-and-exit"), i18n("Import all enabled sources and exit"));
|
||||||
|
QCommandLineOption importDesktopAndExitOption(QStringLiteral("import-desktop-and-exit"), i18n("Import desktop entries and exit"));
|
||||||
|
QCommandLineOption startupAndExitOption(QStringLiteral("startup-and-exit"), i18n("Start without UI and exit after startup auto-import (if enabled)"));
|
||||||
|
parser.addOption(importAllAndExitOption);
|
||||||
|
parser.addOption(importDesktopAndExitOption);
|
||||||
|
parser.addOption(startupAndExitOption);
|
||||||
|
|
||||||
parser.process(app);
|
parser.process(app);
|
||||||
aboutData.processCommandLine(&parser);
|
aboutData.processCommandLine(&parser);
|
||||||
|
|
||||||
KDBusService service(KDBusService::Unique);
|
|
||||||
|
|
||||||
if (qEnvironmentVariableIsEmpty("QT_QUICK_CONTROLS_STYLE")) {
|
if (qEnvironmentVariableIsEmpty("QT_QUICK_CONTROLS_STYLE")) {
|
||||||
QQuickStyle::setStyle(QStringLiteral("org.kde.desktop"));
|
QQuickStyle::setStyle(QStringLiteral("org.kde.desktop"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (parser.isSet(importAllAndExitOption) || parser.isSet(importDesktopAndExitOption) || parser.isSet(startupAndExitOption)) {
|
||||||
|
App *alakarteApp = App::instance();
|
||||||
|
|
||||||
|
QObject::connect(alakarteApp, &App::importCompleted, &app, [&app](int count) {
|
||||||
|
QTextStream(stdout) << count << Qt::endl;
|
||||||
|
app.exit(0);
|
||||||
|
});
|
||||||
|
QObject::connect(alakarteApp, &App::importError, &app, [&app](const QString &error) {
|
||||||
|
QTextStream(stderr) << error << Qt::endl;
|
||||||
|
app.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (parser.isSet(importAllAndExitOption)) {
|
||||||
|
alakarteApp->importAllGames();
|
||||||
|
} else if (parser.isSet(importDesktopAndExitOption)) {
|
||||||
|
alakarteApp->importFromDesktop();
|
||||||
|
} else {
|
||||||
|
if (!alakarteApp->config() || !alakarteApp->config()->autoImportOnStartup()) {
|
||||||
|
QTextStream(stdout) << 0 << Qt::endl;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTimer::singleShot(60000, &app, [&app]() {
|
||||||
|
QTextStream(stderr) << QStringLiteral("Timed out waiting for auto-import") << Qt::endl;
|
||||||
|
app.exit(2);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return app.exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
KDBusService service(KDBusService::Unique);
|
||||||
|
|
||||||
QQmlApplicationEngine engine;
|
QQmlApplicationEngine engine;
|
||||||
|
|
||||||
qmlRegisterSingletonType<App>("org.kde.alakarte", 1, 0, "App", [](QQmlEngine *engine, QJSEngine *) -> QObject * {
|
|
||||||
Q_UNUSED(engine)
|
|
||||||
return App::instance();
|
|
||||||
});
|
|
||||||
|
|
||||||
qmlRegisterSingletonType<GamepadManager>("org.kde.alakarte", 1, 0, "GamepadManager", &GamepadManager::create);
|
|
||||||
|
|
||||||
qmlRegisterType<GameModel>("org.kde.alakarte", 1, 0, "GameModel");
|
|
||||||
qmlRegisterType<GameSortFilterModel>("org.kde.alakarte", 1, 0, "GameSortFilterModel");
|
|
||||||
qmlRegisterUncreatableType<Game>("org.kde.alakarte", 1, 0, "Game", QStringLiteral("Game objects are created by GameModel"));
|
|
||||||
qmlRegisterType<GameLauncher>("org.kde.alakarte", 1, 0, "GameLauncher");
|
|
||||||
qmlRegisterType<Config>("org.kde.alakarte", 1, 0, "Config");
|
|
||||||
|
|
||||||
engine.rootContext()->setContextObject(new KLocalizedContext(&engine));
|
engine.rootContext()->setContextObject(new KLocalizedContext(&engine));
|
||||||
|
|
||||||
engine.loadFromModule("org.kde.alakarte", "Main");
|
engine.loadFromModule("org.kde.alakarte", "Main");
|
||||||
|
|
|
||||||
145
src/mediamanager.cpp
Normal file
145
src/mediamanager.cpp
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
#include "mediamanager.h"
|
||||||
|
|
||||||
|
#include "game.h"
|
||||||
|
#include "screenshotmodel.h"
|
||||||
|
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
|
||||||
|
MediaManager::MediaManager(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
QString MediaManager::screenshotsBasePath() const
|
||||||
|
{
|
||||||
|
return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/media/screenshots");
|
||||||
|
}
|
||||||
|
|
||||||
|
QString MediaManager::gameId(Game *game) const
|
||||||
|
{
|
||||||
|
return game ? game->id() : QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString MediaManager::ensureDirectory(const QString &path) const
|
||||||
|
{
|
||||||
|
if (path.isEmpty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QDir dir(path);
|
||||||
|
if (!dir.exists()) {
|
||||||
|
QDir parent;
|
||||||
|
parent.mkpath(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return QDir(path).absolutePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString MediaManager::gameScreenshotsFolderPath(Game *game) const
|
||||||
|
{
|
||||||
|
const QString id = gameId(game);
|
||||||
|
if (id.isEmpty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return screenshotsBasePath() + QLatin1Char('/') + id;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString MediaManager::screenshotsFolderPath(Game *game) const
|
||||||
|
{
|
||||||
|
const QString path = gameScreenshotsFolderPath(game);
|
||||||
|
return ensureDirectory(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
QUrl MediaManager::screenshotsFolderUrl(Game *game) const
|
||||||
|
{
|
||||||
|
const QString path = screenshotsFolderPath(game);
|
||||||
|
if (path.isEmpty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return QUrl::fromLocalFile(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
QObject *MediaManager::screenshotsModel(Game *game)
|
||||||
|
{
|
||||||
|
const QString id = gameId(game);
|
||||||
|
if (id.isEmpty()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_models.contains(id) && m_models.value(id)) {
|
||||||
|
return m_models.value(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *model = new ScreenshotModel(this);
|
||||||
|
model->setDirectoryPath(screenshotsFolderPath(game));
|
||||||
|
m_models.insert(id, model);
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString MediaManager::uniqueDestinationPath(const QString &folderPath, const QString &fileName) const
|
||||||
|
{
|
||||||
|
const QString baseName = QFileInfo(fileName).completeBaseName();
|
||||||
|
const QString suffix = QFileInfo(fileName).suffix();
|
||||||
|
|
||||||
|
const QString cleanBase = baseName.isEmpty() ? QStringLiteral("screenshot") : baseName;
|
||||||
|
const QString cleanSuffix = suffix.isEmpty() ? QStringLiteral("png") : suffix;
|
||||||
|
|
||||||
|
QString candidate = folderPath + QLatin1Char('/') + cleanBase + QLatin1Char('.') + cleanSuffix;
|
||||||
|
if (!QFileInfo::exists(candidate)) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 1; i < 10000; ++i) {
|
||||||
|
candidate = folderPath + QLatin1Char('/') + cleanBase + QLatin1Char('_') + QString::number(i) + QLatin1Char('.') + cleanSuffix;
|
||||||
|
if (!QFileInfo::exists(candidate)) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return folderPath + QLatin1Char('/') + cleanBase + QLatin1Char('_') + QString::number(QDateTime::currentMSecsSinceEpoch()) + QLatin1Char('.') + cleanSuffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MediaManager::importScreenshot(Game *game, const QUrl &sourceUrl)
|
||||||
|
{
|
||||||
|
if (!game) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sourceUrl.isLocalFile()) {
|
||||||
|
Q_EMIT importError(game, tr("Only local files are supported"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString srcPath = sourceUrl.toLocalFile();
|
||||||
|
QFileInfo srcInfo(srcPath);
|
||||||
|
if (!srcInfo.exists() || !srcInfo.isFile()) {
|
||||||
|
Q_EMIT importError(game, tr("Source file does not exist"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString folder = screenshotsFolderPath(game);
|
||||||
|
if (folder.isEmpty()) {
|
||||||
|
Q_EMIT importError(game, tr("Failed to create screenshots folder"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString destPath = uniqueDestinationPath(folder, srcInfo.fileName());
|
||||||
|
|
||||||
|
if (!QFile::copy(srcPath, destPath)) {
|
||||||
|
Q_EMIT importError(game, tr("Failed to copy file"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString id = gameId(game);
|
||||||
|
if (m_models.contains(id) && m_models.value(id)) {
|
||||||
|
m_models.value(id)->refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_EMIT screenshotsImported(game, 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
41
src/mediamanager.h
Normal file
41
src/mediamanager.h
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QHash>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QPointer>
|
||||||
|
#include <QQmlEngine>
|
||||||
|
#include <QUrl>
|
||||||
|
|
||||||
|
class Game;
|
||||||
|
class ScreenshotModel;
|
||||||
|
|
||||||
|
class MediaManager : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
Q_PROPERTY(QString screenshotsBasePath READ screenshotsBasePath CONSTANT)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit MediaManager(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
QString screenshotsBasePath() const;
|
||||||
|
|
||||||
|
Q_INVOKABLE QString screenshotsFolderPath(Game *game) const;
|
||||||
|
Q_INVOKABLE QUrl screenshotsFolderUrl(Game *game) const;
|
||||||
|
|
||||||
|
Q_INVOKABLE QObject *screenshotsModel(Game *game);
|
||||||
|
|
||||||
|
Q_INVOKABLE bool importScreenshot(Game *game, const QUrl &sourceUrl);
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void screenshotsImported(Game *game, int count);
|
||||||
|
void importError(Game *game, const QString &error);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString gameId(Game *game) const;
|
||||||
|
QString gameScreenshotsFolderPath(Game *game) const;
|
||||||
|
QString ensureDirectory(const QString &path) const;
|
||||||
|
QString uniqueDestinationPath(const QString &folderPath, const QString &fileName) const;
|
||||||
|
|
||||||
|
QHash<QString, QPointer<ScreenshotModel>> m_models;
|
||||||
|
};
|
||||||
126
src/qml/DiagnosticsSheet.qml
Normal file
126
src/qml/DiagnosticsSheet.qml
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls as QQC2
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import org.kde.kirigami as Kirigami
|
||||||
|
import org.kde.alakarte
|
||||||
|
|
||||||
|
Kirigami.OverlaySheet {
|
||||||
|
id: diagnosticsSheet
|
||||||
|
|
||||||
|
property var game
|
||||||
|
property var launchInfo: ({})
|
||||||
|
|
||||||
|
title: i18n("Diagnostics")
|
||||||
|
parent: applicationWindow().overlay
|
||||||
|
|
||||||
|
closePolicy: QQC2.Popup.CloseOnEscape | QQC2.Popup.CloseOnPressOutside
|
||||||
|
|
||||||
|
implicitWidth: {
|
||||||
|
let w = applicationWindow()
|
||||||
|
if (!w) return Kirigami.Units.gridUnit * 30
|
||||||
|
if (w.isMobile) return w.width
|
||||||
|
if (w.isNarrowScreen) return w.width - Kirigami.Units.largeSpacing * 2
|
||||||
|
return Math.min(w.width - Kirigami.Units.gridUnit * 2, Kirigami.Units.gridUnit * 34)
|
||||||
|
}
|
||||||
|
|
||||||
|
implicitHeight: {
|
||||||
|
let w = applicationWindow()
|
||||||
|
if (!w) return Kirigami.Units.gridUnit * 30
|
||||||
|
if (w.isMobile) return w.height
|
||||||
|
return Math.min(w.height - Kirigami.Units.gridUnit * 2, Kirigami.Units.gridUnit * 42)
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpened: {
|
||||||
|
if (game) {
|
||||||
|
launchInfo = App.launcher.resolveLaunchInfo(game)
|
||||||
|
} else {
|
||||||
|
launchInfo = ({ "ok": false, "error": i18n("No game selected") })
|
||||||
|
}
|
||||||
|
closeButton.forceActiveFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
header: Kirigami.ShadowedRectangle {
|
||||||
|
implicitHeight: headerRow.implicitHeight + Kirigami.Units.largeSpacing * 2
|
||||||
|
radius: Kirigami.Units.mediumSpacing
|
||||||
|
color: Kirigami.Theme.backgroundColor
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: headerRow
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Kirigami.Units.largeSpacing
|
||||||
|
spacing: Kirigami.Units.mediumSpacing
|
||||||
|
|
||||||
|
Kirigami.Heading {
|
||||||
|
text: diagnosticsSheet.title
|
||||||
|
level: 2
|
||||||
|
Layout.fillWidth: true
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.ToolButton {
|
||||||
|
id: closeButton
|
||||||
|
text: i18n("Close")
|
||||||
|
icon.name: "dialog-close"
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
onClicked: diagnosticsSheet.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: QQC2.ScrollView {
|
||||||
|
id: scroll
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
width: scroll.availableWidth
|
||||||
|
spacing: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
|
Kirigami.InlineMessage {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
visible: launchInfo && launchInfo.ok === false
|
||||||
|
type: Kirigami.MessageType.Error
|
||||||
|
text: launchInfo && launchInfo.error ? launchInfo.error : i18n("Unknown error")
|
||||||
|
}
|
||||||
|
|
||||||
|
Kirigami.FormLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
QQC2.Label {
|
||||||
|
Kirigami.FormData.label: i18n("Launch command:")
|
||||||
|
text: launchInfo && launchInfo.launchCommand ? launchInfo.launchCommand : ""
|
||||||
|
wrapMode: Text.WrapAnywhere
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Label {
|
||||||
|
Kirigami.FormData.label: i18n("Runner:")
|
||||||
|
text: launchInfo && launchInfo.runner ? launchInfo.runner : i18n("None")
|
||||||
|
wrapMode: Text.WrapAnywhere
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Label {
|
||||||
|
Kirigami.FormData.label: i18n("Program:")
|
||||||
|
text: launchInfo && launchInfo.finalProgram ? launchInfo.finalProgram : ""
|
||||||
|
wrapMode: Text.WrapAnywhere
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Label {
|
||||||
|
Kirigami.FormData.label: i18n("Args:")
|
||||||
|
text: launchInfo && launchInfo.finalArgs ? launchInfo.finalArgs.join(" ") : ""
|
||||||
|
wrapMode: Text.WrapAnywhere
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Label {
|
||||||
|
Kirigami.FormData.label: i18n("Working directory:")
|
||||||
|
text: launchInfo && launchInfo.workingDirectory ? launchInfo.workingDirectory : i18n("Not specified")
|
||||||
|
wrapMode: Text.WrapAnywhere
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Label {
|
||||||
|
Kirigami.FormData.label: i18n("Prefix:")
|
||||||
|
text: launchInfo && launchInfo.resolvedPrefixPath ? launchInfo.resolvedPrefixPath : (launchInfo && launchInfo.prefixPath ? launchInfo.prefixPath : "")
|
||||||
|
wrapMode: Text.WrapAnywhere
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls as QQC2
|
import QtQuick.Controls as QQC2
|
||||||
|
import QtQuick.Dialogs
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Effects
|
import QtQuick.Effects
|
||||||
import org.kde.kirigami as Kirigami
|
import org.kde.kirigami as Kirigami
|
||||||
|
|
@ -11,7 +12,20 @@ import org.kde.alakarte
|
||||||
Kirigami.OverlaySheet {
|
Kirigami.OverlaySheet {
|
||||||
id: detailsSheet
|
id: detailsSheet
|
||||||
|
|
||||||
property var game
|
property var game: null
|
||||||
|
property int lastNonCouchUiMode: Config.Auto
|
||||||
|
|
||||||
|
readonly property bool effectiveCouchMode: {
|
||||||
|
if (App.config.uiMode === Config.Couch) return true
|
||||||
|
if (App.config.uiMode !== Config.Auto) return false
|
||||||
|
if (GamepadManager.connected) {
|
||||||
|
if (InputManager.activeInput === InputManager.Gamepad) return true
|
||||||
|
if (!InputManager.hasSeenKeyboardMouse) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property var screenshotsModel: game ? App.mediaManager.screenshotsModel(game) : null
|
||||||
|
|
||||||
readonly property bool isMobile: {
|
readonly property bool isMobile: {
|
||||||
let w = applicationWindow()
|
let w = applicationWindow()
|
||||||
|
|
@ -45,16 +59,44 @@ Kirigami.OverlaySheet {
|
||||||
|
|
||||||
signal launchRequested()
|
signal launchRequested()
|
||||||
signal editRequested()
|
signal editRequested()
|
||||||
|
signal diagnosticsRequested()
|
||||||
signal removeRequested()
|
signal removeRequested()
|
||||||
|
|
||||||
title: game ? game.name : ""
|
title: game ? game.name : ""
|
||||||
|
|
||||||
onOpened: playButton.forceActiveFocus()
|
onOpened: playButton.forceActiveFocus()
|
||||||
|
|
||||||
|
Shortcut {
|
||||||
|
enabled: detailsSheet.opened
|
||||||
|
sequence: "E"
|
||||||
|
onActivated: {
|
||||||
|
let app = applicationWindow()
|
||||||
|
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
|
||||||
|
detailsSheet.editRequested()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Shortcut {
|
||||||
|
enabled: detailsSheet.opened
|
||||||
|
sequence: "F"
|
||||||
|
onActivated: {
|
||||||
|
let app = applicationWindow()
|
||||||
|
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
|
||||||
|
if (game) game.favorite = !game.favorite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function isDescendant(item, ancestor) {
|
function isDescendant(item, ancestor) {
|
||||||
let p = item
|
let p = item
|
||||||
while (p) {
|
while (p) {
|
||||||
if (p === ancestor) return true
|
if (p === ancestor) return true
|
||||||
|
if (ancestor.contentItem && p === ancestor.contentItem) return true
|
||||||
|
if (ancestor.header && p === ancestor.header) return true
|
||||||
|
if (p.visualParent !== undefined && p.visualParent !== null) {
|
||||||
|
if (detailsSheet.isDescendant(p.visualParent, ancestor)) return true
|
||||||
|
} else if (p.popup !== undefined && p.popup !== null && p.popup.visualParent !== undefined && p.popup.visualParent !== null) {
|
||||||
|
if (detailsSheet.isDescendant(p.popup.visualParent, ancestor)) return true
|
||||||
|
}
|
||||||
p = p.parent
|
p = p.parent
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|
@ -78,6 +120,8 @@ Kirigami.OverlaySheet {
|
||||||
Connections {
|
Connections {
|
||||||
target: GamepadManager
|
target: GamepadManager
|
||||||
function onNavigateLeft() {
|
function onNavigateLeft() {
|
||||||
|
let app = applicationWindow()
|
||||||
|
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
|
||||||
if (!detailsSheet.opened) return
|
if (!detailsSheet.opened) return
|
||||||
let w = applicationWindow()
|
let w = applicationWindow()
|
||||||
if (!w || !w.activeFocusItem) return
|
if (!w || !w.activeFocusItem) return
|
||||||
|
|
@ -85,6 +129,8 @@ Kirigami.OverlaySheet {
|
||||||
detailsSheet.focusNextInChain(false)
|
detailsSheet.focusNextInChain(false)
|
||||||
}
|
}
|
||||||
function onNavigateRight() {
|
function onNavigateRight() {
|
||||||
|
let app = applicationWindow()
|
||||||
|
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
|
||||||
if (!detailsSheet.opened) return
|
if (!detailsSheet.opened) return
|
||||||
let w = applicationWindow()
|
let w = applicationWindow()
|
||||||
if (!w || !w.activeFocusItem) return
|
if (!w || !w.activeFocusItem) return
|
||||||
|
|
@ -92,6 +138,8 @@ Kirigami.OverlaySheet {
|
||||||
detailsSheet.focusNextInChain(true)
|
detailsSheet.focusNextInChain(true)
|
||||||
}
|
}
|
||||||
function onNavigateUp() {
|
function onNavigateUp() {
|
||||||
|
let app = applicationWindow()
|
||||||
|
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
|
||||||
if (!detailsSheet.opened) return
|
if (!detailsSheet.opened) return
|
||||||
let w = applicationWindow()
|
let w = applicationWindow()
|
||||||
if (!w || !w.activeFocusItem) return
|
if (!w || !w.activeFocusItem) return
|
||||||
|
|
@ -99,6 +147,8 @@ Kirigami.OverlaySheet {
|
||||||
detailsSheet.focusNextInChain(false)
|
detailsSheet.focusNextInChain(false)
|
||||||
}
|
}
|
||||||
function onNavigateDown() {
|
function onNavigateDown() {
|
||||||
|
let app = applicationWindow()
|
||||||
|
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
|
||||||
if (!detailsSheet.opened) return
|
if (!detailsSheet.opened) return
|
||||||
let w = applicationWindow()
|
let w = applicationWindow()
|
||||||
if (!w || !w.activeFocusItem) return
|
if (!w || !w.activeFocusItem) return
|
||||||
|
|
@ -106,6 +156,8 @@ Kirigami.OverlaySheet {
|
||||||
detailsSheet.focusNextInChain(true)
|
detailsSheet.focusNextInChain(true)
|
||||||
}
|
}
|
||||||
function onSelectPressed() {
|
function onSelectPressed() {
|
||||||
|
let app = applicationWindow()
|
||||||
|
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
|
||||||
if (!detailsSheet.opened) return
|
if (!detailsSheet.opened) return
|
||||||
let w = applicationWindow()
|
let w = applicationWindow()
|
||||||
if (!w || !w.activeFocusItem) return
|
if (!w || !w.activeFocusItem) return
|
||||||
|
|
@ -377,6 +429,32 @@ Kirigami.OverlaySheet {
|
||||||
onClicked: detailsSheet.editRequested()
|
onClicked: detailsSheet.editRequested()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QQC2.Button {
|
||||||
|
icon.name: "view-fullscreen"
|
||||||
|
text: detailsSheet.effectiveCouchMode ? i18n("Couch mode: On") : i18n("Couch mode: Off")
|
||||||
|
display: detailsSheet.useCompactLayout ? QQC2.AbstractButton.TextUnderIcon : QQC2.AbstractButton.TextBesideIcon
|
||||||
|
onClicked: {
|
||||||
|
if (detailsSheet.effectiveCouchMode) {
|
||||||
|
if (detailsSheet.lastNonCouchUiMode === Config.Auto) {
|
||||||
|
App.config.uiMode = Config.Desktop
|
||||||
|
} else {
|
||||||
|
App.config.uiMode = detailsSheet.lastNonCouchUiMode
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
detailsSheet.lastNonCouchUiMode = App.config.uiMode
|
||||||
|
App.config.uiMode = Config.Couch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Button {
|
||||||
|
icon.name: "dialog-information"
|
||||||
|
text: i18n("Diagnostics")
|
||||||
|
display: detailsSheet.useCompactLayout ? QQC2.AbstractButton.TextUnderIcon : QQC2.AbstractButton.TextBesideIcon
|
||||||
|
enabled: !!game
|
||||||
|
onClicked: detailsSheet.diagnosticsRequested()
|
||||||
|
}
|
||||||
|
|
||||||
QQC2.Button {
|
QQC2.Button {
|
||||||
icon.name: "edit-delete"
|
icon.name: "edit-delete"
|
||||||
text: i18n("Remove")
|
text: i18n("Remove")
|
||||||
|
|
@ -422,6 +500,94 @@ Kirigami.OverlaySheet {
|
||||||
Kirigami.FormData.label: i18n("Install path:")
|
Kirigami.FormData.label: i18n("Install path:")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Kirigami.Heading {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
level: 2
|
||||||
|
text: i18n("Screenshots")
|
||||||
|
visible: !!game
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
visible: !!game
|
||||||
|
|
||||||
|
QQC2.Button {
|
||||||
|
text: i18n("Open Folder")
|
||||||
|
icon.name: "folder-open"
|
||||||
|
enabled: !!game
|
||||||
|
onClicked: if (game) {
|
||||||
|
Qt.openUrlExternally(App.mediaManager.screenshotsFolderUrl(game))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Button {
|
||||||
|
text: i18n("Import Screenshot")
|
||||||
|
icon.name: "document-open"
|
||||||
|
enabled: !!game
|
||||||
|
onClicked: screenshotImportDialog.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
Item { Layout.fillWidth: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
visible: !!game && (!detailsSheet.screenshotsModel || detailsSheet.screenshotsModel.count === 0)
|
||||||
|
text: i18n("No screenshots yet")
|
||||||
|
color: Kirigami.Theme.disabledTextColor
|
||||||
|
}
|
||||||
|
|
||||||
|
GridView {
|
||||||
|
id: screenshotsGrid
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: Kirigami.Units.gridUnit * 12
|
||||||
|
visible: detailsSheet.screenshotsModel && detailsSheet.screenshotsModel.count > 0
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
cellWidth: Kirigami.Units.gridUnit * 6
|
||||||
|
cellHeight: Kirigami.Units.gridUnit * 4
|
||||||
|
model: detailsSheet.screenshotsModel
|
||||||
|
|
||||||
|
delegate: Item {
|
||||||
|
width: screenshotsGrid.cellWidth
|
||||||
|
height: screenshotsGrid.cellHeight
|
||||||
|
|
||||||
|
Kirigami.ShadowedRectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Kirigami.Units.smallSpacing
|
||||||
|
radius: Kirigami.Units.smallSpacing
|
||||||
|
color: Kirigami.Theme.alternateBackgroundColor
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Image {
|
||||||
|
anchors.fill: parent
|
||||||
|
source: url
|
||||||
|
asynchronous: true
|
||||||
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
smooth: true
|
||||||
|
mipmap: App.config.highQualityImages
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: Qt.openUrlExternally(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileDialog {
|
||||||
|
id: screenshotImportDialog
|
||||||
|
title: i18n("Import Screenshot")
|
||||||
|
fileMode: FileDialog.OpenFile
|
||||||
|
nameFilters: [i18n("Image files (*.png *.jpg *.jpeg *.webp)"), i18n("All files (*)")]
|
||||||
|
onAccepted: {
|
||||||
|
if (detailsSheet.game) {
|
||||||
|
App.mediaManager.importScreenshot(detailsSheet.game, selectedFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPlatformColor(platform) {
|
function getPlatformColor(platform) {
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,11 @@ Kirigami.Dialog {
|
||||||
|
|
||||||
property var game: null
|
property var game: null
|
||||||
property bool isEditing: game !== null
|
property bool isEditing: game !== null
|
||||||
|
property string editProfileId: "default"
|
||||||
|
|
||||||
|
ListModel {
|
||||||
|
id: envModel
|
||||||
|
}
|
||||||
|
|
||||||
signal gameCreated(var game)
|
signal gameCreated(var game)
|
||||||
signal gameUpdated(var game)
|
signal gameUpdated(var game)
|
||||||
|
|
@ -31,13 +36,33 @@ Kirigami.Dialog {
|
||||||
Kirigami.Action {
|
Kirigami.Action {
|
||||||
text: isEditing ? i18n("Apply") : i18n("Add")
|
text: isEditing ? i18n("Apply") : i18n("Add")
|
||||||
icon.name: isEditing ? "dialog-ok-apply" : "list-add"
|
icon.name: isEditing ? "dialog-ok-apply" : "list-add"
|
||||||
enabled: nameField.text.trim() !== "" && executableField.text.trim() !== ""
|
enabled: nameField.text.trim() !== "" && executableField.text.trim() !== "" && (runnerCombo.currentIndex !== 3 || runnerPathField.text.trim() !== "")
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
|
let runnerValue = dialog.runnerFromIndex(runnerCombo.currentIndex)
|
||||||
|
let runnerPathValue = runnerPathField.text.trim()
|
||||||
|
let prefixPathValue = prefixPathField.text.trim()
|
||||||
|
if (runnerValue === "") {
|
||||||
|
runnerPathValue = ""
|
||||||
|
prefixPathValue = ""
|
||||||
|
}
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
game.name = nameField.text.trim()
|
game.name = nameField.text.trim()
|
||||||
game.developer = developerField.text.trim()
|
game.developer = developerField.text.trim()
|
||||||
game.launchCommand = executableField.text.trim()
|
game.launchCommand = executableField.text.trim()
|
||||||
game.workingDirectory = workingDirField.text.trim()
|
game.workingDirectory = workingDirField.text.trim()
|
||||||
|
if (dialog.editProfileId === "couch") {
|
||||||
|
game.setLaunchProfileConfig("couch", {
|
||||||
|
"runner": runnerValue,
|
||||||
|
"runnerPath": runnerPathValue,
|
||||||
|
"prefixPath": prefixPathValue,
|
||||||
|
"env": dialog.envModelToMap()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
game.launchEnv = dialog.envModelToMap()
|
||||||
|
game.launchRunner = runnerValue
|
||||||
|
game.launchRunnerPath = runnerPathValue
|
||||||
|
game.launchPrefixPath = prefixPathValue
|
||||||
|
}
|
||||||
if (selectedCoverPath !== "") {
|
if (selectedCoverPath !== "") {
|
||||||
App.setCoverFromFile(game, selectedCoverPath)
|
App.setCoverFromFile(game, selectedCoverPath)
|
||||||
}
|
}
|
||||||
|
|
@ -48,6 +73,10 @@ Kirigami.Dialog {
|
||||||
if (newGame) {
|
if (newGame) {
|
||||||
newGame.developer = developerField.text.trim()
|
newGame.developer = developerField.text.trim()
|
||||||
newGame.workingDirectory = workingDirField.text.trim()
|
newGame.workingDirectory = workingDirField.text.trim()
|
||||||
|
newGame.launchEnv = dialog.envModelToMap()
|
||||||
|
newGame.launchRunner = runnerValue
|
||||||
|
newGame.launchRunnerPath = runnerPathValue
|
||||||
|
newGame.launchPrefixPath = prefixPathValue
|
||||||
if (selectedCoverPath !== "") {
|
if (selectedCoverPath !== "") {
|
||||||
App.setCoverFromFile(newGame, selectedCoverPath)
|
App.setCoverFromFile(newGame, selectedCoverPath)
|
||||||
}
|
}
|
||||||
|
|
@ -67,10 +96,26 @@ Kirigami.Dialog {
|
||||||
|
|
||||||
property string selectedCoverPath: ""
|
property string selectedCoverPath: ""
|
||||||
|
|
||||||
|
readonly property bool anyMenuOpen: (runnerCombo && runnerCombo.popup && runnerCombo.popup.visible) || (profileCombo && profileCombo.popup && profileCombo.popup.visible)
|
||||||
|
|
||||||
|
function closeCurrentMenu() {
|
||||||
|
if (runnerCombo && runnerCombo.popup && runnerCombo.popup.visible) {
|
||||||
|
runnerCombo.popup.close()
|
||||||
|
}
|
||||||
|
if (profileCombo && profileCombo.popup && profileCombo.popup.visible) {
|
||||||
|
profileCombo.popup.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function isDescendant(item, ancestor) {
|
function isDescendant(item, ancestor) {
|
||||||
let p = item
|
let p = item
|
||||||
while (p) {
|
while (p) {
|
||||||
if (p === ancestor || (ancestor.contentItem && p === ancestor.contentItem)) return true
|
if (p === ancestor || (ancestor.contentItem && p === ancestor.contentItem)) return true
|
||||||
|
if (p.visualParent !== undefined && p.visualParent !== null) {
|
||||||
|
if (dialog.isDescendant(p.visualParent, ancestor)) return true
|
||||||
|
} else if (p.popup !== undefined && p.popup !== null && p.popup.visualParent !== undefined && p.popup.visualParent !== null) {
|
||||||
|
if (dialog.isDescendant(p.popup.visualParent, ancestor)) return true
|
||||||
|
}
|
||||||
p = p.parent
|
p = p.parent
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|
@ -99,7 +144,7 @@ Kirigami.Dialog {
|
||||||
item.toggle()
|
item.toggle()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (item.hasOwnProperty("checked")) {
|
if (item.checkable !== undefined && item.checkable && item.checked !== undefined) {
|
||||||
item.checked = !item.checked
|
item.checked = !item.checked
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -109,6 +154,92 @@ Kirigami.Dialog {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function envModelToMap() {
|
||||||
|
let result = ({})
|
||||||
|
for (let i = 0; i < envModel.count; i++) {
|
||||||
|
let row = envModel.get(i)
|
||||||
|
let k = (row.key || "").trim()
|
||||||
|
if (k === "") continue
|
||||||
|
if (k.indexOf("=") !== -1) continue
|
||||||
|
result[k] = String(row.value || "")
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function setEnvVar(key, value) {
|
||||||
|
let k = (key || "").trim()
|
||||||
|
if (k === "") return
|
||||||
|
for (let i = 0; i < envModel.count; i++) {
|
||||||
|
let row = envModel.get(i)
|
||||||
|
if ((row.key || "").trim() === k) {
|
||||||
|
envModel.setProperty(i, "value", String(value || ""))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
envModel.append({ key: k, value: String(value || "") })
|
||||||
|
}
|
||||||
|
|
||||||
|
function runnerFromIndex(idx) {
|
||||||
|
if (idx === 1) return "wine"
|
||||||
|
if (idx === 2) return "proton"
|
||||||
|
if (idx === 3) return "custom"
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function runnerToIndex(runner) {
|
||||||
|
let r = (runner || "").trim().toLowerCase()
|
||||||
|
if (r === "wine") return 1
|
||||||
|
if (r === "proton") return 2
|
||||||
|
if (r === "custom") return 3
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function profileIdFromIndex(idx) {
|
||||||
|
if (idx === 1) return "couch"
|
||||||
|
return "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
function profileIndexFromId(profileId) {
|
||||||
|
if ((profileId || "").trim() === "couch") return 1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadProfileFields() {
|
||||||
|
envModel.clear()
|
||||||
|
|
||||||
|
if (!isEditing || !game) {
|
||||||
|
runnerCombo.currentIndex = 0
|
||||||
|
runnerPathField.text = ""
|
||||||
|
prefixPathField.text = ""
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dialog.editProfileId === "couch") {
|
||||||
|
let cfg = game.launchProfileConfig("couch") || ({})
|
||||||
|
runnerCombo.currentIndex = dialog.runnerToIndex(cfg.runner)
|
||||||
|
runnerPathField.text = cfg.runnerPath || ""
|
||||||
|
prefixPathField.text = cfg.prefixPath || ""
|
||||||
|
let env = cfg.env || ({})
|
||||||
|
let keys = Object.keys(env)
|
||||||
|
keys.sort()
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
let k = keys[i]
|
||||||
|
envModel.append({ key: k, value: String(env[k]) })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
runnerCombo.currentIndex = dialog.runnerToIndex(game.launchRunner)
|
||||||
|
runnerPathField.text = game.launchRunnerPath || ""
|
||||||
|
prefixPathField.text = game.launchPrefixPath || ""
|
||||||
|
let env = game.launchEnv || ({})
|
||||||
|
let keys = Object.keys(env)
|
||||||
|
keys.sort()
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
let k = keys[i]
|
||||||
|
envModel.append({ key: k, value: String(env[k]) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function loadFields() {
|
function loadFields() {
|
||||||
selectedCoverPath = ""
|
selectedCoverPath = ""
|
||||||
if (isEditing && game) {
|
if (isEditing && game) {
|
||||||
|
|
@ -116,17 +247,32 @@ Kirigami.Dialog {
|
||||||
developerField.text = game.developer || ""
|
developerField.text = game.developer || ""
|
||||||
executableField.text = game.launchCommand || ""
|
executableField.text = game.launchCommand || ""
|
||||||
workingDirField.text = game.workingDirectory || ""
|
workingDirField.text = game.workingDirectory || ""
|
||||||
|
|
||||||
|
dialog.editProfileId = (App.config.uiMode === Config.Couch
|
||||||
|
|| (App.config.uiMode === Config.Auto
|
||||||
|
&& GamepadManager.connected
|
||||||
|
&& (InputManager.activeInput === InputManager.Gamepad || !InputManager.hasSeenKeyboardMouse)))
|
||||||
|
? "couch"
|
||||||
|
: "default"
|
||||||
|
profileCombo.currentIndex = dialog.profileIndexFromId(dialog.editProfileId)
|
||||||
|
dialog.loadProfileFields()
|
||||||
} else {
|
} else {
|
||||||
nameField.text = ""
|
nameField.text = ""
|
||||||
developerField.text = ""
|
developerField.text = ""
|
||||||
executableField.text = ""
|
executableField.text = ""
|
||||||
workingDirField.text = ""
|
workingDirField.text = ""
|
||||||
|
|
||||||
|
dialog.editProfileId = "default"
|
||||||
|
profileCombo.currentIndex = 0
|
||||||
|
dialog.loadProfileFields()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: GamepadManager
|
target: GamepadManager
|
||||||
function onNavigateUp() {
|
function onNavigateUp() {
|
||||||
|
let app = applicationWindow()
|
||||||
|
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
|
||||||
if (!dialog.opened) return
|
if (!dialog.opened) return
|
||||||
let w = applicationWindow()
|
let w = applicationWindow()
|
||||||
if (!w || !w.activeFocusItem) return
|
if (!w || !w.activeFocusItem) return
|
||||||
|
|
@ -134,6 +280,8 @@ Kirigami.Dialog {
|
||||||
dialog.focusNextInChain(false)
|
dialog.focusNextInChain(false)
|
||||||
}
|
}
|
||||||
function onNavigateDown() {
|
function onNavigateDown() {
|
||||||
|
let app = applicationWindow()
|
||||||
|
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
|
||||||
if (!dialog.opened) return
|
if (!dialog.opened) return
|
||||||
let w = applicationWindow()
|
let w = applicationWindow()
|
||||||
if (!w || !w.activeFocusItem) return
|
if (!w || !w.activeFocusItem) return
|
||||||
|
|
@ -141,6 +289,8 @@ Kirigami.Dialog {
|
||||||
dialog.focusNextInChain(true)
|
dialog.focusNextInChain(true)
|
||||||
}
|
}
|
||||||
function onNavigateLeft() {
|
function onNavigateLeft() {
|
||||||
|
let app = applicationWindow()
|
||||||
|
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
|
||||||
if (!dialog.opened) return
|
if (!dialog.opened) return
|
||||||
let w = applicationWindow()
|
let w = applicationWindow()
|
||||||
if (!w || !w.activeFocusItem) return
|
if (!w || !w.activeFocusItem) return
|
||||||
|
|
@ -148,6 +298,8 @@ Kirigami.Dialog {
|
||||||
dialog.focusNextInChain(false)
|
dialog.focusNextInChain(false)
|
||||||
}
|
}
|
||||||
function onNavigateRight() {
|
function onNavigateRight() {
|
||||||
|
let app = applicationWindow()
|
||||||
|
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
|
||||||
if (!dialog.opened) return
|
if (!dialog.opened) return
|
||||||
let w = applicationWindow()
|
let w = applicationWindow()
|
||||||
if (!w || !w.activeFocusItem) return
|
if (!w || !w.activeFocusItem) return
|
||||||
|
|
@ -155,6 +307,8 @@ Kirigami.Dialog {
|
||||||
dialog.focusNextInChain(true)
|
dialog.focusNextInChain(true)
|
||||||
}
|
}
|
||||||
function onSelectPressed() {
|
function onSelectPressed() {
|
||||||
|
let app = applicationWindow()
|
||||||
|
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
|
||||||
if (!dialog.opened) return
|
if (!dialog.opened) return
|
||||||
let w = applicationWindow()
|
let w = applicationWindow()
|
||||||
if (!w || !w.activeFocusItem) return
|
if (!w || !w.activeFocusItem) return
|
||||||
|
|
@ -215,6 +369,197 @@ Kirigami.Dialog {
|
||||||
text: ""
|
text: ""
|
||||||
placeholderText: i18n("Optional")
|
placeholderText: i18n("Optional")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FormCard.FormDelegateSeparator {}
|
||||||
|
|
||||||
|
FormCard.FormCard {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
|
FormCard.FormHeader {
|
||||||
|
title: i18n("Compatibility")
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.margins: Kirigami.Units.largeSpacing
|
||||||
|
visible: isEditing
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
spacing: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
|
QQC2.Label {
|
||||||
|
text: i18n("Profile")
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.ComboBox {
|
||||||
|
id: profileCombo
|
||||||
|
Layout.fillWidth: true
|
||||||
|
model: [i18n("Default"), i18n("Couch")]
|
||||||
|
onCurrentIndexChanged: if (dialog.isEditing) {
|
||||||
|
dialog.editProfileId = dialog.profileIdFromIndex(currentIndex)
|
||||||
|
dialog.loadProfileFields()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormDelegateSeparator { visible: isEditing }
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.margins: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
spacing: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
|
QQC2.Label {
|
||||||
|
text: i18n("Runner")
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.ComboBox {
|
||||||
|
id: runnerCombo
|
||||||
|
Layout.fillWidth: true
|
||||||
|
model: [i18n("Native"), i18n("Wine"), i18n("Proton"), i18n("Custom")]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormDelegateSeparator {}
|
||||||
|
|
||||||
|
FormCard.FormTextFieldDelegate {
|
||||||
|
id: runnerPathField
|
||||||
|
label: i18n("Runner Path")
|
||||||
|
text: ""
|
||||||
|
placeholderText: runnerCombo.currentIndex === 2 ? i18n("Auto-detect Proton") : (runnerCombo.currentIndex === 1 ? i18n("Use system Wine") : i18n("Required for Custom"))
|
||||||
|
enabled: runnerCombo.currentIndex !== 0
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormDelegateSeparator {}
|
||||||
|
|
||||||
|
FormCard.FormButtonDelegate {
|
||||||
|
text: i18n("Browse Runner...")
|
||||||
|
icon.name: "document-open"
|
||||||
|
enabled: runnerCombo.currentIndex !== 0
|
||||||
|
onClicked: runnerFileDialog.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormDelegateSeparator {}
|
||||||
|
|
||||||
|
FormCard.FormTextFieldDelegate {
|
||||||
|
id: prefixPathField
|
||||||
|
label: i18n("Prefix Path")
|
||||||
|
text: ""
|
||||||
|
placeholderText: i18n("Default (AppDataLocation/prefixes/<gameId>)")
|
||||||
|
enabled: runnerCombo.currentIndex === 1 || runnerCombo.currentIndex === 2
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormDelegateSeparator {}
|
||||||
|
|
||||||
|
FormCard.FormButtonDelegate {
|
||||||
|
text: i18n("Browse Prefix...")
|
||||||
|
icon.name: "document-open-folder"
|
||||||
|
enabled: runnerCombo.currentIndex === 1 || runnerCombo.currentIndex === 2
|
||||||
|
onClicked: prefixFolderDialog.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormCard {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
|
FormCard.FormHeader {
|
||||||
|
title: i18n("Environment Variables")
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormTextDelegate {
|
||||||
|
text: i18n("These variables are applied only when launching this game from A-La-Karte.")
|
||||||
|
textItem.wrapMode: Text.WordWrap
|
||||||
|
textItem.font: Kirigami.Theme.smallFont
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormDelegateSeparator {}
|
||||||
|
|
||||||
|
FormCard.FormButtonDelegate {
|
||||||
|
text: i18n("Add Variable")
|
||||||
|
icon.name: "list-add"
|
||||||
|
onClicked: envModel.append({ key: "", value: "" })
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormDelegateSeparator {}
|
||||||
|
|
||||||
|
FormCard.FormButtonDelegate {
|
||||||
|
text: i18n("Preset: MangoHud")
|
||||||
|
description: i18n("Sets MANGOHUD=1")
|
||||||
|
icon.name: "games-hint"
|
||||||
|
onClicked: dialog.setEnvVar("MANGOHUD", "1")
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormDelegateSeparator {}
|
||||||
|
|
||||||
|
FormCard.FormButtonDelegate {
|
||||||
|
text: i18n("Preset: GameMode")
|
||||||
|
description: i18n("Sets GAMEMODERUN=1")
|
||||||
|
icon.name: "speedometer"
|
||||||
|
onClicked: dialog.setEnvVar("GAMEMODERUN", "1")
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormDelegateSeparator {}
|
||||||
|
|
||||||
|
FormCard.FormButtonDelegate {
|
||||||
|
text: i18n("Clear Variables")
|
||||||
|
icon.name: "edit-clear"
|
||||||
|
enabled: envModel.count > 0
|
||||||
|
onClicked: envModel.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.margins: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: envModel
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
QQC2.TextField {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredWidth: 1
|
||||||
|
placeholderText: i18n("KEY")
|
||||||
|
text: key
|
||||||
|
onTextChanged: envModel.setProperty(index, "key", text)
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.TextField {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredWidth: 2
|
||||||
|
placeholderText: i18n("VALUE")
|
||||||
|
text: value
|
||||||
|
onTextChanged: envModel.setProperty(index, "value", text)
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.ToolButton {
|
||||||
|
icon.name: "list-remove"
|
||||||
|
onClicked: envModel.remove(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FormCard.FormCard {
|
FormCard.FormCard {
|
||||||
|
|
@ -339,6 +684,23 @@ Kirigami.Dialog {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FileDialog {
|
||||||
|
id: runnerFileDialog
|
||||||
|
title: i18n("Select Runner Executable")
|
||||||
|
fileMode: FileDialog.OpenFile
|
||||||
|
onAccepted: {
|
||||||
|
runnerPathField.text = selectedFile.toString().replace("file://", "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FolderDialog {
|
||||||
|
id: prefixFolderDialog
|
||||||
|
title: i18n("Select Prefix Folder")
|
||||||
|
onAccepted: {
|
||||||
|
prefixPathField.text = selectedFolder.toString().replace("file://", "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function saveCoverImage(game) {
|
function saveCoverImage(game) {
|
||||||
if (selectedCoverPath === "") return
|
if (selectedCoverPath === "") return
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ FocusScope {
|
||||||
|
|
||||||
property string filterSource: "all"
|
property string filterSource: "all"
|
||||||
property bool searchActive: false
|
property bool searchActive: false
|
||||||
property int focusedIndex: 0
|
property int focusedIndex: -1
|
||||||
|
|
||||||
property int adaptiveCardSize: App.config.gridSize
|
property int adaptiveCardSize: App.config.gridSize
|
||||||
property bool isTouchDevice: false
|
property bool isTouchDevice: false
|
||||||
|
|
@ -25,6 +25,12 @@ FocusScope {
|
||||||
readonly property int gameCount: proxyModel.count
|
readonly property int gameCount: proxyModel.count
|
||||||
property url focusedCoverUrl: ""
|
property url focusedCoverUrl: ""
|
||||||
|
|
||||||
|
readonly property bool anyMenuOpen: searchHeader.anyMenuOpen
|
||||||
|
|
||||||
|
function closeCurrentMenu() {
|
||||||
|
searchHeader.closeCurrentMenu()
|
||||||
|
}
|
||||||
|
|
||||||
function focusSearch() {
|
function focusSearch() {
|
||||||
searchField.forceActiveFocus()
|
searchField.forceActiveFocus()
|
||||||
}
|
}
|
||||||
|
|
@ -35,6 +41,14 @@ FocusScope {
|
||||||
}
|
}
|
||||||
|
|
||||||
function restoreFocus() {
|
function restoreFocus() {
|
||||||
|
let w = applicationWindow()
|
||||||
|
if (w && w.hasOwnProperty("pendingSidebarOpen") && w.pendingSidebarOpen) {
|
||||||
|
w.pendingSidebarOpen = false
|
||||||
|
if (w.globalDrawer && typeof w.globalDrawer.open === "function") {
|
||||||
|
w.globalDrawer.open()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
if (libraryRoot.searchActive) {
|
if (libraryRoot.searchActive) {
|
||||||
libraryRoot.focusSearch()
|
libraryRoot.focusSearch()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -47,6 +61,10 @@ FocusScope {
|
||||||
|
|
||||||
function launchFocusedGame() {
|
function launchFocusedGame() {
|
||||||
if (!gameGrid || !proxyModel) return
|
if (!gameGrid || !proxyModel) return
|
||||||
|
if (gameGrid.currentIndex < 0 && proxyModel.count > 0) {
|
||||||
|
gameGrid.currentIndex = 0
|
||||||
|
libraryRoot.focusedIndex = 0
|
||||||
|
}
|
||||||
let game = proxyModel.get(gameGrid.currentIndex)
|
let game = proxyModel.get(gameGrid.currentIndex)
|
||||||
if (game) {
|
if (game) {
|
||||||
libraryRoot.gameLaunched(game)
|
libraryRoot.gameLaunched(game)
|
||||||
|
|
@ -55,6 +73,10 @@ FocusScope {
|
||||||
|
|
||||||
function openDetailsForFocusedGame() {
|
function openDetailsForFocusedGame() {
|
||||||
if (!gameGrid || !proxyModel) return
|
if (!gameGrid || !proxyModel) return
|
||||||
|
if (gameGrid.currentIndex < 0 && proxyModel.count > 0) {
|
||||||
|
gameGrid.currentIndex = 0
|
||||||
|
libraryRoot.focusedIndex = 0
|
||||||
|
}
|
||||||
let game = proxyModel.get(gameGrid.currentIndex)
|
let game = proxyModel.get(gameGrid.currentIndex)
|
||||||
if (game) {
|
if (game) {
|
||||||
libraryRoot.gameSelected(game)
|
libraryRoot.gameSelected(game)
|
||||||
|
|
@ -168,6 +190,16 @@ FocusScope {
|
||||||
}
|
}
|
||||||
|
|
||||||
Keys.onDownPressed: gameGrid.forceActiveFocus()
|
Keys.onDownPressed: gameGrid.forceActiveFocus()
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: GamepadManager
|
||||||
|
function onNavigateDown() {
|
||||||
|
let w = applicationWindow()
|
||||||
|
if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return
|
||||||
|
if (!searchField.activeFocus) return
|
||||||
|
gameGrid.forceActiveFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -249,12 +281,20 @@ FocusScope {
|
||||||
|
|
||||||
Keys.onPressed: function(event) {
|
Keys.onPressed: function(event) {
|
||||||
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||||
|
if (currentIndex < 0 && proxyModel.count > 0) {
|
||||||
|
currentIndex = 0
|
||||||
|
libraryRoot.focusedIndex = 0
|
||||||
|
}
|
||||||
let game = proxyModel.get(currentIndex)
|
let game = proxyModel.get(currentIndex)
|
||||||
if (game) {
|
if (game) {
|
||||||
libraryRoot.gameSelected(game)
|
libraryRoot.gameSelected(game)
|
||||||
}
|
}
|
||||||
event.accepted = true
|
event.accepted = true
|
||||||
} else if (event.key === Qt.Key_Space) {
|
} else if (event.key === Qt.Key_Space) {
|
||||||
|
if (currentIndex < 0 && proxyModel.count > 0) {
|
||||||
|
currentIndex = 0
|
||||||
|
libraryRoot.focusedIndex = 0
|
||||||
|
}
|
||||||
let game = proxyModel.get(currentIndex)
|
let game = proxyModel.get(currentIndex)
|
||||||
if (game) {
|
if (game) {
|
||||||
libraryRoot.gameLaunched(game)
|
libraryRoot.gameLaunched(game)
|
||||||
|
|
|
||||||
1124
src/qml/Main.qml
1124
src/qml/Main.qml
File diff suppressed because it is too large
Load diff
|
|
@ -17,6 +17,105 @@ ColumnLayout {
|
||||||
showPlatformBadgesDelegate.forceActiveFocus()
|
showPlatformBadgesDelegate.forceActiveFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property var pendingDisableImportApply: null
|
||||||
|
property string pendingDisableImportName: ""
|
||||||
|
|
||||||
|
readonly property bool anyConfirmOpen: disableImportConfirmDialog.opened
|
||||||
|
|| fetchAllCoversConfirmDialog.opened
|
||||||
|
|| removeMissingConfirmDialog.opened
|
||||||
|
|| clearConfirmDialog.opened
|
||||||
|
|| resetConfirmDialog.opened
|
||||||
|
|
||||||
|
readonly property bool anyMenuOpen: uiModeMenu.visible
|
||||||
|
|
||||||
|
function currentConfirmDialog() {
|
||||||
|
if (disableImportConfirmDialog.opened) return disableImportConfirmDialog
|
||||||
|
if (fetchAllCoversConfirmDialog.opened) return fetchAllCoversConfirmDialog
|
||||||
|
if (removeMissingConfirmDialog.opened) return removeMissingConfirmDialog
|
||||||
|
if (clearConfirmDialog.opened) return clearConfirmDialog
|
||||||
|
if (resetConfirmDialog.opened) return resetConfirmDialog
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeCurrentConfirmDialog() {
|
||||||
|
let d = currentConfirmDialog()
|
||||||
|
if (!d) return
|
||||||
|
if (typeof d.reject === "function") {
|
||||||
|
d.reject()
|
||||||
|
} else {
|
||||||
|
d.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeCurrentMenu() {
|
||||||
|
if (uiModeMenu.visible) {
|
||||||
|
uiModeMenu.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDescendant(item, ancestor) {
|
||||||
|
let p = item
|
||||||
|
while (p) {
|
||||||
|
if (p === ancestor) return true
|
||||||
|
if (ancestor.contentItem && p === ancestor.contentItem) return true
|
||||||
|
if (p.visualParent !== undefined && p.visualParent !== null) {
|
||||||
|
if (settingsPage.isDescendant(p.visualParent, ancestor)) return true
|
||||||
|
} else if (p.popup !== undefined && p.popup !== null && p.popup.visualParent !== undefined && p.popup.visualParent !== null) {
|
||||||
|
if (settingsPage.isDescendant(p.popup.visualParent, ancestor)) return true
|
||||||
|
}
|
||||||
|
p = p.parent
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusNextInMenu(forward) {
|
||||||
|
let w = applicationWindow()
|
||||||
|
if (!w || !w.activeFocusItem) return
|
||||||
|
let next = w.activeFocusItem
|
||||||
|
for (let i = 0; i < 50; i++) {
|
||||||
|
next = next.nextItemInFocusChain(forward)
|
||||||
|
if (!next) return
|
||||||
|
if (settingsPage.isDescendant(next, uiModeMenu)) {
|
||||||
|
next.forceActiveFocus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateFocusedInMenu() {
|
||||||
|
let w = applicationWindow()
|
||||||
|
if (!w || !w.activeFocusItem) return
|
||||||
|
let item = w.activeFocusItem
|
||||||
|
if (typeof item.triggered === "function") {
|
||||||
|
item.triggered()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (typeof item.clicked === "function") {
|
||||||
|
item.clicked()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusNoButton(dialog) {
|
||||||
|
Qt.callLater(function() {
|
||||||
|
if (!dialog) return
|
||||||
|
if (typeof dialog.standardButton === "function") {
|
||||||
|
let noButton = dialog.standardButton(Kirigami.Dialog.No)
|
||||||
|
if (noButton) {
|
||||||
|
noButton.forceActiveFocus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dialog.forceActiveFocus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestDisableImport(sourceName, applyFn) {
|
||||||
|
pendingDisableImportName = sourceName
|
||||||
|
pendingDisableImportApply = applyFn
|
||||||
|
disableImportConfirmDialog.open()
|
||||||
|
}
|
||||||
|
|
||||||
FormCard.FormHeader {
|
FormCard.FormHeader {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
title: i18n("Appearance")
|
title: i18n("Appearance")
|
||||||
|
|
@ -39,7 +138,7 @@ ColumnLayout {
|
||||||
text: i18n("UI mode")
|
text: i18n("UI mode")
|
||||||
description: {
|
description: {
|
||||||
if (App.config.uiMode === Config.Desktop) return i18n("Desktop")
|
if (App.config.uiMode === Config.Desktop) return i18n("Desktop")
|
||||||
if (App.config.uiMode === Config.Handheld) return i18n("Handheld")
|
if (App.config.uiMode === Config.Couch) return i18n("Couch")
|
||||||
return i18n("Automatic")
|
return i18n("Automatic")
|
||||||
}
|
}
|
||||||
icon.name: "view-fullscreen"
|
icon.name: "view-fullscreen"
|
||||||
|
|
@ -47,8 +146,12 @@ ColumnLayout {
|
||||||
|
|
||||||
QQC2.Menu {
|
QQC2.Menu {
|
||||||
id: uiModeMenu
|
id: uiModeMenu
|
||||||
|
focus: true
|
||||||
|
|
||||||
|
onOpened: Qt.callLater(function() { uiModeAuto.forceActiveFocus() })
|
||||||
|
|
||||||
QQC2.MenuItem {
|
QQC2.MenuItem {
|
||||||
|
id: uiModeAuto
|
||||||
text: i18n("Automatic")
|
text: i18n("Automatic")
|
||||||
checkable: true
|
checkable: true
|
||||||
checked: App.config.uiMode === Config.Auto
|
checked: App.config.uiMode === Config.Auto
|
||||||
|
|
@ -56,6 +159,7 @@ ColumnLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
QQC2.MenuItem {
|
QQC2.MenuItem {
|
||||||
|
id: uiModeDesktop
|
||||||
text: i18n("Desktop")
|
text: i18n("Desktop")
|
||||||
checkable: true
|
checkable: true
|
||||||
checked: App.config.uiMode === Config.Desktop
|
checked: App.config.uiMode === Config.Desktop
|
||||||
|
|
@ -63,20 +167,61 @@ ColumnLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
QQC2.MenuItem {
|
QQC2.MenuItem {
|
||||||
text: i18n("Handheld")
|
id: uiModeCouch
|
||||||
|
text: i18n("Couch")
|
||||||
checkable: true
|
checkable: true
|
||||||
checked: App.config.uiMode === Config.Handheld
|
checked: App.config.uiMode === Config.Couch
|
||||||
onTriggered: App.config.uiMode = Config.Handheld
|
onTriggered: App.config.uiMode = Config.Couch
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FormCard.FormHeader {
|
Connections {
|
||||||
Layout.fillWidth: true
|
target: GamepadManager
|
||||||
title: i18n("Import Sources")
|
function onNavigateUp() {
|
||||||
|
if (!uiModeMenu.visible) return
|
||||||
|
let w = applicationWindow()
|
||||||
|
if (!w || !w.activeFocusItem) return
|
||||||
|
if (!settingsPage.isDescendant(w.activeFocusItem, uiModeMenu)) return
|
||||||
|
settingsPage.focusNextInMenu(false)
|
||||||
|
}
|
||||||
|
function onNavigateDown() {
|
||||||
|
if (!uiModeMenu.visible) return
|
||||||
|
let w = applicationWindow()
|
||||||
|
if (!w || !w.activeFocusItem) return
|
||||||
|
if (!settingsPage.isDescendant(w.activeFocusItem, uiModeMenu)) return
|
||||||
|
settingsPage.focusNextInMenu(true)
|
||||||
|
}
|
||||||
|
function onNavigateLeft() {
|
||||||
|
if (!uiModeMenu.visible) return
|
||||||
|
let w = applicationWindow()
|
||||||
|
if (!w || !w.activeFocusItem) return
|
||||||
|
if (!settingsPage.isDescendant(w.activeFocusItem, uiModeMenu)) return
|
||||||
|
settingsPage.focusNextInMenu(false)
|
||||||
|
}
|
||||||
|
function onNavigateRight() {
|
||||||
|
if (!uiModeMenu.visible) return
|
||||||
|
let w = applicationWindow()
|
||||||
|
if (!w || !w.activeFocusItem) return
|
||||||
|
if (!settingsPage.isDescendant(w.activeFocusItem, uiModeMenu)) return
|
||||||
|
settingsPage.focusNextInMenu(true)
|
||||||
|
}
|
||||||
|
function onSelectPressed() {
|
||||||
|
if (!uiModeMenu.visible) return
|
||||||
|
let w = applicationWindow()
|
||||||
|
if (!w || !w.activeFocusItem) return
|
||||||
|
if (!settingsPage.isDescendant(w.activeFocusItem, uiModeMenu)) return
|
||||||
|
settingsPage.activateFocusedInMenu()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FormCard.FormHeader {
|
||||||
|
Layout.topMargin: Kirigami.Units.mediumSpacing
|
||||||
|
Layout.fillWidth: true
|
||||||
|
title: i18n("Import Sources")
|
||||||
|
}
|
||||||
|
|
||||||
FormCard.FormCard {
|
FormCard.FormCard {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
FormCard.FormCheckDelegate {
|
FormCard.FormCheckDelegate {
|
||||||
|
|
@ -87,8 +232,26 @@ ColumnLayout {
|
||||||
secondary: "steam"
|
secondary: "steam"
|
||||||
resourceFallback: Qt.resolvedUrl("icons/brand/steam-symbolic.svg")
|
resourceFallback: Qt.resolvedUrl("icons/brand/steam-symbolic.svg")
|
||||||
}
|
}
|
||||||
|
property bool restoring: false
|
||||||
checked: App.config.importSteam
|
checked: App.config.importSteam
|
||||||
onToggled: App.config.importSteam = checked
|
onToggled: {
|
||||||
|
if (restoring) return
|
||||||
|
|
||||||
|
if (checked === App.config.importSteam) return
|
||||||
|
|
||||||
|
if (!checked && App.gameModel.hasPlatformPrefix("Steam")) {
|
||||||
|
restoring = true
|
||||||
|
checked = Qt.binding(function() { return App.config.importSteam })
|
||||||
|
restoring = false
|
||||||
|
settingsPage.requestDisableImport(i18n("Steam"), function() { App.config.importSteam = false })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
App.config.importSteam = checked
|
||||||
|
restoring = true
|
||||||
|
checked = Qt.binding(function() { return App.config.importSteam })
|
||||||
|
restoring = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FormCard.FormDelegateSeparator {}
|
FormCard.FormDelegateSeparator {}
|
||||||
|
|
@ -100,8 +263,26 @@ ColumnLayout {
|
||||||
primary: "lutris"
|
primary: "lutris"
|
||||||
secondary: "applications-games"
|
secondary: "applications-games"
|
||||||
}
|
}
|
||||||
|
property bool restoring: false
|
||||||
checked: App.config.importLutris
|
checked: App.config.importLutris
|
||||||
onToggled: App.config.importLutris = checked
|
onToggled: {
|
||||||
|
if (restoring) return
|
||||||
|
|
||||||
|
if (checked === App.config.importLutris) return
|
||||||
|
|
||||||
|
if (!checked && App.gameModel.hasPlatformPrefix("Lutris")) {
|
||||||
|
restoring = true
|
||||||
|
checked = Qt.binding(function() { return App.config.importLutris })
|
||||||
|
restoring = false
|
||||||
|
settingsPage.requestDisableImport(i18n("Lutris"), function() { App.config.importLutris = false })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
App.config.importLutris = checked
|
||||||
|
restoring = true
|
||||||
|
checked = Qt.binding(function() { return App.config.importLutris })
|
||||||
|
restoring = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FormCard.FormDelegateSeparator {}
|
FormCard.FormDelegateSeparator {}
|
||||||
|
|
@ -114,8 +295,26 @@ ColumnLayout {
|
||||||
primary: "com.heroicgameslauncher.hgl"
|
primary: "com.heroicgameslauncher.hgl"
|
||||||
secondary: "applications-games"
|
secondary: "applications-games"
|
||||||
}
|
}
|
||||||
|
property bool restoring: false
|
||||||
checked: App.config.importHeroic
|
checked: App.config.importHeroic
|
||||||
onToggled: App.config.importHeroic = checked
|
onToggled: {
|
||||||
|
if (restoring) return
|
||||||
|
|
||||||
|
if (checked === App.config.importHeroic) return
|
||||||
|
|
||||||
|
if (!checked && App.gameModel.hasPlatformPrefix("Heroic")) {
|
||||||
|
restoring = true
|
||||||
|
checked = Qt.binding(function() { return App.config.importHeroic })
|
||||||
|
restoring = false
|
||||||
|
settingsPage.requestDisableImport(i18n("Heroic Games Launcher"), function() { App.config.importHeroic = false })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
App.config.importHeroic = checked
|
||||||
|
restoring = true
|
||||||
|
checked = Qt.binding(function() { return App.config.importHeroic })
|
||||||
|
restoring = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FormCard.FormDelegateSeparator {}
|
FormCard.FormDelegateSeparator {}
|
||||||
|
|
@ -128,8 +327,26 @@ ColumnLayout {
|
||||||
primary: "user-desktop"
|
primary: "user-desktop"
|
||||||
secondary: "computer"
|
secondary: "computer"
|
||||||
}
|
}
|
||||||
|
property bool restoring: false
|
||||||
checked: App.config.importDesktop
|
checked: App.config.importDesktop
|
||||||
onToggled: App.config.importDesktop = checked
|
onToggled: {
|
||||||
|
if (restoring) return
|
||||||
|
|
||||||
|
if (checked === App.config.importDesktop) return
|
||||||
|
|
||||||
|
if (!checked && App.gameModel.hasPlatformPrefix("Desktop")) {
|
||||||
|
restoring = true
|
||||||
|
checked = Qt.binding(function() { return App.config.importDesktop })
|
||||||
|
restoring = false
|
||||||
|
settingsPage.requestDisableImport(i18n("Desktop Entries"), function() { App.config.importDesktop = false })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
App.config.importDesktop = checked
|
||||||
|
restoring = true
|
||||||
|
checked = Qt.binding(function() { return App.config.importDesktop })
|
||||||
|
restoring = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FormCard.FormDelegateSeparator {}
|
FormCard.FormDelegateSeparator {}
|
||||||
|
|
@ -142,8 +359,26 @@ ColumnLayout {
|
||||||
primary: "com.usebottles.bottles"
|
primary: "com.usebottles.bottles"
|
||||||
secondary: "application-x-executable"
|
secondary: "application-x-executable"
|
||||||
}
|
}
|
||||||
|
property bool restoring: false
|
||||||
checked: App.config.importBottles
|
checked: App.config.importBottles
|
||||||
onToggled: App.config.importBottles = checked
|
onToggled: {
|
||||||
|
if (restoring) return
|
||||||
|
|
||||||
|
if (checked === App.config.importBottles) return
|
||||||
|
|
||||||
|
if (!checked && App.gameModel.hasPlatformPrefix("Bottles")) {
|
||||||
|
restoring = true
|
||||||
|
checked = Qt.binding(function() { return App.config.importBottles })
|
||||||
|
restoring = false
|
||||||
|
settingsPage.requestDisableImport(i18n("Bottles"), function() { App.config.importBottles = false })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
App.config.importBottles = checked
|
||||||
|
restoring = true
|
||||||
|
checked = Qt.binding(function() { return App.config.importBottles })
|
||||||
|
restoring = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FormCard.FormDelegateSeparator {}
|
FormCard.FormDelegateSeparator {}
|
||||||
|
|
@ -156,8 +391,26 @@ ColumnLayout {
|
||||||
primary: "flatpak-discover"
|
primary: "flatpak-discover"
|
||||||
secondary: "applications-games"
|
secondary: "applications-games"
|
||||||
}
|
}
|
||||||
|
property bool restoring: false
|
||||||
checked: App.config.importFlatpak
|
checked: App.config.importFlatpak
|
||||||
onToggled: App.config.importFlatpak = checked
|
onToggled: {
|
||||||
|
if (restoring) return
|
||||||
|
|
||||||
|
if (checked === App.config.importFlatpak) return
|
||||||
|
|
||||||
|
if (!checked && App.gameModel.hasPlatformPrefix("Flatpak")) {
|
||||||
|
restoring = true
|
||||||
|
checked = Qt.binding(function() { return App.config.importFlatpak })
|
||||||
|
restoring = false
|
||||||
|
settingsPage.requestDisableImport(i18n("Flatpak"), function() { App.config.importFlatpak = false })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
App.config.importFlatpak = checked
|
||||||
|
restoring = true
|
||||||
|
checked = Qt.binding(function() { return App.config.importFlatpak })
|
||||||
|
restoring = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FormCard.FormDelegateSeparator {}
|
FormCard.FormDelegateSeparator {}
|
||||||
|
|
@ -171,8 +424,26 @@ ColumnLayout {
|
||||||
secondary: "itch"
|
secondary: "itch"
|
||||||
resourceFallback: Qt.resolvedUrl("icons/brand/itchdotio-symbolic.svg")
|
resourceFallback: Qt.resolvedUrl("icons/brand/itchdotio-symbolic.svg")
|
||||||
}
|
}
|
||||||
|
property bool restoring: false
|
||||||
checked: App.config.importItch
|
checked: App.config.importItch
|
||||||
onToggled: App.config.importItch = checked
|
onToggled: {
|
||||||
|
if (restoring) return
|
||||||
|
|
||||||
|
if (checked === App.config.importItch) return
|
||||||
|
|
||||||
|
if (!checked && App.gameModel.hasPlatformPrefix("itch.io")) {
|
||||||
|
restoring = true
|
||||||
|
checked = Qt.binding(function() { return App.config.importItch })
|
||||||
|
restoring = false
|
||||||
|
settingsPage.requestDisableImport(i18n("itch.io"), function() { App.config.importItch = false })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
App.config.importItch = checked
|
||||||
|
restoring = true
|
||||||
|
checked = Qt.binding(function() { return App.config.importItch })
|
||||||
|
restoring = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FormCard.FormDelegateSeparator {}
|
FormCard.FormDelegateSeparator {}
|
||||||
|
|
@ -185,8 +456,26 @@ ColumnLayout {
|
||||||
primary: "legendary"
|
primary: "legendary"
|
||||||
secondary: "applications-games"
|
secondary: "applications-games"
|
||||||
}
|
}
|
||||||
|
property bool restoring: false
|
||||||
checked: App.config.importLegendary
|
checked: App.config.importLegendary
|
||||||
onToggled: App.config.importLegendary = checked
|
onToggled: {
|
||||||
|
if (restoring) return
|
||||||
|
|
||||||
|
if (checked === App.config.importLegendary) return
|
||||||
|
|
||||||
|
if (!checked && App.gameModel.hasPlatformPrefix("Legendary")) {
|
||||||
|
restoring = true
|
||||||
|
checked = Qt.binding(function() { return App.config.importLegendary })
|
||||||
|
restoring = false
|
||||||
|
settingsPage.requestDisableImport(i18n("Legendary"), function() { App.config.importLegendary = false })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
App.config.importLegendary = checked
|
||||||
|
restoring = true
|
||||||
|
checked = Qt.binding(function() { return App.config.importLegendary })
|
||||||
|
restoring = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FormCard.FormDelegateSeparator {}
|
FormCard.FormDelegateSeparator {}
|
||||||
|
|
@ -200,8 +489,26 @@ ColumnLayout {
|
||||||
secondary: "retroarch"
|
secondary: "retroarch"
|
||||||
resourceFallback: Qt.resolvedUrl("icons/brand/retroarch-symbolic.svg")
|
resourceFallback: Qt.resolvedUrl("icons/brand/retroarch-symbolic.svg")
|
||||||
}
|
}
|
||||||
|
property bool restoring: false
|
||||||
checked: App.config.importRetroArch
|
checked: App.config.importRetroArch
|
||||||
onToggled: App.config.importRetroArch = checked
|
onToggled: {
|
||||||
|
if (restoring) return
|
||||||
|
|
||||||
|
if (checked === App.config.importRetroArch) return
|
||||||
|
|
||||||
|
if (!checked && App.gameModel.hasPlatformPrefix("RetroArch")) {
|
||||||
|
restoring = true
|
||||||
|
checked = Qt.binding(function() { return App.config.importRetroArch })
|
||||||
|
restoring = false
|
||||||
|
settingsPage.requestDisableImport(i18n("RetroArch"), function() { App.config.importRetroArch = false })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
App.config.importRetroArch = checked
|
||||||
|
restoring = true
|
||||||
|
checked = Qt.binding(function() { return App.config.importRetroArch })
|
||||||
|
restoring = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FormCard.FormDelegateSeparator {}
|
FormCard.FormDelegateSeparator {}
|
||||||
|
|
@ -215,6 +522,7 @@ ColumnLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
FormCard.FormHeader {
|
FormCard.FormHeader {
|
||||||
|
Layout.topMargin: Kirigami.Units.mediumSpacing
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
title: i18n("Behavior")
|
title: i18n("Behavior")
|
||||||
}
|
}
|
||||||
|
|
@ -239,6 +547,7 @@ ColumnLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
FormCard.FormHeader {
|
FormCard.FormHeader {
|
||||||
|
Layout.topMargin: Kirigami.Units.mediumSpacing
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
title: i18n("Cover Art")
|
title: i18n("Cover Art")
|
||||||
}
|
}
|
||||||
|
|
@ -263,6 +572,7 @@ ColumnLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
FormCard.FormHeader {
|
FormCard.FormHeader {
|
||||||
|
Layout.topMargin: Kirigami.Units.mediumSpacing
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
title: i18n("SteamGridDB")
|
title: i18n("SteamGridDB")
|
||||||
}
|
}
|
||||||
|
|
@ -305,7 +615,7 @@ ColumnLayout {
|
||||||
description: App.steamGridDB.busy ? i18n("Fetching...") : i18n("Download covers for all games")
|
description: App.steamGridDB.busy ? i18n("Fetching...") : i18n("Download covers for all games")
|
||||||
icon.name: "download"
|
icon.name: "download"
|
||||||
enabled: App.steamGridDB.enabled && App.steamGridDB.apiKey.length > 0 && !App.steamGridDB.busy
|
enabled: App.steamGridDB.enabled && App.steamGridDB.apiKey.length > 0 && !App.steamGridDB.busy
|
||||||
onClicked: App.steamGridDB.fetchAllCovers()
|
onClicked: fetchAllCoversConfirmDialog.open()
|
||||||
}
|
}
|
||||||
|
|
||||||
FormCard.FormDelegateSeparator {}
|
FormCard.FormDelegateSeparator {}
|
||||||
|
|
@ -322,6 +632,7 @@ ColumnLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
FormCard.FormHeader {
|
FormCard.FormHeader {
|
||||||
|
Layout.topMargin: Kirigami.Units.mediumSpacing
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
title: i18n("Library")
|
title: i18n("Library")
|
||||||
}
|
}
|
||||||
|
|
@ -332,15 +643,15 @@ ColumnLayout {
|
||||||
text: i18n("Import All Games")
|
text: i18n("Import All Games")
|
||||||
description: i18n("Scan all enabled sources")
|
description: i18n("Scan all enabled sources")
|
||||||
icon.name: "document-import"
|
icon.name: "document-import"
|
||||||
enabled: !App.importing && App.gameModel.count >= 0 && ((App.config.importSteam && !App.gameModel.hasPlatformPrefix("Steam"))
|
enabled: !App.importing && App.gameModel.count >= 0 && (App.config.importSteam
|
||||||
|| (App.config.importLutris && !App.gameModel.hasPlatformPrefix("Lutris"))
|
|| App.config.importLutris
|
||||||
|| (App.config.importHeroic && !App.gameModel.hasPlatformPrefix("Heroic"))
|
|| App.config.importHeroic
|
||||||
|| (App.config.importDesktop && !App.gameModel.hasPlatformPrefix("Desktop"))
|
|| App.config.importDesktop
|
||||||
|| (App.config.importBottles && !App.gameModel.hasPlatformPrefix("Bottles"))
|
|| App.config.importBottles
|
||||||
|| (App.config.importFlatpak && !App.gameModel.hasPlatformPrefix("Flatpak"))
|
|| App.config.importFlatpak
|
||||||
|| (App.config.importItch && !App.gameModel.hasPlatformPrefix("itch.io"))
|
|| App.config.importItch
|
||||||
|| (App.config.importLegendary && !App.gameModel.hasPlatformPrefix("Legendary"))
|
|| App.config.importLegendary
|
||||||
|| (App.config.importRetroArch && !App.gameModel.hasPlatformPrefix("RetroArch")))
|
|| App.config.importRetroArch)
|
||||||
onClicked: App.importAllGames()
|
onClicked: App.importAllGames()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -350,7 +661,7 @@ ColumnLayout {
|
||||||
text: i18n("Remove Missing Games")
|
text: i18n("Remove Missing Games")
|
||||||
description: i18n("Remove games whose executables no longer exist")
|
description: i18n("Remove games whose executables no longer exist")
|
||||||
icon.name: "edit-delete"
|
icon.name: "edit-delete"
|
||||||
onClicked: App.removeMissingGames()
|
onClicked: removeMissingConfirmDialog.open()
|
||||||
}
|
}
|
||||||
|
|
||||||
FormCard.FormDelegateSeparator {}
|
FormCard.FormDelegateSeparator {}
|
||||||
|
|
@ -372,11 +683,51 @@ ColumnLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Kirigami.PromptDialog {
|
||||||
|
id: disableImportConfirmDialog
|
||||||
|
title: i18n("Disable Import Source")
|
||||||
|
subtitle: i18n("Disabling %1 will remove all games imported from that source. Are you sure?", settingsPage.pendingDisableImportName)
|
||||||
|
standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No
|
||||||
|
onOpened: settingsPage.focusNoButton(disableImportConfirmDialog)
|
||||||
|
onAccepted: {
|
||||||
|
if (settingsPage.pendingDisableImportApply) {
|
||||||
|
settingsPage.pendingDisableImportApply()
|
||||||
|
}
|
||||||
|
settingsPage.pendingDisableImportApply = null
|
||||||
|
settingsPage.pendingDisableImportName = ""
|
||||||
|
}
|
||||||
|
onRejected: {
|
||||||
|
settingsPage.pendingDisableImportApply = null
|
||||||
|
settingsPage.pendingDisableImportName = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Kirigami.PromptDialog {
|
||||||
|
id: fetchAllCoversConfirmDialog
|
||||||
|
title: i18n("Fetch All Covers")
|
||||||
|
subtitle: App.steamGridDB.preferSteamGridDB
|
||||||
|
? i18n("This will download cover art for all games and may replace existing covers. Continue?")
|
||||||
|
: i18n("This will download cover art for games that are missing covers. Continue?")
|
||||||
|
standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No
|
||||||
|
onOpened: settingsPage.focusNoButton(fetchAllCoversConfirmDialog)
|
||||||
|
onAccepted: App.steamGridDB.fetchAllCovers()
|
||||||
|
}
|
||||||
|
|
||||||
|
Kirigami.PromptDialog {
|
||||||
|
id: removeMissingConfirmDialog
|
||||||
|
title: i18n("Remove Missing Games")
|
||||||
|
subtitle: i18n("This will remove games whose executables cannot be found. This cannot be undone. Continue?")
|
||||||
|
standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No
|
||||||
|
onOpened: settingsPage.focusNoButton(removeMissingConfirmDialog)
|
||||||
|
onAccepted: App.removeMissingGames()
|
||||||
|
}
|
||||||
|
|
||||||
Kirigami.PromptDialog {
|
Kirigami.PromptDialog {
|
||||||
id: clearConfirmDialog
|
id: clearConfirmDialog
|
||||||
title: i18n("Clear Library")
|
title: i18n("Clear Library")
|
||||||
subtitle: i18n("Are you sure you want to remove all games?")
|
subtitle: i18n("Are you sure you want to remove all games?")
|
||||||
standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No
|
standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No
|
||||||
|
onOpened: settingsPage.focusNoButton(clearConfirmDialog)
|
||||||
onAccepted: App.clearLibrary()
|
onAccepted: App.clearLibrary()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -385,6 +736,7 @@ ColumnLayout {
|
||||||
title: i18n("Reset Application")
|
title: i18n("Reset Application")
|
||||||
subtitle: i18n("This will remove all games and reset all settings to defaults. Are you sure?")
|
subtitle: i18n("This will remove all games and reset all settings to defaults. Are you sure?")
|
||||||
standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No
|
standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No
|
||||||
|
onOpened: settingsPage.focusNoButton(resetConfirmDialog)
|
||||||
onAccepted: {
|
onAccepted: {
|
||||||
App.clearLibrary()
|
App.clearLibrary()
|
||||||
App.config.resetToDefaults()
|
App.config.resetToDefaults()
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ ColumnLayout {
|
||||||
property string currentSourceName: i18n("All Games")
|
property string currentSourceName: i18n("All Games")
|
||||||
property bool suppressAutoClose: false
|
property bool suppressAutoClose: false
|
||||||
|
|
||||||
|
readonly property bool hasSidebarFocus: sourceList.activeFocus || importAction.activeFocus || settingsAction.activeFocus || aboutAction.activeFocus
|
||||||
|
|
||||||
readonly property int adaptiveFocusRingWidth: 1
|
readonly property int adaptiveFocusRingWidth: 1
|
||||||
|
|
||||||
signal sourceSelected(string source)
|
signal sourceSelected(string source)
|
||||||
|
|
@ -45,6 +47,17 @@ ColumnLayout {
|
||||||
sourceList.positionViewAtIndex(i, ListView.Contain)
|
sourceList.positionViewAtIndex(i, ListView.Contain)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applySourceById(sourceId) {
|
||||||
|
for (let i = 0; i < sourceModel.count; i++) {
|
||||||
|
let item = sourceModel.get(i)
|
||||||
|
if (item && item.sourceId === sourceId) {
|
||||||
|
applySourceAtIndex(i)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
applySourceAtIndex(0)
|
||||||
|
}
|
||||||
|
|
||||||
function cycleSource(delta) {
|
function cycleSource(delta) {
|
||||||
if (sourceModel.count <= 0) return
|
if (sourceModel.count <= 0) return
|
||||||
let i = sourceList.currentIndex
|
let i = sourceList.currentIndex
|
||||||
|
|
@ -97,9 +110,31 @@ ColumnLayout {
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: GamepadManager
|
target: GamepadManager
|
||||||
function onNavigateUp() { if (sourceList.activeFocus) sourceList.decrementCurrentIndex() }
|
function onNavigateUp() {
|
||||||
function onNavigateDown() { if (sourceList.activeFocus) sourceList.incrementCurrentIndex() }
|
let w = applicationWindow()
|
||||||
function onSelectPressed() { if (sourceList.activeFocus) sidebarRoot.activateCurrentItem() }
|
if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return
|
||||||
|
if (!sourceList.activeFocus) return
|
||||||
|
if (sourceList.count > 0 && sourceList.currentIndex <= 0) {
|
||||||
|
aboutAction.forceActiveFocus()
|
||||||
|
} else {
|
||||||
|
sourceList.decrementCurrentIndex()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function onNavigateDown() {
|
||||||
|
let w = applicationWindow()
|
||||||
|
if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return
|
||||||
|
if (!sourceList.activeFocus) return
|
||||||
|
if (sourceList.count > 0 && sourceList.currentIndex >= sourceList.count - 1) {
|
||||||
|
importAction.forceActiveFocus()
|
||||||
|
} else {
|
||||||
|
sourceList.incrementCurrentIndex()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function onSelectPressed() {
|
||||||
|
let w = applicationWindow()
|
||||||
|
if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return
|
||||||
|
if (sourceList.activeFocus) sidebarRoot.activateCurrentItem()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
delegate: QQC2.ItemDelegate {
|
delegate: QQC2.ItemDelegate {
|
||||||
|
|
@ -294,6 +329,8 @@ ColumnLayout {
|
||||||
Connections {
|
Connections {
|
||||||
target: GamepadManager
|
target: GamepadManager
|
||||||
function onNavigateUp() {
|
function onNavigateUp() {
|
||||||
|
let w = applicationWindow()
|
||||||
|
if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return
|
||||||
if (aboutAction.activeFocus) {
|
if (aboutAction.activeFocus) {
|
||||||
settingsAction.forceActiveFocus()
|
settingsAction.forceActiveFocus()
|
||||||
} else if (settingsAction.activeFocus) {
|
} else if (settingsAction.activeFocus) {
|
||||||
|
|
@ -304,13 +341,20 @@ ColumnLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function onNavigateDown() {
|
function onNavigateDown() {
|
||||||
|
let w = applicationWindow()
|
||||||
|
if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return
|
||||||
if (importAction.activeFocus) {
|
if (importAction.activeFocus) {
|
||||||
settingsAction.forceActiveFocus()
|
settingsAction.forceActiveFocus()
|
||||||
} else if (settingsAction.activeFocus) {
|
} else if (settingsAction.activeFocus) {
|
||||||
aboutAction.forceActiveFocus()
|
aboutAction.forceActiveFocus()
|
||||||
|
} else if (aboutAction.activeFocus) {
|
||||||
|
sourceList.forceActiveFocus()
|
||||||
|
sourceList.currentIndex = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function onSelectPressed() {
|
function onSelectPressed() {
|
||||||
|
let w = applicationWindow()
|
||||||
|
if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return
|
||||||
if (importAction.activeFocus) {
|
if (importAction.activeFocus) {
|
||||||
importAction.clicked()
|
importAction.clicked()
|
||||||
} else if (settingsAction.activeFocus) {
|
} else if (settingsAction.activeFocus) {
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,19 @@ import org.kde.alakarte
|
||||||
RowLayout {
|
RowLayout {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
spacing: Kirigami.Units.largeSpacing
|
spacing: uiMode === Config.Couch ? Kirigami.Units.largeSpacing * 1.25 : Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
readonly property bool useGamepadHints: GamepadManager.connected
|
property int uiMode: Config.Auto
|
||||||
|
property int activeInput: InputManager.KeyboardMouse
|
||||||
|
|
||||||
|
readonly property bool useGamepadHints: {
|
||||||
|
if (!GamepadManager.connected) return false
|
||||||
|
if (uiMode === Config.Couch) {
|
||||||
|
if (activeInput === InputManager.KeyboardMouse && InputManager.hasSeenKeyboardMouse) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return activeInput === InputManager.Gamepad
|
||||||
|
}
|
||||||
readonly property int style: GamepadManager.controllerStyle
|
readonly property int style: GamepadManager.controllerStyle
|
||||||
property string context: "library"
|
property string context: "library"
|
||||||
|
|
||||||
|
|
@ -49,6 +59,9 @@ RowLayout {
|
||||||
function iconForAux(action) {
|
function iconForAux(action) {
|
||||||
if (!useGamepadHints) return ""
|
if (!useGamepadHints) return ""
|
||||||
|
|
||||||
|
if (action === "dpad") {
|
||||||
|
return "qrc:/qt/qml/org/kde/alakarte/qml/icons/gamepad/generic/dpad.svg"
|
||||||
|
}
|
||||||
if (action === "lb") {
|
if (action === "lb") {
|
||||||
return "qrc:/qt/qml/org/kde/alakarte/qml/icons/gamepad/generic/lb.svg"
|
return "qrc:/qt/qml/org/kde/alakarte/qml/icons/gamepad/generic/lb.svg"
|
||||||
}
|
}
|
||||||
|
|
@ -72,12 +85,25 @@ RowLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
function keyboardLabel(action) {
|
function keyboardLabel(action) {
|
||||||
|
if (root.context === "confirm" || root.context === "confirm_remove") {
|
||||||
|
switch (action) {
|
||||||
|
case "navigate": return "Tab"
|
||||||
|
case "confirm": return "Enter"
|
||||||
|
case "back": return "Esc"
|
||||||
|
case "menu": return ""
|
||||||
|
default: return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (root.context === "library") {
|
if (root.context === "library") {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
|
case "navigate": return i18n("Arrows")
|
||||||
case "confirm": return "Space"
|
case "confirm": return "Space"
|
||||||
case "back": return "Esc"
|
case "back": return ""
|
||||||
case "details": return "Enter"
|
case "details": return "Enter"
|
||||||
case "search": return "Ctrl+F"
|
case "search": return "Ctrl+F"
|
||||||
|
case "lb": return "Ctrl+PgUp"
|
||||||
|
case "rb": return "Ctrl+PgDown"
|
||||||
case "menu": return "Ctrl+,"
|
case "menu": return "Ctrl+,"
|
||||||
default: return ""
|
default: return ""
|
||||||
}
|
}
|
||||||
|
|
@ -85,6 +111,7 @@ RowLayout {
|
||||||
|
|
||||||
if (root.context === "edit") {
|
if (root.context === "edit") {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
|
case "navigate": return "Tab"
|
||||||
case "confirm": return "Enter"
|
case "confirm": return "Enter"
|
||||||
case "back": return "Esc"
|
case "back": return "Esc"
|
||||||
default: return ""
|
default: return ""
|
||||||
|
|
@ -93,8 +120,11 @@ RowLayout {
|
||||||
|
|
||||||
if (root.context === "details") {
|
if (root.context === "details") {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
|
case "navigate": return "Tab"
|
||||||
case "confirm": return "Enter"
|
case "confirm": return "Enter"
|
||||||
case "back": return "Esc"
|
case "back": return "Esc"
|
||||||
|
case "details": return "F"
|
||||||
|
case "search": return "E"
|
||||||
case "menu": return "Ctrl+,"
|
case "menu": return "Ctrl+,"
|
||||||
default: return ""
|
default: return ""
|
||||||
}
|
}
|
||||||
|
|
@ -102,6 +132,19 @@ RowLayout {
|
||||||
|
|
||||||
if (root.context === "sidebar") {
|
if (root.context === "sidebar") {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
|
case "navigate": return i18n("Arrows/Tab")
|
||||||
|
case "confirm": return "Enter"
|
||||||
|
case "back": return "Esc"
|
||||||
|
case "lb": return "Ctrl+PgUp"
|
||||||
|
case "rb": return "Ctrl+PgDown"
|
||||||
|
case "menu": return "Ctrl+,"
|
||||||
|
default: return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.context === "settings") {
|
||||||
|
switch (action) {
|
||||||
|
case "navigate": return "Tab"
|
||||||
case "confirm": return "Enter"
|
case "confirm": return "Enter"
|
||||||
case "back": return "Esc"
|
case "back": return "Esc"
|
||||||
case "menu": return "Ctrl+,"
|
case "menu": return "Ctrl+,"
|
||||||
|
|
@ -109,8 +152,9 @@ RowLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (root.context === "settings" || root.context === "import" || root.context === "sidebar") {
|
if (root.context === "settings" || root.context === "import" || root.context === "sidebar" || root.context === "about" || root.context === "diagnostics") {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
|
case "navigate": return "Tab"
|
||||||
case "confirm": return "Enter"
|
case "confirm": return "Enter"
|
||||||
case "back": return "Esc"
|
case "back": return "Esc"
|
||||||
default: return ""
|
default: return ""
|
||||||
|
|
@ -121,53 +165,100 @@ RowLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
function actionLabel(action) {
|
function actionLabel(action) {
|
||||||
|
if (root.context === "confirm" || root.context === "confirm_remove") {
|
||||||
|
switch (action) {
|
||||||
|
case "navigate": return (useGamepadHints || keyboardLabel("navigate") !== "") ? i18n("Navigate") : ""
|
||||||
|
case "confirm": return i18n("Select")
|
||||||
|
case "back": return i18n("Cancel")
|
||||||
|
case "menu": return (useGamepadHints && uiMode === Config.Couch) ? i18n("Menu") : ""
|
||||||
|
default: return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (root.context === "library") {
|
if (root.context === "library") {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
|
case "navigate": return (useGamepadHints || keyboardLabel("navigate") !== "") ? i18n("Navigate") : ""
|
||||||
case "confirm": return i18n("Play")
|
case "confirm": return i18n("Play")
|
||||||
case "back": return i18n("Back")
|
case "back": return ""
|
||||||
case "details": return i18n("Details")
|
case "details": return i18n("Details")
|
||||||
case "search": return i18n("Search")
|
case "search": return i18n("Search")
|
||||||
case "lb": return i18n("Prev Source")
|
case "lb": return i18n("Prev Source")
|
||||||
case "rb": return i18n("Next Source")
|
case "rb": return i18n("Next Source")
|
||||||
case "menu": return i18n("Settings")
|
case "menu": return (useGamepadHints && uiMode === Config.Couch) ? i18n("Menu") : i18n("Settings")
|
||||||
|
default: return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.context === "settings") {
|
||||||
|
switch (action) {
|
||||||
|
case "navigate": return (useGamepadHints || keyboardLabel("navigate") !== "") ? i18n("Navigate") : ""
|
||||||
|
case "confirm": return i18n("Select")
|
||||||
|
case "back": return i18n("Back")
|
||||||
|
case "menu": return (useGamepadHints && uiMode === Config.Couch) ? i18n("Menu") : i18n("Close")
|
||||||
|
default: return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.context === "diagnostics") {
|
||||||
|
switch (action) {
|
||||||
|
case "navigate": return (useGamepadHints || keyboardLabel("navigate") !== "") ? i18n("Navigate") : ""
|
||||||
|
case "confirm": return i18n("Select")
|
||||||
|
case "back": return i18n("Back")
|
||||||
|
case "menu": return (useGamepadHints && uiMode === Config.Couch) ? i18n("Menu") : i18n("Close")
|
||||||
default: return ""
|
default: return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (root.context === "edit") {
|
if (root.context === "edit") {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
|
case "navigate": return (useGamepadHints || keyboardLabel("navigate") !== "") ? i18n("Navigate") : ""
|
||||||
case "confirm": return i18n("Select")
|
case "confirm": return i18n("Select")
|
||||||
case "back": return i18n("Back")
|
case "back": return i18n("Back")
|
||||||
|
case "menu": return (useGamepadHints && uiMode === Config.Couch) ? i18n("Menu") : ""
|
||||||
default: return ""
|
default: return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (root.context === "details") {
|
if (root.context === "details") {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case "confirm": return i18n("Play")
|
case "navigate": return (useGamepadHints || keyboardLabel("navigate") !== "") ? i18n("Navigate") : ""
|
||||||
|
case "confirm": return i18n("Select")
|
||||||
case "back": return i18n("Back")
|
case "back": return i18n("Back")
|
||||||
case "details": return i18n("Favorite")
|
case "details": return i18n("Favorite")
|
||||||
case "search": return i18n("Edit")
|
case "search": return i18n("Edit")
|
||||||
case "menu": return i18n("Settings")
|
case "menu": return (useGamepadHints && uiMode === Config.Couch) ? i18n("Menu") : i18n("Settings")
|
||||||
default: return ""
|
default: return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (root.context === "sidebar") {
|
if (root.context === "sidebar") {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
|
case "navigate": return (useGamepadHints || keyboardLabel("navigate") !== "") ? i18n("Navigate") : ""
|
||||||
case "confirm": return i18n("Select")
|
case "confirm": return i18n("Select")
|
||||||
case "back": return i18n("Back")
|
case "back": return i18n("Back")
|
||||||
case "lb": return i18n("Prev Source")
|
case "lb": return i18n("Prev Source")
|
||||||
case "rb": return i18n("Next Source")
|
case "rb": return i18n("Next Source")
|
||||||
case "menu": return i18n("Settings")
|
case "menu": return (useGamepadHints && uiMode === Config.Couch) ? i18n("Close") : i18n("Settings")
|
||||||
default: return ""
|
default: return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (root.context === "settings" || root.context === "import" || root.context === "sidebar") {
|
if (root.context === "settings" || root.context === "import" || root.context === "sidebar") {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
|
case "navigate": return (useGamepadHints || keyboardLabel("navigate") !== "") ? i18n("Navigate") : ""
|
||||||
case "confirm": return i18n("Select")
|
case "confirm": return i18n("Select")
|
||||||
case "back": return i18n("Back")
|
case "back": return i18n("Back")
|
||||||
|
case "menu": return (useGamepadHints && uiMode === Config.Couch) ? i18n("Menu") : ""
|
||||||
|
default: return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.context === "about") {
|
||||||
|
switch (action) {
|
||||||
|
case "navigate": return (useGamepadHints || keyboardLabel("navigate") !== "") ? i18n("Navigate") : ""
|
||||||
|
case "confirm": return i18n("Select")
|
||||||
|
case "back": return i18n("Back")
|
||||||
|
case "menu": return (useGamepadHints && uiMode === Config.Couch) ? i18n("Menu") : ""
|
||||||
default: return ""
|
default: return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -195,7 +286,7 @@ RowLayout {
|
||||||
border.width: 1
|
border.width: 1
|
||||||
border.color: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.2)
|
border.color: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.2)
|
||||||
|
|
||||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 1.4
|
Layout.preferredHeight: uiMode === Config.Couch ? Kirigami.Units.gridUnit * 1.8 : Kirigami.Units.gridUnit * 1.4
|
||||||
Layout.preferredWidth: useGamepadHints
|
Layout.preferredWidth: useGamepadHints
|
||||||
? Layout.preferredHeight
|
? Layout.preferredHeight
|
||||||
: Math.max(keyText.implicitWidth + Kirigami.Units.mediumSpacing * 2, Layout.preferredHeight)
|
: Math.max(keyText.implicitWidth + Kirigami.Units.mediumSpacing * 2, Layout.preferredHeight)
|
||||||
|
|
@ -215,7 +306,7 @@ RowLayout {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: parent.parent.keyLabel
|
text: parent.parent.keyLabel
|
||||||
font.bold: true
|
font.bold: true
|
||||||
font.pointSize: Kirigami.Theme.smallFont.pointSize
|
font.pointSize: uiMode === Config.Couch ? Kirigami.Theme.defaultFont.pointSize : Kirigami.Theme.smallFont.pointSize
|
||||||
color: Kirigami.Theme.textColor
|
color: Kirigami.Theme.textColor
|
||||||
visible: !useGamepadHints
|
visible: !useGamepadHints
|
||||||
}
|
}
|
||||||
|
|
@ -224,11 +315,18 @@ RowLayout {
|
||||||
QQC2.Label {
|
QQC2.Label {
|
||||||
text: parent.label
|
text: parent.label
|
||||||
color: Kirigami.Theme.textColor
|
color: Kirigami.Theme.textColor
|
||||||
font.pointSize: Kirigami.Theme.smallFont.pointSize
|
font.pointSize: uiMode === Config.Couch ? Kirigami.Theme.defaultFont.pointSize : Kirigami.Theme.smallFont.pointSize
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HintItem {
|
||||||
|
action: "navigate"
|
||||||
|
label: actionLabel("navigate")
|
||||||
|
iconSource: iconForAux("dpad")
|
||||||
|
keyLabel: keyboardLabel("navigate")
|
||||||
|
}
|
||||||
|
|
||||||
HintItem {
|
HintItem {
|
||||||
action: "confirm"
|
action: "confirm"
|
||||||
label: actionLabel("confirm")
|
label: actionLabel("confirm")
|
||||||
|
|
@ -261,20 +359,22 @@ RowLayout {
|
||||||
action: "lb"
|
action: "lb"
|
||||||
label: actionLabel("lb")
|
label: actionLabel("lb")
|
||||||
iconSource: (root.context === "library" || root.context === "sidebar") ? iconForAux("lb") : ""
|
iconSource: (root.context === "library" || root.context === "sidebar") ? iconForAux("lb") : ""
|
||||||
keyLabel: ""
|
keyLabel: keyboardLabel("lb")
|
||||||
}
|
}
|
||||||
|
|
||||||
HintItem {
|
HintItem {
|
||||||
action: "rb"
|
action: "rb"
|
||||||
label: actionLabel("rb")
|
label: actionLabel("rb")
|
||||||
iconSource: (root.context === "library" || root.context === "sidebar") ? iconForAux("rb") : ""
|
iconSource: (root.context === "library" || root.context === "sidebar") ? iconForAux("rb") : ""
|
||||||
keyLabel: ""
|
keyLabel: keyboardLabel("rb")
|
||||||
}
|
}
|
||||||
|
|
||||||
HintItem {
|
HintItem {
|
||||||
action: "menu"
|
action: "menu"
|
||||||
label: actionLabel("menu")
|
label: actionLabel("menu")
|
||||||
iconSource: (root.context === "library" || root.context === "details" || root.context === "sidebar") ? iconForAux("menu") : ""
|
iconSource: (useGamepadHints && uiMode === Config.Couch)
|
||||||
|
? iconForAux("menu")
|
||||||
|
: ((root.context === "library" || root.context === "details" || root.context === "sidebar" || root.context === "settings" || root.context === "diagnostics") ? iconForAux("menu") : "")
|
||||||
keyLabel: keyboardLabel("menu")
|
keyLabel: keyboardLabel("menu")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ GridView {
|
||||||
keyNavigationWraps: false
|
keyNavigationWraps: false
|
||||||
activeFocusOnTab: true
|
activeFocusOnTab: true
|
||||||
|
|
||||||
|
currentIndex: -1
|
||||||
|
|
||||||
highlightMoveDuration: Kirigami.Units.shortDuration
|
highlightMoveDuration: Kirigami.Units.shortDuration
|
||||||
highlightFollowsCurrentItem: true
|
highlightFollowsCurrentItem: true
|
||||||
|
|
||||||
|
|
@ -31,24 +33,40 @@ GridView {
|
||||||
Keys.onRightPressed: navigateRight()
|
Keys.onRightPressed: navigateRight()
|
||||||
|
|
||||||
function navigateUp() {
|
function navigateUp() {
|
||||||
|
if (currentIndex < 0 && count > 0) {
|
||||||
|
currentIndex = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
if (currentIndex >= columns) {
|
if (currentIndex >= columns) {
|
||||||
currentIndex -= columns
|
currentIndex -= columns
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function navigateDown() {
|
function navigateDown() {
|
||||||
|
if (currentIndex < 0 && count > 0) {
|
||||||
|
currentIndex = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
if (currentIndex + columns < count) {
|
if (currentIndex + columns < count) {
|
||||||
currentIndex += columns
|
currentIndex += columns
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function navigateLeft() {
|
function navigateLeft() {
|
||||||
|
if (currentIndex < 0 && count > 0) {
|
||||||
|
currentIndex = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
if (currentIndex > 0) {
|
if (currentIndex > 0) {
|
||||||
currentIndex--
|
currentIndex--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function navigateRight() {
|
function navigateRight() {
|
||||||
|
if (currentIndex < 0 && count > 0) {
|
||||||
|
currentIndex = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
if (currentIndex < count - 1) {
|
if (currentIndex < count - 1) {
|
||||||
currentIndex++
|
currentIndex++
|
||||||
}
|
}
|
||||||
|
|
@ -56,12 +74,36 @@ GridView {
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: GamepadManager
|
target: GamepadManager
|
||||||
function onNavigateUp() { if (gridView.activeFocus) gridView.navigateUp() }
|
function onNavigateUp() {
|
||||||
function onNavigateDown() { if (gridView.activeFocus) gridView.navigateDown() }
|
let w = applicationWindow()
|
||||||
function onNavigateLeft() { if (gridView.activeFocus) gridView.navigateLeft() }
|
if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return
|
||||||
function onNavigateRight() { if (gridView.activeFocus) gridView.navigateRight() }
|
if (gridView.activeFocus) gridView.navigateUp()
|
||||||
|
}
|
||||||
|
function onNavigateDown() {
|
||||||
|
let w = applicationWindow()
|
||||||
|
if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return
|
||||||
|
if (gridView.activeFocus) gridView.navigateDown()
|
||||||
|
}
|
||||||
|
function onNavigateLeft() {
|
||||||
|
let w = applicationWindow()
|
||||||
|
if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return
|
||||||
|
if (gridView.activeFocus) gridView.navigateLeft()
|
||||||
|
}
|
||||||
|
function onNavigateRight() {
|
||||||
|
let w = applicationWindow()
|
||||||
|
if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return
|
||||||
|
if (gridView.activeFocus) gridView.navigateRight()
|
||||||
|
}
|
||||||
function onSelectPressed() {
|
function onSelectPressed() {
|
||||||
if (gridView.activeFocus && gridView.currentItem) {
|
let w = applicationWindow()
|
||||||
|
if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return
|
||||||
|
if (!gridView.activeFocus) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (gridView.currentIndex < 0 && gridView.count > 0) {
|
||||||
|
gridView.currentIndex = 0
|
||||||
|
}
|
||||||
|
if (gridView.currentItem) {
|
||||||
if (gridView.currentItem.play) {
|
if (gridView.currentItem.play) {
|
||||||
gridView.currentItem.play()
|
gridView.currentItem.play()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,57 @@ QQC2.ToolBar {
|
||||||
property alias searchField: searchFieldContainer.data
|
property alias searchField: searchFieldContainer.data
|
||||||
property int currentSortMode: 0
|
property int currentSortMode: 0
|
||||||
|
|
||||||
|
readonly property bool anyMenuOpen: sortMenu.visible
|
||||||
|
|
||||||
|
function closeCurrentMenu() {
|
||||||
|
if (sortMenu.visible) {
|
||||||
|
sortMenu.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDescendant(item, ancestor) {
|
||||||
|
let p = item
|
||||||
|
while (p) {
|
||||||
|
if (p === ancestor) return true
|
||||||
|
if (ancestor.contentItem && p === ancestor.contentItem) return true
|
||||||
|
if (p.visualParent !== undefined && p.visualParent !== null) {
|
||||||
|
if (searchHeader.isDescendant(p.visualParent, ancestor)) return true
|
||||||
|
} else if (p.popup !== undefined && p.popup !== null && p.popup.visualParent !== undefined && p.popup.visualParent !== null) {
|
||||||
|
if (searchHeader.isDescendant(p.popup.visualParent, ancestor)) return true
|
||||||
|
}
|
||||||
|
p = p.parent
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusNextInMenu(forward) {
|
||||||
|
let w = applicationWindow()
|
||||||
|
if (!w || !w.activeFocusItem) return
|
||||||
|
let next = w.activeFocusItem
|
||||||
|
for (let i = 0; i < 50; i++) {
|
||||||
|
next = next.nextItemInFocusChain(forward)
|
||||||
|
if (!next) return
|
||||||
|
if (searchHeader.isDescendant(next, sortMenu)) {
|
||||||
|
next.forceActiveFocus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateFocusedInMenu() {
|
||||||
|
let w = applicationWindow()
|
||||||
|
if (!w || !w.activeFocusItem) return
|
||||||
|
let item = w.activeFocusItem
|
||||||
|
if (typeof item.triggered === "function") {
|
||||||
|
item.triggered()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (typeof item.clicked === "function") {
|
||||||
|
item.clicked()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
signal searchChanged(string text)
|
signal searchChanged(string text)
|
||||||
signal sortChanged(int mode)
|
signal sortChanged(int mode)
|
||||||
|
|
||||||
|
|
@ -47,8 +98,12 @@ QQC2.ToolBar {
|
||||||
|
|
||||||
QQC2.Menu {
|
QQC2.Menu {
|
||||||
id: sortMenu
|
id: sortMenu
|
||||||
|
focus: true
|
||||||
|
|
||||||
|
onOpened: Qt.callLater(function() { sortModeLastPlayed.forceActiveFocus() })
|
||||||
|
|
||||||
QQC2.MenuItem {
|
QQC2.MenuItem {
|
||||||
|
id: sortModeLastPlayed
|
||||||
text: i18n("Last Played")
|
text: i18n("Last Played")
|
||||||
checkable: true
|
checkable: true
|
||||||
checked: searchHeader.currentSortMode === 0
|
checked: searchHeader.currentSortMode === 0
|
||||||
|
|
@ -59,6 +114,7 @@ QQC2.ToolBar {
|
||||||
}
|
}
|
||||||
|
|
||||||
QQC2.MenuItem {
|
QQC2.MenuItem {
|
||||||
|
id: sortModeName
|
||||||
text: i18n("Name")
|
text: i18n("Name")
|
||||||
checkable: true
|
checkable: true
|
||||||
checked: searchHeader.currentSortMode === 1
|
checked: searchHeader.currentSortMode === 1
|
||||||
|
|
@ -69,6 +125,7 @@ QQC2.ToolBar {
|
||||||
}
|
}
|
||||||
|
|
||||||
QQC2.MenuItem {
|
QQC2.MenuItem {
|
||||||
|
id: sortModePlayTime
|
||||||
text: i18n("Play Time")
|
text: i18n("Play Time")
|
||||||
checkable: true
|
checkable: true
|
||||||
checked: searchHeader.currentSortMode === 2
|
checked: searchHeader.currentSortMode === 2
|
||||||
|
|
@ -80,4 +137,43 @@ QQC2.ToolBar {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: GamepadManager
|
||||||
|
function onNavigateUp() {
|
||||||
|
if (!sortMenu.visible) return
|
||||||
|
let w = applicationWindow()
|
||||||
|
if (!w || !w.activeFocusItem) return
|
||||||
|
if (!searchHeader.isDescendant(w.activeFocusItem, sortMenu)) return
|
||||||
|
searchHeader.focusNextInMenu(false)
|
||||||
|
}
|
||||||
|
function onNavigateDown() {
|
||||||
|
if (!sortMenu.visible) return
|
||||||
|
let w = applicationWindow()
|
||||||
|
if (!w || !w.activeFocusItem) return
|
||||||
|
if (!searchHeader.isDescendant(w.activeFocusItem, sortMenu)) return
|
||||||
|
searchHeader.focusNextInMenu(true)
|
||||||
|
}
|
||||||
|
function onNavigateLeft() {
|
||||||
|
if (!sortMenu.visible) return
|
||||||
|
let w = applicationWindow()
|
||||||
|
if (!w || !w.activeFocusItem) return
|
||||||
|
if (!searchHeader.isDescendant(w.activeFocusItem, sortMenu)) return
|
||||||
|
searchHeader.focusNextInMenu(false)
|
||||||
|
}
|
||||||
|
function onNavigateRight() {
|
||||||
|
if (!sortMenu.visible) return
|
||||||
|
let w = applicationWindow()
|
||||||
|
if (!w || !w.activeFocusItem) return
|
||||||
|
if (!searchHeader.isDescendant(w.activeFocusItem, sortMenu)) return
|
||||||
|
searchHeader.focusNextInMenu(true)
|
||||||
|
}
|
||||||
|
function onSelectPressed() {
|
||||||
|
if (!sortMenu.visible) return
|
||||||
|
let w = applicationWindow()
|
||||||
|
if (!w || !w.activeFocusItem) return
|
||||||
|
if (!searchHeader.isDescendant(w.activeFocusItem, sortMenu)) return
|
||||||
|
searchHeader.activateFocusedInMenu()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
135
src/screenshotmodel.cpp
Normal file
135
src/screenshotmodel.cpp
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
#include "screenshotmodel.h"
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QtConcurrent>
|
||||||
|
|
||||||
|
ScreenshotModel::ScreenshotModel(QObject *parent)
|
||||||
|
: QAbstractListModel(parent)
|
||||||
|
{
|
||||||
|
connect(&m_watcher, &QFutureWatcher<QVector<ScreenshotEntry>>::finished, this, [this]() {
|
||||||
|
const int token = m_watcher.property("refreshToken").toInt();
|
||||||
|
if (token != m_refreshToken) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setEntries(m_watcher.result());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ScreenshotModel::~ScreenshotModel()
|
||||||
|
{
|
||||||
|
m_refreshToken++;
|
||||||
|
m_watcher.cancel();
|
||||||
|
m_watcher.waitForFinished();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ScreenshotModel::directoryPath() const
|
||||||
|
{
|
||||||
|
return m_directoryPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScreenshotModel::setDirectoryPath(const QString &path)
|
||||||
|
{
|
||||||
|
if (m_directoryPath == path) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_directoryPath = path;
|
||||||
|
Q_EMIT directoryPathChanged();
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
int ScreenshotModel::count() const
|
||||||
|
{
|
||||||
|
return rowCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
int ScreenshotModel::rowCount(const QModelIndex &parent) const
|
||||||
|
{
|
||||||
|
if (parent.isValid()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return m_entries.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant ScreenshotModel::data(const QModelIndex &index, int role) const
|
||||||
|
{
|
||||||
|
if (!index.isValid() || index.row() < 0 || index.row() >= m_entries.size()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ScreenshotEntry &e = m_entries.at(index.row());
|
||||||
|
switch (role) {
|
||||||
|
case UrlRole:
|
||||||
|
return QUrl::fromLocalFile(e.filePath);
|
||||||
|
case FileNameRole:
|
||||||
|
return e.fileName;
|
||||||
|
case CreatedRole:
|
||||||
|
return e.created;
|
||||||
|
case SizeRole:
|
||||||
|
return e.size;
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QHash<int, QByteArray> ScreenshotModel::roleNames() const
|
||||||
|
{
|
||||||
|
QHash<int, QByteArray> roles;
|
||||||
|
roles[UrlRole] = "url";
|
||||||
|
roles[FileNameRole] = "fileName";
|
||||||
|
roles[CreatedRole] = "created";
|
||||||
|
roles[SizeRole] = "size";
|
||||||
|
return roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScreenshotModel::refresh()
|
||||||
|
{
|
||||||
|
const QString path = m_directoryPath;
|
||||||
|
const int token = ++m_refreshToken;
|
||||||
|
|
||||||
|
m_watcher.setProperty("refreshToken", token);
|
||||||
|
|
||||||
|
auto future = QtConcurrent::run([path]() -> QVector<ScreenshotEntry> {
|
||||||
|
QVector<ScreenshotEntry> result;
|
||||||
|
if (path.isEmpty()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDir dir(path);
|
||||||
|
if (!dir.exists()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QStringList nameFilters = {
|
||||||
|
QStringLiteral("*.png"),
|
||||||
|
QStringLiteral("*.jpg"),
|
||||||
|
QStringLiteral("*.jpeg"),
|
||||||
|
QStringLiteral("*.webp"),
|
||||||
|
};
|
||||||
|
|
||||||
|
const QFileInfoList files = dir.entryInfoList(nameFilters, QDir::Files | QDir::Readable, QDir::Time | QDir::Reversed);
|
||||||
|
result.reserve(files.size());
|
||||||
|
|
||||||
|
for (const QFileInfo &fi : files) {
|
||||||
|
ScreenshotEntry e;
|
||||||
|
e.filePath = fi.absoluteFilePath();
|
||||||
|
e.fileName = fi.fileName();
|
||||||
|
e.created = fi.lastModified();
|
||||||
|
e.size = fi.size();
|
||||||
|
result.push_back(std::move(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
m_watcher.setFuture(future);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScreenshotModel::setEntries(QVector<ScreenshotEntry> entries)
|
||||||
|
{
|
||||||
|
beginResetModel();
|
||||||
|
m_entries = std::move(entries);
|
||||||
|
endResetModel();
|
||||||
|
Q_EMIT countChanged();
|
||||||
|
}
|
||||||
59
src/screenshotmodel.h
Normal file
59
src/screenshotmodel.h
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QAbstractListModel>
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QFutureWatcher>
|
||||||
|
#include <QQmlEngine>
|
||||||
|
#include <QUrl>
|
||||||
|
#include <QVector>
|
||||||
|
|
||||||
|
class ScreenshotModel : public QAbstractListModel
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
Q_PROPERTY(QString directoryPath READ directoryPath WRITE setDirectoryPath NOTIFY directoryPathChanged)
|
||||||
|
Q_PROPERTY(int count READ count NOTIFY countChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
enum Roles {
|
||||||
|
UrlRole = Qt::UserRole + 1,
|
||||||
|
FileNameRole,
|
||||||
|
CreatedRole,
|
||||||
|
SizeRole,
|
||||||
|
};
|
||||||
|
|
||||||
|
explicit ScreenshotModel(QObject *parent = nullptr);
|
||||||
|
~ScreenshotModel() override;
|
||||||
|
|
||||||
|
QString directoryPath() const;
|
||||||
|
void setDirectoryPath(const QString &path);
|
||||||
|
|
||||||
|
int count() const;
|
||||||
|
|
||||||
|
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||||
|
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||||
|
|
||||||
|
QHash<int, QByteArray> roleNames() const override;
|
||||||
|
|
||||||
|
Q_INVOKABLE void refresh();
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void directoryPathChanged();
|
||||||
|
void countChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct ScreenshotEntry {
|
||||||
|
QString filePath;
|
||||||
|
QString fileName;
|
||||||
|
QDateTime created;
|
||||||
|
qint64 size = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
void setEntries(QVector<ScreenshotEntry> entries);
|
||||||
|
|
||||||
|
QString m_directoryPath;
|
||||||
|
QVector<ScreenshotEntry> m_entries;
|
||||||
|
|
||||||
|
QFutureWatcher<QVector<ScreenshotEntry>> m_watcher;
|
||||||
|
int m_refreshToken = 0;
|
||||||
|
};
|
||||||
Loading…
Reference in a new issue