36 Commits
main ... v2.4.2

Author SHA1 Message Date
Maarten
b3f8b9360d refactor(workflow): only run action on manual trigger 2024-05-04 14:17:25 +02:00
dependabot[bot]
27b474ca3c chore(deps-dev): bump electron from 29.2.0 to 30.0.2 (#182)
Bumps [electron](https://github.com/electron/electron) from 29.2.0 to 30.0.2.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v29.2.0...v30.0.2)

---
updated-dependencies:
- dependency-name: electron
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-04 13:53:37 +02:00
dependabot[bot]
68ccf63f66 chore(deps-dev): bump eslint from 8.57.0 to 9.1.1 (#177)
Bumps [eslint](https://github.com/eslint/eslint) from 8.57.0 to 9.1.1.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.57.0...v9.1.1)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-04 13:53:22 +02:00
dependabot[bot]
20567d8f8b chore(deps-dev): bump ejs from 3.1.9 to 3.1.10 (#180)
Bumps [ejs](https://github.com/mde/ejs) from 3.1.9 to 3.1.10.
- [Release notes](https://github.com/mde/ejs/releases)
- [Commits](https://github.com/mde/ejs/compare/v3.1.9...v3.1.10)

---
updated-dependencies:
- dependency-name: ejs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-04 13:53:01 +02:00
Maarten van Heusden
c503c2ce51 feat: phase out donations to me 2024-05-04 13:51:47 +02:00
Maarten van Heusden
c403f14e65 chore(docs): rephrase parts of readme.md 2024-05-04 13:43:28 +02:00
Maarten van Heusden
070b5e7d10 chore: update to v2.4.2 2024-05-04 13:23:47 +02:00
Maarten
5b9838cf85 fix(utils): escape % character in passwords (#181) 2024-05-04 13:18:21 +02:00
Maarten
bb978b411f Merge pull request #175 from mmvanheusden/dependabot/npm_and_yarn/electron-29.2.0
chore(deps-dev): bump electron from 29.1.6 to 29.2.0
2024-04-09 08:00:51 +02:00
dependabot[bot]
b2160eda35 chore(deps-dev): bump electron from 29.1.6 to 29.2.0
Bumps [electron](https://github.com/electron/electron) from 29.1.6 to 29.2.0.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v29.1.6...v29.2.0)

---
updated-dependencies:
- dependency-name: electron
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-08 21:03:10 +00:00
Maarten
75488bff61 Merge pull request #173 from mmvanheusden/dependabot/npm_and_yarn/electron-29.1.6
chore(deps-dev): bump electron from 29.1.5 to 29.1.6
2024-04-02 12:35:25 +02:00
dependabot[bot]
60895e972a chore(deps-dev): bump electron from 29.1.5 to 29.1.6
Bumps [electron](https://github.com/electron/electron) from 29.1.5 to 29.1.6.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v29.1.5...v29.1.6)

---
updated-dependencies:
- dependency-name: electron
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-01 21:12:04 +00:00
Maarten
6ac1697c2f Merge pull request #172 from mmvanheusden/dependabot/npm_and_yarn/electron-29.1.5
chore(deps-dev): bump electron from 29.1.4 to 29.1.5
2024-03-27 07:38:01 +01:00
dependabot[bot]
88bca2d35a chore(deps-dev): bump electron from 29.1.4 to 29.1.5
Bumps [electron](https://github.com/electron/electron) from 29.1.4 to 29.1.5.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v29.1.4...v29.1.5)

---
updated-dependencies:
- dependency-name: electron
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-25 21:25:36 +00:00
Maarten
0ef5469943 Merge pull request #171 from mmvanheusden/dependabot/npm_and_yarn/electron-29.1.4
chore(deps-dev): bump electron from 29.1.1 to 29.1.4
2024-03-23 11:15:55 +01:00
dependabot[bot]
93f2718f0d chore(deps-dev): bump electron from 29.1.1 to 29.1.4
Bumps [electron](https://github.com/electron/electron) from 29.1.1 to 29.1.4.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v29.1.1...v29.1.4)

---
updated-dependencies:
- dependency-name: electron
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-18 21:28:55 +00:00
Maarten
0524c3231f Merge pull request #169 from mmvanheusden/dependabot/npm_and_yarn/electron-29.1.1
chore(deps-dev): bump electron from 29.1.0 to 29.1.1
2024-03-17 08:01:53 +01:00
Maarten
8d494e783c Merge pull request #170 from mmvanheusden/dependabot/npm_and_yarn/follow-redirects-1.15.6
chore(deps): bump follow-redirects from 1.15.5 to 1.15.6
2024-03-17 08:01:26 +01:00
dependabot[bot]
0befc10ce2 chore(deps): bump follow-redirects from 1.15.5 to 1.15.6
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.5 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.5...v1.15.6)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-16 23:55:20 +00:00
dependabot[bot]
2b1a287ad8 chore(deps-dev): bump electron from 29.1.0 to 29.1.1
Bumps [electron](https://github.com/electron/electron) from 29.1.0 to 29.1.1.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v29.1.0...v29.1.1)

---
updated-dependencies:
- dependency-name: electron
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-11 21:27:10 +00:00
Maarten
2f85e861b6 feat: npm workflow cache 2024-03-05 10:43:52 +01:00
Maarten
11cb943d53 chore: update build workflow 2024-03-05 10:36:33 +01:00
Maarten
3c840bbdb7 chore: update readme stats action 2024-03-05 10:27:57 +01:00
Maarten
c418185984 Merge pull request #168 from mmvanheusden/dependabot/npm_and_yarn/electron-29.1.0
chore(deps-dev): bump electron from 29.0.1 to 29.1.0
2024-03-05 10:20:41 +01:00
dependabot[bot]
2663ad629c chore(deps-dev): bump electron from 29.0.1 to 29.1.0
Bumps [electron](https://github.com/electron/electron) from 29.0.1 to 29.1.0.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v29.0.1...v29.1.0)

---
updated-dependencies:
- dependency-name: electron
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-05 06:05:55 +00:00
Maarten
a7a98c06e0 Merge pull request #167 from mmvanheusden/dependabot/npm_and_yarn/electron-builder-24.13.3
chore(deps-dev): bump electron-builder from 24.12.0 to 24.13.3
2024-03-05 07:04:55 +01:00
Maarten
e17098e412 Merge pull request #166 from mmvanheusden/dependabot/npm_and_yarn/app-builder-lib-and-electron-builder-24.13.3
chore(deps): bump app-builder-lib and electron-builder
2024-03-05 07:04:37 +01:00
dependabot[bot]
52ee6cd822 chore(deps-dev): bump electron-builder from 24.12.0 to 24.13.3
Bumps [electron-builder](https://github.com/electron-userland/electron-builder/tree/HEAD/packages/electron-builder) from 24.12.0 to 24.13.3.
- [Release notes](https://github.com/electron-userland/electron-builder/releases)
- [Changelog](https://github.com/electron-userland/electron-builder/blob/master/packages/electron-builder/CHANGELOG.md)
- [Commits](https://github.com/electron-userland/electron-builder/commits/v24.13.3/packages/electron-builder)

---
updated-dependencies:
- dependency-name: electron-builder
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-04 21:50:14 +00:00
dependabot[bot]
d0b42d36e8 chore(deps): bump app-builder-lib and electron-builder
Bumps [app-builder-lib](https://github.com/electron-userland/electron-builder/tree/HEAD/packages/app-builder-lib) to 24.13.3 and updates ancestor dependency [electron-builder](https://github.com/electron-userland/electron-builder/tree/HEAD/packages/electron-builder). These dependencies need to be updated together.


Updates `app-builder-lib` from 24.12.0 to 24.13.3
- [Release notes](https://github.com/electron-userland/electron-builder/releases)
- [Changelog](https://github.com/electron-userland/electron-builder/blob/master/packages/app-builder-lib/CHANGELOG.md)
- [Commits](https://github.com/electron-userland/electron-builder/commits/v24.13.3/packages/app-builder-lib)

Updates `electron-builder` from 24.12.0 to 24.13.3
- [Release notes](https://github.com/electron-userland/electron-builder/releases)
- [Changelog](https://github.com/electron-userland/electron-builder/blob/master/packages/electron-builder/CHANGELOG.md)
- [Commits](https://github.com/electron-userland/electron-builder/commits/v24.13.3/packages/electron-builder)

---
updated-dependencies:
- dependency-name: app-builder-lib
  dependency-type: indirect
- dependency-name: electron-builder
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-04 21:19:00 +00:00
Maarten
8a0249af18 chore: update AUR URL in README
@zxp19821005
2024-02-29 21:05:10 +01:00
Maarten
849155dc45 Merge pull request #164 from mmvanheusden/dependabot/npm_and_yarn/electron-builder-24.12.0
chore(deps-dev): bump electron-builder from 24.9.1 to 24.12.0
2024-02-27 13:59:13 +01:00
dependabot[bot]
83bff692f5 chore(deps-dev): bump electron-builder from 24.9.1 to 24.12.0
Bumps [electron-builder](https://github.com/electron-userland/electron-builder/tree/HEAD/packages/electron-builder) from 24.9.1 to 24.12.0.
- [Release notes](https://github.com/electron-userland/electron-builder/releases)
- [Changelog](https://github.com/electron-userland/electron-builder/blob/master/packages/electron-builder/CHANGELOG.md)
- [Commits](https://github.com/electron-userland/electron-builder/commits/v24.12.0/packages/electron-builder)

---
updated-dependencies:
- dependency-name: electron-builder
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-27 12:58:56 +00:00
Maarten
0ba456df4f Merge pull request #165 from mmvanheusden/dependabot/npm_and_yarn/electron-29.0.1
chore(deps-dev): bump electron from 28.2.2 to 29.0.1
2024-02-27 13:58:20 +01:00
Maarten
97d1e6ffd1 Merge pull request #163 from mmvanheusden/dependabot/npm_and_yarn/eslint-8.57.0
chore(deps-dev): bump eslint from 8.56.0 to 8.57.0
2024-02-27 13:58:06 +01:00
dependabot[bot]
32559b7b4a chore(deps-dev): bump electron from 28.2.2 to 29.0.1
Bumps [electron](https://github.com/electron/electron) from 28.2.2 to 29.0.1.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v28.2.2...v29.0.1)

---
updated-dependencies:
- dependency-name: electron
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-26 21:38:01 +00:00
dependabot[bot]
1b4115cd70 chore(deps-dev): bump eslint from 8.56.0 to 8.57.0
Bumps [eslint](https://github.com/eslint/eslint) from 8.56.0 to 8.57.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.56.0...v8.57.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-26 21:36:57 +00:00
67 changed files with 6096 additions and 10218 deletions

21
.eslintrc.yml Normal file
View File

@@ -0,0 +1,21 @@
env:
browser: true
es2021: true
node: true
extends: eslint:recommended
parserOptions:
ecmaVersion: latest
sourceType: module
rules:
indent:
- error
- tab
linebreak-style:
- error
- unix
quotes:
- error
- double
semi:
- error
- never

2
.github/FUNDING.yml vendored
View File

@@ -1 +1 @@
custom: https://paypal.me/onderkin
custom: https://my.fsf.org/donate

View File

@@ -1,20 +0,0 @@
---
name: Bug report
about: Report a bug in this project
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of the bug.
**Is your bug report related to another bug report? Please describe.**
If yes, the issue number and an explanation of why the bug is related to this one.
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Additional context**
Add any other context or screenshots about the bug report here.

View File

@@ -1,23 +0,0 @@
---
name: Download problem
about: Report a difficulty in downloading a game
title: ''
labels: question
assignees: ''
---
**Describe the game you are trying to download**
A clear and concise description of the bug.
**App ID:** <app id>
**Depot ID:** <depot id>
**Manifest ID:** <manifest id>
**Show the error that DepotDownloader produces**
A screenshot or a copy-paste wrapped in a code-block (` ``` `) of the DepotDownloader terminal output
**Do you own the game?**
If no, you probably can't download the game. You must own the game on Steam to be able to download it.
**Additional context**
Add any other context or screenshots about the issue here.

View File

@@ -2,19 +2,19 @@
name: Feature request
about: Suggest an idea for this project
title: ''
labels: feature
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -3,23 +3,4 @@ updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "monthly"
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-major"]
groups:
npm-deps:
patterns:
- "*"
- package-ecosystem: "cargo"
directory: "src-tauri/"
schedule:
interval: "monthly"
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-major"]
groups:
cargo-deps:
patterns:
- "*"
interval: "weekly"

42
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,42 @@
on:
workflow_dispatch
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Check out repositoty
uses: actions/checkout@v4
- name: Set up nodejs
uses: actions/setup-node@v4
with:
cache: 'npm'
node-version: 20
- name: Set up npm project
run: npm install
- name: Build project
run: npm run build
- name: Move x64 exe
run: cp ./dist/SteamDepotDownloaderGUI*.exe ./SteamDepotDownloaderGUI-${{ github.run_number }}.exe
- name: Move x64 AppImage
run: cp `ls -d1 dist/* | grep -E "SteamDepotDownloaderGUI-[0-9]+\.[0-9]+\.[0-9]+\.AppImage"` ./SteamDepotDownloaderGUI-${{ github.run_number }}.AppImage
- name: Move x64 zip
run: cp `ls -d1 dist/* | grep -E "steamdepotdownloadergui-[0-9]+\.[0-9]+\.[0-9]+\.zip"` ./SteamDepotDownloaderGUI-${{ github.run_number }}.zip
- name: Move arm64 AppImage
run: cp `ls -d1 dist/* | grep -E "SteamDepotDownloaderGUI-[0-9]+\.[0-9]+\.[0-9]+\-arm64.AppImage"` ./SteamDepotDownloaderGUI-${{ github.run_number }}.AppImage
- name: Move arm64 zip
run: cp `ls -d1 dist/* | grep -E "steamdepotdownloadergui-[0-9]+\.[0-9]+\.[0-9]+\-arm64.zip"` ./SteamDepotDownloaderGUI-${{ github.run_number }}-arm64.zip
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: SteamDepotDownloaderGUI-${{ github.run_number }}
path: SteamDepotDownloaderGUI-${{ github.run_number }}*.*

View File

@@ -1,101 +0,0 @@
name: 'build_debug'
on:
workflow_dispatch:
jobs:
build-tauri:
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
- platform: 'macos-latest'
arch: 'aarch64'
args: '--target aarch64-apple-darwin --bundles dmg'
- platform: 'macos-latest'
arch: 'x86_64'
args: '--target x86_64-apple-darwin --bundles dmg'
- platform: 'ubuntu-22.04'
args: '--bundles appimage'
- platform: 'windows-latest'
args: ''
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- name: install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
- name: setup pnpm
uses: pnpm/action-setup@v4
- name: setup node
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'pnpm'
- name: install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: './src-tauri -> target'
- name: install frontend dependencies
# If you don't have `beforeBuildCommand` configured you may want to build your frontend here too.
run: pnpm install # change this to npm or pnpm depending on which one you use.
- uses: tauri-apps/tauri-action@v0
id: build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: ${{ matrix.args }}
includeUpdaterJson: false
includeRelease: 'false'
includeDebug: 'true'
- name: fix JSON
if: matrix.platform != 'windows-latest'
id: truncate_paths
run: echo "paths=$(echo '${{ steps.build.outputs.artifactPaths }}' | sed 's/^..//' | sed 's/..$//')" >> $GITHUB_OUTPUT
- name: upload macos artifacts (M1)
if: matrix.platform == 'macos-latest' && matrix.arch == 'aarch64'
uses: actions/upload-artifact@v4
with:
name: macos-m1-artifacts
path: ${{ steps.truncate_paths.outputs.paths }}
- name: upload macos artifacts (Intel)
if: matrix.platform == 'macos-latest' && matrix.arch == 'x86_64'
uses: actions/upload-artifact@v4
with:
name: macos-intel-artifacts
path: ${{ steps.truncate_paths.outputs.paths }}
- name: upload linux artifacts
if: matrix.platform == 'ubuntu-22.04'
uses: actions/upload-artifact@v4
with:
name: linux-artifacts
path: ${{ steps.truncate_paths.outputs.paths }}
- name: upload windows artifacts
if: matrix.platform == 'windows-latest'
uses: actions/upload-artifact@v4
with:
name: windows-artifacts
path: "./src-tauri/target/debug/bundle/*" # fck windows

View File

@@ -1,99 +0,0 @@
name: 'build_release'
on:
workflow_dispatch:
jobs:
build-tauri:
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
- platform: 'macos-latest'
arch: 'aarch64'
args: '--target aarch64-apple-darwin --bundles dmg'
- platform: 'macos-latest'
arch: 'x86_64'
args: '--target x86_64-apple-darwin --bundles dmg'
- platform: 'ubuntu-22.04'
args: '--bundles appimage'
- platform: 'windows-latest'
args: ''
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- name: install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
- name: setup pnpm
uses: pnpm/action-setup@v4
- name: setup node
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'pnpm'
- name: install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: './src-tauri -> target'
- name: install frontend dependencies
# If you don't have `beforeBuildCommand` configured you may want to build your frontend here too.
run: pnpm install # change this to npm or pnpm depending on which one you use.
- uses: tauri-apps/tauri-action@v0
id: build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: ${{ matrix.args }}
includeUpdaterJson: false
- name: fix JSON
if: matrix.platform != 'windows-latest'
id: truncate_paths
run: echo "paths=$(echo '${{ steps.build.outputs.artifactPaths }}' | sed 's/^..//' | sed 's/..$//')" >> $GITHUB_OUTPUT
- name: upload macos artifacts (M1)
if: matrix.platform == 'macos-latest' && matrix.arch == 'aarch64'
uses: actions/upload-artifact@v4
with:
name: macos-m1-artifacts
path: ${{ steps.truncate_paths.outputs.paths }}
- name: upload macos artifacts (Intel)
if: matrix.platform == 'macos-latest' && matrix.arch == 'x86_64'
uses: actions/upload-artifact@v4
with:
name: macos-intel-artifacts
path: ${{ steps.truncate_paths.outputs.paths }}
- name: upload linux artifacts
if: matrix.platform == 'ubuntu-22.04'
uses: actions/upload-artifact@v4
with:
name: linux-artifacts
path: ${{ steps.truncate_paths.outputs.paths }}
- name: upload windows artifacts
if: matrix.platform == 'windows-latest'
uses: actions/upload-artifact@v4
with:
name: windows-artifacts
path: "./src-tauri/target/release/bundle/*" # fck windows

19
.github/workflows/github-repo-stats.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
name: github-repo-stats
on:
schedule:
# Run this once per day, towards the end of the day for keeping the most
# recent data point most meaningful (hours are interpreted in UTC).
- cron: "0 23 * * *"
workflow_dispatch: # Allow for running this manually.
jobs:
j1:
name: github-repo-stats
runs-on: ubuntu-latest
steps:
- name: run-ghrs
# Use latest release.
uses: jgehrcke/github-repo-stats@v1.4.2
with:
ghtoken: ${{ secrets.ghrs_github_api_token }}

153
.gitignore vendored
View File

@@ -4,29 +4,140 @@ logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
.pnpm-debug.log*
node_modules
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.iml
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# SteamDepotDownloaderGUI files
src-tauri/depotdownloader
src-tauri/*.zip
src-tauri/*.exe
**/DepotDownloader
**/DepotDownloader.xml
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# VSCode directory for storing user settings
.vscode
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# IntelliJ
.idea/
# SteamDepotDownloaderGUI specific
depotdownloader
games
*.zip
run.bat
run.sh

View File

@@ -1,6 +0,0 @@
{
"recommendations": [
"tauri-apps.tauri-vscode",
"rust-lang.rust-analyzer"
]
}

View File

126
README.md
View File

@@ -1,74 +1,106 @@
<div align="center">
<h1>SteamDepotDownloaderGUI</h1>
<h4>A graphical wrapper for DepotDownloader, designed to make downloading older versions of Steam games easy.</h4>
<a href="https://www.youtube.com/watch?v=H2COwT5OUOo" target="_blank"><b>Tutorial</b></a> ~
<a href="https://depotdownloader.aphex.cc/" target="_blank"><b>Website</b></a> ~
<a href="https://www.youtube.com/watch?v=ogiDAuH3VdY" target="_blank"><b>Downgrade Subnautica</b></a>
<h1 align="center">SteamDepotDownloaderGUI
<h4 align="center">A graphical wrapper for DepotDownloader, designed to make downloading older versions of Steam games easy. built using <a href="https://www.electronjs.org" target="_blank">Electron</a>.
<br><br>
<a href="https://www.youtube.com/watch?v=H2COwT5OUOo" target="_blank"><b>Tutorial</b></a> ~
<a href="https://github.com/mmvanheusden/SteamDepotDownloaderGUI/tree/steamdeck-dotnet#steamdepotdownloadergui-for-steam-deck" target="_blank"><b>Steam Deck</b></a> ~
<a href="https://www.youtube.com/watch?v=ogiDAuH3VdY" target="_blank"><b>Subnautica</b></a>
</h4>
<a href="https://img.shields.io/github/last-commit/mmvanheusden/SteamDepotDownloaderGUI?color=crimson"><img src="https://img.shields.io/github/last-commit/mmvanheusden/SteamDepotDownloaderGUI?color=crimson" alt="Last contribution badge"></a>
<a href="https://github.com/mmvanheusden/SteamDepotDownloaderGUI/releases/latest"><img src="https://img.shields.io/badge/Download -ffbd03?style=for-the-badge&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAABmJLR0QA/wD/AP+gvaeTAAABd0lEQVRoge2XwUrDQBCGPz3Ee++2UBSvdsGbeLHeFX0lPQhei+8g2AfwSVREK+LJ1oqHUGjRQ7Z0laRJNtldKPPBwoTM7D9/sks2IAiCsKrsAK/AC7DtSmTd1cTAKbAJtIAzVyIuDURGvOFKxKUBL4iB0IiB0IiB0IiB0IiB0IiB0IiB0IgBoA08Ak+AKpCvdO6AGn416zBwDGyRGLljuQmlc9pAEzipKl6HgVtgpOMGSYOdlLyOvtfQ1yPgpgb9WlDAEPjRYwj0jet+yv0iy80ru8AHiyazxiewF6jHXPJMeG3+CoiBy5J1/5dTlWVzAXwD5yXrAJho4YlFbdqesFnzsa6PLWr/PD0bFPAA3GO/YXN7WMspLpLnktweVvooMTPiKDPLHabmLCtpmYF3I96v3E55Doz4zWaCaxYb6Bno4udNRMCR1pzr92wmagFfZH+UfI0xycHPim5gE2Pg0Lb5OU2SVzgAph6anmqtHhWevCAIgh9+AdLMtu/CZhHJAAAAAElFTkSuQmCC" alt="Download latest release badge"></a>
<a href="https://github.com/mmvanheusden/SteamDepotDownloaderGUI/releases/latest"><img src="https://img.shields.io/github/downloads/mmvanheusden/SteamDepotDownloaderGUI/total?color=orange&label=downloads" alt="Download count badge"></a>
<p align="center"><a href="https://github.com/mmvanheusden/SteamDepotDownloaderGUI/releases/latest"><img src="https://img.shields.io/github/downloads/mmvanheusden/SteamDepotDownloaderGUI/total?color=orange&label=downloads" alt="Download count badge"></a><a href="https://img.shields.io/github/last-commit/mmvanheusden/SteamDepotDownloaderGUI?color=crimson"><img src="https://img.shields.io/github/last-commit/mmvanheusden/SteamDepotDownloaderGUI?color=crimson" alt="Last contribution badge"></a><a href="https://github.com/mmvanheusden/SteamDepotDownloaderGUI/releases/latest"><img src="https://img.shields.io/badge/Download -ffbd03?style=for-the-badge&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAABmJLR0QA/wD/AP+gvaeTAAABd0lEQVRoge2XwUrDQBCGPz3Ee++2UBSvdsGbeLHeFX0lPQhei+8g2AfwSVREK+LJ1oqHUGjRQ7Z0laRJNtldKPPBwoTM7D9/sks2IAiCsKrsAK/AC7DtSmTd1cTAKbAJtIAzVyIuDURGvOFKxKUBL4iB0IiB0IiB0IiB0IiB0IiB0IiB0IgBoA08Ak+AKpCvdO6AGn416zBwDGyRGLljuQmlc9pAEzipKl6HgVtgpOMGSYOdlLyOvtfQ1yPgpgb9WlDAEPjRYwj0jet+yv0iy80ru8AHiyazxiewF6jHXPJMeG3+CoiBy5J1/5dTlWVzAXwD5yXrAJho4YlFbdqesFnzsa6PLWr/PD0bFPAA3GO/YXN7WMspLpLnktweVvooMTPiKDPLHabmLCtpmYF3I96v3E55Doz4zWaCaxYb6Bno4udNRMCR1pzr92wmagFfZH+UfI0xycHPim5gE2Pg0Lb5OU2SVzgAph6anmqtHhWevCAIgh9+AdLMtu/CZhHJAAAAAElFTkSuQmCC" alt="Download latest release badge"></a><a href="https://img.shields.io/endpoint?url=https%3A%2F%2Fhits.dwyl.com%2Fmmvanheusden%2Fsteamdepotdownloadergui.json&color=lightblue"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fhits.dwyl.com%2Fmmvanheusden%2Fsteamdepotdownloadergui.json&color=lightblue" alt="Visitor count badge"></a><a href="https://my.fsf.org/donate"><img src="https://img.shields.io/badge/Donate-%23A42E2B?logo=gnu" alt="Donation badge"></a><a href="https://www.codefactor.io/repository/github/mmvanheusden/steamdepotdownloadergui/badge/master"><img src="https://www.codefactor.io/repository/github/mmvanheusden/steamdepotdownloadergui/badge/master" alt="CodeFactor badge"></a></p>
<img src="https://github.com/user-attachments/assets/e2de938d-e54d-4f30-a405-b0d9d2d8e4d9" alt="Steam downgrader interface" />
</div>
<p align="center">
<img alt="Screenshot of the downloader interface" src="screenshot.png"
style="max-width: 60%;"/>
</p>
## Features
> ↓ *The downloader in action:*<br>
> <img src="https://github.com/user-attachments/assets/7d8a97b8-79df-4c54-a74b-fb7b4b85913c" alt="Steam downgrader process" style="width: 25svw;"/>
* **Cross platform**
- Windows
- <s>macOS</s> (in development™)
- Linux
- [Steam Deck](https://github.com/mmvanheusden/SteamDepotDownloaderGUI/tree/steamdeck-dotnet#steamdepotdownloadergui-for-steam-deck)
* **Cross terminal**
- Gnome Terminal
- Konsole
- xfce4-terminal
- Terminator
- Terminology
- xterm
- Kitty
- LXTerminal
- Tilix
- Deepin Terminal
- cool-retro-term
- Manual: produce a script that can be copy pasted into a terminal of choice
* **Support for anonymous downloads**
## Rewrite
Computer resources are not an all-you-can-eat buffet. The executables weigh on average 103 MB.
That's why in the future, this project is fully switching to a different toolset, probably [gtk-rs](https://gtk-rs.org/).
This framework is more suited as we don't need a whole browser engine just to download files.
### Cross-platform support
| OS | Support |
|---------|---------|
| Windows | ✅ |
| Linux | ✅ |
| macOS | ✅ |
## Download
### Windows:
* If you haven't already, install [.NET SDK 6.0](https://dotnet.microsoft.com/en-us/download/dotnet/6.0) on your computer.
Then, download the [latest Windows build](https://github.com/mmvanheusden/SteamDepotDownloaderGUI/releases/latest)
### **Linux:**
#### Arch Linux:
* There is an [AUR package](https://aur.archlinux.org/packages/steamdepotdownloadergui-bin) made. this can be installed using `yay -S steamdepotdownloadergui-bin`.
**This is not maintained by me!!**
#### Other Linux distros:
* **You'll need `unzip`, .NET SDK 6.0.**
* **Ubuntu:**
`sudo apt install unzip dotnet-sdk-6.0`
* **Fedora:**
`sudo dnf install unzip dotnet-sdk-6.0`
* There are several options, AppImage, and a `.zip`, for both x64 and arm64.
[Download latest release](https://github.com/mmvanheusden/SteamDepotDownloaderGUI/releases/latest)
> [!TIP]
> ## Tutorials
> * https://www.youtube.com/watch?v=H2COwT5OUOo How to download older versions of Steam games tutorial
> * https://www.youtube.com/watch?v=ogiDAuH3VdY How to download older versions of Subnautica tutorial
## How to use
## Installation
> [!CAUTION]
> This GitHub repository is the only official place to download this software.
> If you have paid for this software, or downloaded this from a different place than here, **you are an idiot and at risk**.
#### Tutorials:
* https://www.youtube.com/watch?v=H2COwT5OUOo How to download older versions of Steam games. rollback steam games tutorial
* https://www.youtube.com/watch?v=ogiDAuH3VdY How to download older versions of Subnautica tutorial
---
**Enter everything you normally would in the DepotDownloader console and select your OS/terminal and click download**
### Windows
Download the [latest Windows release](https://github.com/mmvanheusden/SteamDepotDownloaderGUI/releases/latest). There are multiple variants to choose from, but you are probably looking for the file that ends with **`.exe`**.
### Linux
You'll need at least one of the supported terminal emulators. You most likely already have one of these.
Download the [latest Linux release](https://github.com/mmvanheusden/SteamDepotDownloaderGUI/releases/latest). There are multiple formats to choose from.
## Credits
This software makes use of the following projects:
- [**DepotDownloader**](https://github.com/SteamRE/DepotDownloader)
- [Tauri](https://tauri.app)
- [Preact](https://preactjs.com)
- [**DepotDownloader**](https://github.com/SteamRE/DepotDownloader/)
- [Electron](https://www.electronjs.org/)
- [Node.js](https://nodejs.org/)
- [Primer CSS](https://primer.style/css/)
- [follow-redirects](https://github.com/follow-redirects/follow-redirects)
- [Hubut Sans](https://github.com/github/hubot-sans) under [license](https://github.com/github/hubot-sans/blob/05d5ea150c20e6434485db8ffd2277ed18a9e911/LICENSE)
## Donate
You can donate [here](paypal.me/onderkin) or through the **donate** button in the interface.
I prefer not to receive any donations. If you want to make me happy open an issue and say thanks ;)
Please donate to the [Free Software Foundation](https://my.fsf.org/donate) instead.
## Contribute
Every pull request is welcome! ;)
Please cleanup the code using:
```console
$ pnpm eslint --fix src/
```
Please also sort Tailwind classes using [RustyWind](https://github.com/avencera/rustywind).
If you would like to contribute to SteamDepotDownloaderGUI, please make sure to follow
the [contributing instructions and guidelines](contributing.md).
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
Please make sure to keep code clean and cross-platform compatible.
<a href="https://www.gnu.org/licenses/gpl-3.0" target="_blank">
<img src="https://github.com/mmvanheusden/SteamDepotDownloaderGUI/assets/50550545/b5649b7f-ea49-45c4-b0cd-5f3788dcd6ca" height="40px" alt="GPL v3">
</a>
![forthebadge](https://forthebadge.com/images/featured/featured-built-with-love.svg)

61
contributing.md Normal file
View File

@@ -0,0 +1,61 @@
# SteamDepotDownloaderGUI development information
SteamDepotDownloaderGUI is a graphical user interface for DepotDownloader.
It is written in Node.js and uses the [Electron](https://electronjs.org/) framework.
The CSS framework being used is [Primer](https://primer.style), by GitHub.
## Setup development environment
**Note that these instructions may differ from operating system to operating system.**
* Install node.js, npm, and git on your computer.
* Clone the repository.
```bash
git clone https://github.com/mmvanheusden/SteamDepotDownloaderGUI
```
* Move into the directory and install npm dependencies.
```bash
cd SteamDepotDownloaderGUI
npm install
```
* **You are now ready to contribute**
## Testing the application
```bash
npm start
```
## Building the application
```bash
npm run build
```
* If you are using macOS, you can build using the following command:
```bash
npm run buildall
```
This will build the application for all supported operating systems.
## Contributing Guidelines
Please make sure to keep code consistent and cross-platform compatible.
After you made a change, clean-up the code using your favorite code editor and run the following command:
```bash
npx eslint *.js --fix
```
Please make sure your git commit message is descriptive and contains the changes you made.
**Happy coding!**

358
downloader.js Normal file
View File

@@ -0,0 +1,358 @@
// Uses a prebuild binary of https://github.com/SteamRE/DepotDownloader
// License can be found at https://github.com/SteamRE/DepotDownloader/blob/master/LICENSE
const {ipcRenderer, shell} = require("electron")
const {
preDownloadCheck,
download,
runCommand,
removeDir,
removeFile,
unzip,
forceTerminals,
generateRunScript,
createCommandWithTerminal,
platformpath
} = require("./utils")
const electron = require("electron")
const {sep} = require("path")
// Initializes the variable that holds the path to the specified download location
let exportedFile = ""
let ready = true
let app_version
const DOTNET_DOWNLOAD_URL = "https://github.com/SteamRE/DepotDownloader/releases/download/DepotDownloader_2.5.0/depotdownloader-2.5.0.zip" // the url to the depotdownloader zip
const DOTNET_DIR = "depotdownloader" // folder where zip is extracted
const DOTNET_ZIP_FILE = DOTNET_DOWNLOAD_URL.split("/").pop() // the file that is being downloaded.
function submitForm() {
// Check if the form is filled in and if dotnet is installed
preDownloadCheck().then(async function () {
//console.log(terminal[1][0])
document.getElementById("dotnetwarning").hidden = true
document.getElementById("dotnetwarning2").hidden = true
document.getElementById("emptywarning").hidden = true
console.info("dotnet found in PATH")
// create variables for the form values
const username = document.getElementById("username").value
const password = document.getElementById("password").value
const appid = document.getElementById("appid").value
const depotid = document.getElementById("depotid").value
const manifestid = document.getElementById("manifestid").value
const terminal_dropdown = document.getElementById("terminal-dropdown").selectedIndex
const os_dropdown = document.getElementById("os-dropdown").selectedIndex
const foldername_element = document.getElementById("folder-name-custom-input")
// Remove the old depotdownloader directory if there are any
await removeDir("depotdownloader")
// Download a prebuild DepotDownloader binary, so it doesn't have to be included in the source code
await download(DOTNET_DOWNLOAD_URL)
// Unzip the DepotDownloader binary
await unzip(DOTNET_ZIP_FILE, DOTNET_DIR)
// Clean up the old files
await removeFile(DOTNET_ZIP_FILE)
// Generate the run script (run.sh for linux, or run.bat for windows).
await generateRunScript(username, password, appid, depotid, manifestid, foldername_element, exportedFile)
let terminal
let os
if (terminal_dropdown === 11) {
terminal = "auto"
} else {
terminal = terminal_dropdown
}
if (os_dropdown === 4) {
os = "auto"
} else {
os = os_dropdown
}
let command
if (process.platform.includes("linux")) {
// if the OS is linux, run the sh file with the chosen terminal
command = await createCommandWithTerminal(`sh ${platformpath().replaceAll(" ", "\\ ")}${sep}run.sh`, terminal, os)
} else if (process.platform.includes("win")) {
// if the OS is windows, just run the batch file
command = await createCommandWithTerminal(`"${platformpath()}${sep}run.bat"`, terminal, os)
} else if (process.platform.includes("darwin")) {
//macOS
}
// Run the final command
if (document.getElementById("os-dropdown").selectedIndex !== 3) await console.debug("Command issued:\n--------\n" + command)
await runCommand(command)
}).catch(function (error) {
if (error === "noDotnet") {
// if dotnet is not found, show the dotnet warning
document.getElementById("dotnetwarning2").hidden = true
document.getElementById("emptywarning").hidden = true
document.getElementById("dotnetwarning").hidden = false
} else if (error === "emptyField") {
// if a required field is empty, show the empty field warning
document.getElementById("dotnetwarning2").hidden = true
document.getElementById("dotnetwarning").hidden = true
document.getElementById("emptywarning").hidden = false
}
})
}
// Combines all buttons that are supposed to open an external URL into one big function.
function openRelevantPage(target) {
const electron = require("electron")
const os = process.platform.toString()
/* eslint-disable indent */
// why are you not indenting nicely eslint?
switch (target) {
case "dotnet":
document.getElementById("dotnetwarning").hidden = true
document.getElementById("dotnetwarning2").hidden = false
if (os.includes("win")) {
console.debug("Opened .NET download page for " + os.charAt(0).toUpperCase() + os.slice(1))
void electron.shell.openExternal("https://aka.ms/dotnet/6.0/dotnet-sdk-win-x64.exe")
}
if (os.includes("linux")) {
const electron = require("electron")
console.debug("Opened .NET download page for " + os.charAt(0).toUpperCase() + os.slice(1))
void electron.shell.openExternal("https://docs.microsoft.com/en-us/dotnet/core/install/linux")
}
if (os.includes("darwin")) {
console.debug("Opened .NET download page for" + os)
//TODO: Apple Silicon(ARM64) URL
void electron.shell.openExternal("https://aka.ms/dotnet/6.0/dotnet-sdk-osx-x64.pkg")
}
break
case "issues":
console.debug("Opened GitHub issues page")
void electron.shell.openExternal("https://github.com/mmvanheusden/SteamDepotDownloaderGUI/issues/new")
break
case "steamdb":
console.debug("Opened SteamDB instant search page")
void electron.shell.openExternal("https://steamdb.info/instantsearch/")
break
case "donate":
console.debug("Opened donation page")
void electron.shell.openExternal("https://my.fsf.org/donate")
break
case "instructions":
console.debug("Opened instructions page")
void electron.shell.openExternal("https://github.com/mmvanheusden/SteamDepotDownloaderGUI/#how-to-use")
break
default:
return
}
/* eslint-enable indent */
}
// Opens the chosen location where the game will be downloaded to
function checkPath() {
toggleFormAccessibility(false)
shell.openPath(exportedFile).then(() => {
console.log("Opened " + exportedFile + " in file explorer.")
})
}
/**
* Fills the values for default os
* Runs when the app is fully loaded.
*/
function fillDefaultValues() {
// [0]: Windows, [1]: macOS [2]: Linux [3]: manual
if (process.platform.toString().includes("linux")) {
document.getElementById("default-os").innerText = "Linux"
} else if (process.platform.toString().includes("win")) {
document.getElementById("default-os").innerText = "Windows"
} else if (process.platform.toString().includes("darwin")) {
document.getElementById("default-os").innerText = "macOS"
}
}
/**
* Validates the choice of the OS dropdown.
* If the choice is 2 (Linux), enable the terminal selection dropdown.
* If the choice is not 2 (Linux), disable the terminal selection dropdown.
* If the choice is 4 (manual), enable the terminal selection dropdown.
* If the choice is not 4 (manual), disable the terminal selection dropdown.
*/
function validateChoice() {
// [0]: Windows, [1]: macOS [2]: Linux [3]: manual
const os_dropdown = document.getElementById("os-dropdown")
const terminal_dropdown = document.getElementById("terminal-dropdown")
// if the choice = 2, enable the terminal selection dropdown.
if (os_dropdown.selectedIndex === (2 || 3)) {
terminal_dropdown.disabled = false
} else {
terminal_dropdown.disabled = true
terminal_dropdown.selectedIndex = 11
}
if (os_dropdown.selectedIndex === 4) if (process.platform.includes("linux")) terminal_dropdown.disabled = false
}
/**
* Enable or disable the form.
* @param disable Enable or disable the form.
*
* * true => disable everything
*
* * false => enable everything
*/
function toggleFormAccessibility(disable) {
document.getElementById("username").disabled = disable
document.getElementById("theform").disabled = disable
document.getElementById("password").disabled = disable
document.getElementById("appid").disabled = disable
document.getElementById("depotid").disabled = disable
document.getElementById("manifestid").disabled = disable
document.getElementById("os-dropdown").disabled = disable
// if the OS dropdown value was 2 (Linux), don't disable the Terminal dropdown.
document.getElementById("terminal-dropdown").disabled = (((document.getElementById("os-dropdown").selectedIndex !== 2)))
document.getElementById("pickpath").ariaDisabled = disable
document.getElementById("pickpath").disabled = disable
document.getElementById("downloadbtn").ariaDisabled = disable
document.getElementById("downloadbtn").disabled = disable
document.getElementById("settings-button").disabled = disable
document.getElementById("downloadbtn").classList.replace(((disable) ? "btn-primary" : "btn-disabled"), ((!disable) ? "btn-primary" : "btn-disabled"))
}
function setTheme(theme) {
document.getElementById("theme-auto").ariaSelected = (theme === "auto").toString()
document.getElementById("theme-light").ariaSelected = (theme === "light").toString()
document.getElementById("theme-dark").ariaSelected = (theme === "dark").toString()
}
// main.js sends a ready message if the page is loaded in. This will be received here.
ipcRenderer.on("ready", async () => {
if (!ready) return
await toggleFormAccessibility(true) // disables the form, while loading
document.getElementById("loader").hidden = false
console.debug("🐰🐰🐰🐰")
await fillDefaultValues() // Set the default values based on OS
const terminals = await forceTerminals()
/* forceTerminals() returns two values:
[bol, list, list]
1: true/false. if true, there are terminals found. if false none are, or not on linux
2: a list of available terminals with their associated number in the terminal dropdown index.
3: a list of available terminals with their associated name in the terminal dropdown.
*/
if (terminals[0]) {
console.log(`${terminals[1].length} terminals found in PATH.`)
document.getElementById("terminals-found").innerText = `${terminals[1].length.toString()} / ${document.getElementById("terminal-dropdown").length - 1}`
document.getElementById("default-terminal").innerText = terminals[2][0]
} else {
console.log("No terminals found in PATH. Continuing with default values") // when no terminals are found on the system, or when linux is not used.
}
await toggleFormAccessibility(false) //enable the form again
await validateChoice() // updates the 'enabled/disabled' html value of the terminal dropdown.
console.debug(`DEBUGGING INFORMATION:
--
OS: ${process.platform}
Version: ${app_version}
--
Working directory locating methods:
process.cwd(): ${process.cwd()}
__dirname: ${__dirname}
PORTABLE_EXECUTABLE_DIR: ${process.env.PORTABLE_EXECUTABLE_DIR}
--
platformpath(): ${platformpath()}
`)
document.getElementById("loader").hidden = true
ready = false
})
// Add event listeners to the buttons
window.addEventListener("DOMContentLoaded", () => {
document.getElementById("dotnetalertbtn").addEventListener("click", () => openRelevantPage("dotnet"))
document.getElementById("smbtn1").addEventListener("click", () => openRelevantPage("issues"))
document.getElementById("smbtn2").addEventListener("click", () => openRelevantPage("steamdb"))
document.getElementById("smbtn3").addEventListener("click", () => openRelevantPage("donate"))
document.getElementById("smbtn4").addEventListener("click", () => openRelevantPage("instructions"))
document.getElementById("pickpath").addEventListener("click", () => {
if (document.getElementById("pickpath").disabled === false) ipcRenderer.send("selectpath")
})
document.getElementById("checkpath").addEventListener("click", () => {
if (document.getElementById("checkpath").disabled === false) checkPath()
})
document.getElementById("os-dropdown").addEventListener("input", validateChoice)
document.getElementById("downloadbtn").addEventListener("click", () => {
if (document.getElementById("downloadbtn").disabled === false) submitForm()
})
document.getElementById("settings-button").addEventListener("click", () => {
if (document.getElementById("settings-button").disabled === false) {
// show the modal. This is done by setting the display to block.
document.getElementById("settings-surrounding").style.display = "block"
}
})
document.getElementById("settings-surrounding").addEventListener("click", (event) => {
if (event.target === document.getElementById("settings-surrounding")) {
document.getElementById("settings-surrounding").style.display = "none"
}
})
document.getElementById("theme-auto").addEventListener("click", () => {
setTheme("auto")
document.getElementById("theme").setAttribute("data-color-mode", "auto")
})
document.getElementById("theme-light").addEventListener("click", () => {
setTheme("light")
document.getElementById("theme").setAttribute("data-color-mode", "light")
})
document.getElementById("theme-dark").addEventListener("click", () => {
setTheme("dark")
document.getElementById("theme").setAttribute("data-color-mode", "dark")
})
document.getElementById("folder-name-appid").addEventListener("click", () => {
document.getElementById("folder-name-custom").ariaSelected = false
document.getElementById("folder-name-appid").ariaSelected = true
document.getElementById("folder-name-custom-input").hidden = true
document.getElementById("folder-name-custom-input").value = ""
})
document.getElementById("folder-name-custom").addEventListener("click", () => {
document.getElementById("folder-name-appid").ariaSelected = false
document.getElementById("folder-name-custom").ariaSelected = true
document.getElementById("folder-name-custom-input").hidden = false
})
document.getElementById("version-info").addEventListener("click", () => {
//todo: update checker
electron.shell.openExternal(`https://github.com/mmvanheusden/SteamDepotDownloaderGUI/releases/v${app_version}`)
})
})
// make sure devtools open in external window.
window.addEventListener("keydown", (e) => {
if (e.key === "F12" || (e.ctrlKey && e.shiftKey && e.key === "I")) {
e.preventDefault()
ipcRenderer.send("open-devtools")
}
})
ipcRenderer.on("file", (event, file) => {
console.log("path selected by user: " + file)
document.getElementById("checkpath").ariaDisabled = false // Makes the check button active
document.getElementById("checkpath").disabled = false // Makes the check button active
exportedFile = file.toString()
})
// receive the version from main.js
ipcRenderer.on("version", (event, version) => {
console.log("version: " + version)
document.getElementById("version-info").innerText = `v${version}`
app_version = version.toString()
})

33
electron-builder.yml Normal file
View File

@@ -0,0 +1,33 @@
appId: rocks.maarten.depotdownloader
productName: SteamDepotDownloaderGUI
copyright: Copyright © mmvanheusden | This work is free. Licensed under the GNU GPLv3 Public License
linux:
executableName: SteamDepotDownloaderGUI
category: Utility;FileTools;
maintainer: mmvanheusden
target:
- target: AppImage
arch:
- x64
- arm64
- target: zip
arch:
- x64
- arm64
# lmao mac building only works on mac
mac:
category: public.app-category.utilities
target:
- target: dmg
arch:
- x64
win:
legalTrademarks: Copyright © mmvanheusden | This work is free. Licensed under the GNU GPLv3 Public License
target:
- target: portable
arch:
- x64
- arm64

View File

@@ -1,21 +0,0 @@
import { defineConfig } from 'eslint/config';
import tseslint from 'typescript-eslint';
export default defineConfig(
tseslint.configs.recommendedTypeChecked,
tseslint.configs.stylisticTypeChecked,
{
languageOptions: {
parserOptions: {
project: 'tsconfig.json',
},
},
files: ["src/**"],
rules: {
"semi": ["error", "always"], // semicolons
"indent": ["error", "tab"], // tabs indents
"linebreak-style": ["error", "unix"],
"quotes": ["error", "double"],
},
},
);

270
index.html Normal file
View File

@@ -0,0 +1,270 @@
<!DOCTYPE html>
<!-- automatic dark mode :) -->
<html data-color-mode="auto" data-dark-theme="dark" data-light-theme="light" id="theme" lang="en">
<head>
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1" name="viewport"/>
<link href="https://unpkg.com/@primer/css@21.1.1/dist/primer.css" rel="stylesheet"/>
<link href="style.css" rel="stylesheet">
<title>SteamDepotDownloaderGUI</title>
<script src=" https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js "></script>
</head>
<body>
<script src="downloader.js"></script>
<div class="mx-auto">
<div hidden id="dotnetwarning">
<div class="flash flash-error mx-2 mt-2 color-shadow-medium" id="dotnetalert">
<svg class="octicon" height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg">
<path d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm6.5-.25A.75.75 0 017.25 7h1a.75.75 0 01.75.75v2.75h.25a.75.75 0 010 1.5h-2a.75.75 0 010-1.5h.25v-2h-.25a.75.75 0 01-.75-.75zM8 6a1 1 0 100-2 1 1 0 000 2z"
fill-rule="evenodd"></path>
</svg>
<code><span class="text-italic">dotnet</span></code> was not found.
<button class="btn btn-sm flash-action" id="dotnetalertbtn">
<svg class="octicon" height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg">
<path d="M7.47 10.78a.75.75 0 001.06 0l3.75-3.75a.75.75 0 00-1.06-1.06L8.75 8.44V1.75a.75.75 0 00-1.5
0v6.69L4.78 5.97a.75.75 0 00-1.06 1.06l3.75 3.75zM3.75 13a.75.75 0 000 1.5h8.5a.75.75 0 000-1.5h-8.5z"
fill-rule="evenodd"></path>
</svg>
<span class="text-bold">Download</span>
</button>
</div>
</div>
<div hidden id="emptywarning">
<div class="flash flash-warn mx-2 mt-2 color-shadow-medium" id="emptyalert">
<svg class="octicon" height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg">
<path d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm6.5-.25A.75.75 0 017.25 7h1a.75.75 0 01.75.75v2.75h.25a.75.75 0 010 1.5h-2a.75.75 0 010-1.5h.25v-2h-.25a.75.75 0 01-.75-.75zM8 6a1 1 0 100-2 1 1 0 000 2z"
fill-rule="evenodd"></path>
</svg>
Please fill in all required fields.
</div>
</div>
<div hidden id="dotnetwarning2">
<div class="flash mx-2 mt-2 color-shadow-medium">
<svg class="octicon" height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg">
<path d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm6.5-.25A.75.75 0 017.25 7h1a.75.75 0 01.75.75v2.75h.25a.75.75 0 010 1.5h-2a.75.75 0 010-1.5h.25v-2h-.25a.75.75 0 01-.75-.75zM8 6a1 1 0 100-2 1 1 0 000 2z"
fill-rule="evenodd"></path>
</svg>
Please re-open after installing .NET.
</div>
</div>
<div class="f1-light text-center">Steam Depot Downloader</div>
<form id="theform">
<div class="form-group mx-3 mt-1">
<div class="form-group-header">
<label for="username">Username</label>
</div>
<input class="form-control input-block" id="username" placeholder="Leave empty for anonymous download"
type="text"/>
</div>
<div class="form-group mx-3 mt-1">
<div class="form-group-header">
<label for="password">Password</label>
</div>
<input class="form-control input-block" id="password" placeholder="Leave empty for anonymous download"
type="password"/>
</div>
<div class="form-group mx-3 mt-1 required">
<div class="form-group-header">
<label for="appid">App ID</label>
</div>
<input class="form-control input-block" id="appid" type="number"/>
</div>
<div class="form-group mx-3 mt-1 required">
<div class="form-group-header">
<label for="depotid">Depot ID</label>
</div>
<input class="form-control input-block" id="depotid" type="number"/>
</div>
<div class="form-group mx-3 mt-1 required">
<div class="form-group-header">
<label for="manifestid">Manifest ID</label>
</div>
<input class="form-control input-block" id="manifestid" type="number"/>
</div>
<div class="mx-3 mt-1 required">
<div class="form-group-header">
<label>Download Location</label>
</div>
<div aria-label="Pick the path/location where the game will be downloaded to."
class="form-control btn btn-sm tooltipped tooltipped-ne" id="pickpath">
Set location
</div>
<div aria-disabled="true"
aria-label="Check the location that has been selected."
class="form-control btn btn-sm ml-2 tooltipped tooltipped-ne" id="checkpath">
Open location
</div>
<span class="Label mt-1 ml-3 Label--warning" id="loader">
<span aria-label="Loading" class="tooltipped tooltipped-n">Loading</span>
<span class="AnimatedEllipsis"></span>
</span>
</div>
</form>
<div class="form-group mt-3 ml-3 mr-3">
<div class="BtnGroup d-flex">
<button class="BtnGroup-item btn btn-block btn-primary flex-1" id="downloadbtn">
<svg class="octicon filter-red" height="16"
style="display: inline-block; user-select: none; vertical-align: text-bottom;" viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg">
<path d="M7.47 10.78a.75.75 0 001.06 0l3.75-3.75a.75.75 0 00-1.06-1.06L8.75 8.44V1.75a.75.75 0 00-1.5 0v6.69L4.78 5.97a.75.75 0 00-1.06 1.06l3.75 3.75zM3.75 13a.75.75 0 000 1.5h8.5a.75.75 0 000-1.5h-8.5z"
fill-rule="evenodd"></path>
</svg>
Download
</button>
<button class="BtnGroup-item btn flex-0" id="settings-button">
<svg fill="#8B949E" height="16"
style="display: inline-block; user-select: none; vertical-align: text-bottom;" viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg">
<path d="M8 0a8.2 8.2 0 0 1 .701.031C9.444.095 9.99.645 10.16 1.29l.288 1.107c.018.066.079.158.212.224.231.114.454.243.668.386.123.082.233.09.299.071l1.103-.303c.644-.176 1.392.021 1.82.63.27.385.506.792.704 1.218.315.675.111 1.422-.364 1.891l-.814.806c-.049.048-.098.147-.088.294.016.257.016.515 0 .772-.01.147.038.246.088.294l.814.806c.475.469.679 1.216.364 1.891a7.977 7.977 0 0 1-.704 1.217c-.428.61-1.176.807-1.82.63l-1.102-.302c-.067-.019-.177-.011-.3.071a5.909 5.909 0 0 1-.668.386c-.133.066-.194.158-.211.224l-.29 1.106c-.168.646-.715 1.196-1.458 1.26a8.006 8.006 0 0 1-1.402 0c-.743-.064-1.289-.614-1.458-1.26l-.289-1.106c-.018-.066-.079-.158-.212-.224a5.738 5.738 0 0 1-.668-.386c-.123-.082-.233-.09-.299-.071l-1.103.303c-.644.176-1.392-.021-1.82-.63a8.12 8.12 0 0 1-.704-1.218c-.315-.675-.111-1.422.363-1.891l.815-.806c.05-.048.098-.147.088-.294a6.214 6.214 0 0 1 0-.772c.01-.147-.038-.246-.088-.294l-.815-.806C.635 6.045.431 5.298.746 4.623a7.92 7.92 0 0 1 .704-1.217c.428-.61 1.176-.807 1.82-.63l1.102.302c.067.019.177.011.3-.071.214-.143.437-.272.668-.386.133-.066.194-.158.211-.224l.29-1.106C6.009.645 6.556.095 7.299.03 7.53.01 7.764 0 8 0Zm-.571 1.525c-.036.003-.108.036-.137.146l-.289 1.105c-.147.561-.549.967-.998 1.189-.173.086-.34.183-.5.29-.417.278-.97.423-1.529.27l-1.103-.303c-.109-.03-.175.016-.195.045-.22.312-.412.644-.573.99-.014.031-.021.11.059.19l.815.806c.411.406.562.957.53 1.456a4.709 4.709 0 0 0 0 .582c.032.499-.119 1.05-.53 1.456l-.815.806c-.081.08-.073.159-.059.19.162.346.353.677.573.989.02.03.085.076.195.046l1.102-.303c.56-.153 1.113-.008 1.53.27.161.107.328.204.501.29.447.222.85.629.997 1.189l.289 1.105c.029.109.101.143.137.146a6.6 6.6 0 0 0 1.142 0c.036-.003.108-.036.137-.146l.289-1.105c.147-.561.549-.967.998-1.189.173-.086.34-.183.5-.29.417-.278.97-.423 1.529-.27l1.103.303c.109.029.175-.016.195-.045.22-.313.411-.644.573-.99.014-.031.021-.11-.059-.19l-.815-.806c-.411-.406-.562-.957-.53-1.456a4.709 4.709 0 0 0 0-.582c-.032-.499.119-1.05.53-1.456l.815-.806c.081-.08.073-.159.059-.19a6.464 6.464 0 0 0-.573-.989c-.02-.03-.085-.076-.195-.046l-1.102.303c-.56.153-1.113.008-1.53-.27a4.44 4.44 0 0 0-.501-.29c-.447-.222-.85-.629-.997-1.189l-.289-1.105c-.029-.11-.101-.143-.137-.146a6.6 6.6 0 0 0-1.142 0ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0ZM9.5 8a1.5 1.5 0 1 0-3.001.001A1.5 1.5 0 0 0 9.5 8Z"></path>
</svg>
</button>
</div>
</div>
<div aria-label="Visit the GitHub issues website." class="btn btn-sm ml-3 tooltipped tooltipped-ne mb-1"
id="smbtn1">
<svg fill="#8B949E" height="16" style="display: inline-block; vertical-align: text-bottom;" viewBox="0 0 16 16"
width="16" xmlns="http://www.w3.org/2000/svg">
<path d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm9 3a1 1 0 11-2 0 1 1 0 012 0zM6.92 6.085c.081-.16.19-.299.34-.398.145-.097.371-.187.74-.187.28 0 .553.087.738.225A.613.613 0 019 6.25c0 .177-.04.264-.077.318a.956.956 0 01-.277.245c-.076.051-.158.1-.258.161l-.007.004a7.728 7.728 0 00-.313.195 2.416 2.416 0 00-.692.661.75.75 0 001.248.832.956.956 0 01.276-.245 6.3 6.3 0 01.26-.16l.006-.004c.093-.057.204-.123.313-.195.222-.149.487-.355.692-.662.214-.32.329-.702.329-1.15 0-.76-.36-1.348-.863-1.725A2.76 2.76 0 008 4c-.631 0-1.155.16-1.572.438-.413.276-.68.638-.849.977a.75.75 0 101.342.67z"
fill-rule="evenodd"></path>
</svg>
Help
</div>
<div aria-label="Visit the SteamDB instant search website." class="btn btn-sm ml-2 tooltipped tooltipped-n mb-1"
id="smbtn2">
<svg aria-hidden="true" class="octicon" fill="#8B949E" height="16" viewBox="0 0 128 128" width="16"
xmlns="http://www.w3.org/2000/svg">
<path d="M63.9 0C30.5 0 3.1 11.9.1 27.1l35.6 6.7c2.9-.9 6.2-1.3 9.6-1.3l16.7-10c-.2-2.5 1.3-5.1 4.7-7.2 4.8-3.1 12.3-4.8 19.9-4.8 5.2-.1 10.5.7 15 2.2 11.2 3.8 13.7 11.1 5.7 16.3-5.1 3.3-13.3 5-21.4 4.8l-22 7.9c-.2 1.6-1.3 3.1-3.4 4.5-5.9 3.8-17.4 4.7-25.6 1.9-3.6-1.2-6-3-7-4.8L2.5 38.4c2.3 3.6 6 6.9 10.8 9.8C5 53 0 59 0 65.5c0 6.4 4.8 12.3 12.9 17.1C4.8 87.3 0 93.2 0 99.6 0 115.3 28.6 128 64 128c35.3 0 64-12.7 64-28.4 0-6.4-4.8-12.3-12.9-17 8.1-4.8 12.9-10.7 12.9-17.1 0-6.5-5-12.6-13.4-17.4 8.3-5.1 13.3-11.4 13.3-18.2 0-16.5-28.7-29.9-64-29.9zm22.8 14.2c-5.2.1-10.2 1.2-13.4 3.3-5.5 3.6-3.8 8.5 3.8 11.1 7.6 2.6 18.1 1.8 23.6-1.8s3.8-8.5-3.8-11c-3.1-1-6.7-1.5-10.2-1.5zm.3 1.7c7.4 0 13.3 2.8 13.3 6.2 0 3.4-5.9 6.2-13.3 6.2s-13.3-2.8-13.3-6.2c0-3.4 5.9-6.2 13.3-6.2zM45.3 34.4c-1.6.1-3.1.2-4.6.4l9.1 1.7a10.8 5 0 1 1-8.1 9.3l-8.9-1.7c1 .9 2.4 1.7 4.3 2.4 6.4 2.2 15.4 1.5 20-1.5s3.2-7.2-3.2-9.3c-2.6-.9-5.7-1.3-8.6-1.3zM109 51v9.3c0 11-20.2 19.9-45 19.9-24.9 0-45-8.9-45-19.9v-9.2c11.5 5.3 27.4 8.6 44.9 8.6 17.6 0 33.6-3.3 45.2-8.7zm0 34.6v8.8c0 11-20.2 19.9-45 19.9-24.9 0-45-8.9-45-19.9v-8.8c11.6 5.1 27.4 8.2 45 8.2s33.5-3.1 45-8.2z"
fill-rule="evenodd"></path>
</svg>
SteamDB
</div>
<div aria-label="Donate to the Free Software Foundation." class="btn btn-sm ml-2 tooltipped tooltipped-nw mb-1"
id="smbtn3">
<svg fill="#8B949E" height="16" style="display: inline-block; vertical-align: text-bottom;" viewBox="0 0 16 16"
width="16" xmlns="http://www.w3.org/2000/svg">
<path d="M2 2.75A2.75 2.75 0 0 1 4.75 0c.983 0 1.873.42 2.57 1.232.268.318.497.668.68 1.042.183-.375.411-.725.68-1.044C9.376.42 10.266 0 11.25 0a2.75 2.75 0 0 1 2.45 4h.55c.966 0 1.75.784 1.75 1.75v2c0 .698-.409 1.301-1 1.582v4.918A1.75 1.75 0 0 1 13.25 16H2.75A1.75 1.75 0 0 1 1 14.25V9.332C.409 9.05 0 8.448 0 7.75v-2C0 4.784.784 4 1.75 4h.55c-.192-.375-.3-.8-.3-1.25ZM7.25 9.5H2.5v4.75c0 .138.112.25.25.25h4.5Zm1.5 0v5h4.5a.25.25 0 0 0 .25-.25V9.5Zm0-4V8h5.5a.25.25 0 0 0 .25-.25v-2a.25.25 0 0 0-.25-.25Zm-7 0a.25.25 0 0 0-.25.25v2c0 .138.112.25.25.25h5.5V5.5h-5.5Zm3-4a1.25 1.25 0 0 0 0 2.5h2.309c-.233-.818-.542-1.401-.878-1.793-.43-.502-.915-.707-1.431-.707ZM8.941 4h2.309a1.25 1.25 0 0 0 0-2.5c-.516 0-1 .205-1.43.707-.337.392-.646.975-.879 1.793Z"
fill-rule="evenodd"></path>
</svg>
Donate
</div>
<div aria-label="View the instructions on how to use SteamDepotDownloaderGUI."
class="btn btn-sm ml-2 tooltipped tooltipped-nw mb-1"
id="smbtn4">
<svg fill="#8B949E" height="16" style="display: inline-block; vertical-align: text-bottom;" viewBox="0 0 16 16"
width="16">
<path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path>
</svg>
Instructions
</div>
</div>
<div class="settings-surrounding" id="settings-surrounding">
<div class="settings-content mx-auto" id="settings-content">
<label class="version-info" id="version-info">UNKNOWN</label>
<h3><b>Settings</b></h3>
<hr>
<form>
<div class="form-group">
<div class="form-group-header">
<label>Theme</label>
</div>
<div class="form-group-body">
<div class="BtnGroup">
<button aria-selected="true" class="BtnGroup-item btn btn-sm" id="theme-auto" type="button">
Auto
</button>
<button class="BtnGroup-item btn btn-sm" id="theme-light" type="button">
Light
</button>
<button class="BtnGroup-item btn btn-sm" id="theme-dark" type="button">
Dark
</button>
</div>
</div>
</div>
<hr>
<div class="form-group">
<div class="form-group-header">
<label for="terminal-dropdown">Override terminal selection (for Linux only)</label>
</div>
<div class="form-group-body">
<div class="mb-2">
<select class="form-select" id="terminal-dropdown">
<option>Gnome Terminal</option>
<option>KDE Konsole</option>
<option>Xfce Terminal</option>
<option>Terminator</option>
<option>Terminology</option>
<option>xterm</option>
<option>Kitty</option>
<option>LXTerminal</option>
<option>Tilix</option>
<option>Deepin Terminal</option>
<option>cool-retro-term</option>
<option selected="selected">Auto</option>
</select>
found: <span class="Counter"><code id="terminals-found">none</code></span>
<br>default: <span class="Counter"><code id="default-terminal">none</code></span>
</div>
</div>
</div>
<hr>
<div class="form-group">
<div class="form-group-header">
<label for="os-dropdown">Override OS selection</label>
</div>
<div class="form-group-body">
<div class="mb-2">
<select class="form-select" id="os-dropdown">
<option>Windows</option>
<option disabled>macOS (NOT YET IMPLEMENTED)</option>
<option>Linux</option>
<option>Print command in console (CTRL-SHIFT-I)</option>
<option selected="selected">Auto</option>
</select>
default: <span class="Counter"><code id="default-os">none</code></span>
</div>
</div>
</div>
<hr>
<div class="form-group">
<div class="form-group-header">
<label for="os-dropdown">Folder name</label>
</div>
<div class="form-group-body">
<div class="BtnGroup">
<button aria-selected="true" class="BtnGroup-item btn btn-sm" id="folder-name-appid"
type="button">
App-ID
</button>
<button class="BtnGroup-item btn btn-sm" id="folder-name-custom" type="button">
Custom
</button>
</div>
<br>
<input class="form-control input-block" hidden id="folder-name-custom-input"
placeholder="Game directory name"
type="text"/>
</div>
</div>
</form>
</div>
</div>
</body>
</html>

82
main.js Normal file
View File

@@ -0,0 +1,82 @@
const {app, BrowserWindow, dialog, ipcMain} = require("electron")
const {platformpath} = require("./utils")
require("v8-compile-cache")
let mainWindow
const createWindow = () => {
// Create the browser window.
mainWindow = new BrowserWindow({
autoHideMenuBar: true,
resizable: false,
width: 445,
height: 650,
useContentSize: true,
maximizable: false,
webPreferences: {
nodeIntegration: true, contextIsolation: false
}
})
// and load the index.html of the app.
mainWindow.loadFile("index.html")
// @formatter:off
// Open the DevTools for debugging, only if not in production. This is removed for release by the build script (package.sh) because it's unreliable.
// disable formatting so the line always stays the same so sed can find it
// eslint-disable-next-line no-undef
if (!app.isPackaged) mainWindow.webContents.openDevTools({mode: "detach"})
// @formatter:on
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
createWindow()
app.on("activate", () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
// If the page is fully loaded in, send a sign.
app.on("web-contents-created", (event, contents) => {
contents.on("dom-ready", () => {
contents.send("ready")
contents.send("version", app.getVersion()) // send the version to the renderer
})
})
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on("window-all-closed", () => {
if (process.platform !== "darwin") app.quit()
})
ipcMain.on("selectpath", (event) => {
dialog.showOpenDialog(mainWindow, {
// Specifying the Directory Selector Property
properties: ["openDirectory"],
title: "Select the path where the game will be downloaded",
defaultPath: platformpath(),
buttonLabel: "Select"
}).then(file => {
//console.debug("Has path selection succeeded: " + ((file.canceled) ? "NO" : "YES; see below")) --- doesn't log to the dev console TODO
if (!file.canceled) {
const filepath = file.filePaths[0].toString()
// console.debug("Path selected is " + filepath) --- same thing
event.reply("file", filepath)
}
}).catch(err => {
console.log(err)
})
})
ipcMain.on("open-devtools", () => {
mainWindow.webContents.openDevTools({mode: "detach"})
})

4468
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,34 +1,41 @@
{
"name": "vectum",
"private": true,
"version": "3.2.0",
"type": "module",
"license": "GPL-3.0-only",
"name": "steamdepotdownloadergui",
"version": "2.4.2",
"description": "DepotDownloader Electron frontend",
"main": "main.js",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"tauri": "tauri"
"test": "echo \"Error: no test specified\" && exit 1",
"start": "electron .",
"build": "electron-builder -c electron-builder.yml -wl -p never",
"buildall": "electron-builder -c electron-builder.yml -mwl -p never",
"buildlinux": "electron-builder -c electron-builder.yml -l -p never",
"buildwin": "electron-builder -c electron-builder.yml -w -p never",
"buildmacos": "electron-builder -c electron-builder.yml -m -p never"
},
"repository": {
"type": "git",
"url": "git+https://github.com/mmvanheusden/SteamDepotDownloaderGUI.git"
},
"keywords": [
"electron",
"steam",
"depotdownloader",
"electron",
"gui"
],
"author": "mmvanheusden",
"license": "GPL-3.0",
"bugs": {
"url": "https://github.com/mmvanheusden/SteamDepotDownloaderGUI/issues"
},
"homepage": "https://github.com/mmvanheusden/SteamDepotDownloaderGUI#readme",
"devDependencies": {
"electron": "^30.0.2",
"electron-builder": "^24.13.3",
"eslint": "^9.1.1"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.18",
"@tauri-apps/api": "2.9.1",
"@tauri-apps/plugin-dialog": "2.6.0",
"@tauri-apps/plugin-opener": "~2.5.3",
"@xterm/addon-fit": "^0.11.0",
"@xterm/xterm": "^6.0.0",
"preact": "^10.25.1",
"tailwindcss": "^4.1.18"
},
"devDependencies": {
"@eslint/js": "^9.39.2",
"@iconify-icon/react": "^3.0.3",
"@preact/preset-vite": "^2.9.3",
"@tauri-apps/cli": "2.9.6",
"eslint": "^9.39.2",
"typescript": "^5.9.3",
"typescript-eslint": "^8.54.0",
"vite": "^7.3.1"
},
"packageManager": "pnpm@10.28.2+sha512.41872f037ad22f7348e3b1debbaf7e867cfd448f2726d9cf74c08f19507c31d2c8e7a11525b983febc2df640b5438dee6023ebb1f84ed43cc2d654d2bc326264"
"follow-redirects": "^1.15.6",
"v8-compile-cache": "^2.4.0"
}
}

25
package.sh Executable file
View File

@@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -e
version=`jq '.version' package.json`
version="${version//\"}"
echo "version that will be build is ${version}"
rm -rf ./dist/
# Disable DevTools for release, because it's unreliable
original_line=$(sed -n '/if (!app.isPackaged) mainWindow.webContents.openDevTools({mode: "detach"})/p' main.js)
# original_line has 1 tab too much, so we remove it
original_line="${original_line// /}"
sed -i 's/if (!app.isPackaged) mainWindow.webContents.openDevTools({mode: "detach"})/\/\/REMOVED FOR RELEASE./g' main.js
npm run build
mkdir -p ./dist/release-ready
cp ./dist/SteamDepotDownloaderGUI*.exe ./dist/release-ready/SteamDepotDownloaderGUI-windows-"${version}".exe
cp `ls -d1 dist/* | grep -E "SteamDepotDownloaderGUI-[0-9]+\.[0-9]+\.[0-9]+\.AppImage"` ./dist/release-ready/SteamDepotDownloaderGUI-linux-"${version}"-x64.AppImage
cp `ls -d1 dist/* | grep -E "steamdepotdownloadergui-[0-9]+\.[0-9]+\.[0-9]+\.zip"` ./dist/release-ready/SteamDepotDownloaderGUI-linux-"${version}"-x64.zip
cp `ls -d1 dist/* | grep -E "SteamDepotDownloaderGUI-[0-9]+\.[0-9]+\.[0-9]+\-arm64.AppImage"` ./dist/release-ready/SteamDepotDownloaderGUI-linux-"${version}"-arm64.AppImage
cp `ls -d1 dist/* | grep -E "steamdepotdownloadergui-[0-9]+\.[0-9]+\.[0-9]+\-arm64.zip"` ./dist/release-ready/SteamDepotDownloaderGUI-linux-"${version}"-arm64.zip
echo "done building!"
echo "reverting changes to main.js"
sed -i "s|\/\/REMOVED FOR RELEASE.|$original_line|g" main.js

2662
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

BIN
screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

16
src-tauri/.gitignore vendored
View File

@@ -1,16 +0,0 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas
# DepotDownloader
depot/
downloads/
.DepotDownloader/
Games/
Depots/
**/*.sh

6057
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,44 +0,0 @@
[package]
name = "vectum"
version = "3.2.0"
description = "Download older versions of Steam games with DepotDownloader"
authors = ["mmvanheusden"]
edition = "2021"
license = "GPL-3.0-only"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "2.4.1", features = [] }
[dependencies]
fix-path-env = { git = "https://github.com/tauri-apps/fix-path-env-rs", rev = "c4c45d503ea115a839aae718d02f79e7c7f0f673" }
serde_json = "1.0.143"
tauri = { version = "2.8.5", features = [] }
tauri-plugin-dialog = "2.4.0"
serde = { version = "1.0.219", features = ["derive"] }
derive-getters = "0.5.0"
reqwest = { version = "0.13.1",features = ["blocking"] }
zip = "7.2.0"
tauri-plugin-opener = "2"
portable-pty = "0.9.0"
# Bacon - https://dystroy.org/bacon/
# Mold - https://github.com/rui314/mold#how-to-use
# https://discord.com/channels/616186924390023171/731495028677148753/1254902668376150149
[profile.dev]
incremental = true
opt-level = 1
debug = 0
[profile.dev.package."*"]
opt-level = 2
[profile.release]
codegen-units = 1 # Allows LLVM to perform better optimization.
lto = true # Enables link-time-optimizations.
opt-level = 3 # Prioritizes small binary size. Use `3` if you prefer speed.
panic = "abort" # Higher performance by disabling panic handlers.
strip = true # Ensures debug symbols are removed.

View File

@@ -1,3 +0,0 @@
fn main() {
tauri_build::build()
}

View File

@@ -1,28 +0,0 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": [
"main"
],
"permissions": [
"core:default",
"dialog:default",
{
"identifier": "opener:allow-open-path",
"allow": [
{
"path": "**"
}
]
},
{
"identifier": "opener:allow-open-url",
"allow": [
{
"url": "**"
}
]
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -1,111 +0,0 @@
use crate::get_os;
use std::fs::File;
use std::io::ErrorKind::AlreadyExists;
use std::path::PathBuf;
use std::{fs, io};
use std::{io::Write, path::Path};
pub static DEPOTDOWNLOADER_VERSION: &str = "3.4.0";
/**
See: [`test_get_depotdownloader_url()`]
*/
pub fn get_depotdownloader_url() -> String {
let arch = match std::env::consts::ARCH {
"x86_64" => "x64",
"aarch64" => "arm64",
"arm" => "arm",
_ => "x86_64",
};
format!("https://github.com/SteamRE/DepotDownloader/releases/download/DepotDownloader_{}/DepotDownloader-{}-{}.zip", DEPOTDOWNLOADER_VERSION, get_os(), arch)
}
/// Downloads a file. The file will be saved to the [`filename`] provided.
pub async fn download_file(url: &str, filename: &Path) -> io::Result<()> {
if filename.exists() {
println!("DEBUG: Not downloading. File already exists.");
return Err(io::Error::from(AlreadyExists));
}
// Create any missing directories.
if let Some(p) = filename.parent() {
if !p.exists() {
fs::create_dir_all(p)?;
}
}
let mut file = File::create(filename)?;
let response = reqwest::get(url)
.await
.expect("Failed to contact internet.");
let content = response.bytes().await.unwrap();
file.write_all(&content)?;
Ok(())
}
/// Unzips DepotDownloader zips
pub fn unzip(zip_file: &Path, working_dir: &PathBuf) -> io::Result<()> {
let file = File::open(zip_file)?;
let mut archive = zip::ZipArchive::new(file)?;
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
let outpath = match file.enclosed_name() {
Some(path) => working_dir.join(path),
None => continue,
};
println!("Extracted {} from archive.", outpath.display());
if let Some(p) = outpath.parent() {
if !p.exists() {
fs::create_dir_all(p)?;
}
}
let mut outfile = File::create(&outpath)?;
io::copy(&mut file, &mut outfile)?;
// Copy over permissions from enclosed file to extracted file on UNIX systems.
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
// If the mode `file.unix_mode()` is something (not None), copy it over to the extracted file.
if let Some(mode) = file.unix_mode() {
fs::set_permissions(&outpath, fs::Permissions::from_mode(mode))?;
}
// Set executable permission.
if outpath.file_name().unwrap() == "DepotDownloader" {
fs::set_permissions(&outpath, fs::Permissions::from_mode(0o755))?;
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use reqwest::blocking;
#[test]
/// checks if all possible DepotDownloader URLs exist.
fn test_get_depotdownloader_url() {
for os in ["windows", "linux", "macos"].iter() {
for arch in ["x64", "arm64", "arm"].iter() {
if arch.eq(&"arm") && !os.eq(&"linux") {
continue;
}
let url = format!("https://github.com/SteamRE/DepotDownloader/releases/download/DepotDownloader_{}/DepotDownloader-{}-{}.zip", DEPOTDOWNLOADER_VERSION, os, arch);
println!("Testing DepotDownloader URL: {}", url);
assert!(blocking::get(url).unwrap().status().is_success());
}
}
}
}

View File

@@ -1,167 +0,0 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod depotdownloader;
mod steam;
mod terminal;
use crate::depotdownloader::{get_depotdownloader_url, DEPOTDOWNLOADER_VERSION};
use crate::terminal::{async_read_from_pty, async_resize_pty, async_write_to_pty};
use portable_pty::{native_pty_system, PtyPair, PtySize};
use std::io::ErrorKind::AlreadyExists;
use std::io::{BufReader, Read, Write};
use std::path::{Path, PathBuf};
use std::sync::{Arc};
use std::time::Duration;
use std::{env, thread};
use tauri::async_runtime::Mutex;
use tauri::{AppHandle, Emitter, Manager, State};
struct AppState {
pty_pair: Arc<Mutex<PtyPair>>,
writer: Arc<Mutex<Box<dyn Write + Send>>>,
reader: Arc<Mutex<BufReader<Box<dyn Read + Send>>>>,
}
#[tauri::command]
async fn start_download(steam_download: steam::SteamDownload, app: AppHandle, state: State<'_, AppState>) -> Result<(), String> {
let working_dir: PathBuf = get_working_dir(&app);
// std::env::set_current_dir(&WORKING_DIR.get().unwrap()).unwrap();
dbg!(&steam_download);
println!("\n-------------------------DEBUG INFO------------------------");
println!("received these values from frontend:");
println!("\t- Username: {}", steam_download.username().as_ref().unwrap_or(&String::from("Not provided")));
// println!("\t- Password: {}", steam_download.password().as_ref().unwrap_or(&String::from("Not provided"))); Don't log in prod lol
println!("\t- App ID: {}", steam_download.app_id());
println!("\t- Depot ID: {}", steam_download.depot_id());
println!("\t- Manifest ID: {}", steam_download.manifest_id());
println!("\t- Output Path: {}", steam_download.output_path());
println!("\t- Working directory: {}", &working_dir.display());
println!("----------------------------------------------------------\n");
/* Build the command and spawn it in our terminal */
let mut cmd = terminal::create_depotdownloader_command(&steam_download, &working_dir);
// add the $TERM env variable so we can use clear and other commands
#[cfg(target_os = "windows")]
cmd.env("TERM", "cygwin");
#[cfg(not(target_os = "windows"))]
cmd.env("TERM", "xterm-256color");
let mut child = state
.pty_pair
.lock()
.await
.slave
.spawn_command(cmd)
.map_err(|err| err.to_string())?;
thread::spawn(move || {
let status = child.wait().unwrap();
println!("Command exited with status: {status}");
app.emit("command-exited", {}).unwrap();
// exit(status.exit_code() as i32)
});
Ok(())
}
/// Downloads the DepotDownloader zip file from the internet based on the OS.
#[tauri::command]
async fn download_depotdownloader(app: AppHandle) {
let working_dir: PathBuf = get_working_dir(&app);
let url = get_depotdownloader_url();
// Where we store the DepotDownloader zip.
let zip_filename = format!("DepotDownloader-v{}-{}.zip", DEPOTDOWNLOADER_VERSION, env::consts::OS);
let depotdownloader_zip = Path::join(&working_dir, Path::new(&zip_filename));
if let Err(e) = depotdownloader::download_file(url.as_str(), depotdownloader_zip.as_path()).await {
if e.kind() == AlreadyExists {
println!("DepotDownloader already exists. Skipping download.");
} else {
println!("Failed to download DepotDownloader: {}", e);
}
return;
} else {
println!("Downloaded DepotDownloader for {} to {}", env::consts::OS, depotdownloader_zip.display());
}
depotdownloader::unzip(depotdownloader_zip.as_path(), &working_dir).unwrap();
println!("Succesfully extracted DepotDownloader zip.");
}
/// Checks internet connectivity using Google
#[tauri::command]
async fn internet_connection() -> bool {
let client = reqwest::Client::builder().timeout(Duration::from_secs(5)).build().unwrap();
client.get("https://connectivitycheck.android.com/generate_204").send().await.is_ok()
}
pub fn get_os() -> &'static str {
match env::consts::OS {
"linux" => "linux",
"macos" => "macos",
"windows" => "windows",
_ => "unknown",
}
}
pub fn get_working_dir(app: &AppHandle) -> PathBuf {
Path::join(&app.path().local_data_dir().unwrap(), "SteamDepotDownloaderGUI")
}
fn main() {
// macOS: change dir to documents because upon opening, our current dir by default is "/".
// todo: Is this still needed ??
/* if get_os() == "macos" {
let _ = fix_path_env::fix();
// let documents_dir = format!(
// "{}/Documents/SteamDepotDownloaderGUI",
// std::env::var_os("HOME").unwrap().to_str().unwrap()
// );
// let documents_dir = Path::new(&documents_dir);
// // println!("{}", documents_dir.display());
// std::fs::create_dir_all(documents_dir).unwrap();
// env::set_current_dir(documents_dir).unwrap();
}*/
/* Initialize the pty system */
let pty_system = native_pty_system();
let pty_pair = pty_system
.openpty(PtySize {
rows: 24,
cols: 80,
pixel_width: 0,
pixel_height: 0,
})
.unwrap();
let reader = pty_pair.master.try_clone_reader().unwrap();
let writer = pty_pair.master.take_writer().unwrap();
println!();
tauri::Builder::default()
.manage(AppState {
pty_pair: Arc::new(Mutex::new(pty_pair)),
writer: Arc::new(Mutex::new(writer)),
reader: Arc::new(Mutex::new(BufReader::new(reader))),
})
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_dialog::init())
.invoke_handler(tauri::generate_handler![
start_download,
download_depotdownloader,
internet_connection,
async_write_to_pty,
async_read_from_pty,
async_resize_pty,
]).run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@@ -1,36 +0,0 @@
use derive_getters::Getters;
use serde::Deserialize;
use std::path::PathBuf;
/// Represents the data required to download a Steam depot.
#[derive(Deserialize, Debug, Getters)]
#[serde(rename_all = "camelCase")]
pub struct SteamDownload {
username: Option<String>,
password: Option<String>,
app_id: String,
depot_id: String,
manifest_id: String,
output_location: Option<PathBuf>,
output_directory_name: Option<String>
}
impl SteamDownload {
/// If a username or password are not provided, the download is considered anonymous
pub fn is_anonymous(&self) -> bool {
self.username.is_none() || self.password.is_none()
}
/// The directory where the download should happen
pub fn output_path(&self) -> String {
let sep = std::path::MAIN_SEPARATOR.to_string();
match (&self.output_location, &self.output_directory_name) {
(Some(output_dir), Some(dir_name)) => format!("{}{}{}", output_dir.display(), sep, dir_name),
(Some(output_dir), None) => format!("{}{}{}", output_dir.display(), sep, &self.manifest_id),
(None, Some(dir_name)) => format!(".{}{}", sep, dir_name),
(None, None) => format!(".{}{}", sep, &self.manifest_id)
}
}
}

View File

@@ -1,79 +0,0 @@
use std::io::BufRead;
use std::path::PathBuf;
use portable_pty::{CommandBuilder, PtySize};
use tauri::State;
use crate::AppState;
use crate::steam::SteamDownload;
/* Parts of this file are derived from https://github.com/cablehead/tauri-xtermjs-nushell/blob/0bdd4a27ee2874de12e99bccd6c91d6ec5d28fbc/src-tauri/src/main.rs */
#[tauri::command]
pub async fn async_write_to_pty(data: &str, state: State<'_, AppState>) -> Result<(), ()> {
write!(state.writer.lock().await, "{}", data).map_err(|_| ())
}
#[tauri::command]
pub async fn async_read_from_pty(state: State<'_, AppState>) -> Result<Option<String>, ()> {
let mut reader = state.reader.lock().await;
let data = {
// Read all available text
let data = reader.fill_buf().map_err(|_| ())?;
// Send the data to the webview if necessary
if !data.is_empty() {
std::str::from_utf8(data)
.map(|v| Some(v.to_string()))
.map_err(|_| ())?
} else {
None
}
};
if let Some(data) = &data {
reader.consume(data.len());
}
Ok(data)
}
#[tauri::command]
pub async fn async_resize_pty(rows: u16, cols: u16, state: State<'_, AppState>) -> Result<(), ()> {
state
.pty_pair
.lock()
.await
.master
.resize(PtySize {
rows,
cols,
..Default::default()
})
.map_err(|_| ())
}
/// Creates the DepotDownloader command necessary to download the requested manifest.
pub fn create_depotdownloader_command(steam_download: &SteamDownload, cwd: &PathBuf) -> CommandBuilder {
let depotdownloader_binary = if cfg!(windows) {
"DepotDownloader.exe"
} else {
"DepotDownloader"
};
let program = cwd.join(depotdownloader_binary);
let mut command = CommandBuilder::new(program);
command.cwd(cwd);
if !steam_download.is_anonymous() {
command.args(["-username", &steam_download.username().clone().unwrap()]);
command.args(["-password", &steam_download.password().clone().unwrap()]);
}
command.args(["-app", &steam_download.app_id().to_string()]);
command.args(["-depot", &steam_download.depot_id().to_string()]);
command.args(["-manifest", &steam_download.manifest_id().to_string()]);
command.args(["-dir", &steam_download.output_path()]);
command
}

View File

@@ -1,37 +0,0 @@
{
"productName": "SteamDepotDownloaderGUI",
"version": "3.2.0",
"identifier": "net.oopium.depotdownloader",
"build": {
"beforeDevCommand": "pnpm dev",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "pnpm build",
"frontendDist": "../dist"
},
"app": {
"withGlobalTauri": true,
"windows": [
{
"title": "SteamDepotDownloaderGUI",
"width": 900,
"height": 720,
"resizable": false,
"maximizable": false
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}

View File

@@ -1,92 +0,0 @@
import { getVersion } from "@tauri-apps/api/app";
import { invoke } from "@tauri-apps/api/core";
import { openUrl } from "@tauri-apps/plugin-opener";
import { useState } from "preact/hooks";
import { DownloaderForm } from "./components/DownloaderForm.tsx";
import { DownloaderOutput } from "./components/DownloaderOutput.tsx";
import { Settings } from "./components/Settings.tsx";
import { AppContext, AppSettings } from "./context.ts";
import "./css/App.css";
const appVersion = await getVersion();
// Settings defaults are defined here.
const DEFAULT_APP_SETTINGS: AppSettings = {
outputDirectoryMode: "Manifest ID"
};
function App() {
const username = useState<string>();
const password = useState<string>();
const appId = useState<string>();
const depotId = useState<string>();
const manifestId = useState<string>();
const outputLocation = useState<string>();
const outputFolderName = useState<string>();
const downloading = useState<boolean>();
const showSettings = useState<boolean>();
const appSettings = useState<AppSettings>(DEFAULT_APP_SETTINGS);
return (
<AppContext.Provider
value={{
username,
password,
appId,
depotId,
manifestId,
outputLocation,
downloading,
showSettings,
outputFolderName,
appSettings,
}}
>
<main class="absolute top-0 right-0 bottom-0 left-0 p-px select-none bg-[#0d1117]">
{showSettings[0]
?<Settings />
: <>
<div class="mb-1 text-4xl font-bold text-center text-white font-['Hubot_Sans']">
Steam Depot Downloader
</div>
<div class="flex gap-5 justify-between max-h-screen">
<div class="pl-3 w-full max-w-1/2">
<DownloaderForm />
</div>
<div class="pr-3 w-full max-w-1/2">
<DownloaderOutput />
</div>
</div>
</>
}
<button tabIndex={-1} type="button" onClick={() => { openUrl(`https://github.com/mmvanheusden/SteamDepotDownloaderGUI/releases/v${appVersion}`).catch((e) => console.error(e)); }} class="absolute right-0 bottom-0 text-white hover:underline">{`v${appVersion}`}</button>
</main>
</AppContext.Provider>
);
}
export default App;
export async function startDownload(options: {
username?: string;
password?: string;
appId: string;
depotId: string;
manifestId: string;
outputLocation?: string;
outputDirectoryName?: string;
}) {
await invoke("download_depotdownloader"); // First make backend download DepotDownloader
await invoke("start_download", {
steamDownload: {
...options,
outputDirectoryName: options.outputDirectoryName == "" ? null : options.outputDirectoryName, // empty string becomes null.
}
}); // First make backend download DepotDownloader
}

View File

@@ -1,108 +0,0 @@
import { Icon } from "@iconify-icon/react";
import { openUrl } from "@tauri-apps/plugin-opener";
import { useContext } from "preact/hooks";
import { startDownload } from "../App";
import { AppContext } from "../context";
import "../css/App.css";
import { BooleanUseState, FileInput, NumberInput, TextInput } from "./FormInput";
export function DownloaderForm() {
const context = useContext(AppContext);
return (
<>
<form class="mb-1">
<div class="flex flex-col gap-0.5 mb-8">
<TextInput id="username" label="Username" valueState={context.username!} placeholder="Leave empty for anonymous download" disabled={context.downloading![0]} />
<TextInput id="password" label="Password" valueState={context.password!} placeholder="Leave empty for anonymous download" disabled={context.downloading![0]} password={true} />
<div class="h-5" />
<NumberInput id="appId" label="App ID" valueState={context.appId!} required={true} disabled={context.downloading![0]} />
<NumberInput id="depotId" label="Depot ID" valueState={context.depotId!} required={true} disabled={context.downloading![0]} />
<NumberInput id="manifestId" label="Manifest ID" valueState={context.manifestId!} required={true} disabled={context.downloading![0]} />
<div class="h-1" />
<FileInput required={true} pathState={context.outputLocation!} disabled={context.downloading![0]} />
</div>
<DownloadButton disabled={context.downloading![0]} downloadingState={context.downloading!} />
</form>
<div class="flex gap-1 justify-between">
<InternetButton icon={"ic:sharp-discord"} title="Discord" href="https://discord.com/invite/3qCt4DT5qe" />
<InternetButton icon={"simple-icons:steamdb"} title="SteamDB" href="https://steamdb.info/instantsearch" />
<InternetButton icon={"mdi:youtube"} title="Tutorials" href="https://youtube.com/playlist?list=PLRAjc5plLScj967hnsYX-I3Vjw9C1v7Ca"/>
<InternetButton icon={"bx:donate-heart"} title="Donate" href="https://paypal.me/onderkin"/>
</div>
</>
);
}
function DownloadButton(
{disabled, downloadingState}: {disabled?: boolean, downloadingState: BooleanUseState}
) {
const [downloading, setDownloading] = downloadingState;
const context = useContext(AppContext);
const onClick = (e: MouseEvent) => {
if (downloading) {
console.warn("Already downloading!");
return;
}
const form: HTMLFormElement = (e.target as HTMLButtonElement).closest("form")!;
e.preventDefault(); // Block refreshing the page
form.reportValidity(); // Display native form validation
if (!form.checkValidity()) {
console.warn("Form invalid!");
return;
}
setDownloading(true);
startDownload({
username: context.username![0],
password: context.password![0],
appId: context.appId![0]!,
depotId: context.depotId![0]!,
manifestId: context.manifestId![0]!,
outputLocation: context.outputLocation![0],
outputDirectoryName: context.outputFolderName![0],
}).catch((e) => console.error(e));
// setDownloading(false)
};
return (
<div class={`flex transition-transform ${disabled ? "cursor-not-allowed" :"active:scale-102"}`}>
<button disabled={disabled} onClick={onClick} type="submit" class="inline-flex justify-between items-center py-1 w-full text-2xl font-bold text-white bg-green-600 rounded-l-md border-2 border-black hover:bg-green-700 active:bg-green-800 disabled:bg-red-500/70">
{downloading
? <>
<div class="flex absolute ml-2">
<Icon icon="line-md:downloading-loop" width="35" height="35" />
</div>
<span class="w-full">Downloading...</span>
</> :
<>
<div class="flex absolute ml-2">
<Icon icon="material-symbols:downloading-rounded" width="35" height="35" />
</div>
<span class="w-full">Download</span>
</>
}
</button>
<button disabled={disabled} onClick={() => context.showSettings![1](s => !s)} type="button" class="inline-flex justify-center items-center py-1 text-2xl font-bold text-center text-white bg-green-600 rounded-r-md border-2 border-l-0 border-black hover:bg-green-700 active:bg-green-800 group w-15 ring-l-gray-800 disabled:bg-red-500/70">
<Icon icon="heroicons:cog" width="30" height="30" class={`animate-spin [animation-play-state:paused] ${!disabled && "group-hover:[animation-play-state:running]"}`}/>
</button>
</div>
);
}
function InternetButton(
{title, icon, href, disabled}: {title: string, icon: string, href?: string, disabled?: boolean}
) {
const onClick = () => {
if (href) openUrl(href).catch((e) => console.error(e));
};
return (
<button disabled={disabled} onClick={onClick} type="button" class="inline-flex gap-0.5 justify-center items-center py-0.5 px-1 font-medium text-white rounded-md border border-black transition-transform disabled:pointer-events-none grow bg-blue-900/90 text-md hover:bg-blue-900/65 active:bg-blue-900/40 active:scale-103 disabled:bg-red-500/70">
<Icon icon={icon} height="20"/>{title}
</button>
);
}

View File

@@ -1,98 +0,0 @@
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { FitAddon } from "@xterm/addon-fit";
import { Terminal } from "@xterm/xterm";
import "@xterm/xterm/css/xterm.css";
import { useContext, useEffect, useRef, useState } from "preact/hooks";
import { AppContext } from "../context";
/* Parts of this file are derived from https://github.com/cablehead/tauri-xtermjs-nushell/blob/0bdd4a27ee2874de12e99bccd6c91d6ec5d28fbc/src/main.ts */
export function DownloaderOutput() {
const context = useContext(AppContext);
const [terminal, setTerminal] = useState<Terminal | undefined>();
// Hook on the "command-exited" Tauri emitter, and wait for it to emit, so we can flip the downloading state.
listen("command-exited", () => {
context.downloading?.[1]?.(false);
}).catch(console.error);
const terminalWindowRef = useRef(null);
useEffect(() => {
setTerminal(registerTerminal(terminalWindowRef.current!));
}, []);
return (
<div class="mx-auto mt-2 w-full h-full">
<div class="text-white bg-gray-900 rounded-md border border-gray-300 shadow shadow-blue-200">
<div class="inline-flex items-center my-px w-full font-semibold text-md">
<span class="w-full text-center">Download output</span>
{terminal &&
<button onClick={() => { if (!context.downloading![0]) terminal.reset(); }} type="button" disabled={context.downloading![0] ?? false} class="py-px px-2 my-1 mr-2 ml-auto font-normal border-2 disabled:text-gray-300 disabled:line-through disabled:cursor-not-allowed rounded-xs border-red-500/75 enabled:hover:bg-red-200/30 enabled:active:bg-red-200/50">
Clear
</button>
}
</div>
<div ref={terminalWindowRef} class="max-h-[74vh]"></div>
</div>
</div>
);
}
const registerTerminal: (terminalElement: HTMLElement) => Terminal = (terminalElement: HTMLElement) => {
/* eslint-disable @typescript-eslint/no-misused-promises */
const fitAddon = new FitAddon();
const term = new Terminal({
fontSize: 10,
cursorBlink: true,
rows: 100,
cols: 100,
theme: {
background: "rgb(30,33,46)",
},
});
term.loadAddon(fitAddon);
term.open(terminalElement);
function fitTerminal() {
fitAddon.fit();
void invoke<string>("async_resize_pty", {
rows: term.rows,
cols: term.cols,
});
}
// Write data from pty into the terminal
function writeToTerminal(data: string) {
return new Promise<void>((r) => {
term.write(data, () => r());
});
}
// Write data from the terminal to the pty
function writeToPty(data: string) {
void invoke("async_write_to_pty", {
data,
});
}
term.onData(writeToPty);
addEventListener("resize", fitTerminal);
fitTerminal();
async function readFromPty() {
const data = await invoke<string>("async_read_from_pty");
if (data) {
await writeToTerminal(data);
}
window.requestAnimationFrame(readFromPty);
}
window.requestAnimationFrame(readFromPty);
return term;
};

View File

@@ -1,95 +0,0 @@
import { Icon } from "@iconify-icon/react";
import { open as openDialog } from "@tauri-apps/plugin-dialog";
import { openPath } from "@tauri-apps/plugin-opener";
import { useState } from "preact/hooks";
import "../css/App.css";
export type StringUseState = ReturnType<typeof useState<string>>;
export type NumberUseState = ReturnType<typeof useState<number>>;
export type BooleanUseState = ReturnType<typeof useState<boolean>>;
export function Label({ forId, text, required }: { forId?: string, text: string, required?: boolean }) {
return (
<label for={forId} class={`text-md font-medium text-white ${required && "after:content-['*'] after:ml-1 after:text-xl after:text-red-500"}`}>
{text}
</label>
);
}
export function TextInput({ id, label, placeholder, valueState, required, password, disabled, className }: { id: string, label?: string, placeholder?: string, valueState: StringUseState, required?: boolean, password?: boolean, disabled?: boolean, className?: string }) {
const [value, setValue] = valueState;
const onInput = (e: InputEvent) => setValue((e.currentTarget as HTMLInputElement).value);
return (
<>
{label && <Label forId={id} text={label} required={required} />}
<input disabled={disabled} id={id} required={required} value={value} onInput={onInput} placeholder={placeholder} type={password ? "password" : "text"} class={`block py-2 px-3 w-full text-sm placeholder-gray-400 text-white rounded-lg border border-gray-600 transition duration-300 focus:border-blue-500 disabled:placeholder-white disabled:bg-gray-700 bg-[#161b22] focus:shadow-[0px_0px_29px_1px_rgba(59,130,246,0.5)] ${className} `} />
</>
);
}
export function NumberInput({ id, label, placeholder, valueState, required, min, max, step, disabled }: { id: string, label: string, placeholder?: string, valueState: StringUseState, required?: boolean, min?: number, max?: number, step?: number, disabled?: boolean }) {
const [value, setValue] = valueState;
const onInput = (e: Event) => {
const newVal: string = (e.currentTarget as HTMLInputElement).value;
// // https://stackoverflow.com/a/73143643
// if (!(!isNaN(parseFloat(newVal)) && !isNaN(+newVal))) { // Check if new value is a number
// console.warn("Not a number!")
// e.preventDefault();
// return;
// }
setValue(newVal);
};
return (
<>
<Label forId={id} text={label} required={required} />
<input disabled={disabled} id={id} required={required} value={value} onInput={onInput} min={min ?? 1} max={max} step={step} placeholder={placeholder} type="number" pattern="[0-9]" class="block py-2 px-3 w-full text-sm placeholder-gray-400 text-white rounded-lg border border-gray-600 transition duration-300 focus:border-blue-500 disabled:placeholder-white disabled:bg-gray-700 bg-[#161b22] focus:shadow-[0px_0px_29px_1px_rgba(59,130,246,0.5)]" />
</>
);
}
export function FileInput({ required, pathState, disabled }: { required?: boolean, pathState: StringUseState, disabled?: boolean }) {
const [path, setPath] = pathState;
const selectPath = () => {
openDialog({
title: "Choose where to save the game download.",
multiple: false,
directory: true,
canCreateDirectories: true,
})
.then((selectedPath) => {
if (!selectedPath) {
console.warn("Nothing selected, doing nothing.");
return;
}
setPath(selectedPath);
})
.catch((e) => console.error(e));
};
const previewPath = () => {
openPath(path!).catch((e) => console.error(e));
};
return (
<>
<Label text="Output directory" required={required} />
<div className="flex gap-2 justify-start w-2/3">
<button disabled={disabled} type="button" onClick={selectPath}
className="inline-flex gap-2 justify-center items-center py-1 px-2 font-medium text-white rounded-md border border-gray-600 shadow-2xl transition-transform disabled:cursor-not-allowed grow bg-blue-900/90 text-md enabled:active:scale-103 hover:bg-blue-900/65 active:bg-blue-900/40 disabled:bg-red-500/70">
<Icon icon="subway:folder-2" height="25" width="25" />Choose
</button>
<input required={required} type="text" hidden
value={path}></input> {/* A hidden text input which holds the path useState value, so the form will be invalid when no path is selected. */}
<button type="button" disabled={!path} onClick={() => {if (path) previewPath(); }}
className="inline-flex gap-2 justify-center items-center py-1 px-2 font-medium text-white rounded-md border border-gray-600 shadow-2xl transition-transform disabled:cursor-not-allowed grow bg-blue-900/90 text-md enabled:active:scale-103 hover:bg-blue-900/65 active:bg-blue-900/40 disabled:bg-red-500/70">
<Icon icon="material-symbols:folder-eye" height="25" width="25" />Preview
</button>
</div>
</>
);
}

View File

@@ -1,46 +0,0 @@
import { Icon } from "@iconify-icon/react";
import { useContext } from "preact/hooks";
import { AppContext } from "../context";
import { TextInput } from "./FormInput";
export function Settings() {
const context = useContext(AppContext);
const [settings, setSettings] = context.appSettings!;
return (
<>
<div class="flex justify-center w-full">
<button onClick={() => context.showSettings![1](s => !s)} type="button" class="inline-flex absolute left-0 items-center py-1 px-2 text-xl font-semibold text-white bg-green-600 rounded translate-x-1 translate-y-1 hover:bg-green-700 active:bg-green-800">
<Icon icon="icon-park-solid:back" width="20" height="20"/>Back
</button>
<div class="mb-1 text-4xl font-bold text-center text-white font-['Hubot_Sans']">
Settings
</div>
</div>
<div class="px-[10%]">
<div class="text-2xl font-semibold text-white">
Directory name
</div>
<p class="mb-3 text-white">
This is the name the folder DepotDownloader will download the game in.<br />
It will be located in the chosen output directory.
</p>
<div class="flex gap-px mb-2 rounded-md shadow-xs">
<button
onClick={() => setSettings(oldSettings => { context.outputFolderName![1](""); return ({ ...oldSettings, outputDirectoryMode: "Manifest ID" }); })}
type="button"
class={`border-black px-3 py-2 border border-r-0 font-medium text-md disabled:bg-red-500/70 disabled:pointer-events-none inline-flex items-center justify-center rounded-s-lg focus:z-10 focus:ring-2 text-white hover:text-white focus:ring-blue-500 focus:text-white ${settings?.outputDirectoryMode === "Manifest ID" ? "bg-blue-500 underline border-0 outline-gray-300 outline shadow-[0px_0px_29px_-8px_cornflowerblue] hover:bg-blue-600 active:bg-blue-700" : "hover:bg-blue-800 active:bg-blue-900 bg-blue-700"}`}>
Manifest ID
</button>
<button
onClick={() => setSettings(oldSettings => ({ ...oldSettings, outputDirectoryMode: "Custom" }))}
type="button"
class={`border-black px-3 py-2 border border-l-0 font-medium text-md disabled:bg-red-500/70 disabled:pointer-events-none inline-flex items-center justify-center rounded-e-lg focus:z-10 focus:ring-2 text-white hover:text-white focus:ring-blue-500 focus:text-white ${settings?.outputDirectoryMode === "Custom" ? "bg-blue-500 underline border-0 outline-gray-300 outline shadow-[0px_0px_29px_-8px_cornflowerblue] hover:bg-blue-600 active:bg-blue-700" : "hover:bg-blue-800 active:bg-blue-900 bg-blue-700"}`}>
Custom
</button>
</div>
<TextInput className="max-w-1/2" disabled={settings?.outputDirectoryMode === "Manifest ID"} id="directoryName" placeholder="DepotDownloader output directory name" valueState={context.outputFolderName!} />
</div>
</>
);
}

View File

@@ -1,31 +0,0 @@
import { createContext } from "preact";
import { SetStateAction } from "preact/compat";
import { Dispatch, useState } from "preact/hooks";
import { BooleanUseState, StringUseState } from "./components/FormInput";
// Source: https://stackoverflow.com/a/75420688
type NoUndefinedState<T> =
T extends [infer S | undefined, Dispatch<SetStateAction<infer S | undefined>>]
? [S, Dispatch<SetStateAction<S>>]
: never;
export interface AppSettings {
outputDirectoryMode: "Manifest ID" | "Custom"
}
interface AppContext {
username: StringUseState;
password: StringUseState;
appId: StringUseState;
depotId: StringUseState;
manifestId: StringUseState;
outputLocation: StringUseState;
outputFolderName: StringUseState;
downloading: BooleanUseState;
showSettings: BooleanUseState;
appSettings: NoUndefinedState<ReturnType<typeof useState<AppSettings>>>
}
export const AppContext = createContext<Partial<AppContext>>({});

View File

@@ -1,20 +0,0 @@
@import "tailwindcss";
@font-face {
font-family: 'Hubot Sans';
src: url('../assets/Hubot-Sans.woff2') format('woff2 supports variations'),
url('../assets/Hubot-Sans.woff2') format('woff2-variations');
font-weight: 700;
font-stretch: expanded;
}
/* Disable + and - buttons for number input */
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
button:enabled {
cursor: pointer;
}

View File

@@ -1,13 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta content="width=device-width, initial-scale=1" name="viewport"/>
<title>SteamDepotDownloaderGUI</title>
</head>
<body>
<div id="root"></div>
<script src="main.tsx" type="module"></script>
</body>
</html>

View File

@@ -1,4 +0,0 @@
import { render } from "preact";
import App from "./App";
render(<App />, document.getElementById("root")!);

1
src/vite-env.d.ts vendored
View File

@@ -1 +0,0 @@
/// <reference types="vite/client" />

68
style.css Normal file
View File

@@ -0,0 +1,68 @@
@font-face {
font-family: 'Hubot Sans';
src: url('Hubot-Sans.woff2') format('woff2 supports variations'),
url('Hubot-Sans.woff2') format('woff2-variations');
font-weight: 700;
font-stretch: expanded;
}
.f1-light {
font-family: 'Hubot Sans', sans-serif;
overflow: hidden;
white-space: nowrap;
}
/* The grey part */
.settings-surrounding {
display: none;
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: hidden;
background-color: rgba(0, 0, 0, 0.33);
}
.settings-content {
position: relative;
border-radius: 10px;
overflow: auto;
/*noinspection CssUnresolvedCustomProperty*/
background-color: var(--bgColor-default, var(--color-canvas-default));
margin: 5%;
padding: 25px;
border: 1px solid #b4dede;
width: 90vw; /* 90vw -> 90% */
height: 90vh; /* 90vh -> 90% */
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1), 0 6px 20px rgba(0, 0, 0, 0.1);
}
.hide {
display: none;
}
hr {
border: 0;
height: 1px;
background: #0d1117 linear-gradient(to right, #0d1117, #ccc, #0d1117);
}
[data-color-mode="light"] hr {
filter: invert(1);
}
@media (prefers-color-scheme: light) {
[data-color-mode="auto"] hr {
filter: invert(1);
}
}
.version-info {
position: absolute;
bottom: 0;
right: 0;
font-size: 0.8em;
padding: 5px 10px; /* space around text */
font-family: monospace;
}

View File

@@ -1,26 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"jsxImportSource": "preact",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -1,10 +0,0 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

395
utils.js Normal file
View File

@@ -0,0 +1,395 @@
var defaultTerminal = ""
/**
* Checks if all required fields are filled and if dotnet is installed in the system path.
* It returns a promise that resolves to true if dotnet is installed and all required fields are filled, false otherwise.
*
* @returns {Promise<unknown>} A promise that resolves to true if dotnet is installed and all required fields are filled, false otherwise.
*
* **rejects**:
* `emptyField` -> One or more required field(s) are not filled in.
* `noDotnet` -> `dotnet` has not been found in the path.
*/
function preDownloadCheck() {
return new Promise((resolve, reject) => {
// Check if all fields are filled
const formInputs = document.forms["theform"]
let unfilledFields = 0
for (const input of formInputs) {
const isInvalid = input.value === "" && input.parentElement.classList.contains("required")
input.parentElement.classList.toggle("errored", isInvalid) // toggle the 'errored' class depending on if isInvalid is true or false.
if (isInvalid) unfilledFields++
}
if (unfilledFields > 0) {
reject("emptyField")
return
}
// Check if dotnet is found, depending on the platform
if (process.platform.toString().includes("win")) {
// Windows
const {exec} = require("child_process")
const command = "dotnet.exe --version"
exec(command, function (error) {
if (error) {
reject("noDotnet")
} else {
resolve(true)
}
})
} else {
// Others
// macOS seems to be broken.
const {exec} = require("child_process")
const command = "dotnet --version"
exec(command, function (error) {
if (error) {
reject("noDotnet")
} else {
resolve(true)
}
})
}
})
}
/**
* Downloads a file from a given URL and saves it to the current directory.
*
* @param {string} url - The URL of the file to download.
* @returns {Promise<void>} A promise that resolves when the download is finished, or rejects if an error occurs.
*
* @throws {Error} If an error occurs during the download or file writing process, the promise is rejected with the error.
*/
function download(url) {
return new Promise((resolve, reject) => {
const {https} = require("follow-redirects") /* Using follow-redirects to follow redirects */
const fs = require("fs")
const path = require("path")
const file = fs.createWriteStream(platformpath() + path.sep + url.split("/").pop())
https.get(url, (response) => {
response.pipe(file)
file.on("finish", () => {
file.close()
resolve()
})
file.on("error", (error) => {
console.error(error)
reject(error)
})
})
})
}
/**
* Removes a file from the current directory.
*
* @param {string} file - The name of the file to be removed.
* @returns {Promise<void>} A promise that resolves when the file is successfully removed, or rejects if an error occurs.
*
* @throws {Error} If an error occurs during the file removal process, the promise is rejected with the error.
*/
function removeFile(file) {
return new Promise((resolve, reject) => {
const fs = require("fs")
const path = require("path")
fs.unlink(platformpath() + path.sep + file, function (error) {
if (error) {
reject(error)
console.error(error)
} else resolve()
})
})
}
/**
* Removes a directory from the current directory.
*
* @param {string} dir - The name of the directory to be removed.
* @returns {Promise<void>} A promise that resolves when the directory is successfully removed, or rejects if an error occurs.
*
* @throws {Error} If an error occurs during the directory removal process, the promise is rejected with the error.
*/
function removeDir(dir,) {
return new Promise((resolve, reject) => {
const fs = require("fs")
const path = require("path")
fs.rm(platformpath() + path.sep + dir, {recursive: true, force: true}, function (error) {
if (error) {
reject(error)
console.error(error)
} else resolve()
})
})
}
/**
* Unzips a file to a specified target directory.
*
* @param {string} file - The name of the zip file to be unzipped.
* @param {string} target - The target directory where the file will be unzipped to.
* @returns {Promise<void>} A promise that resolves when the unzip operation is complete, or rejects if an error occurs.
*
* @throws {Error} If an error occurs during the unzip operation, the promise is rejected with the error.
*/
function unzip(file, target) {
const {exec} = require("child_process")
const path = require("path")
let finalPath = platformpath() + path.sep
return new Promise((resolve, reject) => {
if (process.platform.toString().includes("win")) {
const command = `powershell.exe -Command Expand-Archive -Path '${finalPath + file}' -Destination '${finalPath + target}'`
exec(command, function (error) {
if (error) {
reject(error)
console.error(error)
} else resolve()
})
} else {
const command = `unzip -o ${platformpath().replaceAll(" ", "\\ ") + path.sep + file} -d ${platformpath().replaceAll(" ", "\\ ") + path.sep + target}${path.sep}`
exec(command, function (error) {
if (error) {
reject(error)
console.error(error)
} else resolve()
})
}
})
}
async function createCommandWithTerminal(command, terminal, os) {
let cmd = ""
if (terminal === "auto") {
terminal = defaultTerminal[0]
}
// if os is auto, choose the os for us.
if (os === "auto") {
if (process.platform.toString().includes("win")) {
os = 0
} else if (process.platform.toString().includes("linux")) {
os = 2
}
} else console.log("OS is manually chosen")
/* eslint-disable */
if (os === 0) {
cmd = `start cmd.exe /k ${command}`
} else if (os === 1) {
cmd = `osascript -c 'tell application "Terminal" to do script '${command}'`
} else if (os === 2) {
switch (terminal) {
case 0:
cmd = `gnome-terminal -e 'bash -c "${command};$SHELL"'`
break
case 1:
cmd = `konsole --hold -e "${command}"`
break
case 2:
cmd = `xfce4-terminal -H -e "${command}"`
break
case 3:
cmd = `terminator -e 'bash -c "${command};$SHELL"'`
break
case 4:
cmd = `terminology -H -e "${command}"`
break
case 5:
cmd = `xterm -hold -T "Downloading Depot..." -e "${command}"`
break
case 6:
cmd = `kitty --hold sh -c "${command}"`
break
case 7:
cmd = `lxterminal -e "${command};$SHELL"`
break
case 8:
cmd = `tilix -e sh -c "${command};$SHELL"`
break
case 9:
cmd = `deepin-terminal -e 'sh -c "${command};$SHELL"'`
break
case 10:
cmd = `cool-retro-term -e sh -c "${command}"`
break
default:
console.log("NO TERMINAL. PANIC.")
}
} else if (os === 3) {
if (process.platform.toString().includes("win")) {
console.log(`COPY-PASTE THE FOLLOWING INTO THE TERMINAL:\n\n${command}`)
} else console.log(`COPY-PASTE THE FOLLOWING INTO YOUR TERMINAL:\n\n${command}`)
cmd = ""
}
return cmd
/* eslint-enable */
}
async function generateRunScript(username, password, appid, depotid, manifestid, folderinput, chosenPath) {
const path = require("path")
const fs = require("fs")
const sep = path.sep
let foldername = ""
// allow enormous strings like &$§"&$="§$/"(§NJUIDW>;!%?aQ52V?*['YsDnRy|(+Q 1h6BmnDQp,(Xr& being used as password.
// This is very janky and will be replaced with a better solution in the upcoming rewrite.
password = password.replace(/"/g, "\"\"")
if (process.platform.includes("win")) {
// Replace all % with %% because batch scripts use % as a special character. This is not an elegant solution, but it should work.
password = password.replace(/%/g, "%%")
}
// if either the username or password fields is empty, anonymous login is used
let anonymous = username === "" || password === ""
/* put the username and password flags into one string, allowing for anonymous login.
if anonymous: false true
| |
*/
let userpass = anonymous ? "" : `-username ${username} -password "${password}"`
/* if nothing is inputted by the user in the folder input, it will be defaulted to the appid. else to the value */
foldername = folderinput.value === "" ? appid : folderinput.value
/* if the path isn't selected by the user, go for the path the app is located in. else use the path the user chose. */
chosenPath = chosenPath === "" ? platformpath() : chosenPath
let finalPath = (chosenPath + path.sep + foldername)
if (process.platform.includes("win")) {
if (finalPath.includes(" ")) {
console.log("path contains spaces. adding quotes")
finalPath = `"${finalPath}"`
console.log("Result: " + finalPath)
}
} else {
finalPath = finalPath.replaceAll(" ", "\\ ")
}
/* / or \ if nothing is inputted its appid replaces " " with "\ ", so whitespaces can be in path names.
finalpath: ((the path the user chose) + (seperator) + (the folder name the user chose)).replaceAll()
*/
/* The structure of a DepotDownloader command:
.NET CLI Path to the DepotDownloader dll. Username/pass combination app id depot id manifest id the dir chosen by user or app path controls how much download servers and threads are used. Needs benchmark TODO
| | | | | | | | |
dotnet (path)(sep)depotdownloader(sep)DepotDownloader.dll (userpass) -app (appid) -depot (depotid) -manifest (manifestid) -dir (path)(sep) -max-servers 50 -max-downloads 16
*/
if (process.platform.includes("linux")) {
// if linux, write a bash script.
let content = `#!/usr/bin/env bash
dotnet ${platformpath().replaceAll(" ", "\\ ")}${sep}depotdownloader${sep}DepotDownloader.dll ${userpass} -app ${appid} -depot ${depotid} -manifest ${manifestid} -dir ${finalPath}${sep} -max-servers 50 -max-downloads 16
`
await fs.writeFileSync(`${platformpath()}${sep}run.sh`, content)
await fs.chmodSync(`${platformpath()}${sep}run.sh`, "755") // make it executable
console.log(`Writing:
--------
dotnet ${platformpath().replaceAll(" ", "\\ ")}${sep}depotdownloader${sep}DepotDownloader.dll ${userpass} -app ${appid} -depot ${depotid} -manifest ${manifestid} -dir ${finalPath}${sep} -max-servers 50 -max-downloads 16
to ${platformpath()}${sep}run.sh.
`)
} else if (process.platform.includes("win")) {
// if windows, write a batch script
let content = `dotnet "${platformpath()}${sep}depotdownloader${sep}DepotDownloader.dll" ${userpass} -app ${appid} -depot ${depotid} -manifest ${manifestid} -dir ${finalPath} -max-servers 50 -max-downloads 16`
await fs.writeFileSync(`${platformpath()}${sep}run.bat`, content)
console.log(`Writing:
--------
dotnet "${platformpath()}${sep}depotdownloader${sep}DepotDownloader.dll" ${userpass} -app ${appid} -depot ${depotid} -manifest ${manifestid} -dir ${finalPath} -max-servers 50 -max-downloads 16
to ${platformpath()}${sep}run.bat.
`)
} else { /* macos */
}
}
/**
* Executes a given command in a separate process.
*
* @param {string} command - The command to be executed.
* @returns {Promise<void>} A promise that resolves when the command execution is successful, or rejects if an error occurs.
*
* @throws {Error} If an error occurs during the command execution, the promise is rejected with an error message.
*/
function runCommand(command) {
return new Promise((resolve, reject) => {
const {exec} = require("child_process")
exec(command, function (error) {
if (error) {
const msg = "Running command failed with error:\n" + error
reject(msg)
} else resolve()
})
})
}
/**
* Returns the path where the actual program is being run from, depending on the operating system.
* Because __dirname is inconsistent across operating systems, this function is used to get the correct path.
*
* Windows -> Dev env: __dirname, Portable: process.env.PORTABLE_EXECUTABLE_DIR
* Linux -> process.cwd()
* @returns {string} The absolute path
*/
const platformpath = () => {
// On linux, it must return process.cwd(). On windows, it must return process.env.PORTABLE_EXECUTABLE_DIR, but only if the program is running from a portable exe.
// On linux, __dirname returns the correct path, but on windows, it returns the path to the app.asar file, which is not what we want. Only in dev environment it returns the correct path.
if (process.platform.includes("win")) {
if (process.env.PORTABLE_EXECUTABLE_DIR !== undefined) {
return process.env.PORTABLE_EXECUTABLE_DIR
} else {
return __dirname
}
} else {
return process.cwd()
}
}
/**
* Checks for the availability of terminal emulators on Linux.
* It runs the '--version' command on each terminal emulator and checks if the command is successful.
* If the command is successful, it means the terminal emulator is installed and available.
* The function returns an array of the indices of the available terminal emulators.
* If no terminal emulator is available, it returns false.
* If the operating system is not Linux, it also returns false.
*
* @returns {string[]|boolean} An array of the indices of the available terminal emulators, or false if none are available or the OS is not Linux.
*/
const forceTerminals = async () => {
const commands = [["gnome-terminal", "--version", 0], ["konsole", "--version", 1], ["xfce4-terminal", "--version", 2], ["terminator", "--version", 3], ["terminology", "--version", 4], ["xterm", "-v", 5], ["kitty", "--version", 6], ["lxterminal", "--version", 7], ["tilix", "--version", 8], ["deepin-terminal", "--version", 9], ["cool-retro-term", "--version", 10]]
let availableTerminals = [] // list of IDs of terminals, corrospending to their index in the dropdown menu
let availableNames = [] // list of names of found terminals
if (process.platform === "linux") {
for (let i = 0; i < commands.length; i++) {
await runCommand(`${commands[i][0]} ${commands[i][1]}`).then(() => {
console.log(`Found ${commands[i][0]}`)
availableTerminals.push(commands[i][2])
availableNames.push(commands[i][0])
}).catch(() => {
console.log(`${commands[i][0]} not found on system.`)
})
}
if (availableTerminals.length > 0) {
defaultTerminal = availableTerminals
return [true, availableTerminals, availableNames] // [true, "2,4,7", "twox,fourterm,sevenemulator"] (example)
} else return false
} else return false
}
module.exports = {
preDownloadCheck,
download,
runCommand,
removeDir,
removeFile,
unzip,
platformpath,
forceTerminals,
generateRunScript,
createCommandWithTerminal
}

View File

@@ -1,41 +0,0 @@
import {defineConfig} from "vite";
import preact from "@preact/preset-vite";
import tailwindcss from '@tailwindcss/vite'
// @ts-expect-error process is a nodejs global
const host = process.env.TAURI_DEV_HOST;
// https://vitejs.dev/config/
export default defineConfig(async () => ({
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
//
// 1. prevent vite from obscuring rust errors
clearScreen: false,
// 2. tauri expects a fixed port, fail if that port is not available
server: {
port: 1420,
strictPort: true,
host: host || false,
hmr: host
? {
protocol: "ws",
host,
port: 1421,
}
: undefined,
watch: {
// 3. tell vite to ignore watching `src-tauri`
ignored: ["**/src-tauri/**"],
}
},
root: "src",
build: {
outDir: '../dist',
emptyOutDir: true,
},
plugins: [
tailwindcss(),
preact(),
]
}));