mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-05-07 06:56:04 +02:00
fix friends locations
This commit is contained in:
@@ -58,99 +58,45 @@
|
|||||||
<div v-else class="friend-view__toolbar friend-view__toolbar--loading">
|
<div v-else class="friend-view__toolbar friend-view__toolbar--loading">
|
||||||
<span class="friend-view__loading-text">{{ t('view.friends_locations.loading_more') }}</span>
|
<span class="friend-view__loading-text">{{ t('view.friends_locations.loading_more') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<ScrollArea v-if="settingsReady" ref="scrollbarRef" class="friend-view__scroll" @scroll="handleScroll">
|
<ScrollArea v-if="settingsReady" ref="scrollbarRef" class="friend-view__scroll">
|
||||||
<template v-if="isSameInstanceView">
|
<div v-if="virtualRows.length" class="friend-view__virtual" :style="virtualListStyle">
|
||||||
<div v-if="visibleSameInstanceGroups.length" class="friend-view__instances">
|
<div class="friend-view__virtual-spacer" :style="virtualSpacerStyle">
|
||||||
<section
|
<div
|
||||||
v-for="group in visibleSameInstanceGroups"
|
v-for="vRow in virtualItems"
|
||||||
:key="group.instanceId"
|
:key="String(virtualRows[vRow.index]?.key ?? vRow.key)"
|
||||||
class="friend-view__instance">
|
class="friend-view__virtual-row"
|
||||||
<header class="friend-view__instance-header">
|
:data-index="vRow.index"
|
||||||
<Location class="extra" :location="group.instanceId" style="display: inline" />
|
:ref="(el) => onVirtualRowRef(el)"
|
||||||
<span class="friend-view__instance-count">{{ group.friends.length }}</span>
|
:style="virtualRowStyle(vRow.start)">
|
||||||
</header>
|
<template v-if="virtualRows[vRow.index]?.type === 'header'">
|
||||||
<div
|
<header class="friend-view__instance-header">
|
||||||
class="friend-view__grid"
|
<Location
|
||||||
:style="
|
class="extra"
|
||||||
gridStyle(group.friends.length, {
|
:location="virtualRows[vRow.index].instanceId"
|
||||||
preferredColumns: sameInstanceColumnTarget,
|
style="display: inline" />
|
||||||
disableAutoStretch: true,
|
<span class="friend-view__instance-count">{{ virtualRows[vRow.index].count }}</span>
|
||||||
matchMaxColumnWidth: true
|
</header>
|
||||||
})
|
</template>
|
||||||
">
|
|
||||||
<FriendLocationCard
|
<template v-else-if="virtualRows[vRow.index]?.type === 'divider'">
|
||||||
v-for="friend in group.friends"
|
<div class="friend-view__divider"><span class="friend-view__divider-text"></span></div>
|
||||||
:key="friend.id ?? friend.userId ?? friend.displayName"
|
</template>
|
||||||
:friend="friend"
|
|
||||||
:card-scale="cardScale"
|
<template v-else>
|
||||||
:card-spacing="cardSpacing" />
|
<div class="friend-view__row">
|
||||||
</div>
|
<FriendLocationCard
|
||||||
</section>
|
v-for="item in virtualRows[vRow.index]?.items ?? []"
|
||||||
|
:key="item.key"
|
||||||
|
:friend="item.friend"
|
||||||
|
:card-scale="cardScale"
|
||||||
|
:card-spacing="cardSpacing"
|
||||||
|
:display-instance-info="item.displayInstanceInfo" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="friend-view__empty">{{ t('view.friends_locations.no_matching_friends') }}</div>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="shouldMergeSameInstance">
|
|
||||||
<div v-if="mergedSameInstanceGroups.length" class="friend-view__instances">
|
|
||||||
<section
|
|
||||||
v-for="group in mergedSameInstanceGroups"
|
|
||||||
:key="group.instanceId"
|
|
||||||
class="friend-view__instance">
|
|
||||||
<header class="friend-view__instance-header">
|
|
||||||
<Location class="extra" :location="group.instanceId" style="display: inline" />
|
|
||||||
<span class="friend-view__instance-count">{{ group.friends.length }}</span>
|
|
||||||
</header>
|
|
||||||
<div
|
|
||||||
class="friend-view__grid"
|
|
||||||
:style="
|
|
||||||
gridStyle(group.friends.length, {
|
|
||||||
preferredColumns: sameInstanceColumnTarget,
|
|
||||||
disableAutoStretch: true,
|
|
||||||
matchMaxColumnWidth: true
|
|
||||||
})
|
|
||||||
">
|
|
||||||
<FriendLocationCard
|
|
||||||
v-for="friend in group.friends"
|
|
||||||
:key="friend.id ?? friend.userId ?? friend.displayName"
|
|
||||||
:friend="friend"
|
|
||||||
:card-scale="cardScale"
|
|
||||||
:card-spacing="cardSpacing"
|
|
||||||
:display-instance-info="false" />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
<div v-if="mergedSameInstanceGroups.length && mergedOnlineEntries.length" class="friend-view__divider">
|
|
||||||
<span class="friend-view__divider-text"></span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="mergedOnlineEntries.length"
|
|
||||||
class="friend-view__grid"
|
|
||||||
:style="gridStyle(mergedOnlineEntries.length)">
|
|
||||||
<FriendLocationCard
|
|
||||||
v-for="entry in mergedOnlineEntries"
|
|
||||||
:key="entry.id ?? entry.friend.id ?? entry.friend.displayName"
|
|
||||||
:friend="entry.friend"
|
|
||||||
:card-scale="cardScale"
|
|
||||||
:card-spacing="cardSpacing" />
|
|
||||||
</div>
|
|
||||||
<div v-if="!mergedSameInstanceGroups.length && !mergedOnlineEntries.length" class="friend-view__empty">
|
|
||||||
{{ t('view.friends_locations.no_matching_friends') }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div v-if="visibleFriends.length" class="friend-view__grid" :style="gridStyle(visibleFriends.length)">
|
|
||||||
<FriendLocationCard
|
|
||||||
v-for="entry in visibleFriends"
|
|
||||||
:key="entry.id ?? entry.friend.id ?? entry.friend.displayName"
|
|
||||||
:friend="entry.friend"
|
|
||||||
:card-scale="cardScale"
|
|
||||||
:card-spacing="cardSpacing" />
|
|
||||||
</div>
|
|
||||||
<div v-else class="friend-view__empty">{{ t('view.friends_locations.no_matching_friends') }}</div>
|
|
||||||
</template>
|
|
||||||
<div v-if="isLoadingMore" class="friend-view__loading">
|
|
||||||
<Loader2 class="friend-view__loading-icon" :size="18" />
|
|
||||||
<span>{{ t('view.friends_locations.loading_more') }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="friend-view__empty">{{ t('view.friends_locations.no_matching_friends') }}</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
<div v-else class="friend-view__initial-loading">
|
<div v-else class="friend-view__initial-loading">
|
||||||
<Loader2 class="friend-view__loading-icon" :size="22" />
|
<Loader2 class="friend-view__loading-icon" :size="22" />
|
||||||
@@ -159,7 +105,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, nextTick, onBeforeMount, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
import { computed, nextTick, onBeforeMount, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue';
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { Loader2, Settings } from 'lucide-vue-next';
|
import { Loader2, Settings } from 'lucide-vue-next';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -167,6 +113,7 @@
|
|||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useVirtualizer } from '@tanstack/vue-virtual';
|
||||||
|
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '../../components/ui/popover';
|
import { Popover, PopoverContent, PopoverTrigger } from '../../components/ui/popover';
|
||||||
import { Slider } from '../../components/ui/slider';
|
import { Slider } from '../../components/ui/slider';
|
||||||
@@ -249,21 +196,17 @@
|
|||||||
|
|
||||||
const settingsReady = ref(false);
|
const settingsReady = ref(false);
|
||||||
|
|
||||||
const PAGE_SIZE = 18;
|
|
||||||
const VIEWPORT_BUFFER = 32;
|
|
||||||
|
|
||||||
const activeSegment = ref('online');
|
const activeSegment = ref('online');
|
||||||
const searchTerm = ref('');
|
const searchTerm = ref('');
|
||||||
|
|
||||||
const itemsToShow = ref(PAGE_SIZE);
|
|
||||||
const isLoadingMore = ref(false);
|
|
||||||
const scrollbarRef = ref();
|
const scrollbarRef = ref();
|
||||||
|
const scrollViewportRef = shallowRef(null);
|
||||||
const gridWidth = ref(0);
|
const gridWidth = ref(0);
|
||||||
let resizeObserver;
|
let resizeObserver;
|
||||||
let cleanupResize;
|
let cleanupResize;
|
||||||
|
|
||||||
const updateGridWidth = () => {
|
const updateGridWidth = () => {
|
||||||
const wrap = scrollbarRef.value?.viewportEl?.value;
|
const wrap = scrollViewportRef.value;
|
||||||
if (!wrap) {
|
if (!wrap) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -277,7 +220,7 @@
|
|||||||
cleanupResize = undefined;
|
cleanupResize = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const wrap = scrollbarRef.value?.viewportEl?.value;
|
const wrap = scrollViewportRef.value;
|
||||||
if (!wrap) {
|
if (!wrap) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -418,8 +361,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const visibleFriends = computed(() => filteredFriends.value.slice(0, itemsToShow.value));
|
|
||||||
|
|
||||||
const isSameInstanceView = computed(
|
const isSameInstanceView = computed(
|
||||||
() => showSameInstance.value && activeSegment.value === 'same-instance' && !normalizedSearchTerm.value
|
() => showSameInstance.value && activeSegment.value === 'same-instance' && !normalizedSearchTerm.value
|
||||||
);
|
);
|
||||||
@@ -454,56 +395,34 @@
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const visibleSameInstanceGroups = computed(() => {
|
const sameInstanceGroupsForVirtual = computed(() => {
|
||||||
if (!isSameInstanceView.value) {
|
if (!isSameInstanceView.value) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
return buildSameInstanceGroups(filteredFriends.value);
|
||||||
return buildSameInstanceGroups(visibleFriends.value);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const mergedSameInstanceEntries = computed(() => {
|
const mergedSameInstanceEntries = computed(() => {
|
||||||
if (!shouldMergeSameInstance.value) {
|
if (!shouldMergeSameInstance.value) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
return filteredFriends.value.filter((entry) => entry.section === 'same-instance');
|
||||||
return visibleFriends.value.filter((entry) => entry.section === 'same-instance');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const mergedOnlineEntries = computed(() => {
|
const mergedOnlineEntries = computed(() => {
|
||||||
if (!shouldMergeSameInstance.value) {
|
if (!shouldMergeSameInstance.value) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
return filteredFriends.value.filter((entry) => entry.section !== 'same-instance');
|
||||||
return visibleFriends.value.filter((entry) => entry.section !== 'same-instance');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const mergedSameInstanceGroups = computed(() => {
|
const mergedSameInstanceGroups = computed(() => {
|
||||||
if (!shouldMergeSameInstance.value) {
|
if (!shouldMergeSameInstance.value) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return buildSameInstanceGroups(mergedSameInstanceEntries.value);
|
return buildSameInstanceGroups(mergedSameInstanceEntries.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
const sameInstanceColumnTarget = computed(() => {
|
|
||||||
const groups = isSameInstanceView.value
|
|
||||||
? visibleSameInstanceGroups.value
|
|
||||||
: shouldMergeSameInstance.value
|
|
||||||
? mergedSameInstanceGroups.value
|
|
||||||
: [];
|
|
||||||
|
|
||||||
let maxCount = 0;
|
|
||||||
for (const group of groups) {
|
|
||||||
const size = Array.isArray(group?.friends) ? group.friends.length : 0;
|
|
||||||
if (size > maxCount) {
|
|
||||||
maxCount = size;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return maxCount > 0 ? maxCount : null;
|
|
||||||
});
|
|
||||||
|
|
||||||
const gridStyle = computed(() => {
|
const gridStyle = computed(() => {
|
||||||
const baseWidth = 220;
|
const baseWidth = 220;
|
||||||
const baseGap = 14;
|
const baseGap = 14;
|
||||||
@@ -551,92 +470,191 @@
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleScroll = () => {
|
function resolveScrollViewport() {
|
||||||
if (
|
const rootEl = scrollbarRef.value?.$el ?? null;
|
||||||
isLoadingMore.value ||
|
if (!rootEl) {
|
||||||
filteredFriends.value.length === 0 ||
|
scrollViewportRef.value = null;
|
||||||
itemsToShow.value >= filteredFriends.value.length
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
scrollViewportRef.value = rootEl.querySelector('[data-slot="scroll-area-viewport"]');
|
||||||
const wrap = scrollbarRef.value?.viewportEl?.value;
|
|
||||||
|
|
||||||
if (!wrap) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { scrollHeight, scrollTop, clientHeight } = wrap;
|
|
||||||
|
|
||||||
if (scrollTop + clientHeight >= scrollHeight - 120) {
|
|
||||||
loadMoreFriends();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function loadMoreFriends() {
|
|
||||||
if (isLoadingMore.value || itemsToShow.value >= filteredFriends.value.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoadingMore.value = true;
|
|
||||||
|
|
||||||
window.setTimeout(() => {
|
|
||||||
if (itemsToShow.value < filteredFriends.value.length) {
|
|
||||||
itemsToShow.value = Math.min(itemsToShow.value + PAGE_SIZE, filteredFriends.value.length);
|
|
||||||
}
|
|
||||||
isLoadingMore.value = false;
|
|
||||||
maybeFillViewport();
|
|
||||||
}, 350);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function maybeFillViewport() {
|
const maxColumns = computed(() => {
|
||||||
nextTick(() => {
|
const styleFn = gridStyle.value;
|
||||||
const wrap = scrollbarRef.value?.viewportEl?.value;
|
if (typeof styleFn !== 'function') {
|
||||||
if (!wrap) {
|
return 1;
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const { scrollHeight, clientHeight } = wrap;
|
const containerWidth = Math.max(gridWidth.value ?? 0, 0);
|
||||||
const hasSpace = scrollHeight <= clientHeight + VIEWPORT_BUFFER;
|
|
||||||
|
|
||||||
if (!hasSpace || isLoadingMore.value) {
|
const baseWidth = 220;
|
||||||
return;
|
const baseGap = 14;
|
||||||
}
|
const scale = cardScale.value;
|
||||||
|
const spacing = cardSpacing.value;
|
||||||
|
const minWidth = baseWidth * scale;
|
||||||
|
const gap = Math.max(6, (baseGap + (scale - 1) * 10) * spacing);
|
||||||
|
|
||||||
if (filteredFriends.value.length > visibleFriends.value.length) {
|
return Math.max(1, Math.floor((containerWidth + gap) / (minWidth + gap)) || 1);
|
||||||
loadMoreFriends();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
watch([searchTerm, activeSegment], () => {
|
|
||||||
itemsToShow.value = PAGE_SIZE;
|
|
||||||
nextTick(() => {
|
|
||||||
updateGridWidth();
|
|
||||||
maybeFillViewport();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
const chunk = (items = [], size = 1) => {
|
||||||
() => filteredFriends.value.length,
|
const out = [];
|
||||||
(length) => {
|
const n = Math.max(1, Math.floor(size) || 1);
|
||||||
if (itemsToShow.value > length) {
|
for (let i = 0; i < items.length; i += n) {
|
||||||
itemsToShow.value = length;
|
out.push(items.slice(i, i + n));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
const virtualRows = computed(() => {
|
||||||
|
const rows = [];
|
||||||
|
const columns = maxColumns.value;
|
||||||
|
|
||||||
|
if (isSameInstanceView.value) {
|
||||||
|
for (const group of sameInstanceGroupsForVirtual.value) {
|
||||||
|
rows.push({
|
||||||
|
type: 'header',
|
||||||
|
key: `h:${group.instanceId}`,
|
||||||
|
instanceId: group.instanceId,
|
||||||
|
count: Array.isArray(group.friends) ? group.friends.length : 0
|
||||||
|
});
|
||||||
|
|
||||||
|
const friends = Array.isArray(group.friends) ? group.friends : [];
|
||||||
|
for (const rowFriends of chunk(friends, Math.min(columns, friends.length || 1))) {
|
||||||
|
rows.push({
|
||||||
|
type: 'cards',
|
||||||
|
key: `g:${group.instanceId}:${rowFriends
|
||||||
|
.map((f) => f?.id ?? f?.userId ?? f?.displayName ?? '')
|
||||||
|
.join('|')}`,
|
||||||
|
items: rowFriends.map((friend) => ({
|
||||||
|
key: `f:${friend?.id ?? friend?.userId ?? friend?.displayName ?? Math.random()}`,
|
||||||
|
friend,
|
||||||
|
displayInstanceInfo: true
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
nextTick(() => {
|
|
||||||
updateGridWidth();
|
return rows;
|
||||||
maybeFillViewport();
|
}
|
||||||
|
|
||||||
|
if (shouldMergeSameInstance.value) {
|
||||||
|
for (const group of mergedSameInstanceGroups.value) {
|
||||||
|
rows.push({
|
||||||
|
type: 'header',
|
||||||
|
key: `h:${group.instanceId}`,
|
||||||
|
instanceId: group.instanceId,
|
||||||
|
count: Array.isArray(group.friends) ? group.friends.length : 0
|
||||||
|
});
|
||||||
|
|
||||||
|
const friends = Array.isArray(group.friends) ? group.friends : [];
|
||||||
|
for (const rowFriends of chunk(friends, Math.min(columns, friends.length || 1))) {
|
||||||
|
rows.push({
|
||||||
|
type: 'cards',
|
||||||
|
key: `mg:${group.instanceId}:${rowFriends
|
||||||
|
.map((f) => f?.id ?? f?.userId ?? f?.displayName ?? '')
|
||||||
|
.join('|')}`,
|
||||||
|
items: rowFriends.map((friend) => ({
|
||||||
|
key: `f:${friend?.id ?? friend?.userId ?? friend?.displayName ?? Math.random()}`,
|
||||||
|
friend,
|
||||||
|
displayInstanceInfo: false
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mergedSameInstanceGroups.value.length && mergedOnlineEntries.value.length) {
|
||||||
|
rows.push({ type: 'divider', key: 'divider:merged' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const online = mergedOnlineEntries.value;
|
||||||
|
for (const rowEntries of chunk(online, Math.min(columns, online.length || 1))) {
|
||||||
|
rows.push({
|
||||||
|
type: 'cards',
|
||||||
|
key: `o:${rowEntries.map((e) => e?.id ?? '').join('|')}`,
|
||||||
|
items: rowEntries.map((entry) => ({
|
||||||
|
key: `e:${entry?.id ?? entry?.friend?.id ?? entry?.friend?.displayName ?? Math.random()}`,
|
||||||
|
friend: entry.friend,
|
||||||
|
displayInstanceInfo: true
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = filteredFriends.value;
|
||||||
|
for (const rowEntries of chunk(entries, Math.min(columns, entries.length || 1))) {
|
||||||
|
rows.push({
|
||||||
|
type: 'cards',
|
||||||
|
key: `r:${rowEntries.map((e) => e?.id ?? '').join('|')}`,
|
||||||
|
items: rowEntries.map((entry) => ({
|
||||||
|
key: `e:${entry?.id ?? entry?.friend?.id ?? entry?.friend?.displayName ?? Math.random()}`,
|
||||||
|
friend: entry.friend,
|
||||||
|
displayInstanceInfo: true
|
||||||
|
}))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
return rows;
|
||||||
|
});
|
||||||
|
|
||||||
|
const estimatedRowHeight = computed(() => {
|
||||||
|
const base = 148;
|
||||||
|
return Math.max(64, Math.round(base * cardScale.value * cardSpacing.value));
|
||||||
|
});
|
||||||
|
|
||||||
|
const virtualizerRef = useVirtualizer(
|
||||||
|
computed(() => ({
|
||||||
|
count: virtualRows.value.length,
|
||||||
|
getScrollElement: () => scrollViewportRef.value,
|
||||||
|
estimateSize: (index) => {
|
||||||
|
const row = virtualRows.value[index];
|
||||||
|
if (row?.type === 'header') return 34;
|
||||||
|
if (row?.type === 'divider') return 18;
|
||||||
|
return estimatedRowHeight.value;
|
||||||
|
},
|
||||||
|
overscan: 10
|
||||||
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
watch([cardScale, cardSpacing], () => {
|
const virtualizer = computed(() => virtualizerRef.value);
|
||||||
if (!settingsReady.value) {
|
const virtualItems = computed(() => virtualizer.value?.getVirtualItems?.() ?? []);
|
||||||
|
|
||||||
|
const virtualSpacerStyle = computed(() => {
|
||||||
|
const height = `${virtualizer.value?.getTotalSize?.() ?? 0}px`;
|
||||||
|
return `height:${height};position:relative;width:100%;`;
|
||||||
|
});
|
||||||
|
|
||||||
|
function virtualRowStyle(start) {
|
||||||
|
const y = Number(start) || 0;
|
||||||
|
return `transform:translateY(${y}px);position:absolute;top:0;left:0;width:100%;`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onVirtualRowRef(el) {
|
||||||
|
const target = el?.$el ?? el;
|
||||||
|
if (!target) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
virtualizer.value?.measureElement?.(/** @type {Element} */ (target));
|
||||||
|
}
|
||||||
|
|
||||||
|
const virtualListStyle = computed(() => {
|
||||||
|
const styleFn = gridStyle.value;
|
||||||
|
const total = filteredFriends.value.length;
|
||||||
|
|
||||||
|
// Use matchMaxColumnWidth so rows don't collapse to minWidth on short rows.
|
||||||
|
const vars = typeof styleFn === 'function' ? styleFn(total, { matchMaxColumnWidth: true }) : {};
|
||||||
|
return {
|
||||||
|
...vars
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
watch([searchTerm, activeSegment], () => {
|
||||||
|
virtualizer.value?.scrollToOffset?.(0);
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
|
resolveScrollViewport();
|
||||||
updateGridWidth();
|
updateGridWidth();
|
||||||
maybeFillViewport();
|
virtualizer.value?.measure?.();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -648,18 +666,41 @@
|
|||||||
activeSegment.value = 'online';
|
activeSegment.value = 'online';
|
||||||
}
|
}
|
||||||
|
|
||||||
itemsToShow.value = PAGE_SIZE;
|
virtualizer.value?.scrollToOffset?.(0);
|
||||||
|
nextTick(() => {
|
||||||
|
resolveScrollViewport();
|
||||||
|
updateGridWidth();
|
||||||
|
virtualizer.value?.measure?.();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => filteredFriends.value.length,
|
||||||
|
() => {
|
||||||
|
nextTick(() => {
|
||||||
|
resolveScrollViewport();
|
||||||
|
updateGridWidth();
|
||||||
|
virtualizer.value?.measure?.();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch([cardScale, cardSpacing], () => {
|
||||||
|
if (!settingsReady.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
updateGridWidth();
|
updateGridWidth();
|
||||||
maybeFillViewport();
|
virtualizer.value?.measure?.();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
|
resolveScrollViewport();
|
||||||
setupResizeHandling();
|
setupResizeHandling();
|
||||||
maybeFillViewport();
|
updateGridWidth();
|
||||||
|
virtualizer.value?.measure?.();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -696,9 +737,10 @@
|
|||||||
} finally {
|
} finally {
|
||||||
settingsReady.value = true;
|
settingsReady.value = true;
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
|
resolveScrollViewport();
|
||||||
setupResizeHandling();
|
setupResizeHandling();
|
||||||
updateGridWidth();
|
updateGridWidth();
|
||||||
maybeFillViewport();
|
virtualizer.value?.measure?.();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -747,6 +789,30 @@
|
|||||||
color: var(--el-text-color-regular);
|
color: var(--el-text-color-regular);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.friend-view__virtual {
|
||||||
|
width: 100%;
|
||||||
|
padding: 2px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.friend-view__virtual-spacer {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.friend-view__virtual-row {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.friend-view__row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: var(--friend-card-gap, 14px);
|
||||||
|
align-items: stretch;
|
||||||
|
padding: 2px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
.friend-view__settings-label {
|
.friend-view__settings-label {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|||||||
Reference in New Issue
Block a user