CustomVoiceFilter: Refactored UIs

This commit is contained in:
fox3000foxy 2025-02-22 22:51:20 +01:00
parent cff7492f37
commit ba431e78b1
3 changed files with 216 additions and 211 deletions

View file

@ -1,111 +1,113 @@
/* /*
* Vencord, a Discord client mod * Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors * Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { Button, Flex, Forms, Select, TextInput, useCallback, useMemo, UserStore, useState } from "@webpack/common"; import { Button, Flex, Forms, Select, TextInput, useCallback, useMemo, UserStore, useState } from "@webpack/common";
import { SelectOption } from "@webpack/types"; import { SelectOption } from "@webpack/types";
import { JSX } from "react"; import { JSX } from "react";
import { voices } from "."; import { voices } from ".";
import { openErrorModal } from "./ErrorModal"; import { openErrorModal } from "./ErrorModal";
import { IVoiceFilter, useVoiceFiltersStore } from "./index"; import { openHelpModal } from "./HelpModal";
const requiredFields = ["name", "iconURL", "onnxFileUrl", "previewSoundURLs"] as const satisfies readonly (keyof IVoiceFilter)[]; import { IVoiceFilter, useVoiceFiltersStore } from "./index";
const requiredFields = ["name", "iconURL", "onnxFileUrl", "previewSoundURLs"] as const satisfies readonly (keyof IVoiceFilter)[];
export function openCreateVoiceModal(defaultValue?: Partial<IVoiceFilter>): string {
const key = openModal(modalProps => ( export function openCreateVoiceModal(defaultValue?: Partial<IVoiceFilter>): string {
<CreateVoiceFilterModal modalProps={modalProps} close={() => closeModal(key)} defaultValue={defaultValue} /> const key = openModal(modalProps => (
)); <CreateVoiceFilterModal modalProps={modalProps} close={() => closeModal(key)} defaultValue={defaultValue} />
return key; ));
} return key;
}
interface CreateVoiceFilterModalProps {
modalProps: ModalProps; interface CreateVoiceFilterModalProps {
close: () => void; modalProps: ModalProps;
defaultValue?: Partial<IVoiceFilter>; close: () => void;
} defaultValue?: Partial<IVoiceFilter>;
}
// Create Voice Filter Modal
function CreateVoiceFilterModal({ modalProps, close, defaultValue }: CreateVoiceFilterModalProps): JSX.Element { // Create Voice Filter Modal
const currentUser = useMemo(() => UserStore.getCurrentUser(), []); function CreateVoiceFilterModal({ modalProps, close, defaultValue }: CreateVoiceFilterModalProps): JSX.Element {
const [voiceFilter, setVoiceFilter] = useState(() => ( const currentUser = useMemo(() => UserStore.getCurrentUser(), []);
{ author: currentUser.id, name: "", iconURL: "", styleKey: "", onnxFileUrl: "", ...defaultValue } const [voiceFilter, setVoiceFilter] = useState(() => (
)); { author: currentUser.id, name: "", iconURL: "", styleKey: "", onnxFileUrl: "", ...defaultValue }
));
const update = useCallback(<K extends keyof IVoiceFilter>(value: IVoiceFilter[K], key: K) => {
setVoiceFilter(prev => ({ ...prev, [key]: value })); const update = useCallback(<K extends keyof IVoiceFilter>(value: IVoiceFilter[K], key: K) => {
}, []); setVoiceFilter(prev => ({ ...prev, [key]: value }));
const submit = useCallback(() => { }, []);
if (requiredFields.every(field => voiceFilter[field])) { const submit = useCallback(() => {
useVoiceFiltersStore.getState().downloadVoicepack(JSON.stringify({ if (requiredFields.every(field => voiceFilter[field])) {
id: voiceFilter.author + "-" + voiceFilter.name.toLowerCase().replace(/ /g, "-"), useVoiceFiltersStore.getState().downloadVoicepack(JSON.stringify({
available: true, id: voiceFilter.author + "-" + voiceFilter.name.toLowerCase().replace(/ /g, "-"),
temporarilyAvailable: false, available: true,
custom: true, temporarilyAvailable: false,
splashGradient: "radial-gradient(circle, #d9a5a2 0%, rgba(0,0,0,0) 100%)", custom: true,
baseColor: "#d9a5a2", splashGradient: "radial-gradient(circle, #d9a5a2 0%, rgba(0,0,0,0) 100%)",
...voiceFilter baseColor: "#d9a5a2",
} satisfies IVoiceFilter)); ...voiceFilter
close(); } satisfies IVoiceFilter));
} else { close();
openErrorModal("Please fill in all required fields"); } else {
} openErrorModal("Please fill in all required fields");
}, [voiceFilter]); }
}, [voiceFilter]);
const keyOptions: SelectOption[] = useMemo(() =>
[{ value: "", label: "(empty)" }, ...(voices ? Object.keys(voices).map(name => ({ value: name, label: name })) : [])], const keyOptions: SelectOption[] = useMemo(() =>
[]); [{ value: "", label: "(empty)" }, ...(voices ? Object.keys(voices).map(name => ({ value: name, label: name })) : [])],
[]);
return (
<ModalRoot {...modalProps} size={ModalSize.MEDIUM}> return (
<ModalHeader> <ModalRoot {...modalProps} size={ModalSize.MEDIUM}>
<Forms.FormTitle tag="h2" className="modalTitle"> <ModalHeader>
{voiceFilter.id ? "Edit a voice filter" : "Create a voice filter"} <Forms.FormTitle tag="h2" className="modalTitle">
</Forms.FormTitle> {voiceFilter.id ? "Edit a voice filter" : "Create a voice filter"}
<ModalCloseButton onClick={close} /> </Forms.FormTitle>
</ModalHeader> <ModalCloseButton onClick={close} />
<ModalContent className="vc-voice-filters-modal"> </ModalHeader>
<Flex style={{ gap: "1rem" }} direction={Flex.Direction.VERTICAL}> <ModalContent className="vc-voice-filters-modal">
<Forms.FormSection> <Flex style={{ gap: "1rem" }} direction={Flex.Direction.VERTICAL}>
<Forms.FormTitle>Name<span style={{ color: "var(--text-danger)" }}>*</span></Forms.FormTitle> <Forms.FormSection>
<TextInput placeholder="Model" onChange={update} style={{ width: "100%" }} value={voiceFilter.name} name="name" required /> <Forms.FormTitle>Name<span style={{ color: "var(--text-danger)" }}>*</span></Forms.FormTitle>
</Forms.FormSection> <TextInput placeholder="Model" onChange={update} style={{ width: "100%" }} value={voiceFilter.name} name="name" required />
<Forms.FormSection> </Forms.FormSection>
<Forms.FormTitle>Icon URL<span style={{ color: "var(--text-danger)" }}>*</span></Forms.FormTitle> <Forms.FormSection>
<TextInput placeholder="https://example.com/voicepacks/model/icon.png" onChange={update} style={{ width: "100%" }} value={voiceFilter.iconURL} name="iconURL" required /> <Forms.FormTitle>Icon URL<span style={{ color: "var(--text-danger)" }}>*</span></Forms.FormTitle>
</Forms.FormSection> <TextInput placeholder="https://example.com/voicepacks/model/icon.png" onChange={update} style={{ width: "100%" }} value={voiceFilter.iconURL} name="iconURL" required />
<Forms.FormSection> </Forms.FormSection>
<Forms.FormTitle>Style Key</Forms.FormTitle> <Forms.FormSection>
<Select <Forms.FormTitle>Style Key</Forms.FormTitle>
options={keyOptions} <Select
placeholder={"Select an option"} options={keyOptions}
maxVisibleItems={5} placeholder={"Select an option"}
closeOnSelect={true} maxVisibleItems={5}
select={value => update(value, "styleKey")} closeOnSelect={true}
isSelected={v => v === voiceFilter.styleKey} select={value => update(value, "styleKey")}
serialize={String} isSelected={v => v === voiceFilter.styleKey}
/> serialize={String}
</Forms.FormSection> />
<Forms.FormSection> </Forms.FormSection>
<Forms.FormTitle>ONNX File URL<span style={{ color: "var(--text-danger)" }}>*</span></Forms.FormTitle> <Forms.FormSection>
<TextInput placeholder="https://example.com/voicepacks/model/model.onnx" onChange={update} style={{ width: "100%" }} value={voiceFilter.onnxFileUrl} name="onnxFileUrl" required /> <Forms.FormTitle>ONNX File URL<span style={{ color: "var(--text-danger)" }}>*</span></Forms.FormTitle>
</Forms.FormSection> <TextInput placeholder="https://example.com/voicepacks/model/model.onnx" onChange={update} style={{ width: "100%" }} value={voiceFilter.onnxFileUrl} name="onnxFileUrl" required />
<Forms.FormSection> </Forms.FormSection>
<Forms.FormTitle>Preview Sound URL<span style={{ color: "var(--text-danger)" }}>*</span></Forms.FormTitle> <Forms.FormSection>
<TextInput placeholder="https://example.com/voicepacks/model/preview.mp3" onChange={value => update(value ? [value] : undefined, "previewSoundURLs")} style={{ width: "100%" }} value={voiceFilter.previewSoundURLs?.[0] ?? ""} required /> <Forms.FormTitle>Preview Sound URL<span style={{ color: "var(--text-danger)" }}>*</span></Forms.FormTitle>
</Forms.FormSection> <TextInput placeholder="https://example.com/voicepacks/model/preview.mp3" onChange={value => update(value ? [value] : undefined, "previewSoundURLs")} style={{ width: "100%" }} value={voiceFilter.previewSoundURLs?.[0] ?? ""} required />
</Flex> </Forms.FormSection>
</ModalContent> </Flex>
<ModalFooter> </ModalContent>
<Flex style={{ gap: "0.5rem" }} justify={Flex.Justify.END} align={Flex.Align.CENTER}> <ModalFooter>
<Button color={Button.Colors.TRANSPARENT} onClick={close} >Cancel</Button> <Flex style={{ gap: "0.5rem" }} justify={Flex.Justify.END} align={Flex.Align.CENTER}>
<Button color={Button.Colors.GREEN} onClick={submit}>{voiceFilter.id ? "Save" : "Create"}</Button> <Button color={Button.Colors.TRANSPARENT} onClick={openHelpModal}>How to create a voicepack?</Button>
</Flex> <Button color={Button.Colors.GREEN} onClick={submit}>{voiceFilter.id ? "Save" : "Create"}</Button>
</ModalFooter> <Button color={Button.Colors.TRANSPARENT} onClick={close} >Cancel</Button>
</ModalRoot> </Flex>
); </ModalFooter>
} </ModalRoot>
);
}

View file

@ -1,78 +1,93 @@
/* /*
* Vencord, a Discord client mod * Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors * Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { Button, Flex, Forms, Slider } from "@webpack/common"; import { PluginNative } from "@utils/types";
import { JSX } from "react"; import { Button, Flex, Forms, Slider } from "@webpack/common";
import { JSX } from "react";
import plugin, { settings } from "./index";
import plugin, { settings, useVoiceFiltersStore } from "./index";
export function openSettingsModal(): string { const Native = VencordNative.pluginHelpers.CustomVoiceFilters as PluginNative<typeof import("./native")>;
const key = openModal(modalProps => ( export function openSettingsModal(): string {
<SettingsModal modalProps={modalProps} close={() => closeModal(key)} /> const key = openModal(modalProps => (
)); <SettingsModal modalProps={modalProps} close={() => closeModal(key)} />
return key; ));
} return key;
}
interface SettingsModalProps {
modalProps: ModalProps; interface SettingsModalProps {
close: () => void; modalProps: ModalProps;
} close: () => void;
}
// Create Voice Filter Modal function openModelFolder() {
function SettingsModal({ modalProps, close }: SettingsModalProps): JSX.Element { const { modulePath } = useVoiceFiltersStore.getState();
const settingsState = settings.use(); Native.openFolder(modulePath);
const { settings: { def } } = plugin; }
return ( // Create Voice Filter Modal
<ModalRoot {...modalProps} size={ModalSize.MEDIUM}> function SettingsModal({ modalProps, close }: SettingsModalProps): JSX.Element {
<ModalHeader> const settingsState = settings.use();
<Forms.FormTitle tag="h2" className="modalTitle"> const { settings: { def } } = plugin;
Settings const { deleteAll, exportVoiceFilters, importVoiceFilters } = useVoiceFiltersStore();
</Forms.FormTitle> return (
<ModalCloseButton onClick={close} /> <ModalRoot {...modalProps} size={ModalSize.MEDIUM}>
</ModalHeader> <ModalHeader>
<ModalContent className="vc-voice-filters-modal"> <Forms.FormTitle tag="h2" className="modalTitle">
<Flex style={{ gap: "1rem" }} direction={Flex.Direction.VERTICAL}> Settings
<Forms.FormSection> </Forms.FormTitle>
<Forms.FormTitle>Pitch</Forms.FormTitle> <ModalCloseButton onClick={close} />
<Forms.FormText className={Margins.bottom20} type="description">{def.pitch.description}</Forms.FormText> </ModalHeader>
<Slider <ModalContent className="vc-voice-filters-modal">
markers={def.pitch.markers} <Flex style={{ gap: "1rem" }} direction={Flex.Direction.VERTICAL}>
minValue={def.pitch.markers[0]} <Forms.FormSection>
maxValue={def.pitch.markers.at(-1)} <Forms.FormTitle>Pitch</Forms.FormTitle>
initialValue={settingsState.pitch ?? def.pitch.default} <Forms.FormText className={Margins.bottom20} type="description">{def.pitch.description}</Forms.FormText>
onValueChange={value => settingsState.pitch = value} <Slider
onValueRender={value => `${value}`} markers={def.pitch.markers}
stickToMarkers={true} minValue={def.pitch.markers[0]}
/> maxValue={def.pitch.markers.at(-1)}
</Forms.FormSection> initialValue={settingsState.pitch ?? def.pitch.default}
<Forms.FormSection> onValueChange={value => settingsState.pitch = value}
<Forms.FormTitle>Frequency</Forms.FormTitle> onValueRender={value => `${value}`}
<Forms.FormText className={Margins.bottom20} type="description">{def.frequency.description}</Forms.FormText> stickToMarkers={true}
<Slider />
markers={def.frequency.markers} </Forms.FormSection>
minValue={def.frequency.markers[0]} <Forms.FormSection>
maxValue={def.frequency.markers.at(-1)} <Forms.FormTitle>Frequency</Forms.FormTitle>
initialValue={settingsState.frequency ?? def.frequency.default} <Forms.FormText className={Margins.bottom20} type="description">{def.frequency.description}</Forms.FormText>
onValueChange={value => settingsState.frequency = value} <Slider
onValueRender={value => `${value}Hz`} markers={def.frequency.markers}
stickToMarkers={true} minValue={def.frequency.markers[0]}
/> maxValue={def.frequency.markers.at(-1)}
</Forms.FormSection> initialValue={settingsState.frequency ?? def.frequency.default}
</Flex> onValueChange={value => settingsState.frequency = value}
</ModalContent> onValueRender={value => `${value}Hz`}
<ModalFooter> stickToMarkers={true}
<Flex style={{ gap: "0.5rem" }} justify={Flex.Justify.END} align={Flex.Align.CENTER}> />
<Button color={Button.Colors.GREEN} onClick={close} >Save & Exit</Button> </Forms.FormSection>
</Flex> <Forms.FormSection>
</ModalFooter> <Forms.FormTitle>Voicepacks:</Forms.FormTitle>
</ModalRoot> <Forms.FormText type="description">Here you can manage your voicepacks.</Forms.FormText>
); <Flex style={{ gap: "0.5rem" }}>
} <Button onClick={deleteAll} color={Button.Colors.RED}>Delete all voicepacks</Button>
<Button onClick={openModelFolder} color={Button.Colors.TRANSPARENT}>Open Models Folder</Button>
<Button onClick={exportVoiceFilters} color={Button.Colors.GREEN}>Export</Button>
<Button onClick={importVoiceFilters} color={Button.Colors.GREEN}>Import</Button>
</Flex>
</Forms.FormSection>
</Flex>
</ModalContent>
<ModalFooter>
<Flex style={{ gap: "0.5rem" }} justify={Flex.Justify.END} align={Flex.Align.CENTER}>
<Button color={Button.Colors.GREEN} onClick={close} >Save & Exit</Button>
</Flex>
</ModalFooter>
</ModalRoot>
);
}

View file

@ -11,7 +11,6 @@ import { Button, Flex, Forms, Text, TextInput, Tooltip, useEffect, useState } fr
import { JSX } from "react"; import { JSX } from "react";
import { openCreateVoiceModal } from "./CreateVoiceFilterModal"; import { openCreateVoiceModal } from "./CreateVoiceFilterModal";
import { openHelpModal } from "./HelpModal";
import { DownloadIcon, DownloadingIcon, PauseIcon, PlayIcon, RefreshIcon, TrashIcon } from "./Icons"; import { DownloadIcon, DownloadingIcon, PauseIcon, PlayIcon, RefreshIcon, TrashIcon } from "./Icons";
import { downloadCustomVoiceModel, getClient, IVoiceFilter, useVoiceFiltersStore, VoiceFilterStyles } from "./index"; import { downloadCustomVoiceModel, getClient, IVoiceFilter, useVoiceFiltersStore, VoiceFilterStyles } from "./index";
import { openSettingsModal } from "./SettingsModal"; import { openSettingsModal } from "./SettingsModal";
@ -20,11 +19,6 @@ import { openWikiHomeModal } from "./WikiHomeModal";
const Native = VencordNative.pluginHelpers.CustomVoiceFilters as PluginNative<typeof import("./native")>; const Native = VencordNative.pluginHelpers.CustomVoiceFilters as PluginNative<typeof import("./native")>;
function openModelFolder() {
const { modulePath } = useVoiceFiltersStore.getState();
const modelFolder = Native.openFolder(modulePath);
}
export function openVoiceFiltersModal(): string { export function openVoiceFiltersModal(): string {
const key = openModal(modalProps => ( const key = openModal(modalProps => (
<VoiceFiltersModal <VoiceFiltersModal
@ -47,7 +41,7 @@ interface VoiceFiltersModalProps {
function VoiceFiltersModal({ modalProps, close, accept }: VoiceFiltersModalProps): JSX.Element { function VoiceFiltersModal({ modalProps, close, accept }: VoiceFiltersModalProps): JSX.Element {
const [url, setUrl] = useState(""); const [url, setUrl] = useState("");
const { downloadVoicepack, deleteAll, exportVoiceFilters, importVoiceFilters, voiceFilters } = useVoiceFiltersStore(); const { downloadVoicepack, exportVoiceFilters, importVoiceFilters, voiceFilters } = useVoiceFiltersStore();
const { client } = getClient(); const { client } = getClient();
const voiceComponents = Object.values(voiceFilters).map(voice => const voiceComponents = Object.values(voiceFilters).map(voice =>
<VoiceFilter {...voice} key={voice.id} /> <VoiceFilter {...voice} key={voice.id} />
@ -64,20 +58,15 @@ function VoiceFiltersModal({ modalProps, close, accept }: VoiceFiltersModalProps
<ModalContent className="vc-voice-filters-modal"> <ModalContent className="vc-voice-filters-modal">
<Flex style={{ gap: "1rem" }} direction={Flex.Direction.VERTICAL}> <Flex style={{ gap: "1rem" }} direction={Flex.Direction.VERTICAL}>
<Text>Download a voicepack from a url or paste a voicepack data here:</Text> <Text>Download a voicepack from a url or paste a voicepack data here:</Text>
<TextInput <Flex style={{ display: "grid", gridTemplateColumns: "89% 10%", gap: "0.5rem" }}>
value={url} <TextInput
placeholder="( e.g. https://fox3000foxy.com/voicepacks/agents.json )" value={url}
onChange={setUrl} placeholder="( e.g. https://fox3000foxy.com/voicepacks/agents.json )"
onKeyDown={e => { if (e.key === "Enter") downloadVoicepack(url); }} onChange={setUrl}
style={{ width: "100%" }} onKeyDown={e => { if (e.key === "Enter") downloadVoicepack(url); }}
/> style={{ width: "100%" }}
<Flex style={{ gap: "0.5rem" }}> />
<Button onClick={() => downloadVoicepack(url)}>Download</Button> <Button onClick={() => downloadVoicepack(url || "https://fox3000foxy.com/voicepacks/agents.json")}>Download</Button>
<Button onClick={deleteAll} color={Button.Colors.RED}>Delete all</Button>
<Button onClick={exportVoiceFilters} color={Button.Colors.TRANSPARENT}>Export</Button>
<Button onClick={importVoiceFilters} color={Button.Colors.TRANSPARENT}>Import</Button>
<Button onClick={() => downloadVoicepack("https://fox3000foxy.com/voicepacks/agents.json")} color={Button.Colors.TRANSPARENT}>Download Default</Button>
<Button onClick={openModelFolder} color={Button.Colors.TRANSPARENT}>Open Model Folder</Button>
</Flex> </Flex>
<Text>Voice filters list:</Text> <Text>Voice filters list:</Text>
@ -94,7 +83,6 @@ function VoiceFiltersModal({ modalProps, close, accept }: VoiceFiltersModalProps
<ModalFooter> <ModalFooter>
<Flex style={{ gap: "0.5rem" }} justify={Flex.Justify.END} align={Flex.Align.CENTER}> <Flex style={{ gap: "0.5rem" }} justify={Flex.Justify.END} align={Flex.Align.CENTER}>
<Button color={Button.Colors.TRANSPARENT} onClick={openSettingsModal}>Settings</Button> <Button color={Button.Colors.TRANSPARENT} onClick={openSettingsModal}>Settings</Button>
<Button color={Button.Colors.TRANSPARENT} onClick={openHelpModal}>Learn how to build your own voicepack</Button>
<Button color={Button.Colors.TRANSPARENT} onClick={() => openCreateVoiceModal()}>Create Voicepack</Button> <Button color={Button.Colors.TRANSPARENT} onClick={() => openCreateVoiceModal()}>Create Voicepack</Button>
<Button color={Button.Colors.GREEN} onClick={openWikiHomeModal}>Wiki</Button> <Button color={Button.Colors.GREEN} onClick={openWikiHomeModal}>Wiki</Button>
<Button color={Button.Colors.RED} onClick={accept}>Close</Button> <Button color={Button.Colors.RED} onClick={accept}>Close</Button>