4 Commits

7 changed files with 375 additions and 18 deletions

View File

@@ -7,18 +7,17 @@
"@better-fetch/fetch": "^1.1.18", "@better-fetch/fetch": "^1.1.18",
"@fontsource/sora": "^5.2.6", "@fontsource/sora": "^5.2.6",
"@fontsource/space-mono": "^5.2.8", "@fontsource/space-mono": "^5.2.8",
"@iarna/toml": "2.2.5",
"@number-flow/svelte": "^0.3.9", "@number-flow/svelte": "^0.3.9",
"@tailwindcss/typography": "0.5.16", "@tailwindcss/typography": "0.5.16",
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.11",
"@tauri-apps/api": "2.6.0", "@tauri-apps/api": "2.6.0",
"@tauri-apps/plugin-dialog": "2.3.1", "@tauri-apps/plugin-dialog": "2.3.2",
"@tauri-apps/plugin-fs": "2.4.1", "@tauri-apps/plugin-fs": "2.4.1",
"@tauri-apps/plugin-shell": "2.3.0", "@tauri-apps/plugin-shell": "2.3.0",
"animejs": "^4.0.2", "animejs": "^4.0.2",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"lucide-svelte": "0.525.0", "lucide-svelte": "^0.535.0",
"semver": "^7.7.2", "semver": "^7.7.2",
"svelte-confetti": "^2.0.0", "svelte-confetti": "^2.0.0",
"tw-animate-css": "^1.3.0", "tw-animate-css": "^1.3.0",
@@ -26,7 +25,8 @@
"devDependencies": { "devDependencies": {
"@eslint/compat": "^1.2.5", "@eslint/compat": "^1.2.5",
"@eslint/js": "^9.18.0", "@eslint/js": "^9.18.0",
"@lucide/svelte": "^0.525.0", "@iarna/toml": "2.2.5",
"@lucide/svelte": "^0.535.0",
"@sveltejs/adapter-static": "3.0.8", "@sveltejs/adapter-static": "3.0.8",
"@sveltejs/kit": "2.22.2", "@sveltejs/kit": "2.22.2",
"@sveltejs/vite-plugin-svelte": "^6.1.0", "@sveltejs/vite-plugin-svelte": "^6.1.0",
@@ -171,7 +171,7 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
"@lucide/svelte": ["@lucide/svelte@0.525.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-dyUxkXzepagLUzL8jHQNdeH286nC66ClLACsg+Neu/bjkRJWPWMzkT+H0DKlE70QdkicGCfs1ZGmXCc351hmZA=="], "@lucide/svelte": ["@lucide/svelte@0.535.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-LSVs0G+IXSHHxMl/U6bHTnDP/pbmwpS7/mkCDXmWD9Wi0oQlZihKFoFLjDFhC+6mdfRE6ZgBasXTusvrOYv0lA=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
@@ -293,7 +293,7 @@
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.6.1", "", { "os": "win32", "cpu": "x64" }, "sha512-fBsjPqIIHaaQt7tnjIGmPHu5p/BNBVD4JfOhO3QqIVBzAb+W2bDyIQPdoDMI943soLr/+N10xeTiPu+3L74+dQ=="], "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.6.1", "", { "os": "win32", "cpu": "x64" }, "sha512-fBsjPqIIHaaQt7tnjIGmPHu5p/BNBVD4JfOhO3QqIVBzAb+W2bDyIQPdoDMI943soLr/+N10xeTiPu+3L74+dQ=="],
"@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.3.1", "", { "dependencies": { "@tauri-apps/api": "^2.6.0" } }, "sha512-B7jvyhycV8SI/WHzPjciwtYfdFM6/9EXuMjRgYWZwn8GPDmHxpT80aJdb/eDVN+NgoAFDh9bu4QPonYahoYnZQ=="], "@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.3.2", "", { "dependencies": { "@tauri-apps/api": "^2.6.0" } }, "sha512-cNLo9YeQSC0MF4IgXnotHsqEgJk72MBZLXmQPrLA95qTaaWiiaFQ38hIMdZ6YbGUNkr3oni3EhU+AD5jLHcdUA=="],
"@tauri-apps/plugin-fs": ["@tauri-apps/plugin-fs@2.4.1", "", { "dependencies": { "@tauri-apps/api": "^2.6.0" } }, "sha512-vJlKZVGF3UAFGoIEVT6Oq5L4HGDCD78WmA4uhzitToqYiBKWAvZR61M6zAyQzHqLs0ADemkE4RSy/5sCmZm6ZQ=="], "@tauri-apps/plugin-fs": ["@tauri-apps/plugin-fs@2.4.1", "", { "dependencies": { "@tauri-apps/api": "^2.6.0" } }, "sha512-vJlKZVGF3UAFGoIEVT6Oq5L4HGDCD78WmA4uhzitToqYiBKWAvZR61M6zAyQzHqLs0ADemkE4RSy/5sCmZm6ZQ=="],
@@ -555,7 +555,7 @@
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
"lucide-svelte": ["lucide-svelte@0.525.0", "", { "peerDependencies": { "svelte": "^3 || ^4 || ^5.0.0-next.42" } }, "sha512-kfuN6JcCqTfCz2B76aXnyGLAzEBRSYw5GaUspM5RNHQZS5aI5yaKu06fbaofOk8cDvUtY0AUm/zAix7aUX6Q3A=="], "lucide-svelte": ["lucide-svelte@0.535.0", "", { "peerDependencies": { "svelte": "^3 || ^4 || ^5.0.0-next.42" } }, "sha512-qajkcYp9F2ZaIc0bmiuGWdvOW1A5JminAStI/9A4hZSeQnPeGuCNuHH3kMRzASgi99O3Z38NvMK6/0Lv72yxdQ=="],
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],

