Initial commit

This commit is contained in:
2026-05-08 00:00:00 +00:00
commit 530ec7a144
110 changed files with 18537 additions and 0 deletions
@@ -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;