14 Commits
v1.5.5 ... main

Author SHA1 Message Date
github-actions[bot]
85873379f3 chore: update package.nix for v1.5.6 2026-05-06 14:16:24 +00:00
Tickbase
293273af2d version bump 2026-05-06 16:14:37 +02:00
Tickbase
550d183ee2 changelog 2026-05-06 16:14:31 +02:00
Tickbase
756a6e3d53 fetch DLC ID's from steamcmd #108 2026-05-06 16:12:54 +02:00
Tickbase
cbed56ac33 Delete .github/workflows/update-npm-hash.yml 2026-05-05 17:12:28 +02:00
Tickbase
fbabff0a34 Update workflow to update npm hash automatically credit: @naguiagahnim
Automates computing the npm hash for nix users automatically on release. Credit to: @naguiagahnim
2026-05-05 17:12:12 +02:00
Tickbase
fb6203c4f7 Merge pull request #114 from naguiagahnim/main
Automate NPM hash fetch
2026-05-05 17:01:12 +02:00
Agahnim
9144e52db1 remove shell script
Will be integrated directly into CI/CD
2026-05-05 16:59:22 +02:00
Agahnim
e50f885ba2 add CI/CD to automate npm hash updating 2026-05-05 10:38:39 +02:00
Agahnim
433417ade2 automate npm dependencies hash fetch 2026-05-05 10:38:39 +02:00
Tickbase
6f5d750af9 Merge pull request #113 from naguiagahnim/main
Improve Nix packaging structure and update npm dependencies hash
2026-05-04 15:10:49 +02:00
Agahnim
ef7e183312 update Nix instructions in README to fit new packaging structure 2026-05-03 23:12:54 +02:00
Agahnim
3b4a17b615 improve nix packaging structure and update npm dependencies hash 2026-05-03 23:12:51 +02:00
Tickbase
6ff6c06bec Update README to include Epic Games support
Added Epic Games support for ScreamAPI in the application description and features.
2026-05-01 11:49:22 +02:00
11 changed files with 301 additions and 289 deletions

View File

@@ -53,6 +53,39 @@ jobs:
echo "EOF" echo "EOF"
} >> "$GITHUB_OUTPUT" } >> "$GITHUB_OUTPUT"
- name: install nix
uses: DeterminateSystems/nix-installer-action@main
- name: update package.nix version, date, and npm hash
env:
VERSION: ${{ steps.get-version.outputs.version }}
run: |
# Get today's date in YYYY-MM-DD format
TODAY=$(date -u +%Y-%m-%d)
# Compute new npm deps hash from package-lock.json
HASH=$(nix-shell -p prefetch-npm-deps --run "prefetch-npm-deps package-lock.json" 2>/dev/null)
echo "New hash: $HASH"
# Update version string (e.g. 1.5.5-unstable-2026-05-03)
sed -i "s|version = \"[^\"]*\"|version = \"${VERSION}-unstable-${TODAY}\"|" package.nix
# Update npm deps hash
sed -i "s|hash = \"[^\"]*\"|hash = \"${HASH}\"|" package.nix
echo "Updated package.nix:"
grep -E 'version|hash' package.nix
- name: commit updated package.nix
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add package.nix
if [[ $(git status -s) ]]; then
git commit -m "chore: update package.nix for v${{ steps.get-version.outputs.version }}"
git push
fi
- name: create draft release - name: create draft release
id: create-release id: create-release
uses: actions/github-script@v6 uses: actions/github-script@v6
@@ -88,6 +121,8 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
ref: main
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4

View File

@@ -1,3 +1,8 @@
## [1.5.6] - 06-05-2026
### Added
- DLC fetching now uses steamcmd instead of the Steam store API, which was missing DLCs from many games
## [1.5.5] - 30-04-2026 ## [1.5.5] - 30-04-2026
### Added ### Added

View File

