// Copyright(c) 2019-2020 pypy and individual contributors. // All rights reserved. // // This work is licensed under the terms of the MIT license. // For a copy, see . import Noty from 'noty'; import Vue from 'vue'; import ElementUI from 'element-ui'; import locale from 'element-ui/lib/locale/lang/en'; import sharedRepository from './repository/shared.js'; import configRepository from './repository/config.js'; import webApiService from './service/webapi.js'; speechSynthesis.getVoices(); (async function () { await CefSharp.BindObjectAsync( 'AppApi', 'WebApi', 'SharedVariable', 'SQLite' ); await configRepository.init(); Noty.overrideDefaults({ animation: { open: 'animate__animated animate__fadeIn', close: 'animate__animated animate__zoomOut' }, layout: 'topCenter', theme: 'relax', timeout: 3000 }); Vue.use(ElementUI, { locale }); var escapeTag = (s) => String(s).replace(/["&'<>]/gu, (c) => `&#${c.charCodeAt(0)};`); Vue.filter('escapeTag', escapeTag); var commaNumber = (n) => String(Number(n) || 0).replace(/(\d)(?=(\d{3})+(?!\d))/gu, '$1,'); Vue.filter('commaNumber', commaNumber); var formatDate = (s, format) => { var dt = new Date(s); if (isNaN(dt)) { return escapeTag(s); } var hours = dt.getHours(); var map = { 'YYYY': String(10000 + dt.getFullYear()).substr(-4), 'MM': String(101 + dt.getMonth()).substr(-2), 'DD': String(100 + dt.getDate()).substr(-2), 'HH24': String(100 + hours).substr(-2), 'HH': String(100 + (hours > 12 ? hours - 12 : hours)).substr(-2), 'MI': String(100 + dt.getMinutes()).substr(-2), 'SS': String(100 + dt.getSeconds()).substr(-2), 'AMPM': hours >= 12 ? 'PM' : 'AM' }; return format.replace(/YYYY|MM|DD|HH24|HH|MI|SS|AMPM/gu, (c) => map[c] || c); }; Vue.filter('formatDate', formatDate); var textToHex = (s) => String(s).split('').map((c) => c.charCodeAt(0).toString(16)).join(' '); Vue.filter('textToHex', textToHex); var timeToText = (t) => { var sec = Number(t); if (isNaN(sec)) { return escapeTag(t); } sec = Math.floor(sec / 1000); var arr = []; if (sec < 0) { sec = -sec; } if (sec >= 86400) { arr.push(`${Math.floor(sec / 86400)}d`); sec %= 86400; } if (sec >= 3600) { arr.push(`${Math.floor(sec / 3600)}h`); sec %= 3600; } if (sec >= 60) { arr.push(`${Math.floor(sec / 60)}m`); sec %= 60; } if (sec || !arr.length) { arr.push(`${sec}s`); } return arr.join(' '); }; Vue.filter('timeToText', timeToText); // // API // var API = {}; API.eventHandlers = new Map(); API.$emit = function (name, ...args) { // console.log(name, ...args); var handlers = this.eventHandlers.get(name); if (handlers === undefined) { return; } try { for (var handler of handlers) { handler.apply(this, args); } } catch (err) { console.error(err); } }; API.$on = function (name, handler) { var handlers = this.eventHandlers.get(name); if (handlers === undefined) { handlers = []; this.eventHandlers.set(name, handlers); } handlers.push(handler); }; API.$off = function (name, handler) { var handlers = this.eventHandlers.get(name); if (handlers === undefined) { return; } var { length } = handlers; for (var i = 0; i < length; ++i) { if (handlers[i] === handler) { if (length > 1) { handlers.splice(i, 1); } else { this.eventHandlers.delete(name); } break; } } }; API.pendingGetRequests = new Map(); API.call = function (endpoint, options) { var init = { url: `https://api.vrchat.cloud/api/1/${endpoint}`, method: 'GET', ...options }; var { params } = init; var isGetRequest = init.method === 'GET'; if (isGetRequest === true) { // transform body to url if (params === Object(params)) { var url = new URL(init.url); var { searchParams } = url; for (var key in params) { searchParams.set(key, params[key]); } init.url = url.toString(); } // merge requests var req = this.pendingGetRequests.get(init.url); if (req !== undefined) { return req; } } else { init.headers = { 'Content-Type': 'application/json;charset=utf-8', ...init.headers }; init.body = params === Object(params) ? JSON.stringify(params) : '{}'; } var req = webApiService.execute(init).catch((err) => { this.$throw(0, err); }).then((response) => { try { response.data = JSON.parse(response.data); return response; } catch (e) { } if (response.status === 200) { this.$throw(0, 'Invalid JSON response'); } this.$throw(response.status); }).then(({ data, status }) => { if (data === Object(data)) { if (status === 200) { if (data.success === Object(data.success)) { new Noty({ type: 'success', text: escapeTag(data.success.message) }).show(); } return data; } if (data.error === Object(data.error)) { this.$throw( data.error.status_code || status, data.error.message, data.error.data ); } else if (typeof data.error === 'string') { this.$throw( data.status_code || status, data.error ); } } this.$throw(status, data); return data; }); if (isGetRequest === true) { req.finally(() => { this.pendingGetRequests.delete(init.url); }); this.pendingGetRequests.set(init.url, req); } return req; }; API.statusCodes = { 100: 'Continue', 101: 'Switching Protocols', 102: 'Processing', 103: 'Early Hints', 200: 'OK', 201: 'Created', 202: 'Accepted', 203: 'Non-Authoritative Information', 204: 'No Content', 205: 'Reset Content', 206: 'Partial Content', 207: 'Multi-Status', 208: 'Already Reported', 226: 'IM Used', 300: 'Multiple Choices', 301: 'Moved Permanently', 302: 'Found', 303: 'See Other', 304: 'Not Modified', 305: 'Use Proxy', 306: 'Switch Proxy', 307: 'Temporary Redirect', 308: 'Permanent Redirect', 400: 'Bad Request', 401: 'Unauthorized', 402: 'Payment Required', 403: 'Forbidden', 404: 'Not Found', 405: 'Method Not Allowed', 406: 'Not Acceptable', 407: 'Proxy Authentication Required', 408: 'Request Timeout', 409: 'Conflict', 410: 'Gone', 411: 'Length Required', 412: 'Precondition Failed', 413: 'Payload Too Large', 414: 'URI Too Long', 415: 'Unsupported Media Type', 416: 'Range Not Satisfiable', 417: 'Expectation Failed', 418: "I'm a teapot", 421: 'Misdirected Request', 422: 'Unprocessable Entity', 423: 'Locked', 424: 'Failed Dependency', 425: 'Too Early', 426: 'Upgrade Required', 428: 'Precondition Required', 429: 'Too Many Requests', 431: 'Request Header Fields Too Large', 451: 'Unavailable For Legal Reasons', 500: 'Internal Server Error', 501: 'Not Implemented', 502: 'Bad Gateway', 503: 'Service Unavailable', 504: 'Gateway Timeout', 505: 'HTTP Version Not Supported', 506: 'Variant Also Negotiates', 507: 'Insufficient Storage', 508: 'Loop Detected', 510: 'Not Extended', 511: 'Network Authentication Required', // CloudFlare Error 520: 'Web server returns an unknown error', 521: 'Web server is down', 522: 'Connection timed out', 523: 'Origin is unreachable', 524: 'A timeout occurred', 525: 'SSL handshake failed', 526: 'Invalid SSL certificate', 527: 'Railgun Listener to origin error' }; API.$throw = function (code, error) { var text = []; if (code > 0) { var status = this.statusCodes[code]; if (status === undefined) { text.push(`${code}`); } else { text.push(`${code} ${status}`); } } if (error !== undefined) { text.push(JSON.stringify(error)); } text = text.map((s) => escapeTag(s)).join('
'); if (text.length) { new Noty({ type: 'error', text }).show(); } throw new Error(text); }; // API: Config API.cachedConfig = {}; API.$on('CONFIG', function (args) { args.ref = this.applyConfig(args.json); }); API.applyConfig = function (json) { var ref = { clientApiKey: '', ...json }; this.cachedConfig = ref; return ref; }; API.getConfig = function () { return this.call('config', { method: 'GET' }).then((json) => { var args = { ref: null, json }; this.$emit('CONFIG', args); return args; }); }; // API: Location API.parseLocation = function (tag) { tag = String(tag || ''); var ctx = { tag, isOffline: false, isPrivate: false, worldId: '', instanceId: '', instanceName: '', accessType: '', userId: null, hiddenId: null, privateId: null, friendsId: null, canRequestInvite: false }; if (tag === 'offline') { ctx.isOffline = true; } else if (tag === 'private') { ctx.isPrivate = true; } else if (tag.startsWith('local') === false) { var sep = tag.indexOf(':'); if (sep >= 0) { ctx.worldId = tag.substr(0, sep); ctx.instanceId = tag.substr(sep + 1); ctx.instanceId.split('~').forEach((s, i) => { if (i) { var A = s.indexOf('('); var Z = A >= 0 ? s.lastIndexOf(')') : -1; var key = Z >= 0 ? s.substr(0, A) : s; var value = A < Z ? s.substr(A + 1, Z - A - 1) : ''; if (key === 'hidden') { ctx.hiddenId = value; } else if (key === 'private') { ctx.privateId = value; } else if (key === 'friends') { ctx.friendsId = value; } else if (key === 'canRequestInvite') { ctx.canRequestInvite = true; } } else { ctx.instanceName = s; } }); ctx.accessType = 'public'; if (ctx.privateId !== null) { if (ctx.canRequestInvite) { // InvitePlus ctx.accessType = 'invite+'; } else { // InviteOnly ctx.accessType = 'invite'; } ctx.userId = ctx.privateId; } else if (ctx.friendsId !== null) { // FriendsOnly ctx.accessType = 'friends'; ctx.userId = ctx.friendsId; } else if (ctx.hiddenId !== null) { // FriendsOfGuests ctx.accessType = 'friends+'; ctx.userId = ctx.hiddenId; } } else { ctx.worldId = tag; } } return ctx; }; Vue.component('location', { template: '{{ text }}', props: { location: String }, data() { return { text: this.location }; }, methods: { parse() { var L = API.parseLocation(this.location); if (L.isOffline) { this.text = 'Offline'; } else if (L.isPrivate) { this.text = 'Private'; } else if (L.worldId) { var ref = API.cachedWorlds.get(L.worldId); if (ref === undefined) { API.getWorld({ worldId: L.worldId }).then((args) => { if (L.tag === this.location) { if (L.instanceId) { this.text = `${args.json.name} #${L.instanceName} ${L.accessType}`; } else { this.text = args.json.name; } } return args; }); } else if (L.instanceId) { this.text = `${ref.name} #${L.instanceName} ${L.accessType}`; } else { this.text = ref.name; } } } }, watch: { location() { this.parse(); } }, created() { this.parse(); } }); // API: World API.cachedWorlds = new Map(); API.$on('WORLD', function (args) { args.ref = this.applyWorld(args.json); }); API.applyWorld = function (json) { var ref = this.cachedWorlds.get(json.id); if (ref === undefined) { ref = { id: '', name: '', description: '', authorId: '', authorName: '', capacity: 0, tags: [], releaseStatus: '', imageUrl: '', thumbnailImageUrl: '', assetUrl: '', assetUrlObject: {}, pluginUrl: '', pluginUrlObject: {}, unityPackageUrl: '', unityPackageUrlObject: {}, unityPackages: [], version: 0, favorites: 0, created_at: '', updated_at: '', publicationDate: '', labsPublicationDate: '', visits: 0, popularity: 0, heat: 0, publicOccupants: 0, privateOccupants: 0, occupants: 0, instances: [], // VRCX $isLabs: false, // ...json }; this.cachedWorlds.set(ref.id, ref); } else { Object.assign(ref, json); } ref.$isLabs = ref.tags.includes('system_labs'); return ref; }; /* params: { worldId: string } */ API.getWorld = function (params) { return this.call(`worlds/${params.worldId}`, { method: 'GET' }).then((json) => { var args = { ref: null, json, params }; this.$emit('WORLD', args); return args; }); }; // API: User API.cachedUsers = new Map(); API.$on('USER', function (args) { args.ref = this.applyUser(args.json); }); API.applyUser = function (json) { var ref = this.cachedUsers.get(json.id); if (ref === undefined) { ref = { id: '', username: '', displayName: '', userIcon: '', bio: '', bioLinks: [], currentAvatarImageUrl: '', currentAvatarThumbnailImageUrl: '', status: '', statusDescription: '', state: '', tags: [], developerType: '', last_login: '', last_platform: '', allowAvatarCopying: false, isFriend: false, location: '', worldId: '', instanceId: '', // VRCX ...json }; this.cachedUsers.set(ref.id, ref); } else { var props = {}; for (var prop in ref) { if (ref[prop] !== Object(ref[prop])) { props[prop] = true; } } var $ref = { ...ref }; Object.assign(ref, json); for (var prop in ref) { if (ref[prop] !== Object(ref[prop])) { props[prop] = true; } } for (var prop in props) { var asis = $ref[prop]; var tobe = ref[prop]; if (asis === tobe) { delete props[prop]; } else { props[prop] = [ tobe, asis ]; } } } return ref; }; /* params: { userId: string } */ API.getUser = function (params) { return this.call(`users/${params.userId}`, { method: 'GET' }).then((json) => { var args = { json, params }; this.$emit('USER', args); return args; }); }; /* params: { userId: string } */ API.getCachedUser = function (params) { return new Promise((resolve, reject) => { var ref = this.cachedUsers.get(params.userId); if (ref === undefined) { this.getUser(params).catch(reject).then(resolve); } else { resolve({ cache: true, json: ref, params, ref }); } }); }; var $app = { data: { API, // 1 = 대시보드랑 손목에 보이는거 // 2 = 항상 화면에 보이는 거 appType: location.href.substr(-1), currentTime: new Date().toJSON(), currentUserStatus: null, cpuUsage: 0, config: {}, isGameRunning: false, isGameNoVR: false, lastLocation: '', lastFeedEntry: [], wristFeed: [], notyMap: [], devices: [], }, computed: {}, methods: {}, watch: {}, el: '#x-app', mounted() { // https://media.discordapp.net/attachments/581757976625283083/611170278218924033/unknown.png // 현재 날짜 시간 // 컨트롤러 배터리 상황 // -- // OO is in Let's Just H!!!!! [GPS] // OO has logged in [Online] -> TODO: location // OO has logged out [Offline] -> TODO: location // OO has joined [OnPlayerJoined] // OO has left [OnPlayerLeft] // [Moderation] // OO has blocked you // OO has muted you // OO has hidden you // -- API.getConfig().catch((err) => { // FIXME: 어케 복구하냐 이건 throw err; }).then((args) => { if (this.appType === '1') { this.updateCpuUsageLoop(); } if (this.appType === '2') { this.initNotyMap(); } this.updateLoop(); return args; }); } }; $app.methods.updateVRConfigVars = function () { var newConfig = sharedRepository.getObject('VRConfigVars'); if ((newConfig) && (JSON.stringify(newConfig) !== JSON.stringify(this.config))) { this.config = newConfig; this.lastFeedEntry = []; } }; $app.methods.initNotyMap = function () { var feeds = sharedRepository.getArray('feeds'); if (feeds === null) { return; } var sharedFeedFilters = JSON.parse(configRepository.getString('sharedFeedFilters')); var filter = sharedFeedFilters.noty; var filtered = []; feeds.forEach((feed) => { if (filter[feed.type]) { if ((filter[feed.type] !== 'Off') && ((filter[feed.type] === 'Everyone') || (filter[feed.type] === 'On') || ((filter[feed.type] === 'Friends') && (feed.isFriend)) || ((filter[feed.type] === 'VIP') && (feed.isFavorite)))) { var displayName = ''; if (feed.displayName) { displayName = feed.displayName; } else if (feed.senderUsername) { displayName = feed.senderUsername; } else if (feed.sourceDisplayName) { displayName = feed.sourceDisplayName; } else if (feed.data) { displayName = feed.data; } else { console.error('missing displayName'); } if ((displayName) && (!this.notyMap[displayName]) || (this.notyMap[displayName] < feed.created_at)) { this.notyMap[displayName] = feed.created_at; } } } }); }; $app.methods.updateLoop = async function () { try { this.updateVRConfigVars(); if (!this.config) { return; } this.currentTime = new Date().toJSON(); this.currentUserStatus = sharedRepository.getString('current_user_status'); this.isGameRunning = sharedRepository.getBool('is_game_running'); this.isGameNoVR = sharedRepository.getBool('is_Game_No_VR'); this.lastLocation = sharedRepository.getString('last_location'); if ((!this.config.hideDevicesFromFeed) && (this.appType === '1')) { AppApi.GetVRDevices().then((devices) => { devices.forEach((device) => { device[2] = parseInt(device[2], 10); }); this.devices = devices; }); } else { this.devices = ''; } await this.updateSharedFeeds(); } catch (err) { console.error(err); } setTimeout(() => this.updateLoop(), 500); }; $app.methods.updateCpuUsageLoop = async function () { try { var cpuUsage = await AppApi.CpuUsage(); this.cpuUsage = cpuUsage.toFixed(2); } catch (err) { console.error(err); } setTimeout(() => this.updateCpuUsageLoop(), 1000); }; $app.methods.updateSharedFeeds = async function () { var feeds = sharedRepository.getArray('feeds'); if (feeds === null) { return; } if ((this.lastFeedEntry !== undefined) && (feeds[0].created_at === this.lastFeedEntry.created_at)) { return; } this.lastFeedEntry = feeds[0]; // OnPlayerJoining var bias = new Date(Date.now() - 120000).toJSON(); for (var i = 0; i < feeds.length; i++) { var ctx = feeds[i]; if ((ctx.created_at < bias) || (ctx.type === 'Location')) { break; } if ((ctx.type === 'GPS') && (ctx.location[0] === this.lastLocation)) { var joining = true; for (var k = 0; k < feeds.length; k++) { var feedItem = feeds[k]; if (((feedItem.type === 'OnPlayerJoined') && (feedItem.data === ctx.displayName)) || ((feedItem.type === 'Friend') && (feedItem.displayName === ctx.displayName))) { joining = false; break; } if ((feedItem.created_at < bias) || (feedItem.type === 'Location') || ((feedItem.type === 'GPS') && (feedItem.created_at !== ctx.created_at) && (feedItem.displayName === ctx.displayName))) { break; } } if (joining) { var onPlayerJoining = {}; onPlayerJoining.created_at = ctx.created_at; onPlayerJoining.data = ctx.displayName; onPlayerJoining.isFavorite = ctx.isFavorite; onPlayerJoining.isFriend = ctx.isFriend; onPlayerJoining.type = 'OnPlayerJoining'; feeds.splice(i, 0, onPlayerJoining); i++; } } } // on Location change remove OnPlayerJoined if (this.config.hideOnPlayerJoined) { for (i = 0; i < feeds.length; i++) { var ctx = feeds[i]; if (ctx.type === 'Location') { var bias = new Date(Date.parse(ctx.created_at) + 10000).toJSON(); for (var k = i - 1; k > 0; k--) { var feedItem = feeds[k]; if (feedItem.type === 'OnPlayerJoined') { feeds.splice(k, 1); i--; } if ((feedItem.created_at > bias) || (feedItem.type === 'Location')) { break; } } } } } if (this.config.hidePrivateFromFeed) { for (var i = 0; i < feeds.length; i++) { var feed = feeds[i]; if ((feed.type === 'GPS') && (feed.location[0] === 'private')) { feeds.splice(i, 1); i--; } } } feeds.splice(25); if (this.appType === '1') { this.updateSharedFeedWrist(feeds); } if (this.appType === '2') { this.updateSharedFeedNoty(feeds); } }; $app.methods.updateSharedFeedWrist = async function (feeds) { var filter = this.config.sharedFeedFilters.wrist; var filtered = []; feeds.forEach((feed) => { if (filter[feed.type]) { if ((filter[feed.type] !== 'Off') && ((filter[feed.type] === 'Everyone') || (filter[feed.type] === 'On') || ((filter[feed.type] === 'Friends') && (feed.isFriend)) || ((filter[feed.type] === 'VIP') && (feed.isFavorite)))) { filtered.push(feed); } } else { console.error(`missing feed filter for ${feed.type}`); filtered.push(feed); } }); this.wristFeed = filtered; }; $app.methods.updateSharedFeedNoty = async function (feeds) { var filter = this.config.sharedFeedFilters.noty; var filtered = []; feeds.forEach((feed) => { if (filter[feed.type]) { if ((filter[feed.type] !== 'Off') && ((filter[feed.type] === 'Everyone') || (filter[feed.type] === 'On') || ((filter[feed.type] === 'Friends') && (feed.isFriend)) || ((filter[feed.type] === 'VIP') && (feed.isFavorite)))) { filtered.push(feed); } } }); var notyToPlay = []; filtered.forEach((feed) => { var displayName = ''; if (feed.displayName) { displayName = feed.displayName; } else if (feed.senderUsername) { displayName = feed.senderUsername; } else if (feed.sourceDisplayName) { displayName = feed.sourceDisplayName; } else if (feed.data) { displayName = feed.data; } else { console.error('missing displayName'); } if ((displayName) && (!this.notyMap[displayName]) || (this.notyMap[displayName] < feed.created_at)) { this.notyMap[displayName] = feed.created_at; notyToPlay.push(feed); } }); // disable notifications when busy or game isn't running if ((this.currentUserStatus === 'busy') || (!this.isGameRunning)) { return; } var bias = new Date(Date.now() - 60000).toJSON(); var noty = {}; for (var i = 0; i < notyToPlay.length; i++) { noty = notyToPlay[i]; if (noty.created_at < bias) { continue; } if ((this.config.overlayNotifications) && (!this.isGameNoVR)) { var text = ''; switch (noty.type) { case 'OnPlayerJoined': text = `${noty.data} has joined`; break; case 'OnPlayerLeft': text = `${noty.data} has left`; break; case 'OnPlayerJoining': text = `${noty.data} is joining`; break; case 'GPS': text = '' + noty.displayName + ' is in ' + await this.displayLocation(noty.location[0]); break; case 'Online': text = `${noty.displayName} has logged in`; break; case 'Offline': text = `${noty.displayName} has logged out`; break; case 'Status': text = `${noty.displayName} status is now ${noty.status[0].status} ${noty.status[0].statusDescription}`; break; case 'invite': text = `${noty.senderUsername} has invited you to ${noty.details.worldName}`; break; case 'requestInvite': text = `${noty.senderUsername} has requested an invite`; break; case 'friendRequest': text = `${noty.senderUsername} has sent you a friend request`; break; case 'Friend': text = `${noty.displayName} is now your friend`; break; case 'Unfriend': text = `${noty.displayName} has unfriended you`; break; case 'TrustLevel': text = `${noty.displayName} trust level is now ${noty.trustLevel}`; break; case 'DisplayName': text = `${noty.previousDisplayName} changed their name to ${noty.displayName}`; break; case 'showAvatar': text = `${noty.sourceDisplayName} has shown your avatar`; break; case 'hideAvatar': text = `${noty.sourceDisplayName} has hidden your avatar`; break; case 'block': text = `${noty.sourceDisplayName} has blocked you`; break; case 'mute': text = `${noty.sourceDisplayName} has muted you`; break; case 'unmute': text = `${noty.sourceDisplayName} has unmuted you`; break; default: break; } if (text) { new Noty({ type: 'alert', theme: this.config.notificationTheme, timeout: this.config.notificationTimeout, layout: this.config.notificationPosition, text }).show(); } } if (this.config.notificationTTS) { switch (noty.type) { case 'OnPlayerJoined': this.speak(`${noty.data} has joined`); break; case 'OnPlayerLeft': this.speak(`${noty.data} has left`); break; case 'OnPlayerJoining': this.speak(`${noty.data} is joining`); break; case 'GPS': this.speak(noty.displayName + ' is in ' + await this.displayLocation(noty.location[0])); break; case 'Online': this.speak(`${noty.displayName} has logged in`); break; case 'Offline': this.speak(`${noty.displayName} has logged out`); break; case 'Status': this.speak(`${noty.displayName} status is now ${noty.status[0].status} ${noty.status[0].statusDescription}`); break; case 'invite': this.speak(`${noty.senderUsername} has invited you to ${noty.details.worldName}`); break; case 'requestInvite': this.speak(`${noty.senderUsername} has requested an invite`); break; case 'friendRequest': this.speak(`${noty.senderUsername} has sent you a friend request`); break; case 'Friend': this.speak(`${noty.displayName} is now your friend`); break; case 'Unfriend': this.speak(`${noty.displayName} has unfriended you`); break; case 'TrustLevel': this.speak(`${noty.displayName} trust level is now ${noty.trustLevel}`); break; case 'DisplayName': this.speak(`${noty.previousDisplayName} changed their name to ${noty.displayName}`); break; case 'showAvatar': this.speak(`${noty.sourceDisplayName} has shown your avatar`); break; case 'hideAvatar': this.speak(`${noty.sourceDisplayName} has hidden your avatar`); break; case 'block': this.speak(`${noty.sourceDisplayName} has blocked you`); break; case 'mute': this.speak(`${noty.sourceDisplayName} has muted you`); break; case 'unmute': this.speak(`${noty.sourceDisplayName} has unmuted you`); break; default: break; } } if ((this.config.desktopToast) && (this.isGameNoVR)) { var imageURL = ''; if (noty.userId) { await API.getCachedUser({ userId: noty.userId }).catch((err) => { throw err; }).then((args) => { imageURL = args.json.currentAvatarThumbnailImageUrl; if ((this.config.displayVRCPlusIconsAsAvatar) && (args.json.userIcon)) { imageURL = args.json.userIcon; } }); } switch (noty.type) { case 'OnPlayerJoined': AppApi.DesktopNotification(noty.data, 'has joined', imageURL); break; case 'OnPlayerLeft': AppApi.DesktopNotification(noty.data, 'has left', imageURL); break; case 'OnPlayerJoining': AppApi.DesktopNotification(noty.data, 'is joining', imageURL); break; case 'GPS': AppApi.DesktopNotification(noty.displayName, 'is in ' + await this.displayLocation(noty.location[0]), imageURL); break; case 'Online': AppApi.DesktopNotification(noty.displayName, 'has logged in', imageURL); break; case 'Offline': AppApi.DesktopNotification(noty.displayName, 'has logged out', imageURL); break; case 'Status': AppApi.DesktopNotification(noty.displayName, `status is now ${noty.status[0].status} ${noty.status[0].statusDescription}`, imageURL); break; case 'invite': AppApi.DesktopNotification(noty.senderUsername, `has invited you to ${noty.details.worldName}`, imageURL); break; case 'requestInvite': AppApi.DesktopNotification(noty.senderUsername, 'has requested an invite', imageURL); break; case 'friendRequest': AppApi.DesktopNotification(noty.senderUsername, 'has sent you a friend request', imageURL); break; case 'Friend': AppApi.DesktopNotification(noty.displayName, 'has sent you a friend request', imageURL); break; case 'Unfriend': AppApi.DesktopNotification(noty.displayName, 'has unfriended you', imageURL); break; case 'TrustLevel': AppApi.DesktopNotification(noty.displayName, `trust level is now ${noty.trustLevel}`, imageURL); break; case 'DisplayName': AppApi.DesktopNotification(noty.previousDisplayName, `changed their name to ${noty.displayName}`, imageURL); break; default: break; } } } }; $app.methods.userStatusClass = function (user) { var style = {}; if (user !== undefined) { if (user.location === 'offline') { // Offline style.offline = true; } else if (user.status === 'active') { // Online style.active = true; } else if (user.status === 'join me') { // Join Me style.joinme = true; } else if (user.status === 'ask me') { // Ask Me style.askme = true; } else if (user.status === 'busy') { // Do Not Disturb style.busy = true; } } return style; }; $app.methods.displayLocation = async function (location) { var text = ''; var L = API.parseLocation(location); if (L.isOffline) { text = 'Offline'; } else if (L.isPrivate) { text = 'Private'; } else if (L.worldId) { var ref = API.cachedWorlds.get(L.worldId); if (ref === undefined) { await API.getWorld({ worldId: L.worldId }).then((args) => { if (L.tag === location) { if (L.instanceId) { text = `${args.json.name} ${L.accessType}`; } else { text = args.json.name; } } }); } else if (L.instanceId) { text = `${ref.name} ${L.accessType}`; } else { text = ref.name; } } return text; }; $app.methods.speak = function (text) { var tts = new SpeechSynthesisUtterance(); var voices = speechSynthesis.getVoices(); var voiceIndex = this.config.notificationTTSVoice; tts.voice = voices[voiceIndex]; tts.text = text; speechSynthesis.speak(tts); }; $app = new Vue($app); window.$app = $app; }());