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

View File

@@ -0,0 +1,35 @@
import {
checkVRChatCache,
deleteVRChatCache,
displayLocation,
extractFileId,
extractFileVersion,
extractVariantVersion,
getAvailablePlatforms,
getBundleLocation,
getLaunchURL,
getPrintFileName,
getPrintLocalDate,
isFriendOnline,
isRealInstance,
parseLocation
} from './index';
const utils = {
getAvailablePlatforms,
deleteVRChatCache,
checkVRChatCache,
getLaunchURL,
extractFileId,
extractFileVersion,
extractVariantVersion,
isRealInstance,
displayLocation,
parseLocation,
getPrintFileName,
getPrintLocalDate,
isFriendOnline,
getBundleLocation
};
export { utils };

148
src/shared/utils/avatar.js Normal file
View File

@@ -0,0 +1,148 @@
import { useAuthStore } from '../../stores';
import { replaceBioSymbols } from './common';
/**
*
* @param {object} args
* @param {Map} cachedAvatarNames
* @returns
*/
function storeAvatarImage(args, cachedAvatarNames) {
const refCreatedAt = args.json.versions[0];
const fileCreatedAt = refCreatedAt.created_at;
const fileId = args.params.fileId;
let avatarName = '';
const imageName = args.json.name;
const avatarNameRegex = /Avatar - (.*) - Image -/gi.exec(imageName);
if (avatarNameRegex) {
avatarName = replaceBioSymbols(avatarNameRegex[1]);
}
const ownerId = args.json.ownerId;
const avatarInfo = {
ownerId,
avatarName,
fileCreatedAt
};
cachedAvatarNames.set(fileId, avatarInfo);
return avatarInfo;
}
/**
*
* @param {string} avatar
* @returns {string|null}
*/
function parseAvatarUrl(avatar) {
const url = new URL(avatar);
const urlPath = url.pathname;
if (urlPath.substring(5, 13) === '/avatar/') {
const avatarId = urlPath.substring(13);
return avatarId;
}
return null;
}
/**
*
* @param {object} unityPackages
* @returns
*/
function getPlatformInfo(unityPackages) {
let pc = {};
let android = {};
let ios = {};
if (typeof unityPackages === 'object') {
for (const unityPackage of unityPackages) {
if (
unityPackage.variant &&
unityPackage.variant !== 'standard' &&
unityPackage.variant !== 'security'
) {
continue;
}
if (unityPackage.platform === 'standalonewindows') {
if (
unityPackage.performanceRating === 'None' &&
pc.performanceRating
) {
continue;
}
pc = unityPackage;
} else if (unityPackage.platform === 'android') {
if (
unityPackage.performanceRating === 'None' &&
android.performanceRating
) {
continue;
}
android = unityPackage;
} else if (unityPackage.platform === 'ios') {
if (
unityPackage.performanceRating === 'None' &&
ios.performanceRating
) {
continue;
}
ios = unityPackage;
}
}
}
return { pc, android, ios };
}
/**
*
* @param {string} unitySortNumber
* @returns {boolean}
*/
function compareUnityVersion(unitySortNumber) {
const authStore = useAuthStore();
if (!authStore.cachedConfig.sdkUnityVersion) {
console.error('No cachedConfig.sdkUnityVersion');
return false;
}
// 2022.3.6f1 2022 03 06 000
// 2019.4.31f1 2019 04 31 000
// 5.3.4p1 5 03 04 010
// 2019.4.31f1c1 is a thing
const array = authStore.cachedConfig.sdkUnityVersion.split('.');
if (array.length < 3) {
console.error('Invalid cachedConfig.sdkUnityVersion');
return false;
}
let currentUnityVersion = array[0];
currentUnityVersion += array[1].padStart(2, '0');
const indexFirstLetter = array[2].search(/[a-zA-Z]/);
if (indexFirstLetter > -1) {
currentUnityVersion += array[2]
.substr(0, indexFirstLetter)
.padStart(2, '0');
currentUnityVersion += '0';
const letter = array[2].substr(indexFirstLetter, 1);
if (letter === 'p') {
currentUnityVersion += '1';
} else {
// f
currentUnityVersion += '0';
}
currentUnityVersion += '0';
} else {
// just in case
currentUnityVersion += '000';
}
// just in case
currentUnityVersion = currentUnityVersion.replace(/\D/g, '');
if (parseInt(unitySortNumber, 10) <= parseInt(currentUnityVersion, 10)) {
return true;
}
return false;
}
export {
storeAvatarImage,
parseAvatarUrl,
getPlatformInfo,
compareUnityVersion
};

View File

@@ -0,0 +1,59 @@
/**
*
* @param {array} array
* @param {*} item
* @returns {boolean}
*/
function removeFromArray(array, item) {
const { length } = array;
for (let i = 0; i < length; ++i) {
if (array[i] === item) {
array.splice(i, 1);
return true;
}
}
return false;
}
/**
*
* @param {array} a
* @param {array} b
* @returns {boolean}
*/
function arraysMatch(a, b) {
if (!Array.isArray(a) || !Array.isArray(b)) {
return false;
}
return (
a.length === b.length &&
a.every(
(element, index) =>
JSON.stringify(element) === JSON.stringify(b[index])
)
);
}
/**
*
* @param {array} array
* @param {number} fromIndex
* @param {number} toIndex
* @returns {void}
*/
function moveArrayItem(array, fromIndex, toIndex) {
if (!Array.isArray(array) || fromIndex === toIndex) {
return;
}
if (fromIndex < 0 || fromIndex >= array.length) {
return;
}
if (toIndex < 0 || toIndex >= array.length) {
return;
}
const item = array[fromIndex];
array.splice(fromIndex, 1);
array.splice(toIndex, 0, item);
}
export { removeFromArray, arraysMatch, moveArrayItem };

View File

