diff --git a/.eslintrc.js b/.eslintrc.js index a574bc5c..ac327b68 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -46,6 +46,7 @@ module.exports = { "react/prop-types": "off", "import/no-cycle": "off", "prefer-promise-reject-errors": "off", + "react/jsx-no-target-blank": "off" }, parserOptions: { ecmaVersion: 2020, diff --git a/assets/jsons/translations/de.json b/assets/jsons/translations/de.json index f4347763..f8907647 100644 --- a/assets/jsons/translations/de.json +++ b/assets/jsons/translations/de.json @@ -482,22 +482,29 @@ } }, "steam-login": { - "title": "Steam Login", + "title": "Steam Anmeldung", "inputs": { "username": { - "label": "Nutzername", - "placeholder": "Gebe deinen Nutzernamen ein" + "label": "Mit Accountnamen anmelden", + "placeholder": "Geben Sie Ihren Accountnamen ein" }, "password": { "label": "Passwort", - "placeholder": "Gebe dein Passwort ein", - "max-length-warning": "Das Passwort überschreitet 64 Zeichen! Wenn das Passwort ungültig ist, versuchen Sie nur die ersten 64 Zeichen einzugeben." + "placeholder": "Geben Sie Ihr Passwort ein", + "max-length-warning": "Das Passwort überschreitet 64 Zeichen! Wenn das Passwort ungültig ist, versuchen Sie nur seine ersten 64 Zeichen einzugeben." }, - "stay": { - "label": "Verbunden bleiben" - } + "qr":{ + "label": "Oder mit einem QR-Code", + "note":{ + "use-the":"Anmelden Sie sich über die ", + "steam-mobile-app": "Steam-Mobile-App", + "to-connect-with-qr": " per QR-Code an." + } + }, + "stay": "Angemeldet bleiben" }, - "why-credentials": "Warum braucht ihr meine Anmeldeinformationen?", + "why-credentials": "Warum sind meine Anmeldedaten notwendig?", + "need-help-to-connect": "Ich brauche Hilfe, um auf mein Konto zuzugreifen!", "buttons": { "submit": "Anmelden" } diff --git a/assets/jsons/translations/en.json b/assets/jsons/translations/en.json index 668249a9..3c296b47 100644 --- a/assets/jsons/translations/en.json +++ b/assets/jsons/translations/en.json @@ -485,21 +485,28 @@ "title": "Steam Login", "inputs": { "username": { - "label": "Username", - "placeholder": "Enter your username" + "label": "Sign in with account name", + "placeholder": "Enter your account name" }, "password": { "label": "Password", "placeholder": "Enter your password", - "max-length-warning": "The password exceeds 64 characters! If the password is invalid, try to put its first 64 characters." + "max-length-warning": "The password exceeds 64 characters! If the password is invalid, try to enter only its first 64 characters." }, - "stay": { - "label": "Stay connected" - } + "qr":{ + "label": "Or with a QR code", + "note":{ + "use-the":"Use the ", + "steam-mobile-app": "Steam Mobile App", + "to-connect-with-qr": "to sign in with via QR code." + } + }, + "stay": "Remember me" }, - "why-credentials": "Why do you need my credentials?", + "why-credentials": "Why are my credentials necessary?", + "need-help-to-connect": "I need help to access my account!", "buttons": { - "submit": "Login" + "submit": "Sign in" } }, "steam-credentials": { diff --git a/assets/jsons/translations/es.json b/assets/jsons/translations/es.json index 7aadc3c6..6392ff57 100644 --- a/assets/jsons/translations/es.json +++ b/assets/jsons/translations/es.json @@ -481,24 +481,31 @@ } }, "steam-login": { - "title": "Steam Conectar", + "title": "Inicio de sesión Steam", "inputs": { "username": { - "label": "Nombre de usuario", - "placeholder": "Introduzca su nombre de usuario" + "label": "Iniciar sesión con un nombre de cuenta", + "placeholder": "Introduce tu nombre de cuenta" }, "password": { "label": "Contraseña", - "placeholder": "Introduzca su contraseña", + "placeholder": "Introduce tu contraseña", "max-length-warning": "¡La contraseña supera los 64 caracteres! Si la contraseña es inválida, intenta ingresar solo sus primeros 64 caracteres." }, - "stay": { - "label": "Manténgase conectado" - } + "qr":{ + "label": "O con un código QR", + "note":{ + "use-the":"Usa la ", + "steam-mobile-app": "aplicación Steam Mobile", + "to-connect-with-qr": "para conectarte con un código QR." + } + }, + "stay": "Recuérdame" }, - "why-credentials": "¿Por qué necesitas mis credenciales?", + "why-credentials": "¿Por qué son necesarias mis credenciales?", + "need-help-to-connect": "¡Necesito ayuda para acceder a mi cuenta!", "buttons": { - "submit": "Inicio de sesión" + "submit": "Iniciar sesión" } }, "steam-credentials": { diff --git a/assets/jsons/translations/fr.json b/assets/jsons/translations/fr.json index 7f6c8e27..23ac8c5f 100644 --- a/assets/jsons/translations/fr.json +++ b/assets/jsons/translations/fr.json @@ -484,19 +484,26 @@ "title": "Connexion Steam", "inputs": { "username": { - "label": "Nom d'utilisateur", - "placeholder": "Entre ton nom d'utilisateur" + "label": "Se connecter avec un nom de compte", + "placeholder": "Entre ton nom de compte" }, "password": { "label": "Mot de passe", "placeholder": "Entre ton mot de passe", "max-length-warning": "Le mot de passe dépasse 64 caractères ! Si le mot de passe est invalide, tente d'entrer seulement ses 64 premiers caractères." }, - "stay": { - "label": "Rester connecté" - } + "qr":{ + "label": "Ou avec un QR code", + "note":{ + "use-the":"Utilisez l'", + "steam-mobile-app": "application mobile Steam", + "to-connect-with-qr": "pour vous connecter avec un QR code." + } + }, + "stay": "Se souvenir de moi" }, "why-credentials": "Pourquoi mes identifiants sont nécessaires ?", + "need-help-to-connect": "J'ai besoin d'aide pour accéder à mon compte !", "buttons": { "submit": "Se connecter" } diff --git a/assets/jsons/translations/ru.json b/assets/jsons/translations/ru.json index 3ac69574..cd0e3ecc 100644 --- a/assets/jsons/translations/ru.json +++ b/assets/jsons/translations/ru.json @@ -485,19 +485,26 @@ "title": "Вход в Steam", "inputs": { "username": { - "label": "Имя пользователя", - "placeholder": "Введите имя пользователя" + "label": "Войти с именем пользователя", + "placeholder": "Введите ваше имя пользователя" }, "password": { "label": "Пароль", - "placeholder": "Введите пароль", - "max-length-warning": "Пароль больше 64-х символов! Если пароль неверен, введите первые 64 символа." + "placeholder": "Введите ваш пароль", + "max-length-warning": "Пароль превышает 64 символа! Если пароль недействителен, попробуйте ввести только его первые 64 символа." }, - "stay": { - "label": "Оставаться в аккаунте" - } + "qr":{ + "label": "Или с QR-кодом", + "note":{ + "use-the":"Используйте ", + "steam-mobile-app": "мобильное приложение Steam", + "to-connect-with-qr": "для входа с QR-кодом." + } + }, + "stay": "Запомнить меня" }, - "why-credentials": "Почему нужны мои данные для входа?", + "why-credentials": "Зачем мне нужны учетные данные?", + "need-help-to-connect": "Мне нужна помощь, чтобы войти в свой аккаунт!", "buttons": { "submit": "Войти" } diff --git a/assets/scripts/depot-downloader/DepotDownloader.deps.json b/assets/scripts/depot-downloader/DepotDownloader.deps.json index 027c54c1..3b286c60 100644 --- a/assets/scripts/depot-downloader/DepotDownloader.deps.json +++ b/assets/scripts/depot-downloader/DepotDownloader.deps.json @@ -1,121 +1,166 @@ { - "runtimeTarget": { - "name": ".NETCoreApp,Version=v6.0", - "signature": "" - }, - "compilationOptions": {}, - "targets": { - ".NETCoreApp,Version=v6.0": { - "DepotDownloader/2.4.6": { - "dependencies": { - "SteamKit2": "2.4.1", - "protobuf-net": "3.0.101" - }, - "runtime": { - "DepotDownloader.dll": {} - } - }, - "Microsoft.NETCore.Platforms/5.0.0": {}, - "Microsoft.Win32.Registry/5.0.0": { - "dependencies": { - "System.Security.AccessControl": "5.0.0", - "System.Security.Principal.Windows": "5.0.0" - } - }, - "protobuf-net/3.0.101": { - "dependencies": { - "protobuf-net.Core": "3.0.101" - }, - "runtime": { - "lib/net5.0/protobuf-net.dll": { - "assemblyVersion": "3.0.0.0", - "fileVersion": "3.0.101.59408" - } - } - }, - "protobuf-net.Core/3.0.101": { - "runtime": { - "lib/net5.0/protobuf-net.Core.dll": { - "assemblyVersion": "3.0.0.0", - "fileVersion": "3.0.101.59408" - } - } - }, - "SteamKit2/2.4.1": { - "dependencies": { - "Microsoft.Win32.Registry": "5.0.0", - "protobuf-net": "3.0.101" - }, - "runtime": { - "lib/net6.0/SteamKit2.dll": { - "assemblyVersion": "2.4.1.0", - "fileVersion": "2.4.1.10" - } - } - }, - "System.Security.AccessControl/5.0.0": { - "dependencies": { - "Microsoft.NETCore.Platforms": "5.0.0", - "System.Security.Principal.Windows": "5.0.0" - } - }, - "System.Security.Principal.Windows/5.0.0": {} + "runtimeTarget": { + "name": ".NETCoreApp,Version=v6.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v6.0": { + "DepotDownloader/2.5.0": { + "dependencies": { + "QRCoder": "1.4.3", + "SteamKit2": "2.5.0-Beta.1", + "protobuf-net": "3.2.16" + }, + "runtime": { + "DepotDownloader.dll": {} } - }, - "libraries": { - "DepotDownloader/2.4.6": { - "type": "project", - "serviceable": false, - "sha512": "" - }, - "Microsoft.NETCore.Platforms/5.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==", - "path": "microsoft.netcore.platforms/5.0.0", - "hashPath": "microsoft.netcore.platforms.5.0.0.nupkg.sha512" - }, - "Microsoft.Win32.Registry/5.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", - "path": "microsoft.win32.registry/5.0.0", - "hashPath": "microsoft.win32.registry.5.0.0.nupkg.sha512" - }, - "protobuf-net/3.0.101": { - "type": "package", - "serviceable": true, - "sha512": "sha512-mLm8VGfGylp9q+rbJDp3uw0BOeNzfGENecVz/EYGlzq3ss7tfajGXHCMoZm/GxJ9vSn9pcK22UXWZnQ5GJQMjA==", - "path": "protobuf-net/3.0.101", - "hashPath": "protobuf-net.3.0.101.nupkg.sha512" - }, - "protobuf-net.Core/3.0.101": { - "type": "package", - "serviceable": true, - "sha512": "sha512-gf9QIF0RlctfjmOjbE6eOyMulkNEsH7tHncjKj/4BTu9tkuTyrH3fv5I+35GnolJqKJ6fh9kYaezvNRL3t75+Q==", - "path": "protobuf-net.core/3.0.101", - "hashPath": "protobuf-net.core.3.0.101.nupkg.sha512" - }, - "SteamKit2/2.4.1": { - "type": "package", - "serviceable": true, - "sha512": "sha512-t9yptysn/2YY6KZaI123vOOnumS+ZCFcC6MhFEkTiQ04mdpZ/EWhk5RVY7jU36IQHyTQUXotmn7taKFCXMKp4g==", - "path": "steamkit2/2.4.1", - "hashPath": "steamkit2.2.4.1.nupkg.sha512" - }, - "System.Security.AccessControl/5.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==", - "path": "system.security.accesscontrol/5.0.0", - "hashPath": "system.security.accesscontrol.5.0.0.nupkg.sha512" - }, - "System.Security.Principal.Windows/5.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==", - "path": "system.security.principal.windows/5.0.0", - "hashPath": "system.security.principal.windows.5.0.0.nupkg.sha512" + }, + "Microsoft.NETCore.Platforms/5.0.0": {}, + "Microsoft.Win32.Registry/5.0.0": { + "dependencies": { + "System.Security.AccessControl": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" } + }, + "protobuf-net/3.2.16": { + "dependencies": { + "protobuf-net.Core": "3.2.16" + }, + "runtime": { + "lib/net6.0/protobuf-net.dll": { + "assemblyVersion": "3.0.0.0", + "fileVersion": "3.2.16.54102" + } + } + }, + "protobuf-net.Core/3.2.16": { + "dependencies": { + "System.Collections.Immutable": "7.0.0" + }, + "runtime": { + "lib/net6.0/protobuf-net.Core.dll": { + "assemblyVersion": "3.0.0.0", + "fileVersion": "3.2.16.54102" + } + } + }, + "QRCoder/1.4.3": { + "runtime": { + "lib/net6.0/QRCoder.dll": { + "assemblyVersion": "1.4.3.0", + "fileVersion": "1.4.3.0" + } + } + }, + "SteamKit2/2.5.0-Beta.1": { + "dependencies": { + "Microsoft.Win32.Registry": "5.0.0", + "protobuf-net": "3.2.16" + }, + "runtime": { + "lib/net6.0/SteamKit2.dll": { + "assemblyVersion": "2.5.0.0", + "fileVersion": "2.5.0.0" + } + } + }, + "System.Collections.Immutable/7.0.0": { + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + }, + "runtime": { + "lib/net6.0/System.Collections.Immutable.dll": { + "assemblyVersion": "7.0.0.0", + "fileVersion": "7.0.22.51805" + } + } + }, + "System.Runtime.CompilerServices.Unsafe/6.0.0": {}, + "System.Security.AccessControl/5.0.0": { + "dependencies": { + "Microsoft.NETCore.Platforms": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" + } + }, + "System.Security.Principal.Windows/5.0.0": {} } -} + }, + "libraries": { + "DepotDownloader/2.5.0": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "Microsoft.NETCore.Platforms/5.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==", + "path": "microsoft.netcore.platforms/5.0.0", + "hashPath": "microsoft.netcore.platforms.5.0.0.nupkg.sha512" + }, + "Microsoft.Win32.Registry/5.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", + "path": "microsoft.win32.registry/5.0.0", + "hashPath": "microsoft.win32.registry.5.0.0.nupkg.sha512" + }, + "protobuf-net/3.2.16": { + "type": "package", + "serviceable": true, + "sha512": "sha512-W6RM2B2f5O7SigBGfkXFYzI/a7Mks+rCHtuGlE/b5O3NMupDon7I0akjCLhrfcnqIfdWDkDafseav/MnIzUSHA==", + "path": "protobuf-net/3.2.16", + "hashPath": "protobuf-net.3.2.16.nupkg.sha512" + }, + "protobuf-net.Core/3.2.16": { + "type": "package", + "serviceable": true, + "sha512": "sha512-ZFoY0lfMUWGJ4RoyI0oNj2o2l8t08n3k27YyDD0kYWQ+vXj0hTbWEwYWOpGAfCWvyUPkq9Jcc/4uR74GICZL2g==", + "path": "protobuf-net.core/3.2.16", + "hashPath": "protobuf-net.core.3.2.16.nupkg.sha512" + }, + "QRCoder/1.4.3": { + "type": "package", + "serviceable": true, + "sha512": "sha512-fWuFqjm8GTlEb2GqBl3Hi8HZZeZQwBSHxvRPtPjyNbT82H0ff0JwavKRBmMaXCno1Av6McPC8aJzri0Mj2w9Jw==", + "path": "qrcoder/1.4.3", + "hashPath": "qrcoder.1.4.3.nupkg.sha512" + }, + "SteamKit2/2.5.0-Beta.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-fwwdF0cZZEIdNpcUc1+B9zbP5Q48A3DFOAclPhBh2dMTU03aROaIDktFW+Ww/NN1Z/Emp/LwVqHB+hCM8uxS4Q==", + "path": "steamkit2/2.5.0-beta.1", + "hashPath": "steamkit2.2.5.0-beta.1.nupkg.sha512" + }, + "System.Collections.Immutable/7.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-dQPcs0U1IKnBdRDBkrCTi1FoajSTBzLcVTpjO4MBCMC7f4pDOIPzgBoX8JjG7X6uZRJ8EBxsi8+DR1JuwjnzOQ==", + "path": "system.collections.immutable/7.0.0", + "hashPath": "system.collections.immutable.7.0.0.nupkg.sha512" + }, + "System.Runtime.CompilerServices.Unsafe/6.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==", + "path": "system.runtime.compilerservices.unsafe/6.0.0", + "hashPath": "system.runtime.compilerservices.unsafe.6.0.0.nupkg.sha512" + }, + "System.Security.AccessControl/5.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==", + "path": "system.security.accesscontrol/5.0.0", + "hashPath": "system.security.accesscontrol.5.0.0.nupkg.sha512" + }, + "System.Security.Principal.Windows/5.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==", + "path": "system.security.principal.windows/5.0.0", + "hashPath": "system.security.principal.windows.5.0.0.nupkg.sha512" + } + } +} \ No newline at end of file diff --git a/assets/scripts/depot-downloader/DepotDownloader.dll b/assets/scripts/depot-downloader/DepotDownloader.dll index 424edbbc..3c243b37 100644 Binary files a/assets/scripts/depot-downloader/DepotDownloader.dll and b/assets/scripts/depot-downloader/DepotDownloader.dll differ diff --git a/assets/scripts/depot-downloader/DepotDownloader.exe b/assets/scripts/depot-downloader/DepotDownloader.exe index 85080ee8..30eacd56 100644 Binary files a/assets/scripts/depot-downloader/DepotDownloader.exe and b/assets/scripts/depot-downloader/DepotDownloader.exe differ diff --git a/assets/scripts/depot-downloader/DepotDownloader.pdb b/assets/scripts/depot-downloader/DepotDownloader.pdb index 998d7e10..ac78d771 100644 Binary files a/assets/scripts/depot-downloader/DepotDownloader.pdb and b/assets/scripts/depot-downloader/DepotDownloader.pdb differ diff --git a/assets/scripts/depot-downloader/DepotDownloader.runtimeconfig.json b/assets/scripts/depot-downloader/DepotDownloader.runtimeconfig.json index 668dcab2..3c15255d 100644 --- a/assets/scripts/depot-downloader/DepotDownloader.runtimeconfig.json +++ b/assets/scripts/depot-downloader/DepotDownloader.runtimeconfig.json @@ -1,13 +1,13 @@ { - "runtimeOptions": { - "tfm": "net6.0", - "rollForward": "LatestMajor", - "framework": { - "name": "Microsoft.NETCore.App", - "version": "6.0.0" - }, - "configProperties": { - "System.Reflection.Metadata.MetadataUpdater.IsSupported": false - } + "runtimeOptions": { + "tfm": "net6.0", + "rollForward": "LatestMajor", + "framework": { + "name": "Microsoft.NETCore.App", + "version": "6.0.0" + }, + "configProperties": { + "System.Reflection.Metadata.MetadataUpdater.IsSupported": false } -} + } +} \ No newline at end of file diff --git a/assets/scripts/depot-downloader/QRCoder.dll b/assets/scripts/depot-downloader/QRCoder.dll new file mode 100644 index 00000000..a6fabe90 Binary files /dev/null and b/assets/scripts/depot-downloader/QRCoder.dll differ diff --git a/assets/scripts/depot-downloader/SteamKit2.dll b/assets/scripts/depot-downloader/SteamKit2.dll index 113781ac..c0384d50 100644 Binary files a/assets/scripts/depot-downloader/SteamKit2.dll and b/assets/scripts/depot-downloader/SteamKit2.dll differ diff --git a/assets/scripts/depot-downloader/System.Collections.Immutable.dll b/assets/scripts/depot-downloader/System.Collections.Immutable.dll new file mode 100644 index 00000000..86773fe8 Binary files /dev/null and b/assets/scripts/depot-downloader/System.Collections.Immutable.dll differ diff --git a/assets/scripts/depot-downloader/protobuf-net.Core.dll b/assets/scripts/depot-downloader/protobuf-net.Core.dll index 436c4f26..59ff491c 100644 Binary files a/assets/scripts/depot-downloader/protobuf-net.Core.dll and b/assets/scripts/depot-downloader/protobuf-net.Core.dll differ diff --git a/assets/scripts/depot-downloader/protobuf-net.dll b/assets/scripts/depot-downloader/protobuf-net.dll index c0c20884..361af4f4 100644 Binary files a/assets/scripts/depot-downloader/protobuf-net.dll and b/assets/scripts/depot-downloader/protobuf-net.dll differ diff --git a/assets/scripts/depot-downloader/ref/DepotDownloader.dll b/assets/scripts/depot-downloader/ref/DepotDownloader.dll deleted file mode 100644 index 34f90d0e..00000000 Binary files a/assets/scripts/depot-downloader/ref/DepotDownloader.dll and /dev/null differ diff --git a/package-lock.json b/package-lock.json index 0d5699a8..342e3873 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "node-fetch": "^2.6.7", "node-stream-zip": "^1.15.0", "ps-list": "^7.2.0", + "qrcode.react": "^3.1.0", "react": "^18.2.0", "react-colorful": "^5.6.1", "react-dom": "^18.2.0", @@ -15334,6 +15335,14 @@ "node": ">= 12" } }, + "node_modules/qrcode.react": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-3.1.0.tgz", + "integrity": "sha512-oyF+Urr3oAMUG/OiOuONL3HXM+53wvuH3mtIWQrYmsXoAq0DkvZp2RYUWFSMFtbdOpuS++9v+WAkzNVkMlNW6Q==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -30768,6 +30777,12 @@ } } }, + "qrcode.react": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-3.1.0.tgz", + "integrity": "sha512-oyF+Urr3oAMUG/OiOuONL3HXM+53wvuH3mtIWQrYmsXoAq0DkvZp2RYUWFSMFtbdOpuS++9v+WAkzNVkMlNW6Q==", + "requires": {} + }, "qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", diff --git a/package.json b/package.json index 6b7fe686..73c3a4a7 100644 --- a/package.json +++ b/package.json @@ -248,6 +248,7 @@ "node-fetch": "^2.6.7", "node-stream-zip": "^1.15.0", "ps-list": "^7.2.0", + "qrcode.react": "^3.1.0", "react": "^18.2.0", "react-colorful": "^5.6.1", "react-dom": "^18.2.0", diff --git a/src/main/ipcs/bs-download-ipcs.ts b/src/main/ipcs/bs-download-ipcs.ts index 04074058..e98db7ae 100644 --- a/src/main/ipcs/bs-download-ipcs.ts +++ b/src/main/ipcs/bs-download-ipcs.ts @@ -1,60 +1,18 @@ -import { ipcMain } from "electron"; -import { BSInstallerService, DownloadEventType, DownloadInfo } from "../services/bs-installer.service"; -import { IpcRequest } from "shared/models/ipc"; +import { BSInstallerService, DownloadInfo } from "../services/bs-installer.service"; import { InstallationLocationService } from "../services/installation-location.service"; -import { UtilsService } from "../services/utils.service"; -import { LocalMapsManagerService } from "../services/additional-content/local-maps-manager.service"; import { IpcService } from "../services/ipc.service"; -import { from } from "rxjs"; - -export interface InitDownloadInfoInterface { - cwd: string; - folder: string; - app: string; - depot: string; - manifest: string; - username: string; - stay: boolean; -} +import { from, of } from "rxjs"; const ipc = IpcService.getInstance(); -ipcMain.on("is-dotnet-6-installed", async (event, request: IpcRequest) => { +ipc.on("is-dotnet-6-installed", (_, reply) => { const installer = BSInstallerService.getInstance(); - const utils = UtilsService.getInstance(); - installer.isDotNet6Installed().then(installed => { - utils.ipcSend(request.responceChannel, { success: true, data: installed }); - }); + reply(from(installer.isDotNet6Installed())); }); -ipcMain.on("bs-download.start", async (event, request: IpcRequest) => { - BSInstallerService.getInstance() - .downloadBsVersion(request.args) - .then(async res => { - await LocalMapsManagerService.getInstance() - .linkVersionMaps(request.args.bsVersion, true) - .catch(e => {}); - UtilsService.getInstance().ipcSend(request.responceChannel, { success: true, data: res }); - }) - .catch(e => { - UtilsService.getInstance().ipcSend(request.responceChannel, { success: false, data: e }); - }); -}); - -ipcMain.on(`bs-download.${"[2FA]" as DownloadEventType}`, async (event, args: IpcRequest) => { - BSInstallerService.getInstance().sendInputProcess(args.args); -}); - -ipcMain.on("bs-download.kill", async (event, request: IpcRequest) => { - const res = await BSInstallerService.getInstance().killDownloadProcess(); - if (request.responceChannel) { - UtilsService.getInstance().ipcSend(request.responceChannel, { success: res }); - } -}); - -ipcMain.on("bs-download.installation-folder", async (event, request: IpcRequest) => { - const installationFolder = InstallationLocationService.getInstance().installationDirectory; - UtilsService.getInstance().ipcSend(request.responceChannel, { success: true, data: installationFolder }); +ipc.on("bs-download.installation-folder", (_, reply) => { + const installLocation = InstallationLocationService.getInstance(); + reply(of(installLocation.installationDirectory)); }); ipc.on("bs-download.set-installation-folder", (req, reply) => { @@ -62,15 +20,32 @@ ipc.on("bs-download.set-installation-folder", (req, reply) => { reply(from(installerService.setInstallationDirectory(req.args))); }); -ipcMain.on("bs-download.import-version", (event, request: IpcRequest) => { - const utils = UtilsService.getInstance(); +ipc.on("bs-download.import-version", (req, reply) => { const installer = BSInstallerService.getInstance(); - installer - .importVersion(request.args) - .catch(e => { - utils.ipcSend(request.responceChannel, { success: false, error: e }); - }) - .then(() => { - utils.ipcSend(request.responceChannel, { success: true }); - }); + reply(from(installer.importVersion(req.args))); +}); + +ipc.on("auto-download-bs-version", (req, reply) => { + const bsInstaller = BSInstallerService.getInstance(); + reply(bsInstaller.autoDownloadBsVersion(req.args)); +}); + +ipc.on("download-bs-version", (req, reply) => { + const bsInstaller = BSInstallerService.getInstance(); + reply(bsInstaller.downloadBsVersion(req.args)) +}); + +ipc.on("download-bs-version-qr", (req, reply) => { + const bsInstaller = BSInstallerService.getInstance(); + reply(bsInstaller.downloadBsVersionWithQRCode(req.args)) +}); + +ipc.on("stop-download-bs-version", (_, reply) => { + const bsInstaller = BSInstallerService.getInstance(); + reply(of(bsInstaller.stopDownload())); +}); + +ipc.on("send-input-bs-download", (req, reply) => { + const bsInstaller = BSInstallerService.getInstance(); + reply(of(bsInstaller.sendInput(req.args))); }); diff --git a/src/main/models/depot-downloader.class.ts b/src/main/models/depot-downloader.class.ts new file mode 100644 index 00000000..50ac2849 --- /dev/null +++ b/src/main/models/depot-downloader.class.ts @@ -0,0 +1,91 @@ +import { ChildProcessWithoutNullStreams, SpawnOptionsWithoutStdio, spawn } from "child_process"; +import { Observable, ReplaySubject, Subscriber, filter, map, share } from "rxjs"; +import { DepotDownloaderArgsOptions, DepotDownloaderEvent, DepotDownloaderEventType, DepotDownloaderEventTypes, DepotDownloaderSubTypeOfEventType } from "../../shared/models/depot-downloader.model"; + +export class DepotDownloader { + + private process: ChildProcessWithoutNullStreams; + private processOut$: Observable; + private subscriber: Subscriber; + + public constructor(options: { + command: string, args?: string[], options?: SpawnOptionsWithoutStdio, echoStartData?: unknown + }){ + + this.processOut$ = new Observable(subscriber => { + + this.subscriber = subscriber; + + console.log(`${options.command} ${options.args?.join(" ")}}`) + + this.process = spawn(options.command, options.args ?? [], options.options); + + subscriber.next(`[Info]|[Start]|${JSON.stringify(options.echoStartData) ?? ""}`); + + this.process.stdout.on("data", data => subscriber.next(data.toString())); + this.process.stderr.on("error", error => subscriber.error(error)); + this.process.on("exit", code => subscriber.complete()); + + return () => { + console.log("DepotDownloader process killed") + this.process.kill(); + this.process = null; + } + + }).pipe(share({connector: () => new ReplaySubject(1)})); + } + + public $events(): Observable>{ + return this.processOut$.pipe(map(line => { + + console.log(line.toString()); + + const matched = (line.toString() as string).match(/(?:\[(.*?)\])\|(?:\[(.*?)\]\|)?(.*?)(?=$|\[)/gm)?.[0] ?? null; + + if(!matched){ return null; } + + const splitedLine = matched.split("|").map(str => str.trim().replaceAll("[", "").replaceAll("]", "")) as [DepotDownloaderEventType, DepotDownloaderEventTypes, unknown]; + + if(!Object.values(DepotDownloaderEventType).includes(splitedLine[0]) || !Object.values(DepotDownloaderSubTypeOfEventType[splitedLine[0]]).includes(splitedLine[1])){ + return null; + } + + return { + type: splitedLine[0] as DepotDownloaderEventType, + subType: splitedLine[1] as DepotDownloaderEventTypes, + data: splitedLine[2] as unknown, + } + + }), + filter(Boolean)); + } + + public sendInput(input: string): boolean{ + if(!this.process.stdin.writable){ throw new Error("DepotDownloader stdin is not writable"); } + return this.process.stdin.write(`${input}\n`); + } + + public stop(){ + this.subscriber.complete(); + } + + public get running(){ return !!this.process } + + public static buildArgs(depotDownloaderArgs: DepotDownloaderArgsOptions): string[]{ + const args: string[] = []; + + for(const [key, value] of Object.entries(depotDownloaderArgs)){ + + if(value === true){ + args.push(`-${key}`); + } + else if(value){ + args.push(`-${key}`); + args.push(`${value}`); + } + } + + return args; + } + +} \ No newline at end of file diff --git a/src/main/services/bs-installer.service.ts b/src/main/services/bs-installer.service.ts index a6b82f96..81d1ca8a 100644 --- a/src/main/services/bs-installer.service.ts +++ b/src/main/services/bs-installer.service.ts @@ -2,14 +2,16 @@ import { BS_APP_ID, BS_DEPOT } from "../constants"; import path from "path"; import { BSVersion } from "shared/bs-version.interface"; import { UtilsService } from "./utils.service"; -import { ChildProcessWithoutNullStreams, spawn, spawnSync } from "child_process"; +import { spawnSync } from "child_process"; import log from "electron-log"; import { InstallationLocationService } from "./installation-location.service"; import { BSLocalVersionService } from "./bs-local-version.service"; import { WindowManagerService } from "./window-manager.service"; import { copy } from "fs-extra"; -import { ensureFolderExist, pathExist } from "../helpers/fs.helpers"; -import { net } from "electron"; +import { pathExist } from "../helpers/fs.helpers"; +import { Observable, map } from "rxjs"; +import { DepotDownloaderArgsOptions, DepotDownloaderErrorEvent, DepotDownloaderEvent, DepotDownloaderEventType, DepotDownloaderInfoEvent } from "../../shared/models/depot-downloader.model"; +import { DepotDownloader } from "../models/depot-downloader.class"; export class BSInstallerService { private static instance: BSInstallerService; @@ -19,7 +21,7 @@ export class BSInstallerService { private readonly localVersionService: BSLocalVersionService; private readonly windows: WindowManagerService; - private downloadProcess: ChildProcessWithoutNullStreams; + private depotDownloader: DepotDownloader; private constructor() { this.utils = UtilsService.getInstance(); @@ -28,7 +30,7 @@ export class BSInstallerService { this.windows = WindowManagerService.getInstance(); this.windows.getWindow("index.html")?.on("close", () => { - this.killDownloadProcess(); + this.depotDownloader?.stop(); }); } @@ -43,27 +45,6 @@ export class BSInstallerService { return path.join(this.utils.getAssetsScriptsPath(), "depot-downloader", "DepotDownloader.exe"); } - private removeSpecialSchar(txt: string): string { - return txt.replaceAll(/[\[\]]/g, ""); - } - - private sendDownloadEvent(event: DownloadEventType, data?: string | number, success = true): void { - if (typeof data === "string") { - data = this.removeSpecialSchar(data); - } - this.utils.ipcSend(`bs-download.${event}`, { success, data }); - } - - public sendInputProcess(input: string) { - if (this.downloadProcess.stdin.writable) { - this.downloadProcess.stdin.write(`${input}\n`); - } - } - - public killDownloadProcess(): boolean { - return this.downloadProcess.kill(); - } - public async isDotNet6Installed(): Promise { try { const process = spawnSync(this.getDepotDownloaderExePath()); @@ -78,95 +59,89 @@ export class BSInstallerService { } } - public async downloadBsVersion(downloadInfos: DownloadInfo): Promise { - // TODO : Can be a lot improved by using ipcV2 and Observable -- This will be reworked for qrCode login - - if (this.downloadProcess?.connected) { - throw "AlreadyDownloading"; - } - const { bsVersion } = downloadInfos; - if (!bsVersion) { - return { type: "[Error]" }; - } - if (!net.isOnline()) { - throw "no-internet"; - } - - await ensureFolderExist(this.installLocationService.versionsDirectory); - - const versionPath = await this.localVersionService.getVersionPath(bsVersion); - - const dest = !downloadInfos.isVerification ? await this.getPathNotAleardyExist(versionPath) : versionPath; + private async buildDepotDownloaderInstance(downloadInfos: DownloadInfo, qr?: boolean): Promise { + const versionPath = await this.localVersionService.getVersionPath(downloadInfos.bsVersion); + const dest = downloadInfos.isVerification ? versionPath : await this.getPathNotAleardyExist(versionPath); const downloadVersion: BSVersion = { ...downloadInfos.bsVersion, ...(path.basename(dest) !== downloadInfos.bsVersion.BSVersion && { name: path.basename(dest) }) }; - this.downloadProcess = spawn(this.getDepotDownloaderExePath(), ["-app", BS_APP_ID, "-depot", BS_DEPOT, "-manifest", bsVersion.BSManifest, "-username", downloadInfos.username, "-dir", this.localVersionService.getVersionFolder(downloadVersion)], { cwd: this.installLocationService.versionsDirectory }); + const depotDownloaderOptions: DepotDownloaderArgsOptions = { + app: BS_APP_ID, + depot: BS_DEPOT, + dir: this.localVersionService.getVersionFolder(downloadVersion), + manifest: downloadInfos.bsVersion.BSManifest, + username: downloadInfos.username, + password: downloadInfos.password, + "remember-password": downloadInfos.stay, + validate: downloadInfos.isVerification, + qr + } - this.utils.ipcSend("start-download-version", { success: true, data: downloadVersion }); - - return new Promise((resolve, reject) => { - this.downloadProcess.stdout.on("data", data => { - const matched = (data.toString() as string).match(/(?:\[(.*?)\])\|(?:\[(.*?)\]\|)?(.*?)(?=$|\[)/gm) ?? []; - matched.forEach(match => { - const out = match.split("|"); - - if (out[0] !== "[Progress]" && out[0] !== "[Validated]") { - log.info(data.toString()); - } - - if (out[0] === ("[Password]" as DownloadEventType) && downloadInfos.password) { - this.sendInputProcess(downloadInfos.password); - } else if (out[0] === ("[Password]" as DownloadEventType)) { - this.killDownloadProcess(); - resolve({ type: "[Password]", data: bsVersion }); - } else if (out[0] === ("[2FA]" as DownloadEventType) || out[0] === ("[Guard]" as DownloadEventType)) { - this.sendDownloadEvent("[2FA]"); - } else if (out[0] === ("[Progress]" as DownloadEventType) || (out[0] === ("[Validated]" as DownloadEventType) && parseFloat(out[1]) < 100)) { - this.sendDownloadEvent("[Progress]", parseFloat(out[1].replaceAll(",", "."))); - } else if (out[0] === ("[Finished]" as DownloadEventType) || (out[0] === ("[Validated]" as DownloadEventType) && parseFloat(out[1]) === 100)) { - resolve({ type: "[Finished]" }); - this.killDownloadProcess(); - } else if (out[0] === ("[SteamID]" as DownloadEventType)) { - this.sendDownloadEvent("[SteamID]", out[1]); - } else if (out[0] === ("[Warning]" as DownloadEventType)) { - this.sendDownloadEvent("[Warning]", out[1]); - } else if (out[0] === ("[Error]" as DownloadEventType)) { - reject(this.removeSpecialSchar(out[1])); - this.killDownloadProcess(); - log.error("Download Event, Error", data.toString()); - } - }); - }); - - this.downloadProcess.stdout.on("error", err => { - log.error("BS-DOWNLOAD ERROR", err.toString()); - this.killDownloadProcess(); - if (err.toString().includes(".NET") || err.toString().includes("dotnet") || err.toString().includes(".dll")) { - reject("dotnet"); - } - reject(); - }); - this.downloadProcess.stderr.on("data", err => { - log.error("BS-DOWNLOAD ERROR", err.toString()); - this.killDownloadProcess(); - if (err.toString().includes(".NET") || err.toString().includes("dotnet") || err.toString().includes(".dll")) { - reject("dotnet"); - } - reject(); - }); - this.downloadProcess.stderr.on("error", err => { - log.error("BS-DOWNLOAD ERROR", err.toString()); - this.killDownloadProcess(); - if (err.toString().includes(".NET") || err.toString().includes("dotnet") || err.toString().includes(".dll")) { - reject("dotnet"); - } - reject(); - }); - - this.downloadProcess.on("close", () => reject()); + return new DepotDownloader({ + command: this.getDepotDownloaderExePath(), + args: DepotDownloader.buildArgs(depotDownloaderOptions), + options: { cwd: this.installLocationService.versionsDirectory }, + echoStartData: downloadVersion }); } + private buildDepotDownloaderObservable(downloadInfos: DownloadInfo, qr?: boolean): Observable { + return new Observable(sub => { + + const depotDownloaderBuildPromise = this.buildDepotDownloaderInstance(downloadInfos, qr); + + depotDownloaderBuildPromise.then(depotDownloader => { + + if(this.depotDownloader?.running){ + this.depotDownloader.stop(); + } + + this.depotDownloader = depotDownloader; + + depotDownloader.$events().pipe(map(event => { + if(event.type === DepotDownloaderEventType.Error){ + throw event; + } + return event; + })).subscribe(sub); + + }).catch(err => sub.error({ + type: DepotDownloaderEventType.Error, + subType: DepotDownloaderErrorEvent.Unknown, + data: err + } as DepotDownloaderEvent)); + + return () => { + depotDownloaderBuildPromise.then(depotDownloader => depotDownloader.stop()); + } + }); + } + + public downloadBsVersion(downloadInfos: DownloadInfo): Observable { + return this.buildDepotDownloaderObservable(downloadInfos); + } + + public autoDownloadBsVersion(downloadInfos: DownloadInfo): Observable { + return this.buildDepotDownloaderObservable({...downloadInfos, password: null, stay: true}).pipe(map(event => { + if(event.type === DepotDownloaderEventType.Info && event.subType === DepotDownloaderInfoEvent.Password){ + throw new Error("Ask for password while auto download"); + } + return event; + })); + } + + public downloadBsVersionWithQRCode(downloadInfos: DownloadInfo): Observable { + return this.buildDepotDownloaderObservable(downloadInfos, true); + } + + public sendInput(input: string): boolean { + return this.depotDownloader?.sendInput(input); + } + + public stopDownload(): void { + this.depotDownloader?.stop(); + } + private async getPathNotAleardyExist(path: string): Promise { let destPath = path; let folderExist = await pathExist(destPath); @@ -198,10 +173,10 @@ export class BSInstallerService { export interface DownloadInfo { bsVersion: BSVersion; - username: string; + username?: string; password?: string; stay?: boolean; - isVerification: boolean; + isVerification?: boolean; } export interface DownloadEvent { diff --git a/src/main/services/ipc.service.ts b/src/main/services/ipc.service.ts index 344e2583..9ef6ff4f 100644 --- a/src/main/services/ipc.service.ts +++ b/src/main/services/ipc.service.ts @@ -1,7 +1,7 @@ import { IpcRequest } from "shared/models/ipc"; import { ipcMain } from "electron"; import { Observable } from "rxjs"; -import { IpcCompleteChannel, IpcErrorChannel } from "shared/models/ipc/ipc-response.interface"; +import { IpcCompleteChannel, IpcErrorChannel, IpcTearDownChannel } from "shared/models/ipc/ipc-response.interface"; import { AppWindow } from "shared/models/window-manager/app-window.model"; import { WindowManagerService } from "./window-manager.service"; import { IpcReplier } from "shared/models/ipc/ipc-request.interface"; @@ -31,6 +31,10 @@ export class IpcService { return `${channel}_complete`; } + private getTearDownChannel(channel: string): IpcTearDownChannel { + return `${channel}_teardown`; + } + private buildProxyListener(listener: IpcListener) { return (event: Electron.IpcMainEvent, req: IpcRequest) => { const window = this.windows.getAppWindowFromWebContents(event.sender); @@ -44,18 +48,21 @@ export class IpcService { } private connectStream(channel: string, window: AppWindow, observable: Observable): void { - observable.subscribe( - data => { - this.send(channel, window, data); - }, - error => { + + const sub = observable.subscribe({ + next: data => this.send(channel, window, data), + error: error => { log.error(error); this.send(this.getErrorChannel(channel), window, error); }, - () => { - this.send(this.getCompleteChannel(channel), window); - } - ); + complete: () => this.send(this.getCompleteChannel(channel), window) + }) + + ipcMain.once(this.getTearDownChannel(channel), () => sub.unsubscribe()); + + sub.add(() => { + ipcMain.removeAllListeners(this.getTearDownChannel(channel)); + }); } public on(channel: string, listener: IpcListener): void { diff --git a/src/renderer/components/available-versions/available-version-item.component.tsx b/src/renderer/components/available-versions/available-version-item.component.tsx index fbb90a32..ce2c7569 100644 --- a/src/renderer/components/available-versions/available-version-item.component.tsx +++ b/src/renderer/components/available-versions/available-version-item.component.tsx @@ -1,7 +1,5 @@ import { BSVersion } from "shared/bs-version.interface"; -import { useState, memo } from "react"; -import { BsDownloaderService } from "renderer/services/bs-downloader.service"; -import { distinctUntilChanged, map } from "rxjs/operators"; +import { useState, memo, ComponentProps } from "react"; import defaultImage from "../../../../assets/images/default-version-img.jpg"; import dateFormat from "dateformat"; import { BsmImage } from "../shared/bsm-image.component"; @@ -11,51 +9,38 @@ import { LinkOpenerService } from "renderer/services/link-opener.service"; import { motion } from "framer-motion"; import { GlowEffect } from "../shared/glow-effect.component"; import { useService } from "renderer/hooks/use-service.hook"; -import { useObservable } from "renderer/hooks/use-observable.hook"; +import equal from "fast-deep-equal"; -export const AvailableVersionItem = memo(function AvailableVersionItem(props: { version: BSVersion }) { - const bsDownloaderService = useService(BsDownloaderService) +type Props = { + version: BSVersion; + selected: boolean; + onClick: ComponentProps<"li">["onClick"]; +} + +export const AvailableVersionItem = memo(function AvailableVersionItem({version, selected, onClick}: Props) { const linkOpener = useService(LinkOpenerService) - - const selected = useObservable( - bsDownloaderService.selectedBsVersion$.pipe(distinctUntilChanged(), map(version => version?.BSVersion === props.version.BSVersion)), - false - ); const [hovered, setHovered] = useState(false); const t = useTranslation(); - const formatedDate = (() => { - return dateFormat(+props.version.ReleaseDate * 1000, "ddd. d mmm yyyy"); - })(); - - const toggleSelect = () => { - if (bsDownloaderService.isDownloading) { - return; - } - if (selected) { - bsDownloaderService.selectedBsVersion$.next(null); - } else { - bsDownloaderService.selectedBsVersion$.next(props.version); - } - }; - const openReleasePage = () => { - linkOpener.open(props.version.ReleaseURL); + linkOpener.open(version.ReleaseURL); }; + const formatedDate = (() => dateFormat(+version.ReleaseDate * 1000, "ddd. d mmm yyyy"))(); + return ( - setHovered(true)} onHoverEnd={() => setHovered(false)}> + setHovered(true)} onHoverEnd={() => setHovered(false)}>
- - + +
-

