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( setShownDepMessage(null)} className="h-full w-full items-center justify-center bg-[transparent] backdrop:backdrop-blur-sm [&[open]]:flex" > {shownDepMessage && (

{t('mods.cantApplyYet')}

{shownDepMessage}

setShownDepMessage(null)} className="self-end text-green" > {t('mods.close')}
)}
, document.body )}
); }; export default ModsTab;