@@ -1,6 +1,6 @@
# CreamLinux # CreamLinux
CreamLinux is a GUI application for Linux that simplifies the management of DLC IDs in Steam games. It provides a user-friendly interface to install and configure CreamAPI (for native Linux games) and SmokeAPI (for Windows games running through Proton). CreamLinux is a GUI application for Linux that simplifies the management of DLC IDs in Steam games. It provides a user-friendly interface to install and configure CreamAPI (for native Linux games), SmokeAPI (for Windows games running through Proton) and ScreamAPI (Epic Games).
## Watch the demo here: ## Watch the demo here:
@@ -22,6 +22,7 @@ While the core functionality is working, please be aware that this is an early r
- **Auto-discovery**: Automatically finds Steam games installed on your system - **Auto-discovery**: Automatically finds Steam games installed on your system
- **Native support**: Installs CreamLinux for native Linux games - **Native support**: Installs CreamLinux for native Linux games
- **Proton support**: Installs SmokeAPI for Windows games running through Proton - **Proton support**: Installs SmokeAPI for Windows games running through Proton
- **Epic Games support**: Installs ScreamAPI for games running through Heroic/Legendary
- **DLC management**: Easily select which DLCs to enable - **DLC management**: Easily select which DLCs to enable
- **Modern UI**: Clean, responsive interface that's easy to use - **Modern UI**: Clean, responsive interface that's easy to use
@@ -47,32 +48,30 @@ While the core functionality is working, please be aware that this is an early r
``` ```
### Nix ### Nix
You can fetch this repository in your configuration using `pkgs.fetchFromGithub`: You can add this package to your configuration using `pkgs.fetchFromGitHub`:
```nix ```nix
let let
creamlinux = pkgs.callPackage (pkgs.fetchFromGitHub { creamlinux = import (pkgs.fetchFromGitHub {
owner = "Novattz"; owner = "Novattz";
repo = "creamlinux-installer"; repo = "creamlinux-installer";
rev = "main"; rev = "main"; # replace with a commit hash to pin the version
hash = ""; # You can use nix-prefetch-url to determine which value to put here, or paste the value returned by the error your rebuild will output hash = ""; # paste the value returned by the error your rebuild will output
}) {}; }) { inherit pkgs; };
in in
{ {
environment.systemPackages = [ creamlinux ]; environment.systemPackages = [ creamlinux ];
} }
``` ```
or, using `builtins.fetchTarball`: or, using `builtins.fetchTarball`:
```nix ```nix
let let
creamlinux-src = builtins.fetchTarball { creamlinux = import (builtins.fetchTarball {
url = "https://github.com/Novattz/creamlinux-installer/archive/main.tar.gz"; url = "https://github.com/Novattz/creamlinux-installer/archive/main.tar.gz";
sha256 = ""; # See above sha256 = ""; # See above
}; }) { inherit pkgs; };
in in
{ {
environment.systemPackages = [ environment.systemPackages = [ creamlinux ];
(pkgs.callPackage creamlinux-src {})
];
} }
``` ```
alternatively and if you want to pin the package version, using [npins](https://github.com/andir/npins): alternatively and if you want to pin the package version, using [npins](https://github.com/andir/npins):
@@ -85,12 +84,11 @@ let
in in
{ {
environment.systemPackages = [ environment.systemPackages = [
(pkgs.callPackage "${sources.creamlinux-installer}/default.nix" {}) (import sources.creamlinux-installer { inherit pkgs; })
]; ];
} }
``` ```
Those are the recommended methods to add creamlinux-installer to your environment. However, you could also add it as an input of your flake, like so: Those are the recommended methods to add creamlinux-installer to your environment. However, you could also add it as an input of your flake, like so:
```nix ```nix
{ {
inputs = { inputs = {
@@ -107,7 +105,7 @@ Those are the recommended methods to add creamlinux-installer to your environmen
Then, in your configuration: Then, in your configuration:
```nix ```nix
environment.systemPackages = [ environment.systemPackages = [
(pkgs.callPackage inputs.creamlinux-installer {}) (import inputs.creamlinux-installer { inherit pkgs; })
]; ];
``` ```
Similarly to running the AppImage, you will need to set `WEBKIT_DISABLE_DMABUF_RENDERER=1` if your GPU is from Nvidia in order to run the package. Similarly to running the AppImage, you will need to set `WEBKIT_DISABLE_DMABUF_RENDERER=1` if your GPU is from Nvidia in order to run the package.

View File

@@ -1,57 +1,4 @@
{pkgs ? import <nixpkgs> {}}: let {
cargoRoot = "src-tauri"; pkgs ? import <nixpkgs> { },
src = ./.; }:
pkgs.callPackage ./package.nix { }
patchSassEmbedded = pkgs.writeShellScriptBin "patch-sass-embedded" ''
NIX_LD="$(cat ${pkgs.stdenv.cc}/nix-support/dynamic-linker)"
for dart_bin in node_modules/sass-embedded-linux-*/dart-sass/src/dart; do
if [ -f "$dart_bin" ]; then
${pkgs.patchelf}/bin/patchelf --set-interpreter "$NIX_LD" "$dart_bin"
fi
done
'';
in
pkgs.rustPlatform.buildRustPackage {
pname = "creamlinux-installer";
version = "1.5.0-unstable-2026-04-23";
inherit src;
cargoLock.lockFile = ./src-tauri/Cargo.lock;
npmDeps = pkgs.fetchNpmDeps {
inherit src;
hash = "sha256-anYTERlnfOGDsGW0joy+h7wECJNDy6q+0a2to6t36pg=";
};
nativeBuildInputs =
[
pkgs.cargo-tauri.hook
pkgs.nodejs
pkgs.npmHooks.npmConfigHook
pkgs.pkg-config
]
++ pkgs.lib.optionals pkgs.stdenv.isLinux [
pkgs.wrapGAppsHook4
];
buildInputs = pkgs.lib.optionals pkgs.stdenv.isLinux [
pkgs.glib-networking
pkgs.openssl
pkgs.webkitgtk_4_1
];
inherit cargoRoot;
buildAndTestSubdir = cargoRoot;
postPatch = ''
substituteInPlace src-tauri/tauri.conf.json \
--replace-fail '"createUpdaterArtifacts": true' '"createUpdaterArtifacts": false'
'';
preBuild = ''
${patchSassEmbedded}/bin/patch-sass-embedded
'';
env.NO_STRIP = true;
}

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "creamlinux", "name": "creamlinux",
"version": "1.5.5", "version": "1.5.6",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "creamlinux", "name": "creamlinux",
"version": "1.5.5", "version": "1.5.6",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@tauri-apps/api": "^2.5.0", "@tauri-apps/api": "^2.5.0",

View File

@@ -1,7 +1,7 @@
{ {
"name": "creamlinux", "name": "creamlinux",
"private": true, "private": true,
"version": "1.5.5", "version": "1.5.6",
"type": "module", "type": "module",
"author": "Tickbase", "author": "Tickbase",
"repository": "https://github.com/Novattz/creamlinux-installer", "repository": "https://github.com/Novattz/creamlinux-installer",

72
package.nix Normal file
View File

@@ -0,0 +1,72 @@
{
cargo-tauri,
writeShellScriptBin,
stdenv,
patchelf,
rustPlatform,
fetchNpmDeps,
nodejs,
npmHooks,
pkg-config,
lib,
wrapGAppsHook4,
glib-networking,
openssl,
webkitgtk_4_1,
}:
let
cargoRoot = "src-tauri";
src = ./.;
patchSassEmbedded = writeShellScriptBin "patch-sass-embedded" ''
NIX_LD="$(cat ${stdenv.cc}/nix-support/dynamic-linker)"
for dart_bin in node_modules/sass-embedded-linux-*/dart-sass/src/dart; do
if [ -f "$dart_bin" ]; then
${patchelf}/bin/patchelf --set-interpreter "$NIX_LD" "$dart_bin"
fi
done
'';
in
rustPlatform.buildRustPackage {
pname = "creamlinux-installer";
version = "1.5.6-unstable-2026-05-06";
inherit src;
cargoLock.lockFile = ./src-tauri/Cargo.lock;
npmDeps = fetchNpmDeps {
inherit src;
hash = "sha256-6sQul8tZaCk62JL9SfDKxVShNgYdoGYOS25asugirDo=";
};
nativeBuildInputs = [
cargo-tauri.hook
nodejs
npmHooks.npmConfigHook
pkg-config
]
++ lib.optionals stdenv.isLinux [
wrapGAppsHook4
];
buildInputs = lib.optionals stdenv.isLinux [
glib-networking
openssl
webkitgtk_4_1
];
inherit cargoRoot;
buildAndTestSubdir = cargoRoot;
postPatch = ''
substituteInPlace src-tauri/tauri.conf.json \
--replace-fail '"createUpdaterArtifacts": true' '"createUpdaterArtifacts": false'
'';
preBuild = ''
${patchSassEmbedded}/bin/patch-sass-embedded
'';
env.NO_STRIP = true;
}

2
src-tauri/Cargo.lock generated
View File

@@ -602,7 +602,7 @@ dependencies = [
[[package]] [[package]]
name = "creamlinux-installer" name = "creamlinux-installer"
version = "1.5.5" version = "1.5.6"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"log", "log",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "creamlinux-installer" name = "creamlinux-installer"
version = "1.5.5" version = "1.5.6"
description = "DLC Manager for Steam games on Linux" description = "DLC Manager for Steam games on Linux"
authors = ["tickbase"] authors = ["tickbase"]
license = "MIT" license = "MIT"

View File

@@ -650,106 +650,132 @@ pub async fn uninstall_koaloader(game: EpicGame, app_handle: AppHandle) -> Resul
Ok(()) Ok(())
} }
// Fetch DLC details from Steam API (simple version without progress) // steamcmd helpers
pub async fn fetch_dlc_details(app_id: &str) -> Result<Vec<DlcInfo>, String> {
let client = reqwest::Client::new();
let base_url = format!(
"https://store.steampowered.com/api/appdetails?appids={}",
app_id
);
let response = client /// Calls `https://api.steamcmd.net/v1/info/{app_id}` and returns the per-app
.get(&base_url) /// JSON object (`data[app_id]`), or `None` on any failure.
.timeout(Duration::from_secs(10)) async fn fetch_steamcmd_info(
client: &reqwest::Client,
app_id: &str,
) -> Option<serde_json::Value> {
let url = format!("https://api.steamcmd.net/v1/info/{}", app_id);
let resp = client
.get(&url)
.header("User-Agent", "CreamLinux-Installer")
.timeout(Duration::from_secs(15))
.send() .send()
.await .await
.map_err(|e| format!("Failed to fetch game details: {}", e))?; .ok()?;
if !response.status().is_success() { let json: serde_json::Value = resp.json().await.ok()?;
return Err(format!(
"Failed to fetch game details: HTTP {}", if json.get("status").and_then(|s| s.as_str()) != Some("success") {
response.status() return None;
));
} }
let data: serde_json::Value = response json.get("data").and_then(|d| d.get(app_id)).cloned()
.json() }
.await
.map_err(|e| format!("Failed to parse response: {}", e))?;
let dlc_ids = match data /// Extracts DLC app-IDs from a game's steamcmd info object.
.get(app_id) /// Merges two sources and deduplicates:
.and_then(|app| app.get("data")) /// 1. `extended.listofdlc` - comma-separated string
.and_then(|data| data.get("dlc")) /// 2. `depots[*].dlcappid` - per-depot numeric field
fn extract_dlc_ids(info: &serde_json::Value) -> Vec<String> {
let mut ids: std::collections::HashSet<String> = std::collections::HashSet::new();
// Source 1 - extended.listofdlc
if let Some(list) = info
.get("extended")
.and_then(|e| e.get("listofdlc"))
.and_then(|v| v.as_str())
{ {
Some(dlc_array) => match dlc_array.as_array() { for raw in list.split(',') {
Some(array) => array let id = raw.trim();
.iter() if !id.is_empty() {
.filter_map(|id| id.as_u64().map(|n| n.to_string())) ids.insert(id.to_string());
.collect::<Vec<String>>(), }
_ => Vec::new(),
},
_ => Vec::new(),
};
info!("Found {} DLCs for game ID {}", dlc_ids.len(), app_id);
let mut dlc_details = Vec::new();
for dlc_id in dlc_ids {
let dlc_url = format!(
"https://store.steampowered.com/api/appdetails?appids={}",
dlc_id
);
// Add a small delay to avoid rate limiting
tokio::time::sleep(Duration::from_millis(300)).await;
let dlc_response = client
.get(&dlc_url)
.timeout(Duration::from_secs(10))
.send()
.await
.map_err(|e| format!("Failed to fetch DLC details: {}", e))?;
if dlc_response.status().is_success() {
let dlc_data: serde_json::Value = dlc_response
.json()
.await
.map_err(|e| format!("Failed to parse DLC response: {}", e))?;
let dlc_name = match dlc_data
.get(&dlc_id)
.and_then(|app| app.get("data"))
.and_then(|data| data.get("name"))
{
Some(name) => match name.as_str() {
Some(s) => s.to_string(),
_ => "Unknown DLC".to_string(),
},
_ => "Unknown DLC".to_string(),
};
info!("Found DLC: {} ({})", dlc_name, dlc_id);
dlc_details.push(DlcInfo {
appid: dlc_id,
name: dlc_name,
});
} else if dlc_response.status() == reqwest::StatusCode::TOO_MANY_REQUESTS {
// If rate limited, wait longer
error!("Rate limited by Steam API, waiting 10 seconds");
tokio::time::sleep(Duration::from_secs(10)).await;
} }
} }
// Source 2 - depots[*].dlcappid
if let Some(depots) = info.get("depots").and_then(|d| d.as_object()) {
for (_key, depot) in depots {
if let Some(dlc_id) = depot.get("dlcappid").and_then(|v| {
v.as_u64()
.map(|n| n.to_string())
.or_else(|| v.as_str().map(|s| s.to_string()))
}) {
if !dlc_id.is_empty() {
ids.insert(dlc_id);
}
}
}
}
let mut sorted: Vec<String> = ids.into_iter().collect();
sorted.sort();
sorted
}
info!( /// Fetches the display name for a single DLC from steamcmd.net.
"Successfully retrieved details for {} DLCs", /// Returns `None` if the call fails or the name is empty / "Unknown".
dlc_details.len() async fn fetch_dlc_name(
); client: &reqwest::Client,
dlc_id: &str,
) -> Option<String> {
let info = fetch_steamcmd_info(client, dlc_id).await?;
let name = info
.get("common")
.and_then(|c| c.get("name"))
.and_then(|n| n.as_str())?;
if name.is_empty() || name.eq_ignore_ascii_case("unknown") {
return None;
}
Some(name.to_string())
}
// Fetch DLC details from steamcmd.net (simple version without progress)
pub async fn fetch_dlc_details(app_id: &str) -> Result<Vec<DlcInfo>, String> {
info!("Fetching DLC list via steamcmd.net for game ID: {}", app_id);
let client = reqwest::Client::new();
// Step 1: get game info → extract DLC IDs
let game_info = fetch_steamcmd_info(&client, app_id)
.await
.ok_or_else(|| format!("steamcmd.net returned no data for app {}", app_id))?;
let dlc_ids = extract_dlc_ids(&game_info);
info!("Found {} DLC IDs for game {}", dlc_ids.len(), app_id);
// Step 2: fetch name for each ID; skip any that resolve to Unknown
let mut dlc_details = Vec::new();
for dlc_id in &dlc_ids {
tokio::time::sleep(Duration::from_millis(100)).await;
match fetch_dlc_name(&client, dlc_id).await {
Some(name) => {
info!("Found DLC: {} ({})", name, dlc_id);
dlc_details.push(DlcInfo {
appid: dlc_id.clone(),
name,
});
}
None => {
info!("Skipping DLC {} - no name / unknown", dlc_id);
}
}
}
info!("Successfully retrieved {} named DLCs", dlc_details.len());
Ok(dlc_details) Ok(dlc_details)
} }
// Fetch DLC details from Steam API with progress updates // Fetch DLC details from steamcmd.net with progress updates
pub async fn fetch_dlc_details_with_progress( pub async fn fetch_dlc_details_with_progress(
app_id: &str, app_id: &str,
app_handle: &tauri::AppHandle, app_handle: &tauri::AppHandle,
@@ -758,80 +784,46 @@ pub async fn fetch_dlc_details_with_progress(
"Starting DLC details fetch with progress for game ID: {}", "Starting DLC details fetch with progress for game ID: {}",
app_id app_id
); );
// Get a reference to a cancellation flag from app state
let state = app_handle.state::<AppState>(); let state = app_handle.state::<AppState>();
let should_cancel = state.fetch_cancellation.clone(); let should_cancel = state.fetch_cancellation.clone();
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let base_url = format!(
"https://store.steampowered.com/api/appdetails?appids={}", // Step 1: fetch game info to get DLC ID list
app_id
);
// Emit initial progress
emit_dlc_progress(app_handle, "Looking up game details...", 5, None); emit_dlc_progress(app_handle, "Looking up game details...", 5, None);
info!("Emitted initial DLC progress: 5%");
let game_info = fetch_steamcmd_info(&client, app_id)
let response = client
.get(&base_url)
.timeout(Duration::from_secs(10))
.send()
.await .await
.map_err(|e| format!("Failed to fetch game details: {}", e))?; .ok_or_else(|| format!("steamcmd.net returned no data for app {}", app_id))?;
if !response.status().is_success() { let dlc_ids = extract_dlc_ids(&game_info);
let error_msg = format!("Failed to fetch game details: HTTP {}", response.status()); let total_dlcs = dlc_ids.len();
error!("{}", error_msg);
return Err(error_msg); info!("Found {} DLC IDs for game {}", total_dlcs, app_id);
}
let data: serde_json::Value = response
.json()
.await
.map_err(|e| format!("Failed to parse response: {}", e))?;
let dlc_ids = match data
.get(app_id)
.and_then(|app| app.get("data"))
.and_then(|data| data.get("dlc"))
{
Some(dlc_array) => match dlc_array.as_array() {
Some(array) => array
.iter()
.filter_map(|id| id.as_u64().map(|n| n.to_string()))
.collect::<Vec<String>>(),
_ => Vec::new(),
},
_ => Vec::new(),
};
info!("Found {} DLCs for game ID {}", dlc_ids.len(), app_id);
emit_dlc_progress( emit_dlc_progress(
app_handle, app_handle,
&format!("Found {} DLCs. Fetching details...", dlc_ids.len()), &format!("Found {} DLCs. Fetching details...", total_dlcs),
10, 10,
None, None,
); );
info!("Emitted DLC progress: 10%, found {} DLCs", dlc_ids.len());
// Step 2: fetch each DLC name, emit as we go, skip unknowns
let mut dlc_details = Vec::new(); let mut dlc_details = Vec::new();
let total_dlcs = dlc_ids.len();
for (index, dlc_id) in dlc_ids.iter().enumerate() { for (index, dlc_id) in dlc_ids.iter().enumerate() {
// Check if cancellation was requested // Check for cancellation
if should_cancel.load(Ordering::SeqCst) { if should_cancel.load(Ordering::SeqCst) {
info!("DLC fetch cancelled for game {}", app_id); info!("DLC fetch cancelled for game {}", app_id);
return Err("Operation cancelled by user".to_string()); return Err("Operation cancelled by user".to_string());
} }
let progress_percent = 10.0 + (index as f32 / total_dlcs as f32) * 90.0; let progress_rounded = (10.0 + (index as f32 / total_dlcs as f32) * 90.0) as u32;
let progress_rounded = progress_percent as u32; let remaining = total_dlcs - index;
let remaining_dlcs = total_dlcs - index;
let est_time_left = if remaining > 0 {
// Estimate time remaining (rough calculation - 300ms per DLC) // steamcmd is ~100 ms/request, much faster than the old 300 ms Steam API
let est_time_left = if remaining_dlcs > 0 { let seconds = (remaining as f32 * 0.15).ceil() as u32;
let seconds = (remaining_dlcs as f32 * 0.3).ceil() as u32;
if seconds < 60 { if seconds < 60 {
format!("~{} seconds", seconds) format!("~{} seconds", seconds)
} else { } else {
@@ -840,12 +832,12 @@ pub async fn fetch_dlc_details_with_progress(
} else { } else {
"almost done".to_string() "almost done".to_string()
}; };
info!( info!(
"Processing DLC {}/{} - Progress: {}%", "Processing DLC {}/{} ({})",
index + 1, index + 1,
total_dlcs, total_dlcs,
progress_rounded dlc_id
); );
emit_dlc_progress( emit_dlc_progress(
app_handle, app_handle,
@@ -853,73 +845,37 @@ pub async fn fetch_dlc_details_with_progress(
progress_rounded, progress_rounded,
Some(&est_time_left), Some(&est_time_left),
); );
let dlc_url = format!( // Small delay to be polite to the API
"https://store.steampowered.com/api/appdetails?appids={}", tokio::time::sleep(Duration::from_millis(100)).await;
dlc_id
); match fetch_dlc_name(&client, dlc_id).await {
Some(name) => {
// Add a small delay to avoid rate limiting info!("Found DLC: {} ({})", name, dlc_id);
tokio::time::sleep(Duration::from_millis(300)).await; let dlc_info = DlcInfo {
appid: dlc_id.clone(),
let dlc_response = client name,
.get(&dlc_url) };
.timeout(Duration::from_secs(10))
.send() // Emit immediately so the UI updates as DLCs arrive
.await if let Ok(json) = serde_json::to_string(&dlc_info) {
.map_err(|e| format!("Failed to fetch DLC details: {}", e))?; if let Err(e) = app_handle.emit("dlc-found", json) {
warn!("Failed to emit dlc-found event: {}", e);
if dlc_response.status().is_success() { }
let dlc_data: serde_json::Value = dlc_response
.json()
.await
.map_err(|e| format!("Failed to parse DLC response: {}", e))?;
let dlc_name = match dlc_data
.get(&dlc_id)
.and_then(|app| app.get("data"))
.and_then(|data| data.get("name"))
{
Some(name) => match name.as_str() {
Some(s) => s.to_string(),
_ => "Unknown DLC".to_string(),
},
_ => "Unknown DLC".to_string(),
};
info!("Found DLC: {} ({})", dlc_name, dlc_id);
let dlc_info = DlcInfo {
appid: dlc_id.clone(),
name: dlc_name,
};
// Emit each DLC as we find it
if let Ok(json) = serde_json::to_string(&dlc_info) {
if let Err(e) = app_handle.emit("dlc-found", json) {
warn!("Failed to emit dlc-found event: {}", e);
} else {
info!("Emitted dlc-found event for DLC: {}", dlc_id);
} }
dlc_details.push(dlc_info);
}
None => {
info!("Skipping DLC {} - no name / unknown", dlc_id);
} }
dlc_details.push(dlc_info);
} else if dlc_response.status() == reqwest::StatusCode::TOO_MANY_REQUESTS {
// If rate limited, wait longer
error!("Rate limited by Steam API, waiting 10 seconds");
emit_dlc_progress(
app_handle,
"Rate limited by Steam. Waiting...",
progress_rounded,
None,
);
tokio::time::sleep(Duration::from_secs(10)).await;
} }
} }
// Final progress update
info!( info!(
"Completed DLC fetch. Found {} DLCs in total", "Completed DLC fetch. Found {} named DLCs out of {} IDs",
dlc_details.len() dlc_details.len(),
total_dlcs
); );
emit_dlc_progress( emit_dlc_progress(
app_handle, app_handle,
@@ -927,8 +883,7 @@ pub async fn fetch_dlc_details_with_progress(
100, 100,
None, None,
); );
info!("Emitted final DLC progress: 100%");
Ok(dlc_details) Ok(dlc_details)
} }

View File

@@ -19,7 +19,7 @@
}, },
"productName": "Creamlinux", "productName": "Creamlinux",
"mainBinaryName": "creamlinux", "mainBinaryName": "creamlinux",
"version": "1.5.5", "version": "1.5.6",
"identifier": "com.creamlinux.dev", "identifier": "com.creamlinux.dev",
"app": { "app": {
"withGlobalTauri": false, "withGlobalTauri": false,