Lint functional hooks

This commit is contained in:
Zerebos 2023-03-19 22:23:11 -04:00
parent c2d1e4505f
commit 7b58be079d
26 changed files with 153 additions and 129 deletions

View File

@ -21,6 +21,7 @@
"dotenv": "^16.0.3",
"eslint": "^8.23.0",
"eslint-plugin-react": "^7.31.6",
"eslint-plugin-react-hooks": "^4.6.0",
"mocha": "^10.0.0",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0"

View File

@ -8,6 +8,7 @@ importers:
dotenv: ^16.0.3
eslint: ^8.23.0
eslint-plugin-react: ^7.31.6
eslint-plugin-react-hooks: ^4.6.0
mocha: ^10.0.0
webpack: ^5.74.0
webpack-cli: ^4.10.0
@ -16,6 +17,7 @@ importers:
dotenv: 16.0.3
eslint: 8.23.0
eslint-plugin-react: 7.31.6_eslint@8.23.0
eslint-plugin-react-hooks: 4.6.0_eslint@8.23.0
mocha: 10.0.0
webpack: 5.74.0_webpack-cli@4.10.0
webpack-cli: 4.10.0_webpack@5.74.0
@ -2343,6 +2345,15 @@ packages:
engines: {node: '>=10'}
dev: true
/eslint-plugin-react-hooks/4.6.0_eslint@8.23.0:
resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==}
engines: {node: '>=10'}
peerDependencies:
eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0
dependencies:
eslint: 8.23.0
dev: true
/eslint-plugin-react/7.31.6_eslint@8.23.0:
resolution: {integrity: sha512-CXu4eu28sb8Sd2+cyUYsJVyDvpTlaXPG+bOzzpS9IzZKtye96AYX3ZmHQ6ayn/OAIQ/ufDJP8ElPWd63Pepn9w==}
engines: {node: '>=4'}

View File

@ -1,7 +1,11 @@
{
"extends": ["plugin:react/recommended"],
"extends": [
"plugin:react/recommended",
"plugin:react-hooks/recommended"
],
"plugins": [
"react"
"react",
"react-hooks"
],
"settings": {
"react": {

View File

@ -60,7 +60,7 @@ export default function AddonErrorModal({pluginErrors, themeErrors}) {
}, [pluginErrors, themeErrors]);
const [tabId, setTab] = useState(tabs[0].id);
const switchToTab = useCallback((id) => setTab(id), [tabId]);
const switchToTab = useCallback((id) => setTab(id), []);
const selectedTab = tabs.find(e => e.id === tabId);
return <>

View File

@ -3,15 +3,15 @@ import {React} from "modules";
const {useState, useCallback} = React;
export default function Checkbox(props) {
const [checked, setChecked] = useState(props.checked);
export default function Checkbox({checked: initialState, text, onChange: notifyParent}) {
const [checked, setChecked] = useState(initialState);
const onClick = useCallback(() => {
props?.onChange(!checked);
notifyParent?.(!checked);
setChecked(!checked);
}, [checked]);
}, [notifyParent, checked]);
return <div className="checkbox-item">
<div className="checkbox-label label-JWQiNe da-label">{props.text}</div>
<div className="checkbox-label label-JWQiNe da-label">{text}</div>
<div className="checkbox-wrapper checkbox-3kaeSU da-checkbox checkbox-3EVISJ da-checkbox" onClick={onClick}>
<div className="checkbox-inner checkboxInner-3yjcPe da-checkboxInner">
<input className="checkbox checkboxElement-1qV33p da-checkboxElement" checked={checked} type="checkbox" />

View File

@ -25,27 +25,27 @@ export default forwardRef(function CssEditor({css, openNative, update, save, onC
set value(newValue) {editorRef.current.setValue(newValue);},
get hasUnsavedChanges() {return hasUnsavedChanges;}
};
}, []);
}, [hasUnsavedChanges]);
useEffect(() => {
Events.on("customcss-updated", updateEditor);
return () => Events.off("customcss-updated", updateEditor);
});
}, [updateEditor]);
const toggleLiveUpdate = useCallback((checked) => Settings.set("settings", "customcss", "liveUpdate", checked), []);
const updateCss = useCallback((event, newCSS) => update?.(newCSS), []);
const popoutNative = useCallback(() => openNative?.(), []);
const popout = useCallback((event, currentCSS) => openDetached?.(currentCSS), []);
const updateCss = useCallback((event, newCSS) => update?.(newCSS), [update]);
const popoutNative = useCallback(() => openNative?.(), [openNative]);
const popout = useCallback((event, currentCSS) => openDetached?.(currentCSS), [openDetached]);
const onChange = useCallback((newCSS) => {
notifyParent?.(newCSS);
setUnsaved(true);
}, []);
}, [notifyParent]);
const saveCss = useCallback((event, newCSS) => {
save?.(newCSS);
setUnsaved(false);
}, []);
}, [save]);
return <Editor

