mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-05-06 22:46:06 +02:00
improve friend sidebar settings UI with sections and collapsible advanced options
This commit is contained in:
@@ -187,6 +187,11 @@
|
|||||||
"offline": "OFFLINE",
|
"offline": "OFFLINE",
|
||||||
"pending_offline": "Pending Offline",
|
"pending_offline": "Pending Offline",
|
||||||
"settings": {
|
"settings": {
|
||||||
|
"display": "Display",
|
||||||
|
"sort": "Sort",
|
||||||
|
"advanced": "Advanced",
|
||||||
|
"sorting": "Sorting",
|
||||||
|
"favorites_section": "Favorites",
|
||||||
"group_by_instance": "Group by Instance",
|
"group_by_instance": "Group by Instance",
|
||||||
"split_favorite_friends": "Show Favorite Groups",
|
"split_favorite_friends": "Show Favorite Groups",
|
||||||
"hide_friends_in_same_instance": "Hide Grouped Friends",
|
"hide_friends_in_same_instance": "Hide Grouped Friends",
|
||||||
|
|||||||
+167
-105
@@ -63,6 +63,10 @@
|
|||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent side="bottom" align="end" class="w-64 p-3" @open-auto-focus.prevent>
|
<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 flex-col gap-2.5 text-xs">
|
||||||
|
<!-- Display Section -->
|
||||||
|
<span class="text-[11px] font-medium text-muted-foreground uppercase tracking-wide">
|
||||||
|
{{ t('side_panel.settings.display') }}
|
||||||
|
</span>
|
||||||
<Field orientation="horizontal">
|
<Field orientation="horizontal">
|
||||||
<FieldLabel>{{ t('side_panel.settings.group_by_instance') }}</FieldLabel>
|
<FieldLabel>{{ t('side_panel.settings.group_by_instance') }}</FieldLabel>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -87,64 +91,14 @@
|
|||||||
:model-value="isSidebarDivideByFriendGroup"
|
:model-value="isSidebarDivideByFriendGroup"
|
||||||
@update:modelValue="setIsSidebarDivideByFriendGroup" />
|
@update:modelValue="setIsSidebarDivideByFriendGroup" />
|
||||||
</Field>
|
</Field>
|
||||||
<Button
|
|
||||||
v-if="isSidebarDivideByFriendGroup"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
class="w-full text-sm"
|
|
||||||
@click="
|
|
||||||
isSettingsPopoverOpen = false;
|
|
||||||
isGroupOrderDialogOpen = true;
|
|
||||||
">
|
|
||||||
{{ t('side_panel.settings.edit_group_order') }}
|
|
||||||
</Button>
|
|
||||||
<Field>
|
|
||||||
<FieldLabel>{{ t('side_panel.settings.favorite_groups') }}</FieldLabel>
|
|
||||||
<FieldContent>
|
|
||||||
<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>
|
|
||||||
</FieldContent>
|
|
||||||
</Field>
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
|
<!-- Sort Section -->
|
||||||
|
<span class="text-[11px] font-medium text-muted-foreground uppercase tracking-wide">
|
||||||
|
{{ t('side_panel.settings.sort') }}
|
||||||
|
</span>
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel>{{ t('side_panel.settings.sort_primary') }}</FieldLabel>
|
|
||||||
<FieldContent>
|
<FieldContent>
|
||||||
<Select
|
<Select
|
||||||
:model-value="sidebarSortMethod1"
|
:model-value="sidebarSortMethod1"
|
||||||
@@ -163,54 +117,160 @@
|
|||||||
</Select>
|
</Select>
|
||||||
</FieldContent>
|
</FieldContent>
|
||||||
</Field>
|
</Field>
|
||||||
<Field>
|
|
||||||
<FieldLabel>{{ t('side_panel.settings.sort_secondary') }}</FieldLabel>
|
<Separator />
|
||||||
<FieldContent>
|
|
||||||
<Select
|
<!-- Advanced Section (Collapsible) -->
|
||||||
:model-value="sidebarSortMethod2"
|
<Collapsible v-model:open="isAdvancedOpen">
|
||||||
:disabled="!sidebarSortMethod1"
|
<CollapsibleTrigger as-child>
|
||||||
@update:modelValue="(v) => setSidebarSortMethod2(v === CLEAR_VALUE ? '' : v)">
|
<button
|
||||||
<SelectTrigger size="sm">
|
type="button"
|
||||||
<SelectValue
|
class="flex w-full items-center justify-between py-0.5 text-[11px] font-medium text-muted-foreground uppercase tracking-wide cursor-pointer hover:text-foreground transition-colors">
|
||||||
:placeholder="
|
{{ t('side_panel.settings.advanced') }}
|
||||||
t('view.settings.appearance.side_panel.sorting.placeholder')
|
<ChevronDown
|
||||||
" />
|
class="size-3.5 transition-transform duration-200"
|
||||||
</SelectTrigger>
|
:class="{ 'rotate-180': isAdvancedOpen }" />
|
||||||
<SelectContent>
|
</button>
|
||||||
<SelectItem :value="CLEAR_VALUE">{{
|
</CollapsibleTrigger>
|
||||||
t('dialog.gallery_select.none')
|
<CollapsibleContent>
|
||||||
}}</SelectItem>
|
<div class="flex flex-col gap-2.5 pt-2.5">
|
||||||
<SelectItem v-for="opt in sortOptions" :key="opt.value" :value="opt.value">
|
<!-- Sorting Sub-section -->
|
||||||
{{ opt.label }}
|
<span
|
||||||
</SelectItem>
|
class="text-[10px] font-medium text-muted-foreground/70 uppercase tracking-wide">
|
||||||
</SelectContent>
|
{{ t('side_panel.settings.sorting') }}
|
||||||
</Select>
|
</span>
|
||||||
</FieldContent>
|
<Field>
|
||||||
</Field>
|
<FieldLabel>{{ t('side_panel.settings.sort_secondary') }}</FieldLabel>
|
||||||
<Field>
|
<FieldContent>
|
||||||
<FieldLabel>{{ t('side_panel.settings.sort_tertiary') }}</FieldLabel>
|
<Select
|
||||||
<FieldContent>
|
:model-value="sidebarSortMethod2"
|
||||||
<Select
|
:disabled="!sidebarSortMethod1"
|
||||||
:model-value="sidebarSortMethod3"
|
@update:modelValue="
|
||||||
:disabled="!sidebarSortMethod2"
|
(v) => setSidebarSortMethod2(v === CLEAR_VALUE ? '' : v)
|
||||||
@update:modelValue="(v) => setSidebarSortMethod3(v === CLEAR_VALUE ? '' : v)">
|
">
|
||||||
<SelectTrigger size="sm">
|
<SelectTrigger size="sm">
|
||||||
<SelectValue
|
<SelectValue
|
||||||
:placeholder="
|
:placeholder="
|
||||||
t('view.settings.appearance.side_panel.sorting.placeholder')
|
t(
|
||||||
" />
|
'view.settings.appearance.side_panel.sorting.placeholder'
|
||||||
</SelectTrigger>
|
)
|
||||||
<SelectContent>
|
" />
|
||||||
<SelectItem :value="CLEAR_VALUE">{{
|
</SelectTrigger>
|
||||||
t('dialog.gallery_select.none')
|
<SelectContent>
|
||||||
}}</SelectItem>
|
<SelectItem :value="CLEAR_VALUE">{{
|
||||||
<SelectItem v-for="opt in sortOptions" :key="opt.value" :value="opt.value">
|
t('dialog.gallery_select.none')
|
||||||
{{ opt.label }}
|
}}</SelectItem>
|
||||||
</SelectItem>
|
<SelectItem
|
||||||
</SelectContent>
|
v-for="opt in sortOptions"
|
||||||
</Select>
|
:key="opt.value"
|
||||||
</FieldContent>
|
:value="opt.value">
|
||||||
</Field>
|
{{ opt.label }}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FieldContent>
|
||||||
|
</Field>
|
||||||
|
<Field>
|
||||||
|
<FieldLabel>{{ t('side_panel.settings.sort_tertiary') }}</FieldLabel>
|
||||||
|
<FieldContent>
|
||||||
|
<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>
|
||||||
|
</FieldContent>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<!-- Favorites Sub-section -->
|
||||||
|
<span
|
||||||
|
class="text-[10px] font-medium text-muted-foreground/70 uppercase tracking-wide">
|
||||||
|
{{ t('side_panel.settings.favorites_section') }}
|
||||||
|
</span>
|
||||||
|
<Field>
|
||||||
|
<FieldLabel>{{ t('side_panel.settings.favorite_groups') }}</FieldLabel>
|
||||||
|
<FieldContent>
|
||||||
|
<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>
|
||||||
|
</FieldContent>
|
||||||
|
</Field>
|
||||||
|
<Button
|
||||||
|
v-if="isSidebarDivideByFriendGroup"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
class="w-full text-sm"
|
||||||
|
@click="
|
||||||
|
isSettingsPopoverOpen = false;
|
||||||
|
isGroupOrderDialogOpen = true;
|
||||||
|
">
|
||||||
|
{{ t('side_panel.settings.edit_group_order') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
</div>
|
</div>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
@@ -259,7 +319,7 @@
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue
|
SelectValue
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { Bell, RefreshCw, Search, Settings } from 'lucide-vue-next';
|
import { Bell, ChevronDown, RefreshCw, Search, Settings } from 'lucide-vue-next';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@/components/ui/context-menu';
|
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@/components/ui/context-menu';
|
||||||
import { Field, FieldContent, FieldLabel } from '@/components/ui/field';
|
import { Field, FieldContent, FieldLabel } from '@/components/ui/field';
|
||||||
@@ -267,6 +327,7 @@
|
|||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { useMagicKeys, whenever } from '@vueuse/core';
|
import { useMagicKeys, whenever } from '@vueuse/core';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||||
import { Kbd } from '@/components/ui/kbd';
|
import { Kbd } from '@/components/ui/kbd';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { Spinner } from '@/components/ui/spinner';
|
import { Spinner } from '@/components/ui/spinner';
|
||||||
@@ -375,6 +436,7 @@
|
|||||||
const CLEAR_VALUE = '__clear__';
|
const CLEAR_VALUE = '__clear__';
|
||||||
const isGroupOrderDialogOpen = ref(false);
|
const isGroupOrderDialogOpen = ref(false);
|
||||||
const isSettingsPopoverOpen = ref(false);
|
const isSettingsPopoverOpen = ref(false);
|
||||||
|
const isAdvancedOpen = ref(false);
|
||||||
|
|
||||||
const sortOptions = computed(() => [
|
const sortOptions = computed(() => [
|
||||||
{ value: 'Sort Alphabetically', label: t('view.settings.appearance.side_panel.sorting.alphabetical') },
|
{ value: 'Sort Alphabetically', label: t('view.settings.appearance.side_panel.sorting.alphabetical') },
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ vi.mock('../../../stores', () => ({
|
|||||||
sidebarSortMethod3: ref(''),
|
sidebarSortMethod3: ref(''),
|
||||||
isSidebarGroupByInstance: ref(false),
|
isSidebarGroupByInstance: ref(false),
|
||||||
isHideFriendsInSameInstance: ref(false),
|
isHideFriendsInSameInstance: ref(false),
|
||||||
|
isSameInstanceAboveFavorites: ref(false),
|
||||||
isSidebarDivideByFriendGroup: ref(false),
|
isSidebarDivideByFriendGroup: ref(false),
|
||||||
sidebarFavoriteGroups: ref([]),
|
sidebarFavoriteGroups: ref([]),
|
||||||
setSidebarSortMethod1: vi.fn(),
|
setSidebarSortMethod1: vi.fn(),
|
||||||
@@ -45,6 +46,7 @@ vi.mock('../../../stores', () => ({
|
|||||||
setSidebarSortMethod3: vi.fn(),
|
setSidebarSortMethod3: vi.fn(),
|
||||||
setIsSidebarGroupByInstance: vi.fn(),
|
setIsSidebarGroupByInstance: vi.fn(),
|
||||||
setIsHideFriendsInSameInstance: vi.fn(),
|
setIsHideFriendsInSameInstance: vi.fn(),
|
||||||
|
setIsSameInstanceAboveFavorites: vi.fn(),
|
||||||
setIsSidebarDivideByFriendGroup: vi.fn(),
|
setIsSidebarDivideByFriendGroup: vi.fn(),
|
||||||
setSidebarFavoriteGroups: vi.fn()
|
setSidebarFavoriteGroups: vi.fn()
|
||||||
}),
|
}),
|
||||||
@@ -117,8 +119,14 @@ vi.mock('@/components/ui/kbd', () => ({
|
|||||||
vi.mock('@/components/ui/separator', () => ({
|
vi.mock('@/components/ui/separator', () => ({
|
||||||
Separator: { template: '<hr />' }
|
Separator: { template: '<hr />' }
|
||||||
}));
|
}));
|
||||||
|
vi.mock('@/components/ui/collapsible', () => ({
|
||||||
|
Collapsible: { template: '<div><slot /></div>' },
|
||||||
|
CollapsibleTrigger: { template: '<div><slot /></div>' },
|
||||||
|
CollapsibleContent: { template: '<div><slot /></div>' }
|
||||||
|
}));
|
||||||
vi.mock('lucide-vue-next', () => ({
|
vi.mock('lucide-vue-next', () => ({
|
||||||
Bell: { template: '<i />' },
|
Bell: { template: '<i />' },
|
||||||
|
ChevronDown: { template: '<i />' },
|
||||||
RefreshCw: { template: '<i />' },
|
RefreshCw: { template: '<i />' },
|
||||||
Search: { template: '<i />' },
|
Search: { template: '<i />' },
|
||||||
Settings: { template: '<i />' }
|
Settings: { template: '<i />' }
|
||||||
|
|||||||
Reference in New Issue
Block a user