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();

View File

@@ -1,12 +1,5 @@
<template>
<el-dialog
ref="favoriteDialog"
:before-close="beforeDialogClose"
:visible.sync="isVisible"
:title="$t('dialog.favorite.header')"
width="300px"
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
<safe-dialog ref="favoriteDialog" :visible.sync="isVisible" :title="$t('dialog.favorite.header')" width="300px">
<div v-loading="loading">
<span style="display: block; text-align: center">{{ $t('dialog.favorite.vrchat_favorites') }}</span>
<template v-if="favoriteDialog.currentGroup && favoriteDialog.currentGroup.key">
@@ -69,7 +62,7 @@
</el-button>
</template>
</div>
</el-dialog>
</safe-dialog>
</template>
<script>
@@ -78,7 +71,7 @@
export default {
name: 'ChooseFavoriteGroupDialog',
inject: ['API', 'beforeDialogClose', 'dialogMouseDown', 'dialogMouseUp', 'adjustDialogZ'],
inject: ['API', 'adjustDialogZ'],
props: {
favoriteDialog: {
type: Object,
@@ -186,7 +179,7 @@
this.$emit('remove-local-avatar-favorite', ...args);
},
deleteFavoriteNoConfirm(...args) {
this.$emit('delete-favorite-no-confirm', ...args);
this.$emit('deleteFavoriteNoConfirm', ...args);
}
}
};

View File

@@ -0,0 +1,91 @@
<template>
<safe-dialog
ref="fullscreenImageDialog"
class="x-dialog"
:visible.sync="fullscreenImageDialog.visible"
append-to-body
top="1vh"
width="97vw">
<div>
<div style="margin: 0 0 5px 5px">
<el-button
size="mini"
icon="el-icon-s-order"
circle
@click="copyImageUrl(fullscreenImageDialog.imageUrl)"></el-button>
<el-button
type="default"
size="mini"
icon="el-icon-download"
circle
style="margin-left: 5px"
@click="
downloadAndSaveImage(fullscreenImageDialog.imageUrl, fullscreenImageDialog.fileName)
"></el-button>
</div>
<img v-lazy="fullscreenImageDialog.imageUrl" style="width: 100%; height: 85vh; object-fit: contain" />
</div>
</safe-dialog>
</template>
<script setup>
import { getCurrentInstance } from 'vue';
import utils from '../../classes/utils';
import { copyToClipboard, extractFileId } from '../../composables/shared/utils';
import webApiService from '../../service/webapi';
import Noty from 'noty';
const { proxy } = getCurrentInstance();
const { $message } = proxy;
defineProps({
fullscreenImageDialog: {
type: Object,
default: () => ({})
}
});
function copyImageUrl(imageUrl) {
copyToClipboard(imageUrl, 'ImageUrl copied to clipboard');
}
async function downloadAndSaveImage(url, fileName) {
if (!url) {
return;
}
$message({
message: 'Downloading image...',
type: 'info'
});
try {
const response = await webApiService.execute({
url,
method: 'GET'
});
if (response.status !== 200 || !response.data.startsWith('data:image/png')) {
throw new Error(`Error: ${response.data}`);
}
const link = document.createElement('a');
link.href = response.data;
const fileId = extractFileId(url);
if (!fileName && fileId) {
fileName = `${fileId}.png`;
}
if (!fileName) {
fileName = `${url.split('/').pop()}.png`;
}
if (!fileName) {
fileName = 'image.png';
}
link.setAttribute('download', fileName);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} catch {
new Noty({
type: 'error',
text: utils.escapeTag(`Failed to download image. ${url}`)
}).show();
}
}
</script>

View File