View File

@ -34,7 +34,7 @@ export default forwardRef(function CodeEditor({value, language: requestedLang =
const [theme, setTheme] = useState(() => ThemeStore?.theme === "light" ? "vs" : "vs-dark");
const [editor, setEditor] = useState(null);
const [bindings, setBindings] = useState([]);
const [, setBindings] = useState([]);
const onThemeChange = useCallback(() => {
const newTheme = ThemeStore?.theme === "light" ? "vs" : "vs-dark";
@ -45,7 +45,7 @@ export default forwardRef(function CodeEditor({value, language: requestedLang =
const onChange = useCallback(() => {
notifyParent?.(editor?.getValue());
}, [editor]);
}, [editor, notifyParent]);
const resize = useCallback(() => editor.layout(), [editor]);
const showSettings = useCallback(() => editor.keyBinding.$defaultHandler.commands.showSettingsMenu.exec(editor), [editor]);
@ -56,17 +56,20 @@ export default forwardRef(function CodeEditor({value, language: requestedLang =
get value() {return editor.getValue();},
set value(newValue) {editor.setValue(newValue);}
};
}, [editor]);
}, [editor, resize, showSettings]);
useEffect(() => {
setBindings([...bindings, editor?.onDidChangeModelContent(onChange)]);
setBindings(bins => [...bins, editor?.onDidChangeModelContent(onChange)]);
return () => {
for (const binding of bindings) binding?.dispose();
setBindings([]);
setBindings(bins => {
for (const binding of bins) binding?.dispose();
return [];
});
};
}, [editor]);
}, [editor, onChange]);
useEffect(() => {
let toDispose = null;
if (window.monaco?.editor) {
const monacoEditor = window.monaco.editor.create(document.getElementById(id), {
value: value,
@ -84,6 +87,7 @@ export default forwardRef(function CodeEditor({value, language: requestedLang =
renderWhitespace: Settings.get("settings", "editor", "renderWhitespace")
});
toDispose = monacoEditor;
setEditor(monacoEditor);
}
else {
@ -91,28 +95,35 @@ export default forwardRef(function CodeEditor({value, language: requestedLang =
const textarea = document.createElement("textarea");
textarea.className = "bd-fallback-editor";
textarea.value = value;
textarea.onchange = (e) => onChange(e.target.value);
textarea.oninput = (e) => onChange(e.target.value);
setEditor({
dispose: () => textarea.remove(),
getValue: () => textarea.value,
setValue: (val) => textarea.value = val,
layout: () => {},
onDidChangeModelContent: (cb) => {
textarea.onchange = cb;
textarea.oninput = cb;
}
});
document.getElementById(id).appendChild(textarea);
}
return () => {
toDispose?.dispose?.();
};
}, [id, language, theme, value]);
useEffect(() => {
ThemeStore?.addChangeListener?.(onThemeChange);
window.addEventListener("resize", resize);
return () => {
window.removeEventListener("resize", resize);
ThemeStore?.removeChangeListener?.(onThemeChange);
editor?.dispose();
};
}, []);
}, [onThemeChange, resize]);
if (editor && editor.layout) editor.layout();

View File

@ -12,15 +12,10 @@ function minY() {
}
export default function FloatingWindowContainer() {
useEffect(() => {
Events.on("open-window", open);
return () => Events.off("open-window", open);
}, []);
const [windows, setWindows] = useState([]);
const open = useCallback(window => {
setWindows([...windows, window]);
}, [windows]);
setWindows(wins => [...wins, window]);
}, []);
const close = useCallback(id => {
setWindows(windows.filter(w => {
if (w.id === id && w.onClose) w.onClose();
@ -28,6 +23,11 @@ export default function FloatingWindowContainer() {
}));
}, [windows]);
useEffect(() => {
Events.on("open-window", open);
return () => Events.off("open-window", open);
}, [open]);
return windows.map(window =>
<FloatingWindow {...window} close={() => close(window.id)} minY={minY()} key={window.id}>
{window.children}

View File

@ -5,7 +5,7 @@ import CloseButton from "../icons/close";
import MaximizeIcon from "../icons/fullscreen";
import Modals from "../modals";
const {useState, useCallback, useEffect, useRef, useMemo} = React;
const {useState, useCallback, useEffect, useRef} = React;
function confirmClose(confirmationText) {
@ -19,18 +19,13 @@ function confirmClose(confirmationText) {
});
}
export default function FloatingWindow(props) {
export default function FloatingWindow({id, title, resizable, children, className, center, top: initialTop, left: initialLeft, width: initialWidth, height: initialHeight, minX = 0, minY = 0, maxX = Screen.width, maxY = Screen.height, onResize, close: doClose, confirmClose: doConfirmClose, confirmationText}) {
const [modalOpen, setOpen] = useState(false);
const [isDragging, setDragging] = useState(false);
const [position, setPosition] = useState({x: props.center ? (Screen.width / 2) - (props.width / 2) : props.left, y: props.center ? (Screen.height / 2) - (props.height / 2) : props.top});
const [position, setPosition] = useState({x: center ? (Screen.width / 2) - (initialWidth / 2) : initialLeft, y: center ? (Screen.height / 2) - (initialHeight / 2) : initialTop});
const [offset, setOffset] = useState({x: 0, y: 0});
const [size, setSize] = useState({width: 0, height: 0});
const minX = useMemo(() => props.minX || 0);
const maxX = useMemo(() => props.maxX || Screen.width);
const minY = useMemo(() => props.minY || 0);
const maxY = useMemo(() => props.maxY || Screen.height);
const titlebar = useRef(null);
const window = useRef(null);
@ -51,7 +46,7 @@ export default function FloatingWindow(props) {
if (newLeft + size.width >= maxX) newLeft = maxX - size.width;
setPosition({x: newLeft, y: newTop});
}, [window, offset, size, isDragging]);
}, [offset, size, isDragging, minX, minY, maxX, maxY]);
const onDragStart = useCallback((e) => {
@ -66,7 +61,7 @@ export default function FloatingWindow(props) {
const width = window.current.offsetWidth;
const height = window.current.offsetHeight;
if (width != size.width || height != size.height) {
if (props.onResize) props.onResize();
if (onResize) onResize();
const left = parseInt(window.current.style.left);
const top = parseInt(window.current.style.top);
if (left + width >= maxX) window.current.style.width = (maxX - left) + "px";
@ -74,20 +69,22 @@ export default function FloatingWindow(props) {
}
setSize({width, height});
}, [window, size, onDrag]);
}, [window, size, maxX, maxY, onResize]);
useEffect(() => {
window.current.addEventListener("mousedown", onResizeStart, false);
titlebar.current.addEventListener("mousedown", onDragStart, false);
const winRef = window.current;
const titleRef = titlebar.current;
winRef.addEventListener("mousedown", onResizeStart, false);
titleRef.addEventListener("mousedown", onDragStart, false);
document.addEventListener("mouseup", onDragStop, false);
document.addEventListener("mousemove", onDrag, true);
return () => {
document.removeEventListener("mouseup", onDragStop, false);
document.removeEventListener("mousemove", onDrag, true);
window?.current?.removeEventListener("mousedown", onResizeStart, false);
titlebar?.current?.removeEventListener("mousedown", onDragStart, false);
winRef.removeEventListener("mousedown", onResizeStart, false);
titleRef.removeEventListener("mousedown", onDragStart, false);
};
}, [titlebar, window, onDragStart, onDragStop, onDrag, onResizeStart]);
@ -95,7 +92,7 @@ export default function FloatingWindow(props) {
const maximize = useCallback(() => {
window.current.style.width = "100%";
window.current.style.height = "100%";
if (props.onResize) props.onResize();
if (onResize) onResize();
const width = window.current.offsetWidth;
const height = window.current.offsetHeight;
@ -123,27 +120,27 @@ export default function FloatingWindow(props) {
window.current.style.left = minX + "px";
window.current.style.height = (width - difference) + "px";
}
}, [window, minX, minY, maxX, maxY]);
}, [window, minX, minY, maxX, maxY, onResize]);
const close = useCallback(async () => {
let shouldClose = true;
const didConfirmClose = typeof(props.confirmClose) == "function" ? props.confirmClose() : props.confirmClose;
const didConfirmClose = typeof(doConfirmClose) == "function" ? doConfirmClose() : doConfirmClose;
if (didConfirmClose) {
setOpen(true);
shouldClose = await confirmClose(props.confirmationText);
shouldClose = await confirmClose(confirmationText);
setOpen(false);
}
if (props.close && shouldClose) props.close();
}, []);
if (doClose && shouldClose) doClose();
}, [confirmationText, doClose, doConfirmClose]);
const className = `floating-window${` ${props.className}` || ""}${props.resizable ? " resizable" : ""}${modalOpen ? " modal-open" : ""}`;
const styles = {height: props.height, width: props.width, left: position.x || 0, top: position.y || 0};
return <div id={props.id} className={className} ref={window} style={styles}>
const finalClassname = `floating-window${` ${className}` || ""}${resizable ? " resizable" : ""}${modalOpen ? " modal-open" : ""}`;
const styles = {height: initialHeight, width: initialWidth, left: position.x || 0, top: position.y || 0};
return <div id={id} className={finalClassname} ref={window} style={styles}>
<div className="floating-window-titlebar" ref={titlebar}>
<span className="title">{props.title}</span>
<span className="title">{title}</span>
<div className="floating-window-buttons">
<div className="button maximize-button" onClick={maximize}>
<MaximizeIcon size="18px" />
@ -154,7 +151,7 @@ export default function FloatingWindow(props) {
</div>
</div>
<div className="floating-window-content">
{props.children}
{children}
</div>
</div>;
}

View File

@ -19,14 +19,14 @@ export default forwardRef(function AddonEditor({content, language, save, openNat
set value(newValue) {editorRef.current.setValue(newValue);},
get hasUnsavedChanges() {return hasUnsavedChanges;}
};
}, []);
}, [hasUnsavedChanges]);
const popoutNative = useCallback(() => openNative?.(), []);
const popoutNative = useCallback(() => openNative?.(), [openNative]);
const onChange = useCallback(() => setUnsaved(true), []);
const saveAddon = useCallback((event, newCSS) => {
save?.(newCSS);
setUnsaved(false);
}, []);
}, [save]);
return <Editor
ref={editorRef}

