Merge pull request #540 from Zagrios/qol/dont-reinstall-all-mods-by-default

[qol] dont reinstall all mods by default
This commit is contained in:
MathieuG-P
2024-07-29 21:32:57 +02:00
committed by GitHub
14 changed files with 94 additions and 61 deletions
+2 -1
View File
@@ -73,7 +73,8 @@
"mods-not-available": "Für diese Version von Beat Saber sind noch keine Mods verfügbar",
"buttons": {
"more-infos": "Mehr Infos",
"install-or-update": "Installieren oder aktualisieren"
"install-or-update": "Installieren oder aktualisieren",
"reinstall-all": "Alles neu installieren"
},
"mods-grid": {
"header-bar": {
+2 -1
View File
@@ -73,7 +73,8 @@
"mods-not-available": "No mods are available yet for this version of Beat Saber",
"buttons": {
"more-infos": "More info",
"install-or-update": "Install or update"
"install-or-update": "Install or update",
"reinstall-all": "Reinstall all"
},
"mods-grid": {
"header-bar": {
+2 -1
View File
@@ -73,7 +73,8 @@
"mods-not-available": "Aún no hay mods disponibles para esta versión de Beat Saber",
"buttons": {
"more-infos": "Más información",
"install-or-update": "Instalar o actualizar"
"install-or-update": "Instalar o actualizar",
"reinstall-all": "Reinstalar todo"
},
"mods-grid": {
"header-bar": {
+2 -1
View File
@@ -73,7 +73,8 @@
"mods-not-available": "Aucun mod n'est encore disponible pour cette version de Beat Saber",
"buttons": {
"more-infos": "Plus d'infos",
"install-or-update": "Installer ou mettre à jour"
"install-or-update": "Installer ou mettre à jour",
"reinstall-all": "Tout réinstaller"
},
"mods-grid": {
"header-bar": {
+2 -1
View File
@@ -73,7 +73,8 @@
"mods-not-available": "このバージョンで使用できるMODはまだありません。",
"buttons": {
"more-infos": "詳細情報",
"install-or-update": "インストールとアップデート"
"install-or-update": "インストールとアップデート",
"reinstall-all": "すべて再インストール"
},
"mods-grid": {
"header-bar": {
+2 -1
View File
@@ -73,7 +73,8 @@
"mods-not-available": "Не найдены моды для этой версии Beat Saber",
"buttons": {
"more-infos": "Подробнее",
"install-or-update": "Установить или обновить"
"install-or-update": "Установить или обновить",
"reinstall-all": "Переустановить все"
},
"mods-grid": {
"header-bar": {
+2 -1
View File
@@ -73,7 +73,8 @@
"mods-not-available": "該版本 BeatSaber 暫無可用 Mod",
"buttons": {
"more-infos": "更多資訊",
"install-or-update": "安裝或更新"
"install-or-update": "安裝或更新",
"reinstall-all": "重新安裝全部"
},
"mods-grid": {
"header-bar": {
+2 -1
View File
@@ -73,7 +73,8 @@
"mods-not-available": "该版本 BeatSaber 暂无可用 Mod",
"buttons": {
"more-infos": "更多信息",
"install-or-update": "安装或更新"
"install-or-update": "安装或更新",
"reinstall-all": "重新安装全部"
},
"mods-grid": {
"header-bar": {
@@ -346,15 +346,12 @@ export class BsModsManagerService {
throw CustomError.fromError(new Error("No mods to install"), "no-mods");
}
const deps = await this.resolveDependencies(mods, version);
mods.push(...deps);
const bsipa = mods.find(mod => mod.name.toLowerCase() === "bsipa");
if (bsipa) {
mods = mods.filter(mod => mod.name.toLowerCase() !== "bsipa");
}
this.nbModsToInstall = mods.length + (bsipa && 1);
this.nbModsToInstall = mods.length + (bsipa ? 1 : 0);
this.nbInstalledMods = 0;
if (bsipa) {
@@ -6,11 +6,12 @@ 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" | "none";
export type BsmButtonType = "primary" | "secondary" | "success" | "cancel" | "error" | "none";
type Props = {
className?: string;
style?: CSSProperties;
iconStyle?: CSSProperties;
imgClassName?: string;
iconClassName?: string;
icon?: BsmIconType;
@@ -29,7 +30,7 @@ type Props = {
textClassName?: string;
};
export const BsmButton = forwardRef<unknown, Props>(({ className, style, imgClassName, iconClassName, icon, image, text, type, active, withBar = true, disabled, onClickOutside, onClick, typeColor, color, title, iconColor, textClassName }, forwardedRef) => {
export const BsmButton = forwardRef<unknown, Props>(({ className, style, iconStyle, imgClassName, iconClassName, icon, image, text, type, active, withBar = true, disabled, onClickOutside, onClick, typeColor, color, title, iconColor, textClassName }, forwardedRef) => {
const t = useTranslation();
const { firstColor, secondColor } = useThemeColor();
const ref = useRef<HTMLDivElement>(null);
@@ -86,10 +87,10 @@ export const BsmButton = forwardRef<unknown, Props>(({ className, style, imgClas
return (
<div ref={setRef} onClick={handleClick} title={t(title)} className={`${className} overflow-hidden group ${!withBar && !disabled && (!!typeColor || !!color) && "hover:brightness-[1.15]"} ${disabled ? "brightness-75 cursor-not-allowed" : "cursor-pointer"} ${renderTypeColor}`} style={{ ...style, backgroundColor: primaryColor || color }}>
{image && <BsmImage image={image} className={imgClassName} />}
{icon && <BsmIcon icon={icon} className={iconClassName ?? "h-full w-full text-gray-800 dark:text-white"} style={{ color: iconColor || textColor }} />}
{icon && <BsmIcon icon={icon} className={iconClassName ?? "size-full text-gray-800 dark:text-white"} style={{ ...(iconStyle ?? {}), color: (iconColor || textColor) }} />}
{text &&
(type === "submit" ? (
<button type="submit" className={textClassName || "h-full w-full"} style={{ ...(!!textColor && { color: textColor }) }}>
<button type="submit" className={textClassName || "size-full"} style={{ ...(!!textColor && { color: textColor }) }}>
{t(text)}
</button>
) : (
@@ -99,8 +100,8 @@ export const BsmButton = forwardRef<unknown, Props>(({ className, style, imgClas
))}
{withBar && (
<div className="absolute bottom-0 left-0 w-full h-1 bg-current" style={{ color: secondColor }}>
<div className="absolute top-0 left-0 h-full w-full bg-current brightness-50" />
<div className={`absolute top-0 left-0 h-full w-full bg-inherit -translate-x-full group-hover:translate-x-0 transition-transform shadow-center shadow-current ${active && "translate-x-0"}`} />
<div className="absolute top-0 left-0 size-full bg-current brightness-50" />
<div className={`absolute top-0 left-0 size-full bg-inherit -translate-x-full group-hover:translate-x-0 transition-transform shadow-center shadow-current ${active && "translate-x-0"}`} />
</div>
)}
</div>
@@ -1,9 +1,10 @@
import { forwardRef, LegacyRef, useImperativeHandle, useRef, useState } from "react";
import { BsmIconType, BsmIcon } from "../svgs/bsm-icon.component";
import { BsmButton } from "./bsm-button.component";
import { BsmButton, BsmButtonType } from "./bsm-button.component";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { AnimatePresence } from "framer-motion";
import { useClickOutside } from "renderer/hooks/use-click-outside.hook";
import { cn } from "renderer/helpers/css-class.helpers";
export interface DropDownItem {
text: string;
@@ -11,8 +12,17 @@ export interface DropDownItem {
onClick?: () => void;
}
type ClassNames = {
mainContainer?: string;
button?: string;
itemsContainer?: string;
iconClassName?: string;
}
type Props = {
className?: string;
classNames?: ClassNames;
buttonColor?: BsmButtonType;
items?: DropDownItem[];
align?: "left" | "right" | "center";
withBar?: boolean;
@@ -24,7 +34,7 @@ type Props = {
textClassName?: string;
};
export const BsmDropdownButton = forwardRef(({ className, items, align, withBar = true, icon = "settings", buttonClassName, menuTranslationY, children, text, textClassName }: Props, fowardRed) => {
export const BsmDropdownButton = forwardRef(({ className, classNames, buttonColor, items, align, withBar = true, icon = "settings", buttonClassName, menuTranslationY, children, text, textClassName }: Props, fowardRed) => {
const [expanded, setExpanded] = useState(false);
const t = useTranslation();
const ref = useRef(fowardRed);
@@ -63,9 +73,9 @@ export const BsmDropdownButton = forwardRef(({ className, items, align, withBar
})();
return (
<div ref={ref as unknown as LegacyRef<HTMLDivElement>} className={className}>
<BsmButton onClick={() => setExpanded(!expanded)} className={buttonClassName ?? defaultButtonClassName} icon={icon} active={expanded} textClassName={textClassName} onClickOutside={handleClickOutside} withBar={withBar} text={text} />
<div className={`py-1 w-fit absolute cursor-pointer top-[calc(100%-4px)] rounded-md bg-inherit text-sm text-gray-800 dark:text-gray-200 shadow-md shadow-black transition-[scale] duration-150 ease-in-out ${alignClass}`} style={{ scale: expanded ? "1" : "0", translate: `0 ${menuTranslationY}` }}>
<div ref={ref as unknown as LegacyRef<HTMLDivElement>} className={cn(className, classNames?.mainContainer)}>
<BsmButton onClick={() => setExpanded(!expanded)} className={cn(buttonClassName ?? defaultButtonClassName, classNames?.button)} icon={icon} active={expanded} textClassName={textClassName} onClickOutside={handleClickOutside} withBar={withBar} text={text} typeColor={buttonColor} iconClassName={classNames?.iconClassName}/>
<div className={cn(`py-1 w-fit absolute cursor-pointer top-[calc(100%-4px)] rounded-md bg-inherit text-sm text-gray-800 dark:text-gray-200 shadow-md shadow-black transition-[scale] duration-150 ease-in-out ${alignClass}`, classNames?.itemsContainer)} style={{ scale: expanded ? "1" : "0", translate: `0 ${menuTranslationY}` }}>
{items?.map(
i =>
i && (
@@ -1,7 +1,7 @@
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { BsmCheckbox } from "renderer/components/shared/bsm-checkbox.component";
import { Mod } from "shared/models/mods/mod.interface";
import { CSSProperties, MouseEvent, useRef } from "react";
import { CSSProperties, MouseEvent, useMemo, useRef } from "react";
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
import { BsModsManagerService } from "renderer/services/bs-mods-manager.service";
import { useObservable } from "renderer/hooks/use-observable.hook";
@@ -9,6 +9,7 @@ import { PageStateService } from "renderer/services/page-state.service";
import useDoubleClick from "use-double-click";
import { gt } from "semver";
import { useService } from "renderer/hooks/use-service.hook";
import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
type Props = { className?: string; mod: Mod; installedVersion: string; isDependency?: boolean; isSelected?: boolean; onChange?: (val: boolean) => void; wantInfo?: boolean; onWantInfo?: (mod: Mod) => void };
@@ -20,6 +21,8 @@ export function ModItem({ className, mod, installedVersion, isDependency, isSele
const uninstalling = useObservable(() => modsManager.isUninstalling$);
const clickRef = useRef();
const isChecked = useMemo(() => isDependency || isSelected || mod.required, [isDependency, isSelected, mod.required]);
useDoubleClick({
onSingleClick: e => handleWantInfo(e),
onDoubleClick: e => handleOnChange(e),
@@ -27,6 +30,10 @@ export function ModItem({ className, mod, installedVersion, isDependency, isSele
latency: 175,
});
useOnUpdate(() => {
onChange(isChecked);
}, [isChecked]);
const wantInfoStyle: CSSProperties = wantInfo ? { borderColor: themeColor } : { borderColor: "transparent" };
const isOutDated = installedVersion ? gt(mod.version, installedVersion) : false;
@@ -34,21 +41,19 @@ export function ModItem({ className, mod, installedVersion, isDependency, isSele
modsManager.uninstallMod(mod, pageState.getState());
};
const handleWantInfo = (e: MouseEvent<Element, MouseEvent>) => {
const handleWantInfo = (e: MouseEvent) => {
e.preventDefault();
onWantInfo(mod);
};
const handleOnChange = (e: MouseEvent<Element, MouseEvent>) => {
const handleOnChange = (e: MouseEvent) => {
e.preventDefault();
onChange(!isChecked);
};
const isChecked = isDependency || isSelected || mod.required;
return (
<li ref={clickRef} className={`${className} group`}>
<div className="h-full aspect-square flex items-center justify-center p-[7px] rounded-l-md bg-inherit ml-3 border-2 border-r-0 z-[1] group-hover:brightness-90" style={wantInfoStyle}>
<BsmCheckbox className="h-full aspect-square z-[1] relative bg-inherit" onChange={onChange} disabled={mod.required || isDependency} checked={isChecked} />
<BsmCheckbox className="h-full aspect-square z-[1] relative bg-inherit" onChange={() => onChange(!isChecked)} disabled={mod.required || isDependency} checked={isChecked} />
</div>
<span className="bg-inherit py-2 pl-3 font-bold text-sm whitespace-nowrap border-t-2 border-b-2 blur-none group-hover:brightness-90" style={wantInfoStyle}>
{mod.name}
@@ -12,7 +12,7 @@ import { useService } from "renderer/hooks/use-service.hook";
type Props = { modsMap: Map<string, Mod[]>; installed: Map<string, Mod[]>; modsSelected: Mod[]; onModChange: (selected: boolean, mod: Mod) => void; moreInfoMod?: Mod; onWantInfos: (mod: Mod) => void };
export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreInfoMod, onWantInfos }: Props) {
const pageState = useService(PageStateService);
const modsManager = useService(BsModsManagerService);
@@ -24,7 +24,7 @@ export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreIn
if (!installed?.get(key)) {
return undefined;
}
const installedMod = installed.get(key).find(m => m.name === mod.name);
const installedMod = installed.get(key).find(m => m._id === mod._id);
if (!installedMod) {
return undefined;
}
@@ -36,16 +36,16 @@ export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreIn
const deps = m.dependencies.map(dep =>
Array.from(modsMap.values())
.flat()
.find(m => dep.name === m.name)
.find(m => dep._id === m._id)
);
if (deps.some(depMod => depMod.name === mod.name)) {
if (deps.some(depMod => depMod._id === mod._id)) {
return true;
}
return deps.some(depMod => depMod.dependencies.some(depModDep => depModDep.name === mod.name));
return deps.some(depMod => depMod.dependencies.some(depModDep => depModDep._id === mod._id));
});
};
const isSelected = (mod: Mod): boolean => modsSelected.some(m => m.name === mod.name);
const isSelected = (mod: Mod): boolean => modsSelected.some(m => m._id === mod._id);
const handleInput = (val: string) => setFilter(val.toLowerCase());
@@ -61,8 +61,8 @@ export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreIn
return (
modsMap && (
<div className="grid gap-y-1 grid-cols-[40px_min-content_min-content_min-content_1fr_min-content] bg-light-main-color-2 dark:bg-main-color-2 text-main-color-1 dark:text-light-main-color-1">
<span className="absolute z-10 top-0 w-full h-8 bg-inherit" />
<span className="z-10 sticky flex items-center justify-end top-0 bg-inherit border-b-2 border-main-color-1">
<span className="absolute z-10 top-0 w-full h-8 bg-inherit rounded-tl-md" />
<span className="z-10 sticky flex items-center justify-end top-0 bg-inherit border-b-2 border-main-color-1 rounded-tl-md">
<BsmButton className="rounded-full h-6 w-6 p-[2px]" withBar={false} icon="search" onClick={handleToogleFilter} />
</span>
<span className="z-10 sticky top-0 flex items-center bg-inherit border-main-color-1 border-b-2 h-8 px-1 whitespace-nowrap">{filterEnabled ? <motion.input autoFocus className="bg-main-color-1 rounded-md h-6 px-2" initial={{ width: 0 }} animate={{ width: "250px" }} transition={{ ease: "easeInOut", duration: 0.15 }} onChange={e => handleInput(e.target.value)} /> : <span className="w-full text-center">{t("pages.version-viewer.mods.mods-grid.header-bar.name")}</span>}</span>
@@ -19,12 +19,14 @@ import { ModsDisclaimerModal } from "renderer/components/modal/modal-types/mods-
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";
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);
@@ -35,6 +37,7 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
const [modsInstalled, setModsInstalled] = useState(null as Map<string, Mod[]>);
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 = useObservable(() => modsManager.isInstalling$);
@@ -51,15 +54,17 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
};
const handleModChange = (selected: boolean, mod: Mod) => {
if (selected) {
return setModsSelected([...modsSelected, mod]);
return setModsSelected(mods => {
if (mods.some(m => m._id === mod._id)) {
return mods;
}
return [...mods, mod];
});
}
const mods = [...modsSelected];
mods.splice(
mods.findIndex(m => m.name === mod.name),
1
);
setModsSelected(mods);
setModsSelected(mods => mods.filter(m => m._id !== mod._id));
};
const handleMoreInfo = (mod: Mod) => {
@@ -76,25 +81,28 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
linkOpener.open(moreInfoMod.link);
};
const installMods = () => {
const installMods = (reinstallAll: boolean): void => {
setReinstallAllMods(() => false);
if (installing) {
return;
}
const modsToInstall = modsSelected.filter(mod => {
const corespondingMod = modsAvailable.get(mod.category).find(availabeMod => availabeMod._id === mod._id);
const installedMod = modsInstalled.get(mod.category)?.find(installedMod => installedMod.name === mod.name);
const installedMod = modsInstalled.get(mod.category)?.find(installedMod => installedMod._id === mod._id);
if (corespondingMod?.version && lt(corespondingMod.version, mod.version)) {
return false;
}
if(reinstallAll || !installedMod){ return true; }
if(installedMod?.version && lt(mod.version, installedMod?.version)){
return false;
}
return true;
return lt(installedMod.version, mod.version);
});
if (!modsToInstall.length) {
notification.notifyInfo({ title: "Mods déjà installées", desc: "Tous les mods séléctionnées sont déjà installées" });
loadMods();
return;
}
modsManager.installMods(modsToInstall, version).then(() => {
loadMods();
});
@@ -186,13 +194,17 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
}
return (
<>
<div className="grow overflow-y-scroll w-full min-h-0 scrollbar-default">
<div className="grow overflow-y-scroll w-full min-h-0 scrollbar-default p-0 m-0">
<ModsGrid modsMap={modsAvailable} installed={modsInstalled} modsSelected={modsSelected} onModChange={handleModChange} moreInfoMod={moreInfoMod} onWantInfos={handleMoreInfo} />
</div>
<div className="h-10 shrink-0 flex items-center justify-between px-3">
<BsmButton className="text-center rounded-md px-2 py-[2px]" text="pages.version-viewer.mods.buttons.more-infos" typeColor="cancel" withBar={false} disabled={!moreInfoMod} onClick={handleOpenMoreInfo} style={{ width: downloadWith }} />
<div ref={downloadRef}>
<BsmButton className="text-center rounded-md px-2 py-[2px]" text="pages.version-viewer.mods.buttons.install-or-update" withBar={false} disabled={installing} typeColor="primary" onClick={installMods} />
<div className="shrink-0 flex items-center justify-between px-3 py-2">
<BsmButton className="flex items-center justify-center rounded-md px-1 h-8" text="pages.version-viewer.mods.buttons.more-infos" typeColor="cancel" withBar={false} disabled={!moreInfoMod} onClick={handleOpenMoreInfo} style={{ width: downloadWith }}/>
<div ref={downloadRef} className="flex h-8 justify-center items-center gap-px overflow-hidden rounded-md">
<div className="grow h-full relative">
<BsmButton className="relative left-0 flex items-center justify-center px-2 size-full transition-[top] duration-200 ease-in-out" text="pages.version-viewer.mods.buttons.install-or-update" typeColor="primary" withBar={false} onClick={() => installMods(false)} style={{ top: reinstallAllMods ? "-100%" : "0" }} />
<BsmButton className="relative left-0 flex items-center justify-center px-2 size-full transition-[top] duration-200 ease-in-out " text="pages.version-viewer.mods.buttons.reinstall-all" typeColor="primary" withBar={false} onClick={() => installMods(true)} style={{ top: reinstallAllMods ? "-100%" : "0" }}/>
</div>
<BsmButton className="flex items-center justify-center shrink-0 h-full" iconClassName="transition-transform size-full ease-in-out duration-200" iconStyle={{ transform: reinstallAllMods ? "rotate(360deg)" : "rotate(180deg)" }} icon="chevron-top" typeColor="primary" withBar={false} onClick={() => setReinstallAllMods(prev => !prev)}/>
</div>
</div>
</>
@@ -201,7 +213,7 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
return (
<div ref={ref} className="shrink-0 w-full h-full px-8 pb-7 flex justify-center">
<div className="relative flex flex-col grow-0 bg-light-main-color-2 dark:bg-main-color-2 h-full w-full rounded-md shadow-black shadow-center overflow-hidden">{renderContent()}</div>
<div className="relative flex flex-col grow-0 bg-light-main-color-2 dark:bg-main-color-2 size-full rounded-md shadow-black shadow-center overflow-hidden">{renderContent()}</div>
</div>
);
}