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,76 @@
const statusCodes = {
100: 'Continue',
101: 'Switching Protocols',
102: 'Processing',
103: 'Early Hints',
200: 'OK',
201: 'Created',
202: 'Accepted',
203: 'Non-Authoritative Information',
204: 'No Content',
205: 'Reset Content',
206: 'Partial Content',
207: 'Multi-Status',
208: 'Already Reported',
226: 'IM Used',
300: 'Multiple Choices',
301: 'Moved Permanently',
302: 'Found',
303: 'See Other',
304: 'Not Modified',
305: 'Use Proxy',
306: 'Switch Proxy',
307: 'Temporary Redirect',
308: 'Permanent Redirect',
400: 'Bad Request',
401: 'Unauthorized',
402: 'Payment Required',
403: 'Forbidden',
404: 'Not Found',
405: 'Method Not Allowed',
406: 'Not Acceptable',
407: 'Proxy Authentication Required',
408: 'Request Timeout',
409: 'Conflict',
410: 'Gone',
411: 'Length Required',
412: 'Precondition Failed',
413: 'Payload Too Large',
414: 'URI Too Long',
415: 'Unsupported Media Type',
416: 'Range Not Satisfiable',
417: 'Expectation Failed',
418: "I'm a teapot",
421: 'Misdirected Request',
422: 'Unprocessable Entity',
423: 'Locked',
424: 'Failed Dependency',
425: 'Too Early',
426: 'Upgrade Required',
428: 'Precondition Required',
429: 'Too Many Requests',
431: 'Request Header Fields Too Large',
451: 'Unavailable For Legal Reasons',
500: 'Internal Server Error',
501: 'Not Implemented',
502: 'Bad Gateway',
503: 'Service Unavailable',
504: 'Gateway Timeout',
505: 'HTTP Version Not Supported',
506: 'Variant Also Negotiates',
507: 'Insufficient Storage',
508: 'Loop Detected',
510: 'Not Extended',
511: 'Network Authentication Required',
// CloudFlare Error
520: 'Web server returns an unknown error',
521: 'Web server is down',
522: 'Connection timed out',
523: 'Origin is unreachable',
524: 'A timeout occurred',
525: 'SSL handshake failed',
526: 'Invalid SSL certificate',
527: 'Railgun Listener to origin error'
};
export { statusCodes };

View File

@@ -0,0 +1,28 @@
class ActivityType {
static _Playing = 0;
static _Listening = 2;
static _Watching = 3;
static _Competing = 5;
static get Playing() {
return this._Playing;
}
static get Listening() {
return this._Listening;
}
static get Watching() {
return this._Watching;
}
static get Competing() {
return this._Competing;
}
}
Object.freeze(ActivityType);
Object.defineProperty(ActivityType, '_Playing', { writable: false });
Object.defineProperty(ActivityType, '_Listening', { writable: false });
Object.defineProperty(ActivityType, '_Watching', { writable: false });
Object.defineProperty(ActivityType, '_Competing', { writable: false });
export { ActivityType };

View File

@@ -0,0 +1,34 @@
const emojiAnimationStyleUrl =
'https://assets.vrchat.com/www/images/emoji-previews/';
const emojiAnimationStyleList = {
Aura: 'Preview_B2-Aura.gif',
Bats: 'Preview_B2-Fall_Bats.gif',
Bees: 'Preview_B2-Bees.gif',
Bounce: 'Preview_B2-Bounce.gif',
Cloud: 'Preview_B2-Cloud.gif',
Confetti: 'Preview_B2-Winter_Confetti.gif',
Crying: 'Preview_B2-Crying.gif',
Dislike: 'Preview_B2-Dislike.gif',
Fire: 'Preview_B2-Fire.gif',
Idea: 'Preview_B2-Idea.gif',
Lasers: 'Preview_B2-Lasers.gif',
Like: 'Preview_B2-Like.gif',
Magnet: 'Preview_B2-Magnet.gif',
Mistletoe: 'Preview_B2-Winter_Mistletoe.gif',
Money: 'Preview_B2-Money.gif',
Noise: 'Preview_B2-Noise.gif',
Orbit: 'Preview_B2-Orbit.gif',
Pizza: 'Preview_B2-Pizza.gif',
Rain: 'Preview_B2-Rain.gif',
Rotate: 'Preview_B2-Rotate.gif',
Shake: 'Preview_B2-Shake.gif',
Snow: 'Preview_B2-Spin.gif',
Snowball: 'Preview_B2-Winter_Snowball.gif',
Spin: 'Preview_B2-Spin.gif',
Splash: 'Preview_B2-SummerSplash.gif',
Stop: 'Preview_B2-Stop.gif',
ZZZ: 'Preview_B2-ZZZ.gif'
};
export { emojiAnimationStyleUrl, emojiAnimationStyleList };