@@ -0,0 +1,80 @@
import { useAppearanceSettingsStore } from '../../../stores';
/**
* @param {string} dateStr
* @param {'long'|'short'} format
* @returns {string}
*/
function formatDateFilter(dateStr, format) {
const appearance = useAppearanceSettingsStore();
const {
dtIsoFormat: isoFormat,
dtHour12: hour12,
currentCulture
} = appearance;
if (!dateStr) {
return '-';
}
const dt = new Date(dateStr);
if (isNaN(dt.getTime())) {
return '-';
}
function padZero(num) {
return String(num).padStart(2, '0');
}
function toIsoLong(date) {
const y = date.getFullYear();
const m = padZero(date.getMonth() + 1);
const d = padZero(date.getDate());
const hh = padZero(date.getHours());
const mm = padZero(date.getMinutes());
const ss = padZero(date.getSeconds());
return `${y}-${m}-${d} ${hh}:${mm}:${ss}`;
}
function toLocalShort(date) {
return date
.toLocaleDateString(isoFormat ? 'en-nz' : currentCulture, {
month: '2-digit',
day: '2-digit',
hour: 'numeric',
minute: 'numeric',
hourCycle: hour12 ? 'h12' : 'h23'
})
.replace(' AM', 'am')
.replace(' PM', 'pm')
.replace(',', '');
}
if (isoFormat) {
if (format === 'long') {
return toIsoLong(dt);
}
if (format === 'short') {
return toLocalShort(dt);
}
} else {
if (format === 'long') {
return dt.toLocaleDateString(currentCulture, {
month: '2-digit',
day: '2-digit',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
hourCycle: hour12 ? 'h12' : 'h23'
});
}
if (format === 'short') {
return toLocalShort(dt);
}
}
return '-';
}
export { formatDateFilter };

View File

@@ -0,0 +1,102 @@
import { useAvatarStore, useWorldStore } from '../../../stores';
import { compareUnityVersion } from '../avatar';
import {
extractFileId,
extractFileVersion,
extractVariantVersion
} from '../common';
/**
*
* @param {string} input
* @returns {Promise<string|null>}
*/
async function getBundleLocation(input) {
const worldStore = useWorldStore();
const avatarStore = useAvatarStore();
let unityPackage;
let unityPackages;
let assetUrl = input;
let variant = '';
if (assetUrl) {
// continue
} else if (
avatarStore.avatarDialog.visible &&
avatarStore.avatarDialog.ref.unityPackages.length > 0
) {
unityPackages = avatarStore.avatarDialog.ref.unityPackages;
for (let i = unityPackages.length - 1; i > -1; i--) {
unityPackage = 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;
}
}
} else if (
avatarStore.avatarDialog.visible &&
avatarStore.avatarDialog.ref.assetUrl
) {
assetUrl = avatarStore.avatarDialog.ref.assetUrl;
} else if (
worldStore.worldDialog.visible &&
worldStore.worldDialog.ref.unityPackages.length > 0
) {
unityPackages = worldStore.worldDialog.ref.unityPackages;
for (let i = unityPackages.length - 1; i > -1; i--) {
unityPackage = unityPackages[i];
if (
unityPackage.platform === 'standalonewindows' &&
compareUnityVersion(unityPackage.unitySortNumber)
) {
assetUrl = unityPackage.assetUrl;
break;
}
}
} else if (
worldStore.worldDialog.visible &&
worldStore.worldDialog.ref.assetUrl
) {
assetUrl = worldStore.worldDialog.ref.assetUrl;
}
if (!assetUrl) {
return null;
}
const fileId = extractFileId(assetUrl);
const fileVersion = parseInt(extractFileVersion(assetUrl), 10);
const variantVersion = parseInt(extractVariantVersion(assetUrl), 10);
const assetLocation = await AssetBundleManager.GetVRChatCacheFullLocation(
fileId,
fileVersion,
variant,
variantVersion
);
const cacheInfo = await AssetBundleManager.CheckVRChatCache(
fileId,
fileVersion,
variant,
variantVersion
);
let inCache = false;
if (cacheInfo.Item1 > 0) {
inCache = true;
}
console.log(`InCache: ${inCache}`);
const fullAssetLocation = `${assetLocation}\\__data`;
console.log(fullAssetLocation);
return fullAssetLocation;
}
export { getBundleLocation };

View File

@@ -0,0 +1,97 @@
import { escapeTag } from './string';
/**
*
* @param {number} sec
* @param {boolean} isNeedSeconds
* @returns {string}
*/
function timeToText(sec, isNeedSeconds = false) {
let n = Number(sec);
if (isNaN(n)) {
return escapeTag(sec);
}
n = Math.floor(n / 1000);
const arr = [];
if (n < 0) {
n = -n;
}
if (n >= 86400) {
arr.push(`${Math.floor(n / 86400)}d`);
n %= 86400;
}
if (n >= 3600) {
arr.push(`${Math.floor(n / 3600)}h`);
n %= 3600;
}
if (n >= 60) {
arr.push(`${Math.floor(n / 60)}m`);
n %= 60;
}
if (isNeedSeconds || (arr.length === 0 && n < 60)) {
arr.push(`${n}s`);
}
return arr.join(' ');
}
/**
*
* @param {number} duration
* @returns {string}
*/
function formatSeconds(duration) {
const pad = function (num, size) {
return `000${num}`.slice(size * -1);
},
time = parseFloat(duration).toFixed(3),
hours = Math.floor(time / 60 / 60),
minutes = Math.floor(time / 60) % 60,
seconds = Math.floor(time - minutes * 60);
let hoursOut = '';
if (hours > '0') {
hoursOut = `${pad(hours, 2)}:`;
}
return `${hoursOut + pad(minutes, 2)}:${pad(seconds, 2)}`;
}
/**
*
* @param {string} duration
* @returns {number}
*/
function convertYoutubeTime(duration) {
let a = duration.match(/\d+/g);
if (
duration.indexOf('M') >= 0 &&
duration.indexOf('H') === -1 &&
duration.indexOf('S') === -1
) {
a = [0, a[0], 0];
}
if (duration.indexOf('H') >= 0 && duration.indexOf('M') === -1) {
a = [a[0], 0, a[1]];
}
if (
duration.indexOf('H') >= 0 &&
duration.indexOf('M') === -1 &&
duration.indexOf('S') === -1
) {
a = [a[0], 0, 0];
}
let length = 0;
if (a.length === 3) {
length += parseInt(a[0], 10) * 3600;
length += parseInt(a[1], 10) * 60;
length += parseInt(a[2], 10);
}
if (a.length === 2) {
length += parseInt(a[0], 10) * 60;
length += parseInt(a[1], 10);
}
if (a.length === 1) {
length += parseInt(a[0], 10);
}
return length;
}
export { timeToText, formatSeconds, convertYoutubeTime };

View File

