import { ReactNode, useEffect, useLayoutEffect, useRef, useState } from "react"; import { BsModsManagerService } from "renderer/services/bs-mods-manager.service"; import { BSVersion } from "shared/bs-version.interface"; import { Mod } from "shared/models/mods/mod.interface"; import { ModsGrid } from "./mods-grid.component"; import { ConfigurationService } from "renderer/services/configuration.service"; import { DefaultConfigKey } from "renderer/config/default-configuration.config"; import { BsmButton } from "renderer/components/shared/bsm-button.component"; import BeatWaitingImg from "../../../../../../assets/images/apngs/beat-waiting.png"; import BeatConflictImg from "../../../../../../assets/images/apngs/beat-conflict.png"; import { useObservable } from "renderer/hooks/use-observable.hook"; import { lastValueFrom } from "rxjs"; import { useTranslation } from "renderer/hooks/use-translation.hook"; import { LinkOpenerService } from "renderer/services/link-opener.service"; import { useInView } from "framer-motion"; import { ModalExitCode, ModalService } from "renderer/services/modale.service"; import { ModsDisclaimerModal } from "renderer/components/modal/modal-types/mods-disclaimer-modal.component"; import { OsDiagnosticService } from "renderer/services/os-diagnostic.service"; import { lt } from "semver"; import { useService } from "renderer/hooks/use-service.hook"; import { NotificationService } from "renderer/services/notification.service"; import { noop } from "shared/helpers/function.helpers"; import { UninstallAllModsModal } from "renderer/components/modal/modal-types/uninstall-all-mods-modal.component"; export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion; onDisclamerDecline: () => void }) { const ACCEPTED_DISCLAIMER_KEY = "accepted-mods-disclaimer"; const modsManager = useService(BsModsManagerService); const configService = useService(ConfigurationService); const notification = useService(NotificationService); const linkOpener = useService(LinkOpenerService); const modals = useService(ModalService); const os = useService(OsDiagnosticService); const ref = useRef(null); const isVisible = useInView(ref, { amount: 0.5 }); const [modsAvailable, setModsAvailable] = useState(null as Map); const [modsInstalled, setModsInstalled] = useState(null as Map); const [modsSelected, setModsSelected] = useState([] as Mod[]); const [moreInfoMod, setMoreInfoMod] = useState(null as Mod); const [reinstallAllMods, setReinstallAllMods] = useState(false); const isOnline = useObservable(() => os.isOnline$); const [installing, setInstalling] = useState(false); const [uninstalling, setUninstalling] = useState(false); const downloadRef = useRef(null); const [downloadWith, setDownloadWidth] = useState(0); 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; }; const handleModChange = (selected: boolean, mod: Mod) => { if (selected) { return setModsSelected(mods => { if (mods.some(m => m.name === mod.name)) { return mods; } return [...mods, mod]; }); } setModsSelected(mods => mods.filter(m => m.name !== mod.name)); }; const handleMoreInfo = (mod: Mod) => { if (mod.name === moreInfoMod?.name) { return setMoreInfoMod(null); } setMoreInfoMod(mod); }; const handleOpenMoreInfo = () => { if (!moreInfoMod?.link) { return; } linkOpener.open(moreInfoMod.link); }; const installMods = (reinstallAll: boolean): void => { setReinstallAllMods(() => false); if (installing) { return; } const modsToInstall = modsSelected.filter(mod => { const installedMod = modsInstalled.get(mod.category)?.find(installedMod => installedMod.name === mod.name); if(reinstallAll || !installedMod){ return true; } return lt(installedMod.version, mod.version); }); if (!modsToInstall.length) { notification.notifyInfo({ title: "pages.version-viewer.mods.notifications.all-mods-already-installed.title", desc: "pages.version-viewer.mods.notifications.all-mods-already-installed.description" }); loadMods(); return; } setInstalling(() => true) lastValueFrom(modsManager.installMods(modsToInstall, version)).then(() => { loadMods(); }).catch(noop).finally(() => setInstalling(() => false)); }; const uninstallMod = (mod: Mod): void => { setUninstalling(() => true); lastValueFrom(modsManager.uninstallMod(mod, version)).catch(noop).finally(() => { loadMods(); setUninstalling(() => false); }); }; const uninstallAllMods = async () => { const res = await modals.openModal(UninstallAllModsModal, {data: version}); if (res.exitCode !== ModalExitCode.COMPLETED) { return; } setUninstalling(() => true); lastValueFrom(modsManager.uninstallAllMods(version)).catch(noop).finally(() => { loadMods(); setUninstalling(() => false); }) }; const loadMods = (): Promise => { if (os.isOffline) { return Promise.resolve(); } const promise = async () => { const available = await lastValueFrom(modsManager.getAvailableMods(version)); const installed = await lastValueFrom(modsManager.getInstalledMods(version)); return [available, installed]; } return promise().then(([available, installed]) => { const defaultMods = installed?.length ? [] : configService.get("default_mods" as DefaultConfigKey); setModsAvailable(() => modsToCategoryMap(available)); setModsSelected(() => available.filter(m => m.required || defaultMods.some(d => m.name?.toLowerCase() === d?.toLowerCase()) || installed.some(i => m.name === i.name))); setModsInstalled(() => modsToCategoryMap(installed)); }); }; useEffect(() => { if(!isVisible || !isOnline){ return noop(); } (async () => { if (configService.get(ACCEPTED_DISCLAIMER_KEY)) { return true; } const res = await modals.openModal(ModsDisclaimerModal); const haveAccepted = res.exitCode === ModalExitCode.COMPLETED; if (haveAccepted) { configService.set(ACCEPTED_DISCLAIMER_KEY, true); } return haveAccepted; })().then(canLoad => { if (!canLoad) { return onDisclamerDecline?.(); } loadMods(); }); return () => { setMoreInfoMod(null); setModsAvailable(null); setModsInstalled(null); }; }, [isVisible, isOnline, version]); useLayoutEffect(() => { if (modsAvailable) { setDownloadWidth(downloadRef?.current?.offsetWidth); } }, [modsAvailable]); const renderContent = () => { if (!isOnline) { return ; } if (!modsAvailable) { return ; } if (!modsAvailable.size) { return ( ({version.BSVersion}) ); } return ( <>
installMods(false)} style={{ top: reinstallAllMods ? "-100%" : "0" }} /> installMods(true)} style={{ top: reinstallAllMods ? "-100%" : "0" }}/>
setReinstallAllMods(prev => !prev)}/>
); }; return (
{renderContent()}
); } function ModStatus({ text, image, spin = false, children }: { text: string; image: string; spin?: boolean, children?: ReactNode}) { const t = useTranslation(); return (
 {t(text)} {children}
); }