Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -180,17 +180,21 @@ jobs:
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt update
# The gstreamer plugin packages provide the elements WebKitGTK needs
# for audio/media playback (appsink, autoaudiosink, vorbis, ...). They
# are copied into the AppImage by `bundleMediaFramework: true`, so the
# packages have to be installed here for the bundler to find them.
# The gstreamer plugin packages provide the media elements WebKitGTK
# looks up at runtime. The AppImage bundler copies every installed
# plugin verbatim (`bundleMediaFramework: true`), so only what is
# listed here ends up inside the AppImage:
# -plugins-base: appsink, ogg/vorbis, opus, isomp4
# -plugins-good: autoaudiosink, vpx (VP8/VP9), matroska
# -plugins-bad: fakevideosink, WebVTT encoder, H.264 (openh264), AAC (faad)
sudo apt install -y \
libwebkit2gtk-4.1-dev \
libappindicator3-dev \
librsvg2-dev \
patchelf \
gstreamer1.0-plugins-base \
gstreamer1.0-plugins-good
gstreamer1.0-plugins-good \
gstreamer1.0-plugins-bad

- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
Expand Down
29 changes: 29 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,32 @@ jobs:
name: coverage-report
path: coverage/
retention-days: 14

rust:
name: Rust tests (Linux)
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v7

- name: Install Linux system dependencies
run: |
sudo apt update
sudo apt install -y \
libwebkit2gtk-4.1-dev \
libappindicator3-dev \
librsvg2-dev \
patchelf

- name: Setup Rust
uses: dtolnay/rust-toolchain@stable

- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: src-tauri

- name: Run Rust tests
working-directory: src-tauri
run: cargo test --locked
28 changes: 24 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

### Changes

- Fixed playtime briefly landing in the offline file every time the session was refreshed: the tracker now retries with the fresh session immediately, and the session is refreshed earlier so a token never expires in the middle of a tick.
- Fixed playtime stopping to count while a game was running: the tracker now asks for a fresh session as soon as the server rejects an increment, pauses its requests instead of retrying every minute, and credits the minutes it stored offline as soon as the session works again.
- Fixed playtime that was stored offline being stranded until an app restart: offline minutes are now synced in the background and immediately when the connection or the session returns.
- Fixed the time tracker re-scanning every installed game's folder every minute, which could delay ticks on large installations.
- Fixed playtime being lost when a game's launcher briefly restarts the game process: tracking now continues for a short grace period instead of ending the session.
- Added a Refresh button to the window title bar that reloads the current page.
- Auto-install on Linux now makes extracted shell scripts (`.sh`/`.bash`/`.run`/etc.) executable automatically.
- Fixed "Open Folder" doing nothing on Linux: folder and URL opening now use the official `tauri-plugin-opener`, with WSL support preserved.
Expand All @@ -18,6 +23,7 @@
- Fixed the Wayland startup crash on the Linux AppImage (`Could not create default EGL display: EGL_BAD_PARAMETER`) by preloading the host's `libwayland-client` through a patched AppImage `AppRun` wrapper. The `.deb` build already uses the system library and is unaffected.
- Administrators can delete progress entries of other users
- Added a native OS taskbar/dock download progress indicator for desktop builds (Windows taskbar, macOS dock, Linux launcher)
- Fixed the Windows taskbar progress bar staying stuck at 100% after a download, extraction or installation had finished. It now disappears as soon as there is nothing left to do, instead of only disappearing when you delete the download.
- Added an "Installed" badge to server game cards so already-installed games are easy to spot at a glance.
- The download button on a game's page now collapses into a compact icon-only button when the game is already installed.
- Downloads interrupted by the app quitting are now automatically resumed on the next launch; downloads you intentionally paused or cancelled stay paused.
Expand Down Expand Up @@ -55,16 +61,30 @@
- Fixed downloads aborting with a 401 error when you refresh (F5) mid-download: auto-resumed downloads now wait for a valid session token instead of firing before login has finished.
- Added a keyboard shortcut (Ctrl/Cmd+Shift+O) to toggle Streamer/OPSEC mode from anywhere in the app.
- Cleaned up the Community Page UI

---

