mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-19 14:53:50 +02:00
refactor untils
This commit is contained in:
@@ -600,14 +600,18 @@
|
|||||||
import { avatarRequest } from '../../../api';
|
import { avatarRequest } from '../../../api';
|
||||||
import { database } from '../../../services/database';
|
import { database } from '../../../services/database';
|
||||||
import { formatJsonVars } from '../../../shared/utils/base/ui';
|
import { formatJsonVars } from '../../../shared/utils/base/ui';
|
||||||
import { handleImageUploadInput } from '../../../shared/utils/imageUpload';
|
import { handleImageUploadInput } from '../../../coordinators/imageUploadCoordinator';
|
||||||
import { runDeleteVRChatCacheFlow as deleteVRChatCache } from '../../../coordinators/gameCoordinator';
|
import { runDeleteVRChatCacheFlow as deleteVRChatCache } from '../../../coordinators/gameCoordinator';
|
||||||
import { showAvatarDialog, applyAvatar, selectAvatarWithoutConfirmation } from '../../../coordinators/avatarCoordinator';
|
import {
|
||||||
|
showAvatarDialog,
|
||||||
|
applyAvatar,
|
||||||
|
selectAvatarWithoutConfirmation
|
||||||
|
} from '../../../coordinators/avatarCoordinator';
|
||||||
import { useAvatarDialogCommands } from './useAvatarDialogCommands';
|
import { useAvatarDialogCommands } from './useAvatarDialogCommands';
|
||||||
|
|
||||||
import DialogJsonTab from '../DialogJsonTab.vue';
|
import DialogJsonTab from '../DialogJsonTab.vue';
|
||||||
import ImageCropDialog from '../ImageCropDialog.vue';
|
import ImageCropDialog from '../ImageCropDialog.vue';
|
||||||
import { showUserDialog } from '../../../coordinators/userCoordinator';
|
import { showUserDialog } from '../../../coordinators/userCoordinator';
|
||||||
|
|
||||||
const SetAvatarStylesDialog = defineAsyncComponent(() => import('./SetAvatarStylesDialog.vue'));
|
const SetAvatarStylesDialog = defineAsyncComponent(() => import('./SetAvatarStylesDialog.vue'));
|
||||||
const SetAvatarTagsDialog = defineAsyncComponent(() => import('./SetAvatarTagsDialog.vue'));
|
const SetAvatarTagsDialog = defineAsyncComponent(() => import('./SetAvatarTagsDialog.vue'));
|
||||||
@@ -617,8 +621,7 @@ import { showUserDialog } from '../../../coordinators/userCoordinator';
|
|||||||
const avatarStore = useAvatarStore();
|
const avatarStore = useAvatarStore();
|
||||||
const { cachedAvatarModerations, cachedAvatars } = avatarStore;
|
const { cachedAvatarModerations, cachedAvatars } = avatarStore;
|
||||||
const { avatarDialog } = storeToRefs(avatarStore);
|
const { avatarDialog } = storeToRefs(avatarStore);
|
||||||
const { getAvatarGallery, applyAvatarModeration } =
|
const { getAvatarGallery, applyAvatarModeration } = avatarStore;
|
||||||
avatarStore;
|
|
||||||
const { showFavoriteDialog } = useFavoriteStore();
|
const { showFavoriteDialog } = useFavoriteStore();
|
||||||
const { isGameRunning } = storeToRefs(useGameStore());
|
const { isGameRunning } = storeToRefs(useGameStore());
|
||||||
const { showFullscreenImageDialog } = useGalleryStore();
|
const { showFullscreenImageDialog } = useGalleryStore();
|
||||||
|
|||||||
@@ -30,11 +30,14 @@ vi.mock('../../../../shared/utils', () => ({
|
|||||||
vi.mock('../../../../shared/utils/imageUpload', () => ({
|
vi.mock('../../../../shared/utils/imageUpload', () => ({
|
||||||
handleImageUploadInput: vi.fn(),
|
handleImageUploadInput: vi.fn(),
|
||||||
readFileAsBase64: vi.fn(),
|
readFileAsBase64: vi.fn(),
|
||||||
resizeImageToFitLimits: vi.fn(),
|
|
||||||
uploadImageLegacy: vi.fn(),
|
|
||||||
withUploadTimeout: vi.fn()
|
withUploadTimeout: vi.fn()
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../../coordinators/imageUploadCoordinator', () => ({
|
||||||
|
resizeImageToFitLimits: vi.fn(),
|
||||||
|
uploadImageLegacy: vi.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
const { copyToClipboard, openExternalLink } =
|
const { copyToClipboard, openExternalLink } =
|
||||||
await import('../../../../shared/utils');
|
await import('../../../../shared/utils');
|
||||||
const { favoriteRequest, avatarRequest, avatarModerationRequest } =
|
const { favoriteRequest, avatarRequest, avatarModerationRequest } =
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
avatarModerationRequest,
|
avatarModerationRequest,
|
||||||
avatarRequest,
|
avatarRequest,
|
||||||
@@ -11,18 +12,32 @@ import {
|
|||||||
} from '../../../shared/utils';
|
} from '../../../shared/utils';
|
||||||
import {
|
import {
|
||||||
handleImageUploadInput,
|
handleImageUploadInput,
|
||||||
readFileAsBase64,
|
|
||||||
resizeImageToFitLimits,
|
resizeImageToFitLimits,
|
||||||
uploadImageLegacy,
|
uploadImageLegacy
|
||||||
|
} from '../../../coordinators/imageUploadCoordinator';
|
||||||
|
import {
|
||||||
|
readFileAsBase64,
|
||||||
withUploadTimeout
|
withUploadTimeout
|
||||||
} from '../../../shared/utils/imageUpload';
|
} from '../../../shared/utils/imageUpload';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Composable for AvatarDialog command dispatch.
|
* Composable for AvatarDialog command dispatch.
|
||||||
* Uses a command map pattern instead of nested switch-case chains.
|
* Uses a command map pattern instead of nested switch-case chains.
|
||||||
*
|
|
||||||
* @param {import('vue').Ref} avatarDialog - reactive ref to the avatar dialog state
|
* @param {import('vue').Ref} avatarDialog - reactive ref to the avatar dialog state
|
||||||
* @param {object} deps - external dependencies
|
* @param {object} deps - external dependencies
|
||||||
|
* @param deps.t
|
||||||
|
* @param deps.toast
|
||||||
|
* @param deps.modalStore
|
||||||
|
* @param deps.userDialog
|
||||||
|
* @param deps.currentUser
|
||||||
|
* @param deps.cachedAvatars
|
||||||
|
* @param deps.cachedAvatarModerations
|
||||||
|
* @param deps.showAvatarDialog
|
||||||
|
* @param deps.showFavoriteDialog
|
||||||
|
* @param deps.applyAvatarModeration
|
||||||
|
* @param deps.applyAvatar
|
||||||
|
* @param deps.sortUserDialogAvatars
|
||||||
|
* @param deps.uiStore
|
||||||
* @returns {object} command composable API
|
* @returns {object} command composable API
|
||||||
*/
|
*/
|
||||||
export function useAvatarDialogCommands(
|
export function useAvatarDialogCommands(
|
||||||
@@ -205,6 +220,9 @@ export function useAvatarDialogCommands(
|
|||||||
// String commands: delegate to component callback
|
// String commands: delegate to component callback
|
||||||
// Confirmed commands: { confirm: () => ({title, description, ...}), handler: fn }
|
// Confirmed commands: { confirm: () => ({title, description, ...}), handler: fn }
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
function buildCommandMap() {
|
function buildCommandMap() {
|
||||||
const D = () => avatarDialog.value;
|
const D = () => avatarDialog.value;
|
||||||
|
|
||||||
|
|||||||
@@ -352,16 +352,16 @@
|
|||||||
getFaviconUrl,
|
getFaviconUrl,
|
||||||
hasGroupPermission,
|
hasGroupPermission,
|
||||||
openExternalLink,
|
openExternalLink,
|
||||||
refreshInstancePlayerCount,
|
|
||||||
userImage,
|
userImage,
|
||||||
userStatusClass
|
userStatusClass
|
||||||
} from '../../../shared/utils';
|
} from '../../../shared/utils';
|
||||||
|
import { refreshInstancePlayerCount } from '../../../coordinators/instanceCoordinator';
|
||||||
import { useGalleryStore, useGroupStore, useInstanceStore, useLocationStore, useUserStore } from '../../../stores';
|
import { useGalleryStore, useGroupStore, useInstanceStore, useLocationStore, useUserStore } from '../../../stores';
|
||||||
import { useGroupCalendarEvents } from './useGroupCalendarEvents';
|
import { useGroupCalendarEvents } from './useGroupCalendarEvents';
|
||||||
|
|
||||||
import GroupCalendarEventCard from '../../../views/Tools/components/GroupCalendarEventCard.vue';
|
import GroupCalendarEventCard from '../../../views/Tools/components/GroupCalendarEventCard.vue';
|
||||||
import InstanceActionBar from '../../InstanceActionBar.vue';
|
import InstanceActionBar from '../../InstanceActionBar.vue';
|
||||||
import { showUserDialog } from '../../../coordinators/userCoordinator';
|
import { showUserDialog } from '../../../coordinators/userCoordinator';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
showGroupPostEditDialog: {
|
showGroupPostEditDialog: {
|
||||||
@@ -376,7 +376,6 @@ import { showUserDialog } from '../../../coordinators/userCoordinator';
|
|||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
|
||||||
const { groupDialog } = storeToRefs(useGroupStore());
|
const { groupDialog } = storeToRefs(useGroupStore());
|
||||||
const { lastLocation } = storeToRefs(useLocationStore());
|
const { lastLocation } = storeToRefs(useLocationStore());
|
||||||
const { showFullscreenImageDialog } = useGalleryStore();
|
const { showFullscreenImageDialog } = useGalleryStore();
|
||||||
|
|||||||
@@ -54,7 +54,8 @@
|
|||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
import { miscRequest, userRequest } from '../../../api';
|
import { miscRequest, userRequest } from '../../../api';
|
||||||
import { replaceBioSymbols, saveUserMemo } from '../../../shared/utils';
|
import { replaceBioSymbols } from '../../../shared/utils';
|
||||||
|
import { saveUserMemo } from '../../../coordinators/memoCoordinator';
|
||||||
import { useAppearanceSettingsStore, useUserStore } from '../../../stores';
|
import { useAppearanceSettingsStore, useUserStore } from '../../../stores';
|
||||||
|
|
||||||
const { userDialog } = storeToRefs(useUserStore());
|
const { userDialog } = storeToRefs(useUserStore());
|
||||||
|
|||||||
@@ -483,13 +483,13 @@
|
|||||||
isFriendOnline,
|
isFriendOnline,
|
||||||
isRealInstance,
|
isRealInstance,
|
||||||
openExternalLink,
|
openExternalLink,
|
||||||
refreshInstancePlayerCount,
|
|
||||||
timeToText,
|
timeToText,
|
||||||
userImage,
|
userImage,
|
||||||
userOnlineFor,
|
userOnlineFor,
|
||||||
userOnlineForTimestamp,
|
userOnlineForTimestamp,
|
||||||
userStatusClass
|
userStatusClass
|
||||||
} from '../../../shared/utils';
|
} from '../../../shared/utils';
|
||||||
|
import { refreshInstancePlayerCount } from '../../../coordinators/instanceCoordinator';
|
||||||
import {
|
import {
|
||||||
useAdvancedSettingsStore,
|
useAdvancedSettingsStore,
|
||||||
useAppearanceSettingsStore,
|
useAppearanceSettingsStore,
|
||||||
@@ -505,7 +505,7 @@
|
|||||||
import { queryRequest, userRequest } from '../../../api';
|
import { queryRequest, userRequest } from '../../../api';
|
||||||
|
|
||||||
import InstanceActionBar from '../../InstanceActionBar.vue';
|
import InstanceActionBar from '../../InstanceActionBar.vue';
|
||||||
import { showUserDialog } from '../../../coordinators/userCoordinator';
|
import { showUserDialog } from '../../../coordinators/userCoordinator';
|
||||||
|
|
||||||
const EditNoteAndMemoDialog = defineAsyncComponent(() => import('./EditNoteAndMemoDialog.vue'));
|
const EditNoteAndMemoDialog = defineAsyncComponent(() => import('./EditNoteAndMemoDialog.vue'));
|
||||||
|
|
||||||
|
|||||||
@@ -108,7 +108,7 @@
|
|||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
import { refreshInstancePlayerCount } from '../../../shared/utils';
|
import { refreshInstancePlayerCount } from '../../../coordinators/instanceCoordinator';
|
||||||
import { useUserDisplay } from '../../../composables/useUserDisplay';
|
import { useUserDisplay } from '../../../composables/useUserDisplay';
|
||||||
import {
|
import {
|
||||||
useAppearanceSettingsStore,
|
useAppearanceSettingsStore,
|
||||||
@@ -125,7 +125,7 @@
|
|||||||
const { userImage, userStatusClass } = useUserDisplay();
|
const { userImage, userStatusClass } = useUserDisplay();
|
||||||
|
|
||||||
const { isAgeGatedInstancesVisible } = storeToRefs(useAppearanceSettingsStore());
|
const { isAgeGatedInstancesVisible } = storeToRefs(useAppearanceSettingsStore());
|
||||||
|
|
||||||
const { currentUser } = storeToRefs(useUserStore());
|
const { currentUser } = storeToRefs(useUserStore());
|
||||||
const { worldDialog } = storeToRefs(useWorldStore());
|
const { worldDialog } = storeToRefs(useWorldStore());
|
||||||
const { lastLocation } = storeToRefs(useLocationStore());
|
const { lastLocation } = storeToRefs(useLocationStore());
|
||||||
|
|||||||
@@ -28,11 +28,14 @@ vi.mock('../../../../shared/utils', () => ({
|
|||||||
vi.mock('../../../../shared/utils/imageUpload', () => ({
|
vi.mock('../../../../shared/utils/imageUpload', () => ({
|
||||||
handleImageUploadInput: vi.fn(),
|
handleImageUploadInput: vi.fn(),
|
||||||
readFileAsBase64: vi.fn(),
|
readFileAsBase64: vi.fn(),
|
||||||
resizeImageToFitLimits: vi.fn(),
|
|
||||||
uploadImageLegacy: vi.fn(),
|
|
||||||
withUploadTimeout: vi.fn((p) => p)
|
withUploadTimeout: vi.fn((p) => p)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../../coordinators/imageUploadCoordinator', () => ({
|
||||||
|
resizeImageToFitLimits: vi.fn(),
|
||||||
|
uploadImageLegacy: vi.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
const { favoriteRequest, miscRequest, userRequest, worldRequest } =
|
const { favoriteRequest, miscRequest, userRequest, worldRequest } =
|
||||||
await import('../../../../api');
|
await import('../../../../api');
|
||||||
const { openExternalLink } = await import('../../../../shared/utils');
|
const { openExternalLink } = await import('../../../../shared/utils');
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
import { nextTick, ref } from 'vue';
|
import { nextTick, ref } from 'vue';
|
||||||
|
|
||||||
import {
|
|
||||||
handleImageUploadInput,
|
|
||||||
readFileAsBase64,
|
|
||||||
resizeImageToFitLimits,
|
|
||||||
uploadImageLegacy,
|
|
||||||
withUploadTimeout
|
|
||||||
} from '../../../shared/utils/imageUpload';
|
|
||||||
import {
|
import {
|
||||||
favoriteRequest,
|
favoriteRequest,
|
||||||
miscRequest,
|
miscRequest,
|
||||||
userRequest,
|
userRequest,
|
||||||
worldRequest
|
worldRequest
|
||||||
} from '../../../api';
|
} from '../../../api';
|
||||||
|
import {
|
||||||
|
handleImageUploadInput,
|
||||||
|
resizeImageToFitLimits,
|
||||||
|
uploadImageLegacy
|
||||||
|
} from '../../../coordinators/imageUploadCoordinator';
|
||||||
import { openExternalLink, replaceVrcPackageUrl } from '../../../shared/utils';
|
import { openExternalLink, replaceVrcPackageUrl } from '../../../shared/utils';
|
||||||
|
import {
|
||||||
|
readFileAsBase64,
|
||||||
|
withUploadTimeout
|
||||||
|
} from '../../../shared/utils/imageUpload';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Composable for WorldDialog commands, prompt functions, and image upload.
|
* Composable for WorldDialog commands, prompt functions, and image upload.
|
||||||
@@ -370,6 +372,9 @@ export function useWorldDialogCommands(
|
|||||||
// String commands: delegate to component callback
|
// String commands: delegate to component callback
|
||||||
// Confirmed commands: { confirm: () => ({title, description, ...}), handler: fn }
|
// Confirmed commands: { confirm: () => ({title, description, ...}), handler: fn }
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
function buildCommandMap() {
|
function buildCommandMap() {
|
||||||
const D = () => worldDialog.value;
|
const D = () => worldDialog.value;
|
||||||
|
|
||||||
|
|||||||
194
src/coordinators/cacheCoordinator.js
Normal file
194
src/coordinators/cacheCoordinator.js
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { toast } from 'vue-sonner';
|
||||||
|
|
||||||
|
import {
|
||||||
|
useAuthStore,
|
||||||
|
useAvatarStore,
|
||||||
|
useInstanceStore,
|
||||||
|
useWorldStore
|
||||||
|
} from '../stores';
|
||||||
|
import {
|
||||||
|
extractFileId,
|
||||||
|
extractFileVersion,
|
||||||
|
extractVariantVersion
|
||||||
|
} from '../shared/utils/fileUtils';
|
||||||
|
import { compareUnityVersion } from '../shared/utils/avatar';
|
||||||
|
import { queryRequest } from '../api';
|
||||||
|
|
||||||
|
async function deleteVRChatCache(ref) {
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const sdkUnityVersion = authStore.cachedConfig.sdkUnityVersion;
|
||||||
|
let assetUrl = '';
|
||||||
|
let variant = '';
|
||||||
|
for (let i = ref.unityPackages.length - 1; i > -1; i--) {
|
||||||
|
const unityPackage = ref.unityPackages[i];
|
||||||
|
if (
|
||||||
|
unityPackage.variant &&
|
||||||
|
unityPackage.variant !== 'standard' &&
|
||||||
|
unityPackage.variant !== 'security'
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
unityPackage.platform === 'standalonewindows' &&
|
||||||
|
compareUnityVersion(unityPackage.unitySortNumber, sdkUnityVersion)
|
||||||
|
) {
|
||||||
|
assetUrl = unityPackage.assetUrl;
|
||||||
|
if (!unityPackage.variant || unityPackage.variant === 'standard') {
|
||||||
|
variant = 'security';
|
||||||
|
} else {
|
||||||
|
variant = unityPackage.variant;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const id = extractFileId(assetUrl);
|
||||||
|
const version = parseInt(extractFileVersion(assetUrl), 10);
|
||||||
|
const variantVersion = parseInt(extractVariantVersion(assetUrl), 10);
|
||||||
|
await AssetBundleManager.DeleteCache(id, version, variant, variantVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {object} ref
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async function checkVRChatCache(ref) {
|
||||||
|
if (!ref.unityPackages) {
|
||||||
|
return { Item1: -1, Item2: false, Item3: '' };
|
||||||
|
}
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const sdkUnityVersion = authStore.cachedConfig.sdkUnityVersion;
|
||||||
|
let assetUrl = '';
|
||||||
|
let variant = '';
|
||||||
|
for (let i = ref.unityPackages.length - 1; i > -1; i--) {
|
||||||
|
const unityPackage = ref.unityPackages[i];
|
||||||
|
if (unityPackage.variant && unityPackage.variant !== 'security') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
unityPackage.platform === 'standalonewindows' &&
|
||||||
|
compareUnityVersion(unityPackage.unitySortNumber, sdkUnityVersion)
|
||||||
|
) {
|
||||||
|
assetUrl = unityPackage.assetUrl;
|
||||||
|
if (!unityPackage.variant || unityPackage.variant === 'standard') {
|
||||||
|
variant = 'security';
|
||||||
|
} else {
|
||||||
|
variant = unityPackage.variant;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!assetUrl) {
|
||||||
|
assetUrl = ref.assetUrl;
|
||||||
|
}
|
||||||
|
const id = extractFileId(assetUrl);
|
||||||
|
const version = parseInt(extractFileVersion(assetUrl), 10);
|
||||||
|
const variantVersion = parseInt(extractVariantVersion(assetUrl), 10);
|
||||||
|
if (!id || !version) {
|
||||||
|
return { Item1: -1, Item2: false, Item3: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return AssetBundleManager.CheckVRChatCache(
|
||||||
|
id,
|
||||||
|
version,
|
||||||
|
variant,
|
||||||
|
variantVersion
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed reading VRChat cache size:', err);
|
||||||
|
toast.error(`Failed reading VRChat cache size: ${err}`);
|
||||||
|
return { Item1: -1, Item2: false, Item3: '' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {object} ref
|
||||||
|
* @returns {Promise<object>}
|
||||||
|
*/
|
||||||
|
async function getBundleDateSize(ref) {
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const sdkUnityVersion = authStore.cachedConfig.sdkUnityVersion;
|
||||||
|
const avatarStore = useAvatarStore();
|
||||||
|
const { avatarDialog } = storeToRefs(avatarStore);
|
||||||
|
const worldStore = useWorldStore();
|
||||||
|
const { worldDialog } = storeToRefs(worldStore);
|
||||||
|
const instanceStore = useInstanceStore();
|
||||||
|
const { currentInstanceWorld, currentInstanceLocation } =
|
||||||
|
storeToRefs(instanceStore);
|
||||||
|
const bundleJson = {};
|
||||||
|
for (let i = ref.unityPackages.length - 1; i > -1; i--) {
|
||||||
|
const unityPackage = ref.unityPackages[i];
|
||||||
|
if (!unityPackage) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
unityPackage.variant &&
|
||||||
|
unityPackage.variant !== 'standard' &&
|
||||||
|
unityPackage.variant !== 'security'
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!compareUnityVersion(unityPackage.unitySortNumber, sdkUnityVersion)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const platform = unityPackage.platform;
|
||||||
|
if (bundleJson[platform]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const assetUrl = unityPackage.assetUrl;
|
||||||
|
const fileId = extractFileId(assetUrl);
|
||||||
|
const version = parseInt(extractFileVersion(assetUrl), 10);
|
||||||
|
let variant = '';
|
||||||
|
if (!unityPackage.variant || unityPackage.variant === 'standard') {
|
||||||
|
variant = 'security';
|
||||||
|
} else {
|
||||||
|
variant = unityPackage.variant;
|
||||||
|
}
|
||||||
|
if (!fileId || !version) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const args = await queryRequest.fetch('fileAnalysis', {
|
||||||
|
fileId,
|
||||||
|
version,
|
||||||
|
variant
|
||||||
|
});
|
||||||
|
if (!args?.json?.success) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = args.json;
|
||||||
|
if (typeof json.fileSize !== 'undefined') {
|
||||||
|
json._fileSize = `${(json.fileSize / 1048576).toFixed(2)} MB`;
|
||||||
|
}
|
||||||
|
if (typeof json.uncompressedSize !== 'undefined') {
|
||||||
|
json._uncompressedSize = `${(json.uncompressedSize / 1048576).toFixed(2)} MB`;
|
||||||
|
}
|
||||||
|
if (typeof json.avatarStats?.totalTextureUsage !== 'undefined') {
|
||||||
|
json._totalTextureUsage = `${(json.avatarStats.totalTextureUsage / 1048576).toFixed(2)} MB`;
|
||||||
|
}
|
||||||
|
bundleJson[platform] = json;
|
||||||
|
|
||||||
|
if (avatarDialog.value.id === ref.id) {
|
||||||
|
// update avatar dialog
|
||||||
|
avatarDialog.value.fileAnalysis[platform] = json;
|
||||||
|
}
|
||||||
|
// update world dialog
|
||||||
|
if (worldDialog.value.id === ref.id) {
|
||||||
|
worldDialog.value.fileAnalysis[platform] = json;
|
||||||
|
}
|
||||||
|
// update player list
|
||||||
|
if (currentInstanceLocation.value.worldId === ref.id) {
|
||||||
|
currentInstanceWorld.value.fileAnalysis[platform] = json;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bundleJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { deleteVRChatCache, checkVRChatCache, getBundleDateSize };
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useAppearanceSettingsStore } from '../../../stores';
|
import { useAppearanceSettingsStore } from '../stores';
|
||||||
|
|
||||||
function padZero(num) {
|
function padZero(num) {
|
||||||
return String(num).padStart(2, '0');
|
return String(num).padStart(2, '0');
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
|
|
||||||
import { AppDebug } from '../services/appConfig';
|
import { AppDebug } from '../services/appConfig';
|
||||||
import { migrateMemos } from '../shared/utils';
|
import { migrateMemos } from './memoCoordinator';
|
||||||
import { reconnectWebSocket } from '../services/websocket';
|
import { reconnectWebSocket } from '../services/websocket';
|
||||||
import { useAuthStore } from '../stores/auth';
|
import { useAuthStore } from '../stores/auth';
|
||||||
import { useFriendStore } from '../stores/friend';
|
import { useFriendStore } from '../stores/friend';
|
||||||
|
|||||||
198
src/coordinators/imageUploadCoordinator.js
Normal file
198
src/coordinators/imageUploadCoordinator.js
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
import { toast } from 'vue-sonner';
|
||||||
|
|
||||||
|
import { $throw } from '../services/request';
|
||||||
|
import { AppDebug } from '../services/appConfig.js';
|
||||||
|
import { extractFileId } from '../shared/utils';
|
||||||
|
import { imageRequest } from '../api';
|
||||||
|
|
||||||
|
function resolveMessage(message) {
|
||||||
|
if (typeof message === 'function') {
|
||||||
|
return message();
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInputElement(selector) {
|
||||||
|
if (!selector) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (typeof selector === 'function') {
|
||||||
|
return selector();
|
||||||
|
}
|
||||||
|
if (typeof selector === 'string') {
|
||||||
|
return document.querySelector(selector);
|
||||||
|
}
|
||||||
|
return selector;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleImageUploadInput(event, options = {}) {
|
||||||
|
const {
|
||||||
|
inputSelector,
|
||||||
|
// 20MB
|
||||||
|
maxSize = 20000000,
|
||||||
|
acceptPattern = /image.*/,
|
||||||
|
tooLargeMessage,
|
||||||
|
invalidTypeMessage,
|
||||||
|
onClear
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const clearInput = () => {
|
||||||
|
onClear?.();
|
||||||
|
const input = getInputElement(inputSelector);
|
||||||
|
if (input) {
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const files = event?.target?.files || event?.dataTransfer?.files;
|
||||||
|
if (!files || files.length === 0) {
|
||||||
|
clearInput();
|
||||||
|
return { file: null, clearInput };
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = files[0];
|
||||||
|
if (file.size >= maxSize) {
|
||||||
|
if (tooLargeMessage) {
|
||||||
|
toast.error(resolveMessage(tooLargeMessage));
|
||||||
|
}
|
||||||
|
clearInput();
|
||||||
|
return { file: null, clearInput };
|
||||||
|
}
|
||||||
|
|
||||||
|
let acceptRegex = null;
|
||||||
|
if (acceptPattern) {
|
||||||
|
acceptRegex =
|
||||||
|
acceptPattern instanceof RegExp
|
||||||
|
? acceptPattern
|
||||||
|
: new RegExp(acceptPattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (acceptRegex && !acceptRegex.test(file.type)) {
|
||||||
|
if (invalidTypeMessage) {
|
||||||
|
toast.error(resolveMessage(invalidTypeMessage));
|
||||||
|
}
|
||||||
|
clearInput();
|
||||||
|
return { file: null, clearInput };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { file, clearInput };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} base64Data - base64 encoded image
|
||||||
|
* @returns {Promise<string>} resized base64 encoded image
|
||||||
|
*/
|
||||||
|
export async function resizeImageToFitLimits(base64Data) {
|
||||||
|
// frontend limit check = 20MB
|
||||||
|
return AppApi.ResizeImageToFitLimits(base64Data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload image through AWS
|
||||||
|
* @param {'avatar'|'world'} type
|
||||||
|
* @param {object} opts
|
||||||
|
* @param {string} opts.entityId - avatar or world id
|
||||||
|
* @param {string} opts.imageUrl - current imageUrl on the entity
|
||||||
|
* @param {string} opts.base64File - base64 encoded image data
|
||||||
|
* @param {Blob} opts.blob - the original blob (used for file size)
|
||||||
|
*/
|
||||||
|
export async function uploadImageLegacy(
|
||||||
|
type,
|
||||||
|
{ entityId, imageUrl, base64File, blob }
|
||||||
|
) {
|
||||||
|
const apiMap = {
|
||||||
|
avatar: {
|
||||||
|
uploadImage: imageRequest.uploadAvatarImage,
|
||||||
|
fileStart: imageRequest.uploadAvatarImageFileStart,
|
||||||
|
fileFinish: imageRequest.uploadAvatarImageFileFinish,
|
||||||
|
sigStart: imageRequest.uploadAvatarImageSigStart,
|
||||||
|
sigFinish: imageRequest.uploadAvatarImageSigFinish,
|
||||||
|
setImage: imageRequest.setAvatarImage
|
||||||
|
},
|
||||||
|
world: {
|
||||||
|
uploadImage: imageRequest.uploadWorldImage,
|
||||||
|
fileStart: imageRequest.uploadWorldImageFileStart,
|
||||||
|
fileFinish: imageRequest.uploadWorldImageFileFinish,
|
||||||
|
sigStart: imageRequest.uploadWorldImageSigStart,
|
||||||
|
sigFinish: imageRequest.uploadWorldImageSigFinish,
|
||||||
|
setImage: imageRequest.setWorldImage
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const api = apiMap[type];
|
||||||
|
|
||||||
|
const fileMd5 = await AppApi.MD5File(base64File);
|
||||||
|
const fileSizeInBytes = parseInt(blob.size, 10);
|
||||||
|
const base64SignatureFile = await AppApi.SignFile(base64File);
|
||||||
|
const signatureMd5 = await AppApi.MD5File(base64SignatureFile);
|
||||||
|
const signatureSizeInBytes = parseInt(
|
||||||
|
await AppApi.FileLength(base64SignatureFile),
|
||||||
|
10
|
||||||
|
);
|
||||||
|
const fileId = extractFileId(imageUrl);
|
||||||
|
|
||||||
|
// imageInit
|
||||||
|
const uploadRes = await api.uploadImage(
|
||||||
|
{ fileMd5, fileSizeInBytes, signatureMd5, signatureSizeInBytes },
|
||||||
|
fileId
|
||||||
|
);
|
||||||
|
const uploadedFileId = uploadRes.json.id;
|
||||||
|
const fileVersion =
|
||||||
|
uploadRes.json.versions[uploadRes.json.versions.length - 1].version;
|
||||||
|
|
||||||
|
// imageFileStart
|
||||||
|
const fileStartRes = await api.fileStart({
|
||||||
|
fileId: uploadedFileId,
|
||||||
|
fileVersion
|
||||||
|
});
|
||||||
|
|
||||||
|
// uploadImageFileAWS
|
||||||
|
const fileAwsRes = await webApiService.execute({
|
||||||
|
url: fileStartRes.json.url,
|
||||||
|
uploadFilePUT: true,
|
||||||
|
fileData: base64File,
|
||||||
|
fileMIME: 'image/png',
|
||||||
|
fileMD5: fileMd5
|
||||||
|
});
|
||||||
|
if (fileAwsRes.status !== 200) {
|
||||||
|
$throw(
|
||||||
|
fileAwsRes.status,
|
||||||
|
`${type} image upload failed`,
|
||||||
|
fileStartRes.json.url
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// imageFileFinish
|
||||||
|
await api.fileFinish({ fileId: uploadedFileId, fileVersion });
|
||||||
|
|
||||||
|
// imageSigStart
|
||||||
|
const sigStartRes = await api.sigStart({
|
||||||
|
fileId: uploadedFileId,
|
||||||
|
fileVersion
|
||||||
|
});
|
||||||
|
|
||||||
|
// uploadImageSigAWS
|
||||||
|
const sigAwsRes = await webApiService.execute({
|
||||||
|
url: sigStartRes.json.url,
|
||||||
|
uploadFilePUT: true,
|
||||||
|
fileData: base64SignatureFile,
|
||||||
|
fileMIME: 'application/x-rsync-signature',
|
||||||
|
fileMD5: signatureMd5
|
||||||
|
});
|
||||||
|
if (sigAwsRes.status !== 200) {
|
||||||
|
$throw(
|
||||||
|
sigAwsRes.status,
|
||||||
|
`${type} image upload failed`,
|
||||||
|
sigStartRes.json.url
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// imageSigFinish
|
||||||
|
await api.sigFinish({ fileId: uploadedFileId, fileVersion });
|
||||||
|
|
||||||
|
// imageSet
|
||||||
|
const newImageUrl = `${AppDebug.endpointDomain}/file/${uploadedFileId}/${fileVersion}/file`;
|
||||||
|
const setRes = await api.setImage({ id: entityId, imageUrl: newImageUrl });
|
||||||
|
if (setRes.json.imageUrl !== newImageUrl) {
|
||||||
|
$throw(0, `${type} image change failed`, newImageUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/coordinators/instanceCoordinator.js
Normal file
18
src/coordinators/instanceCoordinator.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { instanceRequest } from '../api';
|
||||||
|
import { parseLocation } from '../shared/utils/locationParser';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {object} instance
|
||||||
|
*/
|
||||||
|
function refreshInstancePlayerCount(instance) {
|
||||||
|
const L = parseLocation(instance);
|
||||||
|
if (L.isRealInstance) {
|
||||||
|
instanceRequest.getInstance({
|
||||||
|
worldId: L.worldId,
|
||||||
|
instanceId: L.instanceId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { refreshInstancePlayerCount };
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useFriendStore, useUserStore } from '../../stores';
|
import { useFriendStore, useUserStore } from '../stores';
|
||||||
import { database } from '../../services/database';
|
import { database } from '../services/database';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
@@ -10,12 +10,12 @@ import {
|
|||||||
evictMapCache,
|
evictMapCache,
|
||||||
extractFileId,
|
extractFileId,
|
||||||
findUserByDisplayName,
|
findUserByDisplayName,
|
||||||
getUserMemo,
|
|
||||||
getWorldName,
|
getWorldName,
|
||||||
isRealInstance,
|
isRealInstance,
|
||||||
parseLocation,
|
parseLocation,
|
||||||
sanitizeUserJson
|
sanitizeUserJson
|
||||||
} from '../shared/utils';
|
} from '../shared/utils';
|
||||||
|
import { getUserMemo } from './memoCoordinator';
|
||||||
import {
|
import {
|
||||||
avatarRequest,
|
avatarRequest,
|
||||||
instanceRequest,
|
instanceRequest,
|
||||||
@@ -73,7 +73,14 @@ export function applyUser(json) {
|
|||||||
const moderationStore = useModerationStore();
|
const moderationStore = useModerationStore();
|
||||||
const photonStore = usePhotonStore();
|
const photonStore = usePhotonStore();
|
||||||
|
|
||||||
const { currentUser, cachedUsers, currentTravelers, customUserTags, state, userDialog } = userStore;
|
const {
|
||||||
|
currentUser,
|
||||||
|
cachedUsers,
|
||||||
|
currentTravelers,
|
||||||
|
customUserTags,
|
||||||
|
state,
|
||||||
|
userDialog
|
||||||
|
} = userStore;
|
||||||
|
|
||||||
let ref = cachedUsers.get(json.id);
|
let ref = cachedUsers.get(json.id);
|
||||||
let hasPropChanged = false;
|
let hasPropChanged = false;
|
||||||
@@ -114,10 +121,8 @@ export function applyUser(json) {
|
|||||||
if (json.state !== 'online') {
|
if (json.state !== 'online') {
|
||||||
runUpdateFriendFlow(ref.id, json.state);
|
runUpdateFriendFlow(ref.id, json.state);
|
||||||
}
|
}
|
||||||
const {
|
const { hasPropChanged: _hasPropChanged, changedProps: _changedProps } =
|
||||||
hasPropChanged: _hasPropChanged,
|
diffObjectProps(ref, json, arraysMatch);
|
||||||
changedProps: _changedProps
|
|
||||||
} = diffObjectProps(ref, json, arraysMatch);
|
|
||||||
for (const prop in json) {
|
for (const prop in json) {
|
||||||
if (typeof json[prop] !== 'undefined') {
|
if (typeof json[prop] !== 'undefined') {
|
||||||
ref[prop] = json[prop];
|
ref[prop] = json[prop];
|
||||||
@@ -235,10 +240,7 @@ export function applyUser(json) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (hasPropChanged) {
|
if (hasPropChanged) {
|
||||||
if (
|
if (changedProps.location && changedProps.location[0] !== 'traveling') {
|
||||||
changedProps.location &&
|
|
||||||
changedProps.location[0] !== 'traveling'
|
|
||||||
) {
|
|
||||||
const ts = Date.now();
|
const ts = Date.now();
|
||||||
changedProps.location.push(ts - ref.$location_at);
|
changedProps.location.push(ts - ref.$location_at);
|
||||||
ref.$location_at = ts;
|
ref.$location_at = ts;
|
||||||
@@ -286,11 +288,7 @@ export function showUserDialog(userId) {
|
|||||||
const D = userDialog;
|
const D = userDialog;
|
||||||
D.visible = true;
|
D.visible = true;
|
||||||
if (isMainDialogOpen && D.id === userId) {
|
if (isMainDialogOpen && D.id === userId) {
|
||||||
uiStore.setDialogCrumbLabel(
|
uiStore.setDialogCrumbLabel('user', D.id, D.ref?.displayName || D.id);
|
||||||
'user',
|
|
||||||
D.id,
|
|
||||||
D.ref?.displayName || D.id
|
|
||||||
);
|
|
||||||
userStore.applyUserDialogLocation(true);
|
userStore.applyUserDialogLocation(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -429,8 +427,7 @@ export function showUserDialog(userId) {
|
|||||||
D.joinCount = ref1.joinCount;
|
D.joinCount = ref1.joinCount;
|
||||||
D.timeSpent = ref1.timeSpent;
|
D.timeSpent = ref1.timeSpent;
|
||||||
}
|
}
|
||||||
const displayNameMap =
|
const displayNameMap = ref1.previousDisplayNames;
|
||||||
ref1.previousDisplayNames;
|
|
||||||
const userNotifications =
|
const userNotifications =
|
||||||
await database.getFriendLogHistoryForUserId(
|
await database.getFriendLogHistoryForUserId(
|
||||||
D.id,
|
D.id,
|
||||||
@@ -457,12 +454,10 @@ export function showUserDialog(userId) {
|
|||||||
}
|
}
|
||||||
D.dateFriendedInfo = dateFriendedInfo;
|
D.dateFriendedInfo = dateFriendedInfo;
|
||||||
if (dateFriendedInfo.length > 0) {
|
if (dateFriendedInfo.length > 0) {
|
||||||
const latestFriendedInfo =
|
const latestFriendedInfo = dateFriendedInfo[0];
|
||||||
dateFriendedInfo[0];
|
|
||||||
D.unFriended =
|
D.unFriended =
|
||||||
latestFriendedInfo.type === 'Unfriend';
|
latestFriendedInfo.type === 'Unfriend';
|
||||||
D.dateFriended =
|
D.dateFriended = latestFriendedInfo.created_at;
|
||||||
latestFriendedInfo.created_at;
|
|
||||||
}
|
}
|
||||||
displayNameMap.forEach(
|
displayNameMap.forEach(
|
||||||
(updated_at, displayName) => {
|
(updated_at, displayName) => {
|
||||||
@@ -473,27 +468,24 @@ export function showUserDialog(userId) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
AppApi.GetVRChatUserModeration(
|
AppApi.GetVRChatUserModeration(currentUser.id, userId).then(
|
||||||
currentUser.id,
|
(result) => {
|
||||||
userId
|
D.avatarModeration = result;
|
||||||
).then((result) => {
|
if (result === 4) {
|
||||||
D.avatarModeration = result;
|
D.isHideAvatar = true;
|
||||||
if (result === 4) {
|
} else if (result === 5) {
|
||||||
D.isHideAvatar = true;
|
D.isShowAvatar = true;
|
||||||
} else if (result === 5) {
|
}
|
||||||
D.isShowAvatar = true;
|
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
if (!currentUser.hasSharedConnectionsOptOut) {
|
if (!currentUser.hasSharedConnectionsOptOut) {
|
||||||
try {
|
try {
|
||||||
queryRequest
|
queryRequest
|
||||||
.fetch('mutualCounts', { userId })
|
.fetch('mutualCounts', { userId })
|
||||||
.then((args) => {
|
.then((args) => {
|
||||||
if (args.params.userId === D.id) {
|
if (args.params.userId === D.id) {
|
||||||
D.mutualFriendCount =
|
D.mutualFriendCount = args.json.friends;
|
||||||
args.json.friends;
|
D.mutualGroupCount = args.json.groups;
|
||||||
D.mutualGroupCount =
|
|
||||||
args.json.groups;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -501,8 +493,7 @@ export function showUserDialog(userId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
D.previousDisplayNames =
|
D.previousDisplayNames = currentUser.pastDisplayNames;
|
||||||
currentUser.pastDisplayNames;
|
|
||||||
database
|
database
|
||||||
.getUserStats(D.ref, inCurrentWorld)
|
.getUserStats(D.ref, inCurrentWorld)
|
||||||
.then((ref1) => {
|
.then((ref1) => {
|
||||||
@@ -673,11 +664,8 @@ export function handleConfig(args) {
|
|||||||
if (typeof args.ref?.whiteListedAssetUrls !== 'object') {
|
if (typeof args.ref?.whiteListedAssetUrls !== 'object') {
|
||||||
console.error('Invalid config whiteListedAssetUrls');
|
console.error('Invalid config whiteListedAssetUrls');
|
||||||
}
|
}
|
||||||
AppApi.PopulateImageHosts(
|
AppApi.PopulateImageHosts(JSON.stringify(args.ref.whiteListedAssetUrls));
|
||||||
JSON.stringify(args.ref.whiteListedAssetUrls)
|
const languages = args.ref?.constants?.LANGUAGE?.SPOKEN_LANGUAGE_OPTIONS;
|
||||||
);
|
|
||||||
const languages =
|
|
||||||
args.ref?.constants?.LANGUAGE?.SPOKEN_LANGUAGE_OPTIONS;
|
|
||||||
if (!languages) {
|
if (!languages) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1047,10 +1035,7 @@ export function updateAutoStateChange() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const params = { status: newStatus };
|
const params = { status: newStatus };
|
||||||
if (
|
if (withCompany && generalSettingsStore.autoStateChangeCompanyDescEnabled) {
|
||||||
withCompany &&
|
|
||||||
generalSettingsStore.autoStateChangeCompanyDescEnabled
|
|
||||||
) {
|
|
||||||
params.statusDescription =
|
params.statusDescription =
|
||||||
generalSettingsStore.autoStateChangeCompanyDesc;
|
generalSettingsStore.autoStateChangeCompanyDesc;
|
||||||
} else if (
|
} else if (
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ import {
|
|||||||
evictMapCache,
|
evictMapCache,
|
||||||
getAvailablePlatforms,
|
getAvailablePlatforms,
|
||||||
getBundleDateSize,
|
getBundleDateSize,
|
||||||
getWorldMemo,
|
|
||||||
isRealInstance,
|
isRealInstance,
|
||||||
parseLocation,
|
parseLocation,
|
||||||
sanitizeEntityJson
|
sanitizeEntityJson
|
||||||
} from '../shared/utils';
|
} from '../shared/utils';
|
||||||
|
import { getWorldMemo } from './memoCoordinator';
|
||||||
import { instanceRequest, queryRequest, worldRequest } from '../api';
|
import { instanceRequest, queryRequest, worldRequest } from '../api';
|
||||||
import { database } from '../services/database';
|
import { database } from '../services/database';
|
||||||
import { patchWorldFromEvent } from '../queries';
|
import { patchWorldFromEvent } from '../queries';
|
||||||
@@ -118,21 +118,13 @@ export function showWorldDialog(tag, shortName = null, options = {}) {
|
|||||||
.then((args) => {
|
.then((args) => {
|
||||||
if (D.id === args.ref.id) {
|
if (D.id === args.ref.id) {
|
||||||
D.ref = args.ref;
|
D.ref = args.ref;
|
||||||
uiStore.setDialogCrumbLabel(
|
uiStore.setDialogCrumbLabel('world', D.id, D.ref?.name || D.id);
|
||||||
'world',
|
|
||||||
D.id,
|
|
||||||
D.ref?.name || D.id
|
|
||||||
);
|
|
||||||
D.visible = true;
|
D.visible = true;
|
||||||
D.loading = false;
|
D.loading = false;
|
||||||
D.isFavorite = favoriteStore.getCachedFavoritesByObjectId(
|
D.isFavorite = favoriteStore.getCachedFavoritesByObjectId(D.id);
|
||||||
D.id
|
|
||||||
);
|
|
||||||
if (!D.isFavorite) {
|
if (!D.isFavorite) {
|
||||||
D.isFavorite =
|
D.isFavorite =
|
||||||
favoriteStore.localWorldFavoritesList.includes(
|
favoriteStore.localWorldFavoritesList.includes(D.id);
|
||||||
D.id
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
let { isPC, isQuest, isIos } = getAvailablePlatforms(
|
let { isPC, isQuest, isIos } = getAvailablePlatforms(
|
||||||
args.ref.unityPackages
|
args.ref.unityPackages
|
||||||
|
|||||||
153
src/shared/utils/__tests__/appActions.test.js
Normal file
153
src/shared/utils/__tests__/appActions.test.js
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => ({
|
||||||
|
toast: {
|
||||||
|
success: vi.fn(),
|
||||||
|
error: vi.fn()
|
||||||
|
},
|
||||||
|
searchStore: {
|
||||||
|
directAccessParse: vi.fn()
|
||||||
|
},
|
||||||
|
modalStore: {
|
||||||
|
confirm: vi.fn()
|
||||||
|
},
|
||||||
|
i18n: {
|
||||||
|
global: {
|
||||||
|
t: vi.fn(() => 'copy failed')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('vue-sonner', () => ({
|
||||||
|
toast: mocks.toast
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../stores', () => ({
|
||||||
|
useSearchStore: () => mocks.searchStore,
|
||||||
|
useModalStore: () => mocks.modalStore
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../plugins/i18n', () => ({
|
||||||
|
i18n: mocks.i18n
|
||||||
|
}));
|
||||||
|
|
||||||
|
import {
|
||||||
|
copyToClipboard,
|
||||||
|
downloadAndSaveJson,
|
||||||
|
openDiscordProfile,
|
||||||
|
openExternalLink,
|
||||||
|
openFolderGeneric
|
||||||
|
} from '../appActions';
|
||||||
|
|
||||||
|
function flushPromises() {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('appActions utils', () => {
|
||||||
|
let consoleErrorSpy;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
vi.clearAllMocks();
|
||||||
|
mocks.searchStore.directAccessParse.mockReturnValue(false);
|
||||||
|
mocks.modalStore.confirm.mockResolvedValue({ ok: false });
|
||||||
|
Object.defineProperty(navigator, 'clipboard', {
|
||||||
|
configurable: true,
|
||||||
|
value: {
|
||||||
|
writeText: vi.fn().mockResolvedValue(undefined)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
globalThis.AppApi = {
|
||||||
|
OpenLink: vi.fn(),
|
||||||
|
OpenDiscordProfile: vi.fn().mockResolvedValue(undefined),
|
||||||
|
OpenFolderAndSelectItem: vi.fn()
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
consoleErrorSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('downloadAndSaveJson returns early for invalid params', () => {
|
||||||
|
downloadAndSaveJson('', { a: 1 });
|
||||||
|
downloadAndSaveJson('name', null);
|
||||||
|
expect(document.querySelectorAll('a[download]').length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('downloadAndSaveJson creates and clicks download link', () => {
|
||||||
|
const appendSpy = vi.spyOn(document.body, 'appendChild');
|
||||||
|
const removeSpy = vi.spyOn(document.body, 'removeChild');
|
||||||
|
|
||||||
|
downloadAndSaveJson('profile', { id: 1 });
|
||||||
|
|
||||||
|
expect(appendSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(removeSpy).toHaveBeenCalledTimes(1);
|
||||||
|
appendSpy.mockRestore();
|
||||||
|
removeSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('copyToClipboard shows success toast', async () => {
|
||||||
|
copyToClipboard('hello', 'copied');
|
||||||
|
await flushPromises();
|
||||||
|
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('hello');
|
||||||
|
expect(mocks.toast.success).toHaveBeenCalledWith('copied');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('copyToClipboard shows translated error toast on failure', async () => {
|
||||||
|
navigator.clipboard.writeText.mockRejectedValue(new Error('denied'));
|
||||||
|
copyToClipboard('hello');
|
||||||
|
await flushPromises();
|
||||||
|
await flushPromises();
|
||||||
|
expect(mocks.toast.error).toHaveBeenCalledWith('copy failed');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('openExternalLink returns early when direct access parse succeeds', async () => {
|
||||||
|
mocks.searchStore.directAccessParse.mockReturnValue(true);
|
||||||
|
openExternalLink('vrcx://user/usr_1');
|
||||||
|
await flushPromises();
|
||||||
|
expect(mocks.modalStore.confirm).not.toHaveBeenCalled();
|
||||||
|
expect(AppApi.OpenLink).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('openExternalLink copies link when confirm is canceled', async () => {
|
||||||
|
mocks.modalStore.confirm.mockResolvedValue({
|
||||||
|
ok: false,
|
||||||
|
reason: 'cancel'
|
||||||
|
});
|
||||||
|
openExternalLink('https://example.com');
|
||||||
|
await flushPromises();
|
||||||
|
await flushPromises();
|
||||||
|
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
|
||||||
|
'https://example.com'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('openExternalLink opens link when confirmed', async () => {
|
||||||
|
mocks.modalStore.confirm.mockResolvedValue({ ok: true });
|
||||||
|
openExternalLink('https://example.com');
|
||||||
|
await flushPromises();
|
||||||
|
expect(AppApi.OpenLink).toHaveBeenCalledWith('https://example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('openDiscordProfile validates empty discord id', () => {
|
||||||
|
openDiscordProfile('');
|
||||||
|
expect(mocks.toast.error).toHaveBeenCalledWith('No Discord ID provided!');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('openDiscordProfile shows error toast when api fails', async () => {
|
||||||
|
AppApi.OpenDiscordProfile.mockRejectedValue(new Error('fail'));
|
||||||
|
openDiscordProfile('123');
|
||||||
|
await flushPromises();
|
||||||
|
expect(mocks.toast.error).toHaveBeenCalledWith(
|
||||||
|
'Failed to open Discord profile!'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('openFolderGeneric delegates to AppApi', () => {
|
||||||
|
openFolderGeneric('/tmp/a.txt');
|
||||||
|
expect(AppApi.OpenFolderAndSelectItem).toHaveBeenCalledWith(
|
||||||
|
'/tmp/a.txt',
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
40
src/shared/utils/__tests__/chart.test.js
Normal file
40
src/shared/utils/__tests__/chart.test.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { describe, expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
|
describe('loadEcharts', () => {
|
||||||
|
test('loads echarts module', async () => {
|
||||||
|
vi.resetModules();
|
||||||
|
vi.doMock('echarts', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
marker: 'mock-echarts'
|
||||||
|
}));
|
||||||
|
const { loadEcharts } = await import('../chart.js');
|
||||||
|
|
||||||
|
const module = await loadEcharts();
|
||||||
|
|
||||||
|
expect(module).toMatchObject({ marker: 'mock-echarts' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns cached module reference on subsequent calls', async () => {
|
||||||
|
vi.resetModules();
|
||||||
|
vi.doMock('echarts', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
marker: 'mock-echarts'
|
||||||
|
}));
|
||||||
|
const { loadEcharts } = await import('../chart.js');
|
||||||
|
|
||||||
|
const first = await loadEcharts();
|
||||||
|
const second = await loadEcharts();
|
||||||
|
|
||||||
|
expect(second).toBe(first);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects when echarts import fails', async () => {
|
||||||
|
vi.resetModules();
|
||||||
|
vi.doMock('echarts', () => {
|
||||||
|
throw new Error('import failed');
|
||||||
|
});
|
||||||
|
const { loadEcharts } = await import('../chart.js');
|
||||||
|
|
||||||
|
await expect(loadEcharts()).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -13,7 +13,7 @@ vi.mock('../../../services/appConfig', () => ({
|
|||||||
AppDebug: { endpointDomain: 'https://api.vrchat.cloud/api/1' }
|
AppDebug: { endpointDomain: 'https://api.vrchat.cloud/api/1' }
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../utils/index.js', () => ({
|
vi.mock('../../../shared/utils', () => ({
|
||||||
extractFileId: vi.fn()
|
extractFileId: vi.fn()
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -22,7 +22,8 @@ vi.mock('../../../api', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
import { handleImageUploadInput, withUploadTimeout } from '../imageUpload';
|
import { withUploadTimeout } from '../imageUpload';
|
||||||
|
import { handleImageUploadInput } from '../../../coordinators/imageUploadCoordinator';
|
||||||
|
|
||||||
// ─── withUploadTimeout ───────────────────────────────────────────────
|
// ─── withUploadTimeout ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
162
src/shared/utils/__tests__/memos.test.js
Normal file
162
src/shared/utils/__tests__/memos.test.js
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => ({
|
||||||
|
friends: new Map(),
|
||||||
|
setUserDialogMemo: vi.fn(),
|
||||||
|
database: {
|
||||||
|
getUserMemo: vi.fn(),
|
||||||
|
setUserMemo: vi.fn(),
|
||||||
|
deleteUserMemo: vi.fn(),
|
||||||
|
getAllUserMemos: vi.fn(),
|
||||||
|
getWorldMemo: vi.fn()
|
||||||
|
},
|
||||||
|
storage: {
|
||||||
|
GetAll: vi.fn(),
|
||||||
|
Remove: vi.fn()
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../stores', () => ({
|
||||||
|
useFriendStore: () => ({
|
||||||
|
friends: mocks.friends
|
||||||
|
}),
|
||||||
|
useUserStore: () => ({
|
||||||
|
setUserDialogMemo: (...args) => mocks.setUserDialogMemo(...args)
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../services/database', () => ({
|
||||||
|
database: mocks.database
|
||||||
|
}));
|
||||||
|
|
||||||
|
import {
|
||||||
|
getAllUserMemos,
|
||||||
|
getUserMemo,
|
||||||
|
getWorldMemo,
|
||||||
|
migrateMemos,
|
||||||
|
saveUserMemo
|
||||||
|
} from '../../../coordinators/memoCoordinator.js';
|
||||||
|
|
||||||
|
describe('memos utils', () => {
|
||||||
|
let consoleErrorSpy;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
consoleErrorSpy = vi
|
||||||
|
.spyOn(console, 'error')
|
||||||
|
.mockImplementation(() => {});
|
||||||
|
mocks.friends = new Map();
|
||||||
|
mocks.setUserDialogMemo.mockReset();
|
||||||
|
mocks.database.getUserMemo.mockReset();
|
||||||
|
mocks.database.setUserMemo.mockReset();
|
||||||
|
mocks.database.deleteUserMemo.mockReset();
|
||||||
|
mocks.database.getAllUserMemos.mockReset();
|
||||||
|
mocks.database.getWorldMemo.mockReset();
|
||||||
|
mocks.storage.GetAll.mockReset();
|
||||||
|
mocks.storage.Remove.mockReset();
|
||||||
|
globalThis.VRCXStorage = mocks.storage;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
consoleErrorSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getUserMemo returns fallback when database throws', async () => {
|
||||||
|
mocks.database.getUserMemo.mockRejectedValue(new Error('boom'));
|
||||||
|
|
||||||
|
const result = await getUserMemo('usr_1');
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
userId: '',
|
||||||
|
editedAt: '',
|
||||||
|
memo: ''
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getWorldMemo returns fallback when database throws', async () => {
|
||||||
|
mocks.database.getWorldMemo.mockRejectedValue(new Error('boom'));
|
||||||
|
|
||||||
|
const result = await getWorldMemo('wrld_1');
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
worldId: '',
|
||||||
|
editedAt: '',
|
||||||
|
memo: ''
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('saveUserMemo persists memo and syncs friend fields', async () => {
|
||||||
|
const friend = { memo: '', $nickName: '' };
|
||||||
|
mocks.friends.set('usr_1', friend);
|
||||||
|
|
||||||
|
await saveUserMemo('usr_1', 'Nick\nmore');
|
||||||
|
|
||||||
|
expect(mocks.database.setUserMemo).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mocks.database.deleteUserMemo).not.toHaveBeenCalled();
|
||||||
|
expect(friend.memo).toBe('Nick\nmore');
|
||||||
|
expect(friend.$nickName).toBe('Nick');
|
||||||
|
expect(mocks.setUserDialogMemo).toHaveBeenCalledWith('Nick\nmore');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('saveUserMemo deletes memo and clears nickname on empty input', async () => {
|
||||||
|
const friend = { memo: 'old', $nickName: 'old' };
|
||||||
|
mocks.friends.set('usr_1', friend);
|
||||||
|
|
||||||
|
await saveUserMemo('usr_1', '');
|
||||||
|
|
||||||
|
expect(mocks.database.deleteUserMemo).toHaveBeenCalledWith('usr_1');
|
||||||
|
expect(friend.memo).toBe('');
|
||||||
|
expect(friend.$nickName).toBe('');
|
||||||
|
expect(mocks.setUserDialogMemo).toHaveBeenCalledWith('');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getAllUserMemos applies memo data to existing cached friends', async () => {
|
||||||
|
const friend1 = { memo: '', $nickName: '' };
|
||||||
|
const friend2 = { memo: '', $nickName: '' };
|
||||||
|
mocks.friends.set('usr_1', friend1);
|
||||||
|
mocks.friends.set('usr_2', friend2);
|
||||||
|
mocks.database.getAllUserMemos.mockResolvedValue([
|
||||||
|
{ userId: 'usr_1', memo: 'Alpha\nline2' },
|
||||||
|
{ userId: 'usr_2', memo: '' },
|
||||||
|
{ userId: 'usr_missing', memo: 'ignored' }
|
||||||
|
]);
|
||||||
|
|
||||||
|
await getAllUserMemos();
|
||||||
|
|
||||||
|
expect(friend1.memo).toBe('Alpha\nline2');
|
||||||
|
expect(friend1.$nickName).toBe('Alpha');
|
||||||
|
expect(friend2.memo).toBe('');
|
||||||
|
expect(friend2.$nickName).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('migrateMemos moves memo_usr entries to database and storage cleanup', async () => {
|
||||||
|
const friend = { memo: '', $nickName: '' };
|
||||||
|
mocks.friends.set('usr_1', friend);
|
||||||
|
mocks.storage.GetAll.mockResolvedValue(
|
||||||
|
JSON.stringify({
|
||||||
|
memo_usr_1: 'hello',
|
||||||
|
other_key: 'x',
|
||||||
|
memo_usr_2: ''
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await migrateMemos();
|
||||||
|
|
||||||
|
expect(mocks.database.setUserMemo).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mocks.database.setUserMemo).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
userId: 'usr_1',
|
||||||
|
memo: 'hello'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(mocks.storage.Remove).toHaveBeenCalledWith('memo_usr_1');
|
||||||
|
expect(mocks.storage.Remove).not.toHaveBeenCalledWith('memo_usr_2');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('migrateMemos rejects for invalid JSON payload', async () => {
|
||||||
|
mocks.storage.GetAll.mockResolvedValue('{bad json');
|
||||||
|
|
||||||
|
await expect(migrateMemos()).rejects.toThrow();
|
||||||
|
expect(mocks.database.setUserMemo).not.toHaveBeenCalled();
|
||||||
|
expect(mocks.storage.Remove).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
102
src/shared/utils/appActions.js
Normal file
102
src/shared/utils/appActions.js
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { toast } from 'vue-sonner';
|
||||||
|
|
||||||
|
import { useModalStore, useSearchStore } from '../../stores';
|
||||||
|
import { escapeTag } from './base/string';
|
||||||
|
import { i18n } from '../../plugins/i18n';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} fileName
|
||||||
|
* @param {*} data
|
||||||
|
*/
|
||||||
|
function downloadAndSaveJson(fileName, data) {
|
||||||
|
if (!fileName || !data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.setAttribute(
|
||||||
|
'href',
|
||||||
|
`data:application/json;charset=utf-8,${encodeURIComponent(
|
||||||
|
JSON.stringify(data, null, 2)
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
link.setAttribute('download', `${fileName}.json`);
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
} catch {
|
||||||
|
toast.error(escapeTag('Failed to download JSON.'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} text
|
||||||
|
* @param {string} message
|
||||||
|
*/
|
||||||
|
function copyToClipboard(text, message = 'Copied successfully!') {
|
||||||
|
navigator.clipboard
|
||||||
|
.writeText(text)
|
||||||
|
.then(() => {
|
||||||
|
toast.success(message);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('Copy failed:', err);
|
||||||
|
toast.error(i18n.global.t('message.copy_failed'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} link
|
||||||
|
*/
|
||||||
|
function openExternalLink(link) {
|
||||||
|
const searchStore = useSearchStore();
|
||||||
|
if (searchStore.directAccessParse(link)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modalStore = useModalStore();
|
||||||
|
modalStore
|
||||||
|
.confirm({
|
||||||
|
description: `${link}`,
|
||||||
|
title: 'Open External Link',
|
||||||
|
confirmText: 'Open',
|
||||||
|
cancelText: 'Copy'
|
||||||
|
})
|
||||||
|
.then(({ ok, reason }) => {
|
||||||
|
if (reason === 'cancel') {
|
||||||
|
copyToClipboard(link, 'Link copied to clipboard!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ok) {
|
||||||
|
AppApi.OpenLink(link);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDiscordProfile(discordId) {
|
||||||
|
if (!discordId) {
|
||||||
|
toast.error('No Discord ID provided!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AppApi.OpenDiscordProfile(discordId).catch((err) => {
|
||||||
|
console.error('Failed to open Discord profile:', err);
|
||||||
|
toast.error('Failed to open Discord profile!');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// #region | App: Random unsorted app methods, data structs, API functions, and an API feedback/file analysis event
|
||||||
|
|
||||||
|
function openFolderGeneric(path) {
|
||||||
|
AppApi.OpenFolderAndSelectItem(path, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
downloadAndSaveJson,
|
||||||
|
copyToClipboard,
|
||||||
|
openExternalLink,
|
||||||
|
openDiscordProfile,
|
||||||
|
openFolderGeneric
|
||||||
|
};
|
||||||
@@ -15,7 +15,7 @@ vi.mock('../../../../plugins/router', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
import { useAppearanceSettingsStore } from '../../../../stores';
|
import { useAppearanceSettingsStore } from '../../../../stores';
|
||||||
import { formatDateFilter } from '../date';
|
import { formatDateFilter } from '../../../../coordinators/dateCoordinator';
|
||||||
|
|
||||||
describe('formatDateFilter', () => {
|
describe('formatDateFilter', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -1,14 +1,3 @@
|
|||||||
import { storeToRefs } from 'pinia';
|
|
||||||
import { toast } from 'vue-sonner';
|
|
||||||
|
|
||||||
import {
|
|
||||||
useAuthStore,
|
|
||||||
useAvatarStore,
|
|
||||||
useInstanceStore,
|
|
||||||
useModalStore,
|
|
||||||
useSearchStore,
|
|
||||||
useWorldStore
|
|
||||||
} from '../../stores';
|
|
||||||
import {
|
import {
|
||||||
extractFileId,
|
extractFileId,
|
||||||
extractFileVersion,
|
extractFileVersion,
|
||||||
@@ -17,148 +6,20 @@ import {
|
|||||||
import { escapeTag, replaceBioSymbols } from './base/string';
|
import { escapeTag, replaceBioSymbols } from './base/string';
|
||||||
import { getFaviconUrl, replaceVrcPackageUrl } from './urlUtils';
|
import { getFaviconUrl, replaceVrcPackageUrl } from './urlUtils';
|
||||||
import { AppDebug } from '../../services/appConfig.js';
|
import { AppDebug } from '../../services/appConfig.js';
|
||||||
import { compareUnityVersion } from './avatar';
|
|
||||||
import { getAvailablePlatforms } from './platformUtils';
|
import { getAvailablePlatforms } from './platformUtils';
|
||||||
import { i18n } from '../../plugins/i18n';
|
|
||||||
import { queryRequest } from '../../api';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} fileName
|
|
||||||
* @param {*} data
|
|
||||||
*/
|
|
||||||
function downloadAndSaveJson(fileName, data) {
|
|
||||||
if (!fileName || !data) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.setAttribute(
|
|
||||||
'href',
|
|
||||||
`data:application/json;charset=utf-8,${encodeURIComponent(
|
|
||||||
JSON.stringify(data, null, 2)
|
|
||||||
)}`
|
|
||||||
);
|
|
||||||
link.setAttribute('download', `${fileName}.json`);
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
} catch {
|
|
||||||
toast.error(escapeTag('Failed to download JSON.'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteVRChatCache(ref) {
|
|
||||||
const authStore = useAuthStore();
|
|
||||||
const sdkUnityVersion = authStore.cachedConfig.sdkUnityVersion;
|
|
||||||
let assetUrl = '';
|
|
||||||
let variant = '';
|
|
||||||
for (let i = ref.unityPackages.length - 1; i > -1; i--) {
|
|
||||||
const unityPackage = ref.unityPackages[i];
|
|
||||||
if (
|
|
||||||
unityPackage.variant &&
|
|
||||||
unityPackage.variant !== 'standard' &&
|
|
||||||
unityPackage.variant !== 'security'
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
unityPackage.platform === 'standalonewindows' &&
|
|
||||||
compareUnityVersion(unityPackage.unitySortNumber, sdkUnityVersion)
|
|
||||||
) {
|
|
||||||
assetUrl = unityPackage.assetUrl;
|
|
||||||
if (!unityPackage.variant || unityPackage.variant === 'standard') {
|
|
||||||
variant = 'security';
|
|
||||||
} else {
|
|
||||||
variant = unityPackage.variant;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const id = extractFileId(assetUrl);
|
|
||||||
const version = parseInt(extractFileVersion(assetUrl), 10);
|
|
||||||
const variantVersion = parseInt(extractVariantVersion(assetUrl), 10);
|
|
||||||
await AssetBundleManager.DeleteCache(id, version, variant, variantVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {object} ref
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
async function checkVRChatCache(ref) {
|
|
||||||
if (!ref.unityPackages) {
|
|
||||||
return { Item1: -1, Item2: false, Item3: '' };
|
|
||||||
}
|
|
||||||
const authStore = useAuthStore();
|
|
||||||
const sdkUnityVersion = authStore.cachedConfig.sdkUnityVersion;
|
|
||||||
let assetUrl = '';
|
|
||||||
let variant = '';
|
|
||||||
for (let i = ref.unityPackages.length - 1; i > -1; i--) {
|
|
||||||
const unityPackage = ref.unityPackages[i];
|
|
||||||
if (unityPackage.variant && unityPackage.variant !== 'security') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
unityPackage.platform === 'standalonewindows' &&
|
|
||||||
compareUnityVersion(unityPackage.unitySortNumber, sdkUnityVersion)
|
|
||||||
) {
|
|
||||||
assetUrl = unityPackage.assetUrl;
|
|
||||||
if (!unityPackage.variant || unityPackage.variant === 'standard') {
|
|
||||||
variant = 'security';
|
|
||||||
} else {
|
|
||||||
variant = unityPackage.variant;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!assetUrl) {
|
|
||||||
assetUrl = ref.assetUrl;
|
|
||||||
}
|
|
||||||
const id = extractFileId(assetUrl);
|
|
||||||
const version = parseInt(extractFileVersion(assetUrl), 10);
|
|
||||||
const variantVersion = parseInt(extractVariantVersion(assetUrl), 10);
|
|
||||||
if (!id || !version) {
|
|
||||||
return { Item1: -1, Item2: false, Item3: '' };
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return AssetBundleManager.CheckVRChatCache(
|
|
||||||
id,
|
|
||||||
version,
|
|
||||||
variant,
|
|
||||||
variantVersion
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed reading VRChat cache size:', err);
|
|
||||||
toast.error(`Failed reading VRChat cache size: ${err}`);
|
|
||||||
return { Item1: -1, Item2: false, Item3: '' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {string} text
|
|
||||||
* @param {string} message
|
|
||||||
*/
|
|
||||||
function copyToClipboard(text, message = 'Copied successfully!') {
|
|
||||||
navigator.clipboard
|
|
||||||
.writeText(text)
|
|
||||||
.then(() => {
|
|
||||||
toast.success(message);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error('Copy failed:', err);
|
|
||||||
toast.error(i18n.global.t('message.copy_failed'));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} url
|
* @param {string} url
|
||||||
* @param {number} resolution
|
* @param {number} resolution
|
||||||
|
* @param endpointDomain
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function convertFileUrlToImageUrl(url, resolution = 128, endpointDomain = AppDebug.endpointDomain) {
|
function convertFileUrlToImageUrl(
|
||||||
|
url,
|
||||||
|
resolution = 128,
|
||||||
|
endpointDomain = AppDebug.endpointDomain
|
||||||
|
) {
|
||||||
if (!url) {
|
if (!url) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -183,137 +44,9 @@ function convertFileUrlToImageUrl(url, resolution = 128, endpointDomain = AppDeb
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} link
|
* @param func
|
||||||
|
* @param delay
|
||||||
*/
|
*/
|
||||||
function openExternalLink(link) {
|
|
||||||
const searchStore = useSearchStore();
|
|
||||||
if (searchStore.directAccessParse(link)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const modalStore = useModalStore();
|
|
||||||
modalStore
|
|
||||||
.confirm({
|
|
||||||
description: `${link}`,
|
|
||||||
title: 'Open External Link',
|
|
||||||
confirmText: 'Open',
|
|
||||||
cancelText: 'Copy'
|
|
||||||
})
|
|
||||||
.then(({ ok, reason }) => {
|
|
||||||
if (reason === 'cancel') {
|
|
||||||
copyToClipboard(link, 'Link copied to clipboard!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (ok) {
|
|
||||||
AppApi.OpenLink(link);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function openDiscordProfile(discordId) {
|
|
||||||
if (!discordId) {
|
|
||||||
toast.error('No Discord ID provided!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
AppApi.OpenDiscordProfile(discordId).catch((err) => {
|
|
||||||
console.error('Failed to open Discord profile:', err);
|
|
||||||
toast.error('Failed to open Discord profile!');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {object} ref
|
|
||||||
* @returns {Promise<object>}
|
|
||||||
*/
|
|
||||||
async function getBundleDateSize(ref) {
|
|
||||||
const authStore = useAuthStore();
|
|
||||||
const sdkUnityVersion = authStore.cachedConfig.sdkUnityVersion;
|
|
||||||
const avatarStore = useAvatarStore();
|
|
||||||
const { avatarDialog } = storeToRefs(avatarStore);
|
|
||||||
const worldStore = useWorldStore();
|
|
||||||
const { worldDialog } = storeToRefs(worldStore);
|
|
||||||
const instanceStore = useInstanceStore();
|
|
||||||
const { currentInstanceWorld, currentInstanceLocation } =
|
|
||||||
storeToRefs(instanceStore);
|
|
||||||
const bundleJson = {};
|
|
||||||
for (let i = ref.unityPackages.length - 1; i > -1; i--) {
|
|
||||||
const unityPackage = ref.unityPackages[i];
|
|
||||||
if (!unityPackage) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
unityPackage.variant &&
|
|
||||||
unityPackage.variant !== 'standard' &&
|
|
||||||
unityPackage.variant !== 'security'
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!compareUnityVersion(unityPackage.unitySortNumber, sdkUnityVersion)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const platform = unityPackage.platform;
|
|
||||||
if (bundleJson[platform]) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const assetUrl = unityPackage.assetUrl;
|
|
||||||
const fileId = extractFileId(assetUrl);
|
|
||||||
const version = parseInt(extractFileVersion(assetUrl), 10);
|
|
||||||
let variant = '';
|
|
||||||
if (!unityPackage.variant || unityPackage.variant === 'standard') {
|
|
||||||
variant = 'security';
|
|
||||||
} else {
|
|
||||||
variant = unityPackage.variant;
|
|
||||||
}
|
|
||||||
if (!fileId || !version) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const args = await queryRequest.fetch('fileAnalysis', {
|
|
||||||
fileId,
|
|
||||||
version,
|
|
||||||
variant
|
|
||||||
});
|
|
||||||
if (!args?.json?.success) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const json = args.json;
|
|
||||||
if (typeof json.fileSize !== 'undefined') {
|
|
||||||
json._fileSize = `${(json.fileSize / 1048576).toFixed(2)} MB`;
|
|
||||||
}
|
|
||||||
if (typeof json.uncompressedSize !== 'undefined') {
|
|
||||||
json._uncompressedSize = `${(json.uncompressedSize / 1048576).toFixed(2)} MB`;
|
|
||||||
}
|
|
||||||
if (typeof json.avatarStats?.totalTextureUsage !== 'undefined') {
|
|
||||||
json._totalTextureUsage = `${(json.avatarStats.totalTextureUsage / 1048576).toFixed(2)} MB`;
|
|
||||||
}
|
|
||||||
bundleJson[platform] = json;
|
|
||||||
|
|
||||||
if (avatarDialog.value.id === ref.id) {
|
|
||||||
// update avatar dialog
|
|
||||||
avatarDialog.value.fileAnalysis[platform] = json;
|
|
||||||
}
|
|
||||||
// update world dialog
|
|
||||||
if (worldDialog.value.id === ref.id) {
|
|
||||||
worldDialog.value.fileAnalysis[platform] = json;
|
|
||||||
}
|
|
||||||
// update player list
|
|
||||||
if (currentInstanceLocation.value.worldId === ref.id) {
|
|
||||||
currentInstanceWorld.value.fileAnalysis[platform] = json;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return bundleJson;
|
|
||||||
}
|
|
||||||
|
|
||||||
// #region | App: Random unsorted app methods, data structs, API functions, and an API feedback/file analysis event
|
|
||||||
|
|
||||||
function openFolderGeneric(path) {
|
|
||||||
AppApi.OpenFolderAndSelectItem(path, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function debounce(func, delay) {
|
function debounce(func, delay) {
|
||||||
let timer = null;
|
let timer = null;
|
||||||
return function (...args) {
|
return function (...args) {
|
||||||
@@ -325,12 +58,23 @@ function debounce(func, delay) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-export from appActions and cacheCoordinator for backward compatibility
|
||||||
export {
|
export {
|
||||||
getAvailablePlatforms,
|
|
||||||
downloadAndSaveJson,
|
downloadAndSaveJson,
|
||||||
|
copyToClipboard,
|
||||||
|
openExternalLink,
|
||||||
|
openDiscordProfile,
|
||||||
|
openFolderGeneric
|
||||||
|
} from './appActions';
|
||||||
|
|
||||||
|
export {
|
||||||
deleteVRChatCache,
|
deleteVRChatCache,
|
||||||
checkVRChatCache,
|
checkVRChatCache,
|
||||||
copyToClipboard,
|
getBundleDateSize
|
||||||
|
} from '../../coordinators/cacheCoordinator';
|
||||||
|
|
||||||
|
export {
|
||||||
|
getAvailablePlatforms,
|
||||||
getFaviconUrl,
|
getFaviconUrl,
|
||||||
convertFileUrlToImageUrl,
|
convertFileUrlToImageUrl,
|
||||||
replaceVrcPackageUrl,
|
replaceVrcPackageUrl,
|
||||||
@@ -338,9 +82,5 @@ export {
|
|||||||
extractFileVersion,
|
extractFileVersion,
|
||||||
extractVariantVersion,
|
extractVariantVersion,
|
||||||
replaceBioSymbols,
|
replaceBioSymbols,
|
||||||
openExternalLink,
|
|
||||||
openDiscordProfile,
|
|
||||||
getBundleDateSize,
|
|
||||||
openFolderGeneric,
|
|
||||||
debounce
|
debounce
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import { toast } from 'vue-sonner';
|
|
||||||
|
|
||||||
import { $throw } from '../../services/request';
|
|
||||||
import { AppDebug } from '../../services/appConfig.js';
|
|
||||||
import { extractFileId } from './index.js';
|
|
||||||
import { imageRequest } from '../../api';
|
|
||||||
|
|
||||||
const UPLOAD_TIMEOUT_MS = 30_000;
|
const UPLOAD_TIMEOUT_MS = 30_000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param promise
|
||||||
|
*/
|
||||||
export function withUploadTimeout(promise) {
|
export function withUploadTimeout(promise) {
|
||||||
return Promise.race([
|
return Promise.race([
|
||||||
promise,
|
promise,
|
||||||
@@ -19,79 +16,6 @@ export function withUploadTimeout(promise) {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveMessage(message) {
|
|
||||||
if (typeof message === 'function') {
|
|
||||||
return message();
|
|
||||||
}
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getInputElement(selector) {
|
|
||||||
if (!selector) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (typeof selector === 'function') {
|
|
||||||
return selector();
|
|
||||||
}
|
|
||||||
if (typeof selector === 'string') {
|
|
||||||
return document.querySelector(selector);
|
|
||||||
}
|
|
||||||
return selector;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handleImageUploadInput(event, options = {}) {
|
|
||||||
const {
|
|
||||||
inputSelector,
|
|
||||||
// 20MB
|
|
||||||
maxSize = 20000000,
|
|
||||||
acceptPattern = /image.*/,
|
|
||||||
tooLargeMessage,
|
|
||||||
invalidTypeMessage,
|
|
||||||
onClear
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
const clearInput = () => {
|
|
||||||
onClear?.();
|
|
||||||
const input = getInputElement(inputSelector);
|
|
||||||
if (input) {
|
|
||||||
input.value = '';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const files = event?.target?.files || event?.dataTransfer?.files;
|
|
||||||
if (!files || files.length === 0) {
|
|
||||||
clearInput();
|
|
||||||
return { file: null, clearInput };
|
|
||||||
}
|
|
||||||
|
|
||||||
const file = files[0];
|
|
||||||
if (file.size >= maxSize) {
|
|
||||||
if (tooLargeMessage) {
|
|
||||||
toast.error(resolveMessage(tooLargeMessage));
|
|
||||||
}
|
|
||||||
clearInput();
|
|
||||||
return { file: null, clearInput };
|
|
||||||
}
|
|
||||||
|
|
||||||
let acceptRegex = null;
|
|
||||||
if (acceptPattern) {
|
|
||||||
acceptRegex =
|
|
||||||
acceptPattern instanceof RegExp
|
|
||||||
? acceptPattern
|
|
||||||
: new RegExp(acceptPattern);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (acceptRegex && !acceptRegex.test(file.type)) {
|
|
||||||
if (invalidTypeMessage) {
|
|
||||||
toast.error(resolveMessage(invalidTypeMessage));
|
|
||||||
}
|
|
||||||
clearInput();
|
|
||||||
return { file: null, clearInput };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { file, clearInput };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* File -> base64
|
* File -> base64
|
||||||
* @param {Blob|File} blob
|
* @param {Blob|File} blob
|
||||||
@@ -113,122 +37,3 @@ export function readFileAsBase64(blob) {
|
|||||||
r.readAsArrayBuffer(blob);
|
r.readAsArrayBuffer(blob);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} base64Data - base64 encoded image
|
|
||||||
* @returns {Promise<string>} resized base64 encoded image
|
|
||||||
*/
|
|
||||||
export async function resizeImageToFitLimits(base64Data) {
|
|
||||||
// frontend limit check = 20MB
|
|
||||||
return AppApi.ResizeImageToFitLimits(base64Data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Upload image through AWS
|
|
||||||
* @param {'avatar'|'world'} type
|
|
||||||
* @param {object} opts
|
|
||||||
* @param {string} opts.entityId - avatar or world id
|
|
||||||
* @param {string} opts.imageUrl - current imageUrl on the entity
|
|
||||||
* @param {string} opts.base64File - base64 encoded image data
|
|
||||||
* @param {Blob} opts.blob - the original blob (used for file size)
|
|
||||||
*/
|
|
||||||
export async function uploadImageLegacy(
|
|
||||||
type,
|
|
||||||
{ entityId, imageUrl, base64File, blob }
|
|
||||||
) {
|
|
||||||
const apiMap = {
|
|
||||||
avatar: {
|
|
||||||
uploadImage: imageRequest.uploadAvatarImage,
|
|
||||||
fileStart: imageRequest.uploadAvatarImageFileStart,
|
|
||||||
fileFinish: imageRequest.uploadAvatarImageFileFinish,
|
|
||||||
sigStart: imageRequest.uploadAvatarImageSigStart,
|
|
||||||
sigFinish: imageRequest.uploadAvatarImageSigFinish,
|
|
||||||
setImage: imageRequest.setAvatarImage
|
|
||||||
},
|
|
||||||
world: {
|
|
||||||
uploadImage: imageRequest.uploadWorldImage,
|
|
||||||
fileStart: imageRequest.uploadWorldImageFileStart,
|
|
||||||
fileFinish: imageRequest.uploadWorldImageFileFinish,
|
|
||||||
sigStart: imageRequest.uploadWorldImageSigStart,
|
|
||||||
sigFinish: imageRequest.uploadWorldImageSigFinish,
|
|
||||||
setImage: imageRequest.setWorldImage
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const api = apiMap[type];
|
|
||||||
|
|
||||||
const fileMd5 = await AppApi.MD5File(base64File);
|
|
||||||
const fileSizeInBytes = parseInt(blob.size, 10);
|
|
||||||
const base64SignatureFile = await AppApi.SignFile(base64File);
|
|
||||||
const signatureMd5 = await AppApi.MD5File(base64SignatureFile);
|
|
||||||
const signatureSizeInBytes = parseInt(
|
|
||||||
await AppApi.FileLength(base64SignatureFile),
|
|
||||||
10
|
|
||||||
);
|
|
||||||
const fileId = extractFileId(imageUrl);
|
|
||||||
|
|
||||||
// imageInit
|
|
||||||
const uploadRes = await api.uploadImage(
|
|
||||||
{ fileMd5, fileSizeInBytes, signatureMd5, signatureSizeInBytes },
|
|
||||||
fileId
|
|
||||||
);
|
|
||||||
const uploadedFileId = uploadRes.json.id;
|
|
||||||
const fileVersion =
|
|
||||||
uploadRes.json.versions[uploadRes.json.versions.length - 1].version;
|
|
||||||
|
|
||||||
// imageFileStart
|
|
||||||
const fileStartRes = await api.fileStart({
|
|
||||||
fileId: uploadedFileId,
|
|
||||||
fileVersion
|
|
||||||
});
|
|
||||||
|
|
||||||
// uploadImageFileAWS
|
|
||||||
const fileAwsRes = await webApiService.execute({
|
|
||||||
url: fileStartRes.json.url,
|
|
||||||
uploadFilePUT: true,
|
|
||||||
fileData: base64File,
|
|
||||||
fileMIME: 'image/png',
|
|
||||||
fileMD5: fileMd5
|
|
||||||
});
|
|
||||||
if (fileAwsRes.status !== 200) {
|
|
||||||
$throw(
|
|
||||||
fileAwsRes.status,
|
|
||||||
`${type} image upload failed`,
|
|
||||||
fileStartRes.json.url
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// imageFileFinish
|
|
||||||
await api.fileFinish({ fileId: uploadedFileId, fileVersion });
|
|
||||||
|
|
||||||
// imageSigStart
|
|
||||||
const sigStartRes = await api.sigStart({
|
|
||||||
fileId: uploadedFileId,
|
|
||||||
fileVersion
|
|
||||||
});
|
|
||||||
|
|
||||||
// uploadImageSigAWS
|
|
||||||
const sigAwsRes = await webApiService.execute({
|
|
||||||
url: sigStartRes.json.url,
|
|
||||||
uploadFilePUT: true,
|
|
||||||
fileData: base64SignatureFile,
|
|
||||||
fileMIME: 'application/x-rsync-signature',
|
|
||||||
fileMD5: signatureMd5
|
|
||||||
});
|
|
||||||
if (sigAwsRes.status !== 200) {
|
|
||||||
$throw(
|
|
||||||
sigAwsRes.status,
|
|
||||||
`${type} image upload failed`,
|
|
||||||
sigStartRes.json.url
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// imageSigFinish
|
|
||||||
await api.sigFinish({ fileId: uploadedFileId, fileVersion });
|
|
||||||
|
|
||||||
// imageSet
|
|
||||||
const newImageUrl = `${AppDebug.endpointDomain}/file/${uploadedFileId}/${fileVersion}/file`;
|
|
||||||
const setRes = await api.setImage({ id: entityId, imageUrl: newImageUrl });
|
|
||||||
if (setRes.json.imageUrl !== newImageUrl) {
|
|
||||||
$throw(0, `${type} image change failed`, newImageUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export * from './base/array';
|
export * from './base/array';
|
||||||
export * from './base/devtool';
|
export * from './base/devtool';
|
||||||
export * from './base/format';
|
export * from './base/format';
|
||||||
export * from './base/date';
|
export { formatDateFilter } from '../../coordinators/dateCoordinator';
|
||||||
export * from './base/string';
|
export * from './base/string';
|
||||||
export * from './avatar';
|
export * from './avatar';
|
||||||
export * from './chart';
|
export * from './chart';
|
||||||
@@ -20,7 +20,6 @@ export * from './gallery';
|
|||||||
export * from './location';
|
export * from './location';
|
||||||
export * from './invite';
|
export * from './invite';
|
||||||
export * from './world';
|
export * from './world';
|
||||||
export * from './memos';
|
|
||||||
export * from './throttle';
|
export * from './throttle';
|
||||||
export * from './retry';
|
export * from './retry';
|
||||||
export * from './gameLog';
|
export * from './gameLog';
|
||||||
|
|||||||
@@ -1,20 +1,3 @@
|
|||||||
import { instanceRequest } from '../../api';
|
|
||||||
import { parseLocation } from './locationParser';
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {object} instance
|
|
||||||
*/
|
|
||||||
function refreshInstancePlayerCount(instance) {
|
|
||||||
const L = parseLocation(instance);
|
|
||||||
if (L.isRealInstance) {
|
|
||||||
instanceRequest.getInstance({
|
|
||||||
worldId: L.worldId,
|
|
||||||
instanceId: L.instanceId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} instanceId
|
* @param {string} instanceId
|
||||||
@@ -131,9 +114,4 @@ function buildLegacyInstanceTag({
|
|||||||
return tags.join('');
|
return tags.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export { isRealInstance, getLaunchURL, buildLegacyInstanceTag };
|
||||||
refreshInstancePlayerCount,
|
|
||||||
isRealInstance,
|
|
||||||
getLaunchURL,
|
|
||||||
buildLegacyInstanceTag
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import {
|
|||||||
createRateLimiter,
|
createRateLimiter,
|
||||||
executeWithBackoff,
|
executeWithBackoff,
|
||||||
getFriendsSortFunction,
|
getFriendsSortFunction,
|
||||||
getUserMemo,
|
|
||||||
isRealInstance
|
isRealInstance
|
||||||
} from '../shared/utils';
|
} from '../shared/utils';
|
||||||
|
import { getUserMemo } from '../coordinators/memoCoordinator';
|
||||||
import { friendRequest, userRequest } from '../api';
|
import { friendRequest, userRequest } from '../api';
|
||||||
import {
|
import {
|
||||||
runInitFriendsListFlow,
|
runInitFriendsListFlow,
|
||||||
@@ -261,8 +261,6 @@ export const useFriendStore = defineStore('Friend', () => {
|
|||||||
|
|
||||||
init();
|
init();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@@ -699,13 +697,11 @@ export const useFriendStore = defineStore('Friend', () => {
|
|||||||
* @param {string} id
|
* @param {string} id
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {object} ref
|
* @param {object} ref
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {object} currentUser
|
* @param {object} currentUser
|
||||||
@@ -1129,7 +1125,6 @@ export const useFriendStore = defineStore('Friend', () => {
|
|||||||
* @param id
|
* @param id
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears all entries in friendLog.
|
* Clears all entries in friendLog.
|
||||||
* Uses .clear() instead of reassignment to keep the same Map reference,
|
* Uses .clear() instead of reassignment to keep the same Map reference,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
vrcPlusImageRequest
|
vrcPlusImageRequest
|
||||||
} from '../api';
|
} from '../api';
|
||||||
import { AppDebug } from '../services/appConfig';
|
import { AppDebug } from '../services/appConfig';
|
||||||
import { handleImageUploadInput } from '../shared/utils/imageUpload';
|
import { handleImageUploadInput } from '../coordinators/imageUploadCoordinator';
|
||||||
import { router } from '../plugins/router';
|
import { router } from '../plugins/router';
|
||||||
import { useAdvancedSettingsStore } from './settings/advanced';
|
import { useAdvancedSettingsStore } from './settings/advanced';
|
||||||
import { useModalStore } from './modal';
|
import { useModalStore } from './modal';
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ import {
|
|||||||
escapeTag,
|
escapeTag,
|
||||||
executeWithBackoff,
|
executeWithBackoff,
|
||||||
findUserByDisplayName,
|
findUserByDisplayName,
|
||||||
getUserMemo,
|
|
||||||
parseLocation,
|
parseLocation,
|
||||||
parseNotificationDetails,
|
parseNotificationDetails,
|
||||||
removeFromArray,
|
removeFromArray,
|
||||||
sanitizeNotificationJson
|
sanitizeNotificationJson
|
||||||
} from '../../shared/utils';
|
} from '../../shared/utils';
|
||||||
|
import { getUserMemo } from '../../coordinators/memoCoordinator';
|
||||||
import {
|
import {
|
||||||
friendRequest,
|
friendRequest,
|
||||||
instanceRequest,
|
instanceRequest,
|
||||||
@@ -347,11 +347,13 @@ export const useNotificationStore = defineStore('Notification', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!checkCanInvite(currentLocation, {
|
if (
|
||||||
currentUserId: userStore.currentUser.id,
|
!checkCanInvite(currentLocation, {
|
||||||
lastLocationStr: locationStore.lastLocation.location,
|
currentUserId: userStore.currentUser.id,
|
||||||
cachedInstances: instanceStore.cachedInstances
|
lastLocationStr: locationStore.lastLocation.location,
|
||||||
})) {
|
cachedInstances: instanceStore.cachedInstances
|
||||||
|
})
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,16 +8,12 @@ import {
|
|||||||
compareByLocationAt,
|
compareByLocationAt,
|
||||||
compareByName,
|
compareByName,
|
||||||
compareByUpdatedAt,
|
compareByUpdatedAt,
|
||||||
getAllUserMemos,
|
|
||||||
getUserMemo,
|
|
||||||
isRealInstance,
|
isRealInstance,
|
||||||
parseLocation,
|
parseLocation,
|
||||||
replaceBioSymbols
|
replaceBioSymbols
|
||||||
} from '../shared/utils';
|
} from '../shared/utils';
|
||||||
import {
|
import { getAllUserMemos, getUserMemo } from '../coordinators/memoCoordinator';
|
||||||
instanceRequest,
|
import { instanceRequest, userRequest } from '../api';
|
||||||
userRequest
|
|
||||||
} from '../api';
|
|
||||||
import { AppDebug } from '../services/appConfig';
|
import { AppDebug } from '../services/appConfig';
|
||||||
import { database } from '../services/database';
|
import { database } from '../services/database';
|
||||||
import { runUpdateCurrentUserLocationFlow } from '../coordinators/locationCoordinator';
|
import { runUpdateCurrentUserLocationFlow } from '../coordinators/locationCoordinator';
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const mocks = vi.hoisted(() => ({
|
|||||||
favoriteFriendGroups: null,
|
favoriteFriendGroups: null,
|
||||||
groupedByGroupKeyFavoriteFriends: null,
|
groupedByGroupKeyFavoriteFriends: null,
|
||||||
localFriendFavorites: null,
|
localFriendFavorites: null,
|
||||||
|
lastLocation: null,
|
||||||
configGetString: vi.fn(),
|
configGetString: vi.fn(),
|
||||||
configGetBool: vi.fn(),
|
configGetBool: vi.fn(),
|
||||||
configSetString: vi.fn(),
|
configSetString: vi.fn(),
|
||||||
@@ -37,6 +38,10 @@ mocks.sidebarSortMethods = mocks.makeRef('status');
|
|||||||
mocks.favoriteFriendGroups = mocks.makeRef([]);
|
mocks.favoriteFriendGroups = mocks.makeRef([]);
|
||||||
mocks.groupedByGroupKeyFavoriteFriends = mocks.makeRef({});
|
mocks.groupedByGroupKeyFavoriteFriends = mocks.makeRef({});
|
||||||
mocks.localFriendFavorites = mocks.makeRef({});
|
mocks.localFriendFavorites = mocks.makeRef({});
|
||||||
|
mocks.lastLocation = mocks.makeRef({
|
||||||
|
location: 'wrld_home:123',
|
||||||
|
friendList: new Map()
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock('pinia', () => ({
|
vi.mock('pinia', () => ({
|
||||||
storeToRefs: (store) => store
|
storeToRefs: (store) => store
|
||||||
@@ -68,6 +73,9 @@ vi.mock('../../../stores', () => ({
|
|||||||
favoriteFriendGroups: mocks.favoriteFriendGroups,
|
favoriteFriendGroups: mocks.favoriteFriendGroups,
|
||||||
groupedByGroupKeyFavoriteFriends: mocks.groupedByGroupKeyFavoriteFriends,
|
groupedByGroupKeyFavoriteFriends: mocks.groupedByGroupKeyFavoriteFriends,
|
||||||
localFriendFavorites: mocks.localFriendFavorites
|
localFriendFavorites: mocks.localFriendFavorites
|
||||||
|
}),
|
||||||
|
useLocationStore: () => ({
|
||||||
|
lastLocation: mocks.lastLocation
|
||||||
})
|
})
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -225,6 +233,10 @@ describe('FriendsLocations.vue', () => {
|
|||||||
mocks.favoriteFriendGroups.value = [];
|
mocks.favoriteFriendGroups.value = [];
|
||||||
mocks.groupedByGroupKeyFavoriteFriends.value = {};
|
mocks.groupedByGroupKeyFavoriteFriends.value = {};
|
||||||
mocks.localFriendFavorites.value = {};
|
mocks.localFriendFavorites.value = {};
|
||||||
|
mocks.lastLocation.value = {
|
||||||
|
location: 'wrld_home:123',
|
||||||
|
friendList: new Map()
|
||||||
|
};
|
||||||
|
|
||||||
mocks.configGetString.mockReset();
|
mocks.configGetString.mockReset();
|
||||||
mocks.configGetBool.mockReset();
|
mocks.configGetBool.mockReset();
|
||||||
|
|||||||
@@ -314,20 +314,24 @@
|
|||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useVirtualizer } from '@tanstack/vue-virtual';
|
import { useVirtualizer } from '@tanstack/vue-virtual';
|
||||||
|
|
||||||
import {
|
|
||||||
handleImageUploadInput,
|
|
||||||
readFileAsBase64,
|
|
||||||
resizeImageToFitLimits,
|
|
||||||
uploadImageLegacy,
|
|
||||||
withUploadTimeout
|
|
||||||
} from '../../shared/utils/imageUpload';
|
|
||||||
import { useAppearanceSettingsStore, useAvatarStore, useModalStore, useUserStore } from '../../stores';
|
import { useAppearanceSettingsStore, useAvatarStore, useModalStore, useUserStore } from '../../stores';
|
||||||
import { ContextMenuContent, ContextMenuItem, ContextMenuSeparator } from '../../components/ui/context-menu';
|
import { ContextMenuContent, ContextMenuItem, ContextMenuSeparator } from '../../components/ui/context-menu';
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '../../components/ui/dropdown-menu';
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '../../components/ui/dropdown-menu';
|
||||||
import { Field, FieldContent, FieldLabel } from '../../components/ui/field';
|
import { Field, FieldContent, FieldLabel } from '../../components/ui/field';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '../../components/ui/popover';
|
import { Popover, PopoverContent, PopoverTrigger } from '../../components/ui/popover';
|
||||||
|
import {
|
||||||
|
applyAvatar,
|
||||||
|
selectAvatarWithoutConfirmation,
|
||||||
|
showAvatarDialog
|
||||||
|
} from '../../coordinators/avatarCoordinator';
|
||||||
|
import {
|
||||||
|
handleImageUploadInput,
|
||||||
|
resizeImageToFitLimits,
|
||||||
|
uploadImageLegacy
|
||||||
|
} from '../../coordinators/imageUploadCoordinator';
|
||||||
import { DataTableEmpty, DataTableLayout } from '../../components/ui/data-table';
|
import { DataTableEmpty, DataTableLayout } from '../../components/ui/data-table';
|
||||||
import { ToggleGroup, ToggleGroupItem } from '../../components/ui/toggle-group';
|
import { ToggleGroup, ToggleGroupItem } from '../../components/ui/toggle-group';
|
||||||
|
import { readFileAsBase64, withUploadTimeout } from '../../shared/utils/imageUpload';
|
||||||
import { Badge } from '../../components/ui/badge';
|
import { Badge } from '../../components/ui/badge';
|
||||||
import { Button } from '../../components/ui/button';
|
import { Button } from '../../components/ui/button';
|
||||||
import { Input } from '../../components/ui/input';
|
import { Input } from '../../components/ui/input';
|
||||||
@@ -347,7 +351,6 @@
|
|||||||
import ManageTagsDialog from './ManageTagsDialog.vue';
|
import ManageTagsDialog from './ManageTagsDialog.vue';
|
||||||
import MyAvatarCard from './components/MyAvatarCard.vue';
|
import MyAvatarCard from './components/MyAvatarCard.vue';
|
||||||
import configRepository from '../../services/config.js';
|
import configRepository from '../../services/config.js';
|
||||||
import { showAvatarDialog, selectAvatarWithoutConfirmation, applyAvatar } from '../../coordinators/avatarCoordinator';
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const appearanceSettingsStore = useAppearanceSettingsStore();
|
const appearanceSettingsStore = useAppearanceSettingsStore();
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ const mocks = vi.hoisted(() => ({
|
|||||||
gameStore: {
|
gameStore: {
|
||||||
isGameRunning: { value: true }
|
isGameRunning: { value: true }
|
||||||
},
|
},
|
||||||
|
instanceStore: {
|
||||||
|
cachedInstances: new Map()
|
||||||
|
},
|
||||||
configRepository: {
|
configRepository: {
|
||||||
getBool: vi.fn(),
|
getBool: vi.fn(),
|
||||||
setBool: vi.fn()
|
setBool: vi.fn()
|
||||||
@@ -111,6 +114,7 @@ vi.mock('../../../../stores', () => ({
|
|||||||
useGameStore: () => mocks.gameStore,
|
useGameStore: () => mocks.gameStore,
|
||||||
useLaunchStore: () => mocks.launchStore,
|
useLaunchStore: () => mocks.launchStore,
|
||||||
useLocationStore: () => mocks.locationStore,
|
useLocationStore: () => mocks.locationStore,
|
||||||
|
useInstanceStore: () => mocks.instanceStore,
|
||||||
useUserStore: () => mocks.userStore
|
useUserStore: () => mocks.userStore
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -233,6 +237,7 @@ describe('FriendsSidebar.vue', () => {
|
|||||||
mocks.friendStore.activeFriends.value = [];
|
mocks.friendStore.activeFriends.value = [];
|
||||||
mocks.friendStore.offlineFriends.value = [];
|
mocks.friendStore.offlineFriends.value = [];
|
||||||
mocks.friendStore.friendsInSameInstance.value = [];
|
mocks.friendStore.friendsInSameInstance.value = [];
|
||||||
|
mocks.instanceStore.cachedInstances = new Map();
|
||||||
|
|
||||||
mocks.appearanceStore.isSidebarGroupByInstance.value = false;
|
mocks.appearanceStore.isSidebarGroupByInstance.value = false;
|
||||||
mocks.appearanceStore.isHideFriendsInSameInstance.value = false;
|
mocks.appearanceStore.isHideFriendsInSameInstance.value = false;
|
||||||
|
|||||||
@@ -18,15 +18,25 @@ const mocks = vi.hoisted(() => ({
|
|||||||
},
|
},
|
||||||
userStore: {
|
userStore: {
|
||||||
cachedUsers: new Map(),
|
cachedUsers: new Map(),
|
||||||
showSendBoopDialog: vi.fn()
|
showSendBoopDialog: vi.fn(),
|
||||||
|
currentUser: { id: 'usr_me' }
|
||||||
|
},
|
||||||
|
friendStore: {
|
||||||
|
friends: new Map()
|
||||||
},
|
},
|
||||||
groupStore: {},
|
groupStore: {},
|
||||||
locationStore: {
|
locationStore: {
|
||||||
lastLocation: { value: { location: 'wrld_home:123' } }
|
lastLocation: {
|
||||||
|
location: 'wrld_home:123',
|
||||||
|
value: { location: 'wrld_home:123' }
|
||||||
|
}
|
||||||
},
|
},
|
||||||
gameStore: {
|
gameStore: {
|
||||||
isGameRunning: { value: true }
|
isGameRunning: { value: true }
|
||||||
},
|
},
|
||||||
|
instanceStore: {
|
||||||
|
cachedInstances: new Map()
|
||||||
|
},
|
||||||
showUserDialog: vi.fn(),
|
showUserDialog: vi.fn(),
|
||||||
showGroupDialog: vi.fn()
|
showGroupDialog: vi.fn()
|
||||||
}));
|
}));
|
||||||
@@ -42,9 +52,11 @@ vi.mock('pinia', async (importOriginal) => {
|
|||||||
vi.mock('../../../../stores', () => ({
|
vi.mock('../../../../stores', () => ({
|
||||||
useNotificationStore: () => mocks.notificationStore,
|
useNotificationStore: () => mocks.notificationStore,
|
||||||
useUserStore: () => mocks.userStore,
|
useUserStore: () => mocks.userStore,
|
||||||
|
useFriendStore: () => mocks.friendStore,
|
||||||
useGroupStore: () => mocks.groupStore,
|
useGroupStore: () => mocks.groupStore,
|
||||||
useLocationStore: () => mocks.locationStore,
|
useLocationStore: () => mocks.locationStore,
|
||||||
useGameStore: () => mocks.gameStore
|
useGameStore: () => mocks.gameStore,
|
||||||
|
useInstanceStore: () => mocks.instanceStore
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../../../coordinators/userCoordinator', () => ({
|
vi.mock('../../../../coordinators/userCoordinator', () => ({
|
||||||
@@ -60,6 +72,13 @@ vi.mock('../../../../shared/utils', () => ({
|
|||||||
userImage: vi.fn(() => 'https://example.com/avatar.png')
|
userImage: vi.fn(() => 'https://example.com/avatar.png')
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../../composables/useUserDisplay', () => ({
|
||||||
|
useUserDisplay: () => ({
|
||||||
|
userImage: vi.fn(() => 'https://example.com/avatar.png'),
|
||||||
|
userStatusClass: vi.fn(() => '')
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock('vue-i18n', () => ({
|
vi.mock('vue-i18n', () => ({
|
||||||
useI18n: () => ({
|
useI18n: () => ({
|
||||||
t: (key) => key,
|
t: (key) => key,
|
||||||
@@ -170,6 +189,8 @@ describe('NotificationItem.vue', () => {
|
|||||||
mocks.userStore.showSendBoopDialog.mockReset();
|
mocks.userStore.showSendBoopDialog.mockReset();
|
||||||
mocks.showGroupDialog.mockReset();
|
mocks.showGroupDialog.mockReset();
|
||||||
mocks.userStore.cachedUsers = new Map();
|
mocks.userStore.cachedUsers = new Map();
|
||||||
|
mocks.friendStore.friends = new Map();
|
||||||
|
mocks.instanceStore.cachedInstances = new Map();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('renders sender and opens user dialog on sender click', async () => {
|
test('renders sender and opens user dialog on sender click', async () => {
|
||||||
|
|||||||
@@ -586,7 +586,8 @@
|
|||||||
} from '../../shared/utils';
|
} from '../../shared/utils';
|
||||||
import { inventoryRequest, miscRequest, userRequest, vrcPlusIconRequest, vrcPlusImageRequest } from '../../api';
|
import { inventoryRequest, miscRequest, userRequest, vrcPlusIconRequest, vrcPlusImageRequest } from '../../api';
|
||||||
import { useAdvancedSettingsStore, useAuthStore, useGalleryStore, useModalStore, useUserStore } from '../../stores';
|
import { useAdvancedSettingsStore, useAuthStore, useGalleryStore, useModalStore, useUserStore } from '../../stores';
|
||||||
import { handleImageUploadInput, readFileAsBase64, withUploadTimeout } from '../../shared/utils/imageUpload';
|
import { readFileAsBase64, withUploadTimeout } from '../../shared/utils/imageUpload';
|
||||||
|
import { handleImageUploadInput } from '../../coordinators/imageUploadCoordinator';
|
||||||
import { emojiAnimationStyleList, emojiAnimationStyleUrl } from '../../shared/constants';
|
import { emojiAnimationStyleList, emojiAnimationStyleUrl } from '../../shared/constants';
|
||||||
import { AppDebug } from '../../services/appConfig';
|
import { AppDebug } from '../../services/appConfig';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user