mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-19 14:53:50 +02:00
Fix favs scrolling
This commit is contained in:
@@ -1,495 +1,512 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="favorites-page x-container">
|
<div class="x-container">
|
||||||
<div class="favorites-toolbar">
|
<div class="favorites-page">
|
||||||
<div>
|
<div class="favorites-toolbar">
|
||||||
<Select :model-value="sortFavorites" @update:modelValue="handleSortFavoritesChange">
|
<div>
|
||||||
<SelectTrigger size="sm" class="favorites-toolbar__select">
|
<Select :model-value="sortFavorites" @update:modelValue="handleSortFavoritesChange">
|
||||||
<span class="flex items-center gap-2">
|
<SelectTrigger size="sm" class="favorites-toolbar__select">
|
||||||
<ArrowUpDown class="h-4 w-4" />
|
<span class="flex items-center gap-2">
|
||||||
<SelectValue
|
<ArrowUpDown class="h-4 w-4" />
|
||||||
:placeholder="t('view.settings.appearance.appearance.sort_favorite_by_name')" />
|
<SelectValue
|
||||||
</span>
|
:placeholder="t('view.settings.appearance.appearance.sort_favorite_by_name')" />
|
||||||
</SelectTrigger>
|
</span>
|
||||||
<SelectContent>
|
</SelectTrigger>
|
||||||
<SelectGroup>
|
<SelectContent>
|
||||||
<SelectItem
|
<SelectGroup>
|
||||||
:value="false"
|
<SelectItem
|
||||||
:text-value="t('view.settings.appearance.appearance.sort_favorite_by_name')">
|
:value="false"
|
||||||
{{ t('view.settings.appearance.appearance.sort_favorite_by_name') }}
|
:text-value="t('view.settings.appearance.appearance.sort_favorite_by_name')">
|
||||||
</SelectItem>
|
{{ t('view.settings.appearance.appearance.sort_favorite_by_name') }}
|
||||||
<SelectItem
|
</SelectItem>
|
||||||
:value="true"
|
<SelectItem
|
||||||
:text-value="t('view.settings.appearance.appearance.sort_favorite_by_date')">
|
:value="true"
|
||||||
{{ t('view.settings.appearance.appearance.sort_favorite_by_date') }}
|
:text-value="t('view.settings.appearance.appearance.sort_favorite_by_date')">
|
||||||
</SelectItem>
|
{{ t('view.settings.appearance.appearance.sort_favorite_by_date') }}
|
||||||
</SelectGroup>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectGroup>
|
||||||
</Select>
|
</SelectContent>
|
||||||
</div>
|
</Select>
|
||||||
<div class="favorites-toolbar__right">
|
</div>
|
||||||
<InputGroupSearch
|
<div class="favorites-toolbar__right">
|
||||||
v-model="avatarFavoriteSearch"
|
<InputGroupSearch
|
||||||
class="favorites-toolbar__search"
|
v-model="avatarFavoriteSearch"
|
||||||
:placeholder="t('view.favorite.avatars.search')"
|
class="favorites-toolbar__search"
|
||||||
@input="searchAvatarFavorites" />
|
:placeholder="t('view.favorite.avatars.search')"
|
||||||
<DropdownMenu v-model:open="avatarToolbarMenuOpen">
|
@input="searchAvatarFavorites" />
|
||||||
<DropdownMenuTrigger as-child>
|
<DropdownMenu v-model:open="avatarToolbarMenuOpen">
|
||||||
<Button class="rounded-full" size="icon-sm" variant="ghost"> <Ellipsis /> </Button>
|
<DropdownMenuTrigger as-child>
|
||||||
</DropdownMenuTrigger>
|
<Button class="rounded-full" size="icon-sm" variant="ghost"> <Ellipsis /> </Button>
|
||||||
<DropdownMenuContent class="favorites-dropdown">
|
</DropdownMenuTrigger>
|
||||||
<li class="favorites-dropdown__control" @click.stop>
|
<DropdownMenuContent class="favorites-dropdown">
|
||||||
<div class="favorites-dropdown__control-header">
|
<li class="favorites-dropdown__control" @click.stop>
|
||||||
<span>Scale</span>
|
<div class="favorites-dropdown__control-header">
|
||||||
<span class="favorites-dropdown__control-value">{{ avatarCardScalePercent }}%</span>
|
<span>Scale</span>
|
||||||
</div>
|
<span class="favorites-dropdown__control-value">{{ avatarCardScalePercent }}%</span>
|
||||||
<Slider
|
|
||||||
v-model="avatarCardScaleValue"
|
|
||||||
class="favorites-dropdown__slider"
|
|
||||||
:min="avatarCardScaleSlider.min"
|
|
||||||
:max="avatarCardScaleSlider.max"
|
|
||||||
:step="avatarCardScaleSlider.step" />
|
|
||||||
</li>
|
|
||||||
<li class="favorites-dropdown__control" @click.stop>
|
|
||||||
<div class="favorites-dropdown__control-header">
|
|
||||||
<span>Spacing</span>
|
|
||||||
<span class="favorites-dropdown__control-value"> {{ avatarCardSpacingPercent }}% </span>
|
|
||||||
</div>
|
|
||||||
<Slider
|
|
||||||
v-model="avatarCardSpacingValue"
|
|
||||||
class="favorites-dropdown__slider"
|
|
||||||
:min="avatarCardSpacingSlider.min"
|
|
||||||
:max="avatarCardSpacingSlider.max"
|
|
||||||
:step="avatarCardSpacingSlider.step" />
|
|
||||||
</li>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem @click="handleAvatarImportClick">
|
|
||||||
{{ t('view.favorite.import') }}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem @click="handleAvatarExportClick">
|
|
||||||
{{ t('view.favorite.export') }}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ResizablePanelGroup
|
|
||||||
ref="avatarSplitterGroupRef"
|
|
||||||
direction="horizontal"
|
|
||||||
class="favorites-splitter"
|
|
||||||
@layout="handleAvatarSplitterLayout">
|
|
||||||
<ResizablePanel
|
|
||||||
ref="avatarSplitterPanelRef"
|
|
||||||
:default-size="avatarSplitterDefaultSize"
|
|
||||||
:min-size="avatarSplitterMinSize"
|
|
||||||
:max-size="avatarSplitterMaxSize"
|
|
||||||
:collapsed-size="0"
|
|
||||||
collapsible
|
|
||||||
:order="1">
|
|
||||||
<div class="favorites-groups-panel">
|
|
||||||
<div class="group-section">
|
|
||||||
<div class="group-section__header">
|
|
||||||
<span>{{ t('view.favorite.avatars.vrchat_favorites') }}</span>
|
|
||||||
<TooltipWrapper side="bottom" :content="t('view.favorite.refresh_favorites_tooltip')">
|
|
||||||
<Button
|
|
||||||
class="rounded-full"
|
|
||||||
variant="outline"
|
|
||||||
size="icon-sm"
|
|
||||||
:disabled="isFavoriteLoading"
|
|
||||||
@click.stop="handleRefreshFavorites">
|
|
||||||
<Spinner v-if="isFavoriteLoading" />
|
|
||||||
<RefreshCw v-else />
|
|
||||||
</Button>
|
|
||||||
</TooltipWrapper>
|
|
||||||
</div>
|
|
||||||
<div class="group-section__list">
|
|
||||||
<template v-if="favoriteAvatarGroups.length">
|
|
||||||
<div
|
|
||||||
v-for="group in favoriteAvatarGroups"
|
|
||||||
:key="group.key"
|
|
||||||
:class="[
|
|
||||||
'group-item',
|
|
||||||
{ 'is-active': !hasSearchInput && isGroupActive('remote', group.key) }
|
|
||||||
]"
|
|
||||||
@click="handleGroupClick('remote', group.key)">
|
|
||||||
<div class="group-item__top">
|
|
||||||
<span class="group-item__name">{{ group.displayName }}</span>
|
|
||||||
<span class="group-item__count">{{ group.count }}/{{ group.capacity }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="group-item__bottom">
|
|
||||||
<Badge variant="outline">
|
|
||||||
{{ t(`view.favorite.visibility.${group.visibility}`) }}
|
|
||||||
</Badge>
|
|
||||||
<DropdownMenu
|
|
||||||
:open="activeGroupMenu === remoteGroupMenuKey(group.key)"
|
|
||||||
@update:open="
|
|
||||||
handleGroupMenuVisible(remoteGroupMenuKey(group.key), $event)
|
|
||||||
">
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button class="rounded-full" variant="ghost" size="icon-sm" @click.stop>
|
|
||||||
<MoreHorizontal />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent side="right" class="w-55">
|
|
||||||
<DropdownMenuItem @click="handleRemoteRename(group)">
|
|
||||||
<span>{{ t('view.favorite.rename_tooltip') }}</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSub>
|
|
||||||
<DropdownMenuSubTrigger>
|
|
||||||
<span>{{ t('view.favorite.visibility_tooltip') }}</span>
|
|
||||||
</DropdownMenuSubTrigger>
|
|
||||||
<DropdownMenuPortal>
|
|
||||||
<DropdownMenuSubContent side="right" align="start" class="w-45">
|
|
||||||
<DropdownMenuCheckboxItem
|
|
||||||
v-for="visibility in avatarGroupVisibilityOptions"
|
|
||||||
:key="visibility"
|
|
||||||
:model-value="group.visibility === visibility"
|
|
||||||
indicator-position="right"
|
|
||||||
@select="handleVisibilitySelection(group, visibility)">
|
|
||||||
<span>{{
|
|
||||||
t(`view.favorite.visibility.${visibility}`)
|
|
||||||
}}</span>
|
|
||||||
</DropdownMenuCheckboxItem>
|
|
||||||
</DropdownMenuSubContent>
|
|
||||||
</DropdownMenuPortal>
|
|
||||||
</DropdownMenuSub>
|
|
||||||
<DropdownMenuItem
|
|
||||||
variant="destructive"
|
|
||||||
@click="handleRemoteClear(group)">
|
|
||||||
<span>{{ t('view.favorite.clear') }}</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
<Slider
|
||||||
<template v-else>
|
v-model="avatarCardScaleValue"
|
||||||
<div
|
class="favorites-dropdown__slider"
|
||||||
v-for="group in avatarGroupPlaceholders"
|
:min="avatarCardScaleSlider.min"
|
||||||
:key="group.key"
|
:max="avatarCardScaleSlider.max"
|
||||||
:class="[
|
:step="avatarCardScaleSlider.step" />
|
||||||
'group-item',
|
</li>
|
||||||
'group-item--placeholder',
|
<li class="favorites-dropdown__control" @click.stop>
|
||||||
{ 'is-active': !hasSearchInput && isGroupActive('remote', group.key) }
|
<div class="favorites-dropdown__control-header">
|
||||||
]">
|
<span>Spacing</span>
|
||||||
<div class="group-item__top">
|
<span class="favorites-dropdown__control-value">
|
||||||
<span class="group-item__name">{{ group.displayName }}</span>
|
{{ avatarCardSpacingPercent }}%
|
||||||
<span class="group-item__count">--/--</span>
|
</span>
|
||||||
</div>
|
|
||||||
<div class="group-item__bottom">
|
|
||||||
<div class="group-item__placeholder-tag"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
<Slider
|
||||||
</div>
|
v-model="avatarCardSpacingValue"
|
||||||
</div>
|
class="favorites-dropdown__slider"
|
||||||
<div class="group-section">
|
:min="avatarCardSpacingSlider.min"
|
||||||
<div class="group-section__header">
|
:max="avatarCardSpacingSlider.max"
|
||||||
<span>{{ t('view.favorite.avatars.local_favorites') }}</span>
|
:step="avatarCardSpacingSlider.step" />
|
||||||
<template v-if="!refreshingLocalFavorites">
|
</li>
|
||||||
<Button
|
<DropdownMenuSeparator />
|
||||||
class="rounded-full"
|
<DropdownMenuItem @click="handleAvatarImportClick">
|
||||||
size="icon"
|
{{ t('view.favorite.import') }}
|
||||||
variant="outline"
|
</DropdownMenuItem>
|
||||||
@click.stop="refreshLocalAvatarFavorites"
|
<DropdownMenuItem @click="handleAvatarExportClick">
|
||||||
><RefreshCcw
|
{{ t('view.favorite.export') }}
|
||||||
/></Button>
|
</DropdownMenuItem>
|
||||||
</template>
|
</DropdownMenuContent>
|
||||||
<Button size="sm" variant="ghost" v-else @click.stop="cancelLocalAvatarRefresh">
|
</DropdownMenu>
|
||||||
<Loader />
|
</div>
|
||||||
|
</div>
|
||||||
{{ t('view.favorite.avatars.cancel_refresh') }}
|
<ResizablePanelGroup
|
||||||
</Button>
|
ref="avatarSplitterGroupRef"
|
||||||
</div>
|
direction="horizontal"
|
||||||
<div class="group-section__list">
|
class="favorites-splitter"
|
||||||
<template v-if="localAvatarFavoriteGroups.length">
|
@layout="handleAvatarSplitterLayout">
|
||||||
<div
|
<ResizablePanel
|
||||||
v-for="group in localAvatarFavoriteGroups"
|
ref="avatarSplitterPanelRef"
|
||||||
:key="group"
|
:default-size="avatarSplitterDefaultSize"
|
||||||
:class="[
|
:min-size="avatarSplitterMinSize"
|
||||||
'group-item',
|
:max-size="avatarSplitterMaxSize"
|
||||||
{ 'is-active': !hasSearchInput && isGroupActive('local', group) }
|
:collapsed-size="0"
|
||||||
]"
|
collapsible
|
||||||
@click="handleGroupClick('local', group)">
|
:order="1">
|
||||||
<div class="group-item__top">
|
<div class="favorites-groups-panel">
|
||||||
<span class="group-item__name">{{ group }}</span>
|
<div class="group-section">
|
||||||
<div class="group-item__right">
|
<div class="group-section__header">
|
||||||
<span class="group-item__count">{{
|
<span>{{ t('view.favorite.avatars.vrchat_favorites') }}</span>
|
||||||
localAvatarFavGroupLength(group)
|
<TooltipWrapper side="bottom" :content="t('view.favorite.refresh_favorites_tooltip')">
|
||||||
}}</span>
|
<Button
|
||||||
|
class="rounded-full"
|
||||||
|
variant="outline"
|
||||||
|
size="icon-sm"
|
||||||
|
:disabled="isFavoriteLoading"
|
||||||
|
@click.stop="handleRefreshFavorites">
|
||||||
|
<Spinner v-if="isFavoriteLoading" />
|
||||||
|
<RefreshCw v-else />
|
||||||
|
</Button>
|
||||||
|
</TooltipWrapper>
|
||||||
|
</div>
|
||||||
|
<div class="group-section__list">
|
||||||
|
<template v-if="favoriteAvatarGroups.length">
|
||||||
|
<div
|
||||||
|
v-for="group in favoriteAvatarGroups"
|
||||||
|
:key="group.key"
|
||||||
|
:class="[
|
||||||
|
'group-item',
|
||||||
|
{ 'is-active': !hasSearchInput && isGroupActive('remote', group.key) }
|
||||||
|
]"
|
||||||
|
@click="handleGroupClick('remote', group.key)">
|
||||||
|
<div class="group-item__top">
|
||||||
|
<span class="group-item__name">{{ group.displayName }}</span>
|
||||||
|
<span class="group-item__count"
|
||||||
|
>{{ group.count }}/{{ group.capacity }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="group-item__bottom">
|
||||||
|
<Badge variant="outline">
|
||||||
|
{{ t(`view.favorite.visibility.${group.visibility}`) }}
|
||||||
|
</Badge>
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
:open="activeGroupMenu === localGroupMenuKey(group)"
|
:open="activeGroupMenu === remoteGroupMenuKey(group.key)"
|
||||||
@update:open="handleGroupMenuVisible(localGroupMenuKey(group), $event)">
|
@update:open="
|
||||||
|
handleGroupMenuVisible(remoteGroupMenuKey(group.key), $event)
|
||||||
|
">
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
class="rounded-full"
|
class="rounded-full"
|
||||||
size="icon-sm"
|
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@click.stop
|
size="icon-sm"
|
||||||
><Ellipsis
|
@click.stop>
|
||||||
/></Button>
|
<MoreHorizontal />
|
||||||
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent side="right" class="w-50">
|
<DropdownMenuContent side="right" class="w-55">
|
||||||
<DropdownMenuItem @click="handleLocalRename(group)">
|
<DropdownMenuItem @click="handleRemoteRename(group)">
|
||||||
<span>{{ t('view.favorite.rename_tooltip') }}</span>
|
<span>{{ t('view.favorite.rename_tooltip') }}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem @click="handleCheckInvalidAvatars(group)">
|
<DropdownMenuSub>
|
||||||
<span>{{ t('view.favorite.avatars.check_invalid') }}</span>
|
<DropdownMenuSubTrigger>
|
||||||
</DropdownMenuItem>
|
<span>{{ t('view.favorite.visibility_tooltip') }}</span>
|
||||||
|
</DropdownMenuSubTrigger>
|
||||||
|
<DropdownMenuPortal>
|
||||||
|
<DropdownMenuSubContent
|
||||||
|
side="right"
|
||||||
|
align="start"
|
||||||
|
class="w-45">
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
v-for="visibility in avatarGroupVisibilityOptions"
|
||||||
|
:key="visibility"
|
||||||
|
:model-value="group.visibility === visibility"
|
||||||
|
indicator-position="right"
|
||||||
|
@select="
|
||||||
|
handleVisibilitySelection(group, visibility)
|
||||||
|
">
|
||||||
|
<span>{{
|
||||||
|
t(`view.favorite.visibility.${visibility}`)
|
||||||
|
}}</span>
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
</DropdownMenuSubContent>
|
||||||
|
</DropdownMenuPortal>
|
||||||
|
</DropdownMenuSub>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
@click="handleLocalDelete(group)">
|
@click="handleRemoteClear(group)">
|
||||||
<span>{{ t('view.favorite.delete_tooltip') }}</span>
|
<span>{{ t('view.favorite.clear') }}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div v-else class="group-empty">
|
|
||||||
<DataTableEmpty type="nodata" />
|
|
||||||
</div>
|
|
||||||
<TooltipWrapper
|
|
||||||
v-if="!isCreatingLocalGroup"
|
|
||||||
:disabled="isLocalUserVrcPlusSupporter"
|
|
||||||
:content="t('view.favorite.avatars.local_favorites')">
|
|
||||||
<div
|
|
||||||
:class="[
|
|
||||||
'group-item',
|
|
||||||
'group-item--new',
|
|
||||||
{ 'is-disabled': !isLocalUserVrcPlusSupporter }
|
|
||||||
]"
|
|
||||||
@click="startLocalGroupCreation">
|
|
||||||
<Plus />
|
|
||||||
<span>{{ t('view.favorite.avatars.new_group') }}</span>
|
|
||||||
</div>
|
|
||||||
</TooltipWrapper>
|
|
||||||
<InputGroupField
|
|
||||||
v-else
|
|
||||||
ref="newLocalGroupInput"
|
|
||||||
v-model="newLocalGroupName"
|
|
||||||
size="sm"
|
|
||||||
class="group-item__input"
|
|
||||||
:placeholder="t('view.favorite.avatars.new_group')"
|
|
||||||
@keyup.enter="handleLocalGroupCreationConfirm"
|
|
||||||
@keyup.esc="cancelLocalGroupCreation"
|
|
||||||
@blur="cancelLocalGroupCreation" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="group-section">
|
|
||||||
<div class="group-section__header">
|
|
||||||
<span>Local History</span>
|
|
||||||
<DropdownMenu
|
|
||||||
:open="activeGroupMenu === historyGroupMenuKey"
|
|
||||||
@update:open="handleGroupMenuVisible(historyGroupMenuKey, $event)">
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button class="rounded-full" size="icon-sm" variant="ghost" @click.stop
|
|
||||||
><Ellipsis
|
|
||||||
/></Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent side="right" class="w-45">
|
|
||||||
<DropdownMenuItem variant="destructive" @click="handleHistoryClear">
|
|
||||||
<span>{{ t('view.favorite.clear_tooltip') }}</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
<div class="group-section__list">
|
|
||||||
<div
|
|
||||||
:class="[
|
|
||||||
'group-item',
|
|
||||||
{ 'is-active': !hasSearchInput && isGroupActive('history', historyGroupKey) }
|
|
||||||
]"
|
|
||||||
@click="handleGroupClick('history', historyGroupKey)">
|
|
||||||
<div class="group-item__top">
|
|
||||||
<span class="group-item__name">Local History</span>
|
|
||||||
<span class="group-item__count">{{ avatarHistory.length }}/100</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ResizablePanel>
|
|
||||||
<ResizableHandle @dragging="setAvatarSplitterDragging" />
|
|
||||||
<ResizablePanel :order="2">
|
|
||||||
<div class="favorites-content">
|
|
||||||
<div class="favorites-content__header">
|
|
||||||
<div class="favorites-content__title">
|
|
||||||
<span v-if="isSearchActive">{{ t('view.favorite.avatars.search') }}</span>
|
|
||||||
<template v-else-if="activeRemoteGroup">
|
|
||||||
<span>
|
|
||||||
{{ activeRemoteGroup.displayName }}
|
|
||||||
<small>{{ activeRemoteGroup.count }}/{{ activeRemoteGroup.capacity }}</small>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="activeLocalGroupName">
|
|
||||||
<span>
|
|
||||||
{{ activeLocalGroupName }}
|
|
||||||
<small>{{ activeLocalGroupCount }}</small>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="isHistorySelected">
|
|
||||||
<span>
|
|
||||||
Local History
|
|
||||||
<small>{{ avatarHistory.length }}/100</small>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<span v-else>No Group Selected</span>
|
|
||||||
</div>
|
|
||||||
<div class="favorites-content__edit">
|
|
||||||
<span>{{ t('view.favorite.edit_mode') }}</span>
|
|
||||||
<Switch v-model="avatarEditMode" :disabled="isSearchActive || !activeRemoteGroup" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="favorites-content__edit-actions">
|
|
||||||
<div
|
|
||||||
v-if="avatarEditMode && !isSearchActive && activeRemoteGroup"
|
|
||||||
class="favorites-content__actions">
|
|
||||||
<Button size="sm" variant="outline" @click="toggleSelectAllAvatars">
|
|
||||||
{{
|
|
||||||
isAllAvatarsSelected
|
|
||||||
? t('view.favorite.deselect_all')
|
|
||||||
: t('view.favorite.select_all')
|
|
||||||
}}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="secondary"
|
|
||||||
:disabled="!hasAvatarSelection"
|
|
||||||
@click="clearSelectedAvatars">
|
|
||||||
{{ t('view.favorite.clear') }}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
:disabled="!hasAvatarSelection"
|
|
||||||
@click="copySelectedAvatars">
|
|
||||||
{{ t('view.favorite.copy') }}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
:disabled="!hasAvatarSelection"
|
|
||||||
@click="showAvatarBulkUnfavoriteSelectionConfirm">
|
|
||||||
{{ t('view.favorite.bulk_unfavorite') }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div ref="avatarFavoritesContainerRef" class="favorites-content__list">
|
|
||||||
<template v-if="isSearchActive">
|
|
||||||
<div class="favorites-content__scroll favorites-content__scroll--native">
|
|
||||||
<div
|
|
||||||
v-if="avatarFavoriteSearchResults.length"
|
|
||||||
class="favorites-search-grid"
|
|
||||||
:style="avatarFavoritesGridStyle(avatarFavoriteSearchResults.length)">
|
|
||||||
<div
|
|
||||||
v-for="favorite in avatarFavoriteSearchResults"
|
|
||||||
:key="favorite.id"
|
|
||||||
class="favorites-search-card"
|
|
||||||
@click="showAvatarDialog(favorite.id)">
|
|
||||||
<div class="favorites-search-card__content">
|
|
||||||
<div
|
|
||||||
class="favorites-search-card__avatar"
|
|
||||||
:class="{ 'is-empty': !favorite.thumbnailImageUrl }">
|
|
||||||
<img
|
|
||||||
v-if="favorite.thumbnailImageUrl"
|
|
||||||
:src="favorite.thumbnailImageUrl"
|
|
||||||
loading="lazy" />
|
|
||||||
</div>
|
|
||||||
<div class="favorites-search-card__detail">
|
|
||||||
<div class="favorites-search-card__title">
|
|
||||||
<span class="name">{{ favorite.name }}</span>
|
|
||||||
</div>
|
|
||||||
<span class="text-xs">{{ favorite.authorName }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else class="favorites-empty">
|
|
||||||
<DataTableEmpty type="nomatch" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="activeRemoteGroup">
|
|
||||||
<div class="favorites-content__scroll favorites-content__scroll--native">
|
|
||||||
<template v-if="currentRemoteFavorites.length">
|
|
||||||
<div
|
|
||||||
class="favorites-card-list"
|
|
||||||
:style="avatarFavoritesGridStyle(currentRemoteFavorites.length)">
|
|
||||||
<FavoritesAvatarItem
|
|
||||||
v-for="favorite in currentRemoteFavorites"
|
|
||||||
:key="favorite.id"
|
|
||||||
:favorite="favorite"
|
|
||||||
:group="activeRemoteGroup"
|
|
||||||
:selected="selectedFavoriteAvatars.includes(favorite.id)"
|
|
||||||
:edit-mode="avatarEditMode"
|
|
||||||
@toggle-select="toggleAvatarSelection(favorite.id, $event)"
|
|
||||||
@click="showAvatarDialog(favorite.id)" />
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<div v-else class="favorites-empty">
|
<template v-else>
|
||||||
<DataTableEmpty type="nodata" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="!remoteAvatarGroupsResolved">
|
|
||||||
<div class="favorites-content__scroll favorites-content__scroll--native">
|
|
||||||
<div
|
|
||||||
class="favorites-card-list"
|
|
||||||
:style="avatarFavoritesGridStyle(avatarGroupPlaceholders.length)">
|
|
||||||
<div
|
<div
|
||||||
v-for="group in avatarGroupPlaceholders"
|
v-for="group in avatarGroupPlaceholders"
|
||||||
:key="group.key"
|
:key="group.key"
|
||||||
class="favorites-card-placeholder-box"></div>
|
:class="[
|
||||||
</div>
|
'group-item',
|
||||||
</div>
|
'group-item--placeholder',
|
||||||
</template>
|
{ 'is-active': !hasSearchInput && isGroupActive('remote', group.key) }
|
||||||
<template v-else-if="activeLocalGroupName">
|
]">
|
||||||
<ScrollArea class="favorites-content__scroll">
|
<div class="group-item__top">
|
||||||
<template v-if="currentLocalFavorites.length">
|
<span class="group-item__name">{{ group.displayName }}</span>
|
||||||
<div
|
<span class="group-item__count">--/--</span>
|
||||||
class="favorites-card-list"
|
</div>
|
||||||
:style="avatarFavoritesGridStyle(currentLocalFavorites.length)">
|
<div class="group-item__bottom">
|
||||||
<FavoritesAvatarItem
|
<div class="group-item__placeholder-tag"></div>
|
||||||
v-for="favorite in currentLocalFavorites"
|
</div>
|
||||||
:key="favorite.id"
|
|
||||||
:favorite="favorite"
|
|
||||||
:group="activeLocalGroupName"
|
|
||||||
is-local-favorite
|
|
||||||
:edit-mode="avatarEditMode"
|
|
||||||
@click="showAvatarDialog(favorite.id)" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-else class="favorites-empty">
|
</div>
|
||||||
<DataTableEmpty type="nodata" />
|
</div>
|
||||||
</div>
|
<div class="group-section">
|
||||||
</ScrollArea>
|
<div class="group-section__header">
|
||||||
</template>
|
<span>{{ t('view.favorite.avatars.local_favorites') }}</span>
|
||||||
<template v-else-if="isHistorySelected">
|
<template v-if="!refreshingLocalFavorites">
|
||||||
<div class="favorites-content__scroll favorites-content__scroll--native">
|
<Button
|
||||||
<template v-if="avatarHistory.length">
|
class="rounded-full"
|
||||||
|
size="icon"
|
||||||
|
variant="outline"
|
||||||
|
@click.stop="refreshLocalAvatarFavorites"
|
||||||
|
><RefreshCcw
|
||||||
|
/></Button>
|
||||||
|
</template>
|
||||||
|
<Button size="sm" variant="ghost" v-else @click.stop="cancelLocalAvatarRefresh">
|
||||||
|
<Loader />
|
||||||
|
|
||||||
|
{{ t('view.favorite.avatars.cancel_refresh') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div class="group-section__list">
|
||||||
|
<template v-if="localAvatarFavoriteGroups.length">
|
||||||
<div
|
<div
|
||||||
class="favorites-card-list"
|
v-for="group in localAvatarFavoriteGroups"
|
||||||
:style="avatarFavoritesGridStyle(avatarHistory.length)">
|
:key="group"
|
||||||
<FavoritesAvatarLocalHistoryItem
|
:class="[
|
||||||
v-for="favorite in avatarHistory"
|
'group-item',
|
||||||
:key="favorite.id"
|
{ 'is-active': !hasSearchInput && isGroupActive('local', group) }
|
||||||
:favorite="favorite"
|
]"
|
||||||
@click="showAvatarDialog(favorite.id)" />
|
@click="handleGroupClick('local', group)">
|
||||||
|
<div class="group-item__top">
|
||||||
|
<span class="group-item__name">{{ group }}</span>
|
||||||
|
<div class="group-item__right">
|
||||||
|
<span class="group-item__count">{{
|
||||||
|
localAvatarFavGroupLength(group)
|
||||||
|
}}</span>
|
||||||
|
<DropdownMenu
|
||||||
|
:open="activeGroupMenu === localGroupMenuKey(group)"
|
||||||
|
@update:open="
|
||||||
|
handleGroupMenuVisible(localGroupMenuKey(group), $event)
|
||||||
|
">
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
class="rounded-full"
|
||||||
|
size="icon-sm"
|
||||||
|
variant="ghost"
|
||||||
|
@click.stop
|
||||||
|
><Ellipsis
|
||||||
|
/></Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent side="right" class="w-50">
|
||||||
|
<DropdownMenuItem @click="handleLocalRename(group)">
|
||||||
|
<span>{{ t('view.favorite.rename_tooltip') }}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem @click="handleCheckInvalidAvatars(group)">
|
||||||
|
<span>{{ t('view.favorite.avatars.check_invalid') }}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
variant="destructive"
|
||||||
|
@click="handleLocalDelete(group)">
|
||||||
|
<span>{{ t('view.favorite.delete_tooltip') }}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-else class="favorites-empty">
|
<div v-else class="group-empty">
|
||||||
<DataTableEmpty type="nodata" />
|
<DataTableEmpty type="nodata" />
|
||||||
</div>
|
</div>
|
||||||
|
<TooltipWrapper
|
||||||
|
v-if="!isCreatingLocalGroup"
|
||||||
|
:disabled="isLocalUserVrcPlusSupporter"
|
||||||
|
:content="t('view.favorite.avatars.local_favorites')">
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
'group-item',
|
||||||
|
'group-item--new',
|
||||||
|
{ 'is-disabled': !isLocalUserVrcPlusSupporter }
|
||||||
|
]"
|
||||||
|
@click="startLocalGroupCreation">
|
||||||
|
<Plus />
|
||||||
|
<span>{{ t('view.favorite.avatars.new_group') }}</span>
|
||||||
|
</div>
|
||||||
|
</TooltipWrapper>
|
||||||
|
<InputGroupField
|
||||||
|
v-else
|
||||||
|
ref="newLocalGroupInput"
|
||||||
|
v-model="newLocalGroupName"
|
||||||
|
size="sm"
|
||||||
|
class="group-item__input"
|
||||||
|
:placeholder="t('view.favorite.avatars.new_group')"
|
||||||
|
@keyup.enter="handleLocalGroupCreationConfirm"
|
||||||
|
@keyup.esc="cancelLocalGroupCreation"
|
||||||
|
@blur="cancelLocalGroupCreation" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
<template v-else>
|
<div class="group-section">
|
||||||
<div class="favorites-empty">No Group Selected</div>
|
<div class="group-section__header">
|
||||||
</template>
|
<span>Local History</span>
|
||||||
|
<DropdownMenu
|
||||||
|
:open="activeGroupMenu === historyGroupMenuKey"
|
||||||
|
@update:open="handleGroupMenuVisible(historyGroupMenuKey, $event)">
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button class="rounded-full" size="icon-sm" variant="ghost" @click.stop
|
||||||
|
><Ellipsis
|
||||||
|
/></Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent side="right" class="w-45">
|
||||||
|
<DropdownMenuItem variant="destructive" @click="handleHistoryClear">
|
||||||
|
<span>{{ t('view.favorite.clear_tooltip') }}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
<div class="group-section__list">
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
'group-item',
|
||||||
|
{ 'is-active': !hasSearchInput && isGroupActive('history', historyGroupKey) }
|
||||||
|
]"
|
||||||
|
@click="handleGroupClick('history', historyGroupKey)">
|
||||||
|
<div class="group-item__top">
|
||||||
|
<span class="group-item__name">Local History</span>
|
||||||
|
<span class="group-item__count">{{ avatarHistory.length }}/100</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ResizablePanel>
|
||||||
</ResizablePanel>
|
<ResizableHandle @dragging="setAvatarSplitterDragging" />
|
||||||
</ResizablePanelGroup>
|
<ResizablePanel :order="2">
|
||||||
|
<div class="favorites-content">
|
||||||
|
<div class="favorites-content__header">
|
||||||
|
<div class="favorites-content__title">
|
||||||
|
<span v-if="isSearchActive">{{ t('view.favorite.avatars.search') }}</span>
|
||||||
|
<template v-else-if="activeRemoteGroup">
|
||||||
|
<span>
|
||||||
|
{{ activeRemoteGroup.displayName }}
|
||||||
|
<small>{{ activeRemoteGroup.count }}/{{ activeRemoteGroup.capacity }}</small>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="activeLocalGroupName">
|
||||||
|
<span>
|
||||||
|
{{ activeLocalGroupName }}
|
||||||
|
<small>{{ activeLocalGroupCount }}</small>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="isHistorySelected">
|
||||||
|
<span>
|
||||||
|
Local History
|
||||||
|
<small>{{ avatarHistory.length }}/100</small>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<span v-else>No Group Selected</span>
|
||||||
|
</div>
|
||||||
|
<div class="favorites-content__edit">
|
||||||
|
<span>{{ t('view.favorite.edit_mode') }}</span>
|
||||||
|
<Switch v-model="avatarEditMode" :disabled="isSearchActive || !activeRemoteGroup" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="favorites-content__edit-actions">
|
||||||
|
<div
|
||||||
|
v-if="avatarEditMode && !isSearchActive && activeRemoteGroup"
|
||||||
|
class="favorites-content__actions">
|
||||||
|
<Button size="sm" variant="outline" @click="toggleSelectAllAvatars">
|
||||||
|
{{
|
||||||
|
isAllAvatarsSelected
|
||||||
|
? t('view.favorite.deselect_all')
|
||||||
|
: t('view.favorite.select_all')
|
||||||
|
}}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="secondary"
|
||||||
|
:disabled="!hasAvatarSelection"
|
||||||
|
@click="clearSelectedAvatars">
|
||||||
|
{{ t('view.favorite.clear') }}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
:disabled="!hasAvatarSelection"
|
||||||
|
@click="copySelectedAvatars">
|
||||||
|
{{ t('view.favorite.copy') }}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
:disabled="!hasAvatarSelection"
|
||||||
|
@click="showAvatarBulkUnfavoriteSelectionConfirm">
|
||||||
|
{{ t('view.favorite.bulk_unfavorite') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div ref="avatarFavoritesContainerRef" class="favorites-content__list">
|
||||||
|
<template v-if="isSearchActive">
|
||||||
|
<div class="favorites-content__scroll favorites-content__scroll--native">
|
||||||
|
<div
|
||||||
|
v-if="avatarFavoriteSearchResults.length"
|
||||||
|
class="favorites-search-grid"
|
||||||
|
:style="avatarFavoritesGridStyle(avatarFavoriteSearchResults.length)">
|
||||||
|
<div
|
||||||
|
v-for="favorite in avatarFavoriteSearchResults"
|
||||||
|
:key="favorite.id"
|
||||||
|
class="favorites-search-card"
|
||||||
|
@click="showAvatarDialog(favorite.id)">
|
||||||
|
<div class="favorites-search-card__content">
|
||||||
|
<div
|
||||||
|
class="favorites-search-card__avatar"
|
||||||
|
:class="{ 'is-empty': !favorite.thumbnailImageUrl }">
|
||||||
|
<img
|
||||||
|
v-if="favorite.thumbnailImageUrl"
|
||||||
|
:src="favorite.thumbnailImageUrl"
|
||||||
|
loading="lazy" />
|
||||||
|
</div>
|
||||||
|
<div class="favorites-search-card__detail">
|
||||||
|
<div class="favorites-search-card__title">
|
||||||
|
<span class="name">{{ favorite.name }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs">{{ favorite.authorName }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="favorites-empty">
|
||||||
|
<DataTableEmpty type="nomatch" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="activeRemoteGroup">
|
||||||
|
<div class="favorites-content__scroll favorites-content__scroll--native">
|
||||||
|
<template v-if="currentRemoteFavorites.length">
|
||||||
|
<div
|
||||||
|
class="favorites-card-list"
|
||||||
|
:style="avatarFavoritesGridStyle(currentRemoteFavorites.length)">
|
||||||
|
<FavoritesAvatarItem
|
||||||
|
v-for="favorite in currentRemoteFavorites"
|
||||||
|
:key="favorite.id"
|
||||||
|
:favorite="favorite"
|
||||||
|
:group="activeRemoteGroup"
|
||||||
|
:selected="selectedFavoriteAvatars.includes(favorite.id)"
|
||||||
|
:edit-mode="avatarEditMode"
|
||||||
|
@toggle-select="toggleAvatarSelection(favorite.id, $event)"
|
||||||
|
@click="showAvatarDialog(favorite.id)" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-else class="favorites-empty">
|
||||||
|
<DataTableEmpty type="nodata" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="!remoteAvatarGroupsResolved">
|
||||||
|
<div class="favorites-content__scroll favorites-content__scroll--native">
|
||||||
|
<div
|
||||||
|
class="favorites-card-list"
|
||||||
|
:style="avatarFavoritesGridStyle(avatarGroupPlaceholders.length)">
|
||||||
|
<div
|
||||||
|
v-for="group in avatarGroupPlaceholders"
|
||||||
|
:key="group.key"
|
||||||
|
class="favorites-card-placeholder-box"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="activeLocalGroupName">
|
||||||
|
<ScrollArea class="favorites-content__scroll">
|
||||||
|
<template v-if="currentLocalFavorites.length">
|
||||||
|
<div
|
||||||
|
class="favorites-card-list"
|
||||||
|
:style="avatarFavoritesGridStyle(currentLocalFavorites.length)">
|
||||||
|
<FavoritesAvatarItem
|
||||||
|
v-for="favorite in currentLocalFavorites"
|
||||||
|
:key="favorite.id"
|
||||||
|
:favorite="favorite"
|
||||||
|
:group="activeLocalGroupName"
|
||||||
|
is-local-favorite
|
||||||
|
:edit-mode="avatarEditMode"
|
||||||
|
@click="showAvatarDialog(favorite.id)" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-else class="favorites-empty">
|
||||||
|
<DataTableEmpty type="nodata" />
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="isHistorySelected">
|
||||||
|
<div class="favorites-content__scroll favorites-content__scroll--native">
|
||||||
|
<template v-if="avatarHistory.length">
|
||||||
|
<div
|
||||||
|
class="favorites-card-list"
|
||||||
|
:style="avatarFavoritesGridStyle(avatarHistory.length)">
|
||||||
|
<FavoritesAvatarLocalHistoryItem
|
||||||
|
v-for="favorite in avatarHistory"
|
||||||
|
:key="favorite.id"
|
||||||
|
:favorite="favorite"
|
||||||
|
@click="showAvatarDialog(favorite.id)" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-else class="favorites-empty">
|
||||||
|
<DataTableEmpty type="nodata" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="favorites-empty">No Group Selected</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ResizablePanel>
|
||||||
|
</ResizablePanelGroup>
|
||||||
|
</div>
|
||||||
<AvatarExportDialog v-model:avatarExportDialogVisible="avatarExportDialogVisible" />
|
<AvatarExportDialog v-model:avatarExportDialogVisible="avatarExportDialogVisible" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,295 +1,309 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="favorites-page x-container">
|
<div class="x-container">
|
||||||
<div class="favorites-toolbar">
|
<div class="favorites-page">
|
||||||
<div>
|
<div class="favorites-toolbar">
|
||||||
<Select :model-value="sortFavorites" @update:modelValue="handleSortFavoritesChange">
|
<div>
|
||||||
<SelectTrigger size="sm" class="favorites-toolbar__select">
|
<Select :model-value="sortFavorites" @update:modelValue="handleSortFavoritesChange">
|
||||||
<span class="flex items-center gap-2">
|
<SelectTrigger size="sm" class="favorites-toolbar__select">
|
||||||
<ArrowUpDown class="h-4 w-4" />
|
<span class="flex items-center gap-2">
|
||||||
<SelectValue
|
<ArrowUpDown class="h-4 w-4" />
|
||||||
:placeholder="t('view.settings.appearance.appearance.sort_favorite_by_name')" />
|
<SelectValue
|
||||||
</span>
|
:placeholder="t('view.settings.appearance.appearance.sort_favorite_by_name')" />
|
||||||
</SelectTrigger>
|
</span>
|
||||||
<SelectContent>
|
</SelectTrigger>
|
||||||
<SelectGroup>
|
<SelectContent>
|
||||||
<SelectItem
|
<SelectGroup>
|
||||||
:value="false"
|
<SelectItem
|
||||||
:text-value="t('view.settings.appearance.appearance.sort_favorite_by_name')">
|
:value="false"
|
||||||
{{ t('view.settings.appearance.appearance.sort_favorite_by_name') }}
|
:text-value="t('view.settings.appearance.appearance.sort_favorite_by_name')">
|
||||||
</SelectItem>
|
{{ t('view.settings.appearance.appearance.sort_favorite_by_name') }}
|
||||||
<SelectItem
|
</SelectItem>
|
||||||
:value="true"
|
<SelectItem
|
||||||
:text-value="t('view.settings.appearance.appearance.sort_favorite_by_date')">
|
:value="true"
|
||||||
{{ t('view.settings.appearance.appearance.sort_favorite_by_date') }}
|
:text-value="t('view.settings.appearance.appearance.sort_favorite_by_date')">
|
||||||
</SelectItem>
|
{{ t('view.settings.appearance.appearance.sort_favorite_by_date') }}
|
||||||
</SelectGroup>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectGroup>
|
||||||
</Select>
|
</SelectContent>
|
||||||
</div>
|
</Select>
|
||||||
<div class="favorites-toolbar__right">
|
|
||||||
<InputGroupSearch
|
|
||||||
v-model="friendFavoriteSearch"
|
|
||||||
class="favorites-toolbar__search"
|
|
||||||
:placeholder="t('view.favorite.worlds.search')"
|
|
||||||
@input="searchFriendFavorites" />
|
|
||||||
<DropdownMenu v-model:open="friendToolbarMenuOpen">
|
|
||||||
<DropdownMenuTrigger as-child>
|
|
||||||
<Button class="rounded-full" size="icon-sm" variant="ghost"><Ellipsis /></Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent class="favorites-dropdown">
|
|
||||||
<li class="favorites-dropdown__control" @click.stop>
|
|
||||||
<div class="favorites-dropdown__control-header">
|
|
||||||
<span>Scale</span>
|
|
||||||
<span class="favorites-dropdown__control-value"> {{ friendCardScalePercent }}% </span>
|
|
||||||
</div>
|
|
||||||
<Slider
|
|
||||||
v-model="friendCardScaleValue"
|
|
||||||
class="favorites-dropdown__slider"
|
|
||||||
:min="friendCardScaleSlider.min"
|
|
||||||
:max="friendCardScaleSlider.max"
|
|
||||||
:step="friendCardScaleSlider.step" />
|
|
||||||
</li>
|
|
||||||
<li class="favorites-dropdown__control" @click.stop>
|
|
||||||
<div class="favorites-dropdown__control-header">
|
|
||||||
<span>Spacing</span>
|
|
||||||
<span class="favorites-dropdown__control-value"> {{ friendCardSpacingPercent }}% </span>
|
|
||||||
</div>
|
|
||||||
<Slider
|
|
||||||
v-model="friendCardSpacingValue"
|
|
||||||
class="favorites-dropdown__slider"
|
|
||||||
:min="friendCardSpacingSlider.min"
|
|
||||||
:max="friendCardSpacingSlider.max"
|
|
||||||
:step="friendCardSpacingSlider.step" />
|
|
||||||
</li>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem @click="handleFriendImportClick">
|
|
||||||
{{ t('view.favorite.import') }}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem @click="handleFriendExportClick">
|
|
||||||
{{ t('view.favorite.export') }}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ResizablePanelGroup
|
|
||||||
ref="friendSplitterGroupRef"
|
|
||||||
direction="horizontal"
|
|
||||||
class="favorites-splitter"
|
|
||||||
@layout="handleFriendSplitterLayout">
|
|
||||||
<ResizablePanel
|
|
||||||
ref="friendSplitterPanelRef"
|
|
||||||
:default-size="friendSplitterDefaultSize"
|
|
||||||
:min-size="friendSplitterMinSize"
|
|
||||||
:max-size="friendSplitterMaxSize"
|
|
||||||
:collapsed-size="0"
|
|
||||||
collapsible
|
|
||||||
:order="1">
|
|
||||||
<div class="favorites-groups-panel">
|
|
||||||
<div class="group-section">
|
|
||||||
<div class="group-section__header">
|
|
||||||
<span>{{ t('view.favorite.worlds.vrchat_favorites') }}</span>
|
|
||||||
<TooltipWrapper side="bottom" :content="t('view.favorite.refresh_favorites_tooltip')">
|
|
||||||
<Button
|
|
||||||
class="rounded-full"
|
|
||||||
variant="outline"
|
|
||||||
size="icon-sm"
|
|
||||||
:disabled="isFavoriteLoading"
|
|
||||||
@click.stop="handleRefreshFavorites">
|
|
||||||
<Spinner v-if="isFavoriteLoading" />
|
|
||||||
<RefreshCw v-else />
|
|
||||||
</Button>
|
|
||||||
</TooltipWrapper>
|
|
||||||
</div>
|
|
||||||
<div class="group-section__list">
|
|
||||||
<template v-if="favoriteFriendGroups.length">
|
|
||||||
<div
|
|
||||||
v-for="group in favoriteFriendGroups"
|
|
||||||
:key="group.key"
|
|
||||||
:class="[
|
|
||||||
'group-item',
|
|
||||||
{ 'is-active': !hasSearchInput && isGroupActive('remote', group.key) }
|
|
||||||
]"
|
|
||||||
@click="handleGroupClick('remote', group.key)">
|
|
||||||
<div class="group-item__top">
|
|
||||||
<span class="group-item__name">{{ group.displayName }}</span>
|
|
||||||
<span class="group-item__count">{{ group.count }}/{{ group.capacity }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="group-item__bottom">
|
|
||||||
<Badge variant="outline">
|
|
||||||
{{ t(`view.favorite.visibility.${group.visibility}`) }}
|
|
||||||
</Badge>
|
|
||||||
<DropdownMenu
|
|
||||||
:open="activeGroupMenu === remoteGroupMenuKey(group.key)"
|
|
||||||
@update:open="
|
|
||||||
handleGroupMenuVisible(remoteGroupMenuKey(group.key), $event)
|
|
||||||
">
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button class="rounded-full" variant="ghost" size="icon-sm" @click.stop>
|
|
||||||
<MoreHorizontal />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent side="right" class="w-55">
|
|
||||||
<DropdownMenuItem @click="handleRemoteRename(group)">
|
|
||||||
<span>{{ t('view.favorite.rename_tooltip') }}</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSub>
|
|
||||||
<DropdownMenuSubTrigger>
|
|
||||||
<span>{{ t('view.favorite.visibility_tooltip') }}</span>
|
|
||||||
</DropdownMenuSubTrigger>
|
|
||||||
<DropdownMenuPortal>
|
|
||||||
<DropdownMenuSubContent
|
|
||||||
side="right"
|
|
||||||
align="start"
|
|
||||||
class="w-[180px]">
|
|
||||||
<DropdownMenuCheckboxItem
|
|
||||||
v-for="visibility in friendGroupVisibilityOptions"
|
|
||||||
:key="visibility"
|
|
||||||
:model-value="group.visibility === visibility"
|
|
||||||
indicator-position="right"
|
|
||||||
@select="handleVisibilitySelection(group, visibility)">
|
|
||||||
<span>{{
|
|
||||||
t(`view.favorite.visibility.${visibility}`)
|
|
||||||
}}</span>
|
|
||||||
</DropdownMenuCheckboxItem>
|
|
||||||
</DropdownMenuSubContent>
|
|
||||||
</DropdownMenuPortal>
|
|
||||||
</DropdownMenuSub>
|
|
||||||
<DropdownMenuItem
|
|
||||||
variant="destructive"
|
|
||||||
@click="handleRemoteClear(group)">
|
|
||||||
<span>{{ t('view.favorite.clear') }}</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div v-else class="group-empty">
|
|
||||||
<DataTableEmpty type="nodata" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</ResizablePanel>
|
<div class="favorites-toolbar__right">
|
||||||
<ResizableHandle @dragging="setFriendSplitterDragging" />
|
<InputGroupSearch
|
||||||
<ResizablePanel :order="2">
|
v-model="friendFavoriteSearch"
|
||||||
<div class="favorites-content">
|
class="favorites-toolbar__search"
|
||||||
<div class="favorites-content__header">
|
:placeholder="t('view.favorite.worlds.search')"
|
||||||
<div class="favorites-content__title">
|
@input="searchFriendFavorites" />
|
||||||
<span v-if="isSearchActive">{{ t('view.favorite.worlds.search') }}</span>
|
<DropdownMenu v-model:open="friendToolbarMenuOpen">
|
||||||
<template v-else-if="activeRemoteGroup">
|
<DropdownMenuTrigger as-child>
|
||||||
<span>
|
<Button class="rounded-full" size="icon-sm" variant="ghost"><Ellipsis /></Button>
|
||||||
{{ activeRemoteGroup.displayName }}
|
</DropdownMenuTrigger>
|
||||||
<small>{{ activeRemoteGroup.count }}/{{ activeRemoteGroup.capacity }}</small>
|
<DropdownMenuContent class="favorites-dropdown">
|
||||||
</span>
|
<li class="favorites-dropdown__control" @click.stop>
|
||||||
</template>
|
<div class="favorites-dropdown__control-header">
|
||||||
<span v-else>No Group Selected</span>
|
<span>Scale</span>
|
||||||
</div>
|
<span class="favorites-dropdown__control-value">
|
||||||
<div class="favorites-content__edit">
|
{{ friendCardScalePercent }}%
|
||||||
<span>{{ t('view.favorite.edit_mode') }}</span>
|
</span>
|
||||||
<Switch v-model="friendEditMode" :disabled="isSearchActive || !activeRemoteGroup" />
|
</div>
|
||||||
</div>
|
<Slider
|
||||||
</div>
|
v-model="friendCardScaleValue"
|
||||||
<div class="favorites-content__edit-actions">
|
class="favorites-dropdown__slider"
|
||||||
<div v-if="friendEditMode && !isSearchActive" class="favorites-content__actions">
|
:min="friendCardScaleSlider.min"
|
||||||
<Button size="sm" variant="outline" @click="toggleSelectAllFriends">
|
:max="friendCardScaleSlider.max"
|
||||||
{{
|
:step="friendCardScaleSlider.step" />
|
||||||
isAllFriendsSelected
|
</li>
|
||||||
? t('view.favorite.deselect_all')
|
<li class="favorites-dropdown__control" @click.stop>
|
||||||
: t('view.favorite.select_all')
|
<div class="favorites-dropdown__control-header">
|
||||||
}}
|
<span>Spacing</span>
|
||||||
</Button>
|
<span class="favorites-dropdown__control-value">
|
||||||
<Button
|
{{ friendCardSpacingPercent }}%
|
||||||
size="sm"
|
</span>
|
||||||
variant="secondary"
|
</div>
|
||||||
:disabled="!hasFriendSelection"
|
<Slider
|
||||||
@click="clearSelectedFriends">
|
v-model="friendCardSpacingValue"
|
||||||
{{ t('view.favorite.clear') }}
|
class="favorites-dropdown__slider"
|
||||||
</Button>
|
:min="friendCardSpacingSlider.min"
|
||||||
<Button
|
:max="friendCardSpacingSlider.max"
|
||||||
size="sm"
|
:step="friendCardSpacingSlider.step" />
|
||||||
variant="outline"
|
</li>
|
||||||
:disabled="!hasFriendSelection"
|
<DropdownMenuSeparator />
|
||||||
@click="copySelectedFriends">
|
<DropdownMenuItem @click="handleFriendImportClick">
|
||||||
{{ t('view.favorite.copy') }}
|
{{ t('view.favorite.import') }}
|
||||||
</Button>
|
</DropdownMenuItem>
|
||||||
<Button
|
<DropdownMenuItem @click="handleFriendExportClick">
|
||||||
size="sm"
|
{{ t('view.favorite.export') }}
|
||||||
variant="outline"
|
</DropdownMenuItem>
|
||||||
:disabled="!hasFriendSelection"
|
</DropdownMenuContent>
|
||||||
@click="showFriendBulkUnfavoriteSelectionConfirm">
|
</DropdownMenu>
|
||||||
{{ t('view.favorite.bulk_unfavorite') }}
|
</div>
|
||||||
</Button>
|
</div>
|
||||||
</div>
|
<ResizablePanelGroup
|
||||||
</div>
|
ref="friendSplitterGroupRef"
|
||||||
<div ref="friendFavoritesContainerRef" class="favorites-content__list">
|
direction="horizontal"
|
||||||
<template v-if="activeRemoteGroup && !isSearchActive">
|
class="favorites-splitter"
|
||||||
<div class="favorites-content__scroll favorites-content__scroll--native">
|
@layout="handleFriendSplitterLayout">
|
||||||
<template v-if="currentFriendFavorites.length">
|
<ResizablePanel
|
||||||
|
ref="friendSplitterPanelRef"
|
||||||
|
:default-size="friendSplitterDefaultSize"
|
||||||
|
:min-size="friendSplitterMinSize"
|
||||||
|
:max-size="friendSplitterMaxSize"
|
||||||
|
:collapsed-size="0"
|
||||||
|
collapsible
|
||||||
|
:order="1">
|
||||||
|
<div class="favorites-groups-panel">
|
||||||
|
<div class="group-section">
|
||||||
|
<div class="group-section__header">
|
||||||
|
<span>{{ t('view.favorite.worlds.vrchat_favorites') }}</span>
|
||||||
|
<TooltipWrapper side="bottom" :content="t('view.favorite.refresh_favorites_tooltip')">
|
||||||
|
<Button
|
||||||
|
class="rounded-full"
|
||||||
|
variant="outline"
|
||||||
|
size="icon-sm"
|
||||||
|
:disabled="isFavoriteLoading"
|
||||||
|
@click.stop="handleRefreshFavorites">
|
||||||
|
<Spinner v-if="isFavoriteLoading" />
|
||||||
|
<RefreshCw v-else />
|
||||||
|
</Button>
|
||||||
|
</TooltipWrapper>
|
||||||
|
</div>
|
||||||
|
<div class="group-section__list">
|
||||||
|
<template v-if="favoriteFriendGroups.length">
|
||||||
<div
|
<div
|
||||||
class="favorites-card-list"
|
v-for="group in favoriteFriendGroups"
|
||||||
:style="friendFavoritesGridStyle(currentFriendFavorites.length)">
|
:key="group.key"
|
||||||
<FavoritesFriendItem
|
:class="[
|
||||||
v-for="favorite in currentFriendFavorites"
|
'group-item',
|
||||||
:key="favorite.id"
|
{ 'is-active': !hasSearchInput && isGroupActive('remote', group.key) }
|
||||||
:favorite="favorite"
|
]"
|
||||||
:group="activeRemoteGroup"
|
@click="handleGroupClick('remote', group.key)">
|
||||||
:selected="selectedFavoriteFriends.includes(favorite.id)"
|
<div class="group-item__top">
|
||||||
:edit-mode="friendEditMode"
|
<span class="group-item__name">{{ group.displayName }}</span>
|
||||||
@toggle-select="toggleFriendSelection(favorite.id, $event)"
|
<span class="group-item__count"
|
||||||
@click="showUserDialog(favorite.id)" />
|
>{{ group.count }}/{{ group.capacity }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="group-item__bottom">
|
||||||
|
<Badge variant="outline">
|
||||||
|
{{ t(`view.favorite.visibility.${group.visibility}`) }}
|
||||||
|
</Badge>
|
||||||
|
<DropdownMenu
|
||||||
|
:open="activeGroupMenu === remoteGroupMenuKey(group.key)"
|
||||||
|
@update:open="
|
||||||
|
handleGroupMenuVisible(remoteGroupMenuKey(group.key), $event)
|
||||||
|
">
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
class="rounded-full"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
@click.stop>
|
||||||
|
<MoreHorizontal />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent side="right" class="w-55">
|
||||||
|
<DropdownMenuItem @click="handleRemoteRename(group)">
|
||||||
|
<span>{{ t('view.favorite.rename_tooltip') }}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSub>
|
||||||
|
<DropdownMenuSubTrigger>
|
||||||
|
<span>{{ t('view.favorite.visibility_tooltip') }}</span>
|
||||||
|
</DropdownMenuSubTrigger>
|
||||||
|
<DropdownMenuPortal>
|
||||||
|
<DropdownMenuSubContent
|
||||||
|
side="right"
|
||||||
|
align="start"
|
||||||
|
class="w-[180px]">
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
v-for="visibility in friendGroupVisibilityOptions"
|
||||||
|
:key="visibility"
|
||||||
|
:model-value="group.visibility === visibility"
|
||||||
|
indicator-position="right"
|
||||||
|
@select="
|
||||||
|
handleVisibilitySelection(group, visibility)
|
||||||
|
">
|
||||||
|
<span>{{
|
||||||
|
t(`view.favorite.visibility.${visibility}`)
|
||||||
|
}}</span>
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
</DropdownMenuSubContent>
|
||||||
|
</DropdownMenuPortal>
|
||||||
|
</DropdownMenuSub>
|
||||||
|
<DropdownMenuItem
|
||||||
|
variant="destructive"
|
||||||
|
@click="handleRemoteClear(group)">
|
||||||
|
<span>{{ t('view.favorite.clear') }}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-else class="favorites-empty">
|
<div v-else class="group-empty">
|
||||||
<DataTableEmpty type="nodata" />
|
<DataTableEmpty type="nodata" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
<template v-else-if="!isSearchActive">
|
</div>
|
||||||
<div class="favorites-empty">No Group Selected</div>
|
</ResizablePanel>
|
||||||
</template>
|
<ResizableHandle @dragging="setFriendSplitterDragging" />
|
||||||
<template v-else>
|
<ResizablePanel :order="2">
|
||||||
<div class="favorites-content__scroll favorites-content__scroll--native">
|
<div class="favorites-content">
|
||||||
<div
|
<div class="favorites-content__header">
|
||||||
v-if="friendFavoriteSearchResults.length"
|
<div class="favorites-content__title">
|
||||||
class="favorites-search-grid"
|
<span v-if="isSearchActive">{{ t('view.favorite.worlds.search') }}</span>
|
||||||
:style="friendFavoritesGridStyle(friendFavoriteSearchResults.length)">
|
<template v-else-if="activeRemoteGroup">
|
||||||
|
<span>
|
||||||
|
{{ activeRemoteGroup.displayName }}
|
||||||
|
<small>{{ activeRemoteGroup.count }}/{{ activeRemoteGroup.capacity }}</small>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<span v-else>No Group Selected</span>
|
||||||
|
</div>
|
||||||
|
<div class="favorites-content__edit">
|
||||||
|
<span>{{ t('view.favorite.edit_mode') }}</span>
|
||||||
|
<Switch v-model="friendEditMode" :disabled="isSearchActive || !activeRemoteGroup" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="favorites-content__edit-actions">
|
||||||
|
<div v-if="friendEditMode && !isSearchActive" class="favorites-content__actions">
|
||||||
|
<Button size="sm" variant="outline" @click="toggleSelectAllFriends">
|
||||||
|
{{
|
||||||
|
isAllFriendsSelected
|
||||||
|
? t('view.favorite.deselect_all')
|
||||||
|
: t('view.favorite.select_all')
|
||||||
|
}}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="secondary"
|
||||||
|
:disabled="!hasFriendSelection"
|
||||||
|
@click="clearSelectedFriends">
|
||||||
|
{{ t('view.favorite.clear') }}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
:disabled="!hasFriendSelection"
|
||||||
|
@click="copySelectedFriends">
|
||||||
|
{{ t('view.favorite.copy') }}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
:disabled="!hasFriendSelection"
|
||||||
|
@click="showFriendBulkUnfavoriteSelectionConfirm">
|
||||||
|
{{ t('view.favorite.bulk_unfavorite') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div ref="friendFavoritesContainerRef" class="favorites-content__list">
|
||||||
|
<template v-if="activeRemoteGroup && !isSearchActive">
|
||||||
|
<div class="favorites-content__scroll favorites-content__scroll--native">
|
||||||
|
<template v-if="currentFriendFavorites.length">
|
||||||
|
<div
|
||||||
|
class="favorites-card-list"
|
||||||
|
:style="friendFavoritesGridStyle(currentFriendFavorites.length)">
|
||||||
|
<FavoritesFriendItem
|
||||||
|
v-for="favorite in currentFriendFavorites"
|
||||||
|
:key="favorite.id"
|
||||||
|
:favorite="favorite"
|
||||||
|
:group="activeRemoteGroup"
|
||||||
|
:selected="selectedFavoriteFriends.includes(favorite.id)"
|
||||||
|
:edit-mode="friendEditMode"
|
||||||
|
@toggle-select="toggleFriendSelection(favorite.id, $event)"
|
||||||
|
@click="showUserDialog(favorite.id)" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-else class="favorites-empty">
|
||||||
|
<DataTableEmpty type="nodata" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="!isSearchActive">
|
||||||
|
<div class="favorites-empty">No Group Selected</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="favorites-content__scroll favorites-content__scroll--native">
|
||||||
<div
|
<div
|
||||||
v-for="favorite in friendFavoriteSearchResults"
|
v-if="friendFavoriteSearchResults.length"
|
||||||
:key="favorite.id"
|
class="favorites-search-grid"
|
||||||
class="favorites-search-card"
|
:style="friendFavoritesGridStyle(friendFavoriteSearchResults.length)">
|
||||||
@click="showUserDialog(favorite.id)">
|
<div
|
||||||
<div class="favorites-search-card__content">
|
v-for="favorite in friendFavoriteSearchResults"
|
||||||
<div class="favorites-search-card__avatar">
|
:key="favorite.id"
|
||||||
<img :src="userImage(favorite, true)" loading="lazy" />
|
class="favorites-search-card"
|
||||||
</div>
|
@click="showUserDialog(favorite.id)">
|
||||||
<div class="favorites-search-card__detail">
|
<div class="favorites-search-card__content">
|
||||||
<div class="favorites-search-card__title">
|
<div class="favorites-search-card__avatar">
|
||||||
<span class="name">{{ favorite.displayName }}</span>
|
<img :src="userImage(favorite, true)" loading="lazy" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="favorites-search-card__detail">
|
||||||
v-if="favorite.location && favorite.location !== 'offline'"
|
<div class="favorites-search-card__title">
|
||||||
class="favorites-search-card__location">
|
<span class="name">{{ favorite.displayName }}</span>
|
||||||
<Location
|
</div>
|
||||||
:location="favorite.location"
|
<div
|
||||||
:traveling="favorite.travelingToLocation"
|
v-if="favorite.location && favorite.location !== 'offline'"
|
||||||
:link="false" />
|
class="favorites-search-card__location">
|
||||||
|
<Location
|
||||||
|
:location="favorite.location"
|
||||||
|
:traveling="favorite.travelingToLocation"
|
||||||
|
:link="false" />
|
||||||
|
</div>
|
||||||
|
<span v-else class="text-xs">{{ favorite.statusDescription }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span v-else class="text-xs">{{ favorite.statusDescription }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="favorites-empty">
|
||||||
|
<DataTableEmpty type="nomatch" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="favorites-empty">
|
</template>
|
||||||
<DataTableEmpty type="nomatch" />
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ResizablePanel>
|
||||||
</ResizablePanel>
|
</ResizablePanelGroup>
|
||||||
</ResizablePanelGroup>
|
</div>
|
||||||
<FriendExportDialog v-model:friendExportDialogVisible="friendExportDialogVisible" />
|
<FriendExportDialog v-model:friendExportDialogVisible="friendExportDialogVisible" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,435 +1,453 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="favorites-page x-container">
|
<div class="x-container">
|
||||||
<div class="favorites-toolbar">
|
<div class="favorites-page">
|
||||||
<div>
|
<div class="favorites-toolbar">
|
||||||
<Select :model-value="sortFavorites" @update:modelValue="handleSortFavoritesChange">
|
<div>
|
||||||
<SelectTrigger size="sm" class="favorites-toolbar__select">
|
<Select :model-value="sortFavorites" @update:modelValue="handleSortFavoritesChange">
|
||||||
<span class="flex items-center gap-2">
|
<SelectTrigger size="sm" class="favorites-toolbar__select">
|
||||||
<ArrowUpDown class="h-4 w-4" />
|
<span class="flex items-center gap-2">
|
||||||
<SelectValue
|
<ArrowUpDown class="h-4 w-4" />
|
||||||
:placeholder="t('view.settings.appearance.appearance.sort_favorite_by_name')" />
|
<SelectValue
|
||||||
</span>
|
:placeholder="t('view.settings.appearance.appearance.sort_favorite_by_name')" />
|
||||||
</SelectTrigger>
|
</span>
|
||||||
<SelectContent>
|
</SelectTrigger>
|
||||||
<SelectGroup>
|
<SelectContent>
|
||||||
<SelectItem
|
<SelectGroup>
|
||||||
:value="false"
|
<SelectItem
|
||||||
:text-value="t('view.settings.appearance.appearance.sort_favorite_by_name')">
|
:value="false"
|
||||||
{{ t('view.settings.appearance.appearance.sort_favorite_by_name') }}
|
:text-value="t('view.settings.appearance.appearance.sort_favorite_by_name')">
|
||||||
</SelectItem>
|
{{ t('view.settings.appearance.appearance.sort_favorite_by_name') }}
|
||||||
<SelectItem
|
</SelectItem>
|
||||||
:value="true"
|
<SelectItem
|
||||||
:text-value="t('view.settings.appearance.appearance.sort_favorite_by_date')">
|
:value="true"
|
||||||
{{ t('view.settings.appearance.appearance.sort_favorite_by_date') }}
|
:text-value="t('view.settings.appearance.appearance.sort_favorite_by_date')">
|
||||||
</SelectItem>
|
{{ t('view.settings.appearance.appearance.sort_favorite_by_date') }}
|
||||||
</SelectGroup>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectGroup>
|
||||||
</Select>
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div class="favorites-toolbar__right">
|
||||||
|
<InputGroupSearch
|
||||||
|
v-model="worldFavoriteSearch"
|
||||||
|
class="favorites-toolbar__search"
|
||||||
|
:placeholder="t('view.favorite.worlds.search')"
|
||||||
|
@input="searchWorldFavorites" />
|
||||||
|
<DropdownMenu v-model:open="worldToolbarMenuOpen">
|
||||||
|
<DropdownMenuTrigger as-child>
|
||||||
|
<Button class="rounded-full" size="icon" variant="ghost"><Ellipsis /></Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent class="favorites-dropdown">
|
||||||
|
<li class="favorites-dropdown__control" @click.stop>
|
||||||
|
<div class="favorites-dropdown__control-header">
|
||||||
|
<span>Scale</span>
|
||||||
|
<span class="favorites-dropdown__control-value">
|
||||||
|
{{ worldCardScalePercent }}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Slider
|
||||||
|
v-model="worldCardScaleValue"
|
||||||
|
class="favorites-dropdown__slider"
|
||||||
|
:min="worldCardScaleSlider.min"
|
||||||
|
:max="worldCardScaleSlider.max"
|
||||||
|
:step="worldCardScaleSlider.step" />
|
||||||
|
</li>
|
||||||
|
<li class="favorites-dropdown__control" @click.stop>
|
||||||
|
<div class="favorites-dropdown__control-header">
|
||||||
|
<span>Spacing</span>
|
||||||
|
<span class="favorites-dropdown__control-value">
|
||||||
|
{{ worldCardSpacingPercent }}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Slider
|
||||||
|
v-model="worldCardSpacingValue"
|
||||||
|
class="favorites-dropdown__slider"
|
||||||
|
:min="worldCardSpacingSlider.min"
|
||||||
|
:max="worldCardSpacingSlider.max"
|
||||||
|
:step="worldCardSpacingSlider.step" />
|
||||||
|
</li>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem @click="handleWorldImportClick">
|
||||||
|
{{ t('view.favorite.import') }}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem @click="handleWorldExportClick">
|
||||||
|
{{ t('view.favorite.export') }}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="favorites-toolbar__right">
|
<ResizablePanelGroup
|
||||||
<InputGroupSearch
|
ref="worldSplitterGroupRef"
|
||||||
v-model="worldFavoriteSearch"
|
direction="horizontal"
|
||||||
class="favorites-toolbar__search"
|
class="favorites-splitter"
|
||||||
:placeholder="t('view.favorite.worlds.search')"
|
@layout="handleWorldSplitterLayout">
|
||||||
@input="searchWorldFavorites" />
|
<ResizablePanel
|
||||||
<DropdownMenu v-model:open="worldToolbarMenuOpen">
|
ref="worldSplitterPanelRef"
|
||||||
<DropdownMenuTrigger as-child>
|
:default-size="worldSplitterDefaultSize"
|
||||||
<Button class="rounded-full" size="icon" variant="ghost"><Ellipsis /></Button>
|
:min-size="worldSplitterMinSize"
|
||||||
</DropdownMenuTrigger>
|
:max-size="worldSplitterMaxSize"
|
||||||
<DropdownMenuContent class="favorites-dropdown">
|
:collapsed-size="0"
|
||||||
<li class="favorites-dropdown__control" @click.stop>
|
collapsible
|
||||||
<div class="favorites-dropdown__control-header">
|
:order="1">
|
||||||
<span>Scale</span>
|
<div class="favorites-groups-panel">
|
||||||
<span class="favorites-dropdown__control-value"> {{ worldCardScalePercent }}% </span>
|
<div class="group-section">
|
||||||
|
<div class="group-section__header">
|
||||||
|
<span>{{ t('view.favorite.worlds.vrchat_favorites') }}</span>
|
||||||
|
<TooltipWrapper side="bottom" :content="t('view.favorite.refresh_favorites_tooltip')">
|
||||||
|
<Button
|
||||||
|
class="rounded-full"
|
||||||
|
variant="outline"
|
||||||
|
size="icon-sm"
|
||||||
|
:disabled="isFavoriteLoading"
|
||||||
|
@click.stop="handleRefreshFavorites">
|
||||||
|
<Spinner v-if="isFavoriteLoading" />
|
||||||
|
<RefreshCw v-else />
|
||||||
|
</Button>
|
||||||
|
</TooltipWrapper>
|
||||||
</div>
|
</div>
|
||||||
<Slider
|
<div class="group-section__list">
|
||||||
v-model="worldCardScaleValue"
|
<template v-if="favoriteWorldGroups.length">
|
||||||
class="favorites-dropdown__slider"
|
<div
|
||||||
:min="worldCardScaleSlider.min"
|
v-for="group in favoriteWorldGroups"
|
||||||
:max="worldCardScaleSlider.max"
|
:key="group.key"
|
||||||
:step="worldCardScaleSlider.step" />
|
:class="[
|
||||||
</li>
|
'group-item',
|
||||||
<li class="favorites-dropdown__control" @click.stop>
|
{ 'is-active': !hasSearchInput && isGroupActive('remote', group.key) }
|
||||||
<div class="favorites-dropdown__control-header">
|
]"
|
||||||
<span>Spacing</span>
|
@click="handleGroupClick('remote', group.key)">
|
||||||
<span class="favorites-dropdown__control-value"> {{ worldCardSpacingPercent }}% </span>
|
<div class="group-item__top">
|
||||||
|
<span class="group-item__name">{{ group.displayName }}</span>
|
||||||
|
<span class="group-item__count"
|
||||||
|
>{{ group.count }}/{{ group.capacity }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="group-item__bottom">
|
||||||
|
<Badge variant="outline">
|
||||||
|
{{ t(`view.favorite.visibility.${group.visibility}`) }}
|
||||||
|
</Badge>
|
||||||
|
<DropdownMenu
|
||||||
|
:open="activeGroupMenu === remoteGroupMenuKey(group.key)"
|
||||||
|
@update:open="
|
||||||
|
handleGroupMenuVisible(remoteGroupMenuKey(group.key), $event)
|
||||||
|
">
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
class="rounded-full"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
@click.stop>
|
||||||
|
<MoreHorizontal />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent side="right" class="w-50">
|
||||||
|
<DropdownMenuItem @click="handleRemoteRename(group)">
|
||||||
|
<span>{{ t('view.favorite.rename_tooltip') }}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSub>
|
||||||
|
<DropdownMenuSubTrigger>
|
||||||
|
<span>{{ t('view.favorite.visibility_tooltip') }}</span>
|
||||||
|
</DropdownMenuSubTrigger>
|
||||||
|
<DropdownMenuPortal>
|
||||||
|
<DropdownMenuSubContent
|
||||||
|
side="right"
|
||||||
|
align="start"
|
||||||
|
class="w-[200px]">
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
v-for="visibility in worldGroupVisibilityOptions"
|
||||||
|
:key="visibility"
|
||||||
|
:model-value="group.visibility === visibility"
|
||||||
|
indicator-position="right"
|
||||||
|
@select="
|
||||||
|
handleVisibilitySelection(group, visibility)
|
||||||
|
">
|
||||||
|
<span>{{
|
||||||
|
t(`view.favorite.visibility.${visibility}`)
|
||||||
|
}}</span>
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
</DropdownMenuSubContent>
|
||||||
|
</DropdownMenuPortal>
|
||||||
|
</DropdownMenuSub>
|
||||||
|
<DropdownMenuItem
|
||||||
|
variant="destructive"
|
||||||
|
@click="handleRemoteClear(group)">
|
||||||
|
<span>{{ t('view.favorite.clear') }}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
v-for="group in worldGroupPlaceholders"
|
||||||
|
:key="group.key"
|
||||||
|
:class="[
|
||||||
|
'group-item',
|
||||||
|
'group-item--placeholder',
|
||||||
|
{ 'is-active': !hasSearchInput && isGroupActive('remote', group.key) }
|
||||||
|
]">
|
||||||
|
<div class="group-item__top">
|
||||||
|
<span class="group-item__name">{{ group.displayName }}</span>
|
||||||
|
<span class="group-item__count">--/--</span>
|
||||||
|
</div>
|
||||||
|
<div class="group-item__bottom">
|
||||||
|
<div class="group-item__placeholder-tag"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<Slider
|
</div>
|
||||||
v-model="worldCardSpacingValue"
|
<div class="group-section">
|
||||||
class="favorites-dropdown__slider"
|
<div class="group-section__header">
|
||||||
:min="worldCardSpacingSlider.min"
|
<span>{{ t('view.favorite.worlds.local_favorites') }}</span>
|
||||||
:max="worldCardSpacingSlider.max"
|
|
||||||
:step="worldCardSpacingSlider.step" />
|
|
||||||
</li>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem @click="handleWorldImportClick">
|
|
||||||
{{ t('view.favorite.import') }}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem @click="handleWorldExportClick">
|
|
||||||
{{ t('view.favorite.export') }}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ResizablePanelGroup
|
|
||||||
ref="worldSplitterGroupRef"
|
|
||||||
direction="horizontal"
|
|
||||||
class="favorites-splitter"
|
|
||||||
@layout="handleWorldSplitterLayout">
|
|
||||||
<ResizablePanel
|
|
||||||
ref="worldSplitterPanelRef"
|
|
||||||
:default-size="worldSplitterDefaultSize"
|
|
||||||
:min-size="worldSplitterMinSize"
|
|
||||||
:max-size="worldSplitterMaxSize"
|
|
||||||
:collapsed-size="0"
|
|
||||||
collapsible
|
|
||||||
:order="1">
|
|
||||||
<div class="favorites-groups-panel">
|
|
||||||
<div class="group-section">
|
|
||||||
<div class="group-section__header">
|
|
||||||
<span>{{ t('view.favorite.worlds.vrchat_favorites') }}</span>
|
|
||||||
<TooltipWrapper side="bottom" :content="t('view.favorite.refresh_favorites_tooltip')">
|
|
||||||
<Button
|
<Button
|
||||||
class="rounded-full"
|
class="rounded-full"
|
||||||
variant="outline"
|
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
:disabled="isFavoriteLoading"
|
variant="outline"
|
||||||
@click.stop="handleRefreshFavorites">
|
v-if="!refreshingLocalFavorites"
|
||||||
<Spinner v-if="isFavoriteLoading" />
|
@click.stop="refreshLocalWorldFavorites"
|
||||||
<RefreshCw v-else />
|
><RefreshCcw
|
||||||
|
/></Button>
|
||||||
|
<Button size="icon-sm" variant="ghost" v-else @click.stop="cancelLocalWorldRefresh">
|
||||||
|
<RefreshCcw />
|
||||||
|
{{ t('view.favorite.worlds.cancel_refresh') }}
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipWrapper>
|
</div>
|
||||||
</div>
|
<div class="group-section__list">
|
||||||
<div class="group-section__list">
|
<template v-if="localWorldFavoriteGroups.length">
|
||||||
<template v-if="favoriteWorldGroups.length">
|
<div
|
||||||
<div
|
v-for="group in localWorldFavoriteGroups"
|
||||||
v-for="group in favoriteWorldGroups"
|
:key="group"
|
||||||
:key="group.key"
|
:class="[
|
||||||
:class="[
|
'group-item',
|
||||||
'group-item',
|
{ 'is-active': !hasSearchInput && isGroupActive('local', group) }
|
||||||
{ 'is-active': !hasSearchInput && isGroupActive('remote', group.key) }
|
]"
|
||||||
]"
|
@click="handleGroupClick('local', group)">
|
||||||
@click="handleGroupClick('remote', group.key)">
|
<div class="group-item__top">
|
||||||
<div class="group-item__top">
|
<span class="group-item__name">{{ group }}</span>
|
||||||
<span class="group-item__name">{{ group.displayName }}</span>
|
<div class="group-item__right">
|
||||||
<span class="group-item__count">{{ group.count }}/{{ group.capacity }}</span>
|
<span class="group-item__count">{{
|
||||||
|
localWorldFavGroupLength(group)
|
||||||
|
}}</span>
|
||||||
|
<div class="group-item__bottom">
|
||||||
|
<DropdownMenu
|
||||||
|
:open="activeGroupMenu === localGroupMenuKey(group)"
|
||||||
|
@update:open="
|
||||||
|
handleGroupMenuVisible(localGroupMenuKey(group), $event)
|
||||||
|
">
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
class="rounded-full"
|
||||||
|
size="icon-sm"
|
||||||
|
variant="ghost"
|
||||||
|
@click.stop
|
||||||
|
><Ellipsis
|
||||||
|
/></Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent side="right" class="w-50">
|
||||||
|
<DropdownMenuItem @click="handleLocalRename(group)">
|
||||||
|
<span>{{ t('view.favorite.rename_tooltip') }}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
variant="destructive"
|
||||||
|
@click="handleLocalDelete(group)">
|
||||||
|
<span>{{ t('view.favorite.delete_tooltip') }}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="group-item__bottom">
|
</template>
|
||||||
<Badge variant="outline">
|
<div v-else class="group-empty">
|
||||||
{{ t(`view.favorite.visibility.${group.visibility}`) }}
|
<DataTableEmpty type="nodata" />
|
||||||
</Badge>
|
</div>
|
||||||
<DropdownMenu
|
<div
|
||||||
:open="activeGroupMenu === remoteGroupMenuKey(group.key)"
|
v-if="!isCreatingLocalGroup"
|
||||||
@update:open="
|
class="group-item group-item--new"
|
||||||
handleGroupMenuVisible(remoteGroupMenuKey(group.key), $event)
|
@click="startLocalGroupCreation">
|
||||||
">
|
<Plus />
|
||||||
<DropdownMenuTrigger asChild>
|
<span>{{ t('view.favorite.worlds.new_group') }}</span>
|
||||||
<Button class="rounded-full" variant="ghost" size="icon-sm" @click.stop>
|
</div>
|
||||||
<MoreHorizontal />
|
<InputGroupField
|
||||||
</Button>
|
v-else
|
||||||
</DropdownMenuTrigger>
|
ref="newLocalGroupInput"
|
||||||
<DropdownMenuContent side="right" class="w-50">
|
v-model="newLocalGroupName"
|
||||||
<DropdownMenuItem @click="handleRemoteRename(group)">
|
size="sm"
|
||||||
<span>{{ t('view.favorite.rename_tooltip') }}</span>
|
class="group-item__input"
|
||||||
</DropdownMenuItem>
|
:placeholder="t('view.favorite.worlds.new_group')"
|
||||||
<DropdownMenuSub>
|
@keyup.enter="handleLocalGroupCreationConfirm"
|
||||||
<DropdownMenuSubTrigger>
|
@keyup.esc="cancelLocalGroupCreation"
|
||||||
<span>{{ t('view.favorite.visibility_tooltip') }}</span>
|
@blur="cancelLocalGroupCreation" />
|
||||||
</DropdownMenuSubTrigger>
|
</div>
|
||||||
<DropdownMenuPortal>
|
</div>
|
||||||
<DropdownMenuSubContent
|
</div>
|
||||||
side="right"
|
</ResizablePanel>
|
||||||
align="start"
|
<ResizableHandle @dragging="setWorldSplitterDragging" />
|
||||||
class="w-[200px]">
|
<ResizablePanel :order="2">
|
||||||
<DropdownMenuCheckboxItem
|
<div class="favorites-content">
|
||||||
v-for="visibility in worldGroupVisibilityOptions"
|
<div class="favorites-content__header">
|
||||||
:key="visibility"
|
<div class="favorites-content__title">
|
||||||
:model-value="group.visibility === visibility"
|
<span v-if="isSearchActive">{{ t('view.favorite.worlds.search') }}</span>
|
||||||
indicator-position="right"
|
<template v-else-if="activeRemoteGroup">
|
||||||
@select="handleVisibilitySelection(group, visibility)">
|
<span
|
||||||
<span>{{
|
>{{ activeRemoteGroup.displayName }} <small
|
||||||
t(`view.favorite.visibility.${visibility}`)
|
>{{ activeRemoteGroup.count }}/{{ activeRemoteGroup.capacity }}</small
|
||||||
}}</span>
|
></span
|
||||||
</DropdownMenuCheckboxItem>
|
>
|
||||||
</DropdownMenuSubContent>
|
</template>
|
||||||
</DropdownMenuPortal>
|
<span v-else-if="activeLocalGroupName">
|
||||||
</DropdownMenuSub>
|
{{ activeLocalGroupName }}
|
||||||
<DropdownMenuItem
|
<small>{{ activeLocalGroupCount }}</small>
|
||||||
variant="destructive"
|
</span>
|
||||||
@click="handleRemoteClear(group)">
|
<span v-else>No Group Selected</span>
|
||||||
<span>{{ t('view.favorite.clear') }}</span>
|
</div>
|
||||||
</DropdownMenuItem>
|
<div class="favorites-content__edit">
|
||||||
</DropdownMenuContent>
|
<span>{{ t('view.favorite.edit_mode') }}</span>
|
||||||
</DropdownMenu>
|
<Switch v-model="worldEditMode" :disabled="isSearchActive" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="favorites-content__edit-actions">
|
||||||
|
<div v-if="worldEditMode && !isSearchActive" class="favorites-content__actions">
|
||||||
|
<Button size="sm" variant="outline" @click="toggleSelectAllWorlds">
|
||||||
|
{{
|
||||||
|
isAllWorldsSelected
|
||||||
|
? t('view.favorite.deselect_all')
|
||||||
|
: t('view.favorite.select_all')
|
||||||
|
}}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="secondary"
|
||||||
|
:disabled="!hasWorldSelection"
|
||||||
|
@click="clearSelectedWorlds">
|
||||||
|
{{ t('view.favorite.clear') }}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
:disabled="!hasWorldSelection"
|
||||||
|
@click="copySelectedWorlds">
|
||||||
|
{{ t('view.favorite.copy') }}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
:disabled="!hasWorldSelection"
|
||||||
|
@click="showWorldBulkUnfavoriteSelectionConfirm">
|
||||||
|
{{ t('view.favorite.bulk_unfavorite') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div ref="worldFavoritesContainerRef" class="favorites-content__list">
|
||||||
|
<template v-if="isSearchActive">
|
||||||
|
<div class="favorites-content__scroll favorites-content__scroll--native">
|
||||||
|
<div
|
||||||
|
v-if="worldFavoriteSearchResults.length"
|
||||||
|
class="favorites-search-grid"
|
||||||
|
:style="worldFavoritesGridStyle(worldFavoriteSearchResults.length)">
|
||||||
|
<div
|
||||||
|
v-for="favorite in worldFavoriteSearchResults"
|
||||||
|
:key="favorite.id"
|
||||||
|
class="favorites-search-card"
|
||||||
|
@click="showWorldDialog(favorite.id)">
|
||||||
|
<div class="favorites-search-card__content">
|
||||||
|
<div
|
||||||
|
class="favorites-search-card__avatar"
|
||||||
|
:class="{ 'is-empty': !favorite.thumbnailImageUrl }">
|
||||||
|
<img
|
||||||
|
v-if="favorite.thumbnailImageUrl"
|
||||||
|
:src="favorite.thumbnailImageUrl"
|
||||||
|
loading="lazy" />
|
||||||
|
</div>
|
||||||
|
<div class="favorites-search-card__detail">
|
||||||
|
<span class="name">{{ favorite.name || favorite.id }}</span>
|
||||||
|
<span class="text-xs">
|
||||||
|
{{ favorite.authorName }}
|
||||||
|
<template v-if="favorite.occupants">
|
||||||
|
({{ favorite.occupants }})
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="favorites-empty">
|
||||||
|
<DataTableEmpty type="nomatch" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div
|
<div
|
||||||
v-for="group in worldGroupPlaceholders"
|
v-if="activeRemoteGroup && isRemoteGroupSelected"
|
||||||
:key="group.key"
|
class="favorites-content__scroll favorites-content__scroll--native">
|
||||||
:class="[
|
<template v-if="currentRemoteFavorites.length">
|
||||||
'group-item',
|
<div
|
||||||
'group-item--placeholder',
|
class="favorites-card-list"
|
||||||
{ 'is-active': !hasSearchInput && isGroupActive('remote', group.key) }
|
:style="worldFavoritesGridStyle(currentRemoteFavorites.length)">
|
||||||
]">
|
<FavoritesWorldItem
|
||||||
<div class="group-item__top">
|
v-for="favorite in currentRemoteFavorites"
|
||||||
<span class="group-item__name">{{ group.displayName }}</span>
|
:key="favorite.id"
|
||||||
<span class="group-item__count">--/--</span>
|
:group="activeRemoteGroup"
|
||||||
</div>
|
:favorite="favorite"
|
||||||
<div class="group-item__bottom">
|
:edit-mode="worldEditMode"
|
||||||
<div class="group-item__placeholder-tag"></div>
|
:selected="selectedFavoriteWorlds.includes(favorite.id)"
|
||||||
</div>
|
@toggle-select="toggleWorldSelection(favorite.id, $event)"
|
||||||
</div>
|
@click="showWorldDialog(favorite.id)" />
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="group-section">
|
|
||||||
<div class="group-section__header">
|
|
||||||
<span>{{ t('view.favorite.worlds.local_favorites') }}</span>
|
|
||||||
<Button
|
|
||||||
class="rounded-full"
|
|
||||||
size="icon-sm"
|
|
||||||
variant="outline"
|
|
||||||
v-if="!refreshingLocalFavorites"
|
|
||||||
@click.stop="refreshLocalWorldFavorites"
|
|
||||||
><RefreshCcw
|
|
||||||
/></Button>
|
|
||||||
<Button size="icon-sm" variant="ghost" v-else @click.stop="cancelLocalWorldRefresh">
|
|
||||||
<RefreshCcw />
|
|
||||||
{{ t('view.favorite.worlds.cancel_refresh') }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div class="group-section__list">
|
|
||||||
<template v-if="localWorldFavoriteGroups.length">
|
|
||||||
<div
|
|
||||||
v-for="group in localWorldFavoriteGroups"
|
|
||||||
:key="group"
|
|
||||||
:class="[
|
|
||||||
'group-item',
|
|
||||||
{ 'is-active': !hasSearchInput && isGroupActive('local', group) }
|
|
||||||
]"
|
|
||||||
@click="handleGroupClick('local', group)">
|
|
||||||
<div class="group-item__top">
|
|
||||||
<span class="group-item__name">{{ group }}</span>
|
|
||||||
<div class="group-item__right">
|
|
||||||
<span class="group-item__count">{{ localWorldFavGroupLength(group) }}</span>
|
|
||||||
<div class="group-item__bottom">
|
|
||||||
<DropdownMenu
|
|
||||||
:open="activeGroupMenu === localGroupMenuKey(group)"
|
|
||||||
@update:open="
|
|
||||||
handleGroupMenuVisible(localGroupMenuKey(group), $event)
|
|
||||||
">
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button
|
|
||||||
class="rounded-full"
|
|
||||||
size="icon-sm"
|
|
||||||
variant="ghost"
|
|
||||||
@click.stop
|
|
||||||
><Ellipsis
|
|
||||||
/></Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent side="right" class="w-50">
|
|
||||||
<DropdownMenuItem @click="handleLocalRename(group)">
|
|
||||||
<span>{{ t('view.favorite.rename_tooltip') }}</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
variant="destructive"
|
|
||||||
@click="handleLocalDelete(group)">
|
|
||||||
<span>{{ t('view.favorite.delete_tooltip') }}</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-else class="favorites-empty">
|
||||||
|
<DataTableEmpty type="nodata" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
<div v-else class="group-empty">
|
|
||||||
<DataTableEmpty type="nodata" />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="!isCreatingLocalGroup"
|
|
||||||
class="group-item group-item--new"
|
|
||||||
@click="startLocalGroupCreation">
|
|
||||||
<Plus />
|
|
||||||
<span>{{ t('view.favorite.worlds.new_group') }}</span>
|
|
||||||
</div>
|
|
||||||
<InputGroupField
|
|
||||||
v-else
|
|
||||||
ref="newLocalGroupInput"
|
|
||||||
v-model="newLocalGroupName"
|
|
||||||
size="sm"
|
|
||||||
class="group-item__input"
|
|
||||||
:placeholder="t('view.favorite.worlds.new_group')"
|
|
||||||
@keyup.enter="handleLocalGroupCreationConfirm"
|
|
||||||
@keyup.esc="cancelLocalGroupCreation"
|
|
||||||
@blur="cancelLocalGroupCreation" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ResizablePanel>
|
|
||||||
<ResizableHandle @dragging="setWorldSplitterDragging" />
|
|
||||||
<ResizablePanel :order="2">
|
|
||||||
<div class="favorites-content">
|
|
||||||
<div class="favorites-content__header">
|
|
||||||
<div class="favorites-content__title">
|
|
||||||
<span v-if="isSearchActive">{{ t('view.favorite.worlds.search') }}</span>
|
|
||||||
<template v-else-if="activeRemoteGroup">
|
|
||||||
<span
|
|
||||||
>{{ activeRemoteGroup.displayName }} <small
|
|
||||||
>{{ activeRemoteGroup.count }}/{{ activeRemoteGroup.capacity }}</small
|
|
||||||
></span
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
<span v-else-if="activeLocalGroupName">
|
|
||||||
{{ activeLocalGroupName }}
|
|
||||||
<small>{{ activeLocalGroupCount }}</small>
|
|
||||||
</span>
|
|
||||||
<span v-else>No Group Selected</span>
|
|
||||||
</div>
|
|
||||||
<div class="favorites-content__edit">
|
|
||||||
<span>{{ t('view.favorite.edit_mode') }}</span>
|
|
||||||
<Switch v-model="worldEditMode" :disabled="isSearchActive" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="favorites-content__edit-actions">
|
|
||||||
<div v-if="worldEditMode && !isSearchActive" class="favorites-content__actions">
|
|
||||||
<Button size="sm" variant="outline" @click="toggleSelectAllWorlds">
|
|
||||||
{{
|
|
||||||
isAllWorldsSelected
|
|
||||||
? t('view.favorite.deselect_all')
|
|
||||||
: t('view.favorite.select_all')
|
|
||||||
}}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="secondary"
|
|
||||||
:disabled="!hasWorldSelection"
|
|
||||||
@click="clearSelectedWorlds">
|
|
||||||
{{ t('view.favorite.clear') }}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
:disabled="!hasWorldSelection"
|
|
||||||
@click="copySelectedWorlds">
|
|
||||||
{{ t('view.favorite.copy') }}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
:disabled="!hasWorldSelection"
|
|
||||||
@click="showWorldBulkUnfavoriteSelectionConfirm">
|
|
||||||
{{ t('view.favorite.bulk_unfavorite') }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div ref="worldFavoritesContainerRef" class="favorites-content__list">
|
|
||||||
<template v-if="isSearchActive">
|
|
||||||
<div class="favorites-content__scroll favorites-content__scroll--native">
|
|
||||||
<div
|
<div
|
||||||
v-if="worldFavoriteSearchResults.length"
|
v-else-if="activeLocalGroupName && isLocalGroupSelected"
|
||||||
class="favorites-search-grid"
|
ref="localFavoritesViewportRef"
|
||||||
:style="worldFavoritesGridStyle(worldFavoriteSearchResults.length)">
|
class="favorites-content__scroll favorites-content__scroll--native favorites-content__scroll--local focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
||||||
<div
|
data-reka-scroll-area-viewport=""
|
||||||
v-for="favorite in worldFavoriteSearchResults"
|
data-slot="scroll-area-viewport"
|
||||||
:key="favorite.id"
|
tabindex="0"
|
||||||
class="favorites-search-card"
|
style="overflow: hidden scroll">
|
||||||
@click="showWorldDialog(favorite.id)">
|
<template v-if="currentLocalFavorites.length">
|
||||||
<div class="favorites-search-card__content">
|
<div class="favorites-card-virtual" :style="localVirtualContainerStyle">
|
||||||
<div
|
<template
|
||||||
class="favorites-search-card__avatar"
|
v-for="item in localVirtualItems"
|
||||||
:class="{ 'is-empty': !favorite.thumbnailImageUrl }">
|
:key="String(item.virtualItem.key)">
|
||||||
<img
|
<div
|
||||||
v-if="favorite.thumbnailImageUrl"
|
v-if="item.row"
|
||||||
:src="favorite.thumbnailImageUrl"
|
class="favorites-card-virtual-row"
|
||||||
loading="lazy" />
|
:data-index="item.virtualItem.index"
|
||||||
</div>
|
:ref="localVirtualizer.measureElement"
|
||||||
<div class="favorites-search-card__detail">
|
:style="{ transform: `translateY(${item.virtualItem.start}px)` }">
|
||||||
<span class="name">{{ favorite.name || favorite.id }}</span>
|
<div class="favorites-card-virtual-row-grid">
|
||||||
<span class="text-xs">
|
<FavoritesWorldLocalItem
|
||||||
{{ favorite.authorName }}
|
v-for="favorite in getLocalRowItems(item.row)"
|
||||||
<template v-if="favorite.occupants">
|
:key="favorite.key"
|
||||||
({{ favorite.occupants }})
|
:group="activeLocalGroupName"
|
||||||
</template>
|
:favorite="favorite.favorite"
|
||||||
</span>
|
:edit-mode="worldEditMode"
|
||||||
</div>
|
@remove-local-world-favorite="removeLocalWorldFavorite"
|
||||||
</div>
|
@click="showWorldDialog(favorite.favorite.id)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div v-else class="favorites-empty">
|
|
||||||
<DataTableEmpty type="nomatch" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div
|
|
||||||
v-if="activeRemoteGroup && isRemoteGroupSelected"
|
|
||||||
class="favorites-content__scroll favorites-content__scroll--native">
|
|
||||||
<template v-if="currentRemoteFavorites.length">
|
|
||||||
<div
|
|
||||||
class="favorites-card-list"
|
|
||||||
:style="worldFavoritesGridStyle(currentRemoteFavorites.length)">
|
|
||||||
<FavoritesWorldItem
|
|
||||||
v-for="favorite in currentRemoteFavorites"
|
|
||||||
:key="favorite.id"
|
|
||||||
:group="activeRemoteGroup"
|
|
||||||
:favorite="favorite"
|
|
||||||
:edit-mode="worldEditMode"
|
|
||||||
:selected="selectedFavoriteWorlds.includes(favorite.id)"
|
|
||||||
@toggle-select="toggleWorldSelection(favorite.id, $event)"
|
|
||||||
@click="showWorldDialog(favorite.id)" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div v-else class="favorites-empty">
|
|
||||||
<DataTableEmpty type="nodata" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else-if="activeLocalGroupName && isLocalGroupSelected"
|
|
||||||
ref="localFavoritesViewportRef"
|
|
||||||
class="favorites-content__scroll favorites-content__scroll--native favorites-content__scroll--local focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
|
||||||
data-reka-scroll-area-viewport=""
|
|
||||||
data-slot="scroll-area-viewport"
|
|
||||||
tabindex="0"
|
|
||||||
style="overflow: hidden scroll">
|
|
||||||
<template v-if="currentLocalFavorites.length">
|
|
||||||
<div class="favorites-card-virtual" :style="localVirtualContainerStyle">
|
|
||||||
<template v-for="item in localVirtualItems" :key="String(item.virtualItem.key)">
|
|
||||||
<div
|
|
||||||
v-if="item.row"
|
|
||||||
class="favorites-card-virtual-row"
|
|
||||||
:data-index="item.virtualItem.index"
|
|
||||||
:ref="localVirtualizer.measureElement"
|
|
||||||
:style="{ transform: `translateY(${item.virtualItem.start}px)` }">
|
|
||||||
<div class="favorites-card-virtual-row-grid">
|
|
||||||
<FavoritesWorldLocalItem
|
|
||||||
v-for="favorite in getLocalRowItems(item.row)"
|
|
||||||
:key="favorite.key"
|
|
||||||
:group="activeLocalGroupName"
|
|
||||||
:favorite="favorite.favorite"
|
|
||||||
:edit-mode="worldEditMode"
|
|
||||||
@remove-local-world-favorite="removeLocalWorldFavorite"
|
|
||||||
@click="showWorldDialog(favorite.favorite.id)" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</template>
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-else class="favorites-empty">
|
||||||
|
<DataTableEmpty type="nodata" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
<div v-else class="favorites-empty">
|
<div v-else class="favorites-empty">
|
||||||
<DataTableEmpty type="nodata" />
|
<DataTableEmpty type="nodata" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
<div v-else class="favorites-empty">
|
</div>
|
||||||
<DataTableEmpty type="nodata" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ResizablePanel>
|
||||||
</ResizablePanel>
|
</ResizablePanelGroup>
|
||||||
</ResizablePanelGroup>
|
</div>
|
||||||
<WorldExportDialog v-model:worldExportDialogVisible="worldExportDialogVisible" />
|
<WorldExportDialog v-model:worldExportDialogVisible="worldExportDialogVisible" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="x-container" ref="playerListRef">
|
<div class="x-container" ref="playerListRef">
|
||||||
<div class="flex h-full min-h-0 flex-col">
|
<div class="flex h-full min-h-0 flex-col overflow-auto">
|
||||||
<div
|
<div
|
||||||
v-if="currentInstanceWorld.ref.id"
|
v-if="currentInstanceWorld.ref.id"
|
||||||
ref="playerListHeaderRef"
|
ref="playerListHeaderRef"
|
||||||
|
|||||||
Reference in New Issue
Block a user