View File

@@ -0,0 +1,323 @@
const getOptions = (optionTypes) => {
const optionMap = {
Off: { label: 'Off', textKey: 'dialog.shared_feed_filters.off' },
On: { label: 'On', textKey: 'dialog.shared_feed_filters.on' },
VIP: {
label: 'VIP',
textKey: 'dialog.shared_feed_filters.favorite'
},
Friends: {
label: 'Friends',
textKey: 'dialog.shared_feed_filters.friends'
},
Everyone: {
label: 'Everyone',
textKey: 'dialog.shared_feed_filters.everyone'
}
};
return optionTypes.map((type) => optionMap[type]);
};
function feedFiltersOptions() {
const baseOptions = [
{
key: 'OnPlayerJoining',
name: 'OnPlayerJoining',
options: getOptions(['Off', 'VIP', 'Friends'])
},
{
key: 'OnPlayerJoined',
name: 'OnPlayerJoined',
options: getOptions(['Off', 'VIP', 'Friends', 'Everyone'])
},
{
key: 'OnPlayerLeft',
name: 'OnPlayerLeft',
options: getOptions(['Off', 'VIP', 'Friends', 'Everyone'])
},
{
key: 'Online',
name: 'Online',
options: getOptions(['Off', 'VIP', 'Friends'])
},
{
key: 'Offline',
name: 'Offline',
options: getOptions(['Off', 'VIP', 'Friends'])
},
{
key: 'GPS',
name: 'GPS',
options: getOptions(['Off', 'VIP', 'Friends'])
},
{
key: 'Status',
name: 'Status',
options: getOptions(['Off', 'VIP', 'Friends'])
},
{
key: 'invite',
name: 'Invite',
options: getOptions(['Off', 'VIP', 'Friends'])
},
{
key: 'requestInvite',
name: 'Request Invite',
options: getOptions(['Off', 'VIP', 'Friends'])
},
{
key: 'inviteResponse',
name: 'Invite Response',
options: getOptions(['Off', 'VIP', 'Friends'])
},
{
key: 'requestInviteResponse',
name: 'Request Invite Response',
options: getOptions(['Off', 'VIP', 'Friends'])
},
{
key: 'friendRequest',
name: 'Friend Request',
options: getOptions(['Off', 'On'])
},
{
key: 'Friend',
name: 'New Friend',
options: getOptions(['Off', 'On'])
},
{
key: 'Unfriend',
name: 'Unfriend',
options: getOptions(['Off', 'On'])
},
{
key: 'DisplayName',
name: 'Display Name Change',
options: getOptions(['Off', 'VIP', 'Friends'])
},
{
key: 'TrustLevel',
name: 'Trust Level Change',
options: getOptions(['Off', 'VIP', 'Friends'])
},
{
key: 'groupChange',
name: 'Group Change',
options: getOptions(['Off', 'On']),
tooltip:
"When you've left or been kicked from a group, group name changed, group owner changed, role added/removed"
},
{
key: 'group.announcement',
name: 'Group Announcement',
options: getOptions(['Off', 'On'])
},
{
key: 'group.informative',
name: 'Group Join',
options: getOptions(['Off', 'On']),
tooltip: 'When your request to join a group has been approved'
},
{
key: 'group.invite',
name: 'Group Invite',
options: getOptions(['Off', 'On']),
tooltip: 'When someone invites you to join a group'
},
{
key: 'group.joinRequest',
name: 'Group Join Request',
options: getOptions(['Off', 'On']),
tooltip:
"When someone requests to join a group you're a moderator for"
},
{
key: 'group.transfer',
name: 'Group Transfer Request',
options: getOptions(['Off', 'On'])
},
{
key: 'group.queueReady',
name: 'Instance Queue Ready',
options: getOptions(['Off', 'On'])
},
{
key: 'instance.closed',
name: 'Instance Closed',
options: getOptions(['Off', 'On']),
tooltip:
"When the instance you're in has been closed preventing anyone from joining"
},
{
key: 'VideoPlay',
name: 'Video Play',
options: getOptions(['Off', 'On']),
tooltip: 'Requires VRCX YouTube API option enabled',
tooltipIcon: 'el-icon-warning'
},
{
key: 'Event',
name: 'Miscellaneous Events',
options: getOptions(['Off', 'On']),
tooltip:
'Misc event from VRC game log: VRC crash auto rejoin, shader keyword limit, joining instance blocked by master, error loading video, audio device changed, error joining instance, kicked from instance, VRChat failing to start OSC server, etc...'
},
{
key: 'External',
name: 'External App',
options: getOptions(['Off', 'On'])
},
{
key: 'BlockedOnPlayerJoined',
name: 'Blocked Player Joins',
options: getOptions(['Off', 'VIP', 'Friends', 'Everyone'])
},
{
key: 'BlockedOnPlayerLeft',
name: 'Blocked Player Leaves',
options: getOptions(['Off', 'VIP', 'Friends', 'Everyone'])
},
{
key: 'MutedOnPlayerJoined',
name: 'Muted Player Joins',
options: getOptions(['Off', 'VIP', 'Friends', 'Everyone'])
},
{
key: 'MutedOnPlayerLeft',
name: 'Muted Player Leaves',
options: getOptions(['Off', 'VIP', 'Friends', 'Everyone'])
},
{
key: 'AvatarChange',
name: 'Lobby Avatar Change',
options: getOptions(['Off', 'VIP', 'Friends', 'Everyone'])
}
];
const photonFeedFiltersOptions = [
{
key: 'PortalSpawn',
name: 'Portal Spawn',
options: getOptions(['Off', 'VIP', 'Friends', 'Everyone'])
},
{
key: 'ChatBoxMessage',
name: 'Lobby ChatBox Message',
options: getOptions(['Off', 'VIP', 'Friends', 'Everyone'])
},
{ key: 'Blocked', name: 'Blocked', options: getOptions(['Off', 'On']) },
{
key: 'Unblocked',
name: 'Unblocked',
options: getOptions(['Off', 'On'])
},
{ key: 'Muted', name: 'Muted', options: getOptions(['Off', 'On']) },
{ key: 'Unmuted', name: 'Unmuted', options: getOptions(['Off', 'On']) }
];
const notyFeedFiltersOptions = baseOptions;
const wristFeedFiltersOptions = [
{
key: 'Location',
name: 'Self Location',
options: getOptions(['Off', 'On'])
},
...baseOptions
];
return {
notyFeedFiltersOptions,
wristFeedFiltersOptions,
photonFeedFiltersOptions
};
}
const sharedFeedFiltersDefaults = {
noty: {
Location: 'Off',
OnPlayerJoined: 'VIP',
OnPlayerLeft: 'VIP',
OnPlayerJoining: 'VIP',
Online: 'VIP',
Offline: 'VIP',
GPS: 'Off',
Status: 'Off',
invite: 'Friends',
requestInvite: 'Friends',
inviteResponse: 'Friends',
requestInviteResponse: 'Friends',
friendRequest: 'On',
Friend: 'On',
Unfriend: 'On',
DisplayName: 'VIP',
TrustLevel: 'VIP',
boop: 'Off',
groupChange: 'On',
'group.announcement': 'On',
'group.informative': 'On',
'group.invite': 'On',
'group.joinRequest': 'Off',
'group.transfer': 'On',
'group.queueReady': 'On',
'instance.closed': 'On',
PortalSpawn: 'Everyone',
Event: 'On',
External: 'On',
VideoPlay: 'Off',
BlockedOnPlayerJoined: 'Off',
BlockedOnPlayerLeft: 'Off',
MutedOnPlayerJoined: 'Off',
MutedOnPlayerLeft: 'Off',
AvatarChange: 'Off',
ChatBoxMessage: 'Off',
Blocked: 'Off',
Unblocked: 'Off',
Muted: 'Off',
Unmuted: 'Off'
},
wrist: {
Location: 'On',
OnPlayerJoined: 'Everyone',
OnPlayerLeft: 'Everyone',
OnPlayerJoining: 'Friends',
Online: 'Friends',
Offline: 'Friends',
GPS: 'Friends',
Status: 'Friends',
invite: 'Friends',
requestInvite: 'Friends',
inviteResponse: 'Friends',
requestInviteResponse: 'Friends',
friendRequest: 'On',
Friend: 'On',
Unfriend: 'On',
DisplayName: 'Friends',
TrustLevel: 'Friends',
boop: 'On',
groupChange: 'On',
'group.announcement': 'On',
'group.informative': 'On',
'group.invite': 'On',
'group.joinRequest': 'On',
'group.transfer': 'On',
'group.queueReady': 'On',
'instance.closed': 'On',
PortalSpawn: 'Everyone',
Event: 'On',
External: 'On',
VideoPlay: 'On',
BlockedOnPlayerJoined: 'Off',
BlockedOnPlayerLeft: 'Off',
MutedOnPlayerJoined: 'Off',
MutedOnPlayerLeft: 'Off',
AvatarChange: 'Everyone',
ChatBoxMessage: 'Off',
Blocked: 'On',
Unblocked: 'On',
Muted: 'On',
Unmuted: 'On'
}
};
export { feedFiltersOptions, sharedFeedFiltersDefaults };

