import { useEffect, useRef, useState } from 'react';
import { ExternalLink, AlertTriangle, Sparkles } from 'lucide-react';
import { createPortal } from 'react-dom';
import cls from 'classnames';
import { api } from '~renderer/utils/api';
import useScrollHint from '~renderer/utils/useScrollHint';
import { useT } from '~renderer/i18n';
import { type ModRowStatus, type ModsStatus } from '~main/types';
import TextButton from '../styled/TextButton';
import CheckboxInput from '../form/CheckboxInput';
import IconSpinner from '../styled/IconSpinner';
const RowState = ({ row }: { row: ModRowStatus }) => {
const t = useT();
if (['downloading', 'installing', 'uninstalling'].includes(row.state))
return ;
if (row.state === 'error')
return (
);
if (
row.installedVersion &&
row.installedVersion !== row.latestVersion &&
!row.ignoreUpdates
)
return {t('mods.update')};
return null;
};
const ModRow = ({ row }: { row: ModRowStatus }) => {
const t = useT();
const toggle = api.mods.toggle.useMutation();
const setIgnore = api.mods.setIgnoreUpdates.useMutation();
const openLink = api.general.openLink.useMutation();
return (
<>
{row.recommended && (
)}
{row.name}
{row.latestVersion}
toggle.mutate({ id: row.id, enabled: v })}
className="justify-self-center"
/>
{row.description}
openLink.mutateAsync(row.repoUrl)}
className="!p-0 text-blueGray"
/>
setIgnore.mutate({ id: row.id, ignore: v })}
label={{t('mods.ignoreUpdates')}}
/>
>
);
};
const ModsTab = () => {
const t = useT();
const [status, setStatus] = useState();
api.mods.observe.useSubscription(undefined, {
onData: setStatus
});
const list = api.mods.list.useQuery(undefined, {
refetchOnMount: true
});
useEffect(() => {
if (!status && list.data) setStatus(list.data);
}, [list.data, status]);
const apply = api.mods.applyAll.useMutation();
const scrollRef = useScrollHint();
const mods = status?.mods ?? [];
const enabledIds = new Set(mods.filter(m => m.enabled).map(m => m.id));
const modName = (id: string) => mods.find(m => m.id === id)?.name ?? id;
const missingDeps = [
...new Set(
mods
.filter(m => m.enabled)
.flatMap(m => m.requires.filter(d => !enabledIds.has(d)))
)
];
const pendingDepMessage = missingDeps
.map(dep => {
const requiredBy = mods
.filter(m => m.enabled && m.requires.includes(dep))
.map(m => m.name)
.join(', ');
return t('mods.depRequired', { mod: modName(dep), requiredBy });
})
.join('\n');
const dialogRef = useRef(null);
const [shownDepMessage, setShownDepMessage] = useState(null);
useEffect(() => {
if (shownDepMessage) {
dialogRef.current?.showModal();
(document.activeElement as HTMLElement | null)?.blur();
} else dialogRef.current?.close();
}, [shownDepMessage]);
const onApply = () => {
if (missingDeps.length) {
setShownDepMessage(pendingDepMessage);
return;
}
apply.mutateAsync();
};
const showApply =
!!status?.dirty || apply.isLoading || status?.state === 'busy';
return (
{t('mods.title')}
{status?.dirty && (
{t('mods.unsavedChanges')}
)}
⚠ {t('mods.warning')}
{missingDeps.length > 0 && (
⚠{' '}
{t('mods.enableRequired', {
mods: missingDeps.map(modName).join(', ')
})}
)}
{status?.mods.map(row => (
))}
{t('mods.highlighted')}{' '}
{t('mods.highlightedRecommended')}
{t('mods.apply')}
{createPortal(
,
document.body
)}
);
};
export default ModsTab;