View File

@ -27,9 +27,9 @@ export default function ServerCard({server, joined, join, navigateTo, defaultAva
setJoined("joining");
const didJoin = await join(server.identifier, server.nativeJoin);
setJoined(didJoin);
}, [hasJoined]);
}, [hasJoined, join, navigateTo, server.identifier, server.nativeJoin]);
const defaultIcon = useMemo(() => defaultAvatar(), []);
const defaultIcon = useMemo(() => defaultAvatar(), [defaultAvatar]);
const currentIcon = !server.iconUrl || isError ? defaultIcon : server.iconUrl;
const addedDate = new Date(server.insertDate * 1000); // Convert from unix timestamp

View File

@ -85,7 +85,7 @@ export default function AddonCard({addon, type, disabled, enabled, onChange: par
const onChange = useCallback(() => {
setEnabled(!isEnabled);
if (parentChange) parentChange(addon.id);
}, []);
}, [addon.id, parentChange, isEnabled]);
const showSettings = useCallback(() => {
if (!hasSettings || !enabled) return;
@ -97,7 +97,7 @@ export default function AddonCard({addon, type, disabled, enabled, onChange: par
Toasts.show(Strings.Addons.settingsError.format({name}), {type: "error"});
Logger.stacktrace("Addon Settings", "Unable to get settings panel for " + name + ".", err);
}
}, [hasSettings, enabled]);
}, [hasSettings, enabled, addon.name, getSettingsPanel]);
const messageAuthor = useCallback(() => {
if (!addon.authorId) return;
@ -127,7 +127,7 @@ export default function AddonCard({addon, type, disabled, enabled, onChange: par
{authorArray}
</div>
];
}, []);
}, [addon.name, addon.version, addon.authorLink, addon.authorId, addon.author, messageAuthor]);
const footer = useMemo(() => {
const links = Object.keys(LinkIcons);
@ -140,7 +140,7 @@ export default function AddonCard({addon, type, disabled, enabled, onChange: par
{deleteAddon && makeButton(Strings.Addons.deleteAddon, <DeleteIcon size={"20px"} />, deleteAddon, {isControl: true, danger: true})}
</div>
</div>;
}, [hasSettings, editAddon, deleteAddon]);
}, [hasSettings, editAddon, deleteAddon, addon, enabled, showSettings]);
return <div id={`${addon.id}-card`} className={"bd-addon-card" + (disabled ? " bd-addon-card-disabled" : "")}>
<div className="bd-addon-header">

