refactor: app.js (#1291)

* refactor: frontend

* Fix avatar gallery sort

* Update .NET dependencies

* Update npm dependencies

electron v37.1.0

* bulkRefreshFriends

* fix dark theme

* Remove crowdin

* Fix config.json dialog not updating

* VRCX log file fixes & add Cef log

* Remove SharedVariable, fix startup

* Revert init theme change

* Logging date not working? Fix WinformThemer designer error

* Add Cef request hander, no more escaping main page

* clean

* fix

* fix

* clean

* uh

* Apply thememode at startup, fixes random user colours

* Split database into files

* Instance info remove empty lines

* Open external VRC links with VRCX

* Electron fixes

* fix userdialog style

* ohhhh

* fix store

* fix store

* fix: load all group members after kicking a user

* fix: world dialog favorite button style

* fix: Clear VRCX Cache Timer input value

* clean

* Fix VR overlay

* Fix VR overlay 2

* Fix Discord discord rich presence for RPC worlds

* Clean up age verified user tags

* Fix playerList being occupied after program reload

* no `this`

* Fix login stuck loading

* writable: false

* Hide dialogs on logout

* add flush sync option

* rm LOGIN event

* rm LOGOUT event

* remove duplicate event listeners

* remove duplicate event listeners

* clean

* remove duplicate event listeners

* clean

* fix theme style

* fix t

* clearable

* clean

* fix ipcEvent

* Small changes

* Popcorn Palace support

* Remove checkActiveFriends

* Clean up

* Fix dragEnterCef

* Block API requests when not logged in

* Clear state on login & logout

* Fix worldDialog instances not updating

* use <script setup>

* Fix avatar change event, CheckGameRunning at startup

* Fix image dragging

* fix

* Remove PWI

* fix updateLoop

* add webpack-dev-server to dev environment

* rm unnecessary chunks

* use <script setup>

* webpack-dev-server changes

* use <script setup>

* use <script setup>

* Fix UGC text size

* Split login event

* t

* use <script setup>

* fix

* Update .gitignore and enable checkJs in jsconfig

* fix i18n t

* use <script setup>

* use <script setup>

* clean

* global types

* fix

* use checkJs for debugging

* Add watchState for login watchers

* fix .vue template

* type fixes

* rm Vue.filter

* Cef v138.0.170, VC++ 2022

* Settings fixes

* Remove 'USER:CURRENT'

* clean up 2FA callbacks

* remove userApply

* rm i18n import

* notification handling to use notification store methods

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

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

* refactor friend handling to use dedicated functions for friend events

* Fix program startup, move lang init

* Fix friend state

* Fix status change error

* Fix user notes diff

* fix

* rm group event

* rm auth event

* rm avatar event

* clean

* clean

* getUser

* getFriends

* getFavoriteWorlds, getFavoriteAvatars

* AvatarGalleryUpload btn style & package.json update

* Fix friend requests

* Apply user

* Apply world

* Fix note diff

* Fix VR overlay

* Fixes

* Update build scripts

* Apply avatar

* Apply instance

* Apply group

* update hidden VRC+ badge

* Fix sameInstance "private"

* fix 502/504 API errors

* fix 502/504 API errors

* clean

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

* Add back in broken friend state repair methods

* add types

---------

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

View File

@@ -1,28 +1,28 @@
<template>
<div v-if="menuActiveIndex === 'profile'" class="x-container">
<div v-show="menuActiveIndex === 'profile'" class="x-container">
<div class="options-container" style="margin-top: 0">
<span class="header">{{ t('view.profile.profile.header') }}</span>
<div class="x-friend-list" style="margin-top: 10px">
<div class="x-friend-item" @click="showUserDialog(API.currentUser.id)">
<div class="x-friend-item" @click="showUserDialog(currentUser.id)">
<div class="avatar">
<img v-lazy="userImage(API.currentUser, true)" />
<img v-lazy="userImage(currentUser, true)" />
</div>
<div class="detail">
<span class="name" v-text="API.currentUser.displayName"></span>
<span class="extra" v-text="API.currentUser.username"></span>
<span class="name" v-text="currentUser.displayName"></span>
<span class="extra" v-text="currentUser.username"></span>
</div>
</div>
<div class="x-friend-item" style="cursor: default">
<div class="detail">
<span class="name">{{ t('view.profile.profile.last_activity') }}</span>
<span class="extra">{{ API.currentUser.last_activity | formatDate('long') }}</span>
<span class="extra">{{ formatDateFilter(currentUser.last_activity, 'long') }}</span>
</div>
</div>
<div class="x-friend-item" style="cursor: default">
<div class="detail">
<span class="name">{{ t('view.profile.profile.two_factor') }}</span>
<span class="extra">{{
API.currentUser.twoFactorAuthEnabled
currentUser.twoFactorAuthEnabled
? t('view.profile.profile.two_factor_enabled')
: t('view.profile.profile.two_factor_disabled')
}}</span>
@@ -101,12 +101,12 @@
icon="el-icon-refresh"
circle
style="margin-left: 5px"
@click="API.getConfig()"></el-button>
@click="getConfig"></el-button>
</el-tooltip>
</div>
<div class="x-friend-list" style="margin-top: 10px">
<div
v-for="(link, item) in API.cachedConfig.downloadUrls"
v-for="(link, item) in cachedConfig.downloadUrls"
:key="item"
class="x-friend-item"
placement="top">
@@ -150,7 +150,7 @@
style="margin-left: 5px"
@click="
inviteMessageTable.visible = true;
refreshInviteMessageTable('message');
refreshInviteMessageTableData('message');
"></el-button>
</el-tooltip>
<el-tooltip placement="top" :content="t('view.profile.clear_results_tooltip')" :disabled="hideTooltips">
@@ -204,7 +204,7 @@
style="margin-left: 5px"
@click="
inviteResponseMessageTable.visible = true;
refreshInviteMessageTable('response');
refreshInviteMessageTableData('response');
"></el-button>
</el-tooltip>
<el-tooltip placement="top" :content="t('view.profile.clear_results_tooltip')" :disabled="hideTooltips">
@@ -261,7 +261,7 @@
style="margin-left: 5px"
@click="
inviteRequestMessageTable.visible = true;
refreshInviteMessageTable('request');
refreshInviteMessageTableData('request');
"></el-button>
</el-tooltip>
<el-tooltip placement="top" :content="t('view.profile.clear_results_tooltip')" :disabled="hideTooltips">
@@ -318,7 +318,7 @@
style="margin-left: 5px"
@click="
inviteRequestResponseMessageTable.visible = true;
refreshInviteMessageTable('requestResponse');
refreshInviteMessageTableData('requestResponse');
"></el-button>
</el-tooltip>
<el-tooltip placement="top" :content="t('view.profile.clear_results_tooltip')" :disabled="hideTooltips">
@@ -371,7 +371,7 @@
prop="updated_at"
sortable="custom">
<template #default="scope">
<span>{{ scope.row.updated_at | formatDate('long') }}</span>
<span>{{ formatDateFilter(scope.row.updated_at, 'long') }}</span>
</template>
</el-table-column>
<el-table-column
@@ -489,92 +489,56 @@
</div>
</template>
<script>
export default {
name: 'ProfileTab'
};
</script>
<script setup>
import { inject, ref, getCurrentInstance } from 'vue';
import { storeToRefs } from 'pinia';
import { ref, getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { inviteMessagesRequest, miscRequest, userRequest } from '../../api';
import utils from '../../classes/utils';
import { parseAvatarUrl } from '../../composables/avatar/utils';
import { authRequest, miscRequest, userRequest } from '../../api';
import {
parseAvatarUrl,
buildTreeData,
openExternalLink,
userImage,
parseUserUrl,
formatDateFilter
} from '../../shared/utils';
import { useAuthStore } from '../../stores';
import DiscordNamesDialog from './dialogs/DiscordNamesDialog.vue';
import ExportFriendsListDialog from './dialogs/ExportFriendsListDialog.vue';
import ExportAvatarsListDialog from './dialogs/ExportAvatarsListDialog.vue';
import {
useAppearanceSettingsStore,
useSearchStore,
useFriendStore,
useUserStore,
useAvatarStore,
useInviteStore,
useGalleryStore,
useUiStore
} from '../../stores';
const { friends } = storeToRefs(useFriendStore());
const { hideTooltips } = storeToRefs(useAppearanceSettingsStore());
const { pastDisplayNameTable, currentUser } = storeToRefs(useUserStore());
const { showUserDialog, lookupUser, getCurrentUser } = useUserStore();
const { showAvatarDialog } = useAvatarStore();
const { showEditInviteMessageDialog, refreshInviteMessageTableData } = useInviteStore();
const {
inviteMessageTable,
inviteResponseMessageTable,
inviteRequestMessageTable,
inviteRequestResponseMessageTable
} = storeToRefs(useInviteStore());
const { showGalleryDialog } = useGalleryStore();
const { menuActiveIndex } = storeToRefs(useUiStore());
const { directAccessWorld } = useSearchStore();
const { logout } = useAuthStore();
const { cachedConfig } = storeToRefs(useAuthStore());
const { t } = useI18n();
const { $prompt, $message } = getCurrentInstance().proxy;
const API = inject('API');
const userImage = inject('userImage');
const showUserDialog = inject('showUserDialog');
const showAvatarDialog = inject('showAvatarDialog');
const showGalleryDialog = inject('showGalleryDialog');
const openExternalLink = inject('openExternalLink');
const props = defineProps({
menuActiveIndex: {
type: String,
default: 'profile'
},
hideTooltips: {
type: Boolean,
default: false
},
inviteMessageTable: {
type: Object,
default: () => ({
visible: false,
data: []
})
},
inviteResponseMessageTable: {
type: Object,
default: () => ({
visible: false,
data: []
})
},
inviteRequestMessageTable: {
type: Object,
default: () => ({
visible: false,
data: []
})
},
inviteRequestResponseMessageTable: {
type: Object,
default: () => ({
visible: false,
data: []
})
},
pastDisplayNameTable: {
type: Object,
default: () => ({
visible: false,
data: []
})
},
friends: {
type: Map,
default: () => new Map()
},
directAccessWorld: {
type: Function,
default: () => {}
},
parseUserUrl: {
type: Function,
default: () => {}
}
});
const emit = defineEmits(['logout', 'lookupUser', 'showEditInviteMessageDialog']);
const vrchatCredit = ref(null);
const configTreeData = ref([]);
const currentUserTreeData = ref([]);
@@ -588,18 +552,13 @@
function getVisits() {
miscRequest.getVisits().then((args) => {
// API.$on('VISITS')
visits.value = args.json;
});
}
function getVRChatCredits() {
// API.$on('VRCCREDITS')
miscRequest.getVRChatCredits().then((args) => (vrchatCredit.value = args.json?.balance));
}
function logout() {
emit('logout');
}
function showDiscordNamesDialog() {
discordNamesDialogVisible.value = true;
@@ -621,7 +580,7 @@
inputErrorMessage: t('prompt.direct_access_username.input_error'),
callback: (action, instance) => {
if (action === 'confirm' && instance.inputValue) {
emit('lookupUser', {
lookupUser({
displayName: instance.inputValue
});
}
@@ -639,7 +598,7 @@
if (action === 'confirm' && instance.inputValue) {
const testUrl = instance.inputValue.substring(0, 15);
if (testUrl === 'https://vrchat.') {
const userId = this.parseUserUrl(instance.inputValue);
const userId = parseUserUrl(instance.inputValue);
if (userId) {
showUserDialog(userId);
} else {
@@ -664,7 +623,7 @@
inputErrorMessage: t('prompt.direct_access_world_id.input_error'),
callback: (action, instance) => {
if (action === 'confirm' && instance.inputValue) {
if (!props.directAccessWorld(instance.inputValue)) {
if (!directAccessWorld(instance.inputValue)) {
$message({
message: t('prompt.direct_access_world_id.message.error'),
type: 'error'
@@ -685,7 +644,7 @@
if (action === 'confirm' && instance.inputValue) {
const testUrl = instance.inputValue.substring(0, 15);
if (testUrl === 'https://vrchat.') {
const avatarId = props.parseAvatarUrl(instance.inputValue);
const avatarId = parseAvatarUrl(instance.inputValue);
if (avatarId) {
showAvatarDialog(avatarId);
} else {
@@ -701,25 +660,21 @@
}
});
}
function showEditInviteMessageDialog(messageType, inviteMessage) {
emit('showEditInviteMessageDialog', messageType, inviteMessage);
}
function refreshInviteMessageTable(messageType) {
inviteMessagesRequest.refreshInviteMessageTableData(messageType);
async function getConfig() {
await authRequest.getConfig();
}
async function refreshConfigTreeData() {
await API.getConfig();
configTreeData.value = utils.buildTreeData(API.cachedConfig);
await getConfig();
configTreeData.value = buildTreeData(cachedConfig.value);
}
async function refreshCurrentUserTreeData() {
await API.getCurrentUser();
currentUserTreeData.value = utils.buildTreeData(API.currentUser);
await getCurrentUser();
currentUserTreeData.value = buildTreeData(currentUser.value);
}
function getCurrentUserFeedback() {
userRequest.getUserFeedback({ userId: API.currentUser.id }).then((args) => {
// API.$on('USER:FEEDBACK')
if (args.params.userId === API.currentUser.id) {
currentUserFeedbackData.value = utils.buildTreeData(args.json);
userRequest.getUserFeedback({ userId: currentUser.value.id }).then((args) => {
if (args.params.userId === currentUser.value.id) {
currentUserFeedbackData.value = buildTreeData(args.json);
}
});
}

View File

@@ -20,12 +20,13 @@
</template>
<script setup>
import { ref, watch, inject } from 'vue';
import { storeToRefs } from 'pinia';
import { ref, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
const API = inject('API');
import { useUserStore } from '../../../stores';
const { t } = useI18n();
const { currentUser } = storeToRefs(useUserStore());
const props = defineProps({
discordNamesDialogVisible: {
@@ -52,7 +53,7 @@
const discordNamesContent = ref('');
function showDiscordNamesContent() {
const { friends } = API.currentUser;
const { friends } = currentUser.value;
if (Array.isArray(friends) === false) {
return;
}

View File

@@ -27,29 +27,23 @@
</template>
<script setup>
import { ref, watch, inject, getCurrentInstance } from 'vue';
import { storeToRefs } from 'pinia';
import { getCurrentInstance, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { inviteMessagesRequest } from '../../../api';
import { useInviteStore } from '../../../stores';
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const API = inject('API');
const props = defineProps({
editInviteMessageDialog: {
type: Object,
default: () => ({
visible: false,
newMessage: ''
})
}
});
const inviteStore = useInviteStore();
const { editInviteMessageDialog } = storeToRefs(inviteStore);
const message = ref('');
watch(
() => props.editInviteMessageDialog,
() => editInviteMessageDialog.value,
(newVal) => {
if (newVal && newVal.visible) {
message.value = newVal.newMessage;
@@ -58,10 +52,8 @@
{ deep: true }
);
const emit = defineEmits(['update:editInviteMessageDialog']);
function saveEditInviteMessage() {
const D = props.editInviteMessageDialog;
const D = editInviteMessageDialog.value;
D.visible = false;
if (D.inviteMessage.message !== message.value) {
const slot = D.inviteMessage.slot;
@@ -75,7 +67,6 @@
throw err;
})
.then((args) => {
API.$emit(`INVITE:${messageType.toUpperCase()}`, args);
if (args.json[slot].message === D.inviteMessage.message) {
$message({
message: "VRChat API didn't update message, try again",
@@ -91,6 +82,6 @@
}
function closeDialog() {
emit('update:editInviteMessageDialog', { ...props.editInviteMessageDialog, visible: false });
editInviteMessageDialog.value.visible = false;
}
</script>

View File

@@ -1,5 +1,5 @@
<template>
<safe-dialog :visible.sync="isVisible" :title="$t('dialog.export_own_avatars.header')" width="650px">
<safe-dialog :visible.sync="isVisible" :title="t('dialog.export_own_avatars.header')" width="650px">
<el-input
v-model="exportAvatarsListCsv"
v-loading="loading"
@@ -13,87 +13,94 @@
</safe-dialog>
</template>
<script>
<script setup>
import { ref, computed, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { storeToRefs } from 'pinia';
import { avatarRequest } from '../../../api';
import { processBulk } from '../../../service/request';
import { useAvatarStore, useUserStore } from '../../../stores';
export default {
name: 'ExportAvatarsListDialog',
inject: ['API'],
props: {
isExportAvatarsListDialogVisible: Boolean
const { t } = useI18n();
const { cachedAvatars } = storeToRefs(useAvatarStore());
const { applyAvatar } = useAvatarStore();
const { currentUser } = storeToRefs(useUserStore());
const props = defineProps({
isExportAvatarsListDialogVisible: {
type: Boolean,
required: true
}
});
const exportAvatarsListCsv = ref('');
const loading = ref(false);
const isVisible = computed({
get() {
return props.isExportAvatarsListDialogVisible;
},
data() {
return {
exportAvatarsListCsv: '',
loading: false
};
},
computed: {
isVisible: {
get() {
return this.isExportAvatarsListDialogVisible;
},
set(value) {
this.$emit('update:is-export-avatars-list-dialog-visible', value);
}
}
},
watch: {
isExportAvatarsListDialogVisible(value) {
if (value) {
this.initExportAvatarsListDialog();
}
}
},
methods: {
initExportAvatarsListDialog() {
this.loading = true;
for (const ref of this.API.cachedAvatars.values()) {
if (ref.authorId === this.API.currentUser.id) {
this.API.cachedAvatars.delete(ref.id);
}
}
const params = {
n: 50,
offset: 0,
sort: 'updated',
order: 'descending',
releaseStatus: 'all',
user: 'me'
};
const map = new Map();
this.API.bulk({
fn: avatarRequest.getAvatars,
N: -1,
params,
handle: (args) => {
for (const json of args.json) {
const $ref = this.API.cachedAvatars.get(json.id);
if (typeof $ref !== 'undefined') {
map.set($ref.id, $ref);
}
}
},
done: () => {
const avatars = Array.from(map.values());
if (Array.isArray(avatars) === false) {
return;
}
const lines = ['AvatarID,AvatarName'];
const _ = function (str) {
if (/[\x00-\x1f,"]/.test(str) === true) {
return `"${str.replace(/"/g, '""')}"`;
}
return str;
};
for (const avatar of avatars) {
lines.push(`${_(avatar.id)},${_(avatar.name)}`);
}
this.exportAvatarsListCsv = lines.join('\n');
this.loading = false;
}
});
set(value) {
emit('update:isExportAvatarsListDialogVisible', value);
}
});
const emit = defineEmits(['update:isExportAvatarsListDialogVisible']);
watch(
() => props.isExportAvatarsListDialogVisible,
(value) => {
if (value) {
initExportAvatarsListDialog();
}
}
};
);
function initExportAvatarsListDialog() {
loading.value = true;
for (const ref of cachedAvatars.value.values()) {
if (ref.authorId === currentUser.value.id) {
cachedAvatars.value.delete(ref.id);
}
}
const params = {
n: 50,
offset: 0,
sort: 'updated',
order: 'descending',
releaseStatus: 'all',
user: 'me'
};
const map = new Map();
processBulk({
fn: avatarRequest.getAvatars,
N: -1,
params,
handle: (args) => {
for (const json of args.json) {
const ref = applyAvatar(json);
map.set(ref.id, ref);
}
},
done: () => {
const avatars = Array.from(map.values());
if (Array.isArray(avatars) === false) {
return;
}
const lines = ['AvatarID,AvatarName'];
const _ = function (str) {
if (/[\x00-\x1f,"]/.test(str) === true) {
return `"${str.replace(/"/g, '""')}"`;
}
return str;
};
for (const avatar of avatars) {
lines.push(`${_(avatar.id)},${_(avatar.name)}`);
}
exportAvatarsListCsv.value = lines.join('\n');
loading.value = false;
}
});
}
</script>

View File

@@ -1,7 +1,7 @@
<template>
<safe-dialog :title="$t('dialog.export_friends_list.header')" :visible.sync="isVisible" width="650px">
<safe-dialog :title="t('dialog.export_friends_list.header')" :visible.sync="isVisible" width="650px">
<el-tabs type="card">
<el-tab-pane :label="$t('dialog.export_friends_list.csv')">
<el-tab-pane :label="t('dialog.export_friends_list.csv')">
<el-input
v-model="exportFriendsListCsv"
type="textarea"
@@ -12,7 +12,7 @@
style="margin-top: 15px"
@click.native="$event.target.tagName === 'TEXTAREA' && $event.target.select()" />
</el-tab-pane>
<el-tab-pane :label="$t('dialog.export_friends_list.json')">
<el-tab-pane :label="t('dialog.export_friends_list.json')">
<el-input
v-model="exportFriendsListJson"
type="textarea"
@@ -27,61 +27,71 @@
</safe-dialog>
</template>
<script>
export default {
name: 'ExportFriendsListDialog',
inject: ['API'],
props: {
friends: Map,
isExportFriendsListDialogVisible: Boolean
<script setup>
import { ref, computed, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { storeToRefs } from 'pinia';
import { useUserStore } from '../../../stores';
const props = defineProps({
friends: {
type: Map,
required: true
},
data() {
return {
exportFriendsListCsv: '',
exportFriendsListJson: ''
};
isExportFriendsListDialogVisible: {
type: Boolean,
required: true
}
});
const emit = defineEmits(['update:isExportFriendsListDialogVisible']);
const { currentUser } = storeToRefs(useUserStore());
const { t } = useI18n();
const exportFriendsListCsv = ref('');
const exportFriendsListJson = ref('');
const isVisible = computed({
get() {
return props.isExportFriendsListDialogVisible;
},
computed: {
isVisible: {
get() {
return this.isExportFriendsListDialogVisible;
},
set(value) {
this.$emit('update:is-export-friends-list-dialog-visible', value);
}
}
},
watch: {
isExportFriendsListDialogVisible(value) {
if (value) {
this.initExportFriendsListDialog();
}
}
},
methods: {
initExportFriendsListDialog() {
const { friends } = this.API.currentUser;
if (Array.isArray(friends) === false) {
return;
}
const lines = ['UserID,DisplayName,Memo'];
const _ = function (str) {
if (/[\x00-\x1f,"]/.test(str) === true) {
return `"${str.replace(/"/g, '""')}"`;
}
return str;
};
const friendsList = [];
for (const userId of friends) {
const ref = this.friends.get(userId);
const name = (typeof ref !== 'undefined' && ref.name) || '';
const memo = (typeof ref !== 'undefined' && ref.memo.replace(/\n/g, ' ')) || '';
lines.push(`${_(userId)},${_(name)},${_(memo)}`);
friendsList.push(userId);
}
this.exportFriendsListJson = JSON.stringify({ friends: friendsList }, null, 4);
this.exportFriendsListCsv = lines.join('\n');
set(value) {
emit('update:isExportFriendsListDialogVisible', value);
}
});
watch(
() => props.isExportFriendsListDialogVisible,
(value) => {
if (value) {
initExportFriendsListDialog();
}
}
};
);
function initExportFriendsListDialog() {
const { friends } = currentUser.value;
if (Array.isArray(friends) === false) {
return;
}
const lines = ['UserID,DisplayName,Memo'];
const _ = function (str) {
if (/[\x00-\x1f,"]/.test(str) === true) {
return `"${str.replace(/"/g, '""')}"`;
}
return str;
};
const friendsList = [];
for (const userId of friends) {
const ref = props.friends.get(userId);
const name = (typeof ref !== 'undefined' && ref.name) || '';
const memo = (typeof ref !== 'undefined' && ref.memo.replace(/\n/g, ' ')) || '';
lines.push(`${_(userId)},${_(name)},${_(memo)}`);
friendsList.push(userId);
}
exportFriendsListJson.value = JSON.stringify({ friends: friendsList }, null, 4);
exportFriendsListCsv.value = lines.join('\n');
}
</script>