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 { OculusDownloaderService } from "renderer/services/bs-version-download/oculus-downloader.service"; 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"; export function SettingsPage() { const configService = useService(ConfigurationService); const themeService = useService(ThemeService); const ipcService = useService(IpcService); const modalService = useService(ModalService); const bsDownloader = useService(BsDownloaderService); const steamDownloader = useService(SteamDownloaderService); const oculusDownloader = useService(OculusDownloaderService); 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 { 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 [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 appVersion = useObservable(ipcService.sendV2("current-version")); useEffect(() => { loadInstallationFolder(); loadDownloadersSession(); mapsManager.isDeepLinksEnabled().then(enabled => setMapDeepLinksEnabled(() => enabled)); playlistsManager.isDeepLinksEnabled().then(enabled => setPlaylistsDeepLinkEnabled(() => enabled)); modelsManager.isDeepLinksEnabled().then(enabled => setModelsDeepLinkEnabled(() => enabled)); }, []); const allDeepLinkEnabled = mapDeepLinksEnabled && playlistsDeepLinkEnabled && modelsDeepLinkEnabled; const resetColors = () => { configService.delete("first-color" as DefaultConfigKey); configService.delete("second-color" as DefaultConfigKey); }; const loadInstallationFolder = () => { steamDownloader.getInstallationFolder().then(res => setInstallationFolder(res)); }; const loadDownloadersSession = () => { if(steamDownloader.sessionExist()){ return setHasDownloaderSession(true); } oculusDownloader.hasAuthToken().then(hasToken => { setHasDownloaderSession(hasToken); }); } const clearDownloadersSession = () => { steamDownloader.deleteSteamSession(); oculusDownloader.clearAuthToken(); 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 setDefaultInstallationFolder = () => { if (!progressBarService.require()) { return; } modalService.openModal(InstallationFolderModal).then(async res => { if (res.exitCode !== ModalExitCode.COMPLETED) { return; } const fileChooserRes = await ipcService.sendV2<{ canceled: boolean; filePaths: string[] }>("choose-folder").toPromise(); 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(steamDownloader.setInstallationFolder(fileChooserRes.filePaths[0])).then(res => { progressBarService.complete(); progressBarService.hide(true); 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(true); 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 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 = () => ipcService.sendLazy("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()); showNotification && (res ? showDeepLinkSuccess(!enable) : 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); res ? showDeepLinkSuccess(allDeepLinkEnabled) : 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}
{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/"); }} />
v{appVersion}
); }