diff --git a/src/renderer/components/modal/modal-types/mods/mods-version-compare-modal.component.tsx b/src/renderer/components/modal/modal-types/mods/mods-version-compare-modal.component.tsx index d418456d1..1a14db7bc 100644 --- a/src/renderer/components/modal/modal-types/mods/mods-version-compare-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/mods/mods-version-compare-modal.component.tsx @@ -1,6 +1,11 @@ import { useEffect, useState } from "react"; import { logRenderError } from "renderer"; import { BsmSelect, BsmSelectOption } from "renderer/components/shared/bsm-select.component"; +import { AddIcon } from "renderer/components/svgs/icons/add-icon.component"; +import { DownIcon } from "renderer/components/svgs/icons/down-icon.component"; +import { EqualIcon } from "renderer/components/svgs/icons/equals-icon.component"; +import { RemoveIcon } from "renderer/components/svgs/icons/remove-icon.component"; +import { UpIcon } from "renderer/components/svgs/icons/up-icon.component"; import { useConstant } from "renderer/hooks/use-constant.hook"; import { useService } from "renderer/hooks/use-service.hook"; import { useTranslationV2 } from "renderer/hooks/use-translation.hook"; @@ -131,10 +136,11 @@ function useHeader({ } // Manual in-memory caching - modsMapCache.set(otherVersion, { + const newCache = new Map(modsMapCache); + newCache.set(otherVersion, { availableModsMap, installedModsMap, }); - setModsMapCache(modsMapCache); + setModsMapCache(newCache); setOtherAvailableModsMap(availableModsMap); setOtherInstalledModsMap(installedModsMap); @@ -144,7 +150,8 @@ function useHeader({ return { - mode, otherAvailableModsMap, otherInstalledModsMap, + mode, + otherVersion, otherAvailableModsMap, otherInstalledModsMap, renderHeader: () => <div className="grid grid-cols-2 text-large mb-2"> <div className="flex justify-center gap-x-2"> @@ -165,7 +172,6 @@ function useHeader({ onChange={setOtherVersion} /> </div> - }; } @@ -174,65 +180,101 @@ function ModCompare({ installed, otherMod, otherInstalled, + otherInstalledLocal, loading, }: Readonly<{ mod: Mod | null; installed: boolean; otherMod: Mod | null; otherInstalled: boolean; + // Check if the other version is installed locally + otherInstalledLocal: boolean; loading: boolean; }>) { const name = mod?.name || otherMod?.name; - const render = (renderMod: Mod | null, installed_: boolean, loading_: boolean = false) => { - if (loading_) { + const renderMod = () => { + if (!mod) { + return <div className="bg-black py-1 px-2">{name}</div> + } + + let modClass = "flex justify-between py-1 px-2 gap-x-2"; + if (installed) { + modClass += " bg-green-700"; + } else { + modClass += " bg-red-700"; + } + + return <div className={modClass}> + <div className="text-ellipsis overflow-hidden">{name}</div> + <div>{mod.version}</div> + </div> + } + + const renderOtherMod = () => { + if (loading) { return <div className="bg-black">TODO: Rainbow Lazy Loading</div> } - if (!renderMod) { + if (!otherMod) { return <div className="bg-black py-1 px-2">{name}</div> } let modClass = "flex justify-between py-1 px-2 gap-x-2"; - if (installed_) { + if (!otherInstalledLocal) { + modClass += " bg-blue-700"; // Change, just for visual prototyping + } else if (otherInstalled) { modClass += " bg-green-700"; } else { modClass += " bg-red-700"; } - return <div key={renderMod._id} className={modClass}> + return <div className={modClass}> <div className="text-ellipsis overflow-hidden">{name}</div> - <div>{renderMod.version}</div> + <div>{otherMod.version}</div> </div> } - const renderSymbol = () => { - let symbol = ""; + const renderIcon = () => { + if (loading) { + return <div /> + } + if (mod && !otherMod) { - symbol = "-"; // Removed / Not Submitted Yet - } else if (!mod && otherMod) { - symbol = "+"; // Added - } else if (mod.version === otherMod.version) { - symbol = "="; // Equals - } else if (safeLt(mod.version, otherMod.version)) { - symbol = "^"; // Upgraded - } else { - symbol = "v"; // Downgraded + // Removed / Not Submitted Yet + return <RemoveIcon className="text-center w-6 h-6" /> + } + + if (!mod && otherMod) { + // Added + return <AddIcon className="text-center w-6 h-6" /> + } + + if (mod.version === otherMod.version) { + // Equals + return <EqualIcon className="text-center w-6 h-6" /> } - return <div className="text-center font-extrabold">{symbol}</div> + if (safeLt(mod.version, otherMod.version)) { + // Upgraded + return <UpIcon className="text-center w-6 h-6" /> + } + + // Downgraded + return <DownIcon className="text-center w-6 h-6" /> } return <> - {render(mod, installed)} - {renderSymbol()} - {render(otherMod, otherInstalled, loading)} + {renderMod()} + {renderIcon()} + {renderOtherMod()} </> } function ModCategory({ mode, category, + otherVersion, availableMods, installedMods, otherAvailableMods, @@ -241,12 +283,15 @@ function ModCategory({ }: Readonly<{ mode: Mode; category: string; + otherVersion: BSVersion; availableMods: Mod[]; installedMods: Mod[]; otherAvailableMods: Mod[]; otherInstalledMods: Mod[]; loading: boolean; }>) { + const otherInstalledLocal = !!otherVersion?.path; + let combinedMods: Mod[] = []; switch (mode) { case Mode.All: @@ -255,7 +300,7 @@ function ModCategory({ break; } - availableMods.forEach(mod => { + otherAvailableMods.forEach(mod => { if (combinedMods.findIndex(cm => cm.name === mod.name) === -1) { combinedMods.push(mod); } @@ -286,7 +331,7 @@ function ModCategory({ </h2> <div className="grid" style={{ - gridTemplateColumns: "350px 25px 350px" + gridTemplateColumns: "350px 24px 350px" }}> {combinedMods.map(mod => <ModCompare @@ -295,6 +340,7 @@ function ModCategory({ installed={installedMods.findIndex(im => im.name === mod.name) > -1} otherMod={otherAvailableMods.find(oam => oam.name === mod.name)} otherInstalled={otherInstalledMods.findIndex(oim => oim.name === mod.name) > -1} + otherInstalledLocal={otherInstalledLocal} loading={loading} /> )} @@ -315,7 +361,8 @@ export const ModsVersionCompareModal: ModalComponent<void, Readonly<{ const [loading, setLoading] = useState(false); const { - mode, otherAvailableModsMap, otherInstalledModsMap, + mode, + otherVersion, otherAvailableModsMap, otherInstalledModsMap, renderHeader, } = useHeader({ version, loading, setLoading }); @@ -343,6 +390,7 @@ export const ModsVersionCompareModal: ModalComponent<void, Readonly<{ key={category} mode={mode} category={category} + otherVersion={otherVersion} availableMods={availableModsMap.get(category) || []} installedMods={installedModsMap.get(category) || []} otherAvailableMods={otherAvailableModsMap.get(category) || []} diff --git a/src/renderer/components/svgs/bsm-icon.component.tsx b/src/renderer/components/svgs/bsm-icon.component.tsx index 237616e36..fe7d17ad6 100644 --- a/src/renderer/components/svgs/bsm-icon.component.tsx +++ b/src/renderer/components/svgs/bsm-icon.component.tsx @@ -67,10 +67,11 @@ import { BrowseIcon } from "./icons/browse-icon.component"; import { AddFileIcon } from "./icons/add-file-icon.component"; import { CancelIcon } from "./icons/cancel-icon.component"; import { WarningIcon } from "./icons/warning-icon.component"; +import { CompareIcon } from "./icons/compare-icon.component"; -export type BsmIconType = SongDetailDiffCharactertistic | ("settings" | "trash" | "favorite" | "folder" | "bsNote" | "check" | "three-dots" | "twitch" | "eye" | "play" | "checkCircleIcon" | "discord" | "info" | "eye-cross" | "terminal" | "desktop" | "oculus" | "add" | "cross" | "task" | "github" | "close" | "thumbUpFill" | "timerFill" | "pause" | "twitter" | "sync" | "chevron-top" | "copy" | "steam" | "edit" | "export" | "patreon" | "search" | "bsMapDifficulty" | "link" | "unlink" | "download" | "filter" | "mee6" | "volume-up" | "volume-off" | "volume-down" | "shortcut" | "backup-restore" | "web-site" | "clean" | "browse" | "add-file" | "cancel" | "warning" | "fr-FR-flag" | "es-ES-flag" | "en-US-flag" | "en-EN-flag" | "de-DE-flag" | "ru-RU-flag" | "zh-CN-flag" | "zh-TW-flag" | "ja-JP-flag" | "ko-KR-flag" | "null" ); +export type BsmIconType = SongDetailDiffCharactertistic | ("settings" | "trash" | "favorite" | "folder" | "bsNote" | "check" | "three-dots" | "twitch" | "eye" | "play" | "checkCircleIcon" | "discord" | "info" | "eye-cross" | "terminal" | "desktop" | "oculus" | "add" | "cross" | "task" | "github" | "close" | "thumbUpFill" | "timerFill" | "pause" | "twitter" | "sync" | "chevron-top" | "copy" | "steam" | "edit" | "export" | "patreon" | "search" | "bsMapDifficulty" | "link" | "unlink" | "download" | "filter" | "mee6" | "volume-up" | "volume-off" | "volume-down" | "shortcut" | "backup-restore" | "web-site" | "clean" | "browse" | "add-file" | "cancel" | "warning" | "compare" | "fr-FR-flag" | "es-ES-flag" | "en-US-flag" | "en-EN-flag" | "de-DE-flag" | "ru-RU-flag" | "zh-CN-flag" | "zh-TW-flag" | "ja-JP-flag" | "ko-KR-flag" | "null" ); export const BsmIcon = memo(({ className, icon, style }: { className?: string; icon: BsmIconType; style?: CSSProperties }) => { // TODO : Very ugly very messy, need to find a better way to do this @@ -288,6 +289,10 @@ export const BsmIcon = memo(({ className, icon, style }: { className?: string; i return <WarningIcon className={className} style={style} /> } + if (icon === "compare") { + return <CompareIcon className={className} style={style} /> + } + return <TrashIcon className={className} style={style} />; diff --git a/src/renderer/components/svgs/icons/compare-icon.component.tsx b/src/renderer/components/svgs/icons/compare-icon.component.tsx new file mode 100644 index 000000000..465a2e11e --- /dev/null +++ b/src/renderer/components/svgs/icons/compare-icon.component.tsx @@ -0,0 +1,15 @@ +import { CSSProperties } from "react"; + +export function CompareIcon(props: Readonly<{ className?: string; style?: CSSProperties }>) { + return ( + <svg + className={props.className} + style={props.style} + xmlns="http://www.w3.org/2000/svg" + viewBox="0 -960 960 960" + fill="currentColor" + > + <path d="M367-320H120q-17 0-28.5-11.5T80-360q0-17 11.5-28.5T120-400h247l-75-75q-11-11-11-27.5t11-28.5q12-12 28.5-12t28.5 12l143 143q6 6 8.5 13t2.5 15q0 8-2.5 15t-8.5 13L348-188q-12 12-28 11.5T292-189q-11-12-11.5-28t11.5-28l75-75Zm226-240 75 75q11 11 11 27.5T668-429q-12 12-28.5 12T611-429L468-572q-6-6-8.5-13t-2.5-15q0-8 2.5-15t8.5-13l144-144q12-12 28-11.5t28 12.5q11 12 11.5 28T668-715l-75 75h247q17 0 28.5 11.5T880-600q0 17-11.5 28.5T840-560H593Z" /> + </svg> + ); +} diff --git a/src/renderer/components/svgs/icons/down-icon.component.tsx b/src/renderer/components/svgs/icons/down-icon.component.tsx new file mode 100644 index 000000000..5abec0a5b --- /dev/null +++ b/src/renderer/components/svgs/icons/down-icon.component.tsx @@ -0,0 +1,15 @@ +import { CSSProperties } from "react"; + +export function DownIcon(props: Readonly<{ className?: string; style?: CSSProperties }>) { + return ( + <svg + className={props.className} + style={props.style} + xmlns="http://www.w3.org/2000/svg" + viewBox="0 -960 960 960" + fill="currentColor" + > + <path d="M440-313v-447q0-17 11.5-28.5T480-800q17 0 28.5 11.5T520-760v447l196-196q12-12 28-11.5t28 12.5q11 12 11.5 28T772-452L508-188q-6 6-13 8.5t-15 2.5q-8 0-15-2.5t-13-8.5L188-452q-11-11-11-27.5t11-28.5q12-12 28.5-12t28.5 12l195 195Z" /> + </svg> + ); +} diff --git a/src/renderer/components/svgs/icons/equals-icon.component.tsx b/src/renderer/components/svgs/icons/equals-icon.component.tsx new file mode 100644 index 000000000..69b51d629 --- /dev/null +++ b/src/renderer/components/svgs/icons/equals-icon.component.tsx @@ -0,0 +1,15 @@ +import { CSSProperties } from "react"; + +export function EqualIcon(props: Readonly<{ className?: string; style?: CSSProperties }>) { + return ( + <svg + className={props.className} + style={props.style} + xmlns="http://www.w3.org/2000/svg" + viewBox="0 -960 960 960" + fill="currentColor" + > + <path d="M220-280q-25 0-42.5-17.5T160-340q0-25 17.5-42.5T220-400h520q25 0 42.5 17.5T800-340q0 25-17.5 42.5T740-280H220Zm0-280q-25 0-42.5-17.5T160-620q0-25 17.5-42.5T220-680h520q25 0 42.5 17.5T800-620q0 25-17.5 42.5T740-560H220Z" /> + </svg> + ); +} diff --git a/src/renderer/components/svgs/icons/remove-icon.component.tsx b/src/renderer/components/svgs/icons/remove-icon.component.tsx new file mode 100644 index 000000000..c035ec268 --- /dev/null +++ b/src/renderer/components/svgs/icons/remove-icon.component.tsx @@ -0,0 +1,15 @@ +import { CSSProperties } from "react"; + +export function RemoveIcon(props: Readonly<{ className?: string; style?: CSSProperties }>) { + return ( + <svg + className={props.className} + style={props.style} + xmlns="http://www.w3.org/2000/svg" + viewBox="0 -960 960 960" + fill="currentColor" + > + <path d="M240-440q-17 0-28.5-11.5T200-480q0-17 11.5-28.5T240-520h480q17 0 28.5 11.5T760-480q0 17-11.5 28.5T720-440H240Z" /> + </svg> + ) +} diff --git a/src/renderer/components/svgs/icons/up-icon.component.tsx b/src/renderer/components/svgs/icons/up-icon.component.tsx new file mode 100644 index 000000000..f8bca33a5 --- /dev/null +++ b/src/renderer/components/svgs/icons/up-icon.component.tsx @@ -0,0 +1,15 @@ +import { CSSProperties } from "react"; + +export function UpIcon(props: Readonly<{ className?: string; style?: CSSProperties }>) { + return ( + <svg + className={props.className} + style={props.style} + xmlns="http://www.w3.org/2000/svg" + viewBox="0 -960 960 960" + fill="currentColor" + > + <path d="M440-647 244-451q-12 12-28 11.5T188-452q-11-12-11.5-28t11.5-28l264-264q6-6 13-8.5t15-2.5q8 0 15 2.5t13 8.5l264 264q11 11 11 27.5T772-452q-12 12-28.5 12T715-452L520-647v447q0 17-11.5 28.5T480-160q-17 0-28.5-11.5T440-200v-447Z" /> + </svg> + ); +} 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 index d5a6e5cbe..6f4c7316a 100644 --- a/src/renderer/components/version-viewer/slides/mods/mods-grid.component.tsx +++ b/src/renderer/components/version-viewer/slides/mods/mods-grid.component.tsx @@ -71,7 +71,7 @@ export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreIn <span className="z-10 sticky top-0 bg-inherit border-b-2 border-main-color-1 h-8 flex justify-start items-center py-1 pl-[3px] min-w-[50px]"> <BsmDropdownButton className="h-full aspect-square relative rounded-full bg-light-main-color-1 dark:bg-main-color-3" withBar={false} icon="three-dots" buttonClassName="!rounded-full !p-[2px] !bg-light-main-color-2 dark:!bg-main-color-2 hover:!bg-light-main-color-1 dark:hover:!bg-main-color-3" menuTranslationY="5px" items={[ { text: "pages.version-viewer.mods.mods-grid.header-bar.dropdown.import-mods", icon: "download", onClick: () => openModsDropZone?.() }, - { text: "pages.version-viewer.mods.mods-grid.header-bar.dropdown.compare-mods", icon: "null", onClick: () => openModsVersionCompare?.() }, + { text: "pages.version-viewer.mods.mods-grid.header-bar.dropdown.compare-mods", icon: "compare", onClick: () => openModsVersionCompare?.() }, { text: "pages.version-viewer.mods.mods-grid.header-bar.dropdown.unselect-all", icon: "cancel", onClick: () => unselectAllMods?.() }, { text: "pages.version-viewer.mods.mods-grid.header-bar.dropdown.uninstall-all", icon: "trash", onClick: () => uninstallAllMods?.() } ]} />