import type { ClusterModel, ModpackFile, ModpackFileKind, PackageModel } from '@/bindings.gen'; import MissingLogo from '@/assets/misc/missingLogo.svg'; import { ModTag } from '@/components'; import { useSettings } from '@/hooks/useSettings'; import { bindings } from '@/main'; import { useCommandMut } from '@onelauncher/common'; import { useQueryClient } from '@tanstack/react-query'; import { createContext, useContext, useEffect, useMemo, useState } from 'react'; import { twMerge } from 'tailwind-merge'; export interface ModInfo { name: string; description: string | null; author: string | null; iconURL: string | null; url: string | null; managed: boolean; packageSlug: string | null; } async function getModAuthor(kind: ModpackFileKind, useVerticalGridLayout?: boolean): Promise { if ('External' in kind) return null; if (!('Managed' in kind)) return null; const authors = await bindings.core.getUsersFromAuthor(kind.Managed[0].provider, kind.Managed[0].author); return parseAuthors(authors.map(author => author.username), useVerticalGridLayout); } function parseAuthors(usernames: Array, useVerticalGridLayout?: boolean): string | null { if (usernames.length === 0) return null; if (useVerticalGridLayout) { let visibleCount = usernames.length; while (visibleCount >= 1) { const hiddenUsers = usernames.length - visibleCount; let string = usernames.slice(0, visibleCount).join(', '); if (hiddenUsers > 0) string += ` and ${hiddenUsers} more`; if (string.length <= 21) return string; visibleCount--; } return `${usernames[0]} and ${usernames.length - 1} more`; } return usernames.join(', '); } export function getModMetaDataName(file: ModpackFile): string { if ('External' in file.kind) return file.overrides?.name ?? file.kind.External.name.replaceAll('.jar', ''); if ('Managed' in file.kind) return file.overrides?.name ?? file.kind.Managed[0].name; else return 'UNKNOWN'; } async function getModMetaData(file: ModpackFile, useVerticalGridLayout?: boolean): Promise { if ('External' in file.kind) return { name: getModMetaDataName(file), description: file.overrides?.description ?? null, author: parseAuthors(file.overrides?.authors ?? [], useVerticalGridLayout), iconURL: file.overrides?.icon ?? null, url: null, managed: false, packageSlug: null, }; return { name: getModMetaDataName(file), description: file.overrides?.description ?? file.kind.Managed[0].short_desc, author: (file.overrides?.authors ?? []).length > 1 ? parseAuthors(file.overrides?.authors ?? []) : await getModAuthor(file.kind, useVerticalGridLayout), iconURL: file.overrides?.icon ?? file.kind.Managed[0].icon_url, url: `https://modrinth.com/project/${file.kind.Managed[0].slug}`, managed: true, packageSlug: file.kind.Managed[0].id, }; } interface ModCardProps { file: ModpackFile; cluster: ClusterModel; } export type onClickOnMod = (file: ModpackFile) => void; export interface ModCardContextApi { enableClickToDownload?: boolean; onClickOnMod?: onClickOnMod; useVerticalGridLayout?: boolean; mods?: Array; installedPackages?: Array; } export const ModCardContext = createContext(null); export function useModCardContext() { const ctx = useContext(ModCardContext); if (!ctx) throw new Error('useModCardContext must be used within a ModCardContext.Provider'); return ctx; } export function ModCard({ file, cluster }: ModCardProps) { const { enableClickToDownload, onClickOnMod, useVerticalGridLayout, mods, installedPackages } = useModCardContext(); const queryClient = useQueryClient(); const [modMetadata, setModMetadata] = useState({ name: 'Loading...', description: null, author: null, iconURL: null, url: null, managed: false, packageSlug: null }); useEffect(() => { (async () => setModMetadata(await getModMetaData(file, useVerticalGridLayout)))(); }, [file, useVerticalGridLayout]); const kind = file.kind; const isInstalled = useMemo(() => { if (installedPackages) if ('Managed' in kind) { const [pkg, _] = kind.Managed; return installedPackages.some(p => p.package_id === pkg.id && p.provider === pkg.provider); } else { return installedPackages.some(p => p.hash === kind.External.sha1); } return mods?.includes(file) ?? false; }, [installedPackages, mods, file]); const [isSelected, setSelected] = useState(isInstalled); useEffect(() => { setSelected(isInstalled); }, [isInstalled]); const download = useCommandMut(async () => { if ('Managed' in kind) { const [pkg, version] = kind.Managed; await bindings.core.downloadPackage(pkg.provider, pkg.id, version.version_id, cluster.id, null); } else { await bindings.core.downloadExternalPackage(kind.External, cluster.id, null, null); } }); const remove = useCommandMut(async () => { let hash: string | undefined; if ('Managed' in kind) { const [_, version] = kind.Managed; const primary = version.files.find(f => f.primary) ?? version.files[0]; hash = primary?.sha1; } else { hash = kind.External.sha1; } if (hash) await bindings.core.removePackage(cluster.id, hash); }); const handleOnClick = () => { if (enableClickToDownload) (async () => { if (isInstalled) await remove.mutateAsync(); else await download.mutateAsync(); await queryClient.invalidateQueries({ queryKey: ['getLinkedPackages', cluster.id] }); })(); else if (onClickOnMod) onClickOnMod(file); }; const { setting } = useSettings(); let useGridLayout = setting('mod_list_use_grid'); if (useVerticalGridLayout) useGridLayout = true; return (

{modMetadata.name}

{useVerticalGridLayout !== true && }

by {' '} {modMetadata.author ?? 'UNKNOWN'}

{useVerticalGridLayout !== true &&

{modMetadata.description ?? 'No Description'}

}
{useVerticalGridLayout === true && modMetadata.description !== null &&

{modMetadata.description}

}
); }