View File

@@ -25,13 +25,13 @@
"@tailwindcss/typography": "0.5.16", "@tailwindcss/typography": "0.5.16",
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.11",
"@tauri-apps/api": "2.6.0", "@tauri-apps/api": "2.6.0",
"@tauri-apps/plugin-dialog": "2.3.1", "@tauri-apps/plugin-dialog": "2.3.2",
"@tauri-apps/plugin-fs": "2.4.1", "@tauri-apps/plugin-fs": "2.4.1",
"@tauri-apps/plugin-shell": "2.3.0", "@tauri-apps/plugin-shell": "2.3.0",
"animejs": "^4.0.2", "animejs": "^4.0.2",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"lucide-svelte": "0.525.0", "lucide-svelte": "^0.535.0",
"semver": "^7.7.2", "semver": "^7.7.2",
"tw-animate-css": "^1.3.0", "tw-animate-css": "^1.3.0",
"svelte-confetti": "^2.0.0" "svelte-confetti": "^2.0.0"
@@ -40,7 +40,7 @@
"@eslint/compat": "^1.2.5", "@eslint/compat": "^1.2.5",
"@eslint/js": "^9.18.0", "@eslint/js": "^9.18.0",
"@iarna/toml": "2.2.5", "@iarna/toml": "2.2.5",
"@lucide/svelte": "^0.525.0", "@lucide/svelte": "^0.535.0",
"@sveltejs/adapter-static": "3.0.8", "@sveltejs/adapter-static": "3.0.8",
"@sveltejs/kit": "2.22.2", "@sveltejs/kit": "2.22.2",
"@sveltejs/vite-plugin-svelte": "^6.1.0", "@sveltejs/vite-plugin-svelte": "^6.1.0",

View File

@@ -23,14 +23,14 @@ tauri-plugin-shell = "2.3.0"
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.141" serde_json = "1.0.141"
serde_repr = "0.1.20" serde_repr = "0.1.20"
tauri-plugin-dialog = "2.3.1" tauri-plugin-dialog = "2.3.2"
tauri-plugin-fs = "2.4.1" tauri-plugin-fs = "2.4.1"
hardware-id = "0.3.0" hardware-id = "0.3.0"
tauri-plugin-cors-fetch = "4.1.0" tauri-plugin-cors-fetch = "4.1.0"
sysinfo = "0.36.1" sysinfo = "0.36.1"
reqwest = { version = "0.12.22", features = ["json", "stream"] } reqwest = { version = "0.12.22", features = ["json", "stream"] }
md5 = "0.8.0" md5 = "0.8.0"
tokio = { version = "1.46.1", features = ["full"] } tokio = { version = "1.47.0", features = ["full"] }
open = "5.3.2" open = "5.3.2"
windows-sys = "0.60.2" windows-sys = "0.60.2"
discord-rich-presence = "0.2.5" discord-rich-presence = "0.2.5"