{props.version.BSVersion}

+

{version.BSVersion}

{formatedDate}
- {props.version.ReleaseURL && ( + {version.ReleaseURL && ( // eslint-disable-next-line jsx-a11y/anchor-is-valid -- link will be reworked { @@ -72,4 +57,4 @@ export const AvailableVersionItem = memo(function AvailableVersionItem(props: {
); -}); +}, equal); diff --git a/src/renderer/components/available-versions/available-versions-slide.component.tsx b/src/renderer/components/available-versions/available-versions-slide.component.tsx index 48b42f08..24004b9c 100644 --- a/src/renderer/components/available-versions/available-versions-slide.component.tsx +++ b/src/renderer/components/available-versions/available-versions-slide.component.tsx @@ -1,22 +1,28 @@ -import { BSVersionManagerService } from "renderer/services/bs-version-manager.service"; -import { filter, map } from "rxjs/operators"; +import { useContext } from "react"; import { AvailableVersionItem } from "./available-version-item.component"; -import { useService } from "renderer/hooks/use-service.hook"; -import { useObservable } from "renderer/hooks/use-observable.hook"; +import { BSVersion } from "shared/bs-version.interface"; +import { AvailableVersionsContext } from "renderer/pages/available-versions-list.components"; +import equal from "fast-deep-equal"; -export function AvailableVersionsSlide(props: { year: string }) { - - const versionsService = useService(BSVersionManagerService); - - const availableVersions = useObservable( - versionsService.availableVersions$.pipe(filter(versions => !!versions?.length), map(() => versionsService.getAvaibleVersionsOfYear(props.year))), - [] - ); +type Props = { + versions: BSVersion[] +} + +export function AvailableVersionsSlide({ versions }: Props) { + + const context = useContext(AvailableVersionsContext); + + const setSelectedVersion = (version: BSVersion) => { + if(equal(version, context.selectedVersion)){ + return context.setSelectedVersion(null); + } + context.setSelectedVersion(version); + } return (
    - {availableVersions.map(version => ( - + {versions.map(version => ( + setSelectedVersion(version)}/> ))}
); diff --git a/src/renderer/components/available-versions/available-versions-slider.component.tsx b/src/renderer/components/available-versions/available-versions-slider.component.tsx index a82ae620..89255959 100644 --- a/src/renderer/components/available-versions/available-versions-slider.component.tsx +++ b/src/renderer/components/available-versions/available-versions-slider.component.tsx @@ -1,7 +1,6 @@ import { useState } from "react"; import { BSVersionManagerService } from "../../services/bs-version-manager.service"; import { AvailableVersionsSlide } from "./available-versions-slide.component"; -import { filter, map } from "rxjs/operators"; import { TabNavBar } from "../shared/tab-nav-bar.component"; import { useService } from "renderer/hooks/use-service.hook"; import { useObservable } from "renderer/hooks/use-observable.hook"; @@ -9,22 +8,28 @@ import { useObservable } from "renderer/hooks/use-observable.hook"; export function AvailableVersionsSlider() { const versionManagerService = useService(BSVersionManagerService); + const availableVersions = useObservable(versionManagerService.availableVersions$); const [yearIndex, setYearIndex] = useState(0); - const availableYears = useObservable( - versionManagerService.availableVersions$.pipe(filter(versions => !!versions?.length), map(() => versionManagerService.getAvailableYears())), - [] - ); + + const availableYears = (() => { + if(!availableVersions?.length) { return []; } + return [...new Set(availableVersions.map(v => v.year))].sort((a, b) => b.localeCompare(a)) + })(); const setSelectedYear = (index: number) => { setYearIndex(index); }; + const getVersionOfYear = (year: string) => { + return availableVersions.filter(v => v.year === year).sort((a, b) => +b.ReleaseDate - +a.ReleaseDate); + }; + return (
    {availableYears.map(year => ( - + ))}
diff --git a/src/renderer/components/modal/modal-types/login-modal.component.tsx b/src/renderer/components/modal/modal-types/login-modal.component.tsx index 4365e0d3..9a3ffd99 100644 --- a/src/renderer/components/modal/modal-types/login-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/login-modal.component.tsx @@ -1,13 +1,19 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { BsmButton } from "renderer/components/shared/bsm-button.component"; -import { BsmImage } from "renderer/components/shared/bsm-image.component"; import { useTranslation } from "renderer/hooks/use-translation.hook"; import { ModalComponent, ModalExitCode, ModalService } from "renderer/services/modale.service"; -import BeatImpatient from "../../../../../assets/images/apngs/beat-impatient.png"; import { WhyCredentialsModal } from "./why-credentials-modal.component"; import { useService } from "renderer/hooks/use-service.hook"; +import { Observable } from "rxjs"; +import { useObservable } from "renderer/hooks/use-observable.hook"; +import { QRCodeSVG } from "qrcode.react"; +import { BsmCheckbox } from "renderer/components/shared/bsm-checkbox.component"; +import { BsmBasicSpinner } from "renderer/components/shared/bsm-basic-spinner/bsm-basic-spinner.component"; -export const LoginModal: ModalComponent<{ username: string; password: string; stay: boolean }> = ({ resolver }) => { +export const LoginModal: ModalComponent< + { username: string; password: string; stay: boolean, method: "form"|"qr" }, + { qrCode$: Observable, logged$: Observable } +> = ({ resolver, data }) => { const modal = useService(ModalService); @@ -15,13 +21,29 @@ export const LoginModal: ModalComponent<{ username: string; password: string; st const [password, setPassword] = useState(""); const [stay, setStay] = useState(false); const [showPassword, setShowPassword] = useState(false); + const qrCodeUrl = useObservable(data.qrCode$); const t = useTranslation(); + useEffect(() => { + + const sub = data.logged$.subscribe({ + next: logged => { + if(!logged) return; + // If the logged event is emitted, it means that the user is logged in by the QR code + resolver({ exitCode: ModalExitCode.COMPLETED, data: { username, password, stay, method: "qr" } }); + }, + error: () => resolver({ exitCode: ModalExitCode.NO_CHOICE}), + complete: () => resolver({ exitCode: ModalExitCode.NO_CHOICE}) + }); + + return () => sub.unsubscribe(); + }, []); + const loggin = () => { if (!username || !password) { return; } - resolver({ exitCode: ModalExitCode.COMPLETED, data: { username, password, stay } }); + resolver({ exitCode: ModalExitCode.COMPLETED, data: { username, password, stay, method: "form" } }); }; const whyCredentials = () => { @@ -29,48 +51,48 @@ export const LoginModal: ModalComponent<{ username: string; password: string; st }; return ( -
{ - e.preventDefault(); - loggin(); - }} - > -

{t("modals.steam-login.title")}

- - -
- - setUsername(e.target.value)} value={username} type="text" name="username" id="username" placeholder={t("modals.steam-login.inputs.username.placeholder")} /> -
-
- -
- setPassword(e.target.value)} value={password} type={showPassword ? "text" : "password"} name="password" id="password" placeholder={t("modals.steam-login.inputs.password.placeholder")} /> - setShowPassword(prev => !prev)} /> + {e.preventDefault(); loggin()}}> +

{t("modals.steam-login.title")}

+
+ +
+ {t("modals.steam-login.inputs.qr.label")} +
+ {(qrCodeUrl ? + : + + )} +
+ + {t("modals.steam-login.inputs.qr.note.use-the")}{t("modals.steam-login.inputs.qr.note.steam-mobile-app")} {t("modals.steam-login.inputs.qr.note.to-connect-with-qr")} +
- {password?.length > 64 && {t("modals.steam-login.inputs.password.max-length-warning")}} -
- - - {t("modals.steam-login.why-credentials")} - - -
- { - resolver({ exitCode: ModalExitCode.CANCELED }); - }} - withBar={false} - text="misc.cancel" - /> -
); diff --git a/src/renderer/components/nav-bar/nav-bar-items/bs-version-item.component.tsx b/src/renderer/components/nav-bar/nav-bar-items/bs-version-item.component.tsx index e024e0fd..22fff626 100644 --- a/src/renderer/components/nav-bar/nav-bar-items/bs-version-item.component.tsx +++ b/src/renderer/components/nav-bar/nav-bar-items/bs-version-item.component.tsx @@ -1,8 +1,8 @@ import { BSVersion } from "shared/bs-version.interface"; import { Link, useLocation } from "react-router-dom"; import { BsDownloaderService } from "renderer/services/bs-downloader.service"; -import { useEffect, useState } from "react"; -import { combineLatest, Subscription } from "rxjs"; +import { useState } from "react"; +import { distinctUntilChanged, map, of, Subscription, switchMap } from "rxjs"; import { BSLauncherService, LaunchMods } from "renderer/services/bs-launcher.service"; import { ConfigurationService } from "renderer/services/configuration.service"; import { BSUninstallerService } from "renderer/services/bs-uninstaller.service"; @@ -13,6 +13,8 @@ import { NavBarItem } from "./nav-bar-item.component"; import useFitText from "use-fit-text"; import Tippy from "@tippyjs/react"; import { useService } from "renderer/hooks/use-service.hook"; +import equal from "fast-deep-equal"; +import { useOnUpdate } from "renderer/hooks/use-on-update.hook"; export function BsVersionItem(props: { version: BSVersion }) { const downloaderService = useService(BsDownloaderService); @@ -24,10 +26,28 @@ export function BsVersionItem(props: { version: BSVersion }) { const { state } = useLocation() as { state: BSVersion }; const { fontSize, ref } = useFitText(); - const [downloading, setDownloading] = useState(false); - const [downloadPercent, setDownloadPercent] = useState(0); + const [isDownloading, setIsDownloading] = useState(false); + const [downloadProgress, setDownloadProgress] = useState(0); const secondColor = useThemeColor("second-color"); + useOnUpdate(() => { + const subs: Subscription[] = [] + + subs.push(downloaderService.currentBsVersionDownload$.pipe(map(download => equal(download, props.version)), distinctUntilChanged()).subscribe(isDownloading => { + setIsDownloading(() => isDownloading); + })); + + subs.push(downloaderService.currentBsVersionDownload$.pipe( + map(download => equal(download, props.version)), + distinctUntilChanged(), + switchMap(isDownloading => isDownloading ? downloaderService.downloadProgress$ : of(0)), + ).subscribe(progress => { + setDownloadProgress(() => progress); + })); + + return () => subs.forEach(sub => sub.unsubscribe()); + }, [props.version]); + const isActive = (): boolean => { return props.version?.BSVersion === state?.BSVersion && props?.version.steam === state?.steam && props?.version.oculus === state?.oculus && props?.version.name === state?.name; }; @@ -44,33 +64,12 @@ export function BsVersionItem(props: { version: BSVersion }) { const cancel = () => { const versionDownload = downloaderService.currentBsVersionDownload$.value; const wasVerification = downloaderService.isVerification; - downloaderService.cancelDownload().then(async res => { - if (!res.success) { - return; - } - if (!wasVerification) { - bsUninstallerService.uninstall(versionDownload).then(res => res && verionManagerService.askInstalledVersions()); - } + downloaderService.stopDownload().then(() => { + if(wasVerification){ return; } + bsUninstallerService.uninstall(versionDownload).then(res => res && verionManagerService.askInstalledVersions()); }); }; - useEffect(() => { - const subs: Subscription[] = []; - const downloadSub = combineLatest([downloaderService.currentBsVersionDownload$, downloaderService.downloadProgress$]).subscribe(vals => { - if (vals[0]?.BSVersion === props.version.BSVersion && vals[0]?.steam === props.version.steam && vals[0].oculus === props.version.oculus && vals[0]?.name === props.version.name) { - setDownloading(true); - setDownloadPercent(vals[1]); - } else { - setDownloading(false); - setDownloadPercent(0); - } - }); - subs.push(downloadSub); - return () => { - subs.forEach(s => s.unsubscribe()); - }; - }, []); - const renderIcon = () => { const classes = "w-[19px] h-[19px] mr-[5px] shrink-0"; if (props.version.steam) { @@ -100,7 +99,7 @@ export function BsVersionItem(props: { version: BSVersion }) { }; return ( - + {props.version.name ? ( diff --git a/src/renderer/pages/available-versions-list.components.tsx b/src/renderer/pages/available-versions-list.components.tsx index 91ebcd4e..305e21e6 100644 --- a/src/renderer/pages/available-versions-list.components.tsx +++ b/src/renderer/pages/available-versions-list.components.tsx @@ -1,7 +1,7 @@ import { AvailableVersionsSlider } from "../components/available-versions/available-versions-slider.component"; import { BsDownloaderService } from "../services/bs-downloader.service"; import { Slideshow } from "renderer/components/slideshow/slideshow.component"; -import { useState } from "react"; +import { createContext, useState } from "react"; import { BsmButton } from "renderer/components/shared/bsm-button.component"; import { AnimatePresence, motion } from "framer-motion"; import { useTranslation } from "renderer/hooks/use-translation.hook"; @@ -12,28 +12,30 @@ import { ModalExitCode, ModalService } from "renderer/services/modale.service"; import { ImportVersionModal } from "renderer/components/modal/modal-types/import-version-modal.component"; import { IpcService } from "renderer/services/ipc.service"; import { NotificationService } from "renderer/services/notification.service"; -import { timer } from "rxjs"; +import { lastValueFrom, map } from "rxjs"; import { useService } from "renderer/hooks/use-service.hook"; +import { BSVersion } from "shared/bs-version.interface"; import { useObservable } from "renderer/hooks/use-observable.hook"; +export const AvailableVersionsContext = createContext<{ selectedVersion: BSVersion; setSelectedVersion: (version: BSVersion) => void }>(null); + export function AvailableVersionsList() { - const bsDownloaderService = useService(BsDownloaderService); - const versionManagerService = useService(BSVersionManagerService); + const bsDownloader = useService(BsDownloaderService); + const versionManager = useService(BSVersionManagerService); const progressBar = useService(ProgressBarService); const modal = useService(ModalService); const ipc = useService(IpcService); const installer = useService(BsDownloaderService); const notification = useService(NotificationService); - const versionSelected = useObservable(bsDownloaderService.selectedBsVersion$); - const [downloading, setDownloading] = useState(false); + const [selectedVersion, setSelectedVersion] = useState(null); + const downloading = useObservable(bsDownloader.currentBsVersionDownload$.pipe(map(v => !!v))); const t = useTranslation(); const startDownload = () => { - setDownloading(() => true); - bsDownloaderService.download(versionSelected).finally(() => { - setDownloading(() => false); - }); + bsDownloader.downloadBsVersion(selectedVersion) + .catch(() => {}) + .finally(() => setSelectedVersion(null)); }; const importVersion = async () => { @@ -47,7 +49,7 @@ export function AvailableVersionsList() { return; } - const folderRes = await ipc.sendV2<{ canceled: boolean; filePaths: string[] }>("choose-folder").toPromise(); + const folderRes = await lastValueFrom(ipc.sendV2<{ canceled: boolean; filePaths: string[] }>("choose-folder")) if (!folderRes || folderRes.canceled || !folderRes.filePaths?.length) { return; @@ -62,10 +64,9 @@ export function AvailableVersionsList() { const imported = await installer.importVersion(toImport); if (imported) { - versionManagerService.askInstalledVersions(); + versionManager.askInstalledVersions(); notification.notifySuccess({ title: "notifications.bs-import-version.success.imported.title", duration: 3_000 }); progressBar.complete(); - await timer(400).toPromise(); } else { notification.notifyError({ title: "notifications.types.error", desc: "notifications.bs-import-version.errors.import-error.desc" }); } @@ -75,13 +76,16 @@ export function AvailableVersionsList() { return (
+

{t("pages.available-versions.title")}

- - + + + + - {versionSelected && !downloading && ( + {selectedVersion && !downloading && ( @@ -92,7 +96,7 @@ export function AvailableVersionsList() { className="absolute top-5 right-5 h-9 w-9 bg-light-main-color-2 dark:bg-main-color-2 rounded-md" icon="settings" items={[ - { icon: "sync", text: "pages.available-versions.dropdown.refresh", onClick: () => versionManagerService.askAvailableVersions() }, + { icon: "sync", text: "pages.available-versions.dropdown.refresh", onClick: () => versionManager.askAvailableVersions() }, { icon: "download", text: "pages.available-versions.dropdown.import-version", onClick: importVersion }, ]} /> diff --git a/src/renderer/pages/settings-page.component.tsx b/src/renderer/pages/settings-page.component.tsx index 63092e57..1f87b81e 100644 --- a/src/renderer/pages/settings-page.component.tsx +++ b/src/renderer/pages/settings-page.component.tsx @@ -34,6 +34,7 @@ import { VersionFolderLinkerService } from "renderer/services/version-folder-lin import { useService } from "renderer/hooks/use-service.hook"; import { lastValueFrom } from "rxjs"; import { BsmException } from "shared/models/bsm-exception.model"; +import { useObservable } from "renderer/hooks/use-observable.hook"; export function SettingsPage() { @@ -73,13 +74,12 @@ export function SettingsPage() { const [mapDeepLinksEnabled, setMapDeepLinksEnabled] = useState(false); const [playlistsDeepLinkEnabled, setPlaylistsDeepLinkEnabled] = useState(false); const [modelsDeepLinkEnabled, setModelsDeepLinkEnabled] = useState(false); - const [appVersion, setAppVersion] = useState(""); + const appVersion = useObservable(ipcService.sendV2("current-version")); const nav = useNavigate(); const t = useTranslation(); useEffect(() => { loadInstallationFolder(); - lastValueFrom(ipcService.sendV2("current-version")).then(res => setAppVersion(res)); mapsManager.isDeepLinksEnabled().then(enabled => setMapDeepLinksEnabled(() => enabled)); playlistsManager.isDeepLinksEnabled().then(enabled => setPlaylistsDeepLinkEnabled(() => enabled)); modelsManager.isDeepLinksEnabled().then(enabled => setModelsDeepLinkEnabled(() => enabled)); diff --git a/src/renderer/services/bs-downloader.service.ts b/src/renderer/services/bs-downloader.service.ts index ab1d0b46..93902338 100644 --- a/src/renderer/services/bs-downloader.service.ts +++ b/src/renderer/services/bs-downloader.service.ts @@ -1,6 +1,6 @@ import { DownloadEvent, DownloadInfo } from "main/services/bs-installer.service"; -import { BehaviorSubject, Observable } from "rxjs"; -import { distinctUntilChanged, filter, throttleTime } from "rxjs/operators"; +import { BehaviorSubject, Observable, Subscription, identity, lastValueFrom, of, throwError } from "rxjs"; +import { distinctUntilChanged, filter, map, take, tap, throttleTime } from "rxjs/operators"; import { IpcResponse } from "shared/models/ipc"; import { BSVersion } from "shared/bs-version.interface"; import { AuthUserService } from "./auth-user.service"; @@ -12,6 +12,8 @@ import { ProgressBarService } from "./progress-bar.service"; import { LoginModal } from "renderer/components/modal/modal-types/login-modal.component"; import { GuardModal } from "renderer/components/modal/modal-types/guard-modal.component"; import { LinkOpenerService } from "./link-opener.service"; +import { DepotDownloaderErrorEvent, DepotDownloaderEvent, DepotDownloaderEventType, DepotDownloaderInfoEvent, DepotDownloaderSubTypeOfEventType, DepotDownloaderWarningEvent } from "../../shared/models/depot-downloader.model"; +import equal from "fast-deep-equal"; export class BsDownloaderService { private static instance: BsDownloaderService; @@ -24,11 +26,9 @@ export class BsDownloaderService { private readonly notificationService: NotificationService; private readonly linkOpener: LinkOpenerService; - private _isVerification: boolean = false; - - public readonly currentBsVersionDownload$: BehaviorSubject = new BehaviorSubject(null); - public readonly downloadProgress$: BehaviorSubject = new BehaviorSubject(0); - public readonly selectedBsVersion$: BehaviorSubject = new BehaviorSubject(null); + private readonly isVerification$ = new BehaviorSubject(false); + public readonly currentBsVersionDownload$ = new BehaviorSubject(null); + public readonly downloadProgress$ = new BehaviorSubject(0); public static getInstance(): BsDownloaderService { if (!BsDownloaderService.instance) { @@ -45,81 +45,19 @@ export class BsDownloaderService { this.progressBarService = ProgressBarService.getInstance(); this.notificationService = NotificationService.getInstance(); this.linkOpener = LinkOpenerService.getInstance(); - this.asignListerners(); - } - private asignListerners(): void { - this.ipcService - .watch("bs-download.[Progress]") - .pipe(distinctUntilChanged()) - .subscribe(response => this.downloadProgress$.next(response.data)); - - this.ipcService - .watch("bs-download.[SteamID]") - .pipe(filter(r => r.success && !!r.data)) - .subscribe(response => this.authService.setSteamID(response.data)); - - this.ipcService - .watch("bs-download.[Warning]") - .pipe( - filter(v => !!v && !!v.data), - distinctUntilChanged(), - throttleTime(10_000) - ) - .subscribe(warning => { - this.notificationService.notifyWarning({ title: "notifications.types.warning", desc: `notifications.bs-download.warnings.msg.${warning.data}` }); - }); - - this.ipcService - .watch("bs-download.[Error]") - .pipe( - filter(v => !!v && !!v.data), - distinctUntilChanged(), - throttleTime(1000) - ) - .subscribe(err => { - this.notificationService.notifyError({ title: "notifications.types.error", desc: `notifications.bs-download.errors.msg.${err.data}` }); - }); - - this.ipcService.watch("bs-download.[2FA]").subscribe(async response => { - if (!response.success) { - this.ipcService.sendLazy("bs-download.kill"); - return; + this.currentBsVersionDownload$.pipe(distinctUntilChanged(equal)).subscribe(version => { + if(version) { + this.bsVersionManager.setInstalledVersions([...this.bsVersionManager.installedVersions$.value, version ]); } - const res = await this.modalService.openModal(GuardModal); - if (res.exitCode !== ModalExitCode.COMPLETED) { - this.progressBarService.hide(true); - this.ipcService.sendLazy("bs-download.kill"); - return; - } - this.ipcService.sendLazy("bs-download.[2FA]", { args: res.data }); - }); - - this.ipcService.watch("start-download-version").subscribe(res => { - this.currentBsVersionDownload$.next(res.data); - }); - - this.currentBsVersionDownload$.subscribe(version => { - if (version) { - this.bsVersionManager.setInstalledVersions([...this.bsVersionManager.installedVersions$.value, version]); - } else { + else { this.bsVersionManager.askInstalledVersions(); } }); } - private resetDownload(): void { - this.currentBsVersionDownload$.next(null); - this.downloadProgress$.next(0); - this.selectedBsVersion$.next(null); - } - - public cancelDownload(): Promise> { - return this.ipcService.send("bs-download.kill"); - } - public isDotNet6Installed(): Promise { - return this.ipcService.send("is-dotnet-6-installed").then(res => res.success && res.data); + return lastValueFrom(this.ipcService.sendV2("is-dotnet-6-installed")); } private async showDotNetNotInstalledError(): Promise { @@ -135,66 +73,16 @@ export class BsDownloaderService { } } - public async download(bsVersion: BSVersion, isVerification?: boolean, isFirstCall = true): Promise> { - // TODO : to remake cause we don't need recursion anymore (will be rework with qr code) - - if (isFirstCall && !this.progressBarService.require()) { - return { success: false }; - } - - if (isFirstCall && !(await this.isDotNet6Installed())) { - await this.showDotNetNotInstalledError(); - return { success: false }; - } - - this.progressBarService.show(this.downloadProgress$); - this._isVerification = isVerification; - - let promise; - if (!this.authService.sessionExist()) { - const res = await this.modalService.openModal(LoginModal); - if (res.exitCode !== ModalExitCode.COMPLETED) { - this.progressBarService.hide(true); - return { success: false }; - } - this.authService.setSteamSession(res.data.username, res.data.stay); - promise = this.ipcService.send("bs-download.start", { args: { bsVersion, username: res.data.username, password: res.data.password, stay: res.data.stay, isVerification } }); - } else { - promise = this.ipcService.send("bs-download.start", { args: { bsVersion, username: this.authService.getSteamUsername(), isVerification } }); - } - - let res = await promise; - - if (res.data?.type === "[Password]") { - this.authService.deleteSteamSession(); - res = await this.download(bsVersion, isVerification, false); - } - - this.progressBarService.hide(true); - this.resetDownload(); - if (res.success && isFirstCall) { - this.notificationService.notifySuccess({ title: `notifications.bs-download.success.titles.${isVerification ? "verification-finished" : "download-success"}`, duration: 3000 }); - } else if (res.data === "dotnet" && isFirstCall) { - await this.showDotNetNotInstalledError(); - return res; - } else if (res.data && isFirstCall) { - this.notificationService.notifyError({ title: `notifications.types.error`, desc: `notifications.bs-download.errors.msg.${res.data}`, duration: 3000 }); - } - - return res; - } - public get isDownloading(): boolean { return !!this.currentBsVersionDownload$.value; } public async getInstallationFolder(): Promise { - const res = await this.ipcService.send("bs-download.installation-folder"); - return res.success ? res.data : ""; + return lastValueFrom(this.ipcService.sendV2("bs-download.installation-folder")); } public get isVerification(): boolean { - return this._isVerification; + return this.isVerification$.value; } public setInstallationFolder(path: string): Observable { @@ -202,7 +90,189 @@ export class BsDownloaderService { } public async importVersion(pathToImport: string): Promise { - const res = await this.ipcService.send("bs-download.import-version", { args: pathToImport }); - return res.success; + return lastValueFrom(this.ipcService.sendV2("bs-download.import-version", { args: pathToImport })) + .then(() => true) + .catch(() => false); + } + + // ### Downloading + + private handleInfoEvents(events$: Observable): Subscription[] { + const subs: Subscription[] = []; + + subs.push(events$.pipe( + filter(event => event.subType === DepotDownloaderInfoEvent.Start), + take(1), + map(event => event.data as string), + ).subscribe(startData => { + const downloadVersion = JSON.parse(startData) as BSVersion; + if(typeof downloadVersion === "object" && downloadVersion?.BSVersion){ + this.currentBsVersionDownload$.next(downloadVersion); + } + })); + + subs.push(events$.pipe( + filter(event => event.subType === DepotDownloaderInfoEvent.Progress || event.subType === DepotDownloaderInfoEvent.Validated), + map(event => event.data as string), + ).subscribe(progress => { + this.downloadProgress$.next(parseFloat(progress.replaceAll(",", "."))); + })); + + subs.push(events$.pipe( + filter(event => event.subType === DepotDownloaderInfoEvent.MobileApp), + take(1), + ).subscribe(async () => { + const logged$ = events$.pipe(filter(event => event.subType === DepotDownloaderInfoEvent.SteamID), take(1)); + // Show awaiting mobile confirmation modal + })); + + subs.push(events$.pipe( + filter(event => event.subType === DepotDownloaderInfoEvent.TwoFA || event.subType === DepotDownloaderInfoEvent.Guard), + take(1), + map(event => event.subType), + ).subscribe(async () => { + const res = await this.modalService.openModal(GuardModal); + if(res.exitCode !== ModalExitCode.COMPLETED){ + return this.stopDownload(); + } + this.sendInput(res.data); + })); + + subs.push(events$.pipe( + filter(event => event.subType === DepotDownloaderInfoEvent.Finished), + take(1), + ).subscribe(() => { + console.log("show success notification"); + })); + + return subs; + } + + private handleWarningEvents(events$: Observable): Subscription[] { + const subs: Subscription[] = []; + return subs; + } + + private hanndleErrorEvents(events$: Observable): Subscription[] { + const subs: Subscription[] = []; + return subs; + } + + private wrapDownload(download$: Observable): Observable { + + return new Observable(sub => { + + const downloadSub = download$.subscribe(sub); + + const subs = [ + ...this.handleInfoEvents(download$.pipe(filter(event => event.type === DepotDownloaderEventType.Info))), + ...this.handleWarningEvents(download$.pipe(filter(event => event.type === DepotDownloaderEventType.Warning))), + ...this.hanndleErrorEvents(download$.pipe(filter(event => event.type === DepotDownloaderEventType.Error))) + ]; + + return () => { + downloadSub.unsubscribe(); + subs.forEach(sub => sub.unsubscribe()); + } + + }).pipe( + tap({error: () => this.authService.deleteSteamSession()}) + ); + + } + + private tryAutoDownloadBsVersion(downloadInfo: DownloadInfo){ + + if(!this.authService.sessionExist()){ + return throwError(() => new Error("No session")); + } + + const infos: DownloadInfo = {...downloadInfo, username: this.authService.getSteamUsername()} + return this.wrapDownload( + this.ipcService.sendV2("auto-download-bs-version", { args: infos }) + ); + } + + private startDownload(downloadInfo: DownloadInfo){ + return this.wrapDownload( + this.ipcService.sendV2("download-bs-version", { args: downloadInfo }) + ); + } + + private startQrCodeDownload(downloadInfo: DownloadInfo){ + return this.wrapDownload( + this.ipcService.sendV2("download-bs-version-qr", { args: downloadInfo }) + ); + } + + private doDownloadBsVersion(bsVersion: BSVersion, isVerification?: boolean): Promise{ + + if(!this.progressBarService.require()){ + return Promise.resolve(); + } + + this.progressBarService.show(this.downloadProgress$, true); + this.isVerification$.next(isVerification); + + const downloadPromise = (async () => { + + const haveDotNet = await this.isDotNet6Installed().catch(() => false); + if(!haveDotNet){ + this.showDotNetNotInstalledError(); + return Promise.reject(new Error("DotNet not installed")); + } + + const downloadInfo: DownloadInfo = {bsVersion, isVerification} + + const autoDownload = await lastValueFrom(this.tryAutoDownloadBsVersion(downloadInfo)).then(() => true).catch(() => false); + + if(autoDownload){ return Promise.resolve(); } + + const qrCodeDownload$ = this.startQrCodeDownload(downloadInfo); + const qrCode$ = qrCodeDownload$.pipe(filter(event => event.type === DepotDownloaderEventType.Info && event.subType === DepotDownloaderInfoEvent.QRCode), map(event => event.data as string)); + const logged$ = qrCodeDownload$.pipe(filter(event => event.type === DepotDownloaderEventType.Info && event.subType === DepotDownloaderInfoEvent.SteamID), map(event => event.data as string), take(1)); + + const loginRes = await this.modalService.openModal(LoginModal, { qrCode$, logged$ }); + + if(loginRes.exitCode !== ModalExitCode.COMPLETED){ + return Promise.resolve(); + } + + if(loginRes.data.stay){ + this.authService.setSteamSession(loginRes.data.username); + } + + const download$ = loginRes.data.method === "qr" ? qrCodeDownload$ : this.startDownload({...downloadInfo, username: loginRes.data.username, password: loginRes.data.password, stay: loginRes.data.stay}); + + return lastValueFrom(download$); + + })(); + + // *** TEST EXPIRATION MOBILE APP APROVAL *** + + return downloadPromise.then(() => {}).finally(() => { + this.downloadProgress$.next(0); + this.currentBsVersionDownload$.next(null); + this.progressBarService.hide(true); + this.isVerification$.next(false); + this.bsVersionManager.askAvailableVersions(); + }); + + } + + private sendInput(input: string){ + return lastValueFrom(this.ipcService.sendV2("send-input-bs-download", { args: input })); + } + + public downloadBsVersion(version: BSVersion): Promise { + return this.doDownloadBsVersion(version).then(() => version); + } + + public verifyBsVersion(version: BSVersion): Promise { + return this.doDownloadBsVersion(version, true).then(() => version); + } + + public stopDownload(): Promise{ + return lastValueFrom(this.ipcService.sendV2("stop-download-bs-version")); } } diff --git a/src/renderer/services/bs-version-manager.service.ts b/src/renderer/services/bs-version-manager.service.ts index 5da004da..d67551d5 100644 --- a/src/renderer/services/bs-version-manager.service.ts +++ b/src/renderer/services/bs-version-manager.service.ts @@ -64,18 +64,10 @@ export class BSVersionManagerService { }); } - public getAvailableYears(): string[] { - return [...new Set(this.availableVersions$.value.map(v => v.year))].sort((a, b) => b.localeCompare(a)); - } - public isVersionInstalled(version: BSVersion): boolean { return !!this.getInstalledVersions().find(v => v.BSVersion === version.BSVersion && v.steam === version.steam && v.oculus === version.oculus); } - public getAvaibleVersionsOfYear(year: string): BSVersion[] { - return this.availableVersions$.value.filter(v => v.year === year).sort((a, b) => +b.ReleaseDate - +a.ReleaseDate); - } - public async editVersion(version: BSVersion): Promise { const modalRes = await this.modalService.openModal(EditVersionModal, { version, clone: false }); if (modalRes.exitCode !== ModalExitCode.COMPLETED) { diff --git a/src/renderer/services/ipc.service.ts b/src/renderer/services/ipc.service.ts index 2e22bba9..39045e86 100644 --- a/src/renderer/services/ipc.service.ts +++ b/src/renderer/services/ipc.service.ts @@ -1,6 +1,7 @@ -import { defaultIfEmpty, shareReplay } from "rxjs/operators"; -import { Observable, identity } from "rxjs"; +import { defaultIfEmpty, share } from "rxjs/operators"; +import { Observable, ReplaySubject, identity } from "rxjs"; import { IpcRequest, IpcResponse } from "shared/models/ipc"; +import { IpcCompleteChannel, IpcErrorChannel, IpcTearDownChannel } from "shared/models/ipc/ipc-response.interface"; export class IpcService { private static instance: IpcService; @@ -65,26 +66,31 @@ export class IpcService { if (!request) { request = { args: null, responceChannel: null }; } - if (!request.responceChannel) { + if (!request.responceChannel) { // TODO : Remove this request.responceChannel = `${channel}_responce_${crypto.randomUUID()}`; } - const completeChannel = `${request.responceChannel}_complete`; - const errorChannel = `${request.responceChannel}_error`; + const completeChannel: IpcCompleteChannel = `${request.responceChannel}_complete`; + const errorChannel: IpcErrorChannel = `${request.responceChannel}_error`; + const teardownChannel: IpcTearDownChannel = `${request.responceChannel}_teardown`; const obs = new Observable(observer => { window.electron.ipcRenderer.on(request.responceChannel, (res: T) => observer.next(res)); window.electron.ipcRenderer.on(errorChannel, (err: Error) => observer.error(err)); window.electron.ipcRenderer.on(completeChannel, () => observer.complete()); - }).pipe(defaultValue ? defaultIfEmpty(defaultValue) : identity, shareReplay(1)); - window.electron.ipcRenderer.once(completeChannel, () => { - window.electron.ipcRenderer.removeAllListeners(request.responceChannel); - window.electron.ipcRenderer.removeAllListeners(errorChannel); - window.electron.ipcRenderer.removeAllListeners(completeChannel); - }); + window.electron.ipcRenderer.sendMessage(channel, request); - window.electron.ipcRenderer.sendMessage(channel, request); + console.log("Subscribe IPC", request.responceChannel, request.args); // TODO : Remove this + + return () => { + console.log("Unsubscribe IPC", request.responceChannel, request.args); // TODO : Remove this + window.electron.ipcRenderer.removeAllListeners(request.responceChannel); + window.electron.ipcRenderer.removeAllListeners(errorChannel); + window.electron.ipcRenderer.removeAllListeners(completeChannel); + window.electron.ipcRenderer.sendMessage(teardownChannel, null); + }; + }).pipe(defaultValue ? defaultIfEmpty(defaultValue) : identity, share({connector: () => new ReplaySubject(1)})); return obs; } diff --git a/src/shared/models/depot-downloader.model.ts b/src/shared/models/depot-downloader.model.ts new file mode 100644 index 00000000..01f5ecd8 --- /dev/null +++ b/src/shared/models/depot-downloader.model.ts @@ -0,0 +1,72 @@ +export enum DepotDownloaderEventType { + Error = "Error", + Warning = "Warning", + Info = "Info", + +} + +export interface DepotDownloaderEvent { + type: DepotDownloaderEventType; + subType: DepotDownloaderEventTypes; + data?: T; +} + +export type DepotDownloaderEventTypes = DepotDownloaderErrorEvent | DepotDownloaderWarningEvent | DepotDownloaderInfoEvent; + +export enum DepotDownloaderInfoEvent { + Start = "Start", + Password = "Password", + Guard = "Guard", + TwoFA = "2FA", + Progress = "Progress", + Validated = "Validated", + Finished = "Finished", + SteamID = "SteamID", + QRCode = "QRCode", + MobileApp = "MobileApp", +} + +export enum DepotDownloaderErrorEvent { + Password = "Password", + InvalidCredentials = "InvalidCredentials", + NoManifest = "NoManifest", + DirectoryCreate = "DirectoryCreate", + NotAvailableApp = "NotAvailableApp", + DepotNotFound = "DepotNotFound", + NotCompleted = "NotCompleted", + InvalidManifest = "InvalidManifest", + NoValidKeys = "NoValidKeys", + NoManifestCode = "NoManifestCode", + _401 = "401", + _404 = "404", + NoServer = "NoServer", + SteamLib = "SteamLib", + NotAllowed = "NotAllowed", + ConnectionTimeout = "ConnectionTimeout", + ConnectionError = "ConnectionError", + TokenRejected = "TokenRejected", + LicenceError = "LicenceError", + Unknown = "Unknown", +} + +export enum DepotDownloaderWarningEvent { + ManifestChecksum = "ManifestChecksum", +} + +export const DepotDownloaderSubTypeOfEventType = { + [DepotDownloaderEventType.Error]: DepotDownloaderErrorEvent, + [DepotDownloaderEventType.Warning]: DepotDownloaderWarningEvent, + [DepotDownloaderEventType.Info]: DepotDownloaderInfoEvent, +} + +export interface DepotDownloaderArgsOptions { + app: number|string, + depot: number|string, + manifest: number|string, + username?: string, + password?: string, + "remember-password"?: boolean, + dir: string, + validate?: boolean, + qr?: boolean, +} \ No newline at end of file diff --git a/src/shared/models/ipc/ipc-response.interface.ts b/src/shared/models/ipc/ipc-response.interface.ts index 953c5484..d1ce20f8 100644 --- a/src/shared/models/ipc/ipc-response.interface.ts +++ b/src/shared/models/ipc/ipc-response.interface.ts @@ -6,5 +6,6 @@ export interface IpcResponse { success: boolean; } -export type IpcErrorChannel = string & `${string}_error`; -export type IpcCompleteChannel = string & `${string}_complete`; +export type IpcErrorChannel = `${string}_error`; +export type IpcCompleteChannel = `${string}_complete`; +export type IpcTearDownChannel = `${string}_teardown`;