diff --git a/App.config b/App.config index 88fa4027..8d234373 100644 --- a/App.config +++ b/App.config @@ -1,6 +1,6 @@ - + - + - \ No newline at end of file + diff --git a/AppApi.cs b/AppApi.cs index a63aabbe..70b63a08 100644 --- a/AppApi.cs +++ b/AppApi.cs @@ -11,6 +11,10 @@ using System.Linq; using System.Management; using System.Text.RegularExpressions; using System.Windows.Forms; +using System.IO; +using System.Net; +using Windows.UI.Notifications; +using Windows.Data.Xml.Dom; namespace VRCX { @@ -179,6 +183,30 @@ namespace VRCX return CpuMonitor.Instance.CpuUsage; } + public void DesktopNotification(string BoldText, string Text, string ImageURL = "") + { + XmlDocument toastXml = ToastNotificationManager.GetTemplateContent(ToastTemplateType.ToastImageAndText02); + XmlNodeList stringElements = toastXml.GetElementsByTagName("text"); + String imagePath = Path.Combine(Program.BaseDirectory, "cache\\toast"); + if (ImageURL == String.Empty) + { + imagePath = Path.Combine(Program.BaseDirectory, "VRCX.ico"); + } + else + { + using (var client = new WebClient()) + { + client.DownloadFile(ImageURL, imagePath); + } + } + stringElements[0].AppendChild(toastXml.CreateTextNode(BoldText)); + stringElements[1].AppendChild(toastXml.CreateTextNode(Text)); + XmlNodeList imageElements = toastXml.GetElementsByTagName("image"); + imageElements[0].Attributes.GetNamedItem("src").NodeValue = imagePath; + ToastNotification toast = new ToastNotification(toastXml); + ToastNotificationManager.CreateToastNotifier("VRCX").Show(toast); + } + public void SetStartup(bool enabled) { try diff --git a/VRCX.csproj b/VRCX.csproj index 3dba55ac..575e2e3d 100644 --- a/VRCX.csproj +++ b/VRCX.csproj @@ -8,7 +8,7 @@ WinExe VRCX VRCX - v4.5.2 + v4.6.2 512 true true @@ -29,6 +29,7 @@ 1.0.0.%2a false true + AnyCPU @@ -135,6 +136,7 @@ True Resources.resx + True SettingsSingleFileGenerator @@ -172,7 +174,10 @@ 86.0.241 - 1.0.166 + 1.0.169 + + + 6.1.1 12.0.3 @@ -193,7 +198,7 @@ 4.2.0 - 1.0.113.6 + 1.0.113.7 diff --git a/html/src/app.js b/html/src/app.js index d1b8e50d..edd7f880 100644 --- a/html/src/app.js +++ b/html/src/app.js @@ -3436,7 +3436,7 @@ speechSynthesis.getVoices(); var { data } = this.gameLogTable; var i = data.length; var j = 0; - while (j < 25) { + while (j < 30) { if (i <= 0) { break; } @@ -3466,7 +3466,7 @@ speechSynthesis.getVoices(); var { data } = this.feedTable; var i = data.length; var j = 0; - while (j < 25) { + while (j < 30) { if (i <= 0) { break; } @@ -3515,8 +3515,8 @@ speechSynthesis.getVoices(); } return 0; }); - if (arr.length > 25) { - arr.length = 25; + if (arr.length > 30) { + arr.length = 30; } sharedRepository.setArray('feeds', arr); }; @@ -3835,23 +3835,16 @@ speechSynthesis.getVoices(); $app.data.orderFriendsGroup1 = configRepository.getBool('orderFriendGroup1'); $app.data.orderFriendsGroup2 = configRepository.getBool('orderFriendGroup2'); $app.data.orderFriendsGroup3 = configRepository.getBool('orderFriendGroup3'); - $app.data.displayVRCPlusIconsAsAvatar = configRepository.getBool('displayVRCPlusIconsAsAvatar'); var saveOrderFriendGroup = function () { configRepository.setBool('orderFriendGroup0', this.orderFriendsGroup0); configRepository.setBool('orderFriendGroup1', this.orderFriendsGroup1); configRepository.setBool('orderFriendGroup2', this.orderFriendsGroup2); configRepository.setBool('orderFriendGroup3', this.orderFriendsGroup3); - configRepository.setBool('displayVRCPlusIconsAsAvatar', this.displayVRCPlusIconsAsAvatar); }; $app.watch.orderFriendsGroup0 = saveOrderFriendGroup; $app.watch.orderFriendsGroup1 = saveOrderFriendGroup; $app.watch.orderFriendsGroup2 = saveOrderFriendGroup; $app.watch.orderFriendsGroup3 = saveOrderFriendGroup; - $app.watch.displayVRCPlusIconsAsAvatar = saveOrderFriendGroup; - if (configRepository.getBool('displayVRCPlusIconsAsAvatar') === null) { - $app.data.displayVRCPlusIconsAsAvatar = true; - configRepository.setBool('displayVRCPlusIconsAsAvatar', $app.data.displayVRCPlusIconsAsAvatar); - } $app.methods.fetchActiveFriend = function (userId) { this.pendingActiveFriends.add(userId); @@ -5706,7 +5699,9 @@ speechSynthesis.getVoices(); $app.data.hidePrivateFromFeed = configRepository.getBool('VRCX_hidePrivateFromFeed'); $app.data.hideDevicesFromFeed = configRepository.getBool('VRCX_hideDevicesFromFeed'); $app.data.overlayNotifications = configRepository.getBool('VRCX_overlayNotifications'); + $app.data.desktopToast = configRepository.getBool('VRCX_desktopToast'); $app.data.minimalFeed = configRepository.getBool('VRCX_minimalFeed'); + $app.data.displayVRCPlusIconsAsAvatar = configRepository.getBool('displayVRCPlusIconsAsAvatar'); $app.data.notificationTTS = configRepository.getBool('VRCX_notificationTTS'); $app.data.notificationTTSVoice = configRepository.getString('VRCX_notificationTTSVoice'); $app.data.notificationTimeout = configRepository.getString('VRCX_notificationTimeout'); @@ -5717,7 +5712,9 @@ speechSynthesis.getVoices(); configRepository.setBool('VRCX_hidePrivateFromFeed', this.hidePrivateFromFeed); configRepository.setBool('VRCX_hideDevicesFromFeed', this.hideDevicesFromFeed); configRepository.setBool('VRCX_overlayNotifications', this.overlayNotifications); + configRepository.setBool('VRCX_desktopToast', this.desktopToast); configRepository.setBool('VRCX_minimalFeed', this.minimalFeed); + configRepository.setBool('displayVRCPlusIconsAsAvatar', this.displayVRCPlusIconsAsAvatar); AppApi.RefreshVR(); }; $app.data.TTSvoices = speechSynthesis.getVoices(); @@ -5735,7 +5732,9 @@ speechSynthesis.getVoices(); $app.watch.hidePrivateFromFeed = saveOpenVROption; $app.watch.hideDevicesFromFeed = saveOpenVROption; $app.watch.overlayNotifications = saveOpenVROption; + $app.watch.desktopToast = saveOpenVROption; $app.watch.minimalFeed = saveOpenVROption; + $app.watch.displayVRCPlusIconsAsAvatar = saveOpenVROption; $app.watch.notificationTTS = saveNotificationTTS; $app.data.isDarkMode = configRepository.getBool('isDarkMode'); $appDarkStyle.disabled = $app.data.isDarkMode === false; @@ -5761,6 +5760,10 @@ speechSynthesis.getVoices(); $app.watch.isAutoLogin = saveVRCXWindowOption; //setting defaults + if (configRepository.getBool('displayVRCPlusIconsAsAvatar') === null) { + $app.data.displayVRCPlusIconsAsAvatar = true; + configRepository.setBool('displayVRCPlusIconsAsAvatar', $app.data.displayVRCPlusIconsAsAvatar); + } if (!configRepository.getString('VRCX_notificationPosition')) { $app.data.notificationPosition = 'topCenter'; configRepository.setString('VRCX_notificationPosition', $app.data.notificationPosition); @@ -5907,6 +5910,17 @@ speechSynthesis.getVoices(); } $app.watch.isGameRunning = isGameRunningStateChange; + sharedRepository.setBool('is_Game_No_VR', false); + var isGameNoVRStateChange = function () { + sharedRepository.setBool('is_Game_No_VR', this.isGameNoVR); + } + $app.watch.isGameNoVR = isGameNoVRStateChange; + + var lastLocationStateChange = function () { + sharedRepository.setString('last_location', $app.lastLocation); + } + $app.watch.lastLocation = lastLocationStateChange; + API.$on('LOGIN', function () { $app.currentUserTreeData = []; $app.pastDisplayNameTable.data = []; diff --git a/html/src/app.scss b/html/src/app.scss index 8cf13ef4..a356a916 100644 --- a/html/src/app.scss +++ b/html/src/app.scss @@ -555,7 +555,12 @@ i.x-user-status.busy { .options-container .header { font-weight: bold; - font-size: 18px; + font-size: 20px; +} + +.options-container .sub-header { + font-weight: bold; + font-size: 15px; } .options-container-item { diff --git a/html/src/index.pug b/html/src/index.pug index 37f6d765..8418e32a 100644 --- a/html/src/index.pug +++ b/html/src/index.pug @@ -557,18 +557,34 @@ html div.options-container-item span.name(style="min-width:137px") Overlay Button el-switch(v-model="overlaybutton" inactive-text="Grip" active-text="Menu" :disabled="!openVR") - div.options-container-item - span.name Hide VR Devices - el-switch(v-model="hideDevicesFromFeed" :disabled="!openVR") - div.options-container-item - span.name Hide Private Worlds - el-switch(v-model="hidePrivateFromFeed" :disabled="!openVR") + br + span.sub-header Display Options div.options-container-item span.name Minimal Feed Icons el-switch(v-model="minimalFeed" :disabled="!openVR") + div.options-container-item + span.name Hide Private Worlds + el-switch(v-model="hidePrivateFromFeed" :disabled="!openVR") + div.options-container-item + span.name Hide VR Devices + el-switch(v-model="hideDevicesFromFeed" :disabled="!openVR") + div.options-container-item + el-button(size="small" icon="el-icon-notebook-2" @click="showWristFeedFiltersDialog()" :disabled="!openVR") Wrist Feed Filters + br + span.sub-header Notification Options div.options-container-item span.name Overlay Notifications el-switch(v-model="overlayNotifications" :disabled="!openVR") + div.options-container-item + span.name Desktop Notifications + el-switch(v-model="desktopToast" :disabled="!openVR") + + div.options-container-item + el-button(size="small" icon="el-icon-chat-square" @click="showNotyFeedFiltersDialog()" :disabled="!openVR") Notification Filters + el-button(size="small" icon="el-icon-time" @click="promptNotificationTimeout()" :disabled="!overlayNotifications || !openVR") Notification Timeout + el-button(size="small" icon="el-icon-rank" @click="showNotificationPositionDialog()" :disabled="!overlayNotifications || !openVR") Notification Position + br + span.sub-header TTS Options div.options-container-item span.name Notification TTS el-switch(v-model="notificationTTS" :disabled="!openVR") @@ -577,13 +593,7 @@ html el-dropdown(@command="(voice) => changeTTSVoice(voice)" trigger="click" size="small") el-button(v-text="TTSvoices[notificationTTSVoice].name" size="mini" :disabled="!openVR || !notificationTTS") el-dropdown-menu(#default="dropdown") - el-dropdown-item(v-if="voice" v-for="(voice, index) in TTSvoices" :key="index" v-text="voice.name" :command="index") - div.options-container-item - el-button(size="small" icon="el-icon-notebook-2" @click="showWristFeedFiltersDialog()" :disabled="!openVR") Wrist Feed Filters - el-button(size="small" icon="el-icon-chat-square" @click="showNotyFeedFiltersDialog()" :disabled="!overlayNotifications || !openVR") Notification Filters - div.options-container-item - el-button(size="small" icon="el-icon-time" @click="promptNotificationTimeout()" :disabled="!overlayNotifications || !openVR") Notification Timeout - el-button(size="small" icon="el-icon-rank" @click="showNotificationPositionDialog()" :disabled="!overlayNotifications || !openVR") Notification Position + el-dropdown-item(v-if="voice" v-for="(voice, index) in TTSvoices" :key="index" v-text="voice.name" :command="index") div.options-container span.header Application div.options-container-item diff --git a/html/src/vr.js b/html/src/vr.js index 16b8d962..6e07077f 100644 --- a/html/src/vr.js +++ b/html/src/vr.js @@ -564,6 +564,113 @@ speechSynthesis.getVoices(); }); }; + // 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; + } + } + var has = false; + for (var prop in props) { + var asis = $ref[prop]; + var tobe = ref[prop]; + if (asis === tobe) { + delete props[prop]; + } else { + has = true; + 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, @@ -574,17 +681,20 @@ speechSynthesis.getVoices(); currentUserStatus: null, cpuUsage: 0, isGameRunning: false, + isGameNoVR: false, lastLocation: '', lastFeedEntry: [], feedFilters: [], wristFeed: [], notyMap: [], devices: [], + desktopToastToggle: false, overlayNotificationsToggle: false, notificationTTSToggle: false, notificationTTSVoice: '0', hideDevicesToggle: false, isMinimalFeed: false, + displayVRCPlusIconsAsAvatar: false, notificationPosition: 'topCenter', notificationTimeout: '3000', notificationTheme: 'relax' @@ -613,14 +723,13 @@ speechSynthesis.getVoices(); throw err; }).then((args) => { this.initConfigVars(); - this.initNotyMap(); + if (this.appType === '1') { + this.updateCpuUsageLoop(); + } + if (this.appType === '2') { + this.initNotyMap(); + } this.updateLoop(); - this.updateCpuUsageLoop(); - this.$nextTick(function () { - if (this.appType === '1') { - this.$el.style.display = ''; - } - }); return args; }); } @@ -630,9 +739,11 @@ speechSynthesis.getVoices(); this.notificationTTSToggle = configRepository.getBool('VRCX_notificationTTS'); this.notificationTTSVoice = configRepository.getString('VRCX_notificationTTSVoice'); this.overlayNotificationsToggle = configRepository.getBool('VRCX_overlayNotifications'); + this.desktopToastToggle = configRepository.getBool('VRCX_desktopToast'); this.hidePrivateFromFeed = configRepository.getBool('VRCX_hidePrivateFromFeed'); this.hideDevicesToggle = configRepository.getBool('VRCX_hideDevicesFromFeed'); this.isMinimalFeed = configRepository.getBool('VRCX_minimalFeed'); + this.displayVRCPlusIconsAsAvatar = configRepository.getBool('displayVRCPlusIconsAsAvatar'); this.feedFilters = JSON.parse(configRepository.getString('sharedFeedFilters')); this.notificationPosition = configRepository.getString('VRCX_notificationPosition'); this.notificationTimeout = configRepository.getString('VRCX_notificationTimeout'); @@ -678,8 +789,9 @@ speechSynthesis.getVoices(); 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.hideDevicesToggle) { + if ((!this.hideDevicesToggle) && (this.appType === '1')) { AppApi.GetVRDevices().then((devices) => { devices.forEach((device) => { device[2] = parseInt(device[2], 10); @@ -824,8 +936,14 @@ speechSynthesis.getVoices(); if ((this.currentUserStatus === 'busy') || (!this.isGameRunning)) { return; } - notyToPlay.forEach(async (noty) => { - if (this.overlayNotificationsToggle) { + 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.overlayNotificationsToggle) && (!this.isGameNoVR)) { var text = ''; switch (noty.type) { case 'OnPlayerJoined': @@ -927,7 +1045,66 @@ speechSynthesis.getVoices(); break; } } - }); + if ((this.desktopToastToggle) && (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.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; + } + } + } }; $app.methods.userStatusClass = function (user) {