mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-17 13:53:52 +02:00
refactor: dialogs (#1224)
* refactor: dialogs * fix: storeAvatarImage * FriendLog.vue * FriendLog.vue * FriendLog.vue * GameLog.vue * fix: next day button jumping to the wrong date * sync master * fix: launchGame * Notification.vue * Feed.vue * Search.vue * Profile.vue * PlayerList.vue * Login.vue * utils * update dialog * del gameLog.pug * fix * fix: group role cannot be displayed currently * fix: "Hide Friends in Same Instance" hides players in unrelated private instances (#1210) * fix * fix: "Hide Friends in Same Instance" does not work when "Split Favorite Friends" is enabled * fix Notification.vue message * fix: deleteFavoriteNoConfirm * fix: feed status style * fix: infinite loading when deleting note * fix: private players will not be hidden when 'Hide Friends in Same Instance', and 'Hide Friends in Same Instance' will not work when 'Split Favorite Friends'
This commit is contained in:
@@ -116,6 +116,7 @@
|
||||
|
||||
<script>
|
||||
import dayjs from 'dayjs';
|
||||
import { parseLocation } from '../../../composables/instance/utils';
|
||||
import database from '../../../service/database';
|
||||
import utils from '../../../classes/utils';
|
||||
import configRepository from '../../../service/config';
|
||||
@@ -379,7 +380,7 @@
|
||||
const timeString = utils.timeToText(param.data, true);
|
||||
const color = param.color;
|
||||
const name = param.name;
|
||||
const location = utils.parseLocation(instanceData.location);
|
||||
const location = parseLocation(instanceData.location);
|
||||
|
||||
return `
|
||||
<div style="display: flex; align-items: center;">
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:before-close="beforeDialogClose"
|
||||
:visible.sync="isDialogVisible"
|
||||
:title="$t('dialog.avatar_export.header')"
|
||||
width="650px"
|
||||
@mousedown.native="dialogMouseDown"
|
||||
@mouseup.native="dialogMouseUp">
|
||||
<safe-dialog :visible.sync="isDialogVisible" :title="$t('dialog.avatar_export.header')" width="650px">
|
||||
<el-checkbox-group
|
||||
v-model="exportSelectedOptions"
|
||||
style="margin-bottom: 10px"
|
||||
@@ -82,13 +76,13 @@
|
||||
readonly
|
||||
style="margin-top: 15px"
|
||||
@click.native="handleCopyAvatarExportData"></el-input>
|
||||
</el-dialog>
|
||||
</safe-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AvatarExportDialog',
|
||||
inject: ['API', 'beforeDialogClose', 'dialogMouseDown', 'dialogMouseUp'],
|
||||
inject: ['API'],
|
||||
props: {
|
||||
avatarExportDialogVisible: Boolean,
|
||||
favoriteAvatars: Array,
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
<safe-dialog
|
||||
ref="avatarImportDialog"
|
||||
:before-close="beforeDialogClose"
|
||||
:visible.sync="isVisible"
|
||||
:title="$t('dialog.avatar_import.header')"
|
||||
width="650px"
|
||||
@mousedown.native="dialogMouseDown"
|
||||
@mouseup.native="dialogMouseUp">
|
||||
width="650px">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
||||
<div style="font-size: 12px">{{ $t('dialog.avatar_import.description') }}</div>
|
||||
<div style="display: flex; align-items: center">
|
||||
@@ -171,7 +168,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</data-tables>
|
||||
</el-dialog>
|
||||
</safe-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -180,16 +177,7 @@
|
||||
|
||||
export default {
|
||||
name: 'AvatarImportDialog',
|
||||
inject: [
|
||||
'API',
|
||||
'beforeDialogClose',
|
||||
'dialogMouseDown',
|
||||
'dialogMouseUp',
|
||||
'adjustDialogZ',
|
||||
'showFullscreenImageDialog',
|
||||
'showUserDialog',
|
||||
'showAvatarDialog'
|
||||
],
|
||||
inject: ['API', 'adjustDialogZ', 'showFullscreenImageDialog', 'showUserDialog', 'showAvatarDialog'],
|
||||
props: {
|
||||
getLocalAvatarFavoriteGroupLength: Function,
|
||||
localAvatarFavoriteGroups: Array,
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:before-close="beforeDialogClose"
|
||||
<safe-dialog
|
||||
:visible.sync="isDialogVisible"
|
||||
class="x-dialog"
|
||||
:title="$t('dialog.friend_export.header')"
|
||||
width="650px"
|
||||
destroy-on-close
|
||||
@mousedown.native="dialogMouseDown"
|
||||
@mouseup.native="dialogMouseUp">
|
||||
destroy-on-close>
|
||||
<el-dropdown trigger="click" size="small" @click.native.stop>
|
||||
<el-button size="mini">
|
||||
<span v-if="friendExportFavoriteGroup">
|
||||
@@ -42,13 +39,13 @@
|
||||
readonly
|
||||
style="margin-top: 15px"
|
||||
@click.native="handleCopyFriendExportData"></el-input>
|
||||
</el-dialog>
|
||||
</safe-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'FriendExportDialog',
|
||||
inject: ['API', 'beforeDialogClose', 'dialogMouseDown', 'dialogMouseUp'],
|
||||
inject: ['API'],
|
||||
props: {
|
||||
friendExportDialogVisible: Boolean,
|
||||
favoriteFriends: Array
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
<safe-dialog
|
||||
ref="friendImportDialog"
|
||||
:before-close="beforeDialogClose"
|
||||
:visible.sync="isVisible"
|
||||
:title="$t('dialog.friend_import.header')"
|
||||
width="650px"
|
||||
@mousedown.native="dialogMouseDown"
|
||||
@mouseup.native="dialogMouseUp">
|
||||
width="650px">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
||||
<div style="font-size: 12px">{{ $t('dialog.friend_import.description') }}</div>
|
||||
<div style="display: flex; align-items: center">
|
||||
@@ -122,7 +119,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</data-tables>
|
||||
</el-dialog>
|
||||
</safe-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -131,17 +128,7 @@
|
||||
|
||||
export default {
|
||||
name: 'FriendImportDialog',
|
||||
inject: [
|
||||
'API',
|
||||
'userImage',
|
||||
'userImageFull',
|
||||
'showFullscreenImageDialog',
|
||||
'showUserDialog',
|
||||
'beforeDialogClose',
|
||||
'dialogMouseDown',
|
||||
'dialogMouseUp',
|
||||
'adjustDialogZ'
|
||||
],
|
||||
inject: ['API', 'userImage', 'userImageFull', 'showFullscreenImageDialog', 'showUserDialog', 'adjustDialogZ'],
|
||||
props: {
|
||||
friendImportDialogVisible: {
|
||||
type: Boolean,
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:before-close="beforeDialogClose"
|
||||
:visible.sync="isDialogVisible"
|
||||
:title="$t('dialog.world_export.header')"
|
||||
width="650px"
|
||||
@mousedown.native="dialogMouseDown"
|
||||
@mouseup.native="dialogMouseUp">
|
||||
<safe-dialog :visible.sync="isDialogVisible" :title="$t('dialog.world_export.header')" width="650px">
|
||||
<el-checkbox-group
|
||||
v-model="exportSelectedOptions"
|
||||
style="margin-bottom: 10px"
|
||||
@@ -84,13 +78,13 @@
|
||||
readonly
|
||||
style="margin-top: 15px"
|
||||
@click.native="handleCopyWorldExportData"></el-input>
|
||||
</el-dialog>
|
||||
</safe-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'WorldExportDialog',
|
||||
inject: ['API', 'beforeDialogClose', 'dialogMouseDown', 'dialogMouseUp'],
|
||||
inject: ['API'],
|
||||
props: {
|
||||
favoriteWorlds: Array,
|
||||
worldExportDialogVisible: Boolean,
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
<safe-dialog
|
||||
ref="worldImportDialog"
|
||||
:before-close="beforeDialogClose"
|
||||
:visible.sync="isVisible"
|
||||
:title="$t('dialog.world_import.header')"
|
||||
width="650px"
|
||||
top="10vh"
|
||||
class="x-dialog"
|
||||
@mousedown.native="dialogMouseDown"
|
||||
@mouseup.native="dialogMouseUp">
|
||||
class="x-dialog">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
||||
<div style="font-size: 12px">{{ $t('dialog.world_import.description') }}</div>
|
||||
<div style="display: flex; align-items: center">
|
||||
@@ -176,7 +173,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</data-tables>
|
||||
</el-dialog>
|
||||
</safe-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -185,16 +182,7 @@
|
||||
|
||||
export default {
|
||||
name: 'WorldImportDialog',
|
||||
inject: [
|
||||
'API',
|
||||
'beforeDialogClose',
|
||||
'dialogMouseDown',
|
||||
'dialogMouseUp',
|
||||
'showFullscreenImageDialog',
|
||||
'showUserDialog',
|
||||
'adjustDialogZ',
|
||||
'showWorldDialog'
|
||||
],
|
||||
inject: ['API', 'showFullscreenImageDialog', 'showUserDialog', 'adjustDialogZ', 'showWorldDialog'],
|
||||
props: {
|
||||
worldImportDialogVisible: Boolean,
|
||||
worldImportDialogInput: String,
|
||||
|
||||
475
src/views/Feed/Feed.vue
Normal file
475
src/views/Feed/Feed.vue
Normal file
@@ -0,0 +1,475 @@
|
||||
<template>
|
||||
<div v-show="menuActiveIndex === 'feed'" class="x-container feed">
|
||||
<div style="margin: 0 0 10px; display: flex; align-items: center">
|
||||
<div style="flex: none; margin-right: 10px; display: flex; align-items: center">
|
||||
<el-tooltip
|
||||
placement="bottom"
|
||||
:content="t('view.feed.favorites_only_tooltip')"
|
||||
:disabled="hideTooltips">
|
||||
<el-switch v-model="feedTable.vip" active-color="#13ce66" @change="feedTableLookup"></el-switch>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-select
|
||||
v-model="feedTable.filter"
|
||||
multiple
|
||||
clearable
|
||||
style="flex: 1; height: 40px"
|
||||
:placeholder="t('view.feed.filter_placeholder')"
|
||||
@change="feedTableLookup">
|
||||
<el-option
|
||||
v-for="type in ['GPS', 'Online', 'Offline', 'Status', 'Avatar', 'Bio']"
|
||||
:key="type"
|
||||
:label="t('view.feed.filters.' + type)"
|
||||
:value="type"></el-option>
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model="feedTable.search"
|
||||
:placeholder="t('view.feed.search_placeholder')"
|
||||
clearable
|
||||
style="flex: none; width: 150px; margin: 0 10px"
|
||||
@keyup.native.13="feedTableLookup"
|
||||
@change="feedTableLookup"></el-input>
|
||||
</div>
|
||||
|
||||
<data-tables v-loading="feedTable.loading" v-bind="feedTable" lazy>
|
||||
<el-table-column type="expand" width="20">
|
||||
<template #default="scope">
|
||||
<div style="position: relative; font-size: 14px">
|
||||
<template v-if="scope.row.type === 'GPS'">
|
||||
<location
|
||||
v-if="scope.row.previousLocation"
|
||||
:location="scope.row.previousLocation"
|
||||
style="display: inline-block"></location>
|
||||
<el-tag type="info" effect="plain" size="mini" style="margin-left: 5px">{{
|
||||
timeToText(scope.row.time)
|
||||
}}</el-tag>
|
||||
<br />
|
||||
<span style="margin-right: 5px">
|
||||
<i class="el-icon-right"></i>
|
||||
</span>
|
||||
<location
|
||||
v-if="scope.row.location"
|
||||
:location="scope.row.location"
|
||||
:hint="scope.row.worldName"
|
||||
:grouphint="scope.row.groupName"></location>
|
||||
</template>
|
||||
<template v-else-if="scope.row.type === 'Offline'">
|
||||
<template v-if="scope.row.location">
|
||||
<location
|
||||
:location="scope.row.location"
|
||||
:hint="scope.row.worldName"
|
||||
:grouphint="scope.row.groupName"></location>
|
||||
<el-tag type="info" effect="plain" size="mini" style="margin-left: 5px">{{
|
||||
timeToText(scope.row.time)
|
||||
}}</el-tag>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else-if="scope.row.type === 'Online'">
|
||||
<location
|
||||
v-if="scope.row.location"
|
||||
:location="scope.row.location"
|
||||
:hint="scope.row.worldName"
|
||||
:grouphint="scope.row.groupName"></location>
|
||||
</template>
|
||||
<template v-else-if="scope.row.type === 'Avatar'">
|
||||
<div style="display: flex; align-items: center">
|
||||
<el-popover placement="right" width="500px" trigger="click">
|
||||
<div
|
||||
slot="reference"
|
||||
style="display: inline-block; vertical-align: top; width: 160px">
|
||||
<template v-if="scope.row.previousCurrentAvatarThumbnailImageUrl">
|
||||
<img
|
||||
v-lazy="scope.row.previousCurrentAvatarThumbnailImageUrl"
|
||||
class="x-link"
|
||||
style="flex: none; width: 160px; height: 120px; border-radius: 4px" />
|
||||
<br />
|
||||
<avatar-info
|
||||
:imageurl="scope.row.previousCurrentAvatarThumbnailImageUrl"
|
||||
:userid="scope.row.userId"
|
||||
:hintownerid="scope.row.previousOwnerId"
|
||||
:hintavatarname="scope.row.previousAvatarName"
|
||||
:avatartags="scope.row.previousCurrentAvatarTags"></avatar-info>
|
||||
</template>
|
||||
</div>
|
||||
<img
|
||||
v-lazy="scope.row.previousCurrentAvatarImageUrl"
|
||||
class="x-link"
|
||||
style="width: 500px; height: 375px"
|
||||
@click="showFullscreenImageDialog(scope.row.previousCurrentAvatarImageUrl)" />
|
||||
</el-popover>
|
||||
<span style="position: relative; margin: 0 10px">
|
||||
<i class="el-icon-right"></i>
|
||||
</span>
|
||||
<el-popover placement="right" width="500px" trigger="click">
|
||||
<div
|
||||
slot="reference"
|
||||
style="display: inline-block; vertical-align: top; width: 160px">
|
||||
<template v-if="scope.row.currentAvatarThumbnailImageUrl">
|
||||
<img
|
||||
v-lazy="scope.row.currentAvatarThumbnailImageUrl"
|
||||
class="x-link"
|
||||
style="flex: none; width: 160px; height: 120px; border-radius: 4px" />
|
||||
<br />
|
||||
<avatar-info
|
||||
:imageurl="scope.row.currentAvatarThumbnailImageUrl"
|
||||
:userid="scope.row.userId"
|
||||
:hintownerid="scope.row.ownerId"
|
||||
:hintavatarname="scope.row.avatarName"
|
||||
:avatartags="scope.row.currentAvatarTags"></avatar-info>
|
||||
</template>
|
||||
</div>
|
||||
<img
|
||||
v-lazy="scope.row.currentAvatarImageUrl"
|
||||
class="x-link"
|
||||
style="width: 500px; height: 375px"
|
||||
@click="showFullscreenImageDialog(scope.row.currentAvatarImageUrl)" />
|
||||
</el-popover>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="scope.row.type === 'Status'">
|
||||
<el-tooltip placement="top">
|
||||
<template #content>
|
||||
<span v-if="scope.row.previousStatus === 'active'">{{
|
||||
t('dialog.user.status.active')
|
||||
}}</span>
|
||||
<span v-else-if="scope.row.previousStatus === 'join me'">{{
|
||||
t('dialog.user.status.join_me')
|
||||
}}</span>
|
||||
<span v-else-if="scope.row.previousStatus === 'ask me'">{{
|
||||
t('dialog.user.status.ask_me')
|
||||
}}</span>
|
||||
<span v-else-if="scope.row.previousStatus === 'busy'">{{
|
||||
t('dialog.user.status.busy')
|
||||
}}</span>
|
||||
<span v-else>{{ t('dialog.user.status.offline') }}</span>
|
||||
</template>
|
||||
<i class="x-user-status" :class="statusClass(scope.row.previousStatus)"></i>
|
||||
</el-tooltip>
|
||||
<span style="margin-left: 5px" v-text="scope.row.previousStatusDescription"></span>
|
||||
<br />
|
||||
<span>
|
||||
<i class="el-icon-right"></i>
|
||||
</span>
|
||||
<el-tooltip placement="top">
|
||||
<template #content>
|
||||
<span v-if="scope.row.status === 'active'">{{
|
||||
t('dialog.user.status.active')
|
||||
}}</span>
|
||||
<span v-else-if="scope.row.status === 'join me'">{{
|
||||
t('dialog.user.status.join_me')
|
||||
}}</span>
|
||||
<span v-else-if="scope.row.status === 'ask me'">{{
|
||||
t('dialog.user.status.ask_me')
|
||||
}}</span>
|
||||
<span v-else-if="scope.row.status === 'busy'">{{
|
||||
t('dialog.user.status.busy')
|
||||
}}</span>
|
||||
<span v-else>{{ t('dialog.user.status.offline') }}</span>
|
||||
</template>
|
||||
<i
|
||||
class="x-user-status"
|
||||
:class="statusClass(scope.row.status)"
|
||||
style="margin: 0 5px"></i>
|
||||
</el-tooltip>
|
||||
<span v-text="scope.row.statusDescription"></span>
|
||||
</template>
|
||||
<template v-else-if="scope.row.type === 'Bio'">
|
||||
<pre
|
||||
style="
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
line-height: 25px;
|
||||
line-height: 22px;
|
||||
"
|
||||
v-html="formatDifference(scope.row.previousBio, scope.row.bio)"></pre>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('table.feed.date')" prop="created_at" sortable="custom" width="120">
|
||||
<template #default="scope">
|
||||
<el-tooltip placement="right">
|
||||
<template #content>
|
||||
<span>{{ scope.row.created_at | formatDate('long') }}</span>
|
||||
</template>
|
||||
<span>{{ scope.row.created_at | formatDate('short') }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('table.feed.type')" prop="type" width="80">
|
||||
<template #default="scope">
|
||||
<span class="x-link" v-text="t('view.feed.filters.' + scope.row.type)"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('table.feed.user')" prop="displayName" width="180">
|
||||
<template #default="scope">
|
||||
<span
|
||||
class="x-link"
|
||||
style="padding-right: 10px"
|
||||
@click="showUserDialog(scope.row.userId)"
|
||||
v-text="scope.row.displayName"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('table.feed.detail')">
|
||||
<template #default="scope">
|
||||
<template v-if="scope.row.type === 'GPS'">
|
||||
<location
|
||||
v-if="scope.row.location"
|
||||
:location="scope.row.location"
|
||||
:hint="scope.row.worldName"
|
||||
:grouphint="scope.row.groupName"></location>
|
||||
</template>
|
||||
<template v-else-if="scope.row.type === 'Offline' || scope.row.type === 'Online'">
|
||||
<location
|
||||
v-if="scope.row.location"
|
||||
:location="scope.row.location"
|
||||
:hint="scope.row.worldName"
|
||||
:grouphint="scope.row.groupName"></location>
|
||||
</template>
|
||||
<template v-else-if="scope.row.type === 'Status'">
|
||||
<template v-if="scope.row.statusDescription === scope.row.previousStatusDescription">
|
||||
<el-tooltip placement="top">
|
||||
<template #content>
|
||||
<span v-if="scope.row.previousStatus === 'active'">{{
|
||||
t('dialog.user.status.active')
|
||||
}}</span>
|
||||
<span v-else-if="scope.row.previousStatus === 'join me'">{{
|
||||
t('dialog.user.status.join_me')
|
||||
}}</span>
|
||||
<span v-else-if="scope.row.previousStatus === 'ask me'">{{
|
||||
t('dialog.user.status.ask_me')
|
||||
}}</span>
|
||||
<span v-else-if="scope.row.previousStatus === 'busy'">{{
|
||||
t('dialog.user.status.busy')
|
||||
}}</span>
|
||||
<span v-else>{{ t('dialog.user.status.offline') }}</span>
|
||||
</template>
|
||||
<i class="x-user-status" :class="statusClass(scope.row.previousStatus)"></i>
|
||||
</el-tooltip>
|
||||
<span style="margin: 0 5px">
|
||||
<i class="el-icon-right"></i>
|
||||
</span>
|
||||
<el-tooltip placement="top">
|
||||
<template #content>
|
||||
<span v-if="scope.row.status === 'active'">{{
|
||||
t('dialog.user.status.active')
|
||||
}}</span>
|
||||
<span v-else-if="scope.row.status === 'join me'">{{
|
||||
t('dialog.user.status.join_me')
|
||||
}}</span>
|
||||
<span v-else-if="scope.row.status === 'ask me'">{{
|
||||
t('dialog.user.status.ask_me')
|
||||
}}</span>
|
||||
<span v-else-if="scope.row.status === 'busy'">{{
|
||||
t('dialog.user.status.busy')
|
||||
}}</span>
|
||||
<span v-else>{{ t('dialog.user.status.offline') }}</span>
|
||||
</template>
|
||||
<i class="x-user-status" :class="statusClass(scope.row.status)"></i>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-tooltip placement="top">
|
||||
<template #content>
|
||||
<span v-if="scope.row.status === 'active'">{{
|
||||
t('dialog.user.status.active')
|
||||
}}</span>
|
||||
<span v-else-if="scope.row.status === 'join me'">{{
|
||||
t('dialog.user.status.join_me')
|
||||
}}</span>
|
||||
<span v-else-if="scope.row.status === 'ask me'">{{
|
||||
t('dialog.user.status.ask_me')
|
||||
}}</span>
|
||||
<span v-else-if="scope.row.status === 'busy'">{{
|
||||
t('dialog.user.status.busy')
|
||||
}}</span>
|
||||
<span v-else>{{ t('dialog.user.status.offline') }}</span>
|
||||
</template>
|
||||
<i
|
||||
class="x-user-status"
|
||||
:class="statusClass(scope.row.status)"
|
||||
style="margin-right: 3px"></i>
|
||||
</el-tooltip>
|
||||
<span v-text="scope.row.statusDescription"></span>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else-if="scope.row.type === 'Avatar'">
|
||||
<avatar-info
|
||||
:imageurl="scope.row.currentAvatarImageUrl"
|
||||
:userid="scope.row.userId"
|
||||
:hintownerid="scope.row.ownerId"
|
||||
:hintavatarname="scope.row.avatarName"
|
||||
:avatartags="scope.row.currentAvatarTags"></avatar-info>
|
||||
</template>
|
||||
<template v-else-if="scope.row.type === 'Bio'">
|
||||
<span v-text="scope.row.bio"></span>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</data-tables>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'FeedTab'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import utils from '../../classes/utils';
|
||||
import Location from '../../components/Location.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const showFullscreenImageDialog = inject('showFullscreenImageDialog');
|
||||
const statusClass = inject('statusClass');
|
||||
const showUserDialog = inject('showUserDialog');
|
||||
|
||||
defineProps({
|
||||
menuActiveIndex: {
|
||||
type: String,
|
||||
default: 'feed'
|
||||
},
|
||||
hideTooltips: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
feedTable: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['feedTableLookup']);
|
||||
|
||||
/**
|
||||
* Function that format the differences between two strings with HTML tags
|
||||
* markerStartTag and markerEndTag are optional, if emitted, the differences will be highlighted with yellow and underlined.
|
||||
* @param {*} s1
|
||||
* @param {*} s2
|
||||
* @param {*} markerStartTag
|
||||
* @param {*} markerEndTag
|
||||
* @returns An array that contains both the string 1 and string 2, which the differences are formatted with HTML tags
|
||||
*/
|
||||
|
||||
//function getWordDifferences
|
||||
function formatDifference(
|
||||
oldString,
|
||||
newString,
|
||||
markerAddition = '<span class="x-text-added">{{text}}</span>',
|
||||
markerDeletion = '<span class="x-text-removed">{{text}}</span>'
|
||||
) {
|
||||
[oldString, newString] = [oldString, newString].map((s) =>
|
||||
s
|
||||
.replaceAll(/&/g, '&')
|
||||
.replaceAll(/</g, '<')
|
||||
.replaceAll(/>/g, '>')
|
||||
.replaceAll(/"/g, '"')
|
||||
.replaceAll(/'/g, ''')
|
||||
.replaceAll(/\n/g, '<br>')
|
||||
);
|
||||
|
||||
const oldWords = oldString.split(/\s+/).flatMap((word) => word.split(/(<br>)/));
|
||||
const newWords = newString.split(/\s+/).flatMap((word) => word.split(/(<br>)/));
|
||||
|
||||
function findLongestMatch(oldStart, oldEnd, newStart, newEnd) {
|
||||
let bestOldStart = oldStart;
|
||||
let bestNewStart = newStart;
|
||||
let bestSize = 0;
|
||||
|
||||
const lookup = new Map();
|
||||
for (let i = oldStart; i < oldEnd; i++) {
|
||||
const word = oldWords[i];
|
||||
if (!lookup.has(word)) lookup.set(word, []);
|
||||
lookup.get(word).push(i);
|
||||
}
|
||||
|
||||
for (let j = newStart; j < newEnd; j++) {
|
||||
const word = newWords[j];
|
||||
if (!lookup.has(word)) continue;
|
||||
|
||||
for (const i of lookup.get(word)) {
|
||||
let size = 0;
|
||||
while (i + size < oldEnd && j + size < newEnd && oldWords[i + size] === newWords[j + size]) {
|
||||
size++;
|
||||
}
|
||||
if (size > bestSize) {
|
||||
bestOldStart = i;
|
||||
bestNewStart = j;
|
||||
bestSize = size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
oldStart: bestOldStart,
|
||||
newStart: bestNewStart,
|
||||
size: bestSize
|
||||
};
|
||||
}
|
||||
|
||||
function buildDiff(oldStart, oldEnd, newStart, newEnd) {
|
||||
const result = [];
|
||||
const match = findLongestMatch(oldStart, oldEnd, newStart, newEnd);
|
||||
|
||||
if (match.size > 0) {
|
||||
// Handle differences before the match
|
||||
if (oldStart < match.oldStart || newStart < match.newStart) {
|
||||
result.push(...buildDiff(oldStart, match.oldStart, newStart, match.newStart));
|
||||
}
|
||||
|
||||
// Add the matched words
|
||||
result.push(oldWords.slice(match.oldStart, match.oldStart + match.size).join(' '));
|
||||
|
||||
// Handle differences after the match
|
||||
if (match.oldStart + match.size < oldEnd || match.newStart + match.size < newEnd) {
|
||||
result.push(...buildDiff(match.oldStart + match.size, oldEnd, match.newStart + match.size, newEnd));
|
||||
}
|
||||
} else {
|
||||
function build(words, start, end, pattern) {
|
||||
let r = [];
|
||||
let ts = words
|
||||
.slice(start, end)
|
||||
.filter((w) => w.length > 0)
|
||||
.join(' ')
|
||||
.split('<br>');
|
||||
for (let i = 0; i < ts.length; i++) {
|
||||
if (i > 0) r.push('<br>');
|
||||
if (ts[i].length < 1) continue;
|
||||
r.push(pattern.replace('{{text}}', ts[i]));
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
// Add deletions
|
||||
if (oldStart < oldEnd) result.push(...build(oldWords, oldStart, oldEnd, markerDeletion));
|
||||
|
||||
// Add insertions
|
||||
if (newStart < newEnd) result.push(...build(newWords, newStart, newEnd, markerAddition));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return buildDiff(0, oldWords.length, 0, newWords.length)
|
||||
.join(' ')
|
||||
.replace(/<br>[ ]+<br>/g, '<br><br>')
|
||||
.replace(/<br> /g, '<br>');
|
||||
}
|
||||
|
||||
function feedTableLookup() {
|
||||
emit('feedTableLookup');
|
||||
}
|
||||
|
||||
function timeToText(time) {
|
||||
return utils.timeToText(time);
|
||||
}
|
||||
</script>
|
||||
@@ -270,9 +270,11 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import removeConfusables, { removeWhitespace } from '../../service/confusables';
|
||||
import utils from '../../classes/utils';
|
||||
import { friendRequest, userRequest } from '../../api';
|
||||
import utils from '../../classes/utils';
|
||||
import { languageClass as _languageClass } from '../../composables/user/utils';
|
||||
import removeConfusables, { removeWhitespace } from '../../service/confusables';
|
||||
import { getFaviconUrl as _getFaviconUrl } from '../../composables/shared/utils';
|
||||
|
||||
export default {
|
||||
name: 'FriendListTab',
|
||||
@@ -282,8 +284,7 @@
|
||||
'showFullscreenImageDialog',
|
||||
'showUserDialog',
|
||||
'statusClass',
|
||||
'openExternalLink',
|
||||
'languageClass'
|
||||
'openExternalLink'
|
||||
],
|
||||
props: {
|
||||
friends: {
|
||||
@@ -322,7 +323,7 @@
|
||||
friendsListLoading: false,
|
||||
friendsListLoadingProgress: '',
|
||||
friendsListSearchFilterVIP: false,
|
||||
// emm
|
||||
// TODO
|
||||
friendsListBulkUnfriendForceUpdate: 0
|
||||
};
|
||||
},
|
||||
@@ -336,6 +337,9 @@
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
languageClass(key) {
|
||||
return _languageClass(key);
|
||||
},
|
||||
friendsListSearchChange() {
|
||||
this.friendsListLoading = true;
|
||||
let query = '';
|
||||
@@ -505,7 +509,7 @@
|
||||
return utils.timeToText(val);
|
||||
},
|
||||
getFaviconUrl(link) {
|
||||
return utils.getFaviconUrl(link);
|
||||
return _getFaviconUrl(link);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
140
src/views/FriendLog/FriendLog.vue
Normal file
140
src/views/FriendLog/FriendLog.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<div v-if="menuActiveIndex === 'friendLog'" class="x-container">
|
||||
<data-tables v-bind="friendLogTable">
|
||||
<template #tool>
|
||||
<div style="margin: 0 0 10px; display: flex; align-items: center">
|
||||
<el-select
|
||||
v-model="friendLogTable.filters[0].value"
|
||||
multiple
|
||||
clearable
|
||||
style="flex: 1"
|
||||
:placeholder="t('view.friend_log.filter_placeholder')"
|
||||
@change="saveTableFilters">
|
||||
<el-option
|
||||
v-for="type in [
|
||||
'Friend',
|
||||
'Unfriend',
|
||||
'FriendRequest',
|
||||
'CancelFriendRequest',
|
||||
'DisplayName',
|
||||
'TrustLevel'
|
||||
]"
|
||||
:key="type"
|
||||
:label="t('view.friend_log.filters.' + type)"
|
||||
:value="type" />
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model="friendLogTable.filters[1].value"
|
||||
:placeholder="t('view.friend_log.search_placeholder')"
|
||||
style="flex: none; width: 150px; margin-left: 10px" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table-column :label="t('table.friendLog.date')" prop="created_at" sortable="custom" width="200">
|
||||
<template #default="scope">
|
||||
<el-tooltip placement="right">
|
||||
<template #content>
|
||||
<span>{{ scope.row.created_at | formatDate('long') }}</span>
|
||||
</template>
|
||||
<span>{{ scope.row.created_at | formatDate('short') }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('table.friendLog.type')" prop="type" width="150">
|
||||
<template #default="scope">
|
||||
<span v-text="t('view.friend_log.filters.' + scope.row.type)"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('table.friendLog.user')" prop="displayName">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.type === 'DisplayName'">
|
||||
{{ scope.row.previousDisplayName }} <i class="el-icon-right"></i>
|
||||
</span>
|
||||
<span
|
||||
class="x-link"
|
||||
style="padding-right: 10px"
|
||||
@click="showUserDialog(scope.row.userId)"
|
||||
v-text="scope.row.displayName || scope.row.userId"></span>
|
||||
<template v-if="scope.row.type === 'TrustLevel'">
|
||||
<span>
|
||||
({{ scope.row.previousTrustLevel }} <i class="el-icon-right"></i>
|
||||
{{ scope.row.trustLevel }})</span
|
||||
>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('table.friendLog.action')" width="80" align="right">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-if="shiftHeld"
|
||||
style="color: #f56c6c"
|
||||
type="text"
|
||||
icon="el-icon-close"
|
||||
size="mini"
|
||||
@click="deleteFriendLog(scope.row)"></el-button>
|
||||
<el-button
|
||||
v-else
|
||||
type="text"
|
||||
icon="el-icon-delete"
|
||||
size="mini"
|
||||
@click="deleteFriendLogPrompt(scope.row)"></el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</data-tables>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'FriendLogTab'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup>
|
||||
import { getCurrentInstance, inject } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import utils from '../../classes/utils';
|
||||
import configRepository from '../../service/config';
|
||||
import database from '../../service/database';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { proxy } = getCurrentInstance();
|
||||
const { $confirm } = proxy;
|
||||
|
||||
const showUserDialog = inject('showUserDialog');
|
||||
|
||||
const props = defineProps({
|
||||
menuActiveIndex: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
friendLogTable: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
shiftHeld: { type: Boolean, default: false }
|
||||
});
|
||||
|
||||
function saveTableFilters() {
|
||||
configRepository.setString('VRCX_friendLogTableFilters', JSON.stringify(props.friendLogTable.filters[0].value));
|
||||
}
|
||||
function deleteFriendLogPrompt(row) {
|
||||
$confirm('Continue? Delete Log', 'Confirm', {
|
||||
confirmButtonText: 'Confirm',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'info',
|
||||
callback: (action) => {
|
||||
if (action === 'confirm') {
|
||||
deleteFriendLog(row);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function deleteFriendLog(row) {
|
||||
utils.removeFromArray(props.friendLogTable.data, row);
|
||||
database.deleteFriendLogHistory(row.rowId);
|
||||
}
|
||||
</script>
|
||||
278
src/views/GameLog/GameLog.vue
Normal file
278
src/views/GameLog/GameLog.vue
Normal file
@@ -0,0 +1,278 @@
|
||||
<template>
|
||||
<div v-show="menuActiveIndex === 'gameLog'" class="x-container">
|
||||
<data-tables v-loading="gameLogTable.loading" v-bind="gameLogTable" lazy>
|
||||
<template #tool>
|
||||
<div style="margin: 0 0 10px; display: flex; align-items: center">
|
||||
<div style="flex: none; margin-right: 10px; display: flex; align-items: center">
|
||||
<el-tooltip
|
||||
placement="bottom"
|
||||
:content="t('view.feed.favorites_only_tooltip')"
|
||||
:disabled="hideTooltips">
|
||||
<el-switch
|
||||
v-model="gameLogTable.vip"
|
||||
active-color="#13ce66"
|
||||
@change="gameLogTableLookup"></el-switch>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-select
|
||||
v-model="gameLogTable.filter"
|
||||
multiple
|
||||
clearable
|
||||
style="flex: 1"
|
||||
:placeholder="t('view.game_log.filter_placeholder')"
|
||||
@change="gameLogTableLookup">
|
||||
<el-option
|
||||
v-for="type in [
|
||||
'Location',
|
||||
'OnPlayerJoined',
|
||||
'OnPlayerLeft',
|
||||
'VideoPlay',
|
||||
'Event',
|
||||
'External',
|
||||
'StringLoad',
|
||||
'ImageLoad'
|
||||
]"
|
||||
:key="type"
|
||||
:label="t('view.game_log.filters.' + type)"
|
||||
:value="type"></el-option>
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model="gameLogTable.search"
|
||||
:placeholder="t('view.game_log.search_placeholder')"
|
||||
clearable
|
||||
style="flex: none; width: 150px; margin: 0 10px"
|
||||
@keyup.native.enter="gameLogTableLookup"
|
||||
@change="gameLogTableLookup"></el-input>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table-column :label="t('table.gameLog.date')" prop="created_at" sortable="custom" width="120">
|
||||
<template #default="scope">
|
||||
<el-tooltip placement="right">
|
||||
<template #content>
|
||||
<span>{{ scope.row.created_at | formatDate('long') }}</span>
|
||||
</template>
|
||||
<span>{{ scope.row.created_at | formatDate('short') }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('table.gameLog.type')" prop="type" width="120">
|
||||
<template #default="scope">
|
||||
<el-tooltip placement="right" :open-delay="500" :disabled="hideTooltips">
|
||||
<template #content>
|
||||
<span>{{ t('view.game_log.filters.' + scope.row.type) }}</span>
|
||||
</template>
|
||||
<span
|
||||
v-if="scope.row.location && scope.row.type !== 'Location'"
|
||||
class="x-link"
|
||||
@click="showWorldDialog(scope.row.location)"
|
||||
v-text="t('view.game_log.filters.' + scope.row.type)"></span>
|
||||
<span v-else v-text="t('view.game_log.filters.' + scope.row.type)"></span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<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">
|
||||
<span>⭐</span>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-else placement="top" content="Friend">
|
||||
<span>💚</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('table.gameLog.user')" prop="displayName" width="180">
|
||||
<template #default="scope">
|
||||
<span
|
||||
v-if="scope.row.displayName"
|
||||
class="x-link"
|
||||
style="padding-right: 10px"
|
||||
@click="lookupUser(scope.row)"
|
||||
v-text="scope.row.displayName"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('table.gameLog.detail')" prop="data">
|
||||
<template #default="scope">
|
||||
<location
|
||||
v-if="scope.row.type === 'Location'"
|
||||
:location="scope.row.location"
|
||||
:hint="scope.row.worldName"
|
||||
:grouphint="scope.row.groupName"></location>
|
||||
<location
|
||||
v-else-if="scope.row.type === 'PortalSpawn'"
|
||||
:location="scope.row.instanceId"
|
||||
:hint="scope.row.worldName"
|
||||
:grouphint="scope.row.groupName"></location>
|
||||
<template v-else-if="scope.row.type === 'Event'">
|
||||
<span v-text="scope.row.data"></span>
|
||||
</template>
|
||||
<template v-else-if="scope.row.type === 'External'">
|
||||
<span v-text="scope.row.message"></span>
|
||||
</template>
|
||||
<template v-else-if="scope.row.type === 'VideoPlay'">
|
||||
<span v-if="scope.row.videoId" style="margin-right: 5px">{{ scope.row.videoId }}:</span>
|
||||
<span v-if="scope.row.videoId === 'LSMedia'" v-text="scope.row.videoName"></span>
|
||||
<span
|
||||
v-else-if="scope.row.videoName"
|
||||
class="x-link"
|
||||
@click="openExternalLink(scope.row.videoUrl)"
|
||||
v-text="scope.row.videoName"></span>
|
||||
<span
|
||||
v-else
|
||||
class="x-link"
|
||||
@click="openExternalLink(scope.row.videoUrl)"
|
||||
v-text="scope.row.videoUrl"></span>
|
||||
</template>
|
||||
<template v-else-if="scope.row.type === 'ImageLoad'">
|
||||
<span
|
||||
class="x-link"
|
||||
@click="openExternalLink(scope.row.resourceUrl)"
|
||||
v-text="scope.row.resourceUrl"></span>
|
||||
</template>
|
||||
<template v-else-if="scope.row.type === 'StringLoad'">
|
||||
<span
|
||||
class="x-link"
|
||||
@click="openExternalLink(scope.row.resourceUrl)"
|
||||
v-text="scope.row.resourceUrl"></span>
|
||||
</template>
|
||||
<template
|
||||
v-else-if="
|
||||
scope.row.type === 'Notification' ||
|
||||
scope.row.type === 'OnPlayerJoined' ||
|
||||
scope.row.type === 'OnPlayerLeft'
|
||||
">
|
||||
</template>
|
||||
<span v-else class="x-link" v-text="scope.row.data"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('table.gameLog.action')" width="80" align="right">
|
||||
<template #default="scope">
|
||||
<template
|
||||
v-if="
|
||||
scope.row.type !== 'OnPlayerJoined' &&
|
||||
scope.row.type !== 'OnPlayerLeft' &&
|
||||
scope.row.type !== 'Location' &&
|
||||
scope.row.type !== 'PortalSpawn'
|
||||
">
|
||||
<el-button
|
||||
v-if="shiftHeld"
|
||||
style="color: #f56c6c"
|
||||
type="text"
|
||||
icon="el-icon-close"
|
||||
size="mini"
|
||||
@click="deleteGameLogEntry(scope.row)"></el-button>
|
||||
<el-button
|
||||
v-else
|
||||
type="text"
|
||||
icon="el-icon-delete"
|
||||
size="mini"
|
||||
@click="deleteGameLogEntryPrompt(scope.row)"></el-button>
|
||||
</template>
|
||||
<el-tooltip placement="top" :content="t('dialog.previous_instances.info')" :disabled="hideTooltips">
|
||||
<el-button
|
||||
v-if="scope.row.type === 'Location'"
|
||||
type="text"
|
||||
icon="el-icon-s-data"
|
||||
size="mini"
|
||||
@click="showPreviousInstancesInfoDialog(scope.row.location)"></el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</data-tables>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'GameLogTab'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup>
|
||||
import { inject, getCurrentInstance } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import utils from '../../classes/utils';
|
||||
import database from '../../service/database';
|
||||
import Location from '../../components/Location.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { $confirm } = getCurrentInstance().proxy;
|
||||
|
||||
const showWorldDialog = inject('showWorldDialog');
|
||||
const openExternalLink = inject('openExternalLink');
|
||||
const showPreviousInstancesInfoDialog = inject('showPreviousInstancesInfoDialog');
|
||||
|
||||
const props = defineProps({
|
||||
menuActiveIndex: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
gameLogTable: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
hideTooltips: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
shiftHeld: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
'gameLogTableLookup',
|
||||
'gameLogIsFriend',
|
||||
'gameLogIsFavorite',
|
||||
'lookupUser',
|
||||
'updateGameLogSessionTable',
|
||||
'updateSharedFeed'
|
||||
]);
|
||||
|
||||
function gameLogTableLookup() {
|
||||
emit('gameLogTableLookup');
|
||||
}
|
||||
|
||||
function gameLogIsFriend(row) {
|
||||
emit('gameLogIsFriend', row);
|
||||
}
|
||||
|
||||
function gameLogIsFavorite(row) {
|
||||
emit('gameLogIsFavorite', row);
|
||||
}
|
||||
|
||||
function lookupUser(ref) {
|
||||
emit('lookupUser', ref);
|
||||
}
|
||||
|
||||
function deleteGameLogEntry(row) {
|
||||
utils.removeFromArray(props.gameLogTable.data, row);
|
||||
database.deleteGameLogEntry(row);
|
||||
console.log('deleteGameLogEntry', row);
|
||||
database.getGamelogDatabase().then((data) => {
|
||||
emit('updateGameLogSessionTable', data);
|
||||
emit('updateSharedFeed', true);
|
||||
});
|
||||
}
|
||||
|
||||
function deleteGameLogEntryPrompt(row) {
|
||||
$confirm('Continue? Delete Log', 'Confirm', {
|
||||
confirmButtonText: 'Confirm',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'info',
|
||||
callback: (action) => {
|
||||
if (action === 'confirm') {
|
||||
deleteGameLogEntry(row);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
227
src/views/Login/Login.vue
Normal file
227
src/views/Login/Login.vue
Normal file
@@ -0,0 +1,227 @@
|
||||
<template>
|
||||
<div v-loading="loginForm.loading" class="x-login-container">
|
||||
<div class="x-login">
|
||||
<div style="position: fixed; top: 0; left: 0; margin: 5px">
|
||||
<el-tooltip placement="top" :content="t('view.login.updater')" :disabled="hideTooltips">
|
||||
<el-button
|
||||
type="default"
|
||||
size="mini"
|
||||
icon="el-icon-download"
|
||||
circle
|
||||
@click="showVRCXUpdateDialog"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip placement="top" :content="t('view.login.proxy_settings')" :disabled="hideTooltips">
|
||||
<el-button
|
||||
type="default"
|
||||
size="mini"
|
||||
icon="el-icon-connection"
|
||||
style="margin-left: 5px"
|
||||
circle
|
||||
@click="promptProxySettings"></el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="x-login-form-container">
|
||||
<div>
|
||||
<h2 style="font-weight: bold; text-align: center; margin: 0">{{ t('view.login.login') }}</h2>
|
||||
<el-form
|
||||
ref="loginFormRef"
|
||||
:model="loginForm"
|
||||
:rules="loginForm.rules"
|
||||
@submit.native.prevent="login()">
|
||||
<el-form-item :label="t('view.login.field.username')" prop="username" required>
|
||||
<el-input
|
||||
v-model="loginForm.username"
|
||||
name="username"
|
||||
:placeholder="t('view.login.field.username')"
|
||||
clearable></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
:label="t('view.login.field.password')"
|
||||
prop="password"
|
||||
required
|
||||
style="margin-top: 10px">
|
||||
<el-input
|
||||
v-model="loginForm.password"
|
||||
type="password"
|
||||
name="password"
|
||||
:placeholder="t('view.login.field.password')"
|
||||
clearable
|
||||
show-password></el-input>
|
||||
</el-form-item>
|
||||
<el-checkbox v-model="loginForm.saveCredentials" style="margin-top: 15px">{{
|
||||
t('view.login.field.saveCredentials')
|
||||
}}</el-checkbox>
|
||||
<el-checkbox
|
||||
v-model="enableCustomEndpoint"
|
||||
style="margin-top: 10px"
|
||||
@change="toggleCustomEndpoint"
|
||||
>{{ t('view.login.field.devEndpoint') }}</el-checkbox
|
||||
>
|
||||
<el-form-item
|
||||
v-if="enableCustomEndpoint"
|
||||
:label="t('view.login.field.endpoint')"
|
||||
prop="endpoint"
|
||||
style="margin-top: 10px">
|
||||
<el-input
|
||||
v-model="loginForm.endpoint"
|
||||
name="endpoint"
|
||||
:placeholder="API.endpointDomainVrchat"
|
||||
clearable></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="enableCustomEndpoint"
|
||||
:label="t('view.login.field.websocket')"
|
||||
prop="websocket"
|
||||
style="margin-top: 10px">
|
||||
<el-input
|
||||
v-model="loginForm.websocket"
|
||||
name="websocket"
|
||||
:placeholder="API.websocketDomainVrchat"
|
||||
clearable></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item style="margin-top: 15px">
|
||||
<el-button native-type="submit" type="primary" style="width: 100%">{{
|
||||
t('view.login.login')
|
||||
}}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-button
|
||||
type="primary"
|
||||
style="width: 100%"
|
||||
@click="openExternalLink('https://vrchat.com/register')"
|
||||
>{{ t('view.login.register') }}</el-button
|
||||
>
|
||||
</div>
|
||||
|
||||
<hr v-if="Object.keys(loginForm.savedCredentials).length !== 0" class="x-vertical-divider" />
|
||||
|
||||
<div v-if="Object.keys(loginForm.savedCredentials).length !== 0">
|
||||
<h2 style="font-weight: bold; text-align: center; margin: 0">
|
||||
{{ t('view.login.savedAccounts') }}
|
||||
</h2>
|
||||
<div class="x-scroll-wrapper" style="margin-top: 10px">
|
||||
<div class="x-saved-account-list">
|
||||
<div
|
||||
v-for="user in loginForm.savedCredentials"
|
||||
:key="user.user.id"
|
||||
class="x-friend-item"
|
||||
@click="relogin(user)">
|
||||
<div class="avatar">
|
||||
<img v-lazy="userImage(user.user)" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span class="name" v-text="user.user.displayName"></span>
|
||||
<span class="extra" v-text="user.user.username"></span>
|
||||
<span class="extra" v-text="user.loginParmas.endpoint"></span>
|
||||
</div>
|
||||
<el-button
|
||||
type="default"
|
||||
size="mini"
|
||||
icon="el-icon-delete"
|
||||
style="margin-left: 10px"
|
||||
circle
|
||||
@click.stop="deleteSavedLogin(user.user.id)"></el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="x-legal-notice-container">
|
||||
<div style="text-align: center; font-size: 12px">
|
||||
<p>
|
||||
<a class="x-link" @click="openExternalLink('https://vrchat.com/home/password')">{{
|
||||
t('view.login.forgotPassword')
|
||||
}}</a>
|
||||
</p>
|
||||
<p>
|
||||
© 2019-2025
|
||||
<a class="x-link" @click="openExternalLink('https://github.com/pypy-vrc')">pypy</a> &
|
||||
<a class="x-link" @click="openExternalLink('https://github.com/Natsumi-sama')">Natsumi</a>
|
||||
</p>
|
||||
<p>{{ t('view.settings.general.legal_notice.info') }}</p>
|
||||
<p>{{ t('view.settings.general.legal_notice.disclaimer1') }}</p>
|
||||
<p>{{ t('view.settings.general.legal_notice.disclaimer2') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'LoginPage'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup>
|
||||
import { inject, onBeforeUnmount, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
const { t } = useI18n();
|
||||
|
||||
const API = inject('API');
|
||||
const openExternalLink = inject('openExternalLink');
|
||||
const userImage = inject('userImage');
|
||||
|
||||
defineProps({
|
||||
hideTooltips: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
loginForm: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
enableCustomEndpoint: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
'showVRCXUpdateDialog',
|
||||
'promptProxySettings',
|
||||
'toggleCustomEndpoint',
|
||||
'deleteSavedLogin',
|
||||
'relogin',
|
||||
'login'
|
||||
]);
|
||||
|
||||
const loginFormRef = ref(null);
|
||||
|
||||
function showVRCXUpdateDialog() {
|
||||
emit('showVRCXUpdateDialog');
|
||||
}
|
||||
|
||||
function promptProxySettings() {
|
||||
emit('promptProxySettings');
|
||||
}
|
||||
|
||||
function toggleCustomEndpoint(...args) {
|
||||
emit('toggleCustomEndpoint', args);
|
||||
}
|
||||
|
||||
function deleteSavedLogin(userId) {
|
||||
emit('deleteSavedLogin', userId);
|
||||
}
|
||||
|
||||
function relogin(user) {
|
||||
emit('relogin', user);
|
||||
}
|
||||
|
||||
function login() {
|
||||
if (loginFormRef.value) {
|
||||
loginFormRef.value.validate((valid) => {
|
||||
valid && emit('login');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// Because v-if actually it is not required
|
||||
if (loginFormRef.value) {
|
||||
loginFormRef.value.resetFields();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
670
src/views/Notifications/Notification.vue
Normal file
670
src/views/Notifications/Notification.vue
Normal file
@@ -0,0 +1,670 @@
|
||||
<template>
|
||||
<div v-if="menuActiveIndex === 'notification'" v-loading="API.isNotificationsLoading" class="x-container">
|
||||
<data-tables v-bind="notificationTable" ref="notificationTableRef" class="notification-table">
|
||||
<template #tool>
|
||||
<div style="margin: 0 0 10px; display: flex; align-items: center">
|
||||
<el-select
|
||||
v-model="notificationTable.filters[0].value"
|
||||
multiple
|
||||
clearable
|
||||
style="flex: 1"
|
||||
:placeholder="t('view.notification.filter_placeholder')"
|
||||
@change="saveTableFilters">
|
||||
<el-option
|
||||
v-for="type in [
|
||||
'requestInvite',
|
||||
'invite',
|
||||
'requestInviteResponse',
|
||||
'inviteResponse',
|
||||
'friendRequest',
|
||||
'ignoredFriendRequest',
|
||||
'message',
|
||||
'boop',
|
||||
'groupChange',
|
||||
'group.announcement',
|
||||
'group.informative',
|
||||
'group.invite',
|
||||
'group.joinRequest',
|
||||
'group.transfer',
|
||||
'group.queueReady',
|
||||
'moderation.warning.group',
|
||||
'moderation.report.closed',
|
||||
'instance.closed'
|
||||
]"
|
||||
:key="type"
|
||||
:label="t('view.notification.filters.' + type)"
|
||||
:value="type" />
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model="notificationTable.filters[1].value"
|
||||
:placeholder="t('view.notification.search_placeholder')"
|
||||
style="flex: none; width: 150px; margin: 0 10px" />
|
||||
<el-tooltip
|
||||
placement="bottom"
|
||||
:content="t('view.notification.refresh_tooltip')"
|
||||
:disabled="hideTooltips">
|
||||
<el-button
|
||||
type="default"
|
||||
:loading="API.isNotificationsLoading"
|
||||
icon="el-icon-refresh"
|
||||
circle
|
||||
style="flex: none"
|
||||
@click="API.refreshNotifications()" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table-column :label="t('table.notification.date')" prop="created_at" sortable="custom" width="120">
|
||||
<template #default="scope">
|
||||
<el-tooltip placement="right">
|
||||
<template #content>
|
||||
<span>{{ scope.row.created_at | formatDate('long') }}</span>
|
||||
</template>
|
||||
<span>{{ scope.row.created_at | formatDate('short') }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('table.notification.type')" prop="type" width="180">
|
||||
<template #default="scope">
|
||||
<span
|
||||
v-if="scope.row.type === 'invite'"
|
||||
class="x-link"
|
||||
@click="showWorldDialog(scope.row.details.worldId)"
|
||||
v-text="t('view.notification.filters.' + scope.row.type)"></span>
|
||||
<el-tooltip
|
||||
v-else-if="scope.row.type === 'group.queueReady' || scope.row.type === 'instance.closed'"
|
||||
placement="top">
|
||||
<template #content>
|
||||
<location
|
||||
v-if="scope.row.location"
|
||||
:location="scope.row.location"
|
||||
:hint="scope.row.worldName"
|
||||
:grouphint="scope.row.groupName"
|
||||
:link="false" />
|
||||
</template>
|
||||
<span
|
||||
class="x-link"
|
||||
@click="showWorldDialog(scope.row.location)"
|
||||
v-text="t('view.notification.filters.' + scope.row.type)"></span>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
v-else-if="scope.row.link"
|
||||
placement="top"
|
||||
:content="scope.row.linkText"
|
||||
:disabled="hideTooltips">
|
||||
<span
|
||||
class="x-link"
|
||||
@click="openNotificationLink(scope.row.link)"
|
||||
v-text="t('view.notification.filters.' + scope.row.type)"></span>
|
||||
</el-tooltip>
|
||||
<span v-else v-text="t('view.notification.filters.' + scope.row.type)"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('table.notification.user_group')" prop="senderUsername" width="150">
|
||||
<template #default="scope">
|
||||
<template v-if="scope.row.type === 'groupChange'">
|
||||
<span
|
||||
class="x-link"
|
||||
@click="showGroupDialog(scope.row.senderUserId)"
|
||||
v-text="scope.row.senderUsername"></span>
|
||||
</template>
|
||||
<template v-else-if="scope.row.senderUserId">
|
||||
<span
|
||||
class="x-link"
|
||||
@click="showUserDialog(scope.row.senderUserId)"
|
||||
v-text="scope.row.senderUsername"></span>
|
||||
</template>
|
||||
<template v-else-if="scope.row.link && scope.row.data?.groupName">
|
||||
<span
|
||||
class="x-link"
|
||||
@click="openNotificationLink(scope.row.link)"
|
||||
v-text="scope.row.data?.groupName"></span>
|
||||
</template>
|
||||
<template v-else-if="scope.row.link">
|
||||
<span
|
||||
class="x-link"
|
||||
@click="openNotificationLink(scope.row.link)"
|
||||
v-text="scope.row.linkText"></span>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('table.notification.photo')" width="100" prop="photo">
|
||||
<template #default="scope">
|
||||
<template v-if="scope.row.details && scope.row.details.imageUrl">
|
||||
<el-popover placement="right" width="500px" trigger="click">
|
||||
<template #reference>
|
||||
<img
|
||||
class="x-link"
|
||||
:src="getSmallThumbnailUrl(scope.row.details.imageUrl)"
|
||||
style="flex: none; height: 50px; border-radius: 4px" />
|
||||
</template>
|
||||
<img
|
||||
v-lazy="scope.row.details.imageUrl"
|
||||
class="x-link"
|
||||
style="width: 500px"
|
||||
@click="showFullscreenImageDialog(scope.row.details.imageUrl)" />
|
||||
</el-popover>
|
||||
</template>
|
||||
<template v-else-if="scope.row.imageUrl">
|
||||
<el-popover placement="right" width="500px" trigger="click">
|
||||
<template #reference>
|
||||
<img
|
||||
class="x-link"
|
||||
:src="getSmallThumbnailUrl(scope.row.imageUrl)"
|
||||
style="flex: none; height: 50px; border-radius: 4px" />
|
||||
</template>
|
||||
<img
|
||||
v-lazy="scope.row.imageUrl"
|
||||
class="x-link"
|
||||
style="width: 500px"
|
||||
@click="showFullscreenImageDialog(scope.row.imageUrl)" />
|
||||
</el-popover>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('table.notification.message')" prop="message">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.type === 'invite'" style="display: flex">
|
||||
<location
|
||||
v-if="scope.row.details"
|
||||
:location="scope.row.details.worldId"
|
||||
:hint="scope.row.details.worldName"
|
||||
:grouphint="scope.row.details.groupName"
|
||||
:link="true" />
|
||||
<br v-if="scope.row.details" />
|
||||
</span>
|
||||
<el-tooltip
|
||||
v-if="
|
||||
scope.row.message &&
|
||||
scope.row.message !== `This is a generated invite to ${scope.row.details?.worldName}`
|
||||
"
|
||||
placement="top">
|
||||
<template #content>
|
||||
<pre
|
||||
class="extra"
|
||||
style="
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
margin: 0;
|
||||
"
|
||||
>{{ scope.row.message || '-' }}</pre
|
||||
>
|
||||
</template>
|
||||
<div v-text="scope.row.message"></div>
|
||||
</el-tooltip>
|
||||
<span
|
||||
v-else-if="scope.row.details && scope.row.details.inviteMessage"
|
||||
v-text="scope.row.details.inviteMessage"></span>
|
||||
<span
|
||||
v-else-if="scope.row.details && scope.row.details.requestMessage"
|
||||
v-text="scope.row.details.requestMessage"></span>
|
||||
<span
|
||||
v-else-if="scope.row.details && scope.row.details.responseMessage"
|
||||
v-text="scope.row.details.responseMessage"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('table.notification.action')" width="100" align="right">
|
||||
<template #default="scope">
|
||||
<template v-if="scope.row.senderUserId !== API.currentUser.id && !scope.row.$isExpired">
|
||||
<template v-if="scope.row.type === 'friendRequest'">
|
||||
<el-tooltip placement="top" content="Accept" :disabled="hideTooltips">
|
||||
<el-button
|
||||
type="text"
|
||||
icon="el-icon-check"
|
||||
style="color: #67c23a"
|
||||
size="mini"
|
||||
@click="acceptFriendRequestNotification(scope.row)" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template v-else-if="scope.row.type === 'invite'">
|
||||
<el-tooltip placement="top" content="Decline with message" :disabled="hideTooltips">
|
||||
<el-button
|
||||
type="text"
|
||||
icon="el-icon-chat-line-square"
|
||||
size="mini"
|
||||
@click="showSendInviteResponseDialog(scope.row)" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template v-else-if="scope.row.type === 'requestInvite'">
|
||||
<template
|
||||
v-if="lastLocation.location && isGameRunning && checkCanInvite(lastLocation.location)">
|
||||
<el-tooltip placement="top" content="Invite" :disabled="hideTooltips">
|
||||
<el-button
|
||||
type="text"
|
||||
icon="el-icon-check"
|
||||
style="color: #67c23a"
|
||||
size="mini"
|
||||
@click="acceptRequestInvite(scope.row)" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-tooltip placement="top" content="Decline with message" :disabled="hideTooltips">
|
||||
<el-button
|
||||
type="text"
|
||||
icon="el-icon-chat-line-square"
|
||||
size="mini"
|
||||
style="margin-left: 5px"
|
||||
@click="showSendInviteRequestResponseDialog(scope.row)" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
|
||||
<template v-if="scope.row.responses">
|
||||
<template v-for="response in scope.row.responses">
|
||||
<el-tooltip placement="top" :content="response.text" :disabled="hideTooltips">
|
||||
<el-button
|
||||
v-if="response.icon === 'check'"
|
||||
type="text"
|
||||
icon="el-icon-check"
|
||||
size="mini"
|
||||
style="margin-left: 5px"
|
||||
@click="
|
||||
sendNotificationResponse(scope.row.id, scope.row.responses, response.type)
|
||||
" />
|
||||
<el-button
|
||||
v-else-if="response.icon === 'cancel'"
|
||||
type="text"
|
||||
icon="el-icon-close"
|
||||
size="mini"
|
||||
style="margin-left: 5px"
|
||||
@click="
|
||||
sendNotificationResponse(scope.row.id, scope.row.responses, response.type)
|
||||
" />
|
||||
<el-button
|
||||
v-else-if="response.icon === 'ban'"
|
||||
type="text"
|
||||
icon="el-icon-circle-close"
|
||||
size="mini"
|
||||
style="margin-left: 5px"
|
||||
@click="
|
||||
sendNotificationResponse(scope.row.id, scope.row.responses, response.type)
|
||||
" />
|
||||
<el-button
|
||||
v-else-if="response.icon === 'bell-slash'"
|
||||
type="text"
|
||||
icon="el-icon-bell"
|
||||
size="mini"
|
||||
style="margin-left: 5px"
|
||||
@click="
|
||||
sendNotificationResponse(scope.row.id, scope.row.responses, response.type)
|
||||
" />
|
||||
<!--//el-button(-->
|
||||
<!--// v-else-if='response.icon === "reply" && scope.row.type === "boop"'-->
|
||||
<!--// type='text'-->
|
||||
<!--// icon='el-icon-chat-line-square'-->
|
||||
<!--// size='mini'-->
|
||||
<!--// style='margin-left: 5px'-->
|
||||
<!--// @click='showSendBoopDialog(scope.row.senderUserId)')-->
|
||||
<el-button
|
||||
v-else-if="response.icon === 'reply'"
|
||||
type="text"
|
||||
icon="el-icon-chat-line-square"
|
||||
size="mini"
|
||||
style="margin-left: 5px"
|
||||
@click="
|
||||
sendNotificationResponse(scope.row.id, scope.row.responses, response.type)
|
||||
" />
|
||||
<el-button
|
||||
v-else
|
||||
type="text"
|
||||
icon="el-icon-collection-tag"
|
||||
size="mini"
|
||||
style="margin-left: 5px"
|
||||
@click="
|
||||
sendNotificationResponse(scope.row.id, scope.row.responses, response.type)
|
||||
" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template
|
||||
v-if="
|
||||
scope.row.type !== 'requestInviteResponse' &&
|
||||
scope.row.type !== 'inviteResponse' &&
|
||||
scope.row.type !== 'message' &&
|
||||
scope.row.type !== 'boop' &&
|
||||
scope.row.type !== 'groupChange' &&
|
||||
!scope.row.type.includes('group.') &&
|
||||
!scope.row.type.includes('moderation.') &&
|
||||
!scope.row.type.includes('instance.')
|
||||
">
|
||||
<el-tooltip placement="top" content="Decline" :disabled="hideTooltips">
|
||||
<el-button
|
||||
v-if="shiftHeld"
|
||||
style="color: #f56c6c; margin-left: 5px"
|
||||
type="text"
|
||||
icon="el-icon-close"
|
||||
size="mini"
|
||||
@click="hideNotification(scope.row)" />
|
||||
<el-button
|
||||
v-else
|
||||
type="text"
|
||||
icon="el-icon-close"
|
||||
size="mini"
|
||||
style="margin-left: 5px"
|
||||
@click="hideNotificationPrompt(scope.row)" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</template>
|
||||
<template v-if="scope.row.type === 'group.queueReady'">
|
||||
<el-tooltip placement="top" content="Delete log" :disabled="hideTooltips">
|
||||
<el-button
|
||||
v-if="shiftHeld"
|
||||
style="color: #f56c6c; margin-left: 5px"
|
||||
type="text"
|
||||
icon="el-icon-close"
|
||||
size="mini"
|
||||
@click="deleteNotificationLog(scope.row)" />
|
||||
<el-button
|
||||
v-else
|
||||
type="text"
|
||||
icon="el-icon-delete"
|
||||
size="mini"
|
||||
style="margin-left: 5px"
|
||||
@click="deleteNotificationLogPrompt(scope.row)" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
|
||||
<template
|
||||
v-if="
|
||||
scope.row.type !== 'friendRequest' &&
|
||||
scope.row.type !== 'ignoredFriendRequest' &&
|
||||
!scope.row.type.includes('group.') &&
|
||||
!scope.row.type.includes('moderation.')
|
||||
">
|
||||
<el-tooltip placement="top" content="Delete log" :disabled="hideTooltips">
|
||||
<el-button
|
||||
v-if="shiftHeld"
|
||||
style="color: #f56c6c; margin-left: 5px"
|
||||
type="text"
|
||||
icon="el-icon-close"
|
||||
size="mini"
|
||||
@click="deleteNotificationLog(scope.row)" />
|
||||
<el-button
|
||||
v-else
|
||||
type="text"
|
||||
icon="el-icon-delete"
|
||||
size="mini"
|
||||
style="margin-left: 5px"
|
||||
@click="deleteNotificationLogPrompt(scope.row)" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</data-tables>
|
||||
<SendInviteResponseDialog
|
||||
:send-invite-response-dialog-visible.sync="sendInviteResponseDialogVisible"
|
||||
:invite-response-message-table="inviteResponseMessageTable"
|
||||
:upload-image="uploadImage"
|
||||
@invite-image-upload="inviteImageUpload" />
|
||||
<SendInviteRequestResponseDialog
|
||||
:send-invite-request-response-dialog-visible.sync="sendInviteRequestResponseDialogVisible"
|
||||
:invite-request-response-message-table="inviteRequestResponseMessageTable"
|
||||
:upload-image="uploadImage"
|
||||
@invite-image-upload="inviteImageUpload" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'NotificationTab'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup>
|
||||
import { getCurrentInstance, inject, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { friendRequest, inviteMessagesRequest, notificationRequest, worldRequest } from '../../api';
|
||||
import utils from '../../classes/utils';
|
||||
import { parseLocation } from '../../composables/instance/utils';
|
||||
import { convertFileUrlToImageUrl } from '../../composables/shared/utils';
|
||||
import configRepository from '../../service/config';
|
||||
import database from '../../service/database';
|
||||
import SendInviteRequestResponseDialog from './dialogs/SendInviteRequestResponseDialog.vue';
|
||||
import SendInviteResponseDialog from './dialogs/SendInviteResponseDialog.vue';
|
||||
import Location from '../../components/Location.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { $confirm, $message } = getCurrentInstance().proxy;
|
||||
|
||||
const API = inject('API');
|
||||
const showWorldDialog = inject('showWorldDialog');
|
||||
const showGroupDialog = inject('showGroupDialog');
|
||||
const showUserDialog = inject('showUserDialog');
|
||||
const showFullscreenImageDialog = inject('showFullscreenImageDialog');
|
||||
|
||||
const props = defineProps({
|
||||
menuActiveIndex: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
notificationTable: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
shiftHeld: { type: Boolean, default: false },
|
||||
hideTooltips: { type: Boolean, default: false },
|
||||
lastLocation: { type: Object, default: () => ({}) },
|
||||
inviteResponseMessageTable: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
uploadImage: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
lastLocationDestination: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
isGameRunning: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
checkCanInvite: {
|
||||
type: Function,
|
||||
default: () => true
|
||||
},
|
||||
inviteRequestResponseMessageTable: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['inviteImageUpload', 'clearInviteImageUpload']);
|
||||
|
||||
const sendInviteResponseDialog = ref({
|
||||
message: '',
|
||||
messageSlot: 0,
|
||||
invite: {}
|
||||
});
|
||||
|
||||
const sendInviteResponseDialogVisible = ref(false);
|
||||
|
||||
const sendInviteRequestResponseDialogVisible = ref(false);
|
||||
|
||||
function inviteImageUpload(event) {
|
||||
emit('inviteImageUpload', event);
|
||||
}
|
||||
|
||||
function saveTableFilters() {
|
||||
configRepository.setString(
|
||||
'VRCX_notificationTableFilters',
|
||||
JSON.stringify(props.notificationTable.filters[0].value)
|
||||
);
|
||||
}
|
||||
|
||||
function openNotificationLink(link) {
|
||||
if (!link) {
|
||||
return;
|
||||
}
|
||||
const data = link.split(':');
|
||||
if (!data.length) {
|
||||
return;
|
||||
}
|
||||
switch (data[0]) {
|
||||
case 'group':
|
||||
showGroupDialog(data[1]);
|
||||
break;
|
||||
case 'user':
|
||||
showUserDialog(data[1]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function getSmallThumbnailUrl(url) {
|
||||
return convertFileUrlToImageUrl(url);
|
||||
}
|
||||
|
||||
function acceptFriendRequestNotification(row) {
|
||||
// FIXME: 메시지 수정
|
||||
$confirm('Continue? Accept Friend Request', 'Confirm', {
|
||||
confirmButtonText: 'Confirm',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'info',
|
||||
callback: (action) => {
|
||||
if (action === 'confirm') {
|
||||
notificationRequest.acceptFriendRequestNotification({
|
||||
notificationId: row.id
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showSendInviteResponseDialog(invite) {
|
||||
sendInviteResponseDialog.value = {
|
||||
invite
|
||||
};
|
||||
inviteMessagesRequest.refreshInviteMessageTableData('response');
|
||||
clearInviteImageUpload();
|
||||
sendInviteResponseDialogVisible.value = true;
|
||||
}
|
||||
|
||||
function clearInviteImageUpload() {
|
||||
emit('clearInviteImageUpload');
|
||||
}
|
||||
|
||||
function acceptRequestInvite(row) {
|
||||
$confirm('Continue? Send Invite', 'Confirm', {
|
||||
confirmButtonText: 'Confirm',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'info',
|
||||
callback: (action) => {
|
||||
if (action === 'confirm') {
|
||||
let currentLocation = props.lastLocation.location;
|
||||
// todo
|
||||
if (props.lastLocation.location === 'traveling') {
|
||||
currentLocation = props.lastLocationDestination;
|
||||
}
|
||||
const L = parseLocation(currentLocation);
|
||||
worldRequest
|
||||
.getCachedWorld({
|
||||
worldId: L.worldId
|
||||
})
|
||||
.then((args) => {
|
||||
notificationRequest
|
||||
.sendInvite(
|
||||
{
|
||||
instanceId: L.tag,
|
||||
worldId: L.tag,
|
||||
worldName: args.ref.name,
|
||||
rsvp: true
|
||||
},
|
||||
row.senderUserId
|
||||
)
|
||||
.then((_args) => {
|
||||
$message('Invite sent');
|
||||
notificationRequest.hideNotification({
|
||||
notificationId: row.id
|
||||
});
|
||||
return _args;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showSendInviteRequestResponseDialog(invite) {
|
||||
sendInviteResponseDialog.value = {
|
||||
invite
|
||||
};
|
||||
inviteMessagesRequest.refreshInviteMessageTableData('requestResponse');
|
||||
clearInviteImageUpload();
|
||||
sendInviteRequestResponseDialogVisible.value = true;
|
||||
}
|
||||
|
||||
function sendNotificationResponse(notificationId, responses, responseType) {
|
||||
if (!Array.isArray(responses) || responses.length === 0) {
|
||||
return null;
|
||||
}
|
||||
let responseData = '';
|
||||
for (let i = 0; i < responses.length; i++) {
|
||||
if (responses[i].type === responseType) {
|
||||
responseData = responses[i].data;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return notificationRequest.sendNotificationResponse({
|
||||
notificationId,
|
||||
responseType,
|
||||
responseData
|
||||
});
|
||||
}
|
||||
|
||||
function hideNotification(row) {
|
||||
if (row.type === 'ignoredFriendRequest') {
|
||||
friendRequest.deleteHiddenFriendRequest(
|
||||
{
|
||||
notificationId: row.id
|
||||
},
|
||||
row.senderUserId
|
||||
);
|
||||
} else {
|
||||
notificationRequest.hideNotification({
|
||||
notificationId: row.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function hideNotificationPrompt(row) {
|
||||
$confirm(`Continue? Decline ${row.type}`, 'Confirm', {
|
||||
confirmButtonText: 'Confirm',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'info',
|
||||
callback: (action) => {
|
||||
if (action === 'confirm') {
|
||||
hideNotification(row);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deleteNotificationLog(row) {
|
||||
utils.removeFromArray(props.notificationTable.data, row);
|
||||
if (row.type !== 'friendRequest' && row.type !== 'ignoredFriendRequest') {
|
||||
database.deleteNotification(row.id);
|
||||
}
|
||||
}
|
||||
|
||||
function deleteNotificationLogPrompt(row) {
|
||||
$confirm(`Continue? Delete ${row.type}`, 'Confirm', {
|
||||
confirmButtonText: 'Confirm',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'info',
|
||||
callback: (action) => {
|
||||
if (action === 'confirm') {
|
||||
deleteNotificationLog(row);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<safe-dialog
|
||||
class="x-dialog"
|
||||
:visible.sync="editAndSendInviteResponseDialog.visible"
|
||||
:title="t('dialog.edit_send_invite_response_message.header')"
|
||||
width="400px"
|
||||
append-to-body>
|
||||
<div style="font-size: 12px">
|
||||
<span>{{ t('dialog.edit_send_invite_response_message.description') }}</span>
|
||||
</div>
|
||||
<el-input
|
||||
v-model="editAndSendInviteResponseDialog.newMessage"
|
||||
type="textarea"
|
||||
size="mini"
|
||||
maxlength="64"
|
||||
show-word-limit
|
||||
:autosize="{ minRows: 2, maxRows: 5 }"
|
||||
placeholder=""
|
||||
style="margin-top: 10px">
|
||||
</el-input>
|
||||
<template #footer>
|
||||
<el-button type="small" @click="cancelEditAndSendInviteResponse">{{
|
||||
t('dialog.edit_send_invite_response_message.cancel')
|
||||
}}</el-button>
|
||||
<el-button type="primary" size="small" @click="saveEditAndSendInviteResponse">{{
|
||||
t('dialog.edit_send_invite_response_message.send')
|
||||
}}</el-button>
|
||||
</template>
|
||||
</safe-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getCurrentInstance, inject } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { inviteMessagesRequest, notificationRequest } from '../../../api';
|
||||
|
||||
const { t } = useI18n();
|
||||
const instance = getCurrentInstance();
|
||||
const $message = instance.proxy.$message;
|
||||
|
||||
const API = inject('API');
|
||||
|
||||
const props = defineProps({
|
||||
editAndSendInviteResponseDialog: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
uploadImage: {
|
||||
type: String
|
||||
},
|
||||
sendInviteResponseDialog: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['closeInviteDialog', 'update:editAndSendInviteResponseDialog']);
|
||||
|
||||
function cancelEditAndSendInviteResponse() {
|
||||
emit('update:editAndSendInviteResponseDialog', { ...props.editAndSendInviteResponseDialog, visible: false });
|
||||
}
|
||||
|
||||
async function saveEditAndSendInviteResponse() {
|
||||
const D = props.editAndSendInviteResponseDialog;
|
||||
D.visible = false;
|
||||
const messageType = D.messageType;
|
||||
const slot = D.inviteMessage.slot;
|
||||
if (D.inviteMessage.message !== D.newMessage) {
|
||||
const params = {
|
||||
message: D.newMessage
|
||||
};
|
||||
await inviteMessagesRequest
|
||||
.editInviteMessage(params, messageType, slot)
|
||||
.catch((err) => {
|
||||
throw err;
|
||||
})
|
||||
.then((args) => {
|
||||
API.$emit(`INVITE:${messageType.toUpperCase()}`, args);
|
||||
if (args.json[slot].message === D.inviteMessage.message) {
|
||||
$message({
|
||||
message: "VRChat API didn't update message, try again",
|
||||
type: 'error'
|
||||
});
|
||||
throw new Error("VRChat API didn't update message, try again");
|
||||
} else {
|
||||
$message('Invite message updated');
|
||||
}
|
||||
return args;
|
||||
});
|
||||
}
|
||||
const I = props.sendInviteResponseDialog;
|
||||
const params = {
|
||||
responseSlot: slot,
|
||||
rsvp: true
|
||||
};
|
||||
if (props.uploadImage) {
|
||||
notificationRequest
|
||||
.sendInviteResponsePhoto(params, I.invite.id)
|
||||
.catch((err) => {
|
||||
throw err;
|
||||
})
|
||||
.then((args) => {
|
||||
notificationRequest.hideNotification({
|
||||
notificationId: I.invite.id
|
||||
});
|
||||
$message({
|
||||
message: 'Invite response message sent',
|
||||
type: 'success'
|
||||
});
|
||||
|
||||
emit('closeInviteDialog');
|
||||
return args;
|
||||
});
|
||||
} else {
|
||||
notificationRequest
|
||||
.sendInviteResponse(params, I.invite.id)
|
||||
.catch((err) => {
|
||||
throw err;
|
||||
})
|
||||
.then((args) => {
|
||||
notificationRequest.hideNotification({
|
||||
notificationId: I.invite.id
|
||||
});
|
||||
$message({
|
||||
message: 'Invite response message sent',
|
||||
type: 'success'
|
||||
});
|
||||
emit('closeInviteDialog');
|
||||
return args;
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<safe-dialog
|
||||
class="x-dialog"
|
||||
:visible="sendInviteRequestResponseDialogVisible"
|
||||
:title="t('dialog.invite_request_response_message.header')"
|
||||
width="800px"
|
||||
append-to-body
|
||||
@close="cancelSendInviteRequestResponse">
|
||||
<template v-if="API.currentUser.$isVRCPlus">
|
||||
<input class="inviteImageUploadButton" type="file" accept="image/*" @change="inviteImageUpload" />
|
||||
</template>
|
||||
|
||||
<data-tables
|
||||
v-bind="inviteRequestResponseMessageTable"
|
||||
style="margin-top: 10px; cursor: pointer"
|
||||
@row-click="showSendInviteResponseConfirmDialog">
|
||||
<el-table-column :label="t('table.profile.invite_messages.slot')" prop="slot" sortable="custom" width="70">
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('table.profile.invite_messages.message')" prop="message"> </el-table-column>
|
||||
<el-table-column
|
||||
:label="t('table.profile.invite_messages.cool_down')"
|
||||
prop="updatedAt"
|
||||
sortable="custom"
|
||||
width="110"
|
||||
align="right">
|
||||
<template #default="scope">
|
||||
<countdown-timer :datetime="scope.row.updatedAt" :hours="1"></countdown-timer>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('table.profile.invite_messages.action')" width="70" align="right">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="text"
|
||||
icon="el-icon-edit"
|
||||
size="mini"
|
||||
@click.stop="showEditAndSendInviteResponseDialog('requestResponse', scope.row)">
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</data-tables>
|
||||
|
||||
<template #footer>
|
||||
<el-button type="small" @click="cancelSendInviteRequestResponse">
|
||||
{{ t('dialog.invite_request_response_message.cancel') }}
|
||||
</el-button>
|
||||
<el-button type="small" @click="API.refreshInviteMessageTableData('requestResponse')">
|
||||
{{ t('dialog.invite_request_response_message.refresh') }}
|
||||
</el-button>
|
||||
</template>
|
||||
<EditAndSendInviteResponseDialog
|
||||
:edit-and-send-invite-response-dialog.sync="editAndSendInviteResponseDialog"
|
||||
:upload-image="uploadImage"
|
||||
:send-invite-response-dialog="sendInviteResponseDialog" />
|
||||
<SendInviteResponseConfirmDialog
|
||||
:send-invite-response-dialog.sync="sendInviteResponseConfirmDialog"
|
||||
:upload-image="uploadImage"
|
||||
:send-invite-response-confirm-dialog="sendInviteResponseDialog" />
|
||||
</safe-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import EditAndSendInviteResponseDialog from './EditAndSendInviteResponseDialog.vue';
|
||||
import SendInviteResponseConfirmDialog from './SendInviteResponseConfirmDialog.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const API = inject('API');
|
||||
|
||||
defineProps({
|
||||
sendInviteRequestResponseDialogVisible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
inviteRequestResponseMessageTable: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
uploadImage: {
|
||||
type: String
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:sendInviteRequestResponseDialogVisible', 'inviteImageUpload']);
|
||||
|
||||
const editAndSendInviteResponseDialog = ref({
|
||||
visible: false,
|
||||
inviteMessage: {},
|
||||
messageType: '',
|
||||
newMessage: ''
|
||||
});
|
||||
|
||||
const sendInviteResponseConfirmDialog = ref({
|
||||
visible: false
|
||||
});
|
||||
|
||||
const sendInviteResponseDialog = ref({
|
||||
message: '',
|
||||
messageSlot: 0,
|
||||
invite: {}
|
||||
});
|
||||
|
||||
function inviteImageUpload(event) {
|
||||
emit('inviteImageUpload', event);
|
||||
}
|
||||
|
||||
function showSendInviteResponseConfirmDialog(row) {
|
||||
sendInviteResponseConfirmDialog.value.visible = true;
|
||||
sendInviteResponseDialog.value.messageSlot = row.slot;
|
||||
}
|
||||
|
||||
function showEditAndSendInviteResponseDialog(messageType, inviteMessage) {
|
||||
editAndSendInviteResponseDialog.value = {
|
||||
newMessage: inviteMessage.message,
|
||||
visible: true,
|
||||
messageType,
|
||||
inviteMessage
|
||||
};
|
||||
}
|
||||
|
||||
function cancelSendInviteRequestResponse() {
|
||||
emit('update:sendInviteRequestResponseDialogVisible', false);
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<safe-dialog
|
||||
class="x-dialog"
|
||||
:visible="sendInviteResponseConfirmDialog.visible"
|
||||
:title="t('dialog.invite_response_message.header')"
|
||||
width="400px"
|
||||
append-to-body
|
||||
@close="cancelInviteResponseConfirm">
|
||||
<div style="font-size: 12px">
|
||||
<span>{{ t('dialog.invite_response_message.confirmation') }}</span>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button type="small" @click="cancelInviteResponseConfirm">{{
|
||||
t('dialog.invite_response_message.cancel')
|
||||
}}</el-button>
|
||||
<el-button type="primary" size="small" @click="sendInviteResponseConfirm">{{
|
||||
t('dialog.invite_response_message.confirm')
|
||||
}}</el-button>
|
||||
</template>
|
||||
</safe-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getCurrentInstance } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { notificationRequest } from '../../../api';
|
||||
const { t } = useI18n();
|
||||
|
||||
const instance = getCurrentInstance();
|
||||
const $message = instance.proxy.$message;
|
||||
|
||||
const props = defineProps({
|
||||
sendInviteResponseConfirmDialog: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
uploadImage: {
|
||||
type: String
|
||||
},
|
||||
sendInviteResponseDialog: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:sendInviteResponseConfirmDialog', 'closeInviteDialog']);
|
||||
|
||||
function cancelInviteResponseConfirm() {
|
||||
emit('update:sendInviteResponseConfirmDialog', { visible: false });
|
||||
}
|
||||
|
||||
function sendInviteResponseConfirm() {
|
||||
const D = props.sendInviteResponseDialog;
|
||||
const params = {
|
||||
responseSlot: D.messageSlot,
|
||||
rsvp: true
|
||||
};
|
||||
if (props.uploadImage) {
|
||||
notificationRequest
|
||||
.sendInviteResponsePhoto(params, D.invite.id, D.messageType)
|
||||
.catch((err) => {
|
||||
throw err;
|
||||
})
|
||||
.then((args) => {
|
||||
notificationRequest.hideNotification({
|
||||
notificationId: D.invite.id
|
||||
});
|
||||
$message({
|
||||
message: 'Invite response photo message sent',
|
||||
type: 'success'
|
||||
});
|
||||
return args;
|
||||
});
|
||||
} else {
|
||||
notificationRequest
|
||||
.sendInviteResponse(params, D.invite.id, D.messageType)
|
||||
.catch((err) => {
|
||||
throw err;
|
||||
})
|
||||
.then((args) => {
|
||||
notificationRequest.hideNotification({
|
||||
notificationId: D.invite.id
|
||||
});
|
||||
$message({
|
||||
message: 'Invite response message sent',
|
||||
type: 'success'
|
||||
});
|
||||
return args;
|
||||
});
|
||||
}
|
||||
cancelInviteResponseConfirm();
|
||||
emit('closeInviteDialog');
|
||||
}
|
||||
</script>
|
||||
126
src/views/Notifications/dialogs/SendInviteResponseDialog.vue
Normal file
126
src/views/Notifications/dialogs/SendInviteResponseDialog.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<safe-dialog
|
||||
class="x-dialog"
|
||||
:visible="sendInviteResponseDialogVisible"
|
||||
:title="t('dialog.invite_response_message.header')"
|
||||
width="800px"
|
||||
append-to-body
|
||||
@close="cancelSendInviteResponse">
|
||||
<template v-if="API.currentUser.$isVRCPlus">
|
||||
<input class="inviteImageUploadButton" type="file" accept="image/*" @change="inviteImageUpload" />
|
||||
</template>
|
||||
|
||||
<data-tables
|
||||
v-bind="inviteResponseMessageTable"
|
||||
style="margin-top: 10px; cursor: pointer"
|
||||
@row-click="showSendInviteResponseConfirmDialog">
|
||||
<el-table-column
|
||||
:label="t('table.profile.invite_messages.slot')"
|
||||
prop="slot"
|
||||
sortable="custom"
|
||||
width="70" />
|
||||
<el-table-column :label="t('table.profile.invite_messages.message')" prop="message" />
|
||||
<el-table-column
|
||||
:label="t('table.profile.invite_messages.cool_down')"
|
||||
prop="updatedAt"
|
||||
sortable="custom"
|
||||
width="110"
|
||||
align="right">
|
||||
<template #default="scope">
|
||||
<countdown-timer :datetime="scope.row.updatedAt" :hours="1" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('table.profile.invite_messages.action')" width="70" align="right">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="text"
|
||||
icon="el-icon-edit"
|
||||
size="mini"
|
||||
@click.stop="showEditAndSendInviteResponseDialog('response', scope.row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</data-tables>
|
||||
|
||||
<template #footer>
|
||||
<el-button type="small" @click="cancelSendInviteResponse">{{
|
||||
t('dialog.invite_response_message.cancel')
|
||||
}}</el-button>
|
||||
<el-button type="small" @click="API.refreshInviteMessageTableData('response')">{{
|
||||
t('dialog.invite_response_message.refresh')
|
||||
}}</el-button>
|
||||
</template>
|
||||
<EditAndSendInviteResponseDialog
|
||||
:edit-and-send-invite-response-dialog.sync="editAndSendInviteResponseDialog"
|
||||
:upload-image="uploadImage"
|
||||
:send-invite-response-confirm-dialog="sendInviteResponseDialog" />
|
||||
<SendInviteResponseConfirmDialog
|
||||
:send-invite-response-dialog.sync="sendInviteResponseConfirmDialog"
|
||||
:upload-image="uploadImage"
|
||||
:send-invite-response-confirm-dialog="sendInviteResponseDialog" />
|
||||
</safe-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import EditAndSendInviteResponseDialog from './EditAndSendInviteResponseDialog.vue';
|
||||
import SendInviteResponseConfirmDialog from './SendInviteResponseConfirmDialog.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const API = inject('API');
|
||||
defineProps({
|
||||
sendInviteResponseDialogVisible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
inviteResponseMessageTable: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
uploadImage: {
|
||||
type: String
|
||||
}
|
||||
});
|
||||
|
||||
const editAndSendInviteResponseDialog = ref({
|
||||
visible: false,
|
||||
inviteMessage: {},
|
||||
messageType: '',
|
||||
newMessage: ''
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:sendInviteResponseDialogVisible', 'inviteImageUpload']);
|
||||
|
||||
const sendInviteResponseConfirmDialog = ref({
|
||||
visible: false
|
||||
});
|
||||
|
||||
const sendInviteResponseDialog = ref({
|
||||
message: '',
|
||||
messageSlot: 0,
|
||||
invite: {}
|
||||
});
|
||||
|
||||
function cancelSendInviteResponse() {
|
||||
emit('update:sendInviteResponseDialogVisible', false);
|
||||
}
|
||||
|
||||
function showEditAndSendInviteResponseDialog(messageType, inviteMessage) {
|
||||
editAndSendInviteResponseDialog.value = {
|
||||
newMessage: inviteMessage.message,
|
||||
visible: true,
|
||||
messageType,
|
||||
inviteMessage
|
||||
};
|
||||
}
|
||||
|
||||
function inviteImageUpload(event) {
|
||||
emit('inviteImageUpload', event);
|
||||
}
|
||||
|
||||
function showSendInviteResponseConfirmDialog(row) {
|
||||
sendInviteResponseConfirmDialog.value.visible = true;
|
||||
sendInviteResponseDialog.value.messageSlot = row.slot;
|
||||
}
|
||||
</script>
|
||||
984
src/views/PlayerList/PlayerList.vue
Normal file
984
src/views/PlayerList/PlayerList.vue
Normal file
@@ -0,0 +1,984 @@
|
||||
<template>
|
||||
<div v-if="menuActiveIndex === 'playerList'" class="x-container" style="padding-top: 5px">
|
||||
<div style="display: flex; flex-direction: column; height: 100%">
|
||||
<div v-if="currentInstanceWorld.ref.id" style="display: flex">
|
||||
<el-popover placement="right" width="500px" trigger="click" style="height: 120px">
|
||||
<img
|
||||
slot="reference"
|
||||
v-lazy="currentInstanceWorld.ref.thumbnailImageUrl"
|
||||
class="x-link"
|
||||
style="flex: none; width: 160px; height: 120px; border-radius: 4px" />
|
||||
<img
|
||||
v-lazy="currentInstanceWorld.ref.imageUrl"
|
||||
class="x-link"
|
||||
style="width: 500px; height: 375px"
|
||||
@click="showFullscreenImageDialog(currentInstanceWorld.ref.imageUrl)" />
|
||||
</el-popover>
|
||||
<div style="margin-left: 10px; display: flex; flex-direction: column; min-width: 320px; width: 100%">
|
||||
<div>
|
||||
<span
|
||||
class="x-link"
|
||||
style="
|
||||
font-weight: bold;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
"
|
||||
@click="showWorldDialog(currentInstanceWorld.ref.id)">
|
||||
<i
|
||||
v-show="
|
||||
API.currentUser.$homeLocation &&
|
||||
API.currentUser.$homeLocation.worldId === currentInstanceWorld.ref.id
|
||||
"
|
||||
class="el-icon-s-home"
|
||||
style="margin-right: 5px"></i>
|
||||
{{ currentInstanceWorld.ref.name }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
class="x-link x-grey"
|
||||
style="font-family: monospace"
|
||||
@click="showUserDialog(currentInstanceWorld.ref.authorId)"
|
||||
v-text="currentInstanceWorld.ref.authorName"></span>
|
||||
</div>
|
||||
<div style="margin-top: 5px">
|
||||
<el-tag
|
||||
v-if="currentInstanceWorld.ref.$isLabs"
|
||||
type="primary"
|
||||
effect="plain"
|
||||
size="mini"
|
||||
style="margin-right: 5px"
|
||||
>{{ t('dialog.world.tags.labs') }}</el-tag
|
||||
>
|
||||
<el-tag
|
||||
v-else-if="currentInstanceWorld.ref.releaseStatus === 'public'"
|
||||
type="success"
|
||||
effect="plain"
|
||||
size="mini"
|
||||
style="margin-right: 5px"
|
||||
>{{ t('dialog.world.tags.public') }}</el-tag
|
||||
>
|
||||
<el-tag
|
||||
v-else-if="currentInstanceWorld.ref.releaseStatus === 'private'"
|
||||
type="danger"
|
||||
effect="plain"
|
||||
size="mini"
|
||||
style="margin-right: 5px"
|
||||
>{{ t('dialog.world.tags.private') }}</el-tag
|
||||
>
|
||||
<el-tag
|
||||
v-if="currentInstanceWorld.isPC"
|
||||
class="x-tag-platform-pc"
|
||||
type="info"
|
||||
effect="plain"
|
||||
size="mini"
|
||||
style="margin-right: 5px"
|
||||
>PC
|
||||
<span
|
||||
v-if="currentInstanceWorld.bundleSizes['standalonewindows']"
|
||||
class="x-grey"
|
||||
style="margin-left: 5px; border-left: inherit; padding-left: 5px"
|
||||
>{{ currentInstanceWorld.bundleSizes['standalonewindows'].fileSize }}</span
|
||||
>
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-if="currentInstanceWorld.isQuest"
|
||||
class="x-tag-platform-quest"
|
||||
type="info"
|
||||
effect="plain"
|
||||
size="mini"
|
||||
style="margin-right: 5px"
|
||||
>Android
|
||||
<span
|
||||
v-if="currentInstanceWorld.bundleSizes['android']"
|
||||
class="x-grey"
|
||||
style="margin-left: 5px; border-left: inherit; padding-left: 5px"
|
||||
>{{ currentInstanceWorld.bundleSizes['android'].fileSize }}</span
|
||||
>
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-if="currentInstanceWorld.isIOS"
|
||||
class="x-tag-platform-ios"
|
||||
type="info"
|
||||
effect="plain"
|
||||
size="mini"
|
||||
style="margin-right: 5px"
|
||||
>iOS
|
||||
<span
|
||||
v-if="currentInstanceWorld.bundleSizes['ios']"
|
||||
class="x-grey"
|
||||
style="margin-left: 5px; border-left: inherit; padding-left: 5px"
|
||||
>{{ currentInstanceWorld.bundleSizes['ios'].fileSize }}</span
|
||||
>
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-if="currentInstanceWorld.avatarScalingDisabled"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
size="mini"
|
||||
style="margin-right: 5px; margin-top: 5px"
|
||||
>{{ t('dialog.world.tags.avatar_scaling_disabled') }}</el-tag
|
||||
>
|
||||
<el-tag
|
||||
v-if="currentInstanceWorld.inCache"
|
||||
type="info"
|
||||
effect="plain"
|
||||
size="mini"
|
||||
style="margin-right: 5px">
|
||||
<span>{{ currentInstanceWorld.cacheSize }} {{ t('dialog.world.tags.cache') }}</span>
|
||||
</el-tag>
|
||||
</div>
|
||||
<div style="margin-top: 5px">
|
||||
<location-world
|
||||
:locationobject="currentInstanceLocation"
|
||||
:currentuserid="API.currentUser.id"
|
||||
@show-launch-dialog="showLaunchDialog"></location-world>
|
||||
<span v-if="lastLocation.playerList.size > 0" style="margin-left: 5px">
|
||||
{{ lastLocation.playerList.size }}
|
||||
<template v-if="lastLocation.friendList.size > 0"
|
||||
>({{ lastLocation.friendList.size }})</template
|
||||
>
|
||||
― <timer v-if="lastLocation.date" :epoch="lastLocation.date"></timer>
|
||||
</span>
|
||||
</div>
|
||||
<div style="margin-top: 5px">
|
||||
<span
|
||||
v-show="currentInstanceWorld.ref.name !== currentInstanceWorld.ref.description"
|
||||
:style="{
|
||||
fontSize: '12px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
display: '-webkit-box',
|
||||
WebkitBoxOrient: 'vertical',
|
||||
WebkitLineClamp: currentInstanceWorldDescriptionExpanded ? 'none' : '2'
|
||||
}"
|
||||
v-text="currentInstanceWorld.ref.description"></span>
|
||||
<div style="display: flex; justify-content: end">
|
||||
<el-button
|
||||
v-if="
|
||||
currentInstanceWorld.ref.description.length > 50 &&
|
||||
!currentInstanceWorldDescriptionExpanded
|
||||
"
|
||||
type="text"
|
||||
size="mini"
|
||||
@click="currentInstanceWorldDescriptionExpanded = true"
|
||||
>{{ !currentInstanceWorldDescriptionExpanded && 'Show more' }}</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; margin-left: 20px">
|
||||
<div class="x-friend-item" style="cursor: default">
|
||||
<div class="detail">
|
||||
<span class="name">{{ t('dialog.world.info.capacity') }}</span>
|
||||
<span class="extra"
|
||||
>{{ currentInstanceWorld.ref.recommendedCapacity | commaNumber }} ({{
|
||||
currentInstanceWorld.ref.capacity | commaNumber
|
||||
}})</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="x-friend-item" style="cursor: default">
|
||||
<div class="detail">
|
||||
<span class="name">{{ t('dialog.world.info.last_updated') }}</span>
|
||||
<span class="extra">{{ currentInstanceWorld.lastUpdated | formatDate('long') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="x-friend-item" style="cursor: default">
|
||||
<div class="detail">
|
||||
<span class="name">{{ t('dialog.world.info.created_at') }}</span>
|
||||
<span class="extra">{{ currentInstanceWorld.ref.created_at | formatDate('long') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="photonLoggingEnabled" class="photon-event-table">
|
||||
<div style="position: absolute; width: 600px; margin-left: 215px; z-index: 1">
|
||||
<el-select
|
||||
v-model="photonEventTableTypeFilter"
|
||||
multiple
|
||||
clearable
|
||||
collapse-tags
|
||||
style="flex: 1; width: 220px"
|
||||
:placeholder="t('view.player_list.photon.filter_placeholder')"
|
||||
@change="photonEventTableFilterChange">
|
||||
<el-option
|
||||
v-for="type in photonEventTableTypeFilterList"
|
||||
:key="type"
|
||||
:label="type"
|
||||
:value="type"></el-option>
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model="photonEventTableFilter"
|
||||
:placeholder="t('view.player_list.photon.search_placeholder')"
|
||||
clearable
|
||||
style="width: 150px; margin-left: 10px"
|
||||
@input="photonEventTableFilterChange"></el-input>
|
||||
<el-button style="margin-left: 10px" @click="showChatboxBlacklistDialog">{{
|
||||
t('view.player_list.photon.chatbox_blacklist')
|
||||
}}</el-button>
|
||||
<el-tooltip
|
||||
placement="bottom"
|
||||
:content="t('view.player_list.photon.status_tooltip')"
|
||||
:disabled="hideTooltips">
|
||||
<div
|
||||
style="
|
||||
display: inline-block;
|
||||
margin-left: 15px;
|
||||
font-size: 14px;
|
||||
vertical-align: text-top;
|
||||
margin-top: 1px;
|
||||
">
|
||||
<span v-if="ipcEnabled && !photonEventIcon">🟢</span>
|
||||
<span v-else-if="ipcEnabled">⚪</span>
|
||||
<span v-else>🔴</span>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-tabs type="card">
|
||||
<el-tab-pane :label="t('view.player_list.photon.current')">
|
||||
<data-tables v-bind="photonEventTable" style="margin-bottom: 10px">
|
||||
<el-table-column :label="t('table.playerList.date')" prop="created_at" width="120">
|
||||
<template #default="scope">
|
||||
<el-tooltip placement="right">
|
||||
<template #content>
|
||||
<span>{{ scope.row.created_at | formatDate('long') }}</span>
|
||||
</template>
|
||||
<span>{{ scope.row.created_at | formatDate('short') }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('table.playerList.user')" prop="photonId" width="160">
|
||||
<template #default="scope">
|
||||
<span
|
||||
class="x-link"
|
||||
style="padding-right: 10px"
|
||||
@click="showUserFromPhotonId(scope.row.photonId)"
|
||||
v-text="scope.row.displayName"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="t('table.playerList.type')"
|
||||
prop="type"
|
||||
width="140"></el-table-column>
|
||||
<el-table-column :label="t('table.playerList.detail')" prop="text">
|
||||
<template #default="scope">
|
||||
<template v-if="scope.row.type === 'ChangeAvatar'">
|
||||
<span
|
||||
class="x-link"
|
||||
@click="showAvatarDialog(scope.row.avatar.id)"
|
||||
v-text="scope.row.avatar.name"></span>
|
||||
|
||||
<span v-if="!scope.row.inCache" style="color: #aaa"
|
||||
><i class="el-icon-download"></i> </span
|
||||
>
|
||||
<span
|
||||
v-if="scope.row.avatar.releaseStatus === 'public'"
|
||||
class="avatar-info-public"
|
||||
>{{ t('dialog.avatar.labels.public') }}</span
|
||||
>
|
||||
<span
|
||||
v-else-if="scope.row.avatar.releaseStatus === 'private'"
|
||||
class="avatar-info-own"
|
||||
>{{ t('dialog.avatar.labels.private') }}</span
|
||||
>
|
||||
<template
|
||||
v-if="
|
||||
scope.row.avatar.description &&
|
||||
scope.row.avatar.name !== scope.row.avatar.description
|
||||
">
|
||||
- {{ scope.row.avatar.description }}
|
||||
</template>
|
||||
</template>
|
||||
<template v-else-if="scope.row.type === 'ChangeStatus'">
|
||||
<template v-if="scope.row.status !== scope.row.previousStatus">
|
||||
<el-tooltip placement="top">
|
||||
<template #content>
|
||||
<span v-if="scope.row.previousStatus === 'active'">{{
|
||||
t('dialog.user.status.active')
|
||||
}}</span>
|
||||
<span v-else-if="scope.row.previousStatus === 'join me'">{{
|
||||
t('dialog.user.status.join_me')
|
||||
}}</span>
|
||||
<span v-else-if="scope.row.previousStatus === 'ask me'">{{
|
||||
t('dialog.user.status.ask_me')
|
||||
}}</span>
|
||||
<span v-else-if="scope.row.previousStatus === 'busy'">{{
|
||||
t('dialog.user.status.busy')
|
||||
}}</span>
|
||||
<span v-else>{{ t('dialog.user.status.offline') }}</span>
|
||||
</template>
|
||||
<i
|
||||
class="x-user-status"
|
||||
:class="statusClass(scope.row.previousStatus)"></i>
|
||||
</el-tooltip>
|
||||
<span>
|
||||
<i class="el-icon-right"></i>
|
||||
</span>
|
||||
<el-tooltip placement="top">
|
||||
<template #content>
|
||||
<span v-if="scope.row.status === 'active'">{{
|
||||
t('dialog.user.status.active')
|
||||
}}</span>
|
||||
<span v-else-if="scope.row.status === 'join me'">{{
|
||||
t('dialog.user.status.join_me')
|
||||
}}</span>
|
||||
<span v-else-if="scope.row.status === 'ask me'">{{
|
||||
t('dialog.user.status.ask_me')
|
||||
}}</span>
|
||||
<span v-else-if="scope.row.status === 'busy'">{{
|
||||
t('dialog.user.status.busy')
|
||||
}}</span>
|
||||
<span v-else>{{ t('dialog.user.status.offline') }}</span>
|
||||
</template>
|
||||
<i
|
||||
class="x-user-status"
|
||||
:class="statusClass(scope.row.status)"
|
||||
style="margin-right: 5px"></i>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<span
|
||||
v-if="scope.row.statusDescription !== scope.row.previousStatusDescription"
|
||||
v-text="scope.row.statusDescription"></span>
|
||||
</template>
|
||||
<template v-else-if="scope.row.type === 'ChangeGroup'">
|
||||
<span
|
||||
v-if="scope.row.previousGroupName"
|
||||
class="x-link"
|
||||
style="margin-right: 5px"
|
||||
@click="showGroupDialog(scope.row.previousGroupId)"
|
||||
v-text="scope.row.previousGroupName"></span>
|
||||
<span
|
||||
v-else
|
||||
class="x-link"
|
||||
style="margin-right: 5px"
|
||||
@click="showGroupDialog(scope.row.previousGroupId)"
|
||||
v-text="scope.row.previousGroupId"></span>
|
||||
<span>
|
||||
<i class="el-icon-right"></i>
|
||||
</span>
|
||||
<span
|
||||
v-if="scope.row.groupName"
|
||||
class="x-link"
|
||||
style="margin-left: 5px"
|
||||
@click="showGroupDialog(scope.row.groupId)"
|
||||
v-text="scope.row.groupName"></span>
|
||||
<span
|
||||
v-else
|
||||
class="x-link"
|
||||
style="margin-left: 5px"
|
||||
@click="showGroupDialog(scope.row.groupId)"
|
||||
v-text="scope.row.groupId"></span>
|
||||
</template>
|
||||
<span
|
||||
v-else-if="scope.row.type === 'PortalSpawn'"
|
||||
class="x-link"
|
||||
@click="showWorldDialog(scope.row.location, scope.row.shortName)">
|
||||
<location
|
||||
:location="scope.row.location"
|
||||
:hint="scope.row.worldName"
|
||||
:grouphint="scope.row.groupName"
|
||||
:link="false"></location>
|
||||
</span>
|
||||
<span
|
||||
v-else-if="scope.row.type === 'ChatBoxMessage'"
|
||||
v-text="scope.row.text"></span>
|
||||
<span v-else-if="scope.row.type === 'OnPlayerJoined'">
|
||||
<span v-if="scope.row.platform === 'Desktop'" style="color: #409eff"
|
||||
>Desktop </span
|
||||
>
|
||||
<span v-else-if="scope.row.platform === 'VR'" style="color: #409eff"
|
||||
>VR </span
|
||||
>
|
||||
<span v-else-if="scope.row.platform === 'Quest'" style="color: #67c23a"
|
||||
>Android </span
|
||||
>
|
||||
<span
|
||||
class="x-link"
|
||||
@click="showAvatarDialog(scope.row.avatar.id)"
|
||||
v-text="scope.row.avatar.name"></span>
|
||||
|
||||
<span v-if="!scope.row.inCache" style="color: #aaa"
|
||||
><i class="el-icon-download"></i> </span
|
||||
>
|
||||
<span
|
||||
v-if="scope.row.avatar.releaseStatus === 'public'"
|
||||
class="avatar-info-public"
|
||||
>{{ t('dialog.avatar.labels.public') }}</span
|
||||
>
|
||||
<span
|
||||
v-else-if="scope.row.avatar.releaseStatus === 'private'"
|
||||
class="avatar-info-own"
|
||||
>{{ t('dialog.avatar.labels.private') }}</span
|
||||
>
|
||||
</span>
|
||||
<span v-else-if="scope.row.type === 'SpawnEmoji'">
|
||||
<span v-if="scope.row.imageUrl">
|
||||
<el-tooltip placement="right">
|
||||
<template #content>
|
||||
<img
|
||||
v-lazy="scope.row.imageUrl"
|
||||
class="friends-list-avatar"
|
||||
style="height: 500px; cursor: pointer"
|
||||
@click="showFullscreenImageDialog(scope.row.imageUrl)" />
|
||||
</template>
|
||||
<span v-text="scope.row.fileId"></span>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
<span v-else v-text="scope.row.text"></span>
|
||||
</span>
|
||||
<span
|
||||
v-else-if="scope.row.color === 'yellow'"
|
||||
style="color: yellow"
|
||||
v-text="scope.row.text"></span>
|
||||
<span v-else v-text="scope.row.text"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</data-tables>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="t('view.player_list.photon.previous')">
|
||||
<data-tables v-bind="photonEventTablePrevious" style="margin-bottom: 10px">
|
||||
<el-table-column :label="t('table.playerList.date')" prop="created_at" width="120">
|
||||
<template #default="scope">
|
||||
<el-tooltip placement="right">
|
||||
<template #content>
|
||||
<span>{{ scope.row.created_at | formatDate('long') }}</span>
|
||||
</template>
|
||||
<span>{{ scope.row.created_at | formatDate('short') }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('table.playerList.user')" prop="photonId" width="160">
|
||||
<template #default="scope">
|
||||
<span
|
||||
class="x-link"
|
||||
style="padding-right: 10px"
|
||||
@click="lookupUser(scope.row)"
|
||||
v-text="scope.row.displayName"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="t('table.playerList.type')"
|
||||
prop="type"
|
||||
width="140"></el-table-column>
|
||||
<el-table-column :label="t('table.playerList.detail')" prop="text">
|
||||
<template #default="scope">
|
||||
<template v-if="scope.row.type === 'ChangeAvatar'">
|
||||
<span
|
||||
class="x-link"
|
||||
@click="showAvatarDialog(scope.row.avatar.id)"
|
||||
v-text="scope.row.avatar.name"></span>
|
||||
|
||||
<span v-if="!scope.row.inCache" style="color: #aaa"
|
||||
><i class="el-icon-download"></i> </span
|
||||
>
|
||||
<span
|
||||
v-if="scope.row.avatar.releaseStatus === 'public'"
|
||||
class="avatar-info-public"
|
||||
>{{ t('dialog.avatar.labels.public') }}</span
|
||||
>
|
||||
<span
|
||||
v-else-if="scope.row.avatar.releaseStatus === 'private'"
|
||||
class="avatar-info-own"
|
||||
>{{ t('dialog.avatar.labels.private') }}</span
|
||||
>
|
||||
<template
|
||||
v-if="
|
||||
scope.row.avatar.description &&
|
||||
scope.row.avatar.name !== scope.row.avatar.description
|
||||
">
|
||||
| - {{ scope.row.avatar.description }}
|
||||
</template>
|
||||
</template>
|
||||
<template v-else-if="scope.row.type === 'ChangeStatus'">
|
||||
<template v-if="scope.row.status !== scope.row.previousStatus">
|
||||
<el-tooltip placement="top">
|
||||
<template #content>
|
||||
<span v-if="scope.row.previousStatus === 'active'">{{
|
||||
t('dialog.user.status.active')
|
||||
}}</span>
|
||||
<span v-else-if="scope.row.previousStatus === 'join me'">{{
|
||||
t('dialog.user.status.join_me')
|
||||
}}</span>
|
||||
<span v-else-if="scope.row.previousStatus === 'ask me'">{{
|
||||
t('dialog.user.status.ask_me')
|
||||
}}</span>
|
||||
<span v-else-if="scope.row.previousStatus === 'busy'">{{
|
||||
t('dialog.user.status.busy')
|
||||
}}</span>
|
||||
<span v-else>{{ t('dialog.user.status.offline') }}</span>
|
||||
</template>
|
||||
<i
|
||||
class="x-user-status"
|
||||
:class="statusClass(scope.row.previousStatus)"></i>
|
||||
</el-tooltip>
|
||||
<span>
|
||||
<i class="el-icon-right"></i>
|
||||
</span>
|
||||
<el-tooltip placement="top">
|
||||
<template #content>
|
||||
<span v-if="scope.row.status === 'active'">{{
|
||||
t('dialog.user.status.active')
|
||||
}}</span>
|
||||
<span v-else-if="scope.row.status === 'join me'">{{
|
||||
t('dialog.user.status.join_me')
|
||||
}}</span>
|
||||
<span v-else-if="scope.row.status === 'ask me'">{{
|
||||
t('dialog.user.status.ask_me')
|
||||
}}</span>
|
||||
<span v-else-if="scope.row.status === 'busy'">{{
|
||||
t('dialog.user.status.busy')
|
||||
}}</span>
|
||||
<span v-else>{{ t('dialog.user.status.offline') }}</span>
|
||||
</template>
|
||||
<i
|
||||
class="x-user-status"
|
||||
:class="statusClass(scope.row.status)"
|
||||
style="margin-right: 5px"></i>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<span
|
||||
v-if="scope.row.statusDescription !== scope.row.previousStatusDescription"
|
||||
v-text="scope.row.statusDescription"></span>
|
||||
</template>
|
||||
<template v-else-if="scope.row.type === 'ChangeGroup'">
|
||||
<span
|
||||
v-if="scope.row.previousGroupName"
|
||||
class="x-link"
|
||||
style="margin-right: 5px"
|
||||
@click="showGroupDialog(scope.row.previousGroupId)"
|
||||
v-text="scope.row.previousGroupName"></span>
|
||||
<span
|
||||
v-else
|
||||
class="x-link"
|
||||
style="margin-right: 5px"
|
||||
@click="showGroupDialog(scope.row.previousGroupId)"
|
||||
v-text="scope.row.previousGroupId"></span>
|
||||
<span>
|
||||
<i class="el-icon-right"></i>
|
||||
</span>
|
||||
<span
|
||||
v-if="scope.row.groupName"
|
||||
class="x-link"
|
||||
style="margin-left: 5px"
|
||||
@click="showGroupDialog(scope.row.groupId)"
|
||||
v-text="scope.row.groupName"></span>
|
||||
<span
|
||||
v-else
|
||||
class="x-link"
|
||||
style="margin-left: 5px"
|
||||
@click="showGroupDialog(scope.row.groupId)"
|
||||
v-text="scope.row.groupId"></span>
|
||||
</template>
|
||||
<span
|
||||
v-else-if="scope.row.type === 'PortalSpawn'"
|
||||
class="x-link"
|
||||
@click="showWorldDialog(scope.row.location, scope.row.shortName)">
|
||||
<location
|
||||
:location="scope.row.location"
|
||||
:hint="scope.row.worldName"
|
||||
:grouphint="scope.row.groupName"
|
||||
:link="false"></location>
|
||||
</span>
|
||||
<span
|
||||
v-else-if="scope.row.type === 'ChatBoxMessage'"
|
||||
v-text="scope.row.text"></span>
|
||||
<span v-else-if="scope.row.type === 'OnPlayerJoined'">
|
||||
<span v-if="scope.row.platform === 'Desktop'" style="color: #409eff"
|
||||
>Desktop </span
|
||||
>
|
||||
<span v-else-if="scope.row.platform === 'VR'" style="color: #409eff"
|
||||
>VR </span
|
||||
>
|
||||
<span v-else-if="scope.row.platform === 'Quest'" style="color: #67c23a"
|
||||
>Android </span
|
||||
>
|
||||
<span
|
||||
class="x-link"
|
||||
@click="showAvatarDialog(scope.row.avatar.id)"
|
||||
v-text="scope.row.avatar.name"></span>
|
||||
|
||||
<span v-if="!scope.row.inCache" style="color: #aaa"
|
||||
><i class="el-icon-download"></i> </span
|
||||
>
|
||||
<span
|
||||
v-if="scope.row.avatar.releaseStatus === 'public'"
|
||||
class="avatar-info-public"
|
||||
>{{ t('dialog.avatar.labels.public') }}</span
|
||||
>
|
||||
<span
|
||||
v-else-if="scope.row.avatar.releaseStatus === 'private'"
|
||||
class="avatar-info-own"
|
||||
>{{ t('dialog.avatar.labels.private') }}</span
|
||||
>
|
||||
</span>
|
||||
<span v-else-if="scope.row.type === 'SpawnEmoji'">
|
||||
<span v-if="scope.row.imageUrl">
|
||||
<el-tooltip placement="right">
|
||||
<template #content>
|
||||
<img
|
||||
v-lazy="scope.row.imageUrl"
|
||||
class="friends-list-avatar"
|
||||
style="height: 500px; cursor: pointer"
|
||||
@click="showFullscreenImageDialog(scope.row.imageUrl)" />
|
||||
</template>
|
||||
<span v-text="scope.row.fileId"></span>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
<span v-else v-text="scope.row.text"></span>
|
||||
</span>
|
||||
<span
|
||||
v-else-if="scope.row.color === 'yellow'"
|
||||
style="color: yellow"
|
||||
v-text="scope.row.text"></span>
|
||||
<span v-else v-text="scope.row.text"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</data-tables>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
<div class="current-instance-table">
|
||||
<data-tables
|
||||
v-bind="currentInstanceUserList"
|
||||
style="margin-top: 10px; cursor: pointer"
|
||||
@row-click="selectCurrentInstanceRow">
|
||||
<el-table-column :label="t('table.playerList.avatar')" width="70" prop="photo">
|
||||
<template #default="scope">
|
||||
<template v-if="userImage(scope.row.ref)">
|
||||
<el-popover placement="right" height="500px" trigger="hover">
|
||||
<img
|
||||
slot="reference"
|
||||
v-lazy="userImage(scope.row.ref)"
|
||||
class="friends-list-avatar" />
|
||||
<img
|
||||
v-lazy="userImageFull(scope.row.ref)"
|
||||
class="friends-list-avatar"
|
||||
style="height: 500px; cursor: pointer"
|
||||
@click="showFullscreenImageDialog(userImageFull(scope.row.ref))" />
|
||||
</el-popover>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('table.playerList.timer')" width="80" prop="timer" sortable>
|
||||
<template #default="scope">
|
||||
<timer :epoch="scope.row.timer"></timer>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="photonLoggingEnabled"
|
||||
:label="t('table.playerList.photonId')"
|
||||
width="110"
|
||||
prop="photonId"
|
||||
sortable>
|
||||
<template #default="scope">
|
||||
<template v-if="chatboxUserBlacklist.has(scope.row.ref.id)">
|
||||
<el-tooltip placement="left" content="Unblock chatbox messages">
|
||||
<el-button
|
||||
type="text"
|
||||
icon="el-icon-turn-off-microphone"
|
||||
size="mini"
|
||||
style="color: red; margin-right: 5px"
|
||||
@click.stop="deleteChatboxUserBlacklist(scope.row.ref.id)"></el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-tooltip placement="left" content="Block chatbox messages">
|
||||
<el-button
|
||||
type="text"
|
||||
icon="el-icon-microphone"
|
||||
size="mini"
|
||||
style="margin-right: 5px"
|
||||
@click.stop="addChatboxUserBlacklist(scope.row.ref)"></el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<span v-text="scope.row.photonId"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('table.playerList.icon')" prop="isMaster" width="70" align="center">
|
||||
<template #default="scope">
|
||||
<el-tooltip v-if="scope.row.isMaster" placement="left" content="Instance Master">
|
||||
<span>👑</span>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="scope.row.isModerator" placement="left" content="Moderator">
|
||||
<span>⚔️</span>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="scope.row.isFriend" placement="left" content="Friend">
|
||||
<span>💚</span>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="scope.row.timeoutTime" placement="left" content="Timeout">
|
||||
<span style="color: red">🔴{{ scope.row.timeoutTime }}s</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('table.playerList.platform')" prop="inVRMode" width="80">
|
||||
<template #default="scope">
|
||||
<template v-if="scope.row.ref.last_platform">
|
||||
<span v-if="scope.row.ref.last_platform === 'standalonewindows'" style="color: #409eff"
|
||||
>PC</span
|
||||
>
|
||||
<span v-else-if="scope.row.ref.last_platform === 'android'" style="color: #67c23a"
|
||||
>A</span
|
||||
>
|
||||
<span v-else-if="scope.row.ref.last_platform === 'ios'" style="color: #c7c7ce"
|
||||
>iOS</span
|
||||
>
|
||||
<span v-else>{{ scope.row.ref.last_platform }}</span>
|
||||
</template>
|
||||
<template v-if="scope.row.inVRMode !== null">
|
||||
<span v-if="scope.row.inVRMode">VR</span>
|
||||
<span
|
||||
v-else-if="
|
||||
scope.row.ref.last_platform === 'android' ||
|
||||
scope.row.ref.last_platform === 'ios'
|
||||
"
|
||||
>M</span
|
||||
>
|
||||
<span v-else>D</span>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="t('table.playerList.displayName')"
|
||||
min-width="140"
|
||||
prop="displayName"
|
||||
sortable="custom">
|
||||
<template #default="scope">
|
||||
<span
|
||||
v-if="randomUserColours"
|
||||
:style="{ color: scope.row.ref.$userColour }"
|
||||
v-text="scope.row.ref.displayName"></span>
|
||||
<span v-else v-text="scope.row.ref.displayName"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('table.playerList.status')" min-width="180" prop="ref.status">
|
||||
<template #default="scope">
|
||||
<template v-if="scope.row.ref.status">
|
||||
<i
|
||||
class="x-user-status"
|
||||
:class="statusClass(scope.row.ref.status)"
|
||||
style="margin-right: 3px"></i>
|
||||
<span v-text="scope.row.ref.statusDescription"></span>
|
||||
<!--//- el-table-column(label="Group" min-width="180" prop="groupOnNameplate" sortable)-->
|
||||
<!--//- template(v-once #default="scope")-->
|
||||
<!--//- span(v-text="scope.row.groupOnNameplate")-->
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="t('table.playerList.rank')"
|
||||
width="110"
|
||||
prop="trustSortNum"
|
||||
sortable="custom">
|
||||
<template #default="scope">
|
||||
<span
|
||||
class="name"
|
||||
:class="scope.row.ref.trustClass"
|
||||
v-text="scope.row.ref.trustLevel"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('table.playerList.language')" width="100" prop="ref.$languages">
|
||||
<template #default="scope">
|
||||
<el-tooltip v-for="item in scope.row.ref.$languages" :key="item.key" placement="top">
|
||||
<template #content>
|
||||
<span>{{ item.value }} ({{ item.key }})</span>
|
||||
</template>
|
||||
<span
|
||||
class="flags"
|
||||
:class="languageClass(item.key)"
|
||||
style="display: inline-block; margin-right: 5px"></span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('table.playerList.bioLink')" width="100" prop="ref.bioLinks">
|
||||
<template #default="scope">
|
||||
<div style="display: flex; align-items: center">
|
||||
<el-tooltip v-for="(link, index) in scope.row.ref.bioLinks" v-if="link" :key="index">
|
||||
<template #content>
|
||||
<span v-text="link"></span>
|
||||
</template>
|
||||
<img
|
||||
:src="getFaviconUrl(link)"
|
||||
style="
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
vertical-align: middle;
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
"
|
||||
@click.stop="openExternalLink(link)" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</data-tables>
|
||||
</div>
|
||||
</div>
|
||||
<ChatboxBlacklistDialog
|
||||
:chatbox-blacklist-dialog="chatboxBlacklistDialog"
|
||||
:chatbox-user-blacklist="chatboxUserBlacklist"
|
||||
@delete-chatbox-user-blacklist="deleteChatboxUserBlacklist" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PlayerListTab'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup>
|
||||
import { inject, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { languageClass } from '../../composables/user/utils';
|
||||
import configRepository from '../../service/config';
|
||||
import ChatboxBlacklistDialog from './dialogs/ChatboxBlacklistDialog.vue';
|
||||
import { getFaviconUrl } from '../../composables/shared/utils';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const API = inject('API');
|
||||
const showFullscreenImageDialog = inject('showFullscreenImageDialog');
|
||||
const showWorldDialog = inject('showWorldDialog');
|
||||
const showUserDialog = inject('showUserDialog');
|
||||
const showLaunchDialog = inject('showLaunchDialog');
|
||||
const showAvatarDialog = inject('showAvatarDialog');
|
||||
const statusClass = inject('statusClass');
|
||||
const showGroupDialog = inject('showGroupDialog');
|
||||
const openExternalLink = inject('openExternalLink');
|
||||
const userImage = inject('userImage');
|
||||
const userImageFull = inject('userImageFull');
|
||||
|
||||
const props = defineProps({
|
||||
menuActiveIndex: {
|
||||
type: String,
|
||||
default: 'playerList'
|
||||
},
|
||||
currentInstanceWorld: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
currentInstanceLocation: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
currentInstanceWorldDescriptionExpanded: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
photonLoggingEnabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
photonEventTableTypeFilter: {
|
||||
type: Array,
|
||||
default: []
|
||||
},
|
||||
photonEventTableTypeFilterList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
photonEventTableFilter: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
hideTooltips: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
ipcEnabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
photonEventIcon: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
photonEventTable: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
photonEventTablePrevious: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
currentInstanceUserList: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
chatboxUserBlacklist: {
|
||||
type: Map
|
||||
},
|
||||
randomUserColours: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
lastLocation: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
'photonEventTableFilterChange',
|
||||
'getCurrentInstanceUserList',
|
||||
'showUserFromPhotonId',
|
||||
'lookupUser'
|
||||
]);
|
||||
|
||||
const chatboxBlacklistDialog = ref({
|
||||
visible: false,
|
||||
loading: false
|
||||
});
|
||||
|
||||
function photonEventTableFilterChange(value) {
|
||||
emit('photonEventTableFilterChange', value);
|
||||
}
|
||||
|
||||
function showChatboxBlacklistDialog() {
|
||||
const D = chatboxBlacklistDialog.value;
|
||||
D.visible = true;
|
||||
}
|
||||
|
||||
function showUserFromPhotonId(photonId) {
|
||||
emit('showUserFromPhotonId', photonId);
|
||||
}
|
||||
|
||||
function lookupUser(user) {
|
||||
emit('lookupUser', user);
|
||||
}
|
||||
|
||||
function selectCurrentInstanceRow(val) {
|
||||
if (val === null) {
|
||||
return;
|
||||
}
|
||||
const ref = val.ref;
|
||||
if (ref.id) {
|
||||
showUserDialog(ref.id);
|
||||
} else {
|
||||
lookupUser(ref);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteChatboxUserBlacklist(userId) {
|
||||
props.chatboxUserBlacklist.delete(userId);
|
||||
await saveChatboxUserBlacklist();
|
||||
emit('getCurrentInstanceUserList');
|
||||
}
|
||||
|
||||
async function saveChatboxUserBlacklist() {
|
||||
await configRepository.setString(
|
||||
'VRCX_chatboxUserBlacklist',
|
||||
JSON.stringify(Object.fromEntries(props.chatboxUserBlacklist))
|
||||
);
|
||||
}
|
||||
|
||||
async function addChatboxUserBlacklist(user) {
|
||||
props.chatboxUserBlacklist.set(user.id, user.displayName);
|
||||
await saveChatboxUserBlacklist();
|
||||
emit('getCurrentInstanceUserList');
|
||||
}
|
||||
</script>
|
||||
@@ -1,12 +1,9 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
<safe-dialog
|
||||
class="x-dialog"
|
||||
:before-close="beforeDialogClose"
|
||||
:visible.sync="chatboxBlacklistDialog.visible"
|
||||
:title="t('dialog.chatbox_blacklist.header')"
|
||||
width="600px"
|
||||
@mousedown.native="dialogMouseDown"
|
||||
@mouseup.native="dialogMouseUp">
|
||||
width="600px">
|
||||
<div v-if="chatboxBlacklistDialog.visible" v-loading="chatboxBlacklistDialog.loading">
|
||||
<h2>{{ t('dialog.chatbox_blacklist.keyword_blacklist') }}</h2>
|
||||
<el-input
|
||||
@@ -42,20 +39,15 @@
|
||||
<span>{{ user[1] }}</span>
|
||||
</el-tag>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</safe-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// TODO: untested
|
||||
import { inject, ref } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import configRepository from '../../../service/config';
|
||||
const { t } = useI18n();
|
||||
|
||||
const beforeDialogClose = inject('beforeDialogClose');
|
||||
const dialogMouseDown = inject('dialogMouseDown');
|
||||
const dialogMouseUp = inject('dialogMouseUp');
|
||||
|
||||
defineProps({
|
||||
chatboxBlacklistDialog: {
|
||||
type: Object,
|
||||
|
||||
725
src/views/Profile/Profile.vue
Normal file
725
src/views/Profile/Profile.vue
Normal file
@@ -0,0 +1,725 @@
|
||||
<template>
|
||||
<div v-if="menuActiveIndex === 'profile'" class="x-container">
|
||||
<div class="options-container" style="margin-top: 0">
|
||||
<span class="header">{{ t('view.profile.profile.header') }}</span>
|
||||
<div class="x-friend-list" style="margin-top: 10px">
|
||||
<div class="x-friend-item" @click="showUserDialog(API.currentUser.id)">
|
||||
<div class="avatar">
|
||||
<img v-lazy="userImage(API.currentUser, true)" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span class="name" v-text="API.currentUser.displayName"></span>
|
||||
<span class="extra" v-text="API.currentUser.username"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="x-friend-item" style="cursor: default">
|
||||
<div class="detail">
|
||||
<span class="name">{{ t('view.profile.profile.last_activity') }}</span>
|
||||
<span class="extra">{{ API.currentUser.last_activity | formatDate('long') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="x-friend-item" style="cursor: default">
|
||||
<div class="detail">
|
||||
<span class="name">{{ t('view.profile.profile.two_factor') }}</span>
|
||||
<span class="extra">{{
|
||||
API.currentUser.twoFactorAuthEnabled
|
||||
? t('view.profile.profile.two_factor_enabled')
|
||||
: t('view.profile.profile.two_factor_disabled')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="x-friend-item" @click="getVRChatCredits()">
|
||||
<div class="detail">
|
||||
<span class="name">{{ t('view.profile.profile.vrchat_credits') }}</span>
|
||||
<span class="extra">{{ vrchatCredit ?? t('view.profile.profile.refresh') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 10px">
|
||||
<el-button
|
||||
size="small"
|
||||
type="danger"
|
||||
plain
|
||||
icon="el-icon-switch-button"
|
||||
style="margin-left: 0; margin-right: 5px; margin-top: 10px"
|
||||
@click="logout()"
|
||||
>{{ t('view.profile.profile.logout') }}</el-button
|
||||
>
|
||||
<el-button
|
||||
size="small"
|
||||
icon="el-icon-picture-outline"
|
||||
style="margin-left: 0; margin-right: 5px; margin-top: 10px"
|
||||
@click="showGalleryDialog()"
|
||||
>{{ t('view.profile.profile.manage_gallery_icon') }}</el-button
|
||||
>
|
||||
<el-button
|
||||
size="small"
|
||||
icon="el-icon-chat-dot-round"
|
||||
style="margin-left: 0; margin-right: 5px; margin-top: 10px"
|
||||
@click="showDiscordNamesDialog()"
|
||||
>{{ t('view.profile.profile.discord_names') }}</el-button
|
||||
>
|
||||
<el-button
|
||||
size="small"
|
||||
icon="el-icon-printer"
|
||||
style="margin-left: 0; margin-right: 5px; margin-top: 10px"
|
||||
@click="showExportFriendsListDialog()"
|
||||
>{{ t('view.profile.profile.export_friend_list') }}</el-button
|
||||
>
|
||||
<el-button
|
||||
size="small"
|
||||
icon="el-icon-user"
|
||||
style="margin-left: 0; margin-right: 5px; margin-top: 10px"
|
||||
@click="showExportAvatarsListDialog()"
|
||||
>{{ t('view.profile.profile.export_own_avatars') }}</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="options-container">
|
||||
<span class="header">{{ t('view.profile.game_info.header') }}</span>
|
||||
<div class="x-friend-list" style="margin-top: 10px">
|
||||
<div class="x-friend-item">
|
||||
<div class="detail" @click="getVisits">
|
||||
<span class="name">{{ t('view.profile.game_info.online_users') }}</span>
|
||||
<span v-if="visits" class="extra">{{
|
||||
t('view.profile.game_info.user_online', { count: visits })
|
||||
}}</span>
|
||||
<span v-else class="extra">{{ t('view.profile.game_info.refresh') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="options-container">
|
||||
<div class="header-bar">
|
||||
<span class="header">{{ t('view.profile.vrc_sdk_downloads.header') }}</span>
|
||||
<el-tooltip placement="top" :content="t('view.profile.refresh_tooltip')" :disabled="hideTooltips">
|
||||
<el-button
|
||||
type="default"
|
||||
size="mini"
|
||||
icon="el-icon-refresh"
|
||||
circle
|
||||
style="margin-left: 5px"
|
||||
@click="API.getConfig()"></el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div class="x-friend-list" style="margin-top: 10px">
|
||||
<div
|
||||
v-for="(link, item) in API.cachedConfig.downloadUrls"
|
||||
:key="item"
|
||||
class="x-friend-item"
|
||||
placement="top">
|
||||
<div class="detail" @click="openExternalLink(link)">
|
||||
<span class="name" v-text="item"></span>
|
||||
<span class="extra" v-text="link"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="options-container">
|
||||
<span class="header">{{ t('view.profile.direct_access.header') }}</span>
|
||||
<div style="margin-top: 10px">
|
||||
<el-button-group>
|
||||
<el-button size="small" @click="promptUsernameDialog()">{{
|
||||
t('view.profile.direct_access.username')
|
||||
}}</el-button>
|
||||
<el-button size="small" @click="promptUserIdDialog()">{{
|
||||
t('view.profile.direct_access.user_id')
|
||||
}}</el-button>
|
||||
<el-button size="small" @click="promptWorldDialog()">{{
|
||||
t('view.profile.direct_access.world_instance')
|
||||
}}</el-button>
|
||||
<el-button size="small" @click="promptAvatarDialog()">{{
|
||||
t('view.profile.direct_access.avatar')
|
||||
}}</el-button>
|
||||
</el-button-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="options-container">
|
||||
<div class="header-bar">
|
||||
<span class="header">{{ t('view.profile.invite_messages') }}</span>
|
||||
<el-tooltip placement="top" :content="t('view.profile.refresh_tooltip')" :disabled="hideTooltips">
|
||||
<el-button
|
||||
type="default"
|
||||
size="mini"
|
||||
icon="el-icon-refresh"
|
||||
circle
|
||||
style="margin-left: 5px"
|
||||
@click="
|
||||
inviteMessageTable.visible = true;
|
||||
refreshInviteMessageTable('message');
|
||||
"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip placement="top" :content="t('view.profile.clear_results_tooltip')" :disabled="hideTooltips">
|
||||
<el-button
|
||||
type="default"
|
||||
size="mini"
|
||||
icon="el-icon-delete"
|
||||
circle
|
||||
style="margin-left: 5px"
|
||||
@click="inviteMessageTable.visible = false"></el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<data-tables v-if="inviteMessageTable.visible" v-bind="inviteMessageTable" style="margin-top: 10px">
|
||||
<el-table-column
|
||||
:label="t('table.profile.invite_messages.slot')"
|
||||
prop="slot"
|
||||
sortable="custom"
|
||||
width="70"></el-table-column>
|
||||
<el-table-column :label="t('table.profile.invite_messages.message')" prop="message"></el-table-column>
|
||||
<el-table-column
|
||||
:label="t('table.profile.invite_messages.cool_down')"
|
||||
prop="updatedAt"
|
||||
sortable="custom"
|
||||
width="110"
|
||||
align="right">
|
||||
<template #default="scope">
|
||||
<countdown-timer :datetime="scope.row.updatedAt" :hours="1"></countdown-timer>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('table.profile.invite_messages.action')" width="60" align="right">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="text"
|
||||
icon="el-icon-edit"
|
||||
size="mini"
|
||||
@click="showEditInviteMessageDialog('message', scope.row)"></el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</data-tables>
|
||||
</div>
|
||||
|
||||
<div class="options-container">
|
||||
<div class="header-bar">
|
||||
<span class="header">{{ t('view.profile.invite_response_messages') }}</span>
|
||||
<el-tooltip placement="top" :content="t('view.profile.refresh_tooltip')" :disabled="hideTooltips">
|
||||
<el-button
|
||||
type="default"
|
||||
size="mini"
|
||||
icon="el-icon-refresh"
|
||||
circle
|
||||
style="margin-left: 5px"
|
||||
@click="
|
||||
inviteResponseMessageTable.visible = true;
|
||||
refreshInviteMessageTable('response');
|
||||
"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip placement="top" :content="t('view.profile.clear_results_tooltip')" :disabled="hideTooltips">
|
||||
<el-button
|
||||
type="default"
|
||||
size="mini"
|
||||
icon="el-icon-delete"
|
||||
circle
|
||||
style="margin-left: 5px"
|
||||
@click="inviteResponseMessageTable.visible = false"></el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<data-tables
|
||||
v-if="inviteResponseMessageTable.visible"
|
||||
v-bind="inviteResponseMessageTable"
|
||||
style="margin-top: 10px">
|
||||
<el-table-column
|
||||
:label="t('table.profile.invite_messages.slot')"
|
||||
prop="slot"
|
||||
sortable="custom"
|
||||
width="70"></el-table-column>
|
||||
<el-table-column :label="t('table.profile.invite_messages.message')" prop="message"></el-table-column>
|
||||
<el-table-column
|
||||
:label="t('table.profile.invite_messages.cool_down')"
|
||||
prop="updatedAt"
|
||||
sortable="custom"
|
||||
width="110"
|
||||
align="right">
|
||||
<template #default="scope">
|
||||
<countdown-timer :datetime="scope.row.updatedAt" :hours="1"></countdown-timer>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('table.profile.invite_messages.action')" width="60" align="right">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="text"
|
||||
icon="el-icon-edit"
|
||||
size="mini"
|
||||
@click="showEditInviteMessageDialog('response', scope.row)"></el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</data-tables>
|
||||
</div>
|
||||
|
||||
<div class="options-container">
|
||||
<div class="header-bar">
|
||||
<span class="header">{{ t('view.profile.invite_request_messages') }}</span>
|
||||
<el-tooltip placement="top" :content="t('view.profile.refresh_tooltip')" :disabled="hideTooltips">
|
||||
<el-button
|
||||
type="default"
|
||||
size="mini"
|
||||
icon="el-icon-refresh"
|
||||
circle
|
||||
style="margin-left: 5px"
|
||||
@click="
|
||||
inviteRequestMessageTable.visible = true;
|
||||
refreshInviteMessageTable('request');
|
||||
"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip placement="top" :content="t('view.profile.clear_results_tooltip')" :disabled="hideTooltips">
|
||||
<el-button
|
||||
type="default"
|
||||
size="mini"
|
||||
icon="el-icon-delete"
|
||||
circle
|
||||
style="margin-left: 5px"
|
||||
@click="inviteRequestMessageTable.visible = false"></el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<data-tables
|
||||
v-if="inviteRequestMessageTable.visible"
|
||||
v-bind="inviteRequestMessageTable"
|
||||
style="margin-top: 10px">
|
||||
<el-table-column
|
||||
:label="t('table.profile.invite_messages.slot')"
|
||||
prop="slot"
|
||||
sortable="custom"
|
||||
width="70"></el-table-column>
|
||||
<el-table-column :label="t('table.profile.invite_messages.message')" prop="message"></el-table-column>
|
||||
<el-table-column
|
||||
:label="t('table.profile.invite_messages.cool_down')"
|
||||
prop="updatedAt"
|
||||
sortable="custom"
|
||||
width="110"
|
||||
align="right">
|
||||
<template #default="scope">
|
||||
<countdown-timer :datetime="scope.row.updatedAt" :hours="1"></countdown-timer>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('table.profile.invite_messages.action')" width="60" align="right">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="text"
|
||||
icon="el-icon-edit"
|
||||
size="mini"
|
||||
@click="showEditInviteMessageDialog('request', scope.row)"></el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</data-tables>
|
||||
</div>
|
||||
|
||||
<div class="options-container">
|
||||
<div class="header-bar">
|
||||
<span class="header">{{ t('view.profile.invite_request_response_messages') }}</span>
|
||||
<el-tooltip placement="top" :content="t('view.profile.refresh_tooltip')" :disabled="hideTooltips">
|
||||
<el-button
|
||||
type="default"
|
||||
size="mini"
|
||||
icon="el-icon-refresh"
|
||||
circle
|
||||
style="margin-left: 5px"
|
||||
@click="
|
||||
inviteRequestResponseMessageTable.visible = true;
|
||||
refreshInviteMessageTable('requestResponse');
|
||||
"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip placement="top" :content="t('view.profile.clear_results_tooltip')" :disabled="hideTooltips">
|
||||
<el-button
|
||||
type="default"
|
||||
size="mini"
|
||||
icon="el-icon-delete"
|
||||
circle
|
||||
style="margin-left: 5px"
|
||||
@click="inviteRequestResponseMessageTable.visible = false"></el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<data-tables
|
||||
v-if="inviteRequestResponseMessageTable.visible"
|
||||
v-bind="inviteRequestResponseMessageTable"
|
||||
style="margin-top: 10px">
|
||||
<el-table-column
|
||||
:label="t('table.profile.invite_messages.slot')"
|
||||
prop="slot"
|
||||
sortable="custom"
|
||||
width="70"></el-table-column>
|
||||
<el-table-column :label="t('table.profile.invite_messages.message')" prop="message"></el-table-column>
|
||||
<el-table-column
|
||||
:label="t('table.profile.invite_messages.cool_down')"
|
||||
prop="updatedAt"
|
||||
sortable="custom"
|
||||
width="110"
|
||||
align="right">
|
||||
<template #default="scope">
|
||||
<countdown-timer :datetime="scope.row.updatedAt" :hours="1"></countdown-timer>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('table.profile.invite_messages.action')" width="60" align="right">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="text"
|
||||
icon="el-icon-edit"
|
||||
size="mini"
|
||||
@click="showEditInviteMessageDialog('requestResponse', scope.row)"></el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</data-tables>
|
||||
</div>
|
||||
|
||||
<div class="options-container">
|
||||
<span class="header">{{ t('view.profile.past_display_names') }}</span>
|
||||
<data-tables v-bind="pastDisplayNameTable" style="margin-top: 10px">
|
||||
<el-table-column
|
||||
:label="t('table.profile.previous_display_name.date')"
|
||||
prop="updated_at"
|
||||
sortable="custom">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.updated_at | formatDate('long') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="t('table.profile.previous_display_name.name')"
|
||||
prop="displayName"></el-table-column>
|
||||
</data-tables>
|
||||
</div>
|
||||
|
||||
<div class="options-container">
|
||||
<div class="header-bar">
|
||||
<span class="header">{{ t('view.profile.config_json') }}</span>
|
||||
<el-tooltip placement="top" :content="t('view.profile.refresh_tooltip')" :disabled="hideTooltips">
|
||||
<el-button
|
||||
type="default"
|
||||
size="mini"
|
||||
icon="el-icon-refresh"
|
||||
circle
|
||||
style="margin-left: 5px"
|
||||
@click="refreshConfigTreeData()"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip placement="top" :content="t('view.profile.clear_results_tooltip')" :disabled="hideTooltips">
|
||||
<el-button
|
||||
type="default"
|
||||
size="mini"
|
||||
icon="el-icon-delete"
|
||||
circle
|
||||
style="margin-left: 5px"
|
||||
@click="configTreeData = []"></el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-tree v-if="configTreeData.length > 0" :data="configTreeData" style="margin-top: 10px; font-size: 12px">
|
||||
<template #default="scope">
|
||||
<span>
|
||||
<span style="font-weight: bold; margin-right: 5px" v-text="scope.data.key"></span>
|
||||
<span v-if="!scope.data.children" v-text="scope.data.value"></span>
|
||||
</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
|
||||
<div class="options-container">
|
||||
<div class="header-bar">
|
||||
<span class="header">{{ t('view.profile.current_user_json') }}</span>
|
||||
<el-tooltip placement="top" :content="t('view.profile.refresh_tooltip')" :disabled="hideTooltips">
|
||||
<el-button
|
||||
type="default"
|
||||
size="mini"
|
||||
icon="el-icon-refresh"
|
||||
circle
|
||||
style="margin-left: 5px"
|
||||
@click="refreshCurrentUserTreeData()"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip placement="top" :content="t('view.profile.clear_results_tooltip')" :disabled="hideTooltips">
|
||||
<el-button
|
||||
type="default"
|
||||
size="mini"
|
||||
icon="el-icon-delete"
|
||||
circle
|
||||
style="margin-left: 5px"
|
||||
@click="currentUserTreeData = []"></el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-tree
|
||||
v-if="currentUserTreeData.length > 0"
|
||||
:data="currentUserTreeData"
|
||||
style="margin-top: 10px; font-size: 12px">
|
||||
<template #default="scope">
|
||||
<span>
|
||||
<span style="font-weight: bold; margin-right: 5px" v-text="scope.data.key"></span>
|
||||
<span v-if="!scope.data.children" v-text="scope.data.value"></span>
|
||||
</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
|
||||
<div class="options-container">
|
||||
<div class="header-bar">
|
||||
<span class="header">{{ t('view.profile.feedback') }}</span>
|
||||
<el-tooltip placement="top" :content="t('view.profile.refresh_tooltip')" :disabled="hideTooltips">
|
||||
<el-button
|
||||
type="default"
|
||||
size="mini"
|
||||
icon="el-icon-refresh"
|
||||
circle
|
||||
style="margin-left: 5px"
|
||||
@click="getCurrentUserFeedback()"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip placement="top" :content="t('view.profile.clear_results_tooltip')" :disabled="hideTooltips">
|
||||
<el-button
|
||||
type="default"
|
||||
size="mini"
|
||||
icon="el-icon-delete"
|
||||
circle
|
||||
style="margin-left: 5px"
|
||||
@click="currentUserFeedbackData = []"></el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-tree
|
||||
v-if="currentUserFeedbackData.length > 0"
|
||||
:data="currentUserFeedbackData"
|
||||
style="margin-top: 10px; font-size: 12px">
|
||||
<template #default="scope">
|
||||
<span>
|
||||
<span style="font-weight: bold; margin-right: 5px" v-text="scope.data.key"></span>
|
||||
<span v-if="!scope.data.children" v-text="scope.data.value"></span>
|
||||
</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
<DiscordNamesDialog :discord-names-dialog-visible.sync="discordNamesDialogVisible" :friends="friends" />
|
||||
<ExportFriendsListDialog
|
||||
:is-export-friends-list-dialog-visible.sync="isExportFriendsListDialogVisible"
|
||||
:friends="friends" />
|
||||
<ExportAvatarsListDialog :is-export-avatars-list-dialog-visible.sync="isExportAvatarsListDialogVisible" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ProfileTab'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup>
|
||||
import { inject, ref, getCurrentInstance } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { inviteMessagesRequest, miscRequest, userRequest } from '../../api';
|
||||
import utils from '../../classes/utils';
|
||||
import { parseAvatarUrl } from '../../composables/avatar/utils';
|
||||
import DiscordNamesDialog from './dialogs/DiscordNamesDialog.vue';
|
||||
import ExportFriendsListDialog from './dialogs/ExportFriendsListDialog.vue';
|
||||
import ExportAvatarsListDialog from './dialogs/ExportAvatarsListDialog.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { $prompt, $message } = getCurrentInstance().proxy;
|
||||
|
||||
const API = inject('API');
|
||||
const userImage = inject('userImage');
|
||||
const showUserDialog = inject('showUserDialog');
|
||||
const showAvatarDialog = inject('showAvatarDialog');
|
||||
const showGalleryDialog = inject('showGalleryDialog');
|
||||
const openExternalLink = inject('openExternalLink');
|
||||
|
||||
const props = defineProps({
|
||||
menuActiveIndex: {
|
||||
type: String,
|
||||
default: 'profile'
|
||||
},
|
||||
hideTooltips: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
inviteMessageTable: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
visible: false,
|
||||
data: []
|
||||
})
|
||||
},
|
||||
inviteResponseMessageTable: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
visible: false,
|
||||
data: []
|
||||
})
|
||||
},
|
||||
inviteRequestMessageTable: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
visible: false,
|
||||
data: []
|
||||
})
|
||||
},
|
||||
inviteRequestResponseMessageTable: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
visible: false,
|
||||
data: []
|
||||
})
|
||||
},
|
||||
pastDisplayNameTable: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
visible: false,
|
||||
data: []
|
||||
})
|
||||
},
|
||||
friends: {
|
||||
type: Map,
|
||||
default: () => new Map()
|
||||
},
|
||||
directAccessWorld: {
|
||||
type: Function,
|
||||
default: () => {}
|
||||
}
|
||||
});
|
||||
const emit = defineEmits(['logout', 'lookupUser', 'showEditInviteMessageDialog']);
|
||||
|
||||
const vrchatCredit = ref(null);
|
||||
const configTreeData = ref([]);
|
||||
const currentUserTreeData = ref([]);
|
||||
const currentUserFeedbackData = ref([]);
|
||||
|
||||
const discordNamesDialogVisible = ref(false);
|
||||
const isExportFriendsListDialogVisible = ref(false);
|
||||
const isExportAvatarsListDialogVisible = ref(false);
|
||||
|
||||
const visits = ref(0);
|
||||
|
||||
function getVisits() {
|
||||
miscRequest.getVisits().then((args) => {
|
||||
// API.$on('VISITS')
|
||||
visits.value = args.json;
|
||||
});
|
||||
}
|
||||
|
||||
function getVRChatCredits() {
|
||||
// API.$on('VRCCREDITS')
|
||||
miscRequest.getVRChatCredits().then((args) => (vrchatCredit.value = args.json?.balance));
|
||||
}
|
||||
function logout() {
|
||||
emit('logout');
|
||||
}
|
||||
|
||||
function showDiscordNamesDialog() {
|
||||
discordNamesDialogVisible.value = true;
|
||||
}
|
||||
|
||||
function showExportFriendsListDialog() {
|
||||
isExportFriendsListDialogVisible.value = true;
|
||||
}
|
||||
|
||||
function showExportAvatarsListDialog() {
|
||||
isExportAvatarsListDialogVisible.value = true;
|
||||
}
|
||||
function promptUsernameDialog() {
|
||||
$prompt(t('prompt.direct_access_username.description'), t('prompt.direct_access_username.header'), {
|
||||
distinguishCancelAndClose: true,
|
||||
confirmButtonText: t('prompt.direct_access_username.ok'),
|
||||
cancelButtonText: t('prompt.direct_access_username.cancel'),
|
||||
inputPattern: /\S+/,
|
||||
inputErrorMessage: t('prompt.direct_access_username.input_error'),
|
||||
callback: (action, instance) => {
|
||||
if (action === 'confirm' && instance.inputValue) {
|
||||
emit('lookupUser', {
|
||||
displayName: instance.inputValue
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function promptUserIdDialog() {
|
||||
$prompt(t('prompt.direct_access_user_id.description'), t('prompt.direct_access_user_id.header'), {
|
||||
distinguishCancelAndClose: true,
|
||||
confirmButtonText: t('prompt.direct_access_user_id.ok'),
|
||||
cancelButtonText: t('prompt.direct_access_user_id.cancel'),
|
||||
inputPattern: /\S+/,
|
||||
inputErrorMessage: t('prompt.direct_access_user_id.input_error'),
|
||||
callback: (action, instance) => {
|
||||
if (action === 'confirm' && instance.inputValue) {
|
||||
const testUrl = instance.inputValue.substring(0, 15);
|
||||
if (testUrl === 'https://vrchat.') {
|
||||
const userId = this.parseUserUrl(instance.inputValue);
|
||||
if (userId) {
|
||||
showUserDialog(userId);
|
||||
} else {
|
||||
$message({
|
||||
message: t('prompt.direct_access_user_id.message.error'),
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
showUserDialog(instance.inputValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function promptWorldDialog() {
|
||||
$prompt(t('prompt.direct_access_world_id.description'), t('prompt.direct_access_world_id.header'), {
|
||||
distinguishCancelAndClose: true,
|
||||
confirmButtonText: t('prompt.direct_access_world_id.ok'),
|
||||
cancelButtonText: t('prompt.direct_access_world_id.cancel'),
|
||||
inputPattern: /\S+/,
|
||||
inputErrorMessage: t('prompt.direct_access_world_id.input_error'),
|
||||
callback: (action, instance) => {
|
||||
if (action === 'confirm' && instance.inputValue) {
|
||||
if (!props.directAccessWorld(instance.inputValue)) {
|
||||
$message({
|
||||
message: t('prompt.direct_access_world_id.message.error'),
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function promptAvatarDialog() {
|
||||
$prompt(t('prompt.direct_access_avatar_id.description'), t('prompt.direct_access_avatar_id.header'), {
|
||||
distinguishCancelAndClose: true,
|
||||
confirmButtonText: t('prompt.direct_access_avatar_id.ok'),
|
||||
cancelButtonText: t('prompt.direct_access_avatar_id.cancel'),
|
||||
inputPattern: /\S+/,
|
||||
inputErrorMessage: t('prompt.direct_access_avatar_id.input_error'),
|
||||
callback: (action, instance) => {
|
||||
if (action === 'confirm' && instance.inputValue) {
|
||||
const testUrl = instance.inputValue.substring(0, 15);
|
||||
if (testUrl === 'https://vrchat.') {
|
||||
const avatarId = parseAvatarUrl(instance.inputValue);
|
||||
if (avatarId) {
|
||||
showAvatarDialog(avatarId);
|
||||
} else {
|
||||
$message({
|
||||
message: t('prompt.direct_access_avatar_id.message.error'),
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
showAvatarDialog(instance.inputValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function showEditInviteMessageDialog(messageType, inviteMessage) {
|
||||
emit('showEditInviteMessageDialog', {
|
||||
messageType,
|
||||
inviteMessage
|
||||
});
|
||||
}
|
||||
function refreshInviteMessageTable(messageType) {
|
||||
inviteMessagesRequest.refreshInviteMessageTableData(messageType);
|
||||
}
|
||||
async function refreshConfigTreeData() {
|
||||
await API.getConfig();
|
||||
configTreeData.value = utils.buildTreeData(API.cachedConfig);
|
||||
}
|
||||
async function refreshCurrentUserTreeData() {
|
||||
await API.getCurrentUser();
|
||||
currentUserTreeData.value = utils.buildTreeData(API.currentUser);
|
||||
}
|
||||
function getCurrentUserFeedback() {
|
||||
userRequest.getUserFeedback({ userId: API.currentUser.id }).then((args) => {
|
||||
// API.$on('USER:FEEDBACK')
|
||||
if (args.params.userId === API.currentUser.id) {
|
||||
currentUserFeedbackData.value = utils.buildTreeData(args.json);
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -1,13 +1,10 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
<safe-dialog
|
||||
class="x-dialog"
|
||||
:before-close="beforeDialogClose"
|
||||
:visible="discordNamesDialogVisible"
|
||||
:title="t('dialog.discord_names.header')"
|
||||
width="650px"
|
||||
@close="closeDialog"
|
||||
@mousedown.native="dialogMouseDown"
|
||||
@mouseup.native="dialogMouseUp">
|
||||
@close="closeDialog">
|
||||
<div style="font-size: 12px">
|
||||
{{ t('dialog.discord_names.description') }}
|
||||
</div>
|
||||
@@ -19,7 +16,7 @@
|
||||
resize="none"
|
||||
readonly
|
||||
style="margin-top: 15px" />
|
||||
</el-dialog>
|
||||
</safe-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -27,9 +24,6 @@
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
|
||||
const API = inject('API');
|
||||
const beforeDialogClose = inject('beforeDialogClose');
|
||||
const dialogMouseDown = inject('dialogMouseDown');
|
||||
const dialogMouseUp = inject('dialogMouseUp');
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
<safe-dialog
|
||||
class="x-dialog"
|
||||
:before-close="beforeDialogClose"
|
||||
:visible="editInviteMessageDialog.visible"
|
||||
:title="t('dialog.edit_invite_message.header')"
|
||||
width="400px"
|
||||
@close="closeDialog"
|
||||
@mousedown.native="dialogMouseDown"
|
||||
@mouseup.native="dialogMouseUp">
|
||||
@close="closeDialog">
|
||||
<div style="font-size: 12px">
|
||||
<span>{{ t('dialog.edit_invite_message.description') }}</span>
|
||||
<el-input
|
||||
@@ -23,10 +20,10 @@
|
||||
<template #footer>
|
||||
<el-button type="small" @click="closeDialog">{{ $t('dialog.edit_invite_message.cancel') }}</el-button>
|
||||
<el-button type="primary" size="small" @click="saveEditInviteMessage">{{
|
||||
$t('dialog.edit_invite_message.save')
|
||||
t('dialog.edit_invite_message.save')
|
||||
}}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</safe-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -38,9 +35,6 @@
|
||||
const instance = getCurrentInstance();
|
||||
const $message = instance.proxy.$message;
|
||||
const API = inject('API');
|
||||
const beforeDialogClose = inject('beforeDialogClose');
|
||||
const dialogMouseDown = inject('dialogMouseDown');
|
||||
const dialogMouseUp = inject('dialogMouseUp');
|
||||
|
||||
const props = defineProps({
|
||||
editInviteMessageDialog: {
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:before-close="beforeDialogClose"
|
||||
:visible.sync="isVisible"
|
||||
:title="$t('dialog.export_own_avatars.header')"
|
||||
width="650px"
|
||||
@mousedown.native="dialogMouseDown"
|
||||
@mouseup.native="dialogMouseUp">
|
||||
<safe-dialog :visible.sync="isVisible" :title="$t('dialog.export_own_avatars.header')" width="650px">
|
||||
<el-input
|
||||
v-model="exportAvatarsListCsv"
|
||||
v-loading="loading"
|
||||
@@ -16,7 +10,7 @@
|
||||
readonly
|
||||
style="margin-top: 15px"
|
||||
@click.native="$event.target.tagName === 'TEXTAREA' && $event.target.select()" />
|
||||
</el-dialog>
|
||||
</safe-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -24,7 +18,7 @@
|
||||
|
||||
export default {
|
||||
name: 'ExportAvatarsListDialog',
|
||||
inject: ['API', 'beforeDialogClose', 'dialogMouseDown', 'dialogMouseUp'],
|
||||
inject: ['API'],
|
||||
props: {
|
||||
isExportAvatarsListDialogVisible: Boolean
|
||||
},
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:before-close="beforeDialogClose"
|
||||
:title="$t('dialog.export_friends_list.header')"
|
||||
:visible.sync="isVisible"
|
||||
width="650px"
|
||||
@mousedown.native="dialogMouseDown"
|
||||
@mouseup.native="dialogMouseUp">
|
||||
<safe-dialog :title="$t('dialog.export_friends_list.header')" :visible.sync="isVisible" width="650px">
|
||||
<el-tabs type="card">
|
||||
<el-tab-pane :label="$t('dialog.export_friends_list.csv')">
|
||||
<el-input
|
||||
@@ -30,13 +24,13 @@
|
||||
@click.native="$event.target.tagName === 'TEXTAREA' && $event.target.select()" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-dialog>
|
||||
</safe-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ExportFriendsListDialog',
|
||||
inject: ['API', 'beforeDialogClose', 'dialogMouseDown', 'dialogMouseUp'],
|
||||
inject: ['API'],
|
||||
props: {
|
||||
friends: Map,
|
||||
isExportFriendsListDialogVisible: Boolean
|
||||
|
||||
721
src/views/Search/Search.vue
Normal file
721
src/views/Search/Search.vue
Normal file
@@ -0,0 +1,721 @@
|
||||
<template>
|
||||
<div v-if="menuActiveIndex === 'search'" class="x-container">
|
||||
<div style="margin: 0 0 10px; display: flex; align-items: center">
|
||||
<el-input
|
||||
:value="searchText"
|
||||
:placeholder="t('view.search.search_placeholder')"
|
||||
style="flex: 1"
|
||||
@input="updateSearchText"
|
||||
@keyup.native.13="search"></el-input>
|
||||
<el-tooltip placement="bottom" :content="t('view.search.clear_results_tooltip')" :disabled="hideTooltips">
|
||||
<el-button
|
||||
type="default"
|
||||
icon="el-icon-delete"
|
||||
circle
|
||||
style="flex: none; margin-left: 10px"
|
||||
@click="clearSearch"></el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-tabs ref="searchTabRef" type="card" style="margin-top: 15px" @tab-click="searchText = ''">
|
||||
<el-tab-pane v-loading="isSearchUserLoading" :label="t('view.search.user.header')" style="min-height: 60px">
|
||||
<el-checkbox v-model="searchUserByBio" style="margin-left: 10px">{{
|
||||
t('view.search.user.search_by_bio')
|
||||
}}</el-checkbox>
|
||||
<el-checkbox v-model="searchUserSortByLastLoggedIn" style="margin-left: 10px">{{
|
||||
t('view.search.user.sort_by_last_logged_in')
|
||||
}}</el-checkbox>
|
||||
<div class="x-friend-list" style="min-height: 500px">
|
||||
<div
|
||||
v-for="user in searchUserResults"
|
||||
:key="user.id"
|
||||
class="x-friend-item"
|
||||
@click="showUserDialog(user.id)">
|
||||
<template>
|
||||
<div class="avatar">
|
||||
<img v-lazy="userImage(user, true)" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span class="name" v-text="user.displayName"></span>
|
||||
<span
|
||||
v-if="randomUserColours"
|
||||
class="extra"
|
||||
:class="user.$trustClass"
|
||||
v-text="user.$trustLevel"></span>
|
||||
<span
|
||||
v-else
|
||||
class="extra"
|
||||
:style="{ color: user.$userColour }"
|
||||
v-text="user.$trustLevel"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<el-button-group v-if="searchUserResults.length" style="margin-top: 15px">
|
||||
<el-button
|
||||
:disabled="!searchUserParams.offset"
|
||||
icon="el-icon-back"
|
||||
size="small"
|
||||
@click="moreSearchUser(-1)"
|
||||
>{{ t('view.search.prev_page') }}</el-button
|
||||
>
|
||||
<el-button
|
||||
:disabled="searchUserResults.length < 10"
|
||||
icon="el-icon-right"
|
||||
size="small"
|
||||
@click="moreSearchUser(1)"
|
||||
>{{ t('view.search.next_page') }}</el-button
|
||||
>
|
||||
</el-button-group>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane
|
||||
v-loading="isSearchWorldLoading"
|
||||
:label="t('view.search.world.header')"
|
||||
style="min-height: 60px">
|
||||
<el-dropdown
|
||||
size="small"
|
||||
trigger="click"
|
||||
style="margin-bottom: 15px"
|
||||
@command="(row) => searchWorld(row)">
|
||||
<el-button size="small"
|
||||
>{{ t('view.search.world.category') }} <i class="el-icon-arrow-down el-icon--right"></i
|
||||
></el-button>
|
||||
<el-dropdown-menu v-slot="dropdown">
|
||||
<el-dropdown-item
|
||||
v-for="row in API.cachedConfig.dynamicWorldRows"
|
||||
:key="row.index"
|
||||
:command="row"
|
||||
v-text="row.name"></el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
<el-checkbox v-model="searchWorldLabs" style="margin-left: 10px">{{
|
||||
t('view.search.world.community_lab')
|
||||
}}</el-checkbox>
|
||||
<div class="x-friend-list" style="min-height: 500px">
|
||||
<div
|
||||
v-for="world in searchWorldResults"
|
||||
:key="world.id"
|
||||
class="x-friend-item"
|
||||
@click="showWorldDialog(world.id)">
|
||||
<template>
|
||||
<div class="avatar">
|
||||
<img v-lazy="world.thumbnailImageUrl" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span class="name" v-text="world.name"></span>
|
||||
<span v-if="world.occupants" class="extra"
|
||||
>{{ world.authorName }} ({{ world.occupants }})</span
|
||||
>
|
||||
<span v-else class="extra" v-text="world.authorName"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<el-button-group v-if="searchWorldResults.length" style="margin-top: 15px">
|
||||
<el-button
|
||||
:disabled="!searchWorldParams.offset"
|
||||
icon="el-icon-back"
|
||||
size="small"
|
||||
@click="moreSearchWorld(-1)"
|
||||
>{{ t('view.search.prev_page') }}</el-button
|
||||
>
|
||||
<el-button
|
||||
:disabled="searchWorldResults.length < 10"
|
||||
icon="el-icon-right"
|
||||
size="small"
|
||||
@click="moreSearchWorld(1)"
|
||||
>{{ t('view.search.next_page') }}</el-button
|
||||
>
|
||||
</el-button-group>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane
|
||||
v-loading="isSearchAvatarLoading"
|
||||
:label="t('view.search.avatar.header')"
|
||||
style="min-height: 60px">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
||||
<div style="display: flex; align-items: center">
|
||||
<el-dropdown
|
||||
v-if="avatarRemoteDatabaseProviderList.length > 1"
|
||||
trigger="click"
|
||||
size="mini"
|
||||
style="margin-right: 5px"
|
||||
@click.native.stop>
|
||||
<el-button size="small"
|
||||
>{{ t('view.search.avatar.search_provider') }}
|
||||
<i class="el-icon-arrow-down el-icon--right"></i
|
||||
></el-button>
|
||||
<el-dropdown-menu v-slot="dropdown">
|
||||
<el-dropdown-item
|
||||
v-for="provider in avatarRemoteDatabaseProviderList"
|
||||
:key="provider"
|
||||
@click.native="setAvatarProvider(provider)">
|
||||
<i
|
||||
v-if="provider === avatarRemoteDatabaseProvider"
|
||||
class="el-icon-check el-icon--left"></i>
|
||||
{{ provider }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
<el-tooltip
|
||||
placement="bottom"
|
||||
:content="t('view.search.avatar.refresh_tooltip')"
|
||||
:disabled="hideTooltips">
|
||||
<el-button
|
||||
type="default"
|
||||
:loading="userDialog.isAvatarsLoading"
|
||||
size="mini"
|
||||
icon="el-icon-refresh"
|
||||
circle
|
||||
@click="refreshUserDialogAvatars"></el-button>
|
||||
</el-tooltip>
|
||||
<span style="font-size: 14px; margin-left: 5px; margin-right: 5px">{{
|
||||
t('view.search.avatar.result_count', {
|
||||
count: searchAvatarResults.length
|
||||
})
|
||||
}}</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center">
|
||||
<el-radio-group
|
||||
v-model="searchAvatarFilter"
|
||||
size="mini"
|
||||
style="margin: 5px; display: block"
|
||||
@change="searchAvatar">
|
||||
<el-radio label="all">{{ t('view.search.avatar.all') }}</el-radio>
|
||||
<el-radio label="public">{{ t('view.search.avatar.public') }}</el-radio>
|
||||
<el-radio label="private">{{ t('view.search.avatar.private') }}</el-radio>
|
||||
</el-radio-group>
|
||||
<el-divider direction="vertical"></el-divider>
|
||||
<el-radio-group
|
||||
v-model="searchAvatarFilterRemote"
|
||||
size="mini"
|
||||
style="margin: 5px; display: block"
|
||||
@change="searchAvatar">
|
||||
<el-radio label="all">{{ t('view.search.avatar.all') }}</el-radio>
|
||||
<el-radio label="local">{{ t('view.search.avatar.local') }}</el-radio>
|
||||
<el-radio label="remote" :disabled="!avatarRemoteDatabase">{{
|
||||
t('view.search.avatar.remote')
|
||||
}}</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: end">
|
||||
<el-radio-group
|
||||
v-model="searchAvatarSort"
|
||||
:disabled="searchAvatarFilterRemote !== 'local'"
|
||||
size="mini"
|
||||
style="margin: 5px; display: block"
|
||||
@change="searchAvatar">
|
||||
<el-radio label="name">{{ t('view.search.avatar.sort_name') }}</el-radio>
|
||||
<el-radio label="update">{{ t('view.search.avatar.sort_update') }}</el-radio>
|
||||
<el-radio label="created">{{ t('view.search.avatar.sort_created') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div class="x-friend-list" style="margin-top: 20px; min-height: 500px">
|
||||
<div
|
||||
v-for="avatar in searchAvatarPage"
|
||||
:key="avatar.id"
|
||||
class="x-friend-item"
|
||||
@click="showAvatarDialog(avatar.id)">
|
||||
<template>
|
||||
<div class="avatar">
|
||||
<img v-if="avatar.thumbnailImageUrl" v-lazy="avatar.thumbnailImageUrl" />
|
||||
<img v-else-if="avatar.imageUrl" v-lazy="avatar.imageUrl" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span class="name" v-text="avatar.name"></span>
|
||||
<span
|
||||
v-if="avatar.releaseStatus === 'public'"
|
||||
class="extra"
|
||||
style="color: #67c23a"
|
||||
v-text="avatar.releaseStatus"></span>
|
||||
<span
|
||||
v-else-if="avatar.releaseStatus === 'private'"
|
||||
class="extra"
|
||||
style="color: #f56c6c"
|
||||
v-text="avatar.releaseStatus"></span>
|
||||
<span v-else class="extra" v-text="avatar.releaseStatus"></span>
|
||||
<span class="extra" v-text="avatar.authorName"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<el-button-group v-if="searchAvatarPage.length" style="margin-top: 15px">
|
||||
<el-button
|
||||
:disabled="!searchAvatarPageNum"
|
||||
icon="el-icon-back"
|
||||
size="small"
|
||||
@click="moreSearchAvatar(-1)"
|
||||
>{{ t('view.search.prev_page') }}</el-button
|
||||
>
|
||||
<el-button
|
||||
:disabled="
|
||||
searchAvatarResults.length < 10 ||
|
||||
(searchAvatarPageNum + 1) * 10 >= searchAvatarResults.length
|
||||
"
|
||||
icon="el-icon-right"
|
||||
size="small"
|
||||
@click="moreSearchAvatar(1)"
|
||||
>{{ t('view.search.next_page') }}</el-button
|
||||
>
|
||||
</el-button-group>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane
|
||||
v-loading="isSearchGroupLoading"
|
||||
:label="t('view.search.group.header')"
|
||||
style="min-height: 60px">
|
||||
<div class="x-friend-list" style="min-height: 500px">
|
||||
<div
|
||||
v-for="group in searchGroupResults"
|
||||
:key="group.id"
|
||||
class="x-friend-item"
|
||||
@click="showGroupDialog(group.id)">
|
||||
<template>
|
||||
<div class="avatar">
|
||||
<img v-lazy="getSmallThumbnailUrl(group.iconUrl)" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span class="name">
|
||||
<span v-text="group.name"></span>
|
||||
<span style="margin-left: 5px; font-weight: normal">({{ group.memberCount }})</span>
|
||||
<span
|
||||
style="
|
||||
margin-left: 5px;
|
||||
color: #909399;
|
||||
font-weight: normal;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
"
|
||||
>{{ group.shortCode }}.{{ group.discriminator }}</span
|
||||
>
|
||||
</span>
|
||||
<span class="extra" v-text="group.description"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<el-button-group v-if="searchGroupResults.length" style="margin-top: 15px">
|
||||
<el-button
|
||||
:disabled="!searchGroupParams.offset"
|
||||
icon="el-icon-back"
|
||||
size="small"
|
||||
@click="moreSearchGroup(-1)"
|
||||
>{{ t('view.search.prev_page') }}</el-button
|
||||
>
|
||||
<el-button
|
||||
:disabled="searchGroupResults.length < 10"
|
||||
icon="el-icon-right"
|
||||
size="small"
|
||||
@click="moreSearchGroup(1)"
|
||||
>{{ t('view.search.next_page') }}</el-button
|
||||
>
|
||||
</el-button-group>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SearchTab'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup>
|
||||
import { inject, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { groupRequest, worldRequest } from '../../api';
|
||||
import utils from '../../classes/utils';
|
||||
import { convertFileUrlToImageUrl } from '../../composables/shared/utils';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const API = inject('API');
|
||||
const showUserDialog = inject('showUserDialog');
|
||||
const userImage = inject('userImage');
|
||||
const showWorldDialog = inject('showWorldDialog');
|
||||
const showAvatarDialog = inject('showAvatarDialog');
|
||||
const showGroupDialog = inject('showGroupDialog');
|
||||
|
||||
const props = defineProps({
|
||||
menuActiveIndex: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
searchText: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
searchUserResults: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
randomUserColours: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
avatarRemoteDatabaseProviderList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
avatarRemoteDatabaseProvider: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
hideTooltips: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
userDialog: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
lookupAvatars: {
|
||||
type: Function,
|
||||
default: () => () => {}
|
||||
},
|
||||
avatarRemoteDatabase: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
'clearSearch',
|
||||
'setAvatarProvider',
|
||||
'refreshUserDialogAvatars',
|
||||
'moreSearchUser',
|
||||
'update:searchText'
|
||||
]);
|
||||
|
||||
const searchTabRef = ref(null);
|
||||
|
||||
const searchUserParams = ref({});
|
||||
const searchUserByBio = ref(false);
|
||||
const searchUserSortByLastLoggedIn = ref(false);
|
||||
|
||||
const isSearchUserLoading = ref(false);
|
||||
const isSearchWorldLoading = ref(false);
|
||||
const isSearchAvatarLoading = ref(false);
|
||||
const isSearchGroupLoading = ref(false);
|
||||
|
||||
const searchWorldOption = ref('');
|
||||
const searchWorldLabs = ref(false);
|
||||
const searchWorldParams = ref({});
|
||||
const searchWorldResults = ref([]);
|
||||
|
||||
const searchAvatarFilter = ref('');
|
||||
const searchAvatarSort = ref('');
|
||||
const searchAvatarFilterRemote = ref('');
|
||||
const searchAvatarPageNum = ref(0);
|
||||
const searchAvatarResults = ref([]);
|
||||
const searchAvatarPage = ref([]);
|
||||
|
||||
const searchGroupParams = ref({});
|
||||
const searchGroupResults = ref([]);
|
||||
|
||||
function getSmallThumbnailUrl(url) {
|
||||
convertFileUrlToImageUrl(url);
|
||||
}
|
||||
|
||||
function clearSearch() {
|
||||
searchUserParams.value = {};
|
||||
searchWorldParams.value = {};
|
||||
searchWorldResults.value = [];
|
||||
searchAvatarResults.value = [];
|
||||
searchAvatarPage.value = [];
|
||||
searchAvatarPageNum.value = 0;
|
||||
searchGroupParams.value = {};
|
||||
searchGroupResults.value = [];
|
||||
emit('clearSearch');
|
||||
}
|
||||
|
||||
function updateSearchText(text) {
|
||||
emit('update:searchText', text);
|
||||
}
|
||||
|
||||
function search() {
|
||||
switch (searchTabRef.value.currentName) {
|
||||
case '0':
|
||||
searchUser();
|
||||
break;
|
||||
case '1':
|
||||
searchWorld({});
|
||||
break;
|
||||
case '2':
|
||||
searchAvatar();
|
||||
break;
|
||||
case '3':
|
||||
searchGroup();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async function searchUser() {
|
||||
searchUserParams.value = {
|
||||
n: 10,
|
||||
offset: 0,
|
||||
search: props.searchText,
|
||||
customFields: searchUserByBio.value ? 'bio' : 'displayName',
|
||||
sort: searchUserSortByLastLoggedIn.value ? 'last_login' : 'relevance'
|
||||
};
|
||||
await moreSearchUser();
|
||||
}
|
||||
|
||||
async function moreSearchUser(go = null) {
|
||||
emit('moreSearchUser', go, searchUserParams.value);
|
||||
}
|
||||
function searchWorld(ref) {
|
||||
searchWorldOption.value = '';
|
||||
const params = {
|
||||
n: 10,
|
||||
offset: 0
|
||||
};
|
||||
switch (ref.sortHeading) {
|
||||
case 'featured':
|
||||
params.sort = 'order';
|
||||
params.featured = 'true';
|
||||
break;
|
||||
case 'trending':
|
||||
params.sort = 'popularity';
|
||||
params.featured = 'false';
|
||||
break;
|
||||
case 'updated':
|
||||
params.sort = 'updated';
|
||||
break;
|
||||
case 'created':
|
||||
params.sort = 'created';
|
||||
break;
|
||||
case 'publication':
|
||||
params.sort = 'publicationDate';
|
||||
break;
|
||||
case 'shuffle':
|
||||
params.sort = 'shuffle';
|
||||
break;
|
||||
case 'active':
|
||||
searchWorldOption.value = 'active';
|
||||
break;
|
||||
case 'recent':
|
||||
searchWorldOption.value = 'recent';
|
||||
break;
|
||||
case 'favorite':
|
||||
searchWorldOption.value = 'favorites';
|
||||
break;
|
||||
case 'labs':
|
||||
params.sort = 'labsPublicationDate';
|
||||
break;
|
||||
case 'heat':
|
||||
params.sort = 'heat';
|
||||
params.featured = 'false';
|
||||
break;
|
||||
default:
|
||||
params.sort = 'relevance';
|
||||
params.search = utils.replaceBioSymbols(props.searchText);
|
||||
break;
|
||||
}
|
||||
params.order = ref.sortOrder || 'descending';
|
||||
if (ref.sortOwnership === 'mine') {
|
||||
params.user = 'me';
|
||||
params.releaseStatus = 'all';
|
||||
}
|
||||
if (ref.tag) {
|
||||
params.tag = ref.tag;
|
||||
}
|
||||
if (!searchWorldLabs.value) {
|
||||
if (params.tag) {
|
||||
params.tag += ',system_approved';
|
||||
} else {
|
||||
params.tag = 'system_approved';
|
||||
}
|
||||
}
|
||||
// TODO: option.platform
|
||||
searchWorldParams.value = params;
|
||||
moreSearchWorld();
|
||||
}
|
||||
|
||||
function moreSearchWorld(go) {
|
||||
const params = searchWorldParams.value;
|
||||
if (go) {
|
||||
params.offset += params.n * go;
|
||||
if (params.offset < 0) {
|
||||
params.offset = 0;
|
||||
}
|
||||
}
|
||||
isSearchWorldLoading.value = true;
|
||||
worldRequest
|
||||
.getWorlds(params, searchWorldOption.value)
|
||||
.finally(() => {
|
||||
isSearchWorldLoading.value = false;
|
||||
})
|
||||
.then((args) => {
|
||||
const map = new Map();
|
||||
for (const json of args.json) {
|
||||
const ref = API.cachedWorlds.get(json.id);
|
||||
if (typeof ref !== 'undefined') {
|
||||
map.set(ref.id, ref);
|
||||
}
|
||||
}
|
||||
searchWorldResults.value = Array.from(map.values());
|
||||
return args;
|
||||
});
|
||||
}
|
||||
|
||||
function setAvatarProvider(provider) {
|
||||
emit('setAvatarProvider', provider);
|
||||
}
|
||||
function refreshUserDialogAvatars(fileId) {
|
||||
emit('refreshUserDialogAvatars', fileId);
|
||||
}
|
||||
|
||||
async function searchAvatar() {
|
||||
let ref;
|
||||
isSearchAvatarLoading.value = true;
|
||||
if (!searchAvatarFilter.value) {
|
||||
searchAvatarFilter.value = 'all';
|
||||
}
|
||||
if (!searchAvatarSort.value) {
|
||||
searchAvatarSort.value = 'name';
|
||||
}
|
||||
if (!searchAvatarFilterRemote.value) {
|
||||
searchAvatarFilterRemote.value = 'all';
|
||||
}
|
||||
if (searchAvatarFilterRemote.value !== 'local') {
|
||||
searchAvatarSort.value = 'name';
|
||||
}
|
||||
const avatars = new Map();
|
||||
const query = props.searchText.toUpperCase();
|
||||
if (!query) {
|
||||
for (ref of API.cachedAvatars.values()) {
|
||||
switch (searchAvatarFilter.value) {
|
||||
case 'all':
|
||||
avatars.set(ref.id, ref);
|
||||
break;
|
||||
case 'public':
|
||||
if (ref.releaseStatus === 'public') {
|
||||
avatars.set(ref.id, ref);
|
||||
}
|
||||
break;
|
||||
case 'private':
|
||||
if (ref.releaseStatus === 'private') {
|
||||
avatars.set(ref.id, ref);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
isSearchAvatarLoading.value = false;
|
||||
} else {
|
||||
if (searchAvatarFilterRemote.value === 'all' || searchAvatarFilterRemote.value === 'local') {
|
||||
for (ref of API.cachedAvatars.values()) {
|
||||
let match = ref.name.toUpperCase().includes(query);
|
||||
if (!match && ref.description) {
|
||||
match = ref.description.toUpperCase().includes(query);
|
||||
}
|
||||
if (!match && ref.authorName) {
|
||||
match = ref.authorName.toUpperCase().includes(query);
|
||||
}
|
||||
if (match) {
|
||||
switch (searchAvatarFilter.value) {
|
||||
case 'all':
|
||||
avatars.set(ref.id, ref);
|
||||
break;
|
||||
case 'public':
|
||||
if (ref.releaseStatus === 'public') {
|
||||
avatars.set(ref.id, ref);
|
||||
}
|
||||
break;
|
||||
case 'private':
|
||||
if (ref.releaseStatus === 'private') {
|
||||
avatars.set(ref.id, ref);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
(searchAvatarFilterRemote.value === 'all' || searchAvatarFilterRemote.value === 'remote') &&
|
||||
props.avatarRemoteDatabase &&
|
||||
query.length >= 3
|
||||
) {
|
||||
const data = await props.lookupAvatars('search', query);
|
||||
if (data && typeof data === 'object') {
|
||||
data.forEach((avatar) => {
|
||||
avatars.set(avatar.id, avatar);
|
||||
});
|
||||
}
|
||||
}
|
||||
isSearchAvatarLoading.value = false;
|
||||
}
|
||||
const avatarsArray = Array.from(avatars.values());
|
||||
if (searchAvatarFilterRemote.value === 'local') {
|
||||
switch (searchAvatarSort.value) {
|
||||
case 'updated':
|
||||
avatarsArray.sort(utils.compareByUpdatedAt);
|
||||
break;
|
||||
case 'created':
|
||||
avatarsArray.sort(utils.compareByCreatedAt);
|
||||
break;
|
||||
case 'name':
|
||||
avatarsArray.sort(utils.compareByName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
searchAvatarPageNum.value = 0;
|
||||
searchAvatarResults.value = avatarsArray;
|
||||
searchAvatarPage.value = avatarsArray.slice(0, 10);
|
||||
}
|
||||
function moreSearchAvatar(n) {
|
||||
let offset;
|
||||
if (n === -1) {
|
||||
searchAvatarPageNum.value--;
|
||||
offset = searchAvatarPageNum.value * 10;
|
||||
}
|
||||
if (n === 1) {
|
||||
searchAvatarPageNum.value++;
|
||||
offset = searchAvatarPageNum.value * 10;
|
||||
}
|
||||
searchAvatarPage.value = searchAvatarResults.value.slice(offset, offset + 10);
|
||||
}
|
||||
async function searchGroup() {
|
||||
searchGroupParams.value = {
|
||||
n: 10,
|
||||
offset: 0,
|
||||
query: utils.replaceBioSymbols(props.searchText)
|
||||
};
|
||||
await moreSearchGroup();
|
||||
}
|
||||
async function moreSearchGroup(go) {
|
||||
const params = searchGroupParams.value;
|
||||
if (go) {
|
||||
params.offset += params.n * go;
|
||||
if (params.offset < 0) {
|
||||
params.offset = 0;
|
||||
}
|
||||
}
|
||||
isSearchGroupLoading.value = true;
|
||||
await groupRequest
|
||||
.groupSearch(params)
|
||||
.finally(() => {
|
||||
isSearchGroupLoading.value = false;
|
||||
})
|
||||
.then((args) => {
|
||||
// API.$on('GROUP:SEARCH', function (args) {
|
||||
for (const json of args.json) {
|
||||
API.$emit('GROUP', {
|
||||
json,
|
||||
params: {
|
||||
groupId: json.id
|
||||
}
|
||||
});
|
||||
}
|
||||
// });
|
||||
const map = new Map();
|
||||
for (const json of args.json) {
|
||||
const ref = API.cachedGroups.get(json.id);
|
||||
if (typeof ref !== 'undefined') {
|
||||
map.set(ref.id, ref);
|
||||
}
|
||||
}
|
||||
searchGroupResults.value = Array.from(map.values());
|
||||
return args;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -1,13 +1,10 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
<safe-dialog
|
||||
class="x-dialog"
|
||||
:visible="isAvatarProviderDialogVisible"
|
||||
:title="t('dialog.avatar_database_provider.header')"
|
||||
width="600px"
|
||||
:before-close="beforeDialogClose"
|
||||
@close="closeDialog"
|
||||
@mousedown.native="dialogMouseDown"
|
||||
@mouseup.native="dialogMouseUp">
|
||||
@close="closeDialog">
|
||||
<div>
|
||||
<el-input
|
||||
v-for="(provider, index) in avatarRemoteDatabaseProviderList"
|
||||
@@ -24,18 +21,13 @@
|
||||
{{ t('dialog.avatar_database_provider.add_provider') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</safe-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
const { t } = useI18n();
|
||||
|
||||
const beforeDialogClose = inject('beforeDialogClose');
|
||||
const dialogMouseDown = inject('dialogMouseDown');
|
||||
const dialogMouseUp = inject('dialogMouseUp');
|
||||
|
||||
defineProps({
|
||||
avatarRemoteDatabaseProviderList: {
|
||||
type: Array,
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
<safe-dialog
|
||||
class="x-dialog"
|
||||
:before-close="beforeDialogClose"
|
||||
:visible="changeLogDialog.visible"
|
||||
:title="t('dialog.change_log.header')"
|
||||
width="800px"
|
||||
top="5vh"
|
||||
@mousedown.native="dialogMouseDown"
|
||||
@mouseup.native="dialogMouseUp"
|
||||
@close="closeDialog">
|
||||
<div v-if="changeLogDialog.visible" class="changelog-dialog">
|
||||
<h2 v-text="changeLogDialog.buildName"></h2>
|
||||
@@ -32,7 +29,7 @@
|
||||
{{ t('dialog.change_log.close') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</safe-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -41,9 +38,6 @@
|
||||
|
||||
const { t } = useI18n();
|
||||
const openExternalLink = inject('openExternalLink');
|
||||
const beforeDialogClose = inject('beforeDialogClose');
|
||||
const dialogMouseDown = inject('dialogMouseDown');
|
||||
const dialogMouseUp = inject('dialogMouseUp');
|
||||
|
||||
const props = defineProps({
|
||||
changeLogDialog: {
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:before-close="beforeDialogClose"
|
||||
<safe-dialog
|
||||
:visible="!!feedFiltersDialogMode"
|
||||
:title="dialogTitle"
|
||||
width="550px"
|
||||
top="5vh"
|
||||
destroy-on-close
|
||||
@mousedown.native="dialogMouseDown"
|
||||
@mouseup.native="dialogMouseUp"
|
||||
@close="handleDialogClose">
|
||||
<div class="toggle-list" style="height: 75vh; overflow-y: auto">
|
||||
<div v-for="setting in currentOptions" :key="setting.key" class="toggle-item">
|
||||
@@ -58,18 +55,14 @@
|
||||
t('dialog.shared_feed_filters.close')
|
||||
}}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</safe-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, inject } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import configRepository from '../../../service/config';
|
||||
import { feedFiltersOptions } from '../../../composables/settings/constants/feedFiltersOptions';
|
||||
|
||||
const beforeDialogClose = inject('beforeDialogClose');
|
||||
const dialogMouseDown = inject('dialogMouseDown');
|
||||
const dialogMouseUp = inject('dialogMouseUp');
|
||||
import { feedFiltersOptions } from '../../../composables/setting/constants/feedFiltersOptions';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
<safe-dialog
|
||||
class="x-dialog"
|
||||
:before-close="beforeDialogClose"
|
||||
:visible="isLaunchOptionsDialogVisible"
|
||||
:title="t('dialog.launch_options.header')"
|
||||
width="600px"
|
||||
@mousedown.native="dialogMouseDown"
|
||||
@mouseup.native="dialogMouseUp"
|
||||
@close="closeDialog">
|
||||
<div style="font-size: 12px">
|
||||
{{ t('dialog.launch_options.description') }} <br />
|
||||
@@ -53,7 +50,7 @@
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</safe-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -61,9 +58,6 @@
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import configRepository from '../../../service/config';
|
||||
|
||||
const beforeDialogClose = inject('beforeDialogClose');
|
||||
const dialogMouseDown = inject('dialogMouseDown');
|
||||
const dialogMouseUp = inject('dialogMouseUp');
|
||||
const openExternalLink = inject('openExternalLink');
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
<safe-dialog
|
||||
class="x-dialog"
|
||||
:before-close="beforeDialogClose"
|
||||
:visible="isNoteExportDialogVisible"
|
||||
:title="t('dialog.note_export.header')"
|
||||
width="1000px"
|
||||
@close="closeDialog"
|
||||
@mousedown.native="dialogMouseDown"
|
||||
@mouseup.native="dialogMouseUp">
|
||||
@close="closeDialog">
|
||||
<div style="font-size: 12px">
|
||||
{{ t('dialog.note_export.description1') }} <br />
|
||||
{{ t('dialog.note_export.description2') }} <br />
|
||||
@@ -87,7 +84,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</data-tables>
|
||||
</el-dialog>
|
||||
</safe-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -99,9 +96,6 @@
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const beforeDialogClose = inject('beforeDialogClose');
|
||||
const dialogMouseDown = inject('dialogMouseDown');
|
||||
const dialogMouseUp = inject('dialogMouseUp');
|
||||
const userImage = inject('userImage');
|
||||
const userImageFull = inject('userImageFull');
|
||||
const showUserDialog = inject('showUserDialog');
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
<safe-dialog
|
||||
class="x-dialog"
|
||||
:visible="isNotificationPositionDialogVisible"
|
||||
:title="t('dialog.notification_position.header')"
|
||||
width="400px"
|
||||
:before-close="beforeDialogClose"
|
||||
@close="closeDialog"
|
||||
@mousedown.native="dialogMouseDown"
|
||||
@mouseup.native="dialogMouseUp">
|
||||
@close="closeDialog">
|
||||
<div style="font-size: 12px">
|
||||
{{ t('dialog.notification_position.description') }}
|
||||
</div>
|
||||
@@ -45,18 +42,13 @@
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</safe-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
const { t } = useI18n();
|
||||
|
||||
const beforeDialogClose = inject('beforeDialogClose');
|
||||
const dialogMouseDown = inject('dialogMouseDown');
|
||||
const dialogMouseUp = inject('dialogMouseUp');
|
||||
|
||||
defineProps({
|
||||
isNotificationPositionDialogVisible: {
|
||||
type: Boolean,
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
<safe-dialog
|
||||
class="x-dialog"
|
||||
:before-close="beforeDialogClose"
|
||||
:visible="ossDialog"
|
||||
:title="t('dialog.open_source.header')"
|
||||
width="650px"
|
||||
@close="closeDialog"
|
||||
@mousedown.native="dialogMouseDown"
|
||||
@mouseup.native="dialogMouseUp">
|
||||
@close="closeDialog">
|
||||
<div v-once style="height: 350px; overflow: hidden scroll; word-break: break-all">
|
||||
<div>
|
||||
<span>{{ t('dialog.open_source.description') }}</span>
|
||||
@@ -18,17 +15,12 @@
|
||||
<pre style="font-size: 12px; white-space: pre-line">{{ lib.licenseText }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</safe-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { openSourceSoftwareLicenses } from '../../../composables/settings/constants/openSourceSoftwareLicenses';
|
||||
|
||||
const beforeDialogClose = inject('beforeDialogClose');
|
||||
const dialogMouseDown = inject('dialogMouseDown');
|
||||
const dialogMouseUp = inject('dialogMouseUp');
|
||||
import { openSourceSoftwareLicenses } from '../../../composables/setting/constants/openSourceSoftwareLicenses';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
<safe-dialog
|
||||
class="x-dialog"
|
||||
:visible.sync="enablePrimaryPasswordDialog.visible"
|
||||
:before-close="enablePrimaryPasswordDialog.beforeClose"
|
||||
@@ -36,7 +36,7 @@
|
||||
{{ t('dialog.primary_password.ok') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</safe-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
<safe-dialog
|
||||
class="x-dialog"
|
||||
:before-close="beforeDialogClose"
|
||||
:visible="isRegistryBackupDialogVisible"
|
||||
:title="t('dialog.registry_backup.header')"
|
||||
width="600px"
|
||||
@close="closeDialog"
|
||||
@closed="clearVrcRegistryDialog"
|
||||
@mousedown.native="dialogMouseDown"
|
||||
@mouseup.native="dialogMouseUp">
|
||||
@closed="clearVrcRegistryDialog">
|
||||
<div style="margin-top: 10px">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; font-size: 12px">
|
||||
<span class="name" style="margin-right: 24px">{{ t('dialog.registry_backup.auto_backup') }}</span>
|
||||
@@ -70,19 +67,17 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</safe-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getCurrentInstance, inject, ref, watch } from 'vue';
|
||||
import { getCurrentInstance, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import configRepository from '../../../service/config';
|
||||
import utils from '../../../classes/utils';
|
||||
const { t } = useI18n();
|
||||
import { downloadAndSaveJson } from '../../../composables/shared/utils';
|
||||
import configRepository from '../../../service/config';
|
||||
|
||||
const beforeDialogClose = inject('beforeDialogClose');
|
||||
const dialogMouseDown = inject('dialogMouseDown');
|
||||
const dialogMouseUp = inject('dialogMouseUp');
|
||||
const { t } = useI18n();
|
||||
|
||||
const instance = getCurrentInstance();
|
||||
const { $confirm, $message, $prompt } = instance.proxy;
|
||||
@@ -172,7 +167,7 @@
|
||||
}
|
||||
|
||||
function saveVrcRegistryBackupToFile(row) {
|
||||
utils.downloadAndSaveJson(row.name, row.data);
|
||||
downloadAndSaveJson(row.name, row.data);
|
||||
}
|
||||
|
||||
async function deleteVrcRegistryBackup(row) {
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
<safe-dialog
|
||||
class="x-dialog"
|
||||
:before-close="beforeDialogClose"
|
||||
:visible.sync="screenshotMetadataDialog.visible"
|
||||
:title="t('dialog.screenshot_metadata.header')"
|
||||
width="1050px"
|
||||
top="10vh"
|
||||
@mousedown.native="dialogMouseDown"
|
||||
@mouseup.native="dialogMouseUp">
|
||||
top="10vh">
|
||||
<div
|
||||
v-if="screenshotMetadataDialog.visible"
|
||||
v-loading="screenshotMetadataDialog.loading"
|
||||
@@ -161,19 +158,16 @@
|
||||
<br />
|
||||
</span>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</safe-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, inject, computed, getCurrentInstance, watch } from 'vue';
|
||||
import { ref, inject, getCurrentInstance, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { vrcPlusImageRequest } from '../../../api';
|
||||
import Location from '../../../components/Location.vue';
|
||||
|
||||
const API = inject('API');
|
||||
const beforeDialogClose = inject('beforeDialogClose');
|
||||
const dialogMouseDown = inject('dialogMouseDown');
|
||||
const dialogMouseUp = inject('dialogMouseUp');
|
||||
const showFullscreenImageDialog = inject('showFullscreenImageDialog');
|
||||
|
||||
const { t } = useI18n();
|
||||
@@ -302,6 +296,9 @@
|
||||
vrcPlusImageRequest
|
||||
.uploadGalleryImage(base64Body)
|
||||
.then((args) => {
|
||||
// about uploadGalleryImage -> emit 'GALLERYIMAGE:ADD'
|
||||
// no need to add to the gallery logic here
|
||||
// because it refreshes when you open the gallery
|
||||
$message({
|
||||
message: t('message.gallery.uploaded'),
|
||||
type: 'success'
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
<safe-dialog
|
||||
class="x-dialog"
|
||||
:before-close="beforeDialogClose"
|
||||
:visible="isVRChatConfigDialogVisible"
|
||||
:title="t('dialog.config_json.header')"
|
||||
width="420px"
|
||||
top="10vh"
|
||||
@close="closeDialog"
|
||||
@mousedown.native="dialogMouseDown"
|
||||
@mouseup.native="dialogMouseUp">
|
||||
@close="closeDialog">
|
||||
<div v-loading="loading">
|
||||
<div style="font-size: 12px; word-break: keep-all">
|
||||
{{ t('dialog.config_json.description1') }} <br />
|
||||
@@ -185,26 +182,24 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</safe-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, inject, getCurrentInstance, computed } from 'vue';
|
||||
import { computed, getCurrentInstance, inject, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import {
|
||||
getVRChatResolution,
|
||||
VRChatScreenshotResolutions,
|
||||
VRChatCameraResolutions
|
||||
} from '../../../composables/settings/constants/vrchatResolutions';
|
||||
VRChatCameraResolutions,
|
||||
VRChatScreenshotResolutions
|
||||
} from '../../../composables/setting/constants/vrchatResolutions';
|
||||
import { getVRChatResolution } from '../../../composables/setting/utils';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const instance = getCurrentInstance();
|
||||
const $confirm = instance.proxy.$confirm;
|
||||
const $message = instance.proxy.$message;
|
||||
|
||||
const beforeDialogClose = inject('beforeDialogClose');
|
||||
const dialogMouseDown = inject('dialogMouseDown');
|
||||
const dialogMouseUp = inject('dialogMouseUp');
|
||||
const openExternalLink = inject('openExternalLink');
|
||||
|
||||
const props = defineProps({
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
<safe-dialog
|
||||
class="x-dialog"
|
||||
:before-close="beforeDialogClose"
|
||||
:visible="isYouTubeApiDialogVisible"
|
||||
:title="t('dialog.youtube_api.header')"
|
||||
width="400px"
|
||||
@close="closeDialog"
|
||||
@mousedown.native="dialogMouseDown"
|
||||
@mouseup.native="dialogMouseUp">
|
||||
@close="closeDialog">
|
||||
<div style="font-size: 12px">{{ t('dialog.youtube_api.description') }} <br /></div>
|
||||
|
||||
<el-input
|
||||
@@ -32,7 +29,7 @@
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</safe-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -44,9 +41,6 @@
|
||||
const instance = getCurrentInstance();
|
||||
const $message = instance.proxy.$message;
|
||||
|
||||
const beforeDialogClose = inject('beforeDialogClose');
|
||||
const dialogMouseDown = inject('dialogMouseDown');
|
||||
const dialogMouseUp = inject('dialogMouseUp');
|
||||
const openExternalLink = inject('openExternalLink');
|
||||
|
||||
const props = defineProps({
|
||||
|
||||
@@ -179,8 +179,8 @@
|
||||
<script>
|
||||
import FriendItem from '../../../components/FriendItem.vue';
|
||||
import Location from '../../../components/Location.vue';
|
||||
import { isRealInstance as _isRealInstance, parseLocation } from '../../../composables/instance/utils';
|
||||
import configRepository from '../../../service/config';
|
||||
import utils from '../../../classes/utils';
|
||||
|
||||
export default {
|
||||
name: 'FriendsSidebar',
|
||||
@@ -234,7 +234,7 @@
|
||||
if (!friend.ref.$location.isRealInstance && this.lastLocation.friendList.has(friend.id)) {
|
||||
locationTag = this.lastLocation.location;
|
||||
}
|
||||
let isRealInstance = this.isRealInstance(locationTag);
|
||||
const isRealInstance = this.isRealInstance(locationTag);
|
||||
if (!isRealInstance) {
|
||||
return;
|
||||
}
|
||||
@@ -254,30 +254,30 @@
|
||||
|
||||
return sortedFriendsList.sort((a, b) => b.length - a.length);
|
||||
},
|
||||
sameInstanceTag() {
|
||||
const sameInstanceTag = new Set();
|
||||
sameInstanceFriendId() {
|
||||
const sameInstanceFriendId = new Set();
|
||||
for (const item of this.friendsInSameInstance) {
|
||||
for (const friend of item) {
|
||||
if (utils.isRealInstance(friend.ref?.$location.tag)) {
|
||||
sameInstanceTag.add(friend.ref?.$location.tag);
|
||||
if (this.isRealInstance(friend.ref?.$location.tag)) {
|
||||
sameInstanceFriendId.add(friend.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
return sameInstanceTag;
|
||||
return sameInstanceFriendId;
|
||||
},
|
||||
onlineFriendsByGroupStatus() {
|
||||
if (!this.isSidebarGroupByInstance || !this.isHideFriendsInSameInstance) {
|
||||
return this.onlineFriends;
|
||||
}
|
||||
|
||||
return this.onlineFriends.filter((item) => !this.sameInstanceTag.has(item.ref?.$location.tag));
|
||||
return this.onlineFriends.filter((item) => !this.sameInstanceFriendId.has(item.id));
|
||||
},
|
||||
vipFriendsByGroupStatus() {
|
||||
if (!this.isSidebarGroupByInstance || !this.isHideFriendsInSameInstance) {
|
||||
return this.vipFriends;
|
||||
}
|
||||
|
||||
return this.vipFriends.filter((item) => !this.sameInstanceTag.has(item.ref?.$location.tag));
|
||||
return this.vipFriends.filter((item) => !this.sameInstanceFriendId.has(item.id));
|
||||
},
|
||||
// VIP friends divide by group
|
||||
vipFriendsDivideByGroup() {
|
||||
@@ -292,7 +292,7 @@
|
||||
const filteredFriends = this.vipFriends.filter((friend) =>
|
||||
groupFriends.some((item) => {
|
||||
if (this.isSidebarGroupByInstance && this.isHideFriendsInSameInstance) {
|
||||
return item.id === friend.id && !this.sameInstanceTag.has(item.ref?.$location.tag);
|
||||
return item.id === friend.id && !this.sameInstanceFriendId.has(item.id);
|
||||
}
|
||||
return item.id === friend.id;
|
||||
})
|
||||
@@ -337,7 +337,7 @@
|
||||
);
|
||||
},
|
||||
isRealInstance(locationTag) {
|
||||
return utils.isRealInstance(locationTag);
|
||||
return _isRealInstance(locationTag);
|
||||
},
|
||||
toggleSwitchGroupByInstanceCollapsed() {
|
||||
this.isSidebarGroupByInstanceCollapsed = !this.isSidebarGroupByInstanceCollapsed;
|
||||
@@ -357,7 +357,7 @@
|
||||
}
|
||||
}
|
||||
for (const friend of friendsArr) {
|
||||
if (utils.isRealInstance(friend.ref?.travelingToLocation)) {
|
||||
if (this.isRealInstance(friend.ref?.travelingToLocation)) {
|
||||
return friend.ref.travelingToLocation;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
<script>
|
||||
import Location from '../../../components/Location.vue';
|
||||
import utils from '../../../classes/utils';
|
||||
import { convertFileUrlToImageUrl } from '../../../composables/shared/utils';
|
||||
|
||||
export default {
|
||||
name: 'GroupsSidebar',
|
||||
@@ -97,7 +97,7 @@
|
||||
},
|
||||
methods: {
|
||||
getSmallGroupIconUrl(url) {
|
||||
return utils.convertFileUrlToImageUrl(url);
|
||||
return convertFileUrlToImageUrl(url);
|
||||
},
|
||||
toggleGroupSidebarCollapse(groupId) {
|
||||
this.groupInstancesCfg[groupId].isCollapsed = !this.groupInstancesCfg[groupId].isCollapsed;
|
||||
|
||||
Reference in New Issue
Block a user