343
src-tauri/nsis/dotnet.nsh Normal file
View File

@@ -0,0 +1,343 @@
; A set of NSIS macros to check whether a dotnet core runtime is installed and, if not, offer to
; download and install it. Supports dotnet versions 3.1 and newer - latest tested version is 7.0.
;
; Inspired by & initially based on NsisDotNetChecker, which does the same thing for .NET framework
; https://github.com/alex-sitnikov/NsisDotNetChecker
!include "WordFunc.nsh"
!include "TextFunc.nsh"
!include "X64.nsh"
!ifndef DOTNETCORE_INCLUDED
!define DOTNETCORE_INCLUDED
; Check that a specific version of the dotnet core runtime is installed and, if not, attempts to
; install it
;
; \param Version The desired dotnet core runtime version as a 2 digit version. e.g. 3.1, 6.0, 7.0
!macro CheckDotNetCore Version
; Save registers
Push $R0
Push $R1
Push $R2
; Push and pop parameters so we don't have conflicts if parameters are $R#
Push ${Version}
Pop $R0 ; Version
!define ID ${__LINE__}
; Check current installed version
!insertmacro DotNetCoreGetInstalledVersion $R0 $R1
; If $R1 is blank then there is no version installed, otherwise it is installed
; todo in future we might want to support "must be at least 6.0.7", for now we only deal with "yes/no" for a major version (e.g. 6.0)
StrCmp $R1 "" notinstalled_${ID}
DetailPrint "dotnet version $R1 already installed"
Goto end_${ID}
notinstalled_${ID}:
DetailPrint "dotnet $R0 is not installed"
!insertmacro DotNetCoreGetLatestVersion $R0 $R1
DetailPrint "Latest Version of $R0 is $R1"
; Get number of input digits
; ${WordFind} $R1 "." "#" $R2
; DetailPrint "version parts count is $R2"
; ${WordFind} $R1 "." "+1" $R2
; DetailPrint "version part 1 is $R2"
; ${WordFind} $R1 "." "+2" $R2
; DetailPrint "version part 2 is $R2"
; ${WordFind} $R1 "." "+3" $R2
; DetailPrint "version part 3 is $R2"
!insertmacro DotNetCoreInstallVersion $R1
end_${ID}:
!undef ID
; Restore registers
Pop $R2
Pop $R1
Pop $R0
!macroend
; Gets the latest version of the runtime for a specified dotnet version. This uses the same endpoint
; as the dotnet-install scripts to determine the latest full version of a dotnet version
;
; \param[in] Version The desired dotnet core runtime version as a 2 digit version. e.g. 3.1, 6.0, 7.0
; \param[out] Result The full version number of the latest version - e.g. 6.0.7
!macro DotNetCoreGetLatestVersion Version Result
; Save registers
Push $R0
Push $R1
Push $R2
; Push and pop parameters so we don't have conflicts if parameters are $R#
Push ${Version}
Pop $R0 ; Version
StrCpy $R1 https://dotnetcli.azureedge.net/dotnet/WindowsDesktop/$R0/latest.version
DetailPrint "Querying latest version of dotnet $R0 from $R1"
; Fetch latest version of the desired dotnet version
; todo error handling in the PS script? so we can check for errors here
StrCpy $R2 "Write-Host (Invoke-WebRequest -UseBasicParsing -URI $\"$R1$\").Content;"
!insertmacro DotNetCorePSExec $R2 $R2
; $R2 contains latest version, e.g. 6.0.7
; todo error handling here
; Push the result onto the stack
${TrimNewLines} $R2 $R2
Push $R2
; Restore registers
Exch
Pop $R2
Exch
Pop $R1
Exch
Pop $R0
; Set result
Pop ${Result}
!macroend
!macro DotNetCoreGetInstalledVersion Version Result
!define DNC_INS_ID ${__LINE__}
; Save registers
Push $R0
Push $R1
Push $R2
; Push and pop parameters so we don't have conflicts if parameters are $R#
Push ${Version}
Pop $R0 ; Version
DetailPrint "Checking installed version of dotnet $R0"
StrCpy $R1 "dotnet --list-runtimes | % { if($$_ -match $\".*WindowsDesktop.*($R0.\d+).*$\") { $$matches[1] } } | Sort-Object {[int]($$_ -replace '\d.\d.(\d+)', '$$1')} -Descending | Select-Object -first 1"
!insertmacro DotNetCorePSExec $R1 $R1
; $R1 contains highest installed version, e.g. 6.0.7
${TrimNewLines} $R1 $R1
; If there is an installed version it should start with the same two "words" as the input version,
; otherwise assume we got an error response
; todo improve this simple test which checks there are at least 3 "words" separated by periods
${WordFind} $R1 "." "E#" $R2
IfErrors error_${DNC_INS_ID}
; $R2 contains number of version parts in R1 (dot separated words = version parts)
; If less than 3 parts, or more than 4 parts, error
IntCmp $R2 3 0 error_${DNC_INS_ID}
IntCmp $R2 4 0 0 error_${DNC_INS_ID}
; todo more error handling here / validation
; Seems to be OK, skip the "set to blank string" error handler
Goto end_${DNC_INS_ID}
error_${DNC_INS_ID}:
StrCpy $R1 "" ; Set result to blank string if any error occurs (means not installed)
end_${DNC_INS_ID}:
!undef DNC_INS_ID
; Push the result onto the stack
Push $R1
; Restore registers
Exch
Pop $R2
Exch
Pop $R1
Exch
Pop $R0
; Set result
Pop ${Result}
!macroend
!macro DotNetCoreInstallVersion Version
; Save registers
Push $R0
Push $R1
Push $R2
Push $R3
; Push and pop parameters so we don't have conflicts if parameters are $R#
Push ${Version}
Pop $R0 ; Version
${If} ${IsNativeAMD64}
StrCpy $R3 "x64"
${ElseIf} ${IsNativeARM64}
StrCpy $R3 "arm64"
${ElseIf} ${IsNativeIA32}
StrCpy $R3 "x86"
${Else}
StrCpy $R3 "unknown"
${EndIf}
; todo can download as a .zip, which is smaller, then we'd need to unzip it before running it...
StrCpy $R1 https://dotnetcli.azureedge.net/dotnet/WindowsDesktop/$R0/windowsdesktop-runtime-$R0-win-$R3.exe
; For dotnet versions less than 5 the WindowsDesktop runtime has a different path
${WordFind} $R0 "." "+1" $R2
IntCmp $R2 5 +2 0 +2
StrCpy $R1 https://dotnetcli.azureedge.net/dotnet/Runtime/$R0/windowsdesktop-runtime-$R0-win-$R3.exe
DetailPrint "Downloading dotnet $R0 from $R1"
; Create destination file
GetTempFileName $R2
nsExec::Exec 'cmd.exe /c rename "$R2" "$R2.exe"' ; Not using Rename to avoid spam in details log
Pop $R3 ; Pop exit code
StrCpy $R2 "$R2.exe"
; Fetch runtime installer
; todo error handling in the PS script? so we can check for errors here
StrCpy $R1 "Invoke-WebRequest -UseBasicParsing -URI $\"$R1$\" -OutFile $\"$R2$\""
!insertmacro DotNetCorePSExec $R1 $R1
; $R1 contains powershell script result
${WordFind} $R1 "BlobNotFound" "E+1{" $R3
ifErrors +3 0
DetailPrint "Dotnet installer $R0 not found."
Goto +10
; todo error handling for PS result, verify download result
IfFileExists $R2 +3, 0
DetailPrint "Dotnet installer did not download."
Goto +7
DetailPrint "Download complete"
DetailPrint "Installing dotnet $R0"
ExecWait "$\"$R2$\" /install /quiet /norestart" $R1
DetailPrint "Installer completed (Result: $R1)"
nsExec::Exec 'cmd.exe /c del "$R2"' ; Not using Delete to avoid spam in details log
Pop $R3 ; Pop exit code
; Error checking? Verify install result?
; Restore registers
Pop $R3
Pop $R2
Pop $R1
Pop $R0
!macroend
; below is adapted from https://nsis.sourceforge.io/PowerShell_support but avoids using the plugin
; directory in favour of a temp file and providing a return variable rather than returning on the
; stack. Methods renamed to avoid conflicting with use of the original macros
; DotNetCorePSExec
; Executes a powershell script
;
; \param[in] PSCommand The powershell command or script to execute
; \param[out] Result The output from the powershell script
!macro DotNetCorePSExec PSCommand Result
Push ${PSCommand}
Call DotNetCorePSExecFn
Pop ${Result}
!macroend
; DotNetCorePSExecFile
; Executes a powershell file
;
; \param[in] FilePath The path to the powershell script file to execute
; \param[out] Result The output from the powershell script
!macro DotNetCorePSExecFile FilePath Result
Push ${FilePath}
Call DotNetCorePSExecFileFn
Pop ${Result}
!macroend
Function DotNetCorePSExecFn
; Read parameters and save registers
Exch $R0 ; Script
Push $R1
Push $R2
; Write the command into a temp file
; Note: Using GetTempFileName to get a temp file name, but since we need to have a .ps1 extension
; on the end we rename it with an extra file extension
GetTempFileName $R1
nsExec::Exec 'cmd.exe /c rename "$R1" "$R1.ps1"' ; Not using Rename to avoid spam in details log
Pop $R2 ; Pop exit code
StrCpy $R1 "$R1.ps1"
FileOpen $R2 $R1 w
FileWrite $R2 $R0
FileClose $R2
; Execute the powershell script and delete the temp file
Push $R1
Call DotNetCorePSExecFileFn
nsExec::Exec 'cmd.exe /c del "$R1"' ; Not using Delete to avoid spam in details log
Pop $R0 ; Pop exit code
; Restore registers
Exch
Pop $R2
Exch
Pop $R1
Exch
Pop $R0
; Stack contains script output only, which we leave as the function result
FunctionEnd
Function DotNetCorePSExecFileFn
; Read parameters and save registers
Exch $R0 ; FilePath
Push $R1
nsExec::ExecToStack 'powershell -inputformat none -ExecutionPolicy RemoteSigned -File "$R0" '
; Stack contain exitCode, scriptOutput, registers
; Pop exit code & validate
Pop $R1
IntCmp $R1 0 +2
SetErrorLevel 2
; Restore registers
Exch
Pop $R1
Exch
Pop $R0
; Stack contains script output only, which we leave as the function result
FunctionEnd
!endif

