Updated customVoicePlugins

This commit is contained in:
fox3000foxy 2025-02-22 15:04:36 +01:00
parent 3972efae9f
commit c6934f664b
11 changed files with 404 additions and 132 deletions

View file

@ -37,7 +37,7 @@ function ConfirmModal({ modalProps, message, accept, close }: ConfirmModalProps)
</Forms.FormTitle> </Forms.FormTitle>
<ModalCloseButton onClick={close} /> <ModalCloseButton onClick={close} />
</ModalHeader> </ModalHeader>
<ModalContent style={{ paddingBlock: "0.5rem" }}> <ModalContent className="vc-voice-filters-modal">
<Text>{message}</Text> <Text>{message}</Text>
</ModalContent> </ModalContent>
<ModalFooter> <ModalFooter>

View file

@ -57,7 +57,7 @@ function CreateVoiceFilterModal({ modalProps, close, defaultValue }: CreateVoice
}, [voiceFilter]); }, [voiceFilter]);
const keyOptions: SelectOption[] = useMemo(() => const keyOptions: SelectOption[] = useMemo(() =>
[{ value: "", label: "(empty)" }, ...Object.keys(voices).map(name => ({ value: name, label: name }))], [{ value: "", label: "(empty)" }, ...(voices ? Object.keys(voices).map(name => ({ value: name, label: name })) : [])],
[]); []);
return ( return (
@ -68,7 +68,7 @@ function CreateVoiceFilterModal({ modalProps, close, defaultValue }: CreateVoice
</Forms.FormTitle> </Forms.FormTitle>
<ModalCloseButton onClick={close} /> <ModalCloseButton onClick={close} />
</ModalHeader> </ModalHeader>
<ModalContent style={{ paddingBlock: "0.5rem" }}> <ModalContent className="vc-voice-filters-modal">
<Flex style={{ gap: "1rem" }} direction={Flex.Direction.VERTICAL}> <Flex style={{ gap: "1rem" }} direction={Flex.Direction.VERTICAL}>
<Forms.FormSection> <Forms.FormSection>
<Forms.FormTitle>Name<span style={{ color: "var(--text-danger)" }}>*</span></Forms.FormTitle> <Forms.FormTitle>Name<span style={{ color: "var(--text-danger)" }}>*</span></Forms.FormTitle>

View file

@ -36,7 +36,7 @@ function ErrorModal({ modalProps, close, message }: ErrorModalProps): JSX.Elemen
</Forms.FormTitle> </Forms.FormTitle>
<ModalCloseButton onClick={close} /> <ModalCloseButton onClick={close} />
</ModalHeader> </ModalHeader>
<ModalContent style={{ paddingBlock: "0.5rem" }}> <ModalContent className="vc-voice-filters-modal">
<Text>{message}</Text> <Text>{message}</Text>
</ModalContent> </ModalContent>
<ModalFooter> <ModalFooter>

View file

@ -4,14 +4,13 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { CodeBlock } from "@components/CodeBlock";
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, Text } from "@webpack/common"; import { Button, Flex, Forms } from "@webpack/common";
import { JSX } from "react"; import { JSX } from "react";
import { templateVoicepack } from "."; import { templateVoicepack, voices } from ".";
import { Markdown } from "./Markdown";
import { downloadFile } from "./utils"; import { downloadFile } from "./utils";
import { openVoiceFiltersModal } from "./VoiceFiltersModal";
export function openHelpModal(): string { export function openHelpModal(): string {
const key = openModal(modalProps => ( const key = openModal(modalProps => (
@ -29,6 +28,16 @@ interface HelpModalProps {
} }
function HelpModal({ modalProps, close }: HelpModalProps): JSX.Element { function HelpModal({ modalProps, close }: HelpModalProps): JSX.Element {
const description = `To build your own voicepack, you need to have a voicepack file. You can download one from the template or look at this tutorial.
The voicepack file is a json file that contains the voicepack data.
A voicepack may have one or multiple voices. Each voice is an object with the following properties:
\`\`\`json
${templateVoicepack}
\`\`\`*Style Key must be "" or one of the following: ${voices ? [...new Set(Object.values(voices).map(({ styleKey }) => styleKey))].join(", ") : ""}*
Once you have the voicepack file, you can use the <vf:main> to manage your voicepacks.`;
return ( return (
<ModalRoot {...modalProps} size={ModalSize.LARGE}> <ModalRoot {...modalProps} size={ModalSize.LARGE}>
<ModalHeader> <ModalHeader>
@ -37,20 +46,16 @@ function HelpModal({ modalProps, close }: HelpModalProps): JSX.Element {
</Forms.FormTitle> </Forms.FormTitle>
<ModalCloseButton onClick={close} /> <ModalCloseButton onClick={close} />
</ModalHeader> </ModalHeader>
<ModalContent style={{ paddingBlock: "0.5rem" }}> <ModalContent className="vc-voice-filters-modal">
<Flex style={{ gap: "1rem" }} direction={Flex.Direction.VERTICAL}> <Markdown content={description} />
<Text>To build your own voicepack, you need to have a voicepack file. You can download one from the template or look at this tutorial.</Text>
<Text>The voicepack file is a json file that contains the voicepack data. You can find the template <a onClick={() => {
downloadFile("voicepack-template.json", templateVoicepack);
}}>here</a></Text>
<Text>Once you have the voicepack file, you can use the <a onClick={openVoiceFiltersModal}>Voice Filters Management Menu</a> to manage your voicepacks.</Text>
<Text>A voicepack may have one or multiple voices. Each voice is an object with the following properties:</Text>
<CodeBlock lang="json" content={templateVoicepack} />
<Text style={{ fontStyle: "italic" }}>Style Key must be "" or one of the following: skye, quinn, axel, sebastien, megaphone, robot, tunes, ghost, spacebunny, justus, harper, villain, solara, cave, deepfried</Text>
</Flex>
</ModalContent> </ModalContent>
<ModalFooter> <ModalFooter>
<Button onClick={close} color={Button.Colors.TRANSPARENT}>Close</Button> <Flex style={{ gap: "0.5rem" }} justify={Flex.Justify.BETWEEN} align={Flex.Align.CENTER}>
<Button onClick={() => downloadFile("voicepack-template.json", templateVoicepack)} color={Button.Colors.BRAND_NEW}>
Download template file
</Button>
<Button onClick={close} color={Button.Colors.TRANSPARENT}>Close</Button>
</Flex>
</ModalFooter> </ModalFooter>
</ModalRoot> </ModalRoot>
); );

View file

@ -5,55 +5,89 @@
*/ */
import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons"; import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons";
import { JSX } from "react"; import { JSX, SVGProps } from "react";
import { VoiceFilterStyles } from "./index";
import { openVoiceFiltersModal } from "./VoiceFiltersModal"; import { openVoiceFiltersModal } from "./VoiceFiltersModal";
export function DownloadIcon(): JSX.Element {
interface IconProps extends SVGProps<SVGSVGElement> { }
export function DownloadIcon(props: IconProps): JSX.Element {
return ( return (
<svg className={VoiceFilterStyles.thumbnail} style={{ zoom: "0.4", margin: "auto", top: "0", left: "0", bottom: "0", right: "0" }} aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"> <svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
<path fill="white" d="M12 2a1 1 0 0 1 1 1v10.59l3.3-3.3a1 1 0 1 1 1.4 1.42l-5 5a1 1 0 0 1-1.4 0l-5-5a1 1 0 1 1 1.4-1.42l3.3 3.3V3a1 1 0 0 1 1-1ZM3 20a1 1 0 1 0 0 2h18a1 1 0 1 0 0-2H3Z" className=""></path> <path fill="currentColor" d="M12 2a1 1 0 0 1 1 1v10.59l3.3-3.3a1 1 0 1 1 1.4 1.42l-5 5a1 1 0 0 1-1.4 0l-5-5a1 1 0 1 1 1.4-1.42l3.3 3.3V3a1 1 0 0 1 1-1ZM3 20a1 1 0 1 0 0 2h18a1 1 0 1 0 0-2H3Z" />
</svg> </svg>
); );
} }
export function DownloadingIcon(): JSX.Element { export function DownloadingIcon(props: IconProps): JSX.Element {
return ( return (
<svg className={VoiceFilterStyles.thumbnail} style={{ zoom: "0.4", margin: "auto", top: "0", left: "0", bottom: "0", right: "0" }} viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" {...props}>
<path fill="white" stroke="black" strokeWidth="1" d="M1023.849566 529.032144C1022.533495 457.744999 1007.544916 386.64064 979.907438 321.641387 952.343075 256.605575 912.349158 197.674868 863.252422 148.980264 814.192243 100.249102 755.992686 61.717486 693.004095 36.310016 630.052062 10.792874 562.347552-1.380777 495.483865 0.081523 428.620178 1.470709 362.012394 16.495846 301.144139 44.206439 240.202769 71.807359 185.000928 111.874391 139.377154 161.044242 93.753381 210.177537 57.707676 268.450209 33.945294 331.475357 10.073239 394.463948-1.296147 462.1319 0.166154 529.032144 1.482224 595.968946 15.593423 662.503615 41.549256 723.371871 67.468531 784.240126 105.013094 839.405409 151.075558 884.956067 197.101464 930.579841 251.645269 966.552431 310.612534 990.241698 369.543241 1014.040637 432.860849 1025.336908 495.483865 1023.874608 558.143438 1022.485422 620.291206 1008.337666 677.174693 982.381833 734.094737 956.462558 785.677384 918.954552 828.230327 872.892089 870.819826 826.902741 904.416179 772.395492 926.533473 713.5379 939.986637 677.85777 949.089457 640.605667 953.915048 602.841758 955.194561 602.951431 956.510631 602.987988 957.790144 602.987988 994.27454 602.987988 1023.849566 572.425909 1023.849566 534.735116 1023.849566 532.834125 1023.739893 530.933135 1023.593663 529.032144L1023.849566 529.032144 1023.849566 529.032144ZM918.892953 710.284282C894.691881 767.021538 859.596671 818.421398 816.568481 860.82811 773.540291 903.307938 722.652236 936.75806 667.706298 958.729124 612.760359 980.773303 553.902767 991.192193 495.483865 989.729893 437.064963 988.377265 379.304096 975.106889 326.441936 950.832702 273.543218 926.668187 225.616322 891.682649 186.097653 848.764132 146.542426 805.91873 115.35887 755.176905 94.959779 700.486869 74.451015 645.796833 64.799833 587.195144 66.189018 529.032144 67.541646 470.869145 79.934642 413.437296 102.563741 360.867595 125.119725 308.297895 157.765582 260.663459 197.759499 221.364135 237.716858 182.064811 284.985719 151.137157 335.910331 130.884296 386.834944 110.55832 441.305634 101.01681 495.483865 102.47911 549.662096 103.868296 603.036061 116.261292 651.876895 138.780718 700.754287 161.22703 745.025432 193.690099 781.509828 233.428113 818.067339 273.166127 846.764984 320.142529 865.518987 370.665008 884.346105 421.224045 893.156465 475.256046 891.76728 529.032144L891.986625 529.032144C891.840395 530.933135 891.76728 532.797568 891.76728 534.735116 891.76728 569.939999 917.540325 598.893547 950.66143 602.585856 944.227308 639.728286 933.589072 675.956779 918.892953 710.284282Z" /> <path fill="currentColor" stroke="black" strokeWidth="1" d="M1023.849566 529.032144C1022.533495 457.744999 1007.544916 386.64064 979.907438 321.641387 952.343075 256.605575 912.349158 197.674868 863.252422 148.980264 814.192243 100.249102 755.992686 61.717486 693.004095 36.310016 630.052062 10.792874 562.347552-1.380777 495.483865 0.081523 428.620178 1.470709 362.012394 16.495846 301.144139 44.206439 240.202769 71.807359 185.000928 111.874391 139.377154 161.044242 93.753381 210.177537 57.707676 268.450209 33.945294 331.475357 10.073239 394.463948-1.296147 462.1319 0.166154 529.032144 1.482224 595.968946 15.593423 662.503615 41.549256 723.371871 67.468531 784.240126 105.013094 839.405409 151.075558 884.956067 197.101464 930.579841 251.645269 966.552431 310.612534 990.241698 369.543241 1014.040637 432.860849 1025.336908 495.483865 1023.874608 558.143438 1022.485422 620.291206 1008.337666 677.174693 982.381833 734.094737 956.462558 785.677384 918.954552 828.230327 872.892089 870.819826 826.902741 904.416179 772.395492 926.533473 713.5379 939.986637 677.85777 949.089457 640.605667 953.915048 602.841758 955.194561 602.951431 956.510631 602.987988 957.790144 602.987988 994.27454 602.987988 1023.849566 572.425909 1023.849566 534.735116 1023.849566 532.834125 1023.739893 530.933135 1023.593663 529.032144L1023.849566 529.032144 1023.849566 529.032144ZM918.892953 710.284282C894.691881 767.021538 859.596671 818.421398 816.568481 860.82811 773.540291 903.307938 722.652236 936.75806 667.706298 958.729124 612.760359 980.773303 553.902767 991.192193 495.483865 989.729893 437.064963 988.377265 379.304096 975.106889 326.441936 950.832702 273.543218 926.668187 225.616322 891.682649 186.097653 848.764132 146.542426 805.91873 115.35887 755.176905 94.959779 700.486869 74.451015 645.796833 64.799833 587.195144 66.189018 529.032144 67.541646 470.869145 79.934642 413.437296 102.563741 360.867595 125.119725 308.297895 157.765582 260.663459 197.759499 221.364135 237.716858 182.064811 284.985719 151.137157 335.910331 130.884296 386.834944 110.55832 441.305634 101.01681 495.483865 102.47911 549.662096 103.868296 603.036061 116.261292 651.876895 138.780718 700.754287 161.22703 745.025432 193.690099 781.509828 233.428113 818.067339 273.166127 846.764984 320.142529 865.518987 370.665008 884.346105 421.224045 893.156465 475.256046 891.76728 529.032144L891.986625 529.032144C891.840395 530.933135 891.76728 532.797568 891.76728 534.735116 891.76728 569.939999 917.540325 598.893547 950.66143 602.585856 944.227308 639.728286 933.589072 675.956779 918.892953 710.284282Z" />
<animateTransform attributeName="transform" type="rotate" from="0 0 0" to="360 0 0" dur="3s" repeatCount="indefinite" /> <animateTransform attributeName="transform" type="rotate" from="0 0 0" to="360 0 0" dur="3s" repeatCount="indefinite" />
</svg> </svg>
); );
} }
export function PlayIcon(): JSX.Element { export function PlayIcon(props: IconProps): JSX.Element {
return ( return (
<svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
<svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" className={`${VoiceFilterStyles.thumbnail} ${VoiceFilterStyles.hoverButtonCircle}`} style={{ margin: "auto", top: "0", left: "0", bottom: "0", right: "0", width: "32px", height: "32px", padding: "24px", transform: "0px 0px" }} fill="none" viewBox="0 0 24 24"> <path fill="currentColor" d="M9.25 3.35C7.87 2.45 6 3.38 6 4.96v14.08c0 1.58 1.87 2.5 3.25 1.61l10.85-7.04a1.9 1.9 0 0 0 0-3.22L9.25 3.35Z" />
<path fill="white" d="M9.25 3.35C7.87 2.45 6 3.38 6 4.96v14.08c0 1.58 1.87 2.5 3.25 1.61l10.85-7.04a1.9 1.9 0 0 0 0-3.22L9.25 3.35Z" className=""></path>
</svg> </svg>
); );
} }
export function PauseIcon(): JSX.Element { export function PauseIcon(props: IconProps): JSX.Element {
return ( return (
<svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" className={`${VoiceFilterStyles.thumbnail} ${VoiceFilterStyles.hoverButtonCircle}`} style={{ margin: "auto", top: "0", left: "0", bottom: "0", right: "0", width: "32px", height: "32px", padding: "24px", transform: "0px 0px" }} fill="none" viewBox="0 0 24 24"> <svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
<path fill="white" d="M6 4a1 1 0 0 0-1 1v14a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H6ZM15 4a1 1 0 0 0-1 1v14a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1h-3Z" className=""></path> <path fill="currentColor" d="M6 4a1 1 0 0 0-1 1v14a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H6ZM15 4a1 1 0 0 0-1 1v14a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1h-3Z" />
</svg>
);
}
export function ChevronIcon(props: IconProps): JSX.Element {
return (
<svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
<path fill="currentColor" d="M5.3 9.3a1 1 0 0 1 1.4 0l5.3 5.29 5.3-5.3a1 1 0 1 1 1.4 1.42l-6 6a1 1 0 0 1-1.4 0l-6-6a1 1 0 0 1 0-1.42Z" />
</svg>
);
}
export function RefreshIcon(props: IconProps): JSX.Element {
return (
<svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" {...props}>
<path fill="currentColor" d="M4 12a8 8 0 0 1 14.93-4H15a1 1 0 1 0 0 2h6a1 1 0 0 0 1-1V3a1 1 0 1 0-2 0v3a9.98 9.98 0 0 0-18 6 10 10 0 0 0 16.29 7.78 1 1 0 0 0-1.26-1.56A8 8 0 0 1 4 12Z" />
</svg>
);
}
export function TrashIcon(props: IconProps): JSX.Element {
return (
<svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" {...props}>
<path fill="currentColor" d="M14.25 1c.41 0 .75.34.75.75V3h5.25c.41 0 .75.34.75.75v.5c0 .41-.34.75-.75.75H3.75A.75.75 0 0 1 3 4.25v-.5c0-.41.34-.75.75-.75H9V1.75c0-.41.34-.75.75-.75h4.5Z" />
<path fill="currentColor" fillRule="evenodd" d="M5.06 7a1 1 0 0 0-1 1.06l.76 12.13a3 3 0 0 0 3 2.81h8.36a3 3 0 0 0 3-2.81l.75-12.13a1 1 0 0 0-1-1.06H5.07ZM11 12a1 1 0 1 0-2 0v6a1 1 0 1 0 2 0v-6Zm3-1a1 1 0 0 1 1 1v6a1 1 0 1 1-2 0v-6a1 1 0 0 1 1-1Z" clipRule="evenodd" />
</svg>
);
}
export function PencilIcon(props: IconProps): JSX.Element {
return (
<svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" {...props}>
<path fill="currentColor" d="m13.96 5.46 4.58 4.58a1 1 0 0 0 1.42 0l1.38-1.38a2 2 0 0 0 0-2.82l-3.18-3.18a2 2 0 0 0-2.82 0l-1.38 1.38a1 1 0 0 0 0 1.42ZM2.11 20.16l.73-4.22a3 3 0 0 1 .83-1.61l7.87-7.87a1 1 0 0 1 1.42 0l4.58 4.58a1 1 0 0 1 0 1.42l-7.87 7.87a3 3 0 0 1-1.6.83l-4.23.73a1.5 1.5 0 0 1-1.73-1.73Z" />
</svg> </svg>
); );
} }
// Custom Voice Filter Icon // Custom Voice Filter Icon
export function CustomVoiceFilterIcon() { export function CustomVoiceFilterIcon(props: IconProps) {
return ( return (
<svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"> <svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
<path d="m19.7.3 4 4a1 1 0 0 1 0 1.4l-4 4a1 1 0 0 1-1.4-1.4L20.58 6H15a1 1 0 1 1 0-2h5.59l-2.3-2.3A1 1 0 0 1 19.71.3Z" fill="currentColor" className=""></path> <path d="m19.7.3 4 4a1 1 0 0 1 0 1.4l-4 4a1 1 0 0 1-1.4-1.4L20.58 6H15a1 1 0 1 1 0-2h5.59l-2.3-2.3A1 1 0 0 1 19.71.3Z" fill="currentColor" />
<path d="M12.62 2.05c.41.06.46.61.17.92A3 3 0 0 0 15 8h.51c.28 0 .5.22.5.5V10a4 4 0 1 1-8 0V6a4 4 0 0 1 4.62-3.95Z" fill="currentColor" className=""></path> <path d="M12.62 2.05c.41.06.46.61.17.92A3 3 0 0 0 15 8h.51c.28 0 .5.22.5.5V10a4 4 0 1 1-8 0V6a4 4 0 0 1 4.62-3.95Z" fill="currentColor" />
<path d="M17.56 12.27a.63.63 0 0 1 .73-.35c.21.05.43.08.65.08.38 0 .72.35.6.7A8 8 0 0 1 13 17.94V20h2a1 1 0 1 1 0 2H9a1 1 0 1 1 0-2h2v-2.06A8 8 0 0 1 4 10a1 1 0 0 1 2 0 6 6 0 0 0 11.56 2.27Z" fill="white" className=""></path> <path d="M17.56 12.27a.63.63 0 0 1 .73-.35c.21.05.43.08.65.08.38 0 .72.35.6.7A8 8 0 0 1 13 17.94V20h2a1 1 0 1 1 0 2H9a1 1 0 1 1 0-2h2v-2.06A8 8 0 0 1 4 10a1 1 0 0 1 2 0 6 6 0 0 0 11.56 2.27Z" fill="currentColor" />
</svg> </svg>
); );
} }

View file

@ -0,0 +1,107 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { proxyLazy } from "@utils/lazy";
import { findByCode, findByProps, findByPropsLazy } from "@webpack";
import { Parser } from "@webpack/common";
import { JSX } from "react";
import { openCreateVoiceModal } from "./CreateVoiceFilterModal";
import { openHelpModal } from "./HelpModal";
import { cl } from "./utils";
import { openVoiceFiltersModal } from "./VoiceFiltersModal";
interface MarkdownRules {
allowDevLinks: boolean;
allowEmojiLinks: boolean;
allowHeading: boolean;
allowLinks: boolean;
allowList: boolean;
channelId: string;
disableAnimatedEmoji: boolean;
disableAutoBlockNewlines: boolean;
forceWhite: boolean;
formatInline: boolean;
isInteracting: boolean;
mentionChannels: string[];
messageId: string;
muted: boolean;
noStyleAndInteraction: boolean;
previewLinkTarget: boolean;
soundboardSounds: string[];
unknownUserMentionPlaceholder: boolean;
viewingChannelId: string;
}
const defaultRules: Partial<MarkdownRules> = { allowLinks: true, allowList: true, allowHeading: true };
const MarkdownContainerClasses = findByPropsLazy("markup", "codeContainer");
const modalLinkRegex = /^<vf:(help|createVoice|main)>/;
const imageRegex = /^!\[((?:\[[^\]]*\]|[^[\]]|\](?=[^[]*\]))*)\]\(\s*((?:\([^)]*\)|[^\s\\]|\\.)*?)\)/;
const actions: Record<string, { action: () => string, name: string; }> = {
help: {
action: openHelpModal,
name: "Help menu"
},
createVoice: {
action: () => openCreateVoiceModal(),
name: "Voice pack creator menu"
},
main: {
action: openVoiceFiltersModal,
name: "Main menu"
},
};
const parser: typeof Parser.parse = proxyLazy(() => {
const DiscordRules = findByProps("AUTO_MODERATION_SYSTEM_MESSAGE_RULES").RULES;
const AdvancedRules = findByCode("channelMention:")({});
const customRules = {
modalLink: {
order: DiscordRules.staticRouteLink,
match: source => modalLinkRegex.exec(source),
parse: ([, target]) => (actions[target]),
react: ({ action, name }) => (
<span className="channelMention interactive vc-voice-filters-modal-link" role="link" onClick={action}>{name}</span>
),
requiredFirstCharacters: ["<"]
},
image: {
...Parser.defaultRules.link,
match: source => imageRegex.exec(source),
parse: ([, title, target]) => ({ title, target }),
react: ({ title, target }) => <div className="vc-voice-filters-md-image">
<img src={target} alt={title} />
</div>,
requiredFirstCharacters: ["!"]
}
};
const builtinRules = new Set([...Object.keys(DiscordRules), ...Object.keys(AdvancedRules)]);
for (const rule of builtinRules) {
customRules[rule] = {
...DiscordRules[rule],
...AdvancedRules[rule],
};
}
return (Parser as any).reactParserFor(customRules);
});
interface MarkdownProps extends Omit<JSX.IntrinsicElements["div"], "children"> {
content: string;
markdownRules?: Partial<MarkdownRules>;
}
export function Markdown({ content, markdownRules = defaultRules, className, ...props }: MarkdownProps) {
return <div className={cl(MarkdownContainerClasses.markup, "vc-voice-filters-md", className)} {...props}>
{parser(content, false, markdownRules)}
</div>;
}

View file

@ -4,16 +4,17 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { PencilIcon } from "@components/Icons";
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 { PluginNative } from "@utils/types"; import { PluginNative } from "@utils/types";
import { Button, Flex, Forms, Text, TextInput, useEffect, useState } from "@webpack/common"; import { Button, Flex, Forms, Text, TextInput, Tooltip, useEffect, useState } from "@webpack/common";
import { JSX } from "react"; import { JSX } from "react";
import { openCreateVoiceModal } from "./CreateVoiceFilterModal"; import { openCreateVoiceModal } from "./CreateVoiceFilterModal";
import { openHelpModal } from "./HelpModal"; import { openHelpModal } from "./HelpModal";
import { DownloadIcon, DownloadingIcon, PauseIcon, PlayIcon } 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 { useAudio } from "./utils"; import { cl, useAudio } from "./utils";
import { openWikiHomeModal } from "./WikiHomeModal"; import { openWikiHomeModal } from "./WikiHomeModal";
const Native = VencordNative.pluginHelpers.CustomVoiceFilters as PluginNative<typeof import("./native")>; const Native = VencordNative.pluginHelpers.CustomVoiceFilters as PluginNative<typeof import("./native")>;
@ -54,7 +55,7 @@ function VoiceFiltersModal({ modalProps, close, accept }: VoiceFiltersModalProps
</Forms.FormTitle> </Forms.FormTitle>
<ModalCloseButton onClick={close} /> <ModalCloseButton onClick={close} />
</ModalHeader> </ModalHeader>
<ModalContent style={{ paddingBlock: "0.5rem" }}> <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 <TextInput
@ -95,7 +96,6 @@ function VoiceFiltersModal({ modalProps, close, accept }: VoiceFiltersModalProps
); );
} }
// Voice Filter // Voice Filter
function VoiceFilter(voiceFilter: IVoiceFilter): JSX.Element { function VoiceFilter(voiceFilter: IVoiceFilter): JSX.Element {
const { name, previewSoundURLs, styleKey, iconURL, id } = voiceFilter; const { name, previewSoundURLs, styleKey, iconURL, id } = voiceFilter;
@ -115,8 +115,19 @@ function VoiceFilter(voiceFilter: IVoiceFilter): JSX.Element {
fetchModelState(); fetchModelState();
}, [modulePath]); }, [modulePath]);
const downloadIconProps = {
className: VoiceFilterStyles.thumbnail,
style: { zoom: 0.4, margin: "auto", inset: 0 }
};
const playPauseIconProps = {
className: cl(VoiceFilterStyles.thumbnail, VoiceFilterStyles.hoverButtonCircle),
style: { margin: "auto", inset: 0, width: "32px", height: "32px", padding: "24px", transform: "0px 0px" }
};
return ( return (
<div className={`${VoiceFilterStyles.filter} ${VoiceFilterStyles[styleKey]}`} onClick={async () => { <div className={cl(VoiceFilterStyles.filter, VoiceFilterStyles[styleKey])} onClick={async () => {
if (!voiceFilter.available) return; if (!voiceFilter.available) return;
// download and preview if downloaded // download and preview if downloaded
@ -126,16 +137,22 @@ function VoiceFilter(voiceFilter: IVoiceFilter): JSX.Element {
if (res.success) setModelState({ status: "downloaded", downloadedBytes: 0 }); if (res.success) setModelState({ status: "downloaded", downloadedBytes: 0 });
} }
}}> }}>
<div className={`${VoiceFilterStyles.selector} ${VoiceFilterStyles.selector}`} role="button" tabIndex={0}> <div className={cl(VoiceFilterStyles.selector, VoiceFilterStyles.selector)} role="button" tabIndex={0}>
<div className={VoiceFilterStyles.iconTreatmentsWrapper}> <div className={VoiceFilterStyles.iconTreatmentsWrapper}>
<div className={`${VoiceFilterStyles.profile} ${!voiceFilter.available || (client === "desktop" && modelState.status !== "downloaded") ? VoiceFilterStyles.underDevelopment : "" <div className={cl(
}`}> "vc-voice-filters-voice-filter",
VoiceFilterStyles.profile,
!voiceFilter.available || (client === "desktop" && modelState.status !== "downloaded")
? VoiceFilterStyles.underDevelopment : "vc-voice-filters-voice-filter-available"
)}>
<img className={VoiceFilterStyles.thumbnail} alt="" src={iconURL ?? ""} draggable={false} /> <img className={VoiceFilterStyles.thumbnail} alt="" src={iconURL ?? ""} draggable={false} />
{client === "desktop" && voiceFilter.available && modelState.status === "not_downloaded" && <div><DownloadIcon /></div>} {voiceFilter.available && <>
{client === "desktop" && voiceFilter.available && modelState.status === "downloading" && <div><DownloadingIcon /></div>} {client === "desktop" && modelState.status === "not_downloaded" && <div><DownloadIcon {...downloadIconProps} /></div>}
{((client === "desktop" && voiceFilter.available && modelState.status === "downloaded") || (client === "web" && voiceFilter.available)) && <div onClick={() => {client === "desktop" && modelState.status === "downloading" && <div><DownloadingIcon {...downloadIconProps} /></div>}
isPlaying ? stopSound() : playSound() {((client === "desktop" && modelState.status === "downloaded") || client === "web") && <div onClick={() =>
}>{isPlaying ? <PauseIcon /> : <PlayIcon />}</div>} isPlaying ? stopSound() : playSound()
}>{isPlaying ? <PauseIcon {...playPauseIconProps} /> : <PlayIcon {...playPauseIconProps} />}</div>}
</>}
</div> </div>
</div> </div>
<Text variant="text-xs/medium" className={VoiceFilterStyles.filterName}> <Text variant="text-xs/medium" className={VoiceFilterStyles.filterName}>
@ -143,30 +160,38 @@ function VoiceFilter(voiceFilter: IVoiceFilter): JSX.Element {
</Text> </Text>
</div> </div>
{voiceFilter.available && ((client === "desktop" && modelState.status === "downloaded") || (client === "web")) ? ( {voiceFilter.available && ((client === "desktop" && modelState.status === "downloaded") || (client === "web")) && (
<> <>
<div onClick={() => updateById(id)} className={className} role="button" tabIndex={-1}> <Tooltip text="Update">
<svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"> {({ ...props }) =>
<path fill="white" d="M4 12a8 8 0 0 1 14.93-4H15a1 1 0 1 0 0 2h6a1 1 0 0 0 1-1V3a1 1 0 1 0-2 0v3a9.98 9.98 0 0 0-18 6 10 10 0 0 0 16.29 7.78 1 1 0 0 0-1.26-1.56A8 8 0 0 1 4 12Z" /> <div className={className} role="button" tabIndex={-1} {...props} onClick={() => updateById(id)}>
</svg> <RefreshIcon width={16} height={16} />
</div> </div>
<div onClick={() => deleteById(id)} className={className} role="button" tabIndex={-1} style={{ left: "65px" }}> }
<svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"> </Tooltip>
<path fill="#f44" d="M14.25 1c.41 0 .75.34.75.75V3h5.25c.41 0 .75.34.75.75v.5c0 .41-.34.75-.75.75H3.75A.75.75 0 0 1 3 4.25v-.5c0-.41.34-.75.75-.75H9V1.75c0-.41.34-.75.75-.75h4.5Z" /> <Tooltip text="Delete">
<path fill="#f44" fillRule="evenodd" d="M5.06 7a1 1 0 0 0-1 1.06l.76 12.13a3 3 0 0 0 3 2.81h8.36a3 3 0 0 0 3-2.81l.75-12.13a1 1 0 0 0-1-1.06H5.07ZM11 12a1 1 0 1 0-2 0v6a1 1 0 1 0 2 0v-6Zm3-1a1 1 0 0 1 1 1v6a1 1 0 1 1-2 0v-6a1 1 0 0 1 1-1Z" clipRule="evenodd" /> {({ ...props }) =>
</svg> <div className={className} role="button" tabIndex={-1} style={{ left: "65px" }} {...props} onClick={() => deleteById(id)}>
</div> <TrashIcon width={16} height={16} style={{ color: "#f44" }} />
<div onClick={() => exportIndividualVoice(id)} className={className} role="button" tabIndex={-1} style={{ top: "65px" }}> </div>
<svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"> }
<path fill="white" d="M12 2a1 1 0 0 1 1 1v10.59l3.3-3.3a1 1 0 1 1 1.4 1.42l-5 5a1 1 0 0 1-1.4 0l-5-5a1 1 0 1 1 1.4-1.42l3.3 3.3V3a1 1 0 0 1 1-1ZM3 20a1 1 0 1 0 0 2h18a1 1 0 1 0 0-2H3Z" /> </Tooltip>
</svg> <Tooltip text="Export">
</div> {({ ...props }) =>
<div onClick={() => openCreateVoiceModal(voiceFilter)} className={className} role="button" tabIndex={-1} style={{ top: "65px", left: "65px" }}> <div className={className} role="button" tabIndex={-1} style={{ top: "65px" }} {...props} onClick={() => exportIndividualVoice(id)}>
<svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"> <DownloadIcon width={16} height={16} />
<path fill="white" d="m13.96 5.46 4.58 4.58a1 1 0 0 0 1.42 0l1.38-1.38a2 2 0 0 0 0-2.82l-3.18-3.18a2 2 0 0 0-2.82 0l-1.38 1.38a1 1 0 0 0 0 1.42ZM2.11 20.16l.73-4.22a3 3 0 0 1 .83-1.61l7.87-7.87a1 1 0 0 1 1.42 0l4.58 4.58a1 1 0 0 1 0 1.42l-7.87 7.87a3 3 0 0 1-1.6.83l-4.23.73a1.5 1.5 0 0 1-1.73-1.73Z" /> </div>
</svg> }
</div> </Tooltip>
</>) : <></>} <Tooltip text="Edit">
{({ ...props }) =>
<div className={className} role="button" tabIndex={-1} style={{ top: "65px", left: "65px" }} {...props} onClick={() => openCreateVoiceModal(voiceFilter)} >
<PencilIcon width={16} height={16} />
</div>
}
</Tooltip>
</>
)}
</div> </div>
); );
} }

