refactor: dialogs (#1224)

* refactor: dialogs

* fix: storeAvatarImage

* FriendLog.vue

* FriendLog.vue

* FriendLog.vue

* GameLog.vue

* fix: next day button jumping to the wrong date

* sync master

* fix: launchGame

* Notification.vue

* Feed.vue

* Search.vue

* Profile.vue

* PlayerList.vue

* Login.vue

* utils

* update dialog

* del gameLog.pug

* fix

* fix: group role cannot be displayed currently

* fix: "Hide Friends in Same Instance" hides players in unrelated private instances (#1210)

* fix

* fix: "Hide Friends in Same Instance" does not work when "Split Favorite Friends" is enabled

* fix Notification.vue message

* fix: deleteFavoriteNoConfirm

* fix: feed status style

* fix: infinite loading when deleting note

* fix: private players will not be hidden when 'Hide Friends in Same Instance', and 'Hide Friends in Same Instance' will not work when 'Split Favorite Friends'
This commit is contained in:
pa
2025-05-14 19:01:15 +09:00
committed by GitHub
parent 5ca028b30a
commit e792ed481b
130 changed files with 14208 additions and 10462 deletions

View File

@@ -1,13 +1,10 @@
<template>
<el-dialog
<safe-dialog
ref="avatarDialogRef"
class="x-dialog x-avatar-dialog"
:before-close="beforeDialogClose"
:visible.sync="avatarDialog.visible"
:show-close="false"
width="600px"
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
width="600px">
<div v-loading="avatarDialog.loading">
<div style="display: flex">
<el-popover placement="right" width="500px" trigger="click">
@@ -506,26 +503,40 @@
</div>
<SetAvatarTagsDialog :set-avatar-tags-dialog="setAvatarTagsDialog" />
<SetAvatarStylesDialog :set-avatar-styles-dialog="setAvatarStylesDialog" />
</el-dialog>
<ChangeAvatarImageDialog
:change-avatar-image-dialog-visible.sync="changeAvatarImageDialogVisible"
:previous-images-table="previousImagesTable"
:avatar-dialog="avatarDialog"
:previous-images-file-id="previousImagesFileId"
@refresh="displayPreviousImages" />
<PreviousImagesDialog
:previous-images-dialog-visible.sync="previousImagesDialogVisible"
:previous-images-table="previousImagesTable" />
</safe-dialog>
</template>
<script setup>
import { inject, computed, getCurrentInstance, reactive, nextTick, watch, ref } from 'vue';
import utils from '../../../classes/utils';
import database from '../../../service/database';
import { avatarModerationRequest, avatarRequest, favoriteRequest, miscRequest } from '../../../api';
import { computed, getCurrentInstance, inject, nextTick, reactive, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import SetAvatarTagsDialog from './SetAvatarTagsDialog.vue';
import { avatarModerationRequest, avatarRequest, favoriteRequest, imageRequest, miscRequest } from '../../../api';
import utils from '../../../classes/utils';
import { compareUnityVersion, storeAvatarImage } from '../../../composables/avatar/utils';
import {
copyToClipboard,
downloadAndSaveJson,
extractFileId,
extractFileVersion,
replaceVrcPackageUrl
} from '../../../composables/shared/utils';
import database from '../../../service/database';
import PreviousImagesDialog from '../PreviousImagesDialog.vue';
import ChangeAvatarImageDialog from './ChangeAvatarImageDialog.vue';
import SetAvatarStylesDialog from './SetAvatarStylesDialog.vue';
import SetAvatarTagsDialog from './SetAvatarTagsDialog.vue';
const API = inject('API');
const beforeDialogClose = inject('beforeDialogClose');
const dialogMouseDown = inject('dialogMouseDown');
const dialogMouseUp = inject('dialogMouseUp');
const showFullscreenImageDialog = inject('showFullscreenImageDialog');
const showUserDialog = inject('showUserDialog');
const displayPreviousImages = inject('displayPreviousImages');
const showAvatarDialog = inject('showAvatarDialog');
const showFavoriteDialog = inject('showFavoriteDialog');
const openExternalLink = inject('openExternalLink');
@@ -533,11 +544,9 @@
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const $confirm = instance.proxy.$confirm;
const $prompt = instance.proxy.$prompt;
const { $message, $confirm, $prompt } = instance.proxy;
const emit = defineEmits(['openFolderGeneric', 'deleteVRChatCache']);
const emit = defineEmits(['openFolderGeneric', 'deleteVRChatCache', 'openPreviousImagesDialog']);
const props = defineProps({
avatarDialog: {
@@ -555,6 +564,10 @@
});
const avatarDialogRef = ref(null);
const changeAvatarImageDialogVisible = ref(false);
const previousImagesFileId = ref('');
const previousImagesDialogVisible = ref(false);
const previousImagesTable = ref([]);
const treeData = ref([]);
const timeSpent = ref(0);
@@ -671,16 +684,16 @@
showAvatarDialog(D.id);
break;
case 'Share':
utils.copyToClipboard(D.id);
copyToClipboard(D.id);
break;
case 'Rename':
promptRenameAvatar(D);
break;
case 'Change Image':
displayPreviousImages('Avatar', 'Change');
displayPreviousImages('Change');
break;
case 'Previous Images':
displayPreviousImages('Avatar', 'Display');
displayPreviousImages('Display');
break;
case 'Change Description':
promptChangeAvatarDescription(D);
@@ -692,7 +705,7 @@
showSetAvatarStylesDialog(D.id);
break;
case 'Download Unity Package':
openExternalLink(utils.replaceVrcPackageUrl(props.avatarDialog.ref.unityPackageUrl));
openExternalLink(replaceVrcPackageUrl(props.avatarDialog.ref.unityPackageUrl));
break;
case 'Add Favorite':
showFavoriteDialog('avatar', D.id);
@@ -858,6 +871,54 @@
}
}
function displayPreviousImages(command) {
previousImagesTable.value = [];
previousImagesFileId.value = '';
const { imageUrl } = props.avatarDialog.ref;
const fileId = extractFileId(imageUrl);
if (!fileId) {
return;
}
const params = {
fileId
};
if (command === 'Display') {
previousImagesDialogVisible.value = true;
}
if (command === 'Change') {
changeAvatarImageDialogVisible.value = true;
}
imageRequest.getAvatarImages(params).then((args) => {
storeAvatarImage(args);
previousImagesFileId.value = args.json.id;
const images = [];
args.json.versions.forEach((item) => {
if (!item.deleted) {
images.unshift(item);
}
});
checkPreviousImageAvailable(images);
});
}
async function checkPreviousImageAvailable(images) {
previousImagesTable.value = [];
for (const image of images) {
if (image.file && image.file.url) {
const response = await fetch(image.file.url, {
method: 'HEAD',
redirect: 'follow'
}).catch((error) => {
console.log(error);
});
if (response.status === 200) {
previousImagesTable.value.push(image);
}
}
}
}
function selectAvatar(id) {
avatarRequest
.selectAvatar({
@@ -937,11 +998,11 @@
}
function copyAvatarId(id) {
utils.copyToClipboard(id);
copyToClipboard(id);
}
function copyAvatarUrl(id) {
utils.copyToClipboard(`https://vrchat.com/home/avatar/${id}`);
copyToClipboard(`https://vrchat.com/home/avatar/${id}`);
}
function timeToText(time) {
@@ -963,10 +1024,7 @@
if (unityPackage.variant !== 'security') {
continue;
}
if (
unityPackage.platform === 'standalonewindows' &&
utils.compareUnityVersion(unityPackage.unitySortNumber)
) {
if (unityPackage.platform === 'standalonewindows' && compareUnityVersion(unityPackage.unitySortNumber)) {
assetUrl = unityPackage.assetUrl;
break;
}
@@ -979,7 +1037,7 @@
}
if (
unityPackage.platform === 'standalonewindows' &&
utils.compareUnityVersion(unityPackage.unitySortNumber)
compareUnityVersion(unityPackage.unitySortNumber)
) {
variant = 'standard';
assetUrl = unityPackage.assetUrl;
@@ -990,8 +1048,8 @@
if (!assetUrl) {
assetUrl = D.ref.assetUrl;
}
const fileId = utils.extractFileId(assetUrl);
const version = parseInt(utils.extractFileVersion(assetUrl), 10);
const fileId = extractFileId(assetUrl);
const version = parseInt(extractFileVersion(assetUrl), 10);
if (!fileId || !version) {
$message({
message: 'File Analysis unavailable',
@@ -1100,8 +1158,4 @@
D.loading = false;
});
}
function downloadAndSaveJson(fileName, data) {
utils.downloadAndSaveJson(fileName, data);
}
</script>

View File

@@ -0,0 +1,393 @@
<template>
<safe-dialog
class="x-dialog"
:visible="changeAvatarImageDialogVisible"
:title="t('dialog.change_content_image.avatar')"
width="850px"
append-to-body
@close="closeDialog">
<div v-loading="changeAvatarImageDialogLoading">
<input
id="AvatarImageUploadButton"
type="file"
accept="image/*"
style="display: none"
@change="onFileChangeAvatarImage" />
<span>{{ t('dialog.change_content_image.description') }}</span>
<br />
<el-button-group style="padding-bottom: 10px; padding-top: 10px">
<el-button type="default" size="small" icon="el-icon-refresh" @click="refresh">
{{ t('dialog.change_content_image.refresh') }}
</el-button>
<el-button type="default" size="small" icon="el-icon-upload2" @click="uploadAvatarImage">
{{ t('dialog.change_content_image.upload') }}
</el-button>
</el-button-group>
<br />
<div
v-for="image in previousImagesTable"
v-if="image.file"
:key="image.version"
style="display: inline-block">
<div
class="x-change-image-item"
style="cursor: pointer"
:class="{ 'current-image': compareCurrentImage(image) }"
@click="setAvatarImage(image)">
<img v-lazy="image.file.url" class="image" />
</div>
</div>
</div>
</safe-dialog>
</template>
<script setup>
import { getCurrentInstance, inject, ref } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { imageRequest } from '../../../api';
import { extractFileId } from '../../../composables/shared/utils';
import webApiService from '../../../service/webapi';
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const API = inject('API');
const props = defineProps({
changeAvatarImageDialogVisible: {
type: Boolean,
default: false
},
previousImagesTable: {
type: Array,
default: () => []
},
avatarDialog: {
type: Object,
default: () => ({})
},
previousImagesFileId: {
type: String,
default: ''
}
});
const changeAvatarImageDialogLoading = ref(false);
const avatarImage = ref({
base64File: '',
fileMd5: '',
base64SignatureFile: '',
signatureMd5: '',
fileId: '',
avatarId: ''
});
const emit = defineEmits(['update:changeAvatarImageDialogVisible', 'refresh']);
function refresh() {
emit('refresh', 'Change');
}
function closeDialog() {
emit('update:changeAvatarImageDialogVisible', false);
}
async function resizeImageToFitLimits(file) {
const response = await AppApi.ResizeImageToFitLimits(file);
return response;
}
async function genMd5(file) {
const response = await AppApi.MD5File(file);
return response;
}
async function genSig(file) {
const response = await AppApi.SignFile(file);
return response;
}
async function genLength(file) {
const response = await AppApi.FileLength(file);
return response;
}
function onFileChangeAvatarImage(e) {
const clearFile = function () {
if (document.querySelector('#AvatarImageUploadButton')) {
document.querySelector('#AvatarImageUploadButton').value = '';
}
};
const files = e.target.files || e.dataTransfer.files;
if (!files.length || !props.avatarDialog.visible || props.avatarDialog.loading) {
clearFile();
return;
}
// validate file
if (files[0].size >= 100000000) {
// 100MB
$message({
message: t('message.file.too_large'),
type: 'error'
});
clearFile();
return;
}
if (!files[0].type.match(/image.*/)) {
$message({
message: t('message.file.not_image'),
type: 'error'
});
clearFile();
return;
}
const r = new FileReader();
r.onload = async function (file) {
try {
const base64File = await resizeImageToFitLimits(btoa(r.result));
// 10MB
const fileMd5 = await genMd5(base64File);
const fileSizeInBytes = parseInt(file.total, 10);
const base64SignatureFile = await genSig(base64File);
const signatureMd5 = await genMd5(base64SignatureFile);
const signatureSizeInBytes = parseInt(await genLength(base64SignatureFile), 10);
const avatarId = props.avatarDialog.id;
const { imageUrl } = props.avatarDialog.ref;
const fileId = extractFileId(imageUrl);
if (!fileId) {
$message({
message: t('message.avatar.image_invalid'),
type: 'error'
});
clearFile();
return;
}
avatarImage.value = {
base64File,
fileMd5,
base64SignatureFile,
signatureMd5,
fileId,
avatarId
};
const params = {
fileMd5,
fileSizeInBytes,
signatureMd5,
signatureSizeInBytes
};
// Upload chaining
await initiateUpload(params, fileId);
} catch (error) {
console.error('Avatar image upload process failed:', error);
} finally {
changeAvatarImageDialogLoading.value = false;
clearFile();
}
};
changeAvatarImageDialogLoading.value = true;
r.readAsBinaryString(files[0]);
}
// ------------ Upload Process Start ------------
async function initiateUpload(params, fileId) {
const res = await imageRequest.uploadAvatarImage(params, fileId);
return avatarImageInit(res);
}
async function avatarImageInit(args) {
// API.$on('AVATARIMAGE:INIT')
const fileId = args.json.id;
const fileVersion = args.json.versions[args.json.versions.length - 1].version;
const params = {
fileId,
fileVersion
};
const res = await imageRequest.uploadAvatarImageFileStart(params);
return avatarImageFileStart(res);
}
async function avatarImageFileStart(args) {
// API.$on('AVATARIMAGE:FILESTART')
const { url } = args.json;
const { fileId, fileVersion } = args.params;
const params = {
url,
fileId,
fileVersion
};
return uploadAvatarImageFileAWS(params);
}
async function uploadAvatarImageFileAWS(params) {
const json = await webApiService.execute({
url: params.url,
uploadFilePUT: true,
fileData: avatarImage.value.base64File,
fileMIME: 'image/png',
headers: {
'Content-MD5': avatarImage.value.fileMd5
}
});
if (json.status !== 200) {
changeAvatarImageDialogLoading.value = false;
API.$throw('Avatar image upload failed', json, params.url);
}
const args = {
json,
params
};
return avatarImageFileAWS(args);
}
async function avatarImageFileAWS(args) {
// API.$on('AVATARIMAGE:FILEAWS')
const { fileId, fileVersion } = args.params;
const params = {
fileId,
fileVersion
};
const res = await imageRequest.uploadAvatarImageFileFinish(params);
return avatarImageFileFinish(res);
}
async function avatarImageFileFinish(args) {
// API.$on('AVATARIMAGE:FILEFINISH')
const { fileId, fileVersion } = args.params;
const params = {
fileId,
fileVersion
};
const res = await imageRequest.uploadAvatarImageSigStart(params);
return avatarImageSigStart(res);
}
async function avatarImageSigStart(args) {
// API.$on('AVATARIMAGE:SIGSTART')
const { url } = args.json;
const { fileId, fileVersion } = args.params;
const params = {
url,
fileId,
fileVersion
};
return uploadAvatarImageSigAWS(params);
}
async function uploadAvatarImageSigAWS(params) {
const json = await webApiService.execute({
url: params.url,
uploadFilePUT: true,
fileData: avatarImage.value.base64SignatureFile,
fileMIME: 'application/x-rsync-signature',
headers: {
'Content-MD5': avatarImage.value.signatureMd5
}
});
if (json.status !== 200) {
changeAvatarImageDialogLoading.value = false;
API.$throw('Avatar image upload failed', json, params.url);
}
const args = {
json,
params
};
return avatarImageSigAWS(args);
}
async function avatarImageSigAWS(args) {
// API.$on('AVATARIMAGE:SIGAWS')
const { fileId, fileVersion } = args.params;
const params = {
fileId,
fileVersion
};
const res = await imageRequest.uploadAvatarImageSigFinish(params);
return avatarImageSigFinish(res);
}
async function avatarImageSigFinish(args) {
// API.$on('AVATARIMAGE:SIGFINISH')
const { fileId, fileVersion } = args.params;
const parmas = {
id: avatarImage.value.avatarId,
imageUrl: `${API.endpointDomain}/file/${fileId}/${fileVersion}/file`
};
const res = await imageRequest.setAvatarImage(parmas);
return avatarImageSet(res);
}
async function avatarImageSet(args) {
// API.$on('AVATARIMAGE:SET')
changeAvatarImageDialogLoading.value = false;
if (args.json.imageUrl === args.params.imageUrl) {
$message({
message: t('message.avatar.image_changed'),
type: 'success'
});
refresh();
} else {
API.$throw(0, 'Avatar image change failed', args.params.imageUrl);
}
}
// ------------ Upload Process End ------------
function uploadAvatarImage() {
document.getElementById('AvatarImageUploadButton').click();
}
function setAvatarImage(image) {
changeAvatarImageDialogLoading.value = true;
const parmas = {
id: props.avatarDialog.id,
imageUrl: `${API.endpointDomain}/file/${props.previousImagesFileId}/${image.version}/file`
};
imageRequest.setAvatarImage(parmas).finally(() => {
changeAvatarImageDialogLoading.value = false;
closeDialog();
});
}
function compareCurrentImage(image) {
return (
`${API.endpointDomain}/file/${props.previousImagesFileId}/${image.version}/file` ===
props.avatarDialog.ref.imageUrl
);
}
// $app.methods.deleteAvatarImage = function () {
// this.changeAvatarImageDialogLoading = true;
// var parmas = {
// fileId: this.previousImagesFileId,
// version: this.previousImagesTable[0].version
// };
// vrcPlusIconRequest
// .deleteFileVersion(parmas)
// .then((args) => {
// this.previousImagesFileId = args.json.id;
// var images = [];
// args.json.versions.forEach((item) => {
// if (!item.deleted) {
// images.unshift(item);
// }
// });
// this.checkPreviousImageAvailable(images);
// })
// .finally(() => {
// this.changeAvatarImageDialogLoading = false;
// });
// };
</script>

View File

@@ -1,14 +1,11 @@
<template>
<el-dialog
<safe-dialog
ref="setAvatarStylesDialog"
class="x-dialog"
:before-close="beforeDialogClose"
:visible.sync="setAvatarStylesDialog.visible"
:title="t('dialog.set_avatar_styles.header')"
width="400px"
append-to-body
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
append-to-body>
<template v-if="setAvatarStylesDialog.visible">
<div>
<span>{{ t('dialog.set_avatar_styles.primary_style') }}</span>
@@ -50,19 +47,15 @@
t('dialog.set_avatar_styles.save')
}}</el-button>
</template>
</el-dialog>
</safe-dialog>
</template>
<script setup>
import { inject, watch, getCurrentInstance } from 'vue';
import { watch, getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { avatarRequest } from '../../../api';
const beforeDialogClose = inject('beforeDialogClose');
const dialogMouseDown = inject('dialogMouseDown');
const dialogMouseUp = inject('dialogMouseUp');
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;

View File

@@ -1,14 +1,11 @@
<template>
<el-dialog
<safe-dialog
ref="setAvatarTagsDialog"
class="x-dialog"
:before-close="beforeDialogClose"
:visible.sync="setAvatarTagsDialog.visible"
:title="t('dialog.set_avatar_tags.header')"
width="770px"
append-to-body
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
append-to-body>
<template v-if="setAvatarTagsDialog.visible">
<el-checkbox v-model="setAvatarTagsDialog.contentHorror" @change="updateSelectedAvatarTags">{{
t('dialog.set_avatar_tags.content_horror')
@@ -93,7 +90,7 @@
t('dialog.set_avatar_tags.save')
}}</el-button>
</template>
</el-dialog>
</safe-dialog>
</template>
<script setup>
@@ -102,9 +99,6 @@
import { useI18n } from 'vue-i18n-bridge';
import { avatarRequest } from '../../../api';
const beforeDialogClose = inject('beforeDialogClose');
const dialogMouseDown = inject('dialogMouseDown');
const dialogMouseUp = inject('dialogMouseUp');
const showAvatarDialog = inject('showAvatarDialog');
const { t } = useI18n();