BetterDiscordApp-rauenzi/renderer/src/ui/settings/addonlist.jsx

191 lines
8.5 KiB
JavaScript

import React from "@modules/react";
import Strings from "@modules/strings";
import Events from "@modules/emitter";
import DataStore from "@modules/datastore";
import DiscordModules from "@modules/discordmodules";
import ipc from "@modules/ipc";
import SettingsTitle from "./title";
import AddonCard from "./addoncard";
import Dropdown from "./components/dropdown";
import Search from "./components/search";
import Modals from "@ui/modals";
import ErrorBoundary from "@ui/errorboundary";
import ListIcon from "@ui/icons/list";
import GridIcon from "@ui/icons/grid";
import NoResults from "@ui/blankslates/noresults";
import EmptyImage from "@ui/blankslates/emptyimage";
const {useState, useCallback, useEffect, useReducer, useMemo} = React;
const buildSortOptions = () => [
{label: Strings.Addons.name, value: "name"},
{label: Strings.Addons.author, value: "author"},
{label: Strings.Addons.version, value: "version"},
{label: Strings.Addons.added, value: "added"},
{label: Strings.Addons.modified, value: "modified"},
{label: Strings.Addons.isEnabled, value: "isEnabled"}
];
const buildDirectionOptions = () => [
{label: Strings.Sorting.ascending, value: true},
{label: Strings.Sorting.descending, value: false}
];
function openFolder(folder) {
ipc.openPath(folder);
}
function blankslate(type, onClick) {
const message = Strings.Addons.blankSlateMessage.format({link: `https://betterdiscord.app/${type}s`, type}).toString();
return <EmptyImage title={Strings.Addons.blankSlateHeader.format({type})} message={message}>
<button className="bd-button" onClick={onClick}>{Strings.Addons.openFolder.format({type})}</button>
</EmptyImage>;
}
function makeControlButton(title, children, action, selected = false) {
return <DiscordModules.Tooltip color="primary" position="top" text={title}>
{(props) => {
return <button {...props} className={"bd-button bd-view-button" + (selected ? " selected" : "")} onClick={action}>{children}</button>;
}}
</DiscordModules.Tooltip>;
}
function getState(type, control, defaultValue) {
const addonlistControls = DataStore.getBDData("addonlistControls") || {};
if (!addonlistControls[type]) return defaultValue;
if (!addonlistControls[type].hasOwnProperty(control)) return defaultValue;
return addonlistControls[type][control];
}
function saveState(type, control, value) {
const addonlistControls = DataStore.getBDData("addonlistControls") || {};
if (!addonlistControls[type]) addonlistControls[type] = {};
addonlistControls[type][control] = value;
DataStore.setBDData("addonlistControls", addonlistControls);
}
function confirmDelete(addon) {
return new Promise(resolve => {
Modals.showConfirmationModal(Strings.Modals.confirmAction, Strings.Addons.confirmDelete.format({name: addon.name}), {
danger: true,
confirmText: Strings.Addons.deleteAddon,
onConfirm: () => {resolve(true);},
onCancel: () => {resolve(false);}
});
});
}
export default function AddonList({prefix, type, title, folder, addonList, addonState, onChange, reload, editAddon, deleteAddon}) {
const [query, setQuery] = useState("");
const [sort, setSort] = useState(getState.bind(null, type, "sort", "name"));
const [ascending, setAscending] = useState(getState.bind(null, type, "ascending", true));
const [view, setView] = useState(getState.bind(null, type, "view", "list"));
const [forced, forceUpdate] = useReducer(x => x + 1, 0);
useEffect(() => {
Events.on(`${prefix}-loaded`, forceUpdate);
Events.on(`${prefix}-unloaded`, forceUpdate);
return () => {
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"), [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), [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.bind(null, folder)} : null;
const renderedCards = useMemo(() => {
let sorted = addonList.sort((a, b) => {
const sortByEnabled = sort === "isEnabled";
const first = sortByEnabled ? addonState[a.id] : a[sort];
const second = sortByEnabled ? addonState[b.id] : b[sort];
const stringSort = (str1, str2) => str1.toLocaleLowerCase().localeCompare(str2.toLocaleLowerCase());
if (typeof(first) == "string") return stringSort(first, second);
if (typeof(first) == "boolean") return (first === second) ? stringSort(a.name, b.name) : first ? -1 : 1;
if (first > second) return 1;
if (second > first) return -1;
return 0;
});
if (!ascending) sorted.reverse();
if (query) {
sorted = sorted.filter(addon => {
let matches = addon.name.toLocaleLowerCase().includes(query);
matches = matches || addon.author.toLocaleLowerCase().includes(query);
matches = matches || addon.description.toLocaleLowerCase().includes(query);
if (!matches) return false;
return true;
});
}
return sorted.map(addon => {
const hasSettings = addon.instance && typeof(addon.instance.getSettingsPanel) === "function";
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, onChange, reload, triggerDelete, triggerEdit, type, sort, ascending, query, forced]); // eslint-disable-line react-hooks/exhaustive-deps
const hasAddonsInstalled = addonList.length !== 0;
const isSearching = !!query;
const hasResults = renderedCards.length !== 0;
return [
<SettingsTitle key="title" text={title} button={button} />,
<div className={"bd-controls bd-addon-controls"}>
<Search onChange={search} placeholder={`${Strings.Addons.search.format({type: title})}...`} />
<div className="bd-controls-advanced">
<div className="bd-addon-dropdowns">
<div className="bd-select-wrapper">
<label className="bd-label">{Strings.Sorting.sortBy}:</label>
<Dropdown options={buildSortOptions()} value={sort} onChange={changeSort} style="transparent" />
</div>
<div className="bd-select-wrapper">
<label className="bd-label">{Strings.Sorting.order}:</label>
<Dropdown options={buildDirectionOptions()} value={ascending} onChange={changeDirection} style="transparent" />
</div>
</div>
<div className="bd-addon-views">
{makeControlButton(Strings.Addons.listView, <ListIcon />, listView, view === "list")}
{makeControlButton(Strings.Addons.gridView, <GridIcon />, gridView, view === "grid")}
</div>
</div>
</div>,
!hasAddonsInstalled && blankslate(type, () => openFolder(folder)),
isSearching && !hasResults && hasAddonsInstalled && <NoResults />,
hasAddonsInstalled && <div key="addonList" className={"bd-addon-list" + (view == "grid" ? " bd-grid-view" : "")}>{renderedCards}</div>
];
}