View File

@@ -0,0 +1,26 @@
const groupDialogSortingOptions = {
joinedAtDesc: {
name: 'dialog.group.members.sorting.joined_at_desc',
value: 'joinedAt:desc'
},
joinedAtAsc: {
name: 'dialog.group.members.sorting.joined_at_asc',
value: 'joinedAt:asc'
},
userId: {
name: 'dialog.group.members.sorting.user_id',
value: ''
}
};
const groupDialogFilterOptions = {
everyone: {
name: 'dialog.group.members.filters.everyone',
id: null
},
usersWithNoRole: {
name: 'dialog.group.members.filters.users_with_no_role',
id: ''
}
};
export { groupDialogSortingOptions, groupDialogFilterOptions };

View File

@@ -0,0 +1,11 @@
export * from './emoji';
export * from './feedFilters';
export * from './language';
export * from './ossLicenses';
export * from './photon';
export * from './settings';
export * from './group';
export * from './user';
export * from './instance';
export * from './world';
export * from './moderation';

View File

@@ -0,0 +1,10 @@
const instanceContentSettings = [
'emoji',
'stickers',
'pedestals',
'prints',
'drones',
'props'
];
export { instanceContentSettings };

View File

@@ -0,0 +1,73 @@
// vrchat to famfamfam language mappings
const languageMappings = {
eng: 'us',
kor: 'kr',
rus: 'ru',
spa: 'es',
por: 'pt',
zho: 'cn',
deu: 'de',
jpn: 'jp',
fra: 'fr',
swe: 'se',
nld: 'nl',
pol: 'pl',
dan: 'dk',
nor: 'no',
ita: 'it',
tha: 'th',
fin: 'fi',
hun: 'hu',
ces: 'cz',
tur: 'tr',
ara: 'ae',
ron: 'ro',
vie: 'vn',
ukr: 'ua',
ase: 'us',
bfi: 'gb',
dse: 'nl',
fsl: 'fr',
jsl: 'jp',
kvk: 'kr',
mlt: 'mt',
ind: 'id',
hrv: 'hr',
heb: 'he',
afr: 'af',
ben: 'be',
bul: 'bg',
cmn: 'cn',
cym: 'cy',
ell: 'el',
est: 'et',
fil: 'ph',
gla: 'gd',
gle: 'ga',
hin: 'hi',
hmn: 'cn',
hye: 'hy',
isl: 'is',
lav: 'lv',
lit: 'lt',
ltz: 'lb',
mar: 'hi',
mkd: 'mk',
msa: 'my',
sco: 'gd',
slk: 'sk',
slv: 'sl',
tel: 'hi',
mri: 'nz',
wuu: 'cn',
yue: 'cn',
tws: 'cn',
asf: 'au',
nzs: 'nz',
gsg: 'de',
epo: 'eo',
tok: 'tok'
};
export { languageMappings };

