diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 831d042..d16204f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,66 @@ on: workflow_dispatch: jobs: + check-skip: + runs-on: ubuntu-latest + outputs: + skip: ${{ steps.check.outputs.skip }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Check commit messages for skip keywords + id: check + shell: bash + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + keywords='\[skip ci\]|\[ci skip\]|skip-ci|skip_ci|SKIP_CI|NO_CI' + + skip_found=false + + if [ -n "${GITHUB_SHA-}" ] && [ -n "${GITHUB_BASE_REF-}" ] || true; then + git fetch --no-tags --depth=50 origin +refs/heads/*:refs/remotes/origin/* || true + if [ -n "${{ github.event.before || '' }}" ] && [ "${{ github.event.before }}" != "0000000000000000000000000000000000000000" ]; then + commit_range="${{ github.event.before }}..${{ github.event.after }}" + else + commit_range="${GITHUB_SHA}" + fi + messages=$(git log --pretty=%B $commit_range 2>/dev/null || true) + else + messages="" + fi + + if [ -z "$(echo "$messages" | tr -d '[:space:]')" ]; then + messages="$(jq -r '.commits[].message' < "$GITHUB_EVENT_PATH" 2>/dev/null || true)" + fi + + echo "---- Collected commit messages ----" + if [ -n "$(echo "$messages" | tr -d '[:space:]')" ]; then + printf "%s\n" "$messages" + else + echo "" + fi + echo "---- End commit messages ----" + + echo "---- Matching lines (case-insensitive) ----" + # Print matching lines with context to aid debugging + echo "$messages" | nl -ba | grep -Ei --color=never "$keywords" || true + echo "---- End matching lines ----" + + if echo "$messages" | grep -Ei "$keywords" >/dev/null; then + echo "Found skip keyword in commit message. Cancelling run." + skip_found=true + else + echo "No skip keyword found in commits. Continuing run." + fi + + echo "skip=$skip_found" >> $GITHUB_OUTPUT + build: + needs: check-skip + if: needs.check-skip.outputs.skip != 'true' strategy: fail-fast: false matrix: @@ -112,6 +171,7 @@ jobs: shell: pwsh run: | mkdir -p artifacts-windows + $version = "${{ steps.package-version-windows.outputs.current-version }}" Get-ChildItem -Path ./src-tauri/target/release/bundle/msi/*.msi -Recurse | ForEach-Object { Copy-Item $_.FullName -Destination artifacts-windows/ @@ -126,7 +186,11 @@ jobs: } Get-ChildItem -Path ./src-tauri/target/release/*.exe | ForEach-Object { - Copy-Item $_.FullName -Destination artifacts-windows/ + $baseName = [System.IO.Path]::GetFileNameWithoutExtension($_.Name) + $extension = [System.IO.Path]::GetExtension($_.Name) + $newName = "${baseName}_${version}${extension}" + $newPath = Join-Path "artifacts-windows" $newName + Copy-Item $_.FullName -Destination $newPath $md5 = Get-FileHash $_.FullName -Algorithm MD5 echo "EXE_MD5=$($md5.Hash)" >> $env:GITHUB_ENV } @@ -137,6 +201,7 @@ jobs: if: matrix.platform == 'ubuntu-latest' run: | mkdir -p artifacts-ubuntu + version="${{ steps.package-version-ubuntu.outputs.current-version }}" find ./src-tauri/target/release/bundle/deb -name "*.deb" | while read file; do cp "$file" artifacts-ubuntu/ @@ -150,6 +215,22 @@ jobs: echo "APPIMAGE_MD5=$MD5" >> $GITHUB_ENV done + # Include the standalone Linux binary with version + find ./src-tauri/target/release -maxdepth 1 -type f -executable -not -name "*.so" -not -name "*.d" | while read file; do + if [ -f "$file" ] && file "$file" | grep -q "ELF.*executable"; then + basename=$(basename "$file") + extension="" + if [[ "$basename" == *.* ]]; then + extension=".${basename##*.}" + basename="${basename%.*}" + fi + versioned_name="${basename}_${version}${extension}" + cp "$file" "artifacts-ubuntu/$versioned_name" + MD5=$(md5sum "$file" | awk '{ print $1 }') + echo "BINARY_MD5=$MD5" >> $GITHUB_ENV + fi + done + echo "ARTIFACT_PATH=artifacts-ubuntu" >> $GITHUB_ENV - name: VirusTotal Scan (Windows only) @@ -168,7 +249,6 @@ jobs: shell: bash run: | ANALYSIS="${{ steps.virustotal.outputs.analysis }}" - # Extract any VirusTotal GUI URLs from the analysis output; handles comma-separated entries urls=$(echo "$ANALYSIS" | tr ',' '\n' | grep -oE 'https?://[^[:space:]]*virustotal[^[:space:]]*' | sed 's/[[:space:]]*$//' | uniq) if [ -n "$urls" ]; then printf 'VIRUSTOTAL_URL<> $GITHUB_ENV @@ -178,13 +258,6 @@ jobs: echo "VIRUSTOTAL_LINE=" >> $GITHUB_ENV fi - - name: Write VirusTotal URL into artifacts (Windows only) - if: matrix.platform == 'windows-latest' && env.VIRUSTOTAL_URL != '' - shell: pwsh - run: | - if (!(Test-Path -Path artifacts-windows)) { New-Item -ItemType Directory -Path artifacts-windows | Out-Null } - Set-Content -Path artifacts-windows/virustotal-url.txt -Value $env:VIRUSTOTAL_URL -Encoding utf8 - - name: Upload artifact uses: actions/upload-artifact@v4 with: @@ -192,7 +265,8 @@ jobs: path: ${{ matrix.platform == 'windows-latest' && 'artifacts-windows/**' || 'artifacts-ubuntu/**' }} create-release: - needs: build + needs: [check-skip, build] + if: needs.check-skip.outputs.skip != 'true' runs-on: ubuntu-latest permissions: contents: write @@ -224,29 +298,6 @@ jobs: - name: List artifacts run: find release-artifacts -type f - - name: Extract VirusTotal URL (if present) - id: virustotal-url - run: | - file=$(find release-artifacts -type f -name 'virustotal-url.txt' | head -n1 || true) - if [ -n "$file" ] && [ -f "$file" ]; then - # Read all lines and create a bullet list - urls=$(sed 's/\r$//' "$file" | sed '/^[[:space:]]*$/d') - if [ -n "$urls" ]; then - # prefix each line with '- ' for bullets - bullets=$(echo "$urls" | sed 's/^/- /') - printf 'VIRUSTOTAL_URL<> $GITHUB_ENV - printf 'VIRUSTOTAL_LINE<> $GITHUB_ENV - echo "url=$urls" >> $GITHUB_OUTPUT - else - echo "VIRUSTOTAL_LINE=" >> $GITHUB_ENV - echo "url=" >> $GITHUB_OUTPUT - fi - else - echo "VIRUSTOTAL_LINE=" >> $GITHUB_ENV - echo "url=" >> $GITHUB_OUTPUT - fi - shell: bash - - name: Create Release uses: softprops/action-gh-release@v1 env: @@ -259,8 +310,8 @@ jobs: Automatic build from GitHub Actions - - Windows (.msi, .exe installer, standalone .exe) - - Linux (.deb) + - Windows (.msi, .exe installer, standalone .exe with version) + - Linux (.deb, standalone binary with version) Note: This is an automated build from the main branch. Commit hash: ${{ env.HASH }} diff --git a/package-lock.json b/package-lock.json index a01902b..feb95d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,33 +8,33 @@ "name": "collapseloader", "version": "0.2.0", "dependencies": { - "@guolao/vue-monaco-editor": "^1.5.5", + "@guolao/vue-monaco-editor": "1.5.5", "@tauri-apps/api": "2.8.0", - "@tauri-apps/plugin-dialog": "^2.4.0", - "@tauri-apps/plugin-fs": "^2.4.2", + "@tauri-apps/plugin-dialog": "2.4.0", + "@tauri-apps/plugin-fs": "2.4.2", "@tauri-apps/plugin-notification": "2.3.1", "@tauri-apps/plugin-opener": "2.5.0", - "axios": "1.12.1", - "daisyui": "5.1.10", + "axios": "1.12.2", + "daisyui": "5.1.13", "gsap": "3.13.0", "lucide-vue-next": "0.544.0", - "monaco-editor": "^0.53.0", + "monaco-editor": "0.53.0", "vue": "3.5.21", - "vue-i18n": "^11.1.12", + "vue-i18n": "11.1.12", "vue3-lottie": "3.3.1" }, "devDependencies": { "@tailwindcss/vite": "4.1.13", "@tauri-apps/cli": "2.8.4", - "@types/node": "24.3.2", - "@typescript-eslint/eslint-plugin": "^8.43.0", - "@typescript-eslint/parser": "^8.43.0", + "@types/node": "24.5.2", + "@typescript-eslint/eslint-plugin": "8.44.0", + "@typescript-eslint/parser": "8.44.0", "@vitejs/plugin-vue": "6.0.1", - "eslint": "^9.35.0", - "eslint-plugin-vue": "^10.4.0", + "eslint": "9.36.0", + "eslint-plugin-vue": "10.4.0", "tailwindcss": "4.1.13", "typescript": "5.9.2", - "vite": "7.1.5", + "vite": "7.1.6", "vue-tsc": "3.0.7" } }, @@ -676,9 +676,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.35.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.35.0.tgz", - "integrity": "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==", + "version": "9.36.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.36.0.tgz", + "integrity": "sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==", "dev": true, "license": "MIT", "engines": { @@ -1819,13 +1819,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.3.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.2.tgz", - "integrity": "sha512-6L8PkB+m1SSb2kaGGFk3iXENxl8lrs7cyVl7AXH6pgdMfulDfM6yUrVdjtxdnGrLrGzzuav8fFnZMY+rcscqcA==", + "version": "24.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", + "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.10.0" + "undici-types": "~7.12.0" } }, "node_modules/@types/trusted-types": { @@ -1835,17 +1835,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.43.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.43.0.tgz", - "integrity": "sha512-8tg+gt7ENL7KewsKMKDHXR1vm8tt9eMxjJBYINf6swonlWgkYn5NwyIgXpbbDxTNU5DgpDFfj95prcTq2clIQQ==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.44.0.tgz", + "integrity": "sha512-EGDAOGX+uwwekcS0iyxVDmRV9HX6FLSM5kzrAToLTsr9OWCIKG/y3lQheCq18yZ5Xh78rRKJiEpP0ZaCs4ryOQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.43.0", - "@typescript-eslint/type-utils": "8.43.0", - "@typescript-eslint/utils": "8.43.0", - "@typescript-eslint/visitor-keys": "8.43.0", + "@typescript-eslint/scope-manager": "8.44.0", + "@typescript-eslint/type-utils": "8.44.0", + "@typescript-eslint/utils": "8.44.0", + "@typescript-eslint/visitor-keys": "8.44.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -1859,22 +1859,22 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.43.0", + "@typescript-eslint/parser": "^8.44.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.43.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.43.0.tgz", - "integrity": "sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.44.0.tgz", + "integrity": "sha512-VGMpFQGUQWYT9LfnPcX8ouFojyrZ/2w3K5BucvxL/spdNehccKhB4jUyB1yBCXpr2XFm0jkECxgrpXBW2ipoAw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.43.0", - "@typescript-eslint/types": "8.43.0", - "@typescript-eslint/typescript-estree": "8.43.0", - "@typescript-eslint/visitor-keys": "8.43.0", + "@typescript-eslint/scope-manager": "8.44.0", + "@typescript-eslint/types": "8.44.0", + "@typescript-eslint/typescript-estree": "8.44.0", + "@typescript-eslint/visitor-keys": "8.44.0", "debug": "^4.3.4" }, "engines": { @@ -1890,14 +1890,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.43.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.43.0.tgz", - "integrity": "sha512-htB/+D/BIGoNTQYffZw4uM4NzzuolCoaA/BusuSIcC8YjmBYQioew5VUZAYdAETPjeed0hqCaW7EHg+Robq8uw==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.44.0.tgz", + "integrity": "sha512-ZeaGNraRsq10GuEohKTo4295Z/SuGcSq2LzfGlqiuEvfArzo/VRrT0ZaJsVPuKZ55lVbNk8U6FcL+ZMH8CoyVA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.43.0", - "@typescript-eslint/types": "^8.43.0", + "@typescript-eslint/tsconfig-utils": "^8.44.0", + "@typescript-eslint/types": "^8.44.0", "debug": "^4.3.4" }, "engines": { @@ -1912,14 +1912,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.43.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.43.0.tgz", - "integrity": "sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.44.0.tgz", + "integrity": "sha512-87Jv3E+al8wpD+rIdVJm/ItDBe/Im09zXIjFoipOjr5gHUhJmTzfFLuTJ/nPTMc2Srsroy4IBXwcTCHyRR7KzA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.43.0", - "@typescript-eslint/visitor-keys": "8.43.0" + "@typescript-eslint/types": "8.44.0", + "@typescript-eslint/visitor-keys": "8.44.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1930,9 +1930,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.43.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.43.0.tgz", - "integrity": "sha512-ALC2prjZcj2YqqL5X/bwWQmHA2em6/94GcbB/KKu5SX3EBDOsqztmmX1kMkvAJHzxk7TazKzJfFiEIagNV3qEA==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.44.0.tgz", + "integrity": "sha512-x5Y0+AuEPqAInc6yd0n5DAcvtoQ/vyaGwuX5HE9n6qAefk1GaedqrLQF8kQGylLUb9pnZyLf+iEiL9fr8APDtQ==", "dev": true, "license": "MIT", "engines": { @@ -1947,15 +1947,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.43.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.43.0.tgz", - "integrity": "sha512-qaH1uLBpBuBBuRf8c1mLJ6swOfzCXryhKND04Igr4pckzSEW9JX5Aw9AgW00kwfjWJF0kk0ps9ExKTfvXfw4Qg==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.44.0.tgz", + "integrity": "sha512-9cwsoSxJ8Sak67Be/hD2RNt/fsqmWnNE1iHohG8lxqLSNY8xNfyY7wloo5zpW3Nu9hxVgURevqfcH6vvKCt6yg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.43.0", - "@typescript-eslint/typescript-estree": "8.43.0", - "@typescript-eslint/utils": "8.43.0", + "@typescript-eslint/types": "8.44.0", + "@typescript-eslint/typescript-estree": "8.44.0", + "@typescript-eslint/utils": "8.44.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -1972,9 +1972,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.43.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.43.0.tgz", - "integrity": "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.44.0.tgz", + "integrity": "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA==", "dev": true, "license": "MIT", "engines": { @@ -1986,16 +1986,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.43.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.43.0.tgz", - "integrity": "sha512-7Vv6zlAhPb+cvEpP06WXXy/ZByph9iL6BQRBDj4kmBsW98AqEeQHlj/13X+sZOrKSo9/rNKH4Ul4f6EICREFdw==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.44.0.tgz", + "integrity": "sha512-lqNj6SgnGcQZwL4/SBJ3xdPEfcBuhCG8zdcwCPgYcmiPLgokiNDKlbPzCwEwu7m279J/lBYWtDYL+87OEfn8Jw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.43.0", - "@typescript-eslint/tsconfig-utils": "8.43.0", - "@typescript-eslint/types": "8.43.0", - "@typescript-eslint/visitor-keys": "8.43.0", + "@typescript-eslint/project-service": "8.44.0", + "@typescript-eslint/tsconfig-utils": "8.44.0", + "@typescript-eslint/types": "8.44.0", + "@typescript-eslint/visitor-keys": "8.44.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2015,16 +2015,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.43.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.43.0.tgz", - "integrity": "sha512-S1/tEmkUeeswxd0GGcnwuVQPFWo8NzZTOMxCvw8BX7OMxnNae+i8Tm7REQen/SwUIPoPqfKn7EaZ+YLpiB3k9g==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.44.0.tgz", + "integrity": "sha512-nktOlVcg3ALo0mYlV+L7sWUD58KG4CMj1rb2HUVOO4aL3K/6wcD+NERqd0rrA5Vg06b42YhF6cFxeixsp9Riqg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.43.0", - "@typescript-eslint/types": "8.43.0", - "@typescript-eslint/typescript-estree": "8.43.0" + "@typescript-eslint/scope-manager": "8.44.0", + "@typescript-eslint/types": "8.44.0", + "@typescript-eslint/typescript-estree": "8.44.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2039,13 +2039,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.43.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.43.0.tgz", - "integrity": "sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.44.0.tgz", + "integrity": "sha512-zaz9u8EJ4GBmnehlrpoKvj/E3dNbuQ7q0ucyZImm3cLqJ8INTc970B1qEqDX/Rzq65r3TvVTN7kHWPBoyW7DWw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.43.0", + "@typescript-eslint/types": "8.44.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -2334,9 +2334,9 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.1.tgz", - "integrity": "sha512-Kn4kbSXpkFHCGE6rBFNwIv0GQs4AvDT80jlveJDKFxjbTYMUeB4QtsdPCv6H8Cm19Je7IU6VFtRl2zWZI0rudQ==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -2505,9 +2505,9 @@ "license": "MIT" }, "node_modules/daisyui": { - "version": "5.1.10", - "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.1.10.tgz", - "integrity": "sha512-p1J/HME2WmaSiy6u2alIbeP3gd5PNVft3+6Bdll0BRSm/UdI4084+pD01LxFug/5wGexNewWqbcEL6nB2n2o+Q==", + "version": "5.1.13", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.1.13.tgz", + "integrity": "sha512-KWPF/4R+EHTJRqKZFNmSDPfAZ5xeS6YWB/2kS7Y6wGKg+atscUi2DOp6HoDD/OgGML0PJTtTpgwpTfeHVfjk7w==", "license": "MIT", "funding": { "url": "https://github.com/saadeghi/daisyui?sponsor=1" @@ -2705,9 +2705,9 @@ } }, "node_modules/eslint": { - "version": "9.35.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.35.0.tgz", - "integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==", + "version": "9.36.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.36.0.tgz", + "integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2717,7 +2717,7 @@ "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.35.0", + "@eslint/js": "9.36.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -4402,9 +4402,9 @@ } }, "node_modules/undici-types": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", - "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", + "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", "dev": true, "license": "MIT" }, @@ -4426,9 +4426,9 @@ "license": "MIT" }, "node_modules/vite": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.5.tgz", - "integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.6.tgz", + "integrity": "sha512-SRYIB8t/isTwNn8vMB3MR6E+EQZM/WG1aKmmIUCfDXfVvKfc20ZpamngWHKzAmmu9ppsgxsg4b2I7c90JZudIQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index babd8d4..859df47 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "collapseloader", "private": true, - "version": "0.2.0", + "version": "0.2.1", "type": "module", "scripts": { "dev": "vite", @@ -15,33 +15,33 @@ "lint:fix": "eslint --ext .ts,.tsx,.vue src --fix" }, "dependencies": { - "@guolao/vue-monaco-editor": "^1.5.5", + "@guolao/vue-monaco-editor": "1.5.5", "@tauri-apps/api": "2.8.0", - "@tauri-apps/plugin-dialog": "^2.4.0", - "@tauri-apps/plugin-fs": "^2.4.2", + "@tauri-apps/plugin-dialog": "2.4.0", + "@tauri-apps/plugin-fs": "2.4.2", "@tauri-apps/plugin-notification": "2.3.1", "@tauri-apps/plugin-opener": "2.5.0", - "axios": "1.12.1", - "daisyui": "5.1.10", + "axios": "1.12.2", + "daisyui": "5.1.13", "gsap": "3.13.0", "lucide-vue-next": "0.544.0", - "monaco-editor": "^0.53.0", + "monaco-editor": "0.53.0", "vue": "3.5.21", - "vue-i18n": "^11.1.12", + "vue-i18n": "11.1.12", "vue3-lottie": "3.3.1" }, "devDependencies": { "@tailwindcss/vite": "4.1.13", "@tauri-apps/cli": "2.8.4", - "@types/node": "24.3.2", - "@typescript-eslint/eslint-plugin": "^8.43.0", - "@typescript-eslint/parser": "^8.43.0", + "@types/node": "24.5.2", + "@typescript-eslint/eslint-plugin": "8.44.0", + "@typescript-eslint/parser": "8.44.0", "@vitejs/plugin-vue": "6.0.1", - "eslint": "^9.35.0", - "eslint-plugin-vue": "^10.4.0", + "eslint": "9.36.0", + "eslint-plugin-vue": "10.4.0", "tailwindcss": "4.1.13", "typescript": "5.9.2", - "vite": "7.1.5", + "vite": "7.1.6", "vue-tsc": "3.0.7" } } diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index b25cf7e..643788b 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -76,6 +76,12 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "ascii" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" + [[package]] name = "ashpd" version = "0.11.0" @@ -557,7 +563,7 @@ dependencies = [ [[package]] name = "collapseloader" -version = "0.2.0" +version = "0.2.1" dependencies = [ "base64 0.22.1", "chrono", @@ -568,6 +574,7 @@ dependencies = [ "futures-util", "lazy_static", "md5", + "native-dialog", "once_cell", "open", "opener", @@ -584,9 +591,9 @@ dependencies = [ "tauri-plugin-fs", "tauri-plugin-notification", "tauri-plugin-opener", + "thiserror 2.0.16", "tokio", "uuid 1.18.1", - "win-msgbox", "winreg", "zip", ] @@ -1017,6 +1024,12 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "embed-resource" version = "3.0.5" @@ -1073,6 +1086,12 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "env_home" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" + [[package]] name = "equivalent" version = "1.0.2" @@ -1219,6 +1238,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "formatx" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8866fac38f53fc87fa3ae1b09ddd723e0482f8fa74323518b4c59df2c55a00a" + [[package]] name = "futf" version = "0.1.5" @@ -2066,6 +2091,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -2419,6 +2453,30 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "native-dialog" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f006431cea71a83e6668378cb5abc2d52af299cbac6dca1780c6eeca90822df" +dependencies = [ + "ascii", + "block2 0.6.1", + "dirs", + "dispatch2", + "formatx", + "objc2 0.6.2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.1", + "raw-window-handle", + "thiserror 2.0.16", + "versions", + "wfd", + "which", + "winapi", +] + [[package]] name = "native-tls" version = "0.2.14" @@ -2491,6 +2549,15 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "normpath" version = "1.4.0" @@ -2625,7 +2692,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" dependencies = [ "bitflags 2.9.4", + "block2 0.6.1", "dispatch2", + "libc", "objc2 0.6.2", ] @@ -2636,10 +2705,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" dependencies = [ "bitflags 2.9.4", + "block2 0.6.1", "dispatch2", + "libc", "objc2 0.6.2", "objc2-core-foundation", "objc2-io-surface", + "objc2-metal 0.3.1", ] [[package]] @@ -2725,6 +2797,17 @@ dependencies = [ "objc2-foundation 0.2.2", ] +[[package]] +name = "objc2-metal" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f246c183239540aab1782457b35ab2040d4259175bd1d0c58e46ada7b47a874" +dependencies = [ + "bitflags 2.9.4", + "objc2 0.6.2", + "objc2-foundation 0.3.1", +] + [[package]] name = "objc2-quartz-core" version = "0.2.2" @@ -2735,7 +2818,7 @@ dependencies = [ "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", - "objc2-metal", + "objc2-metal 0.2.2", ] [[package]] @@ -3778,19 +3861,21 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" dependencies = [ "serde", + "serde_core", ] [[package]] name = "serde" -version = "1.0.219" +version = "1.0.226" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd" dependencies = [ + "serde_core", "serde_derive", ] @@ -3805,11 +3890,20 @@ dependencies = [ "typeid", ] +[[package]] +name = "serde_core" +version = "1.0.226" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4" +dependencies = [ + "serde_derive", +] + [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.226" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33" dependencies = [ "proc-macro2", "quote", @@ -3829,14 +3923,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.143" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] @@ -5111,6 +5206,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "versions" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80a7e511ce1795821207a837b7b1c8d8aca0c648810966ad200446ae58f6667f" +dependencies = [ + "itertools", + "nom", +] + [[package]] name = "vswhom" version = "0.1.0" @@ -5416,12 +5521,25 @@ dependencies = [ ] [[package]] -name = "win-msgbox" -version = "0.2.1" +name = "wfd" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b293d082c6da3f8e9f58a9d2fb076a61737b8f752fb559550a0a864ccfa2bd8" +checksum = "e713040b67aae5bf1a0ae3e1ebba8cc29ab2b90da9aa1bff6e09031a8a41d7a8" dependencies = [ - "windows-sys 0.59.0", + "libc", + "winapi", +] + +[[package]] +name = "which" +version = "7.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762" +dependencies = [ + "either", + "env_home", + "rustix", + "winsafe", ] [[package]] @@ -5897,6 +6015,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + [[package]] name = "wit-bindgen" version = "0.45.1" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6d14037..f153b7c 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "collapseloader" -version = "0.2.0" +version = "0.2.1" description = "CollapseLoader" authors = ["dest4590"] edition = "2021" @@ -17,13 +17,13 @@ tauri-build = { version = "2.4.1", features = [] } tauri = { version = "2.8.5", features = [] } tauri-plugin-opener = "2.5.0" tauri-plugin-notification = "2.3.1" -serde = { version = "1.0.219", features = ["derive"] } -serde_json = "1.0.143" +serde = { version = "1.0.226", features = ["derive"] } +serde_json = "1.0.145" colored = "3.0.0" lazy_static = "1.5.0" rand = "0.9.2" reqwest = { version = "0.12.23", features = ["blocking", "json", "stream"] } -semver = "1.0.26" +semver = "1.0.27" zip = "5.1.1" tokio = { version = "1.47.1", features = ["macros"] } roxmltree = "0.20.0" @@ -41,10 +41,11 @@ tauri-plugin-dialog = "2.4.0" dotenvy = "0.15.7" once_cell = "1.21.3" tauri-plugin-fs = "2.4.2" +thiserror = "2.0.16" +native-dialog = "0.9.0" [target.'cfg(windows)'.dependencies] winreg = { version = "0.55.0" } -win-msgbox = { version = "0.2.1" } [profile.release.package.wry] debug = true diff --git a/src-tauri/build.rs b/src-tauri/build.rs index c3dc033..98600f7 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -22,7 +22,7 @@ fn force_println(msg: &str) { } } -fn main() -> io::Result<()> { +fn main() { const NAME: &str = "CollapseBuilder"; fn banner() { @@ -66,12 +66,8 @@ fn main() -> io::Result<()> { banner(); - let development: bool = std::env::var("DEVELOPMENT") - .map(|v| { - let lv = v.to_ascii_lowercase(); - matches!(lv.as_str(), "1" | "true" | "yes" | "y" | "on") - }) - .unwrap_or_else(|_| { + let development: bool = std::env::var("DEVELOPMENT").map_or_else( + |_| { std::fs::read_to_string("../.env") .map_err(|_| {}) .ok() @@ -90,12 +86,16 @@ fn main() -> io::Result<()> { } }) }) - .map(|v| { + .is_some_and(|v| { let lv = v.to_ascii_lowercase(); matches!(lv.as_str(), "1" | "true" | "yes" | "y" | "on") }) - .unwrap_or(false) - }); + }, + |v| { + let lv = v.to_ascii_lowercase(); + matches!(lv.as_str(), "1" | "true" | "yes" | "y" | "on") + }, + ); println!("cargo:rustc-env=DEVELOPMENT={development}"); println!("cargo:rerun-if-env-changed=DEVELOPMENT"); println!("cargo:rerun-if-changed=../.env"); @@ -128,5 +128,4 @@ fn main() -> io::Result<()> { fence(); tauri_build::build(); - Ok(()) } diff --git a/src-tauri/src/commands/analytics.rs b/src-tauri/src/commands/analytics.rs deleted file mode 100644 index ff245b1..0000000 --- a/src-tauri/src/commands/analytics.rs +++ /dev/null @@ -1,7 +0,0 @@ -use crate::core::network::analytics::Analytics; - -#[tauri::command] -pub fn send_client_analytics(client_id: u32) -> Result<(), String> { - Analytics::send_client_analytics(client_id); - Ok(()) -} diff --git a/src-tauri/src/commands/clients.rs b/src-tauri/src/commands/clients.rs index 8283791..4a447b0 100644 --- a/src-tauri/src/commands/clients.rs +++ b/src-tauri/src/commands/clients.rs @@ -7,7 +7,10 @@ use tauri::AppHandle; use crate::core::{ clients::custom_clients::{CustomClient, Version}, network::analytics::Analytics, - storage::custom_clients::{CustomClientUpdate, CUSTOM_CLIENT_MANAGER}, + storage::{ + custom_clients::{CustomClientUpdate, CUSTOM_CLIENT_MANAGER}, + data::Data, + }, }; use crate::core::{ clients::{client::LaunchOptions, internal::agent_overlay::AgentOverlayManager}, @@ -48,6 +51,7 @@ fn get_client_by_id(id: u32) -> Result { #[tauri::command] pub fn get_app_logs() -> Vec { + log_debug!("Retrieving application logs"); logging::APP_LOGS .lock() .map(|logs| logs.clone()) @@ -57,11 +61,13 @@ pub fn get_app_logs() -> Vec { #[tauri::command] pub async fn initialize_api() -> Result<(), String> { + log_info!("Initializing client manager via API"); initialize_client_manager().await } #[tauri::command] pub fn initialize_rpc() -> Result<(), String> { + log_info!("Initializing Discord RPC"); if let Err(e) = discord_rpc::initialize() { log_error!("Failed to initialize Discord RPC: {}", e); } @@ -70,6 +76,7 @@ pub fn initialize_rpc() -> Result<(), String> { #[tauri::command] pub fn get_server_connectivity_status() -> ServerConnectivityStatus { + log_debug!("Fetching server connectivity status"); let servers = &SERVERS; servers.connectivity_status.lock().unwrap().clone() } @@ -89,7 +96,9 @@ pub async fn launch_client( user_token: String, app_handle: AppHandle, ) -> Result<(), String> { + log_info!("Attempting to launch client with ID: {}", id); let client = get_client_by_id(id)?; + log_debug!("Found client '{}' for launch", client.name); let filename_for_if = if client.filename.contains("fabric/") { client.filename.replace("fabric/", "") @@ -99,23 +108,32 @@ pub async fn launch_client( client.filename.clone() }; - let file_name = DATA.get_filename(&client.filename); + let file_name = Data::get_filename(&client.filename); let jar_path = match client.client_type { ClientType::Default => { DATA.get_local(&format!("{file_name}{MAIN_SEPARATOR}{}", client.filename)) } ClientType::Fabric => DATA.get_local(&format!( - "{file_name}{MAIN_SEPARATOR}mods{MAIN_SEPARATOR}{}", - filename_for_if + "{file_name}{MAIN_SEPARATOR}mods{MAIN_SEPARATOR}{filename_for_if}" )), }; if !jar_path.exists() { + log_warn!( + "Launch failed: Client '{}' is not installed at path: {}", + client.name, + jar_path.display() + ); return Err(format!( "Client {} is not installed. Please download it first.", client.name )); } + log_debug!( + "Client '{}' found at path: {}", + client.name, + jar_path.display() + ); let hash_verify_enabled = { let settings = SETTINGS @@ -125,6 +143,7 @@ pub async fn launch_client( }; if hash_verify_enabled { + log_info!("Hash verification is enabled for client '{}'", client.name); emit_to_main_window( &app_handle, "client-hash-verification-start", @@ -138,8 +157,22 @@ pub async fn launch_client( "Verifying MD5 hash for client {} before launch", client.name ); - let current_hash = calculate_md5_hash(&jar_path)?; - if current_hash != client.md5_hash { + let current_hash = Data::calculate_md5_hash(&jar_path)?; + if current_hash == client.md5_hash { + log_info!( + "MD5 hash verification successful for client {}", + client.name + ); + + emit_to_main_window( + &app_handle, + "client-hash-verification-done", + &serde_json::json!({ + "id": id, + "name": client.name + }), + ); + } else { log_warn!( "Hash mismatch for client {}. Expected: {}, Got: {}. Redownloading...", client.name, @@ -189,20 +222,6 @@ pub async fn launch_client( "Client {} redownloaded and verified successfully", client.name ); - } else { - log_info!( - "MD5 hash verification successful for client {}", - client.name - ); - - emit_to_main_window( - &app_handle, - "client-hash-verification-done", - &serde_json::json!({ - "id": id, - "name": client.name - }), - ); } } else { log_debug!( @@ -211,6 +230,7 @@ pub async fn launch_client( ); } + log_info!("Verifying agent and overlay files before launch"); match AgentOverlayManager::verify_agent_overlay_files().await { Ok(true) => { log_debug!("Agent and overlay files verified successfully"); @@ -228,6 +248,7 @@ pub async fn launch_client( let options = LaunchOptions::new(app_handle.clone(), user_token.clone(), false); + log_info!("Executing client run for '{}'", client.name); client.run(options).await } @@ -240,12 +261,17 @@ pub async fn get_running_client_ids() -> Vec { .collect() }); - handle.await.unwrap_or_else(|_| Vec::new()) + handle.await.unwrap_or_else(|e| { + log_error!("Failed to get running client IDs: {}", e); + Vec::new() + }) } #[tauri::command] pub async fn stop_client(id: u32) -> Result<(), String> { + log_info!("Attempting to stop client with ID: {}", id); let client = get_client_by_id(id)?; + log_debug!("Found client '{}' to stop", client.name); let client_clone = client.clone(); let handle = tokio::task::spawn_blocking(move || client_clone.stop()); @@ -257,6 +283,7 @@ pub async fn stop_client(id: u32) -> Result<(), String> { #[tauri::command] pub fn get_client_logs(id: u32) -> Vec { + log_debug!("Fetching logs for client ID: {}", id); CLIENT_LOGS .lock() .ok() @@ -268,7 +295,7 @@ pub fn get_client_logs(id: u32) -> Vec { pub async fn download_client_only(id: u32, app_handle: AppHandle) -> Result<(), String> { let client = get_client_by_id(id)?; - log_info!("Downloading client: {} (ID: {})", client.name, id); + log_info!("Starting download for client: {} (ID: {})", client.name, id); let client_download = async { client.download().await.map_err(|e| { @@ -287,19 +314,32 @@ pub async fn download_client_only(id: u32, app_handle: AppHandle) -> Result<(), let requirements_download = client.download_requirements(&app_handle); Analytics::send_client_download_analytics(id); + log_debug!("Sent client download analytics for ID: {}", id); tokio::try_join!(client_download, requirements_download)?; + log_info!( + "Successfully downloaded client and requirements for '{}'", + client.name + ); + Ok(()) } #[tauri::command] pub async fn reinstall_client(id: u32, app_handle: AppHandle) -> Result<(), String> { + log_info!("Starting reinstall for client ID: {}", id); let client = get_client_by_id(id)?; + log_debug!("Found client '{}' for reinstall", client.name); let client_clone = client.clone(); let handle = tokio::task::spawn_blocking(move || -> Result<(), String> { + log_info!("Removing existing installation for '{}'", client_clone.name); client_clone.remove_installation()?; + log_info!( + "Successfully removed existing installation for '{}'", + client_clone.name + ); Ok(()) }); @@ -308,6 +348,10 @@ pub async fn reinstall_client(id: u32, app_handle: AppHandle) -> Result<(), Stri .map_err(|e| format!("Reinstall task error: {e}"))??; update_client_installed_status(id, false)?; + log_debug!( + "Updated installed status to false for client '{}'", + client.name + ); let download_result = client.download().await.map_err(|e| { if e.contains("Hash verification failed") { @@ -316,13 +360,28 @@ pub async fn reinstall_client(id: u32, app_handle: AppHandle) -> Result<(), Stri client.name ) } else { + log_error!("Client download failed during reinstall: {}", e); e } }); - download_result.as_ref()?; + if let Err(e) = download_result.as_ref() { + log_error!( + "Aborting reinstall for '{}' due to download failure: {}", + client.name, + e + ); + return Err(e.clone()); + } + log_info!("Client '{}' downloaded successfully", client.name); let result = client.download_requirements(&app_handle).await; + if result.is_ok() { + log_info!( + "Client requirements for '{}' downloaded successfully", + client.name + ); + } if result.is_ok() { let _ = AgentOverlayManager::download_agent_overlay_files() @@ -341,11 +400,18 @@ pub async fn reinstall_client(id: u32, app_handle: AppHandle) -> Result<(), Stri #[tauri::command] pub fn open_client_folder(id: u32) -> Result<(), String> { + log_info!("Attempting to open folder for client ID: {}", id); let client = get_client_by_id(id)?; + log_debug!("Found client '{}' to open folder", client.name); let client_dir_relative = DATA.get_as_folder(&client.filename); if !client_dir_relative.exists() { + log_warn!( + "Cannot open folder for client '{}', it does not exist at path: {}", + client.name, + client_dir_relative.display() + ); return Err("Client folder does not exist".to_string()); } @@ -353,7 +419,16 @@ pub fn open_client_folder(id: u32) -> Result<(), String> { .canonicalize() .map_err(|e| format!("Failed to get absolute path: {e}"))?; + log_debug!( + "Opening client folder at: {}", + client_dir_absolute.display() + ); opener::open(&client_dir_absolute).map_err(|e| { + log_error!( + "Failed to open client folder at {}: {}", + client_dir_absolute.display(), + e + ); format!( "Failed to open client folder: {} at path {}", e, @@ -364,6 +439,7 @@ pub fn open_client_folder(id: u32) -> Result<(), String> { #[tauri::command] pub fn get_latest_client_logs(id: u32) -> Result { + log_debug!("Fetching latest logs for client ID: {}", id); CLIENT_LOGS .lock() .map_err(|_| "Failed to acquire lock on client logs".to_string())? @@ -374,113 +450,130 @@ pub fn get_latest_client_logs(id: u32) -> Result { #[tauri::command] pub fn update_client_installed_status(id: u32, installed: bool) -> Result<(), String> { - let mut manager = CLIENT_MANAGER + log_debug!( + "Updating installed status for client ID {} to {}", + id, + installed + ); + if let Some(client) = CLIENT_MANAGER .lock() - .map_err(|_| "Failed to acquire lock on client manager".to_string())?; - - let manager = manager + .map_err(|_| "Failed to acquire lock on client manager".to_string())? .as_mut() - .ok_or_else(|| "Client manager not initialized".to_string())?; - - let client = manager + .ok_or_else(|| "Client manager not initialized".to_string())? .clients .iter_mut() .find(|c| c.id == id) - .ok_or_else(|| "Client not found".to_string())?; - - client.meta.installed = installed; - Ok(()) + { + client.meta.installed = installed; + log_info!( + "Set installed status of client '{}' to {}", + client.name, + installed + ); + Ok(()) + } else { + log_warn!( + "Could not update installed status: Client ID {} not found", + id + ); + Err("Client not found".to_string()) + } } #[tauri::command] pub async fn delete_client(id: u32) -> Result<(), String> { + log_info!("Attempting to delete client with ID: {}", id); let client = get_client_by_id(id)?; + log_debug!("Found client '{}' for deletion", client.name); let handle = tokio::task::spawn_blocking(move || client.remove_installation()); match handle.await { Ok(result) => { if result.is_ok() { + log_info!("Successfully deleted files for client ID: {}", id); update_client_installed_status(id, false)?; + } else { + log_error!("Failed to delete files for client ID {}: {:?}", id, result); } result } - Err(e) => Err(format!("Delete task error: {e}")), + Err(e) => { + log_error!("Task to delete client ID {} failed: {}", id, e); + Err(format!("Delete task error: {e}")) + } } } #[tauri::command] pub async fn get_client_details(client_id: u32) -> Result { + log_debug!("Fetching details for client ID: {}", client_id); let api_url = get_auth_url().await?; let url = format!("{api_url}api/client/{client_id}/detailed"); + log_debug!("Requesting client details from URL: {}", url); let client = reqwest::Client::new(); - let response = client - .get(&url) - .send() - .await - .map_err(|e| format!("Failed to fetch client details: {e}"))?; + let response = client.get(&url).send().await.map_err(|e| { + log_error!("Failed to fetch client details from {}: {}", url, e); + format!("Failed to fetch client details: {e}") + })?; if !response.status().is_success() { + log_warn!( + "API returned non-success status ({}) for client details request to {}", + response.status(), + url + ); return Err(format!("API returned error: {}", response.status())); } - let details: serde_json::Value = response - .json() - .await - .map_err(|e| format!("Failed to parse client details: {e}"))?; + let details: serde_json::Value = response.json().await.map_err(|e| { + log_error!("Failed to parse client details JSON: {}", e); + format!("Failed to parse client details: {e}") + })?; + log_info!("Successfully fetched details for client ID: {}", client_id); Ok(details) } #[tauri::command] pub fn increment_client_counter(id: u32, counter_type: String) -> Result<(), String> { - let mut manager = CLIENT_MANAGER + if let Some(client) = CLIENT_MANAGER .lock() - .map_err(|_| "Failed to acquire lock on client manager".to_string())?; - - let manager = manager + .map_err(|_| "Failed to acquire lock on client manager".to_string())? .as_mut() - .ok_or_else(|| "Client manager not initialized".to_string())?; - - let client = manager + .ok_or_else(|| "Client manager not initialized".to_string())? .clients .iter_mut() .find(|c| c.id == id) - .ok_or_else(|| "Client not found".to_string())?; - - match counter_type.as_str() { - "download" => { - client.downloads += 1; - log_info!( - "Incremented download counter for client {} (ID: {}). New count: {}", - client.name, - id, - client.downloads - ); - } - "launch" => { - client.launches += 1; - log_info!( - "Incremented launch counter for client {} (ID: {}). New count: {}", - client.name, - id, - client.launches - ); - } - _ => { - return Err(format!("Invalid counter type: {counter_type}")); + { + match counter_type.as_str() { + "download" => { + client.downloads += 1; + log_info!( + "Incremented download counter for client {} (ID: {}). New count: {}", + client.name, + id, + client.downloads + ); + } + "launch" => { + client.launches += 1; + log_info!( + "Incremented launch counter for client {} (ID: {}). New count: {}", + client.name, + id, + client.launches + ); + } + _ => { + return Err(format!("Invalid counter type: {counter_type}")); + } } + Ok(()) + } else { + Err("Client not found".to_string()) } - - Ok(()) -} - -fn calculate_md5_hash(path: &std::path::PathBuf) -> Result { - let bytes = std::fs::read(path).map_err(|e| format!("Failed to read file for hashing: {e}"))?; - - let digest = md5::compute(&bytes); - Ok(format!("{digest:x}")) } #[tauri::command] @@ -500,6 +593,7 @@ pub fn add_custom_client( file_path: String, main_class: String, ) -> Result<(), String> { + log_info!("Adding new custom client: '{}'", name); let mut manager = CUSTOM_CLIENT_MANAGER .lock() .map_err(|_| "Failed to acquire lock on custom client manager".to_string())?; @@ -509,11 +603,13 @@ pub fn add_custom_client( let custom_client = CustomClient::new(0, name, version_enum, filename, path_buf, main_class); + log_debug!("New custom client details: {:?}", custom_client); manager.add_client(custom_client) } #[tauri::command] pub fn remove_custom_client(id: u32) -> Result<(), String> { + log_info!("Removing custom client with ID: {}", id); let mut manager = CUSTOM_CLIENT_MANAGER .lock() .map_err(|_| "Failed to acquire lock on custom client manager".to_string())?; @@ -528,6 +624,7 @@ pub fn update_custom_client( version: Option, main_class: Option, ) -> Result<(), String> { + log_info!("Updating custom client with ID: {}", id); let mut manager = CUSTOM_CLIENT_MANAGER .lock() .map_err(|_| "Failed to acquire lock on custom client manager".to_string())?; @@ -540,6 +637,7 @@ pub fn update_custom_client( main_class, }; + log_debug!("Applying updates to custom client ID {}: {:?}", id, updates); manager.update_client(id, updates) } @@ -549,6 +647,7 @@ pub async fn launch_custom_client( user_token: String, app_handle: AppHandle, ) -> Result<(), String> { + log_info!("Attempting to launch custom client with ID: {}", id); let custom_client = { let mut manager = CUSTOM_CLIENT_MANAGER .lock() @@ -559,6 +658,11 @@ pub async fn launch_custom_client( .ok_or_else(|| "Custom client not found".to_string())?; client.launches += 1; + log_debug!( + "Incremented launch count for custom client '{}' to {}", + client.name, + client.launches + ); let client_clone = client.clone(); manager.save_to_disk(); @@ -566,6 +670,7 @@ pub async fn launch_custom_client( }; custom_client.validate_file()?; + log_debug!("Custom client file validated for '{}'", custom_client.name); log_info!("Launching custom client: {}", custom_client.name); @@ -593,11 +698,15 @@ pub async fn get_running_custom_client_ids() -> Vec { .collect() }); - handle.await.unwrap_or_else(|_| Vec::new()) + handle.await.unwrap_or_else(|e| { + log_error!("Failed to get running custom client IDs: {}", e); + Vec::new() + }) } #[tauri::command] pub async fn stop_custom_client(id: u32) -> Result<(), String> { + log_info!("Attempting to stop custom client with ID: {}", id); let custom_client = { let manager = CUSTOM_CLIENT_MANAGER .lock() @@ -608,6 +717,7 @@ pub async fn stop_custom_client(id: u32) -> Result<(), String> { .cloned() .ok_or_else(|| "Custom client not found".to_string())? }; + log_debug!("Found custom client '{}' to stop", custom_client.name); let client_clone = custom_client.clone(); let handle = tokio::task::spawn_blocking(move || client_clone.stop()); diff --git a/src-tauri/src/commands/discord_rpc.rs b/src-tauri/src/commands/discord_rpc.rs index 16f7491..3925f00 100644 --- a/src-tauri/src/commands/discord_rpc.rs +++ b/src-tauri/src/commands/discord_rpc.rs @@ -1,7 +1,12 @@ -use crate::core::utils::discord_rpc; +use crate::{core::utils::discord_rpc, log_debug}; #[tauri::command] pub fn update_presence(details: String, state: String) -> Result<(), String> { + log_debug!( + "Updating Discord presence: details='{}', state='{}'", + details, + state + ); discord_rpc::update_activity_async(details, state); Ok(()) } diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 31839a1..e257577 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -1,4 +1,3 @@ -pub mod analytics; pub mod clients; pub mod discord_rpc; pub mod presets; diff --git a/src-tauri/src/commands/presets.rs b/src-tauri/src/commands/presets.rs index f12961e..e6c6923 100644 --- a/src-tauri/src/commands/presets.rs +++ b/src-tauri/src/commands/presets.rs @@ -1,4 +1,5 @@ use crate::core::storage::presets::{ThemePreset, PRESET_MANAGER}; +use crate::{log_debug, log_info, log_warn}; use chrono::Utc; use uuid::Uuid; @@ -67,23 +68,26 @@ pub struct UpdatePresetInput { #[tauri::command] pub fn get_all_presets() -> Result, String> { - let preset_manager = PRESET_MANAGER.lock().unwrap(); - let presets = preset_manager - .get_all_presets() - .into_iter() - .cloned() - .collect(); - Ok(presets) + log_debug!("Fetching all theme presets"); + PRESET_MANAGER + .lock() + .map(|p| p.get_all_presets()) + .map_err(|e| { + log_warn!("Failed to get presets: {}", e); + "Failed to get presets".to_string() + }) } #[tauri::command] pub fn get_preset(id: String) -> Result, String> { + log_debug!("Fetching theme preset with ID: {}", id); let preset_manager = PRESET_MANAGER.lock().unwrap(); Ok(preset_manager.get_preset(&id).cloned()) } #[tauri::command] pub fn create_preset(input: CreatePresetInput) -> Result { + log_info!("Creating new theme preset with name: '{}'", input.name); let mut preset_manager = PRESET_MANAGER.lock().unwrap(); let preset = ThemePreset { @@ -118,14 +122,21 @@ pub fn create_preset(input: CreatePresetInput) -> Result { }; preset_manager.add_preset(preset.clone())?; + log_info!( + "Successfully created and saved new preset with ID: {}", + preset.id + ); + drop(preset_manager); Ok(preset) } #[tauri::command] pub fn update_preset(input: UpdatePresetInput) -> Result { + log_info!("Updating theme preset with ID: {}", input.id); let mut preset_manager = PRESET_MANAGER.lock().unwrap(); if !preset_manager.preset_exists(&input.id) { + log_warn!("Update failed: Preset with ID '{}' not found", input.id); return Err(format!("Preset with ID '{}' not found", input.id)); } @@ -164,22 +175,27 @@ pub fn update_preset(input: UpdatePresetInput) -> Result { }; preset_manager.update_preset(preset.clone())?; + log_info!("Successfully updated preset with ID: {}", preset.id); + drop(preset_manager); Ok(preset) } #[tauri::command] pub fn delete_preset(id: String) -> Result<(), String> { + log_info!("Deleting theme preset with ID: {}", id); let mut preset_manager = PRESET_MANAGER.lock().unwrap(); preset_manager.delete_preset(&id) } #[tauri::command] pub fn duplicate_preset(id: String, new_name: String) -> Result { + log_info!("Duplicating theme preset with ID: {} as '{}'", id, new_name); let mut preset_manager = PRESET_MANAGER.lock().unwrap(); - let existing_preset = preset_manager - .get_preset(&id) - .ok_or_else(|| format!("Preset with ID '{id}' not found"))?; + let existing_preset = preset_manager.get_preset(&id).ok_or_else(|| { + log_warn!("Duplication failed: Preset with ID '{}' not found", id); + format!("Preset with ID '{id}' not found") + })?; let new_preset = ThemePreset { id: Uuid::new_v4().to_string(), @@ -213,5 +229,10 @@ pub fn duplicate_preset(id: String, new_name: String) -> Result Settings { @@ -17,14 +18,17 @@ pub fn get_flags() -> Flags { #[tauri::command] pub fn reset_flags() -> Result<(), String> { + log_info!("Resetting application flags to default"); let mut flags = FLAGS_MANAGER.lock().unwrap(); *flags = Flags::default(); flags.save_to_disk(); + drop(flags); Ok(()) } #[tauri::command] pub fn save_settings(input_settings: InputSettings) -> Result<(), String> { + log_info!("Saving application settings"); let mut current_settings = SETTINGS.lock().unwrap(); let config_path = current_settings.config_path.clone(); @@ -32,16 +36,22 @@ pub fn save_settings(input_settings: InputSettings) -> Result<(), String> { current_settings.discord_rpc_enabled.value != input_settings.discord_rpc_enabled.value; let new_discord_rpc_value = input_settings.discord_rpc_enabled.value; + log_debug!("Applying new settings"); let new_settings = Settings::from_input(input_settings, config_path); *current_settings = new_settings.clone(); new_settings.save_to_disk(); + log_info!("Settings saved to disk"); drop(current_settings); if discord_rpc_changed { + log_info!( + "Discord RPC setting changed. Toggling RPC to: {}", + new_discord_rpc_value + ); if let Err(e) = discord_rpc::toggle_rpc(new_discord_rpc_value) { - eprintln!("Failed to toggle Discord RPC: {e}"); + log_error!("Failed to toggle Discord RPC: {e}"); } } @@ -50,85 +60,117 @@ pub fn save_settings(input_settings: InputSettings) -> Result<(), String> { #[tauri::command] pub fn reset_settings() -> Result<(), String> { + log_info!("Resetting application settings to default"); let mut current_settings = SETTINGS.lock().unwrap(); *current_settings = Settings::default(); current_settings.config_path = Settings::default().config_path; current_settings.save_to_disk(); + log_info!("Default settings saved to disk"); + drop(current_settings); Ok(()) } #[tauri::command] pub fn mark_disclaimer_shown() -> Result<(), String> { + log_info!("Marking disclaimer as shown"); let mut flags = FLAGS_MANAGER.lock().unwrap(); flags.disclaimer_shown.value = true; flags.save_to_disk(); + drop(flags); Ok(()) } #[tauri::command] pub fn mark_first_run_shown() -> Result<(), String> { + log_info!("Marking first run as shown"); let mut flags = FLAGS_MANAGER.lock().unwrap(); flags.first_run.value = false; flags.save_to_disk(); + drop(flags); Ok(()) } #[tauri::command] pub fn set_optional_telemetry(enabled: bool) -> Result<(), String> { + log_info!("Setting optional telemetry to: {}", enabled); let mut settings = SETTINGS.lock().unwrap(); settings.optional_telemetry.value = enabled; settings.save_to_disk(); + drop(settings); Ok(()) } #[tauri::command] pub fn get_accounts() -> Vec { - if let Ok(account_manager) = ACCOUNT_MANAGER.lock() { - account_manager.accounts.clone() - } else { - Vec::new() - } + ACCOUNT_MANAGER.lock().map_or_else( + |e| { + log_error!("Failed to acquire lock on account manager: {}", e); + Vec::new() + }, + |account_manager| account_manager.accounts.clone(), + ) } #[tauri::command] pub fn add_account(username: String, tags: Vec) -> Result { - if let Ok(mut account_manager) = ACCOUNT_MANAGER.lock() { - let id = account_manager.add_account(username, tags); - account_manager.save_to_disk(); - Ok(id) - } else { - Err("Failed to acquire lock on account manager".to_string()) - } + log_info!("Adding new account for user: '{}'", username); + ACCOUNT_MANAGER.lock().map_or_else( + |e| { + log_error!("Failed to acquire lock on account manager: {}", e); + Err("Failed to acquire lock on account manager".to_string()) + }, + |mut account_manager| { + let id = account_manager.add_account(username.clone(), tags); + log_debug!("New account created with ID: {}", id); + account_manager.save_to_disk(); + log_info!("Account for '{}' saved to disk", username); + Ok(id) + }, + ) } #[tauri::command] pub fn remove_account(id: String) -> Result<(), String> { - if let Ok(mut account_manager) = ACCOUNT_MANAGER.lock() { - if account_manager.remove_account(&id) { - account_manager.save_to_disk(); - Ok(()) - } else { - Err("Account not found".to_string()) - } - } else { - Err("Failed to acquire lock on account manager".to_string()) - } + log_info!("Removing account with ID: {}", id); + ACCOUNT_MANAGER.lock().map_or_else( + |e| { + log_error!("Failed to acquire lock on account manager: {}", e); + Err("Failed to acquire lock on account manager".to_string()) + }, + |mut account_manager| { + if account_manager.remove_account(&id) { + account_manager.save_to_disk(); + log_info!("Account ID {} removed and saved to disk", id); + Ok(()) + } else { + log_error!("Account with ID {} not found for removal", id); + Err("Account not found".to_string()) + } + }, + ) } #[tauri::command] pub fn set_active_account(id: String) -> Result<(), String> { - if let Ok(mut account_manager) = ACCOUNT_MANAGER.lock() { - if account_manager.set_active_account(&id) { - account_manager.save_to_disk(); - Ok(()) - } else { - Err("Account not found".to_string()) - } - } else { - Err("Failed to acquire lock on account manager".to_string()) - } + log_info!("Setting active account to ID: {}", id); + ACCOUNT_MANAGER.lock().map_or_else( + |e| { + log_error!("Failed to acquire lock on account manager: {}", e); + Err("Failed to acquire lock on account manager".to_string()) + }, + |mut account_manager| { + if account_manager.set_active_account(&id) { + account_manager.save_to_disk(); + log_info!("Active account set to {} and saved to disk", id); + Ok(()) + } else { + log_error!("Account with ID {} not found to set as active", id); + Err("Account not found".to_string()) + } + }, + ) } #[tauri::command] @@ -137,85 +179,117 @@ pub fn update_account( username: Option, tags: Option>, ) -> Result<(), String> { - if let Ok(mut account_manager) = ACCOUNT_MANAGER.lock() { - if account_manager.update_account(&id, username, tags) { - account_manager.save_to_disk(); - Ok(()) - } else { - Err("Account not found".to_string()) - } - } else { - Err("Failed to acquire lock on account manager".to_string()) - } + log_info!("Updating account with ID: {}", id); + ACCOUNT_MANAGER.lock().map_or_else( + |e| { + log_error!("Failed to acquire lock on account manager: {}", e); + Err("Failed to acquire lock on account manager".to_string()) + }, + |mut account_manager| { + if account_manager.update_account(&id, username, tags) { + account_manager.save_to_disk(); + log_info!("Account ID {} updated and saved to disk", id); + Ok(()) + } else { + log_error!("Account with ID {} not found for update", id); + Err("Account not found".to_string()) + } + }, + ) } #[tauri::command] pub fn get_active_account() -> Option { - if let Ok(account_manager) = ACCOUNT_MANAGER.lock() { - account_manager.get_active_account().cloned() - } else { - None - } + log_debug!("Fetching active account"); + ACCOUNT_MANAGER.lock().map_or_else( + |e| { + log_error!("Failed to acquire lock on account manager: {}", e); + None + }, + |account_manager| account_manager.get_active_account().cloned(), + ) } #[tauri::command] pub fn get_favorite_clients() -> Result, String> { - if let Ok(favorite_manager) = FAVORITE_MANAGER.lock() { - Ok(favorite_manager.favorites.clone()) - } else { - Err("Failed to acquire lock on favorite manager".to_string()) - } + FAVORITE_MANAGER.lock().map_or_else( + |e| { + log_error!("Failed to acquire lock on favorite manager: {}", e); + Err("Failed to acquire lock on favorite manager".to_string()) + }, + |favorite_manager| Ok(favorite_manager.favorites.clone()), + ) } #[tauri::command] pub fn add_favorite_client(client_id: u32) -> Result<(), String> { - if let Ok(mut favorite_manager) = FAVORITE_MANAGER.lock() { - favorite_manager.add_favorite(client_id); - favorite_manager.save_to_disk(); - Ok(()) - } else { - Err("Failed to acquire lock on favorite manager".to_string()) - } + log_info!("Adding client ID {} to favorites", client_id); + FAVORITE_MANAGER.lock().map_or_else( + |e| { + log_error!("Failed to acquire lock on favorite manager: {}", e); + Err("Failed to acquire lock on favorite manager".to_string()) + }, + |mut favorite_manager| { + favorite_manager.add_favorite(client_id); + favorite_manager.save_to_disk(); + log_info!("Client ID {} added to favorites and saved", client_id); + Ok(()) + }, + ) } #[tauri::command] pub fn remove_favorite_client(client_id: u32) -> Result<(), String> { - if let Ok(mut favorite_manager) = FAVORITE_MANAGER.lock() { - favorite_manager.remove_favorite(client_id); - favorite_manager.save_to_disk(); - Ok(()) - } else { - Err("Failed to acquire lock on favorite manager".to_string()) - } + log_info!("Removing client ID {} from favorites", client_id); + FAVORITE_MANAGER.lock().map_or_else( + |e| { + log_error!("Failed to acquire lock on favorite manager: {}", e); + Err("Failed to acquire lock on favorite manager".to_string()) + }, + |mut favorite_manager| { + favorite_manager.remove_favorite(client_id); + favorite_manager.save_to_disk(); + log_info!("Client ID {} removed from favorites and saved", client_id); + Ok(()) + }, + ) } #[tauri::command] pub fn is_client_favorite(client_id: u32) -> Result { - if let Ok(favorite_manager) = FAVORITE_MANAGER.lock() { - Ok(favorite_manager.is_favorite(client_id)) - } else { - Err("Failed to acquire lock on favorite manager".to_string()) - } + log_debug!("Checking if client ID {} is a favorite", client_id); + FAVORITE_MANAGER.lock().map_or_else( + |e| { + log_error!("Failed to acquire lock on favorite manager: {}", e); + Err("Failed to acquire lock on favorite manager".to_string()) + }, + |favorite_manager| Ok(favorite_manager.is_favorite(client_id)), + ) } #[tauri::command] pub fn mark_telemetry_consent_shown() -> Result<(), String> { + log_info!("Marking telemetry consent as shown"); let mut flags = FLAGS_MANAGER.lock().unwrap(); flags.telemetry_consent_shown.value = true; flags.save_to_disk(); + drop(flags); Ok(()) } #[tauri::command] pub fn is_telemetry_consent_shown() -> Result { + log_debug!("Checking if telemetry consent has been shown"); let flags = FLAGS_MANAGER.lock().unwrap(); Ok(flags.telemetry_consent_shown.value) } #[tauri::command] pub fn set_custom_clients_display(display: String) -> Result<(), String> { + log_info!("Setting custom clients display to: {}", display); let mut flags = FLAGS_MANAGER.lock().unwrap(); flags.set_custom_clients_display(display); flags.save_to_disk(); + drop(flags); Ok(()) } diff --git a/src-tauri/src/commands/updater.rs b/src-tauri/src/commands/updater.rs index ff75970..556019c 100644 --- a/src-tauri/src/commands/updater.rs +++ b/src-tauri/src/commands/updater.rs @@ -1,6 +1,9 @@ use crate::{ - core::utils::globals::{GITHUB_REPO_NAME, GITHUB_REPO_OWNER}, - log_debug, log_warn, + core::{ + storage::data::Data, + utils::globals::{GITHUB_REPO_NAME, GITHUB_REPO_OWNER}, + }, + log_debug, log_error, log_info, log_warn, }; use serde::{Deserialize, Serialize}; use serde_json::Value as JsonValue; @@ -98,15 +101,18 @@ fn compare_versions(v1: &str, v2: &str) -> Result { #[tauri::command] pub async fn check_for_updates() -> Result { let current_version = env!("CARGO_PKG_VERSION"); + log_info!("Checking for updates. Current version: {}", current_version); let client = reqwest::Client::new(); let url = if std::env::var("LOCAL_UPDATER_URL").unwrap_or_default() == "true" { + log_debug!("Using local updater URL for development"); "http://127.0.0.1:8000/repos/dest4590/CollapseLoader/releases/latest".to_string() } else { format!( "https://api.github.com/repos/{GITHUB_REPO_OWNER}/{GITHUB_REPO_NAME}/releases/latest" ) }; + log_debug!("Fetching latest release from: {}", url); let response = client .get(&url) @@ -115,18 +121,31 @@ pub async fn check_for_updates() -> Result { .header("X-GitHub-Api-Version", "2022-11-28") .send() .await - .map_err(|e| format!("Failed to fetch releases: {e}"))?; + .map_err(|e| { + log_warn!("Failed to fetch releases from GitHub API: {}", e); + format!("Failed to fetch releases: {e}") + })?; if !response.status().is_success() { + log_warn!( + "GitHub API returned non-success status: {}", + response.status() + ); return Err(format!("GitHub API error: {}", response.status())); } - let release: GitHubRelease = response - .json() - .await - .map_err(|e| format!("Failed to parse release data: {e}"))?; + let release: GitHubRelease = response.json().await.map_err(|e| { + log_warn!("Failed to parse release data from GitHub API: {}", e); + format!("Failed to parse release data: {e}") + })?; + + log_debug!( + "Successfully fetched and parsed latest release: '{}'", + release.name + ); if release.prerelease { + log_info!("Latest release is a pre-release, skipping update check."); return Ok(UpdateInfo { available: false, current_version: current_version.to_string(), @@ -141,6 +160,7 @@ pub async fn check_for_updates() -> Result { } let latest_version = release.tag_name.trim_start_matches('v'); + log_info!("Latest stable version found: {}", latest_version); let is_newer = match compare_versions(current_version, latest_version) { Ok(Ordering::Less) => true, @@ -151,16 +171,22 @@ pub async fn check_for_updates() -> Result { } }; + if is_newer { + log_info!("A new version is available: {}", latest_version); + } else { + log_info!("Application is up to date."); + } + let download_url = if cfg!(target_os = "windows") { release .assets .iter() - .find(|asset| asset.name.ends_with(".msi")) + .find(|asset| Data::has_extension(&asset.name, ".msi")) .or_else(|| { release .assets .iter() - .find(|asset| asset.name.ends_with(".exe")) + .find(|asset| Data::has_extension(&asset.name, ".exe")) }) .map(|asset| asset.browser_download_url.clone()) .unwrap_or_else(|| { @@ -168,19 +194,34 @@ pub async fn check_for_updates() -> Result { String::new() }) } else { + log_warn!("Auto-update not supported on this platform, no download URL will be provided."); String::new() }; if download_url.is_empty() { + log_warn!("No suitable installer found for the current platform."); return Err(format!( "No suitable installer found. Please download manually from {}", release.html_url )); } + log_debug!("Found suitable installer URL: {}", download_url); let (parsed_changelog, parsed_translations) = extract_changelog_json_block(&release.body) - .and_then(|content| parse_changelog_and_translations(&content).ok()) - .unwrap_or_default(); + .and_then(|content| { + log_debug!("Found changelog JSON block in release notes"); + parse_changelog_and_translations(&content).ok() + }) + .unwrap_or_else(|| { + log_warn!("No valid changelog JSON block found in release notes"); + Default::default() + }); + + let is_critical = release.body.to_lowercase().contains("security") + || release.body.to_lowercase().contains("critical"); + if is_critical { + log_warn!("Update is marked as critical."); + } Ok(UpdateInfo { available: is_newer, @@ -191,14 +232,15 @@ pub async fn check_for_updates() -> Result { changelog: parsed_changelog, translations: parsed_translations, release_date: release.published_at, - is_critical: release.body.to_lowercase().contains("security") - || release.body.to_lowercase().contains("critical"), + is_critical, }) } #[tauri::command] pub async fn download_and_install_update(download_url: String) -> Result<(), String> { + log_info!("Starting update download and installation process."); if download_url.is_empty() { + log_warn!("Update process aborted: No download URL provided."); return Err("No download URL provided".to_string()); } @@ -207,41 +249,45 @@ pub async fn download_and_install_update(download_url: String) -> Result<(), Str .build() .map_err(|e| format!("Failed to build HTTP client: {e}"))?; - log_debug!("Downloading from: {}", download_url); + log_debug!("Downloading update from: {}", download_url); - let response = client - .get(&download_url) - .send() - .await - .map_err(|e| format!("Failed to download update: {e}"))?; + let response = client.get(&download_url).send().await.map_err(|e| { + log_error!("Failed to download update: {}", e); + format!("Failed to download update: {e}") + })?; if !response.status().is_success() { + log_error!("Update download failed with status: {}", response.status()); return Err(format!( "Download failed with status: {}", response.status() )); } - let bytes = response - .bytes() - .await - .map_err(|e| format!("Failed to read update data: {e}"))?; + let bytes = response.bytes().await.map_err(|e| { + log_error!("Failed to read update data from response: {}", e); + format!("Failed to read update data: {e}") + })?; - log_debug!("Downloaded {} mb", bytes.len() / (1024 * 1024)); + log_debug!("Downloaded {} MB", bytes.len() / (1024 * 1024)); let temp_dir = std::env::temp_dir(); let file_name = download_url.split('/').next_back().unwrap_or("update.msi"); let temp_file = temp_dir.join(file_name); - log_debug!("Writing to temp file: {:?}", temp_file); + log_debug!("Writing update to temp file: {:?}", temp_file); if !file_name.ends_with(".msi") { + log_error!("Downloaded file is not an MSI installer: {}", file_name); return Err(format!( "Downloaded file is not an MSI. Please download manually from {download_url}" )); } - std::fs::write(&temp_file, bytes).map_err(|e| format!("Failed to write update file: {e}"))?; + std::fs::write(&temp_file, bytes).map_err(|e| { + log_error!("Failed to write update file to temp directory: {}", e); + format!("Failed to write update file: {e}") + })?; #[cfg(target_os = "windows")] { @@ -252,11 +298,13 @@ pub async fn download_and_install_update(download_url: String) -> Result<(), Str .ok() .and_then(|p| p.file_name().map(|s| s.to_string_lossy().to_string())) .unwrap_or_else(|| "collapseloader.exe".to_string()); + log_debug!("Current executable name: {}", current_exe_name); let msi_path = temp_file.to_string_lossy().to_string(); let script_path = std::env::temp_dir().join("cl_update_and_restart.bat"); + log_debug!("Updater script path: {:?}", script_path); - let quoted_msi = msi_path; // no-op replace removed + let quoted_msi = msi_path; let script_content = format!( r#"@echo off @@ -300,15 +348,19 @@ exit .map_err(|e| format!("Failed to create updater script: {e}"))?; file.write_all(script_content.as_bytes()) .map_err(|e| format!("Failed to write updater script: {e}"))?; + log_debug!("Updater script created successfully."); } let mut cmd = std::process::Command::new("cmd.exe"); cmd.args(["/C", "start", "", &script_path.to_string_lossy()]); - const DETACHED_PROCESS: u32 = 0x00000008; + const DETACHED_PROCESS: u32 = 0x0000_0008; cmd.creation_flags(DETACHED_PROCESS); - cmd.spawn() - .map_err(|e| format!("Failed to launch updater script: {e}"))?; + cmd.spawn().map_err(|e| { + log_error!("Failed to launch updater script: {}", e); + format!("Failed to launch updater script: {e}") + })?; + log_info!("Updater script launched. Exiting application to allow update."); std::process::exit(0); } @@ -319,7 +371,7 @@ exit } #[tauri::command] -pub fn get_changelog() -> Vec { +pub const fn get_changelog() -> Vec { Vec::new() } @@ -329,6 +381,7 @@ fn extract_changelog_json_block(body: &str) -> Option { } else if let Some(idx) = body.find("``` changelog") { (idx, "``` changelog") } else { + log_debug!("No changelog JSON block marker found in release body."); return None; }; @@ -342,9 +395,11 @@ fn extract_changelog_json_block(body: &str) -> Option { if let Some(closing_rel) = rest.find("```") { let closing_idx = content_start + closing_rel; let content = &body[content_start..closing_idx]; + log_debug!("Extracted changelog content block."); return Some(content.trim().to_string()); } + log_warn!("Found changelog block marker but no closing '```'."); None } @@ -352,10 +407,12 @@ fn parse_changelog_and_translations( content: &str, ) -> Result<(Vec, Option), String> { if let Ok(v) = serde_json::from_str::>(content) { + log_debug!("Parsed changelog as a direct array of entries."); return Ok((v, None)); } if let Ok(entry) = serde_json::from_str::(content) { + log_debug!("Parsed changelog as a single entry object."); return Ok((vec![entry], None)); } @@ -371,9 +428,13 @@ fn parse_changelog_and_translations( .map_err(|e| format!("Failed to serialize entries node: {e}"))?; let entries: Vec = serde_json::from_str(&entries_json) .map_err(|e| format!("Failed to parse entries array: {e}"))?; + log_debug!( + "Parsed changelog from a root object with 'entries' and 'translations' keys." + ); return Ok((entries, translations_val)); } } + log_warn!("Changelog JSON is not in a recognized format."); Err("Changelog JSON is not in a recognized format".to_string()) } diff --git a/src-tauri/src/commands/utils.rs b/src-tauri/src/commands/utils.rs index 42d94e0..236efdc 100644 --- a/src-tauri/src/commands/utils.rs +++ b/src-tauri/src/commands/utils.rs @@ -5,12 +5,14 @@ use crate::commands::clients::{ }; use crate::core::utils::globals::CODENAME; use crate::core::{network::servers::SERVERS, storage::data::DATA}; +use crate::{log_debug, log_error, log_info, log_warn}; use std::{fs, path::PathBuf}; use tauri::{AppHandle, Emitter, Manager}; use tokio::task; #[tauri::command] pub fn get_version() -> Result { + log_debug!("Fetching application version information"); let result = serde_json::json!({ "version": env!("CARGO_PKG_VERSION").to_string(), "codename": CODENAME, @@ -25,14 +27,18 @@ pub fn get_version() -> Result { #[tauri::command] pub fn is_development() -> Result { let development = env!("DEVELOPMENT").to_string(); - Ok(development == "true") + let is_dev = development == "true"; + log_debug!("Checking development status: {}", is_dev); + Ok(is_dev) } #[tauri::command] pub fn open_data_folder() -> Result { let path = DATA.root_dir.to_string_lossy().to_string(); + log_info!("Opening data folder at: {}", path); if let Err(e) = open::that(&path) { + log_error!("Failed to open data folder at {}: {}", path, e); return Err(format!("Failed to open data folder: {e}")); } @@ -41,23 +47,31 @@ pub fn open_data_folder() -> Result { #[tauri::command] pub fn reset_requirements() -> Result<(), String> { + log_info!("Resetting client requirements"); if let Err(e) = DATA.reset_requirements() { + log_error!("Failed to reset requirements: {}", e); return Err(format!("Failed to reset requirements: {e}")); } + log_info!("Client requirements reset successfully"); Ok(()) } #[tauri::command] pub fn reset_cache() -> Result<(), String> { + log_info!("Resetting application cache"); if let Err(e) = DATA.reset_cache() { + log_error!("Failed to reset cache: {}", e); return Err(format!("Failed to reset cache: {e}")); } + log_info!("Application cache reset successfully"); Ok(()) } #[tauri::command] pub fn get_data_folder() -> Result { - Ok(DATA.root_dir.to_string_lossy().to_string()) + let path = DATA.root_dir.to_string_lossy().to_string(); + log_debug!("Getting data folder path: {}", path); + Ok(path) } #[tauri::command] @@ -66,28 +80,43 @@ pub async fn change_data_folder( new_path: String, mode: String, ) -> Result<(), String> { + log_info!( + "Changing data folder to '{}' with mode '{}'", + new_path, + mode + ); let new_dir = PathBuf::from(new_path.clone()); if new_dir.as_os_str().is_empty() { + log_warn!("Change data folder failed: Target path is empty"); return Err("Target path is empty".to_string()); } if !new_dir.exists() { - fs::create_dir_all(&new_dir).map_err(|e| format!("Failed to create target dir: {e}"))?; + log_debug!("Target directory does not exist, creating it: {:?}", new_dir); + fs::create_dir_all(&new_dir).map_err(|e| { + log_error!("Failed to create target directory {:?}: {}", new_dir, e); + format!("Failed to create target dir: {e}") + })?; } + log_info!("Stopping all running clients before changing data folder"); let running: Vec = get_running_client_ids().await; for id in running { + log_debug!("Stopping client with ID: {}", id); let _ = stop_client(id).await; } let running_custom: Vec = get_running_custom_client_ids().await; for id in running_custom { + log_debug!("Stopping custom client with ID: {}", id); let _ = stop_custom_client(id).await; } let current_dir = DATA.root_dir.clone(); + log_debug!("Current data directory is: {:?}", current_dir); if mode == "move" { + log_info!("Moving data from old folder to new folder"); if current_dir.exists() { task::spawn_blocking(move || -> Result<(), String> { fn copy_dir_recursive( @@ -107,58 +136,87 @@ pub async fn change_data_folder( } Ok(()) } + log_debug!( + "Starting recursive copy from {:?} to {:?}", + current_dir, + new_dir + ); copy_dir_recursive(¤t_dir, &new_dir)?; + log_debug!("Finished recursive copy. Removing old directory."); if let Err(e) = fs::remove_dir_all(¤t_dir) { + log_warn!("Failed to remove old data directory: {}", e); let _ = e; } Ok(()) }) .await - .map_err(|e| format!("Task join error: {e}"))??; + .map_err(|e| { + log_error!("Task to move data folder failed: {}", e); + format!("Task join error: {e}") + })??; } } else if mode == "wipe" { + log_info!("Wiping old data folder"); if current_dir.exists() { - fs::remove_dir_all(¤t_dir) - .map_err(|e| format!("Failed to wipe old folder: {e}"))?; + fs::remove_dir_all(¤t_dir).map_err(|e| { + log_error!("Failed to wipe old data folder: {}", e); + format!("Failed to wipe old folder: {e}") + })?; } } else { + log_warn!("Invalid mode for changing data folder: {}", mode); return Err("Invalid mode".to_string()); } let roaming_dir = std::env::var("APPDATA") .unwrap_or_else(|_| std::env::var("HOME").unwrap_or_else(|_| ".".to_string())); let override_file = PathBuf::from(roaming_dir).join("CollapseLoaderRoot.txt"); - fs::write(&override_file, &new_path).map_err(|e| format!("Failed to write override: {e}"))?; + log_info!( + "Writing new data folder path to override file: {:?}", + override_file + ); + fs::write(&override_file, &new_path).map_err(|e| { + log_error!("Failed to write override file: {}", e); + format!("Failed to write override: {e}") + })?; if let Some(window) = app.get_webview_window("main") { + log_debug!("Emitting 'data-folder-changed' event to main window"); let _ = window.emit("data-folder-changed", &new_path); } + log_info!("Data folder change process completed successfully"); Ok(()) } #[tauri::command] pub async fn get_auth_url() -> Result { - if let Some(auth_url) = SERVERS.get_auth_server_url() { - Ok(auth_url) - } else { - Ok("https://auth.collapseloader.org".to_string()) - } + log_debug!("Fetching authentication server URL"); + SERVERS + .get_auth_server_url() + .map_or_else(|| Ok("https://auth.collapseloader.org".to_string()), Ok) } #[tauri::command] pub async fn encode_base64(input: String) -> Result { + log_debug!("Encoding string to Base64"); let encoded = general_purpose::STANDARD.encode(input); Ok(encoded) } #[tauri::command] pub async fn decode_base64(input: String) -> Result { - match general_purpose::STANDARD.decode(&input) { - Ok(decoded) => match String::from_utf8(decoded) { - Ok(decoded_str) => Ok(decoded_str), - Err(_) => Err("Failed to decode base64 to UTF-8 string".to_string()), + log_debug!("Decoding string from Base64"); + general_purpose::STANDARD.decode(&input).ok().map_or_else( + || { + log_warn!("Failed to decode Base64 string"); + Err("Failed to decode base64".to_string()) }, - Err(_) => Err("Failed to decode base64".to_string()), - } + |decoded| { + String::from_utf8(decoded).map_err(|e| { + log_warn!("Failed to convert decoded bytes to UTF-8 string: {}", e); + "Failed to decode base64 to UTF-8 string".to_string() + }) + }, + ) } diff --git a/src-tauri/src/core/clients/client.rs b/src-tauri/src/core/clients/client.rs index 01be97c..a6a6cd0 100644 --- a/src-tauri/src/core/clients/client.rs +++ b/src-tauri/src/core/clients/client.rs @@ -10,12 +10,12 @@ use std::{ use std::os::windows::process::CommandExt; use super::manager::CLIENT_MANAGER; -use crate::core::network::analytics::Analytics; use crate::core::storage::accounts::ACCOUNT_MANAGER; use crate::core::utils::globals::FILE_EXTENSION; use crate::core::utils::helpers::{emit_to_main_window, emit_to_main_window_filtered}; use crate::core::{clients::internal::agent_overlay::AgentArguments, utils::globals::JDK_FOLDER}; use crate::core::{clients::log_checker::LogChecker, utils::globals::IS_LINUX}; +use crate::core::{network::analytics::Analytics, storage::data::Data}; use crate::{ core::storage::{data::DATA, settings::SETTINGS}, log_debug, log_error, log_info, @@ -26,13 +26,14 @@ use serde::{Deserialize, Serialize}; use tauri::AppHandle; use tokio::sync::Semaphore; -lazy_static::lazy_static! { - pub static ref CLIENT_LOGS: Mutex>> = Mutex::new(HashMap::new()); - pub static ref REQUIREMENTS_DOWNLOADING: Mutex = Mutex::new(false); - pub static ref REQUIREMENTS_SEMAPHORE: Arc = Arc::new(Semaphore::new(1)); -} +pub static CLIENT_LOGS: std::sync::LazyLock>>> = + std::sync::LazyLock::new(|| Mutex::new(HashMap::new())); +pub static REQUIREMENTS_DOWNLOADING: std::sync::LazyLock> = + std::sync::LazyLock::new(|| Mutex::new(false)); +pub static REQUIREMENTS_SEMAPHORE: std::sync::LazyLock> = + std::sync::LazyLock::new(|| Arc::new(Semaphore::new(1))); -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)] pub struct Meta { pub is_new: bool, pub asset_index: String, @@ -51,20 +52,20 @@ impl Meta { let asset_index = format!("{}.{}", semver.major, semver.minor); let is_new_version = semver.minor >= 16; - let file_name = DATA.get_filename(filename); + let client_base_name = Data::get_filename(filename); let jar_path = if filename.contains("fabric/") { let jar_basename = std::path::Path::new(filename) .file_name() .and_then(|n| n.to_str()) .unwrap_or(filename); DATA.root_dir - .join(&file_name) + .join(&client_base_name) .join("mods") .join(jar_basename) } else { DATA.get_local(&format!( "{}{}{}", - file_name, + client_base_name, std::path::MAIN_SEPARATOR, filename )) @@ -88,21 +89,16 @@ fn add_log_line(client_id: u32, line: String) { } } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)] pub enum ClientType { #[serde(rename = "default")] + #[default] Default, #[serde(rename = "fabric")] Fabric, } -impl Default for ClientType { - fn default() -> Self { - ClientType::Default - } -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)] pub struct Client { pub id: u32, pub name: String, @@ -128,7 +124,7 @@ pub struct Client { pub created_at: DateTime, } -fn default_meta() -> Meta { +const fn default_meta() -> Meta { Meta { is_new: false, asset_index: String::new(), @@ -149,7 +145,7 @@ pub struct LaunchOptions { } impl LaunchOptions { - pub fn new(app_handle: AppHandle, user_token: String, is_custom: bool) -> Self { + pub const fn new(app_handle: AppHandle, user_token: String, is_custom: bool) -> Self { Self { app_handle, user_token, @@ -246,7 +242,7 @@ impl Client { Ok(()) } - pub fn get_running_clients() -> Vec { + pub fn get_running_clients() -> Vec { let jps_path = DATA .root_dir .join(JDK_FOLDER) @@ -255,7 +251,7 @@ impl Client { let mut command = Command::new(jps_path); #[cfg(windows)] - command.creation_flags(0x08000000); + command.creation_flags(0x0800_0000); let output = match command.arg("-m").output() { Ok(output) => output, @@ -288,7 +284,7 @@ impl Client { let mut command = Command::new(jps_path); #[cfg(windows)] - command.creation_flags(0x08000000); + command.creation_flags(0x0800_0000); let output = command .arg("-m") @@ -306,7 +302,7 @@ impl Client { let mut kill_command = Command::new("taskkill"); #[cfg(windows)] - kill_command.creation_flags(0x08000000); + kill_command.creation_flags(0x0800_0000); let kill_output = kill_command .arg("/PID") @@ -343,52 +339,37 @@ impl Client { Ok(()) } - pub async fn download_requirements(&self, app_handle: &AppHandle) -> Result<(), String> { - let _permit = REQUIREMENTS_SEMAPHORE.acquire().await.map_err(|_| { - log_error!( - "Failed to acquire requirements download semaphore for '{}'", - self.name - ); - "Failed to acquire requirements download semaphore".to_string() - })?; - - let mut requirements_to_check = vec![format!("{JDK_FOLDER}.zip")]; - + fn determine_requirements_to_check(&self) -> Vec { + let mut requirements = vec![format!("{JDK_FOLDER}.zip")]; if self.client_type == ClientType::Fabric { - requirements_to_check.push("assets_fabric.zip".to_string()); + requirements.push("assets_fabric.zip".to_string()); + requirements.push("libraries_fabric.zip".to_string()); } else { - requirements_to_check.push("assets.zip".to_string()); - } - - if self.client_type == ClientType::Default { + requirements.push("assets.zip".to_string()); if self.meta.is_new { - requirements_to_check.push(if !IS_LINUX { + requirements.push(if !IS_LINUX { "natives.zip".to_string() } else { "natives-linux.zip".to_string() }); - requirements_to_check.push("libraries.zip".to_string()); + requirements.push("libraries.zip".to_string()); } else { - requirements_to_check.push(if !IS_LINUX { + requirements.push(if !IS_LINUX { "natives-1.12.zip".to_string() } else { "natives-linux.zip".to_string() }); - requirements_to_check.push("libraries-1.12.zip".to_string()); + requirements.push("libraries-1.12.zip".to_string()); } } + requirements + } - if self.client_type == ClientType::Fabric { - requirements_to_check.push("libraries_fabric.zip".to_string()); - } - - let mut client_jar: Option = None; - if self.client_type == ClientType::Fabric { - let sanitized_version = self.version.replace(' ', "_"); - let fabric_name = format!("fabric_{}.jar", sanitized_version); - client_jar = Some(fabric_name); - } - + fn check_if_download_needed( + &self, + requirements_to_check: &[String], + client_jar: &Option, + ) -> (bool, Vec) { let files_to_download: Vec = requirements_to_check .iter() .filter(|file| !DATA.get_as_folder(file).exists()) @@ -403,180 +384,140 @@ impl Client { ); if let Some(ref fabric_jar) = client_jar { - let dest_path = DATA.root_dir.join("minecraft_versions").join(fabric_jar); - if !dest_path.exists() { + if !DATA + .root_dir + .join("minecraft_versions") + .join(fabric_jar) + .exists() + { need_download = true; } } if self.client_type == ClientType::Fabric { if let Some(mods) = &self.requirement_mods { - let client_base = DATA.get_filename(&self.filename); + let client_base = Data::get_filename(&self.filename); let mods_folder = DATA.root_dir.join(&client_base).join("mods"); - for mod_name in mods.iter() { - let mod_basename = if mod_name.ends_with(".jar") { - mod_name.trim_end_matches(".jar").to_string() - } else { - mod_name.clone() - }; - - let dest_path = mods_folder.join(format!("{}.jar", mod_basename)); - if !dest_path.exists() { - need_download = true; - break; - } + if mods.iter().any(|mod_name| { + let mod_basename = mod_name.trim_end_matches(".jar"); + !mods_folder.join(format!("{mod_basename}.jar")).exists() + }) { + need_download = true; } } } - if !need_download { - log_info!( - "All requirements present for '{}', skipping downloads", - self.name + (need_download, files_to_download) + } + + async fn download_file(&self, file_to_dl: &str) -> Result<(), String> { + log_info!( + "Downloading requirement '{}' for client '{}'", + file_to_dl, + self.name + ); + DATA.download(file_to_dl).await.map_err(|e| { + log_error!( + "Failed to download '{}' for client '{}': {}", + file_to_dl, + self.name, + e ); - return Ok(()); - } + format!("Failed to download {file_to_dl}: {e}") + })?; + if IS_LINUX && file_to_dl.starts_with(JDK_FOLDER) { + let java_path = DATA.root_dir.join(JDK_FOLDER).join("bin").join("java"); + if java_path.exists() { + #[cfg(unix)] + if let Ok(mut perms) = std::fs::metadata(&java_path).map(|m| m.permissions()) { + use std::os::unix::fs::PermissionsExt; + perms.set_mode(0o755); + if let Err(e) = std::fs::set_permissions(&java_path, perms) { + log_error!("Failed to set exec perm on {}: {}", java_path.display(), e); + } else { + log_info!("Set exec perm on {}", java_path.display()); + } + } + } + } log_info!( - "Requirements missing for '{}' -> will download: {:?}", - self.name, - files_to_download + "Successfully downloaded '{}' for '{}'", + file_to_dl, + self.name ); + Ok(()) + } + async fn download_fabric_mods(&self) -> Result<(), String> { + if self.client_type == ClientType::Fabric { + if let Some(mods) = &self.requirement_mods { + for mod_name in mods.iter() { + let mod_basename = mod_name.trim_end_matches(".jar"); + let filename_on_cdn = format!("fabric/deps/{mod_basename}.jar"); + let client_base = Data::get_filename(&self.filename); + let dest_folder = format!("{client_base}/mods"); + let dest_path = DATA + .root_dir + .join(&client_base) + .join("mods") + .join(format!("{mod_basename}.jar")); + + if !dest_path.exists() { + log_info!("Downloading Fabric requirement mod: {}", filename_on_cdn); + DATA.download_to_folder(&filename_on_cdn, &dest_folder) + .await + .map_err(|e| { + log_error!("Failed to download mod {filename_on_cdn}: {e}"); + format!("Failed to download mod {filename_on_cdn}: {e}") + })?; + log_info!("Successfully downloaded mod {}", filename_on_cdn); + } + } + } + } + Ok(()) + } + + async fn download_required_files( + &self, + app_handle: &AppHandle, + files_to_download: Vec, + client_jar: Option, + ) -> Result<(), String> { { let mut downloading = REQUIREMENTS_DOWNLOADING .lock() .map_err(|_| "Failed to lock REQUIREMENTS_DOWNLOADING mutex".to_string())?; *downloading = true; } - emit_to_main_window(app_handle, "requirements-status", true); for file_to_dl in files_to_download { - log_info!( - "Downloading requirement '{}' for client '{}'", - file_to_dl, - self.name - ); - DATA.download(&file_to_dl).await.map_err(|e| { - log_error!( - "Failed to download '{}' for client '{}': {}", - file_to_dl, - self.name, - e - ); - format!("Failed to download {file_to_dl}: {e}") - })?; - - if IS_LINUX && file_to_dl.starts_with(&format!("{JDK_FOLDER}")) - || file_to_dl == format!("{JDK_FOLDER}.zip") - { - let java_path = DATA.root_dir.join(JDK_FOLDER).join("bin").join("java"); - if java_path.exists() { - #[cfg(unix)] - if let Ok(mut perms) = std::fs::metadata(&java_path).map(|m| m.permissions()) { - { - use std::os::unix::fs::PermissionsExt; - perms.set_mode(0o755); - if let Err(e) = std::fs::set_permissions(&java_path, perms) { - log_error!( - "Failed to set executable permission on {}: {}", - java_path.display(), - e - ); - } else { - log_info!("Set executable permission on {}", java_path.display()); - } - } - } - } - } - log_info!( - "Successfully downloaded '{}' for '{}'", - file_to_dl, - self.name - ); + self.download_file(&file_to_dl).await?; } if let Some(client_jar) = client_jar { let dest_path = DATA.root_dir.join("minecraft_versions").join(&client_jar); if !dest_path.exists() { log_info!( - "Downloading minecraft client jar '{}' for '{}'", + "Downloading MC client jar '{}' for '{}'", client_jar, self.name ); DATA.download_to_folder(&client_jar, "minecraft_versions") .await .map_err(|e| { - log_error!( - "Failed to download minecraft client jar '{}' for '{}': {}", - client_jar, - self.name, - e - ); - format!("Failed to download minecraft client jar {client_jar}: {e}") + log_error!("Failed to download MC client jar '{}': {}", client_jar, e); + format!("Failed to download MC client jar {client_jar}: {e}") })?; - log_info!( - "Successfully downloaded minecraft client jar '{}' for '{}'", - client_jar, - self.name - ); - } else { - log_debug!( - "Minecraft client jar '{}' already exists for '{}'", - client_jar, - self.name - ); + log_info!("Successfully downloaded MC client jar '{}'", client_jar); } } - if self.client_type == ClientType::Fabric { - if let Some(mods) = &self.requirement_mods { - for mod_name in mods.iter() { - let mod_basename = if mod_name.ends_with(".jar") { - mod_name.trim_end_matches(".jar").to_string() - } else { - mod_name.clone() - }; - - let filename_on_cdn = format!("fabric/deps/{}.jar", mod_basename); - - let client_base = DATA.get_filename(&self.filename); - let dest_folder = format!("{}/mods", client_base); - let dest_path = DATA - .root_dir - .join(&client_base) - .join("mods") - .join(format!("{}.jar", mod_basename)); - - if !dest_path.exists() { - log_info!( - "Downloading Fabric requirement mod from CDN: {}", - filename_on_cdn - ); - if let Err(e) = DATA - .download_to_folder(&filename_on_cdn, &dest_folder) - .await - { - log_error!( - "Failed to download fabric requirement mod {}: {}", - filename_on_cdn, - e - ); - return Err(format!( - "Failed to download fabric requirement mod {}: {}", - filename_on_cdn, e - )); - } - log_info!("Successfully downloaded mod {}", filename_on_cdn); - } - } - } - } + self.download_fabric_mods().await?; log_info!("All requirements downloaded successfully"); - tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; { @@ -585,12 +526,49 @@ impl Client { .map_err(|_| "Failed to lock REQUIREMENTS_DOWNLOADING mutex".to_string())?; *downloading = false; } - emit_to_main_window(app_handle, "requirements-status", false); Ok(()) } + #[allow(clippy::cognitive_complexity)] + pub async fn download_requirements(&self, app_handle: &AppHandle) -> Result<(), String> { + let _permit = REQUIREMENTS_SEMAPHORE.acquire().await.map_err(|_| { + log_error!( + "Failed to acquire requirements semaphore for '{}'", + self.name + ); + "Failed to acquire requirements semaphore".to_string() + })?; + + let requirements_to_check = self.determine_requirements_to_check(); + let client_jar = if self.client_type == ClientType::Fabric { + Some(format!("fabric_{}.jar", self.version.replace(' ', "_"))) + } else { + None + }; + + let (need_download, files_to_download) = + self.check_if_download_needed(&requirements_to_check, &client_jar); + + if !need_download { + log_info!( + "All requirements present for '{}', skipping downloads", + self.name + ); + return Ok(()); + } + + log_info!( + "Requirements missing for '{}' -> will download: {:?}", + self.name, + files_to_download + ); + + self.download_required_files(app_handle, files_to_download, client_jar) + .await + } + pub async fn run(self, options: LaunchOptions) -> Result<(), String> { if !options.is_custom && SETTINGS.lock().is_ok_and(|s| s.optional_telemetry.value) { Analytics::send_client_analytics(self.id); @@ -612,7 +590,11 @@ impl Client { let agent_arguments = AgentArguments::new( options.user_token.clone(), client_name.clone(), - optional_analytics, + if self.meta.is_custom { + false + } else { + optional_analytics + }, cordshare, irc_chat, ); @@ -631,7 +613,7 @@ impl Client { serde_json::json!({ "id": client_id, "name": client_name.clone(), - "error": e.clone() + "error": e }), ); return Err(e); @@ -663,7 +645,7 @@ impl Client { let jar = folder.join(&self_clone.filename); (folder, jar) } else if self_clone.filename.contains("fabric/") { - let base_name = DATA.get_filename(&self_clone.filename); + let base_name = Data::get_filename(&self_clone.filename); let folder = DATA.root_dir.join(&base_name); let jar_basename = std::path::Path::new(&self_clone.filename) .file_name() @@ -673,7 +655,7 @@ impl Client { } else { let folder = DATA .root_dir - .join(DATA.get_as_folder_string(&self_clone.filename)); + .join(Data::get_as_folder_string(&self_clone.filename)); let jar = folder.join(&self_clone.filename); (folder, jar) }; @@ -720,7 +702,7 @@ impl Client { ); #[cfg(windows)] - command.creation_flags(0x08000000); + command.creation_flags(0x0800_0000); std::env::set_current_dir(&client_folder) .map_err(|e| format!("Failed to set current directory: {e}"))?; @@ -730,7 +712,7 @@ impl Client { .ok() .and_then(|manager| manager.get_active_account().map(|a| a.username.clone())) .unwrap_or_else(|| { - let random_digits = rand::random::() % 100000; + let random_digits = rand::random::() % 100_000; format!("Collapse{random_digits:05}") }); @@ -883,7 +865,7 @@ impl Client { serde_json::json!({ "id": client_id, "name": self_clone.name.clone(), - "error": log_line.clone() + "error": log_line }), ); Err(log_line) diff --git a/src-tauri/src/core/clients/custom_clients.rs b/src-tauri/src/core/clients/custom_clients.rs index ba20103..122f8f0 100644 --- a/src-tauri/src/core/clients/custom_clients.rs +++ b/src-tauri/src/core/clients/custom_clients.rs @@ -3,10 +3,11 @@ use crate::core::{ storage::{custom_clients::CUSTOM_CLIENT_MANAGER, data::DATA}, utils::globals::{FILE_EXTENSION, JDK_FOLDER}, }; +use crate::{log_debug, log_error, log_info, log_warn}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] pub enum Version { V1_16_5, V1_12_2, @@ -15,9 +16,15 @@ pub enum Version { impl Version { pub fn from_string(version: &str) -> Self { match version { - "1.16.5" => Version::V1_16_5, - "1.12.2" => Version::V1_12_2, - _ => Version::V1_16_5, + "1.16.5" => Self::V1_16_5, + "1.12.2" => Self::V1_12_2, + _ => { + log_warn!( + "Unsupported version string '{}', defaulting to 1.16.5", + version + ); + Self::V1_16_5 + } } } } @@ -25,13 +32,13 @@ impl Version { impl std::fmt::Display for Version { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Version::V1_16_5 => write!(f, "1.16.5"), - Version::V1_12_2 => write!(f, "1.12.2"), + Self::V1_16_5 => write!(f, "1.16.5"), + Self::V1_12_2 => write!(f, "1.12.2"), } } } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] pub struct CustomClient { pub id: u32, pub name: String, @@ -54,7 +61,7 @@ impl CustomClient { file_path: PathBuf, main_class: String, ) -> Self { - CustomClient { + Self { id, name, version, @@ -96,12 +103,21 @@ impl CustomClient { } pub fn validate_file(&self) -> Result<(), String> { + log_debug!( + "Validating file for custom client '{}' at path: {}", + self.name, + self.file_path.display() + ); if !self.file_path.exists() { - return Err(format!("File {} does not exist", self.file_path.display())); + let err_msg = format!("File {} does not exist", self.file_path.display()); + log_error!("Validation failed for '{}': {}", self.name, err_msg); + return Err(err_msg); } if !self.file_path.is_file() { - return Err(format!("Path {} is not a file", self.file_path.display())); + let err_msg = format!("Path {} is not a file", self.file_path.display()); + log_error!("Validation failed for '{}': {}", self.name, err_msg); + return Err(err_msg); } let extension = self @@ -111,13 +127,19 @@ impl CustomClient { .unwrap_or(""); if extension != "jar" { - return Err("File must be a .jar file".to_string()); + let err_msg = "File must be a .jar file".to_string(); + log_error!("Validation failed for '{}': {}", self.name, err_msg); + return Err(err_msg); } + log_debug!( + "File validation successful for custom client '{}'", + self.name + ); Ok(()) } - pub fn get_running_custom_clients() -> Vec { + pub fn get_running_custom_clients() -> Vec { use std::process::Command; #[cfg(target_os = "windows")] @@ -131,11 +153,12 @@ impl CustomClient { let mut command = Command::new(jps_path); #[cfg(windows)] - command.creation_flags(0x08000000); + command.creation_flags(0x0800_0000); let output = match command.arg("-m").output() { Ok(output) => output, - Err(_) => { + Err(e) => { + log_error!("Failed to execute jps command: {}", e); return Vec::new(); } }; @@ -149,10 +172,12 @@ impl CustomClient { .map(|manager| manager.clients.clone()) .unwrap_or_default(); - custom_clients + let running_clients: Vec = custom_clients .into_iter() .filter(|client| outputs.iter().any(|line| line.contains(&client.filename))) - .collect() + .collect(); + + running_clients } pub fn stop(&self) -> Result<(), String> { @@ -162,6 +187,7 @@ impl CustomClient { #[cfg(target_os = "windows")] use std::os::windows::process::CommandExt; + log_info!("Attempting to stop custom client '{}'", self.name); let jps_path = DATA .root_dir .join(JDK_FOLDER) @@ -170,12 +196,12 @@ impl CustomClient { let mut command = Command::new(jps_path); #[cfg(windows)] - command.creation_flags(0x08000000); + command.creation_flags(0x0800_0000); - let output = command - .arg("-m") - .output() - .map_err(|e| format!("Failed to execute jps command: {e}"))?; + let output = command.arg("-m").output().map_err(|e| { + log_error!("Failed to execute jps command for stopping: {}", e); + format!("Failed to execute jps command: {e}") + })?; let binding = String::from_utf8_lossy(&output.stdout); let outputs: Vec<&str> = binding.lines().collect(); @@ -185,18 +211,40 @@ impl CustomClient { if line.contains(&self.filename) { process_found = true; let pid = line.split_whitespace().next().unwrap_or_default(); + log_debug!( + "Found process for custom client '{}' with PID: {}", + self.name, + pid + ); let mut kill_command = Command::new("taskkill"); #[cfg(windows)] - kill_command.creation_flags(0x08000000); + kill_command.creation_flags(0x0800_0000); - kill_command + let kill_output = kill_command .arg("/PID") .arg(pid) .arg("/F") .output() - .map_err(|e| format!("Failed to kill process: {e}"))?; + .map_err(|e| { + log_error!("Failed to execute taskkill for PID {}: {}", pid, e); + format!("Failed to kill process: {e}") + })?; + + if kill_output.status.success() { + log_info!( + "Successfully killed process {} for custom client '{}'", + pid, + self.name + ); + } else { + log_error!( + "taskkill failed for PID {}: {}", + pid, + String::from_utf8_lossy(&kill_output.stderr) + ); + } } } diff --git a/src-tauri/src/core/clients/internal/agent_overlay.rs b/src-tauri/src/core/clients/internal/agent_overlay.rs index bb1e704..5d52919 100644 --- a/src-tauri/src/core/clients/internal/agent_overlay.rs +++ b/src-tauri/src/core/clients/internal/agent_overlay.rs @@ -1,5 +1,5 @@ use crate::core::network::servers::SERVERS; -use crate::core::storage::data::DATA; +use crate::core::storage::data::{Data, DATA}; use crate::{log_debug, log_error, log_info, log_warn}; use base64::Engine; use serde::{Deserialize, Serialize}; @@ -22,7 +22,7 @@ pub struct AgentArguments { } impl AgentArguments { - pub fn new( + pub const fn new( token: String, client_name: String, analytics: bool, @@ -62,7 +62,7 @@ pub struct AgentOverlayManager; impl AgentOverlayManager { fn get_api_base_url() -> Result { SERVERS - .selected_auth_server + .selected_auth .as_ref() .map(|server| server.url.clone()) .ok_or_else(|| "No API server available".to_string()) @@ -99,7 +99,7 @@ impl AgentOverlayManager { e })?; - let downloaded_hash = Self::calculate_md5_hash(&agent_path)?; + let downloaded_hash = Data::calculate_md5_hash(&agent_path)?; if downloaded_hash != info.agent_hash { log_error!( "Agent file hash mismatch. expected={} got={}", @@ -123,7 +123,7 @@ impl AgentOverlayManager { e })?; - let downloaded_overlay_hash = Self::calculate_md5_hash(&overlay_path)?; + let downloaded_overlay_hash = Data::calculate_md5_hash(&overlay_path)?; if downloaded_overlay_hash != info.overlay_hash { log_error!( "Overlay file hash mismatch. expected={} got={}", @@ -200,13 +200,6 @@ impl AgentOverlayManager { Ok(()) } - fn calculate_md5_hash(path: &PathBuf) -> Result { - let bytes = fs::read(path).map_err(|e| format!("Failed to read file for hashing: {e}"))?; - - let digest = md5::compute(&bytes); - Ok(format!("{digest:x}")) - } - pub async fn verify_agent_overlay_files() -> Result { log_debug!("Verifying agent and overlay files..."); @@ -233,7 +226,7 @@ impl AgentOverlayManager { let info = Self::get_agent_overlay_info().await?; - let agent_hash = Self::calculate_md5_hash(&agent_path)?; + let agent_hash = Data::calculate_md5_hash(&agent_path)?; if agent_hash != info.agent_hash { log_error!( "Agent file hash verification failed. Expected: {}, Got: {}", @@ -243,7 +236,7 @@ impl AgentOverlayManager { return Ok(false); } - let overlay_hash = Self::calculate_md5_hash(&overlay_path)?; + let overlay_hash = Data::calculate_md5_hash(&overlay_path)?; if overlay_hash != info.overlay_hash { log_error!( "Overlay file hash verification failed. Expected: {}, Got: {}", diff --git a/src-tauri/src/core/clients/log_checker.rs b/src-tauri/src/core/clients/log_checker.rs index f5d0bc8..e126367 100644 --- a/src-tauri/src/core/clients/log_checker.rs +++ b/src-tauri/src/core/clients/log_checker.rs @@ -3,7 +3,7 @@ use tauri::AppHandle; use crate::core::utils::helpers::emit_to_main_window_filtered; use crate::{ core::clients::client::{Client, CLIENT_LOGS}, - log_info, + log_debug, log_info, log_warn, }; pub struct LogChecker { @@ -18,31 +18,49 @@ enum CrashType { } impl LogChecker { - pub fn new(client: Client) -> Self { - LogChecker { client } + pub const fn new(client: Client) -> Self { + Self { client } } pub fn check(&self, app_handle_clone_for_crash_handling: &AppHandle) { + log_debug!("Checking logs for client '{}'", self.client.name); if let Ok(logs_guard) = CLIENT_LOGS.lock() { if let Some(client_logs) = logs_guard.get(&self.client.id) { let full_log_string = client_logs.join("\\\\n"); if let Some(crash_type) = self.detect_crash_type(&full_log_string) { + log_warn!( + "Detected crash for client '{}': {:?}", + self.client.name, + crash_type + ); self.handle_crash(crash_type, client_logs, app_handle_clone_for_crash_handling); + } else { + log_debug!("No crash detected in logs for client '{}'", self.client.name); } + } else { + log_warn!( + "Could not find logs for client ID {} during crash check", + self.client.id + ); } + } else { + log_warn!("Failed to acquire lock on CLIENT_LOGS for crash check"); } } fn detect_crash_type(&self, log_string: &str) -> Option { if log_string.contains("Could not find or load main class") { + log_debug!("Detected MissingMainClass crash type"); Some(CrashType::MissingMainClass) } else if log_string.contains("java.lang.OutOfMemoryError") { + log_debug!("Detected OutOfMemory crash type"); Some(CrashType::OutOfMemory) } else if log_string.contains("#@!@# Game crashed!") || log_string.contains("Error occurred during initialization of VM") || log_string.contains("java.lang.UnsupportedClassVersionError") { + log_debug!("Detected GameCrashed crash type"); Some(CrashType::GameCrashed) } else { None @@ -50,6 +68,11 @@ impl LogChecker { } fn handle_crash(&self, crash_type: CrashType, client_logs: &[String], app_handle: &AppHandle) { + log_info!( + "Handling crash type {:?} for client '{}'", + crash_type, + self.client.name + ); match crash_type { CrashType::MissingMainClass => { log_info!( @@ -83,6 +106,10 @@ impl LogChecker { ); } CrashType::GameCrashed => { + log_warn!( + "Client '{}' crashed with a generic game error.", + self.client.name + ); self.emit_crash_details(client_logs, app_handle); emit_to_main_window_filtered( app_handle, @@ -98,6 +125,10 @@ impl LogChecker { } fn emit_crash_details(&self, client_logs: &[String], app_handle: &AppHandle) { + log_debug!( + "Emitting client-crash-details for client '{}'", + self.client.name + ); emit_to_main_window_filtered( app_handle, "client-crash-details", diff --git a/src-tauri/src/core/clients/manager.rs b/src-tauri/src/core/clients/manager.rs index ba3f8c8..3f2365e 100644 --- a/src-tauri/src/core/clients/manager.rs +++ b/src-tauri/src/core/clients/manager.rs @@ -1,5 +1,8 @@ -use lazy_static::lazy_static; -use std::{fs::File, io::BufReader, sync::Mutex}; +use std::{ + fs::File, + io::BufReader, + sync::{LazyLock, Mutex}, +}; use tauri::AppHandle; use super::client::Client; @@ -46,7 +49,7 @@ impl ClientManager { cached_clients } else { log_warn!("Clients cache not found. Returning empty client list."); - return Ok(ClientManager { + return Ok(Self { clients: Vec::new(), }); } @@ -81,7 +84,7 @@ impl ClientManager { log_debug!("ClientManager initialized with {} clients", clients.len()); - Ok(ClientManager { clients }) + Ok(Self { clients }) } else { log_warn!("API instance not available. Attempting to load clients from cache."); let clients_cache_path = DATA.root_dir.join(API_CACHE_DIR).join("clients.json"); @@ -124,7 +127,7 @@ impl ClientManager { "ClientManager initialized from cache with {} clients — operating offline mode", clients.len() ); - Ok(ClientManager { clients }) + Ok(Self { clients }) } } @@ -152,21 +155,23 @@ impl ClientManager { } } -lazy_static! { - pub static ref CLIENT_MANAGER: Mutex> = Mutex::new(None); -} +pub static CLIENT_MANAGER: LazyLock>> = + LazyLock::new(|| Mutex::new(None)); pub async fn initialize_client_manager() -> Result<(), String> { match ClientManager::new_async().await { Ok(manager) => { log_info!("ClientManager async initialization succeeded — setting global manager"); - if let Ok(mut client_manager) = CLIENT_MANAGER.lock() { - *client_manager = Some(manager); - Ok(()) - } else { - log_error!("Failed to acquire lock on CLIENT_MANAGER during initialization"); - Err("Failed to acquire lock on CLIENT_MANAGER".to_string()) - } + CLIENT_MANAGER.lock().map_or_else( + |_| { + log_error!("Failed to acquire lock on CLIENT_MANAGER during initialization"); + Err("Failed to acquire lock on CLIENT_MANAGER".to_string()) + }, + |mut client_manager| { + *client_manager = Some(manager); + Ok(()) + }, + ) } Err(e) => { log_error!("Failed to initialize ClientManager: {}", e); diff --git a/src-tauri/src/core/error.rs b/src-tauri/src/core/error.rs new file mode 100644 index 0000000..8999bbd --- /dev/null +++ b/src-tauri/src/core/error.rs @@ -0,0 +1,46 @@ +use crate::log_error; +use native_dialog::DialogBuilder; + +#[derive(Debug, thiserror::Error)] +pub enum StartupError { + #[error("WebView2 is not installed. Please install it from https://developer.microsoft.com/en-us/microsoft-edge/webview2/")] + WebView2NotInstalled, + #[error("Failed to install WebView2. Please install it manually from https://developer.microsoft.com/en-us/microsoft-edge/webview2/")] + WebView2InstallFailed, + #[error("Failed to check for WebView2: {0}")] + WebView2CheckFailed(String), + + #[cfg(target_os = "linux")] + #[error("webkit2gtk dependencies are missing. Please install them. For example: \n\nDebian/Ubuntu: sudo apt-get install libwebkit2gtk-4.0-37\nArch: sudo pacman -S webkit2gtk3\nFedora: sudo dnf install webkit2gtk3-devel")] + LinuxDependenciesMissing, + + #[cfg(target_os = "linux")] + #[error("Warning: WEBKIT_DISABLE_DMABUF_RENDERER environment variable is not set to 1.\n\nIf you experience a white screen or rendering issues, please set this variable before launching the application:\n\nWEBKIT_DISABLE_DMABUF_RENDERER=1 collapseloader\n\nOr add it to your shell profile for permanent use.")] + LinuxWebKitWarning, +} + +impl StartupError { + pub fn show_and_exit(&self) { + let title = "Startup Error"; + let message = self.to_string(); + + log_error!("Startup Error: {}", message); + + let _ = DialogBuilder::message().set_text(&message).set_title(title); + + std::process::exit(1); + } + + pub fn show_warning(&self) { + let title = "Warning"; + let message = self.to_string(); + + eprintln!("\n==== WARNING ===="); + eprintln!("{}", message); + eprintln!("================\n"); + + let _ = std::panic::catch_unwind(|| { + let _ = DialogBuilder::message().set_text(&message).set_title(title); + }); + } +} diff --git a/src-tauri/src/core/mod.rs b/src-tauri/src/core/mod.rs index 92162b5..716319f 100644 --- a/src-tauri/src/core/mod.rs +++ b/src-tauri/src/core/mod.rs @@ -1,4 +1,6 @@ pub mod clients; +pub mod error; pub mod network; +pub mod platform; pub mod storage; pub mod utils; diff --git a/src-tauri/src/core/network/analytics.rs b/src-tauri/src/core/network/analytics.rs index 4462957..fcc9cb9 100644 --- a/src-tauri/src/core/network/analytics.rs +++ b/src-tauri/src/core/network/analytics.rs @@ -53,7 +53,7 @@ impl Analytics { } fn get_server_url(analytics_type: &str) -> Option { - match SERVERS.selected_auth_server.clone() { + match SERVERS.selected_auth.clone() { Some(server) => Some(server.url), None => { log_debug!("No Auth server selected for {}", analytics_type); diff --git a/src-tauri/src/core/network/api.rs b/src-tauri/src/core/network/api.rs index 7e94a5e..6ddddcb 100644 --- a/src-tauri/src/core/network/api.rs +++ b/src-tauri/src/core/network/api.rs @@ -1,5 +1,5 @@ -use lazy_static::lazy_static; use serde::de::DeserializeOwned; +use std::sync::LazyLock; use std::{ fs::{self, File}, io::{BufReader, BufWriter}, @@ -86,13 +86,12 @@ impl Api { ); let result: T = serde_json::from_value(cached)?; return Ok(result); - } else { - return Err(format!( - "API returned status {} and no cache available", - response.status() - ) - .into()); } + return Err(format!( + "API returned status {} and no cache available", + response.status() + ) + .into()); } match response.text() { @@ -106,19 +105,15 @@ impl Api { ); let result: T = serde_json::from_value(cached)?; return Ok(result); - } else { - return Err( - "API returned empty response and no cache available".into() - ); } + return Err("API returned empty response and no cache available".into()); } match serde_json::from_str::(&body) { Ok(api_data) => { - let should_update_cache = match &cached_data { - Some(cached) => *cached != api_data, - None => true, - }; + let should_update_cache = cached_data + .as_ref() + .is_none_or(|cached| *cached != api_data); if should_update_cache && cache_dir.exists() { match File::create(&cache_file_path) { @@ -207,14 +202,12 @@ impl Api { } } -lazy_static! { - pub static ref API: Option = { - match SERVERS.selected_auth_server.clone() { - Some(auth_s) => Some(Api { api_server: auth_s }), - _ => { - log_warn!("Required Auth server or CDN server is not available. API functionality will be disabled."); - None - } - } - }; -} +pub static API: LazyLock> = LazyLock::new(|| { + SERVERS.selected_auth.clone().map_or_else( + || { + log_warn!("Required Auth server or CDN server is not available. API functionality will be disabled."); + None + }, + |auth_s| Some(Api { api_server: auth_s }), + ) +}); diff --git a/src-tauri/src/core/network/servers.rs b/src-tauri/src/core/network/servers.rs index dd221b2..e3ee6eb 100644 --- a/src-tauri/src/core/network/servers.rs +++ b/src-tauri/src/core/network/servers.rs @@ -2,8 +2,8 @@ use crate::{ core::utils::globals::{AUTH_SERVERS, CDN_SERVERS}, log_info, log_warn, }; -use lazy_static::lazy_static; use reqwest::blocking::Client; +use std::sync::LazyLock; use std::{sync::Mutex, time::Duration}; #[derive(Debug, Clone, serde::Serialize)] @@ -19,10 +19,10 @@ pub struct ServerConnectivityStatus { #[derive(Debug)] pub struct Servers { - pub cdn_servers: Vec, - pub auth_server: Vec, - pub selected_cdn_server: Option, - pub selected_auth_server: Option, + pub cdns: Vec, + pub auths: Vec, + pub selected_cdn: Option, + pub selected_auth: Option, pub connectivity_status: Mutex, } @@ -37,10 +37,10 @@ impl Server { impl Servers { pub fn new() -> Self { Self { - cdn_servers: CDN_SERVERS.to_vec(), - auth_server: AUTH_SERVERS.to_vec(), - selected_cdn_server: None, - selected_auth_server: None, + cdns: CDN_SERVERS.to_vec(), + auths: AUTH_SERVERS.to_vec(), + selected_cdn: None, + selected_auth: None, connectivity_status: Mutex::new(ServerConnectivityStatus { cdn_online: false, auth_online: false, @@ -54,7 +54,7 @@ impl Servers { .build() .unwrap_or_default(); - for server in &self.cdn_servers { + for server in &self.cdns { let response_result = client.head(&server.url).send(); match response_result { Ok(response) => { @@ -63,7 +63,7 @@ impl Servers { server.url, response.status() ); - self.selected_cdn_server = Some(server.clone()); + self.selected_cdn = Some(server.clone()); break; } Err(e) => { @@ -72,7 +72,7 @@ impl Servers { } } - for server in &self.auth_server { + for server in &self.auths { let response_result = client.head(&server.url).send(); match response_result { Ok(response) => { @@ -81,7 +81,7 @@ impl Servers { server.url, response.status() ); - self.selected_auth_server = Some(server.clone()); + self.selected_auth = Some(server.clone()); break; } Err(e) => { @@ -95,23 +95,18 @@ impl Servers { pub fn set_status(&self) -> ServerConnectivityStatus { let mut status = self.connectivity_status.lock().unwrap(); - status.cdn_online = self.selected_cdn_server.is_some(); - status.auth_online = self.selected_auth_server.is_some(); + status.cdn_online = self.selected_cdn.is_some(); + status.auth_online = self.selected_auth.is_some(); status.clone() } pub fn get_auth_server_url(&self) -> Option { - self.selected_auth_server - .as_ref() - .map(|server| server.url.clone()) + self.selected_auth.as_ref().map(|server| server.url.clone()) } } -lazy_static! { - #[derive(Debug)] - pub static ref SERVERS: Servers = { - let mut servers = Servers::new(); - servers.check_servers(); - servers - }; -} +pub static SERVERS: LazyLock = LazyLock::new(|| { + let mut servers = Servers::new(); + servers.check_servers(); + servers +}); diff --git a/src-tauri/src/core/platform/linux.rs b/src-tauri/src/core/platform/linux.rs new file mode 100644 index 0000000..cd6aed9 --- /dev/null +++ b/src-tauri/src/core/platform/linux.rs @@ -0,0 +1,35 @@ +use crate::core::error::StartupError; + +pub fn check_platform_dependencies() -> Result<(), StartupError> { + let result = std::process::Command::new("pkg-config") + .args(["--print-errors", "webkit2gtk-4.0"]) + .output(); + + match result { + Ok(output) => { + if !output.status.success() { + return Err(StartupError::LinuxDependenciesMissing); + } + } + Err(_) => { + return Err(StartupError::LinuxDependenciesMissing); + } + } + + Ok(()) +} + +pub fn check_webkit_environment() -> Result<(), StartupError> { + match std::env::var("WEBKIT_DISABLE_DMABUF_RENDERER") { + Ok(value) => { + if value != "1" { + return Err(StartupError::LinuxWebKitWarning); + } + } + Err(_) => { + return Err(StartupError::LinuxWebKitWarning); + } + } + + Ok(()) +} diff --git a/src-tauri/src/core/platform/mod.rs b/src-tauri/src/core/platform/mod.rs new file mode 100644 index 0000000..6bd3ff5 --- /dev/null +++ b/src-tauri/src/core/platform/mod.rs @@ -0,0 +1,15 @@ +#[cfg(target_os = "windows")] +pub(crate) mod windows; +#[cfg(target_os = "windows")] +pub use self::windows::check_platform_dependencies; + +#[cfg(target_os = "linux")] +mod linux; +#[cfg(target_os = "linux")] +pub use self::linux::{check_platform_dependencies, check_webkit_environment}; + +#[cfg(not(any(target_os = "windows", target_os = "linux")))] +pub fn check_platform_dependencies() -> Result<(), StartupError> { + // No specific checks for other platforms like macOS for now + Ok(()) +} diff --git a/src-tauri/src/core/platform/windows.rs b/src-tauri/src/core/platform/windows.rs new file mode 100644 index 0000000..a06d41d --- /dev/null +++ b/src-tauri/src/core/platform/windows.rs @@ -0,0 +1,115 @@ +use crate::core::error::StartupError; +use winreg::{enums::HKEY_CURRENT_USER, enums::HKEY_LOCAL_MACHINE, RegKey}; + +fn is_webview2_installed() -> bool { + let reg_subkeys = [ + "SOFTWARE\\WOW6432Node\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}", + "SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}", + ]; + + for subkey in ®_subkeys { + if RegKey::predef(HKEY_LOCAL_MACHINE) + .open_subkey(subkey) + .is_ok() + || RegKey::predef(HKEY_CURRENT_USER) + .open_subkey(subkey) + .is_ok() + { + return true; + } + } + + let candidates = [ + r"Microsoft\EdgeWebView\Application", + r"Microsoft\EdgeWebView2\Application", + r"Microsoft\EdgeWebView2\Runner", + r"Microsoft\EdgeWebView", + ]; + + let check_base = |base: &str| -> bool { + let base_path = std::path::Path::new(base); + for cand in &candidates { + let p = base_path.join(cand); + if p.exists() { + if p.is_dir() { + if std::fs::read_dir(&p) + .map(|mut it| it.next().is_some()) + .unwrap_or(false) + { + return true; + } + } else { + return true; + } + } + + let exe = base_path.join(format!(r"{}\msedgewebview2.exe", cand)); + if exe.exists() { + return true; + } + } + false + }; + + if let Ok(pf) = std::env::var("ProgramFiles") { + if check_base(&pf) { + return true; + } + } + if let Ok(pfx86) = std::env::var("ProgramFiles(x86)") { + if check_base(&pfx86) { + return true; + } + } + + if let Ok(temp) = std::env::var("TEMP") { + let temp_path = std::path::Path::new(&temp).join("MicrosoftEdgeWebview2Setup.exe"); + if temp_path.exists() { + return true; + } + } + + false +} + +fn install_webview2() -> Result<(), StartupError> { + eprintln!("WebView2 is not installed. Attempting to download and install it..."); + + let installer = reqwest::blocking::get("https://go.microsoft.com/fwlink/p/?LinkId=2124703") + .map_err(|e| StartupError::WebView2CheckFailed(e.to_string()))? + .bytes() + .map_err(|e| StartupError::WebView2CheckFailed(e.to_string()))?; + + let path = std::env::temp_dir().join("MicrosoftEdgeWebview2Setup.exe"); + + if std::fs::write(&path, installer).is_ok() { + let mut command = std::process::Command::new(&path); + command.arg("/install"); + + match command.status() { + Ok(status) if status.success() => { + eprintln!("WebView2 installer executed successfully."); + Ok(()) + } + _ => { + eprintln!("Failed to execute WebView2 installer."); + eprintln!("Command: {:?}", command); + Err(StartupError::WebView2InstallFailed) + } + } + } else { + eprintln!("Failed to write WebView2 installer to temporary directory."); + Err(StartupError::WebView2InstallFailed) + } +} + +pub fn check_platform_dependencies() -> Result<(), StartupError> { + if !is_webview2_installed() { + return Err(StartupError::WebView2NotInstalled); + } + Ok(()) +} + +pub fn attempt_install_webview2() -> Result<(), StartupError> { + install_webview2() +} diff --git a/src-tauri/src/core/storage/accounts.rs b/src-tauri/src/core/storage/accounts.rs index 29a98ab..76f07f3 100644 --- a/src-tauri/src/core/storage/accounts.rs +++ b/src-tauri/src/core/storage/accounts.rs @@ -1,7 +1,7 @@ use std::{path::PathBuf, sync::Mutex}; -use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; +use std::sync::LazyLock; use crate::core::storage::data::DATA; @@ -123,8 +123,8 @@ impl Default for AccountManager { } } -lazy_static! { - pub static ref ACCOUNT_MANAGER: Mutex = Mutex::new( - AccountManager::load_from_disk(DATA.get_local("accounts.json")) - ); -} +pub static ACCOUNT_MANAGER: LazyLock> = LazyLock::new(|| { + Mutex::new(AccountManager::load_from_disk( + DATA.get_local("accounts.json"), + )) +}); diff --git a/src-tauri/src/core/storage/custom_clients.rs b/src-tauri/src/core/storage/custom_clients.rs index 5c9cc97..47d76bf 100644 --- a/src-tauri/src/core/storage/custom_clients.rs +++ b/src-tauri/src/core/storage/custom_clients.rs @@ -5,8 +5,8 @@ use crate::core::clients::custom_clients::Version; use crate::core::storage::data::DATA; use crate::core::storage::settings::SETTINGS; use crate::log_warn; -use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; +use std::sync::LazyLock; use super::common::JsonStorage; @@ -164,8 +164,8 @@ impl Default for CustomClientManager { } } -lazy_static! { - pub static ref CUSTOM_CLIENT_MANAGER: Mutex = Mutex::new( - CustomClientManager::load_from_disk(DATA.get_local("custom_clients.json")) - ); -} +pub static CUSTOM_CLIENT_MANAGER: LazyLock> = LazyLock::new(|| { + Mutex::new(CustomClientManager::load_from_disk( + DATA.get_local("custom_clients.json"), + )) +}); diff --git a/src-tauri/src/core/storage/data.rs b/src-tauri/src/core/storage/data.rs index 12dd43a..913b445 100644 --- a/src-tauri/src/core/storage/data.rs +++ b/src-tauri/src/core/storage/data.rs @@ -6,22 +6,20 @@ use crate::core::utils::globals::{JDK_FOLDER, ROOT_DIR}; use crate::core::utils::helpers::emit_to_main_window; use crate::{log_debug, log_error, log_info, log_warn}; use futures_util::StreamExt; -use lazy_static::lazy_static; use std::path::{Path, PathBuf, MAIN_SEPARATOR}; use std::sync::Mutex; use std::time::Duration; use std::{fs, io}; use tokio::io::AsyncWriteExt; -pub struct DataManager { +pub struct Data { pub root_dir: PathBuf, } -lazy_static! { - pub static ref APP_HANDLE: Mutex> = Mutex::new(None); -} +pub static APP_HANDLE: std::sync::LazyLock>> = + std::sync::LazyLock::new(|| Mutex::new(None)); -impl DataManager { +impl Data { pub fn new(root_dir: PathBuf) -> Self { if !root_dir.exists() { log_debug!( @@ -35,6 +33,12 @@ impl DataManager { Self { root_dir } } + pub fn has_extension(file_path: &str, extension: &str) -> bool { + Path::new(file_path) + .extension() + .is_some_and(|ext| ext.eq_ignore_ascii_case(extension)) + } + pub fn get_local(&self, relative_path: &str) -> PathBuf { self.root_dir.join(relative_path) } @@ -115,29 +119,29 @@ impl DataManager { self.root_dir.join(file_name) } - pub fn get_as_folder_string(&self, file: &str) -> String { + pub fn get_as_folder_string(file: &str) -> String { let file_name = Path::new(file).file_stem().unwrap().to_str().unwrap(); format!("{file_name}{MAIN_SEPARATOR}") } - pub fn get_filename(&self, file: &str) -> String { + pub fn get_filename(file: &str) -> String { let file_name = Path::new(file).file_stem().unwrap().to_str().unwrap(); file_name.to_string() } pub async fn download(&self, file: &str) -> Result<(), String> { - let file_name = self.get_filename(file); - let is_fabric_client = file.ends_with(".jar") && file.contains("fabric/"); + let file_name = Self::get_filename(file); + let is_fabric_client = file.starts_with("fabric/") && file.ends_with(".jar"); let is_essential_requirement = file == format!("{JDK_FOLDER}.zip") || file.starts_with("assets") || file.starts_with("natives") || file.starts_with("libraries"); - let file_exists = if file.ends_with(".zip") { + let file_exists = if Self::has_extension(file, "zip") { let extract_path = self.root_dir.join(&file_name); extract_path.exists() - } else if file.ends_with(".jar") { + } else if Self::has_extension(file, "jar") { if is_fabric_client { let jar_basename = Path::new(file) .file_name() @@ -171,7 +175,7 @@ impl DataManager { emit_to_main_window(app_handle, "download-start", &file); } - if file.ends_with(".jar") { + if Self::has_extension(file, "jar") { if is_fabric_client { let mods_dir = self.root_dir.join(&file_name).join("mods"); if let Err(e) = fs::create_dir_all(&mods_dir) { @@ -181,17 +185,15 @@ impl DataManager { e ); return Err(format!("Failed to create mods directory: {e}")); - } else { - log_debug!("Created fabric mods directory: {}", mods_dir.display()); } + log_debug!("Created fabric mods directory: {}", mods_dir.display()); } else { - let local_path = self.get_as_folder(file).to_path_buf(); + let local_path = self.get_as_folder(file); if let Err(e) = fs::create_dir_all(&local_path) { log_error!("Failed to create directory {}: {}", local_path.display(), e); return Err(format!("Failed to create directory: {e}")); - } else { - log_debug!("Created client local directory: {}", local_path.display()); } + log_debug!("Created client local directory: {}", local_path.display()); if SETTINGS .lock() .map(|s| s.sync_client_settings.value) @@ -204,7 +206,7 @@ impl DataManager { } } - let cdn_url = SERVERS.selected_cdn_server.as_ref().map_or_else( + let cdn_url = SERVERS.selected_cdn.as_ref().map_or_else( || { log_error!("No CDN server available for download"); Err("No CDN server available for download.".to_string()) @@ -242,19 +244,17 @@ impl DataManager { } let total_size = response.content_length(); - let dest_path = if file.ends_with(".jar") { - if is_fabric_client { - let jar_basename = Path::new(file) - .file_name() - .and_then(|n| n.to_str()) - .unwrap_or(file); - self.root_dir - .join(&file_name) - .join("mods") - .join(jar_basename) - } else { - self.root_dir.join(format!("{file_name}/{file}")) - } + let dest_path = if is_fabric_client { + let jar_basename = Path::new(file) + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or(file); + self.root_dir + .join(&file_name) + .join("mods") + .join(jar_basename) + } else if Self::has_extension(file, "jar") { + self.root_dir.join(format!("{file_name}/{file}")) } else { self.root_dir.join(file) }; @@ -272,9 +272,6 @@ impl DataManager { let mut last_percentage: u8 = 0; let mut stream = response.bytes_stream(); - use futures_util::StreamExt; - use tokio::io::AsyncWriteExt; - while let Some(chunk) = stream.next().await { let chunk = chunk.map_err(|e| { log_error!("Failed to read response data for {}: {}", file, e); @@ -292,11 +289,10 @@ impl DataManager { downloaded += chunk.len() as u64; - let percentage = if let Some(total) = total_size { - ((downloaded as f64 / total as f64) * 100.0) as u8 - } else { - std::cmp::min(99, (downloaded / 1024 / 1024) as u8) - }; + let percentage = total_size.map_or_else( + || std::cmp::min(99, (downloaded / 1024 / 1024) as u8), + |total| ((downloaded as f64 / total as f64) * 100.0) as u8, + ); if percentage != last_percentage { last_percentage = percentage; @@ -322,24 +318,27 @@ impl DataManager { emit_to_main_window(app_handle, "download-complete", &file); } - if file.ends_with(".zip") { + if Self::has_extension(file, "zip") { self.unzip(file).map_err(|e| { log_error!("Failed to extract {}: {}", file, e); e })?; } - if file.ends_with(".jar") { + if Self::has_extension(file, "jar") { log_debug!("Verifying MD5 hash for client file: {}", file); - self.verify_client_hash(file, &dest_path).await?; + self.verify_client_hash(file, &dest_path).map_err(|e| { + log_error!("Failed to verify client hash for {}: {}", file, e); + e + })?; } Ok(()) } pub fn ensure_client_synced(&self, client_base: &str) -> Result<(), String> { - let folders_to_sync = vec!["resourcepacks"]; - let files_to_sync = vec!["options.txt", "optionsof.txt"]; + let folders_to_sync = ["resourcepacks"]; + let files_to_sync = ["options.txt", "optionsof.txt"]; let global_options_dir = self.root_dir.join("synced_options"); if !global_options_dir.exists() { @@ -355,7 +354,7 @@ impl DataManager { } } - for folder in folders_to_sync.iter() { + for folder in &folders_to_sync { let target = global_options_dir.join(folder); if !target.exists() { if let Err(e) = fs::create_dir_all(&target) { @@ -394,7 +393,7 @@ impl DataManager { } } - for file in files_to_sync.iter() { + for file in &files_to_sync { let global_file = global_options_dir.join(file); if !global_file.exists() { if let Err(e) = fs::write(&global_file, "") { @@ -460,7 +459,7 @@ impl DataManager { emit_to_main_window(app_handle, "download-start", &file); } - let cdn_url = SERVERS.selected_cdn_server.as_ref().map_or_else( + let cdn_url = SERVERS.selected_cdn.as_ref().map_or_else( || { log_error!("No CDN server available for download"); Err("No CDN server available for download.".to_string()) @@ -544,11 +543,10 @@ impl DataManager { downloaded += chunk.len() as u64; - let percentage = if let Some(total) = total_size { - ((downloaded as f64 / total as f64) * 100.0) as u8 - } else { - std::cmp::min(99, (downloaded / 1024 / 1024) as u8) - }; + let percentage = total_size.map_or_else( + || std::cmp::min(99, (downloaded / 1024 / 1024) as u8), + |total| ((downloaded as f64 / total as f64) * 100.0) as u8, + ); if percentage != last_percentage { last_percentage = percentage; @@ -577,7 +575,7 @@ impl DataManager { Ok(()) } - async fn verify_client_hash(&self, filename: &str, file_path: &PathBuf) -> Result<(), String> { + fn verify_client_hash(&self, filename: &str, file_path: &PathBuf) -> Result<(), String> { let hash_verify_enabled = { let settings = SETTINGS .lock() @@ -629,15 +627,15 @@ impl DataManager { filename ); let calculated_hash = if is_fabric { - let client_folder = self.root_dir.join(self.get_filename(filename)); + let client_folder = self.root_dir.join(Self::get_filename(filename)); let jar_basename = std::path::Path::new(filename) .file_name() .and_then(|n| n.to_str()) .ok_or_else(|| "Invalid fabric client filename".to_string())?; let fabric_jar_path = client_folder.join("mods").join(jar_basename); - self.calculate_md5_hash(&fabric_jar_path)? + Self::calculate_md5_hash(&fabric_jar_path)? } else { - self.calculate_md5_hash(file_path)? + Self::calculate_md5_hash(file_path)? }; if calculated_hash != expected_hash { @@ -687,7 +685,7 @@ impl DataManager { Ok(()) } - fn calculate_md5_hash(&self, path: &PathBuf) -> Result { + pub fn calculate_md5_hash(path: &PathBuf) -> Result { let bytes = fs::read(path).map_err(|e| format!("Failed to read file for hashing: {e}"))?; let digest = md5::compute(&bytes); @@ -717,7 +715,7 @@ impl DataManager { "minecraft_versions".to_string(), ]; - for requirement in requirements.iter() { + for requirement in &requirements { let path = self.root_dir.join(requirement); if path.exists() { if path.is_dir() { @@ -740,6 +738,5 @@ impl DataManager { } } -lazy_static! { - pub static ref DATA: DataManager = DataManager::new(ROOT_DIR.clone().into()); -} +pub static DATA: std::sync::LazyLock = + std::sync::LazyLock::new(|| Data::new(ROOT_DIR.clone().into())); diff --git a/src-tauri/src/core/storage/favorites.rs b/src-tauri/src/core/storage/favorites.rs index eb471ca..2f5cbd3 100644 --- a/src-tauri/src/core/storage/favorites.rs +++ b/src-tauri/src/core/storage/favorites.rs @@ -1,7 +1,7 @@ use std::{path::PathBuf, sync::Mutex}; -use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; +use std::sync::LazyLock; use crate::core::storage::data::DATA; @@ -52,8 +52,8 @@ impl Default for FavoriteManager { } } -lazy_static! { - pub static ref FAVORITE_MANAGER: Mutex = Mutex::new( - FavoriteManager::load_from_disk(DATA.get_local("favorites.json")) - ); -} +pub static FAVORITE_MANAGER: LazyLock> = LazyLock::new(|| { + Mutex::new(FavoriteManager::load_from_disk( + DATA.get_local("favorites.json"), + )) +}); diff --git a/src-tauri/src/core/storage/flags.rs b/src-tauri/src/core/storage/flags.rs index cf5b89e..86f7bc9 100644 --- a/src-tauri/src/core/storage/flags.rs +++ b/src-tauri/src/core/storage/flags.rs @@ -1,7 +1,7 @@ use super::common::JsonStorage; use super::data::DATA; -use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; +use std::sync::LazyLock; use std::{path::PathBuf, sync::Mutex}; #[derive(Clone, Debug, Serialize, Deserialize)] @@ -10,7 +10,7 @@ pub struct Flag { } impl Flag { - pub fn new(value: T) -> Self { + pub const fn new(value: T) -> Self { Self { value } } } @@ -56,7 +56,5 @@ impl Default for Flags { } } -lazy_static! { - pub static ref FLAGS_MANAGER: Mutex = - Mutex::new(Flags::load_from_disk(DATA.get_local("flags.json"))); -} +pub static FLAGS_MANAGER: LazyLock> = + LazyLock::new(|| Mutex::new(Flags::load_from_disk(DATA.get_local("flags.json")))); diff --git a/src-tauri/src/core/storage/presets.rs b/src-tauri/src/core/storage/presets.rs index 7d2f83f..2d49227 100644 --- a/src-tauri/src/core/storage/presets.rs +++ b/src-tauri/src/core/storage/presets.rs @@ -1,7 +1,7 @@ use super::common::JsonStorage; use super::data::DATA; -use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; +use std::sync::LazyLock; use std::{collections::HashMap, path::PathBuf, sync::Mutex as StdMutex}; #[derive(Clone, Debug, Serialize, Deserialize)] @@ -102,8 +102,8 @@ impl PresetManager { self.presets.get(id) } - pub fn get_all_presets(&self) -> Vec<&ThemePreset> { - self.presets.values().collect() + pub fn get_all_presets(&self) -> Vec { + self.presets.values().cloned().collect() } pub fn preset_exists(&self, id: &str) -> bool { @@ -111,8 +111,8 @@ impl PresetManager { } } -lazy_static! { - pub static ref PRESET_MANAGER: StdMutex = StdMutex::new( - PresetManager::load_from_disk(DATA.get_local("presets.json")) - ); -} +pub static PRESET_MANAGER: LazyLock> = LazyLock::new(|| { + StdMutex::new(PresetManager::load_from_disk( + DATA.get_local("presets.json"), + )) +}); diff --git a/src-tauri/src/core/storage/settings.rs b/src-tauri/src/core/storage/settings.rs index a176203..132d55d 100644 --- a/src-tauri/src/core/storage/settings.rs +++ b/src-tauri/src/core/storage/settings.rs @@ -4,9 +4,9 @@ use serde::{Deserialize, Serialize}; use std::{fmt, path::PathBuf, sync::Mutex as StdMutex}; use crate::core::utils::globals::ROOT_DIR; -use lazy_static::lazy_static; +use std::sync::LazyLock; -fn default_show_true() -> bool { +const fn default_show_true() -> bool { true } @@ -18,7 +18,7 @@ pub struct Setting { } impl Setting { - pub fn new(value: T, show: bool) -> Self { + pub const fn new(value: T, show: bool) -> Self { Self { value, show } } } @@ -126,8 +126,8 @@ define_settings! { } } -lazy_static! { - pub static ref SETTINGS: StdMutex = StdMutex::new(Settings::load_from_disk( - PathBuf::from(&*ROOT_DIR).join("config.json") - )); -} +pub static SETTINGS: LazyLock> = LazyLock::new(|| { + StdMutex::new(Settings::load_from_disk( + PathBuf::from(&*ROOT_DIR).join("config.json"), + )) +}); diff --git a/src-tauri/src/core/utils/discord_rpc.rs b/src-tauri/src/core/utils/discord_rpc.rs index c9bc618..f7f38ea 100644 --- a/src-tauri/src/core/utils/discord_rpc.rs +++ b/src-tauri/src/core/utils/discord_rpc.rs @@ -2,16 +2,15 @@ use std::sync::Mutex; use std::time::{SystemTime, UNIX_EPOCH}; use discord_rich_presence::{activity, DiscordIpc, DiscordIpcClient}; -use lazy_static::lazy_static; +use std::sync::LazyLock; use crate::core::storage::settings::SETTINGS; use crate::{log_debug, log_error, log_warn}; const DISCORD_APP_ID: &str = "1225803664204234772"; -lazy_static! { - static ref DISCORD_CLIENT: Mutex> = Mutex::new(None); -} +static DISCORD_CLIENT: LazyLock>> = + LazyLock::new(|| Mutex::new(None)); pub fn initialize() -> Result<(), String> { std::thread::spawn(|| { @@ -45,23 +44,25 @@ pub fn update_activity(details: String, state: String) -> Result<(), String> { return Ok(()); } - let mut discord_client_lock = match DISCORD_CLIENT.try_lock() { - Ok(lock) => lock, - Err(_) => { - log_debug!("Could not acquire Discord client lock, skipping update"); - return Ok(()); - } + let Ok(mut discord_client_lock) = DISCORD_CLIENT.try_lock() else { + log_debug!("Could not acquire Discord client lock, skipping update"); + return Ok(()); }; - let discord_client = match &mut *discord_client_lock { - Some(client) => client, - None => return Err("Discord client not initialized".to_string()), + let Some(discord_client) = &mut *discord_client_lock else { + return Err("Discord client not initialized".to_string()); }; - let start_time = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); + let start_time = match SystemTime::now().duration_since(UNIX_EPOCH) { + Ok(dur) => dur.as_secs(), + Err(err) => { + log_warn!( + "System time is before UNIX_EPOCH, using 0 for start time: {:?}", + err + ); + 0 + } + }; let large_text = format!("Version {env}", env = env!("CARGO_PKG_VERSION")); @@ -69,6 +70,7 @@ pub fn update_activity(details: String, state: String) -> Result<(), String> { .large_image("https://i.imgur.com/ZpWg110.gif") .large_text(&large_text); + #[allow(clippy::cast_possible_wrap)] let activity = activity::Activity::new() .details(&details) .state(&state) diff --git a/src-tauri/src/core/utils/globals.rs b/src-tauri/src/core/utils/globals.rs index b37b0f3..0eb3d1e 100644 --- a/src-tauri/src/core/utils/globals.rs +++ b/src-tauri/src/core/utils/globals.rs @@ -1,5 +1,4 @@ -use lazy_static::lazy_static; -use std::{fs, path::PathBuf}; +use std::{fs, path::PathBuf, sync::LazyLock}; use crate::{core::network::servers::Server, log_debug, log_info}; @@ -10,67 +9,69 @@ pub static GITHUB_REPO_NAME: &str = "CollapseLoader"; pub static IS_LINUX: bool = cfg!(target_os = "linux"); pub static FILE_EXTENSION: &str = if IS_LINUX { "" } else { ".exe" }; -pub static JDK_FOLDER: &str = if IS_LINUX { "jdk-21.0.2_linux" } else { "jdk-21.0.2" }; +pub static JDK_FOLDER: &str = if IS_LINUX { + "jdk-21.0.2_linux" +} else { + "jdk-21.0.2" +}; fn parse_env_bool(var: &str) -> bool { - std::env::var(var) - .ok() - .map(|s| { - let s = s.trim().to_ascii_lowercase(); - matches!(s.as_str(), "1" | "true" | "yes" | "on") - }) - .unwrap_or(false) + std::env::var(var).ok().is_some_and(|s| { + let s = s.trim().to_ascii_lowercase(); + matches!(s.as_str(), "1" | "true" | "yes" | "on") + }) } -lazy_static! { - pub static ref LOCAL_DEVELOPMENT: bool = { - let val = parse_env_bool("DEVELOPMENT"); - if val { - log_info!("Local development mode: {}", val); - } - val - }; +pub static USE_LOCAL_SERVER: LazyLock = LazyLock::new(|| { + let val = parse_env_bool("USE_LOCAL_SERVER"); + if val { + log_info!("Using local server: {}", val); + } + val +}); - pub static ref USE_LOCAL_SERVER: bool = { - let val = parse_env_bool("USE_LOCAL_SERVER"); - if val { - log_info!("Using local server: {}", val); - } - val - }; +pub static CDN_SERVERS: LazyLock> = LazyLock::new(|| { + vec![ + Server::new("https://cdn.collapseloader.org/"), + Server::new("https://collapse.ttfdk.lol/cdn/"), + Server::new( + "https://axkanxneklh7.objectstorage.eu-amsterdam-1.oci.customer-oci.com/n/axkanxneklh7/b/collapse/o/", + ), +] +}); - pub static ref CDN_SERVERS: Vec = vec![ - Server::new("https://cdn.collapseloader.org/"), - Server::new("https://collapse.ttfdk.lol/cdn/"), - Server::new( - "https://axkanxneklh7.objectstorage.eu-amsterdam-1.oci.customer-oci.com/n/axkanxneklh7/b/collapse/o/", - ), - ]; - pub static ref AUTH_SERVERS: Vec = vec![if *USE_LOCAL_SERVER { - Server::new("http://localhost:8000/") +pub static AUTH_SERVERS: LazyLock> = LazyLock::new(|| { + if *USE_LOCAL_SERVER { + vec![ + Server::new("http://localhost:8000/"), + Server::new("https://collapse.ttfdk.lol/auth/"), + ] } else { - Server::new("https://auth.collapseloader.org/") - }, Server::new("https://collapse.ttfdk.lol/auth/")]; + vec![ + Server::new("https://auth.collapseloader.org/"), + Server::new("https://collapse.ttfdk.lol/auth/"), + ] + } +}); - pub static ref ROOT_DIR: String = { - let roaming_dir = std::env::var("APPDATA").unwrap_or_else(|_| { - // fallback for non-windows systems (aka linux/mac) - std::env::var("HOME").unwrap_or_else(|_| ".".to_string()) - }); +pub static ROOT_DIR: LazyLock = LazyLock::new(|| { + let roaming_dir = std::env::var("APPDATA").unwrap_or_else(|_| { + // fallback for non-windows systems (aka linux/mac) + std::env::var("HOME").unwrap_or_else(|_| ".".to_string()) + }); - let override_file = PathBuf::from(&roaming_dir).join("CollapseLoaderRoot.txt"); - if let Ok(contents) = fs::read_to_string(&override_file) { - let override_path = contents.trim_matches(['\n', '\r', '"', '\'']).trim(); - if !override_path.is_empty() { - let path = PathBuf::from(override_path); - if path.exists() { - log_debug!("Using override path: {}", path.display()); - return path.to_string_lossy().to_string(); - } + let override_file = PathBuf::from(&roaming_dir).join("CollapseLoaderRoot.txt"); + if let Ok(contents) = fs::read_to_string(&override_file) { + let override_path = contents.trim_matches(['\n', '\r', '"', '\'']).trim(); + if !override_path.is_empty() { + let path = PathBuf::from(override_path); + if path.exists() { + log_debug!("Using override path: {}", path.display()); + return path.to_string_lossy().to_string(); } } + } - let collapse_dir = PathBuf::from(roaming_dir).join("CollapseLoader"); - collapse_dir.to_string_lossy().to_string() - }; -} + let collapse_dir = PathBuf::from(roaming_dir).join("CollapseLoader"); + collapse_dir.to_string_lossy().to_string() +}); diff --git a/src-tauri/src/core/utils/logging.rs b/src-tauri/src/core/utils/logging.rs index 9317d8e..af9b627 100644 --- a/src-tauri/src/core/utils/logging.rs +++ b/src-tauri/src/core/utils/logging.rs @@ -1,9 +1,8 @@ use chrono::Local; -use colored::*; -use lazy_static::lazy_static; +use colored::Colorize; use std::collections::VecDeque; use std::fmt; -use std::sync::Mutex; +use std::sync::{LazyLock, Mutex}; #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum LogLevel { @@ -15,17 +14,14 @@ pub enum LogLevel { pub struct Logger; -lazy_static! { - pub static ref APP_LOGS: Mutex> = Mutex::new(VecDeque::new()); - pub static ref LOG_LEVEL: Mutex = Mutex::new(LogLevel::Debug); -} +pub static APP_LOGS: LazyLock>> = + LazyLock::new(|| Mutex::new(VecDeque::new())); +pub static LOG_LEVEL: LazyLock> = LazyLock::new(|| Mutex::new(LogLevel::Debug)); -impl Logger { - pub fn new() -> Self { - Logger - } +const MAX_APP_LOGS: usize = 1000; - pub fn log_with_module(&self, level: LogLevel, tag: &str, message: &str) { +impl Logger { + pub fn log_with_module(level: LogLevel, tag: &str, message: &str) { let timestamp = Local::now().format("%H:%M:%S").to_string(); let short = tag @@ -52,21 +48,7 @@ impl Logger { let ts_colored = timestamp.dimmed(); let tag_colored = shorted_tag.white(); - fn emoji_for_module(tag: &str) -> Option<&'static str> { - if tag.contains("core.network") { - Some("\u{2601}") // ☁ - } else if tag.contains("core.clients") { - Some("\u{2609}") // ☉ - } else if tag.contains("core.storage") { - Some("\u{26C3}") // ⛃ - } else if tag.contains("core.utils") { - Some("\u{2692}") // ⚒ - } else { - None - } - } - - let emoji = emoji_for_module(tag) + let emoji = Self::emoji_for_module(tag) .map(|e| format!(" {e} |")) .unwrap_or_default(); @@ -80,7 +62,6 @@ impl Logger { ); let plain = format!("{timestamp} [{level_name}] [{short}] {message}"); - const MAX_APP_LOGS: usize = 1000; if let Ok(mut app_logs) = APP_LOGS.lock() { app_logs.push_back(plain); if app_logs.len() > MAX_APP_LOGS { @@ -88,19 +69,29 @@ impl Logger { } } } -} -lazy_static! { - pub static ref LOGGER: Logger = Logger::new(); + fn emoji_for_module(tag: &str) -> Option<&'static str> { + if tag.contains("core.network") { + Some("\u{2601}") // ☁ + } else if tag.contains("core.clients") { + Some("\u{2609}") // ☉ + } else if tag.contains("core.storage") { + Some("\u{26C3}") // ⛃ + } else if tag.contains("core.utils") { + Some("\u{2692}") // ⚒ + } else { + None + } + } } impl LogLevel { - pub fn as_str(&self) -> &'static str { + pub const fn as_str(self) -> &'static str { match self { - LogLevel::Info => "INFO", - LogLevel::Warn => "WARN", - LogLevel::Error => "ERROR", - LogLevel::Debug => "DEBUG", + Self::Info => "INFO", + Self::Warn => "WARN", + Self::Error => "ERROR", + Self::Debug => "DEBUG", } } } @@ -111,29 +102,12 @@ impl fmt::Display for LogLevel { } } -impl Logger { - #[allow(dead_code)] - pub fn set_level(&self, level: LogLevel) { - if let Ok(mut gl) = LOG_LEVEL.lock() { - *gl = level; - } - } - #[allow(dead_code)] - pub fn get_level(&self) -> LogLevel { - if let Ok(gl) = LOG_LEVEL.lock() { - *gl - } else { - LogLevel::Info - } - } -} - #[macro_export] macro_rules! log_info { ($($arg:tt)*) => { { let __clp_tag = $crate::collapse_tag!(); - $crate::core::utils::logging::LOGGER.log_with_module( + $crate::core::utils::logging::Logger::log_with_module( $crate::core::utils::logging::LogLevel::Info, &__clp_tag, &format!($($arg)*) @@ -147,7 +121,7 @@ macro_rules! log_warn { ($($arg:tt)*) => { { let __clp_tag = $crate::collapse_tag!(); - $crate::core::utils::logging::LOGGER.log_with_module( + $crate::core::utils::logging::Logger::log_with_module( $crate::core::utils::logging::LogLevel::Warn, &__clp_tag, &format!($($arg)*) @@ -161,7 +135,7 @@ macro_rules! log_error { ($($arg:tt)*) => { { let __clp_tag = $crate::collapse_tag!(); - $crate::core::utils::logging::LOGGER.log_with_module( + $crate::core::utils::logging::Logger::log_with_module( $crate::core::utils::logging::LogLevel::Error, &__clp_tag, &format!($($arg)*) @@ -175,7 +149,7 @@ macro_rules! log_debug { ($($arg:tt)*) => { { let __clp_tag = $crate::collapse_tag!(); - $crate::core::utils::logging::LOGGER.log_with_module( + $crate::core::utils::logging::Logger::log_with_module( $crate::core::utils::logging::LogLevel::Debug, &__clp_tag, &format!($($arg)*) diff --git a/src-tauri/src/core/utils/tags.rs b/src-tauri/src/core/utils/tags.rs index 006c6ad..8d9bef6 100644 --- a/src-tauri/src/core/utils/tags.rs +++ b/src-tauri/src/core/utils/tags.rs @@ -1,9 +1,8 @@ -use once_cell::sync::Lazy; use std::collections::HashMap; use std::sync::Mutex; -static TAG_CACHE: Lazy>> = - Lazy::new(|| Mutex::new(HashMap::new())); +static TAG_CACHE: std::sync::LazyLock>> = + std::sync::LazyLock::new(|| Mutex::new(HashMap::new())); fn make_tag_from_module_path(module_path: &str) -> String { let cleaned = module_path.replace("::", "."); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index d7bc8d8..a477c8d 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,13 +1,77 @@ use tauri::Manager; -use crate::core::utils::globals::CODENAME; +use crate::core::{ + error::StartupError, platform::check_platform_dependencies, utils::globals::CODENAME, +}; + +#[cfg(target_os = "linux")] +use crate::core::platform::check_webkit_environment; use self::core::network::analytics::Analytics; +pub use crate::core::utils::logging; + +pub mod commands; +pub mod core; + +pub fn check_dependencies() -> Result<(), StartupError> { + log_info!("Checking platform dependencies..."); + check_platform_dependencies() +} + +#[cfg(target_os = "linux")] +pub fn check_webkit_warning() -> Result<(), StartupError> { + log_info!("Checking WebKit environment variables..."); + check_webkit_environment() +} + +#[cfg(target_os = "windows")] +pub fn handle_startup_error(error: &StartupError) { + use native_dialog::DialogBuilder; + + if let StartupError::WebView2NotInstalled = error { + use native_dialog::MessageLevel; -mod commands; -mod core; + let should_install = DialogBuilder::message() + .set_level(MessageLevel::Info) + .set_title("WebView2 Not Installed") + .set_text("WebView2 is not installed. Would you like to download and install it now?") + .confirm() + .show() + .unwrap_or(false); + + if should_install { + if let Err(install_error) = crate::core::platform::windows::attempt_install_webview2() { + install_error.show_and_exit(); + } else { + let message = "WebView2 has been installed. Please restart the application."; + eprintln!("{message}"); + DialogBuilder::message() + .set_text(message) + .set_title("Restart Required"); + + std::process::exit(0); + } + } else { + error.show_and_exit(); + } + } else { + error.show_and_exit(); + } +} + +#[cfg(not(target_os = "windows"))] +pub fn handle_startup_error(error: &StartupError) { + #[cfg(target_os = "linux")] + if let StartupError::LinuxWebKitWarning = error { + error.show_warning(); + return; + } + + error.show_and_exit(); +} #[cfg_attr(mobile, tauri::mobile_entry_point)] +#[allow(clippy::large_stack_frames)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_notification::init()) @@ -76,7 +140,6 @@ pub fn run() { commands::utils::change_data_folder, commands::utils::decode_base64, commands::utils::encode_base64, - commands::analytics::send_client_analytics, commands::discord_rpc::update_presence, commands::updater::check_for_updates, commands::updater::download_and_install_update, @@ -104,12 +167,14 @@ pub fn run() { if is_dev { format!("(development build, {git_hash}, {git_branch} branch)") } else { - "".to_string() + String::new() } ); if let Some(window) = app_handle.get_webview_window("main") { - let _ = window.set_title(&window_title); + if let Err(e) = window.set_title(&window_title) { + log_warn!("Failed to set window title: {}", e); + } } crate::log_info!("Starting CollapseLoader: {}", window_title); @@ -120,6 +185,7 @@ pub fn run() { }) .on_window_event(|_window, event| { if let tauri::WindowEvent::CloseRequested { .. } = event { + log_info!("Window close requested. Shutting down Discord RPC."); core::utils::discord_rpc::shutdown(); } }) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 814049b..01d9ac5 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,80 +1,18 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -#[cfg(target_os = "windows")] -use win_msgbox::Okay; - -pub fn check_webview2() -> Result { - #[cfg(target_os = "windows")] - { - use winreg::{enums::HKEY_CURRENT_USER, enums::HKEY_LOCAL_MACHINE, RegKey}; - - let is_webview2_installed = RegKey::predef(HKEY_LOCAL_MACHINE) - .open_subkey("SOFTWARE\\WOW6432Node\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}") - .is_ok() - || RegKey::predef(HKEY_LOCAL_MACHINE) - .open_subkey("SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}") - .is_ok() - || RegKey::predef(HKEY_CURRENT_USER) - .open_subkey("SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}") - .is_ok(); - - if !is_webview2_installed { - eprintln!("WebView2 is not installed. Attempting to download and install it..."); - - let installer = - reqwest::blocking::get("https://go.microsoft.com/fwlink/p/?LinkId=2124703") - .map_err(|e| e.to_string())? - .bytes() - .map_err(|e| e.to_string())?; - - let path = std::env::temp_dir().join("MicrosoftEdgeWebview2Setup.exe"); - - if std::fs::write(&path, installer).is_ok() { - let mut command = std::process::Command::new(&path); - command.arg("/silent"); - command.arg("/install"); - - match command.output() { - Ok(_) => { - eprintln!("WebView2 installer executed successfully."); - if let Err(msgbox_err) = win_msgbox::show::( - "WebView2 has been installed successfully. Please restart the application.", - ) { - eprintln!("Failed to display restart message box: {msgbox_err:?}"); - } - Ok(true) - } - Err(e) => { - eprintln!("Failed to execute WebView2 installer: {e:?}"); - Ok(false) - } - } - } else { - eprintln!("Failed to write WebView2 installer to temporary directory."); - Ok(false) - } - } else { - Ok(true) - } - } - - #[cfg(not(target_os = "windows"))] - { - Ok(true) - } -} - fn main() { let _ = dotenvy::dotenv(); + collapseloader_lib::log_info!("Application starting..."); + + if let Err(e) = collapseloader_lib::check_dependencies() { + collapseloader_lib::log_error!("Dependency check failed: {}", e); + collapseloader_lib::handle_startup_error(&e); + } - #[cfg(target_os = "windows")] - if let Err(e) = check_webview2() { - eprintln!("WebView2 check failed: {e}"); - if let Err(msgbox_err) = win_msgbox::show::(&format!("Error checking WebView2: {e}")) - { - eprintln!("Failed to display error message box: {msgbox_err:?}"); - } + #[cfg(target_os = "linux")] + if let Err(e) = collapseloader_lib::check_webkit_warning() { + collapseloader_lib::handle_startup_error(&e); } collapseloader_lib::run() diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index d017304..e3fe842 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "collapseloader", - "version": "0.2.0", + "version": "0.2.1", "identifier": "org.collapseloader", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/components/admin/UserEditModal.vue b/src/components/admin/UserEditModal.vue index 4868dbb..c2a417e 100644 --- a/src/components/admin/UserEditModal.vue +++ b/src/components/admin/UserEditModal.vue @@ -35,6 +35,7 @@