mirror of
https://github.com/Equicord/Equicord.git
synced 2025-01-31 03:33:36 -05:00
Settings API: add support for custom objects / arrays (#3154)
This commit is contained in:
parent
317121fc08
commit
5c8ba6e542
13 changed files with 420 additions and 355 deletions
|
@ -105,7 +105,13 @@ export default tseslint.config(
|
||||||
"no-invalid-regexp": "error",
|
"no-invalid-regexp": "error",
|
||||||
"no-constant-condition": ["error", { "checkLoops": false }],
|
"no-constant-condition": ["error", { "checkLoops": false }],
|
||||||
"no-duplicate-imports": "error",
|
"no-duplicate-imports": "error",
|
||||||
"dot-notation": "error",
|
"@typescript-eslint/dot-notation": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"allowPrivateClassPropertyAccess": true,
|
||||||
|
"allowProtectedClassPropertyAccess": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"no-useless-escape": [
|
"no-useless-escape": [
|
||||||
"error",
|
"error",
|
||||||
{
|
{
|
||||||
|
|
|
@ -81,7 +81,8 @@ const Components: Record<OptionType, React.ComponentType<ISettingElementProps<an
|
||||||
[OptionType.BOOLEAN]: SettingBooleanComponent,
|
[OptionType.BOOLEAN]: SettingBooleanComponent,
|
||||||
[OptionType.SELECT]: SettingSelectComponent,
|
[OptionType.SELECT]: SettingSelectComponent,
|
||||||
[OptionType.SLIDER]: SettingSliderComponent,
|
[OptionType.SLIDER]: SettingSliderComponent,
|
||||||
[OptionType.COMPONENT]: SettingCustomComponent
|
[OptionType.COMPONENT]: SettingCustomComponent,
|
||||||
|
[OptionType.CUSTOM]: () => null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function PluginModal({ plugin, onRestartNeeded, onClose, transitionState }: PluginModalProps) {
|
export default function PluginModal({ plugin, onRestartNeeded, onClose, transitionState }: PluginModalProps) {
|
||||||
|
@ -129,7 +130,8 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
||||||
for (const [key, value] of Object.entries(tempSettings)) {
|
for (const [key, value] of Object.entries(tempSettings)) {
|
||||||
const option = plugin.options[key];
|
const option = plugin.options[key];
|
||||||
pluginSettings[key] = value;
|
pluginSettings[key] = value;
|
||||||
option?.onChange?.(value);
|
|
||||||
|
if (option.type === OptionType.CUSTOM) continue;
|
||||||
if (option?.restartNeeded) restartNeeded = true;
|
if (option?.restartNeeded) restartNeeded = true;
|
||||||
}
|
}
|
||||||
if (restartNeeded) onRestartNeeded();
|
if (restartNeeded) onRestartNeeded();
|
||||||
|
@ -141,7 +143,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
||||||
return <Forms.FormText>There are no settings for this plugin.</Forms.FormText>;
|
return <Forms.FormText>There are no settings for this plugin.</Forms.FormText>;
|
||||||
} else {
|
} else {
|
||||||
const options = Object.entries(plugin.options).map(([key, setting]) => {
|
const options = Object.entries(plugin.options).map(([key, setting]) => {
|
||||||
if (setting.hidden) return null;
|
if (setting.type === OptionType.CUSTOM || setting.hidden) return null;
|
||||||
|
|
||||||
function onChange(newValue: any) {
|
function onChange(newValue: any) {
|
||||||
setTempSettings(s => ({ ...s, [key]: newValue }));
|
setTempSettings(s => ({ ...s, [key]: newValue }));
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as DataStore from "@api/DataStore";
|
|
||||||
import { definePluginSettings, Settings } from "@api/Settings";
|
import { definePluginSettings, Settings } from "@api/Settings";
|
||||||
import { getUserSettingLazy } from "@api/UserSettings";
|
import { getUserSettingLazy } from "@api/UserSettings";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
@ -62,7 +61,7 @@ const ToggleIconOff = (activity: IgnoredActivity, fill: string) => ToggleIcon(ac
|
||||||
|
|
||||||
function ToggleActivityComponent(activity: IgnoredActivity, isPlaying = false) {
|
function ToggleActivityComponent(activity: IgnoredActivity, isPlaying = false) {
|
||||||
const s = settings.use(["ignoredActivities"]);
|
const s = settings.use(["ignoredActivities"]);
|
||||||
const { ignoredActivities = [] } = s;
|
const { ignoredActivities } = s;
|
||||||
|
|
||||||
if (ignoredActivities.some(act => act.id === activity.id)) return ToggleIconOff(activity, "var(--status-danger)");
|
if (ignoredActivities.some(act => act.id === activity.id)) return ToggleIconOff(activity, "var(--status-danger)");
|
||||||
return ToggleIconOn(activity, isPlaying ? "var(--green-300)" : "var(--primary-400)");
|
return ToggleIconOn(activity, isPlaying ? "var(--green-300)" : "var(--primary-400)");
|
||||||
|
@ -71,9 +70,9 @@ function ToggleActivityComponent(activity: IgnoredActivity, isPlaying = false) {
|
||||||
function handleActivityToggle(e: React.MouseEvent<HTMLButtonElement, MouseEvent>, activity: IgnoredActivity) {
|
function handleActivityToggle(e: React.MouseEvent<HTMLButtonElement, MouseEvent>, activity: IgnoredActivity) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
const ignoredActivityIndex = getIgnoredActivities().findIndex(act => act.id === activity.id);
|
const ignoredActivityIndex = settings.store.ignoredActivities.findIndex(act => act.id === activity.id);
|
||||||
if (ignoredActivityIndex === -1) settings.store.ignoredActivities = getIgnoredActivities().concat(activity);
|
if (ignoredActivityIndex === -1) settings.store.ignoredActivities.push(activity);
|
||||||
else settings.store.ignoredActivities = getIgnoredActivities().filter((_, index) => index !== ignoredActivityIndex);
|
else settings.store.ignoredActivities.splice(ignoredActivityIndex, 1);
|
||||||
|
|
||||||
recalculateActivities();
|
recalculateActivities();
|
||||||
}
|
}
|
||||||
|
@ -209,14 +208,13 @@ const settings = definePluginSettings({
|
||||||
description: "Ignore all competing activities (These are normally special game activities)",
|
description: "Ignore all competing activities (These are normally special game activities)",
|
||||||
default: false,
|
default: false,
|
||||||
onChange: recalculateActivities
|
onChange: recalculateActivities
|
||||||
|
},
|
||||||
|
ignoredActivities: {
|
||||||
|
type: OptionType.CUSTOM,
|
||||||
|
default: [] as IgnoredActivity[],
|
||||||
|
onChange: recalculateActivities
|
||||||
}
|
}
|
||||||
}).withPrivateSettings<{
|
});
|
||||||
ignoredActivities: IgnoredActivity[];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
function getIgnoredActivities() {
|
|
||||||
return settings.store.ignoredActivities ??= [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function isActivityTypeIgnored(type: number, id?: string) {
|
function isActivityTypeIgnored(type: number, id?: string) {
|
||||||
if (id && settings.store.idsList.includes(id)) {
|
if (id && settings.store.idsList.includes(id)) {
|
||||||
|
@ -284,29 +282,14 @@ export default definePlugin({
|
||||||
],
|
],
|
||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
// Migrate allowedIds
|
if (settings.store.ignoredActivities.length !== 0) {
|
||||||
if (Settings.plugins.IgnoreActivities.allowedIds) {
|
|
||||||
settings.store.idsList = Settings.plugins.IgnoreActivities.allowedIds;
|
|
||||||
delete Settings.plugins.IgnoreActivities.allowedIds; // Remove allowedIds
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldIgnoredActivitiesData = await DataStore.get<Map<IgnoredActivity["id"], IgnoredActivity>>("IgnoreActivities_ignoredActivities");
|
|
||||||
|
|
||||||
if (oldIgnoredActivitiesData != null) {
|
|
||||||
settings.store.ignoredActivities = Array.from(oldIgnoredActivitiesData.values())
|
|
||||||
.map(activity => ({ ...activity, name: "Unknown Name" }));
|
|
||||||
|
|
||||||
DataStore.del("IgnoreActivities_ignoredActivities");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (getIgnoredActivities().length !== 0) {
|
|
||||||
const gamesSeen = RunningGameStore.getGamesSeen() as { id?: string; exePath: string; }[];
|
const gamesSeen = RunningGameStore.getGamesSeen() as { id?: string; exePath: string; }[];
|
||||||
|
|
||||||
for (const [index, ignoredActivity] of getIgnoredActivities().entries()) {
|
for (const [index, ignoredActivity] of settings.store.ignoredActivities.entries()) {
|
||||||
if (ignoredActivity.type !== ActivitiesTypes.Game) continue;
|
if (ignoredActivity.type !== ActivitiesTypes.Game) continue;
|
||||||
|
|
||||||
if (!gamesSeen.some(game => game.id === ignoredActivity.id || game.exePath === ignoredActivity.id)) {
|
if (!gamesSeen.some(game => game.id === ignoredActivity.id || game.exePath === ignoredActivity.id)) {
|
||||||
getIgnoredActivities().splice(index, 1);
|
settings.store.ignoredActivities.splice(index, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -316,11 +299,11 @@ export default definePlugin({
|
||||||
if (isActivityTypeIgnored(props.type, props.application_id)) return false;
|
if (isActivityTypeIgnored(props.type, props.application_id)) return false;
|
||||||
|
|
||||||
if (props.application_id != null) {
|
if (props.application_id != null) {
|
||||||
return !getIgnoredActivities().some(activity => activity.id === props.application_id) || (settings.store.listMode === FilterMode.Whitelist && settings.store.idsList.includes(props.application_id));
|
return !settings.store.ignoredActivities.some(activity => activity.id === props.application_id) || (settings.store.listMode === FilterMode.Whitelist && settings.store.idsList.includes(props.application_id));
|
||||||
} else {
|
} else {
|
||||||
const exePath = RunningGameStore.getRunningGames().find(game => game.name === props.name)?.exePath;
|
const exePath = RunningGameStore.getRunningGames().find(game => game.name === props.name)?.exePath;
|
||||||
if (exePath) {
|
if (exePath) {
|
||||||
return !getIgnoredActivities().some(activity => activity.id === exePath);
|
return !settings.store.ignoredActivities.some(activity => activity.id === exePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ import { addMessageAccessory, removeMessageAccessory } from "@api/MessageAccesso
|
||||||
import { addMessageDecoration, removeMessageDecoration } from "@api/MessageDecorations";
|
import { addMessageDecoration, removeMessageDecoration } from "@api/MessageDecorations";
|
||||||
import { addMessageClickListener, addMessagePreEditListener, addMessagePreSendListener, removeMessageClickListener, removeMessagePreEditListener, removeMessagePreSendListener } from "@api/MessageEvents";
|
import { addMessageClickListener, addMessagePreEditListener, addMessagePreSendListener, removeMessageClickListener, removeMessagePreEditListener, removeMessagePreSendListener } from "@api/MessageEvents";
|
||||||
import { addMessagePopoverButton, removeMessagePopoverButton } from "@api/MessagePopover";
|
import { addMessagePopoverButton, removeMessagePopoverButton } from "@api/MessagePopover";
|
||||||
import { Settings } from "@api/Settings";
|
import { Settings, SettingsStore } from "@api/Settings";
|
||||||
import { Logger } from "@utils/Logger";
|
import { Logger } from "@utils/Logger";
|
||||||
import { canonicalizeFind } from "@utils/patches";
|
import { canonicalizeFind } from "@utils/patches";
|
||||||
import { Patch, Plugin, PluginDef, ReporterTestable, StartAt } from "@utils/types";
|
import { Patch, Plugin, PluginDef, ReporterTestable, StartAt } from "@utils/types";
|
||||||
|
@ -146,6 +146,10 @@ for (const p of pluginsValues) {
|
||||||
for (const [name, def] of Object.entries(p.settings.def)) {
|
for (const [name, def] of Object.entries(p.settings.def)) {
|
||||||
const checks = p.settings.checks?.[name];
|
const checks = p.settings.checks?.[name];
|
||||||
p.options[name] = { ...def, ...checks };
|
p.options[name] = { ...def, ...checks };
|
||||||
|
|
||||||
|
if (def.onChange != null) {
|
||||||
|
SettingsStore.addChangeListener(`plugins.${p.name}.${name}`, def.onChange);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, registerCommand, sendBotMessage, unregisterCommand } from "@api/Commands";
|
import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, registerCommand, sendBotMessage, unregisterCommand } from "@api/Commands";
|
||||||
import * as DataStore from "@api/DataStore";
|
import * as DataStore from "@api/DataStore";
|
||||||
import { Settings } from "@api/Settings";
|
import { definePluginSettings } from "@api/Settings";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
|
||||||
|
@ -29,23 +29,23 @@ const MessageTagsMarker = Symbol("MessageTags");
|
||||||
interface Tag {
|
interface Tag {
|
||||||
name: string;
|
name: string;
|
||||||
message: string;
|
message: string;
|
||||||
enabled: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTags = () => DataStore.get(DATA_KEY).then<Tag[]>(t => t ?? []);
|
function getTags() {
|
||||||
const getTag = (name: string) => DataStore.get(DATA_KEY).then<Tag | null>((t: Tag[]) => (t ?? []).find((tt: Tag) => tt.name === name) ?? null);
|
return settings.store.tagsList;
|
||||||
const addTag = async (tag: Tag) => {
|
}
|
||||||
const tags = await getTags();
|
|
||||||
tags.push(tag);
|
function getTag(name: string) {
|
||||||
DataStore.set(DATA_KEY, tags);
|
return settings.store.tagsList[name] ?? null;
|
||||||
return tags;
|
}
|
||||||
};
|
|
||||||
const removeTag = async (name: string) => {
|
function addTag(tag: Tag) {
|
||||||
let tags = await getTags();
|
settings.store.tagsList[tag.name] = tag;
|
||||||
tags = await tags.filter((t: Tag) => t.name !== name);
|
}
|
||||||
DataStore.set(DATA_KEY, tags);
|
|
||||||
return tags;
|
function removeTag(name: string) {
|
||||||
};
|
delete settings.store.tagsList[name];
|
||||||
|
}
|
||||||
|
|
||||||
function createTagCommand(tag: Tag) {
|
function createTagCommand(tag: Tag) {
|
||||||
registerCommand({
|
registerCommand({
|
||||||
|
@ -53,14 +53,14 @@ function createTagCommand(tag: Tag) {
|
||||||
description: tag.name,
|
description: tag.name,
|
||||||
inputType: ApplicationCommandInputType.BUILT_IN_TEXT,
|
inputType: ApplicationCommandInputType.BUILT_IN_TEXT,
|
||||||
execute: async (_, ctx) => {
|
execute: async (_, ctx) => {
|
||||||
if (!await getTag(tag.name)) {
|
if (!getTag(tag.name)) {
|
||||||
sendBotMessage(ctx.channel.id, {
|
sendBotMessage(ctx.channel.id, {
|
||||||
content: `${EMOTE} The tag **${tag.name}** does not exist anymore! Please reload ur Discord to fix :)`
|
content: `${EMOTE} The tag **${tag.name}** does not exist anymore! Please reload ur Discord to fix :)`
|
||||||
});
|
});
|
||||||
return { content: `/${tag.name}` };
|
return { content: `/${tag.name}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Settings.plugins.MessageTags.clyde) sendBotMessage(ctx.channel.id, {
|
if (settings.store.clyde) sendBotMessage(ctx.channel.id, {
|
||||||
content: `${EMOTE} The tag **${tag.name}** has been sent!`
|
content: `${EMOTE} The tag **${tag.name}** has been sent!`
|
||||||
});
|
});
|
||||||
return { content: tag.message.replaceAll("\\n", "\n") };
|
return { content: tag.message.replaceAll("\\n", "\n") };
|
||||||
|
@ -69,22 +69,38 @@ function createTagCommand(tag: Tag) {
|
||||||
}, "CustomTags");
|
}, "CustomTags");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
export default definePlugin({
|
|
||||||
name: "MessageTags",
|
|
||||||
description: "Allows you to save messages and to use them with a simple command.",
|
|
||||||
authors: [Devs.Luna],
|
|
||||||
options: {
|
|
||||||
clyde: {
|
clyde: {
|
||||||
name: "Clyde message on send",
|
name: "Clyde message on send",
|
||||||
description: "If enabled, clyde will send you an ephemeral message when a tag was used.",
|
description: "If enabled, clyde will send you an ephemeral message when a tag was used.",
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
default: true
|
default: true
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
tagsList: {
|
||||||
|
type: OptionType.CUSTOM,
|
||||||
|
default: {} as Record<string, Tag>,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "MessageTags",
|
||||||
|
description: "Allows you to save messages and to use them with a simple command.",
|
||||||
|
authors: [Devs.Luna],
|
||||||
|
settings,
|
||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
for (const tag of await getTags()) createTagCommand(tag);
|
// TODO: Remove DataStore tags migration once enough time has passed
|
||||||
|
const oldTags = await DataStore.get<Tag[]>(DATA_KEY);
|
||||||
|
if (oldTags != null) {
|
||||||
|
// @ts-ignore
|
||||||
|
settings.store.tagsList = Object.fromEntries(oldTags.map(oldTag => (delete oldTag.enabled, [oldTag.name, oldTag])));
|
||||||
|
await DataStore.del(DATA_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tags = getTags();
|
||||||
|
for (const tagName in tags) {
|
||||||
|
createTagCommand(tags[tagName]);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
commands: [
|
commands: [
|
||||||
|
@ -153,19 +169,18 @@ export default definePlugin({
|
||||||
const name: string = findOption(args[0].options, "tag-name", "");
|
const name: string = findOption(args[0].options, "tag-name", "");
|
||||||
const message: string = findOption(args[0].options, "message", "");
|
const message: string = findOption(args[0].options, "message", "");
|
||||||
|
|
||||||
if (await getTag(name))
|
if (getTag(name))
|
||||||
return sendBotMessage(ctx.channel.id, {
|
return sendBotMessage(ctx.channel.id, {
|
||||||
content: `${EMOTE} A Tag with the name **${name}** already exists!`
|
content: `${EMOTE} A Tag with the name **${name}** already exists!`
|
||||||
});
|
});
|
||||||
|
|
||||||
const tag = {
|
const tag = {
|
||||||
name: name,
|
name: name,
|
||||||
enabled: true,
|
|
||||||
message: message
|
message: message
|
||||||
};
|
};
|
||||||
|
|
||||||
createTagCommand(tag);
|
createTagCommand(tag);
|
||||||
await addTag(tag);
|
addTag(tag);
|
||||||
|
|
||||||
sendBotMessage(ctx.channel.id, {
|
sendBotMessage(ctx.channel.id, {
|
||||||
content: `${EMOTE} Successfully created the tag **${name}**!`
|
content: `${EMOTE} Successfully created the tag **${name}**!`
|
||||||
|
@ -175,13 +190,13 @@ export default definePlugin({
|
||||||
case "delete": {
|
case "delete": {
|
||||||
const name: string = findOption(args[0].options, "tag-name", "");
|
const name: string = findOption(args[0].options, "tag-name", "");
|
||||||
|
|
||||||
if (!await getTag(name))
|
if (!getTag(name))
|
||||||
return sendBotMessage(ctx.channel.id, {
|
return sendBotMessage(ctx.channel.id, {
|
||||||
content: `${EMOTE} A Tag with the name **${name}** does not exist!`
|
content: `${EMOTE} A Tag with the name **${name}** does not exist!`
|
||||||
});
|
});
|
||||||
|
|
||||||
unregisterCommand(name);
|
unregisterCommand(name);
|
||||||
await removeTag(name);
|
removeTag(name);
|
||||||
|
|
||||||
sendBotMessage(ctx.channel.id, {
|
sendBotMessage(ctx.channel.id, {
|
||||||
content: `${EMOTE} Successfully deleted the tag **${name}**!`
|
content: `${EMOTE} Successfully deleted the tag **${name}**!`
|
||||||
|
@ -192,10 +207,8 @@ export default definePlugin({
|
||||||
sendBotMessage(ctx.channel.id, {
|
sendBotMessage(ctx.channel.id, {
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
// @ts-ignore
|
|
||||||
title: "All Tags:",
|
title: "All Tags:",
|
||||||
// @ts-ignore
|
description: Object.values(getTags())
|
||||||
description: (await getTags())
|
|
||||||
.map(tag => `\`${tag.name}\`: ${tag.message.slice(0, 72).replaceAll("\\n", " ")}${tag.message.length > 72 ? "..." : ""}`)
|
.map(tag => `\`${tag.name}\`: ${tag.message.slice(0, 72).replaceAll("\\n", " ")}${tag.message.length > 72 ? "..." : ""}`)
|
||||||
.join("\n") || `${EMOTE} Woops! There are no tags yet, use \`/tags create\` to create one!`,
|
.join("\n") || `${EMOTE} Woops! There are no tags yet, use \`/tags create\` to create one!`,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -208,7 +221,7 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
case "preview": {
|
case "preview": {
|
||||||
const name: string = findOption(args[0].options, "tag-name", "");
|
const name: string = findOption(args[0].options, "tag-name", "");
|
||||||
const tag = await getTag(name);
|
const tag = getTag(name);
|
||||||
|
|
||||||
if (!tag)
|
if (!tag)
|
||||||
return sendBotMessage(ctx.channel.id, {
|
return sendBotMessage(ctx.channel.id, {
|
||||||
|
|
|
@ -7,11 +7,10 @@
|
||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModalLazy } from "@utils/modal";
|
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModalLazy } from "@utils/modal";
|
||||||
import { extractAndLoadChunksLazy, findComponentByCodeLazy, findExportedComponentLazy } from "@webpack";
|
import { extractAndLoadChunksLazy, findComponentByCodeLazy, findExportedComponentLazy } from "@webpack";
|
||||||
import { Button, Forms, Text, TextInput, Toasts, useEffect, useState } from "@webpack/common";
|
import { Button, Forms, Text, TextInput, Toasts, useMemo, useState } from "@webpack/common";
|
||||||
|
|
||||||
import { DEFAULT_COLOR, SWATCHES } from "../constants";
|
import { DEFAULT_COLOR, SWATCHES } from "../constants";
|
||||||
import { categories, Category, createCategory, getCategory, updateCategory } from "../data";
|
import { categoryLen, createCategory, getCategory } from "../data";
|
||||||
import { forceUpdate } from "../index";
|
|
||||||
|
|
||||||
interface ColorPickerProps {
|
interface ColorPickerProps {
|
||||||
color: number | null;
|
color: number | null;
|
||||||
|
@ -39,45 +38,45 @@ const cl = classNameFactory("vc-pindms-modal-");
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
categoryId: string | null;
|
categoryId: string | null;
|
||||||
initalChannelId: string | null;
|
initialChannelId: string | null;
|
||||||
modalProps: ModalProps;
|
modalProps: ModalProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
function useCategory(categoryId: string | null, initalChannelId: string | null) {
|
function useCategory(categoryId: string | null, initalChannelId: string | null) {
|
||||||
const [category, setCategory] = useState<Category | null>(null);
|
const category = useMemo(() => {
|
||||||
|
if (categoryId) {
|
||||||
useEffect(() => {
|
return getCategory(categoryId);
|
||||||
if (categoryId)
|
} else if (initalChannelId) {
|
||||||
setCategory(getCategory(categoryId)!);
|
return {
|
||||||
else if (initalChannelId)
|
|
||||||
setCategory({
|
|
||||||
id: Toasts.genId(),
|
id: Toasts.genId(),
|
||||||
name: `Pin Category ${categories.length + 1}`,
|
name: `Pin Category ${categoryLen() + 1}`,
|
||||||
color: DEFAULT_COLOR,
|
color: DEFAULT_COLOR,
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
channels: [initalChannelId]
|
channels: [initalChannelId]
|
||||||
});
|
};
|
||||||
|
}
|
||||||
}, [categoryId, initalChannelId]);
|
}, [categoryId, initalChannelId]);
|
||||||
|
|
||||||
return {
|
return category;
|
||||||
category,
|
|
||||||
setCategory
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NewCategoryModal({ categoryId, modalProps, initalChannelId }: Props) {
|
export function NewCategoryModal({ categoryId, modalProps, initialChannelId }: Props) {
|
||||||
const { category, setCategory } = useCategory(categoryId, initalChannelId);
|
const category = useCategory(categoryId, initialChannelId);
|
||||||
|
|
||||||
if (!category) return null;
|
if (!category) return null;
|
||||||
|
|
||||||
const onSave = async (e: React.FormEvent<HTMLFormElement> | React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
const [name, setName] = useState(category.name);
|
||||||
e.preventDefault();
|
const [color, setColor] = useState(category.color);
|
||||||
if (!categoryId)
|
|
||||||
await createCategory(category);
|
const onSave = (e: React.FormEvent<HTMLFormElement> | React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
else
|
e.preventDefault();
|
||||||
await updateCategory(category);
|
|
||||||
|
category.name = name;
|
||||||
|
category.color = color;
|
||||||
|
|
||||||
|
if (!categoryId) {
|
||||||
|
createCategory(category);
|
||||||
|
}
|
||||||
|
|
||||||
forceUpdate();
|
|
||||||
modalProps.onClose();
|
modalProps.onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -93,25 +92,25 @@ export function NewCategoryModal({ categoryId, modalProps, initalChannelId }: Pr
|
||||||
<Forms.FormSection>
|
<Forms.FormSection>
|
||||||
<Forms.FormTitle>Name</Forms.FormTitle>
|
<Forms.FormTitle>Name</Forms.FormTitle>
|
||||||
<TextInput
|
<TextInput
|
||||||
value={category.name}
|
value={name}
|
||||||
onChange={e => setCategory({ ...category, name: e })}
|
onChange={e => setName(e)}
|
||||||
/>
|
/>
|
||||||
</Forms.FormSection>
|
</Forms.FormSection>
|
||||||
<Forms.FormDivider />
|
<Forms.FormDivider />
|
||||||
<Forms.FormSection>
|
<Forms.FormSection>
|
||||||
<Forms.FormTitle>Color</Forms.FormTitle>
|
<Forms.FormTitle>Color</Forms.FormTitle>
|
||||||
<ColorPickerWithSwatches
|
<ColorPickerWithSwatches
|
||||||
key={category.name}
|
key={category.id}
|
||||||
defaultColor={DEFAULT_COLOR}
|
defaultColor={DEFAULT_COLOR}
|
||||||
colors={SWATCHES}
|
colors={SWATCHES}
|
||||||
onChange={c => setCategory({ ...category, color: c! })}
|
onChange={c => setColor(c!)}
|
||||||
value={category.color}
|
value={color}
|
||||||
renderDefaultButton={() => null}
|
renderDefaultButton={() => null}
|
||||||
renderCustomButton={() => (
|
renderCustomButton={() => (
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
color={category.color}
|
color={color}
|
||||||
onChange={c => setCategory({ ...category, color: c! })}
|
onChange={c => setColor(c!)}
|
||||||
key={category.name}
|
key={category.id}
|
||||||
showEyeDropper={false}
|
showEyeDropper={false}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -119,7 +118,7 @@ export function NewCategoryModal({ categoryId, modalProps, initalChannelId }: Pr
|
||||||
</Forms.FormSection>
|
</Forms.FormSection>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button type="submit" onClick={onSave} disabled={!category.name}>{categoryId ? "Save" : "Create"}</Button>
|
<Button type="submit" onClick={onSave} disabled={!name}>{categoryId ? "Save" : "Create"}</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</form>
|
</form>
|
||||||
</ModalRoot>
|
</ModalRoot>
|
||||||
|
@ -129,6 +128,6 @@ export function NewCategoryModal({ categoryId, modalProps, initalChannelId }: Pr
|
||||||
export const openCategoryModal = (categoryId: string | null, channelId: string | null) =>
|
export const openCategoryModal = (categoryId: string | null, channelId: string | null) =>
|
||||||
openModalLazy(async () => {
|
openModalLazy(async () => {
|
||||||
await requireSettingsMenu();
|
await requireSettingsMenu();
|
||||||
return modalProps => <NewCategoryModal categoryId={categoryId} modalProps={modalProps} initalChannelId={channelId} />;
|
return modalProps => <NewCategoryModal categoryId={categoryId} modalProps={modalProps} initialChannelId={channelId} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -7,8 +7,8 @@
|
||||||
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
|
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||||
import { Menu } from "@webpack/common";
|
import { Menu } from "@webpack/common";
|
||||||
|
|
||||||
import { addChannelToCategory, canMoveChannelInDirection, categories, isPinned, moveChannel, removeChannelFromCategory } from "../data";
|
import { addChannelToCategory, canMoveChannelInDirection, currentUserCategories, isPinned, moveChannel, removeChannelFromCategory } from "../data";
|
||||||
import { forceUpdate, PinOrder, settings } from "../index";
|
import { PinOrder, settings } from "../index";
|
||||||
import { openCategoryModal } from "./CreateCategoryModal";
|
import { openCategoryModal } from "./CreateCategoryModal";
|
||||||
|
|
||||||
function createPinMenuItem(channelId: string) {
|
function createPinMenuItem(channelId: string) {
|
||||||
|
@ -31,12 +31,12 @@ function createPinMenuItem(channelId: string) {
|
||||||
<Menu.MenuSeparator />
|
<Menu.MenuSeparator />
|
||||||
|
|
||||||
{
|
{
|
||||||
categories.map(category => (
|
currentUserCategories.map(category => (
|
||||||
<Menu.MenuItem
|
<Menu.MenuItem
|
||||||
key={category.id}
|
key={category.id}
|
||||||
id={`pin-category-${category.id}`}
|
id={`pin-category-${category.id}`}
|
||||||
label={category.name}
|
label={category.name}
|
||||||
action={() => addChannelToCategory(channelId, category.id).then(forceUpdate)}
|
action={() => addChannelToCategory(channelId, category.id)}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@ function createPinMenuItem(channelId: string) {
|
||||||
id="unpin-dm"
|
id="unpin-dm"
|
||||||
label="Unpin DM"
|
label="Unpin DM"
|
||||||
color="danger"
|
color="danger"
|
||||||
action={() => removeChannelFromCategory(channelId).then(forceUpdate)}
|
action={() => removeChannelFromCategory(channelId)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -57,7 +57,7 @@ function createPinMenuItem(channelId: string) {
|
||||||
<Menu.MenuItem
|
<Menu.MenuItem
|
||||||
id="move-up"
|
id="move-up"
|
||||||
label="Move Up"
|
label="Move Up"
|
||||||
action={() => moveChannel(channelId, -1).then(forceUpdate)}
|
action={() => moveChannel(channelId, -1)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -67,7 +67,7 @@ function createPinMenuItem(channelId: string) {
|
||||||
<Menu.MenuItem
|
<Menu.MenuItem
|
||||||
id="move-down"
|
id="move-down"
|
||||||
label="Move Down"
|
label="Move Down"
|
||||||
action={() => moveChannel(channelId, 1).then(forceUpdate)}
|
action={() => moveChannel(channelId, 1)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,10 +6,10 @@
|
||||||
|
|
||||||
import * as DataStore from "@api/DataStore";
|
import * as DataStore from "@api/DataStore";
|
||||||
import { Settings } from "@api/Settings";
|
import { Settings } from "@api/Settings";
|
||||||
|
import { useForceUpdater } from "@utils/react";
|
||||||
import { UserStore } from "@webpack/common";
|
import { UserStore } from "@webpack/common";
|
||||||
|
|
||||||
import { DEFAULT_COLOR } from "./constants";
|
import { PinOrder, PrivateChannelSortStore, settings } from "./index";
|
||||||
import { forceUpdate, PinOrder, PrivateChannelSortStore, settings } from "./index";
|
|
||||||
|
|
||||||
export interface Category {
|
export interface Category {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -24,104 +24,92 @@ const CATEGORY_MIGRATED_PINDMS_KEY = "PinDMsMigratedPinDMs";
|
||||||
const CATEGORY_MIGRATED_KEY = "PinDMsMigratedOldCategories";
|
const CATEGORY_MIGRATED_KEY = "PinDMsMigratedOldCategories";
|
||||||
const OLD_CATEGORY_KEY = "BetterPinDMsCategories-";
|
const OLD_CATEGORY_KEY = "BetterPinDMsCategories-";
|
||||||
|
|
||||||
|
let forceUpdateDms: (() => void) | undefined = undefined;
|
||||||
export let categories: Category[] = [];
|
export let currentUserCategories: Category[] = [];
|
||||||
|
|
||||||
export async function saveCats(cats: Category[]) {
|
|
||||||
const { id } = UserStore.getCurrentUser();
|
|
||||||
await DataStore.set(CATEGORY_BASE_KEY + id, cats);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function init() {
|
export async function init() {
|
||||||
const id = UserStore.getCurrentUser()?.id;
|
await migrateData();
|
||||||
await initCategories(id);
|
|
||||||
await migrateData(id);
|
const userId = UserStore.getCurrentUser()?.id;
|
||||||
forceUpdate();
|
if (userId == null) return;
|
||||||
|
|
||||||
|
currentUserCategories = settings.store.userBasedCategoryList[userId] ??= [];
|
||||||
|
forceUpdateDms?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function initCategories(userId: string) {
|
export function usePinnedDms() {
|
||||||
categories = await DataStore.get<Category[]>(CATEGORY_BASE_KEY + userId) ?? [];
|
forceUpdateDms = useForceUpdater();
|
||||||
|
settings.use(["pinOrder", "canCollapseDmSection", "dmSectionCollapsed", "userBasedCategoryList"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCategory(id: string) {
|
export function getCategory(id: string) {
|
||||||
return categories.find(c => c.id === id);
|
return currentUserCategories.find(c => c.id === id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createCategory(category: Category) {
|
export function getCategoryByIndex(index: number) {
|
||||||
categories.push(category);
|
return currentUserCategories[index];
|
||||||
await saveCats(categories);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateCategory(category: Category) {
|
export function createCategory(category: Category) {
|
||||||
const index = categories.findIndex(c => c.id === category.id);
|
currentUserCategories.push(category);
|
||||||
if (index === -1) return;
|
|
||||||
|
|
||||||
categories[index] = category;
|
|
||||||
await saveCats(categories);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addChannelToCategory(channelId: string, categoryId: string) {
|
export function addChannelToCategory(channelId: string, categoryId: string) {
|
||||||
const category = categories.find(c => c.id === categoryId);
|
const category = currentUserCategories.find(c => c.id === categoryId);
|
||||||
if (!category) return;
|
if (category == null) return;
|
||||||
|
|
||||||
if (category.channels.includes(channelId)) return;
|
if (category.channels.includes(channelId)) return;
|
||||||
|
|
||||||
category.channels.push(channelId);
|
category.channels.push(channelId);
|
||||||
await saveCats(categories);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeChannelFromCategory(channelId: string) {
|
export function removeChannelFromCategory(channelId: string) {
|
||||||
const category = categories.find(c => c.channels.includes(channelId));
|
const category = currentUserCategories.find(c => c.channels.includes(channelId));
|
||||||
if (!category) return;
|
if (category == null) return;
|
||||||
|
|
||||||
category.channels = category.channels.filter(c => c !== channelId);
|
category.channels = category.channels.filter(c => c !== channelId);
|
||||||
await saveCats(categories);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeCategory(categoryId: string) {
|
export function removeCategory(categoryId: string) {
|
||||||
const catagory = categories.find(c => c.id === categoryId);
|
const categoryIndex = currentUserCategories.findIndex(c => c.id === categoryId);
|
||||||
if (!catagory) return;
|
if (categoryIndex === -1) return;
|
||||||
|
|
||||||
// catagory?.channels.forEach(c => removeChannelFromCategory(c));
|
currentUserCategories.splice(categoryIndex, 1);
|
||||||
categories = categories.filter(c => c.id !== categoryId);
|
|
||||||
await saveCats(categories);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function collapseCategory(id: string, value = true) {
|
export function collapseCategory(id: string, value = true) {
|
||||||
const category = categories.find(c => c.id === id);
|
const category = currentUserCategories.find(c => c.id === id);
|
||||||
if (!category) return;
|
if (category == null) return;
|
||||||
|
|
||||||
category.collapsed = value;
|
category.collapsed = value;
|
||||||
await saveCats(categories);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// utils
|
// Utils
|
||||||
export function isPinned(id: string) {
|
export function isPinned(id: string) {
|
||||||
return categories.some(c => c.channels.includes(id));
|
return currentUserCategories.some(c => c.channels.includes(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function categoryLen() {
|
export function categoryLen() {
|
||||||
return categories.length;
|
return currentUserCategories.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAllUncollapsedChannels() {
|
export function getAllUncollapsedChannels() {
|
||||||
if (settings.store.pinOrder === PinOrder.LastMessage) {
|
if (settings.store.pinOrder === PinOrder.LastMessage) {
|
||||||
const sortedChannels = PrivateChannelSortStore.getPrivateChannelIds();
|
const sortedChannels = PrivateChannelSortStore.getPrivateChannelIds();
|
||||||
return categories.filter(c => !c.collapsed).flatMap(c => sortedChannels.filter(channel => c.channels.includes(channel)));
|
return currentUserCategories.filter(c => !c.collapsed).flatMap(c => sortedChannels.filter(channel => c.channels.includes(channel)));
|
||||||
}
|
}
|
||||||
|
|
||||||
return categories.filter(c => !c.collapsed).flatMap(c => c.channels);
|
return currentUserCategories.filter(c => !c.collapsed).flatMap(c => c.channels);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSections() {
|
export function getSections() {
|
||||||
return categories.reduce((acc, category) => {
|
return currentUserCategories.reduce((acc, category) => {
|
||||||
acc.push(category.channels.length === 0 ? 1 : category.channels.length);
|
acc.push(category.channels.length === 0 ? 1 : category.channels.length);
|
||||||
return acc;
|
return acc;
|
||||||
}, [] as number[]);
|
}, [] as number[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// move categories
|
// Move categories
|
||||||
export const canMoveArrayInDirection = (array: any[], index: number, direction: -1 | 1) => {
|
export const canMoveArrayInDirection = (array: any[], index: number, direction: -1 | 1) => {
|
||||||
const a = array[index];
|
const a = array[index];
|
||||||
const b = array[index + direction];
|
const b = array[index + direction];
|
||||||
|
@ -130,18 +118,18 @@ export const canMoveArrayInDirection = (array: any[], index: number, direction:
|
||||||
};
|
};
|
||||||
|
|
||||||
export const canMoveCategoryInDirection = (id: string, direction: -1 | 1) => {
|
export const canMoveCategoryInDirection = (id: string, direction: -1 | 1) => {
|
||||||
const index = categories.findIndex(m => m.id === id);
|
const categoryIndex = currentUserCategories.findIndex(m => m.id === id);
|
||||||
return canMoveArrayInDirection(categories, index, direction);
|
return canMoveArrayInDirection(currentUserCategories, categoryIndex, direction);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const canMoveCategory = (id: string) => canMoveCategoryInDirection(id, -1) || canMoveCategoryInDirection(id, 1);
|
export const canMoveCategory = (id: string) => canMoveCategoryInDirection(id, -1) || canMoveCategoryInDirection(id, 1);
|
||||||
|
|
||||||
export const canMoveChannelInDirection = (channelId: string, direction: -1 | 1) => {
|
export const canMoveChannelInDirection = (channelId: string, direction: -1 | 1) => {
|
||||||
const category = categories.find(c => c.channels.includes(channelId));
|
const category = currentUserCategories.find(c => c.channels.includes(channelId));
|
||||||
if (!category) return false;
|
if (category == null) return false;
|
||||||
|
|
||||||
const index = category.channels.indexOf(channelId);
|
const channelIndex = category.channels.indexOf(channelId);
|
||||||
return canMoveArrayInDirection(category.channels, index, direction);
|
return canMoveArrayInDirection(category.channels, channelIndex, direction);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -150,70 +138,44 @@ function swapElementsInArray(array: any[], index1: number, index2: number) {
|
||||||
[array[index1], array[index2]] = [array[index2], array[index1]];
|
[array[index1], array[index2]] = [array[index2], array[index1]];
|
||||||
}
|
}
|
||||||
|
|
||||||
// stolen from PinDMs
|
export function moveCategory(id: string, direction: -1 | 1) {
|
||||||
export async function moveCategory(id: string, direction: -1 | 1) {
|
const a = currentUserCategories.findIndex(m => m.id === id);
|
||||||
const a = categories.findIndex(m => m.id === id);
|
|
||||||
const b = a + direction;
|
const b = a + direction;
|
||||||
|
|
||||||
swapElementsInArray(categories, a, b);
|
swapElementsInArray(currentUserCategories, a, b);
|
||||||
|
|
||||||
await saveCats(categories);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function moveChannel(channelId: string, direction: -1 | 1) {
|
export function moveChannel(channelId: string, direction: -1 | 1) {
|
||||||
const category = categories.find(c => c.channels.includes(channelId));
|
const category = currentUserCategories.find(c => c.channels.includes(channelId));
|
||||||
if (!category) return;
|
if (category == null) return;
|
||||||
|
|
||||||
const a = category.channels.indexOf(channelId);
|
const a = category.channels.indexOf(channelId);
|
||||||
const b = a + direction;
|
const b = a + direction;
|
||||||
|
|
||||||
swapElementsInArray(category.channels, a, b);
|
swapElementsInArray(category.channels, a, b);
|
||||||
|
|
||||||
await saveCats(categories);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Remove DataStore PinnedDms migration once enough time has passed
|
||||||
|
async function migrateData() {
|
||||||
// migrate data
|
if (Settings.plugins.PinDMs.dmSectioncollapsed != null) {
|
||||||
const getPinDMsPins = () => (Settings.plugins.PinDMs.pinnedDMs || void 0)?.split(",") as string[] | undefined;
|
settings.store.dmSectionCollapsed = Settings.plugins.PinDMs.dmSectioncollapsed;
|
||||||
|
delete Settings.plugins.PinDMs.dmSectioncollapsed;
|
||||||
async function migratePinDMs() {
|
|
||||||
if (categories.some(m => m.id === "oldPins")) {
|
|
||||||
return await DataStore.set(CATEGORY_MIGRATED_PINDMS_KEY, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const pindmspins = getPinDMsPins();
|
const dataStoreKeys = await DataStore.keys();
|
||||||
|
const pinDmsKeys = dataStoreKeys.map(key => String(key)).filter(key => key.startsWith(CATEGORY_BASE_KEY));
|
||||||
|
|
||||||
// we dont want duplicate pins
|
if (pinDmsKeys.length === 0) return;
|
||||||
const difference = [...new Set(pindmspins)]?.filter(m => !categories.some(c => c.channels.includes(m)));
|
|
||||||
if (difference?.length) {
|
for (const pinDmsKey of pinDmsKeys) {
|
||||||
categories.push({
|
const categories = await DataStore.get<Category[]>(pinDmsKey);
|
||||||
id: "oldPins",
|
if (categories == null) continue;
|
||||||
name: "Pins",
|
|
||||||
color: DEFAULT_COLOR,
|
const userId = pinDmsKey.replace(CATEGORY_BASE_KEY, "");
|
||||||
channels: difference
|
settings.store.userBasedCategoryList[userId] = categories;
|
||||||
});
|
|
||||||
|
await DataStore.del(pinDmsKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
await DataStore.set(CATEGORY_MIGRATED_PINDMS_KEY, true);
|
await Promise.all([DataStore.del(CATEGORY_MIGRATED_PINDMS_KEY), DataStore.del(CATEGORY_MIGRATED_KEY), DataStore.del(OLD_CATEGORY_KEY)]);
|
||||||
}
|
|
||||||
|
|
||||||
async function migrateOldCategories(userId: string) {
|
|
||||||
const oldCats = await DataStore.get<Category[]>(OLD_CATEGORY_KEY + userId);
|
|
||||||
// dont want to migrate if the user has already has categories.
|
|
||||||
if (categories.length === 0 && oldCats?.length) {
|
|
||||||
categories.push(...(oldCats.filter(m => m.id !== "oldPins")));
|
|
||||||
}
|
|
||||||
await DataStore.set(CATEGORY_MIGRATED_KEY, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function migrateData(userId: string) {
|
|
||||||
const m1 = await DataStore.get(CATEGORY_MIGRATED_KEY), m2 = await DataStore.get(CATEGORY_MIGRATED_PINDMS_KEY);
|
|
||||||
if (m1 && m2) return;
|
|
||||||
|
|
||||||
// want to migrate the old categories first and then slove any conflicts with the PinDMs pins
|
|
||||||
if (!m1) await migrateOldCategories(userId);
|
|
||||||
if (!m2) await migratePinDMs();
|
|
||||||
|
|
||||||
await saveCats(categories);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,13 +12,13 @@ import { Devs } from "@utils/constants";
|
||||||
import { classes } from "@utils/misc";
|
import { classes } from "@utils/misc";
|
||||||
import definePlugin, { OptionType, StartAt } from "@utils/types";
|
import definePlugin, { OptionType, StartAt } from "@utils/types";
|
||||||
import { findByPropsLazy, findStoreLazy } from "@webpack";
|
import { findByPropsLazy, findStoreLazy } from "@webpack";
|
||||||
import { ContextMenuApi, FluxDispatcher, Menu, React } from "@webpack/common";
|
import { Clickable, ContextMenuApi, FluxDispatcher, Menu, React } from "@webpack/common";
|
||||||
import { Channel } from "discord-types/general";
|
import { Channel } from "discord-types/general";
|
||||||
|
|
||||||
import { contextMenus } from "./components/contextMenu";
|
import { contextMenus } from "./components/contextMenu";
|
||||||
import { openCategoryModal, requireSettingsMenu } from "./components/CreateCategoryModal";
|
import { openCategoryModal, requireSettingsMenu } from "./components/CreateCategoryModal";
|
||||||
import { DEFAULT_CHUNK_SIZE } from "./constants";
|
import { DEFAULT_CHUNK_SIZE } from "./constants";
|
||||||
import { canMoveCategory, canMoveCategoryInDirection, categories, Category, categoryLen, collapseCategory, getAllUncollapsedChannels, getSections, init, isPinned, moveCategory, removeCategory } from "./data";
|
import { canMoveCategory, canMoveCategoryInDirection, Category, categoryLen, collapseCategory, getAllUncollapsedChannels, getCategoryByIndex, getSections, init, isPinned, moveCategory, removeCategory, usePinnedDms } from "./data";
|
||||||
|
|
||||||
interface ChannelComponentProps {
|
interface ChannelComponentProps {
|
||||||
children: React.ReactNode,
|
children: React.ReactNode,
|
||||||
|
@ -26,13 +26,11 @@ interface ChannelComponentProps {
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const headerClasses = findByPropsLazy("privateChannelsHeaderContainer");
|
const headerClasses = findByPropsLazy("privateChannelsHeaderContainer");
|
||||||
|
|
||||||
export const PrivateChannelSortStore = findStoreLazy("PrivateChannelSortStore") as { getPrivateChannelIds: () => string[]; };
|
export const PrivateChannelSortStore = findStoreLazy("PrivateChannelSortStore") as { getPrivateChannelIds: () => string[]; };
|
||||||
|
|
||||||
export let instance: any;
|
export let instance: any;
|
||||||
export const forceUpdate = () => instance?.props?._forceUpdate?.();
|
|
||||||
|
|
||||||
export const enum PinOrder {
|
export const enum PinOrder {
|
||||||
LastMessage,
|
LastMessage,
|
||||||
|
@ -46,21 +44,28 @@ export const settings = definePluginSettings({
|
||||||
options: [
|
options: [
|
||||||
{ label: "Most recent message", value: PinOrder.LastMessage, default: true },
|
{ label: "Most recent message", value: PinOrder.LastMessage, default: true },
|
||||||
{ label: "Custom (right click channels to reorder)", value: PinOrder.Custom }
|
{ label: "Custom (right click channels to reorder)", value: PinOrder.Custom }
|
||||||
],
|
]
|
||||||
onChange: () => forceUpdate()
|
|
||||||
},
|
},
|
||||||
|
canCollapseDmSection: {
|
||||||
dmSectioncollapsed: {
|
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
description: "Collapse DM sections",
|
description: "Allow uncategorised DMs section to be collapsable",
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
dmSectionCollapsed: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Collapse DM section",
|
||||||
default: false,
|
default: false,
|
||||||
onChange: () => forceUpdate()
|
hidden: true
|
||||||
|
},
|
||||||
|
userBasedCategoryList: {
|
||||||
|
type: OptionType.CUSTOM,
|
||||||
|
default: {} as Record<string, Category[]>
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "PinDMs",
|
name: "PinDMs",
|
||||||
description: "Allows you to pin private channels to the top of your DM list. To pin/unpin or reorder pins, right click DMs",
|
description: "Allows you to pin private channels to the top of your DM list. To pin/unpin or re-order pins, right click DMs",
|
||||||
authors: [Devs.Ven, Devs.Aria],
|
authors: [Devs.Ven, Devs.Aria],
|
||||||
settings,
|
settings,
|
||||||
contextMenus,
|
contextMenus,
|
||||||
|
@ -124,8 +129,8 @@ export default definePlugin({
|
||||||
{
|
{
|
||||||
find: ".FRIENDS},\"friends\"",
|
find: ".FRIENDS},\"friends\"",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /let{showLibrary:\i,.+?showDMHeader:.+?,/,
|
match: /let{showLibrary:\i,/,
|
||||||
replace: "let forceUpdate = Vencord.Util.useForceUpdater();$&_forceUpdate:forceUpdate,"
|
replace: "$self.usePinnedDms();$&"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -149,6 +154,7 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
sections: null as number[] | null,
|
sections: null as number[] | null,
|
||||||
|
|
||||||
set _instance(i: any) {
|
set _instance(i: any) {
|
||||||
|
@ -162,6 +168,7 @@ export default definePlugin({
|
||||||
CONNECTION_OPEN: init,
|
CONNECTION_OPEN: init,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
usePinnedDms,
|
||||||
isPinned,
|
isPinned,
|
||||||
categoryLen,
|
categoryLen,
|
||||||
getSections,
|
getSections,
|
||||||
|
@ -186,11 +193,11 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
|
|
||||||
makeSpanProps() {
|
makeSpanProps() {
|
||||||
return {
|
return settings.store.canCollapseDmSection ? {
|
||||||
onClick: () => this.collapseDMList(),
|
onClick: () => this.collapseDMList(),
|
||||||
role: "button",
|
role: "button",
|
||||||
style: { cursor: "pointer" }
|
style: { cursor: "pointer" }
|
||||||
};
|
} : undefined;
|
||||||
},
|
},
|
||||||
|
|
||||||
getChunkSize() {
|
getChunkSize() {
|
||||||
|
@ -210,30 +217,27 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
|
|
||||||
isChannelIndex(sectionIndex: number, channelIndex: number) {
|
isChannelIndex(sectionIndex: number, channelIndex: number) {
|
||||||
if (settings.store.dmSectioncollapsed && sectionIndex !== 0)
|
if (settings.store.canCollapseDmSection && settings.store.dmSectionCollapsed && sectionIndex !== 0) {
|
||||||
return true;
|
return true;
|
||||||
const cat = categories[sectionIndex - 1];
|
}
|
||||||
return this.isCategoryIndex(sectionIndex) && (cat?.channels?.length === 0 || cat?.channels[channelIndex]);
|
|
||||||
},
|
|
||||||
|
|
||||||
isDMSectioncollapsed() {
|
const category = getCategoryByIndex(sectionIndex - 1);
|
||||||
return settings.store.dmSectioncollapsed;
|
return this.isCategoryIndex(sectionIndex) && (category?.channels?.length === 0 || category?.channels[channelIndex]);
|
||||||
},
|
},
|
||||||
|
|
||||||
collapseDMList() {
|
collapseDMList() {
|
||||||
settings.store.dmSectioncollapsed = !settings.store.dmSectioncollapsed;
|
settings.store.dmSectionCollapsed = !settings.store.dmSectionCollapsed;
|
||||||
forceUpdate();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
isChannelHidden(categoryIndex: number, channelIndex: number) {
|
isChannelHidden(categoryIndex: number, channelIndex: number) {
|
||||||
if (categoryIndex === 0) return false;
|
if (categoryIndex === 0) return false;
|
||||||
|
|
||||||
if (settings.store.dmSectioncollapsed && this.getSections().length + 1 === categoryIndex)
|
if (settings.store.canCollapseDmSection && settings.store.dmSectionCollapsed && this.getSections().length + 1 === categoryIndex)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
if (!this.instance || !this.isChannelIndex(categoryIndex, channelIndex)) return false;
|
if (!this.instance || !this.isChannelIndex(categoryIndex, channelIndex)) return false;
|
||||||
|
|
||||||
const category = categories[categoryIndex - 1];
|
const category = getCategoryByIndex(categoryIndex - 1);
|
||||||
if (!category) return false;
|
if (!category) return false;
|
||||||
|
|
||||||
return category.collapsed && this.instance.props.selectedChannelId !== this.getCategoryChannels(category)[channelIndex];
|
return category.collapsed && this.instance.props.selectedChannelId !== this.getCategoryChannels(category)[channelIndex];
|
||||||
|
@ -251,18 +255,12 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
|
|
||||||
renderCategory: ErrorBoundary.wrap(({ section }: { section: number; }) => {
|
renderCategory: ErrorBoundary.wrap(({ section }: { section: number; }) => {
|
||||||
const category = categories[section - 1];
|
const category = getCategoryByIndex(section - 1);
|
||||||
|
|
||||||
if (!category) return null;
|
if (!category) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<h2
|
<Clickable
|
||||||
className={classes(headerClasses.privateChannelsHeaderContainer, "vc-pindms-section-container", category.collapsed ? "vc-pindms-collapsed" : "")}
|
onClick={() => collapseCategory(category.id, !category.collapsed)}
|
||||||
style={{ color: `#${category.color.toString(16).padStart(6, "0")}` }}
|
|
||||||
onClick={async () => {
|
|
||||||
await collapseCategory(category.id, !category.collapsed);
|
|
||||||
forceUpdate();
|
|
||||||
}}
|
|
||||||
onContextMenu={e => {
|
onContextMenu={e => {
|
||||||
ContextMenuApi.openContextMenu(e, () => (
|
ContextMenuApi.openContextMenu(e, () => (
|
||||||
<Menu.Menu
|
<Menu.Menu
|
||||||
|
@ -284,14 +282,14 @@ export default definePlugin({
|
||||||
canMoveCategoryInDirection(category.id, -1) && <Menu.MenuItem
|
canMoveCategoryInDirection(category.id, -1) && <Menu.MenuItem
|
||||||
id="vc-pindms-move-category-up"
|
id="vc-pindms-move-category-up"
|
||||||
label="Move Up"
|
label="Move Up"
|
||||||
action={() => moveCategory(category.id, -1).then(() => forceUpdate())}
|
action={() => moveCategory(category.id, -1)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
canMoveCategoryInDirection(category.id, 1) && <Menu.MenuItem
|
canMoveCategoryInDirection(category.id, 1) && <Menu.MenuItem
|
||||||
id="vc-pindms-move-category-down"
|
id="vc-pindms-move-category-down"
|
||||||
label="Move Down"
|
label="Move Down"
|
||||||
action={() => moveCategory(category.id, 1).then(() => forceUpdate())}
|
action={() => moveCategory(category.id, 1)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
|
@ -304,13 +302,17 @@ export default definePlugin({
|
||||||
id="vc-pindms-delete-category"
|
id="vc-pindms-delete-category"
|
||||||
color="danger"
|
color="danger"
|
||||||
label="Delete Category"
|
label="Delete Category"
|
||||||
action={() => removeCategory(category.id).then(() => forceUpdate())}
|
action={() => removeCategory(category.id)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
</Menu.Menu>
|
</Menu.Menu>
|
||||||
));
|
));
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
<h2
|
||||||
|
className={classes(headerClasses.privateChannelsHeaderContainer, "vc-pindms-section-container", category.collapsed ? "vc-pindms-collapsed" : "")}
|
||||||
|
style={{ color: `#${category.color.toString(16).padStart(6, "0")}` }}
|
||||||
>
|
>
|
||||||
<span className={headerClasses.headerText}>
|
<span className={headerClasses.headerText}>
|
||||||
{category?.name ?? "uh oh"}
|
{category?.name ?? "uh oh"}
|
||||||
|
@ -319,6 +321,7 @@ export default definePlugin({
|
||||||
<path fill="currentColor" d="M9.3 5.3a1 1 0 0 0 0 1.4l5.29 5.3-5.3 5.3a1 1 0 1 0 1.42 1.4l6-6a1 1 0 0 0 0-1.4l-6-6a1 1 0 0 0-1.42 0Z"></path>
|
<path fill="currentColor" d="M9.3 5.3a1 1 0 0 0 0 1.4l5.29 5.3-5.3 5.3a1 1 0 1 0 1.42 1.4l6-6a1 1 0 0 0 0-1.4l-6-6a1 1 0 0 0-1.42 0Z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</h2>
|
</h2>
|
||||||
|
</Clickable>
|
||||||
);
|
);
|
||||||
}, { noop: true }),
|
}, { noop: true }),
|
||||||
|
|
||||||
|
@ -341,7 +344,7 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
|
|
||||||
getChannel(sectionIndex: number, index: number, channels: Record<string, Channel>) {
|
getChannel(sectionIndex: number, index: number, channels: Record<string, Channel>) {
|
||||||
const category = categories[sectionIndex - 1];
|
const category = getCategoryByIndex(sectionIndex - 1);
|
||||||
if (!category) return { channel: null, category: null };
|
if (!category) return { channel: null, category: null };
|
||||||
|
|
||||||
const channelId = this.getCategoryChannels(category)[index];
|
const channelId = this.getCategoryChannels(category)[index];
|
||||||
|
|
|
@ -22,7 +22,6 @@ import { Flex } from "@components/Flex";
|
||||||
import { DeleteIcon } from "@components/Icons";
|
import { DeleteIcon } from "@components/Icons";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { Logger } from "@utils/Logger";
|
import { Logger } from "@utils/Logger";
|
||||||
import { useForceUpdater } from "@utils/react";
|
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { Button, Forms, React, TextInput, useState } from "@webpack/common";
|
import { Button, Forms, React, TextInput, useState } from "@webpack/common";
|
||||||
|
|
||||||
|
@ -34,8 +33,6 @@ type Rule = Record<"find" | "replace" | "onlyIfIncludes", string>;
|
||||||
interface TextReplaceProps {
|
interface TextReplaceProps {
|
||||||
title: string;
|
title: string;
|
||||||
rulesArray: Rule[];
|
rulesArray: Rule[];
|
||||||
rulesKey: string;
|
|
||||||
update: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const makeEmptyRule: () => Rule = () => ({
|
const makeEmptyRule: () => Rule = () => ({
|
||||||
|
@ -45,34 +42,36 @@ const makeEmptyRule: () => Rule = () => ({
|
||||||
});
|
});
|
||||||
const makeEmptyRuleArray = () => [makeEmptyRule()];
|
const makeEmptyRuleArray = () => [makeEmptyRule()];
|
||||||
|
|
||||||
let stringRules = makeEmptyRuleArray();
|
|
||||||
let regexRules = makeEmptyRuleArray();
|
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
const settings = definePluginSettings({
|
||||||
replace: {
|
replace: {
|
||||||
type: OptionType.COMPONENT,
|
type: OptionType.COMPONENT,
|
||||||
description: "",
|
description: "",
|
||||||
component: () => {
|
component: () => {
|
||||||
const update = useForceUpdater();
|
const { stringRules, regexRules } = settings.use(["stringRules", "regexRules"]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TextReplace
|
<TextReplace
|
||||||
title="Using String"
|
title="Using String"
|
||||||
rulesArray={stringRules}
|
rulesArray={stringRules}
|
||||||
rulesKey={STRING_RULES_KEY}
|
|
||||||
update={update}
|
|
||||||
/>
|
/>
|
||||||
<TextReplace
|
<TextReplace
|
||||||
title="Using Regex"
|
title="Using Regex"
|
||||||
rulesArray={regexRules}
|
rulesArray={regexRules}
|
||||||
rulesKey={REGEX_RULES_KEY}
|
|
||||||
update={update}
|
|
||||||
/>
|
/>
|
||||||
<TextReplaceTesting />
|
<TextReplaceTesting />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
stringRules: {
|
||||||
|
type: OptionType.CUSTOM,
|
||||||
|
default: makeEmptyRuleArray(),
|
||||||
|
},
|
||||||
|
regexRules: {
|
||||||
|
type: OptionType.CUSTOM,
|
||||||
|
default: makeEmptyRuleArray(),
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function stringToRegex(str: string) {
|
function stringToRegex(str: string) {
|
||||||
|
@ -119,28 +118,24 @@ function Input({ initialValue, onChange, placeholder }: {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TextReplace({ title, rulesArray, rulesKey, update }: TextReplaceProps) {
|
function TextReplace({ title, rulesArray }: TextReplaceProps) {
|
||||||
const isRegexRules = title === "Using Regex";
|
const isRegexRules = title === "Using Regex";
|
||||||
|
|
||||||
async function onClickRemove(index: number) {
|
async function onClickRemove(index: number) {
|
||||||
if (index === rulesArray.length - 1) return;
|
if (index === rulesArray.length - 1) return;
|
||||||
rulesArray.splice(index, 1);
|
rulesArray.splice(index, 1);
|
||||||
|
|
||||||
await DataStore.set(rulesKey, rulesArray);
|
|
||||||
update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onChange(e: string, index: number, key: string) {
|
async function onChange(e: string, index: number, key: string) {
|
||||||
if (index === rulesArray.length - 1)
|
if (index === rulesArray.length - 1) {
|
||||||
rulesArray.push(makeEmptyRule());
|
rulesArray.push(makeEmptyRule());
|
||||||
|
}
|
||||||
|
|
||||||
rulesArray[index][key] = e;
|
rulesArray[index][key] = e;
|
||||||
|
|
||||||
if (rulesArray[index].find === "" && rulesArray[index].replace === "" && rulesArray[index].onlyIfIncludes === "" && index !== rulesArray.length - 1)
|
if (rulesArray[index].find === "" && rulesArray[index].replace === "" && rulesArray[index].onlyIfIncludes === "" && index !== rulesArray.length - 1) {
|
||||||
rulesArray.splice(index, 1);
|
rulesArray.splice(index, 1);
|
||||||
|
}
|
||||||
await DataStore.set(rulesKey, rulesArray);
|
|
||||||
update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -207,20 +202,18 @@ function TextReplaceTesting() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyRules(content: string): string {
|
function applyRules(content: string): string {
|
||||||
if (content.length === 0)
|
if (content.length === 0) {
|
||||||
return content;
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
if (stringRules) {
|
for (const rule of settings.store.stringRules) {
|
||||||
for (const rule of stringRules) {
|
|
||||||
if (!rule.find) continue;
|
if (!rule.find) continue;
|
||||||
if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue;
|
if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue;
|
||||||
|
|
||||||
content = ` ${content} `.replaceAll(rule.find, rule.replace.replaceAll("\\n", "\n")).replace(/^\s|\s$/g, "");
|
content = ` ${content} `.replaceAll(rule.find, rule.replace.replaceAll("\\n", "\n")).replace(/^\s|\s$/g, "");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (regexRules) {
|
for (const rule of settings.store.regexRules) {
|
||||||
for (const rule of regexRules) {
|
|
||||||
if (!rule.find) continue;
|
if (!rule.find) continue;
|
||||||
if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue;
|
if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue;
|
||||||
|
|
||||||
|
@ -231,7 +224,6 @@ function applyRules(content: string): string {
|
||||||
new Logger("TextReplace").error(`Invalid regex: ${rule.find}`);
|
new Logger("TextReplace").error(`Invalid regex: ${rule.find}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
content = content.trim();
|
content = content.trim();
|
||||||
return content;
|
return content;
|
||||||
|
@ -253,7 +245,17 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
stringRules = await DataStore.get(STRING_RULES_KEY) ?? makeEmptyRuleArray();
|
// TODO: Remove DataStore rules migrations once enough time has passed
|
||||||
regexRules = await DataStore.get(REGEX_RULES_KEY) ?? makeEmptyRuleArray();
|
const oldStringRules = await DataStore.get<Rule[]>(STRING_RULES_KEY);
|
||||||
|
if (oldStringRules != null) {
|
||||||
|
settings.store.stringRules = oldStringRules;
|
||||||
|
await DataStore.del(STRING_RULES_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldRegexRules = await DataStore.get<Rule[]>(REGEX_RULES_KEY);
|
||||||
|
if (oldRegexRules != null) {
|
||||||
|
settings.store.regexRules = oldRegexRules;
|
||||||
|
await DataStore.del(REGEX_RULES_KEY);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,6 +6,9 @@
|
||||||
|
|
||||||
import { LiteralUnion } from "type-fest";
|
import { LiteralUnion } from "type-fest";
|
||||||
|
|
||||||
|
export const SYM_IS_PROXY = Symbol("SettingsStore.isProxy");
|
||||||
|
export const SYM_GET_RAW_TARGET = Symbol("SettingsStore.getRawTarget");
|
||||||
|
|
||||||
// Resolves a possibly nested prop in the form of "some.nested.prop" to type of T.some.nested.prop
|
// Resolves a possibly nested prop in the form of "some.nested.prop" to type of T.some.nested.prop
|
||||||
type ResolvePropDeep<T, P> = P extends `${infer Pre}.${infer Suf}`
|
type ResolvePropDeep<T, P> = P extends `${infer Pre}.${infer Suf}`
|
||||||
? Pre extends keyof T
|
? Pre extends keyof T
|
||||||
|
@ -28,6 +31,11 @@ interface SettingsStoreOptions {
|
||||||
// merges the SettingsStoreOptions type into the class
|
// merges the SettingsStoreOptions type into the class
|
||||||
export interface SettingsStore<T extends object> extends SettingsStoreOptions { }
|
export interface SettingsStore<T extends object> extends SettingsStoreOptions { }
|
||||||
|
|
||||||
|
interface ProxyContext<T extends object = any> {
|
||||||
|
root: T;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The SettingsStore allows you to easily create a mutable store that
|
* The SettingsStore allows you to easily create a mutable store that
|
||||||
* has support for global and path-based change listeners.
|
* has support for global and path-based change listeners.
|
||||||
|
@ -35,6 +43,90 @@ export interface SettingsStore<T extends object> extends SettingsStoreOptions {
|
||||||
export class SettingsStore<T extends object> {
|
export class SettingsStore<T extends object> {
|
||||||
private pathListeners = new Map<string, Set<(newData: any) => void>>();
|
private pathListeners = new Map<string, Set<(newData: any) => void>>();
|
||||||
private globalListeners = new Set<(newData: T, path: string) => void>();
|
private globalListeners = new Set<(newData: T, path: string) => void>();
|
||||||
|
private readonly proxyContexts = new WeakMap<any, ProxyContext<T>>();
|
||||||
|
|
||||||
|
private readonly proxyHandler: ProxyHandler<any> = (() => {
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
return {
|
||||||
|
get(target, key: any, receiver) {
|
||||||
|
if (key === SYM_IS_PROXY) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === SYM_GET_RAW_TARGET) {
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
let v = Reflect.get(target, key, receiver);
|
||||||
|
|
||||||
|
const proxyContext = self.proxyContexts.get(target);
|
||||||
|
if (proxyContext == null) {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { root, path } = proxyContext;
|
||||||
|
|
||||||
|
if (!(key in target) && self.getDefaultValue != null) {
|
||||||
|
v = self.getDefaultValue({
|
||||||
|
target,
|
||||||
|
key,
|
||||||
|
root,
|
||||||
|
path
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof v === "object" && v !== null && !v[SYM_IS_PROXY]) {
|
||||||
|
const getPath = `${path}${path && "."}${key}`;
|
||||||
|
return self.makeProxy(v, root, getPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return v;
|
||||||
|
},
|
||||||
|
set(target, key: string, value) {
|
||||||
|
if (value?.[SYM_IS_PROXY]) {
|
||||||
|
value = value[SYM_GET_RAW_TARGET];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target[key] === value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Reflect.set(target, key, value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const proxyContext = self.proxyContexts.get(target);
|
||||||
|
if (proxyContext == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { root, path } = proxyContext;
|
||||||
|
|
||||||
|
const setPath = `${path}${path && "."}${key}`;
|
||||||
|
self.notifyListeners(setPath, value, root);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
deleteProperty(target, key: string) {
|
||||||
|
if (!Reflect.deleteProperty(target, key)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const proxyContext = self.proxyContexts.get(target);
|
||||||
|
if (proxyContext == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { root, path } = proxyContext;
|
||||||
|
|
||||||
|
const deletePath = `${path}${path && "."}${key}`;
|
||||||
|
self.notifyListeners(deletePath, undefined, root);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The store object. Making changes to this object will trigger the applicable change listeners
|
* The store object. Making changes to this object will trigger the applicable change listeners
|
||||||
|
@ -51,39 +143,33 @@ export class SettingsStore<T extends object> {
|
||||||
Object.assign(this, options);
|
Object.assign(this, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
private makeProxy(object: any, root: T = object, path: string = "") {
|
private makeProxy(object: any, root: T = object, path = "") {
|
||||||
const self = this;
|
this.proxyContexts.set(object, {
|
||||||
|
|
||||||
return new Proxy(object, {
|
|
||||||
get(target, key: string) {
|
|
||||||
let v = target[key];
|
|
||||||
|
|
||||||
if (!(key in target) && self.getDefaultValue) {
|
|
||||||
v = self.getDefaultValue({
|
|
||||||
target,
|
|
||||||
key,
|
|
||||||
root,
|
root,
|
||||||
path
|
path
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return new Proxy(object, this.proxyHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof v === "object" && v !== null && !Array.isArray(v))
|
private notifyListeners(pathStr: string, value: any, root: T) {
|
||||||
return self.makeProxy(v, root, `${path}${path && "."}${key}`);
|
const paths = pathStr.split(".");
|
||||||
|
|
||||||
return v;
|
// Because we support any type of settings with OptionType.CUSTOM, and those objects get proxied recursively,
|
||||||
},
|
// the path ends up including all the nested paths (plugins.pluginName.settingName.example.one).
|
||||||
set(target, key: string, value) {
|
// So, we need to extract the top-level setting path (plugins.pluginName.settingName),
|
||||||
if (target[key] === value) return true;
|
// to be able to notify globalListeners and top-level setting name listeners (let { settingName } = settings.use(["settingName"]),
|
||||||
|
// with the new value
|
||||||
|
if (paths.length > 2 && paths[0] === "plugins") {
|
||||||
|
const settingPath = paths.slice(0, 3);
|
||||||
|
const settingPathStr = settingPath.join(".");
|
||||||
|
const settingValue = settingPath.reduce((acc, curr) => acc[curr], root);
|
||||||
|
|
||||||
Reflect.set(target, key, value);
|
this.globalListeners.forEach(cb => cb(root, settingPathStr));
|
||||||
const setPath = `${path}${path && "."}${key}`;
|
this.pathListeners.get(settingPathStr)?.forEach(cb => cb(settingValue));
|
||||||
|
|
||||||
self.globalListeners.forEach(cb => cb(value, setPath));
|
|
||||||
self.pathListeners.get(setPath)?.forEach(cb => cb(value));
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
this.pathListeners.get(pathStr)?.forEach(cb => cb(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -189,6 +189,7 @@ export const enum OptionType {
|
||||||
SELECT,
|
SELECT,
|
||||||
SLIDER,
|
SLIDER,
|
||||||
COMPONENT,
|
COMPONENT,
|
||||||
|
CUSTOM
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SettingsDefinition = Record<string, PluginSettingDef>;
|
export type SettingsDefinition = Record<string, PluginSettingDef>;
|
||||||
|
@ -197,7 +198,7 @@ export type SettingsChecks<D extends SettingsDefinition> = {
|
||||||
(IsDisabled<DefinedSettings<D>> & IsValid<PluginSettingType<D[K]>, DefinedSettings<D>>);
|
(IsDisabled<DefinedSettings<D>> & IsValid<PluginSettingType<D[K]>, DefinedSettings<D>>);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PluginSettingDef = (
|
export type PluginSettingDef = (PluginSettingCustomDef & Pick<PluginSettingCommon, "onChange">) | ((
|
||||||
| PluginSettingStringDef
|
| PluginSettingStringDef
|
||||||
| PluginSettingNumberDef
|
| PluginSettingNumberDef
|
||||||
| PluginSettingBooleanDef
|
| PluginSettingBooleanDef
|
||||||
|
@ -205,7 +206,7 @@ export type PluginSettingDef = (
|
||||||
| PluginSettingSliderDef
|
| PluginSettingSliderDef
|
||||||
| PluginSettingComponentDef
|
| PluginSettingComponentDef
|
||||||
| PluginSettingBigIntDef
|
| PluginSettingBigIntDef
|
||||||
) & PluginSettingCommon;
|
) & PluginSettingCommon);
|
||||||
|
|
||||||
export interface PluginSettingCommon {
|
export interface PluginSettingCommon {
|
||||||
description: string;
|
description: string;
|
||||||
|
@ -259,12 +260,18 @@ export interface PluginSettingSelectDef {
|
||||||
type: OptionType.SELECT;
|
type: OptionType.SELECT;
|
||||||
options: readonly PluginSettingSelectOption[];
|
options: readonly PluginSettingSelectOption[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PluginSettingSelectOption {
|
export interface PluginSettingSelectOption {
|
||||||
label: string;
|
label: string;
|
||||||
value: string | number | boolean;
|
value: string | number | boolean;
|
||||||
default?: boolean;
|
default?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PluginSettingCustomDef {
|
||||||
|
type: OptionType.CUSTOM;
|
||||||
|
default?: any;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PluginSettingSliderDef {
|
export interface PluginSettingSliderDef {
|
||||||
type: OptionType.SLIDER;
|
type: OptionType.SLIDER;
|
||||||
/**
|
/**
|
||||||
|
@ -314,7 +321,9 @@ type PluginSettingType<O extends PluginSettingDef> = O extends PluginSettingStri
|
||||||
O extends PluginSettingSelectDef ? O["options"][number]["value"] :
|
O extends PluginSettingSelectDef ? O["options"][number]["value"] :
|
||||||
O extends PluginSettingSliderDef ? number :
|
O extends PluginSettingSliderDef ? number :
|
||||||
O extends PluginSettingComponentDef ? any :
|
O extends PluginSettingComponentDef ? any :
|
||||||
|
O extends PluginSettingCustomDef ? O extends { default: infer Default; } ? Default : any :
|
||||||
never;
|
never;
|
||||||
|
|
||||||
type PluginSettingDefaultType<O extends PluginSettingDef> = O extends PluginSettingSelectDef ? (
|
type PluginSettingDefaultType<O extends PluginSettingDef> = O extends PluginSettingSelectDef ? (
|
||||||
O["options"] extends { default?: boolean; }[] ? O["options"][number]["value"] : undefined
|
O["options"] extends { default?: boolean; }[] ? O["options"][number]["value"] : undefined
|
||||||
) : O extends { default: infer T; } ? T : undefined;
|
) : O extends { default: infer T; } ? T : undefined;
|
||||||
|
@ -366,13 +375,15 @@ export type PluginOptionsItem =
|
||||||
| PluginOptionBoolean
|
| PluginOptionBoolean
|
||||||
| PluginOptionSelect
|
| PluginOptionSelect
|
||||||
| PluginOptionSlider
|
| PluginOptionSlider
|
||||||
| PluginOptionComponent;
|
| PluginOptionComponent
|
||||||
|
| PluginOptionCustom;
|
||||||
export type PluginOptionString = PluginSettingStringDef & PluginSettingCommon & IsDisabled & IsValid<string>;
|
export type PluginOptionString = PluginSettingStringDef & PluginSettingCommon & IsDisabled & IsValid<string>;
|
||||||
export type PluginOptionNumber = (PluginSettingNumberDef | PluginSettingBigIntDef) & PluginSettingCommon & IsDisabled & IsValid<number | BigInt>;
|
export type PluginOptionNumber = (PluginSettingNumberDef | PluginSettingBigIntDef) & PluginSettingCommon & IsDisabled & IsValid<number | BigInt>;
|
||||||
export type PluginOptionBoolean = PluginSettingBooleanDef & PluginSettingCommon & IsDisabled & IsValid<boolean>;
|
export type PluginOptionBoolean = PluginSettingBooleanDef & PluginSettingCommon & IsDisabled & IsValid<boolean>;
|
||||||
export type PluginOptionSelect = PluginSettingSelectDef & PluginSettingCommon & IsDisabled & IsValid<PluginSettingSelectOption>;
|
export type PluginOptionSelect = PluginSettingSelectDef & PluginSettingCommon & IsDisabled & IsValid<PluginSettingSelectOption>;
|
||||||
export type PluginOptionSlider = PluginSettingSliderDef & PluginSettingCommon & IsDisabled & IsValid<number>;
|
export type PluginOptionSlider = PluginSettingSliderDef & PluginSettingCommon & IsDisabled & IsValid<number>;
|
||||||
export type PluginOptionComponent = PluginSettingComponentDef & PluginSettingCommon;
|
export type PluginOptionComponent = PluginSettingComponentDef & PluginSettingCommon;
|
||||||
|
export type PluginOptionCustom = PluginSettingCustomDef & Pick<PluginSettingCommon, "onChange">;
|
||||||
|
|
||||||
export type PluginNative<PluginExports extends Record<string, (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any>> = {
|
export type PluginNative<PluginExports extends Record<string, (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any>> = {
|
||||||
[key in keyof PluginExports]:
|
[key in keyof PluginExports]:
|
||||||
|
|
14
src/webpack/common/types/components.d.ts
vendored
14
src/webpack/common/types/components.d.ts
vendored
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ComponentType, CSSProperties, FunctionComponent, HtmlHTMLAttributes, HTMLProps, KeyboardEvent, MouseEvent, PropsWithChildren, PropsWithRef, ReactNode, Ref } from "react";
|
import type { ComponentPropsWithRef, ComponentType, CSSProperties, FunctionComponent, HtmlHTMLAttributes, HTMLProps, JSX, KeyboardEvent, MouseEvent, PropsWithChildren, PropsWithRef, ReactNode, Ref } from "react";
|
||||||
|
|
||||||
import { IconNames } from "./iconNames";
|
import { IconNames } from "./iconNames";
|
||||||
|
|
||||||
|
@ -471,15 +471,9 @@ export type ScrollerThin = ComponentType<PropsWithChildren<{
|
||||||
onScroll?(): void;
|
onScroll?(): void;
|
||||||
}>>;
|
}>>;
|
||||||
|
|
||||||
export type Clickable = ComponentType<PropsWithChildren<{
|
export type Clickable = <T extends "a" | "div" | "span" | "li" = "div">(props: PropsWithChildren<ComponentPropsWithRef<T>> & {
|
||||||
className?: string;
|
tag?: T;
|
||||||
|
}) => ReactNode;
|
||||||
href?: string;
|
|
||||||
ignoreKeyPress?: boolean;
|
|
||||||
|
|
||||||
onClick?(): void;
|
|
||||||
onKeyPress?(): void;
|
|
||||||
}>>;
|
|
||||||
|
|
||||||
export type Avatar = ComponentType<PropsWithChildren<{
|
export type Avatar = ComponentType<PropsWithChildren<{
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|
Loading…
Reference in a new issue