Upgrade to Vue3 and Element Plus (#1374)

* Update Vue devtools

* upgrade vue pinia element-plus vue-i18n, add vite

* fix: i18n

* global components

* change v-deep

* upgrade vue-lazyload

* data table

* update enlint and safe-dialog

* package.json and vite.config.js

* el-icon

* el-message

* vue 2 -> vue3 migration changes

* $pinia

* dialog

* el-popover slot

* lint

* chore

* slot

* scss

* remote state access

* misc

* jsconfig

* el-button size mini -> small

* :model-value

* ElMessageBox

* datatable

* remove v-lazyload

* template #dropdown

* mini -> small

* css

* byebye hideTooltips

* use sass-embedded

* Update SQLite, remove unneeded libraries

* Fix shift remove local avatar favorites

* Electron arm64

* arm64 support

* bye pug

* f-word vite hah

* misc

* remove safe dialog component

* Add self invite to launch dialog

* Fix errors

* Icons 1

* improve localfavorite loading performance

* improve favorites world item performance

* dialog visibility changes for Element Plus

* clear element plus error

* import performance

* revert App.vue hah

* hah

* Revert "Add self invite to launch dialog"

This reverts commit 4801cfad58.

* Toggle self invite/open in-game

* Self invite on launch dialog

* el-button icon

* el-icon

* fix user dialog tab switching logic

* fix PlayerList

* Formatting changes

* More icons

* Fix friend log table

* loading margin

* fix markdown

* fix world dialog tab switching issue

* Fixes and formatting

* fix: global i18n.t export

* fix favorites world tab not working

* Create instance, displayName

* Remove group members sort by userId

* Fix loading dialog tabs on swtich

* Star

* charts console.warn

* wip: fix charts

* wip: fix charts

* wip: charts composables

* fix favorite item tooltip warning

* Fixes and formatting

* Clean up image dialogs

* Remove unused method

* Fix platform/size border

* Fix platform/size border

* $vr

* fix friendExportDialogVisible binding

* ElMessageBox and Settings

* Login formatting

* Rename VR overlay query

* Fix image popover and userdialog badges

* Formatting

* Big buttons

* Fixes, update Cef

* Fix gameLog table nav buttons jumping around while using nav buttons

* Fix z-index

* vr overlay

* vite input add theme

* defineAsyncComponent

* ISO 639-1

* fix i18n

* clean t

* Formatting, fix calendar, rotate arrows

* Show user status when user is offline

* Fix VR overlay

* fix theme and clean up

* split InstanceActivity

* tweak

* Fix VR overlay formatting

* fix scss var

* AppDebug hahahaha

* Years

* remove reactive

* improve perf

* state hah…

* fix user rendering poblems when user object is not yet loaded

* improve perf

* Update avatar/world image uploader, licenses, remove previous images dialog (old images are now deleted)

* improve perf 1

* Suppress stray errors

* fix traveling location display issue

* Fix empty instance creator

* improve friend list refresh performance

* fix main charts

* fix chart

* Fix darkmode

* Fix avatar dialog tags

---------

Co-authored-by: pa <maplenagisa@gmail.com>
This commit is contained in:
Natsumi
2025-09-12 10:45:24 +12:00
committed by GitHub
parent b233bbc299
commit 3324d0d279
249 changed files with 12948 additions and 19815 deletions
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible="changeAvatarImageDialogVisible"
:model-value="changeAvatarImageDialogVisible"
:title="t('dialog.change_content_image.avatar')"
width="850px"
append-to-body
@@ -16,73 +16,47 @@
<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">
<el-button type="default" size="small" :icon="Upload" @click="uploadAvatarImage">
{{ t('dialog.change_content_image.upload') }}
</el-button>
</el-button-group>
<br />
<div v-for="image in previousImagesTable" :key="image.version" style="display: inline-block">
<div
v-if="image.file"
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 class="x-change-image-item">
<img :src="currentImageUrl" class="img-size" loading="lazy" />
</div>
</div>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ElMessage } from 'element-plus';
import { Upload } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
import { getCurrentInstance, ref } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { imageRequest } from '../../../api';
import { AppGlobal } from '../../../service/appConfig';
import { $throw } from '../../../service/request';
import { extractFileId } from '../../../shared/utils';
import { useAvatarStore, useGalleryStore } from '../../../stores';
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { avatarRequest } from '../../../api';
import { useAvatarStore } from '../../../stores';
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const { avatarDialog } = storeToRefs(useAvatarStore());
const { previousImagesTable } = storeToRefs(useGalleryStore());
const { applyAvatar } = useAvatarStore();
const props = defineProps({
changeAvatarImageDialogVisible: {
type: Boolean,
default: false
required: true
},
previousImagesFileId: {
previousImageUrl: {
type: String,
default: ''
}
});
const changeAvatarImageDialogLoading = ref(false);
const avatarImage = ref({
base64File: '',
fileMd5: '',
base64SignatureFile: '',
signatureMd5: '',
fileId: '',
avatarId: ''
});
const currentImageUrl = computed(() => props.previousImageUrl);
const emit = defineEmits(['update:changeAvatarImageDialogVisible', 'refresh']);
function refresh() {
emit('refresh', 'Change');
}
const emit = defineEmits(['update:changeAvatarImageDialogVisible', 'update:previousImageUrl']);
function closeDialog() {
emit('update:changeAvatarImageDialogVisible', false);
@@ -93,23 +67,9 @@
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 () {
changeAvatarImageDialogLoading.value = false;
const fileInput = /** @type{HTMLInputElement} */ (document.querySelector('#AvatarImageUploadButton'));
if (fileInput) {
fileInput.value = '';
@@ -124,7 +84,7 @@
// validate file
if (files[0].size >= 100000000) {
// 100MB
$message({
ElMessage({
message: t('message.file.too_large'),
type: 'error'
});
@@ -132,7 +92,7 @@
return;
}
if (!files[0].type.match(/image.*/)) {
$message({
ElMessage({
message: t('message.file.not_image'),
type: 'error'
});
@@ -141,50 +101,14 @@
}
const r = new FileReader();
r.onload = async function (file) {
r.onload = async function () {
try {
const base64File = await resizeImageToFitLimits(btoa(r.result.toString()));
// 10MB
const fileMd5 = await genMd5(base64File);
const fileSizeInBytes = parseInt(file.total.toString(), 10);
const base64SignatureFile = await genSig(base64File);
const signatureMd5 = await genMd5(base64SignatureFile);
const signatureSizeInBytes = parseInt(await genLength(base64SignatureFile), 10);
const avatarId = avatarDialog.value.id;
const { imageUrl } = avatarDialog.value.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);
await initiateUpload(base64File);
} catch (error) {
console.error('Avatar image upload process failed:', error);
} finally {
changeAvatarImageDialogLoading.value = false;
clearFile();
}
};
@@ -193,169 +117,32 @@
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) {
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) {
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
}
async function initiateUpload(base64File) {
const args = await avatarRequest.uploadAvatarImage(base64File);
const fileUrl = args.json.versions[args.json.versions.length - 1].file.url;
const avatarArgs = await avatarRequest.saveAvatar({
id: avatarDialog.value.id,
imageUrl: fileUrl
});
if (json.status !== 200) {
changeAvatarImageDialogLoading.value = false;
$throw(json.status, 'Avatar image upload failed', params.url);
}
const args = {
json,
params
};
return avatarImageFileAWS(args);
}
async function avatarImageFileAWS(args) {
const { fileId, fileVersion } = args.params;
const params = {
fileId,
fileVersion
};
const res = await imageRequest.uploadAvatarImageFileFinish(params);
return avatarImageFileFinish(res);
}
async function avatarImageFileFinish(args) {
const { fileId, fileVersion } = args.params;
const params = {
fileId,
fileVersion
};
const res = await imageRequest.uploadAvatarImageSigStart(params);
return avatarImageSigStart(res);
}
async function avatarImageSigStart(args) {
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;
$throw(json.status, 'Avatar image upload failed', params.url);
}
const args = {
json,
params
};
return avatarImageSigAWS(args);
}
async function avatarImageSigAWS(args) {
const { fileId, fileVersion } = args.params;
const params = {
fileId,
fileVersion
};
const res = await imageRequest.uploadAvatarImageSigFinish(params);
return avatarImageSigFinish(res);
}
async function avatarImageSigFinish(args) {
const { fileId, fileVersion } = args.params;
const parmas = {
id: avatarImage.value.avatarId,
imageUrl: `${AppGlobal.endpointDomain}/file/${fileId}/${fileVersion}/file`
};
const res = await imageRequest.setAvatarImage(parmas);
return avatarImageSet(res);
}
async function avatarImageSet(args) {
applyAvatar(args.json);
const ref = applyAvatar(avatarArgs.json);
emit('update:previousImageUrl', ref.imageUrl);
changeAvatarImageDialogLoading.value = false;
if (args.json.imageUrl === args.params.imageUrl) {
$message({
message: t('message.avatar.image_changed'),
type: 'success'
});
refresh();
} else {
$throw(0, 'Avatar image change failed', args.params.imageUrl);
}
}
ElMessage({
message: t('message.avatar.image_changed'),
type: 'success'
});
// ------------ Upload Process End ------------
// closeDialog();
}
function uploadAvatarImage() {
document.getElementById('AvatarImageUploadButton').click();
}
function setAvatarImage(image) {
changeAvatarImageDialogLoading.value = true;
const parmas = {
id: avatarDialog.value.id,
imageUrl: `${AppGlobal.endpointDomain}/file/${props.previousImagesFileId}/${image.version}/file`
};
imageRequest
.setAvatarImage(parmas)
.then((args) => applyAvatar(args.json))
.finally(() => {
changeAvatarImageDialogLoading.value = false;
closeDialog();
});
}
function compareCurrentImage(image) {
return (
`${AppGlobal.endpointDomain}/file/${props.previousImagesFileId}/${image.version}/file` ===
avatarDialog.value.ref.imageUrl
);
}
</script>
<style lang="scss" scoped>
.img-size {
width: 500px;
height: 375px;
}
</style>
@@ -1,8 +1,8 @@
<template>
<safe-dialog
<el-dialog
ref="setAvatarStylesDialog"
class="x-dialog"
:visible.sync="setAvatarStylesDialog.visible"
v-model="setAvatarStylesDialog.visible"
:title="t('dialog.set_avatar_styles.header')"
width="400px"
append-to-body>
@@ -39,11 +39,11 @@
</el-select>
</div>
<br />
<div style="font-size: 12px; margin-top: 10px">{{ $t('dialog.set_world_tags.author_tags') }}<br /></div>
<div style="font-size: 12px; margin-top: 10px">{{ t('dialog.set_world_tags.author_tags') }}<br /></div>
<el-input
v-model="setAvatarStylesDialog.authorTags"
type="textarea"
size="mini"
size="small"
show-word-limit
:autosize="{ minRows: 2, maxRows: 5 }"
placeholder=""
@@ -57,21 +57,18 @@
t('dialog.set_avatar_styles.save')
}}</el-button>
</template>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { watch, getCurrentInstance } from 'vue';
import { watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { arraysMatch } from '../../../shared/utils';
import { avatarRequest } from '../../../api';
import { useAvatarStore } from '../../../stores';
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const { applyAvatar } = useAvatarStore();
const props = defineProps({
@@ -1,8 +1,8 @@
<template>
<safe-dialog
<el-dialog
ref="setAvatarTagsDialog"
class="x-dialog"
:visible.sync="setAvatarTagsDialog.visible"
v-model="setAvatarTagsDialog.visible"
:title="t('dialog.set_avatar_tags.header')"
width="770px"
append-to-body>
@@ -29,7 +29,7 @@
<br />
<el-input
v-model="setAvatarTagsDialog.selectedTagsCsv"
size="mini"
size="small"
:autosize="{ minRows: 2, maxRows: 5 }"
:placeholder="t('dialog.set_avatar_tags.custom_tags_placeholder')"
style="margin-top: 10px"
@@ -47,19 +47,18 @@
<span style="margin-left: 5px"
>{{ setAvatarTagsDialog.selectedCount }} / {{ setAvatarTagsDialog.ownAvatars.length }}</span
>
<span v-if="setAvatarTagsDialog.loading" style="margin-left: 5px">
<i class="el-icon-loading"></i>
</span>
<el-icon v-if="setAvatarTagsDialog.loading" class="is-loading" style="margin-left: 5px"
><Loading
/></el-icon>
<br />
<div class="x-friend-list" style="margin-top: 10px; min-height: 60px; max-height: 280px">
<div
v-for="avatar in setAvatarTagsDialog.ownAvatars"
:key="avatar.id"
class="x-friend-item x-friend-item-border"
style="width: 350px"
:class="['item-width', 'x-friend-item', 'x-friend-item-border']"
@click="showAvatarDialog(avatar.id)">
<div class="avatar">
<img v-if="avatar.thumbnailImageUrl" v-lazy="avatar.thumbnailImageUrl" />
<img v-if="avatar.thumbnailImageUrl" :src="avatar.thumbnailImageUrl" loading="lazy" />
</div>
<div class="detail">
<span class="name" v-text="avatar.name"></span>
@@ -76,7 +75,7 @@
<span v-else class="extra" v-text="avatar.releaseStatus"></span>
<span class="extra" v-text="avatar.$tagString"></span>
</div>
<el-button type="text" size="mini" style="margin-left: 5px" @click.stop>
<el-button type="text" size="small" style="margin-left: 5px" @click.stop>
<el-checkbox v-model="avatar.$selected" @change="updateAvatarTagsSelection"></el-checkbox>
</el-button>
</div>
@@ -90,21 +89,21 @@
t('dialog.set_avatar_tags.save')
}}</el-button>
</template>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { getCurrentInstance, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { Loading } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import { watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { avatarRequest } from '../../../api';
import { useAvatarStore } from '../../../stores';
const { showAvatarDialog, applyAvatar } = useAvatarStore();
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const props = defineProps({
setAvatarTagsDialog: {
type: Object,
@@ -229,7 +228,7 @@
}
} catch (err) {
console.error(err);
$message({
ElMessage({
message: 'Error saving avatar tags',
type: 'error'
});
@@ -273,4 +272,8 @@
}
</script>
<style scoped></style>
<style scoped>
.item-width {
width: 335px;
}
</style>
@@ -1,12 +1,12 @@
<template>
<safe-dialog ref="favoriteDialogRef" :visible.sync="isVisible" :title="t('dialog.favorite.header')" width="300px">
<el-dialog :z-index="favoriteDialogIndex" v-model="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">
<el-button
style="display: block; width: 100%; margin: 10px 0"
@click="deleteFavoriteNoConfirm(favoriteDialog.objectId)">
<i class="el-icon-check"></i>
<el-icon><Check /></el-icon>
{{ favoriteDialog.currentGroup.displayName }} ({{ favoriteDialog.currentGroup.count }} /
{{ favoriteDialog.currentGroup.capacity }})
</el-button>
@@ -23,18 +23,16 @@
</div>
<div v-if="favoriteDialog.type === 'world'" style="margin-top: 20px">
<span style="display: block; text-align: center">{{ t('dialog.favorite.local_favorites') }}</span>
<template v-for="group in localWorldFavoriteGroups">
<template v-for="group in localWorldFavoriteGroups" :key="group">
<el-button
v-if="hasLocalWorldFavorite(favoriteDialog.objectId, group)"
:key="group"
style="display: block; width: 100%; margin: 10px 0"
@click="removeLocalWorldFavorite(favoriteDialog.objectId, group)">
<i class="el-icon-check"></i>
<el-icon><Check /></el-icon>
{{ group }} ({{ getLocalWorldFavoriteGroupLength(group) }})
</el-button>
<el-button
v-else
:key="group"
style="display: block; width: 100%; margin: 10px 0"
@click="addLocalWorldFavorite(favoriteDialog.objectId, group)">
{{ group }} ({{ getLocalWorldFavoriteGroupLength(group) }})
@@ -43,18 +41,16 @@
</div>
<div v-if="favoriteDialog.type === 'avatar'" style="margin-top: 20px">
<span style="display: block; text-align: center">{{ t('dialog.favorite.local_avatar_favorites') }}</span>
<template v-for="group in localAvatarFavoriteGroups">
<template v-for="group in localAvatarFavoriteGroups" :key="group">
<el-button
v-if="hasLocalAvatarFavorite(favoriteDialog.objectId, group)"
:key="group"
style="display: block; width: 100%; margin: 10px 0"
@click="removeLocalAvatarFavorite(favoriteDialog.objectId, group)">
<i class="el-icon-check"></i>
<el-icon><Check /></el-icon>
{{ group }} ({{ getLocalAvatarFavoriteGroupLength(group) }})
</el-button>
<el-button
v-else
:key="group"
style="display: block; width: 100%; margin: 10px 0"
:disabled="!isLocalUserVrcplusSupporter"
@click="addLocalAvatarFavorite(favoriteDialog.objectId, group)">
@@ -62,16 +58,18 @@
</el-button>
</template>
</div>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { Check } from '@element-plus/icons-vue';
import Noty from 'noty';
import { storeToRefs } from 'pinia';
import { computed, nextTick, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { favoriteRequest } from '../../api';
import { adjustDialogZ } from '../../shared/utils';
import { getNextDialogIndex } from '../../shared/utils';
import { useFavoriteStore, useUserStore } from '../../stores';
const { t } = useI18n();
@@ -98,7 +96,7 @@
} = favoriteStore;
const { currentUser } = storeToRefs(useUserStore());
const favoriteDialogRef = ref(null);
const favoriteDialogIndex = ref(2000);
const groups = ref([]);
const loading = ref(false);
@@ -113,11 +111,12 @@
watch(
() => favoriteDialog.value.visible,
async (value) => {
(value) => {
if (value) {
initFavoriteDialog();
await nextTick();
adjustDialogZ(favoriteDialogRef.value.$el);
nextTick(() => {
favoriteDialogIndex.value = getNextDialogIndex();
});
}
}
);
@@ -1,49 +1,90 @@
<template>
<safe-dialog class="x-dialog" :visible.sync="fullscreenImageDialog.visible" append-to-body width="97vw">
<el-dialog class="x-dialog" v-model="fullscreenImageDialog.visible" append-to-body 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"
:disabled="!fullscreenImageDialog.imageUrl.startsWith('http')"
@click="
downloadAndSaveImage(fullscreenImageDialog.imageUrl, fullscreenImageDialog.fileName)
"></el-button>
<el-tooltip :content="t('dialog.fullscreen_image.copy_image_to_clipboard')" placement="top">
<el-button
size="small"
:icon="CopyDocument"
circle
@click="copyImageToClipboard(fullscreenImageDialog.imageUrl)"></el-button>
</el-tooltip>
<el-tooltip :content="t('dialog.fullscreen_image.download_and_save_image')" placement="top">
<el-button
type="default"
size="small"
:icon="Download"
circle
style="margin-left: 5px"
:disabled="!fullscreenImageDialog.imageUrl.startsWith('http')"
@click="
downloadAndSaveImage(fullscreenImageDialog.imageUrl, fullscreenImageDialog.fileName)
"></el-button>
</el-tooltip>
</div>
<img v-lazy="fullscreenImageDialog.imageUrl" style="width: 100%; height: 85vh; object-fit: contain" />
<img
:src="fullscreenImageDialog.imageUrl"
style="width: 100%; height: 85vh; object-fit: contain"
loading="lazy" />
</div>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ElMessage } from 'element-plus';
import { Download, CopyDocument } from '@element-plus/icons-vue';
import Noty from 'noty';
import { storeToRefs } from 'pinia';
import { getCurrentInstance } from 'vue';
import { copyToClipboard, escapeTag, extractFileId } from '../../shared/utils';
import { useI18n } from 'vue-i18n';
import { escapeTag, extractFileId } from '../../shared/utils';
import { useGalleryStore } from '../../stores';
const { proxy } = getCurrentInstance();
const { $message } = proxy;
const { t } = useI18n();
const { fullscreenImageDialog } = storeToRefs(useGalleryStore());
function copyImageUrl(imageUrl) {
copyToClipboard(imageUrl, 'ImageUrl copied to clipboard');
async function copyImageToClipboard(url) {
if (!url) {
return;
}
const msg = ElMessage({
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}`);
}
await navigator.clipboard.write([
new ClipboardItem({
'image/png': await (await fetch(response.data)).blob()
})
]);
ElMessage({
message: 'Image copied to clipboard',
type: 'success'
});
} catch (error) {
console.error('Error downloading image:', error);
new Noty({
type: 'error',
text: escapeTag(`Failed to download image. ${url}`)
}).show();
} finally {
msg.close();
}
}
async function downloadAndSaveImage(url, fileName) {
if (!url) {
return;
}
$message({
const msg = ElMessage({
message: 'Downloading image...',
type: 'info'
});
@@ -77,6 +118,8 @@
type: 'error',
text: escapeTag(`Failed to download image. ${url}`)
}).show();
} finally {
msg.close();
}
}
</script>
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible.sync="gallerySelectDialog.visible"
v-model="gallerySelectDialog.visible"
:title="t('dialog.gallery_select.header')"
width="100%"
append-to-body>
@@ -16,16 +16,16 @@
style="display: none"
@change="onFileChangeGallery" />
<el-button-group>
<el-button type="default" size="small" icon="el-icon-close" @click="selectImageGallerySelect('', '')">{{
<el-button type="default" size="small" :icon="Close" @click="selectImageGallerySelect('', '')">{{
t('dialog.gallery_select.none')
}}</el-button>
<el-button type="default" size="small" icon="el-icon-refresh" @click="refreshGalleryTable">{{
<el-button type="default" size="small" :icon="Refresh" @click="refreshGalleryTable">{{
t('dialog.gallery_select.refresh')
}}</el-button>
<el-button
type="default"
size="small"
icon="el-icon-upload2"
:icon="Upload"
:disabled="!currentUser.$isVRCPlus"
@click="displayGalleryUpload"
>{{ t('dialog.gallery_select.upload') }}</el-button
@@ -42,24 +42,28 @@
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
<img
:src="image.versions[image.versions.length - 1].file.url"
class="avatar"
loading="lazy" /></div
></template>
</div>
</div>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ElMessage } from 'element-plus';
import { Close, Refresh, Upload } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
import { getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { vrcPlusImageRequest } from '../../../api';
import { useGalleryStore, useUserStore } from '../../../stores';
const { t } = useI18n();
const { proxy } = getCurrentInstance();
const { $message } = proxy;
const { galleryTable } = storeToRefs(useGalleryStore());
const { refreshGalleryTable, handleGalleryImageAdd } = useGalleryStore();
const { currentUser } = storeToRefs(useUserStore());
@@ -95,7 +99,7 @@
}
if (files[0].size >= 100000000) {
// 100MB
$message({
ElMessage({
message: t('message.file.too_large'),
type: 'error'
});
@@ -103,7 +107,7 @@
return;
}
if (!files[0].type.match(/image.*/)) {
$message({
ElMessage({
message: t('message.file.not_image'),
type: 'error'
});
@@ -115,7 +119,7 @@
const base64Body = btoa(r.result.toString());
vrcPlusImageRequest.uploadGalleryImage(base64Body).then((args) => {
handleGalleryImageAdd(args);
$message({
ElMessage({
message: t('message.gallery.uploaded'),
type: 'success'
});
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible.sync="groupMemberModeration.visible"
v-model="groupMemberModeration.visible"
:title="t('dialog.group_member_moderation.header')"
append-to-body
width="90vw">
@@ -12,8 +12,8 @@
<div style="margin-top: 10px">
<el-button
type="default"
size="mini"
icon="el-icon-refresh"
size="small"
:icon="Refresh"
:loading="isGroupMembersLoading"
circle
@click="loadAllGroupMembers" />
@@ -34,21 +34,23 @@
memberSearch.length ||
!hasGroupPermission(groupMemberModeration.groupRef, 'group-bans-manage')
)
"
@click.native.stop>
<el-button size="mini">
">
<el-button size="small" @click.stop>
<span
>{{ t(memberSortOrder.name) }} <i class="el-icon-arrow-down el-icon--right"></i
></span>
>{{ t(memberSortOrder.name) }}
<el-icon style="margin-left: 5px"><ArrowDown /></el-icon>
</span>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item
v-for="item in groupDialogSortingOptions"
:key="item.name"
@click.native="setGroupMemberSortOrder(item)">
{{ t(item.name) }}
</el-dropdown-item>
</el-dropdown-menu>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="item in groupDialogSortingOptions"
:key="item.name"
@click="setGroupMemberSortOrder(item)">
{{ t(item.name) }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<span style="margin-right: 5px">{{ t('dialog.group.members.filter') }}</span>
<el-dropdown
@@ -61,33 +63,36 @@
memberSearch.length ||
!hasGroupPermission(groupMemberModeration.groupRef, 'group-bans-manage')
)
"
@click.native.stop>
<el-button size="mini">
">
<el-button size="small" @click.stop>
<span
>{{ t(memberFilter.name) }} <i class="el-icon-arrow-down el-icon--right"></i
>{{ t(memberFilter.name) }}
<el-icon style="margin-left: 5px"><ArrowDown /></el-icon
></span>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item
v-for="item in groupDialogFilterOptions"
:key="item.name"
@click.native="setGroupMemberFilter(item)"
v-text="t(item.name)"></el-dropdown-item>
<el-dropdown-item
v-for="item in groupMemberModeration.groupRef.roles"
:key="item.name"
@click.native="setGroupMemberFilter(item)"
><span v-if="!item.defaultRole">{{ t(item.name) }}</span></el-dropdown-item
>
</el-dropdown-menu>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="item in groupDialogFilterOptions"
:key="item.name"
@click="setGroupMemberFilter(item)"
v-text="t(item.name)"></el-dropdown-item>
<template v-for="role in groupMemberModeration.groupRef.roles" :key="role.name">
<el-dropdown-item
v-if="!role.defaultRole"
@click="setGroupMemberFilter(role)">
{{ t(role.name) }}
</el-dropdown-item>
</template>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<el-input
v-model="memberSearch"
:disabled="!hasGroupPermission(groupMemberModeration.groupRef, 'group-bans-manage')"
clearable
size="mini"
size="small"
:placeholder="t('dialog.group.members.search')"
style="margin-top: 10px; margin-bottom: 10px"
@input="groupMembersSearch"></el-input>
@@ -95,13 +100,13 @@
<el-button size="small" @click="selectAllGroupMembers">{{
t('dialog.group_member_moderation.select_all')
}}</el-button>
<data-tables
<DataTable
v-if="groupMemberModerationTable.data.length"
v-bind="groupMemberModerationTable"
style="margin-top: 10px">
<el-table-column width="55" prop="$selected">
<template #default="scope">
<el-button type="text" size="mini" @click.stop>
<el-button type="text" size="small" @click.stop>
<el-checkbox
v-model="scope.row.$selected"
@change="
@@ -115,16 +120,19 @@
width="70"
prop="photo">
<template #default="scope">
<el-popover placement="right" height="500px" trigger="hover">
<el-popover placement="right" :width="500" trigger="hover">
<template #reference>
<img
:src="userImage(scope.row.user)"
class="friends-list-avatar"
loading="lazy" />
</template>
<img
slot="reference"
v-lazy="userImage(scope.row.user)"
class="friends-list-avatar" />
<img
v-lazy="userImageFull(scope.row.user)"
class="friends-list-avatar"
style="height: 500px; cursor: pointer"
@click="showFullscreenImageDialog(userImageFull(scope.row.user))" />
:src="userImageFull(scope.row.user)"
:class="['friends-list-avatar', 'x-popover-image']"
style="cursor: pointer"
@click="showFullscreenImageDialog(userImageFull(scope.row.user))"
loading="lazy" />
</el-popover>
</template>
</el-table-column>
@@ -145,9 +153,11 @@
</el-table-column>
<el-table-column :label="t('dialog.group_member_moderation.roles')" prop="roleIds" sortable>
<template #default="scope">
<template v-for="(roleId, index) in scope.row.roleIds">
<template v-for="(role, rIndex) in groupMemberModeration.groupRef.roles">
<span v-if="role?.id === roleId" :key="roleId + rIndex"
<template v-for="(roleId, index) in scope.row.roleIds" :key="roleId">
<template
v-for="(role, rIndex) in groupMemberModeration.groupRef.roles"
:key="roleId + rIndex">
<span v-if="role?.id === roleId"
>{{ role.name
}}<span v-if="index < scope.row.roleIds.length - 1">, </span></span
></template
@@ -181,7 +191,7 @@
<span v-text="scope.row.visibility"></span>
</template>
</el-table-column>
</data-tables>
</DataTable>
</div>
</el-tab-pane>
@@ -191,8 +201,8 @@
<div style="margin-top: 10px">
<el-button
type="default"
size="mini"
icon="el-icon-refresh"
size="small"
:icon="Refresh"
:loading="isGroupMembersLoading"
circle
@click="getAllGroupBans(groupMemberModeration.id)"></el-button>
@@ -203,17 +213,17 @@
<el-input
v-model="groupBansModerationTable.filters[0].value"
clearable
size="mini"
size="small"
:placeholder="t('dialog.group.members.search')"
style="margin-top: 10px; margin-bottom: 10px"></el-input>
<br />
<el-button size="small" @click="selectAllGroupBans">{{
t('dialog.group_member_moderation.select_all')
}}</el-button>
<data-tables v-bind="groupBansModerationTable" style="margin-top: 10px">
<DataTable v-bind="groupBansModerationTable" style="margin-top: 10px">
<el-table-column width="55" prop="$selected">
<template #default="scope">
<el-button type="text" size="mini" @click.stop>
<el-button type="text" size="small" @click.stop>
<el-checkbox
v-model="scope.row.$selected"
@change="
@@ -227,16 +237,19 @@
width="70"
prop="photo">
<template #default="scope">
<el-popover placement="right" height="500px" trigger="hover">
<el-popover placement="right" :width="500" trigger="hover">
<template #reference>
<img
:src="userImage(scope.row.user)"
class="friends-list-avatar"
loading="lazy" />
</template>
<img
slot="reference"
v-lazy="userImage(scope.row.user)"
class="friends-list-avatar" />
<img
v-lazy="userImageFull(scope.row.user)"
class="friends-list-avatar"
style="height: 500px; cursor: pointer"
@click="showFullscreenImageDialog(userImageFull(scope.row.user))" />
:src="userImageFull(scope.row.user)"
:class="['friends-list-avatar', 'x-popover-image']"
style="cursor: pointer"
@click="showFullscreenImageDialog(userImageFull(scope.row.user))"
loading="lazy" />
</el-popover>
</template>
</el-table-column>
@@ -257,16 +270,14 @@
</el-table-column>
<el-table-column :label="t('dialog.group_member_moderation.roles')" prop="roleIds" sortable>
<template #default="scope">
<template v-for="(roleId, index) in scope.row.roleIds">
<template v-for="(roleId, index) in scope.row.roleIds" :key="roleId">
<span
v-for="(role, rIndex) in groupMemberModeration.groupRef.roles"
v-if="role.id === roleId"
:key="rIndex + roleId"
>{{ role.name }}</span
>
<span v-if="index < scope.row.roleIds.length - 1" :key="index + roleId"
>,
</span>
<span v-if="index < scope.row.roleIds.length - 1">, </span>
</template>
</template>
</el-table-column>
@@ -296,7 +307,7 @@
<span>{{ formatDateFilter(scope.row.bannedAt, 'long') }}</span>
</template>
</el-table-column>
</data-tables>
</DataTable>
</div>
</el-tab-pane>
@@ -306,29 +317,29 @@
<div style="margin-top: 10px">
<el-button
type="default"
size="mini"
icon="el-icon-refresh"
size="small"
:icon="Refresh"
:loading="isGroupMembersLoading"
circle
@click="getAllGroupInvitesAndJoinRequests(groupMemberModeration.id)"></el-button>
<br />
<el-tabs>
<el-tab-pane>
<span slot="label">
<template #label>
<span style="font-weight: bold; font-size: 16px">{{
t('dialog.group_member_moderation.sent_invites')
}}</span>
<span style="color: #909399; font-size: 12px; margin-left: 5px">{{
groupInvitesModerationTable.data.length
}}</span>
</span>
</template>
<el-button size="small" @click="selectAllGroupInvites">{{
t('dialog.group_member_moderation.select_all')
}}</el-button>
<data-tables v-bind="groupInvitesModerationTable" style="margin-top: 10px">
<DataTable v-bind="groupInvitesModerationTable" style="margin-top: 10px">
<el-table-column width="55" prop="$selected">
<template #default="scope">
<el-button type="text" size="mini" @click.stop>
<el-button type="text" size="small" @click.stop>
<el-checkbox
v-model="scope.row.$selected"
@change="
@@ -342,16 +353,19 @@
width="70"
prop="photo">
<template #default="scope">
<el-popover placement="right" height="500px" trigger="hover">
<el-popover placement="right" :width="500" trigger="hover">
<template #reference>
<img
:src="userImage(scope.row.user)"
class="friends-list-avatar"
loading="lazy" />
</template>
<img
slot="reference"
v-lazy="userImage(scope.row.user)"
class="friends-list-avatar" />
<img
v-lazy="userImageFull(scope.row.user)"
class="friends-list-avatar"
style="height: 500px; cursor: pointer"
@click="showFullscreenImageDialog(userImageFull(scope.row.user))" />
:src="userImageFull(scope.row.user)"
:class="['friends-list-avatar', 'x-popover-image']"
style="cursor: pointer"
@click="showFullscreenImageDialog(userImageFull(scope.row.user))"
loading="lazy" />
</el-popover>
</template>
</el-table-column>
@@ -378,7 +392,7 @@
<span @click.stop v-text="scope.row.managerNotes"></span>
</template>
</el-table-column>
</data-tables>
</DataTable>
<br />
<el-button
:disabled="
@@ -396,21 +410,21 @@
</el-tab-pane>
<el-tab-pane>
<span slot="label">
<template #label>
<span style="font-weight: bold; font-size: 16px">{{
t('dialog.group_member_moderation.join_requests')
}}</span>
<span style="color: #909399; font-size: 12px; margin-left: 5px">{{
groupJoinRequestsModerationTable.data.length
}}</span>
</span>
</template>
<el-button size="small" @click="selectAllGroupJoinRequests">{{
t('dialog.group_member_moderation.select_all')
}}</el-button>
<data-tables v-bind="groupJoinRequestsModerationTable" style="margin-top: 10px">
<DataTable v-bind="groupJoinRequestsModerationTable" style="margin-top: 10px">
<el-table-column width="55" prop="$selected">
<template #default="scope">
<el-button type="text" size="mini" @click.stop>
<el-button type="text" size="small" @click.stop>
<el-checkbox
v-model="scope.row.$selected"
@change="
@@ -424,16 +438,19 @@
width="70"
prop="photo">
<template #default="scope">
<el-popover placement="right" height="500px" trigger="hover">
<el-popover placement="right" :width="500" trigger="hover">
<template #reference>
<img
:src="userImage(scope.row.user)"
class="friends-list-avatar"
loading="lazy" />
</template>
<img
slot="reference"
v-lazy="userImage(scope.row.user)"
class="friends-list-avatar" />
<img
v-lazy="userImageFull(scope.row.user)"
class="friends-list-avatar"
style="height: 500px; cursor: pointer"
@click="showFullscreenImageDialog(userImageFull(scope.row.user))" />
:src="userImageFull(scope.row.user)"
:class="['friends-list-avatar', 'x-popover-image']"
style="cursor: pointer"
@click="showFullscreenImageDialog(userImageFull(scope.row.user))"
loading="lazy" />
</el-popover>
</template>
</el-table-column>
@@ -460,7 +477,7 @@
<span @click.stop v-text="scope.row.managerNotes"></span>
</template>
</el-table-column>
</data-tables>
</DataTable>
<br />
<el-button
:disabled="
@@ -504,21 +521,21 @@
</el-tab-pane>
<el-tab-pane>
<span slot="label">
<template #label>
<span style="font-weight: bold; font-size: 16px">{{
t('dialog.group_member_moderation.blocked_requests')
}}</span>
<span style="color: #909399; font-size: 12px; margin-left: 5px">{{
groupBlockedModerationTable.data.length
}}</span>
</span>
</template>
<el-button size="small" @click="selectAllGroupBlocked">{{
t('dialog.group_member_moderation.select_all')
}}</el-button>
<data-tables v-bind="groupBlockedModerationTable" style="margin-top: 10px">
<DataTable v-bind="groupBlockedModerationTable" style="margin-top: 10px">
<el-table-column width="55" prop="$selected">
<template #default="scope">
<el-button type="text" size="mini" @click.stop>
<el-button type="text" size="small" @click.stop>
<el-checkbox
v-model="scope.row.$selected"
@change="
@@ -532,16 +549,19 @@
width="70"
prop="photo">
<template #default="scope">
<el-popover placement="right" height="500px" trigger="hover">
<el-popover placement="right" :width="500" trigger="hover">
<template #reference>
<img
:src="userImage(scope.row.user)"
class="friends-list-avatar"
loading="lazy" />
</template>
<img
slot="reference"
v-lazy="userImage(scope.row.user)"
class="friends-list-avatar" />
<img
v-lazy="userImageFull(scope.row.user)"
class="friends-list-avatar"
style="height: 500px; cursor: pointer"
@click="showFullscreenImageDialog(userImageFull(scope.row.user))" />
:src="userImageFull(scope.row.user)"
:class="['friends-list-avatar', 'x-popover-image']"
style="cursor: pointer"
@click="showFullscreenImageDialog(userImageFull(scope.row.user))"
loading="lazy" />
</el-popover>
</template>
</el-table-column>
@@ -568,7 +588,7 @@
<span @click.stop v-text="scope.row.managerNotes"></span>
</template>
</el-table-column>
</data-tables>
</DataTable>
<br />
<el-button
:disabled="
@@ -594,8 +614,8 @@
<div style="margin-top: 10px">
<el-button
type="default"
size="mini"
icon="el-icon-refresh"
size="small"
:icon="Refresh"
:loading="isGroupMembersLoading"
circle
@click="getAllGroupLogs(groupMemberModeration.id)"></el-button>
@@ -637,7 +657,7 @@
</div>
</div>
<br />
<data-tables v-bind="groupLogsModerationTable" style="margin-top: 10px">
<DataTable v-bind="groupLogsModerationTable" style="margin-top: 10px">
<el-table-column
:label="t('dialog.group_member_moderation.created_at')"
width="170"
@@ -684,7 +704,7 @@
v-text="JSON.stringify(scope.row.data)"></span>
</template>
</el-table-column>
</data-tables>
</DataTable>
</div>
</el-tab-pane>
</el-tabs>
@@ -695,7 +715,7 @@
<br />
<el-input
v-model="selectUserId"
size="mini"
size="small"
style="margin-top: 5px; width: 340px"
:placeholder="t('dialog.group_member_moderation.user_id_placeholder')"
clearable></el-input>
@@ -707,8 +727,8 @@
<span class="name">{{ t('dialog.group_member_moderation.selected_users') }}</span>
<el-button
type="default"
size="mini"
icon="el-icon-delete"
size="small"
:icon="Delete"
circle
style="margin-left: 5px"
@click="clearSelectedGroupMembers"></el-button>
@@ -725,7 +745,7 @@
<template #content>
<span>{{ t('dialog.group_member_moderation.user_isnt_in_group') }}</span>
</template>
<i class="el-icon el-icon-warning" style="display: inline-block" />
<el-icon style="margin-left: 3px; display: inline-block"><Warning /></el-icon>
</el-tooltip>
<span v-text="user.user?.displayName || user.userId" style="font-weight: bold; margin-left: 5px"></span>
</el-tag>
@@ -739,7 +759,7 @@
:rows="2"
:autosize="{ minRows: 1, maxRows: 20 }"
:placeholder="t('dialog.group_member_moderation.note_placeholder')"
size="mini"
size="small"
resize="none"
style="margin-top: 5px"></el-input>
<br />
@@ -826,7 +846,7 @@
>{{ t('dialog.group_member_moderation.unban') }}</el-button
>
<span v-if="progressCurrent" style="margin-top: 10px">
<i class="el-icon-loading" style="margin-left: 5px; margin-right: 5px"></i>
<el-icon class="is-loading" style="margin-left: 5px; margin-right: 5px"><Loading /></el-icon>
{{ t('dialog.group_member_moderation.progress') }} {{ progressCurrent }}/{{ progressTotal }}
</span>
<el-button v-if="progressCurrent" style="margin-left: 5px" @click="progressTotal = 0">{{
@@ -834,15 +854,18 @@
}}</el-button>
</div>
<group-member-moderation-export-dialog
:is-group-logs-export-dialog-visible.sync="isGroupLogsExportDialogVisible"
:is-group-logs-export-dialog-visible="isGroupLogsExportDialogVisible"
:group-logs-moderation-table="groupLogsModerationTable" />
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { Refresh, Delete, ArrowDown, Warning, Loading } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import { storeToRefs } from 'pinia';
import { getCurrentInstance, reactive, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { reactive, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import * as workerTimers from 'worker-timers';
import { groupRequest, userRequest } from '../../../api';
import { groupDialogFilterOptions, groupDialogSortingOptions } from '../../../shared/constants';
@@ -857,9 +880,6 @@
const { applyGroupMember, handleGroupMember, handleGroupMemberProps } = useGroupStore();
const { showFullscreenImageDialog } = useGalleryStore();
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const selectedUsers = reactive({});
const selectedUsersArray = ref([]);
const isGroupMembersLoading = ref(false);
@@ -880,7 +900,7 @@
n: 100,
offset: 0,
groupId: '',
sort: '',
sort: 'joinedAt:desc',
roleId: ''
});
@@ -917,7 +937,7 @@
const groupInvitesModerationTable = reactive({
data: [],
tableProps: { stripe: true, size: 'mini' },
tableProps: { stripe: true, size: 'small' },
pageSize: 15,
paginationProps: {
small: true,
@@ -927,7 +947,7 @@
});
const groupJoinRequestsModerationTable = reactive({
data: [],
tableProps: { stripe: true, size: 'mini' },
tableProps: { stripe: true, size: 'small' },
pageSize: 15,
paginationProps: {
small: true,
@@ -937,7 +957,7 @@
});
const groupBlockedModerationTable = reactive({
data: [],
tableProps: { stripe: true, size: 'mini' },
tableProps: { stripe: true, size: 'small' },
pageSize: 15,
paginationProps: {
small: true,
@@ -948,7 +968,7 @@
const groupLogsModerationTable = reactive({
data: [],
filters: [{ prop: ['description'], value: '' }],
tableProps: { stripe: true, size: 'mini' },
tableProps: { stripe: true, size: 'small' },
pageSize: 15,
paginationProps: {
small: true,
@@ -959,7 +979,7 @@
const groupBansModerationTable = reactive({
data: [],
filters: [{ prop: ['$displayName'], value: '' }],
tableProps: { stripe: true, size: 'mini' },
tableProps: { stripe: true, size: 'small' },
pageSize: 15,
paginationProps: {
small: true,
@@ -969,7 +989,7 @@
});
const groupMemberModerationTable = reactive({
data: [],
tableProps: { stripe: true, size: 'mini' },
tableProps: { stripe: true, size: 'small' },
pageSize: 15,
paginationProps: {
small: true,
@@ -1084,7 +1104,7 @@
});
} catch (err) {
console.error(err);
$message({
ElMessage({
message: `Failed to delete group invites: ${err}`,
type: 'error'
});
@@ -1092,7 +1112,7 @@
}
}
if (allSuccess) {
$message({
ElMessage({
message: `Deleted ${memberCount} group invites`,
type: 'success'
});
@@ -1136,7 +1156,7 @@
}
groupBansModerationTable.data = fetchedBans;
} catch {
$message({
ElMessage({
message: 'Failed to get group bans',
type: 'error'
});
@@ -1167,13 +1187,13 @@
await groupRequest.banGroupMember({ groupId: D.id, userId: user.userId });
} catch (err) {
console.error(err);
$message({
ElMessage({
message: `Failed to ban group member: ${err}`,
type: 'error'
});
}
}
$message({ message: `Banned ${memberCount} group members`, type: 'success' });
ElMessage({ message: `Banned ${memberCount} group members`, type: 'success' });
progressCurrent.value = 0;
progressTotal.value = 0;
getAllGroupBans(D.id);
@@ -1196,7 +1216,7 @@
await groupRequest.unbanGroupMember({ groupId: D.id, userId: user.userId });
} catch (err) {
console.error(err);
$message({
ElMessage({
message: `Failed to unban group member: ${err}`,
type: 'error'
});
@@ -1205,7 +1225,7 @@
}
if (allSuccess) {
$message({ message: `Unbanned ${memberCount} group members`, type: 'success' });
ElMessage({ message: `Unbanned ${memberCount} group members`, type: 'success' });
}
progressCurrent.value = 0;
progressTotal.value = 0;
@@ -1230,7 +1250,7 @@
await groupRequest.kickGroupMember({ groupId: D.id, userId: user.userId });
} catch (err) {
console.error(err);
$message({
ElMessage({
message: `Failed to kick group member: ${err}`,
type: 'error'
});
@@ -1238,7 +1258,7 @@
}
}
if (allSuccess) {
$message({ message: `Kicked ${memberCount} group members`, type: 'success' });
ElMessage({ message: `Kicked ${memberCount} group members`, type: 'success' });
}
progressCurrent.value = 0;
progressTotal.value = 0;
@@ -1266,7 +1286,7 @@
handleGroupMemberProps(args);
} catch (err) {
console.error(err);
$message({
ElMessage({
message: `Failed to set group member note for ${err}`,
type: 'error'
});
@@ -1274,7 +1294,7 @@
}
}
if (allSuccess) {
$message({ message: `Saved notes for ${memberCount} group members`, type: 'success' });
ElMessage({ message: `Saved notes for ${memberCount} group members`, type: 'success' });
}
progressCurrent.value = 0;
progressTotal.value = 0;
@@ -1314,7 +1334,7 @@
handleGroupMemberRoleChange(args);
} catch (err) {
console.error(err);
$message({
ElMessage({
message: `Failed to remove group member roles: ${err}`,
type: 'error'
});
@@ -1323,7 +1343,7 @@
}
}
if (allSuccess) {
$message({
ElMessage({
message: `Roles removed`,
type: 'success'
});
@@ -1362,7 +1382,7 @@
handleGroupMemberRoleChange(args);
} catch (err) {
console.error(err);
$message({
ElMessage({
message: `Failed to add group member roles: ${err}`,
type: 'error'
});
@@ -1371,7 +1391,7 @@
}
}
if (allSuccess) {
$message({
ElMessage({
message: `Added group member roles`,
type: 'success'
});
@@ -1466,7 +1486,7 @@
}
}
} catch {
$message({
ElMessage({
message: 'Failed to get group logs',
type: 'error'
});
@@ -1496,7 +1516,7 @@
await groupRequest.deleteBlockedGroupRequest({ groupId: D.id, userId: user.userId });
} catch (err) {
console.error(err);
$message({
ElMessage({
message: `Failed to delete blocked group requests: ${err}`,
type: 'error'
});
@@ -1504,7 +1524,7 @@
}
}
if (allSuccess) {
$message({
ElMessage({
message: `Deleted ${memberCount} blocked group requests`,
type: 'success'
});
@@ -1532,7 +1552,7 @@
await groupRequest.blockGroupInviteRequest({ groupId: D.id, userId: user.userId });
} catch (err) {
console.error(err);
$message({
ElMessage({
message: `Failed to block group join requests: ${err}`,
type: 'error'
});
@@ -1540,7 +1560,7 @@
}
}
if (allSuccess) {
$message({
ElMessage({
message: `Blocked ${memberCount} group join requests`,
type: 'success'
});
@@ -1569,7 +1589,7 @@
await groupRequest.rejectGroupInviteRequest({ groupId: D.id, userId: user.userId });
} catch (err) {
console.error(err);
$message({
ElMessage({
message: `Failed to reject group join requests: ${err}`,
type: 'error'
});
@@ -1577,7 +1597,7 @@
}
}
if (allSuccess) {
$message({
ElMessage({
message: `Rejected ${memberCount} group join requests`,
type: 'success'
});
@@ -1605,7 +1625,7 @@
await groupRequest.acceptGroupInviteRequest({ groupId: D.id, userId: user.userId });
} catch (err) {
console.error(err);
$message({
ElMessage({
message: `Failed to accept group join requests: ${err}`,
type: 'error'
});
@@ -1613,7 +1633,7 @@
}
}
if (allSuccess) {
$message({
ElMessage({
message: `Accepted ${memberCount} group join requests`,
type: 'success'
});
@@ -1661,7 +1681,7 @@
}
}
} catch {
$message({
ElMessage({
message: 'Failed to get group join requests',
type: 'error'
});
@@ -1694,7 +1714,7 @@
}
}
} catch {
$message({
ElMessage({
message: 'Failed to get group join requests',
type: 'error'
});
@@ -1731,7 +1751,7 @@
}
}
} catch {
$message({
ElMessage({
message: 'Failed to get group invites',
type: 'error'
});
@@ -1823,7 +1843,7 @@
members.value = [];
isGroupMembersDone.value = false;
loadMoreGroupMembersParams.value = {
sort: '',
sort: 'joinedAt:desc',
roleId: '',
n: 100,
offset: 0,
@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible="isGroupLogsExportDialogVisible"
:model-value="isGroupLogsExportDialogVisible"
:title="t('dialog.group_member_moderation.export_logs')"
width="650px"
append-to-body
@@ -10,8 +10,8 @@
v-model="checkedGroupLogsExportLogsOptions"
style="margin-bottom: 10px"
@change="updateGroupLogsExportContent">
<template v-for="option in checkGroupsLogsExportLogsOptions">
<el-checkbox :key="option.label" :label="option.label">
<template v-for="option in checkGroupsLogsExportLogsOptions" :key="option.label">
<el-checkbox :label="option.label">
{{ t(option.text) }}
</el-checkbox>
</template>
@@ -20,18 +20,18 @@
<el-input
v-model="groupLogsExportContent"
type="textarea"
size="mini"
rows="15"
size="small"
:rows="15"
resize="none"
readonly
style="margin-top: 15px"
@click.native="handleCopyGroupLogsExportContent" />
</safe-dialog>
@click="handleCopyGroupLogsExportContent" />
</el-dialog>
</template>
<script setup>
import { ref, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { copyToClipboard } from '../../../shared/utils';
const { t } = useI18n();
@@ -1,6 +1,6 @@
<template>
<safe-dialog
:visible.sync="groupPostEditDialog.visible"
<el-dialog
v-model="groupPostEditDialog.visible"
:title="t('dialog.group_post_edit.header')"
width="650px"
append-to-body>
@@ -8,7 +8,7 @@
<h3 v-text="groupPostEditDialog.groupRef.name"></h3>
<el-form :model="groupPostEditDialog" label-width="150px">
<el-form-item :label="t('dialog.group_post_edit.title')">
<el-input v-model="groupPostEditDialog.title" size="mini"></el-input>
<el-input v-model="groupPostEditDialog.title" size="small"></el-input>
</el-form-item>
<el-form-item :label="t('dialog.group_post_edit.message')">
<el-input
@@ -61,29 +61,32 @@
<el-form-item :label="t('dialog.group_post_edit.image')">
<template v-if="gallerySelectDialog.selectedFileId">
<div style="display: inline-block; flex: none; margin-right: 5px">
<el-popover placement="right" width="500px" trigger="click">
<el-popover placement="right" :width="500" trigger="click">
<template #reference>
<img
:src="gallerySelectDialog.selectedImageUrl"
style="
flex: none;
width: 60px;
height: 60px;
border-radius: 4px;
object-fit: cover;
"
loading="lazy" />
</template>
<img
slot="reference"
v-lazy="gallerySelectDialog.selectedImageUrl"
style="
flex: none;
width: 60px;
height: 60px;
border-radius: 4px;
object-fit: cover;
" />
<img
v-lazy="gallerySelectDialog.selectedImageUrl"
style="height: 500px"
@click="showFullscreenImageDialog(gallerySelectDialog.selectedImageUrl)" />
:src="gallerySelectDialog.selectedImageUrl"
:class="['x-link', 'x-popover-image']"
@click="showFullscreenImageDialog(gallerySelectDialog.selectedImageUrl)"
loading="lazy" />
</el-popover>
<el-button size="mini" style="vertical-align: top" @click="clearImageGallerySelect">
<el-button size="small" style="vertical-align: top" @click="clearImageGallerySelect">
{{ t('dialog.invite_message.clear_selected_image') }}
</el-button>
</div>
</template>
<template v-else>
<el-button size="mini" style="margin-right: 5px" @click="showGallerySelectDialog">
<el-button size="small" style="margin-right: 5px" @click="showGallerySelectDialog">
{{ t('dialog.invite_message.select_image') }}
</el-button>
</template>
@@ -105,12 +108,13 @@
:gallery-select-dialog="gallerySelectDialog"
:gallery-table="galleryTable"
@refresh-gallery-table="refreshGalleryTable" />
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ref, computed, getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { ElMessage } from 'element-plus';
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { groupRequest, vrcPlusIconRequest } from '../../../api';
import { useGalleryStore, useGroupStore } from '../../../stores';
import GallerySelectDialog from './GallerySelectDialog.vue';
@@ -125,7 +129,6 @@
const emit = defineEmits(['update:dialogData']);
const { proxy } = getCurrentInstance();
const { t } = useI18n();
const { showFullscreenImageDialog, handleFilesList } = useGalleryStore();
@@ -169,7 +172,7 @@
return;
}
if (!D.title || !D.text) {
proxy.$message({
ElMessage({
message: 'Title and text are required',
type: 'warning'
});
@@ -188,8 +191,8 @@
params.imageId = gallerySelectDialog.value.selectedFileId;
}
groupRequest.editGroupPost(params).then((args) => {
handleGroupPost();
proxy.$message({
handleGroupPost(args);
ElMessage({
message: 'Group post edited',
type: 'success'
});
@@ -200,7 +203,7 @@
function createGroupPost() {
const D = groupPostEditDialog.value;
if (!D.title || !D.text) {
proxy.$message({
ElMessage({
message: 'Title and text are required',
type: 'warning'
});
@@ -220,7 +223,7 @@
}
groupRequest.createGroupPost(params).then((args) => {
handleGroupPost();
proxy.$message({
ElMessage({
message: 'Group post created',
type: 'success'
});
@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible="editAndSendInviteDialog.visible"
:model-value="editAndSendInviteDialog.visible"
:title="t('dialog.edit_send_invite_message.header')"
width="400px"
append-to-body
@@ -13,7 +13,7 @@
<el-input
v-model="editAndSendInviteDialog.newMessage"
type="textarea"
size="mini"
size="small"
maxlength="64"
show-word-limit
:autosize="{ minRows: 2, maxRows: 5 }"
@@ -21,28 +21,25 @@
style="margin-top: 10px"></el-input>
<template #footer>
<el-button type="small" @click="cancelEditAndSendInvite">
<el-button @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>
</el-dialog>
</template>
<script setup>
import { ElMessage } from 'element-plus';
import { storeToRefs } from 'pinia';
import { getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { instanceRequest, inviteMessagesRequest, notificationRequest } from '../../../api';
import { parseLocation } from '../../../shared/utils';
import { useGalleryStore, useUserStore } from '../../../stores';
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const { uploadImage } = storeToRefs(useGalleryStore());
const { clearInviteImageUpload } = useGalleryStore();
const { currentUser } = storeToRefs(useUserStore());
@@ -86,13 +83,13 @@
})
.then((args) => {
if (args.json[slot].message === I.messageSlot.message) {
$message({
ElMessage({
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');
ElMessage('Invite message updated');
}
return args;
});
@@ -139,7 +136,7 @@
} else {
J.loading = false;
J.visible = false;
$message({
ElMessage({
message: 'Invite sent',
type: 'success'
});
@@ -155,7 +152,7 @@
throw err;
})
.then((args) => {
$message({
ElMessage({
message: 'Invite photo message sent',
type: 'success'
});
@@ -168,7 +165,7 @@
throw err;
})
.then((args) => {
$message({
ElMessage({
message: 'Invite message sent',
type: 'success'
});
@@ -185,7 +182,7 @@
throw err;
})
.then((args) => {
$message({
ElMessage({
message: 'Request invite photo message sent',
type: 'success'
});
@@ -198,7 +195,7 @@
throw err;
})
.then((args) => {
$message({
ElMessage({
message: 'Request invite message sent',
type: 'success'
});
@@ -1,25 +1,25 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible.sync="inviteDialog.visible"
v-model="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" />
<br />
<el-button size="mini" style="margin-top: 10px" @click="addSelfToInvite">{{
<el-button size="small" style="margin-top: 10px" @click="addSelfToInvite">{{
t('dialog.invite.add_self')
}}</el-button>
<el-button
size="mini"
size="small"
:disabled="inviteDialog.friendsInInstance.length === 0"
style="margin-top: 10px"
@click="addFriendsInInstanceToInvite"
>{{ t('dialog.invite.add_friends_in_instance') }}</el-button
>
<el-button
size="mini"
size="small"
:disabled="vipFriends.length === 0"
style="margin-top: 10px"
@click="addFavoriteFriendsToInvite"
@@ -34,108 +34,116 @@
filterable
:disabled="inviteDialog.loading"
style="width: 100%; margin-top: 15px">
<el-option-group v-if="currentUser" :label="t('side_panel.me')">
<el-option
class="x-friend-item"
:label="currentUser.displayName"
:value="currentUser.id"
style="height: auto">
<div :class="['avatar', userStatusClass(currentUser)]">
<img v-lazy="userImage(currentUser)" />
</div>
<div class="detail">
<span class="name">{{ 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)" />
<template v-if="currentUser">
<el-option-group :label="t('side_panel.me')">
<el-option
class="x-friend-item"
:label="currentUser.displayName"
:value="currentUser.id"
style="height: auto">
<div :class="['avatar', userStatusClass(currentUser)]">
<img :src="userImage(currentUser)" loading="lazy" />
</div>
<div class="detail">
<span class="name" :style="{ color: friend.ref.$userColour }">{{
friend.ref.displayName
}}</span>
<span class="name">{{ currentUser.displayName }}</span>
</div>
</template>
<span v-else>{{ friend.id }}</span>
</el-option>
</el-option-group>
</el-option>
</el-option-group>
</template>
<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>
<template v-if="inviteDialog.friendsInInstance.length">
<el-option-group :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 :src="userImage(friend.ref)" loading="lazy" />
</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>
</template>
<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>
<template v-if="vipFriends.length">
<el-option-group :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 :src="userImage(friend.ref)" loading="lazy" />
</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>
</template>
<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>
<template v-if="onlineFriends.length">
<el-option-group :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 :src="userImage(friend.ref)" loading="lazy" />
</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>
</template>
<template v-if="activeFriends.length">
<el-option-group :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 :src="userImage(friend.ref)" loading="lazy" /></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>
</template>
</el-select>
</div>
@@ -155,17 +163,19 @@
>
</template>
<SendInviteDialog
:send-invite-dialog-visible.sync="sendInviteDialogVisible"
:send-invite-dialog-visible="sendInviteDialogVisible"
:send-invite-dialog="sendInviteDialog"
:invite-dialog="inviteDialog"
@closeInviteDialog="closeInviteDialog" />
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ElMessage, ElMessageBox } from 'element-plus';
import { storeToRefs } from 'pinia';
import { getCurrentInstance, ref } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { instanceRequest, notificationRequest } from '../../../api';
import { parseLocation, userImage, userStatusClass } from '../../../shared/utils';
import { useFriendStore, useGalleryStore, useInviteStore, useUserStore } from '../../../stores';
@@ -177,10 +187,6 @@
const { clearInviteImageUpload } = useGalleryStore();
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const $confirm = instance.proxy.$confirm;
const props = defineProps({
inviteDialog: {
type: Object,
@@ -238,11 +244,12 @@
}
function sendInvite() {
$confirm('Continue? Invite', 'Confirm', {
ElMessageBox.confirm('Continue? Invite', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
type: 'info'
})
.then((action) => {
const D = props.inviteDialog;
if (action !== 'confirm' || D.loading === true) {
return;
@@ -275,14 +282,14 @@
} else {
D.loading = false;
D.visible = false;
$message({
ElMessage({
message: 'Invite sent',
type: 'success'
});
}
};
inviteLoop();
}
});
})
.catch(() => {});
}
</script>
@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible="visible"
:model-value="visible"
:title="t('dialog.invite_message.header')"
width="400px"
append-to-body
@@ -18,22 +18,19 @@
{{ t('dialog.invite_message.confirm') }}
</el-button>
</template>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ElMessage } from 'element-plus';
import { storeToRefs } from 'pinia';
import { getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { instanceRequest, notificationRequest } from '../../../api';
import { parseLocation } from '../../../shared/utils';
import { useGalleryStore, useUserStore } from '../../../stores';
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const { uploadImage } = storeToRefs(useGalleryStore());
const { clearInviteImageUpload } = useGalleryStore();
const { currentUser } = storeToRefs(useUserStore());
@@ -54,10 +51,10 @@
}
});
const emit = defineEmits(['update:visible', 'closeInviteDialog']);
const emit = defineEmits(['update:model-value', 'closeInviteDialog']);
function cancelInviteConfirm() {
emit('update:visible', false);
emit('update:model-value', false);
}
function sendInviteConfirm() {
@@ -106,7 +103,7 @@
} else {
J.loading = false;
J.visible = false;
$message({
ElMessage({
message: 'Invite message sent',
type: 'success'
});
@@ -122,7 +119,7 @@
throw err;
})
.then((args) => {
$message({
ElMessage({
message: 'Invite photo message sent',
type: 'success'
});
@@ -135,7 +132,7 @@
throw err;
})
.then((args) => {
$message({
ElMessage({
message: 'Invite message sent',
type: 'success'
});
@@ -152,7 +149,7 @@
throw err;
})
.then((args) => {
$message({
ElMessage({
message: 'Request invite photo message sent',
type: 'success'
});
@@ -165,7 +162,7 @@
throw err;
})
.then((args) => {
$message({
ElMessage({
message: 'Request invite message sent',
type: 'success'
});
@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible="sendInviteDialogVisible"
:model-value="sendInviteDialogVisible"
:title="t('dialog.invite_message.header')"
width="800px"
append-to-body
@@ -9,46 +9,46 @@
<template v-if="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">-->
<!-- <el-popover placement="right" :width="500px" trigger="click">-->
<!-- <template #reference>-->
<!-- <img-->
<!-- class="x-link"-->
<!-- v-lazy="gallerySelectDialog.selectedImageUrl"-->
<!-- :src="gallerySelectDialog.selectedImageUrl"-->
<!-- style="flex: none; width: 60px; height: 60px; border-radius: 4px; object-fit: cover" />-->
<!-- </template>-->
<!-- <img-->
<!-- class="x-link"-->
<!-- v-lazy="gallerySelectDialog.selectedImageUrl"-->
<!-- :src="gallerySelectDialog.selectedImageUrl"-->
<!-- style="height: 500px"-->
<!-- @click="showFullscreenImageDialog(gallerySelectDialog.selectedImageUrl)" />-->
<!-- </el-popover>-->
<!-- </div>-->
<!-- <el-button size="mini" @click="clearImageGallerySelect" style="vertical-align: top">-->
<!-- <el-button size="small" @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">-->
<!-- <el-button size="small" @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
<DataTable
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"
:sortable="true"
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"
:sortable="true"
width="110"
align="right">
<template #default="scope">
@@ -59,38 +59,40 @@
<template #default="scope">
<el-button
type="text"
icon="el-icon-edit"
size="mini"
:icon="Edit"
size="small"
@click.stop="showEditAndSendInviteDialog(scope.row)"></el-button>
</template>
</el-table-column>
</data-tables>
</DataTable>
<template #footer>
<el-button type="small" @click="cancelSendInvite">
<el-button @click="cancelSendInvite">
{{ t('dialog.invite_message.cancel') }}
</el-button>
<el-button type="small" @click="refreshInviteMessageTableData('message')">
<el-button @click="refreshInviteMessageTableData('message')">
{{ t('dialog.invite_message.refresh') }}
</el-button>
</template>
<SendInviteConfirmDialog
:visible.sync="isSendInviteConfirmDialogVisible"
v-model="isSendInviteConfirmDialogVisible"
:send-invite-dialog="sendInviteDialog"
:invite-dialog="inviteDialog"
@closeInviteDialog="closeInviteDialog" />
<EditAndSendInviteDialog
:edit-and-send-invite-dialog.sync="editAndSendInviteDialog"
:edit-and-send-invite-dialog="editAndSendInviteDialog"
:send-invite-dialog="sendInviteDialog"
:invite-dialog="inviteDialog"
@closeInviteDialog="closeInviteDialog" />
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { Edit } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { useGalleryStore, useInviteStore, useUserStore } from '../../../stores';
import EditAndSendInviteDialog from './EditAndSendInviteDialog.vue';
import SendInviteConfirmDialog from './SendInviteConfirmDialog.vue';
+37 -33
View File
@@ -1,22 +1,22 @@
<template>
<safe-dialog
ref="inviteGroupDialogRef"
:visible.sync="inviteGroupDialog.visible"
:title="$t('dialog.invite_to_group.header')"
<el-dialog
:z-index="inviteGroupDialogIndex"
v-model="inviteGroupDialog.visible"
:title="t('dialog.invite_to_group.header')"
width="450px"
append-to-body>
<div v-if="inviteGroupDialog.visible" v-loading="inviteGroupDialog.loading">
<span>{{ $t('dialog.invite_to_group.description') }}</span>
<span>{{ t('dialog.invite_to_group.description') }}</span>
<br />
<el-select
v-model="inviteGroupDialog.groupId"
clearable
:placeholder="$t('dialog.invite_to_group.choose_group_placeholder')"
:placeholder="t('dialog.invite_to_group.choose_group_placeholder')"
filterable
:disabled="inviteGroupDialog.loading"
style="margin-top: 15px; width: 100%">
<el-option-group
:label="$t('dialog.invite_to_group.groups_with_invite_permission')"
:label="t('dialog.invite_to_group.groups_with_invite_permission')"
style="width: 410px">
<el-option
v-for="group in groupsWithInvitePermission"
@@ -26,7 +26,7 @@
style="height: auto"
class="x-friend-item">
<div class="avatar">
<img v-lazy="group.iconUrl" />
<img :src="group.iconUrl" loading="lazy" />
</div>
<div class="detail">
<span class="name" v-text="group.name"></span>
@@ -38,11 +38,11 @@
v-model="inviteGroupDialog.userIds"
multiple
clearable
:placeholder="$t('dialog.invite_to_group.choose_friends_placeholder')"
:placeholder="t('dialog.invite_to_group.choose_friends_placeholder')"
filterable
:disabled="inviteGroupDialog.loading"
style="width: 100%; margin-top: 15px">
<el-option-group v-if="inviteGroupDialog.userId" :label="$t('dialog.invite_to_group.selected_users')">
<el-option-group v-if="inviteGroupDialog.userId" :label="t('dialog.invite_to_group.selected_users')">
<el-option
:key="inviteGroupDialog.userObject.id"
:label="inviteGroupDialog.userObject.displayName"
@@ -50,7 +50,7 @@
class="x-friend-item">
<template v-if="inviteGroupDialog.userObject.id">
<div class="avatar" :class="userStatusClass(inviteGroupDialog.userObject)">
<img v-lazy="userImage(inviteGroupDialog.userObject)" />
<img :src="userImage(inviteGroupDialog.userObject)" loading="lazy" />
</div>
<div class="detail">
<span
@@ -62,7 +62,7 @@
<span v-else v-text="inviteGroupDialog.userId"></span>
</el-option>
</el-option-group>
<el-option-group v-if="vipFriends.length" :label="$t('side_panel.favorite')">
<el-option-group v-if="vipFriends.length" :label="t('side_panel.favorite')">
<el-option
v-for="friend in vipFriends"
:key="friend.id"
@@ -72,7 +72,7 @@
class="x-friend-item">
<template v-if="friend.ref">
<div class="avatar" :class="userStatusClass(friend.ref)">
<img v-lazy="userImage(friend.ref)" />
<img :src="userImage(friend.ref)" loading="lazy" />
</div>
<div class="detail">
<span
@@ -84,7 +84,7 @@
<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-group v-if="onlineFriends.length" :label="t('side_panel.online')">
<el-option
v-for="friend in onlineFriends"
:key="friend.id"
@@ -94,7 +94,7 @@
class="x-friend-item">
<template v-if="friend.ref">
<div class="avatar" :class="userStatusClass(friend.ref)">
<img v-lazy="userImage(friend.ref)" />
<img :src="userImage(friend.ref)" loading="lazy" />
</div>
<div class="detail">
<span
@@ -106,7 +106,7 @@
<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-group v-if="activeFriends.length" :label="t('side_panel.active')">
<el-option
v-for="friend in activeFriends"
:key="friend.id"
@@ -116,7 +116,7 @@
class="x-friend-item">
<template v-if="friend.ref">
<div class="avatar">
<img v-lazy="userImage(friend.ref)" />
<img :src="userImage(friend.ref)" loading="lazy" />
</div>
<div class="detail">
<span
@@ -128,7 +128,7 @@
<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-group v-if="offlineFriends.length" :label="t('side_panel.offline')">
<el-option
v-for="friend in offlineFriends"
:key="friend.id"
@@ -138,7 +138,7 @@
class="x-friend-item">
<template v-if="friend.ref">
<div class="avatar">
<img v-lazy="userImage(friend.ref)" />
<img :src="userImage(friend.ref)" loading="lazy" />
</div>
<div class="detail">
<span
@@ -158,24 +158,25 @@
size="small"
:disabled="inviteGroupDialog.loading || !inviteGroupDialog.userIds.length || !inviteGroupDialog.groupId"
@click="sendGroupInvite">
{{ $t('dialog.invite_to_group.invite') }}
{{ t('dialog.invite_to_group.invite') }}
</el-button>
</template>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ref, watch, getCurrentInstance, nextTick, computed } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { ref, watch, nextTick, computed } from 'vue';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { groupRequest, userRequest } from '../../api';
import { adjustDialogZ, hasGroupPermission, userImage, userStatusClass } from '../../shared/utils';
import { getNextDialogIndex, hasGroupPermission, userImage, userStatusClass } from '../../shared/utils';
import { useFriendStore, useGroupStore } from '../../stores';
const { vipFriends, onlineFriends, activeFriends, offlineFriends } = storeToRefs(useFriendStore());
const { currentUserGroups, inviteGroupDialog } = storeToRefs(useGroupStore());
const { applyGroup } = useGroupStore();
const { proxy } = getCurrentInstance();
const { t } = useI18n();
watch(
() => {
@@ -188,7 +189,7 @@
}
);
const inviteGroupDialogRef = ref(null);
const inviteGroupDialogIndex = ref(2000);
const groupsWithInvitePermission = computed(() => {
return Array.from(currentUserGroups.value.values()).filter((group) =>
@@ -197,7 +198,9 @@
});
function initDialog() {
nextTick(() => adjustDialogZ(inviteGroupDialogRef.value.$el));
nextTick(() => {
inviteGroupDialogIndex.value = getNextDialogIndex();
});
const D = inviteGroupDialog.value;
if (D.groupId) {
groupRequest
@@ -236,7 +239,7 @@
}
// not allowed to invite
inviteGroupDialog.value.groupId = '';
proxy.$message({
ElMessage({
type: 'error',
message: 'You are not allowed to invite to this group'
});
@@ -247,11 +250,12 @@
});
}
function sendGroupInvite() {
proxy.$confirm('Continue? Invite User(s) To Group', 'Confirm', {
ElMessageBox.confirm('Continue? Invite User(s) To Group', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
type: 'info'
})
.then((action) => {
const D = inviteGroupDialog.value;
if (action !== 'confirm' || D.loading === true) {
return;
@@ -274,7 +278,7 @@
});
};
inviteLoop();
}
});
})
.catch(() => {});
}
</script>
+56 -45
View File
@@ -1,38 +1,38 @@
<template>
<safe-dialog ref="launchDialogRef" :visible.sync="isVisible" :title="t('dialog.launch.header')" width="450px">
<el-dialog :z-index="launchDialogIndex" v-model="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">
size="small"
style="width: 230px"
@click="$event.target.tagName === 'INPUT' && $event.target.select()" />
<el-tooltip placement="right" :content="t('dialog.launch.copy_tooltip')">
<el-button
size="mini"
icon="el-icon-s-order"
size="small"
:icon="CopyDocument"
style="margin-left: 5px"
circle
@click="copyInstanceMessage(launchDialog.url)" />
</el-tooltip>
</el-form-item>
<el-form-item v-if="launchDialog.shortUrl">
<template slot="label">
<template #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 placement="top" :content="t('dialog.launch.short_url_notice')">
<el-icon style="display: inline-block; margin-left: 5px"><Warning /></el-icon>
</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">
size="small"
style="width: 230px"
@click="$event.target.tagName === 'INPUT' && $event.target.select()" />
<el-tooltip placement="right" :content="t('dialog.launch.copy_tooltip')">
<el-button
size="mini"
icon="el-icon-s-order"
style="margin-left: 5px"
size="small"
:icon="CopyDocument"
style="display: inline-block; margin-left: 5px"
circle
@click="copyInstanceMessage(launchDialog.shortUrl)" />
</el-tooltip>
@@ -40,23 +40,26 @@
<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">
size="small"
style="width: 230px"
@click="$event.target.tagName === 'INPUT' && $event.target.select()" />
<el-tooltip placement="right" :content="t('dialog.launch.copy_tooltip')">
<el-button
size="mini"
icon="el-icon-s-order"
style="margin-left: 5px"
size="small"
:icon="CopyDocument"
style="display: inline-block; 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">
<el-checkbox
v-model="launchDialog.desktop"
style="display: inline-flex; align-items: center; margin-top: 5px"
@change="saveLaunchDialog">
{{ t('dialog.launch.start_as_desktop') }}
</el-checkbox>
<template slot="footer">
<template #footer>
<el-button size="small" @click="showPreviousInstancesInfoDialog(launchDialog.location)">
{{ t('dialog.launch.info') }}
</el-button>
@@ -68,7 +71,6 @@
</el-button>
<template v-if="canOpenInstanceInGame()">
<el-button
type="default"
size="small"
:disabled="!launchDialog.secureOrShortName"
@click="handleLaunchGame(launchDialog.location, launchDialog.shortName, launchDialog.desktop)">
@@ -84,7 +86,6 @@
</template>
<template v-else>
<el-button
type="default"
size="small"
:disabled="!launchDialog.secureOrShortName"
@click="selfInvite(launchDialog.location, launchDialog.shortName)">
@@ -100,18 +101,26 @@
</template>
</template>
<InviteDialog :invite-dialog="inviteDialog" @closeInviteDialog="closeInviteDialog" />
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ref, computed, nextTick, watch, getCurrentInstance } from 'vue';
import { CopyDocument, Warning } from '@element-plus/icons-vue';
import { ref, computed, nextTick, watch } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { instanceRequest, worldRequest } from '../../api';
import configRepository from '../../service/config';
import { adjustDialogZ, checkCanInvite, getLaunchURL, isRealInstance, parseLocation } from '../../shared/utils';
import {
useAppearanceSettingsStore,
getNextDialogIndex,
checkCanInvite,
getLaunchURL,
isRealInstance,
parseLocation
} from '../../shared/utils';
import {
useFriendStore,
useInviteStore,
useInstanceStore,
@@ -121,19 +130,18 @@
} from '../../stores';
import InviteDialog from './InviteDialog/InviteDialog.vue';
const { proxy } = getCurrentInstance();
const { t } = useI18n();
const { friends } = storeToRefs(useFriendStore());
const { hideTooltips } = storeToRefs(useAppearanceSettingsStore());
const { lastLocation } = storeToRefs(useLocationStore());
const { launchGame, tryOpenInstanceInVrc } = useLaunchStore();
const { launchDialogData } = storeToRefs(useLaunchStore());
const { showPreviousInstancesInfoDialog } = useInstanceStore();
const { canOpenInstanceInGame } = useInviteStore();
const { isGameRunning } = storeToRefs(useGameStore());
const launchDialogRef = ref(null);
const launchDialogIndex = ref(2000);
const launchDialog = ref({
loading: false,
@@ -207,17 +215,18 @@
}
function handleLaunchGame(location, shortName, desktop) {
if (isGameRunning.value) {
proxy.$confirm(t('dialog.launch.game_running_warning'), t('dialog.launch.header'), {
ElMessageBox.confirm(t('dialog.launch.game_running_warning'), t('dialog.launch.header'), {
confirmButtonText: t('dialog.launch.confirm_yes'),
cancelButtonText: t('dialog.launch.confirm_no'),
type: 'warning',
callback: (action) => {
type: 'warning'
})
.then((action) => {
if (action === 'confirm') {
launchGame(location, shortName, desktop);
isVisible.value = false;
}
}
});
})
.catch(() => {});
return;
}
launchGame(location, shortName, desktop);
@@ -239,7 +248,7 @@
shortName
})
.then((args) => {
proxy.$message({
ElMessage({
message: 'Self invite sent',
type: 'success'
});
@@ -257,7 +266,9 @@
if (!isRealInstance(tag)) {
return;
}
nextTick(() => adjustDialogZ(launchDialogRef.value.$el));
nextTick(() => {
launchDialogIndex.value = getNextDialogIndex();
});
const D = launchDialog.value;
D.tag = tag;
D.secureOrShortName = shortName;
@@ -300,12 +311,12 @@
async function copyInstanceMessage(input) {
try {
await navigator.clipboard.writeText(input);
proxy.$message({
ElMessage({
message: 'Instance copied to clipboard',
type: 'success'
});
} catch (error) {
proxy.$message({
ElMessage({
message: 'Instance copied failed',
type: 'error'
});
+19 -23
View File
@@ -1,14 +1,14 @@
<template>
<safe-dialog
ref="moderateGroupDialogRef"
:visible.sync="moderateGroupDialog.visible"
:title="$t('dialog.moderate_group.header')"
<el-dialog
:z-index="moderateGroupDialogIndex"
v-model="moderateGroupDialog.visible"
:title="t('dialog.moderate_group.header')"
width="450px"
append-to-body>
<div v-if="moderateGroupDialog.visible">
<div class="x-friend-item" style="cursor: default">
<div class="avatar">
<img v-lazy="userImage(moderateGroupDialog.userObject)" />
<img :src="userImage(moderateGroupDialog.userObject)" loading="lazy" />
</div>
<div class="detail">
<span
@@ -22,12 +22,12 @@
<el-select
v-model="moderateGroupDialog.groupId"
clearable
:placeholder="$t('dialog.moderate_group.choose_group_placeholder')"
:placeholder="t('dialog.moderate_group.choose_group_placeholder')"
filterable
style="margin-top: 15px; width: 100%">
<el-option-group
v-if="currentUserGroups.size"
:label="$t('dialog.moderate_group.groups_with_moderation_permission')">
:label="t('dialog.moderate_group.groups_with_moderation_permission')">
<el-option
v-for="group in groupsWithModerationPermission"
:key="group.id"
@@ -36,7 +36,7 @@
style="height: auto"
class="x-friend-item">
<div class="avatar">
<img v-lazy="group.iconUrl" />
<img :src="group.iconUrl" loading="lazy" />
</div>
<div class="detail">
<span class="name" v-text="group.name"></span>
@@ -54,29 +54,23 @@
showGroupMemberModerationDialog(moderateGroupDialog.groupId, moderateGroupDialog.userId);
moderateGroupDialog.visible = false;
">
{{ $t('dialog.moderate_group.moderation_tools') }}
{{ t('dialog.moderate_group.moderation_tools') }}
</el-button>
</template>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ref, watch, getCurrentInstance, nextTick, computed } from 'vue';
import { ref, watch, nextTick, computed } from 'vue';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { groupRequest, userRequest } from '../../api';
import {
adjustDialogZ,
hasGroupPermission,
hasGroupModerationPermission,
userImage,
userStatusClass
} from '../../shared/utils';
import { getNextDialogIndex, hasGroupModerationPermission, userImage } from '../../shared/utils';
import { useGroupStore } from '../../stores';
const { currentUserGroups, moderateGroupDialog } = storeToRefs(useGroupStore());
const { applyGroup, showGroupMemberModerationDialog } = useGroupStore();
const { proxy } = getCurrentInstance();
const { showGroupMemberModerationDialog } = useGroupStore();
const { t } = useI18n();
const groupsWithModerationPermission = computed(() => {
return Array.from(currentUserGroups.value.values()).filter((group) => hasGroupModerationPermission(group));
@@ -93,10 +87,12 @@
}
);
const moderateGroupDialogRef = ref(null);
const moderateGroupDialogIndex = ref(2000);
function initDialog() {
nextTick(() => adjustDialogZ(moderateGroupDialogRef.value.$el));
nextTick(() => {
moderateGroupDialogIndex.value = getNextDialogIndex();
});
const D = moderateGroupDialog.value;
if (D.groupId) {
groupRequest
+81 -52
View File
@@ -1,15 +1,15 @@
<template>
<safe-dialog
ref="newInstanceDialogRef"
:visible.sync="newInstanceDialog.visible"
<el-dialog
:z-index="newInstanceDialogIndex"
v-model="newInstanceDialog.visible"
:title="t('dialog.new_instance.header')"
width="650px"
append-to-body>
<el-tabs v-model="newInstanceDialog.selectedTab" type="card" @tab-click="newInstanceTabClick">
<el-tab-pane :label="t('dialog.new_instance.normal')">
<el-tab-pane name="Normal" :label="t('dialog.new_instance.normal')">
<el-form :model="newInstanceDialog" label-width="150px">
<el-form-item :label="t('dialog.new_instance.access_type')">
<el-radio-group v-model="newInstanceDialog.accessType" size="mini" @change="buildInstance">
<el-radio-group v-model="newInstanceDialog.accessType" size="small" @change="buildInstance">
<el-radio-button label="public">{{
t('dialog.new_instance.access_type_public')
}}</el-radio-button>
@@ -33,7 +33,10 @@
<el-form-item
v-if="newInstanceDialog.accessType === 'group'"
:label="t('dialog.new_instance.group_access_type')">
<el-radio-group v-model="newInstanceDialog.groupAccessType" size="mini" @change="buildInstance">
<el-radio-group
v-model="newInstanceDialog.groupAccessType"
size="small"
@change="buildInstance">
<el-radio-button
label="members"
:disabled="
@@ -59,7 +62,7 @@
</el-radio-group>
</el-form-item>
<el-form-item :label="t('dialog.new_instance.region')">
<el-radio-group v-model="newInstanceDialog.region" size="mini" @change="buildInstance">
<el-radio-group v-model="newInstanceDialog.region" size="small" @change="buildInstance">
<el-radio-button label="US West">{{ t('dialog.new_instance.region_usw') }}</el-radio-button>
<el-radio-button label="US East">{{ t('dialog.new_instance.region_use') }}</el-radio-button>
<el-radio-button label="Europe">{{ t('dialog.new_instance.region_eu') }}</el-radio-button>
@@ -84,8 +87,16 @@
<el-form-item :label="t('dialog.new_instance.world_id')">
<el-input
v-model="newInstanceDialog.worldId"
size="mini"
@click.native="$event.target.tagName === 'INPUT' && $event.target.select()"
size="small"
@click="$event.target.tagName === 'INPUT' && $event.target.select()"
@change="buildInstance"></el-input>
</el-form-item>
<el-form-item :label="t('dialog.new_instance.display_name')">
<el-input
:disabled="!isLocalUserVrcplusSupporter"
v-model="newInstanceDialog.displayName"
size="small"
@click="$event.target.tagName === 'INPUT' && $event.target.select()"
@change="buildInstance"></el-input>
</el-form-item>
<el-form-item
@@ -114,7 +125,7 @@
hasGroupPermission(group, 'group-instance-open-create'))
">
<div class="avatar">
<img v-lazy="group.iconUrl" />
<img :src="group.iconUrl" loading="lazy" />
</div>
<div class="detail">
<span class="name" v-text="group.name"></span>
@@ -155,22 +166,22 @@
<el-form-item :label="t('dialog.new_instance.location')">
<el-input
v-model="newInstanceDialog.location"
size="mini"
size="small"
readonly
@click.native="$event.target.tagName === 'INPUT' && $event.target.select()"></el-input>
@click="$event.target.tagName === 'INPUT' && $event.target.select()"></el-input>
</el-form-item>
<el-form-item :label="t('dialog.new_instance.url')">
<el-input v-model="newInstanceDialog.url" size="mini" readonly></el-input>
<el-input v-model="newInstanceDialog.url" size="small" readonly></el-input>
</el-form-item>
</template>
</el-form>
</el-tab-pane>
<el-tab-pane :label="t('dialog.new_instance.legacy')">
<el-tab-pane name="Legacy" :label="t('dialog.new_instance.legacy')">
<el-form :model="newInstanceDialog" label-width="150px">
<el-form-item :label="t('dialog.new_instance.access_type')">
<el-radio-group
v-model="newInstanceDialog.accessType"
size="mini"
size="small"
@change="buildLegacyInstance">
<el-radio-button label="public">{{
t('dialog.new_instance.access_type_public')
@@ -197,7 +208,7 @@
:label="t('dialog.new_instance.group_access_type')">
<el-radio-group
v-model="newInstanceDialog.groupAccessType"
size="mini"
size="small"
@change="buildLegacyInstance">
<el-radio-button label="members">{{
t('dialog.new_instance.group_access_type_members')
@@ -211,7 +222,7 @@
</el-radio-group>
</el-form-item>
<el-form-item :label="t('dialog.new_instance.region')">
<el-radio-group v-model="newInstanceDialog.region" size="mini" @change="buildLegacyInstance">
<el-radio-group v-model="newInstanceDialog.region" size="small" @change="buildLegacyInstance">
<el-radio-button label="US West">{{ t('dialog.new_instance.region_usw') }}</el-radio-button>
<el-radio-button label="US East">{{ t('dialog.new_instance.region_use') }}</el-radio-button>
<el-radio-button label="Europe">{{ t('dialog.new_instance.region_eu') }}</el-radio-button>
@@ -226,15 +237,15 @@
<el-form-item :label="t('dialog.new_instance.world_id')">
<el-input
v-model="newInstanceDialog.worldId"
size="mini"
@click.native="$event.target.tagName === 'INPUT' && $event.target.select()"
size="small"
@click="$event.target.tagName === 'INPUT' && $event.target.select()"
@change="buildLegacyInstance"></el-input>
</el-form-item>
<el-form-item :label="t('dialog.new_instance.instance_id')">
<el-input
v-model="newInstanceDialog.instanceName"
:placeholder="t('dialog.new_instance.instance_id_placeholder')"
size="mini"
size="small"
@change="buildLegacyInstance"></el-input>
</el-form-item>
<el-form-item
@@ -254,7 +265,7 @@
:value="currentUser.id"
style="height: auto">
<div class="avatar" :class="userStatusClass(currentUser)">
<img v-lazy="userImage(currentUser)" />
<img :src="userImage(currentUser)" loading="lazy" />
</div>
<div class="detail">
<span class="name" v-text="currentUser.displayName"></span>
@@ -271,7 +282,7 @@
style="height: auto">
<template v-if="friend.ref">
<div class="avatar" :class="userStatusClass(friend.ref)">
<img v-lazy="userImage(friend.ref)" />
<img :src="userImage(friend.ref)" loading="lazy" />
</div>
<div class="detail">
<span
@@ -293,7 +304,7 @@
style="height: auto">
<template v-if="friend.ref">
<div class="avatar" :class="userStatusClass(friend.ref)">
<img v-lazy="userImage(friend.ref)" />
<img :src="userImage(friend.ref)" loading="lazy" />
</div>
<div class="detail">
<span
@@ -315,7 +326,7 @@
style="height: auto">
<template v-if="friend.ref">
<div class="avatar">
<img v-lazy="userImage(friend.ref)" />
<img :src="userImage(friend.ref)" loading="lazy" />
</div>
<div class="detail">
<span
@@ -337,7 +348,7 @@
style="height: auto">
<template v-if="friend.ref">
<div class="avatar">
<img v-lazy="userImage(friend.ref)" />
<img :src="userImage(friend.ref)" loading="lazy" />
</div>
<div class="detail">
<span
@@ -371,7 +382,7 @@
style="height: auto; width: 478px">
<template v-if="group">
<div class="avatar">
<img v-lazy="group.iconUrl" />
<img :src="group.iconUrl" loading="lazy" />
</div>
<div class="detail">
<span class="name" v-text="group.name"></span></div
@@ -383,17 +394,17 @@
<el-form-item :label="t('dialog.new_instance.location')">
<el-input
v-model="newInstanceDialog.location"
size="mini"
size="small"
readonly
@click.native="$event.target.tagName === 'INPUT' && $event.target.select()"></el-input>
@click="$event.target.tagName === 'INPUT' && $event.target.select()"></el-input>
</el-form-item>
<el-form-item :label="t('dialog.new_instance.url')">
<el-input v-model="newInstanceDialog.url" size="mini" readonly></el-input>
<el-input v-model="newInstanceDialog.url" size="small" readonly></el-input>
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
<template v-if="newInstanceDialog.selectedTab === '0'" #footer>
<template v-if="newInstanceDialog.selectedTab === 'Normal'" #footer>
<template v-if="newInstanceDialog.instanceCreated">
<el-button size="small" @click="copyInstanceUrl(newInstanceDialog.location)">{{
t('dialog.new_instance.copy_url')
@@ -412,13 +423,11 @@
>
<template v-if="canOpenInstanceInGame()">
<el-button
type="default"
size="small"
@click="showLaunchDialog(newInstanceDialog.location, newInstanceDialog.shortName)"
>{{ t('dialog.new_instance.launch') }}</el-button
>
<el-button
type="primary"
size="small"
@click="handleAttachGame(newInstanceDialog.location, newInstanceDialog.shortName)">
{{ t('dialog.new_instance.open_ingame') }}
@@ -439,7 +448,7 @@
}}</el-button>
</template>
</template>
<template v-else-if="newInstanceDialog.selectedTab === '1'" #footer>
<template v-else-if="newInstanceDialog.selectedTab === 'Legacy'" #footer>
<el-button size="small" @click="copyInstanceUrl(newInstanceDialog.location)">{{
t('dialog.new_instance.copy_url')
}}</el-button>
@@ -457,7 +466,6 @@
>
<template v-if="canOpenInstanceInGame()">
<el-button
type="default"
size="small"
@click="showLaunchDialog(newInstanceDialog.location, newInstanceDialog.shortName)"
>{{ t('dialog.new_instance.launch') }}</el-button
@@ -479,17 +487,18 @@
</template>
</template>
<InviteDialog :invite-dialog="inviteDialog" @closeInviteDialog="closeInviteDialog" />
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ref, watch, nextTick, getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { ref, watch, nextTick, computed } from 'vue';
import { ElMessage } from 'element-plus';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { groupRequest, instanceRequest, worldRequest } from '../../api';
import configRepository from '../../service/config';
import {
adjustDialogZ,
getNextDialogIndex,
copyToClipboard,
getLaunchURL,
hasGroupPermission,
@@ -518,11 +527,9 @@
const { t } = useI18n();
const { proxy } = getCurrentInstance();
const { friends, vipFriends, onlineFriends, activeFriends, offlineFriends } = storeToRefs(useFriendStore());
const { currentUserGroups, cachedGroups } = storeToRefs(useGroupStore());
const { handleGroupPermissions } = useGroupStore();
const { currentUserGroups } = storeToRefs(useGroupStore());
const { cachedGroups, handleGroupPermissions } = useGroupStore();
const { lastLocation } = storeToRefs(useLocationStore());
const { showLaunchDialog } = useLaunchStore();
const { createNewInstance } = useInstanceStore();
@@ -530,12 +537,12 @@
const { tryOpenInstanceInVrc } = useLaunchStore();
const { canOpenInstanceInGame } = useInviteStore();
const newInstanceDialogRef = ref(null);
const newInstanceDialogIndex = ref(2000);
const newInstanceDialog = ref({
visible: false,
// loading: false,
selectedTab: '0',
selectedTab: 'Normal',
instanceCreated: false,
queueEnabled: false,
worldId: '',
@@ -551,6 +558,7 @@
strict: false,
location: '',
shortName: '',
displayName: '',
url: '',
secureOrShortName: '',
lastSelectedGroupId: '',
@@ -575,6 +583,8 @@
}
);
const isLocalUserVrcplusSupporter = computed(() => currentUser.value.$isVRCPlus);
initializeNewInstanceDialog();
function closeInviteDialog() {
@@ -617,7 +627,9 @@
if (!isRealInstance(tag)) {
return;
}
nextTick(() => adjustDialogZ(newInstanceDialogRef.value.$el));
nextTick(() => {
newInstanceDialogIndex.value = getNextDialogIndex();
});
const D = newInstanceDialog.value;
const L = parseLocation(tag);
if (D.worldId === L.worldId) {
@@ -634,6 +646,9 @@
D.strict = false;
D.shortName = '';
D.secureOrShortName = '';
if (!isLocalUserVrcplusSupporter.value) {
D.displayName = '';
}
const args = await groupRequest.getGroupPermissions({ userId: currentUser.value.id });
handleGroupPermissions(args);
buildInstance();
@@ -673,10 +688,23 @@
configRepository
.getBool('instanceDialogAgeGate', false)
.then((value) => (newInstanceDialog.value.ageGate = value));
configRepository
.getString('instanceDialogDisplayName', '')
.then((value) => (newInstanceDialog.value.displayName = value));
}
function saveNewInstanceDialog() {
const { accessType, region, instanceName, userId, groupId, groupAccessType, queueEnabled, ageGate } =
newInstanceDialog.value;
const {
accessType,
region,
instanceName,
userId,
groupId,
groupAccessType,
queueEnabled,
ageGate,
displayName
} = newInstanceDialog.value;
configRepository.setString('instanceDialogAccessType', accessType);
configRepository.setString('instanceRegion', region);
@@ -686,9 +714,10 @@
configRepository.setString('instanceDialogGroupAccessType', groupAccessType);
configRepository.setBool('instanceDialogQueueEnabled', queueEnabled);
configRepository.setBool('instanceDialogAgeGate', ageGate);
configRepository.setString('instanceDialogDisplayName', displayName);
}
function newInstanceTabClick(tab) {
if (tab === '1') {
function newInstanceTabClick(obj) {
if (obj.props.name === 'Normal') {
buildInstance();
} else {
buildLegacyInstance();
@@ -720,7 +749,7 @@
worldId: L.worldId
})
.then((args) => {
proxy.$message({
ElMessage({
message: 'Self invite sent',
type: 'success'
});
@@ -749,7 +778,7 @@
}
if (D.groupId && D.groupId !== D.lastSelectedGroupId) {
D.roleIds = [];
const ref = cachedGroups.value.get(D.groupId);
const ref = cachedGroups.get(D.groupId);
if (typeof ref !== 'undefined') {
D.groupRef = ref;
D.selectedGroupRoles = ref.roles;
@@ -824,7 +853,7 @@
}
if (D.groupId && D.groupId !== D.lastSelectedGroupId) {
D.roleIds = [];
const ref = cachedGroups.value.get(D.groupId);
const ref = cachedGroups.get(D.groupId);
if (typeof ref !== 'undefined') {
D.groupRef = ref;
D.selectedGroupRoles = ref.roles;
@@ -1,42 +0,0 @@
<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" :key="image.version" style="display: inline-block">
<el-popover
class="x-change-image-item"
placement="right"
width="500px"
trigger="click"
v-if="image.file">
<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 { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n-bridge';
import { useGalleryStore } from '../../stores';
const { t } = useI18n();
const { previousImagesDialogVisible, previousImagesTable } = storeToRefs(useGalleryStore());
const { showFullscreenImageDialog } = useGalleryStore();
function closeDialog() {
previousImagesDialogVisible.value = false;
}
</script>
@@ -1,7 +1,7 @@
<template>
<safe-dialog
ref="previousInstancesGroupDialogRef"
:visible.sync="isVisible"
<el-dialog
:z-index="previousInstancesGroupDialogIndex"
v-model="isVisible"
:title="t('dialog.previous_instances.header')"
width="1000px"
append-to-body>
@@ -13,7 +13,7 @@
style="width: 150px" />
</div>
<data-tables v-loading="loading" v-bind="previousInstancesGroupDialogTable" style="margin-top: 10px">
<DataTable v-loading="loading" v-bind="previousInstancesGroupDialogTable" style="margin-top: 10px">
<el-table-column :label="t('table.previous_instances.date')" prop="created_at" sortable width="170">
<template #default="scope">
<span>{{ formatDateFilter(scope.row.created_at, 'long') }}</span>
@@ -39,48 +39,50 @@
<template #default="scope">
<el-button
type="text"
icon="el-icon-s-data"
size="mini"
:icon="DataLine"
size="small"
@click="showPreviousInstancesInfoDialog(scope.row.location)" />
<el-button
v-if="shiftHeld"
style="color: #f56c6c"
type="text"
icon="el-icon-close"
size="mini"
:icon="Close"
size="small"
@click="deleteGameLogGroupInstance(scope.row)" />
<el-button
v-else
type="text"
icon="el-icon-close"
size="mini"
:icon="Close"
size="small"
@click="deleteGameLogGroupInstancePrompt(scope.row)" />
</template>
</el-table-column>
</data-tables>
</safe-dialog>
</DataTable>
</el-dialog>
</template>
<script setup>
import { DataLine, Close } from '@element-plus/icons-vue';
import { ElMessageBox } from 'element-plus';
import { ref, reactive, computed, watch, nextTick, getCurrentInstance } from 'vue';
import {
parseLocation,
compareByCreatedAt,
timeToText,
removeFromArray,
adjustDialogZ,
getNextDialogIndex,
formatDateFilter
} from '../../../shared/utils';
import { database } from '../../../service/database';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { useInstanceStore, useUiStore } from '../../../stores';
const { proxy } = getCurrentInstance();
const { showPreviousInstancesInfoDialog } = useInstanceStore();
const { shiftHeld } = useUiStore();
const { t } = useI18n();
const previousInstancesGroupDialogRef = ref(null);
const previousInstancesGroupDialogIndex = ref(2000);
const loading = ref(false);
const previousInstancesGroupDialogTable = reactive({
@@ -88,7 +90,7 @@
filters: [{ prop: 'groupName', value: '' }],
tableProps: {
stripe: true,
size: 'mini',
size: 'small',
defaultSort: { prop: 'created_at', order: 'descending' }
},
pageSize: 10,
@@ -115,7 +117,7 @@
() => {
if (props.previousInstancesGroupDialog.visible) {
nextTick(() => {
adjustDialogZ(previousInstancesGroupDialogRef.value.$el);
previousInstancesGroupDialogIndex.value = getNextDialogIndex();
});
refreshPreviousInstancesGroupTable();
}
@@ -144,15 +146,16 @@
}
function deleteGameLogGroupInstancePrompt(row) {
proxy.$confirm('Continue? Delete GameLog Instance', 'Confirm', {
ElMessageBox.confirm('Continue? Delete GameLog Instance', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
type: 'info'
})
.then((action) => {
if (action === 'confirm') {
deleteGameLogGroupInstance(row);
}
}
});
})
.catch(() => {});
}
</script>
@@ -1,8 +1,8 @@
<template>
<safe-dialog
ref="dialogRef"
:visible="previousInstancesInfoDialogVisible"
:title="$t('dialog.previous_instances.info')"
<el-dialog
:z-index="previousInstancesInfoDialogIndex"
:model-value="previousInstancesInfoDialogVisible"
:title="t('dialog.previous_instances.info')"
width="800px"
:fullscreen="fullscreen"
destroy-on-close
@@ -11,12 +11,12 @@
<Location :location="location.tag" style="font-size: 14px" />
<el-input
v-model="dataTable.filters[0].value"
:placeholder="$t('dialog.previous_instances.search_placeholder')"
:placeholder="t('dialog.previous_instances.search_placeholder')"
style="width: 150px"
clearable></el-input>
</div>
<data-tables v-loading="loading" v-bind="dataTable" style="margin-top: 10px">
<el-table-column :label="$t('table.previous_instances.date')" prop="created_at" sortable width="110">
<DataTable v-loading="loading" v-bind="dataTable" style="margin-top: 10px">
<el-table-column :label="t('table.previous_instances.date')" prop="created_at" sortable width="110">
<template #default="scope">
<el-tooltip placement="left">
<template #content>
@@ -26,7 +26,7 @@
</el-tooltip>
</template>
</el-table-column>
<el-table-column :label="$t('table.gameLog.icon')" prop="isFriend" width="70" align="center">
<el-table-column :label="t('table.gameLog.icon')" prop="isFriend" width="70" align="center">
<template #default="scope">
<template v-if="gameLogIsFriend(scope.row)">
<el-tooltip v-if="gameLogIsFavorite(scope.row)" placement="top" content="Favorite">
@@ -36,33 +36,35 @@
<span>💚</span>
</el-tooltip>
</template>
<span v-else></span>
</template>
</el-table-column>
<el-table-column :label="$t('table.previous_instances.display_name')" prop="displayName" sortable>
<el-table-column :label="t('table.previous_instances.display_name')" prop="displayName" sortable>
<template #default="scope">
<span class="x-link" @click="lookupUser(scope.row)">{{ scope.row.displayName }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('table.previous_instances.time')" prop="time" width="100" sortable>
<el-table-column :label="t('table.previous_instances.time')" prop="time" width="100" sortable>
<template #default="scope">
<span>{{ scope.row.timer }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('table.previous_instances.count')" prop="count" width="100" sortable>
<el-table-column :label="t('table.previous_instances.count')" prop="count" width="100" sortable>
<template #default="scope">
<span>{{ scope.row.count }}</span>
</template>
</el-table-column>
</data-tables>
</safe-dialog>
</DataTable>
</el-dialog>
</template>
<script setup>
import { ref, watch, nextTick } from 'vue';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { database } from '../../../service/database';
import {
adjustDialogZ,
getNextDialogIndex,
compareByCreatedAt,
parseLocation,
timeToText,
@@ -74,8 +76,9 @@
const { previousInstancesInfoDialogVisible, previousInstancesInfoDialogInstanceId } =
storeToRefs(useInstanceStore());
const { gameLogIsFriend, gameLogIsFavorite } = useGameLogStore();
const { t } = useI18n();
const dialogRef = ref(null);
const previousInstancesInfoDialogIndex = ref(2000);
const loading = ref(false);
const location = ref({
@@ -111,7 +114,7 @@
],
tableProps: {
stripe: true,
size: 'mini',
size: 'small',
defaultSort: {
prop: 'created_at',
order: 'descending'
@@ -139,7 +142,7 @@
);
function init() {
adjustDialogZ(dialogRef.value.$el);
previousInstancesInfoDialogIndex.value = getNextDialogIndex();
loading.value = true;
location.value = parseLocation(previousInstancesInfoDialogInstanceId.value);
}
@@ -1,7 +1,7 @@
<template>
<safe-dialog
ref="previousInstancesWorldDialogRef"
:visible.sync="isVisible"
<el-dialog
:z-index="previousInstancesWorldDialogIndex"
v-model="isVisible"
:title="t('dialog.previous_instances.header')"
width="1000px"
append-to-body>
@@ -12,7 +12,7 @@
:placeholder="t('dialog.previous_instances.search_placeholder')"
style="display: block; width: 150px"></el-input>
</div>
<data-tables v-loading="loading" v-bind="previousInstancesWorldDialogTable" style="margin-top: 10px">
<DataTable v-loading="loading" v-bind="previousInstancesWorldDialogTable" style="margin-top: 10px">
<el-table-column :label="t('table.previous_instances.date')" prop="created_at" sortable width="170">
<template #default="scope">
<span>{{ formatDateFilter(scope.row.created_at, 'long') }}</span>
@@ -43,35 +43,41 @@
<template #default="scope">
<el-button
type="text"
icon="el-icon-s-data"
size="mini"
:icon="DataLine"
size="small"
class="button-pd-0"
@click="showPreviousInstancesInfoDialog(scope.row.location)"></el-button>
<el-button
v-if="shiftHeld"
style="color: #f56c6c"
type="text"
icon="el-icon-close"
size="mini"
:icon="Close"
size="small"
class="button-pd-0"
@click="deleteGameLogWorldInstance(scope.row)"></el-button>
<el-button
v-else
type="text"
icon="el-icon-close"
size="mini"
:icon="Close"
size="small"
class="button-pd-0"
@click="deleteGameLogWorldInstancePrompt(scope.row)"></el-button>
</template>
</el-table-column>
</data-tables>
</safe-dialog>
</DataTable>
</el-dialog>
</template>
<script setup>
import { DataLine, Close } from '@element-plus/icons-vue';
import { ElMessageBox } from 'element-plus';
import { storeToRefs } from 'pinia';
import { computed, getCurrentInstance, nextTick, reactive, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { computed, nextTick, reactive, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { database } from '../../../service/database';
import {
adjustDialogZ,
getNextDialogIndex,
compareByCreatedAt,
parseLocation,
removeFromArray,
@@ -80,7 +86,6 @@
} from '../../../shared/utils';
import { useInstanceStore, useUiStore, useUserStore } from '../../../stores';
const { proxy } = getCurrentInstance();
const { t } = useI18n();
const props = defineProps({
@@ -100,7 +105,7 @@
filters: [{ prop: 'groupName', value: '' }],
tableProps: {
stripe: true,
size: 'mini',
size: 'small',
defaultSort: { prop: 'created_at', order: 'descending' }
},
pageSize: 10,
@@ -111,7 +116,7 @@
}
});
const loading = ref(false);
const previousInstancesWorldDialogRef = ref(null);
const previousInstancesWorldDialogIndex = ref(2000);
const isVisible = computed({
get: () => props.previousInstancesWorldDialog.visible,
@@ -145,16 +150,17 @@
}
function deleteGameLogWorldInstancePrompt(row) {
proxy.$confirm('Continue? Delete GameLog Instance', 'Confirm', {
ElMessageBox.confirm('Continue? Delete GameLog Instance', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
type: 'info'
})
.then((action) => {
if (action === 'confirm') {
deleteGameLogWorldInstance(row);
}
}
});
})
.catch(() => {});
}
watch(
@@ -162,10 +168,16 @@
() => {
if (props.previousInstancesWorldDialog.visible) {
nextTick(() => {
adjustDialogZ(previousInstancesWorldDialogRef.value.$el);
previousInstancesWorldDialogIndex.value = getNextDialogIndex();
});
refreshPreviousInstancesWorldTable();
}
}
);
</script>
<style scoped>
.button-pd-0 {
padding: 0;
}
</style>
-159
View File
@@ -1,159 +0,0 @@
<template>
<el-dialog
ref="elDialogRef"
:visible="props.visible"
v-bind="attrs"
:close-on-click-modal="false"
@open="handleOpen"
@close="handleClose"
:top="marginTop">
<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 styleObserver = ref(null);
const resizeObserver = ref(null);
const marginTop = ref('5px');
let handleResize = null;
const handleOpen = () => {
emit('open');
nextTick(() => {
addWrapperListeners();
removeTitleAttribute();
adjustDialogMarginTop();
});
};
const removeTitleAttribute = () => {
const wrapper = elDialogRef.value?.$el;
if (wrapper && wrapper.nodeType === Node.ELEMENT_NODE) {
wrapper.removeAttribute('title');
}
};
const handleClose = () => {
emit('close');
removeWrapperListeners();
if (styleObserver.value) {
styleObserver.value.disconnect();
styleObserver.value = null;
}
if (resizeObserver.value) {
resizeObserver.value.disconnect();
resizeObserver.value = null;
}
if (handleResize) {
window.removeEventListener('resize', handleResize);
handleResize = null;
}
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;
};
const adjustDialogMarginTop = () => {
const wrapper = elDialogRef.value?.$el;
if (!wrapper) return;
const dialog = wrapper.querySelector('.el-dialog');
if (!dialog) return;
const applyStyle = () => {
const dialogHeight = dialog.offsetHeight;
const viewportHeight = window.innerHeight;
const topOffset = Math.max(0, (viewportHeight - dialogHeight) * 0.2);
marginTop.value = `${topOffset}px`;
};
applyStyle();
};
onBeforeUnmount(() => {
removeWrapperListeners();
if (styleObserver.value) {
styleObserver.value.disconnect();
styleObserver.value = null;
}
if (resizeObserver.value) {
resizeObserver.value.disconnect();
resizeObserver.value = null;
}
if (handleResize) {
window.removeEventListener('resize', handleResize);
handleResize = null;
}
});
</script>
+8 -8
View File
@@ -1,7 +1,7 @@
<!--<template>-->
<!-- <safe-dialog-->
<!-- <el-dialog-->
<!-- class="x-dialog"-->
<!-- :visible="sendBoopDialog.visible"-->
<!-- :model-value="sendBoopDialog.visible"-->
<!-- :title="t('dialog.boop_dialog.header')"-->
<!-- width="450px"-->
<!-- @close="closeDialog">-->
@@ -20,7 +20,7 @@
<!-- style="height: auto">-->
<!-- <template v-if="friend.ref">-->
<!-- <div class="avatar" :class="userStatusClass(friend.ref)">-->
<!-- <img v-lazy="userImage(friend.ref)" />-->
<!-- <img :src="userImage(friend.ref)" loading="lazy">-->
<!-- </div>-->
<!-- <div class="detail">-->
<!-- <span-->
@@ -42,7 +42,7 @@
<!-- style="height: auto">-->
<!-- <template v-if="friend.ref">-->
<!-- <div class="avatar" :class="userStatusClass(friend.ref)">-->
<!-- <img v-lazy="userImage(friend.ref)" />-->
<!-- <img :src="userImage(friend.ref)" loading="lazy">-->
<!-- </div>-->
<!-- <div class="detail">-->
<!-- <span-->
@@ -64,7 +64,7 @@
<!-- style="height: auto">-->
<!-- <template v-if="friend.ref">-->
<!-- <div class="avatar">-->
<!-- <img v-lazy="userImage(friend.ref)" />-->
<!-- <img :src="userImage(friend.ref)" loading="lazy">-->
<!-- </div>-->
<!-- <div class="detail">-->
<!-- <span-->
@@ -86,7 +86,7 @@
<!-- style="height: auto">-->
<!-- <template v-if="friend.ref">-->
<!-- <div class="avatar">-->
<!-- <img v-lazy="userImage(friend.ref)" />-->
<!-- <img :src="userImage(friend.ref)" loading="lazy">-->
<!-- </div>-->
<!-- <div class="detail">-->
<!-- <span-->
@@ -135,7 +135,7 @@
<!-- </template>-->
<!-- <template v-else>-->
<!-- <img-->
<!-- v-lazy="image.versions[image.versions.length - 1].file.url"-->
<!-- :src="image.versions[image.versions.length - 1].file.url"-->
<!-- class="avatar"-->
<!-- style="width: 200px; height: 200px" />-->
<!-- </template>-->
@@ -162,7 +162,7 @@
<!-- t('dialog.boop_dialog.send')-->
<!-- }}</el-button>-->
<!-- </template>-->
<!-- </safe-dialog>-->
<!-- </el-dialog>-->
<!--</template>-->
<!--<script setup>-->
+13 -15
View File
@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible.sync="bioDialog.visible"
v-model="bioDialog.visible"
:title="t('dialog.bio.header')"
width="600px"
append-to-body>
@@ -9,7 +9,7 @@
<el-input
v-model="bioDialog.bio"
type="textarea"
size="mini"
size="small"
maxlength="512"
show-word-limit
:autosize="{ minRows: 5, maxRows: 20 }"
@@ -23,16 +23,13 @@
v-model="bioDialog.bioLinks[index]"
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)" />
<img :src="getFaviconUrl(link)" style="width: 16px; height: 16px; vertical-align: middle" />
<el-button :icon="Delete" @click="bioDialog.bioLinks.splice(index, 1)" />
</el-input>
<el-button
:disabled="bioDialog.bioLinks.length >= 3"
size="mini"
size="small"
style="margin-top: 5px"
@click="bioDialog.bioLinks.push('')">
{{ t('dialog.bio.add_link') }}
@@ -44,18 +41,19 @@
{{ t('dialog.bio.update') }}
</el-button>
</template>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { ElMessage } from 'element-plus';
import { Delete } from '@element-plus/icons-vue';
import { useI18n } from 'vue-i18n';
import { userRequest } from '../../../api';
import { getFaviconUrl } from '../../../shared/utils';
const { t } = useI18n();
const { $message } = getCurrentInstance().proxy;
const props = defineProps({
bioDialog: {
type: Object,
@@ -79,7 +77,7 @@
})
.then((args) => {
D.visible = false;
$message({
ElMessage({
message: 'Bio updated',
type: 'success'
});
@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible.sync="languageDialog.visible"
v-model="languageDialog.visible"
:title="t('dialog.language.header')"
width="400px"
append-to-body>
@@ -22,7 +22,6 @@
</el-tag>
</div>
<el-select
value=""
:disabled="languageDialog.loading || (currentUser.$languages && currentUser.$languages.length === 3)"
:placeholder="t('dialog.language.select_language')"
style="margin-top: 14px"
@@ -40,12 +39,12 @@
</el-option>
</el-select>
</div>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { userRequest } from '../../../api';
import { languageClass } from '../../../shared/utils';
import { useUserStore } from '../../../stores';
@@ -1,24 +1,24 @@
<template>
<safe-dialog
ref="previousInstancesUserDialogRef"
:visible.sync="isVisible"
:title="$t('dialog.previous_instances.header')"
<el-dialog
:z-index="previousInstancesUserDialogIndex"
v-model="isVisible"
:title="t('dialog.previous_instances.header')"
width="1000px"
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
v-model="previousInstancesUserDialogTable.filters[0].value"
:placeholder="$t('dialog.previous_instances.search_placeholder')"
:placeholder="t('dialog.previous_instances.search_placeholder')"
style="display: block; width: 150px"></el-input>
</div>
<data-tables v-loading="loading" v-bind="previousInstancesUserDialogTable" style="margin-top: 10px">
<el-table-column :label="$t('table.previous_instances.date')" prop="created_at" sortable width="170">
<DataTable v-loading="loading" v-bind="previousInstancesUserDialogTable" style="margin-top: 10px">
<el-table-column :label="t('table.previous_instances.date')" prop="created_at" sortable width="170">
<template #default="scope">
<span>{{ formatDateFilter(scope.row.created_at, 'long') }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('table.previous_instances.world')" prop="name" sortable>
<el-table-column :label="t('table.previous_instances.world')" prop="name" sortable>
<template #default="scope">
<Location
:location="scope.row.location"
@@ -26,53 +26,61 @@
:grouphint="scope.row.groupName" />
</template>
</el-table-column>
<el-table-column :label="$t('table.previous_instances.instance_creator')" prop="location" width="170">
<el-table-column :label="t('table.previous_instances.instance_creator')" prop="location" width="170">
<template #default="scope">
<DisplayName :userid="scope.row.$location.userId" :location="scope.row.$location.tag" />
</template>
</el-table-column>
<el-table-column :label="$t('table.previous_instances.time')" prop="time" width="100" sortable>
<el-table-column :label="t('table.previous_instances.time')" prop="time" width="100" sortable>
<template #default="scope">
<span v-text="scope.row.timer"></span>
</template>
</el-table-column>
<el-table-column :label="$t('table.previous_instances.action')" width="90" align="right">
<el-table-column :label="t('table.previous_instances.action')" width="90" align="right">
<template #default="scope">
<el-button
type="text"
icon="el-icon-switch-button"
size="mini"
:icon="SwitchButton"
size="small"
class="button-pd-0"
@click="showLaunchDialog(scope.row.location)"></el-button>
<el-button
type="text"
icon="el-icon-s-data"
size="mini"
:icon="DataLine"
size="small"
class="button-pd-0"
@click="showPreviousInstancesInfoDialog(scope.row.location)"></el-button>
<el-button
v-if="shiftHeld"
style="color: #f56c6c"
type="text"
icon="el-icon-close"
size="mini"
:icon="Close"
size="small"
class="button-pd-0"
@click="deleteGameLogUserInstance(scope.row)"></el-button>
<el-button
v-else
type="text"
icon="el-icon-close"
size="mini"
:icon="Close"
size="small"
class="button-pd-0"
@click="deleteGameLogUserInstancePrompt(scope.row)"></el-button>
</template>
</el-table-column>
</data-tables>
</safe-dialog>
</DataTable>
</el-dialog>
</template>
<script setup>
import { SwitchButton, DataLine, Close } from '@element-plus/icons-vue';
import { ElMessageBox } from 'element-plus';
import { storeToRefs } from 'pinia';
import { computed, getCurrentInstance, nextTick, reactive, ref, watch } from 'vue';
import { computed, nextTick, reactive, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { database } from '../../../service/database';
import {
adjustDialogZ,
getNextDialogIndex,
compareByCreatedAt,
parseLocation,
removeFromArray,
@@ -93,22 +101,20 @@
previousInstancesTable: {
data: [],
filters: [{ prop: 'displayName', value: '' }],
tableProps: { stripe: true, size: 'mini', height: '400px' }
tableProps: { stripe: true, size: 'small', height: '400px' }
}
})
}
});
const emit = defineEmits(['update:previous-instances-user-dialog']);
const { proxy } = getCurrentInstance();
const loading = ref(false);
const previousInstancesUserDialogTable = reactive({
data: [],
filters: [{ prop: 'worldName', value: '' }],
tableProps: {
stripe: true,
size: 'mini',
size: 'small',
defaultSort: { prop: 'created_at', order: 'descending' }
},
pageSize: 10,
@@ -122,8 +128,9 @@
const { showLaunchDialog } = useLaunchStore();
const { showPreviousInstancesInfoDialog } = useInstanceStore();
const { shiftHeld } = storeToRefs(useUiStore());
const { t } = useI18n();
const previousInstancesUserDialogRef = ref(null);
const previousInstancesUserDialogIndex = ref(2000);
const isVisible = computed({
get: () => props.previousInstancesUserDialog.visible,
@@ -154,7 +161,7 @@
() => {
if (props.previousInstancesUserDialog.visible) {
nextTick(() => {
adjustDialogZ(previousInstancesUserDialogRef.value.$el);
previousInstancesUserDialogIndex.value = getNextDialogIndex();
});
refreshPreviousInstancesUserTable();
}
@@ -172,13 +179,20 @@
}
function deleteGameLogUserInstancePrompt(row) {
proxy.$confirm('Continue? Delete User From GameLog Instance', 'Confirm', {
ElMessageBox.confirm('Continue? Delete User From GameLog Instance', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
type: 'info'
})
.then((action) => {
if (action === 'confirm') deleteGameLogUserInstance(row);
}
});
})
.catch(() => {});
}
</script>
<style scoped>
.button-pd-0 {
padding: 0;
}
</style>
@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible.sync="pronounsDialog.visible"
v-model="pronounsDialog.visible"
:title="t('dialog.pronouns.header')"
width="600px"
append-to-body>
@@ -9,7 +9,7 @@
<el-input
type="textarea"
v-model="pronounsDialog.pronouns"
size="mini"
size="small"
maxlength="32"
show-word-limit
:autosize="{ minRows: 2, maxRows: 5 }"
@@ -21,18 +21,15 @@
{{ t('dialog.pronouns.update') }}
</el-button>
</template>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { ElMessage } from 'element-plus';
import { useI18n } from 'vue-i18n';
import { userRequest } from '../../../api';
const { t } = useI18n();
const { proxy } = getCurrentInstance();
const { $message } = proxy;
const props = defineProps({
pronounsDialog: {
type: Object,
@@ -55,7 +52,7 @@
})
.then((args) => {
D.visible = false;
$message({
ElMessage({
message: 'Pronouns updated',
type: 'success'
});
@@ -1,7 +1,8 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible.sync="sendInviteRequestDialogVisible"
:model-value="sendInviteRequestDialogVisible"
@update:model-value="$emit('update:sendInviteRequestDialogVisible', $event)"
:title="t('dialog.invite_request_message.header')"
width="800px"
append-to-body
@@ -10,20 +11,20 @@
<input class="inviteImageUploadButton" type="file" accept="image/*" @change="inviteImageUpload" />
</template>
<data-tables
<DataTable
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"
:sortable="true"
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"
:sortable="true"
width="110"
align="right">
<template #default="scope">
@@ -34,38 +35,38 @@
<template #default="scope">
<el-button
type="text"
icon="el-icon-edit"
size="mini"
:icon="Edit"
size="small"
@click.stop="showEditAndSendInviteDialog(scope.row)"></el-button>
</template>
</el-table-column>
</data-tables>
</DataTable>
<template #footer>
<el-button type="small" @click="cancelSendInviteRequest">{{
t('dialog.invite_request_message.cancel')
}}</el-button>
<el-button type="small" @click="refreshInviteMessageTableData('request')">{{
<el-button @click="cancelSendInviteRequest">{{ t('dialog.invite_request_message.cancel') }}</el-button>
<el-button @click="refreshInviteMessageTableData('request')">{{
t('dialog.invite_request_message.refresh')
}}</el-button>
</template>
<SendInviteConfirmDialog
:visible.sync="isSendInviteConfirmDialogVisible"
v-model="isSendInviteConfirmDialogVisible"
:send-invite-dialog="sendInviteDialog"
:invite-dialog="inviteDialog"
@closeInviteDialog="closeInviteDialog" />
<EditAndSendInviteDialog
:edit-and-send-invite-dialog.sync="editAndSendInviteDialog"
:edit-and-send-invite-dialog="editAndSendInviteDialog"
:send-invite-dialog="sendInviteDialog"
:invite-dialog="inviteDialog"
@closeInviteDialog="closeInviteDialog" />
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { Edit } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { useGalleryStore, useInviteStore, useUserStore } from '../../../stores';
import EditAndSendInviteDialog from '../InviteDialog/EditAndSendInviteDialog.vue';
import SendInviteConfirmDialog from '../InviteDialog/SendInviteConfirmDialog.vue';
@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible.sync="socialStatusDialog.visible"
v-model="socialStatusDialog.visible"
:title="t('dialog.social_status.header')"
append-to-body
width="400px">
@@ -11,13 +11,13 @@
<template #title>
<span style="font-size: 16px">{{ t('dialog.social_status.history') }}</span>
</template>
<data-tables
<DataTable
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>
</DataTable>
</el-collapse-item>
</el-collapse>
@@ -53,19 +53,18 @@
{{ t('dialog.social_status.update') }}
</el-button>
</template>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ElMessage } from 'element-plus';
import { storeToRefs } from 'pinia';
import { getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { userRequest } from '../../../api';
import { useUserStore } from '../../../stores';
const { t } = useI18n();
const { $message } = getCurrentInstance().proxy;
const { currentUser } = storeToRefs(useUserStore());
const props = defineProps({
@@ -103,7 +102,7 @@
})
.then((args) => {
D.visible = false;
$message({
ElMessage({
message: 'Status updated',
type: 'success'
});
File diff suppressed because it is too large Load Diff
+8 -8
View File
@@ -1,8 +1,8 @@
<template>
<safe-dialog
ref="VRCXUpdateDialogRef"
<el-dialog
:z-index="VRCXUpdateDialogIndex"
class="x-dialog"
:visible.sync="VRCXUpdateDialog.visible"
v-model="VRCXUpdateDialog.visible"
:title="t('dialog.vrcx_updater.header')"
append-to-body
width="400px">
@@ -59,15 +59,15 @@
{{ t('dialog.vrcx_updater.install') }}
</el-button>
</template>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { storeToRefs } from 'pinia';
import { nextTick, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { branches } from '../../shared/constants';
import { adjustDialogZ } from '../../shared/utils';
import { getNextDialogIndex } from '../../shared/utils';
import { useVRCXUpdaterStore } from '../../stores';
const VRCXUpdaterStore = useVRCXUpdaterStore();
@@ -85,14 +85,14 @@
const { t } = useI18n();
const VRCXUpdateDialogRef = ref(null);
const VRCXUpdateDialogIndex = ref(2000);
watch(
() => VRCXUpdateDialog,
(newVal) => {
if (newVal.value.visible) {
nextTick(() => {
adjustDialogZ(VRCXUpdateDialogRef.value.$el);
VRCXUpdateDialogIndex.value = getNextDialogIndex();
});
}
}
@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible="changeWorldImageDialogVisible"
:model-value="changeWorldImageDialogVisible"
:title="t('dialog.change_content_image.world')"
width="850px"
append-to-body
@@ -16,106 +16,60 @@
<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 type="default" size="small" :icon="Upload" @click="uploadWorldImage">
{{ t('dialog.change_content_image.upload') }}
</el-button>
</el-button-group>
<br />
<div v-for="image in previousImagesTable" :key="image.version" style="display: inline-block">
<div
v-if="image.file"
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 class="x-change-image-item">
<img :src="previousImageUrl" class="img-size" loading="lazy" />
</div>
</div>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ElMessage } from 'element-plus';
import { Upload } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
import { getCurrentInstance, ref } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { imageRequest } from '../../../api';
import { AppGlobal } from '../../../service/appConfig';
import { $throw } from '../../../service/request';
import { extractFileId } from '../../../shared/utils';
import { useGalleryStore, useWorldStore } from '../../../stores';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { worldRequest } from '../../../api';
import { useWorldStore } from '../../../stores';
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const { worldDialog } = storeToRefs(useWorldStore());
const { previousImagesTable } = storeToRefs(useGalleryStore());
const { applyWorld } = useWorldStore();
const props = defineProps({
changeWorldImageDialogVisible: {
type: Boolean,
default: false
required: true
},
previousImagesFileId: {
previousImageUrl: {
type: String,
default: ''
}
});
const emit = defineEmits(['update:changeWorldImageDialogVisible', 'refresh']);
const changeWorldImageDialogLoading = ref(false);
const worldImage = ref({
base64File: '',
fileMd5: '',
base64SignatureFile: '',
signatureMd5: '',
fileId: '',
avatarId: '',
worldId: ''
});
function uploadWorldImage() {
document.getElementById('WorldImageUploadButton').click();
}
const emit = defineEmits(['update:changeWorldImageDialogVisible', 'update:previousImageUrl']);
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 () {
const fileInput = /** @type {HTMLInputElement} */ (document.querySelector('#WorldImageUploadButton'));
changeWorldImageDialogLoading.value = false;
const fileInput = /** @type{HTMLInputElement} */ (document.querySelector('#WorldImageUploadButton'));
if (fileInput) {
fileInput.value = '';
}
@@ -125,9 +79,11 @@
clearFile();
return;
}
// validate file
if (files[0].size >= 100000000) {
// 100MB
$message({
ElMessage({
message: t('message.file.too_large'),
type: 'error'
});
@@ -135,223 +91,57 @@
return;
}
if (!files[0].type.match(/image.*/)) {
$message({
ElMessage({
message: t('message.file.not_image'),
type: 'error'
});
clearFile();
return;
}
changeWorldImageDialogLoading.value = true;
const r = new FileReader();
r.onload = async function (file) {
r.onload = async function () {
try {
const base64File = await resizeImageToFitLimits(btoa(r.result.toString()));
// 10MB
const fileMd5 = await genMd5(base64File);
const fileSizeInBytes = parseInt(file.total.toString(), 10);
const base64SignatureFile = await genSig(base64File);
const signatureMd5 = await genMd5(base64SignatureFile);
const signatureSizeInBytes = parseInt(await genLength(base64SignatureFile), 10);
const worldId = worldDialog.value.id;
const { imageUrl } = worldDialog.value.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,
...worldImage.value
};
const params = {
fileMd5,
fileSizeInBytes,
signatureMd5,
signatureSizeInBytes
};
// Upload chaining
await initiateUpload(params, fileId);
await initiateUpload(base64File);
} catch (error) {
console.error('World image upload process failed:', error);
} finally {
changeWorldImageDialogLoading.value = false;
clearFile();
}
};
changeWorldImageDialogLoading.value = true;
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) {
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) {
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) {
changeWorldImageDialogLoading.value = false;
$throw(json.status, 'World image upload failed', params.url);
}
const args = {
json,
params
};
return worldImageFileAWS(args);
}
async function worldImageFileAWS(args) {
const { fileId, fileVersion } = args.params;
const params = {
fileId,
fileVersion
};
const res = await imageRequest.uploadWorldImageFileFinish(params);
return worldImageFileFinish(res);
}
async function worldImageFileFinish(args) {
const { fileId, fileVersion } = args.params;
const params = {
fileId,
fileVersion
};
const res = await imageRequest.uploadWorldImageSigStart(params);
return worldImageSigStart(res);
}
async function worldImageSigStart(args) {
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) {
changeWorldImageDialogLoading.value = false;
$throw(json.status, 'World image upload failed', params.url);
}
const args = {
json,
params
};
return worldImageSigAWS(args);
}
async function worldImageSigAWS(args) {
const { fileId, fileVersion } = args.params;
const params = {
fileId,
fileVersion
};
const res = await imageRequest.uploadWorldImageSigFinish(params);
return worldImageSigFinish(res);
}
async function worldImageSigFinish(args) {
const { fileId, fileVersion } = args.params;
const parmas = {
id: worldImage.value.worldId,
imageUrl: `${AppGlobal.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 {
$throw(0, 'World image change failed', args.params.imageUrl);
}
}
// ------------ Upload Process End ------------
function setWorldImage(image) {
changeWorldImageDialogLoading.value = true;
const parmas = {
async function initiateUpload(base64File) {
const args = await worldRequest.uploadWorldImage(base64File);
const fileUrl = args.json.versions[args.json.versions.length - 1].file.url;
const worldArgs = await worldRequest.saveWorld({
id: worldDialog.value.id,
imageUrl: `${AppGlobal.endpointDomain}/file/${props.previousImagesFileId}/${image.version}/file`
};
imageRequest
.setWorldImage(parmas)
.then((args) => worldImageSet(args))
.finally(() => {
changeWorldImageDialogLoading.value = false;
closeDialog();
});
imageUrl: fileUrl
});
const ref = applyWorld(worldArgs.json);
changeWorldImageDialogLoading.value = false;
emit('update:previousImageUrl', ref.imageUrl);
ElMessage({
message: t('message.world.image_changed'),
type: 'success'
});
// closeDialog();
}
function compareCurrentImage(image) {
if (
`${AppGlobal.endpointDomain}/file/${props.previousImagesFileId}/${image.version}/file` ===
worldDialog.value.ref.imageUrl
) {
return true;
}
return false;
function uploadWorldImage() {
document.getElementById('WorldImageUploadButton').click();
}
</script>
<style lang="scss" scoped>
.img-size {
width: 500px;
height: 375px;
}
</style>
@@ -1,6 +1,6 @@
<template>
<safe-dialog
:visible.sync="isVisible"
<el-dialog
v-model="isVisible"
:title="t('dialog.set_world_tags.header')"
width="400px"
destroy-on-close
@@ -20,7 +20,7 @@
<el-input
v-model="setWorldTagsDialog.authorTags"
type="textarea"
size="mini"
size="small"
show-word-limit
:autosize="{ minRows: 2, maxRows: 5 }"
placeholder=""
@@ -81,12 +81,13 @@
</el-button>
</div>
</template>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ref, computed, watch, getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { ref, computed, watch } from 'vue';
import { ElMessage } from 'element-plus';
import { useI18n } from 'vue-i18n';
import { worldRequest } from '../../../api';
import { useWorldStore } from '../../../stores';
@@ -115,8 +116,6 @@
const { t } = useI18n();
const { proxy } = getCurrentInstance();
const setWorldTagsDialog = ref({
authorTags: '',
contentTags: '',
@@ -296,7 +295,7 @@
tags
})
.then((args) => {
proxy.$message({
ElMessage({
message: 'Tags updated',
type: 'success'
});
@@ -1,6 +1,6 @@
<template>
<safe-dialog
:visible.sync="isVisible"
<el-dialog
v-model="isVisible"
:title="t('dialog.allowed_video_player_domains.header')"
width="600px"
destroy-on-close
@@ -12,9 +12,9 @@
v-model="urlList[index]"
size="small"
style="margin-top: 5px">
<el-button slot="append" icon="el-icon-delete" @click="urlList.splice(index, 1)"></el-button>
<el-button :icon="Delete" @click="urlList.splice(index, 1)"></el-button>
</el-input>
<el-button size="mini" style="margin-top: 5px" @click="urlList.push('')">
<el-button size="small" style="margin-top: 5px" @click="urlList.push('')">
{{ t('dialog.allowed_video_player_domains.add_domain') }}
</el-button>
</div>
@@ -27,12 +27,16 @@
{{ t('dialog.allowed_video_player_domains.save') }}
</el-button>
</template>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ref, computed, watch, getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { Delete } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import { ref, computed, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { worldRequest } from '../../../api';
const props = defineProps({
@@ -44,8 +48,6 @@
const emit = defineEmits(['update:worldAllowedDomainsDialog']);
const { proxy } = getCurrentInstance();
const { t } = useI18n();
const urlList = ref([]);
@@ -79,7 +81,7 @@
urlList: urlList.value
})
.then((args) => {
proxy.$message({
ElMessage({
message: 'Allowed Video Player Domains updated',
type: 'success'
});
File diff suppressed because it is too large Load Diff