diff --git a/package-lock.json b/package-lock.json index 99017021..42f9c0b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10771,6 +10771,14 @@ } } }, + "react-visibility-sensor": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/react-visibility-sensor/-/react-visibility-sensor-5.1.1.tgz", + "integrity": "sha512-cTUHqIK+zDYpeK19rzW6zF9YfT4486TIgizZW53wEZ+/GPBbK7cNS0EHyJVyHYacwFEvvHLEKfgJndbemWhB/w==", + "requires": { + "prop-types": "^15.7.2" + } + }, "read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", diff --git a/package.json b/package.json index 89b7865b..307595a3 100644 --- a/package.json +++ b/package.json @@ -238,6 +238,7 @@ "react-fitty": "^1.0.1", "react-outside-click-handler": "^1.3.0", "react-router-dom": "^6.3.0", + "react-visibility-sensor": "^5.1.1", "regedit": "^5.1.1", "rxjs": "^7.5.6", "tailwind-scrollbar-hide": "^1.1.7", diff --git a/src/main/ipcs/bs-mods-ipcs.ts b/src/main/ipcs/bs-mods-ipcs.ts new file mode 100644 index 00000000..b93bd3d8 --- /dev/null +++ b/src/main/ipcs/bs-mods-ipcs.ts @@ -0,0 +1,14 @@ +import { ipcMain } from "electron"; +import { BsModsManagerService } from "../services/mods/bs-mods-manager.service"; +import { UtilsService } from "../services/utils.service"; +import { BSVersion } from "shared/bs-version.interface"; +import { IpcRequest } from "shared/models/ipc"; + +ipcMain.on("get-available-mods", (event, request: IpcRequest) => { + const utils = UtilsService.getInstance(); + const modsManager = BsModsManagerService.getInstance(); + + modsManager.getAvailableMods(request.args).then(mods => { + utils.ipcSend(request.responceChannel, {success: true, data: mods}); + }).catch(() => utils.ipcSend(request.responceChannel, {success: false})); +}); \ No newline at end of file diff --git a/src/main/ipcs/index.ts b/src/main/ipcs/index.ts index 7d9b243f..397a23be 100644 --- a/src/main/ipcs/index.ts +++ b/src/main/ipcs/index.ts @@ -7,3 +7,4 @@ import './map-ipcs'; import './supporters-ipcs'; import './launcher-ipcs'; import './window-manager-ipcs'; +import './bs-mods-ipcs'; diff --git a/src/main/services/beat-mods-api.service.ts b/src/main/services/mods/beat-mods-api.service.ts similarity index 98% rename from src/main/services/beat-mods-api.service.ts rename to src/main/services/mods/beat-mods-api.service.ts index b472cbe0..c63bb1ad 100644 --- a/src/main/services/beat-mods-api.service.ts +++ b/src/main/services/mods/beat-mods-api.service.ts @@ -1,6 +1,6 @@ import { BSVersion } from "shared/bs-version.interface"; import { Mod } from "shared/models/mods/mod.interface"; -import { RequestService } from "./request.service"; +import { RequestService } from "../request.service"; export class BeatModsApiService { diff --git a/src/main/services/mods/bs-mods-manager.service.ts b/src/main/services/mods/bs-mods-manager.service.ts new file mode 100644 index 00000000..bf86a39f --- /dev/null +++ b/src/main/services/mods/bs-mods-manager.service.ts @@ -0,0 +1,40 @@ +import { BSVersion } from "shared/bs-version.interface"; +import { Mod } from "shared/models/mods/mod.interface";import { BeatModsApiService } from "./beat-mods-api.service"; +; + +export class BsModsManagerService { + + private static instance: BsModsManagerService; + + private beatModsApi: BeatModsApiService; + + public static getInstance(): BsModsManagerService{ + if(!BsModsManagerService.instance){ BsModsManagerService.instance = new BsModsManagerService(); } + return BsModsManagerService.instance; + } + + private constructor(){ + this.beatModsApi = BeatModsApiService.getInstance(); + } + + public getAvailableMods(version: BSVersion): Promise{ + return this.beatModsApi.getVersionMods(version); + } + + public getInstalledMods(version: BSVersion): Promise{ + throw "NOT IMPLEMENTED YET"; + } + + public installMods(mods: Mod[], version: BSVersion){ + throw "NOT IMPLEMENTED YET"; + } + + public uninstallMods(mods: Mod[], version: BSVersion){ + throw "NOT IMPLEMENTED YET"; + } + + public isModsAvailable(version: BSVersion): Promise{ + throw "NOT IMPLEMENTED YET"; + } + +} \ No newline at end of file diff --git a/src/renderer/components/shared/bsm-button.component.tsx b/src/renderer/components/shared/bsm-button.component.tsx index 4589d684..24327605 100644 --- a/src/renderer/components/shared/bsm-button.component.tsx +++ b/src/renderer/components/shared/bsm-button.component.tsx @@ -44,7 +44,7 @@ export function BsmButton({className, style, imgClassName, iconClassName, icon, })(); const renderTypeColor = (() => { - if(typeColor === "primary"){ return null; } + if(typeColor === "primary"){ return ""; } if(!typeColor){ return `bg-light-main-color-2 dark:bg-main-color-2 ${!withBar && "hover:bg-light-main-color-3 dark:hover:bg-main-color-3"}`; } if(typeColor === "cancel"){ return "bg-gray-500"; } if(typeColor === "error"){ return "bg-red-500"; } @@ -54,7 +54,7 @@ export function BsmButton({className, style, imgClassName, iconClassName, icon, return ( onClickOutside && onClickOutside(e)}> -
onClick && onClick(e)} className={`${className} overflow-hidden cursor-pointer group ${!withBar && (!!typeColor || !!color) && "hover:brightness-[1.15]"} ${disabled && "brightness-75 cursor-not-allowed"} ${renderTypeColor}`} style={{...style, ...((!!primaryColor || !!color) && {backgroundColor: primaryColor ?? color})}}> +
onClick && onClick(e)} className={`${className} overflow-hidden cursor-pointer group ${(!withBar && (!!typeColor || !!color)) && "hover:brightness-[1.15]"} ${disabled && "brightness-75 cursor-not-allowed"} ${renderTypeColor}`} style={{...style, ...((!!primaryColor || !!color) && {backgroundColor: primaryColor ?? color})}}> { image && } { icon && } {text && (type === "submit" ? : {t(text)})} diff --git a/src/renderer/components/svgs/bsm-icon.component.tsx b/src/renderer/components/svgs/bsm-icon.component.tsx index ced7ac0e..d7c9530f 100644 --- a/src/renderer/components/svgs/bsm-icon.component.tsx +++ b/src/renderer/components/svgs/bsm-icon.component.tsx @@ -19,11 +19,12 @@ import { CSSProperties, memo } from "react"; import EditIcon from "./icons/edit-icon.component"; import { ExportIcon } from "./icons/export-icon.component"; import PatreonIcon from "./icons/patreon-icon.component"; +import { SearchIcon } from "./icons/search-icon.component"; export type BsmIconType = ( "settings"|"trash"|"favorite"|"folder"|"bsNote"| "terminal"|"desktop"|"oculus"|"add"|"cross"|"task"| - "copy"|"steam"|"edit"|"export"|"patreon"| + "copy"|"steam"|"edit"|"export"|"patreon"|"search"| "fr-FR-flag"|"es-ES-flag"|"en-US-flag"|"en-EN-flag" ); @@ -50,6 +51,7 @@ export const BsmIcon = memo(({className, icon, style}: {className?: string, icon if(icon === "edit"){ return } if(icon === "export"){ return } if(icon === "patreon"){ return } + if(icon === "search"){ return } return } diff --git a/src/renderer/components/svgs/icons/search-icon.component.tsx b/src/renderer/components/svgs/icons/search-icon.component.tsx new file mode 100644 index 00000000..b619113f --- /dev/null +++ b/src/renderer/components/svgs/icons/search-icon.component.tsx @@ -0,0 +1,9 @@ +import { CSSProperties } from "react"; + +export function SearchIcon(props: {className?: string, style?: CSSProperties}) { + return ( + + + + ) +} diff --git a/src/renderer/components/version-viewer/slides/launch/launch-mod-toogle.component.tsx b/src/renderer/components/version-viewer/slides/launch/launch-mod-toogle.component.tsx new file mode 100644 index 00000000..ac21a1e8 --- /dev/null +++ b/src/renderer/components/version-viewer/slides/launch/launch-mod-toogle.component.tsx @@ -0,0 +1,19 @@ +import { BsmIcon, BsmIconType } from "renderer/components/svgs/bsm-icon.component" +import { useTranslation } from "renderer/hooks/use-translation.hook"; + +type props = {onClick: (active: boolean) => void, active: boolean, text: string, icon: BsmIconType} + +export function LaunchModToogle({onClick, active, text, icon}: props) { + + const t = useTranslation(); + + return ( +
onClick(!active)}> +
+
+ + {t(text)} +
+
+ ) +} diff --git a/src/renderer/components/version-viewer/slides/launch/launch-slide.component.tsx b/src/renderer/components/version-viewer/slides/launch/launch-slide.component.tsx new file mode 100644 index 00000000..137de37f --- /dev/null +++ b/src/renderer/components/version-viewer/slides/launch/launch-slide.component.tsx @@ -0,0 +1,51 @@ +import { useState } from "react"; +import { BsmButton } from "renderer/components/shared/bsm-button.component"; +import { useObservable } from "renderer/hooks/use-observable.hook"; +import { BSLauncherService, LaunchMods } from "renderer/services/bs-launcher.service"; +import { ConfigurationService } from "renderer/services/configuration.service"; +import { BSVersion } from "shared/bs-version.interface" +import { LaunchModToogle } from "./launch-mod-toogle.component"; + +type props = {version: BSVersion}; + +export function LaunchSlide({version}: props) { + + const configService = ConfigurationService.getInstance(); + const bsLauncherService = BSLauncherService.getInstance(); + + const [oculusMode, setOculusMode] = useState(!!configService.get(LaunchMods.OCULUS_MOD)); + const [desktopMode, setDesktopMode] = useState(!!configService.get(LaunchMods.DESKTOP_MOD)); + const [debugMode, setDebugMode] = useState(!!configService.get(LaunchMods.DEBUG_MOD)); + + const launchState = useObservable(bsLauncherService.launchState$); + + const setMode = (mode: LaunchMods, value: boolean) => { + if(mode === LaunchMods.DEBUG_MOD){ setDebugMode(value); } + else if(mode === LaunchMods.OCULUS_MOD){ + setOculusMode(value); + setDesktopMode(false); + configService.set(LaunchMods.DESKTOP_MOD, false); + } + else if(mode === LaunchMods.DESKTOP_MOD){ + setDesktopMode(value); + setOculusMode(false); + configService.set(LaunchMods.OCULUS_MOD, false); + } + configService.set(mode, value); + } + + const launch = () => bsLauncherService.launch(version, oculusMode, desktopMode, debugMode) + + return ( +
+
+ setMode(LaunchMods.OCULUS_MOD, !oculusMode)} active={oculusMode} text="pages.version-viewer.launch-mods.oculus"/> + setMode(LaunchMods.DESKTOP_MOD, !desktopMode)} active={desktopMode} text="pages.version-viewer.launch-mods.desktop"/> + setMode(LaunchMods.DEBUG_MOD, !debugMode)} active={debugMode} text="pages.version-viewer.launch-mods.debug"/> +
+
+ +
+
+ ) +} diff --git a/src/renderer/components/version-viewer/slides/mods/mod-item.component.tsx b/src/renderer/components/version-viewer/slides/mods/mod-item.component.tsx new file mode 100644 index 00000000..39cfbdc9 --- /dev/null +++ b/src/renderer/components/version-viewer/slides/mods/mod-item.component.tsx @@ -0,0 +1,17 @@ +import { Mod } from "shared/models/mods/mod.interface" + +type Props = {mod: Mod, installed: Mod} + +export function ModItem({mod, installed}: Props) { + return ( + <> +
+ +
+ {mod.name} + {installed?.version || "-"} + {mod.version} + {mod.description} + + ) +} diff --git a/src/renderer/components/version-viewer/slides/mods/mods-grid.component.tsx b/src/renderer/components/version-viewer/slides/mods/mods-grid.component.tsx new file mode 100644 index 00000000..d872d336 --- /dev/null +++ b/src/renderer/components/version-viewer/slides/mods/mods-grid.component.tsx @@ -0,0 +1,58 @@ +import { motion } from "framer-motion"; +import { useRef, useState } from "react"; +import { BsmButton } from "renderer/components/shared/bsm-button.component"; +import { Mod } from "shared/models/mods/mod.interface" +import { ModItem } from "./mod-item.component" + +type Props = {modsMap: Map, installed: Map} + +export function ModsGrid({modsMap, installed}: Props) { + + const [filter, setFilter] = useState(""); + const [filterEnabled, setFilterEnabled] = useState(false); + + const inputRef = useRef(); + + const installedMod = (key: string, mod: Mod): Mod => { + if(!installed || !installed.get(key)){ return; } + return installed.get(key).find(m => m.name === mod.name); + } + + const handleInput = (val: string) => setFilter(val.toLowerCase()); + const handleToogleFilter = () => { + setFilter(""); + setFilterEnabled(b => !b); + } + + return modsMap && ( + <> +
+ +
+ +
+
+
+ {(filterEnabled ? ( + handleInput(e.target.value)}/> + ):( + Nom + ))} +
+ + Installé + Dernière + Description + + { + Array.from(modsMap.keys()).map(key => ( + <> + {key} + {modsMap.get(key).map(mod => mod.name.toLowerCase().includes(filter) && )} + + )) + } +
+ + ) +} diff --git a/src/renderer/components/version-viewer/slides/mods/mods-slide.component.tsx b/src/renderer/components/version-viewer/slides/mods/mods-slide.component.tsx new file mode 100644 index 00000000..efe51e54 --- /dev/null +++ b/src/renderer/components/version-viewer/slides/mods/mods-slide.component.tsx @@ -0,0 +1,50 @@ +import { useEffect, useState } from "react"; +import { BsModsManagerService } from "renderer/services/bs-mods-manager.service"; +import { BSVersion } from "shared/bs-version.interface"; +import VisibilitySensor from 'react-visibility-sensor'; +import { Mod } from "shared/models/mods/mod.interface"; +import { ModItem } from "./mod-item.component"; +import { ModsGrid } from "./mods-grid.component"; + +export function ModsSlide({version}: {version: BSVersion}) { + + const modsManager = BsModsManagerService.getInstance(); + + const [isVisible, setIsVisible] = useState(false); + const [modsAvailable, setModsAvailable] = useState(null as Map); + const [modsInstalled, setModsInstalled] = useState(null as Map); + + const modsToCategoryMap = (mods: Mod[]): Map => { + if(!mods){ return new Map(); } + const map = new Map(); + mods.forEach(mod => map.set(mod.category, [...map.get(mod.category) ?? [], mod])); + return map; + } + + useEffect(() => { + if(!isVisible){ return; } + + if(!modsAvailable){ + console.log("OUI"); + modsManager.getAvailableMods(version).then(mods => setModsAvailable(modsToCategoryMap(mods))); + modsManager.getInstalledMods(version).then(mods => setModsInstalled(modsToCategoryMap(mods))); + } + }, [isVisible]); + + + return ( + +
+
+
+ +
+
+ +
+
+
+
+ + ) +} diff --git a/src/renderer/pages/version-viewer.component.tsx b/src/renderer/pages/version-viewer.component.tsx index 216ef94b..a1305cd0 100644 --- a/src/renderer/pages/version-viewer.component.tsx +++ b/src/renderer/pages/version-viewer.component.tsx @@ -1,100 +1,56 @@ import { BSVersion } from 'shared/bs-version.interface'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import BSLogo from '../../../assets/images/apngs/bs-logo.png'; -import { BSLauncherService, LaunchMods } from '../services/bs-launcher.service'; import { TabNavBar } from 'renderer/components/shared/tab-nav-bar.component'; import wipGif from "../../../assets/images/gifs/wip.gif" -import { ConfigurationService } from 'renderer/services/configuration.service'; import { BsmDropdownButton } from 'renderer/components/shared/bsm-dropdown-button.component'; -import { BsmIcon, BsmIconType } from 'renderer/components/svgs/bsm-icon.component'; import { BsmImage } from 'renderer/components/shared/bsm-image.component'; -import { BsmButton } from 'renderer/components/shared/bsm-button.component'; import { BSUninstallerService } from '../services/bs-uninstaller.service'; import { BSVersionManagerService } from '../services/bs-version-manager.service'; import { ModalExitCode, ModalService, ModalType } from '../services/modale.service'; import DefautVersionImage from "../../../assets/images/default-version-img.jpg"; import { BsDownloaderService } from 'renderer/services/bs-downloader.service'; -import { useTranslation } from 'renderer/hooks/use-translation.hook'; import { IpcService } from 'renderer/services/ipc.service'; -import { useObservable } from 'renderer/hooks/use-observable.hook'; import { MapService } from 'renderer/services/maps.service'; +import { LaunchSlide } from 'renderer/components/version-viewer/slides/launch/launch-slide.component'; +import { ModsSlide } from 'renderer/components/version-viewer/slides/mods/mods-slide.component'; export function VersionViewer() { - const configService = ConfigurationService.getInstance(); - const bsUninstallerService = BSUninstallerService.getInstance(); - const bsVersionManagerService = BSVersionManagerService.getInstance(); - const modalService = ModalService.getInsance(); - const bsDownloaderService = BsDownloaderService.getInstance(); - const ipcService = IpcService.getInstance(); - const mapService = MapService.getInstance(); + const bsUninstallerService = BSUninstallerService.getInstance(); + const bsVersionManagerService = BSVersionManagerService.getInstance(); + const modalService = ModalService.getInsance(); + const bsDownloaderService = BsDownloaderService.getInstance(); + const ipcService = IpcService.getInstance(); + const mapService = MapService.getInstance(); - const {state} = useLocation() as {state: BSVersion}; - const navigate = useNavigate(); - const bsLauncherService = BSLauncherService.getInstance(); + const {state} = useLocation() as {state: BSVersion}; + const navigate = useNavigate(); + const [currentTabIndex, setCurrentTabIndex] = useState(0); - const [oculusMode, setOculusMode] = useState(false); - const [desktopMode, setDesktopMode] = useState(false); - const [debugMode, setDebugMode] = useState(false); - const [currentTabIndex, setCurrentTabIndex] = useState(0); - const launchState = useObservable(bsLauncherService.launchState$); + const navigateToVersion = (version: BSVersion) => navigate(`/bs-version/${version.BSVersion}`, {state: version}); + const openFolder = () => ipcService.sendLazy("bs-version.open-folder", {args: state}); + const exportMaps = () => mapService.exportVersionMaps(state); + const verifyFiles = () => bsDownloaderService.download(state, true); - useEffect(() => { - setOculusMode(!!configService.get(LaunchMods.OCULUS_MOD)); - setDesktopMode(!!configService.get(LaunchMods.DESKTOP_MOD)); - setDebugMode(!!configService.get(LaunchMods.DEBUG_MOD)); - }, []); - - const navigateToVersion = (version: BSVersion) => { - navigate(`/bs-version/${version.BSVersion}`, {state: version}); - } - - const setMode = (mode: LaunchMods, value: boolean) => { - if(mode === LaunchMods.DEBUG_MOD){ setDebugMode(value); } - else if(mode === LaunchMods.OCULUS_MOD){ - setOculusMode(value); - setDesktopMode(false); - configService.set(LaunchMods.DESKTOP_MOD, false); + const uninstall = async () => { + const modalCompleted = await modalService.openModal(ModalType.UNINSTALL, state) + if(modalCompleted.exitCode === ModalExitCode.COMPLETED){ + bsUninstallerService.uninstall(state).then(() => { + bsVersionManagerService.askInstalledVersions(); + const newVersionPage = bsVersionManagerService.getInstalledVersions()[0]; + navigateToVersion(newVersionPage); + }) + } } - else if(mode === LaunchMods.DESKTOP_MOD){ - setDesktopMode(value); - setOculusMode(false); - configService.set(LaunchMods.OCULUS_MOD, false); + + const edit = () => { + bsVersionManagerService.editVersion(state).then(newVersion => { + if(!newVersion){ return; } + navigateToVersion(newVersion); + }); } - configService.set(mode, value); - } - - const openFolder = () => { - ipcService.sendLazy("bs-version.open-folder", {args: state}); - } - - const uninstall = async () => { - const modalCompleted = await modalService.openModal(ModalType.UNINSTALL, state) - if(modalCompleted.exitCode === ModalExitCode.COMPLETED){ - bsUninstallerService.uninstall(state) - .then(() => { - bsVersionManagerService.askInstalledVersions(); - const newVersionPage = bsVersionManagerService.getInstalledVersions()[0]; - navigateToVersion(newVersionPage); - }) - } - } - - const verifyFiles = () => { - bsDownloaderService.download(state, true); - } - - const launchBs = () => { - bsLauncherService.launch(state, oculusMode, desktopMode, debugMode); - } - - const edit = () => { - bsVersionManagerService.editVersion(state).then(newVersion => { - if(!newVersion){ return; } - navigateToVersion(newVersion); - }); - } const clone = () => { bsVersionManagerService.cloneVersion(state).then(newVersion => { @@ -103,9 +59,6 @@ export function VersionViewer() { }); } - const exportMaps = () => { - mapService.exportVersionMaps(state); - } return ( <> @@ -114,29 +67,15 @@ export function VersionViewer() {

{state.name ? `${state.BSVersion} - ${state.name}` : state.BSVersion}

setCurrentTabIndex(i)}/> -
-
-
- setMode(LaunchMods.OCULUS_MOD, !oculusMode)} active={oculusMode} text="pages.version-viewer.launch-mods.oculus"/> - setMode(LaunchMods.DESKTOP_MOD, !desktopMode)} active={desktopMode} text="pages.version-viewer.launch-mods.desktop"/> - setMode(LaunchMods.DEBUG_MOD, !debugMode)} active={debugMode} text="pages.version-viewer.launch-mods.debug"/> -
-
- -
-
-
-
- - Work In Progress -
-
+
+
Work In Progress
+
) -} - -function ToogleLunchMod(props: {onClick: () => void, active: boolean, text: string, icon: BsmIconType}) { - - const t = useTranslation(); - - return ( -
props.onClick()}> -
-
- - {t(props.text)} -
-
- ); -} +} \ No newline at end of file diff --git a/src/renderer/services/bs-mods-manager.service.ts b/src/renderer/services/bs-mods-manager.service.ts new file mode 100644 index 00000000..e12cb54a --- /dev/null +++ b/src/renderer/services/bs-mods-manager.service.ts @@ -0,0 +1,40 @@ +import { BSVersion } from "shared/bs-version.interface"; +import { Mod } from "shared/models/mods/mod.interface"; +import { IpcService } from "./ipc.service"; + +export class BsModsManagerService { + + private static instance: BsModsManagerService; + + private readonly ipcService: IpcService; + + public static getInstance(): BsModsManagerService{ + if(!BsModsManagerService.instance){ BsModsManagerService.instance = new BsModsManagerService(); } + return BsModsManagerService.instance; + } + + private constructor(){ + this.ipcService = IpcService.getInstance(); + } + + public getAvailableMods(version: BSVersion): Promise{ + return this.ipcService.send("get-available-mods", {args: version}).then(res => res.data); + } + + public async getInstalledMods(version: BSVersion): Promise{ + return []; + } + + public installMods(mods: Mod[], version: BSVersion){ + throw "NOT IMPLEMENTED YET"; + } + + public uninstallMods(mods: Mod[], version: BSVersion){ + throw "NOT IMPLEMENTED YET"; + } + + public isModsAvailable(version: BSVersion): Promise{ + throw "NOT IMPLEMENTED YET"; + } + +} \ No newline at end of file