diff --git a/src/App.vue b/src/App.vue index a8e2cf09..685b85bd 100644 --- a/src/App.vue +++ b/src/App.vue @@ -26,6 +26,7 @@ import { TooltipProvider } from './components/ui/tooltip'; import { createGlobalStores } from './stores'; import { initNoty } from './plugin/noty'; + import { runCheckVRChatDebugLoggingFlow } from './coordinators/gameCoordinator'; import AlertDialogModal from './components/ui/alert-dialog/AlertDialogModal.vue'; import MacOSTitleBar from './components/MacOSTitleBar.vue'; @@ -60,6 +61,6 @@ await store.auth.migrateStoredUsers(); store.auth.autoLoginAfterMounted(); store.vrcx.checkAutoBackupRestoreVrcRegistry(); - store.game.checkVRChatDebugLogging(); + runCheckVRChatDebugLoggingFlow(); }); diff --git a/src/api/__tests__/favoriteQuerySync.test.js b/src/api/__tests__/favoriteQuerySync.test.js index a9570f0b..471bd7bc 100644 --- a/src/api/__tests__/favoriteQuerySync.test.js +++ b/src/api/__tests__/favoriteQuerySync.test.js @@ -11,17 +11,18 @@ vi.mock('../../service/request', () => ({ })); vi.mock('../../stores', () => ({ - useFavoriteStore: () => ({ - handleFavoriteAdd: (...args) => mockHandleFavoriteAdd(...args), - handleFavoriteDelete: (...args) => mockHandleFavoriteDelete(...args), - handleFavoriteGroupClear: (...args) => - mockHandleFavoriteGroupClear(...args) - }), useUserStore: () => ({ currentUser: { id: 'usr_me' } }) })); +vi.mock('../../coordinators/favoriteCoordinator', () => ({ + handleFavoriteAdd: (...args) => mockHandleFavoriteAdd(...args), + handleFavoriteDelete: (...args) => mockHandleFavoriteDelete(...args), + handleFavoriteGroupClear: (...args) => + mockHandleFavoriteGroupClear(...args) +})); + vi.mock('../../queries', () => ({ queryClient: { invalidateQueries: (...args) => mockInvalidateQueries(...args) diff --git a/src/api/__tests__/queryRequest.test.js b/src/api/__tests__/queryRequest.test.js index c26aa5f2..4f16458a 100644 --- a/src/api/__tests__/queryRequest.test.js +++ b/src/api/__tests__/queryRequest.test.js @@ -6,6 +6,9 @@ const mockGetWorlds = vi.fn(); const mockGetGroupCalendar = vi.fn(); vi.mock('../../queries', () => ({ + queryClient: { + invalidateQueries: vi.fn().mockResolvedValue(undefined) + }, entityQueryPolicies: { user: { staleTime: 20000, gcTime: 90000, retry: 1, refetchOnWindowFocus: false }, worldCollection: { staleTime: 60000, gcTime: 300000, retry: 1, refetchOnWindowFocus: false }, diff --git a/src/api/favorite.js b/src/api/favorite.js index 8fdefded..53dc95ba 100644 --- a/src/api/favorite.js +++ b/src/api/favorite.js @@ -1,4 +1,9 @@ import { useFavoriteStore, useUserStore } from '../stores'; +import { + handleFavoriteAdd, + handleFavoriteDelete, + handleFavoriteGroupClear +} from '../coordinators/favoriteCoordinator'; import { queryClient } from '../queries'; import { request } from '../service/request'; @@ -63,7 +68,7 @@ const favoriteReq = { json, params }; - useFavoriteStore().handleFavoriteAdd(args); + handleFavoriteAdd(args); refetchActiveFavoriteQueries(); return args; }); @@ -81,7 +86,7 @@ const favoriteReq = { json, params }; - useFavoriteStore().handleFavoriteDelete(params.objectId); + handleFavoriteDelete(params.objectId); refetchActiveFavoriteQueries(); return args; }); @@ -145,7 +150,7 @@ const favoriteReq = { json, params }; - useFavoriteStore().handleFavoriteGroupClear(args); + handleFavoriteGroupClear(args); refetchActiveFavoriteQueries(); return args; }); diff --git a/src/components/dialogs/AvatarDialog/AvatarDialog.vue b/src/components/dialogs/AvatarDialog/AvatarDialog.vue index 6d1bfdea..65ce0f76 100644 --- a/src/components/dialogs/AvatarDialog/AvatarDialog.vue +++ b/src/components/dialogs/AvatarDialog/AvatarDialog.vue @@ -600,6 +600,7 @@ import { database } from '../../../service/database'; import { formatJsonVars } from '../../../shared/utils/base/ui'; import { handleImageUploadInput } from '../../../shared/utils/imageUpload'; + import { runDeleteVRChatCacheFlow as deleteVRChatCache } from '../../../coordinators/gameCoordinator'; import { useAvatarDialogCommands } from './useAvatarDialogCommands'; import DialogJsonTab from '../DialogJsonTab.vue'; @@ -617,7 +618,6 @@ avatarStore; const { showFavoriteDialog } = useFavoriteStore(); const { isGameRunning } = storeToRefs(useGameStore()); - const { deleteVRChatCache } = useGameStore(); const { showFullscreenImageDialog } = useGalleryStore(); const { isDarkMode } = storeToRefs(useAppearanceSettingsStore()); const modalStore = useModalStore(); diff --git a/src/components/dialogs/ChooseFavoriteGroupDialog.vue b/src/components/dialogs/ChooseFavoriteGroupDialog.vue index 4c4bfd79..10f18bf2 100644 --- a/src/components/dialogs/ChooseFavoriteGroupDialog.vue +++ b/src/components/dialogs/ChooseFavoriteGroupDialog.vue @@ -108,6 +108,14 @@ import { useI18n } from 'vue-i18n'; import { useFavoriteStore, useUserStore } from '../../stores'; + import { + addLocalWorldFavorite, + removeLocalWorldFavorite, + addLocalAvatarFavorite, + removeLocalAvatarFavorite, + addLocalFriendFavorite, + removeLocalFriendFavorite + } from '../../coordinators/favoriteCoordinator'; import { favoriteRequest } from '../../api'; const { t } = useI18n(); @@ -124,18 +132,12 @@ } = storeToRefs(favoriteStore); const { localWorldFavGroupLength, - addLocalWorldFavorite, hasLocalWorldFavorite, hasLocalAvatarFavorite, - addLocalAvatarFavorite, localAvatarFavGroupLength, - removeLocalAvatarFavorite, - removeLocalWorldFavorite, deleteFavoriteNoConfirm, localFriendFavGroupLength, - addLocalFriendFavorite, - hasLocalFriendFavorite, - removeLocalFriendFavorite + hasLocalFriendFavorite } = favoriteStore; const { isLocalUserVrcPlusSupporter } = storeToRefs(useUserStore()); diff --git a/src/components/dialogs/UserDialog/UserDialogFavoriteWorldsTab.vue b/src/components/dialogs/UserDialog/UserDialogFavoriteWorldsTab.vue index fe94cc54..5cd3c16a 100644 --- a/src/components/dialogs/UserDialog/UserDialogFavoriteWorldsTab.vue +++ b/src/components/dialogs/UserDialog/UserDialogFavoriteWorldsTab.vue @@ -76,13 +76,13 @@ import DeprecationAlert from '@/components/DeprecationAlert.vue'; import { useFavoriteStore, useUserStore, useWorldStore } from '../../../stores'; + import { handleFavoriteWorldList } from '../../../coordinators/favoriteCoordinator'; import { favoriteRequest } from '../../../api'; const { t } = useI18n(); const { userDialog, currentUser } = storeToRefs(useUserStore()); const { favoriteLimits } = storeToRefs(useFavoriteStore()); - const { handleFavoriteWorldList } = useFavoriteStore(); const { showWorldDialog } = useWorldStore(); const favoriteWorldsTab = ref('0'); diff --git a/src/components/dialogs/WorldDialog/WorldDialog.vue b/src/components/dialogs/WorldDialog/WorldDialog.vue index 6caba9e7..1c3cbabf 100644 --- a/src/components/dialogs/WorldDialog/WorldDialog.vue +++ b/src/components/dialogs/WorldDialog/WorldDialog.vue @@ -405,6 +405,7 @@ import { deleteVRChatCache, openFolderGeneric } from '../../../shared/utils'; import { Badge } from '../../ui/badge'; import { formatJsonVars } from '../../../shared/utils/base/ui'; + import { runNewInstanceSelfInviteFlow as newInstanceSelfInvite } from '../../../coordinators/inviteCoordinator'; import { useWorldDialogCommands } from './useWorldDialogCommands'; import DialogJsonTab from '../DialogJsonTab.vue'; @@ -421,7 +422,7 @@ const { worldDialog } = storeToRefs(useWorldStore()); const { cachedWorlds, showWorldDialog } = useWorldStore(); const { lastLocation } = storeToRefs(useLocationStore()); - const { newInstanceSelfInvite, canOpenInstanceInGame } = useInviteStore(); + const { canOpenInstanceInGame } = useInviteStore(); const { showFavoriteDialog } = useFavoriteStore(); const { showPreviousInstancesListDialog: openPreviousInstancesListDialog } = useInstanceStore(); const { isGameRunning } = storeToRefs(useGameStore()); diff --git a/src/coordinators/favoriteCoordinator.js b/src/coordinators/favoriteCoordinator.js new file mode 100644 index 00000000..bd008d37 --- /dev/null +++ b/src/coordinators/favoriteCoordinator.js @@ -0,0 +1,1259 @@ +import { toast } from 'vue-sonner'; +import { useFavoriteStore } from '../stores/favorite'; +import { useAppearanceSettingsStore } from '../stores/settings/appearance'; +import { useAvatarStore } from '../stores/avatar'; +import { useFriendStore } from '../stores/friend'; +import { useGeneralSettingsStore } from '../stores/settings/general'; +import { useUserStore } from '../stores/user'; +import { useWorldStore } from '../stores/world'; +import { runUpdateFriendFlow } from './friendPresenceCoordinator'; +import { avatarRequest, favoriteRequest, queryRequest } from '../api'; +import { database } from '../service/database'; +import { i18n } from '../plugin/i18n'; +import { processBulk } from '../service/request'; +import { watchState } from '../service/watchState'; +import { + compareByName, + createDefaultFavoriteCachedRef, + removeFromArray, + replaceReactiveObject +} from '../shared/utils'; + +// --- handleFavorite / handleFavoriteAtDelete / handleFavoriteAdd --- + +/** + * + * @param {object} args + * @returns {void} + */ +export function handleFavorite(args) { + const userStore = useUserStore(); + const worldStore = useWorldStore(); + const avatarStore = useAvatarStore(); + + args.ref = applyFavoriteCached(args.json); + applyFavorite(args.ref.type, args.ref.favoriteId); + runUpdateFriendFlow(args.ref.favoriteId); + const { ref } = args; + const userDialog = userStore.userDialog; + if (userDialog.visible && ref.favoriteId === userDialog.id) { + userStore.setUserDialogIsFavorite(true); + } + const worldDialog = worldStore.worldDialog; + if (worldDialog.visible && ref.favoriteId === worldDialog.id) { + worldStore.setWorldDialogIsFavorite(true); + } + const avatarDialog = avatarStore.avatarDialog; + if (avatarDialog.visible && ref.favoriteId === avatarDialog.id) { + avatarStore.setAvatarDialogIsFavorite(true); + } +} + +/** + * + * @param {object} args + * @returns {void} + */ +export function handleFavoriteAdd(args) { + const favoriteStore = useFavoriteStore(); + const friendStore = useFriendStore(); + const avatarStore = useAvatarStore(); + const generalSettingsStore = useGeneralSettingsStore(); + + handleFavorite({ + json: args.json, + params: { + favoriteId: args.json.id + } + }); + if (!favoriteStore.favoritesSortOrder.includes(args.params.favoriteId)) { + favoriteStore.favoritesSortOrder.unshift(args.params.favoriteId); + } + + if ( + args.params.type === 'avatar' && + !avatarStore.cachedAvatars.has(args.params.favoriteId) + ) { + refreshFavoriteAvatars(args.params.tags); + } + + if ( + args.params.type === 'friend' && + (!generalSettingsStore.localFavoriteFriendsGroups.some( + (key) => !key.startsWith('local:') + ) || + generalSettingsStore.localFavoriteFriendsGroups.includes( + 'friend:' + args.params.tags + )) + ) { + friendStore.updateLocalFavoriteFriends(); + } + favoriteStore.updateFavoriteDialog(args.params.objectId); +} + +/** + * + * @param {object} ref + * @returns {void} + */ +export function handleFavoriteAtDelete(ref) { + const favoriteStore = useFavoriteStore(); + const friendStore = useFriendStore(); + const userStore = useUserStore(); + const worldStore = useWorldStore(); + const avatarStore = useAvatarStore(); + + const favorite = favoriteStore.state.favoriteObjects.get(ref.favoriteId); + removeFromArray(favoriteStore.state.favoriteFriends_, favorite); + removeFromArray(favoriteStore.state.favoriteWorlds_, favorite); + removeFromArray(favoriteStore.state.favoriteAvatars_, favorite); + favoriteStore.cachedFavorites.delete(ref.id); + favoriteStore.cachedFavoritesByObjectId.delete(ref.favoriteId); + favoriteStore.state.favoriteObjects.delete(ref.favoriteId); + friendStore.localFavoriteFriends.delete(ref.favoriteId); + favoriteStore.favoritesSortOrder = favoriteStore.favoritesSortOrder.filter( + (id) => id !== ref.favoriteId + ); + + runUpdateFriendFlow(ref.favoriteId); + friendStore.updateSidebarFavorites(); + const userDialog = userStore.userDialog; + if (userDialog.visible && userDialog.id === ref.favoriteId) { + userStore.setUserDialogIsFavorite(false); + } + const worldDialog = worldStore.worldDialog; + if (worldDialog.visible && worldDialog.id === ref.favoriteId) { + worldStore.setWorldDialogIsFavorite( + favoriteStore.localWorldFavoritesList.includes(worldDialog.id) + ); + } + const avatarDialog = avatarStore.avatarDialog; + if (avatarDialog.visible && avatarDialog.id === ref.favoriteId) { + avatarStore.setAvatarDialogIsFavorite(false); + } + favoriteStore.countFavoriteGroups(); +} + +/** + * + * @param {string} objectId + * @returns {void} + */ +export function handleFavoriteDelete(objectId) { + const favoriteStore = useFavoriteStore(); + const ref = favoriteStore.getCachedFavoritesByObjectId(objectId); + if (typeof ref === 'undefined') { + return; + } + handleFavoriteAtDelete(ref); +} + +/** + * + * @param {object} args + * @returns {void} + */ +export function handleFavoriteGroupClear(args) { + const favoriteStore = useFavoriteStore(); + const key = `${args.params.type}:${args.params.group}`; + for (const ref of favoriteStore.cachedFavorites.values()) { + if (ref.$groupKey !== key) { + continue; + } + handleFavoriteAtDelete(ref); + } +} + +// --- List handlers --- + +/** + * + * @param {object} args + * @returns {void} + */ +export function handleFavoriteWorldList(args) { + const worldStore = useWorldStore(); + for (const json of args.json) { + if (json.id === '???') { + continue; + } + worldStore.applyWorld(json); + } +} + +/** + * + * @param {object} args + */ +export function handleFavoriteAvatarList(args) { + const avatarStore = useAvatarStore(); + for (const json of args.json) { + if (json.releaseStatus === 'hidden') { + continue; + } + avatarStore.applyAvatar(json); + } +} + +// --- applyFavoriteCached / applyFavorite --- + +/** + * + * @param {object} json + * @returns {object} + */ +export function applyFavoriteCached(json) { + const favoriteStore = useFavoriteStore(); + const friendStore = useFriendStore(); + const generalSettingsStore = useGeneralSettingsStore(); + + let ref = favoriteStore.cachedFavorites.get(json.id); + if (typeof ref === 'undefined') { + ref = createDefaultFavoriteCachedRef(json); + favoriteStore.cachedFavorites.set(ref.id, ref); + favoriteStore.cachedFavoritesByObjectId.set(ref.favoriteId, ref); + if ( + ref.type === 'friend' && + (!generalSettingsStore.localFavoriteFriendsGroups.some( + (key) => !key.startsWith('local:') + ) || + generalSettingsStore.localFavoriteFriendsGroups.includes( + ref.$groupKey + )) + ) { + friendStore.localFavoriteFriends.add(ref.favoriteId); + friendStore.updateSidebarFavorites(); + } + if (!favoriteStore.isFavoriteLoading) { + favoriteStore.countFavoriteGroups(); + } + } else { + if (ref.favoriteId !== json.favoriteId) { + favoriteStore.cachedFavoritesByObjectId.delete(ref.favoriteId); + } + Object.assign(ref, json); + favoriteStore.cachedFavoritesByObjectId.set(ref.favoriteId, ref); + } + + return ref; +} + +/** + * + * @param {'friend' | 'world' | 'vrcPlusWorld' | 'avatar'} type + * @param {string} objectId + * @returns {Promise} + */ +export async function applyFavorite(type, objectId) { + const favoriteStore = useFavoriteStore(); + const userStore = useUserStore(); + const friendStore = useFriendStore(); + const worldStore = useWorldStore(); + const avatarStore = useAvatarStore(); + + let ref; + const favorite = favoriteStore.getCachedFavoritesByObjectId(objectId); + let ctx = favoriteStore.state.favoriteObjects.get(objectId); + if (typeof favorite !== 'undefined') { + let isTypeChanged = false; + if (typeof ctx === 'undefined') { + ctx = { + id: objectId, + type, + groupKey: favorite.$groupKey, + ref: null, + name: '' + }; + if (type === 'friend') { + ref = userStore.cachedUsers.get(objectId); + if (typeof ref === 'undefined') { + ref = friendStore.friendLog.get(objectId); + if (typeof ref !== 'undefined' && ref.displayName) { + ctx.name = ref.displayName; + } + } else { + ctx.ref = ref; + ctx.name = ref.displayName; + } + } else if (type === 'world' || type === 'vrcPlusWorld') { + ref = worldStore.cachedWorlds.get(objectId); + if (typeof ref !== 'undefined') { + ctx.ref = ref; + ctx.name = ref.name; + } + } else if (type === 'avatar') { + ref = avatarStore.cachedAvatars.get(objectId); + if (typeof ref !== 'undefined') { + ctx.ref = ref; + ctx.name = ref.name; + } + } + favoriteStore.state.favoriteObjects.set(objectId, ctx); + isTypeChanged = true; + } else { + if (ctx.type !== type) { + // WTF??? + isTypeChanged = true; + if (type === 'friend') { + removeFromArray(favoriteStore.state.favoriteFriends_, ctx); + } else if (type === 'world' || type === 'vrcPlusWorld') { + removeFromArray(favoriteStore.state.favoriteWorlds_, ctx); + } else if (type === 'avatar') { + removeFromArray(favoriteStore.state.favoriteAvatars_, ctx); + } + } + if (type === 'friend') { + ref = userStore.cachedUsers.get(objectId); + if (typeof ref !== 'undefined') { + if (ctx.ref !== ref) { + ctx.ref = ref; + } + if (ctx.name !== ref.displayName) { + ctx.name = ref.displayName; + } + } + // else too bad + } else if (type === 'world' || type === 'vrcPlusWorld') { + ref = worldStore.cachedWorlds.get(objectId); + if (typeof ref !== 'undefined') { + if (ctx.ref !== ref) { + ctx.ref = ref; + } + if (ctx.name !== ref.name) { + ctx.name = ref.name; + } + } else { + // try fetch from local world favorites + const world = + await database.getCachedWorldById(objectId); + if (world) { + ctx.ref = world; + ctx.name = world.name; + ctx.deleted = true; + } + if (!world) { + // try fetch from local world history + const worldName = + await database.getGameLogWorldNameByWorldId( + objectId + ); + if (worldName) { + ctx.name = worldName; + ctx.deleted = true; + } + } + } + } else if (type === 'avatar') { + ref = avatarStore.cachedAvatars.get(objectId); + if (typeof ref !== 'undefined') { + if (ctx.ref !== ref) { + ctx.ref = ref; + } + if (ctx.name !== ref.name) { + ctx.name = ref.name; + } + } else { + // try fetch from local avatar history + const avatar = + await database.getCachedAvatarById(objectId); + if (avatar) { + ctx.ref = avatar; + ctx.name = avatar.name; + ctx.deleted = true; + } + } + } + } + if (isTypeChanged) { + if (type === 'friend') { + favoriteStore.state.favoriteFriends_.push(ctx); + } else if (type === 'world' || type === 'vrcPlusWorld') { + favoriteStore.state.favoriteWorlds_.push(ctx); + } else if (type === 'avatar') { + favoriteStore.state.favoriteAvatars_.push(ctx); + } + } + } +} + +// --- Refresh flows --- + +/** + * + * @returns {void} + */ +export function refreshFavorites() { + const favoriteStore = useFavoriteStore(); + const friendStore = useFriendStore(); + + if (favoriteStore.isFavoriteLoading) { + return; + } + favoriteStore.isFavoriteLoading = true; + queryRequest.fetch('favoriteLimits').then((args) => { + favoriteStore.favoriteLimits = { + ...favoriteStore.favoriteLimits, + ...args.json + }; + }).catch((err) => { + console.error(err); + }); + let newFavoriteSortOrder = []; + processBulk({ + fn: (params) => favoriteRequest.getFavorites(params), + N: -1, + params: { + n: 300, + offset: 0 + }, + handle(args) { + for (const json of args.json) { + newFavoriteSortOrder.push(json.favoriteId); + handleFavorite({ + json, + params: { + favoriteId: json.id + } + }); + } + }, + done(ok) { + if (ok) { + for (const id of favoriteStore.favoritesSortOrder) { + if (!newFavoriteSortOrder.includes(id)) { + const fav = favoriteStore.cachedFavorites.get(id); + if (fav) { + handleFavoriteAtDelete(fav); + } + } + } + favoriteStore.favoritesSortOrder = newFavoriteSortOrder; + } + refreshFavoriteItems(); + favoriteStore.refreshFavoriteGroups(); + friendStore.updateLocalFavoriteFriends(); + favoriteStore.isFavoriteLoading = false; + watchState.isFavoritesLoaded = true; + favoriteStore.countFavoriteGroups(); + } + }); +} + +/** + * + * @param {string} tag + * @returns {void} + */ +export async function refreshFavoriteAvatars(tag) { + const params = { + n: 300, + offset: 0, + tag + }; + const args = await favoriteRequest.getFavoriteAvatars(params); + handleFavoriteAvatarList(args); +} + +/** + * @returns {void} + */ +export function refreshFavoriteItems() { + const favoriteStore = useFavoriteStore(); + const types = { + world: [0, (params) => favoriteRequest.getFavoriteWorlds(params)], + avatar: [0, (params) => favoriteRequest.getFavoriteAvatars(params)] + }; + const tags = []; + for (const ref of favoriteStore.cachedFavorites.values()) { + const type = types[ref.type]; + if (typeof type === 'undefined') { + continue; + } + if (ref.type === 'avatar' && !tags.includes(ref.tags[0])) { + tags.push(ref.tags[0]); + } + ++type[0]; + } + for (const type in types) { + const [N, fn] = types[type]; + if (N > 0) { + if (type === 'avatar') { + for (const tag of tags) { + processBulk({ + fn, + N, + handle: (args) => handleFavoriteAvatarList(args), + params: { + n: 300, + offset: 0, + tag + } + }); + } + } else { + processBulk({ + fn, + N, + handle: (args) => handleFavoriteWorldList(args), + params: { + n: 300, + offset: 0 + } + }); + } + } + } +} + +// --- Sort helpers --- + +/** + * @returns {void} + */ +export function sortLocalAvatarFavorites() { + const favoriteStore = useFavoriteStore(); + const appearanceSettingsStore = useAppearanceSettingsStore(); + if (!appearanceSettingsStore.sortFavorites) { + for (let i = 0; i < favoriteStore.localAvatarFavoriteGroups.length; ++i) { + const group = favoriteStore.localAvatarFavoriteGroups[i]; + if (favoriteStore.localAvatarFavorites[group]) { + favoriteStore.localAvatarFavorites[group].sort(compareByName); + } + } + } +} + +/** + * @returns {void} + */ +export function sortLocalWorldFavorites() { + const favoriteStore = useFavoriteStore(); + const appearanceSettingsStore = useAppearanceSettingsStore(); + if (!appearanceSettingsStore.sortFavorites) { + for (let i = 0; i < favoriteStore.localWorldFavoriteGroups.length; ++i) { + const group = favoriteStore.localWorldFavoriteGroups[i]; + if (favoriteStore.localWorldFavorites[group]) { + favoriteStore.localWorldFavorites[group].sort(compareByName); + } + } + } +} + +// --- Local World Favorites --- + +/** + * + * @param {string} worldId + * @param {string} group + */ +export function addLocalWorldFavorite(worldId, group) { + const favoriteStore = useFavoriteStore(); + const worldStore = useWorldStore(); + + if (favoriteStore.hasLocalWorldFavorite(worldId, group)) { + return; + } + const ref = worldStore.cachedWorlds.get(worldId); + if (typeof ref === 'undefined') { + return; + } + if (!favoriteStore.localWorldFavorites[group]) { + favoriteStore.localWorldFavorites[group] = []; + } + + favoriteStore.localWorldFavorites[group].unshift(ref); + database.addWorldToCache(ref); + database.addWorldToFavorites(worldId, group); + if ( + favoriteStore.favoriteDialog.visible && + favoriteStore.favoriteDialog.objectId === worldId + ) { + favoriteStore.updateFavoriteDialog(worldId); + } + if ( + worldStore.worldDialog.visible && + worldStore.worldDialog.id === worldId + ) { + worldStore.setWorldDialogIsFavorite(true); + } + + // update UI + sortLocalWorldFavorites(); +} + +/** + * + * @param {string} worldId + * @param {string} group + */ +export function removeLocalWorldFavorite(worldId, group) { + const favoriteStore = useFavoriteStore(); + const worldStore = useWorldStore(); + + let i; + const favoriteGroup = favoriteStore.localWorldFavorites[group]; + for (i = 0; i < favoriteGroup.length; ++i) { + if (favoriteGroup[i].id === worldId) { + favoriteGroup.splice(i, 1); + } + } + + // remove from cache if no longer in favorites + let worldInFavorites = false; + for (i = 0; i < favoriteStore.localWorldFavoriteGroups.length; ++i) { + const groupName = favoriteStore.localWorldFavoriteGroups[i]; + if (!favoriteStore.localWorldFavorites[groupName] || group === groupName) { + continue; + } + for (let j = 0; j < favoriteStore.localWorldFavorites[groupName].length; ++j) { + const id = favoriteStore.localWorldFavorites[groupName][j].id; + if (id === worldId) { + worldInFavorites = true; + break; + } + } + } + if (!worldInFavorites) { + database.removeWorldFromCache(worldId); + } + database.removeWorldFromFavorites(worldId, group); + if ( + favoriteStore.favoriteDialog.visible && + favoriteStore.favoriteDialog.objectId === worldId + ) { + favoriteStore.updateFavoriteDialog(worldId); + } + if ( + worldStore.worldDialog.visible && + worldStore.worldDialog.id === worldId + ) { + worldStore.setWorldDialogIsFavorite( + favoriteStore.getCachedFavoritesByObjectId(worldId) + ); + } + + // update UI + sortLocalWorldFavorites(); +} + +/** + * + * @returns {Promise} + */ +export async function getLocalWorldFavorites() { + const favoriteStore = useFavoriteStore(); + const worldStore = useWorldStore(); + + const localGroups = new Set(); + const localListSet = new Set(); + const localFavorites = Object.create(null); + + const worldCache = await database.getWorldCache(); + for (let i = 0; i < worldCache.length; ++i) { + const ref = worldCache[i]; + if (!worldStore.cachedWorlds.has(ref.id)) { + worldStore.applyWorld(ref); + } + } + + const favorites = await database.getWorldFavorites(); + for (let i = 0; i < favorites.length; ++i) { + const favorite = favorites[i]; + + localListSet.add(favorite.worldId); + + if (!localFavorites[favorite.groupName]) { + localFavorites[favorite.groupName] = []; + } + localGroups.add(favorite.groupName); + + let ref = worldStore.cachedWorlds.get(favorite.worldId); + if (typeof ref === 'undefined') { + ref = { id: favorite.worldId }; + } + localFavorites[favorite.groupName].unshift(ref); + } + + let groupsArr = Array.from(localGroups); + if (groupsArr.length === 0) { + localFavorites.Favorites = []; + // default group + groupsArr = ['Favorites']; + } + + replaceReactiveObject(favoriteStore.localWorldFavorites, localFavorites); + + sortLocalWorldFavorites(); +} + +/** + * + * @param {string} newName + * @param {string} group + */ +export function renameLocalWorldFavoriteGroup(newName, group) { + const favoriteStore = useFavoriteStore(); + const { t } = i18n.global; + if (favoriteStore.localWorldFavoriteGroups.includes(newName)) { + toast.error( + t('prompt.local_favorite_group_rename.message.error', { + name: newName + }) + ); + return; + } + favoriteStore.localWorldFavorites[newName] = favoriteStore.localWorldFavorites[group]; + + delete favoriteStore.localWorldFavorites[group]; + database.renameWorldFavoriteGroup(newName, group); + sortLocalWorldFavorites(); +} + +/** + * + * @param {string} group + */ +export function newLocalWorldFavoriteGroup(group) { + const favoriteStore = useFavoriteStore(); + const { t } = i18n.global; + if (favoriteStore.localWorldFavoriteGroups.includes(group)) { + toast.error( + t('prompt.new_local_favorite_group.message.error', { + name: group + }) + ); + return; + } + if (!favoriteStore.localWorldFavorites[group]) { + favoriteStore.localWorldFavorites[group] = []; + } + sortLocalWorldFavorites(); +} + +// --- Local Avatar Favorites --- + +/** + * + * @param {string} avatarId + * @param {string} group + */ +export function addLocalAvatarFavorite(avatarId, group) { + const favoriteStore = useFavoriteStore(); + const avatarStore = useAvatarStore(); + + if (favoriteStore.hasLocalAvatarFavorite(avatarId, group)) { + return; + } + const ref = avatarStore.cachedAvatars.get(avatarId); + if (typeof ref === 'undefined') { + return; + } + if (!favoriteStore.localAvatarFavorites[group]) { + favoriteStore.localAvatarFavorites[group] = []; + } + favoriteStore.localAvatarFavorites[group].unshift(ref); + database.addAvatarToCache(ref); + database.addAvatarToFavorites(avatarId, group); + if ( + favoriteStore.favoriteDialog.visible && + favoriteStore.favoriteDialog.objectId === avatarId + ) { + favoriteStore.updateFavoriteDialog(avatarId); + } + if ( + avatarStore.avatarDialog.visible && + avatarStore.avatarDialog.id === avatarId + ) { + avatarStore.setAvatarDialogIsFavorite(true); + } + + // update UI + sortLocalAvatarFavorites(); +} + +/** + * + * @param {string} avatarId + * @param {string} group + */ +export function removeLocalAvatarFavorite(avatarId, group) { + const favoriteStore = useFavoriteStore(); + const avatarStore = useAvatarStore(); + + let i; + const favoriteGroup = favoriteStore.localAvatarFavorites[group]; + for (i = 0; i < favoriteGroup.length; ++i) { + if (favoriteGroup[i].id === avatarId) { + favoriteGroup.splice(i, 1); + } + } + + // remove from cache if no longer in favorites + let avatarInFavorites = false; + for (i = 0; i < favoriteStore.localAvatarFavoriteGroups.length; ++i) { + const groupName = favoriteStore.localAvatarFavoriteGroups[i]; + if (!favoriteStore.localAvatarFavorites[groupName] || group === groupName) { + continue; + } + for (let j = 0; j < favoriteStore.localAvatarFavorites[groupName].length; ++j) { + const id = favoriteStore.localAvatarFavorites[groupName][j].id; + if (id === avatarId) { + avatarInFavorites = true; + break; + } + } + } + if (!avatarInFavorites) { + if (!avatarStore.avatarHistory.includes(avatarId)) { + database.removeAvatarFromCache(avatarId); + } + } + database.removeAvatarFromFavorites(avatarId, group); + if ( + favoriteStore.favoriteDialog.visible && + favoriteStore.favoriteDialog.objectId === avatarId + ) { + favoriteStore.updateFavoriteDialog(avatarId); + } + if ( + avatarStore.avatarDialog.visible && + avatarStore.avatarDialog.id === avatarId + ) { + avatarStore.setAvatarDialogIsFavorite( + favoriteStore.getCachedFavoritesByObjectId(avatarId) + ); + } + + // update UI + sortLocalAvatarFavorites(); +} + +/** + * + * @param {string} group + */ +export function deleteLocalAvatarFavoriteGroup(group) { + const favoriteStore = useFavoriteStore(); + const avatarStore = useAvatarStore(); + + let i; + // remove from cache if no longer in favorites + const avatarIdRemoveList = new Set(); + const favoriteGroup = favoriteStore.localAvatarFavorites[group]; + for (i = 0; i < favoriteGroup.length; ++i) { + avatarIdRemoveList.add(favoriteGroup[i].id); + } + + delete favoriteStore.localAvatarFavorites[group]; + database.deleteAvatarFavoriteGroup(group); + + for (i = 0; i < favoriteStore.localAvatarFavoriteGroups.length; ++i) { + const groupName = favoriteStore.localAvatarFavoriteGroups[i]; + if (!favoriteStore.localAvatarFavorites[groupName]) { + continue; + } + for (let j = 0; j < favoriteStore.localAvatarFavorites[groupName].length; ++j) { + const avatarId = favoriteStore.localAvatarFavorites[groupName][j].id; + if (avatarIdRemoveList.has(avatarId)) { + avatarIdRemoveList.delete(avatarId); + break; + } + } + } + + avatarIdRemoveList.forEach((id) => { + // remove from cache if no longer in favorites + let avatarInFavorites = false; + loop: for ( + let i = 0; + i < favoriteStore.localAvatarFavoriteGroups.length; + ++i + ) { + const groupName = favoriteStore.localAvatarFavoriteGroups[i]; + if (!favoriteStore.localAvatarFavorites[groupName] || group === groupName) { + continue loop; + } + for ( + let j = 0; + j < favoriteStore.localAvatarFavorites[groupName].length; + ++j + ) { + const avatarId = favoriteStore.localAvatarFavorites[groupName][j].id; + if (id === avatarId) { + avatarInFavorites = true; + break loop; + } + } + } + if (!avatarInFavorites) { + if (!avatarStore.avatarHistory.includes(id)) { + database.removeAvatarFromCache(id); + } + } + }); +} + +/** + * + * @returns {Promise} + */ +export async function getLocalAvatarFavorites() { + const favoriteStore = useFavoriteStore(); + const avatarStore = useAvatarStore(); + + const localGroups = new Set(); + const localListSet = new Set(); + const localFavorites = Object.create(null); + + const avatarCache = await database.getAvatarCache(); + for (let i = 0; i < avatarCache.length; ++i) { + const ref = avatarCache[i]; + if (!avatarStore.cachedAvatars.has(ref.id)) { + avatarStore.applyAvatar(ref); + } + } + + const favorites = await database.getAvatarFavorites(); + for (let i = 0; i < favorites.length; ++i) { + const favorite = favorites[i]; + + localListSet.add(favorite.avatarId); + + if (!localFavorites[favorite.groupName]) { + localFavorites[favorite.groupName] = []; + } + localGroups.add(favorite.groupName); + + let ref = avatarStore.cachedAvatars.get(favorite.avatarId); + if (typeof ref === 'undefined') { + ref = { id: favorite.avatarId }; + } + localFavorites[favorite.groupName].unshift(ref); + } + + let groupsArr = Array.from(localGroups); + if (groupsArr.length === 0) { + // default group + localFavorites.Favorites = []; + groupsArr = ['Favorites']; + } + + replaceReactiveObject(favoriteStore.localAvatarFavorites, localFavorites); + + sortLocalAvatarFavorites(); +} + +/** + * + * @param {string} newName + * @param {string} group + */ +export function renameLocalAvatarFavoriteGroup(newName, group) { + const favoriteStore = useFavoriteStore(); + const { t } = i18n.global; + if (favoriteStore.localAvatarFavoriteGroups.includes(newName)) { + toast.error( + t('prompt.local_favorite_group_rename.message.error', { + name: newName + }) + ); + return; + } + favoriteStore.localAvatarFavorites[newName] = favoriteStore.localAvatarFavorites[group]; + + delete favoriteStore.localAvatarFavorites[group]; + database.renameAvatarFavoriteGroup(newName, group); + sortLocalAvatarFavorites(); +} + +/** + * + * @param {string} group + */ +export function newLocalAvatarFavoriteGroup(group) { + const favoriteStore = useFavoriteStore(); + const { t } = i18n.global; + if (favoriteStore.localAvatarFavoriteGroups.includes(group)) { + toast.error( + t('prompt.new_local_favorite_group.message.error', { + name: group + }) + ); + return; + } + if (!favoriteStore.localAvatarFavorites[group]) { + favoriteStore.localAvatarFavorites[group] = []; + } + sortLocalAvatarFavorites(); +} + +/** + * Check invalid local avatar favorites + * @param {string | null} targetGroup - Target group to check, null for all groups + * @param {Function | null} onProgress - Progress callback function, receives (current, total) parameters + * @returns {Promise<{total: number, invalid: number, invalidIds: string[]}>} + */ +export async function checkInvalidLocalAvatars( + targetGroup = null, + onProgress = null +) { + const favoriteStore = useFavoriteStore(); + const result = { + total: 0, + invalid: 0, + invalidIds: [] + }; + + const groupsToCheck = targetGroup + ? [targetGroup] + : favoriteStore.localAvatarFavoriteGroups; + + for (const group of groupsToCheck) { + const favoriteGroup = favoriteStore.localAvatarFavorites[group]; + if (favoriteGroup && favoriteGroup.length > 0) { + result.total += favoriteGroup.length; + } + } + + let currentIndex = 0; + + for (const group of groupsToCheck) { + const favoriteGroup = favoriteStore.localAvatarFavorites[group]; + if (!favoriteGroup || favoriteGroup.length === 0) { + continue; + } + + for (const favorite of favoriteGroup) { + currentIndex++; + + if (typeof onProgress === 'function') { + onProgress(currentIndex, result.total); + } + + try { + await avatarRequest.getAvatar({ + avatarId: favorite.id + }); + await new Promise((resolve) => setTimeout(resolve, 500)); + } catch (err) { + console.error( + `Failed to fetch avatar ${favorite.id}:`, + err + ); + result.invalid++; + result.invalidIds.push(favorite.id); + } + } + } + + return result; +} + +/** + * Remove invalid avatars from local favorites + * @param {string[]} avatarIds - Array of avatar IDs to remove + * @param {string | null} targetGroup - Target group, null for all groups + * @returns {Promise<{removed: number, removedIds: string[]}>} + */ +export async function removeInvalidLocalAvatars(avatarIds, targetGroup = null) { + const favoriteStore = useFavoriteStore(); + const result = { + removed: 0, + removedIds: [] + }; + + const groupsToCheck = targetGroup + ? [targetGroup] + : favoriteStore.localAvatarFavoriteGroups; + + for (const group of groupsToCheck) { + const favoriteGroup = favoriteStore.localAvatarFavorites[group]; + if (!favoriteGroup) { + continue; + } + + for (const avatarId of avatarIds) { + const index = favoriteGroup.findIndex( + (fav) => fav.id === avatarId + ); + if (index !== -1) { + removeLocalAvatarFavorite(avatarId, group); + result.removed++; + if (!result.removedIds.includes(avatarId)) { + result.removedIds.push(avatarId); + } + } + } + } + + return result; +} + +// --- Local Friend Favorites --- + +/** + * @param {string} userId + * @param {string} group + */ +export function addLocalFriendFavorite(userId, group) { + const favoriteStore = useFavoriteStore(); + const userStore = useUserStore(); + const friendStore = useFriendStore(); + + if (favoriteStore.hasLocalFriendFavorite(userId, group)) { + return; + } + if (!favoriteStore.localFriendFavorites[group]) { + favoriteStore.localFriendFavorites[group] = []; + } + favoriteStore.localFriendFavorites[group].unshift(userId); + database.addFriendToLocalFavorites(userId, group); + if ( + favoriteStore.favoriteDialog.visible && + favoriteStore.favoriteDialog.objectId === userId + ) { + favoriteStore.updateFavoriteDialog(userId); + } + const userDialog = userStore.userDialog; + if (userDialog.visible && userDialog.id === userId) { + userStore.setUserDialogIsFavorite(true); + } + friendStore.updateLocalFavoriteFriends(); +} + +/** + * @param {string} userId + * @param {string} group + */ +export function removeLocalFriendFavorite(userId, group) { + const favoriteStore = useFavoriteStore(); + const userStore = useUserStore(); + const friendStore = useFriendStore(); + + const favoriteGroup = favoriteStore.localFriendFavorites[group]; + if (favoriteGroup) { + const idx = favoriteGroup.indexOf(userId); + if (idx !== -1) { + favoriteGroup.splice(idx, 1); + } + } + database.removeFriendFromLocalFavorites(userId, group); + if ( + favoriteStore.favoriteDialog.visible && + favoriteStore.favoriteDialog.objectId === userId + ) { + favoriteStore.updateFavoriteDialog(userId); + } + const userDialog = userStore.userDialog; + if (userDialog.visible && userDialog.id === userId) { + userStore.setUserDialogIsFavorite( + favoriteStore.getCachedFavoritesByObjectId(userId) || + favoriteStore.isInAnyLocalFriendGroup(userId) + ); + } + friendStore.updateLocalFavoriteFriends(); +} + +/** + * @param {string} group + */ +export function deleteLocalFriendFavoriteGroup(group) { + const favoriteStore = useFavoriteStore(); + const friendStore = useFriendStore(); + + delete favoriteStore.localFriendFavorites[group]; + database.deleteFriendFavoriteGroup(group); + friendStore.updateLocalFavoriteFriends(); +} + +/** + * @returns {Promise} + */ +export async function getLocalFriendFavorites() { + const favoriteStore = useFavoriteStore(); + const friendStore = useFriendStore(); + + const localFavorites = Object.create(null); + + const favorites = await database.getFriendFavorites(); + for (let i = 0; i < favorites.length; ++i) { + const favorite = favorites[i]; + if (!localFavorites[favorite.groupName]) { + localFavorites[favorite.groupName] = []; + } + localFavorites[favorite.groupName].unshift(favorite.userId); + } + + if (Object.keys(localFavorites).length === 0) { + localFavorites.Favorites = []; + } + + replaceReactiveObject(favoriteStore.localFriendFavorites, localFavorites); + friendStore.updateLocalFavoriteFriends(); +} + +/** + * @param {string} newName + * @param {string} group + */ +export function renameLocalFriendFavoriteGroup(newName, group) { + const favoriteStore = useFavoriteStore(); + const generalSettingsStore = useGeneralSettingsStore(); + const { t } = i18n.global; + if (favoriteStore.localFriendFavoriteGroups.includes(newName)) { + toast.error( + t('prompt.local_favorite_group_rename.message.error', { + name: newName + }) + ); + return; + } + favoriteStore.localFriendFavorites[newName] = favoriteStore.localFriendFavorites[group]; + delete favoriteStore.localFriendFavorites[group]; + database.renameFriendFavoriteGroup(newName, group); + const oldKey = `local:${group}`; + const idx = + generalSettingsStore.localFavoriteFriendsGroups.indexOf(oldKey); + if (idx !== -1) { + const updated = [ + ...generalSettingsStore.localFavoriteFriendsGroups + ]; + updated[idx] = `local:${newName}`; + generalSettingsStore.setLocalFavoriteFriendsGroups(updated); + } +} + +// --- Sort / Init --- + +/** + * + */ +export async function saveSortFavoritesOption() { + const appearanceSettingsStore = useAppearanceSettingsStore(); + getLocalWorldFavorites(); + getLocalFriendFavorites(); + appearanceSettingsStore.setSortFavorites(); +} + +/** + * + */ +export async function initFavorites() { + refreshFavorites(); + getLocalWorldFavorites(); + getLocalAvatarFavorites(); + getLocalFriendFavorites(); +} + +/** + * Called by the favorite store watch when login state changes. + * Handles cross-store cleanup that can't stay in the store. + * @param {boolean} isLoggedIn + */ +export function onLoginStateChanged(isLoggedIn) { + const friendStore = useFriendStore(); + friendStore.localFavoriteFriends.clear(); + if (isLoggedIn) { + initFavorites(); + } +} diff --git a/src/coordinators/friendPresenceCoordinator.js b/src/coordinators/friendPresenceCoordinator.js index b7dd6f46..8a7f8ce0 100644 --- a/src/coordinators/friendPresenceCoordinator.js +++ b/src/coordinators/friendPresenceCoordinator.js @@ -3,6 +3,8 @@ import { AppDebug } from '../service/appConfig'; import { database } from '../service/database'; import { useFeedStore } from '../stores/feed'; import { useFriendStore } from '../stores/friend'; +import { useNotificationStore } from '../stores/notification'; +import { useSharedFeedStore } from '../stores/sharedFeed'; import { useUserStore } from '../stores/user'; import { userRequest } from '../api'; import { watchState } from '../service/watchState'; @@ -25,6 +27,8 @@ export async function runUpdateFriendDelayedCheckFlow( ) { const friendStore = useFriendStore(); const feedStore = useFeedStore(); + const notificationStore = useNotificationStore(); + const sharedFeedStore = useSharedFeedStore(); const { friends, localFavoriteFriends } = friendStore; let feed; @@ -72,7 +76,9 @@ export async function runUpdateFriendDelayedCheckFlow( groupName, time }; - feedStore.addFeed(feed); + notificationStore.queueFeedNoty(feed); + sharedFeedStore.addEntry(feed); + feedStore.addFeedEntry(feed); database.addOnlineOfflineToDatabase(feed); } else if ( newState === 'online' && @@ -96,7 +102,9 @@ export async function runUpdateFriendDelayedCheckFlow( groupName, time: '' }; - feedStore.addFeed(feed); + notificationStore.queueFeedNoty(feed); + sharedFeedStore.addEntry(feed); + feedStore.addFeedEntry(feed); database.addOnlineOfflineToDatabase(feed); } if (newState === 'active') { diff --git a/src/coordinators/friendRelationshipCoordinator.js b/src/coordinators/friendRelationshipCoordinator.js index 71b4cff2..afc98a05 100644 --- a/src/coordinators/friendRelationshipCoordinator.js +++ b/src/coordinators/friendRelationshipCoordinator.js @@ -1,5 +1,6 @@ import { database } from '../service/database'; import { friendRequest } from '../api'; +import { handleFavoriteDelete } from './favoriteCoordinator'; import { useAppearanceSettingsStore } from '../stores/settings/appearance'; import { useFavoriteStore } from '../stores/favorite'; import { useFriendStore } from '../stores/friend'; @@ -56,7 +57,7 @@ export function runDeleteFriendshipFlow( sharedFeedStore.addEntry(friendLogHistory); friendLog.delete(id); database.deleteFriendLogCurrent(id); - favoriteStore.handleFavoriteDelete(id); + handleFavoriteDelete(id); if (!appearanceSettingsStore.hideUnfriends) { uiStore.notifyMenu('friend-log'); } diff --git a/src/coordinators/gameCoordinator.js b/src/coordinators/gameCoordinator.js index cb183dd5..562922f4 100644 --- a/src/coordinators/gameCoordinator.js +++ b/src/coordinators/gameCoordinator.js @@ -1,11 +1,24 @@ +import { toast } from 'vue-sonner'; + +import { + deleteVRChatCache as _deleteVRChatCache, + isRealInstance +} from '../shared/utils'; +import { database } from '../service/database'; +import { useAdvancedSettingsStore } from '../stores/settings/advanced'; import { useAvatarStore } from '../stores/avatar'; import { useGameLogStore } from '../stores/gameLog'; import { useGameStore } from '../stores/game'; import { useInstanceStore } from '../stores/instance'; +import { useLaunchStore } from '../stores/launch'; import { useLocationStore } from '../stores/location'; +import { runLastLocationResetFlow } from './locationCoordinator'; +import { useModalStore } from '../stores/modal'; +import { useNotificationStore } from '../stores/notification'; import { useUpdateLoopStore } from '../stores/updateLoop'; import { useUserStore } from '../stores/user'; import { useVrStore } from '../stores/vr'; +import { useWorldStore } from '../stores/world'; import configRepository from '../service/config'; @@ -31,15 +44,208 @@ export async function runGameRunningChangedFlow(isGameRunning) { await configRepository.setBool('isGameNoVR', gameStore.isGameNoVR); userStore.markCurrentUserGameStopped(); instanceStore.removeAllQueuedInstances(); - gameStore.autoVRChatCacheManagement(); - gameStore.checkIfGameCrashed(); + runAutoVRChatCacheManagementFlow(); + runCheckIfGameCrashedFlow(); updateLoopStore.setIpcTimeout(0); avatarStore.addAvatarWearTime(userStore.currentUser.currentAvatar); } - locationStore.lastLocationReset(); + runLastLocationResetFlow(); gameLogStore.clearNowPlaying(); vrStore.updateVRLastLocation(); - workerTimers.setTimeout(() => gameStore.checkVRChatDebugLogging(), 60000); + workerTimers.setTimeout(() => runCheckVRChatDebugLoggingFlow(), 60000); updateLoopStore.setNextDiscordUpdate(0); } + +/** + * Orchestrates the game running state update from IPC. + * @param {boolean} isGameRunningArg Game running flag from IPC. + * @param {boolean} isSteamVRRunningArg SteamVR running flag from IPC. + */ +export async function runUpdateIsGameRunningFlow( + isGameRunningArg, + isSteamVRRunningArg +) { + const gameStore = useGameStore(); + const advancedSettingsStore = useAdvancedSettingsStore(); + const vrStore = useVrStore(); + + if (advancedSettingsStore.gameLogDisabled) { + return; + } + if (isGameRunningArg !== gameStore.isGameRunning) { + gameStore.setIsGameRunning(isGameRunningArg); + await runGameRunningChangedFlow(isGameRunningArg); + console.log(new Date(), 'isGameRunning', isGameRunningArg); + } + + if (isSteamVRRunningArg !== gameStore.isSteamVRRunning) { + gameStore.setIsSteamVRRunning(isSteamVRRunningArg); + console.log('isSteamVRRunning:', isSteamVRRunningArg); + } + vrStore.updateOpenVR(); +} + +/** + * Runs auto cache management if enabled. + */ +function runAutoVRChatCacheManagementFlow() { + const advancedSettingsStore = useAdvancedSettingsStore(); + + if (advancedSettingsStore.autoSweepVRChatCache) { + runSweepVRChatCacheFlow(); + } +} + +/** + * Sweeps VRChat cache and refreshes cache size display if config dialog visible. + */ +export async function runSweepVRChatCacheFlow() { + const gameStore = useGameStore(); + const advancedSettingsStore = useAdvancedSettingsStore(); + + try { + const output = await AssetBundleManager.SweepCache(); + console.log('SweepCache', output); + } catch (e) { + console.error('SweepCache failed', e); + } + if (advancedSettingsStore.isVRChatConfigDialogVisible) { + gameStore.getVRChatCacheSize(); + } +} + +/** + * Deletes VRChat cache for a given ref and refreshes related stores. + * @param {object} ref Avatar or world reference payload. + */ +export async function runDeleteVRChatCacheFlow(ref) { + const gameStore = useGameStore(); + const worldStore = useWorldStore(); + const avatarStore = useAvatarStore(); + + await _deleteVRChatCache(ref); + gameStore.getVRChatCacheSize(); + worldStore.updateVRChatWorldCache(); + avatarStore.updateVRChatAvatarCache(); +} + +/** + * Checks if VRChat crashed and attempts to relaunch. + */ +export function runCheckIfGameCrashedFlow() { + const advancedSettingsStore = useAdvancedSettingsStore(); + const locationStore = useLocationStore(); + const gameStore = useGameStore(); + + if (!advancedSettingsStore.relaunchVRChatAfterCrash) { + return; + } + const { location } = locationStore.lastLocation; + AppApi.VrcClosedGracefully().then((result) => { + if (result || !isRealInstance(location)) { + return; + } + // check if relaunched less than 2mins ago (prevent crash loop) + if ( + gameStore.state.lastCrashedTime && + new Date().getTime() - + gameStore.state.lastCrashedTime.getTime() < + 120_000 + ) { + console.log('VRChat was recently crashed, not relaunching'); + return; + } + gameStore.setLastCrashedTime(new Date()); + // wait a bit for SteamVR to potentially close before deciding to relaunch + let restartDelay = 8000; + if (gameStore.isGameNoVR) { + // wait for game to close before relaunching + restartDelay = 2000; + } + workerTimers.setTimeout( + () => runRestartCrashedGameFlow(location), + restartDelay + ); + }); +} + +/** + * Restarts VRChat after a crash. + * @param {string} location Last known location to relaunch. + */ +function runRestartCrashedGameFlow(location) { + const gameStore = useGameStore(); + const notificationStore = useNotificationStore(); + const gameLogStore = useGameLogStore(); + const launchStore = useLaunchStore(); + + if (!gameStore.isGameNoVR && !gameStore.isSteamVRRunning) { + console.log("SteamVR isn't running, not relaunching VRChat"); + return; + } + AppApi.FocusWindow(); + const message = 'VRChat crashed, attempting to rejoin last instance'; + toast(message); + const entry = { + created_at: new Date().toJSON(), + type: 'Event', + data: message + }; + database.addGamelogEventToDatabase(entry); + notificationStore.queueGameLogNoty(entry); + gameLogStore.addGameLog(entry); + launchStore.launchGame(location, '', gameStore.isGameNoVR); +} + +/** + * Checks and re-enables VRChat debug logging if disabled. + */ +export async function runCheckVRChatDebugLoggingFlow() { + const gameStore = useGameStore(); + const advancedSettingsStore = useAdvancedSettingsStore(); + const modalStore = useModalStore(); + + if (advancedSettingsStore.gameLogDisabled) { + return; + } + try { + const loggingEnabled = await gameStore.getVRChatRegistryKey( + 'LOGGING_ENABLED' + ); + if ( + loggingEnabled === null || + typeof loggingEnabled === 'undefined' + ) { + // key not found + return; + } + if (parseInt(loggingEnabled, 10) === 1) { + // already enabled + return; + } + const result = await AppApi.SetVRChatRegistryKey( + 'LOGGING_ENABLED', + '1', + 4 + ); + if (!result) { + // failed to set key + modalStore.alert({ + description: + 'VRCX has noticed VRChat debug logging is disabled. VRCX requires debug logging in order to function correctly. Please enable debug logging in VRChat quick menu settings > debug > enable debug logging, then rejoin the instance or restart VRChat.', + title: 'Enable debug logging' + }); + console.error('Failed to enable debug logging', result); + return; + } + modalStore.alert({ + description: + 'VRCX has noticed VRChat debug logging is disabled and automatically re-enabled it. VRCX requires debug logging in order to function correctly.', + title: 'Enabled debug logging' + }); + console.log('Enabled debug logging'); + } catch (e) { + console.error(e); + } +} diff --git a/src/coordinators/inviteCoordinator.js b/src/coordinators/inviteCoordinator.js new file mode 100644 index 00000000..f13c5dc5 --- /dev/null +++ b/src/coordinators/inviteCoordinator.js @@ -0,0 +1,48 @@ +import { toast } from 'vue-sonner'; +import { useI18n } from 'vue-i18n'; + +import { instanceRequest } from '../api'; +import { parseLocation } from '../shared/utils'; +import { useInstanceStore } from '../stores/instance'; +import { useInviteStore } from '../stores/invite'; +import { useLaunchStore } from '../stores/launch'; + +/** + * Creates a new instance for the given world and either opens it in-game + * or sends a self-invite, depending on game state. + * @param {string} worldId + */ +export function runNewInstanceSelfInviteFlow(worldId) { + const instanceStore = useInstanceStore(); + const launchStore = useLaunchStore(); + const inviteStore = useInviteStore(); + const { t } = useI18n(); + + instanceStore.createNewInstance(worldId).then((args) => { + const location = args?.json?.location; + if (!location) { + toast.error(t('message.instance.create_failed')); + return; + } + // self invite + const L = parseLocation(location); + if (!L.isRealInstance) { + return; + } + if (inviteStore.canOpenInstanceInGame) { + const secureOrShortName = + args.json.shortName || args.json.secureName; + launchStore.tryOpenInstanceInVrc(location, secureOrShortName); + return; + } + instanceRequest + .selfInvite({ + instanceId: L.instanceId, + worldId: L.worldId + }) + .then((args) => { + toast.success(t('message.invite.self_sent')); + return args; + }); + }); +} diff --git a/src/coordinators/locationCoordinator.js b/src/coordinators/locationCoordinator.js new file mode 100644 index 00000000..96e771ed --- /dev/null +++ b/src/coordinators/locationCoordinator.js @@ -0,0 +1,189 @@ +import { + getGroupName, + getWorldName, + isRealInstance, + parseLocation +} from '../shared/utils'; +import { database } from '../service/database'; +import { useAdvancedSettingsStore } from '../stores/settings/advanced'; +import { useGameLogStore } from '../stores/gameLog'; +import { useGameStore } from '../stores/game'; +import { useInstanceStore } from '../stores/instance'; +import { useLocationStore } from '../stores/location'; +import { useNotificationStore } from '../stores/notification'; +import { usePhotonStore } from '../stores/photon'; +import { useUserStore } from '../stores/user'; +import { useVrStore } from '../stores/vr'; + +export function runUpdateCurrentUserLocationFlow() { + const advancedSettingsStore = useAdvancedSettingsStore(); + const userStore = useUserStore(); + const instanceStore = useInstanceStore(); + const gameStore = useGameStore(); + const locationStore = useLocationStore(); + + const ref = userStore.cachedUsers.get(userStore.currentUser.id); + if (typeof ref === 'undefined') { + return; + } + + // update cached user with both gameLog and API locations + let currentLocation = userStore.currentUser.$locationTag; + const L = parseLocation(currentLocation); + if (L.isTraveling) { + currentLocation = userStore.currentUser.$travelingToLocation; + } + ref.location = userStore.currentUser.$locationTag; + ref.travelingToLocation = userStore.currentUser.$travelingToLocation; + + if ( + gameStore.isGameRunning && + !advancedSettingsStore.gameLogDisabled && + locationStore.lastLocation.location !== '' + ) { + // use gameLog instead of API when game is running + currentLocation = locationStore.lastLocation.location; + if (locationStore.lastLocation.location === 'traveling') { + currentLocation = locationStore.lastLocationDestination; + } + ref.location = locationStore.lastLocation.location; + ref.travelingToLocation = locationStore.lastLocationDestination; + } + + ref.$online_for = userStore.currentUser.$online_for; + ref.$offline_for = userStore.currentUser.$offline_for; + ref.$location = parseLocation(currentLocation); + if (!gameStore.isGameRunning || advancedSettingsStore.gameLogDisabled) { + ref.$location_at = userStore.currentUser.$location_at; + ref.$travelingToTime = userStore.currentUser.$travelingToTime; + userStore.applyUserDialogLocation(); + instanceStore.applyWorldDialogInstances(); + instanceStore.applyGroupDialogInstances(); + } else { + ref.$location_at = locationStore.lastLocation.date; + ref.$travelingToTime = locationStore.lastLocationDestinationTime; + userStore.setCurrentUserTravelingToTime( + locationStore.lastLocationDestinationTime + ); + } +} + +export async function runSetCurrentUserLocationFlow(location, travelingToLocation) { + const userStore = useUserStore(); + const instanceStore = useInstanceStore(); + const notificationStore = useNotificationStore(); + const gameStore = useGameStore(); + const gameLogStore = useGameLogStore(); + const locationStore = useLocationStore(); + + userStore.setCurrentUserLocationState(location, travelingToLocation); + runUpdateCurrentUserLocationFlow(); + + // janky gameLog support for Quest + if (gameStore.isGameRunning) { + // with the current state of things, lets not run this if we don't need to + return; + } + const lastLocationArray = await database.lookupGameLogDatabase( + ['Location'], + [], + 1 + ); + const lastLocationTemp = + lastLocationArray.length > 0 ? lastLocationArray[0].location : ''; + if (lastLocationTemp === location) { + return; + } + locationStore.setLastLocationDestination(''); + locationStore.setLastLocationDestinationTime(0); + + if (isRealInstance(location)) { + const dt = new Date().toJSON(); + const L = parseLocation(location); + + locationStore.setLastLocation({ + ...locationStore.lastLocation, + location, + date: Date.now() + }); + + const entry = { + created_at: dt, + type: 'Location', + location, + worldId: L.worldId, + worldName: await getWorldName(L.worldId), + groupName: await getGroupName(L.groupId), + time: 0 + }; + database.addGamelogLocationToDatabase(entry); + notificationStore.queueGameLogNoty(entry); + gameLogStore.addGameLog(entry); + instanceStore.addInstanceJoinHistory(location, dt); + + userStore.applyUserDialogLocation(); + instanceStore.applyWorldDialogInstances(); + instanceStore.applyGroupDialogInstances(); + } else { + locationStore.setLastLocation({ + ...locationStore.lastLocation, + location: '', + date: null + }); + } +} + +export function runLastLocationResetFlow(gameLogDate) { + const photonStore = usePhotonStore(); + const instanceStore = useInstanceStore(); + const gameLogStore = useGameLogStore(); + const vrStore = useVrStore(); + const userStore = useUserStore(); + const locationStore = useLocationStore(); + + let dateTime = gameLogDate; + if (!gameLogDate) { + dateTime = new Date().toJSON(); + } + const dateTimeStamp = Date.parse(dateTime); + photonStore.resetLocationPhotonState(); + const playerList = Array.from(locationStore.lastLocation.playerList.values()); + const dataBaseEntries = []; + for (const ref of playerList) { + const entry = { + created_at: dateTime, + type: 'OnPlayerLeft', + displayName: ref.displayName, + location: locationStore.lastLocation.location, + userId: ref.userId, + time: dateTimeStamp - ref.joinTime + }; + dataBaseEntries.unshift(entry); + gameLogStore.addGameLog(entry); + } + database.addGamelogJoinLeaveBulk(dataBaseEntries); + if (locationStore.lastLocation.date !== null && locationStore.lastLocation.date > 0) { + const update = { + time: dateTimeStamp - locationStore.lastLocation.date, + created_at: new Date(locationStore.lastLocation.date).toJSON() + }; + database.updateGamelogLocationTimeToDatabase(update); + } + locationStore.setLastLocationDestination(''); + locationStore.setLastLocationDestinationTime(0); + locationStore.setLastLocation({ + date: 0, + location: '', + name: '', + playerList: new Map(), + friendList: new Map() + }); + runUpdateCurrentUserLocationFlow(); + instanceStore.updateCurrentInstanceWorld(); + vrStore.updateVRLastLocation(); + instanceStore.getCurrentInstanceUserList(); + gameLogStore.resetLastMediaUrls(); + userStore.applyUserDialogLocation(); + instanceStore.applyWorldDialogInstances(); + instanceStore.applyGroupDialogInstances(); +} diff --git a/src/coordinators/moderationCoordinator.js b/src/coordinators/moderationCoordinator.js new file mode 100644 index 00000000..ddb7248c --- /dev/null +++ b/src/coordinators/moderationCoordinator.js @@ -0,0 +1,45 @@ +import { avatarModerationRequest, playerModerationRequest } from '../api'; +import { useAvatarStore } from '../stores/avatar'; +import { useModerationStore } from '../stores/moderation'; + +/** + * Refreshes all player and avatar moderations from the API. + * Orchestrates across moderation store and avatar store. + */ +export async function runRefreshPlayerModerationsFlow() { + const moderationStore = useModerationStore(); + const avatarStore = useAvatarStore(); + + if (moderationStore.playerModerationTable.loading) { + return; + } + moderationStore.playerModerationTable.loading = true; + moderationStore.expirePlayerModerations(); + Promise.all([ + playerModerationRequest.getPlayerModerations(), + avatarModerationRequest.getAvatarModerations() + ]) + .finally(() => { + moderationStore.playerModerationTable.loading = false; + }) + .then((res) => { + avatarStore.resetCachedAvatarModerations(); + if (res[1]?.json) { + for (const json of res[1].json) { + avatarStore.applyAvatarModeration(json); + } + } + if (res[0]?.json) { + for (let json of res[0].json) { + moderationStore.applyPlayerModeration(json); + } + } + moderationStore.deleteExpiredPlayerModerations(); + }) + .catch((error) => { + console.error( + 'Failed to load player/avatar moderations:', + error + ); + }); +} diff --git a/src/coordinators/userEventCoordinator.js b/src/coordinators/userEventCoordinator.js index 8cd127d3..3d4cba42 100644 --- a/src/coordinators/userEventCoordinator.js +++ b/src/coordinators/userEventCoordinator.js @@ -7,6 +7,8 @@ import { useFriendStore } from '../stores/friend'; import { useGeneralSettingsStore } from '../stores/settings/general'; import { useGroupStore } from '../stores/group'; import { useInstanceStore } from '../stores/instance'; +import { useNotificationStore } from '../stores/notification'; +import { useSharedFeedStore } from '../stores/sharedFeed'; import { useUserStore } from '../stores/user'; import { useWorldStore } from '../stores/world'; @@ -30,6 +32,8 @@ export async function runHandleUserUpdateFlow( const groupStore = useGroupStore(); const instanceStore = useInstanceStore(); const feedStore = useFeedStore(); + const notificationStore = useNotificationStore(); + const sharedFeedStore = useSharedFeedStore(); const avatarStore = useAvatarStore(); const generalSettingsStore = useGeneralSettingsStore(); @@ -139,7 +143,9 @@ export async function runHandleUserUpdateFlow( previousLocation, time }; - feedStore.addFeed(feed); + notificationStore.queueFeedNoty(feed); + sharedFeedStore.addEntry(feed); + feedStore.addFeedEntry(feed); database.addGPSToDatabase(feed); // clear previousLocation after GPS ref.$previousLocation = ''; @@ -249,7 +255,9 @@ export async function runHandleUserUpdateFlow( currentAvatarTags, previousCurrentAvatarTags }; - feedStore.addFeed(feed); + notificationStore.queueFeedNoty(feed); + sharedFeedStore.addEntry(feed); + feedStore.addFeedEntry(feed); database.addAvatarToDatabase(feed); } } @@ -296,7 +304,9 @@ export async function runHandleUserUpdateFlow( previousStatus, previousStatusDescription }; - feedStore.addFeed(feed); + notificationStore.queueFeedNoty(feed); + sharedFeedStore.addEntry(feed); + feedStore.addFeedEntry(feed); database.addStatusToDatabase(feed); } if (props.bio && props.bio[0] && props.bio[1]) { @@ -316,7 +326,9 @@ export async function runHandleUserUpdateFlow( bio, previousBio }; - feedStore.addFeed(feed); + notificationStore.queueFeedNoty(feed); + sharedFeedStore.addEntry(feed); + feedStore.addFeedEntry(feed); database.addBioToDatabase(feed); } if ( diff --git a/src/service/websocket.js b/src/service/websocket.js index 30648469..dfe68fe2 100644 --- a/src/service/websocket.js +++ b/src/service/websocket.js @@ -17,6 +17,7 @@ import { AppDebug } from './appConfig'; import { groupRequest } from '../api'; import { request } from './request'; import { runUpdateFriendFlow } from '../coordinators/friendPresenceCoordinator'; +import { runSetCurrentUserLocationFlow } from '../coordinators/locationCoordinator'; import { watchState } from './watchState'; import * as workerTimers from 'worker-timers'; @@ -397,7 +398,7 @@ function handlePipeline(args) { // content.worldId // where did worldId go? // content.instance // without worldId, this is useless - locationStore.setCurrentUserLocation( + runSetCurrentUserLocationFlow( content.location, content.travelingToLocation ); diff --git a/src/stores/avatar.js b/src/stores/avatar.js index 8689777c..c20bb227 100644 --- a/src/stores/avatar.js +++ b/src/stores/avatar.js @@ -19,6 +19,7 @@ import { AppDebug } from '../service/appConfig'; import { database } from '../service/database'; import { patchAvatarFromEvent } from '../queries'; import { processBulk } from '../service/request'; +import { applyFavorite } from '../coordinators/favoriteCoordinator'; import { useAdvancedSettingsStore } from './settings/advanced'; import { useAvatarProviderStore } from './avatarProvider'; import { useFavoriteStore } from './favorite'; @@ -114,7 +115,7 @@ export const useAvatarStore = defineStore('Avatar', () => { listing.displayName = replaceBioSymbols(listing.displayName); listing.description = replaceBioSymbols(listing.description); } - favoriteStore.applyFavorite('avatar', ref.id); + applyFavorite('avatar', ref.id); if (favoriteStore.localAvatarFavoritesList.includes(ref.id)) { const avatarRef = ref; favoriteStore.syncLocalAvatarFavoriteRef(avatarRef); diff --git a/src/stores/favorite.js b/src/stores/favorite.js index 74f92b48..bcd958d3 100644 --- a/src/stores/favorite.js +++ b/src/stores/favorite.js @@ -1,34 +1,25 @@ -import { computed, reactive, ref, shallowReactive, watch } from 'vue'; +import { computed, reactive, ref, watch } from 'vue'; import { defineStore } from 'pinia'; import { toast } from 'vue-sonner'; import { useI18n } from 'vue-i18n'; import { compareByName, - createDefaultFavoriteCachedRef, createDefaultFavoriteGroupRef, removeFromArray, replaceReactiveObject } from '../shared/utils'; -import { avatarRequest, favoriteRequest, queryRequest } from '../api'; +import { favoriteRequest } from '../api'; import { database } from '../service/database'; import { processBulk } from '../service/request'; -import { runUpdateFriendFlow } from '../coordinators/friendPresenceCoordinator'; import { useAppearanceSettingsStore } from './settings/appearance'; -import { useAvatarStore } from './avatar'; -import { useFriendStore } from './friend'; -import { useGeneralSettingsStore } from './settings/general'; -import { useUserStore } from './user'; -import { useWorldStore } from './world'; import { watchState } from '../service/watchState'; +import { onLoginStateChanged } from '../coordinators/favoriteCoordinator'; + +import configRepository from '../service/config'; export const useFavoriteStore = defineStore('Favorite', () => { const appearanceSettingsStore = useAppearanceSettingsStore(); - const friendStore = useFriendStore(); - const generalSettingsStore = useGeneralSettingsStore(); - const avatarStore = useAvatarStore(); - const worldStore = useWorldStore(); - const userStore = useUserStore(); const { t } = useI18n(); @@ -235,7 +226,7 @@ export const useFavoriteStore = defineStore('Favorite', () => { watch( () => watchState.isLoggedIn, (isLoggedIn) => { - friendStore.localFavoriteFriends.clear(); + // Internal state reset cachedFavorites.clear(); cachedFavoritesByObjectId.clear(); cachedFavoriteGroups.value = {}; @@ -257,9 +248,8 @@ export const useFavoriteStore = defineStore('Favorite', () => { worldImportDialogVisible.value = false; avatarImportDialogVisible.value = false; friendImportDialogVisible.value = false; - if (isLoggedIn) { - initFavorites(); - } + // Cross-store operations delegated to coordinator + onLoginStateChanged(isLoggedIn); }, { flush: 'sync' } ); @@ -268,22 +258,17 @@ export const useFavoriteStore = defineStore('Favorite', () => { * @returns {void} */ function getCachedFavoriteGroupsByTypeName() { - const group = {}; - - for (const k in favoriteFriendGroups.value) { - const element = favoriteFriendGroups.value[k]; - group[element.key] = element; + const groups = {}; + for (const group of favoriteFriendGroups.value) { + groups[group.key] = group; } - for (const k in favoriteWorldGroups.value) { - const element = favoriteWorldGroups.value[k]; - group[element.key] = element; + for (const group of favoriteWorldGroups.value) { + groups[group.key] = group; } - for (const k in favoriteAvatarGroups.value) { - const element = favoriteAvatarGroups.value[k]; - group[element.key] = element; + for (const group of favoriteAvatarGroups.value) { + groups[group.key] = group; } - - return group; + return groups; } /** @@ -295,80 +280,6 @@ export const useFavoriteStore = defineStore('Favorite', () => { return cachedFavoritesByObjectId.get(objectId); } - /** - * - * @param {object} args - * @returns {void} - */ - function handleFavoriteAdd(args) { - handleFavorite({ - json: args.json, - params: { - favoriteId: args.json.id - } - }); - if (!favoritesSortOrder.value.includes(args.params.favoriteId)) { - favoritesSortOrder.value.unshift(args.params.favoriteId); - } - - if ( - args.params.type === 'avatar' && - !avatarStore.cachedAvatars.has(args.params.favoriteId) - ) { - refreshFavoriteAvatars(args.params.tags); - } - - if ( - args.params.type === 'friend' && - (!generalSettingsStore.localFavoriteFriendsGroups.some( - (key) => !key.startsWith('local:') - ) || - generalSettingsStore.localFavoriteFriendsGroups.includes( - 'friend:' + args.params.tags - )) - ) { - friendStore.updateLocalFavoriteFriends(); - } - updateFavoriteDialog(args.params.objectId); - } - - /** - * - * @param {object} args - * @returns {void} - */ - function handleFavorite(args) { - args.ref = applyFavoriteCached(args.json); - applyFavorite(args.ref.type, args.ref.favoriteId); - runUpdateFriendFlow(args.ref.favoriteId); - const { ref } = args; - const userDialog = userStore.userDialog; - if (userDialog.visible && ref.favoriteId === userDialog.id) { - userStore.setUserDialogIsFavorite(true); - } - const worldDialog = worldStore.worldDialog; - if (worldDialog.visible && ref.favoriteId === worldDialog.id) { - worldStore.setWorldDialogIsFavorite(true); - } - const avatarDialog = avatarStore.avatarDialog; - if (avatarDialog.visible && ref.favoriteId === avatarDialog.id) { - avatarStore.setAvatarDialogIsFavorite(true); - } - } - - /** - * - * @param {string} objectId - * @returns {void} - */ - function handleFavoriteDelete(objectId) { - const ref = getCachedFavoritesByObjectId(objectId); - if (typeof ref === 'undefined') { - return; - } - handleFavoriteAtDelete(ref); - } - /** * * @param {object} args @@ -380,219 +291,6 @@ export const useFavoriteStore = defineStore('Favorite', () => { /** * - * @param {object} args - * @returns {void} - */ - function handleFavoriteGroupClear(args) { - const key = `${args.params.type}:${args.params.group}`; - for (const ref of cachedFavorites.values()) { - if (ref.$groupKey !== key) { - continue; - } - handleFavoriteAtDelete(ref); - } - } - - /** - * - * @param {object} args - * @returns {void} - */ - function handleFavoriteWorldList(args) { - for (const json of args.json) { - if (json.id === '???') { - continue; - } - worldStore.applyWorld(json); - } - } - - /** - * - * @param {object} args - */ - function handleFavoriteAvatarList(args) { - for (const json of args.json) { - if (json.releaseStatus === 'hidden') { - continue; - } - avatarStore.applyAvatar(json); - } - } - - /** - * - * @param {object} ref - * @returns {void} - */ - function handleFavoriteAtDelete(ref) { - const favorite = state.favoriteObjects.get(ref.favoriteId); - removeFromArray(state.favoriteFriends_, favorite); - removeFromArray(state.favoriteWorlds_, favorite); - removeFromArray(state.favoriteAvatars_, favorite); - cachedFavorites.delete(ref.id); - cachedFavoritesByObjectId.delete(ref.favoriteId); - state.favoriteObjects.delete(ref.favoriteId); - friendStore.localFavoriteFriends.delete(ref.favoriteId); - favoritesSortOrder.value = favoritesSortOrder.value.filter( - (id) => id !== ref.favoriteId - ); - - runUpdateFriendFlow(ref.favoriteId); - friendStore.updateSidebarFavorites(); - const userDialog = userStore.userDialog; - if (userDialog.visible && userDialog.id === ref.favoriteId) { - userStore.setUserDialogIsFavorite(false); - } - const worldDialog = worldStore.worldDialog; - if (worldDialog.visible && worldDialog.id === ref.favoriteId) { - worldStore.setWorldDialogIsFavorite( - localWorldFavoritesList.value.includes(worldDialog.id) - ); - } - const avatarDialog = avatarStore.avatarDialog; - if (avatarDialog.visible && avatarDialog.id === ref.favoriteId) { - avatarStore.setAvatarDialogIsFavorite(false); - } - countFavoriteGroups(); - } - - /** - * - * @param {'friend' | 'world' | 'vrcPlusWorld' | 'avatar'} type - * @param {string} objectId - * @returns {Promise} - */ - async function applyFavorite(type, objectId) { - let ref; - const favorite = getCachedFavoritesByObjectId(objectId); - let ctx = state.favoriteObjects.get(objectId); - if (ctx) { - ctx = shallowReactive(ctx); - } - if (typeof favorite !== 'undefined') { - let isTypeChanged = false; - if (typeof ctx === 'undefined') { - ctx = { - id: objectId, - type, - groupKey: favorite.$groupKey, - ref: null, - name: '' - }; - if (type === 'friend') { - ref = userStore.cachedUsers.get(objectId); - if (typeof ref === 'undefined') { - ref = friendStore.friendLog.get(objectId); - if (typeof ref !== 'undefined' && ref.displayName) { - ctx.name = ref.displayName; - } - } else { - ctx.ref = ref; - ctx.name = ref.displayName; - } - } else if (type === 'world' || type === 'vrcPlusWorld') { - ref = worldStore.cachedWorlds.get(objectId); - if (typeof ref !== 'undefined') { - ctx.ref = ref; - ctx.name = ref.name; - } - } else if (type === 'avatar') { - ref = avatarStore.cachedAvatars.get(objectId); - if (typeof ref !== 'undefined') { - ctx.ref = ref; - ctx.name = ref.name; - } - } - state.favoriteObjects.set(objectId, ctx); - isTypeChanged = true; - } else { - if (ctx.type !== type) { - // WTF??? - isTypeChanged = true; - if (type === 'friend') { - removeFromArray(state.favoriteFriends_, ctx); - } else if (type === 'world' || type === 'vrcPlusWorld') { - removeFromArray(state.favoriteWorlds_, ctx); - } else if (type === 'avatar') { - removeFromArray(state.favoriteAvatars_, ctx); - } - } - if (type === 'friend') { - ref = userStore.cachedUsers.get(objectId); - if (typeof ref !== 'undefined') { - if (ctx.ref !== ref) { - ctx.ref = ref; - } - if (ctx.name !== ref.displayName) { - ctx.name = ref.displayName; - } - } - // else too bad - } else if (type === 'world' || type === 'vrcPlusWorld') { - ref = worldStore.cachedWorlds.get(objectId); - if (typeof ref !== 'undefined') { - if (ctx.ref !== ref) { - ctx.ref = ref; - } - if (ctx.name !== ref.name) { - ctx.name = ref.name; - } - } else { - // try fetch from local world favorites - const world = - await database.getCachedWorldById(objectId); - if (world) { - ctx.ref = world; - ctx.name = world.name; - ctx.deleted = true; - } - if (!world) { - // try fetch from local world history - const worldName = - await database.getGameLogWorldNameByWorldId( - objectId - ); - if (worldName) { - ctx.name = worldName; - ctx.deleted = true; - } - } - } - } else if (type === 'avatar') { - ref = avatarStore.cachedAvatars.get(objectId); - if (typeof ref !== 'undefined') { - if (ctx.ref !== ref) { - ctx.ref = ref; - } - if (ctx.name !== ref.name) { - ctx.name = ref.name; - } - } else { - // try fetch from local avatar history - const avatar = - await database.getCachedAvatarById(objectId); - if (avatar) { - ctx.ref = avatar; - ctx.name = avatar.name; - ctx.deleted = true; - } - } - } - } - if (isTypeChanged) { - if (type === 'friend') { - state.favoriteFriends_.push(ctx); - } else if (type === 'world' || type === 'vrcPlusWorld') { - state.favoriteWorlds_.push(ctx); - } else if (type === 'avatar') { - state.favoriteAvatars_.push(ctx); - } - } - } - } - - /** * @returns {void} */ function refreshFavoriteGroups() { @@ -762,65 +460,6 @@ export const useFavoriteStore = defineStore('Favorite', () => { } } - /** - * - * @returns {Promise} - */ - async function refreshFavorites() { - if (isFavoriteLoading.value) { - return; - } - isFavoriteLoading.value = true; - try { - const args = await queryRequest.fetch('favoriteLimits'); - favoriteLimits.value = { - ...favoriteLimits.value, - ...args.json - }; - } catch (err) { - console.error(err); - } - let newFavoriteSortOrder = []; - processBulk({ - fn: (params) => favoriteRequest.getFavorites(params), - N: -1, - params: { - n: 300, - offset: 0 - }, - handle(args) { - for (const json of args.json) { - newFavoriteSortOrder.push(json.favoriteId); - handleFavorite({ - json, - params: { - favoriteId: json.id - } - }); - } - }, - done(ok) { - if (ok) { - for (const id of favoritesSortOrder.value) { - if (!newFavoriteSortOrder.includes(id)) { - const fav = cachedFavorites.get(id); - if (fav) { - handleFavoriteAtDelete(fav); - } - } - } - favoritesSortOrder.value = newFavoriteSortOrder; - } - refreshFavoriteItems(); - refreshFavoriteGroups(); - friendStore.updateLocalFavoriteFriends(); - isFavoriteLoading.value = false; - watchState.isFavoritesLoaded = true; - countFavoriteGroups(); - } - }); - } - /** * * @param {object} json @@ -837,108 +476,6 @@ export const useFavoriteStore = defineStore('Favorite', () => { return ref; } - /** - * - * @param {object} json - * @returns {object} - */ - function applyFavoriteCached(json) { - let ref = cachedFavorites.get(json.id); - if (typeof ref === 'undefined') { - ref = createDefaultFavoriteCachedRef(json); - cachedFavorites.set(ref.id, ref); - cachedFavoritesByObjectId.set(ref.favoriteId, ref); - if ( - ref.type === 'friend' && - (!generalSettingsStore.localFavoriteFriendsGroups.some( - (key) => !key.startsWith('local:') - ) || - generalSettingsStore.localFavoriteFriendsGroups.includes( - ref.$groupKey - )) - ) { - friendStore.localFavoriteFriends.add(ref.favoriteId); - friendStore.updateSidebarFavorites(); - } - if (!isFavoriteLoading.value) { - countFavoriteGroups(); - } - } else { - if (ref.favoriteId !== json.favoriteId) { - cachedFavoritesByObjectId.delete(ref.favoriteId); - } - Object.assign(ref, json); - cachedFavoritesByObjectId.set(ref.favoriteId, ref); - } - - return ref; - } - - /** - * - * @param {string} tag - * @returns {void} - */ - async function refreshFavoriteAvatars(tag) { - const params = { - n: 300, - offset: 0, - tag - }; - const args = await favoriteRequest.getFavoriteAvatars(params); - handleFavoriteAvatarList(args); - } - - /** - * @returns {void} - */ - function refreshFavoriteItems() { - const types = { - world: [0, (params) => favoriteRequest.getFavoriteWorlds(params)], - avatar: [0, (params) => favoriteRequest.getFavoriteAvatars(params)] - }; - const tags = []; - for (const ref of cachedFavorites.values()) { - const type = types[ref.type]; - if (typeof type === 'undefined') { - continue; - } - if (ref.type === 'avatar' && !tags.includes(ref.tags[0])) { - tags.push(ref.tags[0]); - } - ++type[0]; - } - for (const type in types) { - const [N, fn] = types[type]; - if (N > 0) { - if (type === 'avatar') { - for (const tag of tags) { - processBulk({ - fn, - N, - handle: (args) => handleFavoriteAvatarList(args), - params: { - n: 300, - offset: 0, - tag - } - }); - } - } else { - processBulk({ - fn, - N, - handle: (args) => handleFavoriteWorldList(args), - params: { - n: 300, - offset: 0 - } - }); - } - } - } - } - /** * @returns {void} */ @@ -982,7 +519,8 @@ export const useFavoriteStore = defineStore('Favorite', () => { } /** - * @param {object} avatarRef + * + * @param avatarRef */ function syncLocalAvatarFavoriteRef(avatarRef) { if (!avatarRef?.id) { @@ -1002,43 +540,6 @@ export const useFavoriteStore = defineStore('Favorite', () => { } } - /** - * - * @param {string} worldId - * @param {string} group - */ - function addLocalWorldFavorite(worldId, group) { - if (hasLocalWorldFavorite(worldId, group)) { - return; - } - const ref = worldStore.cachedWorlds.get(worldId); - if (typeof ref === 'undefined') { - return; - } - if (!localWorldFavorites[group]) { - localWorldFavorites[group] = []; - } - - localWorldFavorites[group].unshift(ref); - database.addWorldToCache(ref); - database.addWorldToFavorites(worldId, group); - if ( - favoriteDialog.value.visible && - favoriteDialog.value.objectId === worldId - ) { - updateFavoriteDialog(worldId); - } - if ( - worldStore.worldDialog.visible && - worldStore.worldDialog.id === worldId - ) { - worldStore.setWorldDialogIsFavorite(true); - } - - // update UI - sortLocalWorldFavorites(); - } - /** * * @param {string} worldId @@ -1058,42 +559,6 @@ export const useFavoriteStore = defineStore('Favorite', () => { return false; } - /** - * - * @param {string} avatarId - * @param {string} group - */ - function addLocalAvatarFavorite(avatarId, group) { - if (hasLocalAvatarFavorite(avatarId, group)) { - return; - } - const ref = avatarStore.cachedAvatars.get(avatarId); - if (typeof ref === 'undefined') { - return; - } - if (!localAvatarFavorites[group]) { - localAvatarFavorites[group] = []; - } - localAvatarFavorites[group].unshift(ref); - database.addAvatarToCache(ref); - database.addAvatarToFavorites(avatarId, group); - if ( - favoriteDialog.value.visible && - favoriteDialog.value.objectId === avatarId - ) { - updateFavoriteDialog(avatarId); - } - if ( - avatarStore.avatarDialog.visible && - avatarStore.avatarDialog.id === avatarId - ) { - avatarStore.setAvatarDialogIsFavorite(true); - } - - // update UI - sortLocalAvatarFavorites(); - } - /** * * @param {string} avatarId @@ -1116,7 +581,6 @@ export const useFavoriteStore = defineStore('Favorite', () => { /** * * @param {string} objectId - * @returns {void} */ function updateFavoriteDialog(objectId) { const D = favoriteDialog.value; @@ -1148,223 +612,6 @@ export const useFavoriteStore = defineStore('Favorite', () => { } } - /** - * - * @param {string} group - */ - function deleteLocalAvatarFavoriteGroup(group) { - let i; - // remove from cache if no longer in favorites - const avatarIdRemoveList = new Set(); - const favoriteGroup = localAvatarFavorites[group]; - for (i = 0; i < favoriteGroup.length; ++i) { - avatarIdRemoveList.add(favoriteGroup[i].id); - } - - delete localAvatarFavorites[group]; - database.deleteAvatarFavoriteGroup(group); - - for (i = 0; i < localAvatarFavoriteGroups.value.length; ++i) { - const groupName = localAvatarFavoriteGroups.value[i]; - if (!localAvatarFavorites[groupName]) { - continue; - } - for (let j = 0; j < localAvatarFavorites[groupName].length; ++j) { - const avatarId = localAvatarFavorites[groupName][j].id; - if (avatarIdRemoveList.has(avatarId)) { - avatarIdRemoveList.delete(avatarId); - break; - } - } - } - - avatarIdRemoveList.forEach((id) => { - // remove from cache if no longer in favorites - let avatarInFavorites = false; - loop: for ( - let i = 0; - i < localAvatarFavoriteGroups.value.length; - ++i - ) { - const groupName = localAvatarFavoriteGroups.value[i]; - if (!localAvatarFavorites[groupName] || group === groupName) { - continue loop; - } - for ( - let j = 0; - j < localAvatarFavorites[groupName].length; - ++j - ) { - const avatarId = localAvatarFavorites[groupName][j].id; - if (id === avatarId) { - avatarInFavorites = true; - break loop; - } - } - } - if (!avatarInFavorites) { - if (!avatarStore.avatarHistory.includes(id)) { - database.removeAvatarFromCache(id); - } - } - }); - } - - /** - * @returns {void} - */ - function sortLocalAvatarFavorites() { - if (!appearanceSettingsStore.sortFavorites) { - for (let i = 0; i < localAvatarFavoriteGroups.value.length; ++i) { - const group = localAvatarFavoriteGroups.value[i]; - if (localAvatarFavorites[group]) { - localAvatarFavorites[group].sort(compareByName); - } - } - } - } - - /** - * - * @param {string} newName - * @param {string} group - */ - function renameLocalAvatarFavoriteGroup(newName, group) { - if (localAvatarFavoriteGroups.value.includes(newName)) { - toast.error( - t('prompt.local_favorite_group_rename.message.error', { - name: newName - }) - ); - return; - } - localAvatarFavorites[newName] = localAvatarFavorites[group]; - - delete localAvatarFavorites[group]; - database.renameAvatarFavoriteGroup(newName, group); - sortLocalAvatarFavorites(); - } - - /** - * - * @param {string} group - */ - function newLocalAvatarFavoriteGroup(group) { - if (localAvatarFavoriteGroups.value.includes(group)) { - toast.error( - t('prompt.new_local_favorite_group.message.error', { - name: group - }) - ); - return; - } - if (!localAvatarFavorites[group]) { - localAvatarFavorites[group] = []; - } - sortLocalAvatarFavorites(); - } - - /** - * - * @returns {Promise} - */ - async function getLocalAvatarFavorites() { - const localGroups = new Set(); - const localListSet = new Set(); - const localFavorites = Object.create(null); - - const avatarCache = await database.getAvatarCache(); - for (let i = 0; i < avatarCache.length; ++i) { - const ref = avatarCache[i]; - if (!avatarStore.cachedAvatars.has(ref.id)) { - avatarStore.applyAvatar(ref); - } - } - - const favorites = await database.getAvatarFavorites(); - for (let i = 0; i < favorites.length; ++i) { - const favorite = favorites[i]; - - localListSet.add(favorite.avatarId); - - if (!localFavorites[favorite.groupName]) { - localFavorites[favorite.groupName] = []; - } - localGroups.add(favorite.groupName); - - let ref = avatarStore.cachedAvatars.get(favorite.avatarId); - if (typeof ref === 'undefined') { - ref = { id: favorite.avatarId }; - } - localFavorites[favorite.groupName].unshift(ref); - } - - let groupsArr = Array.from(localGroups); - if (groupsArr.length === 0) { - // default group - localFavorites.Favorites = []; - groupsArr = ['Favorites']; - } - - replaceReactiveObject(localAvatarFavorites, localFavorites); - - sortLocalAvatarFavorites(); - } - - /** - * - * @param {string} avatarId - * @param {string} group - */ - function removeLocalAvatarFavorite(avatarId, group) { - let i; - const favoriteGroup = localAvatarFavorites[group]; - for (i = 0; i < favoriteGroup.length; ++i) { - if (favoriteGroup[i].id === avatarId) { - favoriteGroup.splice(i, 1); - } - } - - // remove from cache if no longer in favorites - let avatarInFavorites = false; - for (i = 0; i < localAvatarFavoriteGroups.value.length; ++i) { - const groupName = localAvatarFavoriteGroups.value[i]; - if (!localAvatarFavorites[groupName] || group === groupName) { - continue; - } - for (let j = 0; j < localAvatarFavorites[groupName].length; ++j) { - const id = localAvatarFavorites[groupName][j].id; - if (id === avatarId) { - avatarInFavorites = true; - break; - } - } - } - if (!avatarInFavorites) { - if (!avatarStore.avatarHistory.includes(avatarId)) { - database.removeAvatarFromCache(avatarId); - } - } - database.removeAvatarFromFavorites(avatarId, group); - if ( - favoriteDialog.value.visible && - favoriteDialog.value.objectId === avatarId - ) { - updateFavoriteDialog(avatarId); - } - if ( - avatarStore.avatarDialog.visible && - avatarStore.avatarDialog.id === avatarId - ) { - avatarStore.setAvatarDialogIsFavorite( - getCachedFavoritesByObjectId(avatarId) - ); - } - - // update UI - sortLocalAvatarFavorites(); - } - /** * * @param {string} group @@ -1400,285 +647,6 @@ export const useFavoriteStore = defineStore('Favorite', () => { }); } - /** - * @returns {void} - */ - function sortLocalWorldFavorites() { - if (!appearanceSettingsStore.sortFavorites) { - for (let i = 0; i < localWorldFavoriteGroups.value.length; ++i) { - const group = localWorldFavoriteGroups.value[i]; - if (localWorldFavorites[group]) { - localWorldFavorites[group].sort(compareByName); - } - } - } - } - - /** - * Check invalid local avatar favorites - * @param {string | null} targetGroup - Target group to check, null for all groups - * @param {Function | null} onProgress - Progress callback function, receives (current, total) parameters - * @returns {Promise<{total: number, invalid: number, invalidIds: string[]}>} - */ - async function checkInvalidLocalAvatars( - targetGroup = null, - onProgress = null - ) { - const result = { - total: 0, - invalid: 0, - invalidIds: [] - }; - - const groupsToCheck = targetGroup - ? [targetGroup] - : localAvatarFavoriteGroups.value; - - for (const group of groupsToCheck) { - const favoriteGroup = localAvatarFavorites[group]; - if (favoriteGroup && favoriteGroup.length > 0) { - result.total += favoriteGroup.length; - } - } - - let currentIndex = 0; - - for (const group of groupsToCheck) { - const favoriteGroup = localAvatarFavorites[group]; - if (!favoriteGroup || favoriteGroup.length === 0) { - continue; - } - - for (const favorite of favoriteGroup) { - currentIndex++; - - if (typeof onProgress === 'function') { - onProgress(currentIndex, result.total); - } - - try { - await avatarRequest.getAvatar({ - avatarId: favorite.id - }); - await new Promise((resolve) => setTimeout(resolve, 500)); - } catch (err) { - console.error( - `Failed to fetch avatar ${favorite.id}:`, - err - ); - result.invalid++; - result.invalidIds.push(favorite.id); - } - } - } - - return result; - } - - /** - * Remove invalid avatars from local favorites - * @param {string[]} avatarIds - Array of avatar IDs to remove - * @param {string | null} targetGroup - Target group, null for all groups - * @returns {Promise<{removed: number, removedIds: string[]}>} - */ - async function removeInvalidLocalAvatars(avatarIds, targetGroup = null) { - const result = { - removed: 0, - removedIds: [] - }; - - const groupsToCheck = targetGroup - ? [targetGroup] - : localAvatarFavoriteGroups.value; - - for (const group of groupsToCheck) { - const favoriteGroup = localAvatarFavorites[group]; - if (!favoriteGroup) { - continue; - } - - for (const avatarId of avatarIds) { - const index = favoriteGroup.findIndex( - (fav) => fav.id === avatarId - ); - if (index !== -1) { - removeLocalAvatarFavorite(avatarId, group); - result.removed++; - if (!result.removedIds.includes(avatarId)) { - result.removedIds.push(avatarId); - } - } - } - } - - return result; - } - - /** - * - * @param {string} newName - * @param {string} group - */ - function renameLocalWorldFavoriteGroup(newName, group) { - if (localWorldFavoriteGroups.value.includes(newName)) { - toast.error( - t('prompt.local_favorite_group_rename.message.error', { - name: newName - }) - ); - return; - } - localWorldFavorites[newName] = localWorldFavorites[group]; - - delete localWorldFavorites[group]; - database.renameWorldFavoriteGroup(newName, group); - sortLocalWorldFavorites(); - } - - /** - * - * @param {string} worldId - * @param {string} group - */ - function removeLocalWorldFavorite(worldId, group) { - let i; - const favoriteGroup = localWorldFavorites[group]; - for (i = 0; i < favoriteGroup.length; ++i) { - if (favoriteGroup[i].id === worldId) { - favoriteGroup.splice(i, 1); - } - } - - // remove from cache if no longer in favorites - let worldInFavorites = false; - for (i = 0; i < localWorldFavoriteGroups.value.length; ++i) { - const groupName = localWorldFavoriteGroups.value[i]; - if (!localWorldFavorites[groupName] || group === groupName) { - continue; - } - for (let j = 0; j < localWorldFavorites[groupName].length; ++j) { - const id = localWorldFavorites[groupName][j].id; - if (id === worldId) { - worldInFavorites = true; - break; - } - } - } - if (!worldInFavorites) { - database.removeWorldFromCache(worldId); - } - database.removeWorldFromFavorites(worldId, group); - if ( - favoriteDialog.value.visible && - favoriteDialog.value.objectId === worldId - ) { - updateFavoriteDialog(worldId); - } - if ( - worldStore.worldDialog.visible && - worldStore.worldDialog.id === worldId - ) { - worldStore.setWorldDialogIsFavorite( - getCachedFavoritesByObjectId(worldId) - ); - } - - // update UI - sortLocalWorldFavorites(); - } - - /** - * - * @returns {Promise} - */ - async function getLocalWorldFavorites() { - const localGroups = new Set(); - const localListSet = new Set(); - const localFavorites = Object.create(null); - - const worldCache = await database.getWorldCache(); - for (let i = 0; i < worldCache.length; ++i) { - const ref = worldCache[i]; - if (!worldStore.cachedWorlds.has(ref.id)) { - worldStore.applyWorld(ref); - } - } - - const favorites = await database.getWorldFavorites(); - for (let i = 0; i < favorites.length; ++i) { - const favorite = favorites[i]; - - localListSet.add(favorite.worldId); - - if (!localFavorites[favorite.groupName]) { - localFavorites[favorite.groupName] = []; - } - localGroups.add(favorite.groupName); - - let ref = worldStore.cachedWorlds.get(favorite.worldId); - if (typeof ref === 'undefined') { - ref = { id: favorite.worldId }; - } - localFavorites[favorite.groupName].unshift(ref); - } - - let groupsArr = Array.from(localGroups); - if (groupsArr.length === 0) { - localFavorites.Favorites = []; - // default group - groupsArr = ['Favorites']; - } - - replaceReactiveObject(localWorldFavorites, localFavorites); - - sortLocalWorldFavorites(); - } - - /** - * - * @param {string} group - */ - function newLocalWorldFavoriteGroup(group) { - if (localWorldFavoriteGroups.value.includes(group)) { - toast.error( - t('prompt.new_local_favorite_group.message.error', { - name: group - }) - ); - return; - } - if (!localWorldFavorites[group]) { - localWorldFavorites[group] = []; - } - sortLocalWorldFavorites(); - } - - /** - * @param {string} userId - * @param {string} group - */ - function addLocalFriendFavorite(userId, group) { - if (hasLocalFriendFavorite(userId, group)) { - return; - } - if (!localFriendFavorites[group]) { - localFriendFavorites[group] = []; - } - localFriendFavorites[group].unshift(userId); - database.addFriendToLocalFavorites(userId, group); - if ( - favoriteDialog.value.visible && - favoriteDialog.value.objectId === userId - ) { - updateFavoriteDialog(userId); - } - const userDialog = userStore.userDialog; - if (userDialog.visible && userDialog.id === userId) { - userStore.setUserDialogIsFavorite(true); - } - friendStore.updateLocalFavoriteFriends(); - } - /** * @param {string} userId * @param {string} group @@ -1706,72 +674,6 @@ export const useFavoriteStore = defineStore('Favorite', () => { return false; } - /** - * @param {string} userId - * @param {string} group - */ - function removeLocalFriendFavorite(userId, group) { - const favoriteGroup = localFriendFavorites[group]; - if (favoriteGroup) { - const idx = favoriteGroup.indexOf(userId); - if (idx !== -1) { - favoriteGroup.splice(idx, 1); - } - } - database.removeFriendFromLocalFavorites(userId, group); - if ( - favoriteDialog.value.visible && - favoriteDialog.value.objectId === userId - ) { - updateFavoriteDialog(userId); - } - const userDialog = userStore.userDialog; - if (userDialog.visible && userDialog.id === userId) { - userStore.setUserDialogIsFavorite( - getCachedFavoritesByObjectId(userId) || - isInAnyLocalFriendGroup(userId) - ); - } - friendStore.updateLocalFavoriteFriends(); - } - - /** - * @param {string} group - */ - function deleteLocalFriendFavoriteGroup(group) { - delete localFriendFavorites[group]; - database.deleteFriendFavoriteGroup(group); - friendStore.updateLocalFavoriteFriends(); - } - - /** - * @param {string} newName - * @param {string} group - */ - function renameLocalFriendFavoriteGroup(newName, group) { - if (localFriendFavoriteGroups.value.includes(newName)) { - toast.error( - t('prompt.local_favorite_group_rename.message.error', { - name: newName - }) - ); - return; - } - localFriendFavorites[newName] = localFriendFavorites[group]; - delete localFriendFavorites[group]; - database.renameFriendFavoriteGroup(newName, group); - const oldKey = `local:${group}`; - const idx = - generalSettingsStore.localFavoriteFriendsGroups.indexOf(oldKey); - if (idx !== -1) { - const updated = [ - ...generalSettingsStore.localFavoriteFriendsGroups - ]; - updated[idx] = `local:${newName}`; - generalSettingsStore.setLocalFavoriteFriendsGroups(updated); - } - } - /** * @param {string} group */ @@ -1789,32 +691,9 @@ export const useFavoriteStore = defineStore('Favorite', () => { } } - /** - * @returns {Promise} - */ - async function getLocalFriendFavorites() { - const localFavorites = Object.create(null); - - const favorites = await database.getFriendFavorites(); - for (let i = 0; i < favorites.length; ++i) { - const favorite = favorites[i]; - if (!localFavorites[favorite.groupName]) { - localFavorites[favorite.groupName] = []; - } - localFavorites[favorite.groupName].unshift(favorite.userId); - } - - if (Object.keys(localFavorites).length === 0) { - localFavorites.Favorites = []; - } - - replaceReactiveObject(localFriendFavorites, localFavorites); - friendStore.updateLocalFavoriteFriends(); - } - /** * - * @param {string} objectId + * @param objectId */ function deleteFavoriteNoConfirm(objectId) { if (!objectId) { @@ -1846,25 +725,6 @@ export const useFavoriteStore = defineStore('Favorite', () => { updateFavoriteDialog(objectId); } - /** - * - */ - async function saveSortFavoritesOption() { - getLocalWorldFavorites(); - getLocalFriendFavorites(); - appearanceSettingsStore.setSortFavorites(); - } - - /** - * - */ - async function initFavorites() { - refreshFavorites(); - getLocalWorldFavorites(); - getLocalAvatarFavorites(); - getLocalFriendFavorites(); - } - /** * * @param a @@ -1887,6 +747,7 @@ export const useFavoriteStore = defineStore('Favorite', () => { cachedFavoriteGroups, favoriteLimits, cachedFavorites, + cachedFavoritesByObjectId, favoriteWorldGroups, favoriteAvatarGroups, isFavoriteLoading, @@ -1917,12 +778,8 @@ export const useFavoriteStore = defineStore('Favorite', () => { localAvatarFavGroupLength, favoritesSortOrder, - initFavorites, - applyFavorite, refreshFavoriteGroups, - refreshFavorites, applyFavoriteGroup, - refreshFavoriteAvatars, showWorldImportDialog, showAvatarImportDialog, showFriendImportDialog, @@ -1930,41 +787,18 @@ export const useFavoriteStore = defineStore('Favorite', () => { setWorldImportDialogInput, setFriendImportDialogInput, syncLocalAvatarFavoriteRef, - addLocalWorldFavorite, hasLocalWorldFavorite, hasLocalAvatarFavorite, - addLocalAvatarFavorite, updateFavoriteDialog, - deleteLocalAvatarFavoriteGroup, - renameLocalAvatarFavoriteGroup, - newLocalAvatarFavoriteGroup, - getLocalAvatarFavorites, - removeLocalAvatarFavorite, deleteLocalWorldFavoriteGroup, - sortLocalWorldFavorites, - renameLocalWorldFavoriteGroup, - removeLocalWorldFavorite, - getLocalWorldFavorites, - newLocalWorldFavoriteGroup, deleteFavoriteNoConfirm, showFavoriteDialog, - saveSortFavoritesOption, - handleFavoriteWorldList, - handleFavoriteGroupClear, - handleFavoriteGroup, - handleFavoriteDelete, - handleFavoriteAdd, getCachedFavoritesByObjectId, - checkInvalidLocalAvatars, - removeInvalidLocalAvatars, getCachedFavoriteGroupsByTypeName, - addLocalFriendFavorite, + handleFavoriteGroup, hasLocalFriendFavorite, isInAnyLocalFriendGroup, - removeLocalFriendFavorite, - deleteLocalFriendFavoriteGroup, - renameLocalFriendFavoriteGroup, newLocalFriendFavoriteGroup, - getLocalFriendFavorites + countFavoriteGroups }; }); diff --git a/src/stores/feed.js b/src/stores/feed.js index 64f7fefd..02a3575e 100644 --- a/src/stores/feed.js +++ b/src/stores/feed.js @@ -3,8 +3,6 @@ import { defineStore } from 'pinia'; import { database } from '../service/database'; import { useFriendStore } from './friend'; -import { useNotificationStore } from './notification'; -import { useSharedFeedStore } from './sharedFeed'; import { useVrcxStore } from './vrcx'; import { watchState } from '../service/watchState'; @@ -12,9 +10,7 @@ import configRepository from '../service/config'; export const useFeedStore = defineStore('Feed', () => { const friendStore = useFriendStore(); - const notificationStore = useNotificationStore(); const vrcxStore = useVrcxStore(); - const sharedFeedStore = useSharedFeedStore(); const feedTableData = shallowRef([]); const feedTable = ref({ @@ -170,9 +166,12 @@ export const useFeedStore = defineStore('Feed', () => { } } - function addFeed(feed) { - notificationStore.queueFeedNoty(feed); - sharedFeedStore.addEntry(feed); + /** + * Appends a feed entry to the local table if it passes filters. + * Does NOT trigger notifications or shared feed — that is the caller's responsibility. + * @param {object} feed The feed entry to add. + */ + function addFeedEntry(feed) { if ( feedTable.value.filter.length > 0 && !feedTable.value.filter.includes(feed.type) @@ -222,6 +221,6 @@ export const useFeedStore = defineStore('Feed', () => { feedTableData, initFeedTable, feedTableLookup, - addFeed + addFeedEntry }; }); diff --git a/src/stores/game.js b/src/stores/game.js index bf896fab..be648214 100644 --- a/src/stores/game.js +++ b/src/stores/game.js @@ -1,38 +1,9 @@ import { reactive, ref } from 'vue'; import { defineStore } from 'pinia'; -import { toast } from 'vue-sonner'; - -import { - deleteVRChatCache as _deleteVRChatCache, - isRealInstance -} from '../shared/utils'; -import { database } from '../service/database'; -import { runGameRunningChangedFlow } from '../coordinators/gameCoordinator'; -import { useAdvancedSettingsStore } from './settings/advanced'; -import { useAvatarStore } from './avatar'; -import { useGameLogStore } from './gameLog'; -import { useLaunchStore } from './launch'; -import { useLocationStore } from './location'; -import { useModalStore } from './modal'; -import { useNotificationStore } from './notification'; -import { useVrStore } from './vr'; -import { useWorldStore } from './world'; import configRepository from '../service/config.js'; -import * as workerTimers from 'worker-timers'; - export const useGameStore = defineStore('Game', () => { - const advancedSettingsStore = useAdvancedSettingsStore(); - const locationStore = useLocationStore(); - const notificationStore = useNotificationStore(); - const avatarStore = useAvatarStore(); - const launchStore = useLaunchStore(); - const worldStore = useWorldStore(); - const gameLogStore = useGameLogStore(); - const vrStore = useVrStore(); - const modalStore = useModalStore(); - const state = reactive({ lastCrashedTime: null }); @@ -60,139 +31,13 @@ export const useGameStore = defineStore('Game', () => { init(); - /** - * @param {object} ref Avatar or world reference payload. - */ - async function deleteVRChatCache(ref) { - await _deleteVRChatCache(ref); - getVRChatCacheSize(); - worldStore.updateVRChatWorldCache(); - avatarStore.updateVRChatAvatarCache(); - } + // --- Atomic setters --- /** - * + * @param {boolean} value Game running flag. */ - function autoVRChatCacheManagement() { - if (advancedSettingsStore.autoSweepVRChatCache) { - sweepVRChatCache(); - } - } - - /** - * - */ - async function sweepVRChatCache() { - try { - const output = await AssetBundleManager.SweepCache(); - console.log('SweepCache', output); - } catch (e) { - console.error('SweepCache failed', e); - } - if (advancedSettingsStore.isVRChatConfigDialogVisible) { - getVRChatCacheSize(); - } - } - - /** - * - */ - function checkIfGameCrashed() { - if (!advancedSettingsStore.relaunchVRChatAfterCrash) { - return; - } - const { location } = locationStore.lastLocation; - AppApi.VrcClosedGracefully().then((result) => { - if (result || !isRealInstance(location)) { - return; - } - // check if relaunched less than 2mins ago (prvent crash loop) - if ( - state.lastCrashedTime && - new Date().getTime() - state.lastCrashedTime.getTime() < 120_000 - ) { - console.log('VRChat was recently crashed, not relaunching'); - return; - } - state.lastCrashedTime = new Date(); - // wait a bit for SteamVR to potentially close before deciding to relaunch - let restartDelay = 8000; - if (isGameNoVR.value) { - // wait for game to close before relaunching - restartDelay = 2000; - } - workerTimers.setTimeout( - () => restartCrashedGame(location), - restartDelay - ); - }); - } - - /** - * @param {string} location Last known location to relaunch. - */ - function restartCrashedGame(location) { - if (!isGameNoVR.value && !isSteamVRRunning.value) { - console.log("SteamVR isn't running, not relaunching VRChat"); - return; - } - AppApi.FocusWindow(); - const message = 'VRChat crashed, attempting to rejoin last instance'; - toast(message); - const entry = { - created_at: new Date().toJSON(), - type: 'Event', - data: message - }; - database.addGamelogEventToDatabase(entry); - notificationStore.queueGameLogNoty(entry); - gameLogStore.addGameLog(entry); - launchStore.launchGame(location, '', isGameNoVR.value); - } - - /** - * - */ - async function getVRChatCacheSize() { - VRChatCacheSizeLoading.value = true; - const totalCacheSize = 30; - VRChatTotalCacheSize.value = totalCacheSize; - const usedCacheSize = await AssetBundleManager.GetCacheSize(); - VRChatUsedCacheSize.value = (usedCacheSize / 1073741824).toFixed(2); - VRChatCacheSizeLoading.value = false; - } - - // use in C# - /** - * @param {boolean} isGameRunningArg Game running flag from IPC. - * @param {boolean} isSteamVRRunningArg SteamVR running flag from IPC. - */ - async function updateIsGameRunning(isGameRunningArg, isSteamVRRunningArg) { - if (advancedSettingsStore.gameLogDisabled) { - return; - } - if (isGameRunningArg !== isGameRunning.value) { - isGameRunning.value = isGameRunningArg; - await runGameRunningChangedFlow(isGameRunningArg); - console.log(new Date(), 'isGameRunning', isGameRunningArg); - } - - if (isSteamVRRunningArg !== isSteamVRRunning.value) { - isSteamVRRunning.value = isSteamVRRunningArg; - console.log('isSteamVRRunning:', isSteamVRRunningArg); - } - vrStore.updateOpenVR(); - } - - // use in C# - /** - * @param {boolean} isHmdAfkArg HMD AFK flag from VR polling. - */ - function updateIsHmdAfk(isHmdAfkArg) { - if (isHmdAfkArg !== isHmdAfk.value) { - isHmdAfk.value = isHmdAfkArg; - console.log('isHmdAfk', isHmdAfkArg); - } + function setIsGameRunning(value) { + isGameRunning.value = value; } /** @@ -203,50 +48,38 @@ export const useGameStore = defineStore('Game', () => { } /** - * + * @param {boolean} value SteamVR running flag. */ - async function checkVRChatDebugLogging() { - if (advancedSettingsStore.gameLogDisabled) { - return; - } - try { - const loggingEnabled = - await getVRChatRegistryKey('LOGGING_ENABLED'); - if ( - loggingEnabled === null || - typeof loggingEnabled === 'undefined' - ) { - // key not found - return; - } - if (parseInt(loggingEnabled, 10) === 1) { - // already enabled - return; - } - const result = await AppApi.SetVRChatRegistryKey( - 'LOGGING_ENABLED', - '1', - 4 - ); - if (!result) { - // failed to set key - modalStore.alert({ - description: - 'VRCX has noticed VRChat debug logging is disabled. VRCX requires debug logging in order to function correctly. Please enable debug logging in VRChat quick menu settings > debug > enable debug logging, then rejoin the instance or restart VRChat.', - title: 'Enable debug logging' - }); - console.error('Failed to enable debug logging', result); - return; - } - modalStore.alert({ - description: - 'VRCX has noticed VRChat debug logging is disabled and automatically re-enabled it. VRCX requires debug logging in order to function correctly.', - title: 'Enabled debug logging' - }); - console.log('Enabled debug logging'); - } catch (e) { - console.error(e); - } + function setIsSteamVRRunning(value) { + isSteamVRRunning.value = value; + } + + /** + * @param {boolean} value HMD AFK flag. + */ + function setIsHmdAfk(value) { + isHmdAfk.value = value; + } + + /** + * @param {Date | null} value Last crashed time. + */ + function setLastCrashedTime(value) { + state.lastCrashedTime = value; + } + + // --- Self-contained operations (no cross-store deps) --- + + /** + * Fetches VRChat cache size from AssetBundleManager. + */ + async function getVRChatCacheSize() { + VRChatCacheSizeLoading.value = true; + const totalCacheSize = 30; + VRChatTotalCacheSize.value = totalCacheSize; + const usedCacheSize = await AssetBundleManager.GetCacheSize(); + VRChatUsedCacheSize.value = (usedCacheSize / 1073741824).toFixed(2); + VRChatCacheSizeLoading.value = false; } /** @@ -271,15 +104,12 @@ export const useGameStore = defineStore('Game', () => { isSteamVRRunning, isHmdAfk, - deleteVRChatCache, - sweepVRChatCache, - getVRChatCacheSize, - updateIsGameRunning, + setIsGameRunning, setIsGameNoVR, - getVRChatRegistryKey, - checkVRChatDebugLogging, - autoVRChatCacheManagement, - checkIfGameCrashed, - updateIsHmdAfk + setIsSteamVRRunning, + setIsHmdAfk, + setLastCrashedTime, + getVRChatCacheSize, + getVRChatRegistryKey }; }); diff --git a/src/stores/gameLog/index.js b/src/stores/gameLog/index.js index 9eb5cf0c..8e574d2b 100644 --- a/src/stores/gameLog/index.js +++ b/src/stores/gameLog/index.js @@ -31,6 +31,7 @@ import { useGameStore } from '../game'; import { useGeneralSettingsStore } from '../settings/general'; import { useInstanceStore } from '../instance'; import { useLocationStore } from '../location'; +import { runLastLocationResetFlow, runUpdateCurrentUserLocationFlow } from '../../coordinators/locationCoordinator'; import { useModalStore } from '../modal'; import { useNotificationStore } from '../notification'; import { usePhotonStore } from '../photon'; @@ -376,7 +377,7 @@ export const useGameLogStore = defineStore('GameLog', () => { } }); - locationStore.updateCurrentUserLocation(); + runUpdateCurrentUserLocationFlow(); instanceStore.updateCurrentInstanceWorld(); vrStore.updateVRLastLocation(); instanceStore.getCurrentInstanceUserList(); @@ -560,7 +561,7 @@ export const useGameLogStore = defineStore('GameLog', () => { type: 'LocationDestination', location: gameLog.location }); - locationStore.lastLocationReset(gameLog.dt); + runLastLocationResetFlow(gameLog.dt); locationStore.setLastLocationLocation('traveling'); locationStore.setLastLocationDestination(gameLog.location); locationStore.setLastLocationDestinationTime( @@ -568,7 +569,7 @@ export const useGameLogStore = defineStore('GameLog', () => { ); state.lastLocationAvatarList.clear(); instanceStore.removeQueuedInstance(gameLog.location); - locationStore.updateCurrentUserLocation(); + runUpdateCurrentUserLocationFlow(); clearNowPlaying(); instanceStore.updateCurrentInstanceWorld(); userStore.applyUserDialogLocation(); @@ -583,7 +584,7 @@ export const useGameLogStore = defineStore('GameLog', () => { ); const worldName = replaceBioSymbols(gameLog.worldName); if (gameStore.isGameRunning) { - locationStore.lastLocationReset(gameLog.dt); + runLastLocationResetFlow(gameLog.dt); clearNowPlaying(); locationStore.setLastLocation({ date: Date.parse(gameLog.dt), @@ -593,7 +594,7 @@ export const useGameLogStore = defineStore('GameLog', () => { friendList: new Map() }); instanceStore.removeQueuedInstance(gameLog.location); - locationStore.updateCurrentUserLocation(); + runUpdateCurrentUserLocationFlow(); vrStore.updateVRLastLocation(); instanceStore.updateCurrentInstanceWorld(); userStore.applyUserDialogLocation(); diff --git a/src/stores/invite.js b/src/stores/invite.js index e7b635cb..1ee4bfd3 100644 --- a/src/stores/invite.js +++ b/src/stores/invite.js @@ -1,22 +1,14 @@ import { computed, ref, watch } from 'vue'; import { defineStore } from 'pinia'; -import { toast } from 'vue-sonner'; -import { useI18n } from 'vue-i18n'; -import { instanceRequest, inviteMessagesRequest } from '../api'; -import { parseLocation } from '../shared/utils'; +import { inviteMessagesRequest } from '../api'; import { useAdvancedSettingsStore } from './settings/advanced'; import { useGameStore } from './game'; -import { useInstanceStore } from './instance'; -import { useLaunchStore } from './launch'; import { watchState } from '../service/watchState'; export const useInviteStore = defineStore('Invite', () => { - const instanceStore = useInstanceStore(); const gameStore = useGameStore(); - const launchStore = useLaunchStore(); const advancedSettingsStore = useAdvancedSettingsStore(); - const { t } = useI18n(); const inviteMessageTable = ref({ data: [], @@ -93,35 +85,7 @@ export const useInviteStore = defineStore('Invite', () => { }); } - function newInstanceSelfInvite(worldId) { - instanceStore.createNewInstance(worldId).then((args) => { - const location = args?.json?.location; - if (!location) { - toast.error(t('message.instance.create_failed')); - return; - } - // self invite - const L = parseLocation(location); - if (!L.isRealInstance) { - return; - } - if (canOpenInstanceInGame.value) { - const secureOrShortName = - args.json.shortName || args.json.secureName; - launchStore.tryOpenInstanceInVrc(location, secureOrShortName); - return; - } - instanceRequest - .selfInvite({ - instanceId: L.instanceId, - worldId: L.worldId - }) - .then((args) => { - toast.success(t('message.invite.self_sent')); - return args; - }); - }); - } + return { inviteMessageTable, @@ -129,7 +93,6 @@ export const useInviteStore = defineStore('Invite', () => { inviteRequestMessageTable, inviteRequestResponseMessageTable, refreshInviteMessageTableData, - newInstanceSelfInvite, canOpenInstanceInGame }; }); diff --git a/src/stores/location.js b/src/stores/location.js index 3d817e32..414a243b 100644 --- a/src/stores/location.js +++ b/src/stores/location.js @@ -1,32 +1,7 @@ import { defineStore } from 'pinia'; import { ref } from 'vue'; -import { - getGroupName, - getWorldName, - isRealInstance, - parseLocation -} from '../shared/utils'; -import { database } from '../service/database'; -import { useAdvancedSettingsStore } from './settings/advanced'; -import { useGameLogStore } from './gameLog'; -import { useGameStore } from './game'; -import { useInstanceStore } from './instance'; -import { useNotificationStore } from './notification'; -import { usePhotonStore } from './photon'; -import { useUserStore } from './user'; -import { useVrStore } from './vr'; - export const useLocationStore = defineStore('Location', () => { - const advancedSettingsStore = useAdvancedSettingsStore(); - const userStore = useUserStore(); - const instanceStore = useInstanceStore(); - const notificationStore = useNotificationStore(); - const gameStore = useGameStore(); - const vrStore = useVrStore(); - const photonStore = usePhotonStore(); - const gameLogStore = useGameLogStore(); - const lastLocation = ref({ date: null, location: '', @@ -37,153 +12,6 @@ export const useLocationStore = defineStore('Location', () => { const lastLocationDestination = ref(''); const lastLocationDestinationTime = ref(0); - function updateCurrentUserLocation() { - const ref = userStore.cachedUsers.get(userStore.currentUser.id); - if (typeof ref === 'undefined') { - return; - } - - // update cached user with both gameLog and API locations - let currentLocation = userStore.currentUser.$locationTag; - const L = parseLocation(currentLocation); - if (L.isTraveling) { - currentLocation = userStore.currentUser.$travelingToLocation; - } - ref.location = userStore.currentUser.$locationTag; - ref.travelingToLocation = userStore.currentUser.$travelingToLocation; - - if ( - gameStore.isGameRunning && - !advancedSettingsStore.gameLogDisabled && - lastLocation.value.location !== '' - ) { - // use gameLog instead of API when game is running - currentLocation = lastLocation.value.location; - if (lastLocation.value.location === 'traveling') { - currentLocation = lastLocationDestination.value; - } - ref.location = lastLocation.value.location; - ref.travelingToLocation = lastLocationDestination.value; - } - - ref.$online_for = userStore.currentUser.$online_for; - ref.$offline_for = userStore.currentUser.$offline_for; - ref.$location = parseLocation(currentLocation); - if (!gameStore.isGameRunning || advancedSettingsStore.gameLogDisabled) { - ref.$location_at = userStore.currentUser.$location_at; - ref.$travelingToTime = userStore.currentUser.$travelingToTime; - userStore.applyUserDialogLocation(); - instanceStore.applyWorldDialogInstances(); - instanceStore.applyGroupDialogInstances(); - } else { - ref.$location_at = lastLocation.value.date; - ref.$travelingToTime = lastLocationDestinationTime.value; - userStore.setCurrentUserTravelingToTime( - lastLocationDestinationTime.value - ); - } - } - - async function setCurrentUserLocation(location, travelingToLocation) { - userStore.setCurrentUserLocationState(location, travelingToLocation); - updateCurrentUserLocation(); - - // janky gameLog support for Quest - if (gameStore.isGameRunning) { - // with the current state of things, lets not run this if we don't need to - return; - } - const lastLocationArray = await database.lookupGameLogDatabase( - ['Location'], - [], - 1 - ); - const lastLocationTemp = - lastLocationArray.length > 0 ? lastLocationArray[0].location : ''; - if (lastLocationTemp === location) { - return; - } - lastLocationDestination.value = ''; - lastLocationDestinationTime.value = 0; - - if (isRealInstance(location)) { - const dt = new Date().toJSON(); - const L = parseLocation(location); - - lastLocation.value.location = location; - lastLocation.value.date = Date.now(); - - const entry = { - created_at: dt, - type: 'Location', - location, - worldId: L.worldId, - worldName: await getWorldName(L.worldId), - groupName: await getGroupName(L.groupId), - time: 0 - }; - database.addGamelogLocationToDatabase(entry); - notificationStore.queueGameLogNoty(entry); - gameLogStore.addGameLog(entry); - instanceStore.addInstanceJoinHistory(location, dt); - - userStore.applyUserDialogLocation(); - instanceStore.applyWorldDialogInstances(); - instanceStore.applyGroupDialogInstances(); - } else { - lastLocation.value.location = ''; - lastLocation.value.date = null; - } - } - - function lastLocationReset(gameLogDate) { - let dateTime = gameLogDate; - if (!gameLogDate) { - dateTime = new Date().toJSON(); - } - const dateTimeStamp = Date.parse(dateTime); - photonStore.resetLocationPhotonState(); - const playerList = Array.from(lastLocation.value.playerList.values()); - const dataBaseEntries = []; - for (const ref of playerList) { - const entry = { - created_at: dateTime, - type: 'OnPlayerLeft', - displayName: ref.displayName, - location: lastLocation.value.location, - userId: ref.userId, - time: dateTimeStamp - ref.joinTime - }; - dataBaseEntries.unshift(entry); - gameLogStore.addGameLog(entry); - } - database.addGamelogJoinLeaveBulk(dataBaseEntries); - if (lastLocation.value.date !== null && lastLocation.value.date > 0) { - const update = { - time: dateTimeStamp - lastLocation.value.date, - created_at: new Date(lastLocation.value.date).toJSON() - }; - database.updateGamelogLocationTimeToDatabase(update); - } - lastLocationDestination.value = ''; - lastLocationDestinationTime.value = 0; - lastLocation.value = { - date: 0, - location: '', - name: '', - playerList: new Map(), - friendList: new Map() - }; - updateCurrentUserLocation(); - instanceStore.updateCurrentInstanceWorld(); - vrStore.updateVRLastLocation(); - instanceStore.getCurrentInstanceUserList(); - gameLogStore.resetLastMediaUrls(); - userStore.applyUserDialogLocation(); - instanceStore.applyWorldDialogInstances(); - instanceStore.applyGroupDialogInstances(); - } - /** * @param {{date: number|null, location: string, name: string, playerList: Map, friendList: Map}} value */ @@ -216,12 +44,10 @@ export const useLocationStore = defineStore('Location', () => { lastLocation, lastLocationDestination, lastLocationDestinationTime, - updateCurrentUserLocation, - setCurrentUserLocation, - lastLocationReset, setLastLocation, setLastLocationLocation, setLastLocationDestination, setLastLocationDestinationTime }; }); + diff --git a/src/stores/moderation.js b/src/stores/moderation.js index c4e3fbf3..1784589b 100644 --- a/src/stores/moderation.js +++ b/src/stores/moderation.js @@ -1,13 +1,11 @@ import { reactive, ref, watch } from 'vue'; import { defineStore } from 'pinia'; -import { avatarModerationRequest, playerModerationRequest } from '../api'; -import { useAvatarStore } from './avatar'; +import { playerModerationRequest } from '../api'; import { useUserStore } from './user'; import { watchState } from '../service/watchState'; export const useModerationStore = defineStore('Moderation', () => { - const avatarStore = useAvatarStore(); const userStore = useUserStore(); const cachedPlayerModerations = reactive(new Map()); @@ -37,9 +35,6 @@ export const useModerationStore = defineStore('Moderation', () => { cachedPlayerModerationsUserIds.clear(); playerModerationTable.value.loading = false; playerModerationTable.value.data = []; - if (isLoggedIn) { - refreshPlayerModerations(); - } }, { flush: 'sync' } ); @@ -178,41 +173,7 @@ export const useModerationStore = defineStore('Moderation', () => { } } - async function refreshPlayerModerations() { - if (playerModerationTable.value.loading) { - return; - } - playerModerationTable.value.loading = true; - expirePlayerModerations(); - Promise.all([ - playerModerationRequest.getPlayerModerations(), - avatarModerationRequest.getAvatarModerations() - ]) - .finally(() => { - playerModerationTable.value.loading = false; - }) - .then((res) => { - // TODO: compare with cachedAvatarModerations - avatarStore.resetCachedAvatarModerations(); - if (res[1]?.json) { - for (const json of res[1].json) { - avatarStore.applyAvatarModeration(json); - } - } - if (res[0]?.json) { - for (let json of res[0].json) { - applyPlayerModeration(json); - } - } - deleteExpiredPlayerModerations(); - }) - .catch((error) => { - console.error( - 'Failed to load player/avatar moderations:', - error - ); - }); - } + /** * Get user moderations @@ -257,7 +218,8 @@ export const useModerationStore = defineStore('Moderation', () => { cachedPlayerModerationsUserIds, playerModerationTable, - refreshPlayerModerations, + expirePlayerModerations, + deleteExpiredPlayerModerations, applyPlayerModeration, handlePlayerModerationDelete, getUserModerations diff --git a/src/stores/photon.js b/src/stores/photon.js index 8f471c6a..7f639b80 100644 --- a/src/stores/photon.js +++ b/src/stores/photon.js @@ -1,3 +1,5 @@ +// @deprecated +// This store is no longer maintained. import { computed, reactive, ref } from 'vue'; import { defineStore } from 'pinia'; import { toast } from 'vue-sonner'; diff --git a/src/stores/updateLoop.js b/src/stores/updateLoop.js index 2b2017b1..95fc1cd5 100644 --- a/src/stores/updateLoop.js +++ b/src/stores/updateLoop.js @@ -4,6 +4,8 @@ import { watch } from 'vue'; import { database } from '../service/database'; import { groupRequest } from '../api'; import { runRefreshFriendsListFlow } from '../coordinators/friendSyncCoordinator'; +import { runUpdateIsGameRunningFlow } from '../coordinators/gameCoordinator'; +import { runRefreshPlayerModerationsFlow } from '../coordinators/moderationCoordinator'; import { useAuthStore } from './auth'; import { useDiscordPresenceSettingsStore } from './settings/discordPresence'; import { useFriendStore } from './friend'; @@ -82,7 +84,7 @@ export const useUpdateLoopStore = defineStore('UpdateLoop', () => { new Date(userStore.currentUser.last_activity) > new Date(Date.now() - 3600 * 1000) // 1hour ) { - moderationStore.refreshPlayerModerations(); + runRefreshPlayerModerationsFlow(); } } if (--state.nextGroupInstanceRefresh <= 0) { @@ -133,7 +135,7 @@ export const useUpdateLoopStore = defineStore('UpdateLoop', () => { } if (LINUX && --state.nextGameRunningCheck <= 0) { state.nextGameRunningCheck = 1; - gameStore.updateIsGameRunning( + await runUpdateIsGameRunningFlow( await AppApi.IsGameRunning(), await AppApi.IsSteamVRRunning() ); diff --git a/src/stores/user.js b/src/stores/user.js index d8964cc9..a8a2218a 100644 --- a/src/stores/user.js +++ b/src/stores/user.js @@ -41,7 +41,9 @@ import { AppDebug } from '../service/appConfig'; import { database } from '../service/database'; import { patchUserFromEvent } from '../queries'; import { runHandleUserUpdateFlow } from '../coordinators/userEventCoordinator'; +import { runUpdateCurrentUserLocationFlow } from '../coordinators/locationCoordinator'; import { runUpdateFriendFlow } from '../coordinators/friendPresenceCoordinator'; +import { applyFavorite } from '../coordinators/favoriteCoordinator'; import { useAppearanceSettingsStore } from './settings/appearance'; import { useAuthStore } from './auth'; import { useAvatarStore } from './avatar'; @@ -424,7 +426,7 @@ export const useUserStore = defineStore('User', () => { } else { ref.$travelingToLocation = presence.travelingToWorld; } - locationStore.updateCurrentUserLocation(); + runUpdateCurrentUserLocationFlow(); } const robotUrl = `${AppDebug.endpointDomain}/file/file_0e8c4e32-7444-44ea-ade4-313c010d4bae/1/file`; @@ -546,7 +548,7 @@ export const useUserStore = defineStore('User', () => { if (ref.status) { currentUser.value.status = ref.status; } - locationStore.updateCurrentUserLocation(); + runUpdateCurrentUserLocationFlow(); } // add user ref to playerList, friendList, photonLobby, photonLobbyCurrent const playerListRef = locationStore.lastLocation.playerList.get(ref.id); @@ -586,7 +588,7 @@ export const useUserStore = defineStore('User', () => { if (ref.state === 'online') { runUpdateFriendFlow(ref.id, ref.state); // online/offline } - favoriteStore.applyFavorite('friend', ref.id); + applyFavorite('friend', ref.id); friendStore.userOnFriend(ref); const D = userDialog.value; if (D.visible && D.id === ref.id) { diff --git a/src/stores/vr.js b/src/stores/vr.js index 833f2016..50c86244 100644 --- a/src/stores/vr.js +++ b/src/stores/vr.js @@ -181,7 +181,7 @@ export const useVrStore = defineStore('Vr', () => { newState.overlayHand ); if (!newState.active) { - gameStore.updateIsHmdAfk(false); + gameStore.setIsHmdAfk(false); } if (LINUX) { diff --git a/src/stores/vrcx.js b/src/stores/vrcx.js index 18847b09..51bfd162 100644 --- a/src/stores/vrcx.js +++ b/src/stores/vrcx.js @@ -22,6 +22,10 @@ import { refreshCustomScript } from '../shared/utils/base/ui'; import { useAdvancedSettingsStore } from './settings/advanced'; import { useAvatarProviderStore } from './avatarProvider'; import { useAvatarStore } from './avatar'; +import { + addLocalWorldFavorite, + addLocalAvatarFavorite +} from '../coordinators/favoriteCoordinator'; import { useFavoriteStore } from './favorite'; import { useFriendStore } from './friend'; import { useGalleryStore } from './gallery'; @@ -686,7 +690,7 @@ export const useVrcxStore = defineStore('Vrcx', () => { } queryRequest.fetch('world', { worldId: id }).then(() => { searchStore.directAccessWorld(id); - favoriteStore.addLocalWorldFavorite(id, group); + addLocalWorldFavorite(id, group); }); break; case 'local-favorite-avatar': @@ -698,7 +702,7 @@ export const useVrcxStore = defineStore('Vrcx', () => { } avatarRequest.getAvatar({ avatarId: avatarIdFav }).then(() => { avatarStore.showAvatarDialog(avatarIdFav); - favoriteStore.addLocalAvatarFavorite( + addLocalAvatarFavorite( avatarIdFav, avatarGroup ); diff --git a/src/stores/world.js b/src/stores/world.js index 325e7e83..91c8ba50 100644 --- a/src/stores/world.js +++ b/src/stores/world.js @@ -18,6 +18,7 @@ import { instanceRequest, queryRequest, worldRequest } from '../api'; import { database } from '../service/database'; import { patchWorldFromEvent } from '../queries'; import { processBulk } from '../service/request'; +import { applyFavorite } from '../coordinators/favoriteCoordinator'; import { useFavoriteStore } from './favorite'; import { useInstanceStore } from './instance'; import { useLocationStore } from './location'; @@ -275,7 +276,7 @@ export const useWorldStore = defineStore('World', () => { Object.assign(ref, json); } ref.$isLabs = ref.tags.includes('system_labs'); - favoriteStore.applyFavorite('world', ref.id); + applyFavorite('world', ref.id); const userDialog = userStore.userDialog; if (userDialog.visible && userDialog.$location.worldId === ref.id) { userStore.applyUserDialogLocation(); diff --git a/src/views/Favorites/FavoritesAvatar.vue b/src/views/Favorites/FavoritesAvatar.vue index 9a8ae755..671e14e9 100644 --- a/src/views/Favorites/FavoritesAvatar.vue +++ b/src/views/Favorites/FavoritesAvatar.vue @@ -463,6 +463,15 @@ import { useFavoritesGroupPanel } from './composables/useFavoritesGroupPanel.js'; import { useFavoritesLocalGroups } from './composables/useFavoritesLocalGroups.js'; import { useFavoritesSplitter } from './composables/useFavoritesSplitter.js'; + import { + deleteLocalAvatarFavoriteGroup, + renameLocalAvatarFavoriteGroup, + newLocalAvatarFavoriteGroup, + refreshFavorites, + getLocalAvatarFavorites, + checkInvalidLocalAvatars, + removeInvalidLocalAvatars + } from '../../coordinators/favoriteCoordinator'; import AvatarExportDialog from './dialogs/AvatarExportDialog.vue'; import FavoritesAvatarItem from './components/FavoritesAvatarItem.vue'; @@ -506,15 +515,8 @@ const { showAvatarImportDialog, localAvatarFavGroupLength, - deleteLocalAvatarFavoriteGroup, - renameLocalAvatarFavoriteGroup, - newLocalAvatarFavoriteGroup, localAvatarFavoritesList, - refreshFavorites, - getLocalAvatarFavorites, - handleFavoriteGroup, - checkInvalidLocalAvatars, - removeInvalidLocalAvatars + handleFavoriteGroup } = favoriteStore; const { avatarHistory } = storeToRefs(useAvatarStore()); const { promptClearAvatarHistory, showAvatarDialog, applyAvatar } = useAvatarStore(); diff --git a/src/views/Favorites/FavoritesFriend.vue b/src/views/Favorites/FavoritesFriend.vue index 413e1309..2f1f02a1 100644 --- a/src/views/Favorites/FavoritesFriend.vue +++ b/src/views/Favorites/FavoritesFriend.vue @@ -356,6 +356,14 @@ import { useFavoritesGroupPanel } from './composables/useFavoritesGroupPanel.js'; import { useFavoritesLocalGroups } from './composables/useFavoritesLocalGroups.js'; import { useFavoritesSplitter } from './composables/useFavoritesSplitter.js'; + import { + refreshFavorites, + getLocalWorldFavorites, + getLocalFriendFavorites, + deleteLocalFriendFavoriteGroup, + renameLocalFriendFavoriteGroup, + removeLocalFriendFavorite + } from '../../coordinators/favoriteCoordinator'; import FavoritesContentHeader from './components/FavoritesContentHeader.vue'; import FavoritesFriendItem from './components/FavoritesFriendItem.vue'; @@ -390,15 +398,9 @@ } = storeToRefs(favoriteStore); const { showFriendImportDialog, - refreshFavorites, - getLocalWorldFavorites, - getLocalFriendFavorites, handleFavoriteGroup, localFriendFavGroupLength, - deleteLocalFriendFavoriteGroup, - renameLocalFriendFavoriteGroup, - newLocalFriendFavoriteGroup, - removeLocalFriendFavorite + newLocalFriendFavoriteGroup } = favoriteStore; const userStore = useUserStore(); const { showUserDialog } = userStore; diff --git a/src/views/Favorites/FavoritesWorld.vue b/src/views/Favorites/FavoritesWorld.vue index 34c9c38e..8f6e0a6b 100644 --- a/src/views/Favorites/FavoritesWorld.vue +++ b/src/views/Favorites/FavoritesWorld.vue @@ -395,6 +395,13 @@ import { useFavoritesGroupPanel } from './composables/useFavoritesGroupPanel.js'; import { useFavoritesLocalGroups } from './composables/useFavoritesLocalGroups.js'; import { useFavoritesSplitter } from './composables/useFavoritesSplitter.js'; + import { + renameLocalWorldFavoriteGroup, + removeLocalWorldFavorite, + newLocalWorldFavoriteGroup, + refreshFavorites, + getLocalWorldFavorites + } from '../../coordinators/favoriteCoordinator'; import FavoritesContentHeader from './components/FavoritesContentHeader.vue'; import FavoritesToolbar from './components/FavoritesToolbar.vue'; @@ -426,13 +433,8 @@ showWorldImportDialog, localWorldFavGroupLength, deleteLocalWorldFavoriteGroup, - renameLocalWorldFavoriteGroup, - removeLocalWorldFavorite, - newLocalWorldFavoriteGroup, handleFavoriteGroup, - localWorldFavoritesList, - refreshFavorites, - getLocalWorldFavorites + localWorldFavoritesList } = favoriteStore; const { showWorldDialog } = useWorldStore(); diff --git a/src/views/Favorites/components/FavoritesAvatarItem.vue b/src/views/Favorites/components/FavoritesAvatarItem.vue index 1c7b2f2e..a91d6edd 100644 --- a/src/views/Favorites/components/FavoritesAvatarItem.vue +++ b/src/views/Favorites/components/FavoritesAvatarItem.vue @@ -152,6 +152,7 @@ import { useI18n } from 'vue-i18n'; import { useAvatarStore, useFavoriteStore, useUiStore, useUserStore } from '../../../stores'; + import { removeLocalAvatarFavorite } from '../../../coordinators/favoriteCoordinator'; import { favoriteRequest } from '../../../api'; import FavoritesMoveDropdown from './FavoritesMoveDropdown.vue'; @@ -168,7 +169,7 @@ const { t } = useI18n(); const { favoriteAvatarGroups } = storeToRefs(useFavoriteStore()); - const { removeLocalAvatarFavorite, showFavoriteDialog } = useFavoriteStore(); + const { showFavoriteDialog } = useFavoriteStore(); const { selectAvatarWithConfirmation } = useAvatarStore(); const { shiftHeld } = storeToRefs(useUiStore()); const { currentUser } = storeToRefs(useUserStore()); diff --git a/src/views/Favorites/components/FavoritesFriendItem.vue b/src/views/Favorites/components/FavoritesFriendItem.vue index a9da4654..c0985bc9 100644 --- a/src/views/Favorites/components/FavoritesFriendItem.vue +++ b/src/views/Favorites/components/FavoritesFriendItem.vue @@ -91,6 +91,7 @@ import { useI18n } from 'vue-i18n'; import { favoriteRequest } from '../../../api'; + import { removeLocalFriendFavorite } from '../../../coordinators/favoriteCoordinator'; import { useFavoriteStore } from '../../../stores'; import { userImage } from '../../../shared/utils'; @@ -106,7 +107,7 @@ const emit = defineEmits(['click', 'toggle-select']); const { favoriteFriendGroups } = storeToRefs(useFavoriteStore()); - const { showFavoriteDialog, removeLocalFriendFavorite } = useFavoriteStore(); + const { showFavoriteDialog } = useFavoriteStore(); const { t } = useI18n(); const isSelected = computed({ diff --git a/src/views/Favorites/components/FavoritesWorldItem.vue b/src/views/Favorites/components/FavoritesWorldItem.vue index 91eaa776..73f759e7 100644 --- a/src/views/Favorites/components/FavoritesWorldItem.vue +++ b/src/views/Favorites/components/FavoritesWorldItem.vue @@ -166,6 +166,7 @@ import { useI18n } from 'vue-i18n'; import { useFavoriteStore, useInstanceStore, useInviteStore, useUiStore } from '../../../stores'; + import { runNewInstanceSelfInviteFlow as newInstanceSelfInvite } from '../../../coordinators/inviteCoordinator'; import { favoriteRequest } from '../../../api'; import FavoritesMoveDropdown from './FavoritesMoveDropdown.vue'; @@ -181,7 +182,7 @@ const emit = defineEmits(['toggle-select', 'remove-local-world-favorite', 'click']); const { favoriteWorldGroups } = storeToRefs(useFavoriteStore()); const { showFavoriteDialog } = useFavoriteStore(); - const { newInstanceSelfInvite } = useInviteStore(); + const { t } = useI18n(); const { canOpenInstanceInGame } = useInviteStore(); const { shiftHeld } = storeToRefs(useUiStore()); diff --git a/src/views/Favorites/components/FavoritesWorldLocalItem.vue b/src/views/Favorites/components/FavoritesWorldLocalItem.vue index 1b714dc3..52e3d794 100644 --- a/src/views/Favorites/components/FavoritesWorldLocalItem.vue +++ b/src/views/Favorites/components/FavoritesWorldLocalItem.vue @@ -130,6 +130,7 @@ import { useI18n } from 'vue-i18n'; import { useFavoriteStore, useInstanceStore, useInviteStore, useUiStore } from '../../../stores'; + import { runNewInstanceSelfInviteFlow as newInstanceSelfInvite } from '../../../coordinators/inviteCoordinator'; import FavoritesMoveDropdown from './FavoritesMoveDropdown.vue'; @@ -142,7 +143,7 @@ const emit = defineEmits(['remove-local-world-favorite', 'click']); const { favoriteWorldGroups } = storeToRefs(useFavoriteStore()); const { showFavoriteDialog } = useFavoriteStore(); - const { newInstanceSelfInvite } = useInviteStore(); + const { shiftHeld } = storeToRefs(useUiStore()); const { t } = useI18n(); const { canOpenInstanceInGame } = useInviteStore(); diff --git a/src/views/Favorites/components/__tests__/FavoritesWorldItem.test.js b/src/views/Favorites/components/__tests__/FavoritesWorldItem.test.js index 1259ce8d..9e7b66e4 100644 --- a/src/views/Favorites/components/__tests__/FavoritesWorldItem.test.js +++ b/src/views/Favorites/components/__tests__/FavoritesWorldItem.test.js @@ -11,9 +11,13 @@ const mocks = vi.hoisted(() => ({ createNewInstance: vi.fn() })); -vi.mock('pinia', () => ({ - storeToRefs: (store) => store -})); +vi.mock('pinia', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + storeToRefs: (store) => store + }; +}); vi.mock('vue-i18n', () => ({ useI18n: () => ({ diff --git a/src/views/Favorites/dialogs/AvatarImportDialog.vue b/src/views/Favorites/dialogs/AvatarImportDialog.vue index a7be277e..8ba87009 100644 --- a/src/views/Favorites/dialogs/AvatarImportDialog.vue +++ b/src/views/Favorites/dialogs/AvatarImportDialog.vue @@ -123,6 +123,7 @@ import { useI18n } from 'vue-i18n'; import { useAvatarStore, useFavoriteStore, useGalleryStore, useUserStore } from '../../../stores'; + import { addLocalAvatarFavorite } from '../../../coordinators/favoriteCoordinator'; import { avatarRequest, favoriteRequest } from '../../../api'; import { createColumns } from './avatarImportColumns.jsx'; import { removeFromArray } from '../../../shared/utils'; @@ -133,7 +134,7 @@ const { showUserDialog } = useUserStore(); const { favoriteAvatarGroups, avatarImportDialogInput, avatarImportDialogVisible, localAvatarFavoriteGroups } = storeToRefs(useFavoriteStore()); - const { addLocalAvatarFavorite, localAvatarFavGroupLength, getCachedFavoritesByObjectId } = useFavoriteStore(); + const { localAvatarFavGroupLength, getCachedFavoritesByObjectId } = useFavoriteStore(); const { showAvatarDialog, applyAvatar } = useAvatarStore(); const { showFullscreenImageDialog } = useGalleryStore(); diff --git a/src/views/Favorites/dialogs/FriendImportDialog.vue b/src/views/Favorites/dialogs/FriendImportDialog.vue index 6be2a04f..63d57950 100644 --- a/src/views/Favorites/dialogs/FriendImportDialog.vue +++ b/src/views/Favorites/dialogs/FriendImportDialog.vue @@ -127,6 +127,7 @@ import { removeFromArray, userImage, userImageFull } from '../../../shared/utils'; import { useFavoriteStore, useGalleryStore, useUserStore } from '../../../stores'; + import { addLocalFriendFavorite } from '../../../coordinators/favoriteCoordinator'; import { favoriteRequest, userRequest } from '../../../api'; import { createColumns } from './friendImportColumns.jsx'; import { useVrcxVueTable } from '../../../lib/table/useVrcxVueTable'; @@ -139,7 +140,7 @@ const { favoriteFriendGroups, friendImportDialogInput, friendImportDialogVisible, localFriendFavoriteGroups } = storeToRefs(useFavoriteStore()); const { showFullscreenImageDialog } = useGalleryStore(); - const { getCachedFavoritesByObjectId, localFriendFavGroupLength, addLocalFriendFavorite } = useFavoriteStore(); + const { getCachedFavoritesByObjectId, localFriendFavGroupLength } = useFavoriteStore(); const friendImportDialog = ref({ loading: false, diff --git a/src/views/Favorites/dialogs/WorldImportDialog.vue b/src/views/Favorites/dialogs/WorldImportDialog.vue index a4d4ae22..c43ccf87 100644 --- a/src/views/Favorites/dialogs/WorldImportDialog.vue +++ b/src/views/Favorites/dialogs/WorldImportDialog.vue @@ -127,6 +127,7 @@ import { useI18n } from 'vue-i18n'; import { useFavoriteStore, useGalleryStore, useUserStore, useWorldStore } from '../../../stores'; + import { addLocalWorldFavorite } from '../../../coordinators/favoriteCoordinator'; import { favoriteRequest, worldRequest } from '../../../api'; import { createColumns } from './worldImportColumns.jsx'; import { removeFromArray } from '../../../shared/utils'; @@ -135,7 +136,7 @@ const { showUserDialog } = useUserStore(); const { favoriteWorldGroups, worldImportDialogInput, worldImportDialogVisible, localWorldFavoriteGroups } = storeToRefs(useFavoriteStore()); - const { localWorldFavGroupLength, addLocalWorldFavorite, getCachedFavoritesByObjectId } = useFavoriteStore(); + const { localWorldFavGroupLength, getCachedFavoritesByObjectId } = useFavoriteStore(); const { showWorldDialog } = useWorldStore(); const { showFullscreenImageDialog } = useGalleryStore(); diff --git a/src/views/Moderation/Moderation.vue b/src/views/Moderation/Moderation.vue index a7951790..7b5695fb 100644 --- a/src/views/Moderation/Moderation.vue +++ b/src/views/Moderation/Moderation.vue @@ -58,6 +58,7 @@ import { useI18n } from 'vue-i18n'; import { useAppearanceSettingsStore, useModalStore, useModerationStore, useVrcxStore } from '../../stores'; + import { runRefreshPlayerModerationsFlow as refreshPlayerModerations } from '../../coordinators/moderationCoordinator'; import { DataTableLayout } from '../../components/ui/data-table'; import { createColumns } from './columns.jsx'; import { moderationTypes } from '../../shared/constants'; @@ -69,7 +70,7 @@ const { t } = useI18n(); const { playerModerationTable } = storeToRefs(useModerationStore()); - const { refreshPlayerModerations, handlePlayerModerationDelete } = useModerationStore(); + const { handlePlayerModerationDelete } = useModerationStore(); const appearanceSettingsStore = useAppearanceSettingsStore(); const vrcxStore = useVrcxStore(); const modalStore = useModalStore(); diff --git a/src/views/Moderation/__tests__/Moderation.test.js b/src/views/Moderation/__tests__/Moderation.test.js index 4034e50c..a59ea419 100644 --- a/src/views/Moderation/__tests__/Moderation.test.js +++ b/src/views/Moderation/__tests__/Moderation.test.js @@ -25,9 +25,13 @@ mocks.pagination = mocks.makeRef({ pageSize: 10 }); -vi.mock('pinia', () => ({ - storeToRefs: (store) => store -})); +vi.mock('pinia', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + storeToRefs: (store) => store + }; +}); vi.mock('vue-i18n', () => ({ useI18n: () => ({ @@ -68,10 +72,18 @@ vi.mock('../../../api', () => ({ } })); -vi.mock('../../../shared/constants', () => ({ - moderationTypes: ['block', 'mute', 'unmute'] +vi.mock('../../../coordinators/moderationCoordinator', () => ({ + runRefreshPlayerModerationsFlow: (...args) => mocks.refreshPlayerModerations(...args) })); +vi.mock('../../../shared/constants', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + moderationTypes: ['block', 'mute', 'unmute'] + }; +}); + vi.mock('../columns.jsx', () => ({ createColumns: (handlers) => { mocks.columnHandlers = handlers; diff --git a/src/views/Settings/components/Tabs/AppearanceTab.vue b/src/views/Settings/components/Tabs/AppearanceTab.vue index 156d4d67..8829f7df 100644 --- a/src/views/Settings/components/Tabs/AppearanceTab.vue +++ b/src/views/Settings/components/Tabs/AppearanceTab.vue @@ -365,6 +365,7 @@ import PresetColorPicker from '@/components/PresetColorPicker.vue'; import TableLimitsDialog from '@/components/dialogs/TableLimitsDialog.vue'; + import { saveSortFavoritesOption } from '@/coordinators/favoriteCoordinator'; import SimpleSwitch from '../SimpleSwitch.vue'; @@ -398,7 +399,6 @@ const appLanguageDisplayName = computed(() => getLanguageName(String(appLanguage.value))); - const { saveSortFavoritesOption } = useFavoriteStore(); const { setDisplayVRCPlusIconsAsAvatar, diff --git a/src/views/Settings/dialogs/VRChatConfigDialog.vue b/src/views/Settings/dialogs/VRChatConfigDialog.vue index a8a21750..bec8fa2a 100644 --- a/src/views/Settings/dialogs/VRChatConfigDialog.vue +++ b/src/views/Settings/dialogs/VRChatConfigDialog.vue @@ -184,11 +184,12 @@ import { useI18n } from 'vue-i18n'; import { useAdvancedSettingsStore, useGameStore, useModalStore } from '../../../stores'; + import { runSweepVRChatCacheFlow as sweepVRChatCache } from '../../../coordinators/gameCoordinator'; import { VRChatCameraResolutions, VRChatScreenshotResolutions } from '../../../shared/constants'; import { getVRChatResolution, openExternalLink } from '../../../shared/utils'; const { VRChatUsedCacheSize, VRChatTotalCacheSize, VRChatCacheSizeLoading } = storeToRefs(useGameStore()); - const { sweepVRChatCache, getVRChatCacheSize } = useGameStore(); + const { getVRChatCacheSize } = useGameStore(); const { folderSelectorDialog } = useAdvancedSettingsStore(); const { isVRChatConfigDialogVisible } = storeToRefs(useAdvancedSettingsStore()); const modalStore = useModalStore(); diff --git a/src/views/Settings/dialogs/__tests__/VRChatConfigDialog.test.js b/src/views/Settings/dialogs/__tests__/VRChatConfigDialog.test.js index 6de65b5b..06fab709 100644 --- a/src/views/Settings/dialogs/__tests__/VRChatConfigDialog.test.js +++ b/src/views/Settings/dialogs/__tests__/VRChatConfigDialog.test.js @@ -59,7 +59,6 @@ vi.mock('../../../../stores', () => ({ VRChatUsedCacheSize, VRChatTotalCacheSize, VRChatCacheSizeLoading, - sweepVRChatCache: mocks.sweepVRChatCache, getVRChatCacheSize: mocks.getVRChatCacheSize }), useAdvancedSettingsStore: () => ({ @@ -97,6 +96,10 @@ vi.mock('vue-sonner', () => ({ toast: mocks.toast })); +vi.mock('../../../../coordinators/gameCoordinator', () => ({ + runSweepVRChatCacheFlow: (...args) => mocks.sweepVRChatCache(...args) +})); + // Set global mocks for CefSharp-injected APIs globalThis.AppApi = mocks.appApi; globalThis.AssetBundleManager = mocks.assetBundleManager;