View File

@@ -0,0 +1,12 @@
const moderationTypes = [
'block',
'unblock',
'mute',
'unmute',
'interactOn',
'interactOff',
'muteChat',
'unmuteChat'
];
export { moderationTypes };

View File

@@ -0,0 +1,424 @@
const openSourceSoftwareLicenses = [
{
name: 'animate.css',
licenseText: `The MIT License (MIT)
Copyright (c) 2019 Daniel Eden
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.`
},
{
name: 'CefSharp',
licenseText: `// Copyright © The CefSharp Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
//
// * Neither the name of Google Inc. nor the name Chromium Embedded
// Framework nor the name CefSharp nor the names of its contributors
// may be used to endorse or promote products derived from this software
// without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.`
},
{
name: 'DiscordRichPresence',
licenseText: `MIT License
Copyright (c) 2018 Lachee
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.`
},
{
name: 'element',
licenseText: `The MIT License (MIT)
Copyright (c) 2016-present ElemeFE
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.`
},
{
name: 'librsync.net',
licenseText: `The MIT License (MIT)
Copyright (c) 2015 Brad Dodson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.`
},
{
name: 'Newtonsoft.Json',
licenseText: `The MIT License (MIT)
Copyright (c) 2007 James Newton-King
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.`
},
{
name: 'normalize',
licenseText: `The MIT License (MIT)
Copyright © Nicolas Gallagher and Jonathan Neal
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.`
},
{
name: 'noty',
licenseText: `Copyright (c) 2012 Nedim Arabacı
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.`
},
{
name: 'OpenVR SDK',
licenseText: `Copyright (c) 2015, Valve Corporation
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.`
},
{
name: 'Twemoji',
licenseText: `MIT License
Copyright (c) 2021 Twitter
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.`
},
{
name: 'SharpDX',
licenseText: `Copyright (c) 2010-2014 SharpDX - Alexandre Mutel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.`
},
{
name: 'vue',
licenseText: `The MIT License (MIT)
Copyright (c) 2013-present, Yuxi (Evan) You
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.`
},
{
name: 'vue-data-tables',
licenseText: `The MIT License (MIT)
Copyright (c) 2018 Leon Zhang
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.`
},
{
name: 'vue-lazyload',
licenseText: `The MIT License (MIT)
Copyright (c) 2016 Awe
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.`
},
{
name: 'Encode Sans Font (from Dark Vanilla)',
licenseText: `SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
Copyright (c) 2020 June 20, Impallari Type, Andres Torresi, Jacques Le Bailly
(https://fonts.google.com/specimen/Encode+Sans),
with Reserved Font Name: Encode Sans.
PREAMBLE:
The goals of the Open Font License (OFL) are to stimulate worldwide development
of collaborative font projects, to support the font creation efforts of academic
and linguistic communities, and to provide a free and open framework in which
fonts may be shared and improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and redistributed
freely as long as they are not sold by themselves. The fonts, including any
derivative works, can be bundled, embedded, redistributed and/or sold with any
software provided that any reserved names are not used by derivative works.
The fonts and derivatives, however, cannot be released under any other type of
license. The requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining a copy of
the Font Software, to use, study, copy, merge, embed, modify, redistribute, and
sell modified and unmodified copies of the Font Software, subject to the
following conditions:
1. Neither the Font Software nor any of its individual components, in Original or
Modified Versions, may be sold by itself.
2. Original or Modified Versions of the Font Software may be bundled, redistributed
and/or sold with any software, provided that each copy contains the above copyright
notice and this license. These can be included either as stand-alone text files,
human-readable headers or in the appropriate machine-readable metadata fields within
text or binary files as long as those fields can be easily viewed by the user.
3. No Modified Version of the Font Software may use the Reserved Font Name(s) unless
explicit written permission is granted by the corresponding Copyright Holder. This
restriction only applies to the primary font name as presented to the users.
4. The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall
not be used to promote, endorse or advertise any Modified Version, except to
acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with
their explicit written permission.
5. The Font Software, modified or unmodified, in part or in whole, must be distributed
entirely under this license, and must not be distributed under any other license.
The requirement for fonts to remain under this license does not apply to any document
created using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR
OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL,
OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
DEALINGS IN THE FONT SOFTWARE.`
},
{
name: 'Apache ECharts',
licenseText: `Apache License 2.0
Copyright 2017-2025 The Apache Software Foundation
This product includes software developed at
The Apache Software Foundation (https://www.apache.org/).`
},
{
name: 'dayjs',
licenseText: `MIT License
Copyright (c) 2018-present, iamkun
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.`
}
];
export { openSourceSoftwareLicenses };

