mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-05-06 06:46:04 +02:00
refactor: app.js (#1291)
* refactor: frontend * Fix avatar gallery sort * Update .NET dependencies * Update npm dependencies electron v37.1.0 * bulkRefreshFriends * fix dark theme * Remove crowdin * Fix config.json dialog not updating * VRCX log file fixes & add Cef log * Remove SharedVariable, fix startup * Revert init theme change * Logging date not working? Fix WinformThemer designer error * Add Cef request hander, no more escaping main page * clean * fix * fix * clean * uh * Apply thememode at startup, fixes random user colours * Split database into files * Instance info remove empty lines * Open external VRC links with VRCX * Electron fixes * fix userdialog style * ohhhh * fix store * fix store * fix: load all group members after kicking a user * fix: world dialog favorite button style * fix: Clear VRCX Cache Timer input value * clean * Fix VR overlay * Fix VR overlay 2 * Fix Discord discord rich presence for RPC worlds * Clean up age verified user tags * Fix playerList being occupied after program reload * no `this` * Fix login stuck loading * writable: false * Hide dialogs on logout * add flush sync option * rm LOGIN event * rm LOGOUT event * remove duplicate event listeners * remove duplicate event listeners * clean * remove duplicate event listeners * clean * fix theme style * fix t * clearable * clean * fix ipcEvent * Small changes * Popcorn Palace support * Remove checkActiveFriends * Clean up * Fix dragEnterCef * Block API requests when not logged in * Clear state on login & logout * Fix worldDialog instances not updating * use <script setup> * Fix avatar change event, CheckGameRunning at startup * Fix image dragging * fix * Remove PWI * fix updateLoop * add webpack-dev-server to dev environment * rm unnecessary chunks * use <script setup> * webpack-dev-server changes * use <script setup> * use <script setup> * Fix UGC text size * Split login event * t * use <script setup> * fix * Update .gitignore and enable checkJs in jsconfig * fix i18n t * use <script setup> * use <script setup> * clean * global types * fix * use checkJs for debugging * Add watchState for login watchers * fix .vue template * type fixes * rm Vue.filter * Cef v138.0.170, VC++ 2022 * Settings fixes * Remove 'USER:CURRENT' * clean up 2FA callbacks * remove userApply * rm i18n import * notification handling to use notification store methods * refactor favorite handling to use favorite store methods and clean up event emissions * refactor moderation handling to use dedicated functions for player moderation events * refactor friend handling to use dedicated functions for friend events * Fix program startup, move lang init * Fix friend state * Fix status change error * Fix user notes diff * fix * rm group event * rm auth event * rm avatar event * clean * clean * getUser * getFriends * getFavoriteWorlds, getFavoriteAvatars * AvatarGalleryUpload btn style & package.json update * Fix friend requests * Apply user * Apply world * Fix note diff * Fix VR overlay * Fixes * Update build scripts * Apply avatar * Apply instance * Apply group * update hidden VRC+ badge * Fix sameInstance "private" * fix 502/504 API errors * fix 502/504 API errors * clean * Fix friend in same instance on orange showing twice in friends list * Add back in broken friend state repair methods * add types --------- Co-authored-by: Natsumi <cmcooper123@hotmail.com>
This commit is contained in:
@@ -0,0 +1,518 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { computed, reactive } from 'vue';
|
||||
import { $app } from '../../app';
|
||||
import { t } from '../../plugin';
|
||||
import configRepository from '../../service/config';
|
||||
import { sharedFeedFiltersDefaults } from '../../shared/constants';
|
||||
import { useVrStore } from '../vr';
|
||||
|
||||
export const useNotificationsSettingsStore = defineStore(
|
||||
'NotificationsSettings',
|
||||
() => {
|
||||
const vrStore = useVrStore();
|
||||
const state = reactive({
|
||||
overlayToast: true,
|
||||
openVR: false,
|
||||
overlayNotifications: true,
|
||||
xsNotifications: true,
|
||||
ovrtHudNotifications: true,
|
||||
ovrtWristNotifications: false,
|
||||
imageNotifications: true,
|
||||
desktopToast: 'Never',
|
||||
afkDesktopToast: false,
|
||||
notificationTTS: 'Never',
|
||||
notificationTTSNickName: false,
|
||||
sharedFeedFilters: {
|
||||
noty: {
|
||||
Location: 'Off',
|
||||
OnPlayerJoined: 'VIP',
|
||||
OnPlayerLeft: 'VIP',
|
||||
OnPlayerJoining: 'VIP',
|
||||
Online: 'VIP',
|
||||
Offline: 'VIP',
|
||||
GPS: 'Off',
|
||||
Status: 'Off',
|
||||
invite: 'Friends',
|
||||
requestInvite: 'Friends',
|
||||
inviteResponse: 'Friends',
|
||||
requestInviteResponse: 'Friends',
|
||||
friendRequest: 'On',
|
||||
Friend: 'On',
|
||||
Unfriend: 'On',
|
||||
DisplayName: 'VIP',
|
||||
TrustLevel: 'VIP',
|
||||
boop: 'Off',
|
||||
groupChange: 'On',
|
||||
'group.announcement': 'On',
|
||||
'group.informative': 'On',
|
||||
'group.invite': 'On',
|
||||
'group.joinRequest': 'Off',
|
||||
'group.transfer': 'On',
|
||||
'group.queueReady': 'On',
|
||||
'instance.closed': 'On',
|
||||
PortalSpawn: 'Everyone',
|
||||
Event: 'On',
|
||||
External: 'On',
|
||||
VideoPlay: 'Off',
|
||||
BlockedOnPlayerJoined: 'Off',
|
||||
BlockedOnPlayerLeft: 'Off',
|
||||
MutedOnPlayerJoined: 'Off',
|
||||
MutedOnPlayerLeft: 'Off',
|
||||
AvatarChange: 'Off',
|
||||
ChatBoxMessage: 'Off',
|
||||
Blocked: 'Off',
|
||||
Unblocked: 'Off',
|
||||
Muted: 'Off',
|
||||
Unmuted: 'Off'
|
||||
},
|
||||
wrist: {
|
||||
Location: 'On',
|
||||
OnPlayerJoined: 'Everyone',
|
||||
OnPlayerLeft: 'Everyone',
|
||||
OnPlayerJoining: 'Friends',
|
||||
Online: 'Friends',
|
||||
Offline: 'Friends',
|
||||
GPS: 'Friends',
|
||||
Status: 'Friends',
|
||||
invite: 'Friends',
|
||||
requestInvite: 'Friends',
|
||||
inviteResponse: 'Friends',
|
||||
requestInviteResponse: 'Friends',
|
||||
friendRequest: 'On',
|
||||
Friend: 'On',
|
||||
Unfriend: 'On',
|
||||
DisplayName: 'Friends',
|
||||
TrustLevel: 'Friends',
|
||||
boop: 'On',
|
||||
groupChange: 'On',
|
||||
'group.announcement': 'On',
|
||||
'group.informative': 'On',
|
||||
'group.invite': 'On',
|
||||
'group.joinRequest': 'On',
|
||||
'group.transfer': 'On',
|
||||
'group.queueReady': 'On',
|
||||
'instance.closed': 'On',
|
||||
PortalSpawn: 'Everyone',
|
||||
Event: 'On',
|
||||
External: 'On',
|
||||
VideoPlay: 'On',
|
||||
BlockedOnPlayerJoined: 'Off',
|
||||
BlockedOnPlayerLeft: 'Off',
|
||||
MutedOnPlayerJoined: 'Off',
|
||||
MutedOnPlayerLeft: 'Off',
|
||||
AvatarChange: 'Everyone',
|
||||
ChatBoxMessage: 'Off',
|
||||
Blocked: 'On',
|
||||
Unblocked: 'On',
|
||||
Muted: 'On',
|
||||
Unmuted: 'On'
|
||||
}
|
||||
},
|
||||
isTestTTSVisible: false,
|
||||
notificationTTSVoice: 0,
|
||||
notificationTTSTest: '',
|
||||
TTSvoices: [],
|
||||
notificationPosition: 'topCenter',
|
||||
notificationTimeout: 3000
|
||||
});
|
||||
|
||||
async function initNotificationsSettings() {
|
||||
const [
|
||||
overlayToast,
|
||||
overlayNotifications,
|
||||
openVR,
|
||||
xsNotifications,
|
||||
ovrtHudNotifications,
|
||||
ovrtWristNotifications,
|
||||
imageNotifications,
|
||||
desktopToast,
|
||||
afkDesktopToast,
|
||||
notificationTTS,
|
||||
notificationTTSNickName,
|
||||
sharedFeedFilters,
|
||||
notificationTTSVoice,
|
||||
notificationPosition,
|
||||
notificationTimeout
|
||||
] = await Promise.all([
|
||||
configRepository.getString('VRCX_overlayToast', 'Game Running'),
|
||||
configRepository.getBool('VRCX_overlayNotifications', true),
|
||||
configRepository.getBool('openVR'),
|
||||
configRepository.getBool('VRCX_xsNotifications', true),
|
||||
configRepository.getBool('VRCX_ovrtHudNotifications', true),
|
||||
configRepository.getBool('VRCX_ovrtWristNotifications', false),
|
||||
configRepository.getBool('VRCX_imageNotifications', true),
|
||||
configRepository.getString('VRCX_desktopToast', 'Never'),
|
||||
configRepository.getBool('VRCX_afkDesktopToast', false),
|
||||
configRepository.getString('VRCX_notificationTTS', 'Never'),
|
||||
configRepository.getBool('VRCX_notificationTTSNickName', false),
|
||||
configRepository.getString(
|
||||
'sharedFeedFilters',
|
||||
JSON.stringify(sharedFeedFiltersDefaults)
|
||||
),
|
||||
configRepository.getString('VRCX_notificationTTSVoice', '0'),
|
||||
configRepository.getString(
|
||||
'VRCX_notificationPosition',
|
||||
'topCenter'
|
||||
),
|
||||
configRepository.getString('VRCX_notificationTimeout', '3000')
|
||||
]);
|
||||
|
||||
state.overlayToast = overlayToast;
|
||||
state.openVR = openVR;
|
||||
state.overlayNotifications = overlayNotifications;
|
||||
state.xsNotifications = xsNotifications;
|
||||
state.ovrtHudNotifications = ovrtHudNotifications;
|
||||
state.ovrtWristNotifications = ovrtWristNotifications;
|
||||
state.imageNotifications = imageNotifications;
|
||||
state.desktopToast = desktopToast;
|
||||
state.afkDesktopToast = afkDesktopToast;
|
||||
state.notificationTTS = notificationTTS;
|
||||
state.notificationTTSNickName = notificationTTSNickName;
|
||||
state.sharedFeedFilters = JSON.parse(sharedFeedFilters);
|
||||
state.notificationTTSVoice = notificationTTSVoice;
|
||||
state.TTSvoices = speechSynthesis.getVoices();
|
||||
state.notificationPosition = notificationPosition;
|
||||
state.notificationTimeout = notificationTimeout;
|
||||
|
||||
initSharedFeedFilters();
|
||||
|
||||
if (LINUX) {
|
||||
setTimeout(() => {
|
||||
updateTTSVoices();
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
initNotificationsSettings();
|
||||
|
||||
const overlayToast = computed(() => state.overlayToast);
|
||||
const openVR = computed(() => state.openVR);
|
||||
const overlayNotifications = computed(() => state.overlayNotifications);
|
||||
const xsNotifications = computed(() => state.xsNotifications);
|
||||
const ovrtHudNotifications = computed(() => state.ovrtHudNotifications);
|
||||
const ovrtWristNotifications = computed(
|
||||
() => state.ovrtWristNotifications
|
||||
);
|
||||
const imageNotifications = computed(() => state.imageNotifications);
|
||||
const desktopToast = computed(() => state.desktopToast);
|
||||
const afkDesktopToast = computed(() => state.afkDesktopToast);
|
||||
const notificationTTS = computed(() => state.notificationTTS);
|
||||
const notificationTTSNickName = computed(
|
||||
() => state.notificationTTSNickName
|
||||
);
|
||||
const sharedFeedFilters = computed({
|
||||
get: () => state.sharedFeedFilters,
|
||||
set: (value) => (state.sharedFeedFilters = value)
|
||||
});
|
||||
const isTestTTSVisible = computed({
|
||||
get: () => state.isTestTTSVisible,
|
||||
set: (value) => (state.isTestTTSVisible = value)
|
||||
});
|
||||
const notificationTTSVoice = computed({
|
||||
get: () => state.notificationTTSVoice,
|
||||
set: (value) => (state.notificationTTSVoice = value)
|
||||
});
|
||||
const TTSvoices = computed({
|
||||
get: () => state.TTSvoices,
|
||||
set: (value) => (state.TTSvoices = value)
|
||||
});
|
||||
const notificationTTSTest = computed({
|
||||
get: () => state.notificationTTSTest,
|
||||
set: (value) => (state.notificationTTSTest = value)
|
||||
});
|
||||
const notificationPosition = computed(() => state.notificationPosition);
|
||||
const notificationTimeout = computed({
|
||||
get: () => state.notificationTimeout,
|
||||
set: (value) => (state.notificationTimeout = value)
|
||||
});
|
||||
|
||||
function setOverlayToast(value) {
|
||||
state.overlayToast = value;
|
||||
configRepository.setString('VRCX_overlayToast', value);
|
||||
}
|
||||
function setOverlayNotifications() {
|
||||
state.overlayNotifications = !state.overlayNotifications;
|
||||
configRepository.setBool(
|
||||
'VRCX_overlayNotifications',
|
||||
state.overlayNotifications
|
||||
);
|
||||
}
|
||||
function setOpenVR() {
|
||||
state.openVR = !state.openVR;
|
||||
configRepository.setBool('openVR', state.openVR);
|
||||
}
|
||||
function setXsNotifications() {
|
||||
state.xsNotifications = !state.xsNotifications;
|
||||
configRepository.setBool(
|
||||
'VRCX_xsNotifications',
|
||||
state.xsNotifications
|
||||
);
|
||||
}
|
||||
function setOvrtHudNotifications() {
|
||||
state.ovrtHudNotifications = !state.ovrtHudNotifications;
|
||||
configRepository.setBool(
|
||||
'VRCX_ovrtHudNotifications',
|
||||
state.ovrtHudNotifications
|
||||
);
|
||||
}
|
||||
function setOvrtWristNotifications() {
|
||||
state.ovrtWristNotifications = !state.ovrtWristNotifications;
|
||||
configRepository.setBool(
|
||||
'VRCX_ovrtWristNotifications',
|
||||
state.ovrtWristNotifications
|
||||
);
|
||||
}
|
||||
function setImageNotifications() {
|
||||
state.imageNotifications = !state.imageNotifications;
|
||||
configRepository.setBool(
|
||||
'VRCX_imageNotifications',
|
||||
state.imageNotifications
|
||||
);
|
||||
}
|
||||
|
||||
function changeNotificationPosition(value) {
|
||||
state.notificationPosition = value;
|
||||
configRepository.setString(
|
||||
'VRCX_notificationPosition',
|
||||
state.notificationPosition
|
||||
);
|
||||
vrStore.updateVRConfigVars();
|
||||
}
|
||||
/**
|
||||
* @param {string} value
|
||||
*/
|
||||
function setDesktopToast(value) {
|
||||
state.desktopToast = value;
|
||||
configRepository.setString('VRCX_desktopToast', value);
|
||||
}
|
||||
function setAfkDesktopToast() {
|
||||
state.afkDesktopToast = !state.afkDesktopToast;
|
||||
configRepository.setBool(
|
||||
'VRCX_afkDesktopToast',
|
||||
state.afkDesktopToast
|
||||
);
|
||||
}
|
||||
/**
|
||||
* @param {string} value
|
||||
*/
|
||||
function setNotificationTTS(value) {
|
||||
state.notificationTTS = value;
|
||||
configRepository.setString('VRCX_notificationTTS', value);
|
||||
}
|
||||
function setNotificationTTSNickName() {
|
||||
state.notificationTTSNickName = !state.notificationTTSNickName;
|
||||
configRepository.setBool(
|
||||
'VRCX_notificationTTSNickName',
|
||||
state.notificationTTSNickName
|
||||
);
|
||||
}
|
||||
function initSharedFeedFilters() {
|
||||
if (!state.sharedFeedFilters.noty.Blocked) {
|
||||
state.sharedFeedFilters.noty.Blocked = 'Off';
|
||||
state.sharedFeedFilters.noty.Unblocked = 'Off';
|
||||
state.sharedFeedFilters.noty.Muted = 'Off';
|
||||
state.sharedFeedFilters.noty.Unmuted = 'Off';
|
||||
state.sharedFeedFilters.wrist.Blocked = 'On';
|
||||
state.sharedFeedFilters.wrist.Unblocked = 'On';
|
||||
state.sharedFeedFilters.wrist.Muted = 'On';
|
||||
state.sharedFeedFilters.wrist.Unmuted = 'On';
|
||||
}
|
||||
if (!state.sharedFeedFilters.noty['group.announcement']) {
|
||||
state.sharedFeedFilters.noty['group.announcement'] = 'On';
|
||||
state.sharedFeedFilters.noty['group.informative'] = 'On';
|
||||
state.sharedFeedFilters.noty['group.invite'] = 'On';
|
||||
state.sharedFeedFilters.noty['group.joinRequest'] = 'Off';
|
||||
state.sharedFeedFilters.wrist['group.announcement'] = 'On';
|
||||
state.sharedFeedFilters.wrist['group.informative'] = 'On';
|
||||
state.sharedFeedFilters.wrist['group.invite'] = 'On';
|
||||
state.sharedFeedFilters.wrist['group.joinRequest'] = 'On';
|
||||
}
|
||||
if (!state.sharedFeedFilters.noty['group.queueReady']) {
|
||||
state.sharedFeedFilters.noty['group.queueReady'] = 'On';
|
||||
state.sharedFeedFilters.wrist['group.queueReady'] = 'On';
|
||||
}
|
||||
if (!state.sharedFeedFilters.noty['instance.closed']) {
|
||||
state.sharedFeedFilters.noty['instance.closed'] = 'On';
|
||||
state.sharedFeedFilters.wrist['instance.closed'] = 'On';
|
||||
}
|
||||
if (!state.sharedFeedFilters.noty.External) {
|
||||
state.sharedFeedFilters.noty.External = 'On';
|
||||
state.sharedFeedFilters.wrist.External = 'On';
|
||||
}
|
||||
if (!state.sharedFeedFilters.noty.groupChange) {
|
||||
state.sharedFeedFilters.noty.groupChange = 'On';
|
||||
state.sharedFeedFilters.wrist.groupChange = 'On';
|
||||
}
|
||||
if (!state.sharedFeedFilters.noty['group.transfer']) {
|
||||
state.sharedFeedFilters.noty['group.transfer'] = 'On';
|
||||
state.sharedFeedFilters.wrist['group.transfer'] = 'On';
|
||||
}
|
||||
if (!state.sharedFeedFilters.noty.boop) {
|
||||
state.sharedFeedFilters.noty.boop = 'Off';
|
||||
state.sharedFeedFilters.wrist.boop = 'On';
|
||||
}
|
||||
}
|
||||
function setNotificationTTSVoice(index) {
|
||||
state.notificationTTSVoice = index;
|
||||
configRepository.setString(
|
||||
'VRCX_notificationTTSVoice',
|
||||
state.notificationTTSVoice
|
||||
);
|
||||
}
|
||||
|
||||
function getTTSVoiceName() {
|
||||
let voices;
|
||||
if (LINUX) {
|
||||
voices = state.TTSvoices;
|
||||
} else {
|
||||
voices = speechSynthesis.getVoices();
|
||||
}
|
||||
if (voices.length === 0) {
|
||||
return '';
|
||||
}
|
||||
if (state.notificationTTSVoice >= voices.length) {
|
||||
setNotificationTTSVoice(0);
|
||||
}
|
||||
return voices[state.notificationTTSVoice].name;
|
||||
}
|
||||
|
||||
async function changeTTSVoice(index) {
|
||||
setNotificationTTSVoice(index);
|
||||
let voices;
|
||||
if (LINUX) {
|
||||
voices = state.TTSvoices;
|
||||
} else {
|
||||
voices = speechSynthesis.getVoices();
|
||||
}
|
||||
if (voices.length === 0) {
|
||||
return;
|
||||
}
|
||||
const voiceName = voices[index].name;
|
||||
speechSynthesis.cancel();
|
||||
speak(voiceName);
|
||||
}
|
||||
|
||||
function updateTTSVoices() {
|
||||
state.TTSvoices = speechSynthesis.getVoices();
|
||||
if (LINUX) {
|
||||
const voices = speechSynthesis.getVoices();
|
||||
let uniqueVoices = [];
|
||||
voices.forEach((voice) => {
|
||||
if (!uniqueVoices.some((v) => v.lang === voice.lang)) {
|
||||
uniqueVoices.push(voice);
|
||||
}
|
||||
});
|
||||
uniqueVoices = uniqueVoices.filter((v) =>
|
||||
v.lang.startsWith('en')
|
||||
);
|
||||
state.TTSvoices = uniqueVoices;
|
||||
}
|
||||
}
|
||||
async function saveNotificationTTS(value) {
|
||||
speechSynthesis.cancel();
|
||||
if (
|
||||
(await configRepository.getString('VRCX_notificationTTS')) ===
|
||||
'Never' &&
|
||||
value !== 'Never'
|
||||
) {
|
||||
speak('Notification text-to-speech enabled');
|
||||
}
|
||||
setNotificationTTS(value);
|
||||
}
|
||||
|
||||
function testNotificationTTS() {
|
||||
speechSynthesis.cancel();
|
||||
speak(state.notificationTTSTest);
|
||||
}
|
||||
|
||||
function speak(text) {
|
||||
const tts = new SpeechSynthesisUtterance();
|
||||
const voices = speechSynthesis.getVoices();
|
||||
if (voices.length === 0) {
|
||||
return;
|
||||
}
|
||||
let index = 0;
|
||||
if (state.notificationTTSVoice < voices.length) {
|
||||
index = state.notificationTTSVoice;
|
||||
}
|
||||
tts.voice = voices[index];
|
||||
tts.text = text;
|
||||
speechSynthesis.speak(tts);
|
||||
}
|
||||
|
||||
function promptNotificationTimeout() {
|
||||
$app.$prompt(
|
||||
t('prompt.notification_timeout.description'),
|
||||
t('prompt.notification_timeout.header'),
|
||||
{
|
||||
distinguishCancelAndClose: true,
|
||||
confirmButtonText: t('prompt.notification_timeout.ok'),
|
||||
cancelButtonText: t('prompt.notification_timeout.cancel'),
|
||||
inputValue: state.notificationTimeout / 1000,
|
||||
inputPattern: /\d+$/,
|
||||
inputErrorMessage: t(
|
||||
'prompt.notification_timeout.input_error'
|
||||
),
|
||||
callback: async (action, instance) => {
|
||||
if (
|
||||
action === 'confirm' &&
|
||||
instance.inputValue &&
|
||||
!isNaN(instance.inputValue)
|
||||
) {
|
||||
state.notificationTimeout = Math.trunc(
|
||||
Number(instance.inputValue) * 1000
|
||||
);
|
||||
await configRepository.setString(
|
||||
'VRCX_notificationTimeout',
|
||||
state.notificationTimeout
|
||||
);
|
||||
vrStore.updateVRConfigVars();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
state,
|
||||
|
||||
overlayToast,
|
||||
openVR,
|
||||
overlayNotifications,
|
||||
xsNotifications,
|
||||
ovrtHudNotifications,
|
||||
ovrtWristNotifications,
|
||||
imageNotifications,
|
||||
desktopToast,
|
||||
afkDesktopToast,
|
||||
notificationTTS,
|
||||
notificationTTSNickName,
|
||||
sharedFeedFilters,
|
||||
isTestTTSVisible,
|
||||
notificationTTSVoice,
|
||||
TTSvoices,
|
||||
notificationTTSTest,
|
||||
notificationPosition,
|
||||
notificationTimeout,
|
||||
|
||||
setOverlayToast,
|
||||
setOpenVR,
|
||||
setOverlayNotifications,
|
||||
setXsNotifications,
|
||||
setOvrtHudNotifications,
|
||||
setOvrtWristNotifications,
|
||||
setImageNotifications,
|
||||
setDesktopToast,
|
||||
setAfkDesktopToast,
|
||||
setNotificationTTS,
|
||||
setNotificationTTSNickName,
|
||||
getTTSVoiceName,
|
||||
changeTTSVoice,
|
||||
saveNotificationTTS,
|
||||
testNotificationTTS,
|
||||
speak,
|
||||
changeNotificationPosition,
|
||||
promptNotificationTimeout
|
||||
};
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user