View File

@@ -1,3 +1,9 @@
!incluide "dotnet.nsh"
!macro NSIS_HOOK_PREINSTALL
!insertmacro CheckDotNetCore 8.0
!macroend
!macro NSIS_HOOK_POSTINSTALL !macro NSIS_HOOK_POSTINSTALL
${If} $PassiveMode = 1 ${If} $PassiveMode = 1
${OrIf} ${Silent} ${OrIf} ${Silent}

View File

@@ -286,8 +286,18 @@ pub fn get_window_title_by_pid(pid: Pid) -> String {
} }
pub async fn is_net8_installed() -> bool { pub async fn is_net8_installed() -> bool {
use std::process::Command; use tokio::process::Command;
let output_result = Command::new("dotnet").arg("--list-runtimes").output();
let mut command = Command::new("dotnet");
command.arg("--list-runtimes");
#[cfg(windows)]
{
const CREATE_NO_WINDOW: u32 = 0x08000000;
command.creation_flags(CREATE_NO_WINDOW);
}
let output_result = command.output().await;
match output_result { match output_result {
Ok(output) => { Ok(output) => {
if !output.status.success() { if !output.status.success() {
@@ -300,9 +310,7 @@ pub async fn is_net8_installed() -> bool {
} }
let stdout_str = String::from_utf8_lossy(&output.stdout); let stdout_str = String::from_utf8_lossy(&output.stdout);
stdout_str stdout_str.lines().any(|line| line.starts_with("Microsoft.WindowsDesktop.App 8."))
.lines()
.any(|line| line.starts_with("Microsoft.WindowsDesktop.App 8."))
} }
Err(_) => false, Err(_) => false,
} }

View File

@@ -44,7 +44,7 @@
"nsis": { "nsis": {
"installMode": "currentUser", "installMode": "currentUser",
"compression": "lzma", "compression": "lzma",
"installerHooks": "./nsis-hooks.nsh" "installerHooks": "./nsis/nsis-hooks.nsh"
} }
}, },
"icon": [ "icon": [