View File

@@ -0,0 +1,122 @@
const photonEmojis = [
'Angry',
'Blushing',
'Crying',
'Frown',
'Hand Wave',
'Hang Ten',
'In Love',
'Jack O Lantern',
'Kiss',
'Laugh',
'Skull',
'Smile',
'Spooky Ghost',
'Stoic',
'Sunglasses',
'Thinking',
'Thumbs Down',
'Thumbs Up',
'Tongue Out',
'Wow',
'Arrow Point',
"Can't see",
'Hourglass',
'Keyboard',
'No Headphones',
'No Mic',
'Portal',
'Shush',
'Bats',
'Cloud',
'Fire',
'Snow Fall',
'Snowball',
'Splash',
'Web',
'Beer',
'Candy',
'Candy Cane',
'Candy Corn',
'Champagne',
'Drink',
'Gingerbread',
'Ice Cream',
'Pineapple',
'Pizza',
'Tomato',
'Beachball',
'Coal',
'Confetti',
'Gift',
'Gifts',
'Life Ring',
'Mistletoe',
'Money',
'Neon Shades',
'Sun Lotion',
'Boo',
'Broken Heart',
'Exclamation',
'Go',
'Heart',
'Music Note',
'Question',
'Stop',
'Zzz'
];
const photonEventType = [
'MeshVisibility',
'AnimationFloat',
'AnimationBool',
'AnimationTrigger',
'AudioTrigger',
'PlayAnimation',
'SendMessage',
'SetParticlePlaying',
'TeleportPlayer',
'RunConsoleCommand',
'SetGameObjectActive',
'SetWebPanelURI',
'SetWebPanelVolume',
'SpawnObject',
'SendRPC',
'ActivateCustomTrigger',
'DestroyObject',
'SetLayer',
'SetMaterial',
'AddHealth',
'AddDamage',
'SetComponentActive',
'AnimationInt',
'AnimationIntAdd',
'AnimationIntSubtract',
'AnimationIntMultiply',
'AnimationIntDivide',
'AddVelocity',
'SetVelocity',
'AddAngularVelocity',
'SetAngularVelocity',
'AddForce',
'SetUIText',
'CallUdonMethod'
];
const photonEventTableTypeFilterList = [
'Event',
'OnPlayerJoined',
'OnPlayerLeft',
'ChangeAvatar',
'ChangeStatus',
'ChangeGroup',
'PortalSpawn',
'DeletedPortal',
'ChatBoxMessage',
'Moderation',
'Camera',
'SpawnEmoji',
'MasterMigrate'
];
export { photonEmojis, photonEventType, photonEventTableTypeFilterList };

