Compare commits
No commits in common. "master" and "v0.1.0" have entirely different histories.
3
.gitignore
vendored
|
|
@ -8,6 +8,3 @@
|
|||
/.idea/
|
||||
|
||||
/FEATURE_GAP_ANALYSIS.md
|
||||
|
||||
/third_party/SDL/
|
||||
/kde-builder-persistent-data.json-kde-builder.yaml
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
# kde-builder configuration for A-La-Karte
|
||||
# See https://kde-builder.kde.org/en/configuration/config-file-overview.html
|
||||
|
||||
config-version: 2
|
||||
|
||||
global:
|
||||
branch-group: latest-kf6
|
||||
branch-group: kf6-qt6
|
||||
install-dir: ~/kde/usr
|
||||
source-dir: ~/kde/src
|
||||
build-dir: ~/kde/build
|
||||
|
|
@ -17,8 +15,9 @@ global:
|
|||
num-cores: auto
|
||||
num-cores-low-mem: 2
|
||||
|
||||
install-after-build: true
|
||||
|
||||
project alakarte:
|
||||
repository: .
|
||||
no-src: true
|
||||
cmake-options:
|
||||
- -DCMAKE_INSTALL_PREFIX=${install-dir}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
# 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).
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
project(alakarte VERSION 0.1.1)
|
||||
project(alakarte VERSION 0.1.0)
|
||||
|
||||
set(PROJECT_DEP_VERSION "6.0.0")
|
||||
set(QT_MIN_VERSION "6.6.0")
|
||||
|
|
@ -50,49 +50,7 @@ find_package(Qt6 ${QT_MIN_VERSION} REQUIRED COMPONENTS
|
|||
Sql
|
||||
)
|
||||
|
||||
set(ALAKARTE_SDL3_SOURCE_DIR "" CACHE PATH "Path to an SDL3 source tree to build instead of using the system SDL3 package")
|
||||
|
||||
if (ALAKARTE_SDL3_SOURCE_DIR)
|
||||
if (EXISTS "${ALAKARTE_SDL3_SOURCE_DIR}/CMakeLists.txt")
|
||||
set(SDL_TESTS OFF CACHE BOOL "" FORCE)
|
||||
set(SDL_TEST_LIBRARY OFF CACHE BOOL "" FORCE)
|
||||
set(SDL_DISABLE_INSTALL ON CACHE BOOL "" FORCE)
|
||||
set(SDL_SHARED OFF CACHE BOOL "" FORCE)
|
||||
set(SDL_STATIC ON CACHE BOOL "" FORCE)
|
||||
set(SDL_X11_XSCRNSAVER OFF CACHE BOOL "" FORCE)
|
||||
|
||||
add_subdirectory("${ALAKARTE_SDL3_SOURCE_DIR}" third_party/SDL-build EXCLUDE_FROM_ALL)
|
||||
|
||||
# SDL built from source is treated as a third-party dependency. Silence its
|
||||
# warnings so they don't drown out warnings from A-La-Karte itself.
|
||||
foreach(_alakarte_sdl_target IN ITEMS SDL3-static SDL3-static_a SDL3-shared SDL_uclibc)
|
||||
if (TARGET ${_alakarte_sdl_target})
|
||||
target_compile_options(${_alakarte_sdl_target} PRIVATE
|
||||
$<$<COMPILE_LANG_AND_ID:C,GNU,Clang,AppleClang>:-w>
|
||||
$<$<COMPILE_LANG_AND_ID:CXX,GNU,Clang,AppleClang>:-w>
|
||||
$<$<COMPILE_LANG_AND_ID:C,MSVC>:/w>
|
||||
$<$<COMPILE_LANG_AND_ID:CXX,MSVC>:/w>
|
||||
)
|
||||
endif()
|
||||
endforeach()
|
||||
else()
|
||||
message(FATAL_ERROR "ALAKARTE_SDL3_SOURCE_DIR does not contain a CMakeLists.txt: ${ALAKARTE_SDL3_SOURCE_DIR}")
|
||||
endif()
|
||||
else()
|
||||
find_package(SDL3 REQUIRED)
|
||||
endif()
|
||||
|
||||
if (NOT TARGET SDL3::SDL3)
|
||||
if (TARGET SDL3::SDL3-static)
|
||||
add_library(SDL3::SDL3 ALIAS SDL3::SDL3-static)
|
||||
elseif (TARGET SDL3::SDL3-shared)
|
||||
add_library(SDL3::SDL3 ALIAS SDL3::SDL3-shared)
|
||||
elseif (TARGET SDL3::SDL3::SDL3)
|
||||
add_library(SDL3::SDL3 ALIAS SDL3::SDL3::SDL3)
|
||||
else()
|
||||
message(FATAL_ERROR "SDL3 was found/built, but no usable CMake target was provided (expected SDL3::SDL3 or a compatible variant)")
|
||||
endif()
|
||||
endif()
|
||||
find_package(Qt6 ${QT_MIN_VERSION} QUIET COMPONENTS Gamepad)
|
||||
|
||||
find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS
|
||||
Kirigami
|
||||
|
|
|
|||
62
HACKING.md
|
|
@ -10,66 +10,6 @@ This document is intended for contributors who want to build, run, and debug A-L
|
|||
cmake -B build -S . -DCMAKE_BUILD_TYPE=Debug
|
||||
```
|
||||
|
||||
### SDL3
|
||||
|
||||
A-La-Karte uses SDL3 for gamepad navigation.
|
||||
|
||||
There are two supported ways to satisfy the SDL3 dependency:
|
||||
|
||||
#### Option 1: Use a system SDL3 package
|
||||
|
||||
If your distribution provides SDL3 development packages, install them and configure normally.
|
||||
|
||||
Common package names include `libsdl3-dev` (Debian/Ubuntu), `SDL3-devel` (Fedora/openSUSE), or similarly named SDL3 "-devel" packages.
|
||||
|
||||
At CMake configure time, A-La-Karte uses `find_package(SDL3 REQUIRED)`. This requires that SDL3 provides a CMake package (e.g. `SDL3Config.cmake`) and that CMake can find it.
|
||||
|
||||
If CMake cannot find SDL3, you can point it at the right prefix:
|
||||
|
||||
```bash
|
||||
cmake -B build -S . \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DCMAKE_PREFIX_PATH=/path/to/sdl3/prefix
|
||||
```
|
||||
|
||||
Alternatively you can point directly at SDL3's CMake package directory:
|
||||
|
||||
```bash
|
||||
cmake -B build -S . \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DSDL3_DIR=/path/to/SDL3Config.cmake/parent
|
||||
```
|
||||
|
||||
#### Option 2: Build SDL3 from a source tree
|
||||
|
||||
If your distro does not ship SDL3 yet (or ships a version without a usable CMake package), you can build SDL3 as part of the A-La-Karte build by pointing CMake at an SDL3 source checkout:
|
||||
|
||||
You can obtain a source tree by cloning upstream SDL:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/libsdl-org/SDL.git
|
||||
```
|
||||
|
||||
```bash
|
||||
cmake -B build -S . \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DALAKARTE_SDL3_SOURCE_DIR=/path/to/SDL
|
||||
```
|
||||
|
||||
`ALAKARTE_SDL3_SOURCE_DIR` must point at a directory that contains `CMakeLists.txt` from SDL3.
|
||||
|
||||
This is intended for local development and CI setups. Distributions should prefer packaging SDL3 as a system dependency.
|
||||
|
||||
#### Packaging guidance
|
||||
|
||||
Packagers should depend on the system SDL3 development package and ensure CMake can discover it.
|
||||
|
||||
In particular:
|
||||
|
||||
- The build dependency must include SDL3 headers and the CMake package files.
|
||||
- A-La-Karte links against `SDL3::SDL3`.
|
||||
- Avoid using `ALAKARTE_SDL3_SOURCE_DIR` in distribution packaging.
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
|
|
@ -142,4 +82,4 @@ Then:
|
|||
## Notes
|
||||
|
||||
- SteamGridDB cover fetching requires an API key.
|
||||
- KRunner integration is optional at build time (depending on available dependencies).
|
||||
- KRunner integration and gamepad navigation are optional at build time (depending on available dependencies).
|
||||
|
|
|
|||
47
README.md
|
|
@ -6,32 +6,17 @@
|
|||
|
||||
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
|
||||
|
||||
- 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)
|
||||
- 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
|
||||
- Search by name/developer/publisher and filter by source
|
||||
- Favorites and hidden games views
|
||||
- Last-played timestamp
|
||||
- Optional SteamGridDB cover fetching (requires API key)
|
||||
- Optional KRunner integration (if KF6Runner is available at build time)
|
||||
- Gamepad navigation (via SDL3)
|
||||
- Optional gamepad navigation (if Qt Gamepad is available at build time)
|
||||
|
||||
## Requirements
|
||||
|
||||
|
|
@ -45,7 +30,6 @@ For documentation index, see `docs/README.md`.
|
|||
- KCoreAddons
|
||||
- KConfig
|
||||
- Extra CMake Modules (ECM)
|
||||
- SDL3
|
||||
|
||||
### Runtime Dependencies
|
||||
|
||||
|
|
@ -67,36 +51,10 @@ For documentation index, see `docs/README.md`.
|
|||
|
||||
### Using kde-builder (Recommended)
|
||||
|
||||
This project is not currently part of KDE's repo-metadata, so you need to add it as a custom project in your `~/.config/kde-builder.yaml`.
|
||||
|
||||
1. Set up kde-builder:
|
||||
- https://develop.kde.org/docs/getting-started/building/kde-builder-setup/
|
||||
2. Install distro build dependencies:
|
||||
|
||||
```bash
|
||||
kde-builder --install-distro-packages
|
||||
```
|
||||
|
||||
3. Add a project entry to `~/.config/kde-builder.yaml`:
|
||||
|
||||
```yaml
|
||||
project alakarte:
|
||||
repository: https://invent.kde.org/marcoa/a-la-karte.git
|
||||
cmake-options: -DALAKARTE_SDL3_SOURCE_DIR=/path/to/SDL
|
||||
```
|
||||
|
||||
4. Build:
|
||||
|
||||
```bash
|
||||
kde-builder alakarte
|
||||
```
|
||||
|
||||
To run the built app:
|
||||
|
||||
```bash
|
||||
kde-builder --run alakarte
|
||||
```
|
||||
|
||||
### Manual Build
|
||||
|
||||
```bash
|
||||
|
|
@ -209,8 +167,6 @@ Settings are stored in `~/.config/alakarterc` and include:
|
|||
|
||||
Game library is stored in `~/.local/share/alakarte/library.json`.
|
||||
|
||||
Imported screenshots are stored under `~/.local/share/alakarte/media/screenshots/<gameId>/`.
|
||||
|
||||
## Code Overview
|
||||
|
||||
- **Application entry point**: `src/main.cpp`
|
||||
|
|
@ -218,7 +174,6 @@ Imported screenshots are stored under `~/.local/share/alakarte/media/screenshots
|
|||
- **Data model**:
|
||||
- `src/game.*` stores per-game metadata
|
||||
- `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.)
|
||||
- **Launcher**: `src/gamelauncher.*` launches processes/URLs and records last-played on launch
|
||||
- **UI**: `src/qml/` (Kirigami + Qt Quick)
|
||||
|
|
|
|||
41
changelog.md
|
|
@ -1,41 +0,0 @@
|
|||
# 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
|
||||
|
||||
### Added
|
||||
|
||||
- SDL3-based gamepad navigation
|
||||
- Bottom hint bar with keyboard/gamepad hints and controller icons
|
||||
- UI mode setting (auto/desktop/handheld)
|
||||
|
||||
### Changed
|
||||
|
||||
- Import workflow skips sources that were already imported
|
||||
- Steam importing filters out non-game entries (runtimes/redistributables)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Heroic importing prefers cached cover images and imports sideload apps
|
||||
- Safer QObject lifetime management for games and launcher processes
|
||||
|
||||
## [0.1.0] - 2026-01-17
|
||||
|
||||
- First public preview release
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
# 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.
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
# 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.
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
# 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.
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
# 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,29 +75,6 @@
|
|||
</keywords>
|
||||
|
||||
<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">
|
||||
<description>
|
||||
<p>Preview release with improved navigation and importing</p>
|
||||
<ul>
|
||||
<li>SDL3-based gamepad navigation</li>
|
||||
<li>Bottom hint bar with keyboard/gamepad hints</li>
|
||||
<li>UI mode setting (auto/desktop/handheld)</li>
|
||||
<li>Steam importing filters non-game entries</li>
|
||||
<li>Import workflow avoids re-importing already imported sources</li>
|
||||
<li>Heroic importing prefers cached covers and imports sideload apps</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.1.0" date="2026-01-17">
|
||||
<description>
|
||||
<p>First public preview release of A-La-Karte</p>
|
||||
|
|
|
|||
|
|
@ -8,9 +8,6 @@ add_executable(alakarte
|
|||
gamesortfiltermodel.cpp
|
||||
game.cpp
|
||||
gamepadmanager.cpp
|
||||
inputmanager.cpp
|
||||
mediamanager.cpp
|
||||
screenshotmodel.cpp
|
||||
platformimporter.cpp
|
||||
steamimporter.cpp
|
||||
lutrisimporter.cpp
|
||||
|
|
@ -26,16 +23,16 @@ add_executable(alakarte
|
|||
config.cpp
|
||||
)
|
||||
|
||||
set_source_files_properties(qml/icons/app/org.kde.alakarte.svg PROPERTIES
|
||||
QT_RESOURCE_ALIAS "sc-apps-org.kde.alakarte.svg"
|
||||
)
|
||||
|
||||
qt_add_resources(alakarte alakarte_app_icon
|
||||
PREFIX "/icons"
|
||||
FILES
|
||||
qml/icons/app/org.kde.alakarte.svg
|
||||
)
|
||||
|
||||
set_source_files_properties(qml/icons/app/org.kde.alakarte.svg PROPERTIES
|
||||
QT_RESOURCE_ALIAS "sc-apps-org.kde.alakarte.svg"
|
||||
)
|
||||
|
||||
target_link_libraries(alakarte PRIVATE
|
||||
Qt6::Core
|
||||
Qt6::Quick
|
||||
|
|
@ -46,7 +43,6 @@ target_link_libraries(alakarte PRIVATE
|
|||
Qt6::Concurrent
|
||||
Qt6::DBus
|
||||
Qt6::Svg
|
||||
SDL3::SDL3
|
||||
KF6::Kirigami
|
||||
KF6::I18n
|
||||
KF6::CoreAddons
|
||||
|
|
@ -57,6 +53,11 @@ target_link_libraries(alakarte PRIVATE
|
|||
KF6::WindowSystem
|
||||
)
|
||||
|
||||
if (TARGET Qt6::Gamepad)
|
||||
target_link_libraries(alakarte PRIVATE Qt6::Gamepad)
|
||||
target_compile_definitions(alakarte PRIVATE ALAKARTE_HAVE_QT_GAMEPAD=1)
|
||||
endif()
|
||||
|
||||
ecm_add_qml_module(alakarte URI org.kde.alakarte
|
||||
VERSION 1.0
|
||||
QML_FILES
|
||||
|
|
@ -64,63 +65,25 @@ ecm_add_qml_module(alakarte URI org.kde.alakarte
|
|||
qml/LibraryView.qml
|
||||
qml/GameCard.qml
|
||||
qml/GameDetailsSheet.qml
|
||||
qml/DiagnosticsSheet.qml
|
||||
qml/SettingsPage.qml
|
||||
qml/SidebarView.qml
|
||||
qml/GameEditDialog.qml
|
||||
qml/components/FocusableCard.qml
|
||||
qml/components/GameGridView.qml
|
||||
qml/components/SearchHeader.qml
|
||||
qml/components/IconWithResourceFallback.qml
|
||||
qml/components/EmptyState.qml
|
||||
qml/components/BottomHintBar.qml
|
||||
RESOURCES
|
||||
qml/icons/brand/steam-symbolic.svg
|
||||
qml/icons/brand/itchdotio-symbolic.svg
|
||||
qml/icons/brand/retroarch-symbolic.svg
|
||||
qml/icons/app/org.kde.alakarte.svg
|
||||
qml/icons/gamepad/playstation/cross.svg
|
||||
qml/icons/gamepad/playstation/circle.svg
|
||||
qml/icons/gamepad/playstation/square.svg
|
||||
qml/icons/gamepad/playstation/triangle.svg
|
||||
qml/icons/gamepad/playstation/options.svg
|
||||
qml/icons/gamepad/playstation/share.svg
|
||||
qml/icons/gamepad/xbox/a.svg
|
||||
qml/icons/gamepad/xbox/b.svg
|
||||
qml/icons/gamepad/xbox/x.svg
|
||||
qml/icons/gamepad/xbox/y.svg
|
||||
qml/icons/gamepad/xbox/menu.svg
|
||||
qml/icons/gamepad/xbox/view.svg
|
||||
qml/icons/gamepad/nintendo/a.svg
|
||||
qml/icons/gamepad/nintendo/b.svg
|
||||
qml/icons/gamepad/nintendo/x.svg
|
||||
qml/icons/gamepad/nintendo/y.svg
|
||||
qml/icons/gamepad/nintendo/plus.svg
|
||||
qml/icons/gamepad/nintendo/minus.svg
|
||||
qml/icons/gamepad/nintendo/home.svg
|
||||
qml/icons/gamepad/generic/south.svg
|
||||
qml/icons/gamepad/generic/east.svg
|
||||
qml/icons/gamepad/generic/west.svg
|
||||
qml/icons/gamepad/generic/north.svg
|
||||
qml/icons/gamepad/generic/lb.svg
|
||||
qml/icons/gamepad/generic/rb.svg
|
||||
qml/icons/gamepad/generic/lt.svg
|
||||
qml/icons/gamepad/generic/rt.svg
|
||||
qml/icons/gamepad/generic/dpad.svg
|
||||
qml/icons/gamepad/generic/dpad-up.svg
|
||||
qml/icons/gamepad/generic/dpad-down.svg
|
||||
qml/icons/gamepad/generic/dpad-left.svg
|
||||
qml/icons/gamepad/generic/dpad-right.svg
|
||||
qml/icons/gamepad/generic/lstick.svg
|
||||
qml/icons/gamepad/generic/rstick.svg
|
||||
qml/icons/gamepad/generic/menu.svg
|
||||
qml/icons/gamepad/generic/back.svg
|
||||
SOURCES
|
||||
app.h
|
||||
gamemodel.h
|
||||
gamesortfiltermodel.h
|
||||
game.h
|
||||
gamepadmanager.h
|
||||
inputmanager.h
|
||||
platformimporter.h
|
||||
steamimporter.h
|
||||
lutrisimporter.h
|
||||
|
|
|
|||
840
src/app.cpp
|
|
@ -11,7 +11,6 @@
|
|||
#include "config.h"
|
||||
#include "gamelauncher.h"
|
||||
#include "gamemodel.h"
|
||||
#include "mediamanager.h"
|
||||
#include "steamgriddb.h"
|
||||
|
||||
class App : public QObject
|
||||
|
|
@ -23,7 +22,6 @@ class App : public QObject
|
|||
Q_PROPERTY(GameModel *gameModel READ gameModel CONSTANT)
|
||||
Q_PROPERTY(GameLauncher *launcher READ launcher CONSTANT)
|
||||
Q_PROPERTY(SteamGridDB *steamGridDB READ steamGridDB CONSTANT)
|
||||
Q_PROPERTY(MediaManager *mediaManager READ mediaManager CONSTANT)
|
||||
Q_PROPERTY(Config *config READ config CONSTANT)
|
||||
Q_PROPERTY(bool importing READ importing NOTIFY importingChanged)
|
||||
Q_PROPERTY(QString importStatus READ importStatus NOTIFY importStatusChanged)
|
||||
|
|
@ -35,7 +33,6 @@ public:
|
|||
GameModel *gameModel() const;
|
||||
GameLauncher *launcher() const;
|
||||
SteamGridDB *steamGridDB() const;
|
||||
MediaManager *mediaManager() const;
|
||||
Config *config() const;
|
||||
|
||||
bool importing() const;
|
||||
|
|
@ -75,7 +72,6 @@ private:
|
|||
GameModel *m_gameModel;
|
||||
GameLauncher *m_launcher;
|
||||
SteamGridDB *m_steamGridDB;
|
||||
MediaManager *m_mediaManager;
|
||||
Config *m_config;
|
||||
|
||||
bool m_importing = false;
|
||||
|
|
|
|||
103
src/config.cpp
|
|
@ -26,20 +26,6 @@ void Config::setViewMode(ViewMode mode)
|
|||
}
|
||||
}
|
||||
|
||||
Config::UiMode Config::uiMode() const
|
||||
{
|
||||
return m_uiMode;
|
||||
}
|
||||
|
||||
void Config::setUiMode(UiMode mode)
|
||||
{
|
||||
if (m_uiMode != mode) {
|
||||
m_uiMode = mode;
|
||||
save();
|
||||
Q_EMIT uiModeChanged();
|
||||
}
|
||||
}
|
||||
|
||||
int Config::gridSize() const
|
||||
{
|
||||
return m_gridSize;
|
||||
|
|
@ -279,81 +265,12 @@ 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()
|
||||
{
|
||||
const auto config = KSharedConfig::openConfig(QStringLiteral("alakarterc"));
|
||||
|
||||
KConfigGroup generalGroup(config, QStringLiteral("General"));
|
||||
generalGroup.writeEntry(QStringLiteral("viewMode"), static_cast<int>(m_viewMode));
|
||||
generalGroup.writeEntry(QStringLiteral("uiMode"), static_cast<int>(m_uiMode));
|
||||
generalGroup.writeEntry(QStringLiteral("gridSize"), m_gridSize);
|
||||
generalGroup.writeEntry(QStringLiteral("showPlatformBadges"), m_showPlatformBadges);
|
||||
generalGroup.writeEntry(QStringLiteral("theme"), m_theme);
|
||||
|
|
@ -376,11 +293,6 @@ void Config::save()
|
|||
behaviorGroup.writeEntry(QStringLiteral("exitAfterLaunch"), m_exitAfterLaunch);
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
@ -390,7 +302,6 @@ void Config::load()
|
|||
|
||||
const KConfigGroup generalGroup(config, QStringLiteral("General"));
|
||||
m_viewMode = static_cast<ViewMode>(generalGroup.readEntry(QStringLiteral("viewMode"), static_cast<int>(GridView)));
|
||||
m_uiMode = static_cast<UiMode>(generalGroup.readEntry(QStringLiteral("uiMode"), static_cast<int>(Auto)));
|
||||
m_gridSize = generalGroup.readEntry(QStringLiteral("gridSize"), 180);
|
||||
m_showPlatformBadges = generalGroup.readEntry(QStringLiteral("showPlatformBadges"), true);
|
||||
m_theme = generalGroup.readEntry(QStringLiteral("theme"), QString());
|
||||
|
|
@ -412,17 +323,11 @@ void Config::load()
|
|||
m_animatedCovers = behaviorGroup.readEntry(QStringLiteral("animatedCovers"), false);
|
||||
m_exitAfterLaunch = behaviorGroup.readEntry(QStringLiteral("exitAfterLaunch"), 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()
|
||||
{
|
||||
m_viewMode = GridView;
|
||||
m_uiMode = Auto;
|
||||
m_gridSize = 180;
|
||||
m_showPlatformBadges = true;
|
||||
m_autoImportOnStartup = false;
|
||||
|
|
@ -441,14 +346,9 @@ void Config::resetToDefaults()
|
|||
m_animatedCovers = false;
|
||||
m_theme.clear();
|
||||
|
||||
m_windowWidth = 0;
|
||||
m_windowHeight = 0;
|
||||
m_windowMaximized = false;
|
||||
|
||||
save();
|
||||
|
||||
Q_EMIT viewModeChanged();
|
||||
Q_EMIT uiModeChanged();
|
||||
Q_EMIT gridSizeChanged();
|
||||
Q_EMIT showPlatformBadgesChanged();
|
||||
Q_EMIT autoImportOnStartupChanged();
|
||||
|
|
@ -466,7 +366,4 @@ void Config::resetToDefaults()
|
|||
Q_EMIT highQualityImagesChanged();
|
||||
Q_EMIT animatedCoversChanged();
|
||||
Q_EMIT themeChanged();
|
||||
Q_EMIT windowWidthChanged();
|
||||
Q_EMIT windowHeightChanged();
|
||||
Q_EMIT windowMaximizedChanged();
|
||||
}
|
||||
|
|
|
|||
34
src/config.h
|
|
@ -12,7 +12,6 @@ class Config : public QObject
|
|||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(ViewMode viewMode READ viewMode WRITE setViewMode NOTIFY viewModeChanged)
|
||||
Q_PROPERTY(UiMode uiMode READ uiMode WRITE setUiMode NOTIFY uiModeChanged)
|
||||
Q_PROPERTY(int gridSize READ gridSize WRITE setGridSize NOTIFY gridSizeChanged)
|
||||
Q_PROPERTY(bool showPlatformBadges READ showPlatformBadges WRITE setShowPlatformBadges NOTIFY showPlatformBadgesChanged)
|
||||
Q_PROPERTY(bool autoImportOnStartup READ autoImportOnStartup WRITE setAutoImportOnStartup NOTIFY autoImportOnStartupChanged)
|
||||
|
|
@ -30,9 +29,6 @@ class Config : public QObject
|
|||
Q_PROPERTY(bool animatedCovers READ animatedCovers WRITE setAnimatedCovers NOTIFY animatedCoversChanged)
|
||||
Q_PROPERTY(bool coverLaunchesGame READ coverLaunchesGame WRITE setCoverLaunchesGame NOTIFY coverLaunchesGameChanged)
|
||||
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:
|
||||
enum ViewMode {
|
||||
|
|
@ -42,21 +38,11 @@ public:
|
|||
};
|
||||
Q_ENUM(ViewMode)
|
||||
|
||||
enum UiMode {
|
||||
Auto,
|
||||
Desktop,
|
||||
Couch
|
||||
};
|
||||
Q_ENUM(UiMode)
|
||||
|
||||
explicit Config(QObject *parent = nullptr);
|
||||
|
||||
ViewMode viewMode() const;
|
||||
void setViewMode(ViewMode mode);
|
||||
|
||||
UiMode uiMode() const;
|
||||
void setUiMode(UiMode mode);
|
||||
|
||||
int gridSize() const;
|
||||
void setGridSize(int size);
|
||||
|
||||
|
|
@ -108,24 +94,12 @@ public:
|
|||
QString theme() const;
|
||||
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 load();
|
||||
Q_INVOKABLE void resetToDefaults();
|
||||
|
||||
Q_INVOKABLE void setWindowSize(int width, int height);
|
||||
|
||||
Q_SIGNALS:
|
||||
void viewModeChanged();
|
||||
void uiModeChanged();
|
||||
void gridSizeChanged();
|
||||
void showPlatformBadgesChanged();
|
||||
void autoImportOnStartupChanged();
|
||||
|
|
@ -143,13 +117,9 @@ Q_SIGNALS:
|
|||
void animatedCoversChanged();
|
||||
void coverLaunchesGameChanged();
|
||||
void themeChanged();
|
||||
void windowWidthChanged();
|
||||
void windowHeightChanged();
|
||||
void windowMaximizedChanged();
|
||||
|
||||
private:
|
||||
ViewMode m_viewMode = GridView;
|
||||
UiMode m_uiMode = Auto;
|
||||
int m_gridSize = 180;
|
||||
bool m_showPlatformBadges = true;
|
||||
bool m_autoImportOnStartup = false;
|
||||
|
|
@ -167,8 +137,4 @@ private:
|
|||
bool m_highQualityImages = true;
|
||||
bool m_animatedCovers = false;
|
||||
QString m_theme;
|
||||
|
||||
int m_windowWidth = 0;
|
||||
int m_windowHeight = 0;
|
||||
bool m_windowMaximized = false;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,11 +3,10 @@
|
|||
|
||||
#include "desktopimporter.h"
|
||||
|
||||
#include <KConfigGroup>
|
||||
#include <KDesktopFile>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QRegularExpression>
|
||||
#include <QSettings>
|
||||
#include <QStandardPaths>
|
||||
|
||||
DesktopImporter::DesktopImporter(QObject *parent)
|
||||
|
|
@ -40,13 +39,7 @@ QStringList DesktopImporter::getDesktopFilePaths() const
|
|||
// Add common system locations
|
||||
appDirs << QStringLiteral("/usr/share/applications");
|
||||
appDirs << QStringLiteral("/usr/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
|
||||
appDirs << expandPath(QStringLiteral("~/.local/share/flatpak/exports/share/applications"));
|
||||
|
|
@ -70,38 +63,58 @@ QStringList DesktopImporter::getDesktopFilePaths() const
|
|||
|
||||
bool DesktopImporter::isGameDesktopFile(const QString &filePath) const
|
||||
{
|
||||
KDesktopFile desktopFile(filePath);
|
||||
KConfigGroup desktop = desktopFile.desktopGroup();
|
||||
QSettings desktop(filePath, QSettings::IniFormat);
|
||||
desktop.beginGroup(QStringLiteral("Desktop Entry"));
|
||||
|
||||
const QString type = desktop.readEntry(QStringLiteral("Type"));
|
||||
QString type = desktop.value(QStringLiteral("Type")).toString();
|
||||
if (type != QStringLiteral("Application")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if hidden or not shown
|
||||
if (desktop.readEntry(QStringLiteral("Hidden"), false)) {
|
||||
if (desktop.value(QStringLiteral("Hidden"), false).toBool()) {
|
||||
return false;
|
||||
}
|
||||
if (desktop.readEntry(QStringLiteral("NoDisplay"), false)) {
|
||||
if (desktop.value(QStringLiteral("NoDisplay"), false).toBool()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check categories for game-related entries
|
||||
const QStringList categories = desktop.readEntry(QStringLiteral("Categories")).split(QLatin1Char(';'), Qt::SkipEmptyParts);
|
||||
QString categories = desktop.value(QStringLiteral("Categories")).toString();
|
||||
QStringList gameCategories = {QStringLiteral("Game"),
|
||||
QStringLiteral("ArcadeGame"),
|
||||
QStringLiteral("ActionGame"),
|
||||
QStringLiteral("AdventureGame"),
|
||||
QStringLiteral("BlocksGame"),
|
||||
QStringLiteral("BoardGame"),
|
||||
QStringLiteral("CardGame"),
|
||||
QStringLiteral("KidsGame"),
|
||||
QStringLiteral("LogicGame"),
|
||||
QStringLiteral("RolePlaying"),
|
||||
QStringLiteral("Shooter"),
|
||||
QStringLiteral("Simulation"),
|
||||
QStringLiteral("SportsGame"),
|
||||
QStringLiteral("StrategyGame")};
|
||||
|
||||
return hasGameCategory(categories);
|
||||
for (const QString &cat : gameCategories) {
|
||||
if (categories.contains(cat, Qt::CaseInsensitive)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Game *DesktopImporter::parseDesktopFile(const QString &filePath) const
|
||||
{
|
||||
KDesktopFile desktopFile(filePath);
|
||||
KConfigGroup desktop = desktopFile.desktopGroup();
|
||||
QSettings desktop(filePath, QSettings::IniFormat);
|
||||
desktop.beginGroup(QStringLiteral("Desktop Entry"));
|
||||
|
||||
const QString name = desktop.readEntry(QStringLiteral("Name"));
|
||||
QString exec = desktop.readEntry(QStringLiteral("Exec"));
|
||||
const QString icon = desktop.readEntry(QStringLiteral("Icon"));
|
||||
const QString comment = desktop.readEntry(QStringLiteral("Comment"));
|
||||
const QString genericName = desktop.readEntry(QStringLiteral("GenericName"));
|
||||
QString name = desktop.value(QStringLiteral("Name")).toString();
|
||||
QString exec = desktop.value(QStringLiteral("Exec")).toString();
|
||||
QString icon = desktop.value(QStringLiteral("Icon")).toString();
|
||||
QString comment = desktop.value(QStringLiteral("Comment")).toString();
|
||||
QString genericName = desktop.value(QStringLiteral("GenericName")).toString();
|
||||
|
||||
if (name.isEmpty() || exec.isEmpty()) {
|
||||
return nullptr;
|
||||
|
|
@ -113,13 +126,12 @@ Game *DesktopImporter::parseDesktopFile(const QString &filePath) const
|
|||
|
||||
// Create unique ID from the desktop file name
|
||||
QFileInfo fileInfo(filePath);
|
||||
const QString desktopId = fileInfo.completeBaseName();
|
||||
QString gameId = QStringLiteral("desktop_") + desktopId;
|
||||
QString gameId = QStringLiteral("desktop_") + fileInfo.baseName();
|
||||
|
||||
Game *game = new Game(gameId, name);
|
||||
game->setLaunchCommand(exec);
|
||||
game->setPlatform(platformName());
|
||||
game->setPlatformId(desktopId);
|
||||
game->setPlatformId(platformId());
|
||||
|
||||
if (!comment.isEmpty()) {
|
||||
game->setDescription(comment);
|
||||
|
|
|
|||
|
|
@ -3,12 +3,11 @@
|
|||
|
||||
#include "flatpakimporter.h"
|
||||
|
||||
#include <KConfigGroup>
|
||||
#include <KDesktopFile>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QProcess>
|
||||
#include <QRegularExpression>
|
||||
#include <QSettings>
|
||||
|
||||
FlatpakImporter::FlatpakImporter(QObject *parent)
|
||||
: PlatformImporter(parent)
|
||||
|
|
@ -44,34 +43,54 @@ bool FlatpakImporter::isAvailable() const
|
|||
|
||||
bool FlatpakImporter::isGameApp(const QString &desktopFilePath) const
|
||||
{
|
||||
KDesktopFile desktopFile(desktopFilePath);
|
||||
KConfigGroup desktop = desktopFile.desktopGroup();
|
||||
QSettings desktop(desktopFilePath, QSettings::IniFormat);
|
||||
desktop.beginGroup(QStringLiteral("Desktop Entry"));
|
||||
|
||||
const QString type = desktop.readEntry(QStringLiteral("Type"));
|
||||
QString type = desktop.value(QStringLiteral("Type")).toString();
|
||||
if (type != QStringLiteral("Application")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (desktop.readEntry(QStringLiteral("Hidden"), false)) {
|
||||
if (desktop.value(QStringLiteral("Hidden"), false).toBool()) {
|
||||
return false;
|
||||
}
|
||||
if (desktop.readEntry(QStringLiteral("NoDisplay"), false)) {
|
||||
if (desktop.value(QStringLiteral("NoDisplay"), false).toBool()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QStringList categories = desktop.readEntry(QStringLiteral("Categories")).split(QLatin1Char(';'), Qt::SkipEmptyParts);
|
||||
QString categories = desktop.value(QStringLiteral("Categories")).toString();
|
||||
QStringList gameCategories = {QStringLiteral("Game"),
|
||||
QStringLiteral("ArcadeGame"),
|
||||
QStringLiteral("ActionGame"),
|
||||
QStringLiteral("AdventureGame"),
|
||||
QStringLiteral("BlocksGame"),
|
||||
QStringLiteral("BoardGame"),
|
||||
QStringLiteral("CardGame"),
|
||||
QStringLiteral("KidsGame"),
|
||||
QStringLiteral("LogicGame"),
|
||||
QStringLiteral("RolePlaying"),
|
||||
QStringLiteral("Shooter"),
|
||||
QStringLiteral("Simulation"),
|
||||
QStringLiteral("SportsGame"),
|
||||
QStringLiteral("StrategyGame")};
|
||||
|
||||
return hasGameCategory(categories);
|
||||
for (const QString &cat : gameCategories) {
|
||||
if (categories.contains(cat, Qt::CaseInsensitive)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Game *FlatpakImporter::parseDesktopFile(const QString &filePath, const QString &appId) const
|
||||
{
|
||||
KDesktopFile desktopFile(filePath);
|
||||
KConfigGroup desktop = desktopFile.desktopGroup();
|
||||
QSettings desktop(filePath, QSettings::IniFormat);
|
||||
desktop.beginGroup(QStringLiteral("Desktop Entry"));
|
||||
|
||||
const QString name = desktop.readEntry(QStringLiteral("Name"));
|
||||
const QString icon = desktop.readEntry(QStringLiteral("Icon"));
|
||||
const QString comment = desktop.readEntry(QStringLiteral("Comment"));
|
||||
QString name = desktop.value(QStringLiteral("Name")).toString();
|
||||
QString icon = desktop.value(QStringLiteral("Icon")).toString();
|
||||
QString comment = desktop.value(QStringLiteral("Comment")).toString();
|
||||
|
||||
if (name.isEmpty()) {
|
||||
return nullptr;
|
||||
|
|
@ -81,7 +100,7 @@ Game *FlatpakImporter::parseDesktopFile(const QString &filePath, const QString &
|
|||
|
||||
Game *game = new Game(gameId, name);
|
||||
game->setPlatform(platformName());
|
||||
game->setPlatformId(appId);
|
||||
game->setPlatformId(platformId());
|
||||
game->setLaunchCommand(QStringLiteral("flatpak run %1").arg(appId));
|
||||
|
||||
if (!comment.isEmpty()) {
|
||||
|
|
|
|||
379
src/game.cpp
|
|
@ -4,8 +4,6 @@
|
|||
#include "game.h"
|
||||
|
||||
#include <KLocalizedString>
|
||||
#include <QCryptographicHash>
|
||||
#include <QJsonValue>
|
||||
#include <QLocale>
|
||||
#include <QUuid>
|
||||
|
||||
|
|
@ -133,186 +131,6 @@ 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
|
||||
{
|
||||
return m_platform;
|
||||
|
|
@ -450,84 +268,6 @@ QJsonObject Game::toJson() const
|
|||
obj[QStringLiteral("favorite")] = m_favorite;
|
||||
obj[QStringLiteral("hidden")] = m_hidden;
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -536,135 +276,26 @@ Game *Game::fromJson(const QJsonObject &json, QObject *parent)
|
|||
QString id = json[QStringLiteral("id")].toString();
|
||||
QString name = json[QStringLiteral("name")].toString();
|
||||
|
||||
if (name.isEmpty()) {
|
||||
if (id.isEmpty() || name.isEmpty()) {
|
||||
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->setDescription(json[QStringLiteral("description")].toString());
|
||||
game->setDeveloper(json[QStringLiteral("developer")].toString());
|
||||
game->setPublisher(json[QStringLiteral("publisher")].toString());
|
||||
game->setCoverUrl(QUrl(json[QStringLiteral("coverUrl")].toString()));
|
||||
game->setIconUrl(QUrl(json[QStringLiteral("iconUrl")].toString()));
|
||||
game->setLaunchCommand(launchCommand);
|
||||
game->setWorkingDirectory(workingDirectory);
|
||||
game->setPlatform(platform);
|
||||
game->setPlatformId(platformId);
|
||||
game->setLaunchCommand(json[QStringLiteral("launchCommand")].toString());
|
||||
game->setWorkingDirectory(json[QStringLiteral("workingDirectory")].toString());
|
||||
game->setPlatform(json[QStringLiteral("platform")].toString());
|
||||
game->setPlatformId(json[QStringLiteral("platformId")].toString());
|
||||
game->setDateAdded(QDateTime::fromString(json[QStringLiteral("dateAdded")].toString(), Qt::ISODate));
|
||||
game->setLastPlayed(QDateTime::fromString(json[QStringLiteral("lastPlayed")].toString(), Qt::ISODate));
|
||||
game->setPlayTime(json[QStringLiteral("playTime")].toInteger());
|
||||
game->setFavorite(json[QStringLiteral("favorite")].toBool());
|
||||
game->setHidden(json[QStringLiteral("hidden")].toBool());
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
37
src/game.h
|
|
@ -9,7 +9,6 @@
|
|||
#include <QQmlEngine>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QVariantMap>
|
||||
|
||||
class Game : public QObject
|
||||
{
|
||||
|
|
@ -26,11 +25,6 @@ class Game : public QObject
|
|||
Q_PROPERTY(QUrl iconUrl READ iconUrl WRITE setIconUrl NOTIFY iconUrlChanged)
|
||||
Q_PROPERTY(QString launchCommand READ launchCommand WRITE setLaunchCommand NOTIFY launchCommandChanged)
|
||||
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 platformId READ platformId WRITE setPlatformId NOTIFY platformIdChanged)
|
||||
Q_PROPERTY(QDateTime dateAdded READ dateAdded WRITE setDateAdded NOTIFY dateAddedChanged)
|
||||
|
|
@ -71,26 +65,6 @@ public:
|
|||
QString workingDirectory() const;
|
||||
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;
|
||||
void setPlatform(const QString &platform);
|
||||
|
||||
|
|
@ -134,11 +108,6 @@ Q_SIGNALS:
|
|||
void iconUrlChanged();
|
||||
void launchCommandChanged();
|
||||
void workingDirectoryChanged();
|
||||
void launchEnvChanged();
|
||||
void launchRunnerChanged();
|
||||
void launchRunnerPathChanged();
|
||||
void launchPrefixPathChanged();
|
||||
void activeLaunchProfileChanged();
|
||||
void platformChanged();
|
||||
void platformIdChanged();
|
||||
void dateAddedChanged();
|
||||
|
|
@ -159,12 +128,6 @@ private:
|
|||
QUrl m_iconUrl;
|
||||
QString m_launchCommand;
|
||||
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_platformId;
|
||||
QDateTime m_dateAdded;
|
||||
|
|
|
|||
|
|
@ -3,140 +3,13 @@
|
|||
|
||||
#include "gamelauncher.h"
|
||||
#include "app.h"
|
||||
#include "gamepadmanager.h"
|
||||
#include "inputmanager.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDateTime>
|
||||
#include <QDesktopServices>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QStandardPaths>
|
||||
#include <QTimer>
|
||||
#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)
|
||||
: QObject(parent)
|
||||
{
|
||||
|
|
@ -149,11 +22,8 @@ GameLauncher::~GameLauncher()
|
|||
process->disconnect();
|
||||
process->terminate();
|
||||
process->waitForFinished(3000);
|
||||
process->deleteLater();
|
||||
delete process;
|
||||
}
|
||||
|
||||
m_runningGames.clear();
|
||||
m_processToGame.clear();
|
||||
}
|
||||
|
||||
bool GameLauncher::hasRunningGames() const
|
||||
|
|
@ -161,161 +31,6 @@ bool GameLauncher::hasRunningGames() const
|
|||
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)
|
||||
{
|
||||
if (!game) {
|
||||
|
|
@ -328,14 +43,6 @@ void GameLauncher::launchGame(Game *game)
|
|||
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
|
||||
if (m_runningGames.contains(game->id())) {
|
||||
Q_EMIT gameError(game, tr("Game is already running"));
|
||||
|
|
@ -343,7 +50,7 @@ void GameLauncher::launchGame(Game *game)
|
|||
}
|
||||
|
||||
// Handle Steam URLs
|
||||
if (!hasLaunchOverrides && command.startsWith(QLatin1String("steam://"))) {
|
||||
if (command.startsWith(QLatin1String("steam://"))) {
|
||||
QDesktopServices::openUrl(QUrl(command));
|
||||
game->setLastPlayed(QDateTime::currentDateTime());
|
||||
Q_EMIT gameStarted(game);
|
||||
|
|
@ -351,7 +58,7 @@ void GameLauncher::launchGame(Game *game)
|
|||
}
|
||||
|
||||
// Handle Lutris URLs
|
||||
if (!hasLaunchOverrides && command.startsWith(QLatin1String("lutris "))) {
|
||||
if (command.startsWith(QLatin1String("lutris "))) {
|
||||
QString lutrisCommand = command.mid(7); // Remove "lutris "
|
||||
QDesktopServices::openUrl(QUrl(lutrisCommand));
|
||||
game->setLastPlayed(QDateTime::currentDateTime());
|
||||
|
|
@ -362,21 +69,6 @@ void GameLauncher::launchGame(Game *game)
|
|||
// Start process for other commands
|
||||
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
|
||||
if (!game->workingDirectory().isEmpty()) {
|
||||
process->setWorkingDirectory(game->workingDirectory());
|
||||
|
|
@ -389,14 +81,7 @@ void GameLauncher::launchGame(Game *game)
|
|||
m_processToGame.insert(process, game);
|
||||
|
||||
// Parse command and arguments
|
||||
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);
|
||||
}
|
||||
QStringList parts = QProcess::splitCommand(command);
|
||||
if (parts.isEmpty()) {
|
||||
cleanupProcess(process);
|
||||
Q_EMIT gameError(game, tr("Invalid launch command"));
|
||||
|
|
@ -404,89 +89,7 @@ void GameLauncher::launchGame(Game *game)
|
|||
}
|
||||
|
||||
QString program = parts.takeFirst();
|
||||
|
||||
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);
|
||||
process->start(program, parts);
|
||||
|
||||
if (!process->waitForStarted(5000)) {
|
||||
QString error = process->errorString();
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
#include <QObject>
|
||||
#include <QProcess>
|
||||
#include <QQmlEngine>
|
||||
#include <QVariantMap>
|
||||
|
||||
#include "game.h"
|
||||
|
||||
|
|
@ -25,7 +24,6 @@ public:
|
|||
bool hasRunningGames() const;
|
||||
|
||||
Q_INVOKABLE void launchGame(Game *game);
|
||||
Q_INVOKABLE QVariantMap resolveLaunchInfo(Game *game) const;
|
||||
Q_INVOKABLE void stopGame(Game *game);
|
||||
Q_INVOKABLE bool isGameRunning(Game *game) const;
|
||||
|
||||
|
|
|
|||
|
|
@ -201,84 +201,20 @@ void GameModel::addGame(Game *game)
|
|||
|
||||
// Check for duplicates
|
||||
for (Game *existing : m_games) {
|
||||
if (existing->id() == game->id()) {
|
||||
if (!game->parent()) {
|
||||
game->setParent(this);
|
||||
}
|
||||
game->deleteLater();
|
||||
if (existing->platform() == game->platform() && existing->platformId() == game->platformId()) {
|
||||
delete game;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
game->setParent(this);
|
||||
|
||||
connect(
|
||||
game,
|
||||
&Game::nameChanged,
|
||||
this,
|
||||
[this]() {
|
||||
applyFilter();
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
connect(
|
||||
game,
|
||||
&Game::developerChanged,
|
||||
this,
|
||||
[this]() {
|
||||
applyFilter();
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
connect(
|
||||
game,
|
||||
&Game::publisherChanged,
|
||||
this,
|
||||
[this]() {
|
||||
applyFilter();
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
connect(
|
||||
game,
|
||||
&Game::favoriteChanged,
|
||||
this,
|
||||
[this]() {
|
||||
applyFilter();
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
connect(
|
||||
game,
|
||||
&Game::hiddenChanged,
|
||||
this,
|
||||
[this]() {
|
||||
applyFilter();
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
connect(
|
||||
game,
|
||||
&Game::platformChanged,
|
||||
this,
|
||||
[this]() {
|
||||
applyFilter();
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
connect(
|
||||
game,
|
||||
&Game::lastPlayedChanged,
|
||||
this,
|
||||
[this]() {
|
||||
applyFilter();
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
connect(
|
||||
game,
|
||||
&Game::playTimeChanged,
|
||||
this,
|
||||
[this]() {
|
||||
applyFilter();
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
|
||||
beginInsertRows(QModelIndex(), m_games.count(), m_games.count());
|
||||
m_games.append(game);
|
||||
endInsertRows();
|
||||
|
||||
applyFilter();
|
||||
Q_EMIT countChanged();
|
||||
}
|
||||
|
||||
void GameModel::removeGame(const QString &id)
|
||||
|
|
@ -292,10 +228,7 @@ void GameModel::removeGame(const QString &id)
|
|||
endRemoveRows();
|
||||
}
|
||||
|
||||
Game *game = m_games.takeAt(i);
|
||||
if (game) {
|
||||
game->deleteLater();
|
||||
}
|
||||
delete m_games.takeAt(i);
|
||||
Q_EMIT countChanged();
|
||||
return;
|
||||
}
|
||||
|
|
@ -323,11 +256,7 @@ Game *GameModel::gameById(const QString &id) const
|
|||
void GameModel::clear()
|
||||
{
|
||||
beginResetModel();
|
||||
for (Game *game : m_games) {
|
||||
if (game) {
|
||||
game->deleteLater();
|
||||
}
|
||||
}
|
||||
qDeleteAll(m_games);
|
||||
m_games.clear();
|
||||
m_filteredGames.clear();
|
||||
endResetModel();
|
||||
|
|
@ -346,81 +275,6 @@ QStringList GameModel::platforms() const
|
|||
return result;
|
||||
}
|
||||
|
||||
QList<Game *> GameModel::allGames() const
|
||||
{
|
||||
return m_games;
|
||||
}
|
||||
|
||||
bool GameModel::hasPlatformPrefix(const QString &platformPrefix) const
|
||||
{
|
||||
if (platformPrefix.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (Game *game : m_games) {
|
||||
if (!game) {
|
||||
continue;
|
||||
}
|
||||
if (game->platform().startsWith(platformPrefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
int GameModel::removeByPlatformPrefix(const QString &platformPrefix)
|
||||
{
|
||||
if (platformPrefix.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool hasMatch = false;
|
||||
for (Game *game : m_games) {
|
||||
if (!game) {
|
||||
continue;
|
||||
}
|
||||
if (game->platform().startsWith(platformPrefix)) {
|
||||
hasMatch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasMatch) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
beginResetModel();
|
||||
|
||||
int removed = 0;
|
||||
for (int i = m_games.count() - 1; i >= 0; --i) {
|
||||
Game *game = m_games.at(i);
|
||||
if (!game) {
|
||||
continue;
|
||||
}
|
||||
if (game->platform().startsWith(platformPrefix)) {
|
||||
m_games.takeAt(i)->deleteLater();
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
|
||||
m_filteredGames.clear();
|
||||
for (Game *game : m_games) {
|
||||
if (matchesFilter(game)) {
|
||||
m_filteredGames.append(game);
|
||||
}
|
||||
}
|
||||
applySort();
|
||||
|
||||
endResetModel();
|
||||
|
||||
if (removed > 0) {
|
||||
Q_EMIT countChanged();
|
||||
}
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
bool GameModel::matchesFilter(Game *game) const
|
||||
{
|
||||
if (!m_showHidden && game->hidden()) {
|
||||
|
|
|
|||
|
|
@ -83,10 +83,6 @@ public:
|
|||
Q_INVOKABLE Game *gameById(const QString &id) const;
|
||||
Q_INVOKABLE void clear();
|
||||
Q_INVOKABLE QStringList platforms() const;
|
||||
Q_INVOKABLE bool hasPlatformPrefix(const QString &platformPrefix) const;
|
||||
Q_INVOKABLE int removeByPlatformPrefix(const QString &platformPrefix);
|
||||
|
||||
QList<Game *> allGames() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void countChanged();
|
||||
|
|
|
|||
|
|
@ -3,111 +3,27 @@
|
|||
|
||||
#include "gamepadmanager.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QString>
|
||||
#if defined(ALAKARTE_HAVE_QT_GAMEPAD)
|
||||
|
||||
#include <memory>
|
||||
|
||||
#define SDL_MAIN_HANDLED 1
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_main.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr int PollIntervalMs = 16;
|
||||
constexpr int RepeatIntervalMs = 150;
|
||||
constexpr qint16 AxisThreshold = 16000;
|
||||
|
||||
QString buttonLabelToString(SDL_GamepadButtonLabel label)
|
||||
{
|
||||
switch (label) {
|
||||
case SDL_GAMEPAD_BUTTON_LABEL_A:
|
||||
return QStringLiteral("A");
|
||||
case SDL_GAMEPAD_BUTTON_LABEL_B:
|
||||
return QStringLiteral("B");
|
||||
case SDL_GAMEPAD_BUTTON_LABEL_X:
|
||||
return QStringLiteral("X");
|
||||
case SDL_GAMEPAD_BUTTON_LABEL_Y:
|
||||
return QStringLiteral("Y");
|
||||
case SDL_GAMEPAD_BUTTON_LABEL_CROSS:
|
||||
return QStringLiteral("Cross");
|
||||
case SDL_GAMEPAD_BUTTON_LABEL_CIRCLE:
|
||||
return QStringLiteral("Circle");
|
||||
case SDL_GAMEPAD_BUTTON_LABEL_SQUARE:
|
||||
return QStringLiteral("Square");
|
||||
case SDL_GAMEPAD_BUTTON_LABEL_TRIANGLE:
|
||||
return QStringLiteral("Triangle");
|
||||
default:
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
|
||||
GamepadManager::ControllerStyle controllerStyleFromTypeVendor(SDL_GamepadType type, Uint16 vendor)
|
||||
{
|
||||
switch (type) {
|
||||
case SDL_GAMEPAD_TYPE_XBOX360:
|
||||
case SDL_GAMEPAD_TYPE_XBOXONE:
|
||||
return GamepadManager::XboxController;
|
||||
case SDL_GAMEPAD_TYPE_PS3:
|
||||
case SDL_GAMEPAD_TYPE_PS4:
|
||||
case SDL_GAMEPAD_TYPE_PS5:
|
||||
return GamepadManager::PlayStationController;
|
||||
case SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO:
|
||||
case SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_LEFT:
|
||||
case SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_RIGHT:
|
||||
case SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_PAIR:
|
||||
return GamepadManager::NintendoController;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (vendor == 0x045E) {
|
||||
return GamepadManager::XboxController;
|
||||
}
|
||||
if (vendor == 0x054C) {
|
||||
return GamepadManager::PlayStationController;
|
||||
}
|
||||
if (vendor == 0x057E) {
|
||||
return GamepadManager::NintendoController;
|
||||
}
|
||||
|
||||
if (type == SDL_GAMEPAD_TYPE_STANDARD) {
|
||||
return GamepadManager::GenericController;
|
||||
}
|
||||
|
||||
return GamepadManager::UnknownController;
|
||||
}
|
||||
}
|
||||
#include <QGamepad>
|
||||
#include <QGamepadManager>
|
||||
|
||||
GamepadManager *GamepadManager::s_instance = nullptr;
|
||||
|
||||
GamepadManager::GamepadManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
SDL_SetMainReady();
|
||||
auto *manager = QGamepadManager::instance();
|
||||
|
||||
m_sdlInitialized = SDL_Init(SDL_INIT_GAMEPAD);
|
||||
connect(manager, &QGamepadManager::gamepadConnected, this, &GamepadManager::onGamepadConnected);
|
||||
connect(manager, &QGamepadManager::gamepadDisconnected, this, &GamepadManager::onGamepadDisconnected);
|
||||
|
||||
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() {
|
||||
m_pollTimer.stop();
|
||||
m_repeatTimer.stop();
|
||||
closeGamepad();
|
||||
if (m_sdlInitialized) {
|
||||
SDL_Quit();
|
||||
m_sdlInitialized = false;
|
||||
}
|
||||
});
|
||||
|
||||
m_pollTimer.setInterval(PollIntervalMs);
|
||||
connect(&m_pollTimer, &QTimer::timeout, this, &GamepadManager::pollEvents);
|
||||
|
||||
m_repeatTimer.setInterval(RepeatIntervalMs);
|
||||
m_repeatTimer.setInterval(150);
|
||||
connect(&m_repeatTimer, &QTimer::timeout, this, &GamepadManager::processInput);
|
||||
|
||||
if (m_sdlInitialized) {
|
||||
SDL_SetGamepadEventsEnabled(true);
|
||||
openFirstAvailableGamepad();
|
||||
m_pollTimer.start();
|
||||
const auto gamepads = manager->connectedGamepads();
|
||||
if (!gamepads.isEmpty()) {
|
||||
setupGamepad(gamepads.first());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -136,31 +52,6 @@ bool GamepadManager::active() const
|
|||
return m_active;
|
||||
}
|
||||
|
||||
GamepadManager::ControllerStyle GamepadManager::controllerStyle() const
|
||||
{
|
||||
return m_controllerStyle;
|
||||
}
|
||||
|
||||
QString GamepadManager::confirmButtonLabel() const
|
||||
{
|
||||
return m_confirmButtonLabel;
|
||||
}
|
||||
|
||||
QString GamepadManager::backButtonLabel() const
|
||||
{
|
||||
return m_backButtonLabel;
|
||||
}
|
||||
|
||||
QString GamepadManager::detailsButtonLabel() const
|
||||
{
|
||||
return m_detailsButtonLabel;
|
||||
}
|
||||
|
||||
QString GamepadManager::searchButtonLabel() const
|
||||
{
|
||||
return m_searchButtonLabel;
|
||||
}
|
||||
|
||||
void GamepadManager::setActive(bool active)
|
||||
{
|
||||
if (m_active != active) {
|
||||
|
|
@ -169,279 +60,173 @@ void GamepadManager::setActive(bool active)
|
|||
}
|
||||
}
|
||||
|
||||
void GamepadManager::openFirstAvailableGamepad()
|
||||
void GamepadManager::onGamepadConnected(int deviceId)
|
||||
{
|
||||
if (!m_sdlInitialized || m_gamepad) {
|
||||
return;
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
std::unique_ptr<SDL_JoystickID[], decltype(&SDL_free)> ids(SDL_GetGamepads(&count), SDL_free);
|
||||
if (!ids || count <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const SDL_JoystickID instanceId = ids[0];
|
||||
|
||||
if (SDL_IsGamepad(instanceId)) {
|
||||
openGamepad(static_cast<int>(instanceId));
|
||||
if (!m_gamepad) {
|
||||
setupGamepad(deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
void GamepadManager::openGamepad(int deviceId)
|
||||
void GamepadManager::onGamepadDisconnected(int deviceId)
|
||||
{
|
||||
if (!m_sdlInitialized || m_gamepad) {
|
||||
return;
|
||||
if (m_gamepad && m_gamepad->deviceId() == deviceId) {
|
||||
m_gamepad->deleteLater();
|
||||
m_gamepad = nullptr;
|
||||
m_connected = false;
|
||||
m_repeatTimer.stop();
|
||||
Q_EMIT connectedChanged();
|
||||
|
||||
const auto gamepads = QGamepadManager::instance()->connectedGamepads();
|
||||
if (!gamepads.isEmpty()) {
|
||||
setupGamepad(gamepads.first());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SDL_Gamepad *pad = SDL_OpenGamepad(static_cast<SDL_JoystickID>(deviceId));
|
||||
if (!pad) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_gamepad = pad;
|
||||
m_gamepadId = deviceId;
|
||||
void GamepadManager::setupGamepad(int deviceId)
|
||||
{
|
||||
m_gamepad = new QGamepad(deviceId, this);
|
||||
m_connected = true;
|
||||
Q_EMIT connectedChanged();
|
||||
updateControllerInfo();
|
||||
}
|
||||
|
||||
void GamepadManager::closeGamepad()
|
||||
{
|
||||
if (!m_gamepad) {
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_CloseGamepad(m_gamepad);
|
||||
m_gamepad = nullptr;
|
||||
m_gamepadId = 0;
|
||||
m_connected = false;
|
||||
setActive(false);
|
||||
m_leftX = 0;
|
||||
m_leftY = 0;
|
||||
m_leftDirX = 0;
|
||||
m_leftDirY = 0;
|
||||
m_repeatTimer.stop();
|
||||
Q_EMIT connectedChanged();
|
||||
updateControllerInfo();
|
||||
}
|
||||
|
||||
void GamepadManager::updateControllerInfo()
|
||||
{
|
||||
ControllerStyle style = UnknownController;
|
||||
QString confirmLabel;
|
||||
QString backLabel;
|
||||
QString detailsLabel;
|
||||
QString searchLabel;
|
||||
|
||||
if (m_gamepad) {
|
||||
const SDL_GamepadType type = SDL_GetGamepadType(m_gamepad);
|
||||
const Uint16 vendor = SDL_GetGamepadVendor(m_gamepad);
|
||||
style = controllerStyleFromTypeVendor(type, vendor);
|
||||
|
||||
#if SDL_VERSION_ATLEAST(3, 2, 0)
|
||||
confirmLabel = buttonLabelToString(SDL_GetGamepadButtonLabel(m_gamepad, SDL_GAMEPAD_BUTTON_SOUTH));
|
||||
backLabel = buttonLabelToString(SDL_GetGamepadButtonLabel(m_gamepad, SDL_GAMEPAD_BUTTON_EAST));
|
||||
detailsLabel = buttonLabelToString(SDL_GetGamepadButtonLabel(m_gamepad, SDL_GAMEPAD_BUTTON_WEST));
|
||||
searchLabel = buttonLabelToString(SDL_GetGamepadButtonLabel(m_gamepad, SDL_GAMEPAD_BUTTON_NORTH));
|
||||
#endif
|
||||
|
||||
if (confirmLabel.isEmpty() || backLabel.isEmpty() || detailsLabel.isEmpty() || searchLabel.isEmpty()) {
|
||||
if (style == PlayStationController) {
|
||||
if (confirmLabel.isEmpty()) {
|
||||
confirmLabel = QStringLiteral("Cross");
|
||||
}
|
||||
if (backLabel.isEmpty()) {
|
||||
backLabel = QStringLiteral("Circle");
|
||||
}
|
||||
if (detailsLabel.isEmpty()) {
|
||||
detailsLabel = QStringLiteral("Square");
|
||||
}
|
||||
if (searchLabel.isEmpty()) {
|
||||
searchLabel = QStringLiteral("Triangle");
|
||||
}
|
||||
} else if (style == NintendoController) {
|
||||
if (confirmLabel.isEmpty()) {
|
||||
confirmLabel = QStringLiteral("B");
|
||||
}
|
||||
if (backLabel.isEmpty()) {
|
||||
backLabel = QStringLiteral("A");
|
||||
}
|
||||
if (detailsLabel.isEmpty()) {
|
||||
detailsLabel = QStringLiteral("Y");
|
||||
}
|
||||
if (searchLabel.isEmpty()) {
|
||||
searchLabel = QStringLiteral("X");
|
||||
}
|
||||
} else {
|
||||
if (confirmLabel.isEmpty()) {
|
||||
confirmLabel = QStringLiteral("A");
|
||||
}
|
||||
if (backLabel.isEmpty()) {
|
||||
backLabel = QStringLiteral("B");
|
||||
}
|
||||
if (detailsLabel.isEmpty()) {
|
||||
detailsLabel = QStringLiteral("X");
|
||||
}
|
||||
if (searchLabel.isEmpty()) {
|
||||
searchLabel = QStringLiteral("Y");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const bool changed = (style != m_controllerStyle) || (confirmLabel != m_confirmButtonLabel) || (backLabel != m_backButtonLabel)
|
||||
|| (detailsLabel != m_detailsButtonLabel) || (searchLabel != m_searchButtonLabel);
|
||||
|
||||
m_controllerStyle = style;
|
||||
m_confirmButtonLabel = confirmLabel;
|
||||
m_backButtonLabel = backLabel;
|
||||
m_detailsButtonLabel = detailsLabel;
|
||||
m_searchButtonLabel = searchLabel;
|
||||
|
||||
if (changed) {
|
||||
Q_EMIT controllerInfoChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void GamepadManager::pollEvents()
|
||||
{
|
||||
if (!m_sdlInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_Event e;
|
||||
while (SDL_PollEvent(&e)) {
|
||||
switch (e.type) {
|
||||
case SDL_EVENT_GAMEPAD_ADDED: {
|
||||
if (!m_gamepad) {
|
||||
openGamepad(static_cast<int>(e.gdevice.which));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SDL_EVENT_GAMEPAD_REMOVED: {
|
||||
if (m_gamepad && static_cast<int>(e.gdevice.which) == m_gamepadId) {
|
||||
closeGamepad();
|
||||
openFirstAvailableGamepad();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SDL_EVENT_GAMEPAD_AXIS_MOTION: {
|
||||
if (!m_gamepad || static_cast<int>(e.gaxis.which) != m_gamepadId) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (e.gaxis.axis == SDL_GAMEPAD_AXIS_LEFTX) {
|
||||
m_leftX = e.gaxis.value;
|
||||
} else if (e.gaxis.axis == SDL_GAMEPAD_AXIS_LEFTY) {
|
||||
m_leftY = e.gaxis.value;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
const qint8 dirX = (m_leftX < -AxisThreshold) ? -1 : (m_leftX > AxisThreshold ? 1 : 0);
|
||||
const qint8 dirY = (m_leftY < -AxisThreshold) ? -1 : (m_leftY > AxisThreshold ? 1 : 0);
|
||||
|
||||
if (dirX != 0 || dirY != 0) {
|
||||
connect(m_gamepad, &QGamepad::axisLeftYChanged, this, [this](double value) {
|
||||
if (qAbs(value) > 0.5) {
|
||||
setActive(true);
|
||||
}
|
||||
|
||||
if (dirY != m_leftDirY) {
|
||||
m_leftDirY = dirY;
|
||||
if (dirY < 0) {
|
||||
if (value < -0.5) {
|
||||
Q_EMIT navigateUp();
|
||||
} else if (dirY > 0) {
|
||||
} else {
|
||||
Q_EMIT navigateDown();
|
||||
}
|
||||
}
|
||||
|
||||
if (dirX != m_leftDirX) {
|
||||
m_leftDirX = dirX;
|
||||
if (dirX < 0) {
|
||||
Q_EMIT navigateLeft();
|
||||
} else if (dirX > 0) {
|
||||
Q_EMIT navigateRight();
|
||||
}
|
||||
}
|
||||
|
||||
if (m_leftDirX != 0 || m_leftDirY != 0) {
|
||||
if (!m_repeatTimer.isActive()) {
|
||||
m_repeatTimer.start();
|
||||
}
|
||||
} else {
|
||||
m_repeatTimer.stop();
|
||||
}
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
case SDL_EVENT_GAMEPAD_BUTTON_DOWN: {
|
||||
if (!m_gamepad || static_cast<int>(e.gbutton.which) != m_gamepadId) {
|
||||
break;
|
||||
}
|
||||
|
||||
connect(m_gamepad, &QGamepad::axisLeftXChanged, this, [this](double value) {
|
||||
if (qAbs(value) > 0.5) {
|
||||
setActive(true);
|
||||
|
||||
switch (static_cast<SDL_GamepadButton>(e.gbutton.button)) {
|
||||
case SDL_GAMEPAD_BUTTON_SOUTH:
|
||||
Q_EMIT selectPressed();
|
||||
break;
|
||||
case SDL_GAMEPAD_BUTTON_EAST:
|
||||
case SDL_GAMEPAD_BUTTON_BACK:
|
||||
Q_EMIT backPressed();
|
||||
break;
|
||||
case SDL_GAMEPAD_BUTTON_WEST:
|
||||
Q_EMIT detailsPressed();
|
||||
break;
|
||||
case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER:
|
||||
Q_EMIT leftBumperPressed();
|
||||
break;
|
||||
case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER:
|
||||
Q_EMIT rightBumperPressed();
|
||||
break;
|
||||
case SDL_GAMEPAD_BUTTON_START:
|
||||
Q_EMIT menuPressed();
|
||||
break;
|
||||
case SDL_GAMEPAD_BUTTON_NORTH:
|
||||
Q_EMIT searchPressed();
|
||||
break;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_UP:
|
||||
Q_EMIT navigateUp();
|
||||
break;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_DOWN:
|
||||
Q_EMIT navigateDown();
|
||||
break;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_LEFT:
|
||||
if (value < -0.5) {
|
||||
Q_EMIT navigateLeft();
|
||||
break;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_RIGHT:
|
||||
} else {
|
||||
Q_EMIT navigateRight();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
m_repeatTimer.start();
|
||||
} else {
|
||||
m_repeatTimer.stop();
|
||||
}
|
||||
default:
|
||||
break;
|
||||
});
|
||||
|
||||
connect(m_gamepad, &QGamepad::buttonAChanged, this, [this](bool pressed) {
|
||||
if (pressed) {
|
||||
setActive(true);
|
||||
Q_EMIT selectPressed();
|
||||
}
|
||||
});
|
||||
|
||||
connect(m_gamepad, &QGamepad::buttonBChanged, this, [this](bool pressed) {
|
||||
if (pressed) {
|
||||
setActive(true);
|
||||
Q_EMIT backPressed();
|
||||
}
|
||||
});
|
||||
|
||||
connect(m_gamepad, &QGamepad::buttonStartChanged, this, [this](bool pressed) {
|
||||
if (pressed) {
|
||||
setActive(true);
|
||||
Q_EMIT menuPressed();
|
||||
}
|
||||
});
|
||||
|
||||
connect(m_gamepad, &QGamepad::buttonYChanged, this, [this](bool pressed) {
|
||||
if (pressed) {
|
||||
setActive(true);
|
||||
Q_EMIT searchPressed();
|
||||
}
|
||||
});
|
||||
|
||||
connect(m_gamepad, &QGamepad::buttonUpChanged, this, [this](bool pressed) {
|
||||
if (pressed) {
|
||||
setActive(true);
|
||||
Q_EMIT navigateUp();
|
||||
}
|
||||
});
|
||||
|
||||
connect(m_gamepad, &QGamepad::buttonDownChanged, this, [this](bool pressed) {
|
||||
if (pressed) {
|
||||
setActive(true);
|
||||
Q_EMIT navigateDown();
|
||||
}
|
||||
});
|
||||
|
||||
connect(m_gamepad, &QGamepad::buttonLeftChanged, this, [this](bool pressed) {
|
||||
if (pressed) {
|
||||
setActive(true);
|
||||
Q_EMIT navigateLeft();
|
||||
}
|
||||
});
|
||||
|
||||
connect(m_gamepad, &QGamepad::buttonRightChanged, this, [this](bool pressed) {
|
||||
if (pressed) {
|
||||
setActive(true);
|
||||
Q_EMIT navigateRight();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void GamepadManager::processInput()
|
||||
{
|
||||
if (!m_gamepad) {
|
||||
if (!m_gamepad)
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_leftY < -AxisThreshold) {
|
||||
double y = m_gamepad->axisLeftY();
|
||||
double x = m_gamepad->axisLeftX();
|
||||
|
||||
if (y < -0.5) {
|
||||
Q_EMIT navigateUp();
|
||||
} else if (m_leftY > AxisThreshold) {
|
||||
} else if (y > 0.5) {
|
||||
Q_EMIT navigateDown();
|
||||
}
|
||||
|
||||
if (m_leftX < -AxisThreshold) {
|
||||
if (x < -0.5) {
|
||||
Q_EMIT navigateLeft();
|
||||
} else if (m_leftX > AxisThreshold) {
|
||||
} else if (x > 0.5) {
|
||||
Q_EMIT navigateRight();
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
GamepadManager *GamepadManager::s_instance = nullptr;
|
||||
|
||||
GamepadManager::GamepadManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
GamepadManager *GamepadManager::instance()
|
||||
{
|
||||
if (!s_instance) {
|
||||
s_instance = new GamepadManager();
|
||||
}
|
||||
return s_instance;
|
||||
}
|
||||
|
||||
GamepadManager *GamepadManager::create(QQmlEngine *engine, QJSEngine *scriptEngine)
|
||||
{
|
||||
Q_UNUSED(engine)
|
||||
Q_UNUSED(scriptEngine)
|
||||
return instance();
|
||||
}
|
||||
|
||||
bool GamepadManager::connected() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool GamepadManager::active() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@
|
|||
|
||||
#include <QObject>
|
||||
#include <QQmlEngine>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
|
||||
struct SDL_Gamepad;
|
||||
class QGamepad;
|
||||
class QGamepadManager;
|
||||
|
||||
class GamepadManager : public QObject
|
||||
{
|
||||
|
|
@ -18,38 +18,17 @@ class GamepadManager : public QObject
|
|||
|
||||
Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged)
|
||||
Q_PROPERTY(bool active READ active NOTIFY activeChanged)
|
||||
Q_PROPERTY(ControllerStyle controllerStyle READ controllerStyle NOTIFY controllerInfoChanged)
|
||||
Q_PROPERTY(QString confirmButtonLabel READ confirmButtonLabel NOTIFY controllerInfoChanged)
|
||||
Q_PROPERTY(QString backButtonLabel READ backButtonLabel NOTIFY controllerInfoChanged)
|
||||
Q_PROPERTY(QString detailsButtonLabel READ detailsButtonLabel NOTIFY controllerInfoChanged)
|
||||
Q_PROPERTY(QString searchButtonLabel READ searchButtonLabel NOTIFY controllerInfoChanged)
|
||||
|
||||
public:
|
||||
enum ControllerStyle {
|
||||
UnknownController = 0,
|
||||
XboxController,
|
||||
PlayStationController,
|
||||
NintendoController,
|
||||
GenericController,
|
||||
};
|
||||
Q_ENUM(ControllerStyle)
|
||||
|
||||
static GamepadManager *instance();
|
||||
static GamepadManager *create(QQmlEngine *engine, QJSEngine *scriptEngine);
|
||||
|
||||
bool connected() const;
|
||||
bool active() const;
|
||||
|
||||
ControllerStyle controllerStyle() const;
|
||||
QString confirmButtonLabel() const;
|
||||
QString backButtonLabel() const;
|
||||
QString detailsButtonLabel() const;
|
||||
QString searchButtonLabel() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void connectedChanged();
|
||||
void activeChanged();
|
||||
void controllerInfoChanged();
|
||||
|
||||
void navigateUp();
|
||||
void navigateDown();
|
||||
|
|
@ -57,40 +36,22 @@ Q_SIGNALS:
|
|||
void navigateRight();
|
||||
void selectPressed();
|
||||
void backPressed();
|
||||
void detailsPressed();
|
||||
void menuPressed();
|
||||
void searchPressed();
|
||||
|
||||
void leftBumperPressed();
|
||||
void rightBumperPressed();
|
||||
|
||||
private:
|
||||
explicit GamepadManager(QObject *parent = nullptr);
|
||||
|
||||
static GamepadManager *s_instance;
|
||||
|
||||
SDL_Gamepad *m_gamepad = nullptr;
|
||||
int m_gamepadId = 0;
|
||||
QGamepad *m_gamepad = nullptr;
|
||||
bool m_connected = false;
|
||||
bool m_active = false;
|
||||
bool m_sdlInitialized = false;
|
||||
ControllerStyle m_controllerStyle = UnknownController;
|
||||
QString m_confirmButtonLabel;
|
||||
QString m_backButtonLabel;
|
||||
QString m_detailsButtonLabel;
|
||||
QString m_searchButtonLabel;
|
||||
QTimer m_pollTimer;
|
||||
QTimer m_repeatTimer;
|
||||
qint16 m_leftX = 0;
|
||||
qint16 m_leftY = 0;
|
||||
qint8 m_leftDirX = 0;
|
||||
qint8 m_leftDirY = 0;
|
||||
|
||||
void openFirstAvailableGamepad();
|
||||
void openGamepad(int deviceId);
|
||||
void closeGamepad();
|
||||
void updateControllerInfo();
|
||||
void pollEvents();
|
||||
void onGamepadConnected(int deviceId);
|
||||
void onGamepadDisconnected(int deviceId);
|
||||
void setupGamepad(int deviceId);
|
||||
void processInput();
|
||||
void setActive(bool active);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,22 +10,6 @@ GameSortFilterModel::GameSortFilterModel(QObject *parent)
|
|||
{
|
||||
setDynamicSortFilter(true);
|
||||
sort(0);
|
||||
|
||||
connect(this, &QAbstractItemModel::rowsInserted, this, [this](const QModelIndex &, int, int) {
|
||||
Q_EMIT countChanged();
|
||||
});
|
||||
connect(this, &QAbstractItemModel::rowsRemoved, this, [this](const QModelIndex &, int, int) {
|
||||
Q_EMIT countChanged();
|
||||
});
|
||||
connect(this, &QAbstractItemModel::rowsMoved, this, [this](const QModelIndex &, int, int, const QModelIndex &, int) {
|
||||
Q_EMIT countChanged();
|
||||
});
|
||||
connect(this, &QAbstractItemModel::modelReset, this, [this] {
|
||||
Q_EMIT countChanged();
|
||||
});
|
||||
connect(this, &QAbstractItemModel::layoutChanged, this, [this](const QList<QPersistentModelIndex> &, QAbstractItemModel::LayoutChangeHint) {
|
||||
Q_EMIT countChanged();
|
||||
});
|
||||
}
|
||||
|
||||
QString GameSortFilterModel::filterText() const
|
||||
|
|
@ -185,7 +169,6 @@ bool GameSortFilterModel::lessThan(const QModelIndex &left, const QModelIndex &r
|
|||
|
||||
void GameSortFilterModel::invalidateAndEmit()
|
||||
{
|
||||
beginFilterChange();
|
||||
endFilterChange();
|
||||
invalidateFilter();
|
||||
Q_EMIT countChanged();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
#include "heroicimporter.h"
|
||||
|
||||
#include <QCryptographicHash>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
|
|
@ -28,7 +27,7 @@ QString HeroicImporter::platformId() const
|
|||
|
||||
bool HeroicImporter::isAvailable() const
|
||||
{
|
||||
return directoryExists(findHeroicConfigPath()) || directoryExists(findLegendaryConfigPath());
|
||||
return directoryExists(findHeroicConfigPath());
|
||||
}
|
||||
|
||||
QString HeroicImporter::findHeroicConfigPath() const
|
||||
|
|
@ -45,158 +44,17 @@ QString HeroicImporter::findHeroicConfigPath() const
|
|||
return {};
|
||||
}
|
||||
|
||||
QString HeroicImporter::findLegendaryConfigPath() const
|
||||
{
|
||||
QStringList possiblePaths = {expandPath(QStringLiteral("~/.config/legendary")),
|
||||
expandPath(QStringLiteral("~/.var/app/com.heroicgameslauncher.hgl/config/legendary"))};
|
||||
|
||||
for (const QString &path : possiblePaths) {
|
||||
if (directoryExists(path)) {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
QString HeroicImporter::launchCommandForApp(const QString &appName) const
|
||||
{
|
||||
if (!findExecutable(QStringLiteral("heroic")).isEmpty()) {
|
||||
return QStringLiteral("heroic --launch %1").arg(appName);
|
||||
}
|
||||
if (!findExecutable(QStringLiteral("flatpak")).isEmpty()) {
|
||||
return QStringLiteral("flatpak run com.heroicgameslauncher.hgl --launch %1").arg(appName);
|
||||
}
|
||||
return QStringLiteral("heroic --launch %1").arg(appName);
|
||||
}
|
||||
|
||||
QUrl HeroicImporter::extractCoverUrl(const QJsonObject &gameObj) const
|
||||
{
|
||||
static const QStringList stringKeys = {
|
||||
QStringLiteral("coverUrl"),
|
||||
QStringLiteral("cover_url"),
|
||||
QStringLiteral("cover"),
|
||||
QStringLiteral("image"),
|
||||
QStringLiteral("imageUrl"),
|
||||
QStringLiteral("image_url"),
|
||||
QStringLiteral("art_cover"),
|
||||
QStringLiteral("artCover"),
|
||||
QStringLiteral("art_square"),
|
||||
QStringLiteral("artSquare"),
|
||||
QStringLiteral("thumbnail"),
|
||||
};
|
||||
|
||||
for (const QString &key : stringKeys) {
|
||||
const QString urlString = gameObj.value(key).toString();
|
||||
if (!urlString.isEmpty()) {
|
||||
const QUrl url(urlString);
|
||||
if (url.isValid()) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const QJsonValue keyImagesValue = gameObj.value(QStringLiteral("keyImages"));
|
||||
if (keyImagesValue.isArray()) {
|
||||
const QJsonArray keyImages = keyImagesValue.toArray();
|
||||
for (const QJsonValue &imgValue : keyImages) {
|
||||
const QJsonObject imgObj = imgValue.toObject();
|
||||
const QString type = imgObj.value(QStringLiteral("type")).toString();
|
||||
if (type == QStringLiteral("DieselGameBoxTall") || type == QStringLiteral("Thumbnail") || type == QStringLiteral("Poster")) {
|
||||
const QString urlString = imgObj.value(QStringLiteral("url")).toString();
|
||||
if (!urlString.isEmpty()) {
|
||||
const QUrl url(urlString);
|
||||
if (url.isValid()) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const QJsonValue imagesValue = gameObj.value(QStringLiteral("images"));
|
||||
if (imagesValue.isArray()) {
|
||||
const QJsonArray images = imagesValue.toArray();
|
||||
for (const QJsonValue &imgValue : images) {
|
||||
const QJsonObject imgObj = imgValue.toObject();
|
||||
const QString urlString = imgObj.value(QStringLiteral("url")).toString();
|
||||
if (!urlString.isEmpty()) {
|
||||
const QUrl url(urlString);
|
||||
if (url.isValid()) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
QUrl HeroicImporter::preferCachedCoverUrl(const QUrl &coverUrl) const
|
||||
{
|
||||
if (!coverUrl.isValid()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (coverUrl.isLocalFile() || coverUrl.scheme() == QLatin1String("file")) {
|
||||
return coverUrl;
|
||||
}
|
||||
|
||||
QString urlString = coverUrl.toString();
|
||||
|
||||
// Heroic uses an imagecache:// scheme which percent-encodes the real image URL.
|
||||
if (coverUrl.scheme() == QLatin1String("imagecache")) {
|
||||
const QString prefix = QStringLiteral("imagecache://");
|
||||
if (urlString.startsWith(prefix)) {
|
||||
urlString = QUrl::fromPercentEncoding(urlString.mid(prefix.size()).toUtf8());
|
||||
}
|
||||
}
|
||||
|
||||
const QUrl realUrl(urlString);
|
||||
if (!realUrl.isValid()) {
|
||||
return coverUrl;
|
||||
}
|
||||
|
||||
if (realUrl.scheme() != QLatin1String("http") && realUrl.scheme() != QLatin1String("https")) {
|
||||
return realUrl;
|
||||
}
|
||||
|
||||
const QString configPath = findHeroicConfigPath();
|
||||
if (configPath.isEmpty()) {
|
||||
return realUrl;
|
||||
}
|
||||
|
||||
const QString cacheDir = configPath + QStringLiteral("/images-cache");
|
||||
if (!QDir(cacheDir).exists()) {
|
||||
return realUrl;
|
||||
}
|
||||
|
||||
// Upstream Heroic stores cached images as sha256(url) (no extension) inside images-cache.
|
||||
const QByteArray digest = QCryptographicHash::hash(realUrl.toString().toUtf8(), QCryptographicHash::Sha256).toHex();
|
||||
const QString cachedPath = cacheDir + QStringLiteral("/") + QString::fromLatin1(digest);
|
||||
if (QFile::exists(cachedPath)) {
|
||||
return QUrl::fromLocalFile(cachedPath);
|
||||
}
|
||||
|
||||
return realUrl;
|
||||
}
|
||||
|
||||
QUrl HeroicImporter::findCoverImage(const QString &appName, const QString &store) const
|
||||
{
|
||||
QString configPath = findHeroicConfigPath();
|
||||
if (configPath.isEmpty()) {
|
||||
configPath = findLegendaryConfigPath();
|
||||
return {};
|
||||
}
|
||||
|
||||
// Check images cache
|
||||
QStringList imageDirs = {configPath + QStringLiteral("/images-cache"), configPath + QStringLiteral("/images")};
|
||||
if (!store.isEmpty()) {
|
||||
const QString storeLower = store.toLower();
|
||||
imageDirs.append(configPath + QStringLiteral("/images-cache/") + storeLower);
|
||||
imageDirs.append(configPath + QStringLiteral("/images/") + storeLower);
|
||||
}
|
||||
|
||||
QStringList extensions = {QStringLiteral(".jpg"), QStringLiteral(".jpeg"), QStringLiteral(".png"), QStringLiteral(".webp")};
|
||||
QStringList extensions = {QStringLiteral(".jpg"), QStringLiteral(".png"), QStringLiteral(".webp")};
|
||||
QStringList prefixes = {appName, appName + QStringLiteral("_cover"), appName + QStringLiteral("_library")};
|
||||
|
||||
for (const QString &imageDir : imageDirs) {
|
||||
|
|
@ -213,16 +71,6 @@ QUrl HeroicImporter::findCoverImage(const QString &appName, const QString &store
|
|||
}
|
||||
}
|
||||
|
||||
QStringList wildcardFilters;
|
||||
wildcardFilters.reserve(extensions.count());
|
||||
for (const QString &ext : extensions) {
|
||||
wildcardFilters.append(QStringLiteral("*") + appName + QStringLiteral("*") + ext);
|
||||
}
|
||||
const QStringList wildcardMatches = dir.entryList(wildcardFilters, QDir::Files);
|
||||
if (!wildcardMatches.isEmpty()) {
|
||||
return QUrl::fromLocalFile(imageDir + QStringLiteral("/") + wildcardMatches.first());
|
||||
}
|
||||
|
||||
// Also try looking in subdirectories
|
||||
QStringList subDirs = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
|
||||
for (const QString &subDir : subDirs) {
|
||||
|
|
@ -234,14 +82,6 @@ QUrl HeroicImporter::findCoverImage(const QString &appName, const QString &store
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
QDir sub(imageDir + QStringLiteral("/") + subDir);
|
||||
if (sub.exists()) {
|
||||
const QStringList subMatches = sub.entryList(wildcardFilters, QDir::Files);
|
||||
if (!subMatches.isEmpty()) {
|
||||
return QUrl::fromLocalFile(sub.absoluteFilePath(subMatches.first()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -255,179 +95,6 @@ QList<Game *> HeroicImporter::importGames()
|
|||
games.append(importEpicGames());
|
||||
games.append(importGOGGames());
|
||||
games.append(importAmazonGames());
|
||||
games.append(importSideloadApps());
|
||||
|
||||
return games;
|
||||
}
|
||||
|
||||
QList<Game *> HeroicImporter::importSideloadApps()
|
||||
{
|
||||
QList<Game *> games;
|
||||
const QString configPath = findHeroicConfigPath();
|
||||
if (configPath.isEmpty()) {
|
||||
return games;
|
||||
}
|
||||
|
||||
const QString libraryPath = configPath + QStringLiteral("/sideload_apps/library.json");
|
||||
QFile libraryFile(libraryPath);
|
||||
if (!libraryFile.open(QIODevice::ReadOnly)) {
|
||||
return games;
|
||||
}
|
||||
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(libraryFile.readAll());
|
||||
|
||||
QList<QJsonObject> entries;
|
||||
if (doc.isArray()) {
|
||||
const QJsonArray arr = doc.array();
|
||||
entries.reserve(arr.size());
|
||||
for (const QJsonValue &value : arr) {
|
||||
if (value.isObject()) {
|
||||
entries.append(value.toObject());
|
||||
}
|
||||
}
|
||||
} else if (doc.isObject()) {
|
||||
const QJsonObject root = doc.object();
|
||||
const QStringList arrayKeys = {
|
||||
QStringLiteral("games"),
|
||||
QStringLiteral("apps"),
|
||||
QStringLiteral("library"),
|
||||
QStringLiteral("items"),
|
||||
};
|
||||
|
||||
bool foundArray = false;
|
||||
for (const QString &key : arrayKeys) {
|
||||
const QJsonValue v = root.value(key);
|
||||
if (v.isArray()) {
|
||||
const QJsonArray arr = v.toArray();
|
||||
entries.reserve(arr.size());
|
||||
for (const QJsonValue &item : arr) {
|
||||
if (item.isObject()) {
|
||||
entries.append(item.toObject());
|
||||
}
|
||||
}
|
||||
foundArray = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundArray) {
|
||||
// Some formats may store entries keyed by appName.
|
||||
for (auto it = root.constBegin(); it != root.constEnd(); ++it) {
|
||||
if (it.value().isObject()) {
|
||||
QJsonObject obj = it.value().toObject();
|
||||
if (!obj.contains(QStringLiteral("app_name")) && !obj.contains(QStringLiteral("appName"))) {
|
||||
obj.insert(QStringLiteral("app_name"), it.key());
|
||||
}
|
||||
entries.append(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return games;
|
||||
}
|
||||
|
||||
for (const QJsonObject &obj : entries) {
|
||||
QString appName = obj.value(QStringLiteral("app_name")).toString();
|
||||
if (appName.isEmpty()) {
|
||||
appName = obj.value(QStringLiteral("appName")).toString();
|
||||
}
|
||||
if (appName.isEmpty()) {
|
||||
appName = obj.value(QStringLiteral("id")).toString();
|
||||
}
|
||||
|
||||
QString title = obj.value(QStringLiteral("title")).toString();
|
||||
if (title.isEmpty()) {
|
||||
title = obj.value(QStringLiteral("name")).toString();
|
||||
}
|
||||
|
||||
if (appName.isEmpty() || title.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString launchCommand = obj.value(QStringLiteral("launchCommand")).toString();
|
||||
if (launchCommand.isEmpty()) {
|
||||
launchCommand = obj.value(QStringLiteral("launch_command")).toString();
|
||||
}
|
||||
if (launchCommand.isEmpty()) {
|
||||
launchCommand = obj.value(QStringLiteral("command")).toString();
|
||||
}
|
||||
if (launchCommand.isEmpty()) {
|
||||
launchCommand = obj.value(QStringLiteral("exec")).toString();
|
||||
}
|
||||
if (launchCommand.isEmpty()) {
|
||||
launchCommand = obj.value(QStringLiteral("exe")).toString();
|
||||
}
|
||||
if (launchCommand.isEmpty()) {
|
||||
launchCommand = obj.value(QStringLiteral("executable")).toString();
|
||||
}
|
||||
if (launchCommand.isEmpty()) {
|
||||
launchCommand = obj.value(QStringLiteral("selectedExe")).toString();
|
||||
}
|
||||
|
||||
QString workingDirectory = obj.value(QStringLiteral("workingDirectory")).toString();
|
||||
if (workingDirectory.isEmpty()) {
|
||||
workingDirectory = obj.value(QStringLiteral("install_path")).toString();
|
||||
}
|
||||
if (workingDirectory.isEmpty()) {
|
||||
workingDirectory = obj.value(QStringLiteral("installPath")).toString();
|
||||
}
|
||||
|
||||
bool isInstalled = obj.value(QStringLiteral("installed")).toBool(true);
|
||||
if (obj.contains(QStringLiteral("is_installed"))) {
|
||||
isInstalled = obj.value(QStringLiteral("is_installed")).toBool();
|
||||
}
|
||||
if (obj.contains(QStringLiteral("isInstalled"))) {
|
||||
isInstalled = obj.value(QStringLiteral("isInstalled")).toBool();
|
||||
}
|
||||
|
||||
Game *game = new Game(QStringLiteral("heroic-sideload-%1").arg(appName), title);
|
||||
game->setPlatform(QStringLiteral("Heroic (Sideload)"));
|
||||
game->setPlatformId(appName);
|
||||
if (!workingDirectory.isEmpty()) {
|
||||
game->setWorkingDirectory(workingDirectory);
|
||||
}
|
||||
game->setInstalled(isInstalled);
|
||||
|
||||
// We don't know if Heroic's CLI can always launch sideload apps consistently.
|
||||
// Prefer an explicit command from the library file; fallback to Heroic's CLI if absent.
|
||||
if (!launchCommand.isEmpty()) {
|
||||
game->setLaunchCommand(launchCommand);
|
||||
} else {
|
||||
game->setLaunchCommand(launchCommandForApp(appName));
|
||||
}
|
||||
|
||||
QString cover = obj.value(QStringLiteral("art_square")).toString();
|
||||
if (cover.isEmpty()) {
|
||||
cover = obj.value(QStringLiteral("artSquare")).toString();
|
||||
}
|
||||
if (cover.isEmpty()) {
|
||||
cover = obj.value(QStringLiteral("art_cover")).toString();
|
||||
}
|
||||
if (cover.isEmpty()) {
|
||||
cover = obj.value(QStringLiteral("artCover")).toString();
|
||||
}
|
||||
|
||||
QUrl coverUrl;
|
||||
if (!cover.isEmpty()) {
|
||||
if (cover.startsWith(QLatin1String("/"))) {
|
||||
coverUrl = QUrl::fromLocalFile(cover);
|
||||
} else {
|
||||
coverUrl = QUrl(cover);
|
||||
}
|
||||
} else {
|
||||
coverUrl = extractCoverUrl(obj);
|
||||
}
|
||||
|
||||
coverUrl = preferCachedCoverUrl(coverUrl);
|
||||
if (!coverUrl.isValid()) {
|
||||
coverUrl = findCoverImage(appName, QStringLiteral("sideload"));
|
||||
}
|
||||
if (coverUrl.isValid()) {
|
||||
game->setCoverUrl(coverUrl);
|
||||
}
|
||||
|
||||
games.append(game);
|
||||
}
|
||||
|
||||
return games;
|
||||
}
|
||||
|
|
@ -436,36 +103,23 @@ QList<Game *> HeroicImporter::importEpicGames()
|
|||
{
|
||||
QList<Game *> games;
|
||||
QString configPath = findHeroicConfigPath();
|
||||
QString legendaryConfigPath = findLegendaryConfigPath();
|
||||
|
||||
if (configPath.isEmpty() && legendaryConfigPath.isEmpty()) {
|
||||
if (configPath.isEmpty()) {
|
||||
return games;
|
||||
}
|
||||
|
||||
// Check for installed.json
|
||||
QStringList installedPaths;
|
||||
if (!legendaryConfigPath.isEmpty()) {
|
||||
installedPaths.append(legendaryConfigPath + QStringLiteral("/installed.json"));
|
||||
}
|
||||
if (!configPath.isEmpty()) {
|
||||
installedPaths.append(configPath + QStringLiteral("/legendaryConfig/legendary/installed.json"));
|
||||
installedPaths.append(configPath + QStringLiteral("/store_cache/legendary_library"));
|
||||
installedPaths.append(configPath + QStringLiteral("/store_cache/legendary_library.json"));
|
||||
installedPaths.append(configPath + QStringLiteral("/store/legendary_library"));
|
||||
installedPaths.append(configPath + QStringLiteral("/store/legendary_library.json"));
|
||||
}
|
||||
QString installedPath = configPath + QStringLiteral("/legendaryConfig/legendary/installed.json");
|
||||
QFile installedFile(installedPath);
|
||||
|
||||
QFile installedFile;
|
||||
for (const QString &path : installedPaths) {
|
||||
installedFile.setFileName(path);
|
||||
if (installedFile.open(QIODevice::ReadOnly)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!installedFile.isOpen()) {
|
||||
if (!installedFile.open(QIODevice::ReadOnly)) {
|
||||
// Try alternative path
|
||||
installedPath = configPath + QStringLiteral("/store_cache/legendary_library.json");
|
||||
installedFile.setFileName(installedPath);
|
||||
if (!installedFile.open(QIODevice::ReadOnly)) {
|
||||
return games;
|
||||
}
|
||||
}
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(installedFile.readAll());
|
||||
|
||||
|
|
@ -488,7 +142,7 @@ QList<Game *> HeroicImporter::importEpicGames()
|
|||
Game *game = new Game(QStringLiteral("heroic-epic-%1").arg(appName), title);
|
||||
game->setPlatform(QStringLiteral("Heroic (Epic)"));
|
||||
game->setPlatformId(appName);
|
||||
game->setLaunchCommand(launchCommandForApp(appName));
|
||||
game->setLaunchCommand(QStringLiteral("heroic --launch %1").arg(appName));
|
||||
game->setWorkingDirectory(installPath);
|
||||
game->setInstalled(isInstalled);
|
||||
|
||||
|
|
@ -498,10 +152,7 @@ QList<Game *> HeroicImporter::importEpicGames()
|
|||
game->setDeveloper(developer);
|
||||
game->setPublisher(publisher);
|
||||
|
||||
QUrl coverUrl = preferCachedCoverUrl(extractCoverUrl(gameObj));
|
||||
if (!coverUrl.isValid()) {
|
||||
coverUrl = findCoverImage(appName, QStringLiteral("epic"));
|
||||
}
|
||||
QUrl coverUrl = findCoverImage(appName, QStringLiteral("epic"));
|
||||
if (coverUrl.isValid()) {
|
||||
game->setCoverUrl(coverUrl);
|
||||
}
|
||||
|
|
@ -529,13 +180,10 @@ QList<Game *> HeroicImporter::importEpicGames()
|
|||
Game *game = new Game(QStringLiteral("heroic-epic-%1").arg(appName), title);
|
||||
game->setPlatform(QStringLiteral("Heroic (Epic)"));
|
||||
game->setPlatformId(appName);
|
||||
game->setLaunchCommand(launchCommandForApp(appName));
|
||||
game->setLaunchCommand(QStringLiteral("heroic --launch %1").arg(appName));
|
||||
game->setInstalled(isInstalled);
|
||||
|
||||
QUrl coverUrl = preferCachedCoverUrl(extractCoverUrl(gameObj));
|
||||
if (!coverUrl.isValid()) {
|
||||
coverUrl = findCoverImage(appName, QStringLiteral("epic"));
|
||||
}
|
||||
QUrl coverUrl = findCoverImage(appName, QStringLiteral("epic"));
|
||||
if (coverUrl.isValid()) {
|
||||
game->setCoverUrl(coverUrl);
|
||||
}
|
||||
|
|
@ -558,26 +206,18 @@ QList<Game *> HeroicImporter::importGOGGames()
|
|||
return games;
|
||||
}
|
||||
|
||||
// Check for GOG installed games / library cache
|
||||
QStringList installedPaths = {
|
||||
configPath + QStringLiteral("/gog_store/installed.json"),
|
||||
configPath + QStringLiteral("/store_cache/gog_library"),
|
||||
configPath + QStringLiteral("/store_cache/gog_library.json"),
|
||||
configPath + QStringLiteral("/store/gog_library"),
|
||||
configPath + QStringLiteral("/store/gog_library.json"),
|
||||
};
|
||||
// Check for GOG installed games
|
||||
QString installedPath = configPath + QStringLiteral("/gog_store/installed.json");
|
||||
QFile installedFile(installedPath);
|
||||
|
||||
QFile installedFile;
|
||||
for (const QString &path : installedPaths) {
|
||||
installedFile.setFileName(path);
|
||||
if (installedFile.open(QIODevice::ReadOnly)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!installedFile.isOpen()) {
|
||||
if (!installedFile.open(QIODevice::ReadOnly)) {
|
||||
// Try library file
|
||||
installedPath = configPath + QStringLiteral("/store_cache/gog_library.json");
|
||||
installedFile.setFileName(installedPath);
|
||||
if (!installedFile.open(QIODevice::ReadOnly)) {
|
||||
return games;
|
||||
}
|
||||
}
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(installedFile.readAll());
|
||||
|
||||
|
|
@ -603,13 +243,10 @@ QList<Game *> HeroicImporter::importGOGGames()
|
|||
Game *game = new Game(QStringLiteral("heroic-gog-%1").arg(appName), title);
|
||||
game->setPlatform(QStringLiteral("Heroic (GOG)"));
|
||||
game->setPlatformId(appName);
|
||||
game->setLaunchCommand(launchCommandForApp(appName));
|
||||
game->setLaunchCommand(QStringLiteral("heroic --launch %1").arg(appName));
|
||||
game->setInstalled(isInstalled);
|
||||
|
||||
QUrl coverUrl = preferCachedCoverUrl(extractCoverUrl(gameObj));
|
||||
if (!coverUrl.isValid()) {
|
||||
coverUrl = findCoverImage(appName, QStringLiteral("gog"));
|
||||
}
|
||||
QUrl coverUrl = findCoverImage(appName, QStringLiteral("gog"));
|
||||
if (coverUrl.isValid()) {
|
||||
game->setCoverUrl(coverUrl);
|
||||
}
|
||||
|
|
@ -639,22 +276,10 @@ QList<Game *> HeroicImporter::importAmazonGames()
|
|||
return games;
|
||||
}
|
||||
|
||||
QStringList libraryPaths = {
|
||||
configPath + QStringLiteral("/store_cache/nile_library"),
|
||||
configPath + QStringLiteral("/store_cache/nile_library.json"),
|
||||
configPath + QStringLiteral("/store/nile_library"),
|
||||
configPath + QStringLiteral("/store/nile_library.json"),
|
||||
};
|
||||
QString libraryPath = configPath + QStringLiteral("/store_cache/nile_library.json");
|
||||
QFile libraryFile(libraryPath);
|
||||
|
||||
QFile libraryFile;
|
||||
for (const QString &path : libraryPaths) {
|
||||
libraryFile.setFileName(path);
|
||||
if (libraryFile.open(QIODevice::ReadOnly)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!libraryFile.isOpen()) {
|
||||
if (!libraryFile.open(QIODevice::ReadOnly)) {
|
||||
return games;
|
||||
}
|
||||
|
||||
|
|
@ -679,13 +304,10 @@ QList<Game *> HeroicImporter::importAmazonGames()
|
|||
Game *game = new Game(QStringLiteral("heroic-amazon-%1").arg(appName), title);
|
||||
game->setPlatform(QStringLiteral("Heroic (Amazon)"));
|
||||
game->setPlatformId(appName);
|
||||
game->setLaunchCommand(launchCommandForApp(appName));
|
||||
game->setLaunchCommand(QStringLiteral("heroic --launch %1").arg(appName));
|
||||
game->setInstalled(isInstalled);
|
||||
|
||||
QUrl coverUrl = preferCachedCoverUrl(extractCoverUrl(gameObj));
|
||||
if (!coverUrl.isValid()) {
|
||||
coverUrl = findCoverImage(appName, QStringLiteral("amazon"));
|
||||
}
|
||||
QUrl coverUrl = findCoverImage(appName, QStringLiteral("amazon"));
|
||||
if (coverUrl.isValid()) {
|
||||
game->setCoverUrl(coverUrl);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,13 +19,8 @@ public:
|
|||
|
||||
private:
|
||||
QString findHeroicConfigPath() const;
|
||||
QString findLegendaryConfigPath() const;
|
||||
QList<Game *> importEpicGames();
|
||||
QList<Game *> importGOGGames();
|
||||
QList<Game *> importAmazonGames();
|
||||
QList<Game *> importSideloadApps();
|
||||
QUrl extractCoverUrl(const QJsonObject &gameObj) const;
|
||||
QUrl preferCachedCoverUrl(const QUrl &coverUrl) const;
|
||||
QString launchCommandForApp(const QString &appName) const;
|
||||
QUrl findCoverImage(const QString &appName, const QString &store) const;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,104 +0,0 @@
|
|||
// 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;
|
||||
}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
// 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
|
|
@ -1,15 +1,12 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QCommandLineParser>
|
||||
#include <QGuiApplication>
|
||||
#include <QIcon>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QQmlContext>
|
||||
#include <QQuickStyle>
|
||||
#include <QTextStream>
|
||||
#include <QTimer>
|
||||
|
||||
#include <KAboutData>
|
||||
#include <KCrash>
|
||||
|
|
@ -19,10 +16,15 @@
|
|||
|
||||
#include "alakarte-version.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[])
|
||||
{
|
||||
qputenv("QT_DESKTOP_FILE_NAME", QByteArray("org.kde.alakarte"));
|
||||
QGuiApplication app(argc, argv);
|
||||
QGuiApplication::setDesktopFileName(QStringLiteral("org.kde.alakarte"));
|
||||
|
||||
|
|
@ -50,56 +52,30 @@ int main(int argc, char *argv[])
|
|||
|
||||
QCommandLineParser 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);
|
||||
aboutData.processCommandLine(&parser);
|
||||
|
||||
KDBusService service(KDBusService::Unique);
|
||||
|
||||
if (qEnvironmentVariableIsEmpty("QT_QUICK_CONTROLS_STYLE")) {
|
||||
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;
|
||||
|
||||
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.loadFromModule("org.kde.alakarte", "Main");
|
||||
|
|
|
|||
|
|
@ -1,145 +0,0 @@
|
|||
#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;
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
#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;
|
||||
};
|
||||
|
|
@ -44,31 +44,3 @@ QString PlatformImporter::expandPath(const QString &path) const
|
|||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool PlatformImporter::hasGameCategory(const QStringList &categories)
|
||||
{
|
||||
static const QStringList gameCategories = {QStringLiteral("Game"),
|
||||
QStringLiteral("ArcadeGame"),
|
||||
QStringLiteral("ActionGame"),
|
||||
QStringLiteral("AdventureGame"),
|
||||
QStringLiteral("BlocksGame"),
|
||||
QStringLiteral("BoardGame"),
|
||||
QStringLiteral("CardGame"),
|
||||
QStringLiteral("KidsGame"),
|
||||
QStringLiteral("LogicGame"),
|
||||
QStringLiteral("RolePlaying"),
|
||||
QStringLiteral("Shooter"),
|
||||
QStringLiteral("Simulation"),
|
||||
QStringLiteral("SportsGame"),
|
||||
QStringLiteral("StrategyGame")};
|
||||
|
||||
for (const QString &category : categories) {
|
||||
for (const QString &cat : gameCategories) {
|
||||
if (category.compare(cat, Qt::CaseInsensitive) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
|
||||
#include <QList>
|
||||
#include <QObject>
|
||||
#include <QStringList>
|
||||
|
||||
#include "game.h"
|
||||
|
||||
|
|
@ -30,5 +29,4 @@ protected:
|
|||
QString findExecutable(const QString &name) const;
|
||||
bool directoryExists(const QString &path) const;
|
||||
QString expandPath(const QString &path) const;
|
||||
static bool hasGameCategory(const QStringList &categories);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,126 +0,0 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,14 +15,9 @@ FocusScope {
|
|||
property bool showPlayButton: true
|
||||
property bool focused: activeFocus
|
||||
|
||||
readonly property bool isTouchDevice: {
|
||||
let w = applicationWindow()
|
||||
if (w && w.isTouchDevice !== undefined) return w.isTouchDevice
|
||||
return Kirigami.Settings.tabletMode || Kirigami.Settings.isMobile
|
||||
}
|
||||
readonly property bool isTouchDevice: Kirigami.Settings.tabletMode || Kirigami.Settings.isMobile
|
||||
readonly property int adaptiveFocusRingWidth: 1
|
||||
readonly property real adaptiveHoverScale: 1.015
|
||||
readonly property real adaptiveFocusScale: 1.03
|
||||
|
||||
readonly property bool useAnimatedCover: App.config.animatedCovers
|
||||
&& game
|
||||
|
|
@ -43,50 +38,24 @@ FocusScope {
|
|||
color: Kirigami.Theme.backgroundColor
|
||||
|
||||
shadow {
|
||||
size: gameCard.focused ? Kirigami.Units.mediumSpacing : (hoverHandler.hovered ? Kirigami.Units.smallSpacing * 1.5 : Kirigami.Units.smallSpacing)
|
||||
color: gameCard.focused ? Qt.rgba(0, 0, 0, 0.34) : (hoverHandler.hovered ? Qt.rgba(0, 0, 0, 0.24) : Qt.rgba(0, 0, 0, 0.16))
|
||||
}
|
||||
|
||||
Behavior on shadow.color {
|
||||
ColorAnimation { duration: Kirigami.Units.shortDuration }
|
||||
size: (hoverHandler.hovered || gameCard.focused) ? Kirigami.Units.mediumSpacing : Kirigami.Units.smallSpacing
|
||||
color: (hoverHandler.hovered || gameCard.focused) ? Qt.rgba(0, 0, 0, 0.30) : Qt.rgba(0, 0, 0, 0.16)
|
||||
}
|
||||
|
||||
border.width: gameCard.focused ? gameCard.adaptiveFocusRingWidth : 0
|
||||
border.color: Kirigami.Theme.highlightColor
|
||||
|
||||
Behavior on border.width {
|
||||
NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic }
|
||||
NumberAnimation { duration: Kirigami.Units.shortDuration }
|
||||
}
|
||||
|
||||
Behavior on shadow.size {
|
||||
NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic }
|
||||
NumberAnimation { duration: Kirigami.Units.shortDuration }
|
||||
}
|
||||
|
||||
y: gameCard.focused ? -Kirigami.Units.smallSpacing : (hoverHandler.hovered ? -Kirigami.Units.smallSpacing * 0.5 : 0)
|
||||
Behavior on y {
|
||||
NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic }
|
||||
}
|
||||
|
||||
scale: gameCard.focused ? gameCard.adaptiveFocusScale : (hoverHandler.hovered ? gameCard.adaptiveHoverScale : 1.0)
|
||||
scale: (hoverHandler.hovered || gameCard.focused) ? gameCard.adaptiveHoverScale : 1.0
|
||||
Behavior on scale {
|
||||
NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Kirigami.Units.mediumSpacing
|
||||
color: "transparent"
|
||||
border.width: 2
|
||||
border.color: Kirigami.Theme.highlightColor
|
||||
opacity: 0.0
|
||||
visible: gameCard.focused
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
running: gameCard.focused
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation { from: 0.10; to: 0.28; duration: 900; easing.type: Easing.InOutQuad }
|
||||
NumberAnimation { from: 0.28; to: 0.12; duration: 900; easing.type: Easing.InOutQuad }
|
||||
}
|
||||
NumberAnimation { duration: Kirigami.Units.shortDuration }
|
||||
}
|
||||
|
||||
// Cover image
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Dialogs
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Effects
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
|
@ -12,33 +11,12 @@ import org.kde.alakarte
|
|||
Kirigami.OverlaySheet {
|
||||
id: detailsSheet
|
||||
|
||||
property var game: null
|
||||
property int lastNonCouchUiMode: Config.Auto
|
||||
property var game
|
||||
|
||||
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: {
|
||||
let w = applicationWindow()
|
||||
if (w && w.isMobile !== undefined) return w.isMobile
|
||||
return Kirigami.Settings.isMobile
|
||||
}
|
||||
readonly property bool isMobile: Kirigami.Settings.isMobile
|
||||
readonly property bool isNarrowScreen: applicationWindow() && applicationWindow().width < Kirigami.Units.gridUnit * 30
|
||||
readonly property bool useCompactLayout: isMobile || isNarrowScreen
|
||||
readonly property bool isTouchDevice: {
|
||||
let w = applicationWindow()
|
||||
if (w && w.isTouchDevice !== undefined) return w.isTouchDevice
|
||||
return Kirigami.Settings.tabletMode || Kirigami.Settings.isMobile
|
||||
}
|
||||
readonly property bool isTouchDevice: Kirigami.Settings.tabletMode || Kirigami.Settings.isMobile
|
||||
|
||||
readonly property int actionIconButtonSize: isTouchDevice ? Math.round(Kirigami.Units.gridUnit * 2.5) : Math.round(Kirigami.Units.gridUnit * 2)
|
||||
|
||||
|
|
@ -59,44 +37,16 @@ Kirigami.OverlaySheet {
|
|||
|
||||
signal launchRequested()
|
||||
signal editRequested()
|
||||
signal diagnosticsRequested()
|
||||
signal removeRequested()
|
||||
|
||||
title: game ? game.name : ""
|
||||
|
||||
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) {
|
||||
let p = item
|
||||
while (p) {
|
||||
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
|
||||
}
|
||||
return false
|
||||
|
|
@ -120,8 +70,6 @@ Kirigami.OverlaySheet {
|
|||
Connections {
|
||||
target: GamepadManager
|
||||
function onNavigateLeft() {
|
||||
let app = applicationWindow()
|
||||
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
|
||||
if (!detailsSheet.opened) return
|
||||
let w = applicationWindow()
|
||||
if (!w || !w.activeFocusItem) return
|
||||
|
|
@ -129,8 +77,6 @@ Kirigami.OverlaySheet {
|
|||
detailsSheet.focusNextInChain(false)
|
||||
}
|
||||
function onNavigateRight() {
|
||||
let app = applicationWindow()
|
||||
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
|
||||
if (!detailsSheet.opened) return
|
||||
let w = applicationWindow()
|
||||
if (!w || !w.activeFocusItem) return
|
||||
|
|
@ -138,8 +84,6 @@ Kirigami.OverlaySheet {
|
|||
detailsSheet.focusNextInChain(true)
|
||||
}
|
||||
function onNavigateUp() {
|
||||
let app = applicationWindow()
|
||||
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
|
||||
if (!detailsSheet.opened) return
|
||||
let w = applicationWindow()
|
||||
if (!w || !w.activeFocusItem) return
|
||||
|
|
@ -147,8 +91,6 @@ Kirigami.OverlaySheet {
|
|||
detailsSheet.focusNextInChain(false)
|
||||
}
|
||||
function onNavigateDown() {
|
||||
let app = applicationWindow()
|
||||
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
|
||||
if (!detailsSheet.opened) return
|
||||
let w = applicationWindow()
|
||||
if (!w || !w.activeFocusItem) return
|
||||
|
|
@ -156,8 +98,6 @@ Kirigami.OverlaySheet {
|
|||
detailsSheet.focusNextInChain(true)
|
||||
}
|
||||
function onSelectPressed() {
|
||||
let app = applicationWindow()
|
||||
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
|
||||
if (!detailsSheet.opened) return
|
||||
let w = applicationWindow()
|
||||
if (!w || !w.activeFocusItem) return
|
||||
|
|
@ -373,12 +313,7 @@ Kirigami.OverlaySheet {
|
|||
|
||||
Item { Layout.fillHeight: true }
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
QQC2.Button {
|
||||
|
|
@ -396,25 +331,77 @@ Kirigami.OverlaySheet {
|
|||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
}
|
||||
|
||||
Flow {
|
||||
Layout.fillWidth: true
|
||||
RowLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
QQC2.Button {
|
||||
icon.name: game && game.favorite ? "bookmark-remove" : "bookmark-new"
|
||||
text: game && game.favorite ? i18n("Unfavorite") : i18n("Favorite")
|
||||
display: detailsSheet.useCompactLayout ? QQC2.AbstractButton.TextUnderIcon : QQC2.AbstractButton.TextBesideIcon
|
||||
enabled: !!game
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
implicitWidth: detailsSheet.actionIconButtonSize
|
||||
implicitHeight: detailsSheet.actionIconButtonSize
|
||||
icon.width: Kirigami.Units.iconSizes.smallMedium
|
||||
icon.height: Kirigami.Units.iconSizes.smallMedium
|
||||
hoverEnabled: true
|
||||
flat: true
|
||||
padding: 0
|
||||
|
||||
background: Rectangle {
|
||||
radius: Kirigami.Units.smallSpacing
|
||||
color: parent.activeFocus ? Kirigami.Theme.highlightColor : Kirigami.Theme.alternateBackgroundColor
|
||||
opacity: parent.activeFocus ? 0.18 : (parent.hovered ? 0.08 : 0.0)
|
||||
border.width: parent.activeFocus ? 1 : 0
|
||||
border.color: Kirigami.Theme.highlightColor
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: Kirigami.Units.shortDuration }
|
||||
}
|
||||
|
||||
Behavior on border.width {
|
||||
NumberAnimation { duration: Kirigami.Units.shortDuration }
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
|
||||
onClicked: if (game) game.favorite = !game.favorite
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
icon.name: game && game.hidden ? "view-visible" : "view-hidden"
|
||||
text: game && game.hidden ? i18n("Unhide") : i18n("Hide")
|
||||
display: detailsSheet.useCompactLayout ? QQC2.AbstractButton.TextUnderIcon : QQC2.AbstractButton.TextBesideIcon
|
||||
enabled: !!game
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
implicitWidth: detailsSheet.actionIconButtonSize
|
||||
implicitHeight: detailsSheet.actionIconButtonSize
|
||||
icon.width: Kirigami.Units.iconSizes.smallMedium
|
||||
icon.height: Kirigami.Units.iconSizes.smallMedium
|
||||
hoverEnabled: true
|
||||
flat: true
|
||||
padding: 0
|
||||
|
||||
background: Rectangle {
|
||||
radius: Kirigami.Units.smallSpacing
|
||||
color: parent.activeFocus ? Kirigami.Theme.highlightColor : Kirigami.Theme.alternateBackgroundColor
|
||||
opacity: parent.activeFocus ? 0.18 : (parent.hovered ? 0.08 : 0.0)
|
||||
border.width: parent.activeFocus ? 1 : 0
|
||||
border.color: Kirigami.Theme.highlightColor
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: Kirigami.Units.shortDuration }
|
||||
}
|
||||
|
||||
Behavior on border.width {
|
||||
NumberAnimation { duration: Kirigami.Units.shortDuration }
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
|
||||
onClicked: if (game) {
|
||||
game.hidden = !game.hidden
|
||||
App.saveLibrary()
|
||||
|
|
@ -424,42 +411,70 @@ Kirigami.OverlaySheet {
|
|||
QQC2.Button {
|
||||
icon.name: "document-edit"
|
||||
text: i18n("Edit")
|
||||
display: detailsSheet.useCompactLayout ? QQC2.AbstractButton.TextUnderIcon : QQC2.AbstractButton.TextBesideIcon
|
||||
enabled: !!game
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
implicitWidth: detailsSheet.actionIconButtonSize
|
||||
implicitHeight: detailsSheet.actionIconButtonSize
|
||||
icon.width: Kirigami.Units.iconSizes.smallMedium
|
||||
icon.height: Kirigami.Units.iconSizes.smallMedium
|
||||
hoverEnabled: true
|
||||
flat: true
|
||||
padding: 0
|
||||
|
||||
background: Rectangle {
|
||||
radius: Kirigami.Units.smallSpacing
|
||||
color: parent.activeFocus ? Kirigami.Theme.highlightColor : Kirigami.Theme.alternateBackgroundColor
|
||||
opacity: parent.activeFocus ? 0.18 : (parent.hovered ? 0.08 : 0.0)
|
||||
border.width: parent.activeFocus ? 1 : 0
|
||||
border.color: Kirigami.Theme.highlightColor
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: Kirigami.Units.shortDuration }
|
||||
}
|
||||
|
||||
Behavior on border.width {
|
||||
NumberAnimation { duration: Kirigami.Units.shortDuration }
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
|
||||
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 {
|
||||
icon.name: "edit-delete"
|
||||
text: i18n("Remove")
|
||||
display: detailsSheet.useCompactLayout ? QQC2.AbstractButton.TextUnderIcon : QQC2.AbstractButton.TextBesideIcon
|
||||
enabled: !!game
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
implicitWidth: detailsSheet.actionIconButtonSize
|
||||
implicitHeight: detailsSheet.actionIconButtonSize
|
||||
icon.width: Kirigami.Units.iconSizes.smallMedium
|
||||
icon.height: Kirigami.Units.iconSizes.smallMedium
|
||||
hoverEnabled: true
|
||||
flat: true
|
||||
padding: 0
|
||||
|
||||
background: Rectangle {
|
||||
radius: Kirigami.Units.smallSpacing
|
||||
color: parent.activeFocus ? Kirigami.Theme.highlightColor : Kirigami.Theme.alternateBackgroundColor
|
||||
opacity: parent.activeFocus ? 0.18 : (parent.hovered ? 0.08 : 0.0)
|
||||
border.width: parent.activeFocus ? 1 : 0
|
||||
border.color: Kirigami.Theme.highlightColor
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: Kirigami.Units.shortDuration }
|
||||
}
|
||||
|
||||
Behavior on border.width {
|
||||
NumberAnimation { duration: Kirigami.Units.shortDuration }
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
|
||||
onClicked: detailsSheet.removeRequested()
|
||||
}
|
||||
}
|
||||
|
|
@ -500,94 +515,6 @@ Kirigami.OverlaySheet {
|
|||
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) {
|
||||
|
|
|
|||
|
|
@ -14,11 +14,6 @@ Kirigami.Dialog {
|
|||
|
||||
property var game: null
|
||||
property bool isEditing: game !== null
|
||||
property string editProfileId: "default"
|
||||
|
||||
ListModel {
|
||||
id: envModel
|
||||
}
|
||||
|
||||
signal gameCreated(var game)
|
||||
signal gameUpdated(var game)
|
||||
|
|
@ -27,42 +22,18 @@ Kirigami.Dialog {
|
|||
standardButtons: Kirigami.Dialog.NoButton
|
||||
|
||||
width: Math.min(parent.width - Kirigami.Units.gridUnit * 4, Kirigami.Units.gridUnit * 30)
|
||||
height: Math.min(parent.height - Kirigami.Units.gridUnit * 4, implicitHeight)
|
||||
|
||||
x: Math.round((parent.width - width) / 2)
|
||||
y: Math.round((parent.height - height) / 2)
|
||||
|
||||
customFooterActions: [
|
||||
Kirigami.Action {
|
||||
text: isEditing ? i18n("Apply") : i18n("Add")
|
||||
icon.name: isEditing ? "dialog-ok-apply" : "list-add"
|
||||
enabled: nameField.text.trim() !== "" && executableField.text.trim() !== "" && (runnerCombo.currentIndex !== 3 || runnerPathField.text.trim() !== "")
|
||||
enabled: nameField.text.trim() !== "" && executableField.text.trim() !== ""
|
||||
onTriggered: {
|
||||
let runnerValue = dialog.runnerFromIndex(runnerCombo.currentIndex)
|
||||
let runnerPathValue = runnerPathField.text.trim()
|
||||
let prefixPathValue = prefixPathField.text.trim()
|
||||
if (runnerValue === "") {
|
||||
runnerPathValue = ""
|
||||
prefixPathValue = ""
|
||||
}
|
||||
if (isEditing) {
|
||||
game.name = nameField.text.trim()
|
||||
game.developer = developerField.text.trim()
|
||||
game.launchCommand = executableField.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 !== "") {
|
||||
App.setCoverFromFile(game, selectedCoverPath)
|
||||
}
|
||||
|
|
@ -73,10 +44,6 @@ Kirigami.Dialog {
|
|||
if (newGame) {
|
||||
newGame.developer = developerField.text.trim()
|
||||
newGame.workingDirectory = workingDirField.text.trim()
|
||||
newGame.launchEnv = dialog.envModelToMap()
|
||||
newGame.launchRunner = runnerValue
|
||||
newGame.launchRunnerPath = runnerPathValue
|
||||
newGame.launchPrefixPath = prefixPathValue
|
||||
if (selectedCoverPath !== "") {
|
||||
App.setCoverFromFile(newGame, selectedCoverPath)
|
||||
}
|
||||
|
|
@ -96,227 +63,6 @@ Kirigami.Dialog {
|
|||
|
||||
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) {
|
||||
let p = item
|
||||
while (p) {
|
||||
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
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function focusNextInChain(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 (dialog.isDescendant(next, dialog)) {
|
||||
next.forceActiveFocus()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function activateFocused() {
|
||||
let w = applicationWindow()
|
||||
if (!w || !w.activeFocusItem) return
|
||||
let item = w.activeFocusItem
|
||||
|
||||
if (typeof item.toggle === "function") {
|
||||
item.toggle()
|
||||
return
|
||||
}
|
||||
if (item.checkable !== undefined && item.checkable && item.checked !== undefined) {
|
||||
item.checked = !item.checked
|
||||
return
|
||||
}
|
||||
if (typeof item.clicked === "function") {
|
||||
item.clicked()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
selectedCoverPath = ""
|
||||
if (isEditing && game) {
|
||||
nameField.text = game.name || ""
|
||||
developerField.text = game.developer || ""
|
||||
executableField.text = game.launchCommand || ""
|
||||
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 {
|
||||
nameField.text = ""
|
||||
developerField.text = ""
|
||||
executableField.text = ""
|
||||
workingDirField.text = ""
|
||||
|
||||
dialog.editProfileId = "default"
|
||||
profileCombo.currentIndex = 0
|
||||
dialog.loadProfileFields()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: GamepadManager
|
||||
function onNavigateUp() {
|
||||
let app = applicationWindow()
|
||||
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
|
||||
if (!dialog.opened) return
|
||||
let w = applicationWindow()
|
||||
if (!w || !w.activeFocusItem) return
|
||||
if (!dialog.isDescendant(w.activeFocusItem, dialog)) return
|
||||
dialog.focusNextInChain(false)
|
||||
}
|
||||
function onNavigateDown() {
|
||||
let app = applicationWindow()
|
||||
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
|
||||
if (!dialog.opened) return
|
||||
let w = applicationWindow()
|
||||
if (!w || !w.activeFocusItem) return
|
||||
if (!dialog.isDescendant(w.activeFocusItem, dialog)) return
|
||||
dialog.focusNextInChain(true)
|
||||
}
|
||||
function onNavigateLeft() {
|
||||
let app = applicationWindow()
|
||||
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
|
||||
if (!dialog.opened) return
|
||||
let w = applicationWindow()
|
||||
if (!w || !w.activeFocusItem) return
|
||||
if (!dialog.isDescendant(w.activeFocusItem, dialog)) return
|
||||
dialog.focusNextInChain(false)
|
||||
}
|
||||
function onNavigateRight() {
|
||||
let app = applicationWindow()
|
||||
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
|
||||
if (!dialog.opened) return
|
||||
let w = applicationWindow()
|
||||
if (!w || !w.activeFocusItem) return
|
||||
if (!dialog.isDescendant(w.activeFocusItem, dialog)) return
|
||||
dialog.focusNextInChain(true)
|
||||
}
|
||||
function onSelectPressed() {
|
||||
let app = applicationWindow()
|
||||
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
|
||||
if (!dialog.opened) return
|
||||
let w = applicationWindow()
|
||||
if (!w || !w.activeFocusItem) return
|
||||
if (!dialog.isDescendant(w.activeFocusItem, dialog)) return
|
||||
dialog.activateFocused()
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
|
|
@ -326,7 +72,7 @@ Kirigami.Dialog {
|
|||
FormCard.FormTextFieldDelegate {
|
||||
id: nameField
|
||||
label: i18n("Name")
|
||||
text: ""
|
||||
text: isEditing && game ? game.name : ""
|
||||
placeholderText: i18n("Game title")
|
||||
onAccepted: developerField.forceActiveFocus()
|
||||
}
|
||||
|
|
@ -336,7 +82,7 @@ Kirigami.Dialog {
|
|||
FormCard.FormTextFieldDelegate {
|
||||
id: developerField
|
||||
label: i18n("Developer")
|
||||
text: ""
|
||||
text: isEditing && game ? (game.developer || "") : ""
|
||||
placeholderText: i18n("Optional")
|
||||
onAccepted: executableField.forceActiveFocus()
|
||||
}
|
||||
|
|
@ -346,7 +92,7 @@ Kirigami.Dialog {
|
|||
FormCard.FormTextFieldDelegate {
|
||||
id: executableField
|
||||
label: i18n("Executable")
|
||||
text: ""
|
||||
text: isEditing && game ? game.launchCommand : ""
|
||||
placeholderText: i18n("/path/to/game or command")
|
||||
onAccepted: if (nameField.text.trim() !== "" && text.trim() !== "") {
|
||||
dialog.customFooterActions[0].trigger()
|
||||
|
|
@ -366,200 +112,9 @@ Kirigami.Dialog {
|
|||
FormCard.FormTextFieldDelegate {
|
||||
id: workingDirField
|
||||
label: i18n("Working Directory")
|
||||
text: ""
|
||||
text: isEditing && game ? (game.workingDirectory || "") : ""
|
||||
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 {
|
||||
|
|
@ -684,23 +239,6 @@ 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) {
|
||||
if (selectedCoverPath === "") return
|
||||
|
||||
|
|
@ -714,9 +252,7 @@ Kirigami.Dialog {
|
|||
}
|
||||
|
||||
onOpened: {
|
||||
loadFields()
|
||||
nameField.forceActiveFocus()
|
||||
selectedCoverPath = ""
|
||||
}
|
||||
|
||||
onGameChanged: loadFields()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Effects
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.alakarte
|
||||
import "components"
|
||||
|
|
@ -14,7 +13,7 @@ FocusScope {
|
|||
|
||||
property string filterSource: "all"
|
||||
property bool searchActive: false
|
||||
property int focusedIndex: -1
|
||||
property int focusedIndex: 0
|
||||
|
||||
property int adaptiveCardSize: App.config.gridSize
|
||||
property bool isTouchDevice: false
|
||||
|
|
@ -22,70 +21,20 @@ FocusScope {
|
|||
signal gameSelected(var game)
|
||||
signal gameLaunched(var game)
|
||||
|
||||
readonly property int gameCount: proxyModel.count
|
||||
property url focusedCoverUrl: ""
|
||||
|
||||
readonly property bool anyMenuOpen: searchHeader.anyMenuOpen
|
||||
|
||||
function closeCurrentMenu() {
|
||||
searchHeader.closeCurrentMenu()
|
||||
}
|
||||
|
||||
function focusSearch() {
|
||||
searchField.forceActiveFocus()
|
||||
}
|
||||
|
||||
function clearSearch() {
|
||||
searchField.text = ""
|
||||
proxyModel.filterText = ""
|
||||
}
|
||||
|
||||
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) {
|
||||
libraryRoot.focusSearch()
|
||||
} else {
|
||||
if (libraryRoot.focusedIndex >= 0 && libraryRoot.focusedIndex < proxyModel.count) {
|
||||
gameGrid.currentIndex = libraryRoot.focusedIndex
|
||||
}
|
||||
gameGrid.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
function launchFocusedGame() {
|
||||
if (!gameGrid || !proxyModel) return
|
||||
if (gameGrid.currentIndex < 0 && proxyModel.count > 0) {
|
||||
gameGrid.currentIndex = 0
|
||||
libraryRoot.focusedIndex = 0
|
||||
}
|
||||
let game = proxyModel.get(gameGrid.currentIndex)
|
||||
if (game) {
|
||||
libraryRoot.gameLaunched(game)
|
||||
}
|
||||
}
|
||||
|
||||
function openDetailsForFocusedGame() {
|
||||
if (!gameGrid || !proxyModel) return
|
||||
if (gameGrid.currentIndex < 0 && proxyModel.count > 0) {
|
||||
gameGrid.currentIndex = 0
|
||||
libraryRoot.focusedIndex = 0
|
||||
}
|
||||
let game = proxyModel.get(gameGrid.currentIndex)
|
||||
if (game) {
|
||||
libraryRoot.gameSelected(game)
|
||||
}
|
||||
}
|
||||
|
||||
onSearchActiveChanged: {
|
||||
if (!libraryRoot.searchActive) {
|
||||
libraryRoot.clearSearch()
|
||||
Qt.callLater(function() {
|
||||
gameGrid.forceActiveFocus()
|
||||
})
|
||||
|
|
@ -96,62 +45,6 @@ FocusScope {
|
|||
anchors.fill: parent
|
||||
anchors.margins: 0
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
visible: libraryRoot.gameCount > 0
|
||||
|
||||
Image {
|
||||
id: backgroundCoverA
|
||||
anchors.fill: parent
|
||||
source: ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
visible: source.toString().length > 0
|
||||
smooth: true
|
||||
mipmap: App.config.highQualityImages
|
||||
opacity: 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic }
|
||||
}
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
blurEnabled: true
|
||||
blur: 0.9
|
||||
blurMax: 64
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
id: backgroundCoverB
|
||||
anchors.fill: parent
|
||||
source: ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
visible: source.toString().length > 0
|
||||
smooth: true
|
||||
mipmap: App.config.highQualityImages
|
||||
opacity: 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic }
|
||||
}
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
blurEnabled: true
|
||||
blur: 0.9
|
||||
blurMax: 64
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(0, 0, 0, 0.55)
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
|
@ -190,16 +83,6 @@ FocusScope {
|
|||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -210,27 +93,6 @@ FocusScope {
|
|||
|
||||
cardSize: libraryRoot.adaptiveCardSize
|
||||
|
||||
onCurrentIndexChanged: {
|
||||
if (gameGrid.activeFocus) {
|
||||
libraryRoot.focusedIndex = currentIndex
|
||||
}
|
||||
let game = proxyModel.get(currentIndex)
|
||||
let url = (game && game.coverUrl) ? game.coverUrl : ""
|
||||
if (url === libraryRoot.focusedCoverUrl) return
|
||||
if (backgroundCoverA.opacity > 0.1) {
|
||||
backgroundCoverB.source = url
|
||||
backgroundCoverB.opacity = 0.0
|
||||
backgroundCoverA.opacity = 0.0
|
||||
Qt.callLater(function() { backgroundCoverB.opacity = 0.22 })
|
||||
} else {
|
||||
backgroundCoverA.source = url
|
||||
backgroundCoverA.opacity = 0.0
|
||||
backgroundCoverB.opacity = 0.0
|
||||
Qt.callLater(function() { backgroundCoverA.opacity = 0.22 })
|
||||
}
|
||||
libraryRoot.focusedCoverUrl = url
|
||||
}
|
||||
|
||||
model: GameSortFilterModel {
|
||||
id: proxyModel
|
||||
sourceModel: App.gameModel
|
||||
|
|
@ -249,17 +111,9 @@ FocusScope {
|
|||
height: gameGrid.cellHeight
|
||||
|
||||
function clicked() {
|
||||
gameGrid.currentIndex = index
|
||||
libraryRoot.focusedIndex = index
|
||||
card.clicked()
|
||||
}
|
||||
|
||||
function play() {
|
||||
gameGrid.currentIndex = index
|
||||
libraryRoot.focusedIndex = index
|
||||
card.playClicked()
|
||||
}
|
||||
|
||||
GameCard {
|
||||
id: card
|
||||
width: gameGrid.cardSize
|
||||
|
|
@ -281,20 +135,12 @@ FocusScope {
|
|||
|
||||
Keys.onPressed: function(event) {
|
||||
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)
|
||||
if (game) {
|
||||
libraryRoot.gameSelected(game)
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Space) {
|
||||
if (currentIndex < 0 && proxyModel.count > 0) {
|
||||
currentIndex = 0
|
||||
libraryRoot.focusedIndex = 0
|
||||
}
|
||||
let game = proxyModel.get(currentIndex)
|
||||
if (game) {
|
||||
libraryRoot.gameLaunched(game)
|
||||
|
|
@ -307,13 +153,14 @@ FocusScope {
|
|||
anchors.centerIn: parent
|
||||
visible: proxyModel.count === 0 && !App.importing
|
||||
|
||||
icon: proxyModel.filterText.length > 0 ? "edit-find" : (libraryRoot.filterSource === "favorites" ? "bookmark-new" : (libraryRoot.filterSource === "hidden" ? "view-hidden" : "applications-games"))
|
||||
icon: proxyModel.filterText.length > 0 ? "edit-find" : "applications-games"
|
||||
title: proxyModel.filterText.length > 0 ?
|
||||
i18n("No games found") : (libraryRoot.filterSource === "favorites" ? i18n("No favorites yet") : (libraryRoot.filterSource === "hidden" ? i18n("No hidden games") : i18n("Your library is empty")))
|
||||
i18n("No games found") : i18n("Your library is empty")
|
||||
description: proxyModel.filterText.length > 0 ?
|
||||
i18n("Try adjusting your search") : (libraryRoot.filterSource === "favorites" ? i18n("Mark games as favorites to see them here") : (libraryRoot.filterSource === "hidden" ? i18n("Hidden games will appear here") : i18n("Import games to get started")))
|
||||
i18n("Try adjusting your search") :
|
||||
i18n("Import games to get started")
|
||||
|
||||
actionText: (proxyModel.filterText.length > 0 || libraryRoot.filterSource === "favorites" || libraryRoot.filterSource === "hidden") ? "" : i18n("Import Games")
|
||||
actionText: proxyModel.filterText.length > 0 ? "" : i18n("Import Games")
|
||||
onActionTriggered: App.importAllGames()
|
||||
}
|
||||
|
||||
|
|
@ -323,6 +170,47 @@ FocusScope {
|
|||
visible: App.importing
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.ToolBar {
|
||||
Layout.fillWidth: true
|
||||
visible: proxyModel.count > 0
|
||||
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
topPadding: Kirigami.Units.smallSpacing
|
||||
bottomPadding: Kirigami.Units.smallSpacing
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: Kirigami.Units.mediumSpacing
|
||||
|
||||
QQC2.Label {
|
||||
text: i18np("%1 game", "%1 games", proxyModel.count)
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
font.pointSize: Kirigami.Theme.smallFont.pointSize
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
QQC2.Slider {
|
||||
id: sizeSlider
|
||||
from: 120
|
||||
to: 280
|
||||
stepSize: 20
|
||||
value: App.config.gridSize
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 8
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
onMoved: App.config.gridSize = value
|
||||
|
||||
QQC2.ToolTip {
|
||||
parent: sizeSlider.handle
|
||||
visible: sizeSlider.pressed
|
||||
text: Math.round(sizeSlider.value) + " px"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1401
src/qml/Main.qml
|
|
@ -17,105 +17,6 @@ ColumnLayout {
|
|||
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 {
|
||||
Layout.fillWidth: true
|
||||
title: i18n("Appearance")
|
||||
|
|
@ -130,94 +31,9 @@ ColumnLayout {
|
|||
checked: App.config.showPlatformBadges
|
||||
onToggled: App.config.showPlatformBadges = checked
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
id: uiModeDelegate
|
||||
text: i18n("UI mode")
|
||||
description: {
|
||||
if (App.config.uiMode === Config.Desktop) return i18n("Desktop")
|
||||
if (App.config.uiMode === Config.Couch) return i18n("Couch")
|
||||
return i18n("Automatic")
|
||||
}
|
||||
icon.name: "view-fullscreen"
|
||||
onClicked: uiModeMenu.open()
|
||||
|
||||
QQC2.Menu {
|
||||
id: uiModeMenu
|
||||
focus: true
|
||||
|
||||
onOpened: Qt.callLater(function() { uiModeAuto.forceActiveFocus() })
|
||||
|
||||
QQC2.MenuItem {
|
||||
id: uiModeAuto
|
||||
text: i18n("Automatic")
|
||||
checkable: true
|
||||
checked: App.config.uiMode === Config.Auto
|
||||
onTriggered: App.config.uiMode = Config.Auto
|
||||
}
|
||||
|
||||
QQC2.MenuItem {
|
||||
id: uiModeDesktop
|
||||
text: i18n("Desktop")
|
||||
checkable: true
|
||||
checked: App.config.uiMode === Config.Desktop
|
||||
onTriggered: App.config.uiMode = Config.Desktop
|
||||
}
|
||||
|
||||
QQC2.MenuItem {
|
||||
id: uiModeCouch
|
||||
text: i18n("Couch")
|
||||
checkable: true
|
||||
checked: App.config.uiMode === Config.Couch
|
||||
onTriggered: App.config.uiMode = Config.Couch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: GamepadManager
|
||||
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")
|
||||
}
|
||||
|
|
@ -232,26 +48,8 @@ FormCard.FormHeader {
|
|||
secondary: "steam"
|
||||
resourceFallback: Qt.resolvedUrl("icons/brand/steam-symbolic.svg")
|
||||
}
|
||||
property bool restoring: false
|
||||
checked: App.config.importSteam
|
||||
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
|
||||
}
|
||||
onToggled: App.config.importSteam = checked
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
|
@ -263,26 +61,8 @@ FormCard.FormHeader {
|
|||
primary: "lutris"
|
||||
secondary: "applications-games"
|
||||
}
|
||||
property bool restoring: false
|
||||
checked: App.config.importLutris
|
||||
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
|
||||
}
|
||||
onToggled: App.config.importLutris = checked
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
|
@ -295,26 +75,8 @@ FormCard.FormHeader {
|
|||
primary: "com.heroicgameslauncher.hgl"
|
||||
secondary: "applications-games"
|
||||
}
|
||||
property bool restoring: false
|
||||
checked: App.config.importHeroic
|
||||
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
|
||||
}
|
||||
onToggled: App.config.importHeroic = checked
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
|
@ -327,26 +89,8 @@ FormCard.FormHeader {
|
|||
primary: "user-desktop"
|
||||
secondary: "computer"
|
||||
}
|
||||
property bool restoring: false
|
||||
checked: App.config.importDesktop
|
||||
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
|
||||
}
|
||||
onToggled: App.config.importDesktop = checked
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
|
@ -359,26 +103,8 @@ FormCard.FormHeader {
|
|||
primary: "com.usebottles.bottles"
|
||||
secondary: "application-x-executable"
|
||||
}
|
||||
property bool restoring: false
|
||||
checked: App.config.importBottles
|
||||
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
|
||||
}
|
||||
onToggled: App.config.importBottles = checked
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
|
@ -391,26 +117,8 @@ FormCard.FormHeader {
|
|||
primary: "flatpak-discover"
|
||||
secondary: "applications-games"
|
||||
}
|
||||
property bool restoring: false
|
||||
checked: App.config.importFlatpak
|
||||
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
|
||||
}
|
||||
onToggled: App.config.importFlatpak = checked
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
|
@ -424,26 +132,8 @@ FormCard.FormHeader {
|
|||
secondary: "itch"
|
||||
resourceFallback: Qt.resolvedUrl("icons/brand/itchdotio-symbolic.svg")
|
||||
}
|
||||
property bool restoring: false
|
||||
checked: App.config.importItch
|
||||
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
|
||||
}
|
||||
onToggled: App.config.importItch = checked
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
|
@ -456,26 +146,8 @@ FormCard.FormHeader {
|
|||
primary: "legendary"
|
||||
secondary: "applications-games"
|
||||
}
|
||||
property bool restoring: false
|
||||
checked: App.config.importLegendary
|
||||
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
|
||||
}
|
||||
onToggled: App.config.importLegendary = checked
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
|
@ -489,26 +161,8 @@ FormCard.FormHeader {
|
|||
secondary: "retroarch"
|
||||
resourceFallback: Qt.resolvedUrl("icons/brand/retroarch-symbolic.svg")
|
||||
}
|
||||
property bool restoring: false
|
||||
checked: App.config.importRetroArch
|
||||
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
|
||||
}
|
||||
onToggled: App.config.importRetroArch = checked
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
|
@ -522,7 +176,6 @@ FormCard.FormHeader {
|
|||
}
|
||||
|
||||
FormCard.FormHeader {
|
||||
Layout.topMargin: Kirigami.Units.mediumSpacing
|
||||
Layout.fillWidth: true
|
||||
title: i18n("Behavior")
|
||||
}
|
||||
|
|
@ -547,7 +200,6 @@ FormCard.FormHeader {
|
|||
}
|
||||
|
||||
FormCard.FormHeader {
|
||||
Layout.topMargin: Kirigami.Units.mediumSpacing
|
||||
Layout.fillWidth: true
|
||||
title: i18n("Cover Art")
|
||||
}
|
||||
|
|
@ -572,7 +224,6 @@ FormCard.FormHeader {
|
|||
}
|
||||
|
||||
FormCard.FormHeader {
|
||||
Layout.topMargin: Kirigami.Units.mediumSpacing
|
||||
Layout.fillWidth: true
|
||||
title: i18n("SteamGridDB")
|
||||
}
|
||||
|
|
@ -615,7 +266,7 @@ FormCard.FormHeader {
|
|||
description: App.steamGridDB.busy ? i18n("Fetching...") : i18n("Download covers for all games")
|
||||
icon.name: "download"
|
||||
enabled: App.steamGridDB.enabled && App.steamGridDB.apiKey.length > 0 && !App.steamGridDB.busy
|
||||
onClicked: fetchAllCoversConfirmDialog.open()
|
||||
onClicked: App.steamGridDB.fetchAllCovers()
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
|
@ -632,7 +283,6 @@ FormCard.FormHeader {
|
|||
}
|
||||
|
||||
FormCard.FormHeader {
|
||||
Layout.topMargin: Kirigami.Units.mediumSpacing
|
||||
Layout.fillWidth: true
|
||||
title: i18n("Library")
|
||||
}
|
||||
|
|
@ -643,15 +293,7 @@ FormCard.FormHeader {
|
|||
text: i18n("Import All Games")
|
||||
description: i18n("Scan all enabled sources")
|
||||
icon.name: "document-import"
|
||||
enabled: !App.importing && App.gameModel.count >= 0 && (App.config.importSteam
|
||||
|| App.config.importLutris
|
||||
|| App.config.importHeroic
|
||||
|| App.config.importDesktop
|
||||
|| App.config.importBottles
|
||||
|| App.config.importFlatpak
|
||||
|| App.config.importItch
|
||||
|| App.config.importLegendary
|
||||
|| App.config.importRetroArch)
|
||||
enabled: !App.importing
|
||||
onClicked: App.importAllGames()
|
||||
}
|
||||
|
||||
|
|
@ -661,7 +303,7 @@ FormCard.FormHeader {
|
|||
text: i18n("Remove Missing Games")
|
||||
description: i18n("Remove games whose executables no longer exist")
|
||||
icon.name: "edit-delete"
|
||||
onClicked: removeMissingConfirmDialog.open()
|
||||
onClicked: App.removeMissingGames()
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
|
@ -683,51 +325,11 @@ FormCard.FormHeader {
|
|||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
id: clearConfirmDialog
|
||||
title: i18n("Clear Library")
|
||||
subtitle: i18n("Are you sure you want to remove all games?")
|
||||
standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No
|
||||
onOpened: settingsPage.focusNoButton(clearConfirmDialog)
|
||||
onAccepted: App.clearLibrary()
|
||||
}
|
||||
|
||||
|
|
@ -736,7 +338,6 @@ FormCard.FormHeader {
|
|||
title: i18n("Reset Application")
|
||||
subtitle: i18n("This will remove all games and reset all settings to defaults. Are you sure?")
|
||||
standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No
|
||||
onOpened: settingsPage.focusNoButton(resetConfirmDialog)
|
||||
onAccepted: {
|
||||
App.clearLibrary()
|
||||
App.config.resetToDefaults()
|
||||
|
|
|
|||
|
|
@ -14,9 +14,6 @@ ColumnLayout {
|
|||
|
||||
property string currentSource: "all"
|
||||
property string currentSourceName: i18n("All Games")
|
||||
property bool suppressAutoClose: false
|
||||
|
||||
readonly property bool hasSidebarFocus: sourceList.activeFocus || importAction.activeFocus || settingsAction.activeFocus || aboutAction.activeFocus
|
||||
|
||||
readonly property int adaptiveFocusRingWidth: 1
|
||||
|
||||
|
|
@ -36,39 +33,6 @@ ColumnLayout {
|
|||
}
|
||||
}
|
||||
|
||||
function applySourceAtIndex(i) {
|
||||
if (i < 0 || i >= sourceModel.count) return
|
||||
let item = sourceModel.get(i)
|
||||
if (!item) return
|
||||
sourceList.currentIndex = i
|
||||
sidebarRoot.currentSource = item.sourceId
|
||||
sidebarRoot.currentSourceName = item.name
|
||||
sidebarRoot.sourceSelected(item.sourceId)
|
||||
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) {
|
||||
if (sourceModel.count <= 0) return
|
||||
let i = sourceList.currentIndex
|
||||
if (i < 0) i = 0
|
||||
i = (i + delta) % sourceModel.count
|
||||
if (i < 0) i += sourceModel.count
|
||||
sidebarRoot.suppressAutoClose = true
|
||||
applySourceAtIndex(i)
|
||||
Qt.callLater(function() { sidebarRoot.suppressAutoClose = false })
|
||||
}
|
||||
|
||||
function iconInfoForPlatform(platformName) {
|
||||
let p = (platformName || "").toLowerCase()
|
||||
|
||||
|
|
@ -110,31 +74,9 @@ ColumnLayout {
|
|||
|
||||
Connections {
|
||||
target: GamepadManager
|
||||
function onNavigateUp() {
|
||||
let w = applicationWindow()
|
||||
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()
|
||||
}
|
||||
function onNavigateUp() { if (sourceList.activeFocus) sourceList.decrementCurrentIndex() }
|
||||
function onNavigateDown() { if (sourceList.activeFocus) sourceList.incrementCurrentIndex() }
|
||||
function onSelectPressed() { if (sourceList.activeFocus) sidebarRoot.activateCurrentItem() }
|
||||
}
|
||||
|
||||
delegate: QQC2.ItemDelegate {
|
||||
|
|
@ -151,21 +93,6 @@ ColumnLayout {
|
|||
border.width: (sourceList.activeFocus && ListView.isCurrentItem) ? sidebarRoot.adaptiveFocusRingWidth : 0
|
||||
border.color: Kirigami.Theme.highlightColor
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: Kirigami.Units.smallSpacing
|
||||
width: Kirigami.Units.smallSpacing
|
||||
radius: width / 2
|
||||
color: Kirigami.Theme.highlightColor
|
||||
opacity: sourceDelegate.highlighted ? 0.9 : (sourceDelegate.hovered ? 0.45 : 0.0)
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: Kirigami.Units.shortDuration }
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: Kirigami.Units.shortDuration }
|
||||
}
|
||||
|
|
@ -329,8 +256,6 @@ ColumnLayout {
|
|||
Connections {
|
||||
target: GamepadManager
|
||||
function onNavigateUp() {
|
||||
let w = applicationWindow()
|
||||
if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return
|
||||
if (aboutAction.activeFocus) {
|
||||
settingsAction.forceActiveFocus()
|
||||
} else if (settingsAction.activeFocus) {
|
||||
|
|
@ -341,20 +266,13 @@ ColumnLayout {
|
|||
}
|
||||
}
|
||||
function onNavigateDown() {
|
||||
let w = applicationWindow()
|
||||
if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return
|
||||
if (importAction.activeFocus) {
|
||||
settingsAction.forceActiveFocus()
|
||||
} else if (settingsAction.activeFocus) {
|
||||
aboutAction.forceActiveFocus()
|
||||
} else if (aboutAction.activeFocus) {
|
||||
sourceList.forceActiveFocus()
|
||||
sourceList.currentIndex = 0
|
||||
}
|
||||
}
|
||||
function onSelectPressed() {
|
||||
let w = applicationWindow()
|
||||
if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return
|
||||
if (importAction.activeFocus) {
|
||||
importAction.clicked()
|
||||
} else if (settingsAction.activeFocus) {
|
||||
|
|
|
|||
|
|
@ -1,380 +0,0 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.alakarte
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
spacing: uiMode === Config.Couch ? Kirigami.Units.largeSpacing * 1.25 : Kirigami.Units.largeSpacing
|
||||
|
||||
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
|
||||
property string context: "library"
|
||||
|
||||
function iconBasePath() {
|
||||
if (!useGamepadHints) return ""
|
||||
switch (style) {
|
||||
case GamepadManager.PlayStationController:
|
||||
return "qrc:/qt/qml/org/kde/alakarte/qml/icons/gamepad/playstation/"
|
||||
case GamepadManager.XboxController:
|
||||
return "qrc:/qt/qml/org/kde/alakarte/qml/icons/gamepad/xbox/"
|
||||
case GamepadManager.NintendoController:
|
||||
return "qrc:/qt/qml/org/kde/alakarte/qml/icons/gamepad/nintendo/"
|
||||
default:
|
||||
return "qrc:/qt/qml/org/kde/alakarte/qml/icons/gamepad/generic/"
|
||||
}
|
||||
}
|
||||
|
||||
function iconForButton(buttonLabel) {
|
||||
if (!useGamepadHints) return ""
|
||||
const base = iconBasePath()
|
||||
if (style === GamepadManager.PlayStationController) {
|
||||
if (buttonLabel === "Cross") return base + "cross.svg"
|
||||
if (buttonLabel === "Circle") return base + "circle.svg"
|
||||
if (buttonLabel === "Square") return base + "square.svg"
|
||||
if (buttonLabel === "Triangle") return base + "triangle.svg"
|
||||
}
|
||||
if (style === GamepadManager.XboxController || style === GamepadManager.NintendoController) {
|
||||
return base + buttonLabel.toLowerCase() + ".svg"
|
||||
}
|
||||
if (buttonLabel === "A") return base + "south.svg"
|
||||
if (buttonLabel === "B") return base + "east.svg"
|
||||
if (buttonLabel === "X") return base + "west.svg"
|
||||
if (buttonLabel === "Y") return base + "north.svg"
|
||||
return base + "south.svg"
|
||||
}
|
||||
|
||||
function iconForAux(action) {
|
||||
if (!useGamepadHints) return ""
|
||||
|
||||
if (action === "dpad") {
|
||||
return "qrc:/qt/qml/org/kde/alakarte/qml/icons/gamepad/generic/dpad.svg"
|
||||
}
|
||||
if (action === "lb") {
|
||||
return "qrc:/qt/qml/org/kde/alakarte/qml/icons/gamepad/generic/lb.svg"
|
||||
}
|
||||
if (action === "rb") {
|
||||
return "qrc:/qt/qml/org/kde/alakarte/qml/icons/gamepad/generic/rb.svg"
|
||||
}
|
||||
if (action === "menu") {
|
||||
if (style === GamepadManager.PlayStationController) {
|
||||
return "qrc:/qt/qml/org/kde/alakarte/qml/icons/gamepad/playstation/options.svg"
|
||||
}
|
||||
if (style === GamepadManager.NintendoController) {
|
||||
return "qrc:/qt/qml/org/kde/alakarte/qml/icons/gamepad/nintendo/plus.svg"
|
||||
}
|
||||
if (style === GamepadManager.XboxController) {
|
||||
return "qrc:/qt/qml/org/kde/alakarte/qml/icons/gamepad/xbox/menu.svg"
|
||||
}
|
||||
return "qrc:/qt/qml/org/kde/alakarte/qml/icons/gamepad/generic/menu.svg"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
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") {
|
||||
switch (action) {
|
||||
case "navigate": return i18n("Arrows")
|
||||
case "confirm": return "Space"
|
||||
case "back": return ""
|
||||
case "details": return "Enter"
|
||||
case "search": return "Ctrl+F"
|
||||
case "lb": return "Ctrl+PgUp"
|
||||
case "rb": return "Ctrl+PgDown"
|
||||
case "menu": return "Ctrl+,"
|
||||
default: return ""
|
||||
}
|
||||
}
|
||||
|
||||
if (root.context === "edit") {
|
||||
switch (action) {
|
||||
case "navigate": return "Tab"
|
||||
case "confirm": return "Enter"
|
||||
case "back": return "Esc"
|
||||
default: return ""
|
||||
}
|
||||
}
|
||||
|
||||
if (root.context === "details") {
|
||||
switch (action) {
|
||||
case "navigate": return "Tab"
|
||||
case "confirm": return "Enter"
|
||||
case "back": return "Esc"
|
||||
case "details": return "F"
|
||||
case "search": return "E"
|
||||
case "menu": return "Ctrl+,"
|
||||
default: return ""
|
||||
}
|
||||
}
|
||||
|
||||
if (root.context === "sidebar") {
|
||||
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 "back": return "Esc"
|
||||
case "menu": return "Ctrl+,"
|
||||
default: return ""
|
||||
}
|
||||
}
|
||||
|
||||
if (root.context === "settings" || root.context === "import" || root.context === "sidebar" || root.context === "about" || root.context === "diagnostics") {
|
||||
switch (action) {
|
||||
case "navigate": return "Tab"
|
||||
case "confirm": return "Enter"
|
||||
case "back": return "Esc"
|
||||
default: return ""
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
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") {
|
||||
switch (action) {
|
||||
case "navigate": return (useGamepadHints || keyboardLabel("navigate") !== "") ? i18n("Navigate") : ""
|
||||
case "confirm": return i18n("Play")
|
||||
case "back": return ""
|
||||
case "details": return i18n("Details")
|
||||
case "search": return i18n("Search")
|
||||
case "lb": return i18n("Prev Source")
|
||||
case "rb": return i18n("Next Source")
|
||||
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 ""
|
||||
}
|
||||
}
|
||||
|
||||
if (root.context === "edit") {
|
||||
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 ""
|
||||
}
|
||||
}
|
||||
|
||||
if (root.context === "details") {
|
||||
switch (action) {
|
||||
case "navigate": return (useGamepadHints || keyboardLabel("navigate") !== "") ? i18n("Navigate") : ""
|
||||
case "confirm": return i18n("Select")
|
||||
case "back": return i18n("Back")
|
||||
case "details": return i18n("Favorite")
|
||||
case "search": return i18n("Edit")
|
||||
case "menu": return (useGamepadHints && uiMode === Config.Couch) ? i18n("Menu") : i18n("Settings")
|
||||
default: return ""
|
||||
}
|
||||
}
|
||||
|
||||
if (root.context === "sidebar") {
|
||||
switch (action) {
|
||||
case "navigate": return (useGamepadHints || keyboardLabel("navigate") !== "") ? i18n("Navigate") : ""
|
||||
case "confirm": return i18n("Select")
|
||||
case "back": return i18n("Back")
|
||||
case "lb": return i18n("Prev Source")
|
||||
case "rb": return i18n("Next Source")
|
||||
case "menu": return (useGamepadHints && uiMode === Config.Couch) ? i18n("Close") : i18n("Settings")
|
||||
default: return ""
|
||||
}
|
||||
}
|
||||
|
||||
if (root.context === "settings" || root.context === "import" || root.context === "sidebar") {
|
||||
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 ""
|
||||
}
|
||||
}
|
||||
|
||||
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 ""
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
component HintItem: RowLayout {
|
||||
required property string action
|
||||
property string label: ""
|
||||
property string iconSource: ""
|
||||
property string keyLabel: ""
|
||||
|
||||
visible: {
|
||||
if (root.useGamepadHints) return iconSource != "" && label.length > 0
|
||||
return keyLabel.length > 0 && label.length > 0
|
||||
}
|
||||
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Rectangle {
|
||||
id: buttonFrame
|
||||
radius: Kirigami.Units.smallSpacing
|
||||
color: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.08)
|
||||
border.width: 1
|
||||
border.color: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.2)
|
||||
|
||||
Layout.preferredHeight: uiMode === Config.Couch ? Kirigami.Units.gridUnit * 1.8 : Kirigami.Units.gridUnit * 1.4
|
||||
Layout.preferredWidth: useGamepadHints
|
||||
? Layout.preferredHeight
|
||||
: Math.max(keyText.implicitWidth + Kirigami.Units.mediumSpacing * 2, Layout.preferredHeight)
|
||||
|
||||
Image {
|
||||
id: buttonIcon
|
||||
anchors.centerIn: parent
|
||||
width: parent.height * 0.7
|
||||
height: width
|
||||
source: parent.parent.iconSource
|
||||
visible: useGamepadHints && source != ""
|
||||
sourceSize: Qt.size(width * 2, height * 2)
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
id: keyText
|
||||
anchors.centerIn: parent
|
||||
text: parent.parent.keyLabel
|
||||
font.bold: true
|
||||
font.pointSize: uiMode === Config.Couch ? Kirigami.Theme.defaultFont.pointSize : Kirigami.Theme.smallFont.pointSize
|
||||
color: Kirigami.Theme.textColor
|
||||
visible: !useGamepadHints
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
text: parent.label
|
||||
color: Kirigami.Theme.textColor
|
||||
font.pointSize: uiMode === Config.Couch ? Kirigami.Theme.defaultFont.pointSize : Kirigami.Theme.smallFont.pointSize
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
HintItem {
|
||||
action: "navigate"
|
||||
label: actionLabel("navigate")
|
||||
iconSource: iconForAux("dpad")
|
||||
keyLabel: keyboardLabel("navigate")
|
||||
}
|
||||
|
||||
HintItem {
|
||||
action: "confirm"
|
||||
label: actionLabel("confirm")
|
||||
iconSource: iconForButton(GamepadManager.confirmButtonLabel)
|
||||
keyLabel: keyboardLabel("confirm")
|
||||
}
|
||||
|
||||
HintItem {
|
||||
action: "back"
|
||||
label: actionLabel("back")
|
||||
iconSource: iconForButton(GamepadManager.backButtonLabel)
|
||||
keyLabel: keyboardLabel("back")
|
||||
}
|
||||
|
||||
HintItem {
|
||||
action: "details"
|
||||
label: actionLabel("details")
|
||||
iconSource: iconForButton(GamepadManager.detailsButtonLabel)
|
||||
keyLabel: keyboardLabel("details")
|
||||
}
|
||||
|
||||
HintItem {
|
||||
action: "search"
|
||||
label: actionLabel("search")
|
||||
iconSource: iconForButton(GamepadManager.searchButtonLabel)
|
||||
keyLabel: keyboardLabel("search")
|
||||
}
|
||||
|
||||
HintItem {
|
||||
action: "lb"
|
||||
label: actionLabel("lb")
|
||||
iconSource: (root.context === "library" || root.context === "sidebar") ? iconForAux("lb") : ""
|
||||
keyLabel: keyboardLabel("lb")
|
||||
}
|
||||
|
||||
HintItem {
|
||||
action: "rb"
|
||||
label: actionLabel("rb")
|
||||
iconSource: (root.context === "library" || root.context === "sidebar") ? iconForAux("rb") : ""
|
||||
keyLabel: keyboardLabel("rb")
|
||||
}
|
||||
|
||||
HintItem {
|
||||
action: "menu"
|
||||
label: actionLabel("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")
|
||||
}
|
||||
}
|
||||
|
|
@ -22,8 +22,6 @@ GridView {
|
|||
keyNavigationWraps: false
|
||||
activeFocusOnTab: true
|
||||
|
||||
currentIndex: -1
|
||||
|
||||
highlightMoveDuration: Kirigami.Units.shortDuration
|
||||
highlightFollowsCurrentItem: true
|
||||
|
||||
|
|
@ -33,40 +31,24 @@ GridView {
|
|||
Keys.onRightPressed: navigateRight()
|
||||
|
||||
function navigateUp() {
|
||||
if (currentIndex < 0 && count > 0) {
|
||||
currentIndex = 0
|
||||
return
|
||||
}
|
||||
if (currentIndex >= columns) {
|
||||
currentIndex -= columns
|
||||
}
|
||||
}
|
||||
|
||||
function navigateDown() {
|
||||
if (currentIndex < 0 && count > 0) {
|
||||
currentIndex = 0
|
||||
return
|
||||
}
|
||||
if (currentIndex + columns < count) {
|
||||
currentIndex += columns
|
||||
}
|
||||
}
|
||||
|
||||
function navigateLeft() {
|
||||
if (currentIndex < 0 && count > 0) {
|
||||
currentIndex = 0
|
||||
return
|
||||
}
|
||||
if (currentIndex > 0) {
|
||||
currentIndex--
|
||||
}
|
||||
}
|
||||
|
||||
function navigateRight() {
|
||||
if (currentIndex < 0 && count > 0) {
|
||||
currentIndex = 0
|
||||
return
|
||||
}
|
||||
if (currentIndex < count - 1) {
|
||||
currentIndex++
|
||||
}
|
||||
|
|
@ -74,44 +56,16 @@ GridView {
|
|||
|
||||
Connections {
|
||||
target: GamepadManager
|
||||
function onNavigateUp() {
|
||||
let w = applicationWindow()
|
||||
if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return
|
||||
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 onNavigateUp() { if (gridView.activeFocus) gridView.navigateUp() }
|
||||
function onNavigateDown() { if (gridView.activeFocus) gridView.navigateDown() }
|
||||
function onNavigateLeft() { if (gridView.activeFocus) gridView.navigateLeft() }
|
||||
function onNavigateRight() { if (gridView.activeFocus) gridView.navigateRight() }
|
||||
function onSelectPressed() {
|
||||
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) {
|
||||
gridView.currentItem.play()
|
||||
} else {
|
||||
if (gridView.activeFocus && gridView.currentItem) {
|
||||
gridView.currentItem.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly property int columns: Math.max(1, Math.floor(width / cellWidth))
|
||||
|
||||
|
|
|
|||
|
|
@ -13,57 +13,6 @@ QQC2.ToolBar {
|
|||
property alias searchField: searchFieldContainer.data
|
||||
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 sortChanged(int mode)
|
||||
|
||||
|
|
@ -98,12 +47,8 @@ QQC2.ToolBar {
|
|||
|
||||
QQC2.Menu {
|
||||
id: sortMenu
|
||||
focus: true
|
||||
|
||||
onOpened: Qt.callLater(function() { sortModeLastPlayed.forceActiveFocus() })
|
||||
|
||||
QQC2.MenuItem {
|
||||
id: sortModeLastPlayed
|
||||
text: i18n("Last Played")
|
||||
checkable: true
|
||||
checked: searchHeader.currentSortMode === 0
|
||||
|
|
@ -114,7 +59,6 @@ QQC2.ToolBar {
|
|||
}
|
||||
|
||||
QQC2.MenuItem {
|
||||
id: sortModeName
|
||||
text: i18n("Name")
|
||||
checkable: true
|
||||
checked: searchHeader.currentSortMode === 1
|
||||
|
|
@ -125,7 +69,6 @@ QQC2.ToolBar {
|
|||
}
|
||||
|
||||
QQC2.MenuItem {
|
||||
id: sortModePlayTime
|
||||
text: i18n("Play Time")
|
||||
checkable: true
|
||||
checked: searchHeader.currentSortMode === 2
|
||||
|
|
@ -137,43 +80,4 @@ 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<rect x="2" y="8" width="20" height="10" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||
<rect x="6" y="11" width="5" height="4" fill="currentColor" opacity="0.6"/>
|
||||
<rect x="13" y="11" width="5" height="4" fill="currentColor" opacity="0.6"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 350 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M9 2h6v7h7v6h-7v7H9v-7H2V9h7z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
<rect x="9" y="15" width="6" height="7" fill="currentColor" opacity="0.4"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 287 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M9 2h6v7h7v6h-7v7H9v-7H2V9h7z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
<rect x="2" y="9" width="7" height="6" fill="currentColor" opacity="0.4"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 286 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M9 2h6v7h7v6h-7v7H9v-7H2V9h7z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
<rect x="15" y="9" width="7" height="6" fill="currentColor" opacity="0.4"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 287 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M9 2h6v7h7v6h-7v7H9v-7H2V9h7z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
<rect x="9" y="2" width="6" height="7" fill="currentColor" opacity="0.4"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 286 B |
|
|
@ -1,3 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M9 2h6v7h7v6h-7v7H9v-7H2V9h7z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 209 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||
<path d="M7 12h7M11 9l3 3-3 3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 309 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<rect x="2" y="8" width="20" height="10" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||
<text x="12" y="16" text-anchor="middle" font-family="sans-serif" font-weight="bold" font-size="8" fill="currentColor">LB</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 324 B |
|
|
@ -1,5 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||
<circle cx="12" cy="12" r="5" fill="currentColor" opacity="0.3"/>
|
||||
<circle cx="12" cy="12" r="3" fill="currentColor"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 298 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M4 18V8c0-2.2 1.8-4 4-4h8c2.2 0 4 1.8 4 4v10" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<text x="12" y="16" text-anchor="middle" font-family="sans-serif" font-weight="bold" font-size="8" fill="currentColor">LT</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 354 B |
|
|
@ -1,5 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<rect x="2" y="8" width="20" height="10" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||
<line x1="7" y1="11" x2="17" y2="11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<line x1="7" y1="15" x2="17" y2="15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 403 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||
<path d="M12 17v-7M15 13l-3-3-3 3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 313 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<rect x="2" y="8" width="20" height="10" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||
<text x="12" y="16" text-anchor="middle" font-family="sans-serif" font-weight="bold" font-size="8" fill="currentColor">RB</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 324 B |
|
|
@ -1,5 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||
<circle cx="12" cy="12" r="5" fill="currentColor" opacity="0.3"/>
|
||||
<circle cx="12" cy="12" r="3" fill="currentColor"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 298 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M4 18V8c0-2.2 1.8-4 4-4h8c2.2 0 4 1.8 4 4v10" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<text x="12" y="16" text-anchor="middle" font-family="sans-serif" font-weight="bold" font-size="8" fill="currentColor">RT</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 354 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||
<path d="M12 7v7M9 11l3 3 3-3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 309 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||
<path d="M17 12h-7M13 15l-3-3 3-3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 313 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||
<text x="12" y="16.5" text-anchor="middle" font-family="sans-serif" font-weight="bold" font-size="12" fill="currentColor">A</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 309 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||
<text x="12" y="16.5" text-anchor="middle" font-family="sans-serif" font-weight="bold" font-size="12" fill="currentColor">B</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 309 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||
<path d="M12 7l5 4v6h-3v-4h-4v4H7v-6z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 296 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||
<line x1="7" y1="12" x2="17" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 279 B |
|
|
@ -1,5 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||
<line x1="12" y1="7" x2="12" y2="17" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="7" y1="12" x2="17" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 382 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||
<text x="12" y="16.5" text-anchor="middle" font-family="sans-serif" font-weight="bold" font-size="12" fill="currentColor">X</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 309 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||
<text x="12" y="16.5" text-anchor="middle" font-family="sans-serif" font-weight="bold" font-size="12" fill="currentColor">Y</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 309 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||
<circle cx="12" cy="12" r="5" fill="none" stroke="currentColor" stroke-width="2"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 261 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||
<path d="M8 8l8 8M16 8l-8 8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 271 B |
|
|
@ -1,5 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<rect x="2" y="8" width="20" height="10" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||
<line x1="7" y1="11" x2="17" y2="11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<line x1="7" y1="15" x2="17" y2="15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 403 B |
|
|
@ -1,5 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<rect x="2" y="8" width="20" height="10" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||
<path d="M8 13l4-3 4 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
<line x1="12" y1="10" x2="12" y2="16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 427 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||
<rect x="7" y="7" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2" rx="1"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 279 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||
<path d="M12 6l6.5 11h-13z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 283 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||
<text x="12" y="16.5" text-anchor="middle" font-family="sans-serif" font-weight="bold" font-size="12" fill="currentColor">A</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 309 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||
<text x="12" y="16.5" text-anchor="middle" font-family="sans-serif" font-weight="bold" font-size="12" fill="currentColor">B</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 309 B |
|
|
@ -1,5 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<rect x="2" y="8" width="20" height="10" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||
<line x1="7" y1="11" x2="17" y2="11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<line x1="7" y1="15" x2="17" y2="15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 403 B |
|
|
@ -1,5 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<rect x="2" y="8" width="20" height="10" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||
<rect x="6" y="11" width="5" height="4" fill="currentColor" opacity="0.6"/>
|
||||
<rect x="13" y="11" width="5" height="4" fill="currentColor" opacity="0.6"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 350 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||
<text x="12" y="16.5" text-anchor="middle" font-family="sans-serif" font-weight="bold" font-size="12" fill="currentColor">X</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 309 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||
<text x="12" y="16.5" text-anchor="middle" font-family="sans-serif" font-weight="bold" font-size="12" fill="currentColor">Y</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 309 B |
|
|
@ -1,135 +0,0 @@
|
|||
#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();
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
#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;
|
||||
};
|
||||
|
|
@ -8,7 +8,6 @@
|
|||
#include <QFileInfo>
|
||||
#include <QRegularExpression>
|
||||
#include <QStandardPaths>
|
||||
#include <QStringList>
|
||||
#include <QTextStream>
|
||||
|
||||
SteamImporter::SteamImporter(QObject *parent)
|
||||
|
|
@ -123,54 +122,10 @@ Game *SteamImporter::parseAppManifest(const QString &path)
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
auto isNonGameEntry = [](const QString &title, const QString &dir) {
|
||||
auto normalize = [](QString s) {
|
||||
s = s.toLower();
|
||||
s.remove(QLatin1Char(' '));
|
||||
s.remove(QLatin1Char('_'));
|
||||
s.remove(QLatin1Char('-'));
|
||||
return s;
|
||||
};
|
||||
|
||||
const QString t = normalize(title);
|
||||
const QString d = normalize(dir);
|
||||
static const QStringList patterns = {
|
||||
QStringLiteral("steamlinuxruntime"),
|
||||
QStringLiteral("steamworkscommonredistributables"),
|
||||
QStringLiteral("steamworkssdkredist"),
|
||||
QStringLiteral("shaderprecaching"),
|
||||
QStringLiteral("proton"),
|
||||
QStringLiteral("steamvr"),
|
||||
};
|
||||
for (const QString &p : patterns) {
|
||||
if (t.contains(p) || d.contains(p)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (isNonGameEntry(name, installDir)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Skip tools and other non-game content
|
||||
QString stateFlags = getValue(QStringLiteral("StateFlags"));
|
||||
if (!stateFlags.isEmpty()) {
|
||||
bool ok = false;
|
||||
const int flags = stateFlags.toInt(&ok);
|
||||
if (ok) {
|
||||
constexpr int InstalledFlag = 4;
|
||||
if ((flags & InstalledFlag) == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QFileInfo manifestInfo(path);
|
||||
const QString gameDir = manifestInfo.absolutePath() + QStringLiteral("/common/") + installDir;
|
||||
if (!QDir(gameDir).exists()) {
|
||||
return nullptr;
|
||||
if (stateFlags == QLatin1String("2")) {
|
||||
// Only partially installed
|
||||
}
|
||||
|
||||
Game *game = new Game(QStringLiteral("steam-%1").arg(appId), name);
|
||||
|
|
@ -185,6 +140,8 @@ Game *SteamImporter::parseAppManifest(const QString &path)
|
|||
}
|
||||
|
||||
// Set installation directory
|
||||
QFileInfo manifestInfo(path);
|
||||
QString gameDir = manifestInfo.absolutePath() + QStringLiteral("/common/") + installDir;
|
||||
game->setWorkingDirectory(gameDir);
|
||||
|
||||
game->setInstalled(true);
|
||||
|
|
|
|||