Merge branch 'master' into feature/add-changelog-modal/178

This commit is contained in:
Shidorien
2023-12-21 00:00:00 +01:00
70 changed files with 4037 additions and 1943 deletions
@@ -252,17 +252,24 @@ export class LocalMapsManagerService {
}
const zipUrl = map.versions.at(0).downloadURL;
const mapFolderName = sanitize(`${map.id}-${map.name}`);
const mapsFolder = await this.getMapsFolderPath(version);
const installedMap = await this.loadMapInfoFromPath(path.join(mapsFolder, mapFolderName)).catch(e => {
log.error("loadMapInfoFromPath", e);
return null;
});
if(map.versions.every(version => version.hash === installedMap?.hash)) {
return installedMap;
}
const { zip, zipPath } = await this.downloadMapZip(zipUrl);
if (!zip) {
throw `Cannot download ${zipUrl}`;
}
const mapFolderName = sanitize(`${map.id}-${map.name}`);
const mapPath = path.join(mapsFolder, mapFolderName);
await ensureFolderExist(mapPath);
@@ -159,7 +159,10 @@ export class LocalPlaylistsManagerService {
await this.installBPListFile(playlistPath, version);
const versionMapsFolder = await this.maps.getMapsFolderPath(version);
const realDestMapsFolder = await realpath(versionMapsFolder);
const realDestMapsFolder = await realpath(versionMapsFolder).catch(e => {
log.error(e);
return versionMapsFolder;
});
if(realSourceMapsFolder === realDestMapsFolder) { continue; }
+2
View File
@@ -39,6 +39,7 @@ export class IpcService {
}
public send<T>(channel: string, window: BrowserWindow, response?: T | Error): void {
if(window.webContents?.isDestroyed()){ return; }
window.webContents?.send(channel, response);
}
@@ -53,6 +54,7 @@ export class IpcService {
complete: () => this.send(this.getCompleteChannel(channel), window)
})
window.webContents.once("destroyed", () => sub.unsubscribe());
window.webContents.ipc.once(this.getTearDownChannel(channel), () => sub.unsubscribe());
sub.add(() => {
+12 -24
View File
@@ -1,6 +1,6 @@
import { execOnOs } from "../../helpers/env.helpers";
import { list, createKey, putValue, deleteKey, RegSzValue } from "regedit-rs";
import path from "path";
import regedit from "regedit";
export class LivService {
@@ -23,8 +23,8 @@ export class LivService {
public async isLivInstalled(): Promise<boolean> {
return execOnOs({
win32: async () => {
const regRes = await regedit.promisified.list([this.livRegeditKey]).then(res => res[this.livRegeditKey]);
return regRes?.exists;
const regRes = await list(this.livRegeditKey).then(res => res[this.livRegeditKey]);
return regRes.exists;
},
}, true);
}
@@ -33,25 +33,13 @@ export class LivService {
return execOnOs({
win32: async () => {
const livExternalAppRegeditKey = path.join(this.livExternalAppsRegeditKey, entry.id);
await regedit.promisified.createKey([livExternalAppRegeditKey]);
await regedit.promisified.putValue({
await createKey(livExternalAppRegeditKey);
await putValue({
[livExternalAppRegeditKey]: {
"InstallPath": {
value: entry.installPath,
type: "REG_SZ"
},
"Executable": {
value: entry.executable,
type: "REG_SZ"
},
"Arguments": {
value: entry.arguments,
type: "REG_SZ"
},
"Name": {
value: entry.name,
type: "REG_SZ"
}
InstallPath: new RegSzValue(entry.installPath),
Executable: new RegSzValue(entry.executable),
Arguments: new RegSzValue(entry.arguments),
Name: new RegSzValue(entry.name)
}
});
}
@@ -62,7 +50,7 @@ export class LivService {
return execOnOs({
win32: async () => {
const shotcutsKeys = ids.map(id => path.join(this.livExternalAppsRegeditKey, id));
return regedit.promisified.deleteKey(shotcutsKeys);
return deleteKey(shotcutsKeys);
}
})
}
@@ -70,7 +58,7 @@ export class LivService {
public getLivShortcuts(): Promise<LivEntry[]> {
return execOnOs({
win32: async () => {
const regRes = await regedit.promisified.list([this.livExternalAppsRegeditKey]).then(res => res[this.livExternalAppsRegeditKey]);
const regRes = await list(this.livExternalAppsRegeditKey).then(res => res[this.livExternalAppsRegeditKey]);
if(!regRes.exists){
return [];
@@ -78,7 +66,7 @@ export class LivService {
const promises = regRes.keys.map(async key => {
const shortcutKey = path.join(this.livExternalAppsRegeditKey, key);
const entries = await regedit.promisified.list([shortcutKey]).then(res => res[shortcutKey]);
const entries = await list(shortcutKey).then(res => res[shortcutKey]);
if(!entries.exists){
return undefined;
+12 -13
View File
@@ -1,4 +1,4 @@
import regedit from "regedit";
import { list } from "regedit-rs";
import path from "path";
import { pathExist } from "../helpers/fs.helpers";
import log from "electron-log";
@@ -29,26 +29,25 @@ export class OculusService {
}
const oculusLibsRegKey = "HKCU\\SOFTWARE\\Oculus VR, LLC\\Oculus\\Libraries";
const libsRegData = await list(oculusLibsRegKey).then(data => data[oculusLibsRegKey]);
const libsRegData = (await regedit.promisified.list([oculusLibsRegKey]))["HKCU\\SOFTWARE\\Oculus VR, LLC\\Oculus\\Libraries"];
if (!libsRegData.exists || !libsRegData.keys) {
if (!libsRegData.keys?.length) {
log.info("Registry key \"HKCU\\SOFTWARE\\Oculus VR, LLC\\Oculus\\Libraries\" not found");
return null;
}
const defaultLibraryId = libsRegData.values.DefaultLibrary.value as string;
const libsPath: OculusLibrary[] = (
await Promise.all(
libsRegData.keys.map(async key => {
const originalPath = (await regedit.promisified.list([`${oculusLibsRegKey}\\${key}`]))[`${oculusLibsRegKey}\\${key}`];
if (!originalPath.exists || !libsRegData.values || !originalPath.values.OriginalPath) {
return null;
}
await Promise.all(libsRegData.keys.map(async key => {
const originalPath = await list([`${oculusLibsRegKey}\\${key}`]).then(res => res[`${oculusLibsRegKey}\\${key}`]);
if (!originalPath.values?.OriginalPath) {
return null;
}
return { id: key, path: originalPath.values.OriginalPath.value, isDefault: defaultLibraryId === key } as OculusLibrary
}, [])
)
return { id: key, path: originalPath.values.OriginalPath.value, isDefault: defaultLibraryId === key } as OculusLibrary
}, []))
).filter(Boolean);
this.oculusLibraries = libsPath;
+18 -28
View File
@@ -1,5 +1,4 @@
import { UtilsService } from "./utils.service";
import regedit from "regedit";
import { list, RegDwordValue } from "regedit-rs"
import path from "path";
import { parse } from "@node-steam/vdf";
import { readFile } from "fs/promises";
@@ -11,15 +10,9 @@ import { taskRunning } from "../helpers/os.helpers";
export class SteamService {
private static instance: SteamService;
private readonly utils: UtilsService = UtilsService.getInstance();
private steamPath: string = '';
private constructor(){
const vbsDirectory = path.join(this.utils.getAssetsScriptsPath(), "node-regedit", "vbs");
regedit.setExternalVBSLocation(vbsDirectory);
}
private constructor(){}
public static getInstance(){
if(!SteamService.instance){ SteamService.instance = new SteamService(); }
@@ -27,17 +20,19 @@ export class SteamService {
}
public async getActiveUser(): Promise<number>{
const res = await regedit.promisified.list(["HKCU\\Software\\Valve\\Steam\\ActiveProcess"]);
const keys = res?.["HKCU\\Software\\Valve\\Steam\\ActiveProcess"];
if(!keys?.exists){ throw "Key \"HKCU\\Software\\Valve\\Steam\\ActiveProcess\" not exist"; }
return (keys.values?.ActiveUser.value || undefined) as number;
const res = await list("HKCU\\Software\\Valve\\Steam\\ActiveProcess");
const key = res["HKCU\\Software\\Valve\\Steam\\ActiveProcess"];
if(!key.exists){ throw new Error("Key \"HKCU\\Software\\Valve\\Steam\\ActiveProcess\" not exist"); }
const registryValue = key.values.ActiveUser as RegDwordValue;
if(!registryValue){ throw new Error("Value \"ActiveUser\" not exist"); }
return registryValue.value;
}
public async steamRunning(): Promise<boolean>{
const steamProcessRunning = await taskRunning("steam");
if(process.platform === "linux") { return steamProcessRunning; }
return steamProcessRunning && !!(await this.getActiveUser());
const activeUser = await this.getActiveUser().catch(err => log.error(err));
return steamProcessRunning && !!activeUser;
}
public async getSteamPath(): Promise<string>{
@@ -48,24 +43,19 @@ export class SteamService {
case "linux":
this.steamPath = path.join(app.getPath('home'), '.steam', "steam");
return this.steamPath;
case "win32":
// eslint-disable-next-line no-case-declarations
const [win32Res, win64Res] = await Promise.all([
regedit.promisified.list(['HKLM\\SOFTWARE\\Valve\\Steam']),
regedit.promisified.list(['HKLM\\SOFTWARE\\WOW6432Node\\Valve\\Steam'])
]);
case "win32": {
const res = await list(['HKLM\\SOFTWARE\\Valve\\Steam', 'HKLM\\SOFTWARE\\WOW6432Node\\Valve\\Steam']);
const win64 = res['HKLM\\SOFTWARE\\Valve\\Steam'];
const win32 = res['HKLM\\SOFTWARE\\WOW6432Node\\Valve\\Steam'];
// eslint-disable-next-line no-case-declarations
const [win32, win64] = [win32Res["HKLM\\SOFTWARE\\Valve\\Steam"], win64Res["HKLM\\SOFTWARE\\WOW6432Node\\Valve\\Steam"]];
if(win64.exists && win64?.values?.InstallPath?.value){
if (win64.exists && win64?.values?.InstallPath?.value) {
this.steamPath = win64.values.InstallPath.value as string;
}
else if (win32.exists && win32?.values?.InstallPath?.value){
this.steamPath = win32.values.InstallPath.value as string;
} else if (win32.exists && win32?.values?.InstallPath?.value) {
this.steamPath = win32.values.InstallPath.value as string;
}
return this.steamPath;
}
default:
return null;
}
@@ -37,7 +37,7 @@ export const AvailableVersionItem = memo(function AvailableVersionItem({version,
<span className="text-sm text-gray-700 dark:text-gray-400">{formatedDate}</span>
</div>
{version.ReleaseURL && (
<a href={version.ReleaseURL} target="_blank" className="flex flex-row justify-between items-center rounded-full bg-black bg-opacity-30 text-white pb-px overflow-hidden hover:bg-opacity-50">
<a href={version.ReleaseURL} target="_blank" className="flex flex-row justify-between items-center rounded-full bg-black bg-opacity-30 text-white pb-px overflow-hidden hover:bg-opacity-50" tabIndex={-1}>
<SteamIcon className="w-[25px] h-[25px] transition-transform group-hover:rotate-[-360deg] duration-300" />
<span className="relative -left-px text-sm w-fit max-w-0 text-center overflow-hidden h-full whitespace-nowrap pb-[3px] transition-all group-hover:max-w-[200px] group-hover:px-1 duration-300">{t("pages.available-versions.steam-release")}</span>
</a>
@@ -19,9 +19,18 @@ export function AvailableVersionsSlide({ versions }: Props) {
context.setSelectedVersion(version);
}
const getVersions = () => {
const copy = [...(versions ?? [])];
const recommendedVersion = copy.find(v => v.recommended);
if(!recommendedVersion) { return copy; }
copy.splice(copy.indexOf(recommendedVersion), 1);
copy.unshift(recommendedVersion);
return copy;
}
return (
<ol className="w-full flex items-start justify-center gap-6 shrink-0 content-start flex-wrap p-4 overflow-x-hidden overflow-y-scroll scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900">
{versions.map(version => (
<ol className="w-full flex items-start justify-center gap-6 shrink-0 content-start flex-wrap px-3.5 py-4 overflow-x-hidden overflow-y-scroll scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900">
{getVersions().map(version => (
<AvailableVersionItem key={version.BSManifest} version={version} selected={equal(version, context.selectedVersion)} onClick={() => setSelectedVersion(version)}/>
))}
</ol>
@@ -84,7 +84,7 @@ export function FilterPanel({ className, ref, playlist = false, filter, onChange
};
const renderLabel = (text: string | number, isMax: boolean): JSX.Element => {
return <span className={`bg-inherit absolute top-[calc(100%+4px)] h-5 font-bold rounded-md shadow-center shadow-black px-1 flex items-center ${isMax ? "text-lg" : "text-sm"}`}>{text}</span>;
return <span className={`bg-inherit absolute top-[calc(100%+4px)] whitespace-nowrap h-5 font-bold rounded-md shadow-center shadow-black px-1 flex items-center ${isMax ? "text-lg" : "text-sm"}`}>{text}</span>;
};
const onNpssChange = ([min, max]: number[]) => {
@@ -153,7 +153,7 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
<div className="h-full rounded-full bg-light-main-color-2 dark:bg-main-color-2 grow p-[6px]">
<input type="text" className="h-full w-full bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-2" placeholder={t("pages.version-viewer.maps.search-bar.search-placeholder")} value={tabIndex === 0 ? mapSearch : playlistSearch} onChange={e => handleSearch(e.target.value)} tabIndex={-1} />
</div>
<BsmDropdownButton className="h-full relative z-[1] flex justify-center" buttonClassName="flex items-center justify-center h-full rounded-full px-2 py-1" icon="filter" text="pages.version-viewer.maps.search-bar.filters-btn" withBar={false}>
<BsmDropdownButton className="h-full relative z-[1] flex justify-center" buttonClassName="flex items-center justify-center h-full rounded-full px-2 py-1" icon="filter" text="pages.version-viewer.maps.search-bar.filters-btn" textClassName="whitespace-nowrap" withBar={false}>
<FilterPanel className="absolute top-[calc(100%+3px)] origin-top w-[500px] h-fit p-2 rounded-md shadow-md shadow-black" filter={mapFilter} onChange={setMapFilter} />
</BsmDropdownButton>
<BsmDropdownButton className="h-full flex aspect-square relative rounded-full z-[1] bg-light-main-color-1 dark:bg-main-color-3" buttonClassName="rounded-full h-full w-full p-[6px]" icon="three-dots" withBar={false} items={dropDownItems} menuTranslationY="6px" align="center" />
@@ -7,6 +7,8 @@ import { useState } from "react";
import tailwindConfig from "../../../../../../tailwind.config";
import Color from "color";
import { useNavigate } from "react-router-dom";
import Tippy from "@tippyjs/react";
import { followCursor } from "tippy.js";
export const ChooseStore: ModalComponent<BsStore> = ({ resolver }) => {
@@ -47,10 +49,12 @@ export const ChooseStore: ModalComponent<BsStore> = ({ resolver }) => {
<h1 className="text-3xl uppercase tracking-wide w-full text-center text-gray-800 dark:text-gray-200">{t("modals.choose-store.title")}</h1>
<p className="w-auto text-gray-800 dark:text-gray-200 text-center">{t("modals.choose-store.body")}</p>
<div className="flex flex-row w-full flex-grow gap-3">
<div className="flex flex-col flex-grow basis-0 gap-2 text-center px-5 pt-3 pb-1 rounded-md border-main-color-3 border-2 cursor-pointer" onMouseEnter={() => setOculusHover(true)} onMouseLeave={() => setOculusHover(false)} onClick={() => chooseStore(BsStore.OCULUS)} style={{backgroundColor: oculusHover ? bg.dim : bg.bright}}>
<OculusIcon className="flex-grow aspect-square text-black bg-white rounded-full p-5"/>
<h2 className="font-bold">Oculus Store (PC)</h2>
</div>
<Tippy className="!bg-neutral-900" content={t("modals.choose-store.unavailable")} allowHTML hideOnClick={false} followCursor plugins={[followCursor]} duration={200} arrow={false} maxWidth={300}>
<div className="flex-grow basis-0 flex flex-col gap-2 text-center px-5 pt-3 pb-1 rounded-md border-main-color-3 border-2 cursor-not-allowed" style={{backgroundColor: oculusHover ? bg.dim : bg.bright}}>
<OculusIcon className="flex-grow aspect-square text-black bg-white rounded-full p-5"/>
<h2 className="font-bold">Oculus Store (PC)</h2>
</div>
</Tippy>
<div className="flex flex-col flex-grow basis-0 gap-2 text-center px-5 pt-3 pb-1 rounded-md border-main-color-3 border-2 cursor-pointer" onMouseEnter={() => setSteamHover(true)} onMouseLeave={() => setSteamHover(false)} onClick={() => chooseStore(BsStore.STEAM)} style={{backgroundColor: steamHover ? bg.dim : bg.bright}}>
<SteamIcon className="flex-grow"/>
<h2 className="font-bold">Steam</h2>
@@ -66,7 +66,7 @@ export function ModelsPanel({ version, isActive, goToMods }: { version?: BSVersi
if (config.get("not-remind-models-breaks")) {
return;
}
notification.notifyWarning({ title: "models.notifications.prevent-for-models-breaks.title", desc: "models.notifications.prevent-for-models-breaks.desc", actions: [{ id: "0", title: "models.notifications.prevent-for-mods.not-remind", cancel: true }], duration: 12_000 }).then(res => {
notification.notifyInfo({ title: "models.notifications.prevent-for-models-breaks.title", desc: "models.notifications.prevent-for-models-breaks.desc", actions: [{ id: "0", title: "models.notifications.prevent-for-mods.not-remind", cancel: true }], duration: 12_000 }).then(res => {
if (res === "0") {
config.set("not-remind-models-breaks", true);
}
@@ -38,7 +38,7 @@ export function NavBar() {
return (
<nav id="nav-bar" className="z-10 flex flex-col h-full max-h-full items-center p-1">
<BsManagerIcon className="relative aspect-square w-16 h-16 mb-3" />
<ol id="versions" className="w-fit max-w-[120px] relative left-[2px] grow overflow-y-hidden scrollbar-track-transparent scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900 hover:overflow-y-scroll">
<ol id="versions" className="w-fit max-w-[150px] relative left-[2px] grow overflow-y-hidden scrollbar-track-transparent scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900 hover:overflow-y-scroll">
<SharedNavBarItem />
<NavBarSpliter />
{listVersions().map(version => (
@@ -18,10 +18,15 @@ export function SettingRadioArray<T>({ id, items, selectedItemId, selectedItemVa
return item.id === selectedItemId || item.value === selectedItemValue;
}
const selectItem = (item: RadioItem<T>) => {
if(item.disabled){ return; }
onItemSelected?.(item);
}
return (
<div id={id} className="w-full flex gap-1.5" style={{flexDirection: direction}}>
{items.map(i => (
<div onClick={() => onItemSelected(i)} key={i.id} className={`py-3 w-full flex cursor-pointer justify-between items-center rounded-md px-2 transition-colors duration-200 ${isSelected(i) ? "bg-light-main-color-3 dark:bg-main-color-3" : "bg-light-main-color-1 dark:bg-main-color-1"} ${i.className}`}>
<div onClick={() => selectItem(i)} key={i.id} className={`py-3 w-full flex ${i.disabled ? "cursor-not-allowed" : "cursor-pointer"} ${i.disabled && "brightness-75"} justify-between items-center rounded-md px-2 transition-colors duration-200 ${isSelected(i) ? "bg-light-main-color-3 dark:bg-main-color-3" : "bg-light-main-color-1 dark:bg-main-color-1"} ${i.className}`}>
<div className="flex items-center">
<div className="h-5 rounded-full aspect-square border-2 border-gray-800 dark:border-white p-[3px] mr-2">
<motion.span initial={{ scale: 0 }} animate={{ scale: isSelected(i) ? 1 : 0 }} className="h-full w-full block bg-gray-800 dark:bg-white rounded-full" />
@@ -47,4 +52,5 @@ export interface RadioItem<T> {
textIcon?: string;
className?: string;
value?: T;
disabled?: boolean;
}
@@ -21,9 +21,10 @@ type Props = {
menuTranslationY?: string | number;
children?: JSX.Element;
text?: string;
textClassName?: string;
};
export const BsmDropdownButton = forwardRef(({ className, items, align, withBar = true, icon = "settings", buttonClassName, menuTranslationY, children, text }: Props, fowardRed) => {
export const BsmDropdownButton = forwardRef(({ className, items, align, withBar = true, icon = "settings", buttonClassName, menuTranslationY, children, text, textClassName }: Props, fowardRed) => {
const [expanded, setExpanded] = useState(false);
const t = useTranslation();
const ref = useRef(fowardRed);
@@ -63,7 +64,7 @@ export const BsmDropdownButton = forwardRef(({ className, items, align, withBar
return (
<div ref={ref as unknown as React.LegacyRef<HTMLDivElement>} className={className}>
<BsmButton onClick={() => setExpanded(!expanded)} className={buttonClassName ?? defaultButtonClassName} icon={icon} active={expanded} onClickOutside={handleClickOutside} withBar={withBar} text={text} />
<BsmButton onClick={() => setExpanded(!expanded)} className={buttonClassName ?? defaultButtonClassName} icon={icon} active={expanded} textClassName={textClassName} onClickOutside={handleClickOutside} withBar={withBar} text={text} />
<div className={`py-1 w-fit absolute cursor-pointer top-[calc(100%-4px)] rounded-md bg-inherit text-sm text-gray-800 dark:text-gray-200 shadow-md shadow-black transition-[scale] ease-in-out ${alignClass}`} style={{ scale: expanded ? "1" : "0", translate: `0 ${menuTranslationY}` }}>
{items?.map(
i =>
@@ -2,7 +2,15 @@ import { BsmImage } from "../shared/bsm-image.component";
import "./slideshow.component.css";
export function Slideshow(props: { className: string }) {
const slideshowImages = [require("../../../../assets/images/slideshow-images/image-1-blur.jpg"), require("../../../../assets/images/slideshow-images/image-2-blur.jpg"), require("../../../../assets/images/slideshow-images/image-3-blur.jpg"), require("../../../../assets/images/slideshow-images/image-4-blur.jpg"), require("../../../../assets/images/slideshow-images/image-5-blur.png"), require("../../../../assets/images/slideshow-images/image-6-blur.png"), require("../../../../assets/images/slideshow-images/image-7-blur.png")];
const slideshowImages = [
require("../../../../assets/images/slideshow-images/image-1-blur.jpg"),
require("../../../../assets/images/slideshow-images/image-2-blur.jpg"),
require("../../../../assets/images/slideshow-images/image-3-blur.jpg"),
require("../../../../assets/images/slideshow-images/image-4-blur.jpg"),
require("../../../../assets/images/slideshow-images/image-5-blur.jpg"),
require("../../../../assets/images/slideshow-images/image-6-blur.jpg"),
require("../../../../assets/images/slideshow-images/image-7-blur.jpg")
];
return (
<div className={`slide ${props.className}`}>
@@ -53,6 +53,9 @@ import { VolumeOffIcon } from "./icons/volume-off-icon.component";
import { VolumeDownIcon } from "./icons/volume-down-icon.component";
import { GermanIcon } from "./flags/german-icon.component";
import { RussianIcon } from "./flags/russian-icon.component";
import { ChineseIcon } from "./flags/chinese-icon.component";
import { ChineseTraditionalIcon } from "./flags/chineseTraditional-icon.component";
import { JapanIcon } from "./flags/japan-icon.component";
import { MSModelType } from "shared/models/models/model-saber.model";
import { ModelTypeAvatarIcon } from "./icons/model-type-avatar-icon.component";
import { ModelTypeSaberIcon } from "./icons/model-type-saber-icon.component";
@@ -63,7 +66,7 @@ import { EyeCrossIcon } from "./icons/eye-cross-icon.component";
import { ShortcutIcon } from "./icons/shortcut-icon.component";
import { BackupRestoreIcon } from "./icons/backup-restore-icon.component";
export type BsmIconType = BsvMapCharacteristic | MSModelType | ("settings" | "trash" | "favorite" | "folder" | "bsNote" | "check" | "three-dots" | "twitch" | "eye" | "play" | "checkCircleIcon" | "discord" | "info" | "eye-cross" | "terminal" | "desktop" | "oculus" | "add" | "cross" | "task" | "github" | "close" | "thumbUpFill" | "timerFill" | "pause" | "twitter" | "sync" | "chevron-top" | "copy" | "steam" | "edit" | "export" | "patreon" | "search" | "bsMapDifficulty" | "link" | "unlink" | "download" | "filter" | "mee6" | "volume-up" | "volume-off" | "volume-down" | "shortcut" | "backup-restore" | "fr-FR-flag" | "es-ES-flag" | "en-US-flag" | "en-EN-flag" | "de-DE-flag" | "ru-RU-flag");
export type BsmIconType = BsvMapCharacteristic | MSModelType | ("settings" | "trash" | "favorite" | "folder" | "bsNote" | "check" | "three-dots" | "twitch" | "eye" | "play" | "checkCircleIcon" | "discord" | "info" | "eye-cross" | "terminal" | "desktop" | "oculus" | "add" | "cross" | "task" | "github" | "close" | "thumbUpFill" | "timerFill" | "pause" | "twitter" | "sync" | "chevron-top" | "copy" | "steam" | "edit" | "export" | "patreon" | "search" | "bsMapDifficulty" | "link" | "unlink" | "download" | "filter" | "mee6" | "volume-up" | "volume-off" | "volume-down" | "shortcut" | "backup-restore" | "fr-FR-flag" | "es-ES-flag" | "en-US-flag" | "en-EN-flag" | "de-DE-flag" | "ru-RU-flag" | "zh-CN-flag" | "zh-TW-flag" | "ja-JP-flag");
export const BsmIcon = memo(({ className, icon, style }: { className?: string; icon: BsmIconType; style?: CSSProperties }) => {
// TODO : Very ugly very messy, need to find a better way to do this
@@ -117,6 +120,15 @@ export const BsmIcon = memo(({ className, icon, style }: { className?: string; i
if (icon === "ru-RU-flag") {
return <RussianIcon className={className} style={style} />;
}
if (icon === "zh-CN-flag") {
return <ChineseIcon className={className} style={style} />;
}
if (icon === "zh-TW-flag") {
return <ChineseTraditionalIcon className={className} style={style} />;
}
if (icon === "ja-JP-flag") {
return <JapanIcon className={className} style={style} />;
}
if (icon === "task") {
return <TaskIcon className={className} style={style} />;
}
@@ -0,0 +1,21 @@
import { CSSProperties } from "react";
export function ChineseIcon(props: { className?: string; style?: CSSProperties }) {
return (
<svg width="71px" height="48px" viewBox="0 0 71 48" version="1.1" xmlns="http://www.w3.org/2000/svg" className={props.className} style={props.style}>
<defs />
<g id="Flags" stroke="none" strokeWidth="1" fill="none" fillRule="evenodd" transform="translate(-70.000000, -498.000000)">
<g transform="translate(70.000000, 70.000000)" fillRule="nonzero" id="China">
<g transform="translate(0.000000, 428.000000)">
<rect id="Rounded_Rectangle_7_copy-5" fill="#ED5565" x="0.28" y="0.98" width="70" height="47" rx="6.36" />
<path d="M12.23,26.34 C11.41,26.93 10.93,26.6 11.16,25.61 L12.05,21.93 C12.2430284,20.8254147 11.8671738,19.6978512 11.05,18.93 L8.36,16.6 C7.59,15.94 7.79,15.37 8.8,15.34 L12.57,15.23 C13.6782653,15.1750168 14.6410409,14.4499636 15,13.4 L16.18,9.4 C16.47,8.4 16.98,8.4 17.32,9.4 L18.74,13.4 C19.1553515,14.4093082 20.1106424,15.0927684 21.2,15.16 L25.04,15.16 C26.04,15.16 26.25,15.73 25.49,16.39 L22.56,18.95 C21.7650096,19.7288598 21.4350484,20.866657 21.69,21.95 L22.62,25.19 C22.9,26.19 22.43,26.53 21.57,26 L18.42,24 C17.4584774,23.4711751 16.2849442,23.5095258 15.36,24.1 L12.23,26.34 Z" id="Shape_2_copy_9" fill="#F6D660"/>
<path d="M29.37,10.78 C29.09,10.98 28.93,10.87 29.01,10.52 L29.31,9.24 C29.3656763,8.87495864 29.2472583,8.50490228 28.99,8.24 L28.08,7.43 C27.82,7.2 27.89,7 28.23,6.99 L29.5,6.99 C29.8717689,6.9586152 30.1877729,6.70581199 30.3,6.35 L30.7,4.95 C30.8,4.61 30.97,4.61 31.09,4.95 L31.57,6.33 C31.7126147,6.67114428 32.031835,6.90575195 32.4,6.94 L33.7,6.94 C34.04,6.94 34.11,7.14 33.85,7.37 L32.85,8.26 C32.5912527,8.52348971 32.4790218,8.89759264 32.55,9.26 L32.87,10.39 C32.97,10.73 32.81,10.86 32.52,10.67 L31.46,10 C31.1505989,9.82136721 30.7694011,9.82136721 30.46,10 L29.37,10.78 Z" id="Shape_2_copy_10" fill="#F6D660"/>
<path d="M29.37,30.53 C29.09,30.73 28.93,30.62 29.01,30.27 L29.31,28.99 C29.3656763,28.6249586 29.2472583,28.2549023 28.99,27.99 L28.08,27.18 C27.82,26.95 27.89,26.75 28.23,26.74 L29.5,26.74 C29.8691682,26.7101752 30.1844554,26.4618866 30.3,26.11 L30.7,24.71 C30.8,24.37 30.97,24.37 31.09,24.71 L31.57,26.09 C31.7126147,26.4311443 32.031835,26.6657519 32.4,26.7 L33.7,26.7 C34.04,26.7 34.11,26.9 33.85,27.13 L32.85,28.02 C32.5912527,28.2834897 32.4790218,28.6575926 32.55,29.02 L32.87,30.15 C32.97,30.49 32.81,30.62 32.52,30.43 L31.47,29.76 C31.1605989,29.5813672 30.7794011,29.5813672 30.47,29.76 L29.37,30.53 Z" id="Shape_2_copy_11" fill="#F6D660"/>
<path d="M36.3,25.23 C36.02,25.43 35.86,25.32 35.94,24.98 L36.24,23.7 C36.2956763,23.3349586 36.1772583,22.9649023 35.92,22.7 L35.01,21.89 C34.75,21.66 34.82,21.46 35.16,21.45 L36.43,21.45 C36.7991682,21.4201752 37.1144554,21.1718866 37.23,20.82 L37.63,19.42 C37.73,19.08 37.9,19.08 38.02,19.42 L38.5,20.8 C38.6426147,21.1411443 38.961835,21.3757519 39.33,21.41 L40.63,21.41 C40.97,21.41 41.04,21.61 40.78,21.84 L39.78,22.73 C39.5248508,22.9954093 39.4164332,23.369263 39.49,23.73 L39.81,24.86 C39.91,25.2 39.74,25.33 39.46,25.14 L38.41,24.47 C38.1005989,24.2913672 37.7194011,24.2913672 37.41,24.47 L36.3,25.23 Z" id="Shape_2_copy_12" fill="#F6D660"/>
<path d="M36.3,16.09 C36.02,16.29 35.86,16.18 35.94,15.83 L36.24,14.55 C36.2956763,14.1849586 36.1772583,13.8149023 35.92,13.55 L35,12.7 C34.74,12.47 34.81,12.27 35.15,12.26 L36.42,12.26 C36.7891682,12.2301752 37.1044554,11.9818866 37.22,11.63 L37.62,10.23 C37.72,9.89 37.89,9.89 38.01,10.23 L38.49,11.61 C38.6326147,11.9511443 38.951835,12.1857519 39.32,12.22 L40.62,12.22 C40.96,12.22 41.03,12.42 40.77,12.65 L39.77,13.54 C39.5163739,13.8062871 39.4081897,14.1793363 39.48,14.54 L39.8,15.67 C39.9,16.01 39.73,16.14 39.45,15.95 L38.4,15.28 C38.0905989,15.1013672 37.7094011,15.1013672 37.4,15.28 L36.3,16.09 Z" id="Shape_2_copy_13" fill="#F6D660"/>
</g>
</g>
</g>
</svg>
);
}
@@ -0,0 +1,18 @@
import { CSSProperties } from "react";
export function ChineseTraditionalIcon(props: { className?: string; style?: CSSProperties }) {
return (
<svg width="71px" height="48px" viewBox="0 0 71 48" fill="none" xmlns="http://www.w3.org/2000/svg" className={props.className} style={props.style}>
<defs />
<g id="Flags">
<path d="M65.6826 47.9999H5.31737C2.38058 47.9999 0 45.5435 0 42.5132V5.48699C0 2.45673 2.38058 0.00012207 5.31737 0.00012207H65.6826C68.6193 0.00012207 71 2.45658 71 5.48699V42.5132C71 45.5435 68.6194 47.9999 65.6826 47.9999Z" fill="#FF4B55"/>
<path d="M1.2242 24H34.2759C34.952 24 35.5001 23.4346 35.5001 22.7368V1.26322C35.5001 0.565643 34.952 0 34.2759 0H5.31737C2.38058 0.000143092 0 2.45661 0 5.48701V22.7371C0 23.4346 0.54817 24 1.2242 24Z" fill="#41479B"/>
<path d="M13.3685 12.0001C13.3685 10.7521 13.8585 9.62222 14.6509 8.80416L10.9797 7.72052C10.7666 7.65771 10.6139 7.93073 10.7731 8.0897L13.5203 10.8306L9.81306 11.7871C9.59812 11.8424 9.59812 12.1578 9.81306 12.2133L13.5203 13.1698L10.7731 15.9107C10.6138 16.0697 10.7666 16.3427 10.9797 16.2799L14.6509 15.1962C13.8585 14.378 13.3685 13.2482 13.3685 12.0001Z" fill="#F5F5F5"/>
<path d="M14.6525 8.80222C15.4453 7.98459 16.5403 7.4789 17.7498 7.4789C18.9593 7.4789 20.0542 7.98459 20.847 8.80222L21.8972 5.014C21.958 4.79407 21.6935 4.63652 21.5394 4.80079L18.8831 7.63545L17.9562 3.81002C17.9026 3.58823 17.597 3.58823 17.5431 3.81002L16.6163 7.63545L13.96 4.80079C13.8059 4.63638 13.5412 4.79407 13.6022 5.014L14.6525 8.80222Z" fill="#F5F5F5"/>
<path d="M20.8473 15.1981C20.0545 16.0157 18.9596 16.5214 17.7501 16.5214C16.5406 16.5214 15.4456 16.0157 14.6528 15.1981L13.6027 18.9865C13.5418 19.2064 13.8064 19.364 13.9605 19.1997L16.6167 16.365L17.5436 20.1905C17.5973 20.4123 17.9029 20.4123 17.9567 20.1905L18.8836 16.365L21.5398 19.1997C21.6939 19.3641 21.9585 19.2064 21.8976 18.9865L20.8473 15.1981Z" fill="#F5F5F5"/>
<path d="M25.6871 11.7868L21.9799 10.8304L24.7269 8.08946C24.8863 7.93048 24.7335 7.65746 24.5203 7.72028L20.8491 8.80392C21.6415 9.62212 22.1316 10.7518 22.1316 11.9999C22.1316 13.2479 21.6415 14.3778 20.8491 15.1958L24.5203 16.2795C24.7335 16.3423 24.8861 16.0693 24.7269 15.9103L21.9799 13.1694L25.6871 12.2129C25.9022 12.1579 25.9022 11.8426 25.6871 11.7868Z" fill="#F5F5F5"/>
<path d="M17.7502 15.6007C19.6771 15.6007 21.2392 13.9888 21.2392 12.0005C21.2392 10.0121 19.6771 8.40027 17.7502 8.40027C15.8233 8.40027 14.2612 10.0121 14.2612 12.0005C14.2612 13.9888 15.8233 15.6007 17.7502 15.6007Z" fill="#F5F5F5"/>
</g>
</svg>
);
}
@@ -0,0 +1,22 @@
import { CSSProperties } from "react";
export function JapanIcon(props: { className?: string; style?: CSSProperties }) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="71px" height="48px" viewBox="0 0 71 48" version="1.1" className={props.className} style={props.style}>
<defs />
<g id="Flags" stroke="none" strokeWidth="1" fill="none" fillRule="evenodd" transform="translate(-239,-805)">
<g transform="translate(70,70)" fillRule="nonzero" id="Russian">
<g transform="translate(169,735)">
<g id="Bolivia-9">
<path d="M 0.5 7 L 0.5 41 L 70.5 41 L 70.5 7 C 70.5 3.3 67.5 0.4 63.9 0.4 L 7.2 0.4 C 5.4 0.4 3.7 1.1 2.5 2.3 C 1.2 3.5 0.5 5.2 0.5 7 Z" id="Shape" fill="#ffffff" /> <polygon id="Shape" fill="#fff" points="0.5,33.0 70.5,33.0 70.5,15.0 0.5,15.0" />
<path d="M 0.5 40.8 C 0.5 44.4 3.5 47.4 7.2 47.4 L 63.9 47.4 C 67.5 47.4 70.5 44.4 70.5 40.8 L 70.5 31.8 L 0.5 31.8 L 0.5 40.8 Z" id="Shape" fill="#ffffff" />
<circle cx="35.5" cy="24" r="15" fill="#ec5565" />
</g>
</g>
</g>
</g>
</svg>
);
}
export default JapanIcon;
@@ -65,10 +65,10 @@ export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreIn
<span className="z-10 sticky flex items-center justify-end top-0 bg-inherit border-b-2 border-main-color-1">
<BsmButton className="rounded-full h-6 w-6 p-[2px]" withBar={false} icon="search" onClick={handleToogleFilter} />
</span>
<span className="z-10 sticky top-0 flex items-center bg-inherit border-main-color-1 border-b-2 h-8 px-1">{filterEnabled ? <motion.input autoFocus className="bg-main-color-1 rounded-md h-6 px-2" initial={{ width: 0 }} animate={{ width: "250px" }} transition={{ ease: "easeInOut", duration: 0.15 }} onChange={e => handleInput(e.target.value)} /> : <span className="w-full text-center">{t("pages.version-viewer.mods.mods-grid.header-bar.name")}</span>}</span>
<span className="z-10 sticky flex items-center justify-center top-0 bg-inherit border-b-2 border-main-color-1 h-8 px-2">{t("pages.version-viewer.mods.mods-grid.header-bar.installed")}</span>
<span className="z-10 sticky flex items-center justify-center top-0 bg-inherit border-b-2 border-main-color-1 h-8 px-2">{t("pages.version-viewer.mods.mods-grid.header-bar.latest")}</span>
<span className="z-10 sticky flex items-center justify-center top-0 bg-inherit border-b-2 border-main-color-1 h-8">{t("pages.version-viewer.mods.mods-grid.header-bar.description")}</span>
<span className="z-10 sticky top-0 flex items-center bg-inherit border-main-color-1 border-b-2 h-8 px-1 whitespace-nowrap">{filterEnabled ? <motion.input autoFocus className="bg-main-color-1 rounded-md h-6 px-2" initial={{ width: 0 }} animate={{ width: "250px" }} transition={{ ease: "easeInOut", duration: 0.15 }} onChange={e => handleInput(e.target.value)} /> : <span className="w-full text-center">{t("pages.version-viewer.mods.mods-grid.header-bar.name")}</span>}</span>
<span className="z-10 sticky flex items-center justify-center top-0 bg-inherit border-b-2 border-main-color-1 h-8 px-2 whitespace-nowrap">{t("pages.version-viewer.mods.mods-grid.header-bar.installed")}</span>
<span className="z-10 sticky flex items-center justify-center top-0 bg-inherit border-b-2 border-main-color-1 h-8 px-2 whitespace-nowrap">{t("pages.version-viewer.mods.mods-grid.header-bar.latest")}</span>
<span className="z-10 sticky flex items-center justify-center top-0 bg-inherit border-b-2 border-main-color-1 h-8 whitespace-nowrap">{t("pages.version-viewer.mods.mods-grid.header-bar.description")}</span>
<span className="z-10 sticky top-0 bg-inherit border-b-2 border-main-color-1 h-8 flex justify-start items-center py-1 pl-[3px] min-w-[50px]">
<BsmDropdownButton className="h-full aspect-square relative rounded-full bg-light-main-color-1 dark:bg-main-color-3" withBar={false} icon="three-dots" buttonClassName="!rounded-full !p-[2px] !bg-light-main-color-2 dark:!bg-main-color-2 hover:!bg-light-main-color-1 dark:hover:!bg-main-color-3" menuTranslationY="5px" items={[{ text: "pages.version-viewer.mods.mods-grid.header-bar.dropdown.uninstall-all", icon: "trash", onClick: handleUninstallAll }]} />
</span>
@@ -3,7 +3,7 @@ export const defaultConfiguration: { [key in DefaultConfigKey]: any } = {
"second-color": "#ff4444",
theme: "os",
language: window.navigator.language.length <= 2 ? `${window.navigator.language}-${window.navigator.language.toLocaleUpperCase()}` : window.navigator.language,
supported_languages: ["en-US", "en-EN", "fr-FR", "es-ES", "de-DE", "ru-RU"],
supported_languages: ["en-US", "en-EN", "fr-FR", "es-ES", "de-DE", "ru-RU", "zh-CN", "zh-TW", "ja-JP"],
default_mods: ["SongCore", "WhyIsThereNoLeaderboard", "BeatSaverDownloader", "BeatSaverVoting", "PlaylistManager"],
"default-shared-folders": [
window.electron.path.join("Beat Saber_Data", "CustomLevels"),
@@ -235,7 +235,7 @@ export function SettingsPage() {
<SettingContainer id="choose-default-store" minorTitle="pages.settings.steam-and-oculus.download-platform.title" description="pages.settings.steam-and-oculus.download-platform.desc" className="mt-3">
<SettingRadioArray items={[
{ id: 1, text: "Steam", value: BsStore.STEAM, icon: <SteamIcon className="h-6 w-6 float-left"/> },
{ id: 2, text: "Oculus Store (PC)", value: BsStore.OCULUS, icon: <OculusIcon className="h-6 w-6 float-left bg-white text-black rounded-full p-0.5"/>},
{ id: 2, text: "Oculus Store (PC)", value: BsStore.OCULUS, icon: <OculusIcon className="h-6 w-6 float-left bg-white text-black rounded-full p-0.5"/>, disabled: true},
{ id: 0, text: t("pages.settings.steam-and-oculus.download-platform.always-ask"), value: undefined, },
]} selectedItemValue={downloadStore} onItemSelected={handleChangeBsStore}/>
</SettingContainer>
@@ -75,7 +75,7 @@ export class BsDownloaderService extends AbstractBsDownloaderService {
public async downloadVersion(version: BSVersion, from?: BsStore): Promise<BSVersion> {
if(!from){
from = this.defaultStore ?? await this.chooseStoreToDownloadFrom();
from = this.defaultStore === BsStore.STEAM ? BsStore.STEAM : await this.chooseStoreToDownloadFrom();
}
return this.getStoreDownloader(from).downloadBsVersion(version).then(() => {
@@ -58,7 +58,7 @@ export class SteamDownloaderService extends AbstractBsDownloaderService implemen
duration: 11_000,
title: "notifications.bs-download.steam-download.errors.titles.dotnet-required",
desc: "notifications.bs-download.steam-download.errors.msg.dotnet-required",
actions: [{ id: "0", title: "notifications.bs-download..errors.actions.download-dotnet" }],
actions: [{ id: "0", title: "notifications.bs-download.steam-download.errors.actions.download-dotnet" }],
});
if (choice === "0") {
+16 -2
View File
@@ -33,9 +33,9 @@ export class I18nService {
filter(l => !!l),
distinctUntilChanged()
)
.subscribe(lang => {
.subscribe(async lang => {
this.cache.clear();
this.dictionary = require(`../../../assets/jsons/translations/${lang.split("-")[0]}.json`);
this.dictionary = this.importLang([lang, lang.split("-")[0]], "en");
i18n.dayNames = getProperty(this.dictionary, "dateformat.dayNames");
i18n.monthNames = getProperty(this.dictionary, "dateformat.monthNames");
@@ -43,6 +43,20 @@ export class I18nService {
});
}
private importLang(lang: string[], fallback: string): Record<string, string> {
for (const l of lang) {
try {
return require(`../../../assets/jsons/translations/${l.toLowerCase()}.json`);
}
catch (e) {
continue;
}
}
return require(`../../../assets/jsons/translations/${fallback.toLowerCase()}.json`);
}
public getSupportedLanguages(): string[] {
return this.configService.get("supported_languages" as DefaultConfigKey);
}