forked from OctoWoW/OctoLauncher
Fixed tweaks and mods, added localization, added antivirus walkthrough
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
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';
|
||||
@@ -11,9 +13,8 @@ import CheckboxInput from '../form/CheckboxInput';
|
||||
import IconSpinner from '../styled/IconSpinner';
|
||||
|
||||
const RowState = ({ row }: { row: ModRowStatus }) => {
|
||||
if (row.state === 'downloading' || row.state === 'installing')
|
||||
return <IconSpinner className="text-blueGray" />;
|
||||
if (row.state === 'uninstalling')
|
||||
const t = useT();
|
||||
if (['downloading', 'installing', 'uninstalling'].includes(row.state))
|
||||
return <IconSpinner className="text-blueGray" />;
|
||||
if (row.state === 'error')
|
||||
return (
|
||||
@@ -21,12 +22,17 @@ const RowState = ({ row }: { row: ModRowStatus }) => {
|
||||
<AlertTriangle size={14} className="text-red" />
|
||||
</span>
|
||||
);
|
||||
if (row.installedVersion && row.installedVersion !== row.latestVersion && !row.ignoreUpdates)
|
||||
return <span className="s1 text-pink">update</span>;
|
||||
if (
|
||||
row.installedVersion &&
|
||||
row.installedVersion !== row.latestVersion &&
|
||||
!row.ignoreUpdates
|
||||
)
|
||||
return <span className="s1 text-pink">{t('mods.update')}</span>;
|
||||
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();
|
||||
@@ -37,7 +43,9 @@ const ModRow = ({ row }: { row: ModRowStatus }) => {
|
||||
{row.recommended && (
|
||||
<Sparkles size={12} className="shrink-0 text-warmGreen" />
|
||||
)}
|
||||
<span className={cls(row.recommended && 'text-warmGreen')}>{row.name}</span>
|
||||
<span className={cls(row.recommended && 'text-warmGreen')}>
|
||||
{row.name}
|
||||
</span>
|
||||
<span className="s1 text-warmGreen">{row.latestVersion}</span>
|
||||
</div>
|
||||
<CheckboxInput
|
||||
@@ -59,13 +67,14 @@ const ModRow = ({ row }: { row: ModRowStatus }) => {
|
||||
<CheckboxInput
|
||||
value={row.ignoreUpdates}
|
||||
setValue={v => setIgnore.mutate({ id: row.id, ignore: v })}
|
||||
label={<span className="s1">Ignore updates</span>}
|
||||
label={<span className="s1">{t('mods.ignoreUpdates')}</span>}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ModsTab = () => {
|
||||
const t = useT();
|
||||
const [status, setStatus] = useState<ModsStatus>();
|
||||
api.mods.observe.useSubscription(undefined, {
|
||||
onData: setStatus
|
||||
@@ -82,40 +91,111 @@ const ModsTab = () => {
|
||||
|
||||
const scrollRef = useScrollHint<HTMLDivElement>();
|
||||
|
||||
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<HTMLDialogElement>(null);
|
||||
const [shownDepMessage, setShownDepMessage] = useState<string | null>(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 (
|
||||
<div className="tw-surface flex min-h-0 flex-grow flex-col gap-3">
|
||||
<div className="flex items-baseline justify-between">
|
||||
<h4 className="tw-color">CUSTOM MODS</h4>
|
||||
<h4 className="tw-color">{t('mods.title')}</h4>
|
||||
{status?.dirty && (
|
||||
<span className="s1 text-pink">unsaved changes</span>
|
||||
<span className="s1 text-pink">{t('mods.unsavedChanges')}</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="s1 text-blueGray">
|
||||
<span className="text-orange">⚠</span> Enabling custom mods may not provide
|
||||
any performance benefits or may even cause game crashes depending on your
|
||||
system. Please try disabling them if you experience any issues.
|
||||
<span className="text-orange">⚠</span> {t('mods.warning')}
|
||||
</p>
|
||||
{missingDeps.length > 0 && (
|
||||
<p className="s1 text-orange">
|
||||
⚠{' '}
|
||||
{t('mods.enableRequired', {
|
||||
mods: missingDeps.map(modName).join(', ')
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
<hr />
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="relative -m-4 -mt-0 grid flex-grow grid-cols-[auto_auto_1fr_auto] content-start items-center gap-x-4 gap-y-2 overflow-y-auto p-4 pt-0"
|
||||
>
|
||||
{status?.mods.map(row => <ModRow key={row.id} row={row} />)}
|
||||
{status?.mods.map(row => (
|
||||
<ModRow key={row.id} row={row} />
|
||||
))}
|
||||
</div>
|
||||
<hr />
|
||||
<div className="-mb-4 -mt-3 flex items-center gap-2 py-2">
|
||||
<p className="s1 flex-grow text-blueGray">
|
||||
<span className="text-warmGreen">Highlighted</span> mods are recommended.
|
||||
<span className="text-warmGreen">{t('mods.highlighted')}</span>{' '}
|
||||
{t('mods.highlightedRecommended')}
|
||||
</p>
|
||||
<TextButton
|
||||
type="button"
|
||||
loading={apply.isLoading || status?.state === 'busy'}
|
||||
onClick={() => apply.mutateAsync()}
|
||||
className={cls(status?.dirty && 'text-green')}
|
||||
onClick={onApply}
|
||||
className={cls('text-green', !showApply && 'invisible')}
|
||||
>
|
||||
Apply
|
||||
{t('mods.apply')}
|
||||
</TextButton>
|
||||
</div>
|
||||
{createPortal(
|
||||
<dialog
|
||||
ref={dialogRef}
|
||||
onClose={() => setShownDepMessage(null)}
|
||||
className="h-full w-full items-center justify-center bg-[transparent] backdrop:backdrop-blur-sm [&[open]]:flex"
|
||||
>
|
||||
{shownDepMessage && (
|
||||
<div className="tw-dialog !w-fit min-w-[360px] max-w-[460px] !gap-3">
|
||||
<h3 className="tw-color">{t('mods.cantApplyYet')}</h3>
|
||||
<p className="s1 whitespace-pre-line">{shownDepMessage}</p>
|
||||
<TextButton
|
||||
onClick={() => setShownDepMessage(null)}
|
||||
className="self-end text-green"
|
||||
>
|
||||
{t('mods.close')}
|
||||
</TextButton>
|
||||
</div>
|
||||
)}
|
||||
</dialog>,
|
||||
document.body
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user