mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-05-05 22:36:05 +02:00
improve performance and clean up
This commit is contained in:
@@ -289,7 +289,6 @@ export function showUserDialog(userId) {
|
|||||||
const moderationStore = useModerationStore();
|
const moderationStore = useModerationStore();
|
||||||
const favoriteStore = useFavoriteStore();
|
const favoriteStore = useFavoriteStore();
|
||||||
const locationStore = useLocationStore();
|
const locationStore = useLocationStore();
|
||||||
const searchStore = useSearchStore();
|
|
||||||
const appearanceSettingsStore = useAppearanceSettingsStore();
|
const appearanceSettingsStore = useAppearanceSettingsStore();
|
||||||
const t = i18n.global.t;
|
const t = i18n.global.t;
|
||||||
|
|
||||||
@@ -530,7 +529,6 @@ export function showUserDialog(userId) {
|
|||||||
});
|
});
|
||||||
showUserDialogHistory.delete(userId);
|
showUserDialogHistory.delete(userId);
|
||||||
showUserDialogHistory.add(userId);
|
showUserDialogHistory.add(userId);
|
||||||
searchStore.setQuickSearchItems(searchStore.quickSearchUserHistory());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -235,6 +235,7 @@
|
|||||||
"same_instance": "Same instance",
|
"same_instance": "Same instance",
|
||||||
"active": "Active",
|
"active": "Active",
|
||||||
"offline": "Offline",
|
"offline": "Offline",
|
||||||
|
"search_placeholder": "Search friends",
|
||||||
"spacing": "Spacing",
|
"spacing": "Spacing",
|
||||||
"scale": "Scale",
|
"scale": "Scale",
|
||||||
"separate_same_instance_friends": "Separate Same Instance Friends",
|
"separate_same_instance_friends": "Separate Same Instance Friends",
|
||||||
|
|||||||
+2
-152
@@ -2,37 +2,26 @@ import { computed, ref, watch } from 'vue';
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
|
|
||||||
import { compareByName, localeIncludes } from '../shared/utils';
|
|
||||||
import { instanceRequest, userRequest } from '../api';
|
import { instanceRequest, userRequest } from '../api';
|
||||||
import { groupRequest } from '../api/';
|
import { groupRequest } from '../api/';
|
||||||
import removeConfusables, { removeWhitespace } from '../services/confusables';
|
|
||||||
import { useAppearanceSettingsStore } from './settings/appearance';
|
import { useAppearanceSettingsStore } from './settings/appearance';
|
||||||
import { useFriendStore } from './friend';
|
|
||||||
import { showGroupDialog } from '../coordinators/groupCoordinator';
|
import { showGroupDialog } from '../coordinators/groupCoordinator';
|
||||||
import { showWorldDialog } from '../coordinators/worldCoordinator';
|
import { showWorldDialog } from '../coordinators/worldCoordinator';
|
||||||
import { showAvatarDialog } from '../coordinators/avatarCoordinator';
|
import { showAvatarDialog } from '../coordinators/avatarCoordinator';
|
||||||
import {
|
import { applyUser, showUserDialog } from '../coordinators/userCoordinator';
|
||||||
applyUser,
|
|
||||||
showUserDialog,
|
|
||||||
lookupUser
|
|
||||||
} from '../coordinators/userCoordinator';
|
|
||||||
import { useModalStore } from './modal';
|
import { useModalStore } from './modal';
|
||||||
import { useUserStore } from './user';
|
import { useUserStore } from './user';
|
||||||
import { watchState } from '../services/watchState';
|
import { watchState } from '../services/watchState';
|
||||||
|
|
||||||
export const useSearchStore = defineStore('Search', () => {
|
export const useSearchStore = defineStore('Search', () => {
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const router = useRouter();
|
|
||||||
const appearanceSettingsStore = useAppearanceSettingsStore();
|
const appearanceSettingsStore = useAppearanceSettingsStore();
|
||||||
const friendStore = useFriendStore();
|
|
||||||
const modalStore = useModalStore();
|
const modalStore = useModalStore();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const searchText = ref('');
|
const searchText = ref('');
|
||||||
const searchUserResults = ref([]);
|
const searchUserResults = ref([]);
|
||||||
const quickSearchItems = ref([]);
|
|
||||||
const friendsListSearch = ref('');
|
const friendsListSearch = ref('');
|
||||||
|
|
||||||
const directAccessPrompt = ref(null);
|
const directAccessPrompt = ref(null);
|
||||||
@@ -65,13 +54,6 @@ export const useSearchStore = defineStore('Search', () => {
|
|||||||
searchText.value = value;
|
searchText.value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Array} value
|
|
||||||
*/
|
|
||||||
function setQuickSearchItems(value) {
|
|
||||||
quickSearchItems.value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function searchUserByDisplayName(displayName) {
|
async function searchUserByDisplayName(displayName) {
|
||||||
const params = {
|
const params = {
|
||||||
n: 10,
|
n: 10,
|
||||||
@@ -110,133 +92,6 @@ export const useSearchStore = defineStore('Search', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function quickSearchRemoteMethod(query) {
|
|
||||||
if (!query) {
|
|
||||||
quickSearchItems.value = quickSearchUserHistory();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.length < 2) {
|
|
||||||
quickSearchItems.value = quickSearchUserHistory();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = [];
|
|
||||||
const cleanQuery = removeWhitespace(query);
|
|
||||||
if (!cleanQuery) {
|
|
||||||
quickSearchItems.value = quickSearchUserHistory();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const ctx of friendStore.friends.values()) {
|
|
||||||
if (typeof ctx.ref === 'undefined') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cleanName = removeConfusables(ctx.name);
|
|
||||||
let match = localeIncludes(
|
|
||||||
cleanName,
|
|
||||||
cleanQuery,
|
|
||||||
stringComparer.value
|
|
||||||
);
|
|
||||||
if (!match) {
|
|
||||||
// Also check regular name in case search is with special characters
|
|
||||||
match = localeIncludes(
|
|
||||||
ctx.name,
|
|
||||||
cleanQuery,
|
|
||||||
stringComparer.value
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// Use query with whitespace for notes and memos as people are more
|
|
||||||
// likely to include spaces in memos and notes
|
|
||||||
if (!match && ctx.memo) {
|
|
||||||
match = localeIncludes(ctx.memo, query, stringComparer.value);
|
|
||||||
}
|
|
||||||
if (!match && ctx.ref.note) {
|
|
||||||
match = localeIncludes(
|
|
||||||
ctx.ref.note,
|
|
||||||
query,
|
|
||||||
stringComparer.value
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
results.push({
|
|
||||||
value: ctx.id,
|
|
||||||
label: ctx.name,
|
|
||||||
ref: ctx.ref,
|
|
||||||
name: ctx.name
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
results.sort(function (a, b) {
|
|
||||||
const A =
|
|
||||||
stringComparer.value.compare(
|
|
||||||
a.name.substring(0, cleanQuery.length),
|
|
||||||
cleanQuery
|
|
||||||
) === 0;
|
|
||||||
const B =
|
|
||||||
stringComparer.value.compare(
|
|
||||||
b.name.substring(0, cleanQuery.length),
|
|
||||||
cleanQuery
|
|
||||||
) === 0;
|
|
||||||
if (A && !B) {
|
|
||||||
return -1;
|
|
||||||
} else if (B && !A) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return compareByName(a, b);
|
|
||||||
});
|
|
||||||
if (results.length > 4) {
|
|
||||||
results.length = 4;
|
|
||||||
}
|
|
||||||
results.push({
|
|
||||||
value: `search:${query}`,
|
|
||||||
label: query
|
|
||||||
});
|
|
||||||
|
|
||||||
quickSearchItems.value = results;
|
|
||||||
}
|
|
||||||
|
|
||||||
function quickSearchChange(value) {
|
|
||||||
if (!value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value.startsWith('search:')) {
|
|
||||||
const searchTerm = value.slice(7);
|
|
||||||
if (quickSearchItems.value.length > 1 && searchTerm.length) {
|
|
||||||
friendsListSearch.value = searchTerm;
|
|
||||||
router.push({ name: 'friend-list' });
|
|
||||||
} else {
|
|
||||||
router.push({ name: 'search' });
|
|
||||||
searchText.value = searchTerm;
|
|
||||||
lookupUser({ displayName: searchTerm });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
showUserDialog(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function quickSearchUserHistory() {
|
|
||||||
const userHistory = Array.from(userStore.showUserDialogHistory.values())
|
|
||||||
.reverse()
|
|
||||||
.slice(0, 5);
|
|
||||||
const results = [];
|
|
||||||
userHistory.forEach((userId) => {
|
|
||||||
const ref = userStore.cachedUsers.get(userId);
|
|
||||||
if (typeof ref !== 'undefined') {
|
|
||||||
results.push({
|
|
||||||
value: ref.id,
|
|
||||||
label: ref.name,
|
|
||||||
ref
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function directAccessPaste() {
|
async function directAccessPaste() {
|
||||||
let cbText = '';
|
let cbText = '';
|
||||||
if (LINUX) {
|
if (LINUX) {
|
||||||
@@ -430,20 +285,15 @@ export const useSearchStore = defineStore('Search', () => {
|
|||||||
searchText,
|
searchText,
|
||||||
searchUserResults,
|
searchUserResults,
|
||||||
stringComparer,
|
stringComparer,
|
||||||
quickSearchItems,
|
|
||||||
friendsListSearch,
|
friendsListSearch,
|
||||||
|
|
||||||
clearSearch,
|
clearSearch,
|
||||||
searchUserByDisplayName,
|
searchUserByDisplayName,
|
||||||
moreSearchUser,
|
moreSearchUser,
|
||||||
quickSearchUserHistory,
|
|
||||||
quickSearchRemoteMethod,
|
|
||||||
quickSearchChange,
|
|
||||||
directAccessParse,
|
directAccessParse,
|
||||||
directAccessPaste,
|
directAccessPaste,
|
||||||
directAccessWorld,
|
directAccessWorld,
|
||||||
verifyShortName,
|
verifyShortName,
|
||||||
setSearchText,
|
setSearchText
|
||||||
setQuickSearchItems
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -134,9 +134,7 @@
|
|||||||
useAppearanceSettingsStore,
|
useAppearanceSettingsStore,
|
||||||
useFriendStore,
|
useFriendStore,
|
||||||
useModalStore,
|
useModalStore,
|
||||||
useSearchStore,
|
useSearchStore
|
||||||
useUserStore,
|
|
||||||
useVrcxStore
|
|
||||||
} from '../../stores';
|
} from '../../stores';
|
||||||
import { friendRequest, userRequest } from '../../api';
|
import { friendRequest, userRequest } from '../../api';
|
||||||
import { DataTableLayout } from '../../components/ui/data-table';
|
import { DataTableLayout } from '../../components/ui/data-table';
|
||||||
@@ -261,6 +259,7 @@
|
|||||||
watch(
|
watch(
|
||||||
() => route.path,
|
() => route.path,
|
||||||
() => {
|
() => {
|
||||||
|
refreshFriendStats();
|
||||||
nextTick(() => friendsListSearchChange());
|
nextTick(() => friendsListSearchChange());
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
@@ -269,10 +268,16 @@
|
|||||||
watch(
|
watch(
|
||||||
() => friends.value.size,
|
() => friends.value.size,
|
||||||
() => {
|
() => {
|
||||||
|
refreshFriendStats();
|
||||||
friendsListSearchChange();
|
friendsListSearchChange();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function refreshFriendStats() {
|
||||||
|
getAllUserStats();
|
||||||
|
getAllUserMutualCount();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@@ -319,8 +324,6 @@
|
|||||||
results.push(ctx.ref);
|
results.push(ctx.ref);
|
||||||
}
|
}
|
||||||
friendsListDisplayData.value = results;
|
friendsListDisplayData.value = results;
|
||||||
getAllUserStats();
|
|
||||||
getAllUserMutualCount();
|
|
||||||
table.setPageIndex(0);
|
table.setPageIndex(0);
|
||||||
table.setSorting([...defaultSorting]);
|
table.setSorting([...defaultSorting]);
|
||||||
sorting.value = [...defaultSorting];
|
sorting.value = [...defaultSorting];
|
||||||
|
|||||||
@@ -312,6 +312,8 @@ describe('FriendList.vue', () => {
|
|||||||
|
|
||||||
const wrapper = mount(FriendList);
|
const wrapper = mount(FriendList);
|
||||||
await flushAsync();
|
await flushAsync();
|
||||||
|
expect(mocks.getAllUserStats).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mocks.getAllUserMutualCount).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
wrapper.vm.friendsListSearchFilterVIP = true;
|
wrapper.vm.friendsListSearchFilterVIP = true;
|
||||||
wrapper.vm.friendsListSearchChange();
|
wrapper.vm.friendsListSearchChange();
|
||||||
@@ -320,8 +322,8 @@ describe('FriendList.vue', () => {
|
|||||||
expect(
|
expect(
|
||||||
wrapper.vm.friendsListDisplayData.map((item) => item.id)
|
wrapper.vm.friendsListDisplayData.map((item) => item.id)
|
||||||
).toEqual(['usr_1']);
|
).toEqual(['usr_1']);
|
||||||
expect(mocks.getAllUserStats).toHaveBeenCalled();
|
expect(mocks.getAllUserStats).toHaveBeenCalledTimes(1);
|
||||||
expect(mocks.getAllUserMutualCount).toHaveBeenCalled();
|
expect(mocks.getAllUserMutualCount).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('opens charts tab from toolbar button', async () => {
|
test('opens charts tab from toolbar button', async () => {
|
||||||
|
|||||||
@@ -9,7 +9,10 @@
|
|||||||
</TabsList>
|
</TabsList>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<div class="friend-view__actions">
|
<div class="friend-view__actions">
|
||||||
<InputGroupSearch v-model="searchTerm" class="friend-view__search" placeholder="Search Friend" />
|
<InputGroupSearch
|
||||||
|
v-model="searchTerm"
|
||||||
|
class="friend-view__search"
|
||||||
|
:placeholder="t('view.friends_locations.search_placeholder')" />
|
||||||
<TooltipWrapper :content="t('view.charts.instance_activity.settings.header')" side="top">
|
<TooltipWrapper :content="t('view.charts.instance_activity.settings.header')" side="top">
|
||||||
<div>
|
<div>
|
||||||
<Popover>
|
<Popover>
|
||||||
@@ -151,7 +154,7 @@
|
|||||||
import { Slider } from '../../components/ui/slider';
|
import { Slider } from '../../components/ui/slider';
|
||||||
import { Switch } from '../../components/ui/switch';
|
import { Switch } from '../../components/ui/switch';
|
||||||
import { getFriendsLocations } from '../../shared/utils/location.js';
|
import { getFriendsLocations } from '../../shared/utils/location.js';
|
||||||
import { getFriendsSortFunction } from '../../shared/utils';
|
import { debounce, getFriendsSortFunction } from '../../shared/utils';
|
||||||
|
|
||||||
import FriendLocationCard from './components/FriendsLocationsCard.vue';
|
import FriendLocationCard from './components/FriendsLocationsCard.vue';
|
||||||
import configRepository from '../../services/config.js';
|
import configRepository from '../../services/config.js';
|
||||||
@@ -196,12 +199,18 @@
|
|||||||
|
|
||||||
const cardScaleBase = ref(1);
|
const cardScaleBase = ref(1);
|
||||||
const cardSpacingBase = ref(1);
|
const cardSpacingBase = ref(1);
|
||||||
|
const persistCardScale = debounce((value) => {
|
||||||
|
configRepository.setString('VRCX_FriendLocationCardScale', value.toString());
|
||||||
|
}, 200);
|
||||||
|
const persistCardSpacing = debounce((value) => {
|
||||||
|
configRepository.setString('VRCX_FriendLocationCardSpacing', value.toString());
|
||||||
|
}, 200);
|
||||||
|
|
||||||
const cardScale = computed({
|
const cardScale = computed({
|
||||||
get: () => cardScaleBase.value,
|
get: () => cardScaleBase.value,
|
||||||
set: (value) => {
|
set: (value) => {
|
||||||
cardScaleBase.value = value;
|
cardScaleBase.value = value;
|
||||||
configRepository.setString('VRCX_FriendLocationCardScale', value.toString());
|
persistCardScale(value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -209,7 +218,7 @@
|
|||||||
get: () => cardSpacingBase.value,
|
get: () => cardSpacingBase.value,
|
||||||
set: (value) => {
|
set: (value) => {
|
||||||
cardSpacingBase.value = value;
|
cardSpacingBase.value = value;
|
||||||
configRepository.setString('VRCX_FriendLocationCardSpacing', value.toString());
|
persistCardSpacing(value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -251,6 +260,8 @@
|
|||||||
|
|
||||||
const scrollbarRef = ref();
|
const scrollbarRef = ref();
|
||||||
const gridWidth = ref(0);
|
const gridWidth = ref(0);
|
||||||
|
let measureScheduled = false;
|
||||||
|
let pendingGridWidthUpdate = false;
|
||||||
let resizeObserver;
|
let resizeObserver;
|
||||||
let cleanupResize;
|
let cleanupResize;
|
||||||
|
|
||||||
@@ -314,6 +325,29 @@
|
|||||||
}))
|
}))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
const getFriendIdentity = (friend) => friend?.id ?? friend?.userId ?? friend?.displayName ?? 'unknown';
|
||||||
|
|
||||||
|
const getEntryIdentity = (entry) => entry?.id ?? getFriendIdentity(entry?.friend);
|
||||||
|
|
||||||
|
const scheduleVirtualMeasure = ({ updateGridWidth: shouldUpdateGridWidth = false } = {}) => {
|
||||||
|
pendingGridWidthUpdate = pendingGridWidthUpdate || shouldUpdateGridWidth;
|
||||||
|
if (measureScheduled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
measureScheduled = true;
|
||||||
|
nextTick(() => {
|
||||||
|
measureScheduled = false;
|
||||||
|
|
||||||
|
if (pendingGridWidthUpdate) {
|
||||||
|
pendingGridWidthUpdate = false;
|
||||||
|
updateGridWidth();
|
||||||
|
}
|
||||||
|
|
||||||
|
virtualizer.value?.measure?.();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const sameInstanceGroups = computed(() => {
|
const sameInstanceGroups = computed(() => {
|
||||||
const source = friendsInSameInstance?.value;
|
const source = friendsInSameInstance?.value;
|
||||||
if (!Array.isArray(source) || source.length === 0) return [];
|
if (!Array.isArray(source) || source.length === 0) return [];
|
||||||
@@ -379,10 +413,6 @@
|
|||||||
return allFavoriteOnlineFriends.value.filter((friend) => displayedVipIds.value.has(friend.id));
|
return allFavoriteOnlineFriends.value.filter((friend) => displayedVipIds.value.has(friend.id));
|
||||||
});
|
});
|
||||||
|
|
||||||
const vipFriendsByGroupStatus = computed(() => {
|
|
||||||
return visibleFavoriteOnlineFriends.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
const onlineFriendsByGroupStatus = computed(() => {
|
const onlineFriendsByGroupStatus = computed(() => {
|
||||||
const selectedGroups = sidebarFavoriteGroups.value;
|
const selectedGroups = sidebarFavoriteGroups.value;
|
||||||
if (selectedGroups.length === 0) {
|
if (selectedGroups.length === 0) {
|
||||||
@@ -501,7 +531,7 @@
|
|||||||
return toEntries(onlineFriendsByGroupStatus.value);
|
return toEntries(onlineFriendsByGroupStatus.value);
|
||||||
}
|
}
|
||||||
case 'favorite':
|
case 'favorite':
|
||||||
return toEntries(vipFriendsByGroupStatus.value);
|
return toEntries(visibleFavoriteOnlineFriends.value);
|
||||||
case 'same-instance':
|
case 'same-instance':
|
||||||
return sameInstanceEntries.value;
|
return sameInstanceEntries.value;
|
||||||
case 'active':
|
case 'active':
|
||||||
@@ -554,18 +584,36 @@
|
|||||||
return buildSameInstanceGroups(filteredFriends.value);
|
return buildSameInstanceGroups(filteredFriends.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
const mergedSameInstanceEntries = computed(() => {
|
const mergedEntriesBySection = computed(() => {
|
||||||
if (!shouldMergeSameInstance.value) {
|
if (!shouldMergeSameInstance.value) {
|
||||||
return [];
|
return {
|
||||||
|
sameInstance: [],
|
||||||
|
online: []
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return filteredFriends.value.filter((entry) => entry.section === 'same-instance');
|
|
||||||
|
const sameInstance = [];
|
||||||
|
const online = [];
|
||||||
|
for (const entry of filteredFriends.value) {
|
||||||
|
if (entry.section === 'same-instance') {
|
||||||
|
sameInstance.push(entry);
|
||||||
|
} else {
|
||||||
|
online.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
sameInstance,
|
||||||
|
online
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const mergedSameInstanceEntries = computed(() => {
|
||||||
|
return mergedEntriesBySection.value.sameInstance;
|
||||||
});
|
});
|
||||||
|
|
||||||
const mergedOnlineEntries = computed(() => {
|
const mergedOnlineEntries = computed(() => {
|
||||||
if (!shouldMergeSameInstance.value) {
|
return mergedEntriesBySection.value.online;
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return filteredFriends.value.filter((entry) => entry.section !== 'same-instance');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const mergedSameInstanceGroups = computed(() => {
|
const mergedSameInstanceGroups = computed(() => {
|
||||||
@@ -575,61 +623,13 @@
|
|||||||
return buildSameInstanceGroups(mergedSameInstanceEntries.value);
|
return buildSameInstanceGroups(mergedSameInstanceEntries.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
const gridStyle = computed(() => {
|
const computeGridLayout = (count = 1, options = {}) => {
|
||||||
const baseWidth = 220;
|
const baseWidth = 220;
|
||||||
const baseGap = 14;
|
const baseGap = 14;
|
||||||
const scale = cardScale.value;
|
const scale = cardScale.value;
|
||||||
const spacing = cardSpacing.value;
|
const spacing = cardSpacing.value;
|
||||||
const minWidth = baseWidth * scale;
|
const minWidth = baseWidth * scale;
|
||||||
const gap = Math.max(6, (baseGap + (scale - 1) * 10) * spacing);
|
const gap = Math.max(6, (baseGap + (scale - 1) * 10) * spacing);
|
||||||
|
|
||||||
return (count = 1, options = {}) => {
|
|
||||||
const containerWidth = Math.max(gridWidth.value ?? 0, 0);
|
|
||||||
const itemCount = Math.max(Number(count) || 0, 0);
|
|
||||||
const safeCount = itemCount > 0 ? itemCount : 1;
|
|
||||||
const maxColumns = Math.max(1, Math.floor((containerWidth + gap) / (minWidth + gap)) || 1);
|
|
||||||
const preferredColumns = options?.preferredColumns;
|
|
||||||
const requestedColumns = preferredColumns
|
|
||||||
? Math.max(1, Math.min(Math.round(preferredColumns), maxColumns))
|
|
||||||
: maxColumns;
|
|
||||||
const columns = Math.max(1, Math.min(safeCount, requestedColumns));
|
|
||||||
const forceStretch = Boolean(options?.forceStretch);
|
|
||||||
const disableAutoStretch = Boolean(options?.disableAutoStretch);
|
|
||||||
const matchMaxColumnWidth = Boolean(options?.matchMaxColumnWidth);
|
|
||||||
const shouldStretch = !disableAutoStretch && (forceStretch || itemCount >= maxColumns);
|
|
||||||
|
|
||||||
let cardWidth = minWidth;
|
|
||||||
const maxColumnWidth = maxColumns > 0 ? (containerWidth - gap * (maxColumns - 1)) / maxColumns : minWidth;
|
|
||||||
|
|
||||||
if (shouldStretch && columns > 0) {
|
|
||||||
const columnsWidth = containerWidth - gap * (columns - 1);
|
|
||||||
const rawWidth = columnsWidth > 0 ? columnsWidth / columns : minWidth;
|
|
||||||
|
|
||||||
if (Number.isFinite(rawWidth) && rawWidth > 0) {
|
|
||||||
cardWidth = Math.max(minWidth, rawWidth);
|
|
||||||
}
|
|
||||||
} else if (matchMaxColumnWidth && Number.isFinite(maxColumnWidth) && maxColumnWidth > 0) {
|
|
||||||
cardWidth = Math.max(minWidth, maxColumnWidth);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
'--friend-card-min-width': `${Math.round(minWidth)}px`,
|
|
||||||
'--friend-card-gap': `${Math.round(gap)}px`,
|
|
||||||
'--friend-card-target-width': `${Math.round(cardWidth)}px`,
|
|
||||||
'--friend-grid-columns': `${columns}`,
|
|
||||||
'--friend-card-spacing': `${spacing.toFixed(2)}`
|
|
||||||
};
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const getGridMetrics = (count = 1, options = {}) => {
|
|
||||||
const baseWidth = 220;
|
|
||||||
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);
|
|
||||||
|
|
||||||
const containerWidth = Math.max(gridWidth.value ?? 0, 0);
|
const containerWidth = Math.max(gridWidth.value ?? 0, 0);
|
||||||
const itemCount = Math.max(Number(count) || 0, 0);
|
const itemCount = Math.max(Number(count) || 0, 0);
|
||||||
const safeCount = itemCount > 0 ? itemCount : 1;
|
const safeCount = itemCount > 0 ? itemCount : 1;
|
||||||
@@ -666,12 +666,25 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const gridStyle = computed(() => {
|
||||||
|
return (count = 1, options = {}) => {
|
||||||
|
const { minWidth, gap, cardWidth, columns } = computeGridLayout(count, options);
|
||||||
|
return {
|
||||||
|
'--friend-card-min-width': `${Math.round(minWidth)}px`,
|
||||||
|
'--friend-card-gap': `${Math.round(gap)}px`,
|
||||||
|
'--friend-card-target-width': `${Math.round(cardWidth)}px`,
|
||||||
|
'--friend-grid-columns': `${columns}`,
|
||||||
|
'--friend-card-spacing': `${cardSpacing.value.toFixed(2)}`
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const chunkCardItems = (items = [], keyPrefix = 'row') => {
|
const chunkCardItems = (items = [], keyPrefix = 'row') => {
|
||||||
const safeItems = Array.isArray(items) ? items : [];
|
const safeItems = Array.isArray(items) ? items : [];
|
||||||
if (!safeItems.length) {
|
if (!safeItems.length) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const { columns } = getGridMetrics(safeItems.length, { matchMaxColumnWidth: true });
|
const { columns } = computeGridLayout(safeItems.length, { matchMaxColumnWidth: true });
|
||||||
const safeColumns = Math.max(1, columns || 1);
|
const safeColumns = Math.max(1, columns || 1);
|
||||||
const rows = [];
|
const rows = [];
|
||||||
|
|
||||||
@@ -705,7 +718,7 @@
|
|||||||
const friends = Array.isArray(group.friends) ? group.friends : [];
|
const friends = Array.isArray(group.friends) ? group.friends : [];
|
||||||
if (friends.length) {
|
if (friends.length) {
|
||||||
const items = friends.map((friend) => ({
|
const items = friends.map((friend) => ({
|
||||||
key: `f:${friend?.id ?? friend?.userId ?? friend?.displayName ?? Math.random()}`,
|
key: `f:${getFriendIdentity(friend)}`,
|
||||||
friend,
|
friend,
|
||||||
displayInstanceInfo: true
|
displayInstanceInfo: true
|
||||||
}));
|
}));
|
||||||
@@ -728,7 +741,7 @@
|
|||||||
const friends = Array.isArray(group.friends) ? group.friends : [];
|
const friends = Array.isArray(group.friends) ? group.friends : [];
|
||||||
if (friends.length) {
|
if (friends.length) {
|
||||||
const items = friends.map((friend) => ({
|
const items = friends.map((friend) => ({
|
||||||
key: `f:${friend?.id ?? friend?.userId ?? friend?.displayName ?? Math.random()}`,
|
key: `f:${getFriendIdentity(friend)}`,
|
||||||
friend,
|
friend,
|
||||||
displayInstanceInfo: false
|
displayInstanceInfo: false
|
||||||
}));
|
}));
|
||||||
@@ -743,7 +756,7 @@
|
|||||||
const online = mergedOnlineEntries.value;
|
const online = mergedOnlineEntries.value;
|
||||||
if (online.length) {
|
if (online.length) {
|
||||||
const items = online.map((entry) => ({
|
const items = online.map((entry) => ({
|
||||||
key: `e:${entry?.id ?? entry?.friend?.id ?? entry?.friend?.displayName ?? Math.random()}`,
|
key: `e:${getEntryIdentity(entry)}`,
|
||||||
friend: entry.friend,
|
friend: entry.friend,
|
||||||
displayInstanceInfo: true
|
displayInstanceInfo: true
|
||||||
}));
|
}));
|
||||||
@@ -766,7 +779,7 @@
|
|||||||
});
|
});
|
||||||
if (!isCollapsed) {
|
if (!isCollapsed) {
|
||||||
const items = group.friends.map((friend) => ({
|
const items = group.friends.map((friend) => ({
|
||||||
key: `fg:${group.key}:${friend?.id ?? friend?.userId ?? friend?.displayName ?? Math.random()}`,
|
key: `fg:${group.key}:${getFriendIdentity(friend)}`,
|
||||||
friend,
|
friend,
|
||||||
displayInstanceInfo: true
|
displayInstanceInfo: true
|
||||||
}));
|
}));
|
||||||
@@ -779,7 +792,7 @@
|
|||||||
const entries = filteredFriends.value;
|
const entries = filteredFriends.value;
|
||||||
if (entries.length) {
|
if (entries.length) {
|
||||||
const items = entries.map((entry) => ({
|
const items = entries.map((entry) => ({
|
||||||
key: `e:${entry?.id ?? entry?.friend?.id ?? entry?.friend?.displayName ?? Math.random()}`,
|
key: `e:${getEntryIdentity(entry)}`,
|
||||||
friend: entry.friend,
|
friend: entry.friend,
|
||||||
displayInstanceInfo: true
|
displayInstanceInfo: true
|
||||||
}));
|
}));
|
||||||
@@ -814,7 +827,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const itemCount = Array.isArray(row.items) ? row.items.length : 0;
|
const itemCount = Array.isArray(row.items) ? row.items.length : 0;
|
||||||
const { columns, gap } = getGridMetrics(itemCount, { matchMaxColumnWidth: true });
|
const { columns, gap } = computeGridLayout(itemCount, { matchMaxColumnWidth: true });
|
||||||
const safeColumns = Math.max(1, columns || 1);
|
const safeColumns = Math.max(1, columns || 1);
|
||||||
const rows = Math.max(1, Math.ceil(itemCount / safeColumns));
|
const rows = Math.max(1, Math.ceil(itemCount / safeColumns));
|
||||||
const scale = cardScale.value;
|
const scale = cardScale.value;
|
||||||
@@ -853,10 +866,7 @@
|
|||||||
const getRowCount = (row) => (row && row.type === 'header' ? row.count : 0);
|
const getRowCount = (row) => (row && row.type === 'header' ? row.count : 0);
|
||||||
|
|
||||||
watch([searchTerm, activeSegment], () => {
|
watch([searchTerm, activeSegment], () => {
|
||||||
nextTick(() => {
|
scheduleVirtualMeasure({ updateGridWidth: true });
|
||||||
updateGridWidth();
|
|
||||||
virtualizer.value?.measure?.();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(showSameInstance, (value) => {
|
watch(showSameInstance, (value) => {
|
||||||
@@ -867,19 +877,13 @@
|
|||||||
activeSegment.value = 'online';
|
activeSegment.value = 'online';
|
||||||
}
|
}
|
||||||
|
|
||||||
nextTick(() => {
|
scheduleVirtualMeasure({ updateGridWidth: true });
|
||||||
updateGridWidth();
|
|
||||||
virtualizer.value?.measure?.();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => filteredFriends.value.length,
|
() => filteredFriends.value.length,
|
||||||
() => {
|
() => {
|
||||||
nextTick(() => {
|
scheduleVirtualMeasure({ updateGridWidth: true });
|
||||||
updateGridWidth();
|
|
||||||
virtualizer.value?.measure?.();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -887,23 +891,17 @@
|
|||||||
if (!settingsReady.value) {
|
if (!settingsReady.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
nextTick(() => {
|
scheduleVirtualMeasure({ updateGridWidth: true });
|
||||||
updateGridWidth();
|
|
||||||
virtualizer.value?.measure?.();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(virtualRows, () => {
|
watch(virtualRows, () => {
|
||||||
nextTick(() => {
|
scheduleVirtualMeasure();
|
||||||
virtualizer.value?.measure?.();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
setupResizeHandling();
|
setupResizeHandling();
|
||||||
updateGridWidth();
|
scheduleVirtualMeasure({ updateGridWidth: true });
|
||||||
virtualizer.value?.measure?.();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -944,8 +942,7 @@
|
|||||||
settingsReady.value = true;
|
settingsReady.value = true;
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
setupResizeHandling();
|
setupResizeHandling();
|
||||||
updateGridWidth();
|
scheduleVirtualMeasure({ updateGridWidth: true });
|
||||||
virtualizer.value?.measure?.();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,6 +94,13 @@ vi.mock('../../../shared/utils/location.js', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../../shared/utils', () => ({
|
vi.mock('../../../shared/utils', () => ({
|
||||||
|
debounce: (fn, delay) => {
|
||||||
|
let timer = null;
|
||||||
|
return (...args) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = setTimeout(() => fn(...args), delay);
|
||||||
|
};
|
||||||
|
},
|
||||||
getFriendsSortFunction: () => (a, b) =>
|
getFriendsSortFunction: () => (a, b) =>
|
||||||
String(a?.displayName ?? '').localeCompare(String(b?.displayName ?? ''))
|
String(a?.displayName ?? '').localeCompare(String(b?.displayName ?? ''))
|
||||||
}));
|
}));
|
||||||
@@ -295,13 +302,17 @@ describe('FriendsLocations.vue', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('persists card scale and same-instance preferences', async () => {
|
test('persists card scale and same-instance preferences', async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
const wrapper = mount(FriendsLocations);
|
const wrapper = mount(FriendsLocations);
|
||||||
await flushSettings();
|
await flushSettings();
|
||||||
|
mocks.configSetString.mockClear();
|
||||||
|
mocks.configSetBool.mockClear();
|
||||||
|
|
||||||
await wrapper.get('[data-testid="set-scale"]').trigger('click');
|
await wrapper.get('[data-testid="set-scale"]').trigger('click');
|
||||||
await wrapper
|
await wrapper
|
||||||
.get('[data-testid="toggle-same-instance"]')
|
.get('[data-testid="toggle-same-instance"]')
|
||||||
.trigger('click');
|
.trigger('click');
|
||||||
|
vi.advanceTimersByTime(200);
|
||||||
|
|
||||||
expect(mocks.configSetString).toHaveBeenCalledWith(
|
expect(mocks.configSetString).toHaveBeenCalledWith(
|
||||||
'VRCX_FriendLocationCardScale',
|
'VRCX_FriendLocationCardScale',
|
||||||
@@ -311,6 +322,20 @@ describe('FriendsLocations.vue', () => {
|
|||||||
'VRCX_FriendLocationShowSameInstance',
|
'VRCX_FriendLocationShowSameInstance',
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('coalesces repeated virtualizer measure requests in the same tick', async () => {
|
||||||
|
const wrapper = mount(FriendsLocations);
|
||||||
|
await flushSettings();
|
||||||
|
mocks.virtualMeasure.mockClear();
|
||||||
|
|
||||||
|
wrapper.vm.searchTerm = 'alice';
|
||||||
|
wrapper.vm.activeSegment = 'offline';
|
||||||
|
await nextTick();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(mocks.virtualMeasure).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('renders empty state when no rows match', async () => {
|
test('renders empty state when no rows match', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user