diff --git a/AppApi.cs b/AppApi.cs index e83390a2..c89cf5cb 100644 --- a/AppApi.cs +++ b/AppApi.cs @@ -19,6 +19,8 @@ using Windows.UI.Notifications; using Windows.Data.Xml.Dom; using librsync.net; using System.Net.Sockets; +using System.Text; +using System.Collections.Generic; namespace VRCX { @@ -433,6 +435,23 @@ namespace VRCX } } + private static readonly MD5 _hasher = MD5.Create(); + public int GetColourFromUserID(string userId) + { + var hash = _hasher.ComputeHash(Encoding.UTF8.GetBytes(userId)); + return hash[3] << 8 | hash[4]; + } + + public Dictionary GetColourBulk(List userIds) + { + Dictionary output = new Dictionary(); + foreach (string userId in userIds) + { + output.Add(userId, GetColourFromUserID(userId)); + } + return output; + } + public void SetStartup(bool enabled) { try diff --git a/html/src/app.js b/html/src/app.js index ac06b06b..ea40be0c 100644 --- a/html/src/app.js +++ b/html/src/app.js @@ -1171,6 +1171,7 @@ speechSynthesis.getVoices(); API.applyUserTrustLevel = function (ref) { ref.$isModerator = ref.developerType && ref.developerType !== 'none'; ref.$isTroll = false; + var trustColor = ''; var {tags} = ref; if (tags.includes('admin_moderator')) { ref.$isModerator = true; @@ -1184,33 +1185,46 @@ speechSynthesis.getVoices(); if (tags.includes('system_trust_veteran')) { ref.$trustLevel = 'Trusted User'; ref.$trustClass = 'x-tag-veteran'; + trustColor = 'veteran'; ref.$trustSortNum = 5; } else if (tags.includes('system_trust_trusted')) { ref.$trustLevel = 'Known User'; ref.$trustClass = 'x-tag-trusted'; + trustColor = 'trusted'; ref.$trustSortNum = 4; } else if (tags.includes('system_trust_known')) { ref.$trustLevel = 'User'; ref.$trustClass = 'x-tag-known'; + trustColor = 'known'; ref.$trustSortNum = 3; } else if (tags.includes('system_trust_basic')) { ref.$trustLevel = 'New User'; ref.$trustClass = 'x-tag-basic'; + trustColor = 'basic'; ref.$trustSortNum = 2; } else { ref.$trustLevel = 'Visitor'; ref.$trustClass = 'x-tag-untrusted'; + trustColor = 'untrusted'; ref.$trustSortNum = 1; } - ref.$trustColor = ref.$trustClass; if (ref.$isTroll) { - ref.$trustColor = 'x-tag-troll'; + trustColor = 'troll'; ref.$trustSortNum += 0.1; } if (ref.$isModerator) { - ref.$trustColor = 'x-tag-vip'; + trustColor = 'vip'; ref.$trustSortNum += 0.3; } + if ($app.randomUserColours && $app.friendLogInitStatus) { + if (!ref.$userColour) { + $app.getNameColour(ref.id).then((colour) => { + ref.$userColour = colour; + }); + } + } else { + ref.$userColour = $app.trustColor[trustColor]; + } }; // FIXME: it may performance issue. review here @@ -1278,7 +1292,7 @@ speechSynthesis.getVoices(); $isTroll: false, $trustLevel: 'Visitor', $trustClass: 'x-tag-untrusted', - $trustColor: 'x-tag-untrusted', + $userColour: '', $trustSortNum: 1, $languages: [], // @@ -1408,7 +1422,7 @@ speechSynthesis.getVoices(); $isTroll: false, $trustLevel: 'Visitor', $trustClass: 'x-tag-untrusted', - $trustColor: 'x-tag-untrusted', + $userColour: '', $trustSortNum: 1, $languages: [], $joinCount: 0, @@ -7376,6 +7390,12 @@ speechSynthesis.getVoices(); } else { await $app.initFriendLog(args.json.id); } + if ($app.randomUserColours) { + $app.getNameColour(this.currentUser.id).then((colour) => { + this.currentUser.$userColour = colour; + }); + $app.userColourInit(); + } this.getAuth(); $app.updateSharedFeed(true); if ($app.isGameRunning) { @@ -11432,6 +11452,7 @@ speechSynthesis.getVoices(); 'VRCX_avatarRemoteDatabaseProvider' ); $app.data.sortFavorites = configRepository.getBool('VRCX_sortFavorites'); + $app.data.randomUserColours = configRepository.getBool('VRCX_randomUserColours'); $app.methods.saveOpenVROption = function () { configRepository.setBool('openVR', this.openVR); configRepository.setBool('openVRAlways', this.openVRAlways); @@ -11483,6 +11504,7 @@ speechSynthesis.getVoices(); this.avatarRemoteDatabase ); configRepository.setBool('VRCX_sortFavorites', this.sortFavorites); + configRepository.setBool('VRCX_randomUserColours', this.randomUserColours); this.updateSharedFeed(true); this.updateVRConfigVars(); this.updateVRLastLocation(); @@ -11523,6 +11545,7 @@ speechSynthesis.getVoices(); AppApi.ChangeTheme(0); } this.updateVRConfigVars(); + this.updatetrustColor(); }; if ($app.data.isDarkMode) { AppApi.ChangeTheme(1); @@ -11818,18 +11841,31 @@ speechSynthesis.getVoices(); ); $app.methods.updatetrustColor = function () { - var trustColor = $app.trustColor; - if (trustColor) { + configRepository.setBool('VRCX_randomUserColours', this.randomUserColours); + if (this.trustColor) { configRepository.setString( 'VRCX_trustColor', - JSON.stringify(trustColor) + JSON.stringify(this.trustColor) ); - } else { - trustColor = JSON.parse( - configRepository.getString('VRCX_trustColor') - ); - $app.trustColor = trustColor; } + if (this.randomUserColours) { + this.getNameColour(API.currentUser.id).then((colour) => { + API.currentUser.$userColour = colour; + }); + this.userColourInit(); + } else { + API.applyUserTrustLevel(API.currentUser); + API.cachedUsers.forEach((ref) => { + API.applyUserTrustLevel(ref); + }); + } + this.updatetrustColorClasses(); + }; + + $app.methods.updatetrustColorClasses = function () { + var trustColor = JSON.parse( + configRepository.getString('VRCX_trustColor') + ); if (document.getElementById('trustColor') !== null) { document.getElementById('trustColor').outerHTML = ''; } @@ -11843,7 +11879,7 @@ speechSynthesis.getVoices(); style.innerHTML = newCSS; document.getElementsByTagName('head')[0].appendChild(style); }; - $app.methods.updatetrustColor(); + $app.methods.updatetrustColorClasses(); $app.methods.saveSharedFeedFilters = function () { this.notyFeedFiltersDialog.visible = false; @@ -19153,6 +19189,54 @@ speechSynthesis.getVoices(); } }; + $app.methods.getNameColour = async function (userId) { + var hue = await AppApi.GetColourFromUserID(userId); + return this.HueToHex(hue); + }; + + $app.methods.userColourInit = async function () { + var dictObject = await AppApi.GetColourBulk(Array.from(API.cachedUsers.keys())); + for (var [userId, hue] of Object.entries(dictObject)) { + var ref = API.cachedUsers.get(userId); + if (typeof ref !== 'undefined') { + ref.$userColour = this.HueToHex(hue); + } + } + }; + + $app.methods.HueToHex = function (hue) { + // this.HSVtoRGB(hue / 65535, .8, .8); + if (this.isDarkMode) { + return this.HSVtoRGB(hue / 65535, .6, 1); + } + return this.HSVtoRGB(hue / 65535, 1, .7); + }; + + $app.methods.HSVtoRGB = function (h, s, v) { + var r, g, b, i, f, p, q, t; + if (arguments.length === 1) { + s = h.s, v = h.v, h = h.h; + } + i = Math.floor(h * 6); + f = h * 6 - i; + p = v * (1 - s); + q = v * (1 - f * s); + t = v * (1 - (1 - f) * s); + switch (i % 6) { + case 0: r = v, g = t, b = p; break; + case 1: r = q, g = v, b = p; break; + case 2: r = p, g = v, b = t; break; + case 3: r = p, g = q, b = v; break; + case 4: r = t, g = p, b = v; break; + case 5: r = v, g = p, b = q; break; + } + var red = Math.round(r * 255); + var green = Math.round(g * 255); + var blue = Math.round(b * 255); + var decColor = 0x1000000 + blue + 0x100 * green + 0x10000 * red ; + return '#'+decColor.toString(16).substr(1); + }; + $app = new Vue($app); window.$app = $app; })(); diff --git a/html/src/index.pug b/html/src/index.pug index 238d9e6d..c892b16d 100644 --- a/html/src/index.pug +++ b/html/src/index.pug @@ -165,13 +165,17 @@ html span(v-else) D el-table-column(label="Display Name" min-width="140" prop="ref.displayName") template(v-once #default="scope") - span(v-text="scope.row.ref.displayName" :class="scope.row.ref.$trustColor") + span(v-if="randomUserColours" v-text="scope.row.ref.displayName" :style="{'color':scope.row.ref.$userColour}") + span(v-else v-text="scope.row.ref.displayName") el-table-column(label="Status" min-width="180" prop="ref.status") template(v-once #default="scope") template(v-if="scope.row.ref.status") i.x-user-status(:class="statusClass(scope.row.ref.status)") span ‎ span(v-text="scope.row.ref.statusDescription") + el-table-column(label="Rank" width="110" prop="$trustSortNum" sortable="custom") + template(v-once #default="scope") + span.name(v-text="scope.row.ref.$trustLevel" :class="scope.row.ref.$trustClass") el-table-column(label="Language" width="100" prop="ref.$languages") template(v-once #default="scope") el-tooltip(v-for="item in scope.row.ref.$languages" :key="item.key" placement="top") @@ -498,7 +502,7 @@ html .avatar(:class="userStatusClass(favorite.ref)") img(v-lazy="userImage(favorite.ref)") .detail - span.name(v-text="favorite.ref.displayName" :class="favorite.ref.$trustColor") + span.name(v-text="favorite.ref.displayName" :style="{'color':favorite.ref.$userColour}") location.extra(v-if="favorite.ref.location !== 'offline'" :location="favorite.ref.location" :link="false") span(v-else v-text="favorite.ref.statusDescription") el-tooltip(placement="left" content="Move" :disabled="hideTooltips") @@ -865,10 +869,13 @@ html img.friends-list-avatar(slot="reference" v-lazy="userImage(scope.row)") img.friends-list-avatar(v-lazy="userImageFull(scope.row)" style="height:500px;cursor:pointer" @click="openExternalLink(userImageFull(scope.row))") el-table-column(label="Display Name" min-width="130" prop="displayName" sortable :sort-method="(a, b) => sortAlphabetically(a, b, 'displayName')") + template(v-once #default="scope") + span.name(v-if="randomUserColours" v-text="scope.row.displayName" :style="{'color':scope.row.$userColour}") + span.name(v-else v-text="scope.row.displayName") el-table-column(label="User Name" min-width="120" prop="username" sortable :sort-method="(a, b) => sortAlphabetically(a, b, 'username')") el-table-column(label="Rank" width="110" prop="$trustSortNum" sortable="custom") template(v-once #default="scope") - span.name(v-text="scope.row.$trustLevel" :class="scope.row.$trustColor") + span.name(v-text="scope.row.$trustLevel" :class="scope.row.$trustClass") el-table-column(label="Status" min-width="180" prop="status" sortable :sort-method="(a, b) => sortStatus(a.status, b.status)") template(v-once #default="scope") i.x-user-status(v-if="scope.row.status !== 'offline'" :class="statusClass(scope.row.status)") @@ -1006,6 +1013,9 @@ html el-slider(v-model="asideWidth" @input="setAsideWidth" :show-tooltip="false" :marks="{236: ''}" :min="141" :max="500" style="width:300px") div.options-container span.header User Colors + div.options-container-item + span.name Random colours from user ID + el-switch(v-model="randomUserColours" @change="updatetrustColor") div.options-container-item div el-color-picker(v-model="trustColor.untrusted" @change="updatetrustColor" size="mini" :predefine="['#CCCCCC']") @@ -1280,7 +1290,7 @@ html .x-friend-item template(v-if="item.ref") .detail - span.name(v-text="item.ref.displayName" :class="item.ref.$trustColor") + span.name(v-text="item.ref.displayName" :style="{'color':item.ref.$userColour}") location.extra(:location="item.ref.location" :link="false") img.avatar(v-lazy="userImage(item.ref)") span(v-else) Search More: #[span(v-text="item.label" style="font-weight:bold")] @@ -1297,7 +1307,7 @@ html .avatar(:class="userStatusClass(API.currentUser)") img(v-lazy="userImage(API.currentUser)") .detail - span.name(v-text="API.currentUser.displayName" :class="API.currentUser.$trustColor") + span.name(v-text="API.currentUser.displayName" :style="{'color':API.currentUser.$userColour}") location.extra(v-if="isGameRunning === true" :location="lastLocation.location" :link="false") span.extra(v-else v-text="API.currentUser.statusDescription" :link="false") .x-friend-group(v-show="friendsGroup0.length") @@ -1309,8 +1319,8 @@ html .avatar(:class="userStatusClass(friend.ref)") img(v-lazy="userImage(friend.ref)") .detail - span.name(v-if="friend.$nickName" :class="friend.ref.$trustColor") {{ friend.ref.displayName }} ({{ friend.$nickName }}) - span.name(v-else v-text="friend.ref.displayName" :class="friend.ref.$trustColor") + span.name(v-if="friend.$nickName" :style="{'color':friend.ref.$userColour}") {{ friend.ref.displayName }} ({{ friend.$nickName }}) + span.name(v-else v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") location.extra(:location="friend.ref.location" :link="false") template(v-else) span(v-text="friend.name || friend.id") @@ -1324,8 +1334,8 @@ html .avatar(:class="userStatusClass(friend.ref)") img(v-lazy="userImage(friend.ref)") .detail - span.name(v-if="friend.$nickName" :class="friend.ref.$trustColor") {{ friend.ref.displayName }} ({{ friend.$nickName }}) - span.name(v-else v-text="friend.ref.displayName" :class="friend.ref.$trustColor") + span.name(v-if="friend.$nickName" :style="{'color':friend.ref.$userColour}") {{ friend.ref.displayName }} ({{ friend.$nickName }}) + span.name(v-else v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") location.extra(:location="friend.ref.location" :link="false") template(v-else) span(v-text="friend.name || friend.id") @@ -1339,8 +1349,8 @@ html .avatar img(v-lazy="userImage(friend.ref)") .detail - span.name(v-if="friend.$nickName" :class="friend.ref.$trustColor") {{ friend.ref.displayName }} ({{ friend.$nickName }}) - span.name(v-else v-text="friend.ref.displayName" :class="friend.ref.$trustColor") + span.name(v-if="friend.$nickName" :style="{'color':friend.ref.$userColour}") {{ friend.ref.displayName }} ({{ friend.$nickName }}) + span.name(v-else v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") span.extra(v-text="friend.ref.statusDescription" :link="false") template(v-else) span(v-text="friend.name || friend.id") @@ -1354,8 +1364,8 @@ html .avatar img(v-lazy="userImage(friend.ref)") .detail - span.name(v-if="friend.$nickName" :class="friend.ref.$trustColor") {{ friend.ref.displayName }} ({{ friend.$nickName }}) - span.name(v-else v-text="friend.ref.displayName" :class="friend.ref.$trustColor") + span.name(v-if="friend.$nickName" :style="{'color':friend.ref.$userColour}") {{ friend.ref.displayName }} ({{ friend.$nickName }}) + span.name(v-else v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") span.extra(v-text="friend.ref.statusDescription") template(v-else) span(v-text="friend.name || friend.id") @@ -1479,14 +1489,14 @@ html .avatar(:class="userStatusClass(userDialog.$location.user)") img(v-lazy="userImage(userDialog.$location.user)") .detail - span.name(v-text="userDialog.$location.user.displayName" :class="userDialog.$location.user.$trustColor") + span.name(v-text="userDialog.$location.user.displayName" :style="{'color':userDialog.$location.user.$userColour}") span.extra Instance Creator span(v-else v-text="userDialog.$location.userId") .x-friend-item(v-for="user in userDialog.users" :key="user.id" @click="showUserDialog(user.id)" class="x-friend-item-border") .avatar(:class="userStatusClass(user)") img(v-lazy="userImage(user)") .detail - span.name(v-text="user.displayName" :class="user.$trustColor") + span.name(v-text="user.displayName" :style="{'color':user.$userColour}") span.extra timer(:epoch="user.$location_at") .x-friend-list(style="max-height:none") @@ -1699,14 +1709,14 @@ html .avatar(:class="userStatusClass(room.$location.user)") img(v-lazy="userImage(room.$location.user)") .detail - span.name(v-text="room.$location.user.displayName" :class="room.$location.user.$trustColor") + span.name(v-text="room.$location.user.displayName" :style="{'color':room.$location.user.$userColour}") span.extra Instance Creator span(v-else v-text="room.$location.userId") .x-friend-item(v-for="user in room.users" :key="user.id" @click="showUserDialog(user.id)" class="x-friend-item-border") .avatar(:class="userStatusClass(user)") img(v-lazy="userImage(user)") .detail - span.name(v-text="user.displayName" :class="user.$trustColor") + span.name(v-text="user.displayName" :style="{'color':user.$userColour}") span.extra timer(:epoch="user.$location_at") el-tab-pane(label="Info") @@ -1886,7 +1896,7 @@ html .avatar(:class="userStatusClass(friend.ref)") img(v-lazy="userImage(friend.ref)") .detail - span.name(v-text="friend.ref.displayName" :class="friend.ref.$trustColor") + span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") span(v-else v-text="friend.id") el-option-group(v-if="friendsGroup1.length" label="ONLINE") el-option.x-friend-item(v-for="friend in friendsGroup1" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") @@ -1894,7 +1904,7 @@ html .avatar(:class="userStatusClass(friend.ref)") img(v-lazy="userImage(friend.ref)") .detail - span.name(v-text="friend.ref.displayName" :class="friend.ref.$trustColor") + span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") span(v-else v-text="friend.id") el-option-group(v-if="friendsGroup2.length" label="ACTIVE") el-option.x-friend-item(v-for="friend in friendsGroup2" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") @@ -1902,7 +1912,7 @@ html .avatar img(v-lazy="userImage(friend.ref)") .detail - span.name(v-text="friend.ref.displayName" :class="friend.ref.$trustColor") + span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") span(v-else v-text="friend.id") template(#footer) el-button(size="small" :disabled="inviteDialog.loading || !inviteDialog.userIds.length" @click="showSendInviteDialog()") Invite With Message @@ -1997,7 +2007,7 @@ html .avatar(:class="userStatusClass(friend.ref)") img(v-lazy="userImage(friend.ref)") .detail - span.name(v-text="friend.ref.displayName" :class="friend.ref.$trustColor") + span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") span(v-else v-text="friend.id") el-option-group(v-if="friendsGroup1.length" label="ONLINE") el-option.x-friend-item(v-for="friend in friendsGroup1" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") @@ -2005,7 +2015,7 @@ html .avatar(:class="userStatusClass(friend.ref)") img(v-lazy="userImage(friend.ref)") .detail - span.name(v-text="friend.ref.displayName" :class="friend.ref.$trustColor") + span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") span(v-else v-text="friend.id") el-option-group(v-if="friendsGroup2.length" label="ACTIVE") el-option.x-friend-item(v-for="friend in friendsGroup2" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") @@ -2013,7 +2023,7 @@ html .avatar img(v-lazy="userImage(friend.ref)") .detail - span.name(v-text="friend.ref.displayName" :class="friend.ref.$trustColor") + span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") span(v-else v-text="friend.id") el-option-group(v-if="friendsGroup2.length" label="OFFLINE") el-option.x-friend-item(v-for="friend in friendsGroup3" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") @@ -2021,7 +2031,7 @@ html .avatar img(v-lazy="userImage(friend.ref)") .detail - span.name(v-text="friend.ref.displayName" :class="friend.ref.$trustColor") + span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") span(v-else v-text="friend.id") el-form-item(label="Location") el-input(v-model="newInstanceDialog.location" size="mini" readonly @click.native="$event.target.tagName === 'INPUT' && $event.target.select()")