diff --git a/html/src/app.js b/html/src/app.js
index 23b691ef..8d3598b0 100644
--- a/html/src/app.js
+++ b/html/src/app.js
@@ -2607,7 +2607,7 @@ speechSynthesis.getVoices();
var {unityPackages} = ref;
Object.assign(ref, json);
if (
- json.unityPackages.length > 0 &&
+ json.unityPackages?.length > 0 &&
unityPackages.length > 0 &&
!json.unityPackages.assetUrl
) {
@@ -16798,7 +16798,8 @@ speechSynthesis.getVoices();
fileSize: '',
inCache: false,
cacheSize: 0,
- cacheLocked: false
+ cacheLocked: false,
+ fileAnalysis: {}
};
API.$on('LOGOUT', function () {
@@ -16828,6 +16829,7 @@ speechSynthesis.getVoices();
D.visible = true;
D.loading = true;
D.id = avatarId;
+ D.fileAnalysis = {};
D.treeData = [];
D.fileSize = '';
D.inCache = false;
@@ -24917,27 +24919,22 @@ speechSynthesis.getVoices();
D.members = [];
this.isGroupMembersDone = false;
this.loadMoreGroupMembersParams = {
- n: 25,
+ n: 100,
offset: 0,
groupId: D.id
};
- if (this.hasGroupPermission(D.ref, 'group-members-viewall')) {
- // friend only group view perms only allow max n=25
- this.loadMoreGroupMembersParams.n = 100;
+ if (D.inGroup) {
+ await API.getGroupMember({
+ groupId: D.id,
+ userId: API.currentUser.id
+ }).then((args) => {
+ if (args.json) {
+ args.json.user = API.currentUser;
+ D.members.push(args.json);
+ }
+ return args;
+ });
}
- if (!D.inGroup) {
- return;
- }
- await API.getGroupMember({
- groupId: D.id,
- userId: API.currentUser.id
- }).then((args) => {
- if (args.json) {
- args.json.user = API.currentUser;
- D.members.push(args.json);
- }
- return args;
- });
await this.loadMoreGroupMembers();
};
@@ -24949,7 +24946,6 @@ speechSynthesis.getVoices();
this.isGroupMembersLoading = true;
await API.getGroupMembers(params)
.finally(() => {
- params.offset += params.n;
this.isGroupMembersLoading = false;
})
.then((args) => {
@@ -24969,10 +24965,25 @@ speechSynthesis.getVoices();
...this.groupDialog.members,
...args.json
];
+ params.offset += params.n;
return args;
+ })
+ .catch((err) => {
+ this.isGroupMembersDone = true;
+ throw err;
});
};
+ $app.methods.loadAllGroupMembers = async function () {
+ if (this.isGroupMembersLoading) {
+ return;
+ }
+ await this.getGroupDialogGroupMembers();
+ while (this.groupDialog.visible && !this.isGroupMembersDone) {
+ await this.loadMoreGroupMembers();
+ }
+ };
+
$app.methods.hasGroupPermission = function (ref, permission) {
if (
ref &&
@@ -25289,6 +25300,30 @@ speechSynthesis.getVoices();
}
};
+ $app.methods.downloadAndSaveJson = function (fileName, data) {
+ if (!fileName || !data) {
+ return;
+ }
+ try {
+ var 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();
+ }
+ };
+
$app.methods.setPlayerModeration = function (userId, type) {
var D = this.userDialog;
AppApi.SetVRChatUserModeration(API.currentUser.id, userId, type).then(
@@ -25452,6 +25487,53 @@ speechSynthesis.getVoices();
return text.replace(/([^!])\[[^\]]+\]\([^)]+\)/g, '$1');
};
+ /*
+ params: {
+ fileId: string,
+ version: number
+ }
+ */
+ API.getFileAnalysis = function (params) {
+ return this.call(`analysis/${params.fileId}/${params.version}`, {
+ method: 'GET'
+ }).then((json) => {
+ var args = {
+ json,
+ params
+ };
+ this.$emit('FILE:ANALYSIS', args);
+ return args;
+ });
+ };
+
+ API.$on('FILE:ANALYSIS', function (args) {
+ if (!$app.avatarDialog.visible) {
+ return;
+ }
+ $app.avatarDialog.fileAnalysis = buildTreeData(args.json);
+ });
+
+ $app.methods.getAvatarFileAnalysis = function () {
+ var D = this.avatarDialog;
+ var assetUrl = '';
+ for (let i = D.ref.unityPackages.length - 1; i > -1; i--) {
+ var unityPackage = D.ref.unityPackages[i];
+ if (
+ unityPackage.platform === 'standalonewindows' &&
+ this.compareUnityVersion(unityPackage.unityVersion)
+ ) {
+ assetUrl = unityPackage.assetUrl;
+ break;
+ }
+ }
+ var fileId = extractFileId(assetUrl);
+ var version = parseInt(extractFileVersion(assetUrl), 10);
+ if (!fileId || !version) {
+ return;
+ }
+ API.getFileAnalysis({fileId, version});
+ };
+
$app = new Vue($app);
window.$app = $app;
})();
diff --git a/html/src/index.pug b/html/src/index.pug
index b9fad337..56dfa873 100644
--- a/html/src/index.pug
+++ b/html/src/index.pug
@@ -1998,6 +1998,7 @@ html
span.extra(v-text="avatar.releaseStatus" v-else)
el-tab-pane(:label="$t('dialog.user.json.header')")
el-button(type="default" @click="refreshUserDialogTreeData()" size="mini" icon="el-icon-refresh" circle)
+ el-button(type="default" @click="downloadAndSaveJson(userDialog.id, userDialog.ref)" size="mini" icon="el-icon-download" circle style="margin-left:5px")
el-tree(:data="userDialog.treeData" style="margin-top:5px;font-size:12px")
template(#default="scope")
span
@@ -2172,6 +2173,7 @@ html
span.extra(v-else) {{ worldDialog.timeSpent | timeToText }}
el-tab-pane(:label="$t('dialog.world.json.header')")
el-button(type="default" @click="refreshWorldDialogTreeData()" size="mini" icon="el-icon-refresh" circle)
+ el-button(type="default" @click="downloadAndSaveJson(worldDialog.id, worldDialog.ref)" size="mini" icon="el-icon-download" circle style="margin-left:5px")
el-tree(:data="worldDialog.treeData" style="margin-top:5px;font-size:12px")
template(#default="scope")
span
@@ -2258,6 +2260,13 @@ html
span.extra(v-else) -
el-tab-pane(:label="$t('dialog.avatar.json.header')")
el-button(type="default" @click="refreshAvatarDialogTreeData()" size="mini" icon="el-icon-refresh" circle)
+ el-button(type="default" @click="getAvatarFileAnalysis" size="mini" icon="el-icon-question" circle style="margin-left:5px")
+ el-button(type="default" @click="downloadAndSaveJson(avatarDialog.id, avatarDialog.ref)" size="mini" icon="el-icon-download" circle style="margin-left:5px")
+ el-tree(v-if="Object.keys(avatarDialog.fileAnalysis).length > 0" :data="avatarDialog.fileAnalysis" style="margin-top:5px;font-size:12px")
+ template(#default="scope")
+ span
+ span(v-text="scope.data.key" style="font-weight:bold;margin-right:5px")
+ span(v-if="!scope.data.children" v-text="scope.data.value")
el-tree(:data="avatarDialog.treeData" style="margin-top:5px;font-size:12px")
template(#default="scope")
span
@@ -2449,11 +2458,12 @@ html
br
span {{ role.name }}{{ rIndex < groupDialog.memberRoles.length - 1 ? ', ' : '' }}
el-tab-pane(:label="$t('dialog.group.members.header')")
- template(v-if="groupDialog.visible && groupDialog.ref.membershipStatus === 'member'")
+ template(v-if="groupDialog.visible")
span(v-if="hasGroupPermission(groupDialog.ref, 'group-members-viewall')" style="font-weight:bold;font-size:16px") {{ $t('dialog.group.members.all_members') }}
span(v-else style="font-weight:bold;font-size:16px") {{ $t('dialog.group.members.friends_only') }}
br
- el-button(type="default" @click="getGroupDialogGroupMembers()" size="mini" icon="el-icon-refresh" circle)
+ el-button(type="default" @click="loadAllGroupMembers" size="mini" icon="el-icon-refresh" :loading="isGroupMembersLoading" circle)
+ el-button(type="default" @click="downloadAndSaveJson(`${groupDialog.id}_members`, groupDialog.members)" size="mini" icon="el-icon-download" circle style="margin-left:5px")
span(style="font-size:14px;margin-left:5px;margin-right:5px") {{ groupDialog.members.length }}/{{ groupDialog.ref.memberCount }}
ul.infinite-list.x-friend-list(v-if="groupDialog.members.length > 0" v-infinite-scroll="loadMoreGroupMembers" style="margin-top:10px;overflow:auto;max-height:250px")
li.infinite-list-item.x-friend-item(v-for="user in groupDialog.members" :key="user.id" @click="showUserDialog(user.userId)" class="x-friend-item-border")
@@ -2466,7 +2476,7 @@ html
.detail(v-if="!isGroupMembersLoading")
span.name {{ $t('dialog.group.members.load_more') }}
el-tab-pane(:label="$t('dialog.group.gallery.header')")
- el-button(type="default" size="mini" icon="el-icon-refresh" @click="getGroupGalleries" circle)
+ el-button(type="default" size="mini" icon="el-icon-refresh" @click="getGroupGalleries" :loading="isGroupGalleryLoading" circle)
el-tabs(type="card" v-loading="isGroupGalleryLoading" ref="groupDialogGallery")
template(v-for="(gallery, index) in groupDialog.ref.galleries")
el-tab-pane
@@ -2482,6 +2492,7 @@ html
img.x-link(v-lazy="image.imageUrl" style="height:700px" @click="downloadAndSaveImage(image.imageUrl)")
el-tab-pane(:label="$t('dialog.group.json.header')")
el-button(type="default" @click="refreshGroupDialogTreeData()" size="mini" icon="el-icon-refresh" circle)
+ el-button(type="default" @click="downloadAndSaveJson(groupDialog.id, groupDialog.ref)" size="mini" icon="el-icon-download" circle style="margin-left:5px")
el-tree(:data="groupDialog.treeData" style="margin-top:5px;font-size:12px")
template(#default="scope")
span
@@ -3459,16 +3470,13 @@ html
el-button(type="default" @click="deleteVRCPlusIcon(image.id)" size="mini" icon="el-icon-delete" circle style="margin-left:5px")
//- dialog Table: Previous Instances User
- el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="previousInstancesUserDialog" :visible.sync="previousInstancesUserDialog.visible" :title="$t('dialog.previous_instances.header')" width="800px")
+ el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="previousInstancesUserDialog" :visible.sync="previousInstancesUserDialog.visible" :title="$t('dialog.previous_instances.header')" width="1000px")
span(v-text="previousInstancesUserDialog.userRef.displayName" style="font-size:14px")
el-input(v-model="previousInstancesUserDialogTable.filters[0].value" :placeholder="$t('dialog.previous_instances.search_placeholder')" style="display:block;width:150px;margin-top:15px")
data-tables(v-if="previousInstancesUserDialog.visible" v-bind="previousInstancesUserDialogTable" v-loading="previousInstancesUserDialog.loading" style="margin-top:10px")
- el-table-column(:label="$t('table.previous_instances.date')" prop="created_at" sortable width="120")
+ el-table-column(:label="$t('table.previous_instances.date')" prop="created_at" sortable width="170")
template(v-once #default="scope")
- el-tooltip(placement="left")
- template(#content)
- span {{ scope.row.created_at | formatDate('long') }}
- span {{ scope.row.created_at | formatDate('short') }}
+ span {{ scope.row.created_at | formatDate('long') }}
el-table-column(:label="$t('table.previous_instances.world')" prop="name" sortable)
template(v-once #default="scope")
location(:location="scope.row.location" :hint="scope.row.worldName" :grouphint="scope.row.groupName")
@@ -3485,16 +3493,13 @@ html
el-button(type="text" icon="el-icon-close" size="mini" @click="confirmDeleteGameLogUserInstance(scope.row)")
//- dialog Table: Previous Instances World
- el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="previousInstancesWorldDialog" :visible.sync="previousInstancesWorldDialog.visible" :title="$t('dialog.previous_instances.header')" width="800px")
+ el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="previousInstancesWorldDialog" :visible.sync="previousInstancesWorldDialog.visible" :title="$t('dialog.previous_instances.header')" width="1000px")
span(v-text="previousInstancesWorldDialog.worldRef.name" style="font-size:14px")
el-input(v-model="previousInstancesWorldDialogTable.filters[0].value" :placeholder="$t('dialog.previous_instances.search_placeholder')" style="display:block;width:150px;margin-top:15px")
data-tables(v-if="previousInstancesWorldDialog.visible" v-bind="previousInstancesWorldDialogTable" v-loading="previousInstancesWorldDialog.loading" style="margin-top:10px")
- el-table-column(:label="$t('table.previous_instances.date')" prop="created_at" sortable width="120")
+ el-table-column(:label="$t('table.previous_instances.date')" prop="created_at" sortable width="170")
template(v-once #default="scope")
- el-tooltip(placement="left")
- template(#content)
- span {{ scope.row.created_at | formatDate('long') }}
- span {{ scope.row.created_at | formatDate('short') }}
+ span {{ scope.row.created_at | formatDate('long') }}
el-table-column(:label="$t('table.previous_instances.instance_name')" prop="name")
template(v-once #default="scope")
location-world(:locationobject="scope.row.$location" :grouphint="scope.row.groupName" :currentuserid="API.currentUser.id")