mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
Merge branch 'v1.5.0' into feature/playlists/107
This commit is contained in:
@@ -32,7 +32,6 @@ ipc.on<LaunchOption>("create-launch-shortcut", (req, reply) => {
|
||||
reply(from(bsLauncher.createLaunchShortcut(req.args)));
|
||||
});
|
||||
|
||||
|
||||
ipc.on<void>("bs-launch.restore-steamvr", (_, reply) => {
|
||||
const steamLauncher = SteamLauncherService.getInstance();
|
||||
reply(from(steamLauncher.restoreSteamVR()));
|
||||
@@ -43,4 +42,4 @@ ipc.on<void>("restore-original-oculus-folder", (_, reply) => {
|
||||
reply(from(
|
||||
oculusLauncher.deleteBsSymlinks().then(() => oculusLauncher.restoreOriginalBeatSaber())
|
||||
));
|
||||
})
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import { AbstractLauncherService } from "./abstract-launcher.service";
|
||||
import { taskRunning } from "../../helpers/os.helpers";
|
||||
import { CustomError } from "../../../shared/models/exceptions/custom-error.class";
|
||||
import { InstallationLocationService } from "../installation-location.service";
|
||||
import { ensurePathNotAlreadyExist } from "../../helpers/fs.helpers";
|
||||
|
||||
export class OculusLauncherService extends AbstractLauncherService implements StoreLauncherInterface {
|
||||
|
||||
@@ -86,7 +87,7 @@ export class OculusLauncherService extends AbstractLauncherService implements St
|
||||
private async backupOriginalBeatSaber(): Promise<void>{
|
||||
const bsFolder = await this.oculus.getGameFolder(OCULUS_BS_DIR);
|
||||
if(!bsFolder){ return; }
|
||||
const backupPath = path.join(bsFolder, "..", OCULUS_BS_BACKUP_DIR);
|
||||
const backupPath = await ensurePathNotAlreadyExist(path.join(bsFolder, "..", OCULUS_BS_BACKUP_DIR));
|
||||
log.info("Backing up original Beat Saber", bsFolder, backupPath);
|
||||
return rename(bsFolder, backupPath);
|
||||
}
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
.content * {
|
||||
all: revert;
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import { ChangelogVersion } from "renderer/services/auto-updater.service";
|
||||
import { LinkOpenerService } from "renderer/services/link-opener.service";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { ModalComponent } from "renderer/services/modale.service";
|
||||
import DOMPurify from 'dompurify';
|
||||
import './changelog-modal.component.css';
|
||||
import Tippy from "@tippyjs/react";
|
||||
|
||||
export const ChangelogModal: ModalComponent<void, ChangelogVersion> = ({ resolver, data: changelog }) => {
|
||||
|
||||
const linkOpener = useService(LinkOpenerService);
|
||||
const openGithub = () => linkOpener.open("https://github.com/Zagrios/bs-manager");
|
||||
const openTwitter = () => linkOpener.open("https://twitter.com/BSManager_");
|
||||
const openSupportPage = () => linkOpener.open("https://www.patreon.com/bsmanager");
|
||||
const openDiscord = () => linkOpener.open("https://discord.gg/uSqbHVpKdV");
|
||||
const date = changelog?.timestamp ? new Date(changelog.timestamp * 1000).toLocaleDateString() : '';
|
||||
|
||||
return (
|
||||
<form className="w-[350px] text-gray-800 dark:text-gray-200 h-[70vh] flex flex-col justify-between">
|
||||
<h1 className=" p-4 pt-1 text-3xl uppercase tracking-wide w-full text-center text-gray-800 dark:text-gray-200 font-bold">{changelog?.title}</h1>
|
||||
<div className=" overflow-y-scroll h-full content grow" dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(changelog?.htmlBody) }}/>
|
||||
<span className="block w-[100%] mx-auto mt-0 mb-4 h-1 rounded-full bg-main-color-1" />
|
||||
<div className="flex flex-row justify-between">
|
||||
<div className="my-auto flex flex-col text-sm">
|
||||
<span>{date}</span>
|
||||
<span>v{changelog?.version}</span>
|
||||
</div>
|
||||
<div className="flex flex-row gap-2 my-auto">
|
||||
<Tippy content="Twitter" placement="top" className="font-bold !bg-neutral-900" duration={[200, 0]} arrow={false}>
|
||||
<div><BsmButton onClick={openTwitter} className="rounded-md p-1 w-7 h-7" icon="twitter" withBar={false} iconColor="#fff" color="#000"/></div>
|
||||
</Tippy>
|
||||
<Tippy content="Github" placement="top" className="font-bold !bg-neutral-900" duration={[200, 0]} arrow={false}>
|
||||
<div><BsmButton onClick={openGithub} className="rounded-md p-1 w-7 h-7" icon="github" withBar={false} iconColor="#fff" color="#000"/></div>
|
||||
</Tippy>
|
||||
<Tippy content="Discord" placement="top" className="font-bold !bg-neutral-900" duration={[200, 0]} arrow={false}>
|
||||
<div><BsmButton onClick={openDiscord} className="rounded-md p-1 w-7 h-7" icon="discord" withBar={false} iconColor="#fff" color="#000"/></div>
|
||||
</Tippy>
|
||||
<Tippy content="Patreon" placement="top" className="font-bold !bg-neutral-900" duration={[200, 0]} arrow={false}>
|
||||
<div><BsmButton onClick={openSupportPage} className="rounded-md p-1 w-7 h-7 " icon="patreon" withBar={false} iconColor="#fff" color="#000"/></div>
|
||||
</Tippy>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { useClickOutside } from "renderer/hooks/use-click-outside.hook";
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
import { getCorrectTextColor } from "renderer/helpers/correct-text-color";
|
||||
|
||||
type BsmButtonType = "primary" | "secondary" | "success" | "cancel" | "error";
|
||||
type BsmButtonType = "primary" | "secondary" | "success" | "cancel" | "error" | "none";
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
@@ -50,7 +50,7 @@ export function BsmButton({ className, style, imgClassName, iconClassName, icon,
|
||||
if (primaryColor) {
|
||||
return getCorrectTextColor(primaryColor);
|
||||
}
|
||||
return typeColor ? "white" : undefined;
|
||||
return typeColor && typeColor !== "none" ? "white" : undefined;
|
||||
})();
|
||||
|
||||
const renderTypeColor = (() => {
|
||||
|
||||
@@ -2,9 +2,8 @@ import { CSSProperties } from "react";
|
||||
|
||||
export default function PatreonIcon(props: { className?: string; style?: CSSProperties }) {
|
||||
return (
|
||||
<svg className={props.className} style={props.style} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180">
|
||||
<path fill="white" d="M108.8135992 26.06720125c-26.468266 0-48.00213212 21.53066613-48.00213212 47.99733213 0 26.38653268 21.53386613 47.85426547 48.00213213 47.85426547 26.38639937 0 47.8530655-21.4677328 47.8530655-47.85426547 0-26.466666-21.46666613-47.99733213-47.85306547-47.99733213" />
|
||||
<path fill="#052a49" d="M23.333335 153.93333178V26.0666679h23.46666576v127.8666639z" />
|
||||
<svg className={props.className} style={props.style} xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 1080 1080" xmlSpace="preserve" fill="currentColor">
|
||||
<path d="M1033.05,324.45c-0.19-137.9-107.59-250.92-233.6-291.7c-156.48-50.64-362.86-43.3-512.28,27.2 C106.07,145.41,49.18,332.61,47.06,519.31c-1.74,153.5,13.58,557.79,241.62,560.67c169.44,2.15,194.67-216.18,273.07-321.33 c55.78-74.81,127.6-95.94,216.01-117.82C929.71,603.22,1033.27,483.3,1033.05,324.45z"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,10 +2,8 @@ import { CSSProperties } from "react";
|
||||
|
||||
export function TwitterIcon(props: { className?: string; style?: CSSProperties }) {
|
||||
return (
|
||||
<svg className={props.className} style={props.style} viewBox="0 0 24 24" fill="currentColor">
|
||||
<g>
|
||||
<path d="M23.643 4.937c-.835.37-1.732.62-2.675.733.962-.576 1.7-1.49 2.048-2.578-.9.534-1.897.922-2.958 1.13-.85-.904-2.06-1.47-3.4-1.47-2.572 0-4.658 2.086-4.658 4.66 0 .364.042.718.12 1.06-3.873-.195-7.304-2.05-9.602-4.868-.4.69-.63 1.49-.63 2.342 0 1.616.823 3.043 2.072 3.878-.764-.025-1.482-.234-2.11-.583v.06c0 2.257 1.605 4.14 3.737 4.568-.392.106-.803.162-1.227.162-.3 0-.593-.028-.877-.082.593 1.85 2.313 3.198 4.352 3.234-1.595 1.25-3.604 1.995-5.786 1.995-.376 0-.747-.022-1.112-.065 2.062 1.323 4.51 2.093 7.14 2.093 8.57 0 13.255-7.098 13.255-13.254 0-.2-.005-.402-.014-.602.91-.658 1.7-1.477 2.323-2.41z" />
|
||||
</g>
|
||||
<svg className={props.className} style={props.style} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 1227" fill="currentColor">
|
||||
<path d="M714.163 519.284L1160.89 0H1055.03L667.137 450.887L357.328 0H0L468.492 681.821L0 1226.37H105.866L515.491 750.218L842.672 1226.37H1200L714.137 519.284H714.163ZM569.165 687.828L521.697 619.934L144.011 79.6944H306.615L611.412 515.685L658.88 583.579L1055.08 1150.3H892.476L569.165 687.854V687.828Z"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,9 +40,12 @@ 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";
|
||||
|
||||
|
||||
export function SettingsPage() {
|
||||
|
||||
|
||||
const configService = useService(ConfigurationService);
|
||||
const themeService = useService(ThemeService);
|
||||
const ipcService = useService(IpcService);
|
||||
@@ -58,6 +61,7 @@ export function SettingsPage() {
|
||||
const playlistsManager = useService(PlaylistsManagerService);
|
||||
const modelsManager = useService(ModelsManagerService);
|
||||
const versionLinker = useService(VersionFolderLinkerService);
|
||||
const autoUpdater = useService(AutoUpdaterService);
|
||||
|
||||
const { firstColor, secondColor } = useThemeColor();
|
||||
|
||||
@@ -89,6 +93,9 @@ export function SettingsPage() {
|
||||
const [hasDownloaderSession, setHasDownloaderSession] = useState(false);
|
||||
const appVersion = useObservable(() => ipcService.sendV2<string>("current-version"));
|
||||
|
||||
const [isChangelogAvailable, setIsChangelogAvailable] = useState(true);
|
||||
const [changlogsLoading, setChanglogsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadInstallationFolder();
|
||||
loadDownloadersSession();
|
||||
@@ -137,6 +144,23 @@ export function SettingsPage() {
|
||||
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(() => {
|
||||
setIsChangelogAvailable(() => false);
|
||||
})
|
||||
.finally(() => { isChangelogResolved = true; });
|
||||
|
||||
setChanglogsLoading(() => false);
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
|
||||
const setDefaultInstallationFolder = () => {
|
||||
if (!progressBarService.require()) {
|
||||
return;
|
||||
@@ -232,7 +256,7 @@ export function SettingsPage() {
|
||||
|
||||
<SettingContainer title="pages.settings.steam-and-oculus.title" description="pages.settings.steam-and-oculus.description">
|
||||
<BsmButton onClick={clearDownloadersSession} className="w-fit px-3 py-[2px] text-white rounded-md" withBar={false} text="pages.settings.steam-and-oculus.logout" typeColor="error" disabled={!hasDownloaderSession}/>
|
||||
|
||||
|
||||
<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"/> },
|
||||
@@ -240,7 +264,7 @@ export function SettingsPage() {
|
||||
{ id: 0, text: t("pages.settings.steam-and-oculus.download-platform.always-ask"), value: null, },
|
||||
]} selectedItemValue={downloadStore} onItemSelected={handleChangeBsStore}/>
|
||||
</SettingContainer>
|
||||
|
||||
|
||||
</SettingContainer>
|
||||
|
||||
<SettingContainer title="pages.settings.appearance.title" description="pages.settings.appearance.description">
|
||||
@@ -426,7 +450,7 @@ export function SettingsPage() {
|
||||
<SettingContainer className="mt-3" description="pages.settings.discord.description">
|
||||
<div className="flex gap-2">
|
||||
<BsmButton className="flex w-fit rounded-md h-8 px-2 font-bold py-1 !text-white" withBar={false} text="Discord" icon="discord" iconClassName="p-0.5 mr-1" color="#5865f2" onClick={openDiscord} />
|
||||
<BsmButton className="flex w-fit rounded-md h-8 px-2 font-bold py-1 !text-white" withBar={false} text="Twitter" icon="twitter" iconClassName="p-0.5 mr-1" color="#1A8CD8" onClick={openTwitter} />
|
||||
<BsmButton className="flex w-fit rounded-md h-8 px-2 font-bold py-1 !text-white" withBar={false} text="Twitter" icon="twitter" iconClassName="p-0.5 mr-1" color="#000" onClick={openTwitter} />
|
||||
</div>
|
||||
</SettingContainer>
|
||||
<SettingContainer className="pt-3" description="pages.settings.contribution.description">
|
||||
@@ -442,10 +466,13 @@ export function SettingsPage() {
|
||||
</div>
|
||||
</SettingContainer>
|
||||
</SettingContainer>
|
||||
|
||||
<span className="bg-light-main-color-1 dark:bg-main-color-1 rounded-md py-1 px-2 font-bold float-right mb-5">v{appVersion}</span>
|
||||
<Tippy content={isChangelogAvailable ? t("pages.settings.changelogs.open") : t("pages.settings.changelogs.not-founds")} placement="left" className="font-bold bg-main-color-3">
|
||||
<div className="!bg-light-main-color-1 dark:!bg-main-color-1 rounded-md py-1 px-2 font-bold float-right mb-5 hover:brightness-125 h-auto w-auto">
|
||||
<BsmButton onClick={handleVersionClick} text={`v${appVersion}`} withBar={false} typeColor="none"/>
|
||||
</div>
|
||||
</Tippy>
|
||||
<BsmImage className={`h-7 my-auto spin-loading float-right ${changlogsLoading ? "" : "hidden"}`} image={BeatWaitingImg} loading="eager" />
|
||||
</div>
|
||||
|
||||
<SupportersView isVisible={showSupporters} setVisible={setShowSupporters} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
import { Observable, lastValueFrom } from "rxjs";
|
||||
import { map } from "rxjs/operators";
|
||||
import { IpcService } from "./ipc.service";
|
||||
import { ProgressBarService } from "./progress-bar.service";
|
||||
import { ModalService } from "renderer/services/modale.service";
|
||||
import { ChangelogModal } from "renderer/components/modal/modal-types/chabgelog-modal/changelog-modal.component";
|
||||
import { ConfigurationService } from "./configuration.service";
|
||||
import { Observable, lastValueFrom } from "rxjs";
|
||||
|
||||
|
||||
export interface Changelog {
|
||||
[version: string]: ChangelogVersion;
|
||||
}
|
||||
export interface ChangelogVersion {
|
||||
htmlBody: string;
|
||||
title: string;
|
||||
timestamp : number;
|
||||
version: string;
|
||||
}
|
||||
export class AutoUpdaterService {
|
||||
private static instance: AutoUpdaterService;
|
||||
|
||||
@@ -11,6 +24,12 @@ export class AutoUpdaterService {
|
||||
|
||||
private downloadProgress$: Observable<number>;
|
||||
|
||||
private modal: ModalService;
|
||||
|
||||
private configurationService: ConfigurationService;
|
||||
|
||||
private cacheChangelog: Changelog;
|
||||
|
||||
public static getInstance(): AutoUpdaterService {
|
||||
if (!AutoUpdaterService.instance) {
|
||||
AutoUpdaterService.instance = new AutoUpdaterService();
|
||||
@@ -21,6 +40,8 @@ export class AutoUpdaterService {
|
||||
private constructor() {
|
||||
this.progressService = ProgressBarService.getInstance();
|
||||
this.ipcService = IpcService.getInstance();
|
||||
this.modal = ModalService.getInstance();
|
||||
this.configurationService = ConfigurationService.getInstance();
|
||||
|
||||
this.downloadProgress$ = this.ipcService.watch<number>("update-download-progress").pipe(map(res => (res.success ? res.data : 0)));
|
||||
}
|
||||
@@ -35,11 +56,59 @@ export class AutoUpdaterService {
|
||||
});
|
||||
|
||||
this.progressService.show(this.downloadProgress$, true);
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
public quitAndInstall() {
|
||||
this.ipcService.sendLazy("install-update");
|
||||
}
|
||||
|
||||
public getLastAppVersion(): string {
|
||||
return this.configurationService.get("last-app-version");
|
||||
}
|
||||
|
||||
public setLastAppVersion(value : string){
|
||||
this.configurationService.set("last-app-version", value);
|
||||
}
|
||||
|
||||
private async getChangelog(): Promise<Changelog> {
|
||||
if (this.cacheChangelog) {
|
||||
return this.cacheChangelog;
|
||||
}
|
||||
|
||||
const path = `https://raw.githubusercontent.com/Zagrios/bs-manager/master/assets/jsons/changelogs.json`
|
||||
const response = await fetch(path);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch changelogs (${response.status})`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!data) {
|
||||
throw new Error(`Failed to parse changelogs`);
|
||||
}
|
||||
|
||||
this.cacheChangelog = data;
|
||||
return data;
|
||||
}
|
||||
|
||||
private async getChangelogVersion(version:string): Promise<ChangelogVersion> {
|
||||
const changelogs = await this.getChangelog();
|
||||
|
||||
const changelogVersion = changelogs[version];
|
||||
if (!changelogVersion) {
|
||||
throw new Error(`No changelog found for this version (${version})`);
|
||||
}
|
||||
|
||||
return changelogVersion;
|
||||
}
|
||||
|
||||
public getAppVersion() : Observable<string> {
|
||||
return this.ipcService.sendV2<string>("current-version");
|
||||
}
|
||||
|
||||
public async showChangelog(version:string): Promise<void>{
|
||||
const changelog = await this.getChangelogVersion(version);
|
||||
|
||||
this.modal.openModal(ChangelogModal, changelog);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,12 @@ 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 { NotificationService } from "renderer/services/notification.service";
|
||||
import { timer } from "rxjs";
|
||||
import { lastValueFrom, timer } from "rxjs";
|
||||
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 { gt } from "semver"
|
||||
|
||||
export default function App() {
|
||||
useService(OsDiagnosticService);
|
||||
@@ -31,14 +33,27 @@ export default function App() {
|
||||
const models = useService(ModelsManagerService);
|
||||
const notification = useService(NotificationService);
|
||||
const config = useService(ConfigurationService);
|
||||
const autoUpdater = useService(AutoUpdaterService);
|
||||
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
checkIsUpdated();
|
||||
checkOneClicks();
|
||||
}, []);
|
||||
|
||||
const checkIsUpdated = async () => {
|
||||
const appVersion = await lastValueFrom(autoUpdater.getAppVersion());
|
||||
const lastAppVersion = autoUpdater.getLastAppVersion();
|
||||
|
||||
autoUpdater.setLastAppVersion(appVersion);
|
||||
|
||||
if (lastAppVersion && gt(appVersion, lastAppVersion)) {
|
||||
autoUpdater.showChangelog(appVersion);
|
||||
}
|
||||
};
|
||||
|
||||
const checkOneClicks = async () => {
|
||||
if (config.get("not-remind-oneclick") === true) {
|
||||
return;
|
||||
|
||||
@@ -9,7 +9,6 @@ import { WindowManagerService } from "../services/window-manager.service";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
|
||||
export default function Launcher() {
|
||||
|
||||
const updaterService = useService(AutoUpdaterService);
|
||||
const windowService = useService(WindowManagerService);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user