import { Dispatch, SetStateAction, useEffect, useState } from "react"; import SettingColorChooser from "renderer/components/settings/setting-color-chooser.component"; import { SettingContainer } from "renderer/components/settings/setting-container.component"; import { RadioItem, SettingRadioArray } from "renderer/components/settings/setting-radio-array.component"; import { BsmButton } from "renderer/components/shared/bsm-button.component"; import { BsmIcon, BsmIconType } from "renderer/components/svgs/bsm-icon.component"; import { DefaultConfigKey, ThemeConfig } from "renderer/config/default-configuration.config"; import { useThemeColor } from "renderer/hooks/use-theme-color.hook"; import { SteamDownloaderService } from "renderer/services/bs-version-download/steam-downloader.service"; import { ConfigurationService } from "renderer/services/configuration.service"; import { I18nService } from "renderer/services/i18n.service"; import { IpcService } from "renderer/services/ipc.service"; import { ModalExitCode, ModalService } from "renderer/services/modale.service"; import { NotificationService } from "renderer/services/notification.service"; import { ProgressBarService } from "renderer/services/progress-bar.service"; import { ThemeService } from "renderer/services/theme.service"; import { SupportersView } from "renderer/components/settings/supporters-view/supporters-view.component"; import { LinkOpenerService } from "renderer/services/link-opener.service"; import { useNavigate } from "react-router-dom"; import { InstallationFolderModal } from "renderer/components/modal/modal-types/installation-folder-modal.component"; import { BsmCheckbox } from "renderer/components/shared/bsm-checkbox.component"; import { BsmImage } from "renderer/components/shared/bsm-image.component"; import modelSaberIcon from "../../../assets/images/third-party-icons/model-saber.svg"; import beatSaverIcon from "../../../assets/images/third-party-icons/beat-saver.png"; import beastSaberIcon from "../../../assets/images/third-party-icons/beast-saber.png"; import scoreSaberIcon from "../../../assets/images/third-party-icons/score-saber.png"; import beatleaderIcon from "../../../assets/images/third-party-icons/beat-leader.png"; import Tippy from "@tippyjs/react"; import { MapsManagerService } from "renderer/services/maps-manager.service"; import { PlaylistsManagerService } from "renderer/services/playlists-manager.service"; import { ModelsManagerService } from "renderer/services/models-management/models-manager.service"; import { useTranslation } from "renderer/hooks/use-translation.hook"; import { VersionFolderLinkerService } from "renderer/services/version-folder-linker.service"; 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"; import { BsStore } from "shared/models/bs-store.enum"; import { SteamIcon } from "renderer/components/svgs/icons/steam-icon.component"; import { OculusIcon } from "renderer/components/svgs/icons/oculus-icon.component"; import { BsDownloaderService } from "renderer/services/bs-version-download/bs-downloader.service"; import { AutoUpdaterService } from "renderer/services/auto-updater.service"; import BeatWaitingImg from "../../../assets/images/apngs/beat-waiting.png"; import BeatConflict from "../../../assets/images/apngs/beat-conflict.png"; import { logRenderError } from "renderer"; import { BSLauncherService } from "renderer/services/bs-launcher.service"; import { SettingToogleSwitchGrid } from "renderer/components/settings/setting-toogle-switch-grid.component"; 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() { const configService = useService(ConfigurationService); const themeService = useService(ThemeService); const ipcService = useService(IpcService); const modalService = useService(ModalService); const bsDownloader = useService(BsDownloaderService); const bsLauncher = useService(BSLauncherService); const steamDownloader = useService(SteamDownloaderService); const progressBarService = useService(ProgressBarService); const notificationService = useService(NotificationService); const i18nService = useService(I18nService); const linkOpener = useService(LinkOpenerService); const mapsManager = useService(MapsManagerService); const playlistsManager = useService(PlaylistsManagerService); const modelsManager = useService(ModelsManagerService); const versionLinker = useService(VersionFolderLinkerService); const autoUpdater = useService(AutoUpdaterService); const staticConfig = useService(StaticConfigurationService); const installationLocationService = useService(InstallationLocationService); const { firstColor, secondColor } = useThemeColor(); const themeItem: RadioItem[] = [ { id: 0, text: "pages.settings.appearance.themes.dark", value: "dark" as ThemeConfig }, { id: 1, text: "pages.settings.appearance.themes.light", value: "light" as ThemeConfig }, { id: 3, text: "pages.settings.appearance.themes.os", value: "os" as ThemeConfig }, ]; const languagesItems: RadioItem[] = i18nService .getSupportedLanguages() .map((l, index) => { return { id: index, text: `pages.settings.language.languages.${l}`, value: l, textIcon: `pages.settings.language.languages.translated.${l}`, icon: }; }) .sort((a, b) => a.text.localeCompare(b.text)); const nav = useNavigate(); const t = useTranslation(); const themeSelected = useObservable(() => themeService.theme$, "os"); const languageSelected = useObservable(() => i18nService.currentLanguage$, i18nService.getFallbackLanguage()); const downloadStore = useObservable(() => bsDownloader.defaultStore$); const [installationFolder, setInstallationFolder] = useState(null); const [protonPath, setProtonPath] = useState(bsLauncher.getProtonPath()); const [showSupporters, setShowSupporters] = useState(false); const [mapDeepLinksEnabled, setMapDeepLinksEnabled] = useState(false); const [playlistsDeepLinkEnabled, setPlaylistsDeepLinkEnabled] = useState(false); const [modelsDeepLinkEnabled, setModelsDeepLinkEnabled] = useState(false); const [hasDownloaderSession, setHasDownloaderSession] = useState(false); const [hardwareAccelerationEnabled, setHardwareAccelerationEnabled] = useState(true); const [useSymlink, setUseSymlink] = useState(false); const appVersion = useObservable(() => ipcService.sendV2("current-version")); const [isChangelogAvailable, setIsChangelogAvailable] = useState(true); const [changlogsLoading, setChanglogsLoading] = useState(false); useEffect(() => { loadInstallationFolder(); loadDownloadersSession(); mapsManager.isDeepLinksEnabled().then(enabled => setMapDeepLinksEnabled(() => enabled)); playlistsManager.isDeepLinksEnabled().then(enabled => setPlaylistsDeepLinkEnabled(() => enabled)); modelsManager.isDeepLinksEnabled().then(enabled => setModelsDeepLinkEnabled(() => enabled)); staticConfig.get("disable-hadware-acceleration").then(disabled =>setHardwareAccelerationEnabled(() => disabled !== true)); staticConfig.get("use-symlinks").then(useSymlinks => setUseSymlink(() => useSymlinks)); }, []); const allDeepLinkEnabled = mapDeepLinksEnabled && playlistsDeepLinkEnabled && modelsDeepLinkEnabled; const resetColors = () => { configService.delete("first-color" as DefaultConfigKey); configService.delete("second-color" as DefaultConfigKey); }; const loadInstallationFolder = () => { installationLocationService.getInstallationFolder().then(res => { setInstallationFolder(res); }); }; const loadDownloadersSession = () => { setHasDownloaderSession(steamDownloader.sessionExist()); } const clearDownloadersSession = () => { if (!hasDownloaderSession) { return; } steamDownloader.deleteSteamSession(); notificationService.notifyInfo({ title: "pages.settings.steam-and-oculus.logout-success", }); loadDownloadersSession(); } const setFirstColorSetting = (hex: string) => configService.set("first-color", hex); const setSecondColorSetting = (hex: string) => configService.set("second-color", hex); const handleChangeBsStore = (item: RadioItem) => { bsDownloader.setDefaultStore(item.value); } const handleChangeTheme = (item: RadioItem) => { themeService.setTheme(item.value); }; const handleChangeLanguage = (item: RadioItem) => { i18nService.setLanguage(item.value); }; const handleVersionClick = async () => { let isChangelogResolved = false; const timeoutId = setTimeout(() => setChanglogsLoading(() => !isChangelogResolved), 100); await autoUpdater.showChangelog(await lastValueFrom(autoUpdater.getAppVersion())) .then(() => { setIsChangelogAvailable(() => true); }) .catch(err => { logRenderError(err); setIsChangelogAvailable(() => false); }) .finally(() => { isChangelogResolved = true; }); setChanglogsLoading(() => false); clearTimeout(timeoutId); }; const setDefaultProtonPath = () => { if (!progressBarService.require()) { return; } lastValueFrom(ipcService.sendV2("choose-file")).then(res => { if (!res.canceled && res.filePaths?.length) { const protonPath = res.filePaths[0]; setProtonPath(protonPath); bsLauncher.setProtonPath(protonPath); } }); }; const setDefaultInstallationFolder = () => { if (!progressBarService.require()) { return; } modalService.openModal(InstallationFolderModal).then(async res => { if (res.exitCode !== ModalExitCode.COMPLETED) { return; } const fileChooserRes = await lastValueFrom(ipcService.sendV2("choose-folder")); if (!fileChooserRes.canceled && fileChooserRes.filePaths?.length) { progressBarService.showFake(0.008); notificationService.notifySuccess({ title: "notifications.settings.move-folder.success.titles.transfer-started", desc: "notifications.settings.move-folder.success.descs.transfer-started" }); lastValueFrom(installationLocationService.setInstallationFolder(fileChooserRes.filePaths[0], true)).then(res => { progressBarService.complete(); progressBarService.hide(); setInstallationFolder(res); notificationService.notifySuccess({ title: "notifications.settings.move-folder.success.titles.transfer-finished", duration: 3000 }); // Restore links of external BS versions (steam, oculus, etc.) lastValueFrom(versionLinker.relinkAllVersionsFolders()).catch(() => { notificationService.notifyError({ title: "notifications.types.error", desc: "notifications.settings.move-folder.errors.descs.restore-linked-folders", duration: 15_000 }); }); }).catch((err: BsmException) => { progressBarService.hide(); if (err?.code === "COPY_TO_SUBPATH") { notificationService.notifyError({ title: "notifications.settings.move-folder.errors.titles.transfer-failed", desc: "notifications.settings.move-folder.errors.descs.COPY_TO_SUBPATH", duration: 10_000 }); return; } notificationService.notifyError({ title: "notifications.settings.move-folder.errors.titles.transfer-failed" }); }); } }); }; const onChangeHardwareAcceleration = async (newHardwareAccelerationEnabled: boolean) => { if(newHardwareAccelerationEnabled === hardwareAccelerationEnabled){ return; } const res = await modalService.openModal(BasicModal, { data: { title: "pages.settings.advanced.hardware-acceleration.modal.title", body: "pages.settings.advanced.hardware-acceleration.modal.body", image: BeatConflict, buttons: [ { id: "cancel", text: "misc.cancel", type: "cancel", isCancel: true }, { id: "confirm", text: "pages.settings.advanced.hardware-acceleration.modal.confirm-btn", type: "error" } ] }}); if(res.exitCode !== ModalExitCode.COMPLETED || res.data !== "confirm"){ return; } const { error } = await tryit(() => staticConfig.set("disable-hadware-acceleration", !newHardwareAccelerationEnabled)); if(error){ notificationService.notifyError({ title: "notifications.types.error", desc: "pages.settings.advanced.hardware-acceleration.error-notification.message" }); setHardwareAccelerationEnabled(() => !newHardwareAccelerationEnabled); return; } setHardwareAccelerationEnabled(() => newHardwareAccelerationEnabled); if(!progressBarService.require()){ return; } await lastValueFrom(ipcService.sendV2("restart-app")); }; const onChangeUseSymlinks = async (newUseSymlink: boolean) => { if(newUseSymlink === useSymlink){ return; } if(newUseSymlink){ const res = await modalService.openModal(BasicModal, { data: { title: "pages.settings.advanced.use-symlinks.modal.title", body: "pages.settings.advanced.use-symlinks.modal.body", image: BeatConflict, buttons: [ { id: "cancel", text: "misc.cancel", type: "cancel", isCancel: true }, { id: "confirm", text: "pages.settings.advanced.use-symlinks.modal.confirm-btn", type: "error" } ] }}); if(res.exitCode !== ModalExitCode.COMPLETED || res.data !== "confirm"){ return; } } const { error } = await tryit(() => staticConfig.set("use-symlinks", newUseSymlink)); if(error){ notificationService.notifyError({ title: "notifications.types.error", desc: "pages.settings.advanced.use-symlinks.error-notification.message" }); return; } setUseSymlink(() => newUseSymlink); } const toogleShowSupporters = () => { setShowSupporters(show => !show); }; const openSupportPage = () => linkOpener.open("https://www.patreon.com/bsmanager"); const openGithub = () => linkOpener.open("https://github.com/Zagrios/bs-manager"); const openReportBug = () => linkOpener.open("https://github.com/Zagrios/bs-manager/issues/new?assignees=Zagrios&labels=bug&template=-bug--bug-report.md&title=%5BBUG%5D+%3A+"); const openRequestFeatures = () => linkOpener.open("https://github.com/Zagrios/bs-manager/issues/new?assignees=Zagrios&labels=enhancement&template=-feat---feature-request.md&title=%5BFEAT.%5D+%3A+"); const openDiscord = () => linkOpener.open("https://discord.gg/uSqbHVpKdV"); const openTwitter = () => linkOpener.open("https://twitter.com/BSManager_"); const openLogs = () => lastValueFrom(ipcService.sendV2("open-logs")); const showDeepLinkError = (isDeactivation: boolean) => { const desc = isDeactivation ? "notifications.settings.additional-content.deep-link.deactivation.error.description" : "notifications.settings.additional-content.deep-link.activation.error.description"; notificationService.notifyError({ title: "notifications.types.error", desc, duration: 3000 }); }; const showDeepLinkSuccess = (isDeactivation: boolean) => { const desc = isDeactivation ? "notifications.settings.additional-content.deep-link.deactivation.success.description" : "notifications.settings.additional-content.deep-link.activation.success.description"; const title = isDeactivation ? "notifications.settings.additional-content.deep-link.deactivation.success.title" : "notifications.settings.additional-content.deep-link.activation.success.title"; notificationService.notifySuccess({ title, desc, duration: 3000 }); }; const switchDeepLink = async (manager: MapsManagerService | PlaylistsManagerService | ModelsManagerService, enable: boolean, showNotification: boolean, setter: Dispatch>) => { const res = await (enable ? manager.enableDeepLink() : manager.disableDeepLink()).then(() => true).catch(() => false); if(showNotification && res){ showDeepLinkSuccess(enable) } else if(showNotification && !res){ showDeepLinkError(enable); } const isEnable = await manager.isDeepLinksEnabled(); setter(() => isEnable); return res; }; const toogleMapDeepLinks = (showNotification = true) => switchDeepLink(mapsManager, !mapDeepLinksEnabled, showNotification, setMapDeepLinksEnabled); const tooglePlaylistsDeepLinks = (showNotification = true) => switchDeepLink(playlistsManager, !playlistsDeepLinkEnabled, showNotification, setPlaylistsDeepLinkEnabled); const toogleModelsDeepLinks = (showNotification = true) => switchDeepLink(modelsManager, !modelsDeepLinkEnabled, showNotification, setModelsDeepLinkEnabled); const toogleAllDeepLinks = async () => { const res = (await Promise.all([switchDeepLink(mapsManager, !allDeepLinkEnabled, false, setMapDeepLinksEnabled), switchDeepLink(playlistsManager, !allDeepLinkEnabled, false, setPlaylistsDeepLinkEnabled), switchDeepLink(modelsManager, !allDeepLinkEnabled, false, setModelsDeepLinkEnabled)])).every(activation => activation === true); if(res){ showDeepLinkSuccess(allDeepLinkEnabled); } else{ showDeepLinkError(allDeepLinkEnabled); } }; return (
nav(-1)} icon="close" withBar={false} />
}, { id: 2, text: "Oculus Store (PC)", value: BsStore.OCULUS, icon: }, { id: 0, text: t("pages.settings.steam-and-oculus.download-platform.always-ask"), value: null, }, ]} selectedItemValue={downloadStore} onItemSelected={handleChangeBsStore}/>
{installationFolder}
{protonPath}
{t("notifications.settings.additional-content.deep-link.select-all")}
{ e.stopPropagation(); linkOpener.open("https://beatsaver.com/"); }} /> { e.stopPropagation(); linkOpener.open("https://bsaber.com/"); }} /> { e.stopPropagation(); linkOpener.open("https://scoresaber.com/"); }} /> { e.stopPropagation(); linkOpener.open("https://www.beatleader.xyz/"); }} /> { e.stopPropagation(); linkOpener.open("https://modelsaber.com/"); }} />
  • toogleMapDeepLinks()}>
    toogleMapDeepLinks()} checked={mapDeepLinksEnabled} /> {t("misc.maps")}
    { e.stopPropagation(); linkOpener.open("https://beatsaver.com/"); }} /> { e.stopPropagation(); linkOpener.open("https://bsaber.com/"); }} /> { e.stopPropagation(); linkOpener.open("https://scoresaber.com/"); }} /> { e.stopPropagation(); linkOpener.open("https://www.beatleader.xyz/"); }} />
  • tooglePlaylistsDeepLinks()}>
    tooglePlaylistsDeepLinks()} checked={playlistsDeepLinkEnabled} /> {t("misc.playlists")}
    { e.stopPropagation(); linkOpener.open("https://beatsaver.com/"); }} />
  • toogleModelsDeepLinks()}>
    toogleModelsDeepLinks()} checked={modelsDeepLinkEnabled} /> {t("misc.models")}
    { e.stopPropagation(); linkOpener.open("https://modelsaber.com/"); }} />
); }