@@ -0,0 +1,136 @@
<template>
<safe-dialog
class="x-dialog"
:visible.sync="gallerySelectDialog.visible"
:title="t('dialog.gallery_select.header')"
width="100%"
append-to-body>
<div>
<span>{{ t('dialog.gallery_select.gallery') }}</span>
<span style="color: #909399; font-size: 12px; margin-left: 5px">{{ galleryTable.length }}/64</span>
<br />
<input
id="GalleryUploadButton"
type="file"
accept="image/*"
style="display: none"
@change="onFileChangeGallery" />
<el-button-group>
<el-button type="default" size="small" icon="el-icon-close" @click="selectImageGallerySelect('', '')">{{
t('dialog.gallery_select.none')
}}</el-button>
<el-button type="default" size="small" icon="el-icon-refresh" @click="refreshGalleryTable">{{
t('dialog.gallery_select.refresh')
}}</el-button>
<el-button
type="default"
size="small"
icon="el-icon-upload2"
:disabled="!API.currentUser.$isVRCPlus"
@click="displayGalleryUpload"
>{{ t('dialog.gallery_select.upload') }}</el-button
>
</el-button-group>
<br />
<div
v-for="image in galleryTable"
v-if="image.versions && image.versions.length > 0"
:key="image.id"
class="x-friend-item"
style="display: inline-block; margin-top: 10px; width: unset; cursor: default">
<div
v-if="image.versions[image.versions.length - 1].file.url"
class="vrcplus-icon"
@click="selectImageGallerySelect(image.versions[image.versions.length - 1].file.url, image.id)">
<img v-lazy="image.versions[image.versions.length - 1].file.url" class="avatar" />
</div>
</div>
</div>
</safe-dialog>
</template>
<script setup>
import { inject, getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { vrcPlusImageRequest } from '../../../api';
const { t } = useI18n();
const { proxy } = getCurrentInstance();
const { $message } = proxy;
const API = inject('API');
const props = defineProps({
gallerySelectDialog: {
type: Object,
required: true
},
galleryTable: {
type: Array,
required: true
}
});
const emit = defineEmits(['refreshGalleryTable']);
function selectImageGallerySelect(imageUrl, fileId) {
const D = props.gallerySelectDialog;
D.selectedFileId = fileId;
D.selectedImageUrl = imageUrl;
D.visible = false;
}
function displayGalleryUpload() {
document.getElementById('GalleryUploadButton').click();
}
function onFileChangeGallery(e) {
const clearFile = function () {
if (document.querySelector('#GalleryUploadButton')) {
document.querySelector('#GalleryUploadButton').value = '';
}
};
const files = e.target.files || e.dataTransfer.files;
if (!files.length) {
return;
}
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 = function () {
const base64Body = btoa(r.result);
vrcPlusImageRequest.uploadGalleryImage(base64Body).then((args) => {
$message({
message: t('message.gallery.uploaded'),
type: 'success'
});
// API.$on('GALLERYIMAGE:ADD')
if (Object.keys(props.galleryTable).length !== 0) {
props.galleryTable.unshift(args.json);
}
return args;
});
};
r.readAsBinaryString(files[0]);
clearFile();
}
function refreshGalleryTable() {
emit('refreshGalleryTable');
}
</script>

View File

@@ -1,14 +1,11 @@
<template>
<el-dialog
<safe-dialog
ref="groupDialogRef"
:before-close="beforeDialogClose"
:visible.sync="groupDialog.visible"
:show-close="false"
width="770px"
top="10vh"
class="x-dialog x-group-dialog"
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
class="x-dialog x-group-dialog">
<div class="group-banner-image">
<el-popover placement="right" width="500px" trigger="click">
<img
@@ -1151,10 +1148,7 @@
</el-tabs>
</div>
<!--Nested-->
<GroupPostEditDialog
:gallery-select-dialog="gallerySelectDialog"
:dialog-data.sync="groupPostEditDialog"
@clear-image-gallery-select="clearImageGallerySelect" />
<GroupPostEditDialog :dialog-data.sync="groupPostEditDialog" :selected-gallery-file="selectedGalleryFile" />
<GroupMemberModerationDialog
:group-dialog="groupDialog"
:is-group-members-loading.sync="isGroupMembersLoading"
@@ -1167,25 +1161,32 @@
@load-all-group-members="loadAllGroupMembers"
@set-group-member-filter="setGroupMemberFilter"
@set-group-member-sort-order="setGroupMemberSortOrder" />
</el-dialog>
<InviteGroupDialog
:dialog-data.sync="inviteGroupDialog"
:vip-friends="vipFriends"
:online-friends="onlineFriends"
:offline-friends="offlineFriends"
:active-friends="activeFriends" />
</safe-dialog>
</template>
<script setup>
import { getCurrentInstance, nextTick, reactive, ref, watch, inject } from 'vue';
import { getCurrentInstance, inject, nextTick, reactive, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import utils from '../../../classes/utils';
import { groupRequest } from '../../../api';
import Location from '../../Location.vue';
import GroupPostEditDialog from './GroupPostEditDialog.vue';
import GroupMemberModerationDialog from './GroupMemberModerationDialog.vue';
import * as workerTimers from 'worker-timers';
import { groupRequest } from '../../../api';
import utils from '../../../classes/utils';
import { hasGroupPermission } from '../../../composables/group/utils';
import { refreshInstancePlayerCount } from '../../../composables/instance/utils';
import { copyToClipboard, downloadAndSaveJson, getFaviconUrl } from '../../../composables/shared/utils';
import { languageClass } from '../../../composables/user/utils';
import Location from '../../Location.vue';
import InviteGroupDialog from '../InviteGroupDialog.vue';
import GroupMemberModerationDialog from './GroupMemberModerationDialog.vue';
import GroupPostEditDialog from './GroupPostEditDialog.vue';
const API = inject('API');
const beforeDialogClose = inject('beforeDialogClose');
const dialogMouseDown = inject('dialogMouseDown');
const dialogMouseUp = inject('dialogMouseUp');
const showFullscreenImageDialog = inject('showFullscreenImageDialog');
const languageClass = inject('languageClass');
const showUserDialog = inject('showUserDialog');
const userStatusClass = inject('userStatusClass');
const userImage = inject('userImage');
@@ -1222,28 +1223,33 @@
type: Object,
required: true
},
gallerySelectDialog: {
type: Object,
default: () => ({})
},
randomUserColours: {
type: Boolean,
default: true
},
vipFriends: {
type: Array,
default: () => []
},
onlineFriends: {
type: Array,
default: () => []
},
offlineFriends: {
type: Array,
default: () => []
},
activeFriends: {
type: Array,
default: () => []
}
});
const emit = defineEmits([
'update:group-dialog',
'update:gallery-select-dialog',
'update:group-member-moderation',
'group-dialog-command',
'update:group-dialog',
'groupDialogCommand',
'get-group-dialog-group',
'get-group-dialog-group-members',
'refresh-instance-player-count',
'update-group-post-search',
'set-group-member-sort-order',
'clear-image-gallery-select'
'updateGroupPostSearch'
]);
const groupDialogRef = ref(null);
@@ -1254,6 +1260,10 @@
const groupDialogGalleryCurrentName = ref('0');
const groupDialogTabCurrentName = ref('0');
const isGroupGalleryLoading = ref(false);
const selectedGalleryFile = ref({
selectedFileId: '',
selectedImageUrl: ''
});
const groupPostEditDialog = reactive({
visible: false,
groupRef: {},
@@ -1273,6 +1283,16 @@
auditLogTypes: []
});
const inviteGroupDialog = ref({
visible: false,
loading: false,
groupId: '',
groupName: '',
userId: '',
userIds: [],
userObject: {}
});
let loadMoreGroupMembersParams = {};
watch(
@@ -1293,8 +1313,15 @@
}
);
function getFaviconUrl(link) {
return utils.getFaviconUrl(link);
function showInviteGroupDialog(groupId, userId) {
const D = inviteGroupDialog.value;
D.userIds = '';
D.groups = [];
D.groupId = groupId;
D.groupName = groupId;
D.userId = userId;
D.userObject = {};
D.visible = true;
}
function setGroupRepresentation(groupId) {
@@ -1439,9 +1466,7 @@
}
});
}
function copyToClipboard(text) {
utils.copyToClipboard(text);
}
function groupGalleryStatus(gallery) {
const style = {};
if (!gallery.membersOnly) {
@@ -1455,6 +1480,10 @@
}
function groupDialogCommand(command) {
const D = props.groupDialog;
if (D.visible === false) {
return;
}
switch (command) {
case 'Share':
copyToClipboard(props.groupDialog.ref.$url);
@@ -1465,8 +1494,11 @@
case 'Moderation Tools':
showGroupMemberModerationDialog(props.groupDialog.id);
break;
case 'Invite To Group':
showInviteGroupDialog(D.id, '');
break;
default:
emit('group-dialog-command', command);
emit('groupDialogCommand', command);
}
}
@@ -1481,7 +1513,7 @@
D.auditLogTypes = [];
API.getCachedGroup({ groupId }).then((args) => {
D.groupRef = args.ref;
if (utils.hasGroupPermission(D.groupRef, 'group-audit-view')) {
if (hasGroupPermission(D.groupRef, 'group-audit-view')) {
groupRequest.getGroupAuditLogTypes({ groupId }).then((args) => {
// API.$on('GROUP:AUDITLOGTYPES', function (args) {
if (groupMemberModeration.id !== args.params.groupId) {
@@ -1569,18 +1601,21 @@
D.roleIds = [];
D.postId = '';
D.groupId = groupId;
emit('update:gallery-select-dialog', { ...D, selectedFileId: '', selectedImageUrl: '' });
selectedGalleryFile.value = {
selectedFileId: '',
selectedImageUrl: ''
};
if (post) {
D.title = post.title;
D.text = post.text;
D.visibility = post.visibility;
D.roleIds = post.roleIds;
D.postId = post.id;
emit('update:gallery-select-dialog', {
...D,
selectedGalleryFile.value = {
selectedFileId: post.imageId,
selectedImageUrl: post.imageUrl
});
};
}
API.getCachedGroup({ groupId }).then((args) => {
D.groupRef = args.ref;
@@ -1763,9 +1798,6 @@
await getGroupDialogGroupMembers();
}
function hasGroupPermission(ref, permission) {
return utils.hasGroupPermission(ref, permission);
}
function updateGroupDialogData(obj) {
// Be careful with the deep merge
emit('update:group-dialog', obj);
@@ -1773,16 +1805,7 @@
function getGroupDialogGroup(groupId) {
emit('get-group-dialog-group', groupId);
}
function refreshInstancePlayerCount(tag) {
emit('refresh-instance-player-count', tag);
}
function updateGroupPostSearch() {
emit('update-group-post-search');
}
function downloadAndSaveJson(fileName, data) {
utils.downloadAndSaveJson(fileName, data);
}
function clearImageGallerySelect() {
emit('clear-image-gallery-select');
emit('updateGroupPostSearch');
}
</script>

View File

@@ -1,15 +1,12 @@
<template>
<el-dialog
<safe-dialog
class="x-dialog"
:before-close="beforeDialogClose"
:visible="groupMemberModeration.visible"
:title="t('dialog.group_member_moderation.header')"
append-to-body
top="5vh"
width="90vw"
@close="closeDialog"
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
@close="closeDialog">
<div>
<h3>{{ groupMemberModeration.groupRef.name }}</h3>
<el-tabs type="card" style="height: 100%">
@@ -806,21 +803,18 @@
<group-member-moderation-export-dialog
:is-group-logs-export-dialog-visible.sync="isGroupLogsExportDialogVisible"
:group-logs-moderation-table="groupLogsModerationTable" />
</el-dialog>
</safe-dialog>
</template>
<script setup>
import { getCurrentInstance, inject, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import utils from '../../../classes/utils';
import { groupRequest, userRequest } from '../../../api';
import { useModerationTable, useSelectedUsers } from '../../../composables/group/useGroupMemberModeration';
import { hasGroupPermission } from '../../../composables/group/utils';
import GroupMemberModerationExportDialog from './GroupMemberModerationExportDialog.vue';
import { useModerationTable, useSelectedUsers } from '../../../composables/groups/useGroupMemberModeration';
const API = inject('API');
const beforeDialogClose = inject('beforeDialogClose');
const dialogMouseDown = inject('dialogMouseDown');
const dialogMouseUp = inject('dialogMouseUp');
const showUserDialog = inject('showUserDialog');
const userImage = inject('userImage');
const userImageFull = inject('userImageFull');
@@ -1687,8 +1681,4 @@
.replace(/\./g, ' ')
.replace(/\b\w/g, (l) => l.toUpperCase());
}
function hasGroupPermission(ref, permission) {
return utils.hasGroupPermission(ref, permission);
}
</script>

View File

@@ -1,14 +1,11 @@
<template>
<el-dialog
<safe-dialog
class="x-dialog"
:before-close="beforeDialogClose"
:visible="isGroupLogsExportDialogVisible"
:title="t('dialog.group_member_moderation.export_logs')"
width="650px"
append-to-body
@close="setIsGroupLogsExportDialogVisible"
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
@close="setIsGroupLogsExportDialogVisible">
<el-checkbox-group
v-model="checkedGroupLogsExportLogsOptions"
style="margin-bottom: 10px"
@@ -29,17 +26,14 @@
readonly
style="margin-top: 15px"
@click.native="handleCopyGroupLogsExportContent" />
</el-dialog>
</safe-dialog>
</template>
<script setup>
import { inject, ref, watch } from 'vue';
import { ref, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import utils from '../../../classes/utils';
import { copyToClipboard } from '../../../composables/shared/utils';
const beforeDialogClose = inject('beforeDialogClose');
const dialogMouseDown = inject('dialogMouseDown');
const dialogMouseUp = inject('dialogMouseUp');
const { t } = useI18n();
const props = defineProps({
@@ -101,7 +95,7 @@
}
function handleCopyGroupLogsExportContent() {
utils.copyToClipboard(groupLogsExportContent.value);
copyToClipboard(groupLogsExportContent.value);
}
function setIsGroupLogsExportDialogVisible() {

View File

@@ -1,13 +1,9 @@
<template>
<el-dialog
ref="groupPostEditDialog"
:before-close="beforeDialogClose"
<safe-dialog
:visible.sync="groupPostEditDialog.visible"
:title="$t('dialog.group_post_edit.header')"
width="650px"
append-to-body
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
append-to-body>
<div v-if="groupPostEditDialog.visible">
<h3 v-text="groupPostEditDialog.groupRef.name"></h3>
<el-form :model="groupPostEditDialog" label-width="150px">
@@ -107,30 +103,39 @@
{{ $t('dialog.group_post_edit.create_post') }}
</el-button>
</template>
</el-dialog>
<GallerySelectDialog
:gallery-select-dialog="gallerySelectDialog"
:gallery-table="galleryTable"
@refresh-gallery-table="refreshGalleryTable" />
</safe-dialog>
</template>
<script>
import { groupRequest } from '../../../api';
import { groupRequest, vrcPlusIconRequest } from '../../../api';
import GallerySelectDialog from './GallerySelectDialog.vue';
export default {
name: 'GroupPostEditDialog',
inject: [
'beforeDialogClose',
'showFullscreenImageDialog',
'dialogMouseDown',
'dialogMouseUp',
'showGallerySelectDialog'
],
components: {
GallerySelectDialog
},
inject: ['showFullscreenImageDialog'],
props: {
dialogData: {
type: Object,
required: true
},
gallerySelectDialog: {
type: Object,
required: true
}
selectedGalleryFile: { type: Object, default: () => ({}) }
},
data() {
return {
gallerySelectDialog: {
visible: false,
selectedFileId: '',
selectedImageUrl: ''
},
galleryTable: []
};
},
computed: {
groupPostEditDialog: {
@@ -143,6 +148,22 @@
}
},
methods: {
showGallerySelectDialog() {
const D = this.gallerySelectDialog;
D.visible = true;
this.refreshGalleryTable();
},
async refreshGalleryTable() {
const params = {
n: 100,
tag: 'gallery'
};
const args = await vrcPlusIconRequest.getFileList(params);
// API.$on('FILES:LIST')
if (args.params.tag === 'gallery') {
this.galleryTable = args.json.reverse();
}
},
editGroupPost() {
const D = this.groupPostEditDialog;
if (!D.groupId || !D.postId) {
@@ -193,7 +214,9 @@
D.visible = false;
},
clearImageGallerySelect() {
this.$emit('clear-image-gallery-select');
const D = this.gallerySelectDialog;
D.selectedFileId = '';
D.selectedImageUrl = '';
}
}
};

View File

@@ -0,0 +1,212 @@
<template>
<safe-dialog
class="x-dialog"
:visible="editAndSendInviteDialog.visible"
:title="t('dialog.edit_send_invite_message.header')"
width="400px"
append-to-body
@close="cancelEditAndSendInvite">
<div style="font-size: 12px">
<span>{{ t('dialog.edit_send_invite_message.description') }}</span>
</div>
<el-input
v-model="editAndSendInviteDialog.newMessage"
type="textarea"
size="mini"
maxlength="64"
show-word-limit
:autosize="{ minRows: 2, maxRows: 5 }"
placeholder=""
style="margin-top: 10px"></el-input>
<template #footer>
<el-button type="small" @click="cancelEditAndSendInvite">
{{ t('dialog.edit_send_invite_message.cancel') }}
</el-button>
<el-button type="primary" size="small" @click="saveEditAndSendInvite">
{{ t('dialog.edit_send_invite_message.send') }}
</el-button>
</template>
</safe-dialog>
</template>
<script setup>
import { getCurrentInstance, inject } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { instanceRequest, inviteMessagesRequest, notificationRequest } from '../../../api';
import { parseLocation } from '../../../composables/instance/utils';
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const API = inject('API');
const props = defineProps({
editAndSendInviteDialog: {
type: Object,
required: true
},
sendInviteDialog: {
type: Object,
required: true
},
inviteDialog: {
type: Object,
required: false,
default: () => ({})
},
uploadImage: {
type: String
}
});
const emit = defineEmits(['update:editAndSendInviteDialog', 'closeInviteDialog']);
function cancelEditAndSendInvite() {
emit('update:editAndSendInviteDialog', { ...props.editAndSendInviteDialog, visible: false });
}
async function saveEditAndSendInvite() {
const D = props.editAndSendInviteDialog;
D.visible = false;
const messageType = D.messageType;
const slot = D.inviteMessage.slot;
if (D.inviteMessage.message !== D.newMessage) {
const params = {
message: D.newMessage
};
await inviteMessagesRequest
.editInviteMessage(params, messageType, slot)
.catch((err) => {
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",
type: 'error'
});
throw new Error("VRChat API didn't update message, try again");
} else {
$message('Invite message updated');
}
return args;
});
}
const I = props.sendInviteDialog;
const J = props.inviteDialog;
if (J?.visible) {
const inviteLoop = () => {
if (J.userIds.length > 0) {
const receiverUserId = J.userIds.shift();
if (receiverUserId === API.currentUser.id) {
// can't invite self!?
const L = parseLocation(J.worldId);
instanceRequest
.selfInvite({
instanceId: L.instanceId,
worldId: L.worldId
})
.finally(inviteLoop);
} else if (props.uploadImage) {
notificationRequest
.sendInvitePhoto(
{
instanceId: J.worldId,
worldId: J.worldId,
worldName: J.worldName,
messageSlot: slot
},
receiverUserId
)
.finally(inviteLoop);
} else {
notificationRequest
.sendInvite(
{
instanceId: J.worldId,
worldId: J.worldId,
worldName: J.worldName,
messageSlot: slot
},
receiverUserId
)
.finally(inviteLoop);
}
} else {
J.loading = false;
J.visible = false;
$message({
message: 'Invite sent',
type: 'success'
});
}
};
inviteLoop();
} else if (I.messageType === 'invite') {
I.params.messageSlot = slot;
if (props.uploadImage) {
notificationRequest
.sendInvitePhoto(I.params, I.userId)
.catch((err) => {
throw err;
})
.then((args) => {
$message({
message: 'Invite photo message sent',
type: 'success'
});
return args;
});
} else {
notificationRequest
.sendInvite(I.params, I.userId)
.catch((err) => {
throw err;
})
.then((args) => {
$message({
message: 'Invite message sent',
type: 'success'
});
return args;
});
}
} else if (I.messageType === 'requestInvite') {
I.params.requestSlot = slot;
if (props.uploadImage) {
notificationRequest
.sendRequestInvitePhoto(I.params, I.userId)
.catch((err) => {
this.clearInviteImageUpload();
throw err;
})
.then((args) => {
$message({
message: 'Request invite photo message sent',
type: 'success'
});
return args;
});
} else {
notificationRequest
.sendRequestInvite(I.params, I.userId)
.catch((err) => {
throw err;
})
.then((args) => {
$message({
message: 'Request invite message sent',
type: 'success'
});
return args;
});
}
}
emit('closeInviteDialog');
}
</script>

View File

@@ -0,0 +1,309 @@
<template>
<safe-dialog
class="x-dialog"
:visible.sync="inviteDialog.visible"
:title="t('dialog.invite.header')"
width="500px"
append-to-body>
<div v-if="inviteDialog.visible" v-loading="inviteDialog.loading">
<location :location="inviteDialog.worldId" :link="false"></location>
<br />
<el-button size="mini" style="margin-top: 10px" @click="addSelfToInvite">{{
t('dialog.invite.add_self')
}}</el-button>
<el-button
size="mini"
:disabled="inviteDialog.friendsInInstance.length === 0"
style="margin-top: 10px"
@click="addFriendsInInstanceToInvite"
>{{ t('dialog.invite.add_friends_in_instance') }}</el-button
>
<el-button
size="mini"
:disabled="vipFriends.length === 0"
style="margin-top: 10px"
@click="addFavoriteFriendsToInvite"
>{{ t('dialog.invite.add_favorite_friends') }}</el-button
>
<el-select
v-model="inviteDialog.userIds"
multiple
clearable
:placeholder="t('dialog.invite.select_placeholder')"
filterable
:disabled="inviteDialog.loading"
style="width: 100%; margin-top: 15px">
<el-option-group v-if="API.currentUser" :label="t('side_panel.me')">
<el-option
class="x-friend-item"
:label="API.currentUser.displayName"
:value="API.currentUser.id"
style="height: auto">
<div :class="['avatar', userStatusClass(API.currentUser)]">
<img v-lazy="userImage(API.currentUser)" />
</div>
<div class="detail">
<span class="name">{{ API.currentUser.displayName }}</span>
</div>
</el-option>
</el-option-group>
<el-option-group
v-if="inviteDialog.friendsInInstance.length"
:label="t('dialog.invite.friends_in_instance')">
<el-option
v-for="friend in inviteDialog.friendsInInstance"
:key="friend.id"
class="x-friend-item"
:label="friend.name"
:value="friend.id"
style="height: auto">
<template v-if="friend.ref">
<div :class="['avatar', userStatusClass(friend.ref)]">
<img v-lazy="userImage(friend.ref)" />
</div>
<div class="detail">
<span class="name" :style="{ color: friend.ref.$userColour }">{{
friend.ref.displayName
}}</span>
</div>
</template>
<span v-else>{{ friend.id }}</span>
</el-option>
</el-option-group>
<el-option-group v-if="vipFriends.length" :label="t('side_panel.favorite')">
<el-option
v-for="friend in vipFriends"
:key="friend.id"
class="x-friend-item"
:label="friend.name"
:value="friend.id"
style="height: auto">
<template v-if="friend.ref">
<div :class="['avatar', userStatusClass(friend.ref)]">
<img v-lazy="userImage(friend.ref)" />
</div>
<div class="detail">
<span class="name" :style="{ color: friend.ref.$userColour }">{{
friend.ref.displayName
}}</span>
</div>
</template>
<span v-else>{{ friend.id }}</span>
</el-option>
</el-option-group>
<el-option-group v-if="onlineFriends.length" :label="t('side_panel.online')">
<el-option
v-for="friend in onlineFriends"
:key="friend.id"
class="x-friend-item"
:label="friend.name"
:value="friend.id"
style="height: auto">
<template v-if="friend.ref">
<div :class="['avatar', userStatusClass(friend.ref)]">
<img v-lazy="userImage(friend.ref)" />
</div>
<div class="detail">
<span class="name" :style="{ color: friend.ref.$userColour }">{{
friend.ref.displayName
}}</span>
</div>
</template>
<span v-else>{{ friend.id }}</span>
</el-option>
</el-option-group>
<el-option-group v-if="activeFriends.length" :label="t('side_panel.active')">
<el-option
v-for="friend in activeFriends"
:key="friend.id"
class="x-friend-item"
:label="friend.name"
:value="friend.id"
style="height: auto">
<template v-if="friend.ref">
<div class="avatar"><img v-lazy="userImage(friend.ref)" /></div>
<div class="detail">
<span class="name" :style="{ color: friend.ref.$userColour }">{{
friend.ref.displayName
}}</span>
</div>
</template>
<span v-else>{{ friend.id }}</span>
</el-option>
</el-option-group>
</el-select>
</div>
<template #footer>
<el-button
size="small"
:disabled="inviteDialog.loading || !inviteDialog.userIds.length"
@click="showSendInviteDialog"
>{{ t('dialog.invite.invite_with_message') }}</el-button
>
<el-button
type="primary"
size="small"
:disabled="inviteDialog.loading || !inviteDialog.userIds.length"
@click="sendInvite"
>{{ t('dialog.invite.invite') }}</el-button
>
</template>
<SendInviteDialog
:send-invite-dialog-visible.sync="sendInviteDialogVisible"
:invite-message-table="inviteMessageTable"
:send-invite-dialog="sendInviteDialog"
:invite-dialog="inviteDialog"
:upload-image="uploadImage"
@close-invite-dialog="closeInviteDialog" />
</safe-dialog>
</template>
<script setup>
import { getCurrentInstance, inject, ref } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { instanceRequest, inviteMessagesRequest, notificationRequest } from '../../../api';
import { parseLocation } from '../../../composables/instance/utils';
import Location from '../../Location.vue';
import SendInviteDialog from './SendInviteDialog.vue';
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const $confirm = instance.proxy.$confirm;
const userStatusClass = inject('userStatusClass');
const userImage = inject('userImage');
const API = inject('API');
const props = defineProps({
inviteDialog: {
type: Object,
required: true
},
vipFriends: {
type: Array,
required: true
},
onlineFriends: {
type: Array,
required: true
},
activeFriends: {
type: Array,
required: true
},
// SendInviteDialog
inviteMessageTable: {
type: Object,
default: () => ({})
},
uploadImage: {
type: String,
default: ''
}
});
const emit = defineEmits(['clearInviteImageUpload', 'inviteImageUpload', 'closeInviteDialog']);
const sendInviteDialogVisible = ref(false);
const sendInviteDialog = ref({ message: '', messageSlot: 0, userId: '', messageType: '', params: {} });
function closeInviteDialog() {
emit('closeInviteDialog');
}
function showSendInviteDialog(params, userId) {
sendInviteDialog.value = {
params,
userId,
messageType: 'invite'
};
inviteMessagesRequest.refreshInviteMessageTableData('message');
clearInviteImageUpload();
sendInviteDialogVisible.value = true;
}
function clearInviteImageUpload() {
emit('clearInviteImageUpload');
}
function addSelfToInvite() {
const D = props.inviteDialog;
if (!D.userIds.includes(API.currentUser.id)) {
D.userIds.push(API.currentUser.id);
}
}
function addFriendsInInstanceToInvite() {
const D = props.inviteDialog;
for (const friend of D.friendsInInstance) {
if (!D.userIds.includes(friend.id)) {
D.userIds.push(friend.id);
}
}
}
function addFavoriteFriendsToInvite() {
const D = props.inviteDialog;
for (const friend of props.vipFriends) {
if (!D.userIds.includes(friend.id)) {
D.userIds.push(friend.id);
}
}
}
function sendInvite() {
$confirm('Continue? Invite', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
const D = props.inviteDialog;
if (action !== 'confirm' || D.loading === true) {
return;
}
D.loading = true;
const inviteLoop = () => {
if (D.userIds.length > 0) {
const receiverUserId = D.userIds.shift();
if (receiverUserId === API.currentUser.id) {
// can't invite self!?
const L = parseLocation(D.worldId);
instanceRequest
.selfInvite({
instanceId: L.instanceId,
worldId: L.worldId
})
.finally(inviteLoop);
} else {
notificationRequest
.sendInvite(
{
instanceId: D.worldId,
worldId: D.worldId,
worldName: D.worldName
},
receiverUserId
)
.finally(inviteLoop);
}
} else {
D.loading = false;
D.visible = false;
$message({
message: 'Invite sent',
type: 'success'
});
}
};
inviteLoop();
}
});
}
</script>

View File

@@ -0,0 +1,176 @@
<template>
<safe-dialog
class="x-dialog"
:visible="visible"
:title="t('dialog.invite_message.header')"
width="400px"
append-to-body
@close="cancelInviteConfirm">
<div style="font-size: 12px">
<span>{{ t('dialog.invite_message.confirmation') }}</span>
</div>
<template #footer>
<el-button type="small" @click="cancelInviteConfirm">
{{ t('dialog.invite_message.cancel') }}
</el-button>
<el-button type="primary" size="small" @click="sendInviteConfirm">
{{ t('dialog.invite_message.confirm') }}
</el-button>
</template>
</safe-dialog>
</template>
<script setup>
import { getCurrentInstance, inject } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { instanceRequest, notificationRequest } from '../../../api';
import { parseLocation } from '../../../composables/instance/utils';
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const API = inject('API');
const props = defineProps({
visible: {
type: Boolean,
required: true
},
sendInviteDialog: {
type: Object,
required: true
},
inviteDialog: {
type: Object,
required: false,
default: () => ({})
},
uploadImage: {
type: String
}
});
const emit = defineEmits(['update:visible', 'closeInviteDialog']);
function cancelInviteConfirm() {
emit('update:visible', false);
}
function sendInviteConfirm() {
const D = props.sendInviteDialog;
const J = props.inviteDialog;
if (J?.visible) {
const inviteLoop = () => {
if (J.userIds.length > 0) {
const receiverUserId = J.userIds.shift();
if (receiverUserId === API.currentUser.id) {
// can't invite self!?
const L = parseLocation(J.worldId);
instanceRequest
.selfInvite({
instanceId: L.instanceId,
worldId: L.worldId
})
.finally(inviteLoop);
} else if (props.uploadImage) {
notificationRequest
.sendInvitePhoto(
{
instanceId: J.worldId,
worldId: J.worldId,
worldName: J.worldName,
messageSlot: D.messageSlot
},
receiverUserId
)
.finally(inviteLoop);
} else {
notificationRequest
.sendInvite(
{
instanceId: J.worldId,
worldId: J.worldId,
worldName: J.worldName,
messageSlot: D.messageSlot
},
receiverUserId
)
.finally(inviteLoop);
}
} else {
J.loading = false;
J.visible = false;
$message({
message: 'Invite message sent',
type: 'success'
});
}
};
inviteLoop();
} else if (D.messageType === 'invite') {
D.params.messageSlot = D.messageSlot;
if (props.uploadImage) {
notificationRequest
.sendInvitePhoto(D.params, D.userId)
.catch((err) => {
throw err;
})
.then((args) => {
$message({
message: 'Invite photo message sent',
type: 'success'
});
return args;
});
} else {
notificationRequest
.sendInvite(D.params, D.userId)
.catch((err) => {
throw err;
})
.then((args) => {
$message({
message: 'Invite message sent',
type: 'success'
});
return args;
});
}
} else if (D.messageType === 'requestInvite') {
D.params.requestSlot = D.messageSlot;
if (props.uploadImage) {
notificationRequest
.sendRequestInvitePhoto(D.params, D.userId)
.catch((err) => {
this.clearInviteImageUpload();
throw err;
})
.then((args) => {
$message({
message: 'Request invite photo message sent',
type: 'success'
});
return args;
});
} else {
notificationRequest
.sendRequestInvite(D.params, D.userId)
.catch((err) => {
throw err;
})
.then((args) => {
$message({
message: 'Request invite message sent',
type: 'success'
});
return args;
});
}
}
cancelInviteConfirm();
emit('closeInviteDialog');
}
</script>

View File

@@ -0,0 +1,165 @@
<template>
<safe-dialog
class="x-dialog"
:visible.sync="sendInviteDialogVisible"
:title="t('dialog.invite_message.header')"
width="800px"
append-to-body
@close="cancelSendInvite">
<template v-if="API.currentUser.$isVRCPlus">
<!-- <template v-if="gallerySelectDialog.selectedFileId">-->
<!-- <div style="display: inline-block; flex: none; margin-right: 5px">-->
<!-- <el-popover placement="right" width="500px" trigger="click">-->
<!-- <template #reference>-->
<!-- <img-->
<!-- class="x-link"-->
<!-- v-lazy="gallerySelectDialog.selectedImageUrl"-->
<!-- style="flex: none; width: 60px; height: 60px; border-radius: 4px; object-fit: cover" />-->
<!-- </template>-->
<!-- <img-->
<!-- class="x-link"-->
<!-- v-lazy="gallerySelectDialog.selectedImageUrl"-->
<!-- style="height: 500px"-->
<!-- @click="showFullscreenImageDialog(gallerySelectDialog.selectedImageUrl)" />-->
<!-- </el-popover>-->
<!-- </div>-->
<!-- <el-button size="mini" @click="clearImageGallerySelect" style="vertical-align: top">-->
<!-- {{ t('dialog.invite_message.clear_selected_image') }}-->
<!-- </el-button>-->
<!-- </template>-->
<!-- <template v-else>-->
<!-- <el-button size="mini" @click="showGallerySelectDialog" style="margin-right: 5px">-->
<!-- {{ t('dialog.invite_message.select_image') }}-->
<!-- </el-button>-->
<!-- </template>-->
<input class="inviteImageUploadButton" type="file" accept="image/*" @change="inviteImageUpload" />
</template>
<data-tables
v-bind="inviteMessageTable"
style="margin-top: 10px; cursor: pointer"
@row-click="showSendInviteConfirmDialog">
<el-table-column
:label="t('table.profile.invite_messages.slot')"
prop="slot"
sortable="custom"
width="70"></el-table-column>
<el-table-column :label="t('table.profile.invite_messages.message')" prop="message"></el-table-column>
<el-table-column
:label="t('table.profile.invite_messages.cool_down')"
prop="updatedAt"
sortable="custom"
width="110"
align="right">
<template #default="scope">
<countdown-timer :datetime="scope.row.updatedAt" :hours="1"></countdown-timer>
</template>
</el-table-column>
<el-table-column :label="t('table.profile.invite_messages.action')" width="70" align="right">
<template #default="scope">
<el-button
type="text"
icon="el-icon-edit"
size="mini"
@click.stop="showEditAndSendInviteDialog('message', scope.row)"></el-button>
</template>
</el-table-column>
</data-tables>
<template #footer>
<el-button type="small" @click="cancelSendInvite">
{{ t('dialog.invite_message.cancel') }}
</el-button>
<el-button type="small" @click="API.refreshInviteMessageTableData('message')">
{{ t('dialog.invite_message.refresh') }}
</el-button>
</template>
<SendInviteConfirmDialog
:visible.sync="isSendInviteConfirmDialogVisible"
:send-invite-dialog="sendInviteDialog"
:invite-dialog="inviteDialog"
:upload-image="uploadImage"
@closeInviteDialog="closeInviteDialog" />
<EditAndSendInviteDialog
:edit-and-send-invite-dialog.sync="editAndSendInviteDialog"
:send-invite-dialog="sendInviteDialog"
:invite-dialog="inviteDialog"
:upload-image="uploadImage"
@closeInviteDialog="closeInviteDialog" />
</safe-dialog>
</template>
<script setup>
import { inject, ref } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import EditAndSendInviteDialog from './EditAndSendInviteDialog.vue';
import SendInviteConfirmDialog from './SendInviteConfirmDialog.vue';
const { t } = useI18n();
const API = inject('API');
const props = defineProps({
sendInviteDialogVisible: {
type: Boolean,
default: false
},
inviteMessageTable: {
type: Object,
default: () => ({})
},
sendInviteDialog: {
type: Object,
required: true
},
inviteDialog: {
type: Object,
required: false,
default: () => ({})
},
uploadImage: {
type: String,
default: ''
}
});
const emit = defineEmits(['inviteImageUpload', 'update:sendInviteDialogVisible', 'closeInviteDialog']);
const isSendInviteConfirmDialogVisible = ref(false);
const editAndSendInviteDialog = ref({
visible: false,
messageType: '',
newMessage: '',
inviteMessage: {}
});
function inviteImageUpload(event) {
emit('inviteImageUpload', event);
}
function showSendInviteConfirmDialog(val) {
isSendInviteConfirmDialogVisible.value = true;
//
props.sendInviteDialog.messageSlot = val.slot;
}
function showEditAndSendInviteDialog(messageType, inviteMessage) {
// todo
editAndSendInviteDialog.value = {
newMessage: inviteMessage.message,
visible: true,
messageType,
inviteMessage
};
}
function cancelSendInvite() {
emit('update:sendInviteDialogVisible', false);
}
function closeInviteDialog() {
cancelSendInvite();
emit('closeInviteDialog');
}
</script>

View File

@@ -1,12 +1,10 @@
<template>
<el-dialog
<safe-dialog
ref="inviteGroupDialog"
:visible.sync="inviteGroupDialog.visible"
:before-close="beforeDialogClose"
:title="$t('dialog.invite_to_group.header')"
width="450px"
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
append-to-body>
<div v-if="inviteGroupDialog.visible" v-loading="inviteGroupDialog.loading">
<span>{{ $t('dialog.invite_to_group.description') }}</span>
<br />
@@ -165,24 +163,16 @@
Invite
</el-button>
</template>
</el-dialog>
</safe-dialog>
</template>
<script>
import { groupRequest, userRequest } from '../../../api';
import utils from '../../../classes/utils';
import { groupRequest, userRequest } from '../../api';
import { hasGroupPermission } from '../../composables/group/utils';
export default {
name: 'InviteGroupDialog',
inject: [
'API',
'dialogMouseDown',
'dialogMouseUp',
'beforeDialogClose',
'userStatusClass',
'userImage',
'adjustDialogZ'
],
inject: ['API', 'userStatusClass', 'userImage', 'adjustDialogZ'],
props: {
dialogData: {
type: Object,
@@ -256,7 +246,7 @@
groupRequest
.getGroup({ groupId })
.then((args) => {
if (utils.hasGroupPermission(args.ref, 'group-invites-manage')) {
if (hasGroupPermission(args.ref, 'group-invites-manage')) {
return args;
}
// not allowed to invite

View File

@@ -0,0 +1,282 @@
<template>
<safe-dialog ref="launchDialog" :visible.sync="isVisible" :title="$t('dialog.launch.header')" width="450px">
<el-form :model="launchDialog" label-width="100px">
<el-form-item :label="$t('dialog.launch.url')">
<el-input
v-model="launchDialog.url"
size="mini"
style="width: 260px"
@click.native="$event.target.tagName === 'INPUT' && $event.target.select()" />
<el-tooltip placement="right" :content="$t('dialog.launch.copy_tooltip')" :disabled="hideTooltips">
<el-button
size="mini"
icon="el-icon-s-order"
style="margin-left: 5px"
circle
@click="copyInstanceMessage(launchDialog.url)" />
</el-tooltip>
</el-form-item>
<el-form-item v-if="launchDialog.shortUrl">
<template slot="label">
<span>{{ $t('dialog.launch.short_url') }}</span>
<el-tooltip
placement="top"
style="margin-left: 5px"
:content="$t('dialog.launch.short_url_notice')">
<i class="el-icon-warning" />
</el-tooltip>
</template>
<el-input
v-model="launchDialog.shortUrl"
size="mini"
style="width: 260px"
@click.native="$event.target.tagName === 'INPUT' && $event.target.select()" />
<el-tooltip placement="right" :content="$t('dialog.launch.copy_tooltip')" :disabled="hideTooltips">
<el-button
size="mini"
icon="el-icon-s-order"
style="margin-left: 5px"
circle
@click="copyInstanceMessage(launchDialog.shortUrl)" />
</el-tooltip>
</el-form-item>
<el-form-item :label="$t('dialog.launch.location')">
<el-input
v-model="launchDialog.location"
size="mini"
style="width: 260px"
@click.native="$event.target.tagName === 'INPUT' && $event.target.select()" />
<el-tooltip placement="right" :content="$t('dialog.launch.copy_tooltip')" :disabled="hideTooltips">
<el-button
size="mini"
icon="el-icon-s-order"
style="margin-left: 5px"
circle
@click="copyInstanceMessage(launchDialog.location)" />
</el-tooltip>
</el-form-item>
</el-form>
<el-checkbox v-model="launchDialog.desktop" style="float: left; margin-top: 5px" @change="saveLaunchDialog">
{{ $t('dialog.launch.start_as_desktop') }}
</el-checkbox>
<template slot="footer">
<el-button size="small" @click="showPreviousInstancesInfoDialog(launchDialog.location)">
{{ $t('dialog.launch.info') }}
</el-button>
<el-button
size="small"
:disabled="!checkCanInvite(launchDialog.location)"
@click="showInviteDialog(launchDialog.location)">
{{ $t('dialog.launch.invite') }}
</el-button>
<el-button
type="primary"
size="small"
:disabled="!launchDialog.secureOrShortName"
@click="launchGame(launchDialog.location, launchDialog.shortName, launchDialog.desktop)">
{{ $t('dialog.launch.launch') }}
</el-button>
</template>
<InviteDialog
:invite-dialog="inviteDialog"
:vip-friends="vipFriends"
:online-friends="onlineFriends"
:active-friends="activeFriends"
:invite-message-table="inviteMessageTable"
:upload-image="uploadImage"
@close-invite-dialog="closeInviteDialog" />
</safe-dialog>
</template>
<script>
import { instanceRequest, worldRequest } from '../../api';
import { isRealInstance, parseLocation } from '../../composables/instance/utils';
import { getLaunchURL } from '../../composables/shared/utils';
import configRepository from '../../service/config';
import InviteDialog from './InviteDialog/InviteDialog.vue';
export default {
name: 'LaunchDialog',
components: { InviteDialog },
inject: ['showPreviousInstancesInfoDialog', 'adjustDialogZ'],
props: {
hideTooltips: Boolean,
launchDialogData: { type: Object, required: true },
checkCanInvite: {
type: Function,
required: true
},
vipFriends: {
type: Array,
default: () => []
},
onlineFriends: {
type: Array,
default: () => []
},
activeFriends: {
type: Array,
default: () => []
},
inviteMessageTable: {
type: Object,
default: () => ({})
},
uploadImage: {
type: String,
default: ''
},
lastLocation: {
type: Object,
default: () => ({})
}
},
data() {
return {
launchDialog: {
loading: false,
desktop: false,
tag: '',
location: '',
url: '',
shortName: '',
shortUrl: '',
secureOrShortName: ''
},
inviteDialog: {
visible: false,
loading: false,
worldId: '',
worldName: '',
userIds: [],
friendsInInstance: []
}
};
},
computed: {
isVisible: {
get() {
return this.launchDialogData.visible;
},
set(value) {
this.$emit('update:launch-dialog-data', { ...this.launchDialogData, visible: value });
}
}
},
watch: {
'launchDialogData.loading': {
handler() {
this.getConfig();
this.initLaunchDialog();
}
}
},
created() {
this.getConfig();
},
methods: {
closeInviteDialog() {
this.inviteDialog.visible = false;
},
showInviteDialog(tag) {
if (!isRealInstance(tag)) {
return;
}
const L = parseLocation(tag);
worldRequest
.getCachedWorld({
worldId: L.worldId
})
.then((args) => {
const D = this.inviteDialog;
D.userIds = [];
D.worldId = L.tag;
D.worldName = args.ref.name;
D.friendsInInstance = [];
const friendsInCurrentInstance = this.lastLocation.friendList;
for (const friend of friendsInCurrentInstance.values()) {
const ctx = this.friends.get(friend.userId);
if (typeof ctx.ref === 'undefined') {
continue;
}
D.friendsInInstance.push(ctx);
}
D.visible = true;
});
},
launchGame(location, shortName, desktop) {
this.$emit('launchGame', location, shortName, desktop);
this.isVisible = false;
},
getConfig() {
configRepository.getBool('launchAsDesktop').then((value) => (this.launchDialog.desktop = value));
},
saveLaunchDialog() {
configRepository.setBool('launchAsDesktop', this.launchDialog.desktop);
},
async initLaunchDialog() {
const { tag, shortName } = this.launchDialogData;
if (!isRealInstance(tag)) {
return;
}
this.$nextTick(() => this.adjustDialogZ(this.$refs.launchDialog.$el));
const D = this.launchDialog;
D.tag = tag;
D.secureOrShortName = shortName;
D.shortUrl = '';
D.shortName = shortName;
const L = parseLocation(tag);
L.shortName = shortName;
if (shortName) {
D.shortUrl = `https://vrch.at/${shortName}`;
}
if (L.instanceId) {
D.location = `${L.worldId}:${L.instanceId}`;
} else {
D.location = L.worldId;
}
D.url = getLaunchURL(L);
if (!shortName) {
const res = await instanceRequest.getInstanceShortName({
worldId: L.worldId,
instanceId: L.instanceId
});
// NOTE:
// splitting the 'INSTANCE:SHORTNAME' event and put code here
if (!res.json) {
return;
}
const resLocation = `${res.instance.worldId}:${res.instance.instanceId}`;
if (resLocation === this.launchDialog.tag) {
const resShortName = res.json.shortName;
const secureOrShortName = res.json.shortName || res.json.secureName;
const parsedL = parseLocation(resLocation);
parsedL.shortName = resShortName;
this.launchDialog.shortName = resShortName;
this.launchDialog.secureOrShortName = secureOrShortName;
if (resShortName) {
this.launchDialog.shortUrl = `https://vrch.at/${resShortName}`;
}
this.launchDialog.url = getLaunchURL(parsedL);
}
}
},
async copyInstanceMessage(input) {
try {
await navigator.clipboard.writeText(input);
this.$message({
message: 'Instance copied to clipboard',
type: 'success'
});
} catch (error) {
this.$message({
message: 'Instance copied failed',
type: 'error'
});
console.error(error.message);
}
}
}
};
</script>

View File

@@ -1,13 +1,10 @@
<template>
<el-dialog
<safe-dialog
ref="newInstanceDialog"
:before-close="beforeDialogClose"
:visible.sync="newInstanceDialog.visible"
:title="$t('dialog.new_instance.header')"
width="650px"
append-to-body
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
append-to-body>
<el-tabs v-model="newInstanceDialog.selectedTab" type="card" @tab-click="newInstanceTabClick">
<el-tab-pane :label="$t('dialog.new_instance.normal')">
<el-form :model="newInstanceDialog" label-width="150px">
@@ -486,27 +483,30 @@
>{{ $t('dialog.new_instance.launch') }}</el-button
>
</template>
</el-dialog>
<InviteDialog
:invite-dialog="inviteDialog"
:vip-friends="vipFriends"
:online-friends="onlineFriends"
:active-friends="activeFriends"
:invite-message-table="inviteMessageTable"
:upload-image="uploadImage"
@close-invite-dialog="closeInviteDialog" />
</safe-dialog>
</template>
<script>
import { groupRequest, instanceRequest } from '../../api';
import { groupRequest, instanceRequest, worldRequest } from '../../api';
import utils from '../../classes/utils';
import { hasGroupPermission as _hasGroupPermission } from '../../composables/group/utils';
import { isRealInstance, parseLocation } from '../../composables/instance/utils';
import { getLaunchURL } from '../../composables/shared/utils';
import configRepository from '../../service/config';
import InviteDialog from './InviteDialog/InviteDialog.vue';
export default {
name: 'NewInstanceDialog',
inject: [
'API',
'userImage',
'userStatusClass',
'beforeDialogClose',
'dialogMouseDown',
'dialogMouseUp',
'showInviteDialog',
'showLaunchDialog',
'adjustDialogZ'
],
components: { InviteDialog },
inject: ['API', 'userImage', 'userStatusClass', 'showLaunchDialog', 'adjustDialogZ'],
props: {
vipFriends: {
type: Array,
@@ -535,6 +535,18 @@
newInstanceDialogLocationTag: {
type: String,
required: true
},
inviteMessageTable: {
type: Object,
default: () => ({})
},
uploadImage: {
type: String,
default: ''
},
lastLocation: {
type: Object,
default: () => ({})
}
},
data() {
@@ -566,6 +578,14 @@
groupRef: {},
contentSettings: this.instanceContentSettings,
selectedContentSettings: []
},
inviteDialog: {
visible: false,
loading: false,
worldId: '',
worldName: '',
userIds: [],
friendsInInstance: []
}
};
},
@@ -578,13 +598,42 @@
this.initializeNewInstanceDialog();
},
methods: {
closeInviteDialog() {
this.inviteDialog.visible = false;
},
showInviteDialog(tag) {
if (!isRealInstance(tag)) {
return;
}
const L = parseLocation(tag);
worldRequest
.getCachedWorld({
worldId: L.worldId
})
.then((args) => {
const D = this.inviteDialog;
D.userIds = [];
D.worldId = L.tag;
D.worldName = args.ref.name;
D.friendsInInstance = [];
const friendsInCurrentInstance = this.lastLocation.friendList;
for (const friend of friendsInCurrentInstance.values()) {
const ctx = this.friends.get(friend.userId);
if (typeof ctx.ref === 'undefined') {
continue;
}
D.friendsInInstance.push(ctx);
}
D.visible = true;
});
},
initNewInstanceDialog(tag) {
if (!utils.isRealInstance(tag)) {
if (!isRealInstance(tag)) {
return;
}
this.$nextTick(() => this.adjustDialogZ(this.$refs.newInstanceDialog.$el));
const D = this.newInstanceDialog;
const L = utils.parseLocation(tag);
const L = parseLocation(tag);
if (D.worldId === L.worldId) {
// reopening dialog, keep last open instance
D.visible = true;
@@ -682,16 +731,16 @@
} else {
D.location = D.worldId;
}
const L = utils.parseLocation(D.location);
const L = parseLocation(D.location);
if (noChanges) {
L.shortName = D.shortName;
} else {
D.shortName = '';
}
D.url = utils.getLaunchURL(L);
D.url = getLaunchURL(L);
},
selfInvite(location) {
const L = utils.parseLocation(location);
const L = parseLocation(location);
if (!L.isRealInstance) {
return;
}
@@ -831,7 +880,7 @@
this.saveNewInstanceDialog();
},
async copyInstanceUrl(location) {
const L = utils.parseLocation(location);
const L = parseLocation(location);
const args = await instanceRequest.getInstanceShortName({
worldId: L.worldId,
instanceId: L.instanceId
@@ -870,7 +919,7 @@
}
},
hasGroupPermission(ref, permission) {
return utils.hasGroupPermission(ref, permission);
return _hasGroupPermission(ref, permission);
}
}
};

View File

@@ -0,0 +1,51 @@
<template>
<safe-dialog
class="x-dialog"
:visible="previousImagesDialogVisible"
:title="t('dialog.previous_images.header')"
width="800px"
append-to-body
@close="closeDialog">
<div>
<div
v-for="image in previousImagesTable"
v-if="image.file"
:key="image.version"
style="display: inline-block">
<el-popover class="x-change-image-item" placement="right" width="500px" trigger="click">
<img slot="reference" v-lazy="image.file.url" class="x-link" />
<img
v-lazy="image.file.url"
class="x-link"
style="width: 500px; height: 375px"
@click="showFullscreenImageDialog(image.file.url)" />
</el-popover>
</div>
</div>
</safe-dialog>
</template>
<script setup>
import { inject } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
const { t } = useI18n();
const showFullscreenImageDialog = inject('showFullscreenImageDialog');
defineProps({
previousImagesDialogVisible: {
type: Boolean,
required: true
},
previousImagesTable: {
type: Array,
required: true
}
});
const emit = defineEmits(['update:previousImagesDialogVisible']);
function closeDialog() {
emit('update:previousImagesDialogVisible', false);
}
</script>

View File

@@ -1,14 +1,11 @@
<template>
<el-dialog
<safe-dialog
ref="dialog"
:before-close="beforeDialogClose"
:visible="visible"
:title="$t('dialog.previous_instances.info')"
width="800px"
:fullscreen="fullscreen"
destroy-on-close
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp"
@close="$emit('update:visible', false)">
<div style="display: flex; align-items: center; justify-content: space-between">
<location :location="location.tag" style="font-size: 14px"></location>
@@ -57,13 +54,14 @@
</template>
</el-table-column>
</data-tables>
</el-dialog>
</safe-dialog>
</template>
<script>
import utils from '../../../classes/utils';
import database from '../../../service/database';
import dayjs from 'dayjs';
import utils from '../../../classes/utils';
import { parseLocation } from '../../../composables/instance/utils';
import database from '../../../service/database';
import Location from '../../Location.vue';
export default {
@@ -71,7 +69,7 @@
components: {
Location
},
inject: ['adjustDialogZ', 'beforeDialogClose', 'dialogMouseDown', 'dialogMouseUp'],
inject: ['adjustDialogZ'],
props: {
visible: {
type: Boolean,
@@ -144,7 +142,7 @@
init() {
this.adjustDialogZ(this.$refs.dialog.$el);
this.loading = true;
this.location = utils.parseLocation(this.instanceId);
this.location = parseLocation(this.instanceId);
},
refreshPreviousInstancesInfoTable() {
database.getPlayersFromInstance(this.location.tag).then((data) => {

View File

@@ -1,13 +1,10 @@
<template>
<el-dialog
<safe-dialog
ref="previousInstancesWorldDialog"
:before-close="beforeDialogClose"
:visible.sync="isVisible"
:title="$t('dialog.previous_instances.header')"
width="1000px"
append-to-body
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
append-to-body>
<div style="display: flex; align-items: center; justify-content: space-between">
<span style="font-size: 14px" v-text="previousInstancesWorldDialog.worldRef.name"></span>
<el-input
@@ -66,24 +63,17 @@
</template>
</el-table-column>
</data-tables>
</el-dialog>
</safe-dialog>
</template>
<script>
import utils from '../../../classes/utils';
import { parseLocation } from '../../../composables/instance/utils';
import database from '../../../service/database';
export default {
name: 'PreviousInstancesWorldDialog',
inject: [
'API',
'showLaunchDialog',
'showPreviousInstancesInfoDialog',
'adjustDialogZ',
'beforeDialogClose',
'dialogMouseDown',
'dialogMouseUp'
],
inject: ['API', 'showLaunchDialog', 'showPreviousInstancesInfoDialog', 'adjustDialogZ'],
props: {
previousInstancesWorldDialog: {
type: Object,
@@ -149,7 +139,7 @@
database.getpreviousInstancesByWorldId(D.worldRef).then((data) => {
const array = [];
for (const ref of data.values()) {
ref.$location = utils.parseLocation(ref.location);
ref.$location = parseLocation(ref.location);
if (ref.time > 0) {
ref.timer = utils.timeToText(ref.time);
} else {

View File

@@ -0,0 +1,96 @@
<template>
<el-dialog
ref="elDialogRef"
:visible="props.visible"
v-bind="attrs"
:close-on-click-modal="false"
@open="handleOpen"
@close="handleClose">
<slot></slot>
<template v-if="slots.title" #title>
<slot name="title"></slot>
</template>
<template v-if="slots.footer" #footer>
<slot name="footer"></slot>
</template>
</el-dialog>
</template>
<script setup>
import { ref, onBeforeUnmount, nextTick, useAttrs, useSlots } from 'vue';
const props = defineProps({
visible: {
type: Boolean,
required: true
}
});
const emit = defineEmits(['update:visible', 'open', 'close']);
const attrs = useAttrs();
const slots = useSlots();
const elDialogRef = ref(null);
const wrapperElement = ref(null);
const mouseDownOnWrapper = ref(false);
const handleOpen = () => {
emit('open');
nextTick(() => {
addWrapperListeners();
});
};
const handleClose = () => {
emit('close');
removeWrapperListeners();
emit('update:visible', false);
};
const handleWrapperMouseDown = (event) => {
if (event.target === wrapperElement.value) {
mouseDownOnWrapper.value = true;
}
};
const handleWrapperMouseUp = (event) => {
if (event.target === wrapperElement.value && mouseDownOnWrapper.value) {
handleClose();
}
mouseDownOnWrapper.value = false;
};
const addWrapperListeners = () => {
const wrapper = elDialogRef.value?.$el;
if (
wrapper &&
wrapper.nodeType === Node.ELEMENT_NODE &&
wrapper.classList &&
wrapper.classList.contains('el-dialog__wrapper')
) {
wrapperElement.value = wrapper;
wrapperElement.value.addEventListener('mousedown', handleWrapperMouseDown);
wrapperElement.value.addEventListener('mouseup', handleWrapperMouseUp);
} else {
wrapperElement.value = null;
}
};
const removeWrapperListeners = () => {
if (wrapperElement.value) {
wrapperElement.value.removeEventListener('mousedown', handleWrapperMouseDown);
wrapperElement.value.removeEventListener('mouseup', handleWrapperMouseUp);
wrapperElement.value = null;
}
mouseDownOnWrapper.value = false;
};
onBeforeUnmount(() => {
removeWrapperListeners();
});
</script>

View File

@@ -0,0 +1,277 @@
<!--<template>-->
<!-- <safe-dialog-->
<!-- class="x-dialog"-->
<!-- :visible="sendBoopDialog.visible"-->
<!-- :title="t('dialog.boop_dialog.header')"-->
<!-- width="450px"-->
<!-- @close="closeDialog">-->
<!-- <el-select-->
<!-- v-model="sendBoopDialog.userId"-->
<!-- :placeholder="t('dialog.new_instance.instance_creator_placeholder')"-->
<!-- filterable-->
<!-- style="width: 100%">-->
<!-- <el-option-group v-if="vipFriends.length" :label="t('side_panel.favorite')">-->
<!-- <el-option-->
<!-- v-for="friend in vipFriends"-->
<!-- :key="friend.id"-->
<!-- class="x-friend-item"-->
<!-- :label="friend.name"-->
<!-- :value="friend.id"-->
<!-- style="height: auto">-->
<!-- <template v-if="friend.ref">-->
<!-- <div class="avatar" :class="userStatusClass(friend.ref)">-->
<!-- <img v-lazy="userImage(friend.ref)" />-->
<!-- </div>-->
<!-- <div class="detail">-->
<!-- <span-->
<!-- class="name"-->
<!-- :style="{ color: friend.ref.$userColour }"-->
<!-- v-text="friend.ref.displayName"></span>-->
<!-- </div>-->
<!-- </template>-->
<!-- <span v-else v-text="friend.id"></span>-->
<!-- </el-option>-->
<!-- </el-option-group>-->
<!-- <el-option-group v-if="onlineFriends.length" :label="t('side_panel.online')">-->
<!-- <el-option-->
<!-- v-for="friend in onlineFriends"-->
<!-- :key="friend.id"-->
<!-- class="x-friend-item"-->
<!-- :label="friend.name"-->
<!-- :value="friend.id"-->
<!-- style="height: auto">-->
<!-- <template v-if="friend.ref">-->
<!-- <div class="avatar" :class="userStatusClass(friend.ref)">-->
<!-- <img v-lazy="userImage(friend.ref)" />-->
<!-- </div>-->
<!-- <div class="detail">-->
<!-- <span-->
<!-- class="name"-->
<!-- :style="{ color: friend.ref.$userColour }"-->
<!-- v-text="friend.ref.displayName"></span>-->
<!-- </div>-->
<!-- </template>-->
<!-- <span v-else v-text="friend.id"></span>-->
<!-- </el-option>-->
<!-- </el-option-group>-->
<!-- <el-option-group v-if="activeFriends.length" :label="t('side_panel.active')">-->
<!-- <el-option-->
<!-- v-for="friend in activeFriends"-->
<!-- :key="friend.id"-->
<!-- class="x-friend-item"-->
<!-- :label="friend.name"-->
<!-- :value="friend.id"-->
<!-- style="height: auto">-->
<!-- <template v-if="friend.ref">-->
<!-- <div class="avatar">-->
<!-- <img v-lazy="userImage(friend.ref)" />-->
<!-- </div>-->
<!-- <div class="detail">-->
<!-- <span-->
<!-- class="name"-->
<!-- :style="{ color: friend.ref.$userColour }"-->
<!-- v-text="friend.ref.displayName"></span>-->
<!-- </div>-->
<!-- </template>-->
<!-- <span v-else v-text="friend.id"></span>-->
<!-- </el-option>-->
<!-- </el-option-group>-->
<!-- <el-option-group v-if="offlineFriends.length" :label="t('side_panel.offline')">-->
<!-- <el-option-->
<!-- v-for="friend in offlineFriends"-->
<!-- :key="friend.id"-->
<!-- class="x-friend-item"-->
<!-- :label="friend.name"-->
<!-- :value="friend.id"-->
<!-- style="height: auto">-->
<!-- <template v-if="friend.ref">-->
<!-- <div class="avatar">-->
<!-- <img v-lazy="userImage(friend.ref)" />-->
<!-- </div>-->
<!-- <div class="detail">-->
<!-- <span-->
<!-- class="name"-->
<!-- :style="{ color: friend.ref.$userColour }"-->
<!-- v-text="friend.ref.displayName"></span>-->
<!-- </div>-->
<!-- </template>-->
<!-- <span v-else v-text="friend.id"></span>-->
<!-- </el-option>-->
<!-- </el-option-group>-->
<!-- </el-select>-->
<!-- <br />-->
<!-- <br />-->
<!-- <el-select-->
<!-- v-model="fileId"-->
<!-- clearable-->
<!-- :placeholder="t('dialog.boop_dialog.select_emoji')"-->
<!-- size="small"-->
<!-- style="width: 100%"-->
<!-- popper-class="max-height-el-select">-->
<!-- <el-option-group :label="t('dialog.boop_dialog.my_emojis')">-->
<!-- <el-option-->
<!-- v-for="image in emojiTable"-->
<!-- v-if="image.versions && image.versions.length > 0"-->
<!-- :key="image.id"-->
<!-- :value="image.id"-->
<!-- style="width: 100%; height: 100%">-->
<!-- <div-->
<!-- v-if="image.versions[image.versions.length - 1].file.url"-->
<!-- class="vrcplus-icon"-->
<!-- style="overflow: hidden; width: 200px; height: 200px; padding: 10px">-->
<!-- <template v-if="image.frames">-->
<!-- <div-->
<!-- class="avatar"-->
<!-- :style="-->
<!-- generateEmojiStyle(-->
<!-- image.versions[image.versions.length - 1].file.url,-->
<!-- image.framesOverTime,-->
<!-- image.frames,-->
<!-- image.loopStyle-->
<!-- )-->
<!-- "></div>-->
<!-- </template>-->
<!-- <template v-else>-->
<!-- <img-->
<!-- v-lazy="image.versions[image.versions.length - 1].file.url"-->
<!-- class="avatar"-->
<!-- style="width: 200px; height: 200px" />-->
<!-- </template>-->
<!-- </div>-->
<!-- </el-option>-->
<!-- </el-option-group>-->
<!-- <el-option-group :label="t('dialog.boop_dialog.default_emojis')">-->
<!-- <el-option-->
<!-- v-for="emojiName in photonEmojis"-->
<!-- :key="emojiName"-->
<!-- :value="getEmojiValue(emojiName)"-->
<!-- style="width: 100%; height: 100%">-->
<!-- <span v-text="emojiName"></span>-->
<!-- </el-option>-->
<!-- </el-option-group>-->
<!-- </el-select>-->
<!-- <template #footer>-->
<!-- <el-button size="small" @click="showGalleryDialog(2)">{{-->
<!-- t('dialog.boop_dialog.emoji_manager')-->
<!-- }}</el-button>-->
<!-- <el-button size="small" @click="closeDialog">{{ t('dialog.boop_dialog.cancel') }}</el-button>-->
<!-- <el-button size="small" :disabled="!sendBoopDialog.userId" @click="sendBoop">{{-->
<!-- t('dialog.boop_dialog.send')-->
<!-- }}</el-button>-->
<!-- </template>-->
<!-- </safe-dialog>-->
<!--</template>-->
<!--<script setup>-->
<!-- import { inject, ref } from 'vue';-->
<!-- import { useI18n } from 'vue-i18n-bridge';-->
<!-- import { photonEmojis } from '../../composables/shared/constants/photon.js';-->
<!-- import { notificationRequest } from '../../api';-->
<!-- // import { miscRequest } from '../../api';-->
<!-- const { t } = useI18n();-->
<!-- const userStatusClass = inject('userStatusClass');-->
<!-- const userImage = inject('userImage');-->
<!-- const showGalleryDialog = inject('showGalleryDialog');-->
<!-- const props = defineProps({-->
<!-- sendBoopDialog: {-->
<!-- type: Object,-->
<!-- required: true-->
<!-- },-->
<!-- emojiTable: {-->
<!-- type: Array,-->
<!-- required: true-->
<!-- },-->
<!-- vipFriends: {-->
<!-- type: Array,-->
<!-- required: true-->
<!-- },-->
<!-- onlineFriends: {-->
<!-- type: Array,-->
<!-- required: true-->
<!-- },-->
<!-- activeFriends: {-->
<!-- type: Array,-->
<!-- required: true-->
<!-- },-->
<!-- offlineFriends: {-->
<!-- type: Array,-->
<!-- required: true-->
<!-- },-->
<!-- generateEmojiStyle: {-->
<!-- type: Function,-->
<!-- required: true-->
<!-- },-->
<!-- notificationTable: {-->
<!-- type: Object,-->
<!-- required: true-->
<!-- }-->
<!-- });-->
<!-- const emit = defineEmits(['update:sendBoopDialog']);-->
<!-- const fileId = ref('');-->
<!-- // $app.data.sendBoopDialog = {-->
<!-- // visible: false,-->
<!-- // userId: ''-->
<!-- // };-->
<!-- // $app.methods.showSendBoopDialog = function (userId) {-->
<!-- // this.$nextTick(() =>-->
<!-- // $app.adjustDialogZ(this.$refs.sendBoopDialog.$el)-->
<!-- // );-->
<!-- // const D = this.sendBoopDialog;-->
<!-- // D.userId = userId;-->
<!-- // D.visible = true;-->
<!-- // if (this.emojiTable.length === 0 && API.currentUser.$isVRCPlus) {-->
<!-- // this.refreshEmojiTable();-->
<!-- // }-->
<!-- // };-->
<!-- function closeDialog() {-->
<!-- emit('update:sendBoopDialog', {-->
<!-- ...props.sendBoopDialog,-->
<!-- visible: false-->
<!-- });-->
<!-- }-->
<!-- function getEmojiValue(emojiName) {-->
<!-- if (!emojiName) {-->
<!-- return '';-->
<!-- }-->
<!-- return `vrchat_${emojiName.replace(/ /g, '_').toLowerCase()}`;-->
<!-- }-->
<!-- function sendBoop() {-->
<!-- const D = props.sendBoopDialog;-->
<!-- dismissBoop(D.userId);-->
<!-- const params = {-->
<!-- userId: D.userId-->
<!-- };-->
<!-- if (fileId.value) {-->
<!-- params.emojiId = fileId.value;-->
<!-- }-->
<!-- // miscRequest.sendBoop(params);-->
<!-- D.visible = false;-->
<!-- }-->
<!-- function dismissBoop(userId) {-->
<!-- // JANK: This is a hack to remove boop notifications when responding-->
<!-- const array = props.notificationTable.data;-->
<!-- for (let i = array.length - 1; i >= 0; i&#45;&#45;) {-->
<!-- const ref = array[i];-->
<!-- if (ref.type !== 'boop' || ref.$isExpired || ref.senderUserId !== userId) {-->
<!-- continue;-->
<!-- }-->
<!-- notificationRequest.sendNotificationResponse({-->
<!-- notificationId: ref.id,-->
<!-- responseType: 'delete',-->
<!-- responseData: ''-->
<!-- });-->
<!-- }-->
<!-- }-->
<!--</script>-->

View File

@@ -0,0 +1,90 @@
<template>
<safe-dialog
class="x-dialog"
:visible.sync="bioDialog.visible"
:title="t('dialog.bio.header')"
width="600px"
append-to-body>
<div v-loading="bioDialog.loading">
<el-input
v-model="bioDialog.bio"
type="textarea"
size="mini"
maxlength="512"
show-word-limit
:autosize="{ minRows: 5, maxRows: 20 }"
:placeholder="t('dialog.bio.bio_placeholder')"
style="margin-bottom: 10px">
</el-input>
<el-input
v-for="(link, index) in bioDialog.bioLinks"
:key="index"
v-model="bioDialog.bioLinks[index]"
:value="link"
size="small"
style="margin-top: 5px">
<img
slot="prepend"
:src="getFaviconUrl(link)"
style="width: 16px; height: 16px; vertical-align: middle" />
<el-button slot="append" icon="el-icon-delete" @click="bioDialog.bioLinks.splice(index, 1)" />
</el-input>
<el-button
:disabled="bioDialog.bioLinks.length >= 3"
size="mini"
style="margin-top: 5px"
@click="bioDialog.bioLinks.push('')">
{{ t('dialog.bio.add_link') }}
</el-button>
</div>
<template #footer>
<el-button type="primary" size="small" :disabled="bioDialog.loading" @click="saveBio">
{{ t('dialog.bio.update') }}
</el-button>
</template>
</safe-dialog>
</template>
<script setup>
import { getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { userRequest } from '../../../api';
import { getFaviconUrl } from '../../../composables/shared/utils';
const { t } = useI18n();
const { $message } = getCurrentInstance().proxy;
const props = defineProps({
bioDialog: {
type: Object,
required: true
}
});
function saveBio() {
const D = props.bioDialog;
if (D.loading) {
return;
}
D.loading = true;
userRequest
.saveCurrentUser({
bio: D.bio,
bioLinks: D.bioLinks
})
.finally(() => {
D.loading = false;
})
.then((args) => {
D.visible = false;
$message({
message: 'Bio updated',
type: 'success'
});
return args;
});
}
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,96 @@
<template>
<safe-dialog
class="x-dialog"
:visible.sync="languageDialog.visible"
:title="t('dialog.language.header')"
width="400px"
append-to-body>
<div v-loading="languageDialog.loading">
<div v-for="item in API.currentUser.$languages" :key="item.key" style="margin: 6px 0">
<el-tag
size="small"
type="info"
effect="plain"
closable
style="margin-right: 5px"
@close="removeUserLanguage(item.key)">
<span
class="flags"
:class="languageClass(item.key)"
style="display: inline-block; margin-right: 5px"></span>
{{ item.value }} ({{ item.key.toUpperCase() }})
</el-tag>
</div>
<el-select
value=""
:disabled="
languageDialog.loading || (API.currentUser.$languages && API.currentUser.$languages.length === 3)
"
:placeholder="t('dialog.language.select_language')"
style="margin-top: 14px"
@change="addUserLanguage">
<el-option
v-for="item in languageDialog.languages"
:key="item.key"
:value="item.key"
:label="item.value">
<span
class="flags"
:class="languageClass(item.key)"
style="display: inline-block; margin-right: 5px"></span>
{{ item.value }} ({{ item.key.toUpperCase() }})
</el-option>
</el-select>
</div>
</safe-dialog>
</template>
<script setup>
import { inject } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { userRequest } from '../../../api';
import { languageClass } from '../../../composables/user/utils';
const { t } = useI18n();
const API = inject('API');
const props = defineProps({
languageDialog: {
type: Object,
required: true
}
});
function removeUserLanguage(language) {
if (language !== String(language)) {
return;
}
const D = props.languageDialog;
D.loading = true;
userRequest
.removeUserTags({
tags: [`language_${language}`]
})
.finally(function () {
D.loading = false;
});
}
function addUserLanguage(language) {
if (language !== String(language)) {
return;
}
const D = props.languageDialog;
D.loading = true;
userRequest
.addUserTags({
tags: [`language_${language}`]
})
.finally(function () {
D.loading = false;
});
}
</script>

View File

@@ -1,12 +1,10 @@
<template>
<el-dialog
<safe-dialog
ref="previousInstancesUserDialog"
:before-close="beforeDialogClose"
:visible.sync="isVisible"
:title="$t('dialog.previous_instances.header')"
width="1000px"
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
append-to-body>
<div style="display: flex; align-items: center; justify-content: space-between">
<span style="font-size: 14px" v-text="previousInstancesUserDialog.userRef.displayName"></span>
<el-input
@@ -68,11 +66,12 @@
</template>
</el-table-column>
</data-tables>
</el-dialog>
</safe-dialog>
</template>
<script>
import utils from '../../../classes/utils';
import { parseLocation } from '../../../composables/instance/utils';
import database from '../../../service/database';
import Location from '../../Location.vue';
@@ -81,14 +80,7 @@
components: {
Location
},
inject: [
'beforeDialogClose',
'dialogMouseDown',
'dialogMouseUp',
'adjustDialogZ',
'showLaunchDialog',
'showPreviousInstancesInfoDialog'
],
inject: ['adjustDialogZ', 'showLaunchDialog', 'showPreviousInstancesInfoDialog'],
props: {
previousInstancesUserDialog: {
type: Object,
@@ -176,7 +168,7 @@
database.getpreviousInstancesByUserId(this.previousInstancesUserDialog.userRef).then((data) => {
const array = [];
for (const ref of data.values()) {
ref.$location = utils.parseLocation(ref.location);
ref.$location = parseLocation(ref.location);
if (ref.time > 0) {
ref.timer = utils.timeToText(ref.time);
} else {

View File

@@ -0,0 +1,65 @@
<template>
<safe-dialog
class="x-dialog"
:visible.sync="pronounsDialog.visible"
:title="t('dialog.pronouns.header')"
width="600px"
append-to-body>
<div v-loading="pronounsDialog.loading">
<el-input
type="textarea"
v-model="pronounsDialog.pronouns"
size="mini"
maxlength="32"
show-word-limit
:autosize="{ minRows: 2, maxRows: 5 }"
:placeholder="t('dialog.pronouns.pronouns_placeholder')">
</el-input>
</div>
<template #footer>
<el-button type="primary" size="small" :disabled="pronounsDialog.loading" @click="savePronouns">
{{ t('dialog.pronouns.update') }}
</el-button>
</template>
</safe-dialog>
</template>
<script setup>
import { getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { userRequest } from '../../../api';
const { t } = useI18n();
const { proxy } = getCurrentInstance();
const { $message } = proxy;
const props = defineProps({
pronounsDialog: {
type: Object,
required: true
}
});
function savePronouns() {
const D = props.pronounsDialog;
if (D.loading) {
return;
}
D.loading = true;
userRequest
.saveCurrentUser({
pronouns: D.pronouns
})
.finally(() => {
D.loading = false;
})
.then((args) => {
D.visible = false;
$message({
message: 'Pronouns updated',
type: 'success'
});
return args;
});
}
</script>

View File

@@ -0,0 +1,138 @@
<template>
<safe-dialog
class="x-dialog"
:visible.sync="sendInviteRequestDialogVisible"
:title="t('dialog.invite_request_message.header')"
width="800px"
append-to-body
@close="cancelSendInviteRequest">
<template v-if="API.currentUser.$isVRCPlus">
<input class="inviteImageUploadButton" type="file" accept="image/*" @change="inviteImageUpload" />
</template>
<data-tables
v-bind="inviteRequestMessageTable"
style="margin-top: 10px; cursor: pointer"
@row-click="showSendInviteConfirmDialog">
<el-table-column
:label="t('table.profile.invite_messages.slot')"
prop="slot"
sortable="custom"
width="70"></el-table-column>
<el-table-column :label="t('table.profile.invite_messages.message')" prop="message"></el-table-column>
<el-table-column
:label="t('table.profile.invite_messages.cool_down')"
prop="updatedAt"
sortable="custom"
width="110"
align="right">
<template #default="scope">
<countdown-timer :datetime="scope.row.updatedAt" :hours="1"></countdown-timer>
</template>
</el-table-column>
<el-table-column :label="t('table.profile.invite_messages.action')" width="70" align="right">
<template #default="scope">
<el-button
type="text"
icon="el-icon-edit"
size="mini"
@click.stop="showEditAndSendInviteDialog('request', scope.row)"></el-button>
</template>
</el-table-column>
</data-tables>
<template #footer>
<el-button type="small" @click="cancelSendInviteRequest">{{
t('dialog.invite_request_message.cancel')
}}</el-button>
<el-button type="small" @click="API.refreshInviteMessageTableData('request')">{{
t('dialog.invite_request_message.refresh')
}}</el-button>
</template>
<SendInviteConfirmDialog
:visible.sync="isSendInviteConfirmDialogVisible"
:send-invite-dialog="sendInviteDialog"
:invite-dialog="inviteDialog"
:upload-image="uploadImage"
@closeInviteDialog="closeInviteDialog" />
<EditAndSendInviteDialog
:edit-and-send-invite-dialog.sync="editAndSendInviteDialog"
:send-invite-dialog="sendInviteDialog"
:invite-dialog="inviteDialog"
:upload-image="uploadImage"
@closeInviteDialog="closeInviteDialog" />
</safe-dialog>
</template>
<script setup>
import { inject, ref } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import EditAndSendInviteDialog from '../InviteDialog/EditAndSendInviteDialog.vue';
import SendInviteConfirmDialog from '../InviteDialog/SendInviteConfirmDialog.vue';
const { t } = useI18n();
const API = inject('API');
const props = defineProps({
sendInviteRequestDialogVisible: {
type: Boolean,
default: false
},
inviteRequestMessageTable: {
type: Object,
default: () => ({})
},
sendInviteDialog: {
type: Object,
default: () => ({})
},
inviteDialog: {
type: Object,
require: false,
default: () => ({})
},
uploadImage: {
type: String,
default: ''
}
});
const emit = defineEmits(['inviteImageUpload', 'update:sendInviteRequestDialogVisible', 'closeInviteDialog']);
const isSendInviteConfirmDialogVisible = ref(false);
const editAndSendInviteDialog = ref({
visible: false,
messageType: '',
newMessage: '',
inviteMessage: {}
});
function inviteImageUpload(event) {
emit('inviteImageUpload', event);
}
function showSendInviteConfirmDialog(val) {
isSendInviteConfirmDialogVisible.value = true;
//
props.sendInviteDialog.messageSlot = val.slot;
}
function showEditAndSendInviteDialog(messageType, inviteMessage) {
editAndSendInviteDialog.value = {
newMessage: inviteMessage.message,
visible: true,
messageType,
inviteMessage
};
}
function cancelSendInviteRequest() {
emit('update:sendInviteRequestDialogVisible', false);
}
function closeInviteDialog() {
cancelSendInviteRequest();
}
</script>

View File

@@ -0,0 +1,109 @@
<template>
<safe-dialog
class="x-dialog"
:visible.sync="socialStatusDialog.visible"
:title="t('dialog.social_status.header')"
append-to-body
width="400px">
<div v-loading="socialStatusDialog.loading">
<el-collapse style="border: 0">
<el-collapse-item>
<template #title>
<span style="font-size: 16px">{{ t('dialog.social_status.history') }}</span>
</template>
<data-tables
v-bind="socialStatusHistoryTable"
style="cursor: pointer"
@row-click="setSocialStatusFromHistory">
<el-table-column :label="t('table.social_status.no')" prop="no" width="50"></el-table-column>
<el-table-column :label="t('table.social_status.status')" prop="status"></el-table-column>
</data-tables>
</el-collapse-item>
</el-collapse>
<el-select v-model="socialStatusDialog.status" style="display: block; margin-top: 10px">
<el-option :label="t('dialog.user.status.join_me')" value="join me">
<i class="x-user-status joinme"></i> {{ t('dialog.user.status.join_me') }}
</el-option>
<el-option :label="t('dialog.user.status.online')" value="active">
<i class="x-user-status online"></i> {{ t('dialog.user.status.online') }}
</el-option>
<el-option :label="t('dialog.user.status.ask_me')" value="ask me">
<i class="x-user-status askme"></i> {{ t('dialog.user.status.ask_me') }}
</el-option>
<el-option :label="t('dialog.user.status.busy')" value="busy">
<i class="x-user-status busy"></i> {{ t('dialog.user.status.busy') }}
</el-option>
<el-option v-if="API.currentUser.$isModerator" :label="t('dialog.user.status.offline')" value="offline">
<i class="x-user-status offline"></i> {{ t('dialog.user.status.offline') }}
</el-option>
</el-select>
<el-input
v-model="socialStatusDialog.statusDescription"
:placeholder="t('dialog.social_status.status_placeholder')"
maxlength="32"
show-word-limit
style="display: block; margin-top: 10px"></el-input>
</div>
<template #footer>
<el-button type="primary" size="small" :disabled="socialStatusDialog.loading" @click="saveSocialStatus">
{{ t('dialog.social_status.update') }}
</el-button>
</template>
</safe-dialog>
</template>
<script setup>
import { inject, getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { userRequest } from '../../../api';
const { t } = useI18n();
const { $message } = getCurrentInstance().proxy;
const API = inject('API');
const props = defineProps({
socialStatusDialog: {
type: Object,
required: true
},
socialStatusHistoryTable: {
type: Object,
required: true
}
});
function setSocialStatusFromHistory(val) {
if (val === null) {
return;
}
const D = props.socialStatusDialog;
D.statusDescription = val.status;
}
function saveSocialStatus() {
const D = props.socialStatusDialog;
if (D.loading) {
return;
}
D.loading = true;
userRequest
.saveCurrentUser({
status: D.status,
statusDescription: D.statusDescription
})
.finally(() => {
D.loading = false;
})
.then((args) => {
D.visible = false;
$message({
message: 'Status updated',
type: 'success'
});
return args;
});
}
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,10 @@
<template>
<el-dialog
<safe-dialog
ref="VRCXUpdateDialogRef"
class="x-dialog"
:before-close="beforeDialogClose"
:visible.sync="VRCXUpdateDialog.visible"
:title="t('dialog.vrcx_updater.header')"
width="400px"
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
width="400px">
<div v-loading="checkingForVRCXUpdate" style="margin-top: 15px">
<template v-if="updateInProgress">
<el-progress :percentage="updateProgress" :format="updateProgressText"></el-progress>
@@ -62,7 +59,7 @@
{{ t('dialog.vrcx_updater.install') }}
</el-button>
</template>
</el-dialog>
</safe-dialog>
</template>
<script setup>
@@ -71,9 +68,6 @@
import { useI18n } from 'vue-i18n-bridge';
const { t } = useI18n();
const beforeDialogClose = inject('beforeDialogClose');
const dialogMouseDown = inject('dialogMouseDown');
const dialogMouseUp = inject('dialogMouseUp');
const adjustDialogZ = inject('adjustDialogZ');
const props = defineProps({

View File

@@ -0,0 +1,394 @@
<template>
<safe-dialog
class="x-dialog"
:visible="changeWorldImageDialogVisible"
:title="t('dialog.change_content_image.world')"
width="850px"
append-to-body
@close="closeDialog">
<div v-loading="changeWorldImageDialogLoading">
<input
id="WorldImageUploadButton"
type="file"
accept="image/*"
style="display: none"
@change="onFileChangeWorldImage" />
<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="uploadWorldImage">{{
t('dialog.change_content_image.upload')
}}</el-button>
<!-- el-button(type="default" size="small" @click="deleteWorldImage" icon="el-icon-delete") Delete Latest Image-->
</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="setWorldImage(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 API = inject('API');
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const props = defineProps({
changeWorldImageDialogVisible: {
type: Boolean,
default: false
},
previousImagesTable: {
type: Array,
default: () => []
},
previousImagesFileId: {
type: String,
default: ''
},
worldDialog: {
type: Object,
default: () => ({})
}
});
const emit = defineEmits(['update:changeWorldImageDialogVisible', 'refresh']);
const changeWorldImageDialogLoading = ref(false);
const worldImage = ref({
base64File: '',
fileMd5: '',
base64SignatureFile: '',
signatureMd5: '',
fileId: '',
avatarId: ''
});
function uploadWorldImage() {
document.getElementById('WorldImageUploadButton').click();
}
function closeDialog() {
emit('update:changeWorldImageDialogVisible', false);
}
function refresh() {
emit('refresh', 'Change');
}
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 onFileChangeWorldImage(e) {
const clearFile = function () {
if (document.querySelector('#WorldImageUploadButton')) {
document.querySelector('#WorldImageUploadButton').value = '';
}
};
const files = e.target.files || e.dataTransfer.files;
if (!files.length || !props.worldDialog.visible || props.worldDialog.loading) {
clearFile();
return;
}
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;
}
changeWorldImageDialogLoading.value = true;
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 worldId = props.worldDialog.id;
const { imageUrl } = props.worldDialog.ref;
const fileId = extractFileId(imageUrl);
if (!fileId) {
$message({
message: t('message.world.image_invalid'),
type: 'error'
});
clearFile();
return;
}
worldImage.value = {
base64File,
fileMd5,
base64SignatureFile,
signatureMd5,
fileId,
worldId
};
const params = {
fileMd5,
fileSizeInBytes,
signatureMd5,
signatureSizeInBytes
};
// Upload chaining
await initiateUpload(params, fileId);
} catch (error) {
console.error('World image upload process failed:', error);
} finally {
changeWorldImageDialogLoading.value = false;
clearFile();
}
};
r.readAsBinaryString(files[0]);
}
// ------------ Upload Process Start ------------
async function initiateUpload(params, fileId) {
const res = await imageRequest.uploadWorldImage(params, fileId);
return worldImageInit(res);
}
async function worldImageInit(args) {
// API.$on('WORLDIMAGE: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.uploadWorldImageFileStart(params);
return worldImageFileStart(res);
}
async function worldImageFileStart(args) {
// API.$on('WORLDIMAGE:FILESTART')
const { url } = args.json;
const { fileId, fileVersion } = args.params;
const params = {
url,
fileId,
fileVersion
};
return uploadWorldImageFileAWS(params);
}
async function uploadWorldImageFileAWS(params) {
const json = await webApiService.execute({
url: params.url,
uploadFilePUT: true,
fileData: worldImage.value.base64File,
fileMIME: 'image/png',
headers: {
'Content-MD5': worldImage.value.fileMd5
}
});
if (json.status !== 200) {
// $app.worldDialog.loading = false;
changeWorldImageDialogLoading.value = false;
API.$throw('World image upload failed', json, params.url);
}
const args = {
json,
params
};
return worldImageFileAWS(args);
}
async function worldImageFileAWS(args) {
// API.$on('WORLDIMAGE:FILEAWS')
const { fileId, fileVersion } = args.params;
const params = {
fileId,
fileVersion
};
const res = await imageRequest.uploadWorldImageFileFinish(params);
return worldImageFileFinish(res);
}
async function worldImageFileFinish(args) {
// API.$on('WORLDIMAGE:FILEFINISH')
const { fileId, fileVersion } = args.params;
const params = {
fileId,
fileVersion
};
const res = await imageRequest.uploadWorldImageSigStart(params);
return worldImageSigStart(res);
}
async function worldImageSigStart(args) {
// API.$on('WORLDIMAGE:SIGSTART')
const { url } = args.json;
const { fileId, fileVersion } = args.params;
const params = {
url,
fileId,
fileVersion
};
return uploadWorldImageSigAWS(params);
}
async function uploadWorldImageSigAWS(params) {
const json = await webApiService.execute({
url: params.url,
uploadFilePUT: true,
fileData: worldImage.value.base64SignatureFile,
fileMIME: 'application/x-rsync-signature',
headers: {
'Content-MD5': worldImage.value.signatureMd5
}
});
if (json.status !== 200) {
// $app.worldDialog.loading = false;
changeWorldImageDialogLoading.value = false;
API.$throw('World image upload failed', json, params.url);
}
const args = {
json,
params
};
return worldImageSigAWS(args);
}
async function worldImageSigAWS(args) {
// API.$on('WORLDIMAGE:SIGAWS')
const { fileId, fileVersion } = args.params;
const params = {
fileId,
fileVersion
};
const res = await imageRequest.uploadWorldImageSigFinish(params);
return worldImageSigFinish(res);
}
async function worldImageSigFinish(args) {
// API.$on('WORLDIMAGE:SIGFINISH')
const { fileId, fileVersion } = args.params;
const parmas = {
id: worldImage.value.worldId,
imageUrl: `${API.endpointDomain}/file/${fileId}/${fileVersion}/file`
};
const res = await imageRequest.setWorldImage(parmas);
return worldImageSet(res);
}
function worldImageSet(args) {
changeWorldImageDialogLoading.value = false;
if (args.json.imageUrl === args.params.imageUrl) {
$message({
message: t('message.world.image_changed'),
type: 'success'
});
refresh();
} else {
API.$throw(0, 'World image change failed', args.params.imageUrl);
}
}
// ------------ Upload Process End ------------
function setWorldImage(image) {
changeWorldImageDialogLoading.value = true;
const parmas = {
id: props.worldDialog.id,
imageUrl: `${API.endpointDomain}/file/${props.previousImagesFileId}/${image.version}/file`
};
imageRequest
.setWorldImage(parmas)
.then((args) => worldImageSet(args))
.finally(() => {
changeWorldImageDialogLoading.value = false;
closeDialog();
});
}
function compareCurrentImage(image) {
if (
`${API.endpointDomain}/file/${props.previousImagesFileId}/${image.version}/file` ===
// FIXME: old:avatarDialog -> new:worldDialog, is this correct?
props.worldDialog.ref.imageUrl
) {
return true;
}
return false;
}
// $app.methods.deleteWorldImage = function () {
// this.changeWorldImageDialogLoading = true;
// var parmas = {
// fileId: this.previousImagesTableFileId,
// version: this.previousImagesTable[0].version
// };
// vrcPlusIconRequest
// .deleteFileVersion(parmas)
// .then((args) => {
// this.previousImagesTableFileId = args.json.id;
// var images = [];
// args.json.versions.forEach((item) => {
// if (!item.deleted) {
// images.unshift(item);
// }
// });
// this.checkPreviousImageAvailable(images);
// })
// .finally(() => {
// this.changeWorldImageDialogLoading = false;
// });
// };
</script>

View File

@@ -1,13 +1,10 @@
<template>
<el-dialog
:before-close="beforeDialogClose"
<safe-dialog
:visible.sync="isVisible"
:title="$t('dialog.set_world_tags.header')"
width="400px"
destroy-on-close
append-to-body
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
append-to-body>
<el-checkbox v-model="setWorldTagsDialog.avatarScalingDisabled">
{{ $t('dialog.set_world_tags.avatar_scaling_disabled') }}
</el-checkbox>
@@ -80,7 +77,7 @@
</el-button>
</div>
</template>
</el-dialog>
</safe-dialog>
</template>
<script>
@@ -88,7 +85,7 @@
export default {
name: 'SetWorldTagsDialog',
inject: ['beforeDialogClose', 'dialogMouseDown', 'dialogMouseUp', 'showWorldDialog'],
inject: ['showWorldDialog'],
props: {
oldTags: {
type: Array,

View File

@@ -1,13 +1,10 @@
<template>
<el-dialog
:before-close="beforeDialogClose"
<safe-dialog
:visible.sync="isVisible"
:title="$t('dialog.allowed_video_player_domains.header')"
width="600px"
destroy-on-close
append-to-body
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
append-to-body>
<div>
<el-input
v-for="(domain, index) in urlList"
@@ -31,7 +28,7 @@
{{ $t('dialog.allowed_video_player_domains.save') }}
</el-button>
</template>
</el-dialog>
</safe-dialog>
</template>
<script>
@@ -39,7 +36,6 @@
export default {
name: 'WorldAllowedDomainsDialog',
inject: ['beforeDialogClose', 'dialogMouseDown', 'dialogMouseUp'],
props: {
worldAllowedDomainsDialog: {
type: Object,

View File

@@ -1,13 +1,10 @@
<template>
<el-dialog
<safe-dialog
ref="worldDialog"
:before-close="beforeDialogClose"
class="x-dialog x-world-dialog"
:visible.sync="isDialogVisible"
:show-close="false"
width="770px"
@mousedown.native="dialogMouseDown"
@mouseup.native="dialogMouseUp">
width="770px">
<div v-loading="worldDialog.loading">
<div style="display: flex">
<el-popover placement="right" width="500px" trigger="click">
@@ -760,22 +757,49 @@
:offline-friends="offlineFriends"
:active-friends="activeFriends"
:online-friends="onlineFriends"
:vip-friends="vipFriends" />
</el-dialog>
:vip-friends="vipFriends"
:invite-message-table="inviteMessageTable"
:upload-image="uploadImage"
:last-location="lastLocation" />
<ChangeWorldImageDialog
:change-world-image-dialog-visible.sync="changeWorldImageDialogVisible"
:previous-images-table="previousImagesTable"
:previous-images-file-id="previousImagesFileId"
:world-dialog="worldDialog"
@refresh="displayPreviousImages" />
<PreviousImagesDialog
:previous-images-dialog-visible.sync="previousImagesDialogVisible"
:previous-images-table="previousImagesTable" />
</safe-dialog>
</template>
<script>
import { favoriteRequest, imageRequest, miscRequest, userRequest, worldRequest } from '../../../api';
import utils from '../../../classes/utils';
import { refreshInstancePlayerCount as _refreshInstancePlayerCount } from '../../../composables/instance/utils';
import {
downloadAndSaveJson as _downloadAndSaveJson,
extractFileId,
replaceVrcPackageUrl as _replaceVrcPackageUrl
} from '../../../composables/shared/utils';
import database from '../../../service/database.js';
import WorldAllowedDomainsDialog from './WorldAllowedDomainsDialog.vue';
import SetWorldTagsDialog from './SetWorldTagsDialog.vue';
import PreviousInstancesWorldDialog from '../PreviousInstancesDialog/PreviousInstancesWorldDialog.vue';
import NewInstanceDialog from '../NewInstanceDialog.vue';
import { favoriteRequest, miscRequest, worldRequest } from '../../../api';
import PreviousImagesDialog from '../PreviousImagesDialog.vue';
import PreviousInstancesWorldDialog from '../PreviousInstancesDialog/PreviousInstancesWorldDialog.vue';
import ChangeWorldImageDialog from './ChangeWorldImageDialog.vue';
import SetWorldTagsDialog from './SetWorldTagsDialog.vue';
import WorldAllowedDomainsDialog from './WorldAllowedDomainsDialog.vue';
export default {
name: 'WorldDialog',
components: { SetWorldTagsDialog, WorldAllowedDomainsDialog, PreviousInstancesWorldDialog, NewInstanceDialog },
components: {
PreviousImagesDialog,
SetWorldTagsDialog,
WorldAllowedDomainsDialog,
PreviousInstancesWorldDialog,
NewInstanceDialog,
ChangeWorldImageDialog
},
inject: [
'API',
'showUserDialog',
@@ -785,10 +809,6 @@
'showPreviousInstancesInfoDialog',
'showLaunchDialog',
'showFullscreenImageDialog',
'beforeDialogClose',
'dialogMouseDown',
'dialogMouseUp',
'displayPreviousImages',
'showWorldDialog',
'showFavoriteDialog',
'openExternalLink'
@@ -808,6 +828,8 @@
activeFriends: Array,
onlineFriends: Array,
vipFriends: Array,
inviteMessageTable: Object,
uploadImage: String,
// TODO: Remove
updateInstanceInfo: Number
@@ -826,7 +848,11 @@
openFlg: false,
worldRef: {}
},
newInstanceDialogLocationTag: ''
newInstanceDialogLocationTag: '',
changeWorldImageDialogVisible: false,
previousImagesFileId: '',
previousImagesDialogVisible: false,
previousImagesTable: []
};
},
computed: {
@@ -907,6 +933,51 @@
}
},
methods: {
displayPreviousImages(command) {
this.previousImagesFileId = '';
this.previousImagesTable = [];
const { imageUrl } = this.worldDialog.ref;
const fileId = extractFileId(imageUrl);
if (!fileId) {
return;
}
const params = {
fileId
};
if (command === 'Display') {
this.previousImagesDialogVisible = true;
}
if (command === 'Change') {
this.changeWorldImageDialogVisible = true;
}
imageRequest.getWorldImages(params).then((args) => {
this.previousImagesFileId = args.json.id;
const images = [];
args.json.versions.forEach((item) => {
if (!item.deleted) {
images.unshift(item);
}
});
this.checkPreviousImageAvailable(images);
});
},
async checkPreviousImageAvailable(images) {
this.previousImagesTable = [];
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) {
this.previousImagesTable.push(image);
}
}
}
},
showNewInstanceDialog(tag) {
// trigger watcher
this.newInstanceDialogLocationTag = '';
@@ -946,26 +1017,30 @@
});
break;
case 'Make Home':
this.API.saveCurrentUser({
homeLocation: D.id
}).then((args) => {
this.$message({
message: 'Home world updated',
type: 'success'
userRequest
.saveCurrentUser({
homeLocation: D.id
})
.then((args) => {
this.$message({
message: 'Home world updated',
type: 'success'
});
return args;
});
return args;
});
break;
case 'Reset Home':
this.API.saveCurrentUser({
homeLocation: ''
}).then((args) => {
this.$message({
message: 'Home world has been reset',
type: 'success'
userRequest
.saveCurrentUser({
homeLocation: ''
})
.then((args) => {
this.$message({
message: 'Home world has been reset',
type: 'success'
});
return args;
});
return args;
});
break;
case 'Publish':
worldRequest
@@ -1040,10 +1115,10 @@
this.openExternalLink(this.replaceVrcPackageUrl(this.worldDialog.ref.unityPackageUrl));
break;
case 'Change Image':
this.displayPreviousImages('World', 'Change');
this.displayPreviousImages('Change');
break;
case 'Previous Images':
this.displayPreviousImages('World', 'Display');
this.displayPreviousImages('Display');
break;
case 'Refresh':
this.showWorldDialog(D.id);
@@ -1055,12 +1130,15 @@
this.showFavoriteDialog('world', D.id);
break;
default:
this.$emit('world-dialog-command', command);
this.$emit('worldDialogCommand', command);
break;
}
},
replaceVrcPackageUrl(url) {
_replaceVrcPackageUrl(url);
},
refreshInstancePlayerCount(tag) {
this.$emit('refresh-instance-player-count', tag);
_refreshInstancePlayerCount(tag);
},
onWorldMemoChange() {
const worldId = this.worldDialog.id;
@@ -1087,7 +1165,7 @@
this.treeData = utils.buildTreeData(this.worldDialog.ref);
},
downloadAndSaveJson(fileName, data) {
utils.downloadAndSaveJson(fileName, data);
_downloadAndSaveJson(fileName, data);
},
copyWorldId() {
navigator.clipboard