File diff suppressed because one or more lines are too long

View file

@ -5,6 +5,8 @@
*/ */
// Imports // Imports
import "./style.css";
import { DataStore } from "@api/index"; import { DataStore } from "@api/index";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { proxyLazy } from "@utils/lazy"; import { proxyLazy } from "@utils/lazy";
@ -17,7 +19,7 @@ import { openConfirmModal } from "./ConfirmModal";
import { openErrorModal } from "./ErrorModal"; import { openErrorModal } from "./ErrorModal";
import { CustomVoiceFilterChatBarIcon } from "./Icons"; import { CustomVoiceFilterChatBarIcon } from "./Icons";
import { downloadFile } from "./utils"; import { downloadFile } from "./utils";
export let voices: any = null; export let voices: Record<string, IVoiceFilter> | null = null;
export let VoiceFilterStyles: any = null; // still 'skye' export let VoiceFilterStyles: any = null; // still 'skye'
export let VoiceFilterStore: any = null; export let VoiceFilterStore: any = null;
@ -223,10 +225,11 @@ export const useVoiceFiltersStore: ZustandStore<CustomVoiceFilterStore> = proxyL
}; };
let i = 0; let i = 0;
for (const [, val] of Object.entries(voices) as [string, IVoiceFilter][]) { if (voices)
if (!Object.values(voiceFilterState.voiceFilters).find(x => x.name === val.name)) for (const [, val] of Object.entries(voices) as [string, IVoiceFilter][]) {
voiceFilterState.voiceFilters[++i] = { ...val, id: i, available: true, temporarilyAvailable: false }; if (!Object.values(voiceFilterState.voiceFilters).find(x => x.name === val.name))
} voiceFilterState.voiceFilters[++i] = { ...val, id: i, available: true, temporarilyAvailable: false };
}
const { voiceFilters } = get(); const { voiceFilters } = get();
Object.values(voiceFilters).forEach(voice => { Object.values(voiceFilters).forEach(voice => {
@ -304,8 +307,10 @@ export default definePlugin({
useVoiceFiltersStore.subscribe(store => store.updateVoicesList()); useVoiceFiltersStore.subscribe(store => store.updateVoicesList());
const modulePath = await DiscordNative.fileManager.getModulePath(); if (getClient().client === "desktop") {
useVoiceFiltersStore.getState().modulePath = modulePath; const modulePath = await DiscordNative.fileManager.getModulePath();
useVoiceFiltersStore.getState().modulePath = modulePath;
}
// // ============ DEMO ============ // // ============ DEMO ============
// const templaceVoicePackObject: IVoiceFilter = JSON.parse(templateVoicepack); // const templaceVoicePackObject: IVoiceFilter = JSON.parse(templateVoicepack);

View file

@ -0,0 +1,77 @@
.vc-voice-filters-wiki {
max-width: var(--modal-width-large);
}
.vc-voice-filters-md {
display: flex;
flex-direction: column;
align-items: stretch;
}
.vc-voice-filters-md-image {
width: 100%;
max-height: 20rem;
overflow: hidden;
display: flex;
margin-block: 0.5rem;
img {
width: 100%;
background: var(--background-tertiary);
border-radius: 0.5rem;
object-fit: contain;
}
}
.vc-voice-filters-card {
width: 100%;
.vc-voice-filters-card-title {
cursor: pointer;
background: var(--background-primary);
padding: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.vc-voice-filters-card-icon {
transition: all 100ms ease-in-out;
}
&.vc-voice-filters-card-open .vc-voice-filters-card-icon {
transform: rotate(180deg);
}
}
.vc-voice-filters-modal-link {
border-radius: 3px;
padding: 0 2px;
font-weight: 500;
unicode-bidi: plaintext;
color: var(--mention-foreground);
background: var(--mention-background);
text-overflow: ellipsis;
overflow: hidden;
}
.vc-voice-filters-details {
interpolate-size: allow-keywords;
transition: all 100ms ease-in-out;
overflow-y: hidden;
height: 0;
padding-inline: 1.2rem;
.vc-voice-filters-card-open & {
height: auto;
}
}
.vc-voice-filters-modal {
padding-block: 0.5rem;
color: var(--text-normal);
}
.vc-voice-filters-voice-filter:not(.vc-voice-filters-voice-filter-available) img {
filter: brightness(0.5);
}

View file

@ -4,6 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { classNameFactory } from "@api/Styles";
import { useCallback, useEffect, useRef, useState } from "@webpack/common"; import { useCallback, useEffect, useRef, useState } from "@webpack/common";
export function downloadFile(name: string, data: string): void { export function downloadFile(name: string, data: string): void {
@ -91,3 +92,5 @@ export function useAudio({ source, key = defaultKey }: UseAudioOptions = {}) {
return { isPlaying, playSound, stopSound, preloadSound }; return { isPlaying, playSound, stopSound, preloadSound };
} }
export const cl = classNameFactory();