[feature] Outdated mod notification

This commit is contained in:
MathieuG-P
2025-01-20 22:07:47 +01:00
committed by Zagrios
parent 0b4331bef2
commit 950ca71a0a
15 changed files with 239 additions and 92 deletions
@@ -1,7 +1,7 @@
import { ReactNode, useEffect, useLayoutEffect, useRef, useState } from "react";
import { forwardRef, ReactNode, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
import { BsModsManagerService } from "renderer/services/bs-mods-manager.service";
import { BSVersion } from "shared/bs-version.interface";
import { BbmCategories, BbmFullMod, BbmModVersion } from "shared/models/mods/mod.interface";
import { BbmCategories, BbmFullMod } from "shared/models/mods/mod.interface";
import { ModsGrid } from "./mods-grid.component";
import { ConfigurationService } from "renderer/services/configuration.service";
import { BsmButton } from "renderer/components/shared/bsm-button.component";
@@ -19,11 +19,17 @@ 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";
import { Dropzone } from "renderer/components/shared/dropzone.component";
import Tippy from "@tippyjs/react";
import { ProgressBarService } from "renderer/services/progress-bar.service";
import { Dropzone } from "renderer/components/shared/dropzone.component";
export function ModsSlide({ version, isActive, onDisclamerDecline }: { version: BSVersion; isActive?: boolean, onDisclamerDecline: () => void }) {
export type ModsSlideRef = {
loadMods: () => Promise<void>;
}
type Props = { version: BSVersion; isActive?: boolean, onDisclamerDecline: () => void };
export const ModsSlide = forwardRef<ModsSlideRef, Props>(({ version, isActive, onDisclamerDecline }, forwaredRef) => {
const ACCEPTED_DISCLAIMER_KEY = "accepted-mods-disclaimer";
const { text: t } = useTranslationV2();
@@ -100,14 +106,7 @@ export function ModsSlide({ version, isActive, onDisclamerDecline }: { version:
return Array.from(collectedDependencies);
};
const installMods = (reinstallAll: boolean): void => {
setReinstallAllMods(false);
if (installing) {
return;
}
const getModsToInstall = (reinstallAll?: boolean): BbmFullMod[] => {
let modsToInstall = [
...modsSelected,
...getAllDependencies(modsSelected, Array.from(modsAvailable.values()).flat())
@@ -127,18 +126,30 @@ export function ModsSlide({ version, isActive, onDisclamerDecline }: { version:
set.delete(null);
set.delete(undefined);
modsToInstall = Array.from(set); // Remove duplicates
return Array.from(set); // Remove duplicates
}
const installMods = (reinstallAll: boolean): Promise<void> => {
setReinstallAllMods(false);
if (installing) {
return Promise.resolve();
}
const modsToInstall = getModsToInstall(reinstallAll);
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;
return loadMods();
}
setInstalling(() => true)
lastValueFrom(modsManager.installMods(modsToInstall, version)).then(() => {
loadMods();
}).catch(noop).finally(() => setInstalling(() => false));
return lastValueFrom(modsManager.installMods(modsToInstall, version)).then(() => (
loadMods()
)).catch(noop).finally(() => (
setInstalling(() => false)
));
};
const importMods = (files: string[]): void => {
@@ -179,26 +190,19 @@ export function ModsSlide({ version, isActive, onDisclamerDecline }: { version:
return Promise.resolve();
}
const promise = async (): Promise<[BbmFullMod[], BbmModVersion[]]> => {
const available = await lastValueFrom(modsManager.getAvailableMods(version));
const installed = await lastValueFrom(modsManager.getInstalledMods(version));
return [available, installed];
}
return promise().then(([available, installed]) => {
return modsManager.getVersionModsState(version).then(({ available, installed }) => {
const defaultMods = installed?.length ? [] : available.filter(m => m.mod.category === BbmCategories.Core || m.mod.category === BbmCategories.Essential);
setModsAvailable(() => modsToCategoryMap(available));
const installedMods: BbmFullMod[] = installed.map(version => {
const mod = available.find(m => m.mod.id === version.modId);
return mod ? { ...mod, version } : null;
}).filter(mod => mod);
setModsSelected(available.filter(m => m.mod.category === BbmCategories.Core || defaultMods.some(d => m.mod.name.toLowerCase() === d.mod.name.toLowerCase()) || installedMods.some(i => m.mod.id === i.mod.id)));
setModsInstalled(modsToCategoryMap(installedMods));
setModsSelected(available.filter(m => m.mod.category === BbmCategories.Core || defaultMods.some(d => m.mod.name.toLowerCase() === d.mod.name.toLowerCase()) || installed.some(i => m.mod.id === i.mod.id)));
setModsInstalled(modsToCategoryMap(installed));
});
};
useImperativeHandle(forwaredRef, () => ({
loadMods
}), [version]);
useEffect(() => {
if(!isActive || !isOnline){
@@ -325,7 +329,7 @@ export function ModsSlide({ version, isActive, onDisclamerDecline }: { version:
</Dropzone>
</div>
);
}
});
function ModStatus({ text, image, spin = false, children }: { text: string; image: string; spin?: boolean, children?: ReactNode}) {
const t = useTranslation();
@@ -1,5 +1,5 @@
import { BSVersion } from "shared/bs-version.interface";
import { useState } from "react";
import { useRef, useState } from "react";
import { Navigate, useLocation, useNavigate } from "react-router-dom";
import { TabNavBar } from "renderer/components/shared/tab-nav-bar.component";
import { BsmDropdownButton } from "renderer/components/shared/bsm-dropdown-button.component";
@@ -10,7 +10,7 @@ import { ModalExitCode, ModalService } from "../services/modale.service";
import DefautVersionImage from "../../../assets/images/default-version-img.jpg";
import { IpcService } from "renderer/services/ipc.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";
import { ModsSlide, ModsSlideRef } from "renderer/components/version-viewer/slides/mods/mods-slide.component";
import { UninstallModal } from "renderer/components/modal/modal-types/uninstall-modal.component";
import { MapsPlaylistsPanel } from "renderer/components/maps-playlists-panel/maps-playlists-panel.component";
import { ShareFoldersModal } from "renderer/components/modal/modal-types/share-folders-modal.component";
@@ -25,12 +25,18 @@ import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
import { safeLt } from "shared/helpers/semver.helpers";
import { ConfigurationService } from "renderer/services/configuration.service";
import { logRenderError } from "renderer";
import { BsModsManagerService } from "renderer/services/bs-mods-manager.service";
import { noop } from "shared/helpers/function.helpers";
import { useTranslationV2 } from "renderer/hooks/use-translation.hook";
export function VersionViewer() {
const { text: t } = useTranslationV2();
const bsUninstallerService = useService(BSUninstallerService);
const bsVersionManagerService = useService(BSVersionManagerService);
const modalService = useService(ModalService);
const modsService = useService(BsModsManagerService);
const bsDownloader = useService(BsDownloaderService);
const ipcService = useService(IpcService);
const bsLauncher = useService(BSLauncherService);
@@ -40,10 +46,12 @@ export function VersionViewer() {
const { state, pathname: url } = useLocation() as { state: BSVersion; pathname: string };
const navigate = useNavigate();
const [currentTabIndex, setCurrentTabIndex] = useState(0);
const modsSlideRef = useRef<ModsSlideRef>(null);
useOnUpdate(() => {
checkIsVersionOutaded();
checkOutdatedMods();
}, [state]);
@@ -70,6 +78,42 @@ export function VersionViewer() {
}
}
const checkOutdatedMods = async () => {
if(config.get("not-show-outdated-mods-notification")){
return;
}
const { available: availableMods, installed: installedMods } = await modsService.getVersionModsState(state);
const modsToUpdate = availableMods.filter(availableMod => {
const installedMod = installedMods.find(installedMod => installedMod.mod.id === availableMod.mod.id);
return installedMod && safeLt(installedMod.version.modVersion, availableMod.version.modVersion);
});
if(!modsToUpdate.length){
return;
}
const choice = await notification.notifyInfo({
title: modsToUpdate.length > 1 ? t("pages.version-viewer.mods.notifications.outdated-mods.title-plural") : t("pages.version-viewer.mods.notifications.outdated-mods.title"),
desc: modsToUpdate.length > 1 ? t("pages.version-viewer.mods.notifications.outdated-mods.description-plural", { nb: `${modsToUpdate.length}` }) : t("pages.version-viewer.mods.notifications.outdated-mods.description", { name: `${modsToUpdate[0].mod.name}` }),
duration: 9000,
actions: [
{ id: "0", title: "pages.version-viewer.mods.notifications.outdated-mods.dont-remind-me", cancel: true },
{ id: "1", title: "pages.version-viewer.mods.notifications.outdated-mods.update" }
]
});
if(choice === "1"){
lastValueFrom(modsService.installMods(modsToUpdate, state)).then(() => {
modsSlideRef?.current?.loadMods?.();
}).catch(noop);
} else if(choice === "0"){
config.set("not-show-outdated-mods-notification", true);
}
}
const navigateToVersion = (version?: BSVersion) => {
if (!version) {
return navigate("/available-versions");
@@ -153,7 +197,7 @@ export function VersionViewer() {
<div className="w-full shrink-0 px-3 pb-3 flex flex-col items-center">
<ModelsPanel version={state} isActive={currentTabIndex === 2} goToMods={() => setCurrentTabIndex(() => 3)} />
</div>
<ModsSlide version={state} isActive={currentTabIndex === 3} onDisclamerDecline={handleModsDisclaimerDecline} />
<ModsSlide ref={modsSlideRef} version={state} isActive={currentTabIndex === 3} onDisclamerDecline={handleModsDisclaimerDecline} />
</div>
</div>
<BsmDropdownButton className="absolute top-3 right-4 h-9 w-9 bg-light-main-color-2 dark:bg-main-color-2 rounded-md" items={[
@@ -7,6 +7,7 @@ import { NotificationService } from "./notification.service";
import { Progression } from "main/helpers/fs.helpers";
import { ProgressionInterface } from "shared/models/progress-bar";
import { BbmFullMod, BbmModVersion } from "shared/models/mods/mod.interface";
import { logRenderError } from "renderer";
export class BsModsManagerService {
private static instance: BsModsManagerService;
@@ -180,4 +181,27 @@ export class BsModsManagerService {
});
}
public async getVersionModsState(version: BSVersion): Promise<{available: BbmFullMod[], installed: BbmFullMod[]}> {
const [available, installed]: [BbmFullMod[], BbmModVersion[]] = await (async () => {
const available = await lastValueFrom(this.getAvailableMods(version));
const installed = await lastValueFrom(this.getInstalledMods(version));
return [available ?? [], installed ?? []] as [BbmFullMod[], BbmModVersion[]]; // Make TS happy
})().catch(e => {
logRenderError(e);
return [[], []] as [BbmFullMod[], BbmModVersion[]]; // Make TS happy
})
const installedMods: BbmFullMod[] = installed.reduce((acc, installedMod) => {
const mod = available.find(m => m.mod.id === installedMod.modId);
if(mod){
acc.push({ ...mod, version: installedMod } as BbmFullMod);
}
return acc;
}, []);
return { available: (available ?? []), installed: (installedMods ?? []) };
}
}