mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-26 18:23:47 +02:00
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:
@@ -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>
|
||||
|
||||
393
src/components/dialogs/AvatarDialog/ChangeAvatarImageDialog.vue
Normal file
393
src/components/dialogs/AvatarDialog/ChangeAvatarImageDialog.vue
Normal 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>
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
91
src/components/dialogs/FullscreenImageDialog.vue
Normal file
91
src/components/dialogs/FullscreenImageDialog.vue
Normal 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>
|
||||
136
src/components/dialogs/GroupDialog/GallerySelectDialog.vue
Normal file
136
src/components/dialogs/GroupDialog/GallerySelectDialog.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
212
src/components/dialogs/InviteDialog/EditAndSendInviteDialog.vue
Normal file
212
src/components/dialogs/InviteDialog/EditAndSendInviteDialog.vue
Normal 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>
|
||||
309
src/components/dialogs/InviteDialog/InviteDialog.vue
Normal file
309
src/components/dialogs/InviteDialog/InviteDialog.vue
Normal 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>
|
||||
176
src/components/dialogs/InviteDialog/SendInviteConfirmDialog.vue
Normal file
176
src/components/dialogs/InviteDialog/SendInviteConfirmDialog.vue
Normal 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>
|
||||
165
src/components/dialogs/InviteDialog/SendInviteDialog.vue
Normal file
165
src/components/dialogs/InviteDialog/SendInviteDialog.vue
Normal 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>
|
||||
@@ -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
|
||||
282
src/components/dialogs/LaunchDialog.vue
Normal file
282
src/components/dialogs/LaunchDialog.vue
Normal 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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
51
src/components/dialogs/PreviousImagesDialog.vue
Normal file
51
src/components/dialogs/PreviousImagesDialog.vue
Normal 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>
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
96
src/components/dialogs/SafeDialog.vue
Normal file
96
src/components/dialogs/SafeDialog.vue
Normal 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>
|
||||
277
src/components/dialogs/SendBoopDialog.vue
Normal file
277
src/components/dialogs/SendBoopDialog.vue
Normal 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--) {-->
|
||||
<!-- const ref = array[i];-->
|
||||
<!-- if (ref.type !== 'boop' || ref.$isExpired || ref.senderUserId !== userId) {-->
|
||||
<!-- continue;-->
|
||||
<!-- }-->
|
||||
<!-- notificationRequest.sendNotificationResponse({-->
|
||||
<!-- notificationId: ref.id,-->
|
||||
<!-- responseType: 'delete',-->
|
||||
<!-- responseData: ''-->
|
||||
<!-- });-->
|
||||
<!-- }-->
|
||||
<!-- }-->
|
||||
<!--</script>-->
|
||||
90
src/components/dialogs/UserDialog/BioDialog.vue
Normal file
90
src/components/dialogs/UserDialog/BioDialog.vue
Normal 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>
|
||||
1024
src/components/dialogs/UserDialog/GalleryDialog.vue
Normal file
1024
src/components/dialogs/UserDialog/GalleryDialog.vue
Normal file
File diff suppressed because it is too large
Load Diff
96
src/components/dialogs/UserDialog/LanguageDialog.vue
Normal file
96
src/components/dialogs/UserDialog/LanguageDialog.vue
Normal 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>
|
||||
@@ -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 {
|
||||
65
src/components/dialogs/UserDialog/PronounsDialog.vue
Normal file
65
src/components/dialogs/UserDialog/PronounsDialog.vue
Normal 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>
|
||||
138
src/components/dialogs/UserDialog/SendInviteRequestDialog.vue
Normal file
138
src/components/dialogs/UserDialog/SendInviteRequestDialog.vue
Normal 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>
|
||||
109
src/components/dialogs/UserDialog/SocialStatusDialog.vue
Normal file
109
src/components/dialogs/UserDialog/SocialStatusDialog.vue
Normal 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>
|
||||
3179
src/components/dialogs/UserDialog/UserDialog.vue
Normal file
3179
src/components/dialogs/UserDialog/UserDialog.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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({
|
||||
|
||||
394
src/components/dialogs/WorldDialog/ChangeWorldImageDialog.vue
Normal file
394
src/components/dialogs/WorldDialog/ChangeWorldImageDialog.vue
Normal 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>
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user