refactor: app.js (#1291)

* refactor: frontend

* Fix avatar gallery sort

* Update .NET dependencies

* Update npm dependencies

electron v37.1.0

* bulkRefreshFriends

* fix dark theme

* Remove crowdin

* Fix config.json dialog not updating

* VRCX log file fixes & add Cef log

* Remove SharedVariable, fix startup

* Revert init theme change

* Logging date not working? Fix WinformThemer designer error

* Add Cef request hander, no more escaping main page

* clean

* fix

* fix

* clean

* uh

* Apply thememode at startup, fixes random user colours

* Split database into files

* Instance info remove empty lines

* Open external VRC links with VRCX

* Electron fixes

* fix userdialog style

* ohhhh

* fix store

* fix store

* fix: load all group members after kicking a user

* fix: world dialog favorite button style

* fix: Clear VRCX Cache Timer input value

* clean

* Fix VR overlay

* Fix VR overlay 2

* Fix Discord discord rich presence for RPC worlds

* Clean up age verified user tags

* Fix playerList being occupied after program reload

* no `this`

* Fix login stuck loading

* writable: false

* Hide dialogs on logout

* add flush sync option

* rm LOGIN event

* rm LOGOUT event

* remove duplicate event listeners

* remove duplicate event listeners

* clean

* remove duplicate event listeners

* clean

* fix theme style

* fix t

* clearable

* clean

* fix ipcEvent

* Small changes

* Popcorn Palace support

* Remove checkActiveFriends

* Clean up

* Fix dragEnterCef

* Block API requests when not logged in

* Clear state on login & logout

* Fix worldDialog instances not updating

* use <script setup>

* Fix avatar change event, CheckGameRunning at startup

* Fix image dragging

* fix

* Remove PWI

* fix updateLoop

* add webpack-dev-server to dev environment

* rm unnecessary chunks

* use <script setup>

* webpack-dev-server changes

* use <script setup>

* use <script setup>

* Fix UGC text size

* Split login event

* t

* use <script setup>

* fix

* Update .gitignore and enable checkJs in jsconfig

* fix i18n t

* use <script setup>

* use <script setup>

* clean

* global types

* fix

* use checkJs for debugging

* Add watchState for login watchers

* fix .vue template

* type fixes

* rm Vue.filter

* Cef v138.0.170, VC++ 2022

* Settings fixes

* Remove 'USER:CURRENT'

* clean up 2FA callbacks

* remove userApply

* rm i18n import

* notification handling to use notification store methods

* refactor favorite handling to use favorite store methods and clean up event emissions

* refactor moderation handling to use dedicated functions for player moderation events

* refactor friend handling to use dedicated functions for friend events

* Fix program startup, move lang init

* Fix friend state

* Fix status change error

* Fix user notes diff

* fix

* rm group event

* rm auth event

* rm avatar event

* clean

* clean

* getUser

* getFriends

* getFavoriteWorlds, getFavoriteAvatars

* AvatarGalleryUpload btn style & package.json update

* Fix friend requests

* Apply user

* Apply world

* Fix note diff

* Fix VR overlay

* Fixes

* Update build scripts

* Apply avatar

* Apply instance

* Apply group

* update hidden VRC+ badge

* Fix sameInstance "private"

* fix 502/504 API errors

* fix 502/504 API errors

* clean

* Fix friend in same instance on orange showing twice in friends list

* Add back in broken friend state repair methods

* add types

---------

Co-authored-by: Natsumi <cmcooper123@hotmail.com>
This commit is contained in:
pa
2025-07-14 12:00:08 +09:00
committed by GitHub
parent 952fd77ed5
commit f4f78bb5ec
323 changed files with 47745 additions and 43326 deletions

541
src/shared/utils/common.js Normal file
View File

@@ -0,0 +1,541 @@
import Noty from 'noty';
import { storeToRefs } from 'pinia';
import { miscRequest } from '../../api';
import { $app } from '../../app';
import {
useAvatarStore,
useInstanceStore,
useWorldStore,
useSearchStore
} from '../../stores';
import { compareUnityVersion } from './avatar';
import { escapeTag } from './base/string';
/**
*
* @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 !== 'standard') {
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 !== 'standard') {
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: '' };
}
return AssetBundleManager.CheckVRChatCache(
id,
version,
variant,
variantVersion
);
}
/**
*
* @param {string} text
* @param {string} message
*/
function copyToClipboard(text, message = 'Copied successfully!') {
navigator.clipboard
.writeText(text)
.then(() => {
$app.$message({
message: message,
type: 'success'
});
})
.catch((err) => {
console.error('Copy failed:', err);
$app.$message.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 `https://api.vrchat.cloud/api/1/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();
if (A < B) {
return -1;
}
if (A > B) {
return 1;
}
return 0;
});
return node;
}
/**
*
* @param {string} text
* @returns {string}
*/
function replaceBioSymbols(text) {
if (!text) {
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;
}
$app.$confirm(`${link}`, 'Open External Link', {
distinguishCancelAndClose: true,
confirmButtonText: 'Open',
cancelButtonText: 'Copy',
type: 'info',
callback: (action) => {
if (action === 'confirm') {
AppApi.OpenLink(link);
} else if (action === 'cancel') {
copyLink(link);
}
}
});
}
/**
*
* @param {string} text
*/
function copyLink(text) {
$app.$message({
message: 'Link copied to clipboard',
type: 'success'
});
copyToClipboard(text);
}
/**
*
* @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 bundleSizes = [];
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 (!compareUnityVersion(unityPackage.unitySortNumber)) {
continue;
}
const platform = unityPackage.platform;
if (bundleSizes[platform]) {
continue;
}
const assetUrl = unityPackage.assetUrl;
const fileId = extractFileId(assetUrl);
const fileVersion = parseInt(extractFileVersion(assetUrl), 10);
if (!fileId) {
continue;
}
const args = await miscRequest.getBundles(fileId);
if (!args?.json?.versions) {
continue;
}
let { versions } = args.json;
for (let j = versions.length - 1; j > -1; j--) {
const version = versions[j];
if (version.version === fileVersion) {
const createdAt = version.created_at;
const fileSize = `${(
version.file.sizeInBytes / 1048576
).toFixed(2)} MB`;
bundleSizes[platform] = {
createdAt,
fileSize
};
// update avatar dialog
if (avatarDialog.value.id === ref.id) {
avatarDialog.value.bundleSizes[platform] =
bundleSizes[platform];
if (avatarDialog.value.lastUpdated < version.created_at) {
avatarDialog.value.lastUpdated = version.created_at;
}
}
// update world dialog
if (worldDialog.value.id === ref.id) {
worldDialog.value.bundleSizes[platform] =
bundleSizes[platform];
if (worldDialog.value.lastUpdated < version.created_at) {
worldDialog.value.lastUpdated = version.created_at;
}
}
// update player list
if (currentInstanceLocation.value.worldId === ref.id) {
currentInstanceWorld.value.bundleSizes[platform] =
bundleSizes[platform];
if (
currentInstanceWorld.value.lastUpdated <
version.created_at
) {
currentInstanceWorld.value.lastUpdated =
version.created_at;
}
}
break;
}
}
}
return bundleSizes;
}
// #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,
copyLink,
getBundleDateSize,
openFolderGeneric,
debounce
};