View File

@@ -0,0 +1,35 @@
const VRChatScreenshotResolutions = [
{ name: '1280x720 (720p)', width: 1280, height: 720 },
{ name: '1920x1080 (1080p Default)', width: '', height: '' },
{ name: '2560x1440 (1440p)', width: 2560, height: 1440 },
{ name: '3840x2160 (4K)', width: 3840, height: 2160 }
];
const VRChatCameraResolutions = [
{ name: '1280x720 (720p)', width: 1280, height: 720 },
{ name: '1920x1080 (1080p Default)', width: '', height: '' },
{ name: '2560x1440 (1440p)', width: 2560, height: 1440 },
{ name: '3840x2160 (4K)', width: 3840, height: 2160 },
{ name: '7680x4320 (8K)', width: 7680, height: 4320 }
];
const branches = {
Stable: {
name: 'Stable',
urlReleases: 'https://api0.vrcx.app/releases/stable',
urlLatest: 'https://api0.vrcx.app/releases/stable/latest'
},
Nightly: {
name: 'Nightly',
urlReleases: 'https://api0.vrcx.app/releases/nightly',
urlLatest: 'https://api0.vrcx.app/releases/nightly/latest'
}
// LinuxTest: {
// name: 'LinuxTest',
// urlReleases: 'https://api.github.com/repos/rs189/VRCX/releases',
// urlLatest:
// 'https://api.github.com/repos/rs189/VRCX/releases/latest'
// }
};
export { VRChatScreenshotResolutions, VRChatCameraResolutions, branches };

