206 lines
7.7 KiB
TypeScript
206 lines
7.7 KiB
TypeScript
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<string | null> {
|
|
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<string>, 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<ModInfo> {
|
|
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<ModpackFile>;
|
|
installedPackages?: Array<PackageModel>;
|
|
}
|
|
|
|
export const ModCardContext = createContext<ModCardContextApi | null>(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<ModInfo>({ 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 (
|
|
<div className={twMerge('rounded-lg m-1 break-inside-avoid flex bg-component-bg border border-gray-100/5 p-2 gap-2 outline-2 outline-component-bg', useVerticalGridLayout && useGridLayout ? '' : 'justify-between', useGridLayout ? 'flex-col' : 'flex-row', isSelected ? 'outline-brand bg-brand/20' : '')} onClick={handleOnClick}>
|
|
<div className="flex flex-row gap-2">
|
|
<div className={twMerge('flex flex-col items-center justify-center', useGridLayout ? (useVerticalGridLayout ? 'size-14' : 'size-20') : 'size-18')}>
|
|
<div className={twMerge('rounded-lg bg-component-bg-disabled border border-gray-100/5', useGridLayout ? (useVerticalGridLayout ? 'size-14' : 'size-20') : 'size-18')}>
|
|
<img className={twMerge('rounded-lg', modMetadata.iconURL === null ? 'hidden' : '', useGridLayout ? (useVerticalGridLayout ? 'size-14' : 'size-20') : 'size-18')} src={modMetadata.iconURL ?? MissingLogo} />
|
|
</div>
|
|
</div>
|
|
<div className={twMerge('flex flex-col', useVerticalGridLayout && useGridLayout ? 'justify-center' : '')}>
|
|
<div className="flex flex-row flex-wrap">
|
|
<p className={twMerge('text-fg-primary break-words', useGridLayout ? 'text-lg' : 'text-xl', useGridLayout && !useVerticalGridLayout ? 'max-w-3/5' : '')}>{modMetadata.name}</p>
|
|
{useVerticalGridLayout !== true && <ModTag cluster={cluster} modData={modMetadata} />}
|
|
</div>
|
|
|
|
<p className={twMerge(modMetadata.description === null ? 'text-fg-secondary/25' : 'text-fg-secondary', useGridLayout ? 'text-sm' : 'text-base')}>
|
|
by
|
|
{' '}
|
|
<span className="font-semibold">{modMetadata.author ?? 'UNKNOWN'}</span>
|
|
</p>
|
|
{useVerticalGridLayout !== true && <p className={twMerge('font-normal', modMetadata.description === null ? 'text-fg-secondary/25' : 'text-fg-secondary', useGridLayout ? 'text-sm' : 'text-base')}>{modMetadata.description ?? 'No Description'}</p>}
|
|
</div>
|
|
</div>
|
|
{useVerticalGridLayout === true && modMetadata.description !== null && <p className={twMerge('font-normal text-fg-secondary', useGridLayout ? 'text-sm' : 'text-base')}>{modMetadata.description}</p>}
|
|
</div>
|
|
);
|
|
}
|