mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-17 22:03:50 +02:00
Relocate sidebar settings from the Appearance tab to a new popover within the Sidebar
This commit is contained in:
@@ -84,7 +84,17 @@
|
||||
"online": "ONLINE",
|
||||
"active": "ACTIVE",
|
||||
"offline": "OFFLINE",
|
||||
"pending_offline": "Pending Offline"
|
||||
"pending_offline": "Pending Offline",
|
||||
"settings": {
|
||||
"group_by_instance": "Group by Instance",
|
||||
"split_favorite_friends": "Show Favorite Groups",
|
||||
"hide_friends_in_same_instance": "Hide Grouped Friends",
|
||||
"sort_primary": "Sort by",
|
||||
"sort_secondary": "Then by",
|
||||
"sort_tertiary": "Then by",
|
||||
"favorite_groups": "Favorite Groups",
|
||||
"favorite_groups_placeholder": "All Groups"
|
||||
}
|
||||
},
|
||||
"view": {
|
||||
"login": {
|
||||
@@ -635,7 +645,6 @@
|
||||
"yellow": "Yellow"
|
||||
},
|
||||
"side_panel": {
|
||||
"header": "Side Panel",
|
||||
"sorting": {
|
||||
"header": "Sort Order",
|
||||
"alphabetical": "Alphabetical",
|
||||
@@ -646,13 +655,7 @@
|
||||
"last_seen": "Last Seen",
|
||||
"time_in_instance": "Time in Instance",
|
||||
"placeholder": "Sort Order"
|
||||
},
|
||||
"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.",
|
||||
"hide_friends_in_same_instance": "Hide Friends in Same Instance",
|
||||
"hide_friends_in_same_instance_tooltip": "Hide Friends from Friend List When They Are in the Same Instance.",
|
||||
"split_favorite_friends": "Split Favorite Friends",
|
||||
"split_favorite_friends_tooltip": "Separate favorite friends into their individual groups."
|
||||
}
|
||||
},
|
||||
"user_dialog": {
|
||||
"header": "User Dialog",
|
||||
|
||||
@@ -292,9 +292,12 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
||||
|
||||
if (
|
||||
args.params.type === 'friend' &&
|
||||
generalSettingsStore.localFavoriteFriendsGroups.includes(
|
||||
'friend:' + args.params.tags
|
||||
)
|
||||
(!generalSettingsStore.localFavoriteFriendsGroups.some(
|
||||
(key) => !key.startsWith('local:')
|
||||
) ||
|
||||
generalSettingsStore.localFavoriteFriendsGroups.includes(
|
||||
'friend:' + args.params.tags
|
||||
))
|
||||
) {
|
||||
friendStore.updateLocalFavoriteFriends();
|
||||
}
|
||||
@@ -793,17 +796,19 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
||||
};
|
||||
cachedFavorites.set(ref.id, ref);
|
||||
cachedFavoritesByObjectId.set(ref.favoriteId, ref);
|
||||
ref.$groupKey = `${ref.type}:${String(ref.tags[0])}`;
|
||||
if (
|
||||
ref.type === 'friend' &&
|
||||
(generalSettingsStore.localFavoriteFriendsGroups.length === 0 ||
|
||||
(!generalSettingsStore.localFavoriteFriendsGroups.some(
|
||||
(key) => !key.startsWith('local:')
|
||||
) ||
|
||||
generalSettingsStore.localFavoriteFriendsGroups.includes(
|
||||
ref.groupKey
|
||||
ref.$groupKey
|
||||
))
|
||||
) {
|
||||
friendStore.localFavoriteFriends.add(ref.favoriteId);
|
||||
friendStore.updateSidebarFavorites();
|
||||
}
|
||||
ref.$groupKey = `${ref.type}:${String(ref.tags[0])}`;
|
||||
if (!isFavoriteLoading.value) {
|
||||
countFavoriteGroups();
|
||||
}
|
||||
@@ -1551,13 +1556,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
||||
if (userDialog.visible && userDialog.id === userId) {
|
||||
userDialog.isFavorite = true;
|
||||
}
|
||||
if (
|
||||
generalSettingsStore.localFavoriteFriendsGroups.includes(
|
||||
`local:${group}`
|
||||
)
|
||||
) {
|
||||
friendStore.updateLocalFavoriteFriends();
|
||||
}
|
||||
friendStore.updateLocalFavoriteFriends();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1612,13 +1611,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
||||
getCachedFavoritesByObjectId(userId) ||
|
||||
isInAnyLocalFriendGroup(userId);
|
||||
}
|
||||
if (
|
||||
generalSettingsStore.localFavoriteFriendsGroups.includes(
|
||||
`local:${group}`
|
||||
)
|
||||
) {
|
||||
friendStore.updateLocalFavoriteFriends();
|
||||
}
|
||||
friendStore.updateLocalFavoriteFriends();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1627,13 +1620,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
||||
function deleteLocalFriendFavoriteGroup(group) {
|
||||
delete localFriendFavorites[group];
|
||||
database.deleteFriendFavoriteGroup(group);
|
||||
if (
|
||||
generalSettingsStore.localFavoriteFriendsGroups.includes(
|
||||
`local:${group}`
|
||||
)
|
||||
) {
|
||||
friendStore.updateLocalFavoriteFriends();
|
||||
}
|
||||
friendStore.updateLocalFavoriteFriends();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1701,6 +1688,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
||||
}
|
||||
|
||||
replaceReactiveObject(localFriendFavorites, localFavorites);
|
||||
friendStore.updateLocalFavoriteFriends();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -305,26 +305,25 @@ export const useFriendStore = defineStore('Friend', () => {
|
||||
function updateLocalFavoriteFriends() {
|
||||
const favoriteStore = useFavoriteStore();
|
||||
localFavoriteFriends.clear();
|
||||
const groups = generalSettingsStore.localFavoriteFriendsGroups;
|
||||
const hasRemoteGroupFilter = groups.some(
|
||||
(key) => !key.startsWith('local:')
|
||||
);
|
||||
// Remote favorites: filter by selected remote groups
|
||||
for (const ref of favoriteStore.cachedFavorites.values()) {
|
||||
if (
|
||||
ref.type === 'friend' &&
|
||||
(generalSettingsStore.localFavoriteFriendsGroups.includes(
|
||||
ref.$groupKey
|
||||
) ||
|
||||
generalSettingsStore.localFavoriteFriendsGroups.length ===
|
||||
0)
|
||||
(!hasRemoteGroupFilter || groups.includes(ref.$groupKey))
|
||||
) {
|
||||
localFavoriteFriends.add(ref.favoriteId);
|
||||
}
|
||||
}
|
||||
for (const selectedKey of generalSettingsStore.localFavoriteFriendsGroups) {
|
||||
if (selectedKey.startsWith('local:')) {
|
||||
const groupName = selectedKey.slice(6);
|
||||
const userIds = favoriteStore.localFriendFavorites[groupName];
|
||||
if (userIds) {
|
||||
for (let i = 0; i < userIds.length; ++i) {
|
||||
localFavoriteFriends.add(userIds[i]);
|
||||
}
|
||||
// Local favorites: always include all
|
||||
for (const groupName in favoriteStore.localFriendFavorites) {
|
||||
const userIds = favoriteStore.localFriendFavorites[groupName];
|
||||
if (userIds) {
|
||||
for (let i = 0; i < userIds.length; ++i) {
|
||||
localFavoriteFriends.add(userIds[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ export const useAppearanceSettingsStore = defineStore(
|
||||
const isSidebarGroupByInstance = ref(true);
|
||||
const isHideFriendsInSameInstance = ref(false);
|
||||
const isSidebarDivideByFriendGroup = ref(false);
|
||||
const sidebarFavoriteGroups = ref([]);
|
||||
const hideUserNotes = ref(false);
|
||||
const hideUserMemos = ref(false);
|
||||
const hideUnfriends = ref(false);
|
||||
@@ -150,6 +151,7 @@ export const useAppearanceSettingsStore = defineStore(
|
||||
isSidebarGroupByInstanceConfig,
|
||||
isHideFriendsInSameInstanceConfig,
|
||||
isSidebarDivideByFriendGroupConfig,
|
||||
sidebarFavoriteGroupsConfig,
|
||||
hideUserNotesConfig,
|
||||
hideUserMemosConfig,
|
||||
hideUnfriendsConfig,
|
||||
@@ -205,6 +207,7 @@ export const useAppearanceSettingsStore = defineStore(
|
||||
'VRCX_sidebarDivideByFriendGroup',
|
||||
true
|
||||
),
|
||||
configRepository.getString('VRCX_sidebarFavoriteGroups', '[]'),
|
||||
configRepository.getBool('VRCX_hideUserNotes', false),
|
||||
configRepository.getBool('VRCX_hideUserMemos', false),
|
||||
configRepository.getBool('VRCX_hideUnfriends', false),
|
||||
@@ -293,6 +296,9 @@ export const useAppearanceSettingsStore = defineStore(
|
||||
isHideFriendsInSameInstanceConfig;
|
||||
isSidebarDivideByFriendGroup.value =
|
||||
isSidebarDivideByFriendGroupConfig;
|
||||
sidebarFavoriteGroups.value = JSON.parse(
|
||||
sidebarFavoriteGroupsConfig
|
||||
);
|
||||
hideUserNotes.value = hideUserNotesConfig;
|
||||
hideUserMemos.value = hideUserMemosConfig;
|
||||
hideUnfriends.value = hideUnfriendsConfig;
|
||||
@@ -701,6 +707,16 @@ export const useAppearanceSettingsStore = defineStore(
|
||||
isSidebarDivideByFriendGroup.value
|
||||
);
|
||||
}
|
||||
/**
|
||||
* @param {string[]} value
|
||||
*/
|
||||
function setSidebarFavoriteGroups(value) {
|
||||
sidebarFavoriteGroups.value = value;
|
||||
configRepository.setString(
|
||||
'VRCX_sidebarFavoriteGroups',
|
||||
JSON.stringify(value)
|
||||
);
|
||||
}
|
||||
function setHideUserNotes() {
|
||||
hideUserNotes.value = !hideUserNotes.value;
|
||||
configRepository.setBool('VRCX_hideUserNotes', hideUserNotes.value);
|
||||
@@ -958,6 +974,7 @@ export const useAppearanceSettingsStore = defineStore(
|
||||
isSidebarGroupByInstance,
|
||||
isHideFriendsInSameInstance,
|
||||
isSidebarDivideByFriendGroup,
|
||||
sidebarFavoriteGroups,
|
||||
hideUserNotes,
|
||||
hideUserMemos,
|
||||
hideUnfriends,
|
||||
@@ -995,6 +1012,7 @@ export const useAppearanceSettingsStore = defineStore(
|
||||
setIsSidebarGroupByInstance,
|
||||
setIsHideFriendsInSameInstance,
|
||||
setIsSidebarDivideByFriendGroup,
|
||||
setSidebarFavoriteGroups,
|
||||
setHideUserNotes,
|
||||
setHideUserMemos,
|
||||
setHideUnfriends,
|
||||
|
||||
@@ -224,123 +224,7 @@
|
||||
:value="dtIsoFormat"
|
||||
@change="setDtIsoFormat" />
|
||||
</div>
|
||||
<div class="options-container">
|
||||
<span class="header">{{ t('view.settings.appearance.side_panel.header') }}</span>
|
||||
<br />
|
||||
<div class="options-container-item">
|
||||
<span class="name">{{ t('view.settings.appearance.side_panel.sorting.header') }}</span>
|
||||
<Select :model-value="sidebarSortMethod1" @update:modelValue="setSidebarSortMethod1">
|
||||
<SelectTrigger style="width: 170px" size="sm">
|
||||
<SelectValue :placeholder="t('view.settings.appearance.side_panel.sorting.placeholder')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Sort Alphabetically">{{
|
||||
t('view.settings.appearance.side_panel.sorting.alphabetical')
|
||||
}}</SelectItem>
|
||||
<SelectItem value="Sort by Status">{{
|
||||
t('view.settings.appearance.side_panel.sorting.status')
|
||||
}}</SelectItem>
|
||||
<SelectItem value="Sort Private to Bottom">{{
|
||||
t('view.settings.appearance.side_panel.sorting.private_to_bottom')
|
||||
}}</SelectItem>
|
||||
<SelectItem value="Sort by Last Active">{{
|
||||
t('view.settings.appearance.side_panel.sorting.last_active')
|
||||
}}</SelectItem>
|
||||
<SelectItem value="Sort by Last Seen">{{
|
||||
t('view.settings.appearance.side_panel.sorting.last_seen')
|
||||
}}</SelectItem>
|
||||
<SelectItem value="Sort by Time in Instance">{{
|
||||
t('view.settings.appearance.side_panel.sorting.time_in_instance')
|
||||
}}</SelectItem>
|
||||
<SelectItem value="Sort by Location">{{
|
||||
t('view.settings.appearance.side_panel.sorting.location')
|
||||
}}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<ArrowRight style="margin: 5px" />
|
||||
<Select
|
||||
:model-value="sidebarSortMethod2"
|
||||
:disabled="!sidebarSortMethod1"
|
||||
@update:modelValue="(v) => setSidebarSortMethod2(v === SELECT_CLEAR_VALUE ? '' : v)">
|
||||
<SelectTrigger style="width: 170px" size="sm">
|
||||
<SelectValue :placeholder="t('view.settings.appearance.side_panel.sorting.placeholder')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="SELECT_CLEAR_VALUE">{{ t('dialog.gallery_select.none') }}</SelectItem>
|
||||
<SelectItem value="Sort Alphabetically">{{
|
||||
t('view.settings.appearance.side_panel.sorting.alphabetical')
|
||||
}}</SelectItem>
|
||||
<SelectItem value="Sort by Status">{{
|
||||
t('view.settings.appearance.side_panel.sorting.status')
|
||||
}}</SelectItem>
|
||||
<SelectItem value="Sort Private to Bottom">{{
|
||||
t('view.settings.appearance.side_panel.sorting.private_to_bottom')
|
||||
}}</SelectItem>
|
||||
<SelectItem value="Sort by Last Active">{{
|
||||
t('view.settings.appearance.side_panel.sorting.last_active')
|
||||
}}</SelectItem>
|
||||
<SelectItem value="Sort by Last Seen">{{
|
||||
t('view.settings.appearance.side_panel.sorting.last_seen')
|
||||
}}</SelectItem>
|
||||
<SelectItem value="Sort by Time in Instance">{{
|
||||
t('view.settings.appearance.side_panel.sorting.time_in_instance')
|
||||
}}</SelectItem>
|
||||
<SelectItem value="Sort by Location">{{
|
||||
t('view.settings.appearance.side_panel.sorting.location')
|
||||
}}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<ArrowRight style="margin: 5px" />
|
||||
<Select
|
||||
:model-value="sidebarSortMethod3"
|
||||
:disabled="!sidebarSortMethod2"
|
||||
@update:modelValue="(v) => setSidebarSortMethod3(v === SELECT_CLEAR_VALUE ? '' : v)">
|
||||
<SelectTrigger style="width: 170px" size="sm">
|
||||
<SelectValue :placeholder="t('view.settings.appearance.side_panel.sorting.placeholder')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="SELECT_CLEAR_VALUE">{{ t('dialog.gallery_select.none') }}</SelectItem>
|
||||
<SelectItem value="Sort Alphabetically">{{
|
||||
t('view.settings.appearance.side_panel.sorting.alphabetical')
|
||||
}}</SelectItem>
|
||||
<SelectItem value="Sort by Status">{{
|
||||
t('view.settings.appearance.side_panel.sorting.status')
|
||||
}}</SelectItem>
|
||||
<SelectItem value="Sort Private to Bottom">{{
|
||||
t('view.settings.appearance.side_panel.sorting.private_to_bottom')
|
||||
}}</SelectItem>
|
||||
<SelectItem value="Sort by Last Active">{{
|
||||
t('view.settings.appearance.side_panel.sorting.last_active')
|
||||
}}</SelectItem>
|
||||
<SelectItem value="Sort by Last Seen">{{
|
||||
t('view.settings.appearance.side_panel.sorting.last_seen')
|
||||
}}</SelectItem>
|
||||
<SelectItem value="Sort by Time in Instance">{{
|
||||
t('view.settings.appearance.side_panel.sorting.time_in_instance')
|
||||
}}</SelectItem>
|
||||
<SelectItem value="Sort by Location">{{
|
||||
t('view.settings.appearance.side_panel.sorting.location')
|
||||
}}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<simple-switch
|
||||
:label="t('view.settings.appearance.side_panel.group_by_instance')"
|
||||
:value="isSidebarGroupByInstance"
|
||||
:tooltip="t('view.settings.appearance.side_panel.group_by_instance_tooltip')"
|
||||
@change="setIsSidebarGroupByInstance"></simple-switch>
|
||||
<simple-switch
|
||||
v-if="isSidebarGroupByInstance"
|
||||
:label="t('view.settings.appearance.side_panel.hide_friends_in_same_instance')"
|
||||
:value="isHideFriendsInSameInstance"
|
||||
:tooltip="t('view.settings.appearance.side_panel.hide_friends_in_same_instance_tooltip')"
|
||||
@change="setIsHideFriendsInSameInstance"></simple-switch>
|
||||
<simple-switch
|
||||
:label="t('view.settings.appearance.side_panel.split_favorite_friends')"
|
||||
:value="isSidebarDivideByFriendGroup"
|
||||
:tooltip="t('view.settings.appearance.side_panel.split_favorite_friends_tooltip')"
|
||||
@change="setIsSidebarDivideByFriendGroup"></simple-switch>
|
||||
</div>
|
||||
|
||||
<div class="options-container">
|
||||
<span class="header">{{ t('view.settings.appearance.user_dialog.header') }}</span>
|
||||
<simple-switch
|
||||
@@ -457,9 +341,9 @@
|
||||
TagsInputItemDelete,
|
||||
TagsInputItemText
|
||||
} from '@/components/ui/tags-input';
|
||||
import { ArrowRight, CheckIcon, ChevronDown, Info } from 'lucide-vue-next';
|
||||
import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
||||
import { CheckIcon, ChevronDown, Info } from 'lucide-vue-next';
|
||||
import { useAppearanceSettingsStore, useFavoriteStore, useVrStore } from '@/stores';
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||
import { getLanguageName, languageCodes } from '@/localization';
|
||||
@@ -490,12 +374,6 @@
|
||||
instanceUsersSortAlphabetical,
|
||||
dtHour12,
|
||||
dtIsoFormat,
|
||||
sidebarSortMethod1,
|
||||
sidebarSortMethod2,
|
||||
sidebarSortMethod3,
|
||||
isSidebarGroupByInstance,
|
||||
isHideFriendsInSameInstance,
|
||||
isSidebarDivideByFriendGroup,
|
||||
hideUserNotes,
|
||||
hideUserMemos,
|
||||
hideUnfriends,
|
||||
@@ -519,12 +397,6 @@
|
||||
setInstanceUsersSortAlphabetical,
|
||||
setDtHour12,
|
||||
setDtIsoFormat,
|
||||
setSidebarSortMethod1,
|
||||
setSidebarSortMethod2,
|
||||
setSidebarSortMethod3,
|
||||
setIsSidebarGroupByInstance,
|
||||
setIsHideFriendsInSameInstance,
|
||||
setIsSidebarDivideByFriendGroup,
|
||||
setHideUserNotes,
|
||||
setHideUserMemos,
|
||||
setHideUnfriends,
|
||||
@@ -592,8 +464,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
const SELECT_CLEAR_VALUE = '__clear__';
|
||||
|
||||
const TABLE_PAGE_SIZE_SUGGESTIONS = Object.freeze([5, 10, 15, 20, 25, 30, 50, 75, 100, 150, 200, 250, 500, 1000]);
|
||||
|
||||
const tablePageSizesOpen = ref(false);
|
||||
|
||||
@@ -62,19 +62,150 @@
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center mx-1 gap-1">
|
||||
<TooltipWrapper side="bottom" :content="t('side_panel.refresh_tooltip')">
|
||||
<Button
|
||||
class="rounded-full"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
:disabled="isRefreshFriendsLoading"
|
||||
style="margin-right: 10px"
|
||||
@click="refreshFriendsList">
|
||||
<Spinner v-if="isRefreshFriendsLoading" />
|
||||
<RefreshCw v-else />
|
||||
</Button>
|
||||
</TooltipWrapper>
|
||||
<Popover>
|
||||
<PopoverTrigger as-child>
|
||||
<Button class="rounded-full" variant="ghost" size="icon-sm">
|
||||
<Settings />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent side="bottom" align="end" class="w-64 p-3" @open-auto-focus.prevent>
|
||||
<div class="flex flex-col gap-2.5 text-xs">
|
||||
<div class="flex items-center justify-between">
|
||||
<span>{{ t('side_panel.settings.group_by_instance') }}</span>
|
||||
<Switch
|
||||
:model-value="isSidebarGroupByInstance"
|
||||
@update:modelValue="setIsSidebarGroupByInstance" />
|
||||
</div>
|
||||
<div v-if="isSidebarGroupByInstance" class="flex items-center justify-between">
|
||||
<span>{{ t('side_panel.settings.hide_friends_in_same_instance') }}</span>
|
||||
<Switch
|
||||
:model-value="isHideFriendsInSameInstance"
|
||||
@update:modelValue="setIsHideFriendsInSameInstance" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>{{ t('side_panel.settings.split_favorite_friends') }}</span>
|
||||
<Switch
|
||||
:model-value="isSidebarDivideByFriendGroup"
|
||||
@update:modelValue="setIsSidebarDivideByFriendGroup" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<span>{{ t('side_panel.settings.favorite_groups') }}</span>
|
||||
<Select
|
||||
:model-value="resolvedSidebarFavoriteGroups"
|
||||
multiple
|
||||
@update:modelValue="handleFavoriteGroupsChange">
|
||||
<SelectTrigger size="sm" class="w-full overflow-hidden">
|
||||
<SelectValue
|
||||
:placeholder="t('side_panel.settings.favorite_groups_placeholder')">
|
||||
<template v-if="resolvedSidebarFavoriteGroups.length">
|
||||
<span class="truncate">{{ selectedFavGroupLabel }}</span>
|
||||
<span
|
||||
v-if="resolvedSidebarFavoriteGroups.length > 1"
|
||||
class="bg-primary text-primary-foreground shrink-0 rounded px-1 text-xs">
|
||||
+{{ resolvedSidebarFavoriteGroups.length - 1 }}
|
||||
</span>
|
||||
</template>
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem
|
||||
v-for="group in favoriteFriendGroups"
|
||||
:key="group.key"
|
||||
:value="group.key">
|
||||
{{ group.displayName }}
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
<template v-if="localFriendFavoriteGroups.length">
|
||||
<SelectSeparator />
|
||||
<SelectGroup>
|
||||
<SelectItem
|
||||
v-for="group in localFriendFavoriteGroups"
|
||||
:key="'local:' + group"
|
||||
:value="'local:' + group">
|
||||
{{ group }}
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</template>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Separator />
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<span>{{ t('side_panel.settings.sort_primary') }}</span>
|
||||
<Select :model-value="sidebarSortMethod1" @update:modelValue="setSidebarSortMethod1">
|
||||
<SelectTrigger size="sm">
|
||||
<SelectValue
|
||||
:placeholder="
|
||||
t('view.settings.appearance.side_panel.sorting.placeholder')
|
||||
" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="opt in sortOptions" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<span>{{ t('side_panel.settings.sort_secondary') }}</span>
|
||||
<Select
|
||||
:model-value="sidebarSortMethod2"
|
||||
:disabled="!sidebarSortMethod1"
|
||||
@update:modelValue="(v) => setSidebarSortMethod2(v === CLEAR_VALUE ? '' : v)">
|
||||
<SelectTrigger size="sm">
|
||||
<SelectValue
|
||||
:placeholder="
|
||||
t('view.settings.appearance.side_panel.sorting.placeholder')
|
||||
" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="CLEAR_VALUE">{{
|
||||
t('dialog.gallery_select.none')
|
||||
}}</SelectItem>
|
||||
<SelectItem v-for="opt in sortOptions" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<span>{{ t('side_panel.settings.sort_tertiary') }}</span>
|
||||
<Select
|
||||
:model-value="sidebarSortMethod3"
|
||||
:disabled="!sidebarSortMethod2"
|
||||
@update:modelValue="(v) => setSidebarSortMethod3(v === CLEAR_VALUE ? '' : v)">
|
||||
<SelectTrigger size="sm">
|
||||
<SelectValue
|
||||
:placeholder="
|
||||
t('view.settings.appearance.side_panel.sorting.placeholder')
|
||||
" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="CLEAR_VALUE">{{
|
||||
t('dialog.gallery_select.none')
|
||||
}}</SelectItem>
|
||||
<SelectItem v-for="opt in sortOptions" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<TabsUnderline
|
||||
@@ -108,18 +239,35 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@/components/ui/select';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { RefreshCw, Settings } from 'lucide-vue-next';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { DataTableEmpty } from '@/components/ui/data-table';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { RefreshCw } from 'lucide-vue-next';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { TabsUnderline } from '@/components/ui/tabs';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { useFriendStore, useGroupStore, useSearchStore } from '../../stores';
|
||||
import {
|
||||
useAppearanceSettingsStore,
|
||||
useFavoriteStore,
|
||||
useFriendStore,
|
||||
useGroupStore,
|
||||
useSearchStore
|
||||
} from '../../stores';
|
||||
import { debounce, userImage } from '../../shared/utils';
|
||||
|
||||
import FriendsSidebar from './components/FriendsSidebar.vue';
|
||||
@@ -131,6 +279,79 @@
|
||||
const { quickSearchItems } = storeToRefs(useSearchStore());
|
||||
const { groupInstances } = storeToRefs(useGroupStore());
|
||||
const { t } = useI18n();
|
||||
|
||||
const appearanceSettingsStore = useAppearanceSettingsStore();
|
||||
const {
|
||||
sidebarSortMethod1,
|
||||
sidebarSortMethod2,
|
||||
sidebarSortMethod3,
|
||||
isSidebarGroupByInstance,
|
||||
isHideFriendsInSameInstance,
|
||||
isSidebarDivideByFriendGroup,
|
||||
sidebarFavoriteGroups
|
||||
} = storeToRefs(appearanceSettingsStore);
|
||||
const {
|
||||
setSidebarSortMethod1,
|
||||
setSidebarSortMethod2,
|
||||
setSidebarSortMethod3,
|
||||
setIsSidebarGroupByInstance,
|
||||
setIsHideFriendsInSameInstance,
|
||||
setIsSidebarDivideByFriendGroup,
|
||||
setSidebarFavoriteGroups
|
||||
} = appearanceSettingsStore;
|
||||
|
||||
const favoriteStore = useFavoriteStore();
|
||||
const { favoriteFriendGroups, localFriendFavoriteGroups } = storeToRefs(favoriteStore);
|
||||
|
||||
const allFavoriteGroupKeys = computed(() => {
|
||||
const keys = favoriteFriendGroups.value.map((g) => g.key);
|
||||
for (const group of localFriendFavoriteGroups.value) {
|
||||
keys.push('local:' + group);
|
||||
}
|
||||
return keys;
|
||||
});
|
||||
|
||||
const resolvedSidebarFavoriteGroups = computed(() => {
|
||||
if (sidebarFavoriteGroups.value.length === 0) {
|
||||
return allFavoriteGroupKeys.value;
|
||||
}
|
||||
return sidebarFavoriteGroups.value;
|
||||
});
|
||||
|
||||
function handleFavoriteGroupsChange(value) {
|
||||
if (!value || value.length === 0) {
|
||||
// Deselected all → reset to all (store as empty)
|
||||
setSidebarFavoriteGroups([]);
|
||||
return;
|
||||
}
|
||||
// If all groups are selected, store as empty (= all)
|
||||
const allKeys = allFavoriteGroupKeys.value;
|
||||
if (value.length >= allKeys.length && allKeys.every((k) => value.includes(k))) {
|
||||
setSidebarFavoriteGroups([]);
|
||||
return;
|
||||
}
|
||||
setSidebarFavoriteGroups(value);
|
||||
}
|
||||
|
||||
const selectedFavGroupLabel = computed(() => {
|
||||
const key = resolvedSidebarFavoriteGroups.value[0];
|
||||
if (!key) return '';
|
||||
if (key.startsWith('local:')) return key.slice(6);
|
||||
return favoriteFriendGroups.value.find((g) => g.key === key)?.displayName || key;
|
||||
});
|
||||
|
||||
const CLEAR_VALUE = '__clear__';
|
||||
|
||||
const sortOptions = computed(() => [
|
||||
{ value: 'Sort Alphabetically', label: t('view.settings.appearance.side_panel.sorting.alphabetical') },
|
||||
{ value: 'Sort by Status', label: t('view.settings.appearance.side_panel.sorting.status') },
|
||||
{ value: 'Sort Private to Bottom', label: t('view.settings.appearance.side_panel.sorting.private_to_bottom') },
|
||||
{ value: 'Sort by Last Active', label: t('view.settings.appearance.side_panel.sorting.last_active') },
|
||||
{ value: 'Sort by Last Seen', label: t('view.settings.appearance.side_panel.sorting.last_seen') },
|
||||
{ value: 'Sort by Time in Instance', label: t('view.settings.appearance.side_panel.sorting.time_in_instance') },
|
||||
{ value: 'Sort by Location', label: t('view.settings.appearance.side_panel.sorting.location') }
|
||||
]);
|
||||
|
||||
const sidebarTabs = computed(() => [
|
||||
{ value: 'friends', label: t('side_panel.friends') },
|
||||
{ value: 'groups', label: t('side_panel.groups') }
|
||||
|
||||
@@ -59,13 +59,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="item.row.type === 'vip-subheader'">
|
||||
<div>
|
||||
<span class="text-xs">{{ item.row.label }}</span>
|
||||
<span class="text-xs ml-1.5">{{ `(${item.row.count})` }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="item.row.type === 'instance-header'">
|
||||
<div class="mb-1 flex items-center">
|
||||
<Location class="inline text-xs" :location="item.row.location" />
|
||||
@@ -89,7 +82,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { ChevronDown } from 'lucide-vue-next';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
@@ -101,7 +94,6 @@
|
||||
useFavoriteStore,
|
||||
useFriendStore,
|
||||
useGameStore,
|
||||
useGeneralSettingsStore,
|
||||
useLocationStore,
|
||||
useUserStore
|
||||
} from '../../../stores';
|
||||
@@ -115,13 +107,15 @@
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const generalSettingsStore = useGeneralSettingsStore();
|
||||
|
||||
const friendStore = useFriendStore();
|
||||
const { vipFriends, onlineFriends, activeFriends, offlineFriends, friendsInSameInstance } =
|
||||
storeToRefs(friendStore);
|
||||
const { isSidebarGroupByInstance, isHideFriendsInSameInstance, isSidebarDivideByFriendGroup } =
|
||||
storeToRefs(useAppearanceSettingsStore());
|
||||
const {
|
||||
isSidebarGroupByInstance,
|
||||
isHideFriendsInSameInstance,
|
||||
isSidebarDivideByFriendGroup,
|
||||
sidebarFavoriteGroups
|
||||
} = storeToRefs(useAppearanceSettingsStore());
|
||||
const { gameLogDisabled } = storeToRefs(useAdvancedSettingsStore());
|
||||
const { showUserDialog } = useUserStore();
|
||||
const { favoriteFriendGroups, groupedByGroupKeyFavoriteFriends, localFriendFavorites } =
|
||||
@@ -136,6 +130,7 @@
|
||||
const isActiveFriends = ref(true);
|
||||
const isOfflineFriends = ref(true);
|
||||
const isSidebarGroupByInstanceCollapsed = ref(false);
|
||||
const collapsedFavGroups = reactive(new Set());
|
||||
const scrollViewportRef = ref(null);
|
||||
const scrollRootRef = ref(null);
|
||||
|
||||
@@ -164,30 +159,53 @@
|
||||
|
||||
const onlineFriendsByGroupStatus = computed(() => excludeSameInstance(onlineFriends.value));
|
||||
|
||||
const vipFriendsByGroupStatus = computed(() => excludeSameInstance(vipFriends.value));
|
||||
const vipFriendsByGroupStatus = computed(() => {
|
||||
const selectedGroups = sidebarFavoriteGroups.value;
|
||||
const hasFilter = selectedGroups.length > 0;
|
||||
if (!hasFilter) {
|
||||
return excludeSameInstance(vipFriends.value);
|
||||
}
|
||||
// Filter to only include VIP friends whose group key is in selectedGroups
|
||||
const allowedIds = new Set();
|
||||
const remoteFriendsByGroup = groupedByGroupKeyFavoriteFriends.value;
|
||||
for (const key of selectedGroups) {
|
||||
if (key.startsWith('local:')) {
|
||||
const groupName = key.slice(6);
|
||||
const userIds = localFriendFavorites.value?.[groupName];
|
||||
if (userIds) {
|
||||
for (const id of userIds) allowedIds.add(id);
|
||||
}
|
||||
} else if (remoteFriendsByGroup[key]) {
|
||||
for (const f of remoteFriendsByGroup[key]) allowedIds.add(f.id);
|
||||
}
|
||||
}
|
||||
return excludeSameInstance(vipFriends.value.filter((f) => allowedIds.has(f.id)));
|
||||
});
|
||||
|
||||
// VIP friends divide by group
|
||||
const vipFriendsDivideByGroup = computed(() => {
|
||||
const remoteFriendsByGroup = groupedByGroupKeyFavoriteFriends.value;
|
||||
const selectedGroups = sidebarFavoriteGroups.value;
|
||||
const hasFilter = selectedGroups.length > 0;
|
||||
|
||||
// Build a normalized list of { key, groupName, memberIds }
|
||||
const groups = [];
|
||||
|
||||
for (const key in remoteFriendsByGroup) {
|
||||
if (Object.hasOwn(remoteFriendsByGroup, key)) {
|
||||
if (hasFilter && !selectedGroups.includes(key)) continue;
|
||||
const groupName = favoriteFriendGroups.value.find((g) => g.key === key)?.displayName || '';
|
||||
const memberIds = new Set(remoteFriendsByGroup[key].map((f) => f.id));
|
||||
groups.push({ key, groupName, memberIds });
|
||||
}
|
||||
}
|
||||
|
||||
for (const selectedKey of generalSettingsStore.localFavoriteFriendsGroups) {
|
||||
if (selectedKey.startsWith('local:')) {
|
||||
const groupName = selectedKey.slice(6);
|
||||
const userIds = localFriendFavorites.value?.[groupName];
|
||||
if (userIds?.length) {
|
||||
groups.push({ key: selectedKey, groupName, memberIds: new Set(userIds) });
|
||||
}
|
||||
for (const groupName in localFriendFavorites.value) {
|
||||
const selectedKey = `local:${groupName}`;
|
||||
if (hasFilter && !selectedGroups.includes(selectedKey)) continue;
|
||||
const userIds = localFriendFavorites.value[groupName];
|
||||
if (userIds?.length) {
|
||||
groups.push({ key: selectedKey, groupName, memberIds: new Set(userIds) });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,13 +247,7 @@
|
||||
paddingBottom: options.paddingBottom,
|
||||
itemStyle: options.itemStyle
|
||||
});
|
||||
const buildVipSubheaderRow = (label, count, key) => ({
|
||||
type: 'vip-subheader',
|
||||
key,
|
||||
label,
|
||||
count,
|
||||
paddingBottom: 4
|
||||
});
|
||||
|
||||
const buildInstanceHeaderRow = (location, count, key) => ({
|
||||
type: 'instance-header',
|
||||
key,
|
||||
@@ -262,7 +274,7 @@
|
||||
}
|
||||
|
||||
const vipFriendCount = isSidebarDivideByFriendGroup.value
|
||||
? vipFriendsDivideByGroup.value.length
|
||||
? vipFriendsDivideByGroup.value.reduce((sum, group) => sum + group.length, 0)
|
||||
: vipFriendsByGroupStatus.value.length;
|
||||
|
||||
if (vipFriendCount) {
|
||||
@@ -282,16 +294,34 @@
|
||||
vipFriendsDivideByGroup.value.forEach((group, groupIndex) => {
|
||||
const groupName = group?.[0]?.groupName ?? '';
|
||||
const groupKey = group?.[0]?.key ?? groupIndex;
|
||||
const isExpanded = !collapsedFavGroups.has(groupKey);
|
||||
if (groupName) {
|
||||
rows.push(buildVipSubheaderRow(groupName, group.length, `vip-subheader:${groupKey}`));
|
||||
}
|
||||
group.forEach((friend, idx) => {
|
||||
rows.push(
|
||||
buildFriendRow(friend, `vip:${groupKey}:${friend?.id ?? idx}`, {
|
||||
paddingBottom: idx === group.length - 1 ? 10 : undefined
|
||||
buildToggleRow({
|
||||
key: `vip-subheader:${groupKey}`,
|
||||
label: groupName,
|
||||
count: group.length,
|
||||
expanded: isExpanded,
|
||||
headerPadding: '4px 0 4px 4px',
|
||||
onClick: () => {
|
||||
if (collapsedFavGroups.has(groupKey)) {
|
||||
collapsedFavGroups.delete(groupKey);
|
||||
} else {
|
||||
collapsedFavGroups.add(groupKey);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
if (isExpanded) {
|
||||
group.forEach((friend, idx) => {
|
||||
rows.push(
|
||||
buildFriendRow(friend, `vip:${groupKey}:${friend?.id ?? idx}`, {
|
||||
paddingBottom: idx === group.length - 1 ? 10 : undefined
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
vipFriendsByGroupStatus.value.forEach((friend, idx) => {
|
||||
|
||||
Reference in New Issue
Block a user