Initial commit
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
import {
|
||||
AlertOctagon,
|
||||
AlertTriangle,
|
||||
DownloadCloud,
|
||||
Github,
|
||||
HelpCircle,
|
||||
Trash2
|
||||
} from 'lucide-react';
|
||||
import cls from 'classnames';
|
||||
|
||||
import { type AddonData } from '~main/types';
|
||||
import { api } from '~renderer/utils/api';
|
||||
import TextButton from '~renderer/components/styled/TextButton';
|
||||
import { ColoredText } from '~renderer/components/styled/ColoredText';
|
||||
import IconSpinner from '~renderer/components/styled/IconSpinner';
|
||||
import DialogButton from '~renderer/components/styled/DialogButton';
|
||||
import { isNotUndef } from '~common/utils';
|
||||
import CloseButton from '~renderer/components/styled/CloseButton';
|
||||
|
||||
import AddonDetail from './AddonDetail';
|
||||
|
||||
export type Dependencies = {
|
||||
[folder: string]: 'installed' | 'available' | string;
|
||||
};
|
||||
|
||||
export type LocalDependencies = {
|
||||
name: string;
|
||||
optional: boolean;
|
||||
status: 'installed' | 'available' | 'missing' | string;
|
||||
}[];
|
||||
|
||||
type Props = Omit<AddonData, 'ref'> & {
|
||||
row: number;
|
||||
dependencies: Dependencies;
|
||||
gitRef?: string;
|
||||
};
|
||||
|
||||
const toRepoUrl = (git?: string) =>
|
||||
git ? git.replace(/\.git$/, '') : undefined;
|
||||
|
||||
const AddonListItem = ({ row, dependencies, ...addon }: Props) => {
|
||||
const update = api.addons.update.useMutation();
|
||||
const remove = api.addons.remove.useMutation();
|
||||
const openLink = api.general.openLink.useMutation();
|
||||
const repoUrl = toRepoUrl(addon.git);
|
||||
|
||||
const localDependencies: LocalDependencies = [
|
||||
...(addon.toc?.Dependencies?.split(', ')?.map(d => [d, false] as const) ??
|
||||
[]),
|
||||
...(addon.toc?.OptionalDeps?.split(', ')?.map(d => [d, true] as const) ??
|
||||
[])
|
||||
].map<LocalDependencies[number]>(([d, optional]) => ({
|
||||
name: d,
|
||||
optional,
|
||||
status: dependencies[d] ?? 'missing'
|
||||
}));
|
||||
|
||||
const warnings = [
|
||||
addon.toc && addon.toc?.Interface !== '11200'
|
||||
? {
|
||||
full: `This addon seems to be made for different game version (${addon.toc?.Interface}) and it may not function correctly`,
|
||||
short: 'Incorrect version'
|
||||
}
|
||||
: undefined,
|
||||
localDependencies.some(d => d.status !== 'installed' && !d.optional)
|
||||
? {
|
||||
full: `This addon has missing dependencies: ${localDependencies
|
||||
.filter(d => d.status !== 'installed' && !d.optional)
|
||||
.map(d => d.name)
|
||||
.join(', ')}`,
|
||||
short: 'Missing dependencies'
|
||||
}
|
||||
: undefined
|
||||
].filter(isNotUndef);
|
||||
|
||||
return (
|
||||
<div className="contents hover-row:bg-purple/30">
|
||||
<div
|
||||
className="-mx-4 h-full w-[200%]"
|
||||
style={{ gridRow: row + 1, gridColumn: '1/4' }}
|
||||
/>
|
||||
{addon.status === 'fetching' ? (
|
||||
<IconSpinner
|
||||
className="text-blueGray"
|
||||
size={18}
|
||||
style={{ gridRow: row + 1, gridColumn: 1 }}
|
||||
/>
|
||||
) : (
|
||||
<DialogButton
|
||||
clickAway
|
||||
dialog={close => (
|
||||
<AddonDetail
|
||||
close={close}
|
||||
warnings={warnings}
|
||||
dependencies={localDependencies}
|
||||
{...addon}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
{open => (
|
||||
<TextButton
|
||||
icon={
|
||||
addon.status === 'invalid'
|
||||
? AlertOctagon
|
||||
: warnings.length
|
||||
? AlertTriangle
|
||||
: HelpCircle
|
||||
}
|
||||
onClick={open}
|
||||
title="Details"
|
||||
size={18}
|
||||
className={cls(
|
||||
'-mx-2',
|
||||
addon.status === 'invalid'
|
||||
? 'text-red'
|
||||
: warnings.length
|
||||
? 'text-yellow'
|
||||
: 'text-blueGray'
|
||||
)}
|
||||
style={{ gridRow: row + 1, gridColumn: 1 }}
|
||||
/>
|
||||
)}
|
||||
</DialogButton>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="-ml-2 flex items-center gap-1 whitespace-nowrap"
|
||||
style={{ gridRow: row + 1, gridColumn: 2 }}
|
||||
>
|
||||
<ColoredText>{addon.toc?.Title ?? addon.folder}</ColoredText>
|
||||
{repoUrl && (
|
||||
<TextButton
|
||||
icon={Github}
|
||||
size={14}
|
||||
title={`Open ${repoUrl} on GitHub`}
|
||||
onClick={() => openLink.mutateAsync(repoUrl)}
|
||||
className="!p-1 text-blueGray/60 hocus:text-pink"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ColoredText
|
||||
className="s1 py-1 text-blueGray"
|
||||
style={{ gridRow: row + 1, gridColumn: 3 }}
|
||||
>
|
||||
{addon.toc?.Notes ?? addon.description ?? ''}
|
||||
</ColoredText>
|
||||
|
||||
<div
|
||||
className="-m-2 flex items-center justify-end gap-2"
|
||||
style={{ gridRow: row + 1, gridColumn: 4 }}
|
||||
>
|
||||
{addon.status === 'downloading' ? (
|
||||
<>
|
||||
<p className="s1 text-blueGray">{addon.progress}</p>
|
||||
<IconSpinner size={18} className="text-blueGray" />
|
||||
</>
|
||||
) : addon.status === 'invalid' ? (
|
||||
<p className="s1 text-red">{addon.error}</p>
|
||||
) : warnings.length ? (
|
||||
<p className="s1 text-yellow">{warnings[0].short}</p>
|
||||
) : (
|
||||
<p className="s1 text-blueGray/50">
|
||||
{addon.status === 'upToDate'
|
||||
? 'Up to date'
|
||||
: !addon.git
|
||||
? 'Not versioned'
|
||||
: ''}
|
||||
</p>
|
||||
)}
|
||||
{addon.status === 'outOfDate' && (
|
||||
<TextButton
|
||||
onClick={() => update.mutateAsync({ toUpdate: [addon.folder] })}
|
||||
className="s1 -mx-2 justify-self-end"
|
||||
>
|
||||
Update
|
||||
</TextButton>
|
||||
)}
|
||||
{addon.status === 'available' ? (
|
||||
<TextButton
|
||||
// TODO: With dependencies checkbox
|
||||
onClick={() => update.mutateAsync({ toUpdate: [addon.folder] })}
|
||||
className="text-warmGreen"
|
||||
icon={DownloadCloud}
|
||||
size={18}
|
||||
title="Download"
|
||||
/>
|
||||
) : (
|
||||
<DialogButton
|
||||
clickAway
|
||||
dialog={close => (
|
||||
<div className="tw-dialog">
|
||||
<CloseButton close={close} />
|
||||
<h4 className="tw-color">Are you sure?</h4>
|
||||
<hr />
|
||||
<p className="text-blueGray">
|
||||
Are you sure you want to delete <span>{addon.folder}</span>{' '}
|
||||
addon?
|
||||
</p>
|
||||
<p className="text-blueGray">
|
||||
This will delete all files in the addon folder.
|
||||
</p>
|
||||
<TextButton
|
||||
icon={Trash2}
|
||||
onClick={async () => {
|
||||
await remove.mutateAsync({ toDelete: [addon.folder] });
|
||||
close();
|
||||
}}
|
||||
disabled={remove.isLoading}
|
||||
className="self-end text-red"
|
||||
>
|
||||
Delete
|
||||
</TextButton>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
{open => (
|
||||
<TextButton
|
||||
onClick={open}
|
||||
className="text-red/50"
|
||||
icon={Trash2}
|
||||
size={18}
|
||||
title="Remove"
|
||||
/>
|
||||
)}
|
||||
</DialogButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddonListItem;
|
||||
Reference in New Issue
Block a user