diff --git a/assets/jsons/translations/de.json b/assets/jsons/translations/de.json index f3edef0c..9c91e605 100644 --- a/assets/jsons/translations/de.json +++ b/assets/jsons/translations/de.json @@ -14,7 +14,8 @@ "refuse": "Ablehnen", "apply": "Übernehmen", "copy": "Kopieren", - "copied": "Kopiert!" + "copied": "Kopiert!", + "confirm": "Bestätigen" }, "nav-bar": { "add-version": "Version hinzufügen", @@ -138,7 +139,7 @@ }, "installation-folder": { "title": "Installations-Ordner", - "description": "Ändere den Standardordner für Beat Saber-Versionen und andere kommende Funktionen.", + "description": "Ändern Sie den Ordner, der alle von BSManager heruntergeladenen Inhalte enthalten wird.", "choose-folder": "Ordner wählen" }, "additional-content": { @@ -767,6 +768,13 @@ }, "launch-as-admin": "Als Administrator starten", "not-remind-me": "Nicht mehr erinnern" + }, + "ask-install-path": { + "title": "Installations Ordner", + "choose-folder-description": "Wählen Sie den Ordner aus, der alle von BSManager heruntergeladenen Inhalte enthalten soll. (Versionen, Mods, Karten, Playlists, etc.)", + "choose-folder": "Ordner Wählen", + "default": "Standard", + "default-tooltip": "Standardmäßig in Ihrem persönlichen Ordner" } }, "maps": { diff --git a/assets/jsons/translations/en.json b/assets/jsons/translations/en.json index 3d9b1392..fcfdaa82 100644 --- a/assets/jsons/translations/en.json +++ b/assets/jsons/translations/en.json @@ -14,7 +14,8 @@ "refuse": "Refuse", "apply": "Apply", "copy": "Copy", - "copied": "Copied!" + "copied": "Copied!", + "confirm": "Confirm" }, "nav-bar": { "add-version": "Add a version", @@ -138,7 +139,7 @@ }, "installation-folder": { "title": "Installation folder", - "description": "Change the default folder for Beat Saber versions and other upcoming features.", + "description": "Change the folder that will contain all the content downloaded by BSManager.", "choose-folder": "Choose folder" }, "proton-path": { @@ -774,6 +775,13 @@ }, "launch-as-admin": "Launch as Administrator", "not-remind-me": "Do not remind me" + }, + "ask-install-path": { + "title": "Installation folder", + "choose-folder-description": "Choose the folder that will contain all the content downloaded by BSManager. (versions, mods, maps, playlists, etc.)", + "choose-folder": "Choose folder", + "default": "Default", + "default-tooltip": "Defaults to your home folder" } }, "maps": { diff --git a/assets/jsons/translations/es.json b/assets/jsons/translations/es.json index e3122355..9494b412 100644 --- a/assets/jsons/translations/es.json +++ b/assets/jsons/translations/es.json @@ -14,7 +14,8 @@ "refuse": "Rechazar", "apply": "Aplicar", "copy": "Copiar", - "copied": "¡Copiado!" + "copied": "¡Copiado!", + "confirm": "Confirmar" }, "nav-bar": { "add-version": "Agregar una versión", @@ -138,7 +139,7 @@ }, "installation-folder": { "title": "Carpeta de instalación", - "description": "Cambia la carpeta por defecto para las versiones de Beat Saber y próximas funciones.", + "description": "Cambiar la carpeta que contendrá todo el contenido descargado por BSManager.", "choose-folder": "Elige la carpeta" }, "additional-content": { @@ -767,6 +768,13 @@ }, "launch-as-admin": "Iniciar como Administrador", "not-remind-me": "No volver a recordármelo" + }, + "ask-install-path": { + "title": "Carpeta de instalación", + "choose-folder-description": "Elija la carpeta que contendrá todo el contenido descargado por BSManager. (versiones, mods, mapas, listas de reproducción, etc.)", + "choose-folder": "Elige la carpeta", + "default": "Predeterminado", + "default-tooltip": "Por defecto, en su carpeta personal" } }, "maps": { diff --git a/assets/jsons/translations/fr.json b/assets/jsons/translations/fr.json index d2748d53..2a4c5e14 100644 --- a/assets/jsons/translations/fr.json +++ b/assets/jsons/translations/fr.json @@ -14,7 +14,8 @@ "refuse": "Refuser", "apply": "Appliquer", "copy": "Copier", - "copied": "Copié !" + "copied": "Copié !", + "confirm": "Confirmer" }, "nav-bar": { "add-version": "Ajouter une version", @@ -138,7 +139,7 @@ }, "installation-folder": { "title": "Dossier d'installation", - "description": "Change le dossier par défaut pour les versions de Beat Saber et d'autres fonctionnalités à venir.", + "description": "Changer le dossier qui contiendra tout le contenu téléchargé par BSManager.", "choose-folder": "Choisir un dossier" }, "additional-content": { @@ -768,6 +769,13 @@ }, "launch-as-admin": "Lancer en administateur", "not-remind-me": "Ne plus me rappeler" + }, + "ask-install-path": { + "title": "Dossier d'installation", + "choose-folder-description": "Choisissez le dossier qui contiendra tout le contenu téléchargé par BSManager. (versions, mods, cartes, playlists, etc.)", + "choose-folder": "Choisir un dossier", + "default": "Par défaut", + "default-tooltip": "Par défaut, dans votre dossier personnel" } }, "maps": { diff --git a/assets/jsons/translations/ja.json b/assets/jsons/translations/ja.json index 948f31e8..5fb10e10 100644 --- a/assets/jsons/translations/ja.json +++ b/assets/jsons/translations/ja.json @@ -14,7 +14,8 @@ "refuse": "拒否する", "apply": "適用", "copy": "コピー", - "copied": "コピー済み!" + "copied": "コピー済み!", + "confirm": "確認" }, "nav-bar": { "add-version": "バージョンを追加", @@ -138,7 +139,7 @@ }, "installation-folder": { "title": "インストールフォルダー", - "description": "BeatSaberのバージョンとその他の今後の機能を入れるフォルダを変更します。", + "description": "BSManager によってダウンロードされたすべてのコンテンツを含むフォルダを変更します。", "choose-folder": "フォルダーを選択" }, "additional-content": { @@ -767,6 +768,13 @@ }, "launch-as-admin": "管理者として起動", "not-remind-me": "Больше не напоминать" + }, + "ask-install-path": { + "title": "インストールフォルダー", + "choose-folder-description": "BSManager によってダウンロードされたすべてのコンテンツ (バージョン、MOD、マップ、プレイリストなど) を含むフォルダを選択してください。", + "choose-folder": "フォルダーを選択", + "default": "デフォルト", + "default-tooltip": "デフォルトでは、ホームフォルダに設定されます" } }, "maps": { diff --git a/assets/jsons/translations/ru.json b/assets/jsons/translations/ru.json index c246de4e..4442078a 100644 --- a/assets/jsons/translations/ru.json +++ b/assets/jsons/translations/ru.json @@ -14,7 +14,8 @@ "refuse": "Отказаться", "apply": "Применить", "copy": "Скопировать", - "copied": "Скопировано!" + "copied": "Скопировано!", + "confirm": "Подтвердить" }, "nav-bar": { "add-version": "Добавить версию игры", @@ -138,7 +139,7 @@ }, "installation-folder": { "title": "Папка установок", - "description": "Измените стандартную папку, где будут версии Beat Saber и прочее.", + "description": "Изменить папку, которая будет содержать весь контент, загруженный BSManager.", "choose-folder": "Изменить папку" }, "additional-content": { @@ -766,6 +767,13 @@ "info-3": "Обратите внимание, что не рекомендуется предоставлять права администратора Steam, так как это также влияет на установленные игры и моды, представляя угрозу безопасности." }, "launch-as-admin": "Запустить от имени администратора" + }, + "ask-install-path": { + "title": "Папка установок", + "choose-folder-description": "Выберите папку, которая будет содержать весь контент, загруженный BSManager. (версии, моды, карты, плейлисты и т.д.)", + "choose-folder": "Изменить папку", + "default": "По умолчанию", + "default-tooltip": "По умолчанию в вашей домашней папке" } }, "maps": { diff --git a/assets/jsons/translations/zh-tw.json b/assets/jsons/translations/zh-tw.json index 95ab1ff8..ce36d636 100644 --- a/assets/jsons/translations/zh-tw.json +++ b/assets/jsons/translations/zh-tw.json @@ -14,7 +14,8 @@ "refuse": "拒絕", "apply": "應用", "copy": "複製", - "copied": "已複製!" + "copied": "已複製!", + "confirm": "確認" }, "nav-bar": { "add-version": "新增版本", @@ -138,7 +139,7 @@ }, "installation-folder": { "title": "安裝文件夾", - "description": "為 BeatSaber 不同版本及未來其他特性修改預設文件夾", + "description": "更改將包含 BSManager 下載的所有內容的文件夾。", "choose-folder": "選擇文件夾" }, "additional-content": { @@ -767,6 +768,13 @@ }, "launch-as-admin": "以管理員身份啟動", "not-remind-me": "不再提醒我" + }, + "ask-install-path": { + "title": "安裝文件夾", + "choose-folder-description": "選擇將包含 BSManager 下載的所有內容的文件夾。(版本、mod、地圖、播放列表等)", + "choose-folder": "選擇文件夾", + "default": "預設", + "default-tooltip": "預設為您的主資料夾" } }, "maps": { diff --git a/assets/jsons/translations/zh.json b/assets/jsons/translations/zh.json index b052d4c2..8be7a687 100644 --- a/assets/jsons/translations/zh.json +++ b/assets/jsons/translations/zh.json @@ -14,7 +14,8 @@ "refuse": "拒绝", "apply": "应用", "copy": "复制", - "copied": "已复制!" + "copied": "已复制!", + "confirm": "确认" }, "nav-bar": { "add-version": "添加版本", @@ -138,7 +139,7 @@ }, "installation-folder": { "title": "安装文件夹", - "description": "为 BeatSaber 不同版本及未来其他特性修改默认文件夹", + "description": "更改将包含 BSManager 下载的所有内容的文件夹。", "choose-folder": "选择文件夹" }, "additional-content": { @@ -767,6 +768,13 @@ }, "launch-as-admin": "以管理员身份启动", "not-remind-me": "不再提醒我" + }, + "ask-install-path": { + "title": "安装文件夹", + "choose-folder-description": "选择将包含 BSManager 下载的所有内容的文件夹。(版本、mod、地图、播放列表等)", + "choose-folder": "选择文件夹", + "default": "默认", + "default-tooltip": "默认为您的主文件夹" } }, "maps": { diff --git a/src/main/ipcs/bs-installer-ipcs.ts b/src/main/ipcs/bs-installer-ipcs.ts new file mode 100644 index 00000000..893b697e --- /dev/null +++ b/src/main/ipcs/bs-installer-ipcs.ts @@ -0,0 +1,27 @@ +import { pathExistsSync } from "fs-extra"; +import { from, of } from "rxjs"; + +import { InstallationLocationService } from "main/services/installation-location.service"; +import { IpcService } from "main/services/ipc.service"; + +const ipc = IpcService.getInstance(); + +ipc.on("bs-installer.folder-exists", (_, reply) => { + const service = InstallationLocationService.getInstance(); + reply(of(pathExistsSync(service.installationDirectory()))); +}); + +ipc.on("bs-installer.default-install-path", (_, reply) => { + const service = InstallationLocationService.getInstance(); + reply(of(service.defaultInstallationDirectory())); +}); + +ipc.on("bs-installer.install-path", (_, reply) => { + const service = InstallationLocationService.getInstance(); + reply(of(service.installationDirectory())); +}); + +ipc.on("bs-installer.set-install-path", (args, reply) => { + const service = InstallationLocationService.getInstance(); + reply(from(service.setInstallationDirectory(args.path, args.move))); +}); diff --git a/src/main/ipcs/bs-version-download/bs-download-ipcs.ts b/src/main/ipcs/bs-version-download/bs-download-ipcs.ts index c27c591d..dfc4e682 100644 --- a/src/main/ipcs/bs-version-download/bs-download-ipcs.ts +++ b/src/main/ipcs/bs-version-download/bs-download-ipcs.ts @@ -1,8 +1,7 @@ import { BsOculusDownloaderService } from "../../services/bs-version-download/bs-oculus-downloader.service"; import { BsSteamDownloaderService } from "../../services/bs-version-download/bs-steam-downloader.service"; -import { InstallationLocationService } from "../../services/installation-location.service"; import { IpcService } from "../../services/ipc.service"; -import { from, of } from "rxjs"; +import { of } from "rxjs"; import { BSLocalVersionService } from "../../services/bs-local-version.service"; const ipc = IpcService.getInstance(); @@ -14,16 +13,6 @@ ipc.on("import-version", (args, reply) => { // #region Steam -ipc.on("bs-download.installation-folder", (_, reply) => { - const installLocation = InstallationLocationService.getInstance(); - reply(of(installLocation.installationDirectory())); -}); - -ipc.on("bs-download.set-installation-folder", (args, reply) => { - const installerService = InstallationLocationService.getInstance(); - reply(from(installerService.setInstallationDirectory(args))); -}); - ipc.on("auto-download-bs-version", (args, reply) => { const bsInstaller = BsSteamDownloaderService.getInstance(); reply(bsInstaller.autoDownloadBsVersion(args)); diff --git a/src/main/ipcs/index.ts b/src/main/ipcs/index.ts index 38384c27..f80cf9f8 100644 --- a/src/main/ipcs/index.ts +++ b/src/main/ipcs/index.ts @@ -1,4 +1,5 @@ import "./os-controls-ipcs"; +import "./bs-installer-ipcs.ts"; import "./bs-launcher-ipcs"; import "./bs-version-ipcs"; import "./bs-uninstall-ipcs"; diff --git a/src/main/preload.ts b/src/main/preload.ts index 243f5e76..a4958a7f 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -24,6 +24,9 @@ contextBridge.exposeInMainWorld("electron", { }, path: { sep, + basename: (path: string): string => { + return !path ? "" : path.split(sep).at(-1); + }, join: (...args: string[]): string => { return args.join(sep); } diff --git a/src/main/services/configuration.service.ts b/src/main/services/configuration.service.ts index 9049a009..c1270769 100644 --- a/src/main/services/configuration.service.ts +++ b/src/main/services/configuration.service.ts @@ -1,5 +1,7 @@ import ElectronStore from "electron-store"; +import fs from "fs-extra"; import { InstallationLocationService } from "./installation-location.service"; +import { CustomError } from "shared/models/exceptions/custom-error.class"; export class ConfigurationService { private static instance: ConfigurationService; @@ -13,17 +15,23 @@ export class ConfigurationService { private readonly locations: InstallationLocationService; + private contentPath: string; private store: ElectronStore; private constructor() { this.locations = InstallationLocationService.getInstance(); - this.initStore(); + this.initStore(false); - this.locations.onInstallLocationUpdate(() => { this.initStore() }); + this.locations.onInstallLocationUpdate(() => this.initStore(true)); } - private async initStore() { + private async initStore(createFolder: boolean) { const contentPath = this.locations.installationDirectory(); + if (!createFolder && !fs.pathExistsSync(contentPath)) { + return; + } + + this.contentPath = contentPath; this.store = new ElectronStore({ cwd: contentPath, name: "config", @@ -32,15 +40,25 @@ export class ConfigurationService { }); } + private checkStore(): void { + // Can be null if config.cfg does not exist or corrupted + if (!this.store) { + throw CustomError.fromError(new Error(`Can't read config.cfg on ${this.contentPath}`)); + } + } + public set(key: string, value: unknown): void { + this.checkStore(); this.store.set(key, value); } public get(key: string): T { + this.checkStore(); return this.store.get(key) as T; } public delete(key: string): void { + this.checkStore(); this.store.delete(key); } } diff --git a/src/main/services/installation-location.service.ts b/src/main/services/installation-location.service.ts index f1c012f1..6acb0ace 100644 --- a/src/main/services/installation-location.service.ts +++ b/src/main/services/installation-location.service.ts @@ -40,18 +40,22 @@ export class InstallationLocationService { this.updateListeners.forEach(listener => listener()); } - public async setInstallationDirectory(newDir: string): Promise { + /** + * @param move - if true, move the old installation path to the path param + */ + public async setInstallationDirectory(newDir: string, move: boolean): Promise { newDir = path.basename(newDir) === this.INSTALLATION_FOLDER ? path.join(newDir, "..") : newDir; - const oldDir = this.installationDirectory(); - await ensureFolderExist(oldDir); - await copyDirectoryWithJunctions(oldDir, path.join(newDir, this.INSTALLATION_FOLDER), { overwrite: true }); + if (move) { + const oldDir = this.installationDirectory(); + await ensureFolderExist(oldDir); + await copyDirectoryWithJunctions(oldDir, path.join(newDir, this.INSTALLATION_FOLDER), { overwrite: true }); + deleteFolder(oldDir); + } this._installationDirectory = newDir; this.staticConfig.set(this.STORE_INSTALLATION_PATH_KEY, newDir); - deleteFolder(oldDir); - return this.installationDirectory(); } @@ -59,6 +63,12 @@ export class InstallationLocationService { this.updateListeners.add(fn); } + public defaultInstallationDirectory(): string { + const { result: oldPath } = tryit(() => path.join(app.getPath("documents"), this.INSTALLATION_FOLDER)); + const installationDirectory = (oldPath && pathExistsSync(oldPath)) ? app.getPath("documents") : app.getPath("home"); + return path.join(installationDirectory, this.INSTALLATION_FOLDER); + } + public installationDirectory(): string { const installParentPath = () => { diff --git a/src/renderer/components/modal/modal-types/ask-install-path.component.tsx b/src/renderer/components/modal/modal-types/ask-install-path.component.tsx new file mode 100644 index 00000000..4df0691d --- /dev/null +++ b/src/renderer/components/modal/modal-types/ask-install-path.component.tsx @@ -0,0 +1,109 @@ +import { lastValueFrom } from "rxjs"; +import { useEffect, useState } from "react"; +import { useTranslation } from "renderer/hooks/use-translation.hook"; +import { useService } from "renderer/hooks/use-service.hook"; +import Tippy from "@tippyjs/react"; + +import { IpcService } from "renderer/services/ipc.service"; +import { ModalComponent, ModalExitCode } from "renderer/services/modale.service"; + +import { BsmButton } from "renderer/components/shared/bsm-button.component"; + +export const AskInstallPathModal: ModalComponent<{ installPath: string }, {}> = ({ resolver }) => { + + const t = useTranslation(); + const ipcService = useService(IpcService); + + const [installPath, setInstallPath] = useState(""); + const [installFolder, setInstallFolder] = useState(""); + const [defaultInstallPath, setDefaultInstallPath] = useState(""); + + useEffect(() => { + lastValueFrom(ipcService.sendV2("bs-installer.default-install-path")) + .then(defaultPath => { + setInstallPath(defaultPath); + setDefaultInstallPath(defaultPath); + setInstallFolder(window.electron.path.basename(defaultPath)); + }); + }, []); + + const selectInstallPath = async () => { + const response = await lastValueFrom(ipcService.sendV2("choose-folder")); + if (response.canceled || !response.filePaths?.length) { + return; + } + + const path = response.filePaths[0]; + setInstallPath( + window.electron.path.basename(path) === installFolder ? + path : + window.electron.path.join(response.filePaths[0], installFolder) + ); + } + + const onDefaultButtonPressed = () => { + setInstallPath(defaultInstallPath); + } + + const onConfirmButtonPressed = () => { + resolver({ + data: { installPath }, + exitCode: ModalExitCode.COMPLETED + }); + } + + return ( +
{ + event.preventDefault(); + onConfirmButtonPressed(); + }}> + +

+ {t("modals.ask-install-path.title")} +

+ +

+ {t("modals.ask-install-path.choose-folder-description")} +

+ +
+ + {installPath} + + +
+ +
+ + + + +
+
+ ) +} diff --git a/src/renderer/components/modal/modal.component.tsx b/src/renderer/components/modal/modal.component.tsx index fe047633..2d06fbe3 100644 --- a/src/renderer/components/modal/modal.component.tsx +++ b/src/renderer/components/modal/modal.component.tsx @@ -17,7 +17,7 @@ export function Modal() { useEffect(() => { const onEscape = (e: KeyboardEvent) => { - if (e.key !== "Escape") { + if (currentModal.options.closable === false || e.key !== "Escape") { return; } currentModal.resolver({ exitCode: ModalExitCode.CLOSED }); @@ -34,6 +34,20 @@ export function Modal() { }; }, [currentModal]); + const renderCloseButton = (modal: ModalObject) => { + return ( +
{ + e.stopPropagation(); + modal.resolver({ exitCode: ModalExitCode.CLOSED }); + }} + > + +
+ ) + } + const renderModal = (modal: ModalObject) => { if (!modal?.modal) { return null; } @@ -44,23 +58,23 @@ export function Modal() { return (
-
{ - e.stopPropagation(); - modal.resolver({ exitCode: ModalExitCode.CLOSED }); - }} - > - -
+ {modal.options?.closable === false ? undefined : renderCloseButton(modal)}
) } + const onOverlayClicked = () => { + if (currentModal.options.closable === false) { + return; + } + + currentModal.resolver({ exitCode: ModalExitCode.NO_CHOICE }); + } + return ( - {currentModal ? currentModal.resolver({ exitCode: ModalExitCode.NO_CHOICE })} className="fixed size-full bg-black z-[90]" initial={{ opacity: 0 }} animate={{ opacity: currentModal && 0.6 }} exit={{ opacity: 0 }} transition={{ duration: 0.2 }} /> : undefined} + {currentModal ? : undefined} {modals?.map(modal => ( {renderModal(modal)} diff --git a/src/renderer/pages/settings-page.component.tsx b/src/renderer/pages/settings-page.component.tsx index ea059fa7..f7a1fd1b 100644 --- a/src/renderer/pages/settings-page.component.tsx +++ b/src/renderer/pages/settings-page.component.tsx @@ -48,6 +48,7 @@ import { SettingToogleSwitchGrid } from "renderer/components/settings/setting-to import { BasicModal } from "renderer/components/modal/basic-modal.component"; import { StaticConfigurationService } from "renderer/services/static-configuration.service"; import { tryit } from "shared/helpers/error.helpers"; +import { InstallationLocationService } from "renderer/services/installation-location.service"; export function SettingsPage() { @@ -68,6 +69,7 @@ export function SettingsPage() { const versionLinker = useService(VersionFolderLinkerService); const autoUpdater = useService(AutoUpdaterService); const staticConfig = useService(StaticConfigurationService); + const installationLocationService = useService(InstallationLocationService); const { firstColor, secondColor } = useThemeColor(); @@ -124,7 +126,7 @@ export function SettingsPage() { }; const loadInstallationFolder = () => { - steamDownloader.getInstallationFolder().then(res => { + installationLocationService.getInstallationFolder().then(res => { setInstallationFolder(res); }); }; @@ -202,7 +204,7 @@ export function SettingsPage() { notificationService.notifySuccess({ title: "notifications.settings.move-folder.success.titles.transfer-started", desc: "notifications.settings.move-folder.success.descs.transfer-started" }); - lastValueFrom(steamDownloader.setInstallationFolder(fileChooserRes.filePaths[0])).then(res => { + lastValueFrom(installationLocationService.setInstallationFolder(fileChooserRes.filePaths[0], true)).then(res => { progressBarService.complete(); progressBarService.hide(true); diff --git a/src/renderer/preload.d.ts b/src/renderer/preload.d.ts index 29aace8b..51f2568b 100644 --- a/src/renderer/preload.d.ts +++ b/src/renderer/preload.d.ts @@ -10,6 +10,7 @@ declare global { }; path: { sep: "/"|"\\"; + basename: (path: string) => string; join: (...args: string[]) => string; }; }; diff --git a/src/renderer/services/bs-version-download/steam-downloader.service.ts b/src/renderer/services/bs-version-download/steam-downloader.service.ts index 56e3e7c3..313a1e4a 100644 --- a/src/renderer/services/bs-version-download/steam-downloader.service.ts +++ b/src/renderer/services/bs-version-download/steam-downloader.service.ts @@ -8,7 +8,6 @@ import { NotificationService } from "../notification.service"; import { ProgressBarService } from "../progress-bar.service"; import { LoginToSteamModal } from "renderer/components/modal/modal-types/bs-downgrade/login-to-steam-modal.component"; import { SteamGuardModal } from "renderer/components/modal/modal-types/bs-downgrade/steam-guard-modal.component"; -import { LinkOpenerService } from "../link-opener.service"; import { DepotDownloaderErrorEvent, DepotDownloaderEvent, DepotDownloaderEventType, DepotDownloaderInfoEvent, DepotDownloaderWarningEvent } from "../../../shared/models/bs-version-download/depot-downloader.model"; import { SteamMobileApproveModal } from "renderer/components/modal/modal-types/bs-downgrade/steam-mobile-approve-modal.component"; import { DownloaderServiceInterface } from "./bs-store-downloader.interface"; @@ -30,7 +29,6 @@ export class SteamDownloaderService extends AbstractBsDownloaderService implemen private readonly ipcService: IpcService; private readonly progressBarService: ProgressBarService; private readonly notificationService: NotificationService; - private readonly linkOpener: LinkOpenerService; private readonly STEAM_SESSION_USERNAME_KEY = "STEAM-USERNAME"; @@ -42,7 +40,6 @@ export class SteamDownloaderService extends AbstractBsDownloaderService implemen this.modalService = ModalService.getInstance(); this.progressBarService = ProgressBarService.getInstance(); this.notificationService = NotificationService.getInstance(); - this.linkOpener = LinkOpenerService.getInstance(); } private setSteamSession(username: string): void { localStorage.setItem(this.STEAM_SESSION_USERNAME_KEY, username); } @@ -50,14 +47,6 @@ export class SteamDownloaderService extends AbstractBsDownloaderService implemen public deleteSteamSession(): void { localStorage.removeItem(this.STEAM_SESSION_USERNAME_KEY); } public sessionExist(): boolean { return !!localStorage.getItem(this.STEAM_SESSION_USERNAME_KEY); } - public async getInstallationFolder(): Promise { - return lastValueFrom(this.ipcService.sendV2("bs-download.installation-folder")); - } - - public setInstallationFolder(path: string): Observable { - return this.ipcService.sendV2("bs-download.set-installation-folder", path); - } - // ### Downloading private handleInfoEvents(events$: Observable): Subscription[] { diff --git a/src/renderer/services/installation-location.service.ts b/src/renderer/services/installation-location.service.ts new file mode 100644 index 00000000..45445e07 --- /dev/null +++ b/src/renderer/services/installation-location.service.ts @@ -0,0 +1,35 @@ +import { Observable, lastValueFrom } from "rxjs"; +import { IpcService } from "./ipc.service"; + + +export class InstallationLocationService { + + private static instance: InstallationLocationService; + + private readonly ipcService: IpcService; + + public static getInstance(): InstallationLocationService { + if (!InstallationLocationService.instance) { + InstallationLocationService.instance = new InstallationLocationService(); + } + return InstallationLocationService.instance; + } + + private constructor() { + this.ipcService = IpcService.getInstance(); + } + + public async getInstallationFolder(): Promise { + return lastValueFrom(this.ipcService.sendV2("bs-installer.install-path")); + } + + /** + * @param move - if true, move the old installation path to the path param + */ + public setInstallationFolder(path: string, move: boolean): Observable { + return this.ipcService.sendV2( + "bs-installer.set-install-path", + { path, move } + ); + } +} diff --git a/src/renderer/services/modale.service.ts b/src/renderer/services/modale.service.ts index f1391b81..2ff9e6f9 100644 --- a/src/renderer/services/modale.service.ts +++ b/src/renderer/services/modale.service.ts @@ -39,7 +39,7 @@ export class ModalService { } } -export type ModalOptions = { readonly data?: T, readonly noStyle?: boolean } +export type ModalOptions = { readonly data?: T, readonly noStyle?: boolean, readonly closable?: boolean } export type ModalComponent = ({ resolver, options }: { readonly resolver: (x: ModalResponse) => void; readonly options?: ModalOptions }) => JSX.Element; export type ModalObject = {modal: ModalComponent, resolver: (value: ModalResponse | PromiseLike) => void, options: ModalOptions}; diff --git a/src/renderer/services/setup.service.ts b/src/renderer/services/setup.service.ts new file mode 100644 index 00000000..5bb38f18 --- /dev/null +++ b/src/renderer/services/setup.service.ts @@ -0,0 +1,64 @@ +import { lastValueFrom } from "rxjs"; +import { logRenderError } from "renderer"; + +import { BSVersionManagerService } from "./bs-version-manager.service"; +import { InstallationLocationService } from "./installation-location.service"; +import { IpcService } from "./ipc.service"; +import { ModalService } from "./modale.service"; + +import { AskInstallPathModal } from "renderer/components/modal/modal-types/ask-install-path.component"; + +// Handle setup modals/prompts, ordering of the modals/prompts may be done here +export class SetupService { + private static instance: SetupService; + + private readonly installationLocationService: InstallationLocationService; + private readonly ipcService: IpcService; + private readonly modalService: ModalService; + private readonly versionManagerService: BSVersionManagerService; + + private constructor() { + this.installationLocationService = InstallationLocationService.getInstance(); + this.ipcService = IpcService.getInstance(); + this.modalService = ModalService.getInstance(); + this.versionManagerService = BSVersionManagerService.getInstance(); + } + + public static getInstance(): SetupService { + if (!SetupService.instance) { + SetupService.instance = new SetupService(); + } + return SetupService.instance; + } + + public async check(): Promise { + try { + // NOTE: for modal sequencing + await this.checkInstallationPath(); + } catch (error) { + logRenderError(error); + } + } + + private async checkInstallationPath(): Promise { + try { + const exists = await lastValueFrom(this.ipcService.sendV2("bs-installer.folder-exists")); + if (exists) { + return; + } + + const modalResponse = await this.modalService.openModal( + AskInstallPathModal, + { closable: false } + ); + + await lastValueFrom(this.installationLocationService.setInstallationFolder(modalResponse.data.installPath, false)); + + // Refresh the versions tab + await this.versionManagerService.askInstalledVersions(); + } catch (error) { + logRenderError(error); + } + } + +} diff --git a/src/renderer/windows/App.tsx b/src/renderer/windows/App.tsx index e7812ca5..44addcde 100644 --- a/src/renderer/windows/App.tsx +++ b/src/renderer/windows/App.tsx @@ -23,6 +23,7 @@ import { ConfigurationService } from "renderer/services/configuration.service"; import { OsDiagnosticService } from "renderer/services/os-diagnostic.service"; import { useService } from "renderer/hooks/use-service.hook"; import { AutoUpdaterService } from "renderer/services/auto-updater.service"; +import { SetupService } from "renderer/services/setup.service"; import { gt, parse } from "semver" import { logRenderError } from "renderer"; @@ -35,13 +36,17 @@ export default function App() { const notification = useService(NotificationService); const config = useService(ConfigurationService); const autoUpdater = useService(AutoUpdaterService); + const setup = useService(SetupService); const location = useLocation(); const navigate = useNavigate(); useEffect(() => { checkIsUpdated(); - checkOneClicks(); + setup.check() + .then(() => { + checkOneClicks(); + }) }, []); const checkIsUpdated = async () => { diff --git a/src/shared/models/ipc/ipc-routes.ts b/src/shared/models/ipc/ipc-routes.ts index 2589a7c0..b145955f 100644 --- a/src/shared/models/ipc/ipc-routes.ts +++ b/src/shared/models/ipc/ipc-routes.ts @@ -27,8 +27,6 @@ export interface IpcChannelMapping { /* ** bs-download-ipcs ** */ "import-version": { request: ImportVersionOptions, response: Progression}; - "bs-download.installation-folder": { request: void, response: string}; - "bs-download.set-installation-folder": { request: string, response: string}; "auto-download-bs-version": { request: DownloadSteamInfo, response: DepotDownloaderEvent}; "download-bs-version": { request: DownloadSteamInfo, response: DepotDownloaderEvent}; "download-bs-version-qr": { request: DownloadSteamInfo, response: DepotDownloaderEvent}; @@ -44,6 +42,12 @@ export interface IpcChannelMapping { "bsv-search-playlist": {request: PlaylistSearchParams, response: BsvPlaylist[]}; "bsv-get-playlist-details-by-id": {request: {id: string, page: number}, response: BsvPlaylistPage}; + /* ** bs-installer-ipcs ** */ + "bs-installer.folder-exists": { request: void, response: boolean }; + "bs-installer.default-install-path": { request: void, response: string }; + "bs-installer.install-path": { request: void, response: string}; + "bs-installer.set-install-path": { request: { path: string, move: boolean }, response: string}; + /* ** bs-launcher-ipcs ** */ "create-launch-shortcut": { request: LaunchOption, response: boolean }; "bs-launch.need-start-as-admin": { request: void, response: boolean };