2025-04-07 00:58:07 +02:00
/ *
* Vencord , a Discord client mod
* Copyright ( c ) 2025 Vendicated and contributors
* SPDX - License - Identifier : GPL - 3.0 - or - later
* /
2025-10-10 21:11:31 -04:00
import { Heading } from "@components/Heading" ;
2025-10-11 21:50:14 -04:00
import { Paragraph } from "@components/Paragraph" ;
import { SearchableSelect , useMemo , useState } from "@webpack/common" ;
2025-04-07 00:58:07 +02:00
import { getCurrentVoice , settings } from "./settings" ;
// TODO: replace by [Object.groupBy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/groupBy) once it has more maturity
function groupBy < T extends object , K extends PropertyKey > ( arr : T [ ] , fn : ( obj : T ) = > K ) {
return arr . reduce ( ( acc , obj ) = > {
const value = fn ( obj ) ;
acc [ value ] ? ? = [ ] ;
acc [ value ] . push ( obj ) ;
return acc ;
} , { } as Record < K , T [ ] > ) ;
}
interface PickerProps {
voice : string | undefined ;
voices : SpeechSynthesisVoice [ ] ;
}
function SimplePicker ( { voice , voices } : PickerProps ) {
const options = voices . map ( voice = > ( {
label : voice.name ,
value : voice.voiceURI ,
default : voice . default ,
} ) ) ;
return (
< SearchableSelect
placeholder = "Select a voice"
maxVisibleItems = { 5 }
options = { options }
2026-02-06 03:34:29 +01:00
value = { options . find ( o = > o . value === voice ) ? . value }
2025-04-07 00:58:07 +02:00
onChange = { v = > settings . store . voice = v }
closeOnSelect
/ >
) ;
}
const languageNames = new Intl . DisplayNames ( [ "en" ] , { type : "language" } ) ;
function ComplexPicker ( { voice , voices } : PickerProps ) {
const groupedVoices = useMemo ( ( ) = > groupBy ( voices , voice = > voice . lang ) , [ voices ] ) ;
const languageNameMapping = useMemo ( ( ) = > {
const list = [ ] as Record < "name" | "friendlyName" , string > [ ] ;
for ( const name in groupedVoices ) {
try {
const friendlyName = languageNames . of ( name ) ;
if ( friendlyName ) {
list . push ( { name , friendlyName } ) ;
}
} catch { }
}
return list ;
} , [ groupedVoices ] ) ;
const [ selectedLanguage , setSelectedLanguage ] = useState ( ( ) = > getCurrentVoice ( ) ? . lang ? ? languageNameMapping [ 0 ] . name ) ;
if ( languageNameMapping . length === 1 ) {
return (
< SimplePicker
voice = { voice }
voices = { groupedVoices [ languageNameMapping [ 0 ] . name ] }
/ >
) ;
}
const voicesForLanguage = groupedVoices [ selectedLanguage ] ;
const languageOptions = languageNameMapping . map ( l = > ( {
label : l.friendlyName ,
value : l.name
} ) ) ;
return (
< >
2025-10-10 21:11:31 -04:00
< Heading > Language < / Heading >
2025-04-07 00:58:07 +02:00
< SearchableSelect
placeholder = "Select a language"
options = { languageOptions }
2026-02-06 03:34:29 +01:00
value = { languageOptions . find ( l = > l . value === selectedLanguage ) ? . value }
2025-04-07 00:58:07 +02:00
onChange = { v = > setSelectedLanguage ( v ) }
maxVisibleItems = { 5 }
closeOnSelect
/ >
2025-10-10 21:11:31 -04:00
< Heading > Voice < / Heading >
2025-04-07 00:58:07 +02:00
< SimplePicker
voice = { voice }
voices = { voicesForLanguage }
/ >
< / >
) ;
}
function VoiceSetting() {
const voices = useMemo ( ( ) = > window . speechSynthesis ? . getVoices ( ) ? ? [ ] , [ ] ) ;
const { voice } = settings . use ( [ "voice" ] ) ;
if ( ! voices . length )
2025-10-11 21:50:14 -04:00
return < Paragraph > No voices found . < / Paragraph > ;
2025-04-07 00:58:07 +02:00
// espeak on Linux has a ridiculous amount of voices (26k for me).
// If there are more than 20 voices, we split it up into two pickers, one for language, then one with only the voices for that language.
// This way, there are around 200-ish options per language
const Picker = voices . length > 20 ? ComplexPicker : SimplePicker ;
return < Picker voice = { voice } voices = { voices } / > ;
}
export function VoiceSettingSection() {
return (
2025-10-09 21:01:48 +02:00
< section >
2025-10-10 21:11:31 -04:00
< Heading > Voice < / Heading >
2025-04-07 00:58:07 +02:00
< VoiceSetting / >
2025-10-09 21:01:48 +02:00
< / section >
2025-04-07 00:58:07 +02:00
) ;
}