@@ -0,0 +1,105 @@
/**
*
* @param {string} tag
* @returns {string}
*/
function escapeTag(tag) {
const s = String(tag);
return s.replace(/["&'<>]/g, (c) => `&#${c.charCodeAt(0)};`);
}
/**
*
* @param {object} obj
* @returns {object}
*/
function escapeTagRecursive(obj) {
if (typeof obj === 'string') {
return escapeTag(obj);
}
if (typeof obj === 'object') {
for (const key in obj) {
obj[key] = escapeTagRecursive(obj[key]);
}
}
return obj;
}
/**
*
* @param {string} text
* @returns {string}
*/
function textToHex(text) {
const s = String(text);
return s
.split('')
.map((c) => c.charCodeAt(0).toString(16))
.join(' ');
}
/**
*
* @param {number} num
* @returns {string}
*/
function commaNumber(num) {
if (!num) {
return '0';
}
const s = String(Number(num));
return s.replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
}
/**
*
* @param {string} str
* @param {string} search
* @param {object} comparer
* @returns {boolean}
*/
function localeIncludes(str, search, comparer) {
// These checks are stolen from https://stackoverflow.com/a/69623589/11030436
if (search === '') {
return true;
} else if (!str || !search) {
return false;
}
const strObj = String(str);
const searchObj = String(search);
if (strObj.length === 0) {
return false;
}
if (searchObj.length > strObj.length) {
return false;
}
// Now simply loop through each substring and compare them
for (let i = 0; i < str.length - searchObj.length + 1; i++) {
const substr = strObj.substring(i, i + searchObj.length);
if (comparer.compare(substr, searchObj) === 0) {
return true;
}
}
return false;
}
/**
*
* @param {string} text
* @returns {string}
*/
function changeLogRemoveLinks(text) {
return text.replace(/([^!])\[[^\]]+\]\([^)]+\)/g, '$1');
}
export {
escapeTag,
escapeTagRecursive,
textToHex,
commaNumber,
localeIncludes,
changeLogRemoveLinks
};

284
src/shared/utils/base/ui.js Normal file
View File

@@ -0,0 +1,284 @@
import { storeToRefs } from 'pinia';
import { useAppearanceSettingsStore } from '../../../stores';
/**
*
* @returns {boolean}
*/
function systemIsDarkMode() {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
/**
*
* @param {boolean}isDark
*/
function changeAppDarkStyle(isDark) {
if (isDark) {
AppApi.ChangeTheme(1);
} else {
AppApi.ChangeTheme(0);
}
}
/**
*
* @param {string} themeMode
* @returns
*/
function changeAppThemeStyle(themeMode) {
const themeStyle = {};
switch (themeMode) {
case 'light':
themeStyle.href = '';
break;
case 'dark':
themeStyle.href = '';
break;
case 'darkvanillaold':
themeStyle.href = 'theme.darkvanillaold.css';
break;
case 'darkvanilla':
themeStyle.href = 'theme.darkvanilla.css';
break;
case 'pink':
themeStyle.href = 'theme.pink.css';
break;
case 'material3':
themeStyle.href = 'theme.material3.css';
break;
case 'system':
themeStyle.href = '';
break;
}
/**
* prevents flickering
* giving absolute paths does prevent flickering
* when switching from another dark theme to 'dark' theme
* <del>works on my machine</del>
*/
let filePathPrefix = 'file://vrcx/';
if (LINUX) {
filePathPrefix = './';
}
let $appThemeStyle = document.getElementById('app-theme-style');
if (!$appThemeStyle) {
$appThemeStyle = document.createElement('link');
$appThemeStyle.setAttribute('id', 'app-theme-style');
$appThemeStyle.rel = 'stylesheet';
document.head.appendChild($appThemeStyle);
}
$appThemeStyle.href = themeStyle.href
? `${filePathPrefix}${themeStyle.href}`
: '';
let $appThemeDarkStyle = document.getElementById('app-theme-dark-style');
const darkThemeCssPath = `${filePathPrefix}theme.dark.css`;
if (!$appThemeDarkStyle && themeMode !== 'light') {
if (themeMode === 'system' && !systemIsDarkMode()) {
return;
}
$appThemeDarkStyle = document.createElement('link');
$appThemeDarkStyle.setAttribute('id', 'app-theme-dark-style');
$appThemeDarkStyle.rel = 'stylesheet';
$appThemeDarkStyle.href = darkThemeCssPath;
document.head.insertBefore($appThemeDarkStyle, $appThemeStyle);
} else {
if (themeMode === 'system' && systemIsDarkMode()) {
if ($appThemeDarkStyle.href === darkThemeCssPath) {
return;
}
$appThemeDarkStyle.href = darkThemeCssPath;
} else if (themeMode !== 'light' && themeMode !== 'system') {
if ($appThemeDarkStyle.href === darkThemeCssPath) {
return;
}
$appThemeDarkStyle.href = darkThemeCssPath;
} else {
$appThemeDarkStyle && $appThemeDarkStyle.remove();
}
}
}
/**
* CJK character in Japanese, Korean, Chinese are different
* so change font-family order when users change language to display CJK character correctly
* @param {string} lang
*/
function changeCJKFontsOrder(lang) {
const otherFonts = window
.getComputedStyle(document.body)
.fontFamily.split(',')
.filter((item) => !item.includes('Noto Sans'))
.join(', ');
const notoSans = 'Noto Sans';
const fontFamilies = {
ja_JP: ['JP', 'KR', 'TC', 'SC'],
ko: ['KR', 'JP', 'TC', 'SC'],
zh_TW: ['TC', 'JP', 'KR', 'SC'],
zh_CN: ['SC', 'JP', 'KR', 'TC']
};
if (fontFamilies[lang]) {
const CJKFamily = fontFamilies[lang]
.map((item) => `${notoSans} ${item}`)
.join(', ');
document.body.style.fontFamily = `${CJKFamily}, ${otherFonts}`;
}
}
/**
*
* @param {object} trustColor
*/
function updateTrustColorClasses(trustColor) {
if (document.getElementById('trustColor') !== null) {
document.getElementById('trustColor').outerHTML = '';
}
const style = document.createElement('style');
style.id = 'trustColor';
style.type = 'text/css';
let newCSS = '';
for (const rank in trustColor) {
newCSS += `.x-tag-${rank} { color: ${trustColor[rank]} !important; border-color: ${trustColor[rank]} !important; } `;
}
style.innerHTML = newCSS;
document.getElementsByTagName('head')[0].appendChild(style);
}
function refreshCustomCss() {
if (document.contains(document.getElementById('app-custom-style'))) {
document.getElementById('app-custom-style').remove();
}
AppApi.CustomCssPath().then((customCss) => {
const head = document.head;
if (customCss) {
const $appCustomStyle = document.createElement('link');
$appCustomStyle.setAttribute('id', 'app-custom-style');
$appCustomStyle.rel = 'stylesheet';
$appCustomStyle.href = `file://${customCss}?_=${Date.now()}`;
head.appendChild($appCustomStyle);
}
});
}
function refreshCustomScript() {
if (document.contains(document.getElementById('app-custom-script'))) {
document.getElementById('app-custom-script').remove();
}
AppApi.CustomScriptPath().then((customScript) => {
const head = document.head;
if (customScript) {
const $appCustomScript = document.createElement('script');
$appCustomScript.setAttribute('id', 'app-custom-script');
$appCustomScript.src = `file://${customScript}?_=${Date.now()}`;
head.appendChild($appCustomScript);
}
});
}
/**
*
* @param {number} hue
* @returns {string}
*/
function HueToHex(hue) {
const appSettingsStore = useAppearanceSettingsStore();
const { isDarkMode } = storeToRefs(appSettingsStore);
// this.HSVtoRGB(hue / 65535, .8, .8);
if (isDarkMode.value) {
return HSVtoRGB(hue / 65535, 0.6, 1);
}
return HSVtoRGB(hue / 65535, 1, 0.7);
}
/**
*
* @param {number} h
* @param {number} s
* @param {number} v
* @returns {string}
*/
function HSVtoRGB(h, s, v) {
let r = 0;
let g = 0;
let b = 0;
if (arguments.length === 1) {
s = h.s;
v = h.v;
h = h.h;
}
const i = Math.floor(h * 6);
const f = h * 6 - i;
const p = v * (1 - s);
const q = v * (1 - f * s);
const t = v * (1 - (1 - f) * s);
switch (i % 6) {
case 0:
r = v;
g = t;
b = p;
break;
case 1:
r = q;
g = v;
b = p;
break;
case 2:
r = p;
g = v;
b = t;
break;
case 3:
r = p;
g = q;
b = v;
break;
case 4:
r = t;
g = p;
b = v;
break;
case 5:
r = v;
g = p;
b = q;
break;
}
const red = Math.round(r * 255);
const green = Math.round(g * 255);
const blue = Math.round(b * 255);
const decColor = 0x1000000 + blue + 0x100 * green + 0x10000 * red;
return `#${decColor.toString(16).substr(1)}`;
}
function adjustDialogZ(el) {
let z = 0;
document.querySelectorAll('.v-modal,.el-dialog__wrapper').forEach((v) => {
const _z = Number(v.style.zIndex) || 0;
if (_z && _z > z && v !== el) {
z = _z;
}
});
if (z) {
el.style.zIndex = z + 1;
}
}
export {
systemIsDarkMode,
changeAppDarkStyle,
changeAppThemeStyle,
changeCJKFontsOrder,
updateTrustColorClasses,
refreshCustomCss,
refreshCustomScript,
HueToHex,
HSVtoRGB,
adjustDialogZ
};

13
src/shared/utils/chart.js Normal file
View File

@@ -0,0 +1,13 @@
let echarts = null;
// lazy load echarts
function loadEcharts() {
if (echarts) {
return Promise.resolve(echarts);
}
return import('echarts').then((module) => {
echarts = module;
return echarts;
});
}
export { loadEcharts };

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
};

