VCNarrator Migrate Settings

This commit is contained in:
thororen1234 2025-03-04 08:35:35 -05:00
parent 1630db4736
commit cb2dfd926e
No known key found for this signature in database
2 changed files with 101 additions and 146 deletions

View file

@ -10,7 +10,6 @@ import { Margins } from "@utils/margins";
import { wordsToTitle } from "@utils/text";
import definePlugin, {
OptionType,
PluginOptionsItem,
} from "@utils/types";
import { findByPropsLazy } from "@webpack";
import {
@ -232,10 +231,9 @@ const settings = definePluginSettings({
export default definePlugin({
name: "VcNarratorCustom",
description:
"Announces when users join, leave, or move voice channels via narrator. TikTok TTS version; speechSynthesis is pretty boring. Ported from https://github.com/Loukious/Vencord",
description: "Announces when users join, leave, or move voice channels via narrator. TikTok TTS version; speechSynthesis is pretty boring. Ported from https://github.com/Loukious/Vencord",
authors: [Devs.Ven, Devs.Nyako, EquicordDevs.Loukios, EquicordDevs.examplegit],
settings,
flux: {
VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) {
const myGuildId = SelectedGuildStore.getGuildId();
@ -297,13 +295,10 @@ export default definePlugin({
},
},
optionsCache: null as Record<string, PluginOptionsItem> | null,
settings,
settingsAboutComponent({ tempSettings: s }) {
const types = useMemo(
() =>
Object.keys(settings.store)
Object.keys(settings.store!)
.filter(k => k.endsWith("Message"))
.map(k => k.slice(0, -7)),
[]

View file

@ -16,13 +16,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Settings } from "@api/Settings";
import { definePluginSettings, Settings } from "@api/Settings";
import { ErrorCard } from "@components/ErrorCard";
import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins";
import { wordsToTitle } from "@utils/text";
import definePlugin, { OptionType, PluginOptionsItem, ReporterTestable } from "@utils/types";
import definePlugin, { OptionType, ReporterTestable } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { Button, ChannelStore, Forms, GuildMemberStore, SelectedChannelStore, SelectedGuildStore, useMemo, UserStore } from "@webpack/common";
import { ReactElement } from "react";
@ -39,197 +39,25 @@ interface VoiceState {
const VoiceStateStore = findByPropsLazy("getVoiceStatesForChannel", "getCurrentClientVoiceChannelId");
// Mute/Deaf for other people than you is commented out, because otherwise someone can spam it and it will be annoying
// Filtering out events is not as simple as just dropping duplicates, as otherwise mute, unmute, mute would
// not say the second mute, which would lead you to believe they're unmuted
function speak(text: string, settings: any = Settings.plugins.VcNarrator) {
if (!text) return;
const speech = new SpeechSynthesisUtterance(text);
let voice = speechSynthesis.getVoices().find(v => v.voiceURI === settings.voice);
if (!voice) {
new Logger("VcNarrator").error(`Voice "${settings.voice}" not found. Resetting to default.`);
voice = speechSynthesis.getVoices().find(v => v.default);
settings.voice = voice?.voiceURI;
if (!voice) return; // This should never happen
}
speech.voice = voice!;
speech.volume = settings.volume;
speech.rate = settings.rate;
speechSynthesis.speak(speech);
}
function clean(str: string) {
const replacer = Settings.plugins.VcNarrator.latinOnly
? /[^\p{Script=Latin}\p{Number}\p{Punctuation}\s]/gu
: /[^\p{Letter}\p{Number}\p{Punctuation}\s]/gu;
return str.normalize("NFKC")
.replace(replacer, "")
.replace(/_{2,}/g, "_")
.trim();
}
function formatText(str: string, user: string, channel: string, displayName: string, nickname: string) {
return str
.replaceAll("{{USER}}", clean(user) || (user ? "Someone" : ""))
.replaceAll("{{CHANNEL}}", clean(channel) || "channel")
.replaceAll("{{DISPLAY_NAME}}", clean(displayName) || (displayName ? "Someone" : ""))
.replaceAll("{{NICKNAME}}", clean(nickname) || (nickname ? "Someone" : ""));
}
/*
let StatusMap = {} as Record<string, {
mute: boolean;
deaf: boolean;
}>;
*/
// For every user, channelId and oldChannelId will differ when moving channel.
// Only for the local user, channelId and oldChannelId will be the same when moving channel,
// for some ungodly reason
let myLastChannelId: string | undefined;
function getTypeAndChannelId({ channelId, oldChannelId }: VoiceState, isMe: boolean) {
if (isMe && channelId !== myLastChannelId) {
oldChannelId = myLastChannelId;
myLastChannelId = channelId;
}
if (channelId !== oldChannelId) {
if (channelId) return [oldChannelId ? "move" : "join", channelId];
if (oldChannelId) return ["leave", oldChannelId];
}
/*
if (channelId) {
if (deaf || selfDeaf) return ["deafen", channelId];
if (mute || selfMute) return ["mute", channelId];
const oldStatus = StatusMap[userId];
if (oldStatus.deaf) return ["undeafen", channelId];
if (oldStatus.mute) return ["unmute", channelId];
}
*/
return ["", ""];
}
/*
function updateStatuses(type: string, { deaf, mute, selfDeaf, selfMute, userId, channelId }: VoiceState, isMe: boolean) {
if (isMe && (type === "join" || type === "move")) {
StatusMap = {};
const states = VoiceStateStore.getVoiceStatesForChannel(channelId!) as Record<string, VoiceState>;
for (const userId in states) {
const s = states[userId];
StatusMap[userId] = {
mute: s.mute || s.selfMute,
deaf: s.deaf || s.selfDeaf
};
}
return;
}
if (type === "leave" || (type === "move" && channelId !== SelectedChannelStore.getVoiceChannelId())) {
if (isMe)
StatusMap = {};
else
delete StatusMap[userId];
return;
}
StatusMap[userId] = {
deaf: deaf || selfDeaf,
mute: mute || selfMute
};
}
*/
function playSample(tempSettings: any, type: string) {
const settings = Object.assign({}, Settings.plugins.VcNarrator, tempSettings);
const currentUser = UserStore.getCurrentUser();
const myGuildId = SelectedGuildStore.getGuildId();
speak(formatText(settings[type + "Message"], currentUser.username, "general", (currentUser as any).globalName ?? currentUser.username, GuildMemberStore.getNick(myGuildId, currentUser.id) ?? currentUser.username), settings);
}
export default definePlugin({
name: "VcNarrator",
description: "Announces when users join, leave, or move voice channels via narrator",
authors: [Devs.Ven],
reporterTestable: ReporterTestable.None,
flux: {
VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) {
const myGuildId = SelectedGuildStore.getGuildId();
const myChanId = SelectedChannelStore.getVoiceChannelId();
const myId = UserStore.getCurrentUser().id;
if (ChannelStore.getChannel(myChanId!)?.type === 13 /* Stage Channel */) return;
for (const state of voiceStates) {
const { userId, channelId, oldChannelId } = state;
const isMe = userId === myId;
if (!isMe) {
if (!myChanId) continue;
if (channelId !== myChanId && oldChannelId !== myChanId) continue;
}
const [type, id] = getTypeAndChannelId(state, isMe);
if (!type) continue;
const template = Settings.plugins.VcNarrator[type + "Message"];
const user = isMe && !Settings.plugins.VcNarrator.sayOwnName ? "" : UserStore.getUser(userId).username;
const displayName = user && ((UserStore.getUser(userId) as any).globalName ?? user);
const nickname = user && (GuildMemberStore.getNick(myGuildId, userId) ?? user);
const channel = ChannelStore.getChannel(id).name;
speak(formatText(template, user, channel, displayName, nickname));
// updateStatuses(type, state, isMe);
}
},
AUDIO_TOGGLE_SELF_MUTE() {
const chanId = SelectedChannelStore.getVoiceChannelId()!;
const s = VoiceStateStore.getVoiceStateForChannel(chanId) as VoiceState;
if (!s) return;
const event = s.mute || s.selfMute ? "unmute" : "mute";
speak(formatText(Settings.plugins.VcNarrator[event + "Message"], "", ChannelStore.getChannel(chanId).name, "", ""));
},
AUDIO_TOGGLE_SELF_DEAF() {
const chanId = SelectedChannelStore.getVoiceChannelId()!;
const s = VoiceStateStore.getVoiceStateForChannel(chanId) as VoiceState;
if (!s) return;
const event = s.deaf || s.selfDeaf ? "undeafen" : "deafen";
speak(formatText(Settings.plugins.VcNarrator[event + "Message"], "", ChannelStore.getChannel(chanId).name, "", ""));
}
},
start() {
if (typeof speechSynthesis === "undefined" || speechSynthesis.getVoices().length === 0) {
new Logger("VcNarrator").warn(
"SpeechSynthesis not supported or no Narrator voices found. Thus, this plugin will not work. Check my Settings for more info"
);
return;
}
},
optionsCache: null as Record<string, PluginOptionsItem> | null,
get options() {
return this.optionsCache ??= {
const settings = definePluginSettings({
voice: {
type: OptionType.SELECT,
description: "Narrator Voice",
options: window.speechSynthesis?.getVoices().map(v => ({
label: v.name,
value: v.voiceURI,
default: v.default
})) ?? []
options: [
{
label: "Microsoft David - English (United States)",
value: "Microsoft David - English (United States)",
default: true
},
{
label: "Microsoft Mark - English (United States)",
value: "Microsoft Mark - English (United States)"
},
{
label: "Microsoft Zira - English (United States)",
value: "Microsoft Zira - English (United States)"
}
],
},
volume: {
type: OptionType.SLIDER,
@ -290,7 +118,139 @@ export default definePlugin({
description: "Undeafen Message (only self for now)",
default: "{{USER}} undeafened"
}
} satisfies Record<string, PluginOptionsItem>;
});
// Mute/Deaf for other people than you is commented out, because otherwise someone can spam it and it will be annoying
// Filtering out events is not as simple as just dropping duplicates, as otherwise mute, unmute, mute would
// not say the second mute, which would lead you to believe they're unmuted
function speak(text: string) {
if (!text) return;
const set = settings.store;
const speech = new SpeechSynthesisUtterance(text);
let voice = speechSynthesis.getVoices().find(v => v.voiceURI === settings.store.voice);
if (!voice) {
new Logger("VcNarrator").error(`Voice "${settings.store.voice}" not found. Resetting to default.`);
voice = speechSynthesis.getVoices().find(v => v.default);
if (!voice) return; // This should never happen
settings.store.voice = voice.voiceURI;
}
speech.voice = voice!;
speech.volume = settings.store.volume;
speech.rate = settings.store.rate;
speechSynthesis.speak(speech);
}
function clean(str: string) {
const replacer = settings.store.latinOnly
? /[^\p{Script=Latin}\p{Number}\p{Punctuation}\s]/gu
: /[^\p{Letter}\p{Number}\p{Punctuation}\s]/gu;
return str.normalize("NFKC")
.replace(replacer, "")
.replace(/_{2,}/g, "_")
.trim();
}
function formatText(str: string, user: string, channel: string, displayName: string, nickname: string) {
return str
.replaceAll("{{USER}}", clean(user) || (user ? "Someone" : ""))
.replaceAll("{{CHANNEL}}", clean(channel) || "channel")
.replaceAll("{{DISPLAY_NAME}}", clean(displayName) || (displayName ? "Someone" : ""))
.replaceAll("{{NICKNAME}}", clean(nickname) || (nickname ? "Someone" : ""));
}
// For every user, channelId and oldChannelId will differ when moving channel.
// Only for the local user, channelId and oldChannelId will be the same when moving channel,
// for some ungodly reason
let myLastChannelId: string | undefined;
function getTypeAndChannelId({ channelId, oldChannelId }: VoiceState, isMe: boolean) {
if (isMe && channelId !== myLastChannelId) {
oldChannelId = myLastChannelId;
myLastChannelId = channelId;
}
if (channelId !== oldChannelId) {
if (channelId) return [oldChannelId ? "move" : "join", channelId];
if (oldChannelId) return ["leave", oldChannelId];
}
return ["", ""];
}
function playSample(tempSettings: any, type: string) {
const settingsobj = Object.assign({}, settings.store, tempSettings);
const currentUser = UserStore.getCurrentUser();
const myGuildId = SelectedGuildStore.getGuildId();
speak(formatText(settingsobj[type + "Message"], currentUser.username, "general", (currentUser as any).globalName ?? currentUser.username, GuildMemberStore.getNick(myGuildId, currentUser.id) ?? currentUser.username));
}
export default definePlugin({
name: "VcNarrator",
description: "Announces when users join, leave, or move voice channels via narrator",
authors: [Devs.Ven],
reporterTestable: ReporterTestable.None,
settings,
flux: {
VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) {
const myGuildId = SelectedGuildStore.getGuildId();
const myChanId = SelectedChannelStore.getVoiceChannelId();
const myId = UserStore.getCurrentUser().id;
if (ChannelStore.getChannel(myChanId!)?.type === 13 /* Stage Channel */) return;
for (const state of voiceStates) {
const { userId, channelId, oldChannelId } = state;
const isMe = userId === myId;
if (!isMe) {
if (!myChanId) continue;
if (channelId !== myChanId && oldChannelId !== myChanId) continue;
}
const [type, id] = getTypeAndChannelId(state, isMe);
if (!type) continue;
const template = settings.store[type + "Message"];
const user = isMe && !settings.store.sayOwnName ? "" : UserStore.getUser(userId).username;
const displayName = user && ((UserStore.getUser(userId) as any).globalName ?? user);
const nickname = user && (GuildMemberStore.getNick(myGuildId, userId) ?? user);
const channel = ChannelStore.getChannel(id).name;
speak(formatText(template, user, channel, displayName, nickname));
// updateStatuses(type, state, isMe);
}
},
AUDIO_TOGGLE_SELF_MUTE() {
const chanId = SelectedChannelStore.getVoiceChannelId()!;
const s = VoiceStateStore.getVoiceStateForChannel(chanId) as VoiceState;
if (!s) return;
const event = s.mute || s.selfMute ? "unmute" : "mute";
speak(formatText(Settings.plugins.VcNarrator[event + "Message"], "", ChannelStore.getChannel(chanId).name, "", ""));
},
AUDIO_TOGGLE_SELF_DEAF() {
const chanId = SelectedChannelStore.getVoiceChannelId()!;
const s = VoiceStateStore.getVoiceStateForChannel(chanId) as VoiceState;
if (!s) return;
const event = s.deaf || s.selfDeaf ? "undeafen" : "deafen";
speak(formatText(Settings.plugins.VcNarrator[event + "Message"], "", ChannelStore.getChannel(chanId).name, "", ""));
}
},
start() {
if (typeof speechSynthesis === "undefined" || speechSynthesis.getVoices().length === 0) {
new Logger("VcNarrator").warn(
"SpeechSynthesis not supported or no Narrator voices found. Thus, this plugin will not work. Check my Settings for more info"
);
return;
}
},
settingsAboutComponent({ tempSettings: s }) {
@ -300,7 +260,7 @@ export default definePlugin({
}, []);
const types = useMemo(
() => Object.keys(Vencord.Plugins.plugins.VcNarrator.options!).filter(k => k.endsWith("Message")).map(k => k.slice(0, -7)),
() => Object.keys(settings.store!).filter(k => k.endsWith("Message")).map(k => k.slice(0, -7)),
[],
);