diff --git a/src/app.js b/src/app.js index 81c08663..eebffed6 100644 --- a/src/app.js +++ b/src/app.js @@ -23140,22 +23140,48 @@ console.log(`isLinux: ${LINUX}`); 'VRCX_sidebarGroupByInstance', true ); - $app.computed.onlineFriendsInSameInstance = function () { - const groupedItems = {}; - this.onlineFriends.forEach((item) => { - const key = item.ref?.$location.tag; + $app.methods.handleSwitchGroupByInstance = function () { + this.isSidebarGroupByInstance = !this.isSidebarGroupByInstance; + configRepository.setBool( + 'VRCX_sidebarGroupByInstance', + this.isSidebarGroupByInstance + ); + }; + + $app.data.isSidebarGroupByInstanceCollapsed = + await configRepository.getBool( + 'VRCX_sidebarGroupByInstanceCollapsed', + false + ); + + $app.methods.toggleSwitchGroupByInstanceCollapsed = function () { + this.isSidebarGroupByInstanceCollapsed = + !this.isSidebarGroupByInstanceCollapsed; + configRepository.setBool( + 'VRCX_sidebarGroupByInstanceCollapsed', + this.isSidebarGroupByInstanceCollapsed + ); + }; + + $app.computed.friendsInSameInstance = function () { + const friendsList = {}; + + const allFriends = [...this.vipFriends, ...this.onlineFriends]; + + allFriends.forEach((friend) => { + const key = friend.ref?.$location.tag; if (!key || key === 'private' || key === 'offline') return; - if (!groupedItems[key]) { - groupedItems[key] = []; + if (!friendsList[key]) { + friendsList[key] = []; } - groupedItems[key].push(item); + friendsList[key].push(friend); }); - const sortedGroups = []; - for (const group of Object.values(groupedItems)) { + const sortedFriendsList = []; + for (const group of Object.values(friendsList)) { if (group.length > 1) { - sortedGroups.push( + sortedFriendsList.push( group.sort( (a, b) => a.ref?.$location_at - b.ref?.$location_at ) @@ -23163,55 +23189,39 @@ console.log(`isLinux: ${LINUX}`); } } - return sortedGroups.sort((a, b) => b.length - a.length); + return sortedFriendsList.sort((a, b) => b.length - a.length); }; - $app.computed.onlineFriendsNotInSameInstance = function () { - const friendsInSameInstance = new Set( - this.onlineFriendsInSameInstance.flat().map((friend) => friend.id) + $app.computed.onlineFriendsByGroupStatus = function () { + const sameInstanceTag = new Set( + this.friendsInSameInstance.flatMap((item) => + item.map((friend) => friend.ref?.$location.tag) + ) ); return this.onlineFriends.filter( - (friend) => !friendsInSameInstance.has(friend.id) + (item) => !sameInstanceTag.has(item.ref?.$location.tag) ); }; - $app.computed.vipFriendsInSameInstance = function () { - const groupedItems = {}; - - this.vipFriends.forEach((item) => { - const key = item.ref?.$location.tag; - if (!key || key === 'private' || key === 'offline') return; - if (!groupedItems[key]) { - groupedItems[key] = []; - } - groupedItems[key].push(item); - }); - - const sortedGroups = []; - for (const group of Object.values(groupedItems)) { - if (group.length > 1) { - sortedGroups.push( - group.sort( - (a, b) => a.ref?.$location_at - b.ref?.$location_at - ) - ); - } - } - - return sortedGroups.sort((a, b) => b.length - a.length); - }; - - $app.computed.vipFriendsNotInSameInstance = function () { - const friendsInSameInstance = new Set( - this.vipFriendsInSameInstance.flat().map((friend) => friend.id) + $app.computed.vipFriendsByGroupStatus = function () { + const sameInstanceTag = new Set( + this.friendsInSameInstance.flatMap((item) => + item.map((friend) => friend.ref?.$location.tag) + ) ); return this.vipFriends.filter( - (friend) => !friendsInSameInstance.has(friend.id) + (item) => !sameInstanceTag.has(item.ref?.$location.tag) ); }; + $app.methods.getFriendsLocations = function (friendsArr) { + // prevent the instance title display as "Traveling". + return friendsArr.find((friend) => !friend.ref?.$location?.isTraveling) + ?.ref?.location; + }; + // #endregion // #region | Electron diff --git a/src/localization/en/en.json b/src/localization/en/en.json index 5fc24fec..1487d6b9 100644 --- a/src/localization/en/en.json +++ b/src/localization/en/en.json @@ -633,6 +633,7 @@ "friends": "Friends", "me": "ME", "favorite": "FAVORITES", + "same_instance": "Same Instance", "online": "ONLINE", "active": "ACTIVE", "offline": "OFFLINE", diff --git a/src/localization/zh-CN/en.json b/src/localization/zh-CN/en.json index a150a270..7b1864b5 100644 --- a/src/localization/zh-CN/en.json +++ b/src/localization/zh-CN/en.json @@ -633,6 +633,7 @@ "friends": "好友", "me": "我", "favorite": "星标好友", + "same_instance": "同一房间", "online": "在线", "active": "活跃中(仅登录网页端)", "offline": "离线", @@ -2014,4 +2015,4 @@ } } } -} +} \ No newline at end of file diff --git a/src/mixins/friendsListSidebar.pug b/src/mixins/friendsListSidebar.pug index 2c801381..d380dc7d 100644 --- a/src/mixins/friendsListSidebar.pug +++ b/src/mixins/friendsListSidebar.pug @@ -70,34 +70,72 @@ mixin friendsListSidebar span.extra(v-else v-text='API.currentUser.statusDescription') .x-friend-group.x-link( @click='isVIPFriends = !isVIPFriends; saveFriendsGroupStates()' - v-show='vipFriends.length') + v-show='vipFriendsByGroupStatus.length') i.el-icon-arrow-right(:class='{ rotate: isVIPFriends }') - span(style='margin-left: 5px') {{ $t('side_panel.favorite') }} ― {{ vipFriends.length }} + span(style='margin-left: 5px') {{ $t('side_panel.favorite') }} ― {{ vipFriendsByGroupStatus.length }} div(v-show='isVIPFriends') - template(v-if='isSidebarGroupByInstance') - div( - v-for='(friendArr, idx) in vipFriendsInSameInstance' - :key='friendArr[0].ref?.location.tag') + .x-friend-item( + v-for='friend in vipFriendsByGroupStatus' + :key='friend.id' + @click='showUserDialog(friend.id)') + template(v-if='friend.ref') + .avatar(:class='userStatusClass(friend.ref, friend.pendingOffline)') + img(v-lazy='userImage(friend.ref)') + .detail + span.name( + v-if='!hideNicknames && 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-if='friend.pendingOffline') #[i.el-icon-warning-outline] {{ $t('side_panel.pending_offline') }} + location.extra( + v-else + :location='friend.ref.location' + :traveling='friend.ref.travelingToLocation' + :link='false') + template(v-else) + span(v-text='friend.name || friend.id') + el-button( + type='text' + icon='el-icon-close' + size='mini' + @click.stop='confirmDeleteFriend(friend.id)' + style='margin-left: 5px') + + //- Group By Instance + template(v-if='isSidebarGroupByInstance && friendsInSameInstance.length') + .x-friend-group.x-link(@click='toggleSwitchGroupByInstanceCollapsed') + i.el-icon-arrow-right(:class='{ rotate: isSidebarGroupByInstanceCollapsed }') + span(style='margin-left: 5px') {{ $t('side_panel.same_instance') }} ― {{ friendsInSameInstance.length }} + div(v-show='!isSidebarGroupByInstanceCollapsed') + div(v-for='friendArr in friendsInSameInstance' :key='friendArr[0].ref?.location.tag') div(style='margin-bottom: 3px') - location.extra(:location='friendArr[0].ref?.location' style='color: #c7c7c7') + location.extra(:location='getFriendsLocations(friendArr)' style='color: #c7c7c7') span(style='margin-left: 5px') {{ `(${friendArr.length})` }} - div(style='margin-bottom: 10px') + div .x-friend-item( v-if='friendArr && friendArr.length' - v-for='friend in friendArr' + v-for='(friend, idx) in friendArr' :key='friend.id' - @click='showUserDialog(friend.id)') + @click='showUserDialog(friend.id)' + :style='{ "margin-bottom": idx === friendArr.length - 1 ? "5px" : undefined }') template(v-if='friend.ref') .avatar(:class='userStatusClass(friend.ref, friend.pendingOffline)') img(v-lazy='userImage(friend.ref)') .detail - span.name( - v-if='!hideNicknames && 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 }') + div(style='display: flex; align-items: center') + span.name( + v-if='!hideNicknames && 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 }') + i.el-icon-star-on( + v-if='friend.isVIP' + style='color: #ffd700; margin: 1px 0 0 2px') span.extra(v-if='friend.pendingOffline') #[i.el-icon-warning-outline] {{ $t('side_panel.pending_offline') }} template(v-else) i.el-icon.el-icon-loading( @@ -112,170 +150,42 @@ mixin friendsListSidebar size='mini' @click.stop='confirmDeleteFriend(friend.id)' style='margin-left: 5px') - div(v-if='idx === vipFriendsInSameInstance.length - 1' style='color: #c7c7c7') {{ 'Others' }} - .x-friend-item( - v-for='friend in vipFriendsNotInSameInstance' - :key='friend.id' - @click='showUserDialog(friend.id)') - template(v-if='friend.ref') - .avatar(:class='userStatusClass(friend.ref, friend.pendingOffline)') - img(v-lazy='userImage(friend.ref)') - .detail - span.name( - v-if='!hideNicknames && 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-if='friend.pendingOffline') #[i.el-icon-warning-outline] {{ $t('side_panel.pending_offline') }} - location.extra( - v-else - :location='friend.ref.location' - :traveling='friend.ref.travelingToLocation' - :link='false') - template(v-else) - span(v-text='friend.name || friend.id') - el-button( - type='text' - icon='el-icon-close' - size='mini' - @click.stop='confirmDeleteFriend(friend.id)' - style='margin-left: 5px') - template(v-else) - .x-friend-item( - v-for='friend in vipFriends' - :key='friend.id' - @click='showUserDialog(friend.id)') - template(v-if='friend.ref') - .avatar(:class='userStatusClass(friend.ref, friend.pendingOffline)') - img(v-lazy='userImage(friend.ref)') - .detail - span.name( - v-if='!hideNicknames && 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-if='friend.pendingOffline') #[i.el-icon-warning-outline] {{ $t('side_panel.pending_offline') }} - location.extra( - v-else - :location='friend.ref.location' - :traveling='friend.ref.travelingToLocation' - :link='false') - template(v-else) - span(v-text='friend.name || friend.id') - el-button( - type='text' - icon='el-icon-close' - size='mini' - @click.stop='confirmDeleteFriend(friend.id)' - style='margin-left: 5px') + .x-friend-group.x-link( @click='isOnlineFriends = !isOnlineFriends; saveFriendsGroupStates()' - v-show='onlineFriends.length') + v-show='onlineFriendsByGroupStatus.length') i.el-icon-arrow-right(:class='{ rotate: isOnlineFriends }') - span(style='margin-left: 5px') {{ $t('side_panel.online') }} ― {{ onlineFriends.length }} + span(style='margin-left: 5px') {{ $t('side_panel.online') }} ― {{ onlineFriendsByGroupStatus.length }} div(v-show='isOnlineFriends') - template(v-if='isSidebarGroupByInstance') - div( - v-for='(friendArr, idx) in onlineFriendsInSameInstance' - :key='friendArr[0].ref?.location.tag') - div(style='margin-bottom: 3px') - location.extra(:location='friendArr[0].ref?.location' style='color: #c7c7c7') - span(style='margin-left: 5px') {{ `(${friendArr.length})` }} - div(style='margin-bottom: 10px') - .x-friend-item( - v-if='friendArr && friendArr.length' - v-for='friend in friendArr' - :key='friend.id' - @click='showUserDialog(friend.id)') - template(v-if='friend.ref') - .avatar(:class='userStatusClass(friend.ref, friend.pendingOffline)') - img(v-lazy='userImage(friend.ref)') - .detail - span.name( - v-if='!hideNicknames && 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-if='friend.pendingOffline') #[i.el-icon-warning-outline] {{ $t('side_panel.pending_offline') }} - template(v-else) - i.el-icon.el-icon-loading( - v-if='friend.ref.travelingToLocation' - style='display: inline-block; margin-right: 5px') - timer(:epoch='friend.ref?.$location_at' style='color: #c7c7c7') - template(v-else) - span(v-text='friend.name || friend.id') - el-button( - type='text' - icon='el-icon-close' - size='mini' - @click.stop='confirmDeleteFriend(friend.id)' - style='margin-left: 5px') - div(v-if='idx === onlineFriendsInSameInstance.length - 1' style='color: #c7c7c7') {{ 'Others' }} - .x-friend-item( - v-for='friend in onlineFriendsNotInSameInstance' - :key='friend.id' - @click='showUserDialog(friend.id)') - template(v-if='friend.ref') - .avatar(:class='userStatusClass(friend.ref, friend.pendingOffline)') - img(v-lazy='userImage(friend.ref)') - .detail - span.name( - v-if='!hideNicknames && 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-if='friend.pendingOffline') #[i.el-icon-warning-outline] {{ $t('side_panel.pending_offline') }} - location.extra( - v-else - :location='friend.ref.location' - :traveling='friend.ref.travelingToLocation' - :link='false') - template(v-else) - span(v-text='friend.name || friend.id') - el-button( - type='text' - icon='el-icon-close' - size='mini' - @click.stop='confirmDeleteFriend(friend.id)' - style='margin-left: 5px') - template(v-else) - .x-friend-item( - v-for='friend in onlineFriends' - :key='friend.id' - @click='showUserDialog(friend.id)') - template(v-if='friend.ref') - .avatar(:class='userStatusClass(friend.ref, friend.pendingOffline)') - img(v-lazy='userImage(friend.ref)') - .detail - span.name( - v-if='!hideNicknames && 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-if='friend.pendingOffline') #[i.el-icon-warning-outline] {{ $t('side_panel.pending_offline') }} - location.extra( - v-else - :location='friend.ref.location' - :traveling='friend.ref.travelingToLocation' - :link='false') - template(v-else) - span(v-text='friend.name || friend.id') - el-button( - type='text' - icon='el-icon-close' - size='mini' - @click.stop='confirmDeleteFriend(friend.id)' - style='margin-left: 5px') + .x-friend-item( + v-for='friend in onlineFriendsByGroupStatus' + :key='friend.id' + @click='showUserDialog(friend.id)') + template(v-if='friend.ref') + .avatar(:class='userStatusClass(friend.ref, friend.pendingOffline)') + img(v-lazy='userImage(friend.ref)') + .detail + span.name( + v-if='!hideNicknames && 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-if='friend.pendingOffline') #[i.el-icon-warning-outline] {{ $t('side_panel.pending_offline') }} + location.extra( + v-else + :location='friend.ref.location' + :traveling='friend.ref.travelingToLocation' + :link='false') + template(v-else) + span(v-text='friend.name || friend.id') + el-button( + type='text' + icon='el-icon-close' + size='mini' + @click.stop='confirmDeleteFriend(friend.id)' + style='margin-left: 5px') .x-friend-group.x-link( @click='isActiveFriends = !isActiveFriends; saveFriendsGroupStates()' v-show='activeFriends.length')