262
src/shared/utils/compare.js Normal file
View File

@@ -0,0 +1,262 @@
import { sortStatus } from './friend';
/**
*
* @param {object} a
* @param {object} b
* @returns
*/
function compareByName(a, b) {
if (typeof a.name !== 'string' || typeof b.name !== 'string') {
return 0;
}
return a.name.localeCompare(b.name);
}
/**
* descending
* @param {object} a
* @param {object} b
* @returns
*/
function compareByCreatedAt(a, b) {
if (typeof a.created_at !== 'string' || typeof b.created_at !== 'string') {
return 0;
}
const A = a.created_at.toUpperCase();
const B = b.created_at.toUpperCase();
if (A < B) {
return 1;
}
if (A > B) {
return -1;
}
return 0;
}
/**
* ascending
* @param {object} a
* @param {object} b
* @returns
*/
function compareByCreatedAtAscending(a, b) {
const A = a.created_at;
const B = b.created_at;
if (A < B) {
return -1;
}
if (A > B) {
return 1;
}
return 0;
}
/**
* descending
* @param {object} a
* @param {object} b
* @returns
*/
function compareByUpdatedAt(a, b) {
if (typeof a.updated_at !== 'string' || typeof b.updated_at !== 'string') {
return 0;
}
const A = a.updated_at.toUpperCase();
const B = b.updated_at.toUpperCase();
if (A < B) {
return 1;
}
if (A > B) {
return -1;
}
return 0;
}
/**
* ascending
* @param {object} a
* @param {object} b
* @returns
*/
function compareByDisplayName(a, b) {
if (
typeof a.displayName !== 'string' ||
typeof b.displayName !== 'string'
) {
return 0;
}
return a.displayName.localeCompare(b.displayName);
}
/**
*
* @param {object} a
* @param {object} b
* @returns
*/
function compareByMemberCount(a, b) {
if (
typeof a.memberCount !== 'number' ||
typeof b.memberCount !== 'number'
) {
return 0;
}
return a.memberCount - b.memberCount;
}
/**
* private
* @param {object} a
* @param {object} b
* @returns
*/
function compareByPrivate(a, b) {
if (typeof a.ref === 'undefined' || typeof b.ref === 'undefined') {
return 0;
}
if (a.ref.location === 'private' && b.ref.location === 'private') {
return 0;
} else if (a.ref.location === 'private') {
return 1;
} else if (b.ref.location === 'private') {
return -1;
}
return 0;
}
/**
*
* @param {object} a
* @param {object} b
* @returns
*/
function compareByStatus(a, b) {
if (typeof a.ref === 'undefined' || typeof b.ref === 'undefined') {
return 0;
}
if (a.ref.status === b.ref.status) {
return 0;
}
if (a.ref.state === 'offline') {
return 1;
}
return sortStatus(a.ref.status, b.ref.status);
}
/**
* last active
* @param {object} a
* @param {object} b
* @returns
*/
function compareByLastActive(a, b) {
if (a.state === 'online' && b.state === 'online') {
if (
a.ref?.$online_for &&
b.ref?.$online_for &&
a.ref.$online_for === b.ref.$online_for
) {
compareByActivityField(a, b, 'last_login');
}
return compareByActivityField(a, b, '$online_for');
}
return compareByActivityField(a, b, 'last_activity');
}
/**
* last seen
* @param {object} a
* @param {object} b
* @returns
*/
function compareByLastSeen(a, b) {
return compareByActivityField(a, b, '$lastSeen');
}
/**
*
* @param {object} a
* @param {object} b
* @param {string} field
* @returns
*/
function compareByActivityField(a, b, field) {
if (typeof a.ref === 'undefined' || typeof b.ref === 'undefined') {
return 0;
}
// When the field is just and empty string, it means they've been
// in whatever active state for the longest
if (
a.ref[field] < b.ref[field] ||
(a.ref[field] !== '' && b.ref[field] === '')
) {
return 1;
}
if (
a.ref[field] > b.ref[field] ||
(a.ref[field] === '' && b.ref[field] !== '')
) {
return -1;
}
return 0;
}
/**
* location at
* @param {object} a
* @param {object} b
* @returns
*/
function compareByLocationAt(a, b) {
if (a.location === 'traveling' && b.location === 'traveling') {
return 0;
}
if (a.location === 'traveling') {
return 1;
}
if (b.location === 'traveling') {
return -1;
}
if (a.$location_at < b.$location_at) {
return -1;
}
if (a.$location_at > b.$location_at) {
return 1;
}
return 0;
}
/**
* location at but for the sidebar
* @param {object} a
* @param {object} b
* @returns
*/
function compareByLocation(a, b) {
if (typeof a.ref === 'undefined' || typeof b.ref === 'undefined') {
return 0;
}
if (a.state !== 'online' || b.state !== 'online') {
return 0;
}
return a.ref.location.localeCompare(b.ref.location);
}
export {
compareByName,
compareByCreatedAt,
compareByCreatedAtAscending,
compareByUpdatedAt,
compareByDisplayName,
compareByMemberCount,
compareByPrivate,
compareByStatus,
compareByLastActive,
compareByLastSeen,
compareByLocationAt,
compareByLocation
};