- Administrators can now view a game's files (their paths, sizes and other metadata) in the game settings and delete individual game files from the server.
- Made the game settings and user settings dialogs more responsive and easier to use on tablets and phones, and removed the release date from the game files list.
- Deleting a game's last version file now closes the settings, removes the game from the library, and returns you to the library if you were viewing that game.
- Settings → About now shows the full system diagnostics (operating system, display and monitors, GPU/WebGL, WebGPU, CPU/memory, platform, language and user agent) that previously only lived in the Developer Tools menu, replacing the shorter system info list there.
- Moved the Hardware Acceleration setting from the Developer Tools menu into Settings → Appearance (Linux desktop builds), where it now sits next to Smooth Scrolling.
- Fixed the Linux AppImage failing to play sounds with `GStreamer element appsink not found. Please install it.`: the GStreamer plugins WebKitGTK needs are now bundled into the AppImage (which grows by ~15–35 MB), so no GStreamer packages have to be installed on the system.
- Fixed the Linux AppImage failing to play sound effects and trailers with `GStreamer element appsink not found. Please install it.` (followed by `fakevideosink` and WebVTT encoder errors): the full GStreamer plugin set WebKitGTK needs — base, good and bad, including H.264/AAC decoding, MSE support and the WebVTT encoder — is now bundled into the AppImage, so no GStreamer packages have to be installed on the system.
- Fixed the system information under Settings → About not matching the machine it runs on
- [#36](https://github.com/Phalcode/gamevault-frontend/issues/36) Fixed disc images that only contain a UDF filesystem (e.g. UDF-only DVD/Blu-ray ISOs) failing to extract with an error. Disc images are now read by a combined ISO 9660 and UDF reader.

---

as

- Added a one-time warning on the first launch of an Early Access or unstable build, explaining that these builds are meant for the early access program/developers and testers, that extra setup steps may be needed, and that more issues are to be expected than on a stable release.
- Refreshing the app (F5) or using the window's Refresh button no longer interrupts work in progress: downloads, extractions, installations, umu-launcher setup and app updates keep running in the background, their progress is picked back up instead of starting over, and a refresh can no longer start a second download, extraction, installation or update over the one that is still running.
- Fixed the app freezing while uninstalling a game, while launching a Windows game that still needs umu-launcher set up, and while deleting or scanning large game folders.
- The Early Access/unstable warning is now shown only once per installation instead of again after every app update.
- Linux/umu-launcher defaults (GAMEID, STORE and Proton version) can now be defined once per game as custom metadata on the server: the game settings dialog has matching fields, and newly downloaded games inherit them automatically instead of needing per-client setup.
- Fixed Linux/umu-launcher Wine/Proton prefixes: installing, launching and uninstalling a game now all use the same prefix per game (named like the game's install folder). Previously a game could run in a different prefix than it was installed into, and without a GAMEID every game shared umu's default prefix. Existing prefixes are reused, so nothing has to be moved and no save games are lost.
- Uninstalling a game now offers to also delete its Wine/Proton prefix, showing the folder path and keeping it by default (local saves and game settings can live inside it). Only prefixes GameVault manages are offered.
- The per-game Wine Prefix field now shows the path that is actually used (with a click-to-copy) instead of a path GameVault never used, and the prefix settings explain the real default location.
- Fixed uninstalling a game silently dropping the per-game umu-launcher overrides (GAMEID, STORE, Proton version, Wine prefix) and the "run as administrator" flag from the game's configuration.
- Added launch logs: the complete output of every game start (Proton/Wine/umu output included) is written to rotating log files and shown in a dedicated log window. A new setting ("Always show launch logs") opens that window automatically while a game starts, a failed launch opens it regardless, and earlier logs can be browsed or cleared from the window. Installer runs are logged too.
- The per-game Proton (PROTONPATH) field now offers a dropdown with the Proton builds already installed in `compatibilitytools.d` (Steam and Flatpak Steam), so version names don't have to be typed by hand.

## 17.0.0

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,5 @@ The choice is persisted in `localStorage` (`gv_update_channel`).
Generate the keys once with `cargo tauri signer generate -w ~/.tauri/gamevault-updater.key`. Keep the private key and password safe — losing them breaks updates for existing installs.

**Local builds** – a normal `tauri build` produces no updater artifacts. To create them locally, build with the release config and signing env (`pnpm tauri build --config src-tauri/tauri.release.conf.json`). CI uses `src-tauri/tauri.release.generated.json` with the pubkey injected, since the Tauri bundler requires `plugins.updater.pubkey` in the parsed config.

**Linux AppImage media support** – `bundle.linux.appimage.bundleMediaFramework` is enabled, which makes the bundler copy the GStreamer plugins installed on the _build machine_ into the AppImage (they end up in `usr/lib/gstreamer-1.0`). A local AppImage build therefore needs `gstreamer1.0-plugins-base`, `gstreamer1.0-plugins-good` and `gstreamer1.0-plugins-bad` installed (the same packages CI installs); without them the AppImage ships without audio/video support and WebKitGTK logs `GStreamer element ... not found`.
28 changes: 4 additions & 24 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ flate2 = "1.1.2"
bzip2 = "0.5.2"
xz2 = "0.1.7"
zstd = "0.13.3"
iso9660_simple = "0.2.7"
isomage = "2.1.0"

[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3", features = ["fileapi", "shellapi", "winnt", "winuser", "synchapi", "handleapi", "winbase", "winerror", "processthreadsapi", "sysinfoapi"] }
Expand Down
13 changes: 13 additions & 0 deletions src-tauri/capabilities/launch-log.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "launch-log",
"description": "Permissions for the launch log window",
"windows": ["launch-log"],
"permissions": [
"core:default",
"core:window:allow-close",
"core:window:allow-minimize",
"core:window:allow-start-dragging",
"core:window:allow-set-focus"
]
}
92 changes: 89 additions & 3 deletions src-tauri/src/downloads.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,19 @@ use std::fs;
use std::path::PathBuf;

#[tauri::command]
pub(crate) fn recover_download_cards(selected_root: String) -> Result<Vec<RecoveredDownloadCard>, String> {
pub(crate) async fn recover_download_cards(
selected_root: String,
) -> Result<Vec<RecoveredDownloadCard>, String> {
// Scans every version folder under the root to rebuild the download cards,
// which must happen off the UI thread.
tauri::async_runtime::spawn_blocking(move || recover_download_cards_blocking(selected_root))
.await
.map_err(|error| format!("Recovering downloads failed: {error}"))?
}

pub(crate) fn recover_download_cards_blocking(
selected_root: String,
) -> Result<Vec<RecoveredDownloadCard>, String> {
let candidate = PathBuf::from(&selected_root).join("GameVault");
let root = if candidate.exists() {
candidate
Expand Down Expand Up @@ -209,9 +221,10 @@ pub(crate) fn recover_download_cards(selected_root: String) -> Result<Vec<Recove

use crate::events::DownloadProgressEvent;
use crate::state::{control_flags, DOWNLOAD_CONTROL_RUNNING, DOWNLOAD_CONTROL_PAUSE, DOWNLOAD_CONTROL_CANCEL};
use std::collections::HashMap;
use std::io::SeekFrom;
use std::sync::Arc;
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
use std::sync::atomic::{AtomicU8, AtomicU64, Ordering};
use std::time::{Duration, Instant};
use tauri::Emitter;
use tokio::fs::{File, OpenOptions};
Expand Down Expand Up @@ -299,6 +312,55 @@ fn filename_from_content_disposition(header: Option<&reqwest::header::HeaderValu
None
}

/// Tracks which download tasks are currently alive, per game.
///
/// Download tasks keep running when the webview is reloaded (F5), and the
/// frontend resumes whatever it finds on disk afterwards. Without this guard a
/// second task would start writing the same file, so `download_game_version`
/// refuses to start while a live task for the game exists.
///
/// Entries are owned by the task that created them: a task that is winding down
/// (paused/cancelled) may still be alive when a newer task starts, and its
/// cleanup must not remove the newer task's entry.
struct ActiveDownloadEntry {
game_id: i64,
token: u64,
}

impl Drop for ActiveDownloadEntry {
fn drop(&mut self) {
if let Ok(mut tasks) = active_download_tasks().lock() {
if tasks.get(&self.game_id) == Some(&self.token) {
tasks.remove(&self.game_id);
}
}
}
}

static ACTIVE_DOWNLOAD_TASKS: OnceLock<Mutex<HashMap<i64, u64>>> = OnceLock::new();
static DOWNLOAD_TASK_TOKEN: AtomicU64 = AtomicU64::new(1);

fn active_download_tasks() -> &'static Mutex<HashMap<i64, u64>> {
ACTIVE_DOWNLOAD_TASKS.get_or_init(|| Mutex::new(HashMap::new()))
}

/// Registers a new download task and returns a guard that deregisters it again,
/// on every exit path including panics.
fn register_download_task(game_id: i64) -> ActiveDownloadEntry {
let token = DOWNLOAD_TASK_TOKEN.fetch_add(1, Ordering::Relaxed);
if let Ok(mut tasks) = active_download_tasks().lock() {
tasks.insert(game_id, token);
}
ActiveDownloadEntry { game_id, token }
}

fn is_download_task_live(game_id: i64) -> bool {
active_download_tasks()
.lock()
.map(|tasks| tasks.contains_key(&game_id))
.unwrap_or(false)
}

#[tauri::command]
pub(crate) fn download_game_version(
app: tauri::AppHandle,
Expand All @@ -311,6 +373,28 @@ pub(crate) fn download_game_version(
resume_position: Option<u64>,
) -> Result<(), String> {
let control_flag = Arc::new(AtomicU8::new(DOWNLOAD_CONTROL_RUNNING));

// A download for this game may already be running: the task survives a
// webview reload, and the frontend resumes downloads it finds on disk
// afterwards. Starting a second task would put two writers on the same file,
// so keep the running one and let the UI re-attach to its progress events.
//
// A task that was paused or cancelled is on its way out and does not count:
// retrying right after cancelling must still work.
let previous_is_still_downloading = control_flags()
.lock()
.ok()
.and_then(|flags| flags.get(&game_id).cloned())
.map(|flag| flag.load(Ordering::Relaxed) == DOWNLOAD_CONTROL_RUNNING)
.unwrap_or(false);
if previous_is_still_downloading && is_download_task_live(game_id) {
return Ok(());
}

// Registered before the task starts so a second call in the same tick can
// never slip past the guard above.
let active_entry = register_download_task(game_id);

{
let mut guard = control_flags()
.lock()
Expand All @@ -319,6 +403,8 @@ pub(crate) fn download_game_version(
}

tauri::async_runtime::spawn(async move {
let _active_entry = active_entry;

let client = reqwest::Client::new();
let mut req = client.get(&url).header("Accept", "*/*");
if let Some(auth) = auth_header.as_ref() {
Expand Down
Loading
Loading