Merge pull request #122 from Zagrios/feature/import-external-version/121

feature/import-external-version/121
This commit is contained in:
MathieuG-P
2023-01-23 02:17:42 +01:00
committed by GitHub
24 changed files with 322 additions and 79 deletions
+28 -1
View File
@@ -85,7 +85,11 @@
},
"available-versions": {
"title": "Select a version",
"steam-release": "Release page"
"steam-release": "Release page",
"dropdown": {
"refresh": "Refresh list",
"import-version" :"Import a version"
}
},
"settings": {
"steam": {
@@ -233,6 +237,22 @@
}
}
},
"bs-import-version":{
"success":{
"start-import": {
"title": "Import in progress 👌",
"desc": "The import may take several minutes depending on your configuration."
},
"imported": {
"title": "Imported version 🎉"
}
},
"errors":{
"import-error":{
"desc": "Check that the selected folder is a BeatSaber installation."
}
}
},
"settings": {
"move-folder": {
"success": {
@@ -427,6 +447,13 @@
"submit": "Login"
}
},
"bs-import-version":{
"title": "Import a version",
"description": "Imports a version of BeatSaber to use with BSManager. The import will copy the selected BeatSaber installation folder to the BSManager versions folder.",
"buttons":{
"submit": "Import a version"
}
},
"bs-uninstall": {
"title": "Uninstall",
"description": "Are you sure you want to uninstall BeatSaber {version} ? You will have to download it again to play it.",
+28 -1
View File
@@ -85,7 +85,11 @@
},
"available-versions": {
"title": "Seleccione una versión",
"steam-release": "Publicación Página"
"steam-release": "Publicación Página",
"dropdown": {
"refresh": "Actualizar la lista",
"import-version" :"Importar una versión"
}
},
"settings": {
"steam": {
@@ -233,6 +237,22 @@
}
}
},
"bs-import-version":{
"success":{
"start-import": {
"title": "Importación en curso 👌",
"desc": "La importación puede tardar varios minutos dependiendo de su configuración."
},
"imported": {
"title": "Versión importada 🎉"
}
},
"errors":{
"import-error":{
"desc": "Compruebe que la carpeta seleccionada es una instalación de BeatSaber."
}
}
},
"settings": {
"move-folder": {
"success": {
@@ -427,6 +447,13 @@
"submit": "Inicio de sesión"
}
},
"bs-import-version":{
"title": "Importar una versión",
"description": "Importa una versión de BeatSaber para usar con BSManager. La importación copiará la carpeta de instalación seleccionada de BeatSaber en la carpeta de versiones de BSManager.",
"buttons":{
"submit": "Importar una versión"
}
},
"bs-uninstall": {
"title": "Desinstalar",
"description": "¿Estás seguro de que quieres desinstalar BeatSaber {version}? Tendrás que descargarlo de nuevo para poder jugar.",
+28 -1
View File
@@ -85,7 +85,11 @@
},
"available-versions": {
"title": "Choisi une version",
"steam-release": "Notes de version"
"steam-release": "Notes de version",
"dropdown": {
"refresh": "Rafraichir la liste",
"import-version" :"Importer une version"
}
},
"settings": {
"steam": {
@@ -233,6 +237,22 @@
}
}
},
"bs-import-version":{
"success":{
"start-import": {
"title": "Importation en cours 👌",
"desc": "L'importation peut durer plusieurs minutes selon votre configuration."
},
"imported": {
"title": "Version importée 🎉"
}
},
"errors":{
"import-error":{
"desc": "Vérifier que le dossier sélectionné est une installation de BeatSaber."
}
}
},
"settings": {
"move-folder": {
"success": {
@@ -427,6 +447,13 @@
"submit": "Se connecter"
}
},
"bs-import-version":{
"title": "Importer une version",
"description": "Importe une version de BeatSaber pour profiter des fonctionnalités de BSManager. L'importation copiera le dossier d'installation de BeatSaber sélectionné dans le dossier de versions de BSManager.",
"buttons":{
"submit": "Importer une version"
}
},
"bs-uninstall": {
"title": "Désinstaller",
"description": "Est-tu sûr de vouloir désinstaller BeatSaber {version} ? Tu vas devoir la retélécharger pour y jouer.",
+10
View File
@@ -65,4 +65,14 @@ ipcMain.on('bs-download.set-installation-folder', (event, request: IpcRequest<st
});
})
ipcMain.on('bs-download.import-version', (event, request: IpcRequest<string>) => {
const utils = UtilsService.getInstance();
const installer = BSInstallerService.getInstance();
installer.importVersion(request.args).catch(e => {
utils.ipcSend(request.responceChannel, {success: false, error: e});
}).then(() => {
utils.ipcSend(request.responceChannel, {success: true});
});
})
+1 -1
View File
@@ -33,7 +33,7 @@ ipcMain.on('new-window', async (event, request: IpcRequest<string>) => {
});
ipcMain.on('choose-folder', async (event, request: IpcRequest<void>) => {
dialog.showOpenDialog({properties: ['openDirectory']}).then(res => {
dialog.showOpenDialog({properties: ['openDirectory'],}).then(res => {
UtilsService.getInstance().ipcSend(request.responceChannel, {success: true, data: res});
});
});
+25 -1
View File
@@ -1,6 +1,6 @@
import { BS_APP_ID, BS_DEPOT } from "../constants";
import path from "path";
import { BSVersion } from 'shared/bs-version.interface';
import { BSVersion, PartialBSVersion } from 'shared/bs-version.interface';
import { UtilsService } from "./utils.service";
import { ChildProcessWithoutNullStreams, spawn, spawnSync } from "child_process";
import log from "electron-log";
@@ -9,6 +9,7 @@ import { ctrlc } from "ctrlc-windows";
import { BSLocalVersionService } from "./bs-local-version.service";
import isOnline from 'is-online';
import { WindowManagerService } from "./window-manager.service";
import { copy, copySync } from "fs-extra";
export class BSInstallerService{
@@ -159,6 +160,29 @@ export class BSInstallerService{
})
}
public async importVersion(path: string): Promise<PartialBSVersion>{
const rawBsVersion = await this.localVersionService.getVersionOfBSFolder(path);
if(!rawBsVersion){ throw new Error("NOT_BS_FOLDER"); }
const originalPath = await this.localVersionService.getVersionPath(rawBsVersion);
let destPath = originalPath;
let folderExist = this.utils.pathExist(destPath);
let i = 0;
while(folderExist){
i++;
destPath = `${originalPath} (${i})`;
folderExist = this.utils.pathExist(destPath);
}
await copy(path, destPath, {dereference: true});
return rawBsVersion;
}
}
+21 -16
View File
@@ -1,5 +1,5 @@
import { BSVersionLibService } from "./bs-version-lib.service";
import { BSVersion } from 'shared/bs-version.interface';
import { BSVersion, PartialBSVersion } from 'shared/bs-version.interface';
import { InstallationLocationService } from "./installation-location.service";
import { SteamService } from "./steam.service";
import { UtilsService } from "./utils.service";
@@ -33,27 +33,32 @@ export class BSLocalVersionService{
return BSLocalVersionService.instance;
}
private constructor(){
this.installLocationService = InstallationLocationService.getInstance();
this.utilsService = UtilsService.getInstance();
this.steamService = SteamService.getInstance();
this.oculusService = OculusService.getInstance();
this.remoteVersionService = BSVersionLibService.getInstance();
this.configService = ConfigurationService.getInstance();
}
private constructor(){
this.installLocationService = InstallationLocationService.getInstance();
this.utilsService = UtilsService.getInstance();
this.steamService = SteamService.getInstance();
this.oculusService = OculusService.getInstance();
this.remoteVersionService = BSVersionLibService.getInstance();
this.configService = ConfigurationService.getInstance();
}
private async getVersionOfBSFolder(bsPath: string): Promise<string>{
public async getVersionOfBSFolder(bsPath: string): Promise<PartialBSVersion>{
const versionFilePath = path.join(bsPath, 'Beat Saber_Data', 'globalgamemanagers');
if(!this.utilsService.pathExist(versionFilePath)){ return null; }
const versionsAvailable = await this.remoteVersionService.getAvailableVersions();
return new Promise<string>(resolve => {
return new Promise<PartialBSVersion>(resolve => {
const stream = createReadStream(versionFilePath);
let findVersion: string = null;
let findVersion: PartialBSVersion = null;
stream.on('data', (line) => {
line = line.toString();
for(const version of versionsAvailable){
if(line.includes(version.BSVersion)){
findVersion = version.BSVersion;
findVersion = {BSVersion: version.BSVersion};
if(findVersion.BSVersion !== path.basename(bsPath)){
findVersion.name = path.basename(bsPath);
}
stream.close();
stream.destroy();
break;
@@ -118,7 +123,7 @@ export class BSLocalVersionService{
const steamBsVersion = await this.getVersionOfBSFolder(steamBsFolder);
if(!steamBsVersion){ return null; }
const version = await this.remoteVersionService.getVersionDetails(steamBsVersion);
const version = await this.remoteVersionService.getVersionDetails(steamBsVersion.BSVersion);
if(!version){ return null; }
return {...version, steam: true};
}
@@ -128,7 +133,7 @@ export class BSLocalVersionService{
if(!oculusBsFolder){ return null; }
const oculusBsVersion = await this.getVersionOfBSFolder(oculusBsFolder);
if(!oculusBsVersion){ return null; }
const version = await this.remoteVersionService.getVersionDetails(oculusBsVersion);
const version = await this.remoteVersionService.getVersionDetails(oculusBsVersion.BSVersion);
if(!version){ return null; }
return {...version, oculus: true};
}
@@ -151,7 +156,7 @@ export class BSLocalVersionService{
log.info("try get version from folder", f);
const rawVersion = await this.getVersionOfBSFolder(f);
if(!rawVersion){ continue; }
const bsVersion = {...await this.remoteVersionService.getVersionDetails(rawVersion)};
const bsVersion = {...await this.remoteVersionService.getVersionDetails(rawVersion.BSVersion)};
if(!bsVersion){ continue; }
bsVersion.name = path.basename(f) !== bsVersion.BSVersion ? path.basename(f) : undefined;
@@ -8,6 +8,8 @@ import { BsmImage } from "../shared/bsm-image.component";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { BsmIcon } from "../svgs/bsm-icon.component";
import { LinkOpenerService } from 'renderer/services/link-opener.service';
import { motion } from 'framer-motion';
import { GlowEffect } from '../shared/glow-effect.component';
export const AvailableVersionItem = memo(function AvailableVersionItem(props: {version: BSVersion}) {
@@ -15,6 +17,7 @@ export const AvailableVersionItem = memo(function AvailableVersionItem(props: {v
const linkOpener = LinkOpenerService.getInstance();
const [selected, setSelected] = useState(false);
const [hovered, setHovered] = useState(false);
const t = useTranslation();
const formatedDate = (() => { return dateFormat(+props.version.ReleaseDate*1000, "ddd. d mmm yyyy"); })()
@@ -38,8 +41,8 @@ export const AvailableVersionItem = memo(function AvailableVersionItem(props: {v
}, [])
return (
<li className="group relative w-72 h-60 transition-transform active:scale-[.98]" onClick={toggleSelect}>
<span className={`absolute glow-on-hover group-hover:opacity-100 ${selected && "!opacity-100"}`}/>
<motion.li className="group relative w-72 h-60 transition-transform active:scale-[.98]" onClick={toggleSelect} onHoverStart={() => setHovered(true)} onHoverEnd={() => setHovered(false)}>
<GlowEffect visible={hovered} className="absolute"/>
<div className={`relative flex flex-col overflow-hidden rounded-md w-72 h-60 cursor-pointer group-hover:shadow-none duration-300 bg-light-main-color-2 dark:bg-main-color-2 ${!selected && "shadow-lg shadow-gray-900"}`}>
<BsmImage image={props.version.ReleaseImg ? props.version.ReleaseImg : defaultImage} errorImage={defaultImage} placeholder={defaultImage} className="absolute top-0 right-0 w-full h-full opacity-40 blur-xl object-cover" loading="lazy"/>
<BsmImage image={props.version.ReleaseImg ? props.version.ReleaseImg : defaultImage} errorImage={defaultImage} placeholder={defaultImage} className="bg-black w-full h-3/4 object-cover" loading="lazy"/>
@@ -56,6 +59,6 @@ export const AvailableVersionItem = memo(function AvailableVersionItem(props: {v
)}
</div>
</div>
</li>
</motion.li>
)
})
@@ -1,7 +1,7 @@
import { BSVersion } from 'shared/bs-version.interface';
import { useEffect, useState } from "react"
import { BSVersionManagerService } from "renderer/services/bs-version-manager.service";
import { filter, take } from "rxjs/operators";
import { filter } from "rxjs/operators";
import { AvailableVersionItem } from "./available-version-item.component";
export function AvailableVersionsSlide(props: {year: string}) {
@@ -11,7 +11,7 @@ export function AvailableVersionsSlide(props: {year: string}) {
const versionsService = BSVersionManagerService.getInstance();
useEffect(() => {
versionsService.availableVersions$.pipe(filter(versions => !!versions?.length), take(1)).subscribe(() => {
versionsService.availableVersions$.pipe(filter(versions => !!versions?.length)).subscribe(() => {
setAvailableVersions(versionsService.getAvaibleVersionsOfYear(props.year));
});
}, [])
@@ -1,29 +1,29 @@
import { useEffect, useState } from "react"
import { BSVersionManagerService } from "../../services/bs-version-manager.service";
import { AvailableVersionsSlide } from "./available-versions-slide.component";
import { filter, take } from "rxjs/operators";
import { filter} from "rxjs/operators";
import { TabNavBar } from "../shared/tab-nav-bar.component";
export function AvailableVersionsSlider() {
const [availableYears, setAvailableYears] = useState([]);
const [yearIndex, setYearIndex] = useState(0);
const versionManagerService = BSVersionManagerService.getInstance();
const versionManagerService = BSVersionManagerService.getInstance();
const [availableYears, setAvailableYears] = useState([]);
const [yearIndex, setYearIndex] = useState(0);
const setSelectedYear = (index: number) => {
setYearIndex(index);
}
const setSelectedYear = (index: number) => {
setYearIndex(index);
}
useEffect(() => {
versionManagerService.availableVersions$.pipe(filter(versions => !!versions?.length), take(1)).subscribe(() => {
setAvailableYears(versionManagerService.getAvailableYears());
});
}, [])
useEffect(() => {
versionManagerService.availableVersions$.pipe(filter(versions => !!versions?.length)).subscribe(() => {
setAvailableYears(versionManagerService.getAvailableYears());
});
}, [])
return (
<div className="w-full h-fit max-h-full flex flex-col items-center grow min-h-0">
<TabNavBar className="mb-3" tabIndex={yearIndex} tabsText={availableYears} onTabChange={setSelectedYear}/>
<div className="w-full h-fit max-h-full flex flex-col items-center grow min-h-0 gap-3">
<TabNavBar tabIndex={yearIndex} tabsText={availableYears} onTabChange={setSelectedYear}/>
<ol className="w-full min-h-0 flex transition-transform duration-300" style={{transform: `translate(${-(yearIndex * 100)}%, 0)`}}>
{ availableYears.map(year =>
<AvailableVersionsSlide key={year} year={year}/>
@@ -14,6 +14,7 @@ import { MAP_DIFFICULTIES_COLORS } from "renderer/partials/maps/map-difficulties
import { BsmButton } from "../shared/bsm-button.component"
import equal from "fast-deep-equal/es6";
import clone from "rfdc";
import { GlowEffect } from "../shared/glow-effect.component"
export type Props = {
className?: string,
@@ -214,7 +215,7 @@ export function FilterPanel({className, ref, playlist = false, filter, onChange,
{onApply && (
<div className="inline float-right relative w-fit h-fit mt-2">
<BsmButton className="inline float-right rounded-md font-bold px-1 py-0.5 text-sm" text="Appliquer" typeColor="primary" withBar={false} onClick={handleApply}/>
{haveChanged && <div className="glow-on-hover !opacity-100"></div>}
<GlowEffect visible={haveChanged}/>
</div>
)}
</motion.div>
@@ -11,6 +11,7 @@ import { BsmImage } from "../shared/bsm-image.component"
import BeatConflict from "../../../../assets/images/apngs/beat-conflict.png"
import { BsmButton } from "../shared/bsm-button.component"
import { useTranslation } from "renderer/hooks/use-translation.hook"
import BeatWaitingImg from "../../../../assets/images/apngs/beat-waiting.png"
type Props = {
version: BSVersion,
@@ -26,7 +27,7 @@ export const LocalMapsListPanel = forwardRef(({version, className, filter, searc
const ref = useRef(null)
const isVisible = useInView(ref, {once: true});
const [maps, setMaps] = useState<BsmLocalMap[]>([]);
const [maps, setMaps] = useState<BsmLocalMap[]>(null);
const [subs] = useState<Subscription[]>([]);
const [selectedMaps, setSelectedMaps] = useState([]);
const t = useTranslation();
@@ -51,7 +52,7 @@ export const LocalMapsListPanel = forwardRef(({version, className, filter, searc
}
return () => {
setMaps(() => []);
setMaps(() => null);
subs.forEach(s => s.unsubscribe());
mapsDownloader.removeOnMapDownloadedListene(loadMaps);
}
@@ -248,11 +249,18 @@ export const LocalMapsListPanel = forwardRef(({version, className, filter, searc
<div ref={ref} className={className}>
<ul className="p-3 w-full grow flex flex-wrap justify-center content-start gap-2 overflow-y-scroll scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900">
{maps?.length ? renderMaps() : (
<div className="h-full flex flex-col items-center justify-center flex-wrap gap-1">
<BsmImage className="h-32" image={BeatConflict}/>
<span className="font-bold">{t("pages.version-viewer.maps.tabs.maps.empty-maps.text")}</span>
<BsmButton className="font-bold rounded-md p-2" text="pages.version-viewer.maps.tabs.maps.empty-maps.button" typeColor="primary" withBar={false} onClick={e => {e.preventDefault(); mapsDownloader.openDownloadMapModal(version)}}/>
</div>
maps === null ? (
<div className="h-full flex flex-col items-center justify-center flex-wrap gap-1">
<img className="w-32 h-32 spin-loading" src={BeatWaitingImg} alt=" "/>
<span className="font-bold">{t("modals.download-maps.loading-maps")}</span>
</div>
) : (
<div className="h-full flex flex-col items-center justify-center flex-wrap gap-1">
<BsmImage className="h-32" image={BeatConflict}/>
<span className="font-bold">{t("pages.version-viewer.maps.tabs.maps.empty-maps.text")}</span>
<BsmButton className="font-bold rounded-md p-2" text="pages.version-viewer.maps.tabs.maps.empty-maps.button" typeColor="primary" withBar={false} onClick={e => {e.preventDefault(); mapsDownloader.openDownloadMapModal(version)}}/>
</div>
)
)}
</ul>
</div>
@@ -20,6 +20,7 @@ import { useTranslation } from "renderer/hooks/use-translation.hook";
import { MAP_DIFFICULTIES } from "renderer/partials/maps/map-difficulties/map-difficulties";
import { MAP_DIFFICULTIES_COLORS } from "renderer/partials/maps/map-difficulties/map-difficulties-colors"
import useDoubleClick from "use-double-click";
import { GlowEffect } from "../shared/glow-effect.component";
export type ParsedMapDiff = {type: BsvMapDifficultyType, name: string, stars: number}
@@ -148,7 +149,7 @@ export const MapItem = memo(({hash, title, autor, songAutor, coverUrl, songUrl,
return (
<motion.li className="relative h-[100px] min-w-[370px] shrink-0 grow basis-0 text-white group cursor-pointer" onHoverStart={() => setHovered(true)} onHoverEnd={() => setHovered(false)} style={{zIndex: hovered && 5, transform: "translateZ(0) scale(1.0, 1.0)", backfaceVisibility: "hidden"}}>
{(hovered || selected) && onSelected && <motion.span className="glow-on-hover !transition-none" animate={{opacity: 1}} transition={{duration: .2, ease: "linear"}}/>}
<GlowEffect visible={hovered || selected && !!onSelected}/>
<AnimatePresence>
{(diffsPanelHovered || bottomBarHovered) && (
<motion.ul key={hash} className="absolute top-[calc(100%-10px)] w-full h-fit max-h-[200%] pt-4 pb-2 px-2 overflow-y-scroll bg-light-main-color-3 dark:bg-main-color-3 text-main-color-1 dark:text-current brightness-125 rounded-md flex flex-col gap-3 scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900 shadow-sm shadow-black" initial={{opacity: 0}} animate={{opacity: 1}} exit={{opacity: 0}} transition={{duration: .15}} onHoverStart={diffsPanelHoverStart} onHoverEnd={diffsPanelHoverEnd}>
@@ -0,0 +1,22 @@
import { BsmButton } from 'renderer/components/shared/bsm-button.component'
import { BsmImage } from 'renderer/components/shared/bsm-image.component'
import { useTranslation } from 'renderer/hooks/use-translation.hook'
import { ModalComponent, ModalExitCode } from 'renderer/services/modale.service'
import BeatImpatient from "../../../../../assets/images/apngs/beat-impatient.png"
export const ImportVersionModal: ModalComponent<void> = ({resolver}) => {
const t = useTranslation();
return (
<form>
<h1 className="text-3xl uppercase tracking-wide w-full text-center text-gray-800 dark:text-gray-200">{t("modals.bs-import-version.title")}</h1>
<BsmImage className="mx-auto h-20" image={BeatImpatient}/>
<p className="max-w-sm text-gray-800 dark:text-gray-200">{t("modals.bs-import-version.description")}</p>
<div className="grid grid-flow-col grid-cols-2 gap-4 mt-4">
<BsmButton typeColor="cancel" className="rounded-md text-center transition-all" onClick={() => resolver({exitCode: ModalExitCode.CANCELED})} withBar={false} text="misc.cancel"/>
<BsmButton typeColor="primary" className="rounded-md text-center transition-all" onClick={() => resolver({exitCode: ModalExitCode.COMPLETED})} withBar={false} text="modals.bs-import-version.buttons.submit"/>
</div>
</form>
)
}
@@ -20,7 +20,7 @@ export function NavBar() {
const color = useThemeColor("first-color");
const t = useTranslation();
const _ = useObservable(I18nService.getInstance().currentLanguage$)
useObservable(I18nService.getInstance().currentLanguage$)
return (
<nav id='nav-bar' className='z-10 flex flex-col h-full max-h-full items-center p-1 bg-light-main-color-1 dark:bg-main-color-1'>
@@ -1,11 +1,15 @@
import { BsmIcon } from "../svgs/bsm-icon.component"
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
import { getCorrectTextColor } from "renderer/helpers/correct-text-color"
import { GlowEffect } from "./glow-effect.component";
import { motion } from "framer-motion";
import { useState } from "react";
type Props = {className?:string, checked?: boolean, onChange?: (val: boolean) => void, disabled?: boolean}
export function BsmCheckbox({className, checked, onChange, disabled} : Props) {
const [hovered, setHovered] = useState(false);
const checkedColor = useThemeColor("first-color");
const iconColor = getCorrectTextColor(checkedColor);
@@ -15,12 +19,12 @@ export function BsmCheckbox({className, checked, onChange, disabled} : Props) {
}
return (
<div className={`${className} group-one`}>
{!disabled && <span className="glow-on-hover !w-[calc(100%+6px)] !h-[calc(100%+6px)] !-top-[3px] !-left-[3px] group-one-hover:opacity-100"/>}
<motion.div className={`${className} group-one`} onHoverStart={() => setHovered(true)} onHoverEnd={() => setHovered(false)}>
<GlowEffect visible={!disabled && hovered} className="!w-[calc(100%+6px)] !h-[calc(100%+6px)] !-top-[3px] !-left-[3px]"/>
<span className={`w-full h-full flex items-center justify-center rounded-md border-2 border-current overflow-hidden bg-light-main-color-3 dark:bg-main-color-1 ${disabled ? "opacity-30 cursor-not-allowed" : "cursor-pointer"}`} onClickCapture={e => {e.stopPropagation(); handleClick()}}>
<BsmIcon className="w-full h-full" icon="check" style={{color: iconColor, backgroundColor: checkedColor, visibility: checked ? "visible" : "hidden"}}/>
</span>
</div>
</motion.div>
)
}
@@ -0,0 +1,14 @@
import { AnimatePresence, motion } from "framer-motion"
type Props = {
visible?: boolean,
className?: string
}
export function GlowEffect({visible, className}: Props) {
return (
<AnimatePresence>
{visible && <motion.div transition={{duration: .1}} initial={{opacity: 0}} animate={{opacity: 1}} exit={{opacity: 0}} className={`${className} glow-on-hover`}/>}
</AnimatePresence>
)
}
@@ -47,10 +47,11 @@ import DiscordIcon from "./icons/discord-icon.component";
import { TwitterIcon } from "./icons/twitter-icon.component";
import { Mee6Icon } from "./icons/mee6-icon.component";
import { InfoIcon } from "./icons/info-icon.component";
import { SyncIcon } from "./icons/sync-icon.component";
export type BsmIconType = BsvMapCharacteristic | (
"settings"|"trash"|"favorite"|"folder"|"bsNote"|"check"|"three-dots"|"twitch"|"eye"|"play"|"checkCircleIcon"|"discord"|"info"|
"terminal"|"desktop"|"oculus"|"add"|"cross"|"task"|"github"|"close"|"thumbUpFill"|"timerFill"|"pause"|"twitter"|
"terminal"|"desktop"|"oculus"|"add"|"cross"|"task"|"github"|"close"|"thumbUpFill"|"timerFill"|"pause"|"twitter"|"sync"|
"copy"|"steam"|"edit"|"export"|"patreon"|"search"|"bsMapDifficulty"|"link"|"unlink"|"download"|"filter"|"mee6"|
"fr-FR-flag"|"es-ES-flag"|"en-US-flag"|"en-EN-flag"
);
@@ -105,6 +106,7 @@ export const BsmIcon = memo(({className, icon, style}: {className?: string, icon
if(icon === "twitter"){ return <TwitterIcon className={className} style={style}/> }
if(icon === "mee6"){ return <Mee6Icon className={className} style={style}/> }
if(icon === "info"){ return <InfoIcon className={className} style={style}/> }
if(icon === "sync"){ return <SyncIcon className={className} style={style}/> }
return <TrashIcon className={className} style={style}/>
}
@@ -0,0 +1,9 @@
import { CSSProperties } from "react";
export function SyncIcon(props: {className?: string, style?: CSSProperties}) {
return (
<svg className={props.className} style={props.style} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
<path fill="currentColor" d="M25.625 32.542q-.917.416-1.667-.084t-.75-1.625q0-.5.354-1 .355-.5.855-.75 2.666-1.25 4.187-3.75 1.521-2.5 1.521-5.458 0-1.833-.854-3.708-.854-1.875-2.313-3.375l-.791-.709v2.75q0 .667-.459 1.105-.458.437-1.083.437-.667 0-1.104-.437-.438-.438-.438-1.105V8.125q0-.792.542-1.312.542-.521 1.292-.521h6.75q.666 0 1.104.437.437.438.437 1.104 0 .667-.437 1.084-.438.416-1.104.416h-3l.375.417q2.416 2.292 3.604 4.938 1.187 2.645 1.187 5.187 0 4.208-2.229 7.583t-5.979 5.084ZM8.417 33.708q-.667 0-1.084-.437-.416-.438-.416-1.104 0-.667.416-1.084.417-.416 1.084-.416h2.958L11 30.333q-2.5-2.208-3.667-4.729-1.166-2.521-1.166-5.479 0-4.208 2.271-7.604 2.27-3.396 5.979-5.063.916-.416 1.645.105.73.52.73 1.604 0 .5-.334 1-.333.5-.833.75-2.625 1.25-4.187 3.75-1.563 2.5-1.563 5.458 0 2.25.854 4.042.854 1.791 2.396 3.208l.792.542v-2.75q0-.667.458-1.105.458-.437 1.125-.437.625 0 1.062.437.438.438.438 1.105v6.708q0 .792-.542 1.313-.541.52-1.291.52Z"/>
</svg>
)
}
@@ -1,4 +1,5 @@
import Tippy from "@tippyjs/react";
import { GlowEffect } from "renderer/components/shared/glow-effect.component";
import { BsmIcon, BsmIconType } from "renderer/components/svgs/bsm-icon.component"
import { useTranslation } from "renderer/hooks/use-translation.hook";
@@ -16,7 +17,7 @@ export function LaunchModToogle({onClick, active, text, icon, infoText}: props)
return (
<div className={`relative rounded-full cursor-pointer group active:scale-95 transition-transform ${!active && "shadow-md shadow-black"}`} onClick={() => onClick(!active)}>
<div className={`absolute glow-on-hover !rounded-full ${active && "!opacity-100 blur-[2px]"}`}/>
<GlowEffect visible={active} className="absolute !rounded-full blur-[2px]"/>
<div className='w-full h-full px-6 flex gap-1.5 justify-center items-center bg-light-main-color-2 dark:bg-main-color-2 p-3 rounded-full text-gray-800 dark:text-white group-hover:bg-light-main-color-1 dark:group-hover:bg-main-color-1'>
<BsmIcon icon={icon} className='h-7 text-gray-800 dark:text-white'/>
<span className='w-fit min-w-fit text-lg font-bold uppercase tracking-wide italic '>{t(text)}</span>
-2
View File
@@ -50,8 +50,6 @@
width: calc(100% + 8px);
height: calc(100% + 8px);
animation: glowing 20s linear infinite;
opacity: 0;
transition: opacity .2s ease-in-out;
}
@keyframes glowing {
@@ -7,11 +7,23 @@ import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { AnimatePresence, motion } from "framer-motion";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { BSVersionManagerService } from "renderer/services/bs-version-manager.service";
import { BsmDropdownButton } from "renderer/components/shared/bsm-dropdown-button.component";
import { ProgressBarService } from "renderer/services/progress-bar.service";
import { ModalExitCode, ModalService } from "renderer/services/modale.service";
import { ImportVersionModal } from "renderer/components/modal/modal-types/import-version-modal.component";
import { IpcService } from "renderer/services/ipc.service";
import { NotificationService } from "renderer/services/notification.service";
import { timer } from "rxjs";
export function AvailableVersionsList() {
const bsDownloaderService: BsDownloaderService = BsDownloaderService.getInstance();
const versionManagerService: BSVersionManagerService = BSVersionManagerService.getInstance();
const bsDownloaderService = BsDownloaderService.getInstance();
const versionManagerService = BSVersionManagerService.getInstance();
const progressBar = ProgressBarService.getInstance();
const modal = ModalService.getInsance();
const ipc = IpcService.getInstance();
const installer = BsDownloaderService.getInstance();
const notification = NotificationService.getInstance();
const [versionSelected, setVersionSelected] = useState(null as BSVersion);
const [downloading, setDownloading] = useState(false);
@@ -24,30 +36,70 @@ export function AvailableVersionsList() {
});
}
useEffect(() => {
const versionSelectedSub = bsDownloaderService.selectedBsVersion$.subscribe(version => {
setVersionSelected(version);
});
useEffect(() => {
const versionSelectedSub = bsDownloaderService.selectedBsVersion$.subscribe(version => {
setVersionSelected(version);
});
return () => {
versionSelectedSub.unsubscribe();
}
}, [])
const importVersion = async () => {
if(!progressBar.require()){ return; }
const modalRes = await modal.openModal(ImportVersionModal);
if(modalRes.exitCode !== ModalExitCode.COMPLETED){ return; }
const folderRes = await ipc.send<{canceled: boolean, filePaths: string[]}>("choose-folder");
if(!folderRes.success || folderRes.data.canceled || !folderRes.data.filePaths?.length){ return; }
notification.notifySuccess({title: "notifications.bs-import-version.success.start-import.title", desc: "notifications.bs-import-version.success.start-import.desc", duration: 6_000});
progressBar.showFake(.008);
const toImport = folderRes.data.filePaths.at(0);
const imported = await installer.importVersion(toImport);
if(imported){
versionManagerService.askInstalledVersions();
notification.notifySuccess({title: "notifications.bs-import-version.success.imported.title", duration: 3_000});
progressBar.complete();
await timer(400).toPromise();
}
else{
notification.notifyError({title: "notifications.types.error", desc: "notifications.bs-import-version.errors.import-error.desc"})
}
progressBar.hide(true);
return () => {
versionSelectedSub.unsubscribe();
}
}, [])
return (
<div className="relative h-full w-full flex items-center flex-col pt-2">
<Slideshow className="absolute w-full h-full top-0"/>
<h1 className="text-gray-100 text-2xl mb-4 z-[1]">{t("pages.available-versions.title")}</h1>
<AvailableVersionsSlider/>
<Slideshow className="absolute w-full h-full top-0"/>
<h1 className="text-gray-100 text-2xl mb-4 z-[1]">{t("pages.available-versions.title")}</h1>
<AnimatePresence>
{ versionSelected && !downloading && (
<motion.div initial={{y:"150%"}} animate={{y:"0%"}} exit={{y:"150%"}} className="absolute bottom-5" onClick={startDownload}>
<BsmButton text={versionManagerService.isVersionInstalled(versionSelected) ? "misc.verify" : "misc.download"} className="relative text-gray-800 dark:text-gray-100 rounded-md text-3xl font-bold italic tracking-wide px-3 pb-2 pt-1 shadow-md shadow-black"/>
</motion.div>
)}
</AnimatePresence>
<AvailableVersionsSlider/>
<AnimatePresence>
{ versionSelected && !downloading && (
<motion.div initial={{y:"150%"}} animate={{y:"0%"}} exit={{y:"150%"}} className="absolute bottom-5" onClick={startDownload}>
<BsmButton text={versionManagerService.isVersionInstalled(versionSelected) ? "misc.verify" : "misc.download"} className="relative text-gray-800 dark:text-gray-100 rounded-md text-3xl font-bold italic tracking-wide px-3 pb-2 pt-1 shadow-md shadow-black"/>
</motion.div>
)}
</AnimatePresence>
<BsmDropdownButton className="absolute top-5 right-5 h-9 w-9 bg-light-main-color-2 dark:bg-main-color-2 rounded-md" icon="settings" items={[
{icon: "sync", text: "pages.available-versions.dropdown.refresh", onClick: () => versionManagerService.askAvailableVersions()},
{icon: "download", text: "pages.available-versions.dropdown.import-version", onClick: importVersion}
]}/>
</div>
)
}
@@ -156,4 +156,9 @@ export class BsDownloaderService{
return this.ipcService.send<string>("bs-download.set-installation-folder", {args: path});
}
public async importVersion(pathToImport: string): Promise<boolean>{
const res = await this.ipcService.send<void>("bs-download.import-version", {args: pathToImport});
return res.success;
}
}
+6 -3
View File
@@ -1,5 +1,9 @@
export interface BSVersion {
BSVersion: string,
export interface PartialBSVersion {
BSVersion: string,
name?: string
}
export interface BSVersion extends PartialBSVersion {
BSManifest?: string,
ReleaseURL?: string,
ReleaseImg?: string,
@@ -7,6 +11,5 @@ export interface BSVersion {
year?: string,
steam?: boolean,
oculus?: boolean,
name?: string,
color?: string
}