View File

@@ -0,0 +1,54 @@
const userDialogWorldSortingOptions = {
name: {
name: 'dialog.user.worlds.sorting.name',
value: 'name'
},
updated: {
name: 'dialog.user.worlds.sorting.updated',
value: 'updated'
},
created: {
name: 'dialog.user.worlds.sorting.created',
value: 'created'
},
favorites: {
name: 'dialog.user.worlds.sorting.favorites',
value: 'favorites'
},
popularity: {
name: 'dialog.user.worlds.sorting.popularity',
value: 'popularity'
}
};
const userDialogWorldOrderOptions = {
descending: {
name: 'dialog.user.worlds.order.descending',
value: 'descending'
},
ascending: {
name: 'dialog.user.worlds.order.ascending',
value: 'ascending'
}
};
const userDialogGroupSortingOptions = {
alphabetical: {
name: 'dialog.user.groups.sorting.alphabetical',
value: 'alphabetical'
},
members: {
name: 'dialog.user.groups.sorting.members',
value: 'members'
},
inGame: {
name: 'dialog.user.groups.sorting.in_game',
value: 'inGame'
}
};
export {
userDialogWorldSortingOptions,
userDialogWorldOrderOptions,
userDialogGroupSortingOptions
};

View File

@@ -0,0 +1,17 @@
const rpcWorlds = [
'wrld_f20326da-f1ac-45fc-a062-609723b097b1',
'wrld_42377cf1-c54f-45ed-8996-5875b0573a83',
'wrld_dd6d2888-dbdc-47c2-bc98-3d631b2acd7c',
'wrld_52bdcdab-11cd-4325-9655-0fb120846945',
'wrld_2d40da63-8f1f-4011-8a9e-414eb8530acd',
'wrld_10e5e467-fc65-42ed-8957-f02cace1398c',
'wrld_04899f23-e182-4a8d-b2c7-2c74c7c15534',
'wrld_435bbf25-f34f-4b8b-82c6-cd809057eb8e',
'wrld_db9d878f-6e76-4776-8bf2-15bcdd7fc445',
'wrld_f767d1c8-b249-4ecc-a56f-614e433682c8',
'wrld_74970324-58e8-4239-a17b-2c59dfdf00db',
'wrld_266523e8-9161-40da-acd0-6bd82e075833',
'wrld_27c7e6b2-d938-447e-a270-3d1a873e2cf3'
];
export { rpcWorlds };

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