View File

@ -90,36 +90,36 @@ export default function AddonList({prefix, type, title, folder, addonList, addon
Events.off(`${prefix}-loaded`, forceUpdate);
Events.off(`${prefix}-unloaded`, forceUpdate);
};
}, []);
}, [prefix]);
const changeView = useCallback((value) => {
saveState(type, "view", value);
setView(value);
}, []);
}, [type]);
const listView = useCallback(() => changeView("list"), []);
const gridView = useCallback(() => changeView("grid"), []);
const listView = useCallback(() => changeView("list"), [changeView]);
const gridView = useCallback(() => changeView("grid"), [changeView]);
const changeDirection = useCallback((value) => {
saveState(type, "ascending", value);
setAscending(value);
}, []);
}, [type]);
const changeSort = useCallback((value) => {
saveState(type, "sort", value);
setSort(value);
}, []);
}, [type]);
const search = useCallback((e) => setQuery(e.target.value.toLocaleLowerCase()), []);
const triggerEdit = useCallback((id) => editAddon?.(id), []);
const triggerEdit = useCallback((id) => editAddon?.(id), [editAddon]);
const triggerDelete = useCallback(async (id) => {
const addon = addonList.find(a => a.id == id);
const shouldDelete = await confirmDelete(addon);
if (!shouldDelete) return;
if (deleteAddon) deleteAddon(addon);
}, []);
}, [addonList, deleteAddon]);
const button = folder ? {title: Strings.Addons.openFolder.format({type: title}), onClick: openFolder} : null;
const button = folder ? {title: Strings.Addons.openFolder.format({type: title}), onClick: openFolder.bind(null, folder)} : null;
const renderedCards = useMemo(() => {
let sorted = addonList.sort((a, b) => {
const sortByEnabled = sort === "isEnabled";
@ -150,7 +150,7 @@ export default function AddonList({prefix, type, title, folder, addonList, addon
const getSettings = hasSettings && addon.instance.getSettingsPanel.bind(addon.instance);
return <ErrorBoundary><AddonCard disabled={addon.partial} type={type} editAddon={() => triggerEdit(addon.id)} deleteAddon={() => triggerDelete(addon.id)} key={addon.id} enabled={addonState[addon.id]} addon={addon} onChange={onChange} reload={reload} hasSettings={hasSettings} getSettingsPanel={getSettings} /></ErrorBoundary>;
});
}, [addonList, addonState, sort, ascending, query]);
}, [addonList, addonState, onChange, reload, triggerDelete, triggerEdit, type, sort, ascending, query]);
const hasAddonsInstalled = addonList.length !== 0;
const isSearching = !!query;