147
src/shared/utils/friend.js Normal file
View File

@@ -0,0 +1,147 @@
import {
compareByLastActive,
compareByLastSeen,
compareByLocation,
compareByLocationAt,
compareByName,
compareByPrivate,
compareByStatus
} from './compare';
/**
*
* @param {string[]} sortMethods
* @returns
*/
function getFriendsSortFunction(sortMethods) {
const sorts = [];
for (const sortMethod of sortMethods) {
switch (sortMethod) {
case 'Sort Alphabetically':
sorts.push(compareByName);
break;
case 'Sort Private to Bottom':
sorts.push(compareByPrivate);
break;
case 'Sort by Status':
sorts.push(compareByStatus);
break;
case 'Sort by Last Active':
sorts.push(compareByLastActive);
break;
case 'Sort by Last Seen':
sorts.push(compareByLastSeen);
break;
case 'Sort by Time in Instance':
sorts.push((a, b) => {
if (
typeof a.ref === 'undefined' ||
typeof b.ref === 'undefined'
) {
return 0;
}
if (a.state !== 'online' || b.state !== 'online') {
return 0;
}
return compareByLocationAt(b.ref, a.ref);
});
break;
case 'Sort by Location':
sorts.push(compareByLocation);
break;
case 'None':
sorts.push(() => 0);
break;
}
}
/**
* @param {object} a
* @param {object} b
* @returns {number}
*/
return (a, b) => {
let res;
for (const sort of sorts) {
res = sort(a, b);
if (res !== 0) {
return res;
}
}
return res;
};
}
/**
*
* @param {string} a
* @param {string} b
* @returns {number}
*/
function sortStatus(a, b) {
switch (b) {
case 'join me':
switch (a) {
case 'active':
return 1;
case 'ask me':
return 1;
case 'busy':
return 1;
}
break;
case 'active':
switch (a) {
case 'join me':
return -1;
case 'ask me':
return 1;
case 'busy':
return 1;
}
break;
case 'ask me':
switch (a) {
case 'join me':
return -1;
case 'active':
return -1;
case 'busy':
return 1;
}
break;
case 'busy':
switch (a) {
case 'join me':
return -1;
case 'active':
return -1;
case 'ask me':
return -1;
}
break;
}
return 0;
}
/**
*
* @param {object} friend
* @returns {boolean}
*/
function isFriendOnline(friend) {
if (typeof friend === 'undefined' || typeof friend.ref === 'undefined') {
return false;
}
if (friend.state === 'online') {
return true;
}
if (friend.state !== 'online' && friend.ref.location !== 'private') {
// wat
return true;
}
return false;
}
export { getFriendsSortFunction, sortStatus, isFriendOnline };

View File

@@ -0,0 +1,57 @@
/**
*
* @param {object} print
* @returns
*/
function getPrintFileName(print) {
const authorName = print.authorName;
// fileDate format: 2024-11-03_16-14-25.757
const createdAt = getPrintLocalDate(print);
const fileNameDate = createdAt
.toISOString()
.replace(/:/g, '-')
.replace(/T/g, '_')
.replace(/Z/g, '');
const fileName = `${authorName}_${fileNameDate}_${print.id}.png`;
return fileName;
}
/**
*
* @param {object} print
* @returns
*/
function getPrintLocalDate(print) {
if (print.createdAt) {
const createdAt = new Date(print.createdAt);
// cursed convert to local time
createdAt.setMinutes(
createdAt.getMinutes() - createdAt.getTimezoneOffset()
);
return createdAt;
}
if (print.timestamp) {
return new Date(print.timestamp);
}
const createdAt = new Date();
// cursed convert to local time
createdAt.setMinutes(
createdAt.getMinutes() - createdAt.getTimezoneOffset()
);
return createdAt;
}
/**
* @param {object} emoji
*/
function getEmojiFileName(emoji) {
if (emoji.frames) {
const loopStyle = emoji.loopStyle || 'linear';
return `${emoji.name}_${emoji.animationStyle}animationStyle_${emoji.frames}frames_${emoji.framesOverTime}fps_${loopStyle}loopStyle.png`;
} else {
return `${emoji.name}_${emoji.animationStyle}animationStyle.png`;
}
}
export { getPrintLocalDate, getPrintFileName, getEmojiFileName };

