mirror of
https://github.com/Novattz/creamlinux-installer.git
synced 2026-05-11 00:59:34 -04:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85873379f3 | ||
|
|
293273af2d | ||
|
|
550d183ee2 | ||
|
|
756a6e3d53 | ||
|
|
cbed56ac33 | ||
|
|
fbabff0a34 | ||
|
|
fb6203c4f7 | ||
|
|
9144e52db1 | ||
|
|
e50f885ba2 | ||
|
|
433417ade2 | ||
|
|
6f5d750af9 | ||
|
|
ef7e183312 | ||
|
|
3b4a17b615 | ||
|
|
6ff6c06bec |
35
.github/workflows/build.yml
vendored
35
.github/workflows/build.yml
vendored
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
28
README.md
28
README.md
@@ -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.
|
||||||
|
|||||||
61
default.nix
61
default.nix
@@ -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
4
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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
72
package.nix
Normal 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
2
src-tauri/Cargo.lock
generated
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user