forked from OctoWoW/OctoLauncher
OctoLauncher 1.3.1
Manifest-based CDN updater and mod manager for the OctoWoW 1.12.1 client: launcher-owned realmlist, torrent-backed content sync with bundled aria2c, antivirus and Defender exclusion handling, hardware-aware render distance, optional client tweaks and mods, and the in-launcher news feed.
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import type { ButtonHTMLAttributes } from 'react';
|
||||
import cls from 'classnames';
|
||||
import { type LucideIcon } from 'lucide-react';
|
||||
|
||||
import IconSpinner from './IconSpinner';
|
||||
|
||||
type Props = ButtonHTMLAttributes<HTMLButtonElement> & {
|
||||
primary?: boolean;
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
icon?: LucideIcon;
|
||||
};
|
||||
|
||||
const Button = ({
|
||||
primary,
|
||||
loading,
|
||||
disabled,
|
||||
icon: Icon,
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: Props) => (
|
||||
<button
|
||||
{...props}
|
||||
onClick={props.onClick}
|
||||
tabIndex={!!loading || !!disabled ? -1 : props.tabIndex}
|
||||
className={cls('tw-button', className, {
|
||||
'pointer-events-none': !!disabled || !!loading,
|
||||
'grayscale': disabled,
|
||||
'tw-button-primary': primary
|
||||
})}
|
||||
>
|
||||
<span className={cls('select-none', { 'ml-[-12px]': !!loading || !!Icon })}>
|
||||
{loading ? (
|
||||
<IconSpinner size={23} strokeWidth={1.5} />
|
||||
) : Icon ? (
|
||||
<Icon size={23} strokeWidth={1.5} />
|
||||
) : null}
|
||||
{children}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
|
||||
export default Button;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { X } from 'lucide-react';
|
||||
|
||||
import TextButton from './TextButton';
|
||||
|
||||
const CloseButton = ({ close }: { close: () => void }) => (
|
||||
<TextButton
|
||||
title="Close"
|
||||
icon={X}
|
||||
size={16}
|
||||
onClick={close}
|
||||
className="absolute right-1 top-1 text-blueGray hocus:text-red"
|
||||
/>
|
||||
);
|
||||
export default CloseButton;
|
||||
@@ -0,0 +1,72 @@
|
||||
type Run = { text: string; color?: string };
|
||||
|
||||
// Keep the WoW "|c" color runs, strip every other "|" escape (textures, links, pipes).
|
||||
const ESCAPE_RE =
|
||||
/\|\||\|c([0-9a-f]{8})|\|r|\|T[^|]*\|t|\|H[^|]*\|h|\|h|\|./gi;
|
||||
|
||||
const tokenize = (s: string): Run[] => {
|
||||
const runs: Run[] = [];
|
||||
let color: string | undefined;
|
||||
let buf = '';
|
||||
let i = 0;
|
||||
|
||||
const flush = () => {
|
||||
if (buf) runs.push({ text: buf, color });
|
||||
buf = '';
|
||||
};
|
||||
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = ESCAPE_RE.exec(s)) !== null) {
|
||||
buf += s.slice(i, m.index);
|
||||
i = ESCAPE_RE.lastIndex;
|
||||
|
||||
const tok = m[0];
|
||||
if (tok === '||') {
|
||||
buf += '|';
|
||||
} else if (m[1]) {
|
||||
// drop the leading alpha byte, keep RGB
|
||||
flush();
|
||||
color = `#${m[1].slice(2).toLowerCase()}`;
|
||||
} else if (tok.toLowerCase() === '|r') {
|
||||
flush();
|
||||
color = undefined;
|
||||
}
|
||||
}
|
||||
buf += s.slice(i);
|
||||
flush();
|
||||
return runs.filter(r => r.text.length > 0);
|
||||
};
|
||||
|
||||
export const stripColorCodes = (s: string) =>
|
||||
tokenize(s)
|
||||
.map(r => r.text)
|
||||
.join('');
|
||||
|
||||
export const ColoredText = ({
|
||||
children,
|
||||
className,
|
||||
style
|
||||
}: {
|
||||
children: string;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}) => {
|
||||
const runs = tokenize(children);
|
||||
return (
|
||||
<p className={className} style={style}>
|
||||
{runs.map((r, i) =>
|
||||
r.color ? (
|
||||
<span
|
||||
key={i}
|
||||
className="text-size-inherit text-inherit"
|
||||
style={{ color: r.color }}
|
||||
>
|
||||
{r.text}
|
||||
</span>
|
||||
) : (
|
||||
<span key={i}>{r.text}</span>
|
||||
)
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
import cls from 'classnames';
|
||||
import {
|
||||
useRef,
|
||||
type ReactElement,
|
||||
useEffect,
|
||||
useCallback,
|
||||
type FC,
|
||||
isValidElement
|
||||
} from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
type Props = {
|
||||
clickAway?: boolean;
|
||||
noBlur?: boolean;
|
||||
focusOnOpen?: boolean;
|
||||
afterClose?: () => void;
|
||||
dialog: ReactElement | ((close: () => void) => ReactElement);
|
||||
children: ReactElement | ((open: () => void) => ReactElement);
|
||||
};
|
||||
|
||||
const DialogButton = ({
|
||||
clickAway,
|
||||
noBlur,
|
||||
focusOnOpen,
|
||||
afterClose,
|
||||
dialog,
|
||||
children
|
||||
}: Props) => {
|
||||
const ref = useRef<HTMLDialogElement>(null);
|
||||
|
||||
const open = useCallback(() => {
|
||||
if (!ref.current) return;
|
||||
!focusOnOpen && (ref.current.inert = true);
|
||||
ref.current.showModal();
|
||||
!focusOnOpen && (ref.current.inert = false);
|
||||
}, [focusOnOpen]);
|
||||
|
||||
const close = useCallback(() => {
|
||||
ref.current?.close();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!clickAway) return;
|
||||
const callback = (e: MouseEvent) => e.target === ref.current && close();
|
||||
window.addEventListener('click', callback);
|
||||
return () => window.removeEventListener('click', callback);
|
||||
}, [clickAway, close]);
|
||||
|
||||
useEffect(() => {
|
||||
const callback = () => {
|
||||
afterClose?.();
|
||||
return (document.activeElement as HTMLElement)?.blur();
|
||||
};
|
||||
const r = ref.current;
|
||||
r?.addEventListener('close', callback);
|
||||
return () => r?.removeEventListener('close', callback);
|
||||
}, [afterClose]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{createPortal(
|
||||
<dialog
|
||||
ref={ref}
|
||||
onSubmit={e => e.stopPropagation()}
|
||||
className={cls(
|
||||
'h-full w-full items-center justify-center bg-[transparent] [&[open]]:flex',
|
||||
{ 'backdrop:backdrop-blur-md': !noBlur }
|
||||
)}
|
||||
>
|
||||
{typeof dialog === 'function' ? dialog(close) : dialog}
|
||||
</dialog>,
|
||||
document.body
|
||||
)}
|
||||
{typeof children === 'function' ? children(open) : children}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DialogButton;
|
||||
@@ -0,0 +1,8 @@
|
||||
import cls from 'classnames';
|
||||
import { Loader2, type LucideProps } from 'lucide-react';
|
||||
|
||||
const IconSpinner = ({ className, ...props }: LucideProps) => (
|
||||
<Loader2 {...props} className={cls(className, 'animate-spin')} />
|
||||
);
|
||||
|
||||
export default IconSpinner;
|
||||
@@ -0,0 +1,66 @@
|
||||
import cls from 'classnames';
|
||||
import { type LucideIcon } from 'lucide-react';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
import IconSpinner from './IconSpinner';
|
||||
|
||||
type Props = {
|
||||
active?: boolean;
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
size?: number;
|
||||
className?: cls.Value;
|
||||
style?: React.CSSProperties;
|
||||
} & (
|
||||
| { type: 'submit'; onClick?: never }
|
||||
| { type?: never; onClick: () => void }
|
||||
) &
|
||||
(
|
||||
| { children: ReactNode; icon?: LucideIcon; title?: never }
|
||||
| { children?: never; icon: LucideIcon; title: string }
|
||||
);
|
||||
|
||||
const TextButton = ({
|
||||
title,
|
||||
type,
|
||||
active,
|
||||
loading,
|
||||
disabled,
|
||||
icon: Icon,
|
||||
size,
|
||||
onClick,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: Props) => (
|
||||
<button
|
||||
title={title ?? (typeof children === 'string' ? children : undefined)}
|
||||
type={type ?? 'button'}
|
||||
onClick={onClick}
|
||||
tabIndex={!!loading || !!disabled ? -1 : undefined}
|
||||
className={cls(
|
||||
'flex cursor-pointer items-center gap-2 border-0 p-2',
|
||||
className,
|
||||
{
|
||||
'tw-color drop-shadow-[0px_0px_10px_white]':
|
||||
active && !loading && !disabled,
|
||||
'pointer-events-none text-gray': !!loading || !!disabled,
|
||||
'tw-hocus': !loading && !disabled
|
||||
}
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{loading ? (
|
||||
<IconSpinner size={size ?? 24} strokeWidth={1.5} />
|
||||
) : (
|
||||
Icon && <Icon size={size} className="shrink-0" />
|
||||
)}
|
||||
{children && (
|
||||
<span className="cursor-pointer select-none tracking-wide text-inherit [font-size:_inherit]">
|
||||
{children}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
|
||||
export default TextButton;
|
||||
Reference in New Issue
Block a user