52
src/shared/utils/group.js Normal file
View File

@@ -0,0 +1,52 @@
import groupRequest from '../../api/group';
import { parseLocation } from './location';
/**
*
* @param {object} ref
* @param {string} permission
* @returns {boolean}
*/
function hasGroupPermission(ref, permission) {
if (
ref &&
ref.myMember &&
ref.myMember.permissions &&
(ref.myMember.permissions.includes('*') ||
ref.myMember.permissions.includes(permission))
) {
return true;
}
return false;
}
/**
*
* @param {string} data
* @returns {Promise<string>}
*/
async function getGroupName(data) {
if (!data) {
return '';
}
let groupName = '';
let groupId = data;
if (!data.startsWith('grp_')) {
const L = parseLocation(data);
groupId = L.groupId;
if (!L.groupId) {
return '';
}
}
try {
const args = await groupRequest.getCachedGroup({
groupId
});
groupName = args.ref.name;
} catch (err) {
console.error(err);
}
return groupName;
}
export { hasGroupPermission, getGroupName };

20
src/shared/utils/index.js Normal file
View File

@@ -0,0 +1,20 @@
export * from './base/array';
export * from './base/devtool';
export * from './base/format';
export * from './base/date';
export * from './base/string';
export * from './base/ui';
export * from './avatar';
export * from './chart';
export * from './common';
export * from './compare';
export * from './friend';
export * from './group';
export * from './instance';
export * from './setting';
export * from './user';
export * from './gallery';
export * from './location';
export * from './invite';
export * from './world';
export * from './memos';

View File

@@ -0,0 +1,65 @@
import { instanceRequest } from '../../api';
import { parseLocation } from './location';
/**
*
* @param {object} instance
*/
function refreshInstancePlayerCount(instance) {
const L = parseLocation(instance);
if (L.isRealInstance) {
instanceRequest.getInstance({
worldId: L.worldId,
instanceId: L.instanceId
});
}
}
/**
*
* @param {string} instanceId
* @returns
*/
function isRealInstance(instanceId) {
if (!instanceId) {
return false;
}
switch (instanceId) {
case ':':
case 'offline':
case 'offline:offline':
case 'private':
case 'private:private':
case 'traveling':
case 'traveling:traveling':
case instanceId.startsWith('local'):
return false;
}
return true;
}
/**
*
* @param {object} instance
* @returns {string}
*/
function getLaunchURL(instance) {
const L = instance;
if (L.instanceId) {
if (L.shortName) {
return `https://vrchat.com/home/launch?worldId=${encodeURIComponent(
L.worldId
)}&instanceId=${encodeURIComponent(
L.instanceId
)}&shortName=${encodeURIComponent(L.shortName)}`;
}
return `https://vrchat.com/home/launch?worldId=${encodeURIComponent(
L.worldId
)}&instanceId=${encodeURIComponent(L.instanceId)}`;
}
return `https://vrchat.com/home/launch?worldId=${encodeURIComponent(
L.worldId
)}`;
}
export { refreshInstancePlayerCount, isRealInstance, getLaunchURL };

View File

@@ -0,0 +1,62 @@
import {
useFriendStore,
useInstanceStore,
useLocationStore,
useUserStore
} from '../../stores';
import { parseLocation } from './location';
/**
*
* @param {string} location
* @returns
*/
function checkCanInvite(location) {
const userStore = useUserStore();
const locationStore = useLocationStore();
const instanceStore = useInstanceStore();
const L = parseLocation(location);
const instance = instanceStore.cachedInstances.get(location);
if (instance?.closedAt) {
return false;
}
if (
L.accessType === 'public' ||
L.accessType === 'group' ||
L.userId === userStore.currentUser.id
) {
return true;
}
if (L.accessType === 'invite' || L.accessType === 'friends') {
return false;
}
if (locationStore.lastLocation.location === location) {
return true;
}
return false;
}
/**
*
* @param {string} location
* @returns
*/
function checkCanInviteSelf(location) {
const userStore = useUserStore();
const instanceStore = useInstanceStore();
const friendStore = useFriendStore();
const L = parseLocation(location);
const instance = instanceStore.cachedInstances.get(location);
if (instance?.closedAt) {
return false;
}
if (L.userId === userStore.currentUser.id) {
return true;
}
if (L.accessType === 'friends' && !friendStore.friends.has(L.userId)) {
return false;
}
return true;
}
export { checkCanInvite, checkCanInviteSelf };

View File

