From 2418d902cfa608676cb977f12040e5ffc8b22011 Mon Sep 17 00:00:00 2001 From: Natsumi Date: Mon, 30 Aug 2021 12:05:42 +1200 Subject: [PATCH] Massive commit that will break everything! --- AppApi.cs | 17 +- LogWatcher.cs | 89 ++- VRCXVR.cs | 4 +- html/src/app.js | 892 ++++++++++++++++++--------- html/src/index.pug | 55 +- html/src/repository/database.js | 176 +++++- html/src/service/gamelog.js | 131 ++-- html/src/vr.js | 1001 +++++++------------------------ html/src/vr.pug | 80 ++- html/src/vr.scss | 19 +- 10 files changed, 1252 insertions(+), 1212 deletions(-) diff --git a/AppApi.cs b/AppApi.cs index 2a0afb4c..e8aee4a3 100644 --- a/AppApi.cs +++ b/AppApi.cs @@ -314,13 +314,28 @@ namespace VRCX System.Environment.Exit(0); } - public bool checkForUpdateZip() + public bool CheckForUpdateZip() { if (File.Exists(Path.Combine(Program.AppDataDirectory, "update.exe"))) return true; return false; } + public void ExecuteAppFunction(string function, string json) + { + MainForm.Instance.Browser.ExecuteScriptAsync($"$app.{function}", json); + } + + public void ExecuteVrFeedFunction(string function, string json) + { + VRCXVR._browser1.ExecuteScriptAsync($"$app.{function}", json); + } + + public void ExecuteVrOverlayFunction(string function, string json) + { + VRCXVR._browser2.ExecuteScriptAsync($"$app.{function}", json); + } + public void SetStartup(bool enabled) { try diff --git a/LogWatcher.cs b/LogWatcher.cs index b7f23005..9a929e34 100644 --- a/LogWatcher.cs +++ b/LogWatcher.cs @@ -3,6 +3,7 @@ // This work is licensed under the terms of the MIT license. // For a copy, see . +using CefSharp; using System; using System.Collections.Generic; using System.Globalization; @@ -30,6 +31,8 @@ namespace VRCX private readonly List m_LogList; private Thread m_Thread; private bool m_ResetLog; + private bool m_FirstRun = true; + private static DateTime tillDate = DateTime.Now; // NOTE // FileSystemWatcher() is unreliable @@ -64,6 +67,17 @@ namespace VRCX thread.Interrupt(); thread.Join(); } + + public void Reset() + { + m_ResetLog = true; + m_Thread?.Interrupt(); + } + + public void SetDateTill(string date) + { + tillDate = DateTime.Parse(date); + } private void ThreadLoop() { @@ -86,6 +100,7 @@ namespace VRCX { if (m_ResetLog == true) { + m_FirstRun = true; m_ResetLog = false; m_LogContextMap.Clear(); m_LogListLock.EnterWriteLock(); @@ -109,26 +124,17 @@ namespace VRCX // sort by creation time Array.Sort(fileInfos, (a, b) => a.CreationTimeUtc.CompareTo(b.CreationTimeUtc)); - var utcNow = DateTime.UtcNow; - var minLimitDateTime = utcNow.AddDays(-7d); - var minRefreshDateTime = utcNow.AddMinutes(-3d); - foreach (var fileInfo in fileInfos) { - var lastWriteTimeUtc = fileInfo.LastWriteTimeUtc; - - if (lastWriteTimeUtc < minLimitDateTime) + fileInfo.Refresh(); + if (fileInfo.Exists == false) { continue; } - if (lastWriteTimeUtc >= minRefreshDateTime) + if (DateTime.Compare(fileInfo.LastWriteTime, tillDate) < 0) { - fileInfo.Refresh(); - if (fileInfo.Exists == false) - { - continue; - } + continue; } if (m_LogContextMap.TryGetValue(fileInfo.Name, out LogContext logContext) == true) @@ -155,6 +161,8 @@ namespace VRCX { m_LogContextMap.Remove(name); } + + m_FirstRun = false; } private void ParseLog(FileInfo fileInfo, LogContext logContext) @@ -184,6 +192,20 @@ namespace VRCX continue; } + if (DateTime.TryParseExact( + line.Substring(0, 19), + "yyyy.MM.dd HH:mm:ss", + CultureInfo.InvariantCulture, + DateTimeStyles.AssumeLocal, + out DateTime lineDate + )) + { + if (DateTime.Compare(lineDate, tillDate) <= 0) + { + continue; + } + } + var offset = 34; if (line[offset] == '[') { @@ -194,7 +216,8 @@ namespace VRCX ParseLogJoinBlocked(fileInfo, logContext, line, offset) == true || ParseLogAvatarPedestalChange(fileInfo, logContext, line, offset) == true || ParseLogVideoError(fileInfo, logContext, line, offset) == true || - ParseLogVideoPlay(fileInfo, logContext, line, offset) == true) + ParseLogVideoPlay(fileInfo, logContext, line, offset) == true || + ParseLogWorldVRCX(fileInfo, logContext, line, offset) == true) { continue; } @@ -221,6 +244,12 @@ namespace VRCX m_LogListLock.EnterWriteLock(); try { + if (!m_FirstRun) + { + var logLine = System.Text.Json.JsonSerializer.Serialize(item); + if (MainForm.Instance != null && MainForm.Instance.Browser != null) + MainForm.Instance.Browser.ExecuteScriptAsync("$app.addGameLogEvent", logLine); + } m_LogList.Add(item); } finally @@ -244,7 +273,7 @@ namespace VRCX dt = DateTime.UtcNow; } - return $"{dt:yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'}"; + return $"{dt:yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'}"; } private bool ParseLogLocation(FileInfo fileInfo, LogContext logContext, string line, int offset) @@ -515,6 +544,28 @@ namespace VRCX return true; } + private bool ParseLogWorldVRCX(FileInfo fileInfo, LogContext logContext, string line, int offset) + { + // [VRCX] VideoPlay(PyPyDance) "https://jd.pypy.moe/api/v1/videos/-Q3pdlsQxOk.mp4",0.5338666,260.6938,"1339 : Le Freak (Random)" + + if (string.Compare(line, offset, "[VRCX] ", 0, 7, StringComparison.Ordinal) == 0) + { + var data = line.Substring(offset + 7); + + AppendLog(new[] + { + fileInfo.Name, + ConvertLogTimeToISO8601(line), + "vrcx", + data + }); + + return true; + } + + return false; + } + private bool ParseLogSDK2VideoPlay(FileInfo fileInfo, LogContext logContext, string line, int offset) { // 2021.04.23 13:12:25 Log - User Natsumi-sama added URL https://www.youtube.com/watch?v=dQw4w9WgXcQ @@ -579,14 +630,10 @@ namespace VRCX return true; } - public void Reset() - { - m_ResetLog = true; - m_Thread?.Interrupt(); - } - public string[][] Get() { + Update(); + if (m_ResetLog == false && m_LogList.Count > 0) { diff --git a/VRCXVR.cs b/VRCXVR.cs index a8e38694..8d5a9dd3 100644 --- a/VRCXVR.cs +++ b/VRCXVR.cs @@ -34,8 +34,8 @@ namespace VRCX private Device _device; private Texture2D _texture1; private Texture2D _texture2; - private OffScreenBrowser _browser1; - private OffScreenBrowser _browser2; + public static OffScreenBrowser _browser1; + public static OffScreenBrowser _browser2; private bool _active; static VRCXVR() diff --git a/html/src/app.js b/html/src/app.js index 194a3c6d..958f41ac 100644 --- a/html/src/app.js +++ b/html/src/app.js @@ -15,7 +15,6 @@ import locale from 'element-ui/lib/locale/lang/en'; import {v4 as uuidv4} from 'uuid'; import {appVersion} from './constants.js'; -import sharedRepository from './repository/shared.js'; import configRepository from './repository/config.js'; import webApiService from './service/webapi.js'; import gameLogService from './service/gamelog.js'; @@ -463,6 +462,9 @@ speechSynthesis.getVoices(); if (init.method === 'GET' && status === 404) { this.failedGetRequests.set(endpoint, Date.now()); } + if (status === 404 && endpoint.substring(0, 6) === 'users/') { + throw new Error("404: Can't find user!"); + } if (data.error === Object(data.error)) { this.$throw( data.error.status_code || status, @@ -1273,7 +1275,6 @@ speechSynthesis.getVoices(); ref }); } - sharedRepository.setString('current_user_status', ref.status); return ref; }; @@ -1494,6 +1495,24 @@ speechSynthesis.getVoices(); }); }; + /* + params: { + username: string + } + */ + API.getUserByUsername = function (params) { + return this.call(`users/${params.username}/name`, { + method: 'GET' + }).then((json) => { + var args = { + json, + params + }; + this.$emit('USER', args); + return args; + }); + }; + /* params: { status: string ('active', 'offline', 'busy', 'ask me', 'join me'), @@ -3441,7 +3460,7 @@ speechSynthesis.getVoices(); }); API.$on('USER:CURRENT', function () { - if (this.webSocket === null) { + if ($app.friendLogInitStatus && this.webSocket === null) { this.getAuth(); } }); @@ -3897,7 +3916,7 @@ speechSynthesis.getVoices(); API.$on('SHOW_WORLD_DIALOG', (tag) => this.showWorldDialog(tag)); API.$on('SHOW_LAUNCH_DIALOG', (tag) => this.showLaunchDialog(tag)); this.updateLoop(); - this.updateGameLogLoop(); + this.getGameLogTable(); this.$nextTick(function () { this.$el.style.display = ''; if (!this.enablePrimaryPassword) { @@ -3969,8 +3988,12 @@ speechSynthesis.getVoices(); if (isGameRunning !== this.isGameRunning) { this.isGameRunning = isGameRunning; Discord.SetTimestamps(Date.now(), 0); + this.updateVRConfigVars(); + } + if (isGameNoVR !== this.isGameNoVR) { + this.isGameNoVR = isGameNoVR; + this.updateVRConfigVars(); } - this.isGameNoVR = isGameNoVR; this.updateDiscord(); this.updateOpenVR(); } @@ -3986,6 +4009,8 @@ speechSynthesis.getVoices(); $app.data.debugWebRequests = false; $app.data.debugWebSocket = false; + $app.data.APILastOnline = new Map(); + $app.data.sharedFeed = { gameLog: { wrist: [], @@ -4010,22 +4035,8 @@ speechSynthesis.getVoices(); pendingUpdate: false }; - $app.data.appInit = false; - $app.data.notyInit = false; - - API.$on('LOGIN', function () { - sharedRepository.setArray('wristFeed', []); - sharedRepository.setArray('notyFeed', []); - setTimeout(function () { - $app.appInit = true; - $app.updateSharedFeed(true); - $app.notyInit = true; - sharedRepository.setBool('VRInit', true); - }, 10000); - }); - $app.methods.updateSharedFeed = function (forceUpdate) { - if (!this.appInit) { + if (!this.friendLogInitStatus) { return; } this.updateSharedFeedGameLog(forceUpdate); @@ -4085,7 +4096,7 @@ speechSynthesis.getVoices(); } } var playersInInstance = this.lastLocation.playerList; - if (playersInInstance.includes(ctx.displayName)) { + if (playersInInstance.has(ctx.displayName)) { continue; } var joining = true; @@ -4103,7 +4114,7 @@ speechSynthesis.getVoices(); } if ( gameLogItem.type === 'OnPlayerJoined' && - gameLogItem.data === ctx.displayName + gameLogItem.displayName === ctx.displayName ) { joining = false; break; @@ -4161,15 +4172,18 @@ speechSynthesis.getVoices(); } return 0; }); - notyFeed.splice(1); - sharedRepository.setArray('wristFeed', wristFeed); - sharedRepository.setArray('notyFeed', notyFeed); + notyFeed.splice(5); + AppApi.ExecuteVrFeedFunction( + 'wristFeedUpdate', + JSON.stringify(wristFeed) + ); if (this.userDialog.visible) { this.applyUserDialogLocation(); } if (this.worldDialog.visible) { this.applyWorldDialogInstances(); } + this.getCurrentInstanceUserList(); this.playNoty(notyFeed); feeds.pendingUpdate = false; }; @@ -4197,9 +4211,6 @@ speechSynthesis.getVoices(); var n = 0; var wristFilter = this.sharedFeedFilters.wrist; var notyFilter = this.sharedFeedFilters.noty; - var playerCountIndex = 0; - var playerList = []; - var friendList = []; var currentUserJoinTime = ''; var currentUserLeaveTime = ''; for (var i = data.length - 1; i > -1; i--) { @@ -4210,15 +4221,12 @@ speechSynthesis.getVoices(); if (ctx.type === 'Notification') { continue; } - if (playerCountIndex === 0 && ctx.type === 'Location') { - playerCountIndex = i; - } // on Location change remove OnPlayerLeft if (ctx.type === 'OnPlayerLeft') { if (ctx.created_at.slice(0, -4) === currentUserLeaveTime) { continue; } - if (ctx.data === API.currentUser.displayName) { + if (ctx.displayName === API.currentUser.displayName) { var {created_at} = ctx; currentUserLeaveTime = created_at.slice(0, -4); for (var k = w - 1; k > -1; k--) { @@ -4251,7 +4259,7 @@ speechSynthesis.getVoices(); if (ctx.created_at.slice(0, -4) === currentUserJoinTime) { continue; } - if (ctx.data === API.currentUser.displayName) { + if (ctx.displayName === API.currentUser.displayName) { var {created_at} = ctx; currentUserJoinTime = created_at.slice(0, -4); for (var k = w - 1; k > -1; k--) { @@ -4284,7 +4292,7 @@ speechSynthesis.getVoices(); (ctx.type === 'OnPlayerJoined' || ctx.type === 'OnPlayerLeft' || ctx.type === 'PortalSpawn') && - ctx.data === API.currentUser.displayName + ctx.displayName === API.currentUser.displayName ) { continue; } @@ -4293,20 +4301,28 @@ speechSynthesis.getVoices(); if ( ctx.type === 'OnPlayerJoined' || ctx.type === 'OnPlayerLeft' || - ctx.type === 'PortalSpawn' + ctx.type === 'PortalSpawn' || + ctx.type === 'AvatarChange' ) { - for (var ref of API.cachedUsers.values()) { - if (ref.displayName === ctx.data) { - isFriend = this.friends.has(ref.id); - isFavorite = API.cachedFavoritesByObjectId.has(ref.id); - break; + if (ctx.userId) { + isFriend = this.friends.has(ctx.userId); + isFavorite = API.cachedFavoritesByObjectId.has(ctx.userId); + } else { + for (var ref of API.cachedUsers.values()) { + if (ref.displayName === ctx.displayName) { + isFriend = this.friends.has(ref.id); + isFavorite = API.cachedFavoritesByObjectId.has( + ref.id + ); + break; + } } } } // BlockedOnPlayerJoined, BlockedOnPlayerLeft, MutedOnPlayerJoined, MutedOnPlayerLeft if (ctx.type === 'OnPlayerJoined' || ctx.type === 'OnPlayerLeft') { for (var ref of this.playerModerationTable.data) { - if (ref.targetDisplayName === ctx.data) { + if (ref.targetDisplayName === ctx.displayName) { if (ref.type === 'block') { var type = `Blocked${ctx.type}`; } else if (ref.type === 'mute') { @@ -4350,6 +4366,22 @@ speechSynthesis.getVoices(); } } } + if (ctx.type === 'VideoPlay' && ctx.displayName) { + if (ctx.userId) { + isFriend = this.friends.has(ctx.userId); + isFavorite = API.cachedFavoritesByObjectId.has(ctx.userId); + } else { + for (var ref of API.cachedUsers.values()) { + if (ref.displayName === ctx.displayName) { + isFriend = this.friends.has(ref.id); + isFavorite = API.cachedFavoritesByObjectId.has( + ref.id + ); + break; + } + } + } + } if ( w < 20 && wristFilter[ctx.type] && @@ -4366,7 +4398,7 @@ speechSynthesis.getVoices(); ++w; } if ( - n < 1 && + n < 5 && notyFilter[ctx.type] && (notyFilter[ctx.type] === 'On' || notyFilter[ctx.type] === 'Everyone' || @@ -4381,41 +4413,6 @@ speechSynthesis.getVoices(); ++n; } } - // instance player list - for (var i = playerCountIndex + 1; i < data.length; i++) { - var ctx = data[i]; - if (ctx.type === 'OnPlayerJoined') { - playerList.push(ctx.data); - var isFriend = false; - for (var ref of API.cachedUsers.values()) { - if (ref.displayName === ctx.data) { - isFriend = this.friends.has(ref.id); - break; - } - } - if (ctx.data === API.currentUser.displayName) { - isFriend = true; - } - if (isFriend) { - friendList.push(ctx.data); - } - } - if (ctx.type === 'OnPlayerLeft') { - var index = playerList.indexOf(ctx.data); - if (index > -1) { - playerList.splice(index, 1); - } - var index = friendList.indexOf(ctx.data); - if (index > -1) { - friendList.splice(index, 1); - } - } - } - if (this.isGameRunning) { - this.lastLocation.playerList = playerList; - this.lastLocation.friendList = friendList; - sharedRepository.setObject('last_location', this.lastLocation); - } this.sharedFeed.gameLog.wrist = wristArr; this.sharedFeed.gameLog.noty = notyArr; this.sharedFeed.pendingUpdate = true; @@ -4476,7 +4473,7 @@ speechSynthesis.getVoices(); ++w; } if ( - n < 1 && + n < 5 && notyFilter[ctx.type] && (notyFilter[ctx.type] === 'Friends' || (notyFilter[ctx.type] === 'VIP' && isFavorite)) @@ -4545,7 +4542,7 @@ speechSynthesis.getVoices(); ++w; } if ( - n < 1 && + n < 5 && notyFilter[ctx.type] && (notyFilter[ctx.type] === 'On' || notyFilter[ctx.type] === 'Friends' || @@ -4613,7 +4610,7 @@ speechSynthesis.getVoices(); ++w; } if ( - n < 1 && + n < 5 && notyFilter[ctx.type] && (notyFilter[ctx.type] === 'On' || notyFilter[ctx.type] === 'Friends' || @@ -4661,7 +4658,15 @@ speechSynthesis.getVoices(); if (this.xsNotifications && this.isGameRunning && !this.isGameNoVR) { playXSNotification = true; } - if (this.currentUserStatus === 'busy' || !this.notyInit) { + var playOverlayNotification = false; + if ( + this.overlayNotifications && + !this.isGameNoVR && + this.isGameRunning + ) { + playOverlayNotification = true; + } + if (API.currentUser.status === 'busy' || !this.notyInit) { return; } var notyToPlay = []; @@ -4675,8 +4680,6 @@ speechSynthesis.getVoices(); displayName = feed.sourceDisplayName; } else if (feed.data) { displayName = feed.data; - } else { - console.error('missing displayName'); } if ( (displayName && !this.notyMap[displayName]) || @@ -4712,8 +4715,13 @@ speechSynthesis.getVoices(); if (playNotificationTTS) { this.playNotyTTS(noty, message); } + if (playOverlayNotification) { + this.notyGetImage(noty).then((imageUrl) => { + this.displayOverlayNotification(noty, message, imageUrl); + }); + } if (playDesktopToast || playXSNotification) { - this.notyGetImage(noty).then((image) => { + this.notySaveImage(noty).then((image) => { if (playXSNotification) { this.displayXSNotification(noty, message, image); } @@ -4726,7 +4734,7 @@ speechSynthesis.getVoices(); }; $app.methods.notyGetImage = async function (noty) { - var imageURL = ''; + var imageUrl = ''; var userId = ''; if (noty.userId) { userId = noty.userId; @@ -4734,23 +4742,25 @@ speechSynthesis.getVoices(); userId = noty.senderUserId; } else if (noty.sourceUserId) { userId = noty.sourceUserId; - } else if (noty.data) { + } else if (noty.displayName) { for (var ref of API.cachedUsers.values()) { - if (ref.displayName === noty.data) { + if (ref.displayName === noty.displayName) { userId = ref.id; break; } } } - if (noty.details && noty.details.imageUrl) { - imageURL = noty.details.imageUrl; + if (noty.thumbnailImageUrl) { + imageUrl = noty.thumbnailImageUrl; + } else if (noty.details && noty.details.imageUrl) { + imageUrl = noty.details.imageURL; } else if (userId) { - imageURL = await API.getCachedUser({ + imageUrl = await API.getCachedUser({ userId }) .catch((err) => { console.error(err); - return false; + return ''; }) .then((args) => { if ( @@ -4759,14 +4769,19 @@ speechSynthesis.getVoices(); ) { return args.json.userIcon; } + if (args.json.profilePicOverride) { + return args.json.profilePicOverride; + } return args.json.currentAvatarThumbnailImageUrl; }); } - if (!imageURL) { - return false; - } + return imageUrl; + }; + + $app.methods.notySaveImage = async function (noty) { + var imageUrl = await this.notyGetImage(noty); try { - await fetch(imageURL, { + await fetch(imageUrl, { method: 'GET', redirect: 'follow', headers: { @@ -4791,13 +4806,24 @@ speechSynthesis.getVoices(); } }; + $app.methods.displayOverlayNotification = function ( + noty, + message, + imageUrl + ) { + AppApi.ExecuteVrOverlayFunction( + 'playNoty', + JSON.stringify({noty, message, imageUrl}) + ); + }; + $app.methods.playNotyTTS = function (noty, message) { switch (noty.type) { case 'OnPlayerJoined': - this.speak(`${noty.data} has joined`); + this.speak(`${noty.displayName} has joined`); break; case 'OnPlayerLeft': - this.speak(`${noty.data} has left`); + this.speak(`${noty.displayName} has left`); break; case 'OnPlayerJoining': this.speak(`${noty.displayName} is joining`); @@ -4868,13 +4894,31 @@ speechSynthesis.getVoices(); ); break; case 'PortalSpawn': - this.speak(`${noty.data} has spawned a portal`); + var locationName = ''; + if (noty.worldName) { + locationName = ` to ${this.displayLocation( + noty.instanceId, + noty.worldName + )}`; + } + this.speak( + `${noty.displayName} has spawned a portal${locationName}` + ); + break; + case 'AvatarChange': + this.speak( + `${noty.displayName} changed into avatar ${noty.name}` + ); break; case 'Event': this.speak(noty.data); break; case 'VideoPlay': - this.speak(`Now playing: ${noty.data}`); + var videoName = ''; + if (noty.videoName) { + videoName = `: ${noty.videoName}`; + } + this.speak(`Now playing video${videoName}`); break; case 'BlockedOnPlayerJoined': this.speak(`Blocked user ${noty.displayName} has joined`); @@ -4897,7 +4941,7 @@ speechSynthesis.getVoices(); case 'OnPlayerJoined': AppApi.XSNotification( 'VRCX', - `${noty.data} has joined`, + `${noty.displayName} has joined`, timeout, image ); @@ -4905,7 +4949,7 @@ speechSynthesis.getVoices(); case 'OnPlayerLeft': AppApi.XSNotification( 'VRCX', - `${noty.data} has left`, + `${noty.displayName} has left`, timeout, image ); @@ -5031,9 +5075,24 @@ speechSynthesis.getVoices(); ); break; case 'PortalSpawn': + var locationName = ''; + if (noty.worldName) { + locationName = ` to ${this.displayLocation( + noty.instanceId, + noty.worldName + )}`; + } AppApi.XSNotification( 'VRCX', - `${noty.data} has spawned a portal`, + `${noty.displayName} has spawned a portal${locationName}`, + timeout, + image + ); + break; + case 'AvatarChange': + AppApi.XSNotification( + 'VRCX', + `${noty.displayName} changed into avatar ${noty.name}`, timeout, image ); @@ -5042,9 +5101,13 @@ speechSynthesis.getVoices(); AppApi.XSNotification('VRCX', noty.data, timeout, image); break; case 'VideoPlay': + var videoName = noty.videoUrl; + if (noty.videoName) { + videoName = noty.videoName; + } AppApi.XSNotification( 'VRCX', - `Now playing: ${noty.data}`, + `Now playing: ${videoName}`, timeout, image ); @@ -5087,10 +5150,14 @@ speechSynthesis.getVoices(); $app.methods.displayDesktopToast = function (noty, message, image) { switch (noty.type) { case 'OnPlayerJoined': - AppApi.DesktopNotification(noty.data, 'has joined', image); + AppApi.DesktopNotification( + noty.displayName, + 'has joined', + image + ); break; case 'OnPlayerLeft': - AppApi.DesktopNotification(noty.data, 'has left', image); + AppApi.DesktopNotification(noty.displayName, 'has left', image); break; case 'OnPlayerJoining': AppApi.DesktopNotification( @@ -5197,9 +5264,23 @@ speechSynthesis.getVoices(); ); break; case 'PortalSpawn': + var locationName = ''; + if (noty.worldName) { + locationName = ` to ${this.displayLocation( + noty.instanceId, + noty.worldName + )}`; + } AppApi.DesktopNotification( - noty.data, - `has spawned a portal`, + noty.displayName, + `has spawned a portal${locationName}`, + image + ); + break; + case 'AvatarChange': + AppApi.DesktopNotification( + noty.displayName, + `changed into avatar ${noty.name}`, image ); break; @@ -5207,7 +5288,11 @@ speechSynthesis.getVoices(); AppApi.DesktopNotification('Event', noty.data, image); break; case 'VideoPlay': - AppApi.DesktopNotification('Now playing', noty.data, image); + var videoName = noty.videoUrl; + if (noty.videoName) { + videoName = noty.videoName; + } + AppApi.DesktopNotification('Now playing', videoName, image); break; case 'BlockedOnPlayerJoined': AppApi.DesktopNotification( @@ -5441,7 +5526,6 @@ speechSynthesis.getVoices(); )}!` }).show(); $app.$refs.menu.activeIndex = 'feed'; - $app.resetGameLog(); }); API.$on('LOGIN', function (args) { @@ -5955,7 +6039,10 @@ speechSynthesis.getVoices(); }); $app.methods.checkActiveFriends = function (ref) { - if (Array.isArray(ref.activeFriends) === false || !this.appInit) { + if ( + Array.isArray(ref.activeFriends) === false || + !this.friendLogInitStatus + ) { return; } for (var userId of ref.activeFriends) { @@ -6013,6 +6100,9 @@ speechSynthesis.getVoices(); }); API.$on('FRIEND:STATE', function (args) { + if (args.json.state === 'online') { + $app.APILastOnline.set(args.params.userId, Date.now()); + } $app.updateFriend(args.params.userId, args.json.state); }); @@ -6149,7 +6239,7 @@ speechSynthesis.getVoices(); // AddFriend (CurrentUser) 이후, // 서버에서 오는 순서라고 보면 될 듯. if (ctx.state === 'online') { - if (this.appInit) { + if (this.friendLogInitStatus) { API.getUser({ userId: id }); @@ -6212,6 +6302,8 @@ speechSynthesis.getVoices(); ) { API.getUser({ userId: id + }).catch(() => { + this.updateFriendInProgress.delete(id); }); } } else { @@ -6224,6 +6316,22 @@ speechSynthesis.getVoices(); ) { var {location, $location_at} = ref; } + // prevent status flapping + if ( + ctx.state === 'online' && + (stateInput === 'active' || stateInput === 'offline') + ) { + await new Promise((resolve) => { + setTimeout(resolve, 50000); + }); + if (this.APILastOnline.has(id)) { + var date = this.APILastOnline.get(id); + if (date > Date.now() - 60000) { + this.updateFriendInProgress.delete(id); + return; + } + } + } try { var args = await API.getUser({ userId: id @@ -6421,6 +6529,10 @@ speechSynthesis.getVoices(); return $app.sortStatus(a.ref.status, b.ref.status); }; + $app.methods.sortByStatus = function (a, b, field) { + return this.sortStatus(a[field], b[field]); + }; + $app.methods.sortStatus = function (a, b) { switch (b) { case 'join me': @@ -6759,14 +6871,23 @@ speechSynthesis.getVoices(); }; API.$on('LOGIN', async function (args) { + $app.feedTable.data = []; $app.friendLogInitStatus = false; - await database.init(args.json.id); + await database.initUserTables(args.json.id); $app.feedTable.data = await database.getFeedDatabase(); $app.sweepFeed(); if (configRepository.getBool(`friendLogInit_${args.json.id}`)) { - $app.getFriendLog(); + await $app.getFriendLog(); } else { - $app.initFriendLog(args.json.id); + await $app.initFriendLog(args.json.id); + } + this.getAuth(); + + $app.updateSharedFeed(true); + $app.notyInit = true; + + if ($app.isGameRunning) { + $app.loadPlayerList(); } // remove old data from json file and migrate to SQLite if (VRCXStorage.Get(`${args.json.id}_friendLogUpdatedAt`)) { @@ -6776,6 +6897,63 @@ speechSynthesis.getVoices(); } }); + $app.methods.loadPlayerList = function () { + var {data} = this.gameLogTable; + if (data.length === 0) { + return; + } + var length = 0; + for (var i = data.length - 1; i > -1; i--) { + var ctx = data[i]; + if (ctx.type === 'Location') { + this.lastLocation = { + date: Date.parse(ctx.created_at), + location: ctx.location, + name: ctx.worldName, + playerList: new Map(), + friendList: new Map() + }; + length = i; + break; + } + } + if (length > 0) { + for (var i = length + 1; i < data.length; i++) { + var ctx = data[i]; + if (ctx.type === 'OnPlayerJoined') { + if (!ctx.userId) { + for (var ref of API.cachedUsers.values()) { + if (ref.displayName === ctx.displayName) { + ctx.userId = ref.id; + break; + } + } + } + var userMap = { + displayName: ctx.displayName, + userId: ctx.userId, + joinTime: Date.parse(ctx.created_at) + }; + this.lastLocation.playerList.set(ctx.displayName, userMap); + if ( + this.friends.has(ctx.userId) || + API.currentUser.displayName === ctx.displayName + ) { + this.lastLocation.friendList.set( + ctx.displayName, + userMap + ); + } + } + if (ctx.type === 'OnPlayerLeft') { + this.lastLocation.playerList.delete(ctx.displayName); + this.lastLocation.friendList.delete(ctx.displayName); + } + } + this.updateVRLastLocation(); + } + }; + API.$on('USER:UPDATE', async function (args) { var {ref, props} = args; if ($app.friends.has(ref.id) === false) { @@ -6973,9 +7151,42 @@ speechSynthesis.getVoices(); date: 0, location: '', name: '', - playerList: [], - friendList: [] + playerList: new Map(), + friendList: new Map() }; + + $app.methods.lastLocationReset = function () { + var playerList = Array.from(this.lastLocation.playerList.values()); + for (var ref of playerList) { + var time = new Date().getTime() - ref.joinTime; + var entry = { + created_at: new Date().toJSON(), + type: 'OnPlayerLeft', + displayName: ref.displayName, + location: this.lastLocation.location, + userId: ref.userId, + time + }; + this.addGameLog(entry); + database.addGamelogJoinLeaveToDatabase(entry); + } + if (this.lastLocation.date !== 0) { + var timeLocation = new Date().getTime() - this.lastLocation.date; + var update = { + time: timeLocation, + created_at: new Date(this.lastLocation.date).toJSON() + }; + database.updateGamelogLocationTimeToDatabase(update); + } + this.lastLocation = { + date: 0, + location: '', + name: '', + playerList: new Map(), + friendList: new Map() + }; + }; + $app.data.lastLocation$ = {}; $app.data.discordActive = configRepository.getBool('discordActive'); $app.data.discordInstance = configRepository.getBool('discordInstance'); @@ -6997,13 +7208,14 @@ speechSynthesis.getVoices(); filter.value.some((v) => v === row.type) }, { - prop: 'data', + prop: 'displayName', value: '' }, { - prop: 'data', + prop: 'displayName', value: true, - filterFn: (row) => row.data !== API.currentUser.displayName + filterFn: (row) => + row.displayName !== API.currentUser.displayName }, { prop: 'type', @@ -7029,117 +7241,200 @@ speechSynthesis.getVoices(); $app.methods.resetGameLog = async function () { await gameLogService.reset(); - await gameLogService.poll(); this.gameLogTable.data = []; - this.lastLocation = { - date: 0, - location: '', - name: '', - playerList: [], - friendList: [] - }; + this.lastLocationReset(); }; - $app.methods.updateGameLogLoop = async function () { - try { - if (API.isLoggedIn === true) { - await this.updateGameLog(); - this.sweepGameLog(); - var length = this.gameLogTable.data.length; - if (length > 0) { - if ( - this.gameLogTable.data[length - 1].created_at !== - this.gameLogTable.lastEntryDate - ) { - this.notifyMenu('gameLog'); - } - this.gameLogTable.lastEntryDate = - this.gameLogTable.data[length - 1].created_at; - } - this.updateSharedFeed(false); + $app.methods.refreshEntireGameLog = async function () { + await gameLogService.setDateTill('1970-01-01'); + await database.initTables(); + await this.resetGameLog(); + var location = ''; + var pushToTable = false; + for (var gameLog of await gameLogService.getAll()) { + if (gameLog.type === 'location') { + location = gameLog.location; } - } catch (err) { - console.error(err); + this.addGameLogEntry(gameLog, location, pushToTable); } - setTimeout(() => this.updateGameLogLoop(), 500); + this.getGameLogTable(); }; - $app.methods.updateGameLog = async function () { - for (var gameLog of await gameLogService.poll()) { - var tableData = null; + $app.methods.getGameLogTable = async function () { + await database.initTables(); + this.gameLogTable.data = await database.getGamelogDatabase(); + this.sweepGameLog(); + var length = this.gameLogTable.data.length; + if (length > 1) { + this.updateGameLog(this.gameLogTable.data[length - 1].created_at); + } + }; - switch (gameLog.type) { - case 'location': - if (this.isGameRunning) { - this.lastLocation = { - date: Date.parse(gameLog.dt), - location: gameLog.location, - name: gameLog.worldName, - playerList: [], - friendList: [] - }; - } - tableData = { - created_at: gameLog.dt, - type: 'Location', - data: [gameLog.location, gameLog.worldName] - }; - break; - - case 'player-joined': - tableData = { - created_at: gameLog.dt, - type: 'OnPlayerJoined', - data: gameLog.userDisplayName - }; - break; - - case 'player-left': - tableData = { - created_at: gameLog.dt, - type: 'OnPlayerLeft', - data: gameLog.userDisplayName - }; - break; - - case 'notification': - tableData = { - created_at: gameLog.dt, - type: 'Notification', - data: gameLog.json - }; - break; - - case 'portal-spawn': - tableData = { - created_at: gameLog.dt, - type: 'PortalSpawn', - data: gameLog.userDisplayName - }; - break; - - case 'event': - tableData = { - created_at: gameLog.dt, - type: 'Event', - data: gameLog.event - }; - break; - - case 'video-play': - tableData = { - created_at: gameLog.dt, - type: 'VideoPlay', - data: gameLog.videoURL, - displayName: gameLog.displayName - }; - break; + $app.methods.updateGameLog = async function (dateTill) { + await gameLogService.setDateTill(dateTill); + await gameLogService.reset(); + await new Promise((resolve) => { + setTimeout(resolve, 1000); + }); + var location = ''; + var pushToTable = true; + for (var gameLog of await gameLogService.getAll()) { + if (gameLog.type === 'location') { + location = gameLog.location; } + this.addGameLogEntry(gameLog, location, pushToTable); + } + }; - if (tableData !== null) { - this.gameLogTable.data.push(tableData); + $app.methods.addGameLogEvent = async function (json) { + var rawLogs = JSON.parse(json); + var gameLog = gameLogService.parseRawGameLog( + rawLogs[1], + rawLogs[2], + rawLogs.slice(3) + ); + var pushToTable = true; + await this.addGameLogEntry( + gameLog, + this.lastLocation.location, + pushToTable + ); + this.updateSharedFeed(false); + this.notifyMenu('gameLog'); + this.sweepGameLog(); + }; + + $app.methods.addGameLogEntry = function ( + gameLog, + location, + pushToTable + ) { + var userId = ''; + if (gameLog.userDisplayName) { + for (var ref of API.cachedUsers.values()) { + if (ref.displayName === gameLog.userDisplayName) { + userId = ref.id; + break; + } } } + switch (gameLog.type) { + case 'location': + if (this.isGameRunning) { + this.lastLocationReset(); + this.lastLocation = { + date: Date.parse(gameLog.dt), + location: gameLog.location, + name: gameLog.worldName, + playerList: new Map(), + friendList: new Map() + }; + this.updateVRLastLocation(); + this.checkVRChatCacheDownload(this.lastLocation.location); + } + var L = API.parseLocation(gameLog.location); + var entry = { + created_at: gameLog.dt, + type: 'Location', + location: gameLog.location, + worldId: L.worldId, + worldName: gameLog.worldName, + time: 0 + }; + database.addGamelogLocationToDatabase(entry); + break; + case 'player-joined': + var userMap = { + displayName: gameLog.userDisplayName, + userId, + joinTime: Date.parse(gameLog.dt) + }; + this.lastLocation.playerList.set( + gameLog.userDisplayName, + userMap + ); + if (this.friends.has(userId)) { + this.lastLocation.friendList.set( + gameLog.userDisplayName, + userMap + ); + } + this.updateVRLastLocation(); + var entry = { + created_at: gameLog.dt, + type: 'OnPlayerJoined', + displayName: gameLog.userDisplayName, + location, + userId, + time: 0 + }; + database.addGamelogJoinLeaveToDatabase(entry); + break; + case 'player-left': + var time = 0; + var ref = this.lastLocation.playerList.get( + gameLog.userDisplayName + ); + if (typeof ref !== 'undefined') { + time = new Date().getTime() - ref.joinTime; + this.lastLocation.playerList.delete( + gameLog.userDisplayName + ); + this.lastLocation.friendList.delete( + gameLog.userDisplayName + ); + } + this.updateVRLastLocation(); + var entry = { + created_at: gameLog.dt, + type: 'OnPlayerLeft', + displayName: gameLog.userDisplayName, + location, + userId, + time + }; + database.addGamelogJoinLeaveToDatabase(entry); + break; + case 'portal-spawn': + var entry = { + created_at: gameLog.dt, + type: 'PortalSpawn', + displayName: gameLog.userDisplayName, + location, + userId, + instanceId: '', + worldName: '' + }; + database.addGamelogPortalSpawnToDatabase(entry); + break; + case 'video-play': + var entry = { + created_at: gameLog.dt, + type: 'VideoPlay', + data: gameLog.videoUrl, + displayName: gameLog.displayName + }; + database.addGamelogVideoPlayToDatabase(entry); + break; + case 'notification': + var entry = { + created_at: gameLog.dt, + type: 'Notification', + data: gameLog.json + }; + break; + case 'event': + var entry = { + created_at: gameLog.dt, + type: 'Event', + data: gameLog.event + }; + database.addGamelogEventToDatabase(entry); + break; + } + if (pushToTable && entry) { + this.gameLogTable.data.push(entry); + } }; $app.methods.sweepGameLog = function () { @@ -7208,20 +7503,32 @@ speechSynthesis.getVoices(); Discord.SetActive(this.discordActive); }; - $app.methods.lookupUser = async function (name) { - for (var ref of API.cachedUsers.values()) { - if (ref.displayName === name) { - this.showUserDialog(ref.id); + $app.methods.lookupUser = async function (ref) { + if (ref.userId) { + this.showUserDialog(ref.userId); + return; + } + for (var ctx of API.cachedUsers.values()) { + if (ctx.displayName === ref.displayName) { + this.showUserDialog(ctx.id); return; } } - this.searchText = name; + try { + var username = encodeURIComponent(ref.displayName.toLowerCase()); + var args = await API.getUserByUsername({username}); + if (args.ref.displayName === ref.displayName) { + this.showUserDialog(args.ref.id); + return; + } + } catch (err) {} + this.searchText = ref.displayName; await this.searchUser(); - for (var ref of this.searchUserResults) { - if (ref.displayName === name) { + for (var ctx of this.searchUserResults) { + if (ctx.displayName === ref.displayName) { this.searchText = ''; this.clearSearch(); - this.showUserDialog(ref.id); + this.showUserDialog(ctx.id); return; } } @@ -7797,6 +8104,7 @@ speechSynthesis.getVoices(); } this.friendLogTable.data = []; this.friendLogTable.data = await database.getFriendLogHistory(); + await API.refreshFriends(); this.friendLogInitStatus = true; }; @@ -8448,6 +8756,7 @@ speechSynthesis.getVoices(); 'VRCX_vrBackgroundEnabled', this.vrBackgroundEnabled ); + this.updateSharedFeed(true); this.updateVRConfigVars(); }; $app.data.TTSvoices = speechSynthesis.getVoices(); @@ -8631,6 +8940,7 @@ speechSynthesis.getVoices(); DisplayName: 'VIP', TrustLevel: 'VIP', PortalSpawn: 'Everyone', + AvatarChange: 'Off', Event: 'On', VideoPlay: 'Off', BlockedOnPlayerJoined: 'Off', @@ -8657,6 +8967,7 @@ speechSynthesis.getVoices(); DisplayName: 'Friends', TrustLevel: 'Friends', PortalSpawn: 'Everyone', + AvatarChange: 'Everyone', Event: 'On', VideoPlay: 'On', BlockedOnPlayerJoined: 'Off', @@ -8775,16 +9086,8 @@ speechSynthesis.getVoices(); this.updateVRConfigVars(); }; - sharedRepository.setBool('is_game_running', false); var isGameRunningStateChange = function () { - sharedRepository.setBool('is_game_running', this.isGameRunning); - this.lastLocation = { - date: 0, - location: '', - name: '', - playerList: [], - friendList: [] - }; + this.lastLocationReset(); if (this.isGameRunning) { API.currentUser.$online_for = Date.now(); API.currentUser.$offline_for = ''; @@ -8796,23 +9099,15 @@ speechSynthesis.getVoices(); }; $app.watch.isGameRunning = isGameRunningStateChange; - sharedRepository.setBool('is_Game_No_VR', false); - var isGameNoVRStateChange = function () { - sharedRepository.setBool('is_Game_No_VR', this.isGameNoVR); + var downloadProgressStateChange = function () { + this.updateVRConfigVars(); }; - $app.watch.isGameNoVR = isGameNoVRStateChange; - - var lastLocationStateChange = function () { - sharedRepository.setObject('last_location', $app.lastLocation); - $app.checkVRChatCacheDownload($app.lastLocation.location); - }; - $app.watch['lastLocation.location'] = lastLocationStateChange; + $app.watch.downloadProgress = downloadProgressStateChange; $app.methods.updateVRConfigVars = function () { - if (configRepository.getBool('isDarkMode')) { - var notificationTheme = 'sunset'; - } else { - var notificationTheme = 'relax'; + var notificationTheme = 'relax'; + if (this.isDarkMode) { + notificationTheme = 'sunset'; } var VRConfigVars = { notificationTTS: this.notificationTTS, @@ -8823,15 +9118,34 @@ speechSynthesis.getVoices(); notificationPosition: this.notificationPosition, notificationTimeout: this.notificationTimeout, notificationTheme, - backgroundEnabled: this.vrBackgroundEnabled + backgroundEnabled: this.vrBackgroundEnabled, + isGameRunning: this.isGameRunning, + isGameNoVR: this.isGameNoVR, + downloadProgress: this.downloadProgress }; - sharedRepository.setObject('VRConfigVars', VRConfigVars); - this.updateSharedFeed(true); + var json = JSON.stringify(VRConfigVars); + AppApi.ExecuteVrFeedFunction('configUpdate', json); + AppApi.ExecuteVrOverlayFunction('configUpdate', json); }; - API.$on('LOGIN', function () { - $app.updateVRConfigVars(); - }); + $app.methods.updateVRLastLocation = function () { + var lastLocation = { + date: this.lastLocation.date, + location: this.lastLocation.location, + name: this.lastLocation.name, + playerList: Array.from(this.lastLocation.playerList.values()), + friendList: Array.from(this.lastLocation.friendList.values()) + }; + var json = JSON.stringify(lastLocation); + AppApi.ExecuteVrFeedFunction('lastLocationUpdate', json); + AppApi.ExecuteVrOverlayFunction('lastLocationUpdate', json); + }; + + $app.methods.vrInit = function () { + this.updateVRConfigVars(); + this.updateVRLastLocation(); + this.updateSharedFeed(true); + }; API.$on('LOGIN', function () { $app.currentUserTreeData = []; @@ -9567,33 +9881,28 @@ speechSynthesis.getVoices(); var playersInInstance = this.lastLocation.playerList; if ( this.lastLocation.location === L.tag && - playersInInstance.length > 0 + playersInInstance.size > 0 ) { var ref = API.cachedUsers.get(API.currentUser.id); if (typeof ref === 'undefined') { ref = API.currentUser; } - if (playersInInstance.includes(ref.displayName)) { - users.push(ref); + if (playersInInstance.has(ref.displayName)) { + users.push(ref); // add self } var friendsInInstance = this.lastLocation.friendList; - for (var i = 0; i < friendsInInstance.length; i++) { + for (var friend of friendsInInstance.values()) { + // if friend isn't in instance add them var addUser = true; - var player = friendsInInstance[i]; for (var k = 0; k < users.length; k++) { var user = users[k]; - if (user.displayName === player) { + if (friend.displayName === user.displayName) { addUser = false; break; } } - if (addUser) { - for (var ref of API.cachedUsers.values()) { - if (ref.displayName === player) { - users.push(ref); - break; - } - } + if (addUser && API.cachedUsers.has(friend.userId)) { + users.push(API.cachedUsers.get(friend.userId)); } } } else if (L.isOffline === false) { @@ -9617,7 +9926,7 @@ speechSynthesis.getVoices(); if (L.worldId && this.lastLocation.location === D.ref.location) { D.instance = { id: D.ref.location, - occupants: this.lastLocation.playerList.length + occupants: this.lastLocation.playerList.size }; } if (L.isOffline || L.isPrivate || L.worldId === '') { @@ -10156,7 +10465,17 @@ speechSynthesis.getVoices(); D.isFavorite = API.cachedFavoritesByObjectId.has(D.id); this.updateVRChatWorldCache(); if (args.cache) { - API.getWorld(args.params); + API.getWorld(args.params) + .catch((err) => { + throw err; + }) + .then((args1) => { + if (D.id === args1.ref.id) { + D.ref = args1.ref; + this.updateVRChatWorldCache(); + } + return args1; + }); } } return args; @@ -10186,7 +10505,7 @@ speechSynthesis.getVoices(); if (lastLocation$.worldId === D.id) { var instance = { id: lastLocation$.instanceId, - occupants: playersInInstance.length, + occupants: playersInInstance.size, users: [] }; instances[instance.id] = instance; @@ -10194,26 +10513,24 @@ speechSynthesis.getVoices(); if (typeof ref === 'undefined') { ref = API.currentUser; } - if (playersInInstance.includes(ref.displayName)) { - instance.users.push(ref); + if (playersInInstance.has(ref.displayName)) { + instance.users.push(ref); // add self } var friendsInInstance = this.lastLocation.friendList; - for (var i = 0; i < friendsInInstance.length; i++) { + for (var friend of friendsInInstance.values()) { + // if friend isn't in instance add them var addUser = true; - var player = friendsInInstance[i]; for (var k = 0; k < instance.users.length; k++) { var user = instance.users[k]; - if (user.displayName === player) { + if (friend.displayName === user.displayName) { addUser = false; break; } } if (addUser) { - for (var ref of API.cachedUsers.values()) { - if (ref.displayName === player) { - instance.users.push(ref); - break; - } + var ref = API.cachedUsers.get(friend.userId); + if (typeof ref !== 'undefined') { + instance.users.push(ref); } } } @@ -12282,6 +12599,10 @@ speechSynthesis.getVoices(); if (val === null) { return; } + if (!val.id) { + this.lookupUser(val); + return; + } this.showUserDialog(val.id); }; @@ -13869,11 +14190,6 @@ speechSynthesis.getVoices(); $app.data.downloadQueue = new Map(); $app.data.downloadCurrent = {}; - var downloadProgressUpdateWrist = function () { - sharedRepository.setInt('downloadProgress', this.downloadProgress); - }; - $app.watch.downloadProgress = downloadProgressUpdateWrist; - $app.methods.downloadVRChatCacheProgress = async function () { var downloadProgress = await AssetBundleCacher.CheckDownloadProgress(); switch (downloadProgress) { @@ -14480,7 +14796,7 @@ speechSynthesis.getVoices(); this.$nextTick(() => adjustDialogZ(this.$refs.VRCXUpdateDialog.$el)); var D = this.VRCXUpdateDialog; D.visible = true; - D.updatePending = await AppApi.checkForUpdateZip(); + D.updatePending = await AppApi.CheckForUpdateZip(); this.loadBranchVersions(); }; @@ -14571,7 +14887,7 @@ speechSynthesis.getVoices(); }; $app.methods.checkForVRCXUpdate = async function () { - if (await AppApi.checkForUpdateZip()) { + if (await AppApi.CheckForUpdateZip()) { return; } var url = this.branches[this.branch].urlLatest; diff --git a/html/src/index.pug b/html/src/index.pug index 0286b08c..8bf3a478 100644 --- a/html/src/index.pug +++ b/html/src/index.pug @@ -185,10 +185,10 @@ html template(#tool) div(style="margin:0 0 10px;display:flex;align-items:center") el-select(v-model="gameLogTable.filters[0].value" @change="saveTableFilters" multiple clearable collapse-tags style="flex:1" placeholder="Filter") - el-option(v-once v-for="type in ['Location', 'OnPlayerJoined', 'OnPlayerLeft', 'PortalSpawn', 'Event', 'VideoPlay']" :key="type" :label="type" :value="type") + el-option(v-once v-for="type in ['Location', 'OnPlayerJoined', 'OnPlayerLeft', 'PortalSpawn', 'AvatarChange', 'Event', 'VideoPlay']" :key="type" :label="type" :value="type") el-input(v-model="gameLogTable.filters[1].value" placeholder="Search" style="flex:none;width:150px;margin:0 10px") el-tooltip(placement="bottom" content="Reset game log" :disabled="hideTooltips") - el-button(type="default" @click="resetGameLog()" icon="el-icon-refresh" circle style="flex:none") + el-button(type="default" @click="refreshGameLog()" icon="el-icon-refresh" circle style="flex:none") el-table-column(label="Date" prop="created_at" sortable="custom" width="90") template(v-once #default="scope") el-tooltip(placement="right") @@ -196,17 +196,28 @@ html span {{ scope.row.created_at | formatDate('YYYY-MM-DD HH24:MI:SS') }} span {{ scope.row.created_at | formatDate('MM-DD HH24:MI') }} el-table-column(label="Type" prop="type" width="120") + template(v-once #default="scope") + span.x-link(v-if="scope.row.location && scope.row.type !== 'Location'" v-text="scope.row.type" @click="showWorldDialog(scope.row.location)") + span(v-else v-text="scope.row.type") + el-table-column(label="User" prop="displayName" width="160") + template(v-once #default="scope") + span.x-link(v-text="scope.row.displayName" @click="lookupUser(scope.row)") el-table-column(label="Detail" prop="data") template(v-once #default="scope") - location(v-if="scope.row.type === 'Location'" :location="scope.row.data[0]" :hint="scope.row.data[1]") + location(v-if="scope.row.type === 'Location'" :location="scope.row.location" :hint="scope.row.worldName") + location(v-else-if="scope.row.type === 'PortalSpawn'" :location="scope.row.instanceId" :hint="scope.row.worldName") + template(v-else-if="scope.row.type === 'AvatarChange'") + span.x-link(@click="showUserDialog(scope.row.authorId)" v-text="scope.row.name") + template(v-if="scope.row.description && scope.row.name !== scope.row.description") + | - {{ scope.row.description }} template(v-else-if="scope.row.type === 'Event'") span(v-text="scope.row.data") template(v-else-if="scope.row.type === 'VideoPlay'") - span.x-link(v-text="scope.row.data" @click="openExternalLink(scope.row.data)") - template(v-if="scope.row.displayName") - span.x-link(@click="lookupUser(scope.row.displayName)") ({{ scope.row.displayName }}) - template(v-else-if="scope.row.type === 'Notification'") - span.x-link(v-else v-text="scope.row.data" @click="lookupUser(scope.row.data)") + span(v-if="scope.row.videoId") {{ scope.row.videoId }}: + span.x-link(v-if="scope.row.videoName" @click="openExternalLink(scope.row.videoUrl)" v-text="scope.row.videoName") + span.x-link(v-else @click="openExternalLink(scope.row.videoUrl)" v-text="scope.row.videoUrl") + template(v-else-if="scope.row.type === 'Notification' || scope.row.type === 'OnPlayerJoined' || scope.row.type === 'OnPlayerLeft'") + span.x-link(v-else v-text="scope.row.data") //- search .x-container(v-show="$refs.menu && $refs.menu.activeIndex === 'search'") @@ -1924,15 +1935,22 @@ html el-radio-button(label="Friends") el-radio-button(label="Everyone") .toggle-item - span.toggle-name Events - el-radio-group(v-model="sharedFeedFilters.noty.Event" size="mini") + span.toggle-name Avatar Change + el-radio-group(v-model="sharedFeedFilters.noty.AvatarChange" size="mini") el-radio-button(label="Off") - el-radio-button(label="On") + el-radio-button(label="VIP") + el-radio-button(label="Friends") + el-radio-button(label="Everyone") .toggle-item span.toggle-name Video Play el-radio-group(v-model="sharedFeedFilters.noty.VideoPlay" size="mini") el-radio-button(label="Off") el-radio-button(label="On") + .toggle-item + span.toggle-name Events + el-radio-group(v-model="sharedFeedFilters.noty.Event" size="mini") + el-radio-button(label="Off") + el-radio-button(label="On") .toggle-item span.toggle-name Blocked Player Joins el-radio-group(v-model="sharedFeedFilters.noty.BlockedOnPlayerJoined" size="mini") @@ -2076,13 +2094,20 @@ html el-radio-button(label="Friends") el-radio-button(label="Everyone") .toggle-item - span.toggle-name Events - el-radio-group(v-model="sharedFeedFilters.wrist.Event" size="mini") + span.toggle-name Avatar Change + el-radio-group(v-model="sharedFeedFilters.wrist.AvatarChange" size="mini") + el-radio-button(label="Off") + el-radio-button(label="VIP") + el-radio-button(label="Friends") + el-radio-button(label="Everyone") + .toggle-item + span.toggle-name Video Play + el-radio-group(v-model="sharedFeedFilters.wrist.VideoPlay" size="mini") el-radio-button(label="Off") el-radio-button(label="On") .toggle-item - span.toggle-name Video Play - el-radio-group(v-model="sharedFeedFilters.noty.VideoPlay" size="mini") + span.toggle-name Events + el-radio-group(v-model="sharedFeedFilters.wrist.Event" size="mini") el-radio-button(label="Off") el-radio-button(label="On") .toggle-item diff --git a/html/src/repository/database.js b/html/src/repository/database.js index 0ec42f4a..0ff1042a 100644 --- a/html/src/repository/database.js +++ b/html/src/repository/database.js @@ -1,7 +1,7 @@ import sqliteService from '../service/sqlite.js'; class Database { - async init(userId) { + async initUserTables(userId) { Database.userId = userId.replaceAll('-', '').replaceAll('_', ''); await sqliteService.executeNonQuery( `CREATE TABLE IF NOT EXISTS ${Database.userId}_feed_gps (id INTEGER PRIMARY KEY, created_at TEXT, user_id TEXT, display_name TEXT, location TEXT, world_name TEXT, previous_location TEXT, time INTEGER)` @@ -26,6 +26,24 @@ class Database { ); } + async initTables() { + await sqliteService.executeNonQuery( + `CREATE TABLE IF NOT EXISTS gamelog_location (id INTEGER PRIMARY KEY, created_at TEXT, location TEXT, world_id TEXT, world_name TEXT, time INTEGER, UNIQUE(created_at, location))` + ); + await sqliteService.executeNonQuery( + `CREATE TABLE IF NOT EXISTS gamelog_join_leave (id INTEGER PRIMARY KEY, created_at TEXT, type TEXT, display_name TEXT, location TEXT, user_id TEXT, time INTEGER, UNIQUE(created_at, type, display_name))` + ); + await sqliteService.executeNonQuery( + `CREATE TABLE IF NOT EXISTS gamelog_portal_spawn (id INTEGER PRIMARY KEY, created_at TEXT, display_name TEXT, location TEXT, user_id TEXT, instance_id TEXT, world_name TEXT, UNIQUE(created_at, display_name))` + ); + await sqliteService.executeNonQuery( + `CREATE TABLE IF NOT EXISTS gamelog_video_play (id INTEGER PRIMARY KEY, created_at TEXT, video_url TEXT, video_name TEXT, video_id TEXT, location TEXT, display_name TEXT, user_id TEXT, UNIQUE(created_at, video_url))` + ); + await sqliteService.executeNonQuery( + `CREATE TABLE IF NOT EXISTS gamelog_event (id INTEGER PRIMARY KEY, created_at TEXT, data TEXT, UNIQUE(created_at, data))` + ); + } + async getFeedDatabase() { var feedDatabase = []; var date = new Date(); @@ -349,6 +367,162 @@ class Database { } ); } + + async getGamelogDatabase() { + var gamelogDatabase = []; + var date = new Date(); + date.setDate(date.getDate() - 3); // 3 day limit + var dateOffset = date.toJSON(); + await sqliteService.execute((dbRow) => { + var row = { + rowId: dbRow[0], + created_at: dbRow[1], + type: 'Location', + location: dbRow[2], + worldId: dbRow[3], + worldName: dbRow[4], + time: dbRow[5] + }; + gamelogDatabase.unshift(row); + }, `SELECT * FROM gamelog_location WHERE created_at >= date('${dateOffset}')`); + await sqliteService.execute((dbRow) => { + var row = { + rowId: dbRow[0], + created_at: dbRow[1], + type: dbRow[2], + displayName: dbRow[3], + location: dbRow[4], + userId: dbRow[5], + time: dbRow[6] + }; + gamelogDatabase.unshift(row); + }, `SELECT * FROM gamelog_join_leave WHERE created_at >= date('${dateOffset}')`); + await sqliteService.execute((dbRow) => { + var row = { + rowId: dbRow[0], + created_at: dbRow[1], + type: 'PortalSpawn', + displayName: dbRow[2], + location: dbRow[3], + userId: dbRow[4], + instanceId: dbRow[5], + worldName: dbRow[6] + }; + gamelogDatabase.unshift(row); + }, `SELECT * FROM gamelog_portal_spawn WHERE created_at >= date('${dateOffset}')`); + await sqliteService.execute((dbRow) => { + var row = { + rowId: dbRow[0], + created_at: dbRow[1], + type: 'VideoPlay', + videoUrl: dbRow[2], + videoName: dbRow[3], + videoId: dbRow[4], + location: dbRow[5], + displayName: dbRow[6], + userId: dbRow[7] + }; + gamelogDatabase.unshift(row); + }, `SELECT * FROM gamelog_video_play WHERE created_at >= date('${dateOffset}')`); + await sqliteService.execute((dbRow) => { + var row = { + rowId: dbRow[0], + created_at: dbRow[1], + type: 'Event', + data: dbRow[2] + }; + gamelogDatabase.unshift(row); + }, `SELECT * FROM gamelog_event WHERE created_at >= date('${dateOffset}')`); + var compareByCreatedAt = function (a, b) { + var A = a.created_at; + var B = b.created_at; + if (A < B) { + return -1; + } + if (A > B) { + return 1; + } + return 0; + }; + gamelogDatabase.sort(compareByCreatedAt); + return gamelogDatabase; + } + + addGamelogLocationToDatabase(entry) { + sqliteService.executeNonQuery( + `INSERT OR IGNORE INTO gamelog_location (created_at, location, world_id, world_name, time) VALUES (@created_at, @location, @world_id, @world_name, @time)`, + { + '@created_at': entry.created_at, + '@location': entry.location, + '@world_id': entry.worldId, + '@world_name': entry.worldName, + '@time': entry.time + } + ); + } + + updateGamelogLocationTimeToDatabase(entry) { + sqliteService.executeNonQuery( + `UPDATE gamelog_location SET time = @time WHERE created_at = @created_at`, + { + '@created_at': entry.created_at, + '@time': entry.time + } + ); + } + + addGamelogJoinLeaveToDatabase(entry) { + sqliteService.executeNonQuery( + `INSERT OR IGNORE INTO gamelog_join_leave (created_at, type, display_name, location, user_id, time) VALUES (@created_at, @type, @display_name, @location, @user_id, @time)`, + { + '@created_at': entry.created_at, + '@type': entry.type, + '@display_name': entry.displayName, + '@location': entry.location, + '@user_id': entry.userId, + '@time': entry.time + } + ); + } + + addGamelogPortalSpawnToDatabase(entry) { + sqliteService.executeNonQuery( + `INSERT OR IGNORE INTO gamelog_portal_spawn (created_at, display_name, location, user_id, instance_id, world_name) VALUES (@created_at, @display_name, @location, @user_id, @instance_id, @world_name)`, + { + '@created_at': entry.created_at, + '@display_name': entry.displayName, + '@location': entry.location, + '@user_id': entry.userId, + '@instance_id': entry.instanceId, + '@world_name': entry.worldName + } + ); + } + + addGamelogVideoPlayToDatabase(entry) { + sqliteService.executeNonQuery( + `INSERT OR IGNORE INTO gamelog_video_play (created_at, video_url, video_name, video_id, location, display_name, user_id) VALUES (@created_at, @video_url, @video_name, @video_id, @location, @display_name, @user_id)`, + { + '@created_at': entry.created_at, + '@video_url': entry.videoUrl, + '@video_name': entry.videoName, + '@video_id': entry.videoId, + '@location': entry.location, + '@display_name': entry.displayName, + '@user_id': entry.userId + } + ); + } + + addGamelogEventToDatabase(entry) { + sqliteService.executeNonQuery( + `INSERT OR IGNORE INTO gamelog_event (created_at, data) VALUES (@created_at, @data)`, + { + '@created_at': entry.created_at, + '@data': entry.data + } + ); + } } var self = new Database(); diff --git a/html/src/service/gamelog.js b/html/src/service/gamelog.js index 598e435d..20c4c015 100644 --- a/html/src/service/gamelog.js +++ b/html/src/service/gamelog.js @@ -1,93 +1,78 @@ // requires binding of LogWatcher -// -var contextMap = new Map(); - -function parseRawGameLog(dt, type, args) { - var gameLog = { - dt, - type - }; - - switch (type) { - case 'location': - gameLog.location = args[0]; - gameLog.worldName = args[1]; - break; - - case 'player-joined': - gameLog.userDisplayName = args[0]; - gameLog.userType = args[1]; - break; - - case 'player-left': - gameLog.userDisplayName = args[0]; - break; - - case 'notification': - gameLog.json = args[0]; - break; - - case 'portal-spawn': - gameLog.userDisplayName = args[0]; - break; - - case 'event': - gameLog.event = args[0]; - break; - - case 'video-play': - gameLog.videoURL = args[0]; - gameLog.displayName = args[1]; - break; - - default: - break; - } - - return gameLog; -} - class GameLogService { - async poll() { - var rawGameLogs = await LogWatcher.Get(); - var gameLogs = []; - var now = Date.now(); + parseRawGameLog(dt, type, args) { + var gameLog = { + dt, + type + }; - for (var [fileName, dt, type, ...args] of rawGameLogs) { - var context = contextMap.get(fileName); - if (typeof context === 'undefined') { - context = { - updatedAt: null, + switch (type) { + case 'location': + gameLog.location = args[0]; + gameLog.worldName = args[1]; + break; - // location - location: null - }; - contextMap.set(fileName, context); - } + case 'player-joined': + gameLog.userDisplayName = args[0]; + gameLog.userType = args[1]; + break; - var gameLog = parseRawGameLog(dt, type, args); + case 'player-left': + gameLog.userDisplayName = args[0]; + break; - switch (gameLog.type) { - case 'location': - context.location = gameLog.location; - break; + case 'notification': + gameLog.json = args[0]; + break; - default: - break; - } + case 'portal-spawn': + gameLog.userDisplayName = args[0]; + break; - context.updatedAt = now; + case 'event': + gameLog.event = args[0]; + break; - gameLogs.push(gameLog); + case 'video-play': + gameLog.videoUrl = args[0]; + gameLog.displayName = args[1]; + break; + + case 'vrcx': + gameLog.data = args[0]; + break; + + default: + break; } + return gameLog; + } + + async getAll() { + var gameLogs = []; + var done = false; + while (!done) { + var rawGameLogs = await LogWatcher.Get(); + // eslint-disable-next-line no-unused-vars + for (var [fileName, dt, type, ...args] of rawGameLogs) { + var gameLog = this.parseRawGameLog(dt, type, args); + gameLogs.push(gameLog); + } + if (rawGameLogs.length === 0) { + done = true; + } + } return gameLogs; } + async setDateTill(dateTill) { + await LogWatcher.SetDateTill(dateTill); + } + async reset() { await LogWatcher.Reset(); - contextMap.clear(); } } diff --git a/html/src/vr.js b/html/src/vr.js index 84684d3e..87117845 100644 --- a/html/src/vr.js +++ b/html/src/vr.js @@ -9,10 +9,7 @@ import Vue from 'vue'; import ElementUI from 'element-ui'; import locale from 'element-ui/lib/locale/lang/en'; -import {appVersion} from './constants.js'; -import sharedRepository from './repository/shared.js'; import configRepository from './repository/config.js'; -import webApiService from './service/webapi.js'; speechSynthesis.getVoices(); @@ -109,273 +106,104 @@ speechSynthesis.getVoices(); }; 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 (typeof handlers === 'undefined') { - return; - } - try { - for (var handler of handlers) { - handler.apply(this, args); + Vue.component('location', { + template: + '{{ text }}', + props: { + location: String, + hint: { + type: String, + default: '' } - } catch (err) { - console.error(err); - } - }; - - API.$on = function (name, handler) { - var handlers = this.eventHandlers.get(name); - if (typeof handlers === 'undefined') { - handlers = []; - this.eventHandlers.set(name, handlers); - } - handlers.push(handler); - }; - - API.$off = function (name, handler) { - var handlers = this.eventHandlers.get(name); - if (typeof 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 (typeof req !== 'undefined') { - return req; - } - } else { - init.headers = { - 'Content-Type': 'application/json;charset=utf-8', - ...init.headers + }, + data() { + return { + text: this.location, + region: this.region }; - init.body = - params === Object(params) ? JSON.stringify(params) : '{}'; - } - init.headers = { - 'User-Agent': appVersion, - ...init.headers - }; - 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); - return {}; - }) - .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; + }, + methods: { + parse() { + this.text = this.location; + var L = $app.parseLocation(this.location); + if (L.isOffline) { + this.text = 'Offline'; + } else if (L.isPrivate) { + this.text = 'Private'; + } else if (typeof this.hint === 'string' && this.hint !== '') { + if (L.instanceId) { + this.text = `${this.hint} #${L.instanceName} ${L.accessType}`; + } else { + this.text = this.hint; } - 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); + } else if (L.worldId) { + if (L.instanceId) { + this.text = ` #${L.instanceName} ${L.accessType}`; + } else { + this.text = this.location; + } + } + this.region = ''; + if ( + this.location !== '' && + L.instanceId && + !L.isOffline && + !L.isPrivate + ) { + if (L.region === 'eu') { + this.region = 'europeanunion'; + } else if (L.region === 'jp') { + this.region = 'jp'; + } else { + this.region = 'us'; } } - 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 (typeof status === 'undefined') { - text.push(`${code}`); - } else { - text.push(`${code} ${status}`); } + }, + watch: { + location() { + this.parse(); + } + }, + created() { + this.parse(); } - if (typeof 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; + var $app = { + data: { + // 1 = 대시보드랑 손목에 보이는거 + // 2 = 항상 화면에 보이는 거 + appType: location.href.substr(-1), + currentTime: new Date().toJSON(), + cpuUsage: 0, + config: {}, + lastLocation: { + date: 0, + location: '', + name: '', + playerList: [], + friendList: [] + }, + lastLocationTimer: '', + wristFeed: [], + devices: [] + }, + computed: {}, + methods: {}, + watch: {}, + el: '#x-app', + mounted() { + setTimeout(function () { + AppApi.ExecuteAppFunction('vrInit', ''); + }, 1000); + if (this.appType === '1') { + this.updateStatsLoop(); + } + } }; - 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) { + $app.methods.parseLocation = function (tag) { var _tag = String(tag || ''); var ctx = { tag: _tag, @@ -448,380 +276,32 @@ speechSynthesis.getVoices(); return ctx; }; - Vue.component('location', { - template: - '{{ text }}', - props: { - location: String, - hint: { - type: String, - default: '' - } - }, - data() { - return { - text: this.location, - region: this.region - }; - }, - methods: { - parse() { - this.text = this.location; - var L = API.parseLocation(this.location); - if (L.isOffline) { - this.text = 'Offline'; - } else if (L.isPrivate) { - this.text = 'Private'; - } else if (typeof this.hint === 'string' && this.hint !== '') { - if (L.instanceId) { - this.text = `${this.hint} #${L.instanceName} ${L.accessType}`; - } else { - this.text = this.hint; - } - } else if (L.worldId) { - if (L.instanceId) { - this.text = ` #${L.instanceName} ${L.accessType}`; - } else { - this.text = this.location; - } - } - this.region = ''; - if ( - this.location !== '' && - L.instanceId && - !L.isOffline && - !L.isPrivate - ) { - if (L.region === 'eu') { - this.region = 'europeanunion'; - } else if (L.region === 'jp') { - this.region = 'jp'; - } else { - this.region = 'us'; - } - } - } - }, - 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 (typeof 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; + $app.methods.configUpdate = function (json) { + this.config = JSON.parse(json); }; - /* - 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; - }); + $app.methods.lastLocationUpdate = function (json) { + this.lastLocation = JSON.parse(json); }; - // 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 (typeof 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; + $app.methods.wristFeedUpdate = function (json) { + this.wristFeed = JSON.parse(json); }; - /* - 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; - }); - }; + $app.methods.updateStatsLoop = async function () { + try { + this.currentTime = new Date().toJSON(); + var cpuUsage = await AppApi.CpuUsage(); + this.cpuUsage = cpuUsage.toFixed(0); - /* - params: { - userId: string - } - */ - API.getCachedUser = function (params) { - return new Promise((resolve, reject) => { - var ref = this.cachedUsers.get(params.userId); - if (typeof 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, - downloadProgress: 0, - lastLocation: { - date: 0, - location: '', - name: '', - playerList: [], - friendList: [] - }, - lastLocationTimer: '', - wristFeedLastEntry: '', - notyFeedLastEntry: '', - 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(); - } - this.initLoop(); - return args; - }); - } - }; - - $app.methods.updateVRConfigVars = function () { - this.currentUserStatus = sharedRepository.getString( - 'current_user_status' - ); - this.isGameRunning = sharedRepository.getBool('is_game_running'); - this.isGameNoVR = sharedRepository.getBool('is_Game_No_VR'); - this.downloadProgress = sharedRepository.getInt('downloadProgress'); - var lastLocation = sharedRepository.getObject('last_location'); - if (lastLocation) { - this.lastLocation = lastLocation; + this.lastLocationTimer = ''; if (this.lastLocation.date !== 0) { this.lastLocationTimer = timeToText( Date.now() - this.lastLocation.date ); - } else { - this.lastLocationTimer = ''; } - } - var newConfig = sharedRepository.getObject('VRConfigVars'); - if (newConfig) { - if (JSON.stringify(newConfig) !== JSON.stringify(this.config)) { - this.config = newConfig; - this.notyFeedLastEntry = ''; - this.wristFeedLastEntry = ''; - if (this.appType === '2') { - this.initNotyMap(); - } - } - } else { - throw 'config not set'; - } - }; - $app.methods.initNotyMap = function () { - var notyFeed = sharedRepository.getArray('notyFeed'); - if (notyFeed === null) { - return; - } - notyFeed.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; - } - }); - }; - - $app.methods.initLoop = function () { - if (!sharedRepository.getBool('VRInit')) { - setTimeout(this.initLoop, 500); - } else { - this.updateLoop(); - } - }; - - $app.methods.updateLoop = async function () { - try { - this.currentTime = new Date().toJSON(); - await this.updateVRConfigVars(); - if (!this.config.hideDevicesFromFeed && this.appType === '1') { + if (!this.config.hideDevicesFromFeed) { AppApi.GetVRDevices().then((devices) => { devices.forEach((device) => { device[2] = parseInt(device[2], 10); @@ -831,183 +311,124 @@ speechSynthesis.getVoices(); } else { this.devices = ''; } - await this.updateSharedFeeds(); } catch (err) { console.error(err); } - setTimeout(() => this.updateLoop(), 500); + setTimeout(() => this.updateStatsLoop(), 500); }; - $app.methods.updateCpuUsageLoop = async function () { - try { - var cpuUsage = await AppApi.CpuUsage(); - this.cpuUsage = cpuUsage.toFixed(0); - } catch (err) { - console.error(err); + $app.methods.playNoty = function (json) { + var {noty, message, imageUrl} = JSON.parse(json); + var text = ''; + var img = ''; + if (imageUrl) { + img = ``; } - setTimeout(() => this.updateCpuUsageLoop(), 1000); - }; - - $app.methods.updateSharedFeeds = function () { - if (this.appType === '1') { - this.wristFeed = sharedRepository.getArray('wristFeed'); - } - if (this.appType === '2') { - var notyFeed = sharedRepository.getArray('notyFeed'); - this.updateSharedFeedNoty(notyFeed); - } - }; - - $app.methods.updateSharedFeedNoty = function (notyFeed) { - var notyToPlay = []; - notyFeed.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 - if (this.currentUserStatus === 'busy') { - return; - } - var bias = new Date(Date.now() - 60000).toJSON(); - var noty = {}; - var messageList = [ - 'inviteMessage', - 'requestMessage', - 'responseMessage' - ]; - for (var i = 0; i < notyToPlay.length; i++) { - noty = notyToPlay[i]; - if (noty.created_at < bias) { - continue; - } - var message = ''; - for (var k = 0; k < messageList.length; k++) { - if ( - typeof noty.details !== 'undefined' && - typeof noty.details[messageList[k]] !== 'undefined' - ) { - message = noty.details[messageList[k]]; + switch (noty.type) { + case 'OnPlayerJoined': + text = `${noty.displayName} has joined`; + break; + case 'OnPlayerLeft': + text = `${noty.displayName} has left`; + break; + case 'OnPlayerJoining': + text = `${noty.displayName} is joining`; + break; + case 'GPS': + text = `${ + noty.displayName + } is in ${this.displayLocation( + noty.location, + noty.worldName + )}`; + 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} ${noty.statusDescription}`; + break; + case 'invite': + text = `${ + noty.senderUsername + } has invited you to ${this.displayLocation( + noty.details.worldId, + noty.details.worldName + )}${message}`; + break; + case 'requestInvite': + text = `${noty.senderUsername} has requested an invite ${message}`; + break; + case 'inviteResponse': + text = `${noty.senderUsername} has responded to your invite ${message}`; + break; + case 'requestInviteResponse': + text = `${noty.senderUsername} has responded to your invite request ${message}`; + 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} is no longer your friend`; + 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 'PortalSpawn': + var locationName = ''; + if (noty.worldName) { + locationName = ` to ${this.displayLocation( + noty.instanceId, + noty.worldName + )}`; } - } - if (message) { - message = `, ${message}`; - } - if ( - this.config.overlayNotifications && - !this.isGameNoVR && - this.isGameRunning - ) { - 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.displayName} is joining`; - break; - case 'GPS': - text = `${ - noty.displayName - } is in ${this.displayLocation( - noty.location, - noty.worldName - )}`; - 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} ${noty.statusDescription}`; - break; - case 'invite': - text = `${ - noty.senderUsername - } has invited you to ${this.displayLocation( - noty.details.worldId, - noty.details.worldName - )}${message}`; - break; - case 'requestInvite': - text = `${noty.senderUsername} has requested an invite ${message}`; - break; - case 'inviteResponse': - text = `${noty.senderUsername} has responded to your invite ${message}`; - break; - case 'requestInviteResponse': - text = `${noty.senderUsername} has responded to your invite request ${message}`; - 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} is no longer your friend`; - 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 'PortalSpawn': - text = `${noty.data} has spawned a portal`; - break; - case 'Event': - text = noty.data; - break; - case 'VideoPlay': - text = `Now playing: ${noty.data}`; - break; - case 'BlockedOnPlayerJoined': - text = `Blocked user ${noty.displayName} has joined`; - break; - case 'BlockedOnPlayerLeft': - text = `Blocked user ${noty.displayName} has left`; - break; - case 'MutedOnPlayerJoined': - text = `Muted user ${noty.displayName} has joined`; - break; - case 'MutedOnPlayerLeft': - text = `Muted user ${noty.displayName} has left`; - break; - default: - break; + text = `${noty.displayName} has spawned a portal${locationName}`; + break; + case 'AvatarChange': + text = `${noty.displayName} changed into avatar ${noty.name}`; + break; + case 'Event': + text = noty.data; + break; + case 'VideoPlay': + var videoName = noty.videoUrl; + if (noty.videoName) { + videoName = noty.videoName; } - if (text) { - new Noty({ - type: 'alert', - theme: this.config.notificationTheme, - timeout: this.config.notificationTimeout, - layout: this.config.notificationPosition, - text - }).show(); - } - } + text = `Now playing: ${videoName}`; + break; + case 'BlockedOnPlayerJoined': + text = `Blocked user ${noty.displayName} has joined`; + break; + case 'BlockedOnPlayerLeft': + text = `Blocked user ${noty.displayName} has left`; + break; + case 'MutedOnPlayerJoined': + text = `Muted user ${noty.displayName} has joined`; + break; + case 'MutedOnPlayerLeft': + text = `Muted user ${noty.displayName} has left`; + break; + default: + break; + } + if (text) { + new Noty({ + type: 'alert', + theme: this.config.notificationTheme, + timeout: this.config.notificationTimeout, + layout: this.config.notificationPosition, + text: `${img}
${text}
` + }).show(); } }; @@ -1033,7 +454,7 @@ speechSynthesis.getVoices(); $app.methods.displayLocation = function (location, worldName) { var text = ''; - var L = API.parseLocation(location); + var L = this.parseLocation(location); if (L.isOffline) { text = 'Offline'; } else if (L.isPrivate) { diff --git a/html/src/vr.pug b/html/src/vr.pug index 516220b2..64b7580a 100644 --- a/html/src/vr.pug +++ b/html/src/vr.pug @@ -36,17 +36,23 @@ html .detail span.extra span.time {{ feed.created_at | formatDate('HH:MI') }} - | #[span.name(v-text="feed.displayName")] #[i.x-user-status(:class="statusClass(feed.status)")] {{feed.statusDescription}} + | #[span.name(v-text="feed.displayName")] + template(v-if="feed.statusDescription === feed.previousStatusDescription") + i.x-user-status(:class="statusClass(feed.previousStatus)") + i.el-icon-right + i.x-user-status(:class="statusClass(feed.status)") + template(v-else) + | #[i.x-user-status(:class="statusClass(feed.status)")] {{feed.statusDescription}} div(v-else-if="feed.type === 'OnPlayerJoined'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }") .detail span.extra span.time {{ feed.created_at | formatDate('HH:MI') }} - | ▶️ #[span.name(v-text="feed.data")] + | ▶️ #[span.name(v-text="feed.displayName")] div(v-else-if="feed.type === 'OnPlayerLeft'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }") .detail span.extra span.time {{ feed.created_at | formatDate('HH:MI') }} - | ◀️ #[span.name(v-text="feed.data")] + | ◀️ #[span.name(v-text="feed.displayName")] div(v-else-if="feed.type === 'OnPlayerJoining'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }") .detail span.extra @@ -57,7 +63,16 @@ html .detail span.extra span.time {{ feed.created_at | formatDate('HH:MI') }} - location(:location="feed.data[0]" :hint="feed.data[1]") + location(:location="feed.location" :hint="feed.worldName") + div(v-else-if="feed.type === 'VideoPlay'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }") + .detail + span.extra + span.time {{ feed.created_at | formatDate('HH:MI') }} + | 🎵 #[span.name(v-text="feed.displayName")] + template(v-if="feed.videoName") + | #[span(v-text="feed.videoName")] + template(v-else) + | #[span(v-text="feed.videoUrl")] div(v-else-if="feed.type === 'invite'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }") .detail span.extra @@ -107,7 +122,16 @@ html .detail span.extra span.time {{ feed.created_at | formatDate('HH:MI') }} - | ✨ #[span.name(v-text="feed.data")] + | ✨ #[span.name(v-text="feed.displayName")] + template(v-if="feed.worldName") + | #[location(:location="feed.instanceId" :hint="feed.worldName")] + div(v-else-if="feed.type === 'AvatarChange'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }") + .detail + span.extra + span.time {{ feed.created_at | formatDate('HH:MI') }} + | 🧍 #[span.name(v-text="feed.displayName")] {{ feed.name }} + template(v-if="feed.description && feed.description !== feed.name") + | - {{ feed.description }} div(v-else-if="feed.type === 'Event'" class="x-friend-item") .detail span.extra @@ -159,17 +183,23 @@ html .detail span.extra span.time {{ feed.created_at | formatDate('HH:MI') }} - | #[span.name(v-text="feed.displayName")] is #[i.x-user-status(:class="statusClass(feed.status)")] {{feed.statusDescription}} + | #[span.name(v-text="feed.displayName")] + template(v-if="feed.statusDescription === feed.previousStatusDescription") + i.x-user-status(:class="statusClass(feed.previousStatus)") + i.el-icon-right + i.x-user-status(:class="statusClass(feed.status)") + template(v-else) + | #[i.x-user-status(:class="statusClass(feed.status)")] {{feed.statusDescription}} div(v-else-if="feed.type === 'OnPlayerJoined'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }") .detail span.extra span.time {{ feed.created_at | formatDate('HH:MI') }} - | #[span.name(v-text="feed.data")] has joined + | #[span.name(v-text="feed.displayName")] has joined div(v-else-if="feed.type === 'OnPlayerLeft'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }") .detail span.extra span.time {{ feed.created_at | formatDate('HH:MI') }} - | #[span.name(v-text="feed.data")] has left + | #[span.name(v-text="feed.displayName")] has left div(v-else-if="feed.type === 'OnPlayerJoining'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }") .detail span.extra @@ -179,7 +209,16 @@ html .detail span.extra span.time {{ feed.created_at | formatDate('HH:MI') }} - location(:location="feed.data[0]" :hint="feed.data[1]") + location(:location="feed.location" :hint="feed.worldName") + div(v-else-if="feed.type === 'VideoPlay'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }") + .detail + span.extra + span.time {{ feed.created_at | formatDate('HH:MI') }} + | #[span.name(v-text="feed.displayName")] changed video to + template(v-if="feed.videoName") + | #[span(v-text="feed.videoName")] + template(v-else) + | #[span(v-text="feed.videoUrl")] div(v-else-if="feed.type === 'invite'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }") .detail span.extra @@ -229,7 +268,16 @@ html .detail span.extra span.time {{ feed.created_at | formatDate('HH:MI') }} - | #[span.name(v-text="feed.data")] has spawned a portal + | #[span.name(v-text="feed.displayName")] has spawned a portal + template(v-if="feed.worldName") + | to #[location(:location="feed.instanceId" :hint="feed.worldName")] + div(v-else-if="feed.type === 'AvatarChange'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }") + .detail + span.extra + span.time {{ feed.created_at | formatDate('HH:MI') }} + | #[span.name(v-text="feed.displayName")] changed into avatar {{ feed.name }} + template(v-if="feed.description && feed.description !== feed.name") + | - {{ feed.description }} div(v-else-if="feed.type === 'Event'" class="x-friend-item") .detail span.extra @@ -302,19 +350,19 @@ html span {{ device[2] }}% .x-containerbottom template(v-if="config && config.minimalFeed") - template(v-if="downloadProgress === 100") + template(v-if="config.downloadProgress === 100") span(style="display:inline-block;margin-right:5px") #[i.el-icon-loading] - template(v-else-if="downloadProgress > 0") - span(style="display:inline-block;margin-right:5px") {{ downloadProgress }}% + template(v-else-if="config.downloadProgress > 0") + span(style="display:inline-block;margin-right:5px") {{ config.downloadProgress }}% template(v-if="lastLocation.date != 0") span(style="float:right") {{ lastLocationTimer }} span(style="display:inline-block") {{ lastLocation.playerList.length }} span(style="display:inline-block;font-weight:bold") {{ lastLocation.friendList.length !== 0 ? `‎‎‎‎‎‎‎‎‏‏‎ ‎(${lastLocation.friendList.length})` : ''}} template(v-else) - template(v-if="downloadProgress === 100") + template(v-if="config.downloadProgress === 100") span(style="display:inline-block;margin-right:5px") Downloading: #[i.el-icon-loading] - template(v-else-if="downloadProgress > 0") - span(style="display:inline-block;margin-right:5px") Downloading: {{ downloadProgress }}% + template(v-else-if="config.downloadProgress > 0") + span(style="display:inline-block;margin-right:5px") Downloading: {{ config.downloadProgress }}% template(v-if="lastLocation.date != 0") span(style="float:right") Timer: {{ lastLocationTimer }} span(style="display:inline-block") Players: {{ lastLocation.playerList.length }} diff --git a/html/src/vr.scss b/html/src/vr.scss index ad8fd3b2..f1b2495e 100644 --- a/html/src/vr.scss +++ b/html/src/vr.scss @@ -22,9 +22,6 @@ .noty_body { display: block; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; } .noty_layout { @@ -34,6 +31,7 @@ .noty_theme__relax.noty_bar, .noty_theme__sunset.noty_bar { + height: 42px; position: relative; margin: 4px 0; overflow: hidden; @@ -42,9 +40,7 @@ .noty_theme__relax.noty_bar .noty_body, .noty_theme__sunset.noty_bar .noty_body { - padding: 5px 10px 10px; font-size: 15px; - text-align: center; text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1); } @@ -143,6 +139,19 @@ opacity: 0.6; } +.noty-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding: 8px 8px 0 11px; +} + +.noty-img { + height: 42px; + float: left; + border-radius: 4px; +} + ::-webkit-scrollbar { width: 8px; height: 8px;