mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-18 22:33:50 +02:00
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:
76
src/shared/constants/api.js
Normal file
76
src/shared/constants/api.js
Normal 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 };
|
||||
28
src/shared/constants/discord.js
Normal file
28
src/shared/constants/discord.js
Normal 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 };
|
||||
34
src/shared/constants/emoji.js
Normal file
34
src/shared/constants/emoji.js
Normal 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 };
|
||||
323
src/shared/constants/feedFilters.js
Normal file
323
src/shared/constants/feedFilters.js
Normal 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 };
|
||||
26
src/shared/constants/group.js
Normal file
26
src/shared/constants/group.js
Normal 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 };
|
||||
11
src/shared/constants/index.js
Normal file
11
src/shared/constants/index.js
Normal 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';
|
||||
10
src/shared/constants/instance.js
Normal file
10
src/shared/constants/instance.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const instanceContentSettings = [
|
||||
'emoji',
|
||||
'stickers',
|
||||
'pedestals',
|
||||
'prints',
|
||||
'drones',
|
||||
'props'
|
||||
];
|
||||
|
||||
export { instanceContentSettings };
|
||||
73
src/shared/constants/language.js
Normal file
73
src/shared/constants/language.js
Normal 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 };
|
||||
12
src/shared/constants/moderation.js
Normal file
12
src/shared/constants/moderation.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const moderationTypes = [
|
||||
'block',
|
||||
'unblock',
|
||||
'mute',
|
||||
'unmute',
|
||||
'interactOn',
|
||||
'interactOff',
|
||||
'muteChat',
|
||||
'unmuteChat'
|
||||
];
|
||||
|
||||
export { moderationTypes };
|
||||
424
src/shared/constants/ossLicenses.js
Normal file
424
src/shared/constants/ossLicenses.js
Normal 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 };
|
||||
122
src/shared/constants/photon.js
Normal file
122
src/shared/constants/photon.js
Normal 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 };
|
||||
35
src/shared/constants/settings.js
Normal file
35
src/shared/constants/settings.js
Normal 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 };
|
||||
54
src/shared/constants/user.js
Normal file
54
src/shared/constants/user.js
Normal 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
|
||||
};
|
||||
17
src/shared/constants/world.js
Normal file
17
src/shared/constants/world.js
Normal 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 };
|
||||
35
src/shared/utils/_utils.js
Normal file
35
src/shared/utils/_utils.js
Normal 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
148
src/shared/utils/avatar.js
Normal 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
|
||||
};
|
||||
59
src/shared/utils/base/array.js
Normal file
59
src/shared/utils/base/array.js
Normal 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 };
|
||||
80
src/shared/utils/base/date.js
Normal file
80
src/shared/utils/base/date.js
Normal 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 };
|
||||
102
src/shared/utils/base/devtool.js
Normal file
102
src/shared/utils/base/devtool.js
Normal 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 };
|
||||
97
src/shared/utils/base/format.js
Normal file
97
src/shared/utils/base/format.js
Normal 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 };
|
||||
105
src/shared/utils/base/string.js
Normal file
105
src/shared/utils/base/string.js
Normal 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
284
src/shared/utils/base/ui.js
Normal 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
13
src/shared/utils/chart.js
Normal 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
541
src/shared/utils/common.js
Normal 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
262
src/shared/utils/compare.js
Normal 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
147
src/shared/utils/friend.js
Normal 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 };
|
||||
57
src/shared/utils/gallery.js
Normal file
57
src/shared/utils/gallery.js
Normal 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
52
src/shared/utils/group.js
Normal 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
20
src/shared/utils/index.js
Normal 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';
|
||||
65
src/shared/utils/instance.js
Normal file
65
src/shared/utils/instance.js
Normal 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 };
|
||||
62
src/shared/utils/invite.js
Normal file
62
src/shared/utils/invite.js
Normal 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 };
|
||||
144
src/shared/utils/location.js
Normal file
144
src/shared/utils/location.js
Normal 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
127
src/shared/utils/memos.js
Normal 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
|
||||
};
|
||||
22
src/shared/utils/setting.js
Normal file
22
src/shared/utils/setting.js
Normal 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
294
src/shared/utils/user.js
Normal 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
36
src/shared/utils/world.js
Normal 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 };
|
||||
Reference in New Issue
Block a user