@@ -0,0 +1,144 @@
/**
*
* @param {string} location
* @param {string} worldName
* @param {string} groupName
* @returns {string}
*/
function displayLocation(location, worldName, groupName) {
let text = worldName;
const L = parseLocation(location);
if (L.isOffline) {
text = 'Offline';
} else if (L.isPrivate) {
text = 'Private';
} else if (L.isTraveling) {
text = 'Traveling';
} else if (L.worldId) {
if (groupName) {
text = `${worldName} ${L.accessTypeName}(${groupName})`;
} else if (L.instanceId) {
text = `${worldName} ${L.accessTypeName}`;
}
}
return text;
}
/**
*
* @param {string} tag
* @returns {object}
*/
function parseLocation(tag) {
let _tag = String(tag || '');
const ctx = {
tag: _tag,
isOffline: false,
isPrivate: false,
isTraveling: false,
isRealInstance: false,
worldId: '',
instanceId: '',
instanceName: '',
accessType: '',
accessTypeName: '',
region: '',
shortName: '',
userId: null,
hiddenId: null,
privateId: null,
friendsId: null,
groupId: null,
groupAccessType: null,
canRequestInvite: false,
strict: false,
ageGate: false
};
if (_tag === 'offline' || _tag === 'offline:offline') {
ctx.isOffline = true;
} else if (_tag === 'private' || _tag === 'private:private') {
ctx.isPrivate = true;
} else if (_tag === 'traveling' || _tag === 'traveling:traveling') {
ctx.isTraveling = true;
} else if (!_tag.startsWith('local')) {
ctx.isRealInstance = true;
const sep = _tag.indexOf(':');
// technically not part of instance id, but might be there when coping id from url so why not support it
const shortNameQualifier = '&shortName=';
const shortNameIndex = _tag.indexOf(shortNameQualifier);
if (shortNameIndex >= 0) {
ctx.shortName = _tag.substr(
shortNameIndex + shortNameQualifier.length
);
_tag = _tag.substr(0, shortNameIndex);
}
if (sep >= 0) {
ctx.worldId = _tag.substr(0, sep);
ctx.instanceId = _tag.substr(sep + 1);
ctx.instanceId.split('~').forEach((s, i) => {
if (i) {
const A = s.indexOf('(');
const Z = A >= 0 ? s.lastIndexOf(')') : -1;
const key = Z >= 0 ? s.substr(0, A) : s;
const value = A < Z ? s.substr(A + 1, Z - A - 1) : '';
if (key === 'hidden') {
ctx.hiddenId = value;
} else if (key === 'private') {
ctx.privateId = value;
} else if (key === 'friends') {
ctx.friendsId = value;
} else if (key === 'canRequestInvite') {
ctx.canRequestInvite = true;
} else if (key === 'region') {
ctx.region = value;
} else if (key === 'group') {
ctx.groupId = value;
} else if (key === 'groupAccessType') {
ctx.groupAccessType = value;
} else if (key === 'strict') {
ctx.strict = true;
} else if (key === 'ageGate') {
ctx.ageGate = true;
}
} else {
ctx.instanceName = s;
}
});
ctx.accessType = 'public';
if (ctx.privateId !== null) {
if (ctx.canRequestInvite) {
// InvitePlus
ctx.accessType = 'invite+';
} else {
// InviteOnly
ctx.accessType = 'invite';
}
ctx.userId = ctx.privateId;
} else if (ctx.friendsId !== null) {
// FriendsOnly
ctx.accessType = 'friends';
ctx.userId = ctx.friendsId;
} else if (ctx.hiddenId !== null) {
// FriendsOfGuests
ctx.accessType = 'friends+';
ctx.userId = ctx.hiddenId;
} else if (ctx.groupId !== null) {
// Group
ctx.accessType = 'group';
}
ctx.accessTypeName = ctx.accessType;
if (ctx.groupAccessType !== null) {
if (ctx.groupAccessType === 'public') {
ctx.accessTypeName = 'groupPublic';
} else if (ctx.groupAccessType === 'plus') {
ctx.accessTypeName = 'groupPlus';
}
}
} else {
ctx.worldId = _tag;
}
}
return ctx;
}
export { parseLocation, displayLocation };

127
src/shared/utils/memos.js Normal file
View File

@@ -0,0 +1,127 @@
import { storeToRefs } from 'pinia';
import { database } from '../../service/database.js';
import { useFriendStore } from '../../stores';
/**
* @returns {Promise<void>}
*/
async function migrateMemos() {
var json = JSON.parse(await VRCXStorage.GetAll());
for (var line in json) {
if (line.substring(0, 8) === 'memo_usr') {
var userId = line.substring(5);
var memo = json[line];
if (memo) {
await saveUserMemo(userId, memo);
VRCXStorage.Remove(`memo_${userId}`);
}
}
}
}
/**
*
* @param {string} userId
* @returns
*/
async function getUserMemo(userId) {
try {
return await database.getUserMemo(userId);
} catch (err) {
console.error(err);
return {
userId: '',
editedAt: '',
memo: ''
};
}
}
/**
*
* @param {string} id
* @param {string} memo
*/
async function saveUserMemo(id, memo) {
const friendStore = useFriendStore();
const { friends } = storeToRefs(friendStore);
if (memo) {
await database.setUserMemo({
userId: id,
editedAt: new Date().toJSON(),
memo
});
} else {
await database.deleteUserMemo(id);
}
var ref = friends.value.get(id);
if (ref) {
ref.memo = String(memo || '');
if (memo) {
var array = memo.split('\n');
ref.$nickName = array[0];
} else {
ref.$nickName = '';
}
}
}
/**
* @returns {Promise<void>}
*/
async function getAllUserMemos() {
const friendStore = useFriendStore();
const { friends } = storeToRefs(friendStore);
var memos = await database.getAllUserMemos();
memos.forEach((memo) => {
var ref = friends.value.get(memo.userId);
if (typeof ref !== 'undefined') {
ref.memo = memo.memo;
ref.$nickName = '';
if (memo.memo) {
var array = memo.memo.split('\n');
ref.$nickName = array[0];
}
}
});
}
/**
*
* @param {string} worldId
* @returns
*/
async function getWorldMemo(worldId) {
try {
return await database.getWorldMemo(worldId);
} catch (err) {
console.error(err);
return {
worldId: '',
editedAt: '',
memo: ''
};
}
}
// async function getAvatarMemo(avatarId) {
// try {
// return await database.getAvatarMemoDB(avatarId);
// } catch (err) {
// console.error(err);
// return {
// avatarId: '',
// editedAt: '',
// memo: ''
// };
// }
// }
export {
migrateMemos,
getUserMemo,
saveUserMemo,
getAllUserMemos,
getWorldMemo
// getAvatarMemo
};

View File

@@ -0,0 +1,22 @@
/**
*
* @param {string} res
* @returns
*/
function getVRChatResolution(res) {
switch (res) {
case '1280x720':
return '1280x720 (720p)';
case '1920x1080':
return '1920x1080 (1080p)';
case '2560x1440':
return '2560x1440 (1440p)';
case '3840x2160':
return '3840x2160 (4K)';
case '7680x4320':
return '7680x4320 (8K)';
}
return `${res} (Custom)`;
}
export { getVRChatResolution };

294
src/shared/utils/user.js Normal file
View File