View File

@ -60,7 +60,7 @@ export default function Color({value: initialValue, onChange, colors = defaultCo
const change = useCallback((e) => {
onChange?.(resolveColor(e.target.value));
setValue(e.target.value);
}, []);
}, [onChange]);
const intValue = resolveColor(value, false);
return <div className="bd-color-picker-container">

View File

@ -9,7 +9,7 @@ export default function Select({value: initialValue, options, style, onChange})
const change = useCallback((val) => {
onChange?.(val);
setValue(val);
}, []);
}, [onChange]);
const hideMenu = useCallback(() => {
@ -26,7 +26,7 @@ export default function Select({value: initialValue, options, style, onChange})
setOpen(next);
if (!next) return;
document.addEventListener("click", hideMenu);
}, [open]);
}, [hideMenu, open]);
// ?? options[0] provides a double failsafe

View File

@ -26,19 +26,19 @@ export default function Keybind({value: initialValue, onChange, max = 2, clearab
if (onChange) onChange(state.accum);
setState({value: state.accum.slice(0), isRecording: false, accum: []});
}
}, [state]);
const onClick = useCallback((e) => {
if (e.target?.className?.includes?.("bd-keybind-clear") || e.target?.closest(".bd-button")?.className?.includes("bd-keybind-clear")) return clearKeybind(e);
setState({...state, isRecording: !state.isRecording});
}, [state]);
}, [state, max, onChange]);
const clearKeybind = useCallback((event) => {
event.stopPropagation();
event.preventDefault();
if (onChange) onChange([]);
setState({...state, value: [], accum: []});
}, []);
}, [onChange, state]);
const onClick = useCallback((e) => {
if (e.target?.className?.includes?.("bd-keybind-clear") || e.target?.closest(".bd-button")?.className?.includes("bd-keybind-clear")) return clearKeybind(e);
setState({...state, isRecording: !state.isRecording});
}, [state, clearKeybind]);
const displayValue = state.isRecording ? "Recording..." : !state.value.length ? "N/A" : state.value.join(" + ");

