Files
VRCX/src/shared/utils/common.js
2026-02-10 19:11:24 +13:00

556 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner';
import Noty from 'noty';
import {
useAvatarStore,
useInstanceStore,
useModalStore,
useSearchStore,
useWorldStore
} from '../../stores';
import { AppDebug } from '../../service/appConfig.js';
import { compareUnityVersion } from './avatar';
import { escapeTag } from './base/string';
import { miscRequest } from '../../api';
/**
*
* @param {object} unityPackages
* @returns
*/
function getAvailablePlatforms(unityPackages) {
let isPC = false;
let isQuest = false;
let isIos = false;
if (typeof unityPackages === 'object') {
for (const unityPackage of unityPackages) {
if (
unityPackage.variant &&
unityPackage.variant !== 'standard' &&
unityPackage.variant !== 'security'
) {
continue;
}
if (unityPackage.platform === 'standalonewindows') {
isPC = true;
} else if (unityPackage.platform === 'android') {
isQuest = true;
} else if (unityPackage.platform === 'ios') {
isIos = true;
}
}
}
return { isPC, isQuest, isIos };
}
/**
* @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 {
new Noty({
type: 'error',
text: escapeTag('Failed to download JSON.')
}).show();
}
}
async function deleteVRChatCache(ref) {
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)
) {
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: '' };
}
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)
) {
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('Copy failed!');
});
}
/**
*
* @param {string} resource
* @returns {string}
*/
function getFaviconUrl(resource) {
if (!resource) {
return '';
}
try {
const url = new URL(resource);
return `https://icons.duckduckgo.com/ip2/${url.host}.ico`;
} catch (err) {
console.error('Invalid URL:', resource, err);
return '';
}
}
/**
*
* @param {string} url
* @param {number} resolution
* @returns {string}
*/
function convertFileUrlToImageUrl(url, resolution = 128) {
if (!url) {
return '';
}
/**
* possible patterns?
* /file/file_fileId/version
* /file/file_fileId/version/
* /file/file_fileId/version/file
* /file/file_fileId/version/file/
*/
const pattern = /file\/file_([a-f0-9-]+)\/(\d+)(\/file)?\/?$/;
const match = url.match(pattern);
if (match) {
const fileId = match[1];
const version = match[2];
return `${AppDebug.endpointDomain}/image/file_${fileId}/${version}/${resolution}`;
}
// no match return origin url
return url;
}
/**
*
* @param {string} url
* @returns {string}
*/
function replaceVrcPackageUrl(url) {
if (!url) {
return '';
}
return url.replace('https://api.vrchat.cloud/', 'https://vrchat.com/');
}
/**
*
* @param {string} s
* @returns {string}
*/
function extractFileId(s) {
const match = String(s).match(/file_[0-9A-Za-z-]+/);
return match ? match[0] : '';
}
/**
*
* @param {string} s
* @returns {string}
*/
function extractFileVersion(s) {
const match = /(?:\/file_[0-9A-Za-z-]+\/)([0-9]+)/gi.exec(s);
return match ? match[1] : '';
}
/**
*
* @param {string} url
* @returns {string}
*/
function extractVariantVersion(url) {
if (!url) {
return '0';
}
try {
const params = new URLSearchParams(new URL(url).search);
const version = params.get('v');
if (version) {
return version;
}
return '0';
} catch {
return '0';
}
}
/**
*
* @param {object} json
* @returns {Array}
*/
function buildTreeData(json) {
const node = [];
for (const key in json) {
if (key[0] === '$') {
continue;
}
const value = json[key];
if (Array.isArray(value) && value.length === 0) {
node.push({
key,
value: '[]'
});
} else if (value === Object(value) && Object.keys(value).length === 0) {
node.push({
key,
value: '{}'
});
} else if (Array.isArray(value)) {
node.push({
children: value.map((val, idx) => {
if (val === Object(val)) {
return {
children: buildTreeData(val),
key: idx
};
}
return {
key: idx,
value: val
};
}),
key
});
} else if (value === Object(value)) {
node.push({
children: buildTreeData(value),
key
});
} else {
node.push({
key,
value: String(value)
});
}
}
node.sort(function (a, b) {
const A = String(a.key).toUpperCase();
const B = String(b.key).toUpperCase();
// sort _ to top
if (A.startsWith('_') && !B.startsWith('_')) {
return -1;
}
if (B.startsWith('_') && !A.startsWith('_')) {
return 1;
}
if (A < B) {
return -1;
}
if (A > B) {
return 1;
}
return 0;
});
return node;
}
/**
*
* @param {string} text
* @returns {string}
*/
function replaceBioSymbols(text) {
if (typeof text !== 'string') {
return '';
}
const symbolList = {
'@': '',
'#': '',
$: '',
'%': '',
'&': '',
'=': '',
'+': '',
'/': '',
'\\': '',
';': ';',
':': '˸',
',': '',
'?': '',
'!': 'ǃ',
'"': '',
'<': '≺',
'>': '≻',
'.': '',
'^': '',
'{': '',
'}': '',
'[': '',
']': '',
'(': '',
')': '',
'|': '',
'*': ''
};
let newText = text;
for (const key in symbolList) {
const regex = new RegExp(symbolList[key], 'g');
newText = newText.replace(regex, key);
}
return newText.replace(/ {1,}/g, ' ').trimRight();
}
/**
*
* @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!');
});
}
/**
*
* @param {object} ref
* @returns {Promise<object>}
*/
async function getBundleDateSize(ref) {
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)) {
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 miscRequest.getFileAnalysis({
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) {
let timer = null;
return function (...args) {
const context = this;
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(context, args);
}, delay);
};
}
export {
getAvailablePlatforms,
downloadAndSaveJson,
deleteVRChatCache,
checkVRChatCache,
copyToClipboard,
getFaviconUrl,
convertFileUrlToImageUrl,
replaceVrcPackageUrl,
extractFileId,
extractFileVersion,
extractVariantVersion,
buildTreeData,
replaceBioSymbols,
openExternalLink,
openDiscordProfile,
getBundleDateSize,
openFolderGeneric,
debounce
};