@@ -0,0 +1,294 @@
import { storeToRefs } from 'pinia';
import { useAppearanceSettingsStore, useUserStore } from '../../stores';
import { languageMappings } from '../constants';
import { timeToText } from './base/format';
import { HueToHex } from './base/ui';
import { convertFileUrlToImageUrl } from './common';
/**
*
* @param {object} ctx
* @returns {number}
*/
function userOnlineForTimestamp(ctx) {
if (ctx.ref.state === 'online' && ctx.ref.$online_for) {
return ctx.ref.$online_for;
} else if (ctx.ref.state === 'active' && ctx.ref.$active_for) {
return ctx.ref.$active_for;
} else if (ctx.ref.$offline_for) {
return ctx.ref.$offline_for;
}
return 0;
}
/**
*
* @param {string} language
* @returns
*/
function languageClass(language) {
const style = {};
const mapping = languageMappings[language];
if (typeof mapping !== 'undefined') {
style[mapping] = true;
} else {
style.unknown = true;
}
return style;
}
/**
*
* @param {string} userId
* @returns
*/
async function getNameColour(userId) {
const hue = await AppApi.GetColourFromUserID(userId);
return HueToHex(hue);
}
/**
*
* @param {string} text
* @returns
*/
function removeEmojis(text) {
if (!text) {
return '';
}
return text
.replace(
/([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g,
''
)
.replace(/\s+/g, ' ')
.trim();
}
/**
*
* @param {object} user
* @param {boolean} pendingOffline
* @returns
*/
function userStatusClass(user, pendingOffline = false) {
const userStore = useUserStore();
const style = {};
if (typeof user === 'undefined') {
return style;
}
let id = '';
if (user.id) {
id = user.id;
} else if (user.userId) {
id = user.userId;
}
if (id === userStore.currentUser.id) {
return statusClass(user.status);
}
if (!user.isFriend) {
return style;
}
if (pendingOffline) {
// Pending offline
style.offline = true;
} else if (
user.status !== 'active' &&
user.location === 'private' &&
user.state === '' &&
id &&
!userStore.currentUser.onlineFriends.includes(id)
) {
// temp fix
if (userStore.currentUser.activeFriends.includes(id)) {
// Active
style.active = true;
} else {
// Offline
style.offline = true;
}
} else if (user.state === 'active') {
// Active
style.active = true;
} else if (user.location === 'offline') {
// Offline
style.offline = true;
} else if (user.status === 'active') {
// Online
style.online = true;
} else if (user.status === 'join me') {
// Join Me
style.joinme = true;
} else if (user.status === 'ask me') {
// Ask Me
style.askme = true;
} else if (user.status === 'busy') {
// Do Not Disturb
style.busy = true;
}
if (
user.platform &&
user.platform !== 'standalonewindows' &&
user.platform !== 'web'
) {
style.mobile = true;
}
if (
user.last_platform &&
user.last_platform !== 'standalonewindows' &&
user.platform === 'web'
) {
style.mobile = true;
}
return style;
}
/**
*
* @param {string} status
* @returns {object}
*/
function statusClass(status) {
const style = {};
if (typeof status !== 'undefined') {
if (status === 'active') {
// Online
style.online = true;
} else if (status === 'join me') {
// Join Me
style.joinme = true;
} else if (status === 'ask me') {
// Ask Me
style.askme = true;
} else if (status === 'busy') {
// Do Not Disturb
style.busy = true;
}
}
return style;
}
/**
* @param {object} user - User Ref Object
* @param {boolean} isIcon - is use for icon (about 40x40)
* @param {string} resolution - requested icon resolution (default 128),
* @param {boolean} isUserDialogIcon - is use for user dialog icon
* @returns {string} - img url
*/
function userImage(
user,
isIcon = false,
resolution = '128',
isUserDialogIcon = false
) {
const appAppearanceSettingsStore = useAppearanceSettingsStore();
const { displayVRCPlusIconsAsAvatar } = storeToRefs(
appAppearanceSettingsStore
);
if (!user) {
return '';
}
if (
(isUserDialogIcon && user.userIcon) ||
(displayVRCPlusIconsAsAvatar.value && user.userIcon)
) {
if (isIcon) {
return convertFileUrlToImageUrl(user.userIcon);
}
return user.userIcon;
}
if (user.profilePicOverrideThumbnail) {
if (isIcon) {
return user.profilePicOverrideThumbnail.replace(
'/256',
`/${resolution}`
);
}
return user.profilePicOverrideThumbnail;
}
if (user.profilePicOverride) {
return user.profilePicOverride;
}
if (user.thumbnailUrl) {
return user.thumbnailUrl;
}
if (user.currentAvatarThumbnailImageUrl) {
if (isIcon) {
return user.currentAvatarThumbnailImageUrl.replace(
'/256',
`/${resolution}`
);
}
return user.currentAvatarThumbnailImageUrl;
}
if (user.currentAvatarImageUrl) {
if (isIcon) {
return convertFileUrlToImageUrl(user.currentAvatarImageUrl);
}
return user.currentAvatarImageUrl;
}
return '';
}
/**
*
* @param {object} user
* @returns {string|*}
*/
function userImageFull(user) {
const appAppearanceSettingsStore = useAppearanceSettingsStore();
const { displayVRCPlusIconsAsAvatar } = storeToRefs(
appAppearanceSettingsStore
);
if (displayVRCPlusIconsAsAvatar.value && user.userIcon) {
return user.userIcon;
}
if (user.profilePicOverride) {
return user.profilePicOverride;
}
return user.currentAvatarImageUrl;
}
/**
*
* @param {string} user
* @returns {*|string}
*/
function parseUserUrl(user) {
const url = new URL(user);
const urlPath = url.pathname;
if (urlPath.substring(5, 11) === '/user/') {
const userId = urlPath.substring(11);
return userId;
}
}
/**
*
* @param {object} ctx
* @returns {string}
*/
function userOnlineFor(ctx) {
if (ctx.ref.state === 'online' && ctx.ref.$online_for) {
return timeToText(Date.now() - ctx.ref.$online_for);
} else if (ctx.ref.state === 'active' && ctx.ref.$active_for) {
return timeToText(Date.now() - ctx.ref.$active_for);
} else if (ctx.ref.$offline_for) {
return timeToText(Date.now() - ctx.ref.$offline_for);
}
return '-';
}
export {
userOnlineForTimestamp,
languageClass,
getNameColour,
removeEmojis,
userStatusClass,
statusClass,
userImage,
userImageFull,
parseUserUrl,
userOnlineFor
};

36
src/shared/utils/world.js Normal file
View File

@@ -0,0 +1,36 @@
import { worldRequest } from '../../api';
import { parseLocation } from './location';
import { rpcWorlds } from '../constants';
/**
*
* @param {string} location
* @returns {Promise<string>}
*/
async function getWorldName(location) {
let worldName = '';
const L = parseLocation(location);
if (L.isRealInstance && L.worldId) {
const args = await worldRequest.getCachedWorld({
worldId: L.worldId
});
worldName = args.ref.name;
}
return worldName;
}
/**
*
* @param {string} location
* @returns
*/
function isRpcWorld(location) {
const L = parseLocation(location);
if (rpcWorlds.includes(L.worldId)) {
return true;
}
return false;
}
export { getWorldName, isRpcWorld };