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