View File

@ -8,7 +8,7 @@ export default function Number({value: initialValue, min, max, step, onChange})
const change = useCallback((e) => {
onChange?.(e.target.value);
setValue(e.target.value);
}, []);
}, [onChange]);
return <input onChange={change} type="number" className="bd-number-input" min={min} max={max} step={step} value={value} />;
}

View File

@ -12,7 +12,7 @@ export default function Radio({name, value, options, onChange}) {
const newValue = options[newIndex].value;
onChange?.(newValue);
setIndex(newIndex);
}, [index, options]);
}, [options, onChange]);
function renderOption(opt, i) {
const isSelected = index === i;

View File

@ -9,7 +9,7 @@ export default function Search({onChange, className, onKeyDown, placeholder}) {
const change = useCallback((e) => {
onChange?.(e);
setValue(e.target.value);
}, []);
}, [onChange]);
return <div className={"bd-search-wrapper" + (className ? ` ${className}` : "")}>

View File

@ -8,7 +8,7 @@ export default function Slider({value: initialValue, min, max, step, onChange})
const change = useCallback((e) => {
onChange?.(e.target.value);
setValue(e.target.value);
}, []);
}, [onChange]);
return <div className="bd-slider-wrap">
<div className="bd-slider-label">{value}</div><input onChange={change} type="range" className="bd-slider-input" min={min} max={max} step={step} value={value} style={{backgroundSize: (value - min) * 100 / (max - min) + "% 100%"}} />

View File

@ -8,7 +8,7 @@ export default function Switch({id, checked: initialValue, disabled, onChange})
const change = useCallback(() => {
onChange?.(!checked);
setChecked(!checked);
}, [checked]);
}, [checked, onChange]);
const enabledClass = disabled ? " bd-switch-disabled" : "";
const checkedClass = checked ? " bd-switch-checked" : "";

View File

