From fbf4c7cd4bc02d2363d7879e2df3398980b2cfaa Mon Sep 17 00:00:00 2001 From: pa Date: Wed, 22 Jan 2025 01:43:58 +0900 Subject: [PATCH] feat: Friends Sidebar Group By Instance (#1069) (#1087) * feat: Friends Sidebar Group By Instance * fix and improve performance of filter data * rm log --- src/app.js | 89 +++++++++++++++++++++++ src/localization/en/en.json | 6 +- src/mixins/friendsListSidebar.pug | 116 +++++++++++++++++++++++------- src/mixins/tabs/settings.pug | 2 + 4 files changed, 187 insertions(+), 26 deletions(-) diff --git a/src/app.js b/src/app.js index 1d00754f..ef9392bb 100644 --- a/src/app.js +++ b/src/app.js @@ -185,6 +185,10 @@ console.log(`isLinux: ${LINUX}`); this.checkForVRCXUpdate(); } await AppApi.CheckGameRunning(); + this.isSidebarGroupByInstance = + (await configRepository.getBool( + 'VRCX_sidebarGroupByInstance' + )) ?? true; this.isGameNoVR = await configRepository.getBool('isGameNoVR'); await AppApi.SetAppLauncherSettings( this.enableAppLauncher, @@ -23108,6 +23112,91 @@ console.log(`isLinux: ${LINUX}`); return LINUX; }; + // friendsListSiderBar + $app.methods.handleSwitchGroupByInstance = async function () { + this.isSidebarGroupByInstance = !this.isSidebarGroupByInstance; + await configRepository.setBool( + 'VRCX_sidebarGroupByInstance', + this.isSidebarGroupByInstance + ); + }; + + // friendsListSidebar + // - SidebarGroupByInstance + + $app.data.isSidebarGroupByInstance = true; + $app.computed.onlineFriendsInSameInstance = function () { + const groupedItems = {}; + + this.onlineFriends.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.onlineFriendsNotInSameInstance = function () { + const friendsInSameInstance = new Set( + this.onlineFriendsInSameInstance.flat().map((friend) => friend.id) + ); + + return this.onlineFriends.filter( + (friend) => !friendsInSameInstance.has(friend.id) + ); + }; + + $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) + ); + + return this.vipFriends.filter( + (friend) => !friendsInSameInstance.has(friend.id) + ); + }; + // #endregion // #region | Electron diff --git a/src/localization/en/en.json b/src/localization/en/en.json index 9fda9062..fd48cef9 100644 --- a/src/localization/en/en.json +++ b/src/localization/en/en.json @@ -383,7 +383,9 @@ "placeholder": "Sort Order", "dropdown_header": "Choose Sort Order" }, - "width": "Width" + "width": "Width", + "group_by_instance": "Group by Instance", + "group_by_instance_tooltip": "Enabling this will group friends by instance when there is more than one friend in the same instance." }, "user_dialog": { "header": "User Dialog", @@ -2011,4 +2013,4 @@ } } } -} +} \ No newline at end of file diff --git a/src/mixins/friendsListSidebar.pug b/src/mixins/friendsListSidebar.pug index 00d9d454..c753a953 100644 --- a/src/mixins/friendsListSidebar.pug +++ b/src/mixins/friendsListSidebar.pug @@ -40,34 +40,102 @@ mixin friendsListSidebar() i.el-icon-arrow-right(:class="{ rotate: isVIPFriends }") span(style="margin-left:5px") {{ $t('side_panel.favorite') }} ― {{ vipFriends.length }} div(v-show="isVIPFriends") - .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") + template(v-if='isSidebarGroupByInstance') + div(v-for="(friendArr, idx) in vipFriendsInSameInstance" :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") + div.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(v-if="friend.ref.travelingToLocation" class="el-icon el-icon-loading" 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 === 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") i.el-icon-arrow-right(:class="{ rotate: isOnlineFriends }") span(style="margin-left:5px") {{ $t('side_panel.online') }} ― {{ onlineFriends.length }} div(v-show="isOnlineFriends") - .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") + 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") + div.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(v-if="friend.ref.travelingToLocation" class="el-icon el-icon-loading" 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-group.x-link(@click="isActiveFriends = !isActiveFriends; saveFriendsGroupStates()" v-show="activeFriends.length") i.el-icon-arrow-right(:class="{ rotate: isActiveFriends }") span(style="margin-left:5px") {{ $t('side_panel.active') }} ― {{ activeFriends.length }} diff --git a/src/mixins/tabs/settings.pug b/src/mixins/tabs/settings.pug index b11d3b9a..f797c4ec 100644 --- a/src/mixins/tabs/settings.pug +++ b/src/mixins/tabs/settings.pug @@ -231,6 +231,8 @@ mixin settingsTab() div.options-container-item span.name(style="vertical-align:top;padding-top:10px") {{ $t('view.settings.appearance.side_panel.width') }} el-slider(v-model="asideWidth" @input="setAsideWidth" :show-tooltip="false" :marks="{300: ''}" :min="200" :max="500" style="display:inline-block;width:300px") + div.options-container-item + simple-switch(:label='$t("view.settings.appearance.side_panel.group_by_instance")' :value='isSidebarGroupByInstance' @change='handleSwitchGroupByInstance' :tooltip='$t("view.settings.appearance.side_panel.group_by_instance_tooltip")') //- Appearance | User Dialog div.options-container span.header {{ $t('view.settings.appearance.user_dialog.header') }}