@ -8,7 +8,7 @@ export default function Textbox({value: initialValue, maxLength, placeholder, on
const change = useCallback((e) => {
onChange?.(e.target.value);
setValue(e.target.value);
}, []);
}, [onChange]);
return <input onChange={change} onKeyDown={onKeyDown} type="text" className="bd-text-input" placeholder={placeholder} maxLength={maxLength} value={value} />;
}

View File

@ -22,7 +22,7 @@ export default function Drawer({name, collapsible, shown = true, showDivider, ch
drawer.classList.remove("animating");
}, timeout);
}, [collapsed]);
}, [collapsed, onDrawerToggle]);
const onClick = useCallback((event) => {

View File

@ -17,7 +17,7 @@ export default function Group({onChange, id, name, button, shown, onDrawerToggle
const change = useCallback((settingId, value) => {
if (id) onChange?.(id, settingId, value);
else onChange?.(settingId, value);
}, [id]);
}, [id, onChange]);
return <Drawer collapsible={collapsible} name={name} button={button} shown={shown} onDrawerToggle={onDrawerToggle} showDivider={showDivider}>
{settings.filter(s => !s.hidden).map((setting) => {

View File

@ -11,7 +11,7 @@ export default function SettingsTitle({isGroup, className, button, onClick, text
event.stopPropagation();
event.preventDefault();
button?.onClick?.(event);
}, []);
}, [button]);
const baseClass = isGroup ? groupClass : basicClass;

View File

@ -9,50 +9,50 @@ import Checkmark from "./icons/check";
const {useState, useCallback, useEffect} = React;
function CoreUpdaterPanel(props) {
function CoreUpdaterPanel({hasUpdate, remoteVersion, update}) {
return <Drawer name="BetterDiscord" collapsible={true}>
<SettingItem name={`Core v${Config.version}`} note={props.hasUpdate ? Strings.Updater.versionAvailable.format({version: props.remoteVersion}) : Strings.Updater.noUpdatesAvailable} inline={true} id={"core-updater"}>
{!props.hasUpdate && <div className="bd-filled-checkmark"><Checkmark /></div>}
{props.hasUpdate && <button className="bd-button" onClick={props.update}>{Strings.Updater.updateButton}</button>}
<SettingItem name={`Core v${Config.version}`} note={hasUpdate ? Strings.Updater.versionAvailable.format({version: remoteVersion}) : Strings.Updater.noUpdatesAvailable} inline={true} id={"core-updater"}>
{!hasUpdate && <div className="bd-filled-checkmark"><Checkmark /></div>}
{hasUpdate && <button className="bd-button" onClick={update}>{Strings.Updater.updateButton}</button>}
</SettingItem>
</Drawer>;
}
function NoUpdates(props) {
function NoUpdates({type}) {
return <div className="bd-empty-updates">
<Checkmark size="48px" />
{Strings.Updater.upToDateBlankslate.format({type: props.type})}
{Strings.Updater.upToDateBlankslate.format({type: type})}
</div>;
}
function AddonUpdaterPanel(props) {
const filenames = props.pending;
return <Drawer name={Strings.Panels[props.type]} collapsible={true} button={filenames.length ? {title: Strings.Updater.updateAll, onClick: () => props.updateAll(props.type)} : null}>
{!filenames.length && <NoUpdates type={props.type} />}
function AddonUpdaterPanel({pending, type, updater, update, updateAll}) {
const filenames = pending;
return <Drawer name={Strings.Panels[type]} collapsible={true} button={filenames.length ? {title: Strings.Updater.updateAll, onClick: () => updateAll(type)} : null}>
{!filenames.length && <NoUpdates type={type} />}
{filenames.map(f => {
const info = props.updater.cache[f];
const addon = props.updater.manager.addonList.find(a => a.filename === f);
const info = updater.cache[f];
const addon = updater.manager.addonList.find(a => a.filename === f);
return <SettingItem name={`${addon.name} v${addon.version}`} note={Strings.Updater.versionAvailable.format({version: info.version})} inline={true} id={addon.name}>
<button className="bd-button" onClick={() => props.update(props.type, f)}>{Strings.Updater.updateButton}</button>
<button className="bd-button" onClick={() => update(type, f)}>{Strings.Updater.updateButton}</button>
</SettingItem>;
})}
</Drawer>;
}
export default function UpdaterPanel(props) {
const [hasCoreUpdate, setCoreUpdate] = useState(props.coreUpdater.hasUpdate);
const [updates, setUpdates] = useState({plugins: props.pluginUpdater.pending.slice(0), themes: props.themeUpdater.pending.slice(0)});
export default function UpdaterPanel({coreUpdater, pluginUpdater, themeUpdater}) {
const [hasCoreUpdate, setCoreUpdate] = useState(coreUpdater.hasUpdate);
const [updates, setUpdates] = useState({plugins: pluginUpdater.pending.slice(0), themes: themeUpdater.pending.slice(0)});
const checkAddons = useCallback(async (type) => {
const updater = type === "plugins" ? props.pluginUpdater : props.themeUpdater;
const updater = type === "plugins" ? pluginUpdater : themeUpdater;
await updater.checkAll(false);
setUpdates({...updates, [type]: updater.pending.slice(0)});
}, []);
}, [updates, pluginUpdater, themeUpdater]);
const update = useCallback(() => {
checkAddons("plugins");
checkAddons("themes");
}, []);
}, [checkAddons]);
useEffect(() => {
Events.on(`plugin-loaded`, update);
@ -65,12 +65,12 @@ export default function UpdaterPanel(props) {
Events.off(`theme-loaded`, update);
Events.off(`theme-unloaded`, update);
};
}, []);
}, [update]);
const checkCoreUpdate = useCallback(async () => {
await props.coreUpdater.checkForUpdate(false);
setCoreUpdate(props.coreUpdater.hasUpdate);
}, []);
await coreUpdater.checkForUpdate(false);
setCoreUpdate(coreUpdater.hasUpdate);
}, [coreUpdater]);
const checkForUpdates = useCallback(async () => {
Toasts.info(Strings.Updater.checking);
@ -78,33 +78,33 @@ export default function UpdaterPanel(props) {
await checkAddons("plugins");
await checkAddons("themes");
Toasts.info(Strings.Updater.finishedChecking);
});
}, [checkAddons, checkCoreUpdate]);
const updateCore = useCallback(async () => {
await props.coreUpdater.update();
await coreUpdater.update();
setCoreUpdate(false);
}, []);
}, [coreUpdater]);
const updateAddon = useCallback(async (type, filename) => {
const updater = type === "plugins" ? props.pluginUpdater : props.themeUpdater;
const updater = type === "plugins" ? pluginUpdater : themeUpdater;
await updater.updateAddon(filename);
setUpdates(prev => {
prev[type].splice(prev[type].indexOf(filename), 1);
return prev;
});
}, []);
}, [pluginUpdater, themeUpdater]);
const updateAllAddons = useCallback(async (type) => {
const toUpdate = updates[type].slice(0);
for (const filename of toUpdate) {
await updateAddon(type, filename);
}
}, []);
}, [updateAddon, updates]);
return [
<SettingsTitle text={Strings.Panels.updates} button={{title: Strings.Updater.checkForUpdates, onClick: checkForUpdates}} />,
<CoreUpdaterPanel remoteVersion={props.coreUpdater.remoteVersion} hasUpdate={hasCoreUpdate} update={updateCore} />,
<AddonUpdaterPanel type="plugins" pending={updates.plugins} update={updateAddon} updateAll={updateAllAddons} updater={props.pluginUpdater} />,
<AddonUpdaterPanel type="themes" pending={updates.themes} update={updateAddon} updateAll={updateAllAddons} updater={props.themeUpdater} />,
<CoreUpdaterPanel remoteVersion={coreUpdater.remoteVersion} hasUpdate={hasCoreUpdate} update={updateCore} />,
<AddonUpdaterPanel type="plugins" pending={updates.plugins} update={updateAddon} updateAll={updateAllAddons} updater={pluginUpdater} />,
<AddonUpdaterPanel type="themes" pending={updates.themes} update={updateAddon} updateAll={updateAllAddons} updater={themeUpdater} />,
];
}