mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-19 06:43:51 +02:00
refactor: Organize Project Structure (#1211)
* refactor: Organize Project Structure * fix * fix * rm security * fix
This commit is contained in:
@@ -63,7 +63,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Location from '../common/Location.vue';
|
||||
import Location from './Location.vue';
|
||||
|
||||
export default {
|
||||
name: 'FriendItem',
|
||||
224
src/components/LaunchDialog.vue
Normal file
224
src/components/LaunchDialog.vue
Normal file
@@ -0,0 +1,224 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
ref="launchDialog"
|
||||
:before-close="beforeDialogClose"
|
||||
:visible.sync="isVisible"
|
||||
:title="$t('dialog.launch.header')"
|
||||
width="450px"
|
||||
@mousedown.native="dialogMouseDown"
|
||||
@mouseup.native="dialogMouseUp">
|
||||
<el-form :model="launchDialog" label-width="100px">
|
||||
<el-form-item :label="$t('dialog.launch.url')">
|
||||
<el-input
|
||||
v-model="launchDialog.url"
|
||||
size="mini"
|
||||
style="width: 260px"
|
||||
@click.native="$event.target.tagName === 'INPUT' && $event.target.select()" />
|
||||
<el-tooltip placement="right" :content="$t('dialog.launch.copy_tooltip')" :disabled="hideTooltips">
|
||||
<el-button
|
||||
size="mini"
|
||||
icon="el-icon-s-order"
|
||||
style="margin-left: 5px"
|
||||
circle
|
||||
@click="copyInstanceMessage(launchDialog.url)" />
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="launchDialog.shortUrl">
|
||||
<template slot="label">
|
||||
<span>{{ $t('dialog.launch.short_url') }}</span>
|
||||
<el-tooltip
|
||||
placement="top"
|
||||
style="margin-left: 5px"
|
||||
:content="$t('dialog.launch.short_url_notice')">
|
||||
<i class="el-icon-warning" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-input
|
||||
v-model="launchDialog.shortUrl"
|
||||
size="mini"
|
||||
style="width: 260px"
|
||||
@click.native="$event.target.tagName === 'INPUT' && $event.target.select()" />
|
||||
<el-tooltip placement="right" :content="$t('dialog.launch.copy_tooltip')" :disabled="hideTooltips">
|
||||
<el-button
|
||||
size="mini"
|
||||
icon="el-icon-s-order"
|
||||
style="margin-left: 5px"
|
||||
circle
|
||||
@click="copyInstanceMessage(launchDialog.shortUrl)" />
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('dialog.launch.location')">
|
||||
<el-input
|
||||
v-model="launchDialog.location"
|
||||
size="mini"
|
||||
style="width: 260px"
|
||||
@click.native="$event.target.tagName === 'INPUT' && $event.target.select()" />
|
||||
<el-tooltip placement="right" :content="$t('dialog.launch.copy_tooltip')" :disabled="hideTooltips">
|
||||
<el-button
|
||||
size="mini"
|
||||
icon="el-icon-s-order"
|
||||
style="margin-left: 5px"
|
||||
circle
|
||||
@click="copyInstanceMessage(launchDialog.location)" />
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-checkbox v-model="launchDialog.desktop" style="float: left; margin-top: 5px" @change="saveLaunchDialog">
|
||||
{{ $t('dialog.launch.start_as_desktop') }}
|
||||
</el-checkbox>
|
||||
<template slot="footer">
|
||||
<el-button size="small" @click="showPreviousInstancesInfoDialog(launchDialog.location)">
|
||||
{{ $t('dialog.launch.info') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
:disabled="!checkCanInvite(launchDialog.location)"
|
||||
@click="showInviteDialog(launchDialog.location)">
|
||||
{{ $t('dialog.launch.invite') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
:disabled="!launchDialog.secureOrShortName"
|
||||
@click="launchGame(launchDialog.location, launchDialog.shortName, launchDialog.desktop)">
|
||||
{{ $t('dialog.launch.launch') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import utils from '../classes/utils';
|
||||
import configRepository from '../service/config';
|
||||
import { instanceRequest } from '../api';
|
||||
|
||||
export default {
|
||||
name: 'LaunchDialog',
|
||||
inject: [
|
||||
'beforeDialogClose',
|
||||
'dialogMouseDown',
|
||||
'dialogMouseUp',
|
||||
'showPreviousInstancesInfoDialog',
|
||||
'showInviteDialog',
|
||||
'adjustDialogZ'
|
||||
],
|
||||
props: {
|
||||
hideTooltips: Boolean,
|
||||
launchDialogData: { type: Object, required: true },
|
||||
checkCanInvite: {
|
||||
type: Function,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
launchDialog: {
|
||||
loading: false,
|
||||
desktop: false,
|
||||
tag: '',
|
||||
location: '',
|
||||
url: '',
|
||||
shortName: '',
|
||||
shortUrl: '',
|
||||
secureOrShortName: ''
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isVisible: {
|
||||
get() {
|
||||
return this.launchDialogData.visible;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:launch-dialog-data', { ...this.launchDialogData, visible: value });
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'launchDialogData.loading': {
|
||||
handler() {
|
||||
this.getConfig();
|
||||
this.initLaunchDialog();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.getConfig();
|
||||
},
|
||||
methods: {
|
||||
launchGame(location, shortName, desktop) {
|
||||
this.$emit('launch-game', location, shortName, desktop);
|
||||
this.isVisible = false;
|
||||
},
|
||||
getConfig() {
|
||||
configRepository.getBool('launchAsDesktop').then((value) => (this.launchDialog.desktop = value));
|
||||
},
|
||||
saveLaunchDialog() {
|
||||
configRepository.setBool('launchAsDesktop', this.launchDialog.desktop);
|
||||
},
|
||||
async initLaunchDialog() {
|
||||
const { tag, shortName } = this.launchDialogData;
|
||||
if (!utils.isRealInstance(tag)) {
|
||||
return;
|
||||
}
|
||||
this.$nextTick(() => this.adjustDialogZ(this.$refs.launchDialog.$el));
|
||||
const D = this.launchDialog;
|
||||
D.tag = tag;
|
||||
D.secureOrShortName = shortName;
|
||||
D.shortUrl = '';
|
||||
D.shortName = shortName;
|
||||
const L = utils.parseLocation(tag);
|
||||
L.shortName = shortName;
|
||||
if (shortName) {
|
||||
D.shortUrl = `https://vrch.at/${shortName}`;
|
||||
}
|
||||
if (L.instanceId) {
|
||||
D.location = `${L.worldId}:${L.instanceId}`;
|
||||
} else {
|
||||
D.location = L.worldId;
|
||||
}
|
||||
D.url = utils.getLaunchURL(L);
|
||||
if (!shortName) {
|
||||
const res = await instanceRequest.getInstanceShortName({
|
||||
worldId: L.worldId,
|
||||
instanceId: L.instanceId
|
||||
});
|
||||
// NOTE:
|
||||
// splitting the 'INSTANCE:SHORTNAME' event and put code here
|
||||
if (!res.json) {
|
||||
return;
|
||||
}
|
||||
const resLocation = `${res.instance.worldId}:${res.instance.instanceId}`;
|
||||
if (resLocation === this.launchDialog.tag) {
|
||||
const resShortName = res.json.shortName;
|
||||
const secureOrShortName = res.json.shortName || res.json.secureName;
|
||||
const parsedL = utils.parseLocation(resLocation);
|
||||
parsedL.shortName = resShortName;
|
||||
this.launchDialog.shortName = resShortName;
|
||||
this.launchDialog.secureOrShortName = secureOrShortName;
|
||||
if (resShortName) {
|
||||
this.launchDialog.shortUrl = `https://vrch.at/${resShortName}`;
|
||||
}
|
||||
this.launchDialog.url = utils.getLaunchURL(parsedL);
|
||||
}
|
||||
}
|
||||
},
|
||||
async copyInstanceMessage(input) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(input);
|
||||
this.$message({
|
||||
message: 'Instance copied to clipboard',
|
||||
type: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
this.$message({
|
||||
message: 'Instance copied failed',
|
||||
type: 'error'
|
||||
});
|
||||
console.error(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -16,7 +16,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import utils from '../../classes/utils';
|
||||
import utils from '../classes/utils';
|
||||
|
||||
export default {
|
||||
// eslint-disable-next-line vue/multi-word-component-names
|
||||
@@ -142,7 +142,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
handleShowGroupDialog(){
|
||||
handleShowGroupDialog() {
|
||||
let location = this.location;
|
||||
if (this.isTraveling) {
|
||||
location = this.traveling;
|
||||
104
src/components/NavMenu.vue
Normal file
104
src/components/NavMenu.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<el-menu
|
||||
ref="navRef"
|
||||
collapse
|
||||
@select="$emit('select', $event)"
|
||||
:default-active="menuActiveIndex"
|
||||
:collapse-transition="false">
|
||||
<el-menu-item index="feed">
|
||||
<i class="el-icon-news"></i>
|
||||
<template slot="title">
|
||||
<span>{{ $t('nav_tooltip.feed') }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item index="gameLog">
|
||||
<i class="el-icon-s-data"></i>
|
||||
<template slot="title">
|
||||
<span>{{ $t('nav_tooltip.game_log') }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item index="playerList">
|
||||
<i class="el-icon-tickets"></i>
|
||||
<template slot="title">
|
||||
<span>{{ $t('nav_tooltip.player_list') }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item index="search">
|
||||
<i class="el-icon-search"></i>
|
||||
<template slot="title">
|
||||
<span>{{ $t('nav_tooltip.search') }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item index="favorite">
|
||||
<i class="el-icon-star-off"></i>
|
||||
<template slot="title">
|
||||
<span>{{ $t('nav_tooltip.favorites') }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item index="friendLog">
|
||||
<i class="el-icon-notebook-2"></i>
|
||||
<template slot="title">
|
||||
<span>{{ $t('nav_tooltip.friend_log') }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item index="moderation">
|
||||
<i class="el-icon-finished"></i>
|
||||
<template slot="title">
|
||||
<span>{{ $t('nav_tooltip.moderation') }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item index="notification">
|
||||
<i class="el-icon-bell"></i>
|
||||
<template slot="title">
|
||||
<span>{{ $t('nav_tooltip.notification') }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item index="friendList">
|
||||
<i class="el-icon-s-management"></i>
|
||||
<template slot="title">
|
||||
<span>{{ $t('nav_tooltip.friend_list') }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item index="charts">
|
||||
<i class="el-icon-data-analysis"></i>
|
||||
<template slot="title">
|
||||
<span>{{ $t('nav_tooltip.charts') }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item index="profile">
|
||||
<i class="el-icon-user"></i>
|
||||
<template slot="title">
|
||||
<span>{{ $t('nav_tooltip.profile') }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item index="settings">
|
||||
<i class="el-icon-s-tools"></i>
|
||||
<template slot="title">
|
||||
<span>{{ $t('nav_tooltip.settings') }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'NavMenu',
|
||||
props: {
|
||||
menuActiveIndex: {
|
||||
type: String,
|
||||
default: 'feed'
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,826 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="options-container instance-activity" style="margin-top: 0">
|
||||
<div>
|
||||
<span>{{ $t('view.charts.instance_activity.header') }}</span>
|
||||
<el-popover placement="bottom-start" trigger="hover" width="300">
|
||||
<div class="tips-popover">
|
||||
<div>{{ $t('view.charts.instance_activity.tips.online_time') }}</div>
|
||||
<div>{{ $t('view.charts.instance_activity.tips.click_Y_axis') }}</div>
|
||||
<div>{{ $t('view.charts.instance_activity.tips.click_instance_name') }}</div>
|
||||
<div>
|
||||
<i class="el-icon-warning-outline"></i
|
||||
><i>{{ $t('view.charts.instance_activity.tips.accuracy_notice') }}</i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<i
|
||||
slot="reference"
|
||||
class="el-icon-info"
|
||||
style="margin-left: 5px; font-size: 12px; opacity: 0.7"></i>
|
||||
</el-popover>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<el-tooltip :content="$t('view.charts.instance_activity.refresh')" placement="top"
|
||||
><el-button icon="el-icon-refresh" circle style="margin-right: 9px" @click="reloadData"></el-button
|
||||
></el-tooltip>
|
||||
<el-tooltip :content="$t('view.charts.instance_activity.settings.header')" placement="top">
|
||||
<el-popover placement="bottom" trigger="click" style="margin-right: 9px">
|
||||
<div class="settings">
|
||||
<div>
|
||||
<span>{{ $t('view.charts.instance_activity.settings.bar_width') }}</span>
|
||||
<div>
|
||||
<el-slider
|
||||
v-model.lazy="barWidth"
|
||||
:max="50"
|
||||
:min="1"
|
||||
@change="changeBarWidth"></el-slider>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ $t('view.charts.instance_activity.settings.show_detail') }}</span>
|
||||
<el-switch v-model="isDetailVisible" @change="changeIsDetailInstanceVisible">
|
||||
</el-switch>
|
||||
</div>
|
||||
<div v-if="isDetailVisible">
|
||||
<span>{{ $t('view.charts.instance_activity.settings.show_solo_instance') }}</span>
|
||||
<el-switch v-model="isSoloInstanceVisible" @change="changeIsSoloInstanceVisible">
|
||||
</el-switch>
|
||||
</div>
|
||||
<div v-if="isDetailVisible">
|
||||
<span>{{ $t('view.charts.instance_activity.settings.show_no_friend_instance') }}</span>
|
||||
<el-switch
|
||||
v-model="isNoFriendInstanceVisible"
|
||||
@change="changeIsNoFriendInstanceVisible">
|
||||
</el-switch>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-button slot="reference" icon="el-icon-setting" circle></el-button>
|
||||
</el-popover>
|
||||
</el-tooltip>
|
||||
<el-button-group style="margin-right: 10px">
|
||||
<el-tooltip :content="$t('view.charts.instance_activity.previous_day')" placement="top">
|
||||
<el-button
|
||||
icon="el-icon-arrow-left"
|
||||
:disabled="isPrevDayBtnDisabled"
|
||||
@click="changeSelectedDateFromBtn(false)"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="$t('view.charts.instance_activity.next_day')" placement="top">
|
||||
<el-button :disabled="isNextDayBtnDisabled" @click="changeSelectedDateFromBtn(true)"
|
||||
><i class="el-icon-arrow-right el-icon--right"></i
|
||||
></el-button>
|
||||
</el-tooltip>
|
||||
</el-button-group>
|
||||
<el-date-picker
|
||||
v-model="selectedDate"
|
||||
type="date"
|
||||
:clearable="false"
|
||||
align="right"
|
||||
:picker-options="{
|
||||
disabledDate: (time) => getDatePickerDisabledDate(time)
|
||||
}"
|
||||
@change="reloadData"></el-date-picker>
|
||||
</div>
|
||||
</div>
|
||||
<div style="position: relative">
|
||||
<el-statistic :title="$t('view.charts.instance_activity.online_time')">
|
||||
<template #formatter>
|
||||
<span :style="isDarkMode ? 'color:rgb(120,120,120)' : ''">{{ totalOnlineTime }}</span>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</div>
|
||||
|
||||
<div ref="activityChartRef" style="width: 100%"></div>
|
||||
<div v-if="!isLoading && activityData.length === 0" class="nodata">
|
||||
<span>No data here, try another day</span>
|
||||
</div>
|
||||
|
||||
<transition name="el-fade-in-linear">
|
||||
<div v-show="isDetailVisible && !isLoading && !activityData.length === 0" class="divider">
|
||||
<el-divider>·</el-divider>
|
||||
</div>
|
||||
</transition>
|
||||
<InstanceActivityDetail
|
||||
v-for="arr in filteredActivityDetailData"
|
||||
:key="arr[0].location + arr[0].created_at"
|
||||
ref="activityDetailChartRef"
|
||||
:activity-detail-data="arr"
|
||||
:is-dark-mode="isDarkMode"
|
||||
:dt-hour12="dtHour12"
|
||||
:bar-width="barWidth"
|
||||
@open-previous-instance-info-dialog="$emit('open-previous-instance-info-dialog', $event)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import dayjs from 'dayjs';
|
||||
import database from '../../repository/database';
|
||||
import utils from '../../classes/utils';
|
||||
import configRepository from '../../repository/config';
|
||||
import InstanceActivityDetail from './InstanceActivityDetail.vue';
|
||||
|
||||
export default {
|
||||
name: 'InstanceActivity',
|
||||
components: {
|
||||
InstanceActivityDetail
|
||||
},
|
||||
inject: ['API'],
|
||||
props: {
|
||||
getWorldName: { type: Function, default: () => [] },
|
||||
isDarkMode: Boolean,
|
||||
dtHour12: Boolean,
|
||||
friendsMap: Map,
|
||||
localFavoriteFriends: Set
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// echarts and observer
|
||||
echarts: null,
|
||||
echartsInstance: null,
|
||||
resizeObserver: null,
|
||||
intersectionObservers: [],
|
||||
selectedDate: dayjs().add(-1, 'day'),
|
||||
// data
|
||||
activityData: [],
|
||||
activityDetailData: [],
|
||||
allDateOfActivity: new Set(),
|
||||
worldNameArray: [],
|
||||
// options
|
||||
isLoading: true,
|
||||
// settings
|
||||
barWidth: 25,
|
||||
isDetailVisible: true,
|
||||
isSoloInstanceVisible: true,
|
||||
isNoFriendInstanceVisible: true
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
totalOnlineTime() {
|
||||
return utils.timeToText(
|
||||
this.activityData.reduce((acc, item) => acc + item.time, 0),
|
||||
true
|
||||
);
|
||||
},
|
||||
isNextDayBtnDisabled() {
|
||||
return dayjs(this.selectedDate).isSameOrAfter(this.allDateOfActivityArray[0], 'day');
|
||||
},
|
||||
isPrevDayBtnDisabled() {
|
||||
return dayjs(this.selectedDate).isSame(
|
||||
this.allDateOfActivityArray[this.allDateOfActivityArray.length - 1],
|
||||
'day'
|
||||
);
|
||||
},
|
||||
allDateOfActivityArray() {
|
||||
return this.allDateOfActivity
|
||||
? Array.from(this.allDateOfActivity)
|
||||
.map((item) => dayjs(item))
|
||||
.sort((a, b) => b.valueOf() - a.valueOf())
|
||||
: [];
|
||||
},
|
||||
filteredActivityDetailData() {
|
||||
if (!this.isDetailVisible) {
|
||||
return [];
|
||||
}
|
||||
let result = [...this.activityDetailData];
|
||||
if (!this.isSoloInstanceVisible) {
|
||||
result = result.filter((arr) => arr.length > 1);
|
||||
}
|
||||
if (!this.isNoFriendInstanceVisible) {
|
||||
result = result.filter((arr) => {
|
||||
// solo instance
|
||||
if (arr.length === 1) {
|
||||
return true;
|
||||
}
|
||||
return arr.some((item) => item.isFriend);
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
isDarkMode() {
|
||||
if (this.echartsInstance) {
|
||||
this.echartsInstance.dispose();
|
||||
this.echartsInstance = null;
|
||||
this.initEcharts();
|
||||
}
|
||||
},
|
||||
dtHour12() {
|
||||
if (this.echartsInstance) {
|
||||
this.initEcharts();
|
||||
}
|
||||
}
|
||||
},
|
||||
activated() {
|
||||
// first time also call activated
|
||||
if (this.echartsInstance) {
|
||||
this.reloadData();
|
||||
}
|
||||
},
|
||||
deactivated() {
|
||||
// prevent resize animation when switch tab
|
||||
this.resizeObserver.disconnect();
|
||||
},
|
||||
created() {
|
||||
this.resizeObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
this.echartsInstance.resize({
|
||||
width: entry.contentRect.width,
|
||||
animation: {
|
||||
duration: 300
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
configRepository.getInt('VRCX_InstanceActivityBarWidth', 25).then((value) => {
|
||||
this.barWidth = value;
|
||||
});
|
||||
configRepository.getBool('VRCX_InstanceActivityDetailVisible', true).then((value) => {
|
||||
this.isDetailVisible = value;
|
||||
});
|
||||
configRepository.getBool('VRCX_InstanceActivitySoloInstanceVisible', true).then((value) => {
|
||||
this.isSoloInstanceVisible = value;
|
||||
});
|
||||
configRepository.getBool('VRCX_InstanceActivityNoFriendInstanceVisible', true).then((value) => {
|
||||
this.isNoFriendInstanceVisible = value;
|
||||
});
|
||||
},
|
||||
async mounted() {
|
||||
try {
|
||||
this.getAllDateOfActivity();
|
||||
const [echartsModule] = await Promise.all([
|
||||
// lazy load echarts
|
||||
utils.loadEcharts().catch((error) => {
|
||||
console.error('lazy load echarts failed', error);
|
||||
return null;
|
||||
}),
|
||||
this.getActivityData()
|
||||
]);
|
||||
if (echartsModule) {
|
||||
this.echarts = echartsModule;
|
||||
}
|
||||
if (echartsModule) {
|
||||
// activity data is ready, but world name data isn't ready
|
||||
// so init echarts with empty data, reduce the render time of init screen
|
||||
this.initEcharts(true);
|
||||
this.getWorldNameData();
|
||||
} else {
|
||||
this.isLoading = false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('error in mounted', error);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async reloadData() {
|
||||
this.isLoading = true;
|
||||
await this.getActivityData();
|
||||
this.getWorldNameData();
|
||||
// possibility past 24:00
|
||||
this.getAllDateOfActivity();
|
||||
},
|
||||
|
||||
// echarts - start
|
||||
initEcharts() {
|
||||
const chartsHeight = this.activityData.length * (this.barWidth + 10) + 200;
|
||||
const chartDom = this.$refs.activityChartRef;
|
||||
|
||||
const afterInit = () => {
|
||||
this.echartsInstance.resize({
|
||||
height: chartsHeight,
|
||||
animation: {
|
||||
duration: 300
|
||||
}
|
||||
});
|
||||
|
||||
const handleClickYAxisLabel = (params) => {
|
||||
const detailDataIdx = this.filteredActivityDetailData.findIndex((arr) => {
|
||||
const sameLocation = arr[0]?.location === this.activityData[params?.dataIndex]?.location;
|
||||
const sameJoinTime = arr
|
||||
.find((item) => item.user_id === this.API.currentUser.id)
|
||||
?.joinTime.isSame(this.activityData[params?.dataIndex].joinTime);
|
||||
return sameLocation && sameJoinTime;
|
||||
});
|
||||
if (detailDataIdx === -1) {
|
||||
// no detail chart down below, it's hidden, so can't find instance data index
|
||||
console.error(
|
||||
"handleClickYAxisLabel failed, likely current user wasn't in this instance.",
|
||||
params
|
||||
);
|
||||
} else {
|
||||
this.$refs.activityDetailChartRef[detailDataIdx].$el.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const options = this.activityData.length ? this.getNewOption() : {};
|
||||
|
||||
this.echartsInstance.setOption(options, { lazyUpdate: true });
|
||||
this.echartsInstance.on('click', 'yAxis', handleClickYAxisLabel);
|
||||
this.isLoading = false;
|
||||
};
|
||||
|
||||
const initEchartsInstance = () => {
|
||||
this.echartsInstance = this.echarts.init(chartDom, `${this.isDarkMode ? 'dark' : null}`, {
|
||||
height: chartsHeight
|
||||
});
|
||||
this.resizeObserver.observe(chartDom);
|
||||
};
|
||||
|
||||
const loadEchartsWithTimeout = () => {
|
||||
const timeout = 5000;
|
||||
let time = 0;
|
||||
const timer = setInterval(() => {
|
||||
if (this.echarts) {
|
||||
initEchartsInstance();
|
||||
afterInit();
|
||||
clearInterval(timer);
|
||||
return;
|
||||
}
|
||||
time += 100;
|
||||
if (time >= timeout) {
|
||||
clearInterval(timer);
|
||||
console.error('echarts init timeout');
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
if (!this.echartsInstance) {
|
||||
if (!this.echarts) {
|
||||
loadEchartsWithTimeout();
|
||||
} else {
|
||||
initEchartsInstance();
|
||||
afterInit();
|
||||
}
|
||||
} else {
|
||||
afterInit();
|
||||
}
|
||||
},
|
||||
getNewOption() {
|
||||
const getTooltip = (params) => {
|
||||
const activityData = this.activityData;
|
||||
const param = params[1];
|
||||
|
||||
if (!activityData || !activityData[param.dataIndex]) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const instanceData = activityData[param.dataIndex];
|
||||
|
||||
const format = this.dtHour12 ? 'hh:mm:ss A' : 'HH:mm:ss';
|
||||
|
||||
const formattedLeftDateTime = dayjs(instanceData.leaveTime).format(format);
|
||||
const formattedJoinDateTime = dayjs(instanceData.joinTime).format(format);
|
||||
|
||||
const timeString = utils.timeToText(param.data, true);
|
||||
const color = param.color;
|
||||
const name = param.name;
|
||||
const location = utils.parseLocation(instanceData.location);
|
||||
|
||||
return `
|
||||
<div style="display: flex; align-items: center;">
|
||||
<div style="width: 10px; height: 55px; background-color: ${color}; margin-right: 5px;"></div>
|
||||
<div>
|
||||
<div>${name} #${location.instanceName} ${location.accessTypeName}</div>
|
||||
<div>${formattedJoinDateTime} - ${formattedLeftDateTime}</div>
|
||||
<div>${timeString}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
const format = this.dtHour12 ? 'hh:mm A' : 'HH:mm';
|
||||
|
||||
const echartsOption = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
},
|
||||
formatter: getTooltip
|
||||
},
|
||||
grid: {
|
||||
top: 50,
|
||||
left: 160,
|
||||
right: 90
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
axisLabel: {
|
||||
interval: 0,
|
||||
formatter: (value) => (value.length > 20 ? `${value.slice(0, 20)}...` : value)
|
||||
},
|
||||
inverse: true,
|
||||
data: this.worldNameArray,
|
||||
triggerEvent: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
// axisLabel max 24:00
|
||||
max: 24 * 60 * 60 * 1000,
|
||||
// axisLabel interval 3hr
|
||||
interval: 3 * 60 * 60 * 1000,
|
||||
axisLine: { show: true },
|
||||
axisLabel: {
|
||||
formatter: (value) => dayjs(value).utc().format(format)
|
||||
},
|
||||
splitLine: { lineStyle: { type: 'dashed' } }
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'Placeholder',
|
||||
type: 'bar',
|
||||
stack: 'Total',
|
||||
itemStyle: {
|
||||
borderColor: 'transparent',
|
||||
color: 'transparent'
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
borderColor: 'transparent',
|
||||
color: 'transparent'
|
||||
}
|
||||
},
|
||||
data: this.activityData.map((item, idx) => {
|
||||
if (idx === 0) {
|
||||
const midnight = dayjs.tz(this.selectedDate).startOf('day');
|
||||
if (midnight.isAfter(item.joinTime)) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return item.joinTime - dayjs.tz(this.selectedDate).startOf('day');
|
||||
})
|
||||
},
|
||||
{
|
||||
name: 'Time',
|
||||
type: 'bar',
|
||||
stack: 'Total',
|
||||
colorBy: 'data',
|
||||
barWidth: this.barWidth,
|
||||
emphasis: {
|
||||
focus: 'self'
|
||||
},
|
||||
itemStyle: {
|
||||
borderRadius: 2,
|
||||
shadowBlur: 2,
|
||||
shadowOffsetX: 0.7,
|
||||
shadowOffsetY: 0.5
|
||||
},
|
||||
data: this.activityData.map((item, idx) => {
|
||||
// If the joinTime of the first data is on the previous day,
|
||||
// and the data traverses midnight, the duration starts at midnight
|
||||
if (idx === 0) {
|
||||
const midnight = dayjs.tz(this.selectedDate).startOf('day');
|
||||
if (midnight.isAfter(item.joinTime)) {
|
||||
return item.leaveTime - dayjs.tz(midnight);
|
||||
}
|
||||
}
|
||||
return item.time;
|
||||
})
|
||||
}
|
||||
],
|
||||
backgroundColor: 'transparent'
|
||||
};
|
||||
|
||||
return echartsOption;
|
||||
},
|
||||
// echarts - end
|
||||
|
||||
// settings - start
|
||||
changeBarWidth(value) {
|
||||
this.barWidth = value;
|
||||
this.initEcharts();
|
||||
configRepository.setInt('VRCX_InstanceActivityBarWidth', value).finally(() => {
|
||||
this.handleChangeSettings();
|
||||
});
|
||||
},
|
||||
changeIsDetailInstanceVisible(value) {
|
||||
this.isDetailVisible = value;
|
||||
configRepository.setBool('VRCX_InstanceActivityDetailVisible', value).finally(() => {
|
||||
this.handleChangeSettings();
|
||||
});
|
||||
},
|
||||
changeIsSoloInstanceVisible(value) {
|
||||
this.isSoloInstanceVisible = value;
|
||||
configRepository.setBool('VRCX_InstanceActivitySoloInstanceVisible', value).finally(() => {
|
||||
this.handleChangeSettings();
|
||||
});
|
||||
},
|
||||
changeIsNoFriendInstanceVisible(value) {
|
||||
this.isNoFriendInstanceVisible = value;
|
||||
configRepository.setBool('VRCX_InstanceActivityNoFriendInstanceVisible', value).finally(() => {
|
||||
this.handleChangeSettings();
|
||||
});
|
||||
},
|
||||
handleChangeSettings() {
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.activityDetailChartRef) {
|
||||
this.$refs.activityDetailChartRef.forEach((child) => {
|
||||
requestAnimationFrame(() => {
|
||||
if (child.echartsInstance) {
|
||||
child.initEcharts();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
//rerender detail chart
|
||||
},
|
||||
// settings - end
|
||||
|
||||
// options - start
|
||||
changeSelectedDateFromBtn(isNext = false) {
|
||||
if (!this.allDateOfActivityArray || this.allDateOfActivityArray.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const idx = this.allDateOfActivityArray.findIndex((date) => date.isSame(this.selectedDate, 'day'));
|
||||
if (idx !== -1) {
|
||||
const newIdx = isNext ? idx - 1 : idx + 1;
|
||||
|
||||
if (newIdx >= 0 && newIdx < this.allDateOfActivityArray.length) {
|
||||
this.selectedDate = this.allDateOfActivityArray[newIdx];
|
||||
this.reloadData();
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.selectedDate = isNext
|
||||
? this.allDateOfActivityArray[this.allDateOfActivityArray.length - 1]
|
||||
: this.allDateOfActivityArray[0];
|
||||
this.reloadData();
|
||||
},
|
||||
getDatePickerDisabledDate(time) {
|
||||
if (
|
||||
time > Date.now() ||
|
||||
this.allDateOfActivityArray[this.allDateOfActivityArray.length - 1]
|
||||
?.add('-1', 'day')
|
||||
.isAfter(time, 'day') ||
|
||||
!this.allDateOfActivity
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return !this.allDateOfActivity.has(dayjs(time).format('YYYY-MM-DD'));
|
||||
},
|
||||
// options - end
|
||||
|
||||
// data - start
|
||||
async getWorldNameData() {
|
||||
this.worldNameArray = await Promise.all(
|
||||
this.activityData.map(async (item) => {
|
||||
try {
|
||||
return await this.getWorldName(item.location);
|
||||
} catch {
|
||||
// TODO: no notification
|
||||
console.error('getWorldName failed location', item.location);
|
||||
return 'Unknown world';
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
if (this.worldNameArray) {
|
||||
this.initEcharts();
|
||||
}
|
||||
},
|
||||
async getAllDateOfActivity() {
|
||||
const utcDateStrings = await database.getDateOfInstanceActivity();
|
||||
const uniqueDates = new Set();
|
||||
|
||||
for (const utcString of utcDateStrings) {
|
||||
const formattedDate = dayjs.utc(utcString).tz().format('YYYY-MM-DD');
|
||||
uniqueDates.add(formattedDate);
|
||||
}
|
||||
|
||||
this.allDateOfActivity = uniqueDates;
|
||||
},
|
||||
async getActivityData() {
|
||||
const localStartDate = dayjs.tz(this.selectedDate).startOf('day').toISOString();
|
||||
const localEndDate = dayjs.tz(this.selectedDate).endOf('day').toISOString();
|
||||
const dbData = await database.getInstanceActivity(localStartDate, localEndDate);
|
||||
|
||||
const transformData = (item) => ({
|
||||
...item,
|
||||
joinTime: dayjs(item.created_at).subtract(item.time, 'millisecond'),
|
||||
leaveTime: dayjs(item.created_at),
|
||||
time: item.time < 0 ? 0 : item.time,
|
||||
isFriend: item.user_id === this.API.currentUser.id ? null : this.friendsMap.has(item.user_id),
|
||||
isFavorite:
|
||||
item.user_id === this.API.currentUser.id ? null : this.localFavoriteFriends.has(item.user_id)
|
||||
});
|
||||
|
||||
this.activityData = dbData.currentUserData.map(transformData);
|
||||
|
||||
const transformAndSort = (arr) => {
|
||||
return arr.map(transformData).sort((a, b) => {
|
||||
const timeDiff = Math.abs(a.joinTime.diff(b.joinTime, 'second'));
|
||||
// recording delay, under 3s is considered the same time entry, beautify the chart
|
||||
return timeDiff < 3 ? a.leaveTime - b.leaveTime : a.joinTime - b.joinTime;
|
||||
});
|
||||
};
|
||||
|
||||
const filterByLocation = (innerArray, locationSet) => {
|
||||
return innerArray.every((innerObject) => locationSet.has(innerObject.location));
|
||||
};
|
||||
const locationSet = new Set(this.activityData.map((item) => item.location));
|
||||
|
||||
const preSplitActivityDetailData = Array.from(dbData.detailData.values())
|
||||
.map(transformAndSort)
|
||||
.filter((innerArray) => filterByLocation(innerArray, locationSet));
|
||||
|
||||
this.activityDetailData = this.handleSplitActivityDetailData(
|
||||
preSplitActivityDetailData,
|
||||
this.API.currentUser.id
|
||||
);
|
||||
|
||||
if (this.activityDetailData.length) {
|
||||
this.$nextTick(() => {
|
||||
this.handleIntersectionObserver();
|
||||
});
|
||||
}
|
||||
},
|
||||
handleSplitActivityDetailData(activityDetailData, currentUserId) {
|
||||
function countTargetIdOccurrences(innerArray, targetId) {
|
||||
let count = 0;
|
||||
for (const obj of innerArray) {
|
||||
if (obj.user_id === targetId) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
function areIntervalsOverlapping(objA, objB) {
|
||||
const isObj1EndTimeBeforeObj2StartTime = objA.leaveTime.isBefore(objB.joinTime, 'second');
|
||||
const isObj2EndTimeBeforeObj1StartTime = objB.leaveTime.isBefore(objA.joinTime, 'second');
|
||||
return !(isObj1EndTimeBeforeObj2StartTime || isObj2EndTimeBeforeObj1StartTime);
|
||||
}
|
||||
|
||||
function buildOverlapGraph(innerArray) {
|
||||
const numObjects = innerArray.length;
|
||||
const adjacencyList = Array.from({ length: numObjects }, () => []);
|
||||
|
||||
for (let i = 0; i < numObjects; i++) {
|
||||
for (let j = i + 1; j < numObjects; j++) {
|
||||
if (areIntervalsOverlapping(innerArray[i], innerArray[j])) {
|
||||
adjacencyList[i].push(j);
|
||||
adjacencyList[j].push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
return adjacencyList;
|
||||
}
|
||||
|
||||
function depthFirstSearch(nodeIndex, visited, graph, component) {
|
||||
visited[nodeIndex] = true;
|
||||
component.push(nodeIndex);
|
||||
for (const neighborIndex of graph[nodeIndex]) {
|
||||
if (!visited[neighborIndex]) {
|
||||
depthFirstSearch(neighborIndex, visited, graph, component);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findConnectedComponents(graph, numNodes) {
|
||||
const visited = new Array(numNodes).fill(false);
|
||||
const components = [];
|
||||
|
||||
for (let i = 0; i < numNodes; i++) {
|
||||
if (!visited[i]) {
|
||||
const component = [];
|
||||
depthFirstSearch(i, visited, graph, component);
|
||||
components.push(component);
|
||||
}
|
||||
}
|
||||
return components;
|
||||
}
|
||||
|
||||
function processOuterArrayWithTargetId(outerArray, targetId) {
|
||||
let i = 0;
|
||||
while (i < outerArray.length) {
|
||||
let currentInnerArray = outerArray[i];
|
||||
let targetIdCount = countTargetIdOccurrences(currentInnerArray, targetId);
|
||||
if (targetIdCount > 1) {
|
||||
let graph = buildOverlapGraph(currentInnerArray);
|
||||
let connectedComponents = findConnectedComponents(graph, currentInnerArray.length);
|
||||
let newInnerArrays = connectedComponents.map((componentIndices) => {
|
||||
return componentIndices.map((index) => currentInnerArray[index]);
|
||||
});
|
||||
outerArray.splice(i, 1, ...newInnerArrays);
|
||||
i += newInnerArrays.length;
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
return outerArray.sort((a, b) => a[0].joinTime - b[0].joinTime);
|
||||
}
|
||||
|
||||
return processOuterArrayWithTargetId(activityDetailData, currentUserId);
|
||||
},
|
||||
// data - end
|
||||
|
||||
// intersection observer - start
|
||||
handleIntersectionObserver() {
|
||||
this.$refs.activityDetailChartRef?.forEach((child, index) => {
|
||||
const observer = new IntersectionObserver(this.handleIntersection.bind(this, index));
|
||||
observer.observe(child.$el);
|
||||
this.intersectionObservers[index] = observer;
|
||||
});
|
||||
},
|
||||
handleIntersection(index, entries) {
|
||||
if (!entries) {
|
||||
console.error('handleIntersection failed');
|
||||
return;
|
||||
}
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting && this.$refs.activityDetailChartRef[index]) {
|
||||
this.$refs.activityDetailChartRef[index].initEcharts();
|
||||
this.intersectionObservers[index].unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}
|
||||
// intersection observer - end
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
%flex {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
%flex-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.instance-activity {
|
||||
@extend %flex;
|
||||
@extend %flex-between;
|
||||
& > div:first-child {
|
||||
@extend %flex-between;
|
||||
}
|
||||
& > div {
|
||||
@extend %flex;
|
||||
> span {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.tips-popover {
|
||||
& > div {
|
||||
margin-bottom: 5px;
|
||||
font-size: 12px;
|
||||
}
|
||||
& > div:last-child {
|
||||
@extend %flex;
|
||||
margin-top: 10px;
|
||||
i {
|
||||
margin-right: 3px;
|
||||
}
|
||||
}
|
||||
& .el-icon-warning-outline {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
.settings {
|
||||
& > div {
|
||||
@extend %flex;
|
||||
@extend %flex-between;
|
||||
padding: 0 2px;
|
||||
height: 30px;
|
||||
> span {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
& > div:first-child {
|
||||
> div {
|
||||
width: 160px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nodata {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 100px;
|
||||
color: #5c5c5c;
|
||||
}
|
||||
.divider {
|
||||
padding: 0 400px;
|
||||
transition: top 0.3s ease;
|
||||
}
|
||||
|
||||
// override el-ui
|
||||
.el-date-editor.el-input,
|
||||
.el-date-editor.el-input__inner {
|
||||
width: 200px;
|
||||
}
|
||||
.el-divider__text {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,343 +0,0 @@
|
||||
<template>
|
||||
<div style="width: 100%">
|
||||
<div style="height: 25px; margin-top: 60px">
|
||||
<transition name="el-fade-in-linear">
|
||||
<location
|
||||
v-show="!isLoading"
|
||||
class="location"
|
||||
:location="activityDetailData[0].location"
|
||||
is-open-previous-instance-info-dialog
|
||||
@open-previous-instance-info-dialog="
|
||||
$emit('open-previous-instance-info-dialog', $event)
|
||||
"></location>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<div ref="activityDetailChart"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import dayjs from 'dayjs';
|
||||
import utils from '../../classes/utils';
|
||||
import Location from '../common/Location.vue';
|
||||
|
||||
export default {
|
||||
name: 'InstanceActivityDetail',
|
||||
components: {
|
||||
Location
|
||||
},
|
||||
inject: ['API', 'showUserDialog'],
|
||||
props: {
|
||||
activityDetailData: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
isDarkMode: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
dtHour12: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
barWidth: {
|
||||
type: Number,
|
||||
required: true,
|
||||
default: 10
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
echarts: null,
|
||||
isLoading: true,
|
||||
echartsInstance: null,
|
||||
usersFirstActivity: null,
|
||||
resizeObserver: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
startTimeStamp() {
|
||||
return this.activityDetailData
|
||||
.find((item) => item.user_id === this.API.currentUser.id)
|
||||
?.joinTime.valueOf();
|
||||
},
|
||||
endTimeStamp() {
|
||||
return this.activityDetailData
|
||||
.find((item) => item.user_id === this.API.currentUser.id)
|
||||
?.leaveTime.valueOf();
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
isDarkMode() {
|
||||
if (this.echartsInstance) {
|
||||
this.echartsInstance.dispose();
|
||||
this.echartsInstance = null;
|
||||
this.initEcharts();
|
||||
}
|
||||
},
|
||||
dtHour12() {
|
||||
if (this.echartsInstance) {
|
||||
this.initEcharts();
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.resizeObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
this.echartsInstance.resize({
|
||||
width: entry.contentRect.width,
|
||||
animation: {
|
||||
duration: 300
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
mounted() {
|
||||
this.initEcharts(true);
|
||||
},
|
||||
deactivated() {
|
||||
// prevent switch tab play resize animation
|
||||
this.resizeObserver.disconnect();
|
||||
},
|
||||
|
||||
methods: {
|
||||
async initEcharts(isFirstLoad = false) {
|
||||
if (!this.echarts) {
|
||||
this.echarts = await utils.loadEcharts();
|
||||
}
|
||||
|
||||
const chartsHeight = this.activityDetailData.length * (this.barWidth + 10) + 200;
|
||||
const chartDom = this.$refs.activityDetailChart;
|
||||
if (!this.echartsInstance) {
|
||||
this.echartsInstance = this.echarts.init(chartDom, `${this.isDarkMode ? 'dark' : null}`, {
|
||||
height: chartsHeight,
|
||||
useDirtyRect: this.activityDetailData.length > 80
|
||||
});
|
||||
this.resizeObserver.observe(chartDom);
|
||||
}
|
||||
|
||||
this.echartsInstance.resize({
|
||||
height: chartsHeight,
|
||||
animation: {
|
||||
duration: 300
|
||||
}
|
||||
});
|
||||
|
||||
this.echartsInstance.setOption(isFirstLoad ? {} : this.getNewOption(), { lazyUpdate: true });
|
||||
this.echartsInstance.on('click', 'yAxis', this.handleClickYAxisLabel);
|
||||
|
||||
setTimeout(() => {
|
||||
this.isLoading = false;
|
||||
}, 200);
|
||||
},
|
||||
handleClickYAxisLabel(params) {
|
||||
const userData = this.usersFirstActivity[params.dataIndex];
|
||||
if (userData?.user_id) {
|
||||
this.showUserDialog(userData.user_id);
|
||||
}
|
||||
},
|
||||
getNewOption() {
|
||||
// grouping player activity entries by user_id and calculate below:
|
||||
// 1. offset: the time from startTimeStamp or the previous entry's tail to the current entry's joinTime
|
||||
// 2. time: the time the user spent in the instance
|
||||
// 3. tail: the time from startTimeStamp to the current entry's leaveTime
|
||||
// 4. entry: the original activity detail entry
|
||||
const userGroupedEntries = new Map();
|
||||
// uniqueUserEntries has each user's first entry and used to keep the order of the users calculated in InstanceActivity.vue
|
||||
const uniqueUserEntries = [];
|
||||
for (const entry of this.activityDetailData) {
|
||||
if (!userGroupedEntries.has(entry.user_id)) {
|
||||
userGroupedEntries.set(entry.user_id, []);
|
||||
uniqueUserEntries.push(entry);
|
||||
}
|
||||
const elements = userGroupedEntries.get(entry.user_id);
|
||||
const offset = Math.max(
|
||||
0,
|
||||
elements.length === 0
|
||||
? entry.joinTime.valueOf() - this.startTimeStamp
|
||||
: entry.joinTime.valueOf() - this.startTimeStamp - elements[elements.length - 1].tail
|
||||
);
|
||||
const tail =
|
||||
elements.length === 0
|
||||
? offset + entry.time
|
||||
: elements[elements.length - 1].tail + offset + entry.time;
|
||||
const element = { offset, time: entry.time, tail, entry };
|
||||
elements.push(element);
|
||||
}
|
||||
this.usersFirstActivity = uniqueUserEntries;
|
||||
|
||||
const generateSeries = () => {
|
||||
const maxEntryCount = Math.max(
|
||||
...Array.from(userGroupedEntries.values()).map((entries) => entries.length)
|
||||
);
|
||||
const placeholderSeries = (data) => {
|
||||
return {
|
||||
name: 'Placeholder',
|
||||
type: 'bar',
|
||||
stack: 'Total',
|
||||
itemStyle: {
|
||||
borderColor: 'transparent',
|
||||
color: 'transparent'
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
borderColor: 'transparent',
|
||||
color: 'transparent'
|
||||
}
|
||||
},
|
||||
data
|
||||
};
|
||||
};
|
||||
const timeSeries = (data) => {
|
||||
return {
|
||||
name: 'Time',
|
||||
type: 'bar',
|
||||
stack: 'Total',
|
||||
colorBy: 'data',
|
||||
barWidth: this.barWidth,
|
||||
emphasis: {
|
||||
focus: 'self'
|
||||
},
|
||||
itemStyle: {
|
||||
borderRadius: 2,
|
||||
shadowBlur: 2,
|
||||
shadowOffsetX: 0.7,
|
||||
shadowOffsetY: 0.5
|
||||
},
|
||||
data
|
||||
};
|
||||
};
|
||||
|
||||
// generate series having placeholder and time series for each user
|
||||
const series = Array(maxEntryCount)
|
||||
.fill(0)
|
||||
.flatMap((_, index) => {
|
||||
return [
|
||||
placeholderSeries(
|
||||
uniqueUserEntries.map((entry) => {
|
||||
const element = userGroupedEntries.get(entry.user_id)[index];
|
||||
return element ? element.offset : 0;
|
||||
})
|
||||
),
|
||||
timeSeries(
|
||||
uniqueUserEntries.map((entry) => {
|
||||
const element = userGroupedEntries.get(entry.user_id)[index];
|
||||
return element ? element.time : 0;
|
||||
})
|
||||
)
|
||||
];
|
||||
});
|
||||
|
||||
return series;
|
||||
};
|
||||
|
||||
const friendOrFavIcon = (display_name) => {
|
||||
const foundItem = this.activityDetailData.find((item) => item.display_name === display_name);
|
||||
|
||||
if (!foundItem) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (foundItem.isFavorite) {
|
||||
return '⭐';
|
||||
}
|
||||
if (foundItem.isFriend) {
|
||||
return '💚';
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const getTooltip = (params) => {
|
||||
const activityDetailData = this.activityDetailData;
|
||||
const param = params;
|
||||
const userData = uniqueUserEntries[param.dataIndex];
|
||||
const isTimeSeries = params.seriesIndex % 2 === 1;
|
||||
if (!isTimeSeries) {
|
||||
return '';
|
||||
}
|
||||
const targetEntryIndex = Math.floor(params.seriesIndex / 2);
|
||||
|
||||
if (!activityDetailData || !userData) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// first, find the user's entries, then get the focused entry
|
||||
const instanceData = userGroupedEntries.get(userData.user_id)[targetEntryIndex].entry;
|
||||
|
||||
const format = this.dtHour12 ? 'hh:mm:ss A' : 'HH:mm:ss';
|
||||
const formattedLeftDateTime = dayjs(instanceData.leaveTime).format(format);
|
||||
const formattedJoinDateTime = dayjs(instanceData.joinTime).format(format);
|
||||
|
||||
const timeString = utils.timeToText(instanceData.time, true);
|
||||
const color = param.color;
|
||||
|
||||
return `
|
||||
<div style="display: flex; align-items: center;">
|
||||
<div style="width: 10px; height: 55px; background-color: ${color}; margin-right: 5px;"></div>
|
||||
<div>
|
||||
<div>${instanceData.display_name} ${friendOrFavIcon(instanceData.display_name)}</div>
|
||||
<div>${formattedJoinDateTime} - ${formattedLeftDateTime}</div>
|
||||
<div>${timeString}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
const format = this.dtHour12 ? 'hh:mm A' : 'HH:mm';
|
||||
|
||||
const echartsOption = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
},
|
||||
formatter: getTooltip
|
||||
},
|
||||
grid: {
|
||||
top: 50,
|
||||
left: 160,
|
||||
right: 90
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
axisLabel: {
|
||||
interval: 0,
|
||||
formatter: (value) => {
|
||||
const MAX_LENGTH = 20;
|
||||
const len = value.length;
|
||||
return `${friendOrFavIcon(value)} ${len > MAX_LENGTH ? `${value.substring(0, MAX_LENGTH)}...` : value}`;
|
||||
}
|
||||
},
|
||||
inverse: true,
|
||||
data: uniqueUserEntries.map((item) => item.display_name),
|
||||
triggerEvent: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
max: this.endTimeStamp - this.startTimeStamp,
|
||||
axisLine: { show: true },
|
||||
axisLabel: {
|
||||
formatter: (value) => dayjs(value + this.startTimeStamp).format(format)
|
||||
},
|
||||
splitLine: { lineStyle: { type: 'dashed' } }
|
||||
},
|
||||
series: generateSeries(),
|
||||
backgroundColor: 'rgba(0, 0, 0, 0)'
|
||||
};
|
||||
|
||||
return echartsOption;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.location {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
1108
src/components/dialogs/AvatarDialog/AvatarDialog.vue
Normal file
1108
src/components/dialogs/AvatarDialog/AvatarDialog.vue
Normal file
File diff suppressed because it is too large
Load Diff
130
src/components/dialogs/AvatarDialog/SetAvatarStylesDialog.vue
Normal file
130
src/components/dialogs/AvatarDialog/SetAvatarStylesDialog.vue
Normal file
@@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
ref="setAvatarStylesDialog"
|
||||
class="x-dialog"
|
||||
:before-close="beforeDialogClose"
|
||||
:visible.sync="setAvatarStylesDialog.visible"
|
||||
:title="t('dialog.set_avatar_styles.header')"
|
||||
width="400px"
|
||||
append-to-body
|
||||
@mousedown.native="dialogMouseDown"
|
||||
@mouseup.native="dialogMouseUp">
|
||||
<template v-if="setAvatarStylesDialog.visible">
|
||||
<div>
|
||||
<span>{{ t('dialog.set_avatar_styles.primary_style') }}</span>
|
||||
<el-select
|
||||
v-model="setAvatarStylesDialog.primaryStyle"
|
||||
:placeholder="t('dialog.set_avatar_styles.select_style')"
|
||||
size="small"
|
||||
clearable
|
||||
style="display: inline-block">
|
||||
<el-option
|
||||
v-for="(style, index) in setAvatarStylesDialog.availableAvatarStyles"
|
||||
:key="index"
|
||||
:label="style"
|
||||
:value="style"></el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
<span>{{ t('dialog.set_avatar_styles.secondary_style') }}</span>
|
||||
<el-select
|
||||
v-model="setAvatarStylesDialog.secondaryStyle"
|
||||
:placeholder="t('dialog.set_avatar_styles.select_style')"
|
||||
size="small"
|
||||
clearable
|
||||
style="display: inline-block">
|
||||
<el-option
|
||||
v-for="(style, index) in setAvatarStylesDialog.availableAvatarStyles"
|
||||
:key="index"
|
||||
:label="style"
|
||||
:value="style"></el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<el-button size="small" @click="setAvatarStylesDialog.visible = false">{{
|
||||
t('dialog.set_avatar_styles.cancel')
|
||||
}}</el-button>
|
||||
<el-button type="primary" size="small" @click="saveSetAvatarStylesDialog">{{
|
||||
t('dialog.set_avatar_styles.save')
|
||||
}}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject, watch, getCurrentInstance } from 'vue';
|
||||
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { avatarRequest } from '../../../api';
|
||||
|
||||
const beforeDialogClose = inject('beforeDialogClose');
|
||||
const dialogMouseDown = inject('dialogMouseDown');
|
||||
const dialogMouseUp = inject('dialogMouseUp');
|
||||
|
||||
const { t } = useI18n();
|
||||
const instance = getCurrentInstance();
|
||||
const $message = instance.proxy.$message;
|
||||
|
||||
const props = defineProps({
|
||||
setAvatarStylesDialog: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.setAvatarStylesDialog.visible,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
getAvatarStyles();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
async function getAvatarStyles() {
|
||||
const ref = await avatarRequest.getAvailableAvatarStyles();
|
||||
const styles = [];
|
||||
const stylesMap = new Map();
|
||||
for (const style of ref.json) {
|
||||
styles.push(style.styleName);
|
||||
stylesMap.set(style.styleName, style.id);
|
||||
}
|
||||
props.setAvatarStylesDialog.availableAvatarStyles = styles;
|
||||
props.setAvatarStylesDialog.availableAvatarStylesMap = stylesMap;
|
||||
}
|
||||
|
||||
function saveSetAvatarStylesDialog() {
|
||||
if (
|
||||
props.setAvatarStylesDialog.initialPrimaryStyle === props.setAvatarStylesDialog.primaryStyle &&
|
||||
props.setAvatarStylesDialog.initialSecondaryStyle === props.setAvatarStylesDialog.secondaryStyle
|
||||
) {
|
||||
props.setAvatarStylesDialog.visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const primaryStyleId =
|
||||
props.setAvatarStylesDialog.availableAvatarStylesMap.get(props.setAvatarStylesDialog.primaryStyle) || '';
|
||||
const secondaryStyleId =
|
||||
props.setAvatarStylesDialog.availableAvatarStylesMap.get(props.setAvatarStylesDialog.secondaryStyle) || '';
|
||||
|
||||
const params = {
|
||||
id: props.setAvatarStylesDialog.avatarId,
|
||||
primaryStyle: primaryStyleId,
|
||||
secondaryStyle: secondaryStyleId
|
||||
};
|
||||
avatarRequest
|
||||
.saveAvatar(params)
|
||||
.then(() => {
|
||||
$message.success(t('dialog.set_avatar_styles.save_success'));
|
||||
props.setAvatarStylesDialog.visible = false;
|
||||
})
|
||||
.catch((error) => {
|
||||
$message.error(t('dialog.set_avatar_styles.save_failed'));
|
||||
console.error('Error saving avatar styles:', error);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
289
src/components/dialogs/AvatarDialog/SetAvatarTagsDialog.vue
Normal file
289
src/components/dialogs/AvatarDialog/SetAvatarTagsDialog.vue
Normal file
@@ -0,0 +1,289 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
ref="setAvatarTagsDialog"
|
||||
class="x-dialog"
|
||||
:before-close="beforeDialogClose"
|
||||
:visible.sync="setAvatarTagsDialog.visible"
|
||||
:title="t('dialog.set_avatar_tags.header')"
|
||||
width="770px"
|
||||
append-to-body
|
||||
@mousedown.native="dialogMouseDown"
|
||||
@mouseup.native="dialogMouseUp">
|
||||
<template v-if="setAvatarTagsDialog.visible">
|
||||
<el-checkbox v-model="setAvatarTagsDialog.contentHorror" @change="updateSelectedAvatarTags">{{
|
||||
t('dialog.set_avatar_tags.content_horror')
|
||||
}}</el-checkbox>
|
||||
<br />
|
||||
<el-checkbox v-model="setAvatarTagsDialog.contentGore" @change="updateSelectedAvatarTags">{{
|
||||
t('dialog.set_avatar_tags.content_gore')
|
||||
}}</el-checkbox>
|
||||
<br />
|
||||
<el-checkbox v-model="setAvatarTagsDialog.contentViolence" @change="updateSelectedAvatarTags">{{
|
||||
t('dialog.set_avatar_tags.content_violence')
|
||||
}}</el-checkbox>
|
||||
<br />
|
||||
<el-checkbox v-model="setAvatarTagsDialog.contentAdult" @change="updateSelectedAvatarTags">{{
|
||||
t('dialog.set_avatar_tags.content_adult')
|
||||
}}</el-checkbox>
|
||||
<br />
|
||||
<el-checkbox v-model="setAvatarTagsDialog.contentSex" @change="updateSelectedAvatarTags">{{
|
||||
t('dialog.set_avatar_tags.content_sex')
|
||||
}}</el-checkbox>
|
||||
<br />
|
||||
<el-input
|
||||
v-model="setAvatarTagsDialog.selectedTagsCsv"
|
||||
size="mini"
|
||||
:autosize="{ minRows: 2, maxRows: 5 }"
|
||||
:placeholder="t('dialog.set_avatar_tags.custom_tags_placeholder')"
|
||||
style="margin-top: 10px"
|
||||
@input="updateInputAvatarTags"></el-input>
|
||||
<template v-if="setAvatarTagsDialog.ownAvatars.length === setAvatarTagsDialog.selectedCount">
|
||||
<el-button size="small" @click="setAvatarTagsSelectToggle">{{
|
||||
t('dialog.set_avatar_tags.select_none')
|
||||
}}</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button size="small" @click="setAvatarTagsSelectToggle">{{
|
||||
t('dialog.set_avatar_tags.select_all')
|
||||
}}</el-button>
|
||||
</template>
|
||||
<span style="margin-left: 5px"
|
||||
>{{ setAvatarTagsDialog.selectedCount }} / {{ setAvatarTagsDialog.ownAvatars.length }}</span
|
||||
>
|
||||
<span v-if="setAvatarTagsDialog.loading" style="margin-left: 5px">
|
||||
<i class="el-icon-loading"></i>
|
||||
</span>
|
||||
<br />
|
||||
<div class="x-friend-list" style="margin-top: 10px; min-height: 60px; max-height: 280px">
|
||||
<div
|
||||
v-for="avatar in setAvatarTagsDialog.ownAvatars"
|
||||
:key="avatar.id"
|
||||
class="x-friend-item x-friend-item-border"
|
||||
style="width: 350px"
|
||||
@click="showAvatarDialog(avatar.id)">
|
||||
<div class="avatar">
|
||||
<img v-if="avatar.thumbnailImageUrl" v-lazy="avatar.thumbnailImageUrl" />
|
||||
</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.$tagString"></span>
|
||||
</div>
|
||||
<el-button type="text" size="mini" style="margin-left: 5px" @click.stop>
|
||||
<el-checkbox v-model="avatar.$selected" @change="updateAvatarTagsSelection"></el-checkbox>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<el-button size="small" @click="setAvatarTagsDialog.visible = false">{{
|
||||
t('dialog.set_avatar_tags.cancel')
|
||||
}}</el-button>
|
||||
<el-button type="primary" size="small" @click="saveSetAvatarTagsDialog">{{
|
||||
t('dialog.set_avatar_tags.save')
|
||||
}}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject, watch, getCurrentInstance } from 'vue';
|
||||
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { avatarRequest } from '../../../api';
|
||||
|
||||
const beforeDialogClose = inject('beforeDialogClose');
|
||||
const dialogMouseDown = inject('dialogMouseDown');
|
||||
const dialogMouseUp = inject('dialogMouseUp');
|
||||
const showAvatarDialog = inject('showAvatarDialog');
|
||||
|
||||
const { t } = useI18n();
|
||||
const instance = getCurrentInstance();
|
||||
const $message = instance.proxy.$message;
|
||||
|
||||
const props = defineProps({
|
||||
setAvatarTagsDialog: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.setAvatarTagsDialog.visible,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
updateAvatarTagsSelection();
|
||||
updateSelectedAvatarTags();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function updateSelectedAvatarTags() {
|
||||
const D = props.setAvatarTagsDialog;
|
||||
if (D.contentHorror) {
|
||||
if (!D.selectedTags.includes('content_horror')) {
|
||||
D.selectedTags.push('content_horror');
|
||||
}
|
||||
} else if (D.selectedTags.includes('content_horror')) {
|
||||
D.selectedTags.splice(D.selectedTags.indexOf('content_horror'), 1);
|
||||
}
|
||||
if (D.contentGore) {
|
||||
if (!D.selectedTags.includes('content_gore')) {
|
||||
D.selectedTags.push('content_gore');
|
||||
}
|
||||
} else if (D.selectedTags.includes('content_gore')) {
|
||||
D.selectedTags.splice(D.selectedTags.indexOf('content_gore'), 1);
|
||||
}
|
||||
if (D.contentViolence) {
|
||||
if (!D.selectedTags.includes('content_violence')) {
|
||||
D.selectedTags.push('content_violence');
|
||||
}
|
||||
} else if (D.selectedTags.includes('content_violence')) {
|
||||
D.selectedTags.splice(D.selectedTags.indexOf('content_violence'), 1);
|
||||
}
|
||||
if (D.contentAdult) {
|
||||
if (!D.selectedTags.includes('content_adult')) {
|
||||
D.selectedTags.push('content_adult');
|
||||
}
|
||||
} else if (D.selectedTags.includes('content_adult')) {
|
||||
D.selectedTags.splice(D.selectedTags.indexOf('content_adult'), 1);
|
||||
}
|
||||
if (D.contentSex) {
|
||||
if (!D.selectedTags.includes('content_sex')) {
|
||||
D.selectedTags.push('content_sex');
|
||||
}
|
||||
} else if (D.selectedTags.includes('content_sex')) {
|
||||
D.selectedTags.splice(D.selectedTags.indexOf('content_sex'), 1);
|
||||
}
|
||||
|
||||
D.selectedTagsCsv = D.selectedTags.join(',').replace(/content_/g, '');
|
||||
}
|
||||
|
||||
function updateAvatarTagsSelection() {
|
||||
const D = props.setAvatarTagsDialog;
|
||||
D.selectedCount = 0;
|
||||
for (const ref of D.ownAvatars) {
|
||||
if (ref.$selected) {
|
||||
D.selectedCount++;
|
||||
}
|
||||
ref.$tagString = '';
|
||||
const contentTags = [];
|
||||
ref.tags.forEach((tag) => {
|
||||
if (tag.startsWith('content_')) {
|
||||
contentTags.push(tag.substring(8));
|
||||
}
|
||||
});
|
||||
for (let i = 0; i < contentTags.length; ++i) {
|
||||
const tag = contentTags[i];
|
||||
if (i < contentTags.length - 1) {
|
||||
ref.$tagString += `${tag}, `;
|
||||
} else {
|
||||
ref.$tagString += tag;
|
||||
}
|
||||
}
|
||||
}
|
||||
// props.setAvatarTagsDialog.forceUpdate++;
|
||||
}
|
||||
|
||||
function setAvatarTagsSelectToggle() {
|
||||
const D = props.setAvatarTagsDialog;
|
||||
const allSelected = D.ownAvatars.length === D.selectedCount;
|
||||
for (const ref of D.ownAvatars) {
|
||||
ref.$selected = !allSelected;
|
||||
}
|
||||
updateAvatarTagsSelection();
|
||||
}
|
||||
|
||||
async function saveSetAvatarTagsDialog() {
|
||||
const D = props.setAvatarTagsDialog;
|
||||
if (D.loading) {
|
||||
return;
|
||||
}
|
||||
D.loading = true;
|
||||
try {
|
||||
for (let i = D.ownAvatars.length - 1; i >= 0; --i) {
|
||||
const ref = D.ownAvatars[i];
|
||||
if (!D.visible) {
|
||||
break;
|
||||
}
|
||||
if (!ref.$selected) {
|
||||
continue;
|
||||
}
|
||||
const tags = [...D.selectedTags];
|
||||
for (const tag of ref.tags) {
|
||||
if (!tag.startsWith('content_')) {
|
||||
tags.push(tag);
|
||||
}
|
||||
}
|
||||
await avatarRequest.saveAvatar({
|
||||
id: ref.id,
|
||||
tags
|
||||
});
|
||||
D.selectedCount--;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
$message({
|
||||
message: 'Error saving avatar tags',
|
||||
type: 'error'
|
||||
});
|
||||
} finally {
|
||||
D.loading = false;
|
||||
D.visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
function updateInputAvatarTags() {
|
||||
const D = props.setAvatarTagsDialog;
|
||||
D.contentHorror = false;
|
||||
D.contentGore = false;
|
||||
D.contentViolence = false;
|
||||
D.contentAdult = false;
|
||||
D.contentSex = false;
|
||||
const tags = D.selectedTagsCsv.split(',');
|
||||
D.selectedTags = [];
|
||||
for (const tag of tags) {
|
||||
switch (tag) {
|
||||
case 'horror':
|
||||
D.contentHorror = true;
|
||||
break;
|
||||
case 'gore':
|
||||
D.contentGore = true;
|
||||
break;
|
||||
case 'violence':
|
||||
D.contentViolence = true;
|
||||
break;
|
||||
case 'adult':
|
||||
D.contentAdult = true;
|
||||
break;
|
||||
case 'sex':
|
||||
D.contentSex = true;
|
||||
break;
|
||||
}
|
||||
if (!D.selectedTags.includes(`content_${tag}`)) {
|
||||
D.selectedTags.push(`content_${tag}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// useless
|
||||
// $app.data.avatarContentTags = [
|
||||
// 'content_horror',
|
||||
// 'content_gore',
|
||||
// 'content_violence',
|
||||
// 'content_adult',
|
||||
// 'content_sex'
|
||||
// ];
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
193
src/components/dialogs/ChooseFavoriteGroupDialog.vue
Normal file
193
src/components/dialogs/ChooseFavoriteGroupDialog.vue
Normal file
@@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
ref="favoriteDialog"
|
||||
:before-close="beforeDialogClose"
|
||||
:visible.sync="isVisible"
|
||||
:title="$t('dialog.favorite.header')"
|
||||
width="300px"
|
||||
@mousedown.native="dialogMouseDown"
|
||||
@mouseup.native="dialogMouseUp">
|
||||
<div v-loading="loading">
|
||||
<span style="display: block; text-align: center">{{ $t('dialog.favorite.vrchat_favorites') }}</span>
|
||||
<template v-if="favoriteDialog.currentGroup && favoriteDialog.currentGroup.key">
|
||||
<el-button
|
||||
style="display: block; width: 100%; margin: 10px 0"
|
||||
@click="deleteFavoriteNoConfirm(favoriteDialog.objectId)">
|
||||
<i class="el-icon-check"></i>
|
||||
{{ favoriteDialog.currentGroup.displayName }} ({{ favoriteDialog.currentGroup.count }} /
|
||||
{{ favoriteDialog.currentGroup.capacity }})
|
||||
</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button
|
||||
v-for="group in groups"
|
||||
:key="group.key"
|
||||
style="display: block; width: 100%; margin: 10px 0"
|
||||
@click="addFavorite(group)">
|
||||
{{ group.displayName }} ({{ group.count }} / {{ group.capacity }})
|
||||
</el-button>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="favoriteDialog.type === 'world'" style="margin-top: 20px">
|
||||
<span style="display: block; text-align: center">{{ $t('dialog.favorite.local_favorites') }}</span>
|
||||
<template v-for="group in localWorldFavoriteGroups">
|
||||
<el-button
|
||||
v-if="hasLocalWorldFavorite(favoriteDialog.objectId, group)"
|
||||
:key="group"
|
||||
style="display: block; width: 100%; margin: 10px 0"
|
||||
@click="removeLocalWorldFavorite(favoriteDialog.objectId, group)">
|
||||
<i class="el-icon-check"></i>
|
||||
{{ group }} ({{ getLocalWorldFavoriteGroupLength(group) }})
|
||||
</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
:key="group"
|
||||
style="display: block; width: 100%; margin: 10px 0"
|
||||
@click="addLocalWorldFavorite(favoriteDialog.objectId, group)">
|
||||
{{ group }} ({{ getLocalWorldFavoriteGroupLength(group) }})
|
||||
</el-button>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="favoriteDialog.type === 'avatar'" style="margin-top: 20px">
|
||||
<span style="display: block; text-align: center">{{ $t('dialog.favorite.local_avatar_favorites') }}</span>
|
||||
<template v-for="group in localAvatarFavoriteGroups">
|
||||
<el-button
|
||||
v-if="hasLocalAvatarFavorite(favoriteDialog.objectId, group)"
|
||||
:key="group"
|
||||
style="display: block; width: 100%; margin: 10px 0"
|
||||
@click="removeLocalAvatarFavorite(favoriteDialog.objectId, group)">
|
||||
<i class="el-icon-check"></i>
|
||||
{{ group }} ({{ getLocalAvatarFavoriteGroupLength(group) }})
|
||||
</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
:key="group"
|
||||
style="display: block; width: 100%; margin: 10px 0"
|
||||
:disabled="!isLocalUserVrcplusSupporter"
|
||||
@click="addLocalAvatarFavorite(favoriteDialog.objectId, group)">
|
||||
{{ group }} ({{ getLocalAvatarFavoriteGroupLength(group) }})
|
||||
</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { favoriteRequest } from '../../api';
|
||||
import Noty from 'noty';
|
||||
|
||||
export default {
|
||||
name: 'ChooseFavoriteGroupDialog',
|
||||
inject: ['API', 'beforeDialogClose', 'dialogMouseDown', 'dialogMouseUp', 'adjustDialogZ'],
|
||||
props: {
|
||||
favoriteDialog: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
visible: false,
|
||||
type: '',
|
||||
objectId: '',
|
||||
currentGroup: {}
|
||||
})
|
||||
},
|
||||
localWorldFavoriteGroups: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
localAvatarFavoriteGroups: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
hasLocalWorldFavorite: {
|
||||
type: Function,
|
||||
default: () => () => false
|
||||
},
|
||||
getLocalWorldFavoriteGroupLength: {
|
||||
type: Function,
|
||||
default: () => () => 0
|
||||
},
|
||||
hasLocalAvatarFavorite: {
|
||||
type: Function,
|
||||
default: () => () => false
|
||||
},
|
||||
getLocalAvatarFavoriteGroupLength: {
|
||||
type: Function,
|
||||
default: () => () => 0
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
groups: [],
|
||||
loading: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isVisible: {
|
||||
get() {
|
||||
return this.favoriteDialog.visible;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:favorite-dialog', { ...this.favoriteDialog, visible: value });
|
||||
}
|
||||
},
|
||||
isLocalUserVrcplusSupporter() {
|
||||
return this.API.currentUser.$isVRCPlus;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'favoriteDialog.visible'(value) {
|
||||
if (value) {
|
||||
this.initFavoriteDialog();
|
||||
this.$nextTick(() => {
|
||||
this.adjustDialogZ(this.$refs.favoriteDialog.$el);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initFavoriteDialog() {
|
||||
if (this.favoriteDialog.type === 'friend') {
|
||||
this.groups = this.API.favoriteFriendGroups;
|
||||
} else if (this.favoriteDialog.type === 'world') {
|
||||
this.groups = this.API.favoriteWorldGroups;
|
||||
} else if (this.favoriteDialog.type === 'avatar') {
|
||||
this.groups = this.API.favoriteAvatarGroups;
|
||||
}
|
||||
},
|
||||
addFavorite(group) {
|
||||
const D = this.favoriteDialog;
|
||||
this.loading = true;
|
||||
favoriteRequest
|
||||
.addFavorite({
|
||||
type: D.type,
|
||||
favoriteId: D.objectId,
|
||||
tags: group.name
|
||||
})
|
||||
.then(() => {
|
||||
this.isVisible = false;
|
||||
new Noty({
|
||||
type: 'success',
|
||||
text: 'Favorite added'
|
||||
}).show();
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
addLocalWorldFavorite(...args) {
|
||||
this.$emit('add-local-world-favorite', ...args);
|
||||
},
|
||||
removeLocalWorldFavorite(...args) {
|
||||
this.$emit('remove-local-world-favorite', ...args);
|
||||
},
|
||||
addLocalAvatarFavorite(...args) {
|
||||
this.$emit('add-local-avatar-favorite', ...args);
|
||||
},
|
||||
removeLocalAvatarFavorite(...args) {
|
||||
this.$emit('remove-local-avatar-favorite', ...args);
|
||||
},
|
||||
deleteFavoriteNoConfirm(...args) {
|
||||
this.$emit('delete-favorite-no-confirm', ...args);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
1788
src/components/dialogs/GroupDialog/GroupDialog.vue
Normal file
1788
src/components/dialogs/GroupDialog/GroupDialog.vue
Normal file
File diff suppressed because it is too large
Load Diff
1694
src/components/dialogs/GroupDialog/GroupMemberModerationDialog.vue
Normal file
1694
src/components/dialogs/GroupDialog/GroupMemberModerationDialog.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
class="x-dialog"
|
||||
:before-close="beforeDialogClose"
|
||||
:visible="isGroupLogsExportDialogVisible"
|
||||
:title="t('dialog.group_member_moderation.export_logs')"
|
||||
width="650px"
|
||||
append-to-body
|
||||
@close="setIsGroupLogsExportDialogVisible"
|
||||
@mousedown.native="dialogMouseDown"
|
||||
@mouseup.native="dialogMouseUp">
|
||||
<el-checkbox-group
|
||||
v-model="checkedGroupLogsExportLogsOptions"
|
||||
style="margin-bottom: 10px"
|
||||
@change="updateGroupLogsExportContent">
|
||||
<template v-for="option in checkGroupsLogsExportLogsOptions">
|
||||
<el-checkbox :key="option.label" :label="option.label">
|
||||
{{ t(option.text) }}
|
||||
</el-checkbox>
|
||||
</template>
|
||||
</el-checkbox-group>
|
||||
<br />
|
||||
<el-input
|
||||
v-model="groupLogsExportContent"
|
||||
type="textarea"
|
||||
size="mini"
|
||||
rows="15"
|
||||
resize="none"
|
||||
readonly
|
||||
style="margin-top: 15px"
|
||||
@click.native="handleCopyGroupLogsExportContent" />
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import utils from '../../../classes/utils';
|
||||
|
||||
const beforeDialogClose = inject('beforeDialogClose');
|
||||
const dialogMouseDown = inject('dialogMouseDown');
|
||||
const dialogMouseUp = inject('dialogMouseUp');
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
isGroupLogsExportDialogVisible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
groupLogsModerationTable: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:isGroupLogsExportDialogVisible']);
|
||||
watch(
|
||||
() => props.isGroupLogsExportDialogVisible,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
updateGroupLogsExportContent();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const groupLogsExportContent = ref('');
|
||||
|
||||
const checkGroupsLogsExportLogsOptions = [
|
||||
{ label: 'created_at', text: 'dialog.group_member_moderation.created_at' },
|
||||
{ label: 'eventType', text: 'dialog.group_member_moderation.type' },
|
||||
{ label: 'actorDisplayName', text: 'dialog.group_member_moderation.display_name' },
|
||||
{ label: 'description', text: 'dialog.group_member_moderation.description' },
|
||||
{ label: 'data', text: 'dialog.group_member_moderation.data' }
|
||||
];
|
||||
const checkedGroupLogsExportLogsOptions = ref([
|
||||
'created_at',
|
||||
'eventType',
|
||||
'actorDisplayName',
|
||||
'description',
|
||||
'data'
|
||||
]);
|
||||
|
||||
function updateGroupLogsExportContent() {
|
||||
const formatter = (str) => (/[\x00-\x1f,"]/.test(str) ? `"${str.replace(/"/g, '""')}"` : str);
|
||||
|
||||
const sortedCheckedOptions = checkGroupsLogsExportLogsOptions
|
||||
.filter((option) => checkedGroupLogsExportLogsOptions.value.includes(option.label))
|
||||
.map((option) => option.label);
|
||||
|
||||
const header = `${sortedCheckedOptions.join(',')}\n`;
|
||||
|
||||
const content = props.groupLogsModerationTable.data
|
||||
.map((item) =>
|
||||
sortedCheckedOptions
|
||||
.map((key) => formatter(key === 'data' ? JSON.stringify(item[key]) : item[key]))
|
||||
.join(',')
|
||||
)
|
||||
.join('\n');
|
||||
|
||||
groupLogsExportContent.value = header + content; // Update ref
|
||||
}
|
||||
|
||||
function handleCopyGroupLogsExportContent() {
|
||||
utils.copyToClipboard(groupLogsExportContent.value);
|
||||
}
|
||||
|
||||
function setIsGroupLogsExportDialogVisible() {
|
||||
emit('update:isGroupLogsExportDialogVisible', false);
|
||||
}
|
||||
</script>
|
||||
200
src/components/dialogs/GroupDialog/GroupPostEditDialog.vue
Normal file
200
src/components/dialogs/GroupDialog/GroupPostEditDialog.vue
Normal file
@@ -0,0 +1,200 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
ref="groupPostEditDialog"
|
||||
:before-close="beforeDialogClose"
|
||||
:visible.sync="groupPostEditDialog.visible"
|
||||
:title="$t('dialog.group_post_edit.header')"
|
||||
width="650px"
|
||||
append-to-body
|
||||
@mousedown.native="dialogMouseDown"
|
||||
@mouseup.native="dialogMouseUp">
|
||||
<div v-if="groupPostEditDialog.visible">
|
||||
<h3 v-text="groupPostEditDialog.groupRef.name"></h3>
|
||||
<el-form :model="groupPostEditDialog" label-width="150px">
|
||||
<el-form-item :label="$t('dialog.group_post_edit.title')">
|
||||
<el-input v-model="groupPostEditDialog.title" size="mini"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('dialog.group_post_edit.message')">
|
||||
<el-input
|
||||
v-model="groupPostEditDialog.text"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
:autosize="{ minRows: 4, maxRows: 20 }"
|
||||
style="margin-top: 10px"
|
||||
resize="none"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-checkbox
|
||||
v-if="!groupPostEditDialog.postId"
|
||||
v-model="groupPostEditDialog.sendNotification"
|
||||
size="small">
|
||||
{{ $t('dialog.group_post_edit.send_notification') }}
|
||||
</el-checkbox>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('dialog.group_post_edit.post_visibility')">
|
||||
<el-radio-group v-model="groupPostEditDialog.visibility" size="small">
|
||||
<el-radio label="public">
|
||||
{{ $t('dialog.group_post_edit.visibility_public') }}
|
||||
</el-radio>
|
||||
<el-radio label="group">
|
||||
{{ $t('dialog.group_post_edit.visibility_group') }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="groupPostEditDialog.visibility === 'group'"
|
||||
:label="$t('dialog.new_instance.roles')">
|
||||
<el-select
|
||||
v-model="groupPostEditDialog.roleIds"
|
||||
multiple
|
||||
clearable
|
||||
:placeholder="$t('dialog.new_instance.role_placeholder')"
|
||||
style="width: 100%">
|
||||
<el-option-group :label="$t('dialog.new_instance.role_placeholder')">
|
||||
<el-option
|
||||
v-for="role in groupPostEditDialog.groupRef?.roles"
|
||||
:key="role.id"
|
||||
:label="role.name"
|
||||
:value="role.id"
|
||||
style="height: auto; width: 478px">
|
||||
<div class="detail">
|
||||
<span class="name" v-text="role.name"></span>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-option-group>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('dialog.group_post_edit.image')">
|
||||
<template v-if="gallerySelectDialog.selectedFileId">
|
||||
<div style="display: inline-block; flex: none; margin-right: 5px">
|
||||
<el-popover placement="right" width="500px" trigger="click">
|
||||
<img
|
||||
slot="reference"
|
||||
v-lazy="gallerySelectDialog.selectedImageUrl"
|
||||
style="
|
||||
flex: none;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
" />
|
||||
<img
|
||||
v-lazy="gallerySelectDialog.selectedImageUrl"
|
||||
style="height: 500px"
|
||||
@click="showFullscreenImageDialog(gallerySelectDialog.selectedImageUrl)" />
|
||||
</el-popover>
|
||||
<el-button size="mini" style="vertical-align: top" @click="clearImageGallerySelect">
|
||||
{{ $t('dialog.invite_message.clear_selected_image') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button size="mini" style="margin-right: 5px" @click="showGallerySelectDialog">
|
||||
{{ $t('dialog.invite_message.select_image') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button size="small" @click="groupPostEditDialog.visible = false">
|
||||
{{ $t('dialog.group_post_edit.cancel') }}
|
||||
</el-button>
|
||||
<el-button v-if="groupPostEditDialog.postId" size="small" @click="editGroupPost">
|
||||
{{ $t('dialog.group_post_edit.edit_post') }}
|
||||
</el-button>
|
||||
<el-button v-else size="small" @click="createGroupPost">
|
||||
{{ $t('dialog.group_post_edit.create_post') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { groupRequest } from '../../../api';
|
||||
|
||||
export default {
|
||||
name: 'GroupPostEditDialog',
|
||||
inject: [
|
||||
'beforeDialogClose',
|
||||
'showFullscreenImageDialog',
|
||||
'dialogMouseDown',
|
||||
'dialogMouseUp',
|
||||
'showGallerySelectDialog'
|
||||
],
|
||||
props: {
|
||||
dialogData: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
gallerySelectDialog: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
groupPostEditDialog: {
|
||||
get() {
|
||||
return this.dialogData;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:dialog-data', value);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
editGroupPost() {
|
||||
const D = this.groupPostEditDialog;
|
||||
if (!D.groupId || !D.postId) {
|
||||
return;
|
||||
}
|
||||
const params = {
|
||||
groupId: D.groupId,
|
||||
postId: D.postId,
|
||||
title: D.title,
|
||||
text: D.text,
|
||||
roleIds: D.roleIds,
|
||||
visibility: D.visibility,
|
||||
imageId: null
|
||||
};
|
||||
if (this.gallerySelectDialog.selectedFileId) {
|
||||
params.imageId = this.gallerySelectDialog.selectedFileId;
|
||||
}
|
||||
groupRequest.editGroupPost(params).then((args) => {
|
||||
this.$message({
|
||||
message: 'Group post edited',
|
||||
type: 'success'
|
||||
});
|
||||
return args;
|
||||
});
|
||||
D.visible = false;
|
||||
},
|
||||
createGroupPost() {
|
||||
const D = this.groupPostEditDialog;
|
||||
const params = {
|
||||
groupId: D.groupId,
|
||||
title: D.title,
|
||||
text: D.text,
|
||||
roleIds: D.roleIds,
|
||||
visibility: D.visibility,
|
||||
sendNotification: D.sendNotification,
|
||||
imageId: null
|
||||
};
|
||||
if (this.gallerySelectDialog.selectedFileId) {
|
||||
params.imageId = this.gallerySelectDialog.selectedFileId;
|
||||
}
|
||||
groupRequest.createGroupPost(params).then((args) => {
|
||||
this.$message({
|
||||
message: 'Group post created',
|
||||
type: 'success'
|
||||
});
|
||||
return args;
|
||||
});
|
||||
D.visible = false;
|
||||
},
|
||||
clearImageGallerySelect() {
|
||||
this.$emit('clear-image-gallery-select');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
307
src/components/dialogs/GroupDialog/InviteGroupDialog.vue
Normal file
307
src/components/dialogs/GroupDialog/InviteGroupDialog.vue
Normal file
@@ -0,0 +1,307 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
ref="inviteGroupDialog"
|
||||
:visible.sync="inviteGroupDialog.visible"
|
||||
:before-close="beforeDialogClose"
|
||||
:title="$t('dialog.invite_to_group.header')"
|
||||
width="450px"
|
||||
@mousedown.native="dialogMouseDown"
|
||||
@mouseup.native="dialogMouseUp">
|
||||
<div v-if="inviteGroupDialog.visible" v-loading="inviteGroupDialog.loading">
|
||||
<span>{{ $t('dialog.invite_to_group.description') }}</span>
|
||||
<br />
|
||||
<el-select
|
||||
v-model="inviteGroupDialog.groupId"
|
||||
clearable
|
||||
:placeholder="$t('dialog.invite_to_group.choose_group_placeholder')"
|
||||
filterable
|
||||
:disabled="inviteGroupDialog.loading"
|
||||
style="margin-top: 15px"
|
||||
@change="isAllowedToInviteToGroup">
|
||||
<el-option-group
|
||||
v-if="API.currentUserGroups.size"
|
||||
:label="$t('dialog.invite_to_group.groups')"
|
||||
style="width: 410px">
|
||||
<el-option
|
||||
v-for="group in API.currentUserGroups.values()"
|
||||
:key="group.id"
|
||||
:label="group.name"
|
||||
:value="group.id"
|
||||
style="height: auto"
|
||||
class="x-friend-item">
|
||||
<div class="avatar">
|
||||
<img v-lazy="group.iconUrl" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span class="name" v-text="group.name"></span>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-option-group>
|
||||
</el-select>
|
||||
<el-select
|
||||
v-model="inviteGroupDialog.userIds"
|
||||
multiple
|
||||
clearable
|
||||
:placeholder="$t('dialog.invite_to_group.choose_friends_placeholder')"
|
||||
filterable
|
||||
:disabled="inviteGroupDialog.loading"
|
||||
style="width: 100%; margin-top: 15px">
|
||||
<el-option-group v-if="inviteGroupDialog.userId" :label="$t('dialog.invite_to_group.selected_users')">
|
||||
<el-option
|
||||
:key="inviteGroupDialog.userObject.id"
|
||||
:label="inviteGroupDialog.userObject.displayName"
|
||||
:value="inviteGroupDialog.userObject.id"
|
||||
class="x-friend-item">
|
||||
<template v-if="inviteGroupDialog.userObject.id">
|
||||
<div class="avatar" :class="userStatusClass(inviteGroupDialog.userObject)">
|
||||
<img v-lazy="userImage(inviteGroupDialog.userObject)" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span
|
||||
class="name"
|
||||
:style="{ color: inviteGroupDialog.userObject.$userColour }"
|
||||
v-text="inviteGroupDialog.userObject.displayName"></span>
|
||||
</div>
|
||||
</template>
|
||||
<span v-else v-text="inviteGroupDialog.userId"></span>
|
||||
</el-option>
|
||||
</el-option-group>
|
||||
<el-option-group v-if="vipFriends.length" :label="$t('side_panel.favorite')">
|
||||
<el-option
|
||||
v-for="friend in vipFriends"
|
||||
:key="friend.id"
|
||||
:label="friend.name"
|
||||
:value="friend.id"
|
||||
style="height: auto"
|
||||
class="x-friend-item">
|
||||
<template v-if="friend.ref">
|
||||
<div class="avatar" :class="userStatusClass(friend.ref)">
|
||||
<img v-lazy="userImage(friend.ref)" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span
|
||||
class="name"
|
||||
:style="{ color: friend.ref.$userColour }"
|
||||
v-text="friend.ref.displayName"></span>
|
||||
</div>
|
||||
</template>
|
||||
<span v-else v-text="friend.id"></span>
|
||||
</el-option>
|
||||
</el-option-group>
|
||||
<el-option-group v-if="onlineFriends.length" :label="$t('side_panel.online')">
|
||||
<el-option
|
||||
v-for="friend in onlineFriends"
|
||||
:key="friend.id"
|
||||
:label="friend.name"
|
||||
:value="friend.id"
|
||||
style="height: auto"
|
||||
class="x-friend-item">
|
||||
<template v-if="friend.ref">
|
||||
<div class="avatar" :class="userStatusClass(friend.ref)">
|
||||
<img v-lazy="userImage(friend.ref)" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span
|
||||
class="name"
|
||||
:style="{ color: friend.ref.$userColour }"
|
||||
v-text="friend.ref.displayName"></span>
|
||||
</div>
|
||||
</template>
|
||||
<span v-else v-text="friend.id"></span>
|
||||
</el-option>
|
||||
</el-option-group>
|
||||
<el-option-group v-if="activeFriends.length" :label="$t('side_panel.active')">
|
||||
<el-option
|
||||
v-for="friend in activeFriends"
|
||||
:key="friend.id"
|
||||
:label="friend.name"
|
||||
:value="friend.id"
|
||||
style="height: auto"
|
||||
class="x-friend-item">
|
||||
<template v-if="friend.ref">
|
||||
<div class="avatar">
|
||||
<img v-lazy="userImage(friend.ref)" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span
|
||||
class="name"
|
||||
:style="{ color: friend.ref.$userColour }"
|
||||
v-text="friend.ref.displayName"></span>
|
||||
</div>
|
||||
</template>
|
||||
<span v-else v-text="friend.id"></span>
|
||||
</el-option>
|
||||
</el-option-group>
|
||||
<el-option-group v-if="offlineFriends.length" :label="$t('side_panel.offline')">
|
||||
<el-option
|
||||
v-for="friend in offlineFriends"
|
||||
:key="friend.id"
|
||||
:label="friend.name"
|
||||
:value="friend.id"
|
||||
style="height: auto"
|
||||
class="x-friend-item">
|
||||
<template v-if="friend.ref">
|
||||
<div class="avatar">
|
||||
<img v-lazy="userImage(friend.ref)" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span
|
||||
class="name"
|
||||
:style="{ color: friend.ref.$userColour }"
|
||||
v-text="friend.ref.displayName"></span>
|
||||
</div>
|
||||
</template>
|
||||
<span v-else v-text="friend.id"></span>
|
||||
</el-option>
|
||||
</el-option-group>
|
||||
</el-select>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
:disabled="inviteGroupDialog.loading || !inviteGroupDialog.userIds.length"
|
||||
@click="sendGroupInvite">
|
||||
Invite
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { groupRequest, userRequest } from '../../../api';
|
||||
import utils from '../../../classes/utils';
|
||||
|
||||
export default {
|
||||
name: 'InviteGroupDialog',
|
||||
inject: [
|
||||
'API',
|
||||
'dialogMouseDown',
|
||||
'dialogMouseUp',
|
||||
'beforeDialogClose',
|
||||
'userStatusClass',
|
||||
'userImage',
|
||||
'adjustDialogZ'
|
||||
],
|
||||
props: {
|
||||
dialogData: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
vipFriends: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
onlineFriends: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
activeFriends: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
offlineFriends: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
inviteGroupDialog: {
|
||||
get() {
|
||||
return this.dialogData;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:dialog-data', value);
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'dialogData.visible'(value) {
|
||||
if (value) {
|
||||
this.initDialog();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initDialog() {
|
||||
this.$nextTick(() => this.adjustDialogZ(this.$refs.inviteGroupDialog.$el));
|
||||
const D = this.inviteGroupDialog;
|
||||
if (D.groupId) {
|
||||
this.API.getCachedGroup({
|
||||
groupId: D.groupId
|
||||
})
|
||||
.then((args) => {
|
||||
D.groupName = args.ref.name;
|
||||
})
|
||||
.catch(() => {
|
||||
D.groupId = '';
|
||||
});
|
||||
this.isAllowedToInviteToGroup();
|
||||
}
|
||||
|
||||
if (D.userId) {
|
||||
userRequest.getCachedUser({ userId: D.userId }).then((args) => {
|
||||
D.userObject = args.ref;
|
||||
D.userIds = [D.userId];
|
||||
});
|
||||
}
|
||||
},
|
||||
isAllowedToInviteToGroup() {
|
||||
const D = this.inviteGroupDialog;
|
||||
const groupId = D.groupId;
|
||||
if (!groupId) {
|
||||
return;
|
||||
}
|
||||
this.inviteGroupDialog.loading = true;
|
||||
groupRequest
|
||||
.getGroup({ groupId })
|
||||
.then((args) => {
|
||||
if (utils.hasGroupPermission(args.ref, 'group-invites-manage')) {
|
||||
return args;
|
||||
}
|
||||
// not allowed to invite
|
||||
this.inviteGroupDialog.groupId = '';
|
||||
this.$message({
|
||||
type: 'error',
|
||||
message: 'You are not allowed to invite to this group'
|
||||
});
|
||||
return args;
|
||||
})
|
||||
.finally(() => {
|
||||
this.inviteGroupDialog.loading = false;
|
||||
});
|
||||
},
|
||||
sendGroupInvite() {
|
||||
this.$confirm('Continue? Invite User(s) To Group', 'Confirm', {
|
||||
confirmButtonText: 'Confirm',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'info',
|
||||
callback: (action) => {
|
||||
const D = this.inviteGroupDialog;
|
||||
if (action !== 'confirm' || D.loading === true) {
|
||||
return;
|
||||
}
|
||||
D.loading = true;
|
||||
const inviteLoop = () => {
|
||||
if (D.userIds.length === 0) {
|
||||
D.loading = false;
|
||||
return;
|
||||
}
|
||||
const receiverUserId = D.userIds.shift();
|
||||
groupRequest
|
||||
.sendGroupInvite({
|
||||
groupId: D.groupId,
|
||||
userId: receiverUserId
|
||||
})
|
||||
.then(inviteLoop)
|
||||
.catch(() => {
|
||||
D.loading = false;
|
||||
});
|
||||
};
|
||||
inviteLoop();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
877
src/components/dialogs/NewInstanceDialog.vue
Normal file
877
src/components/dialogs/NewInstanceDialog.vue
Normal file
@@ -0,0 +1,877 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
ref="newInstanceDialog"
|
||||
:before-close="beforeDialogClose"
|
||||
:visible.sync="newInstanceDialog.visible"
|
||||
:title="$t('dialog.new_instance.header')"
|
||||
width="650px"
|
||||
append-to-body
|
||||
@mousedown.native="dialogMouseDown"
|
||||
@mouseup.native="dialogMouseUp">
|
||||
<el-tabs v-model="newInstanceDialog.selectedTab" type="card" @tab-click="newInstanceTabClick">
|
||||
<el-tab-pane :label="$t('dialog.new_instance.normal')">
|
||||
<el-form :model="newInstanceDialog" label-width="150px">
|
||||
<el-form-item :label="$t('dialog.new_instance.access_type')">
|
||||
<el-radio-group v-model="newInstanceDialog.accessType" size="mini" @change="buildInstance">
|
||||
<el-radio-button label="public">{{
|
||||
$t('dialog.new_instance.access_type_public')
|
||||
}}</el-radio-button>
|
||||
<el-radio-button label="group">{{
|
||||
$t('dialog.new_instance.access_type_group')
|
||||
}}</el-radio-button>
|
||||
<el-radio-button label="friends+">{{
|
||||
$t('dialog.new_instance.access_type_friend_plus')
|
||||
}}</el-radio-button>
|
||||
<el-radio-button label="friends">{{
|
||||
$t('dialog.new_instance.access_type_friend')
|
||||
}}</el-radio-button>
|
||||
<el-radio-button label="invite+">{{
|
||||
$t('dialog.new_instance.access_type_invite_plus')
|
||||
}}</el-radio-button>
|
||||
<el-radio-button label="invite">{{
|
||||
$t('dialog.new_instance.access_type_invite')
|
||||
}}</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="newInstanceDialog.accessType === 'group'"
|
||||
:label="$t('dialog.new_instance.group_access_type')">
|
||||
<el-radio-group v-model="newInstanceDialog.groupAccessType" size="mini" @change="buildInstance">
|
||||
<el-radio-button
|
||||
label="members"
|
||||
:disabled="
|
||||
!hasGroupPermission(newInstanceDialog.groupRef, 'group-instance-open-create')
|
||||
"
|
||||
>{{ $t('dialog.new_instance.group_access_type_members') }}</el-radio-button
|
||||
>
|
||||
<el-radio-button
|
||||
label="plus"
|
||||
:disabled="
|
||||
!hasGroupPermission(newInstanceDialog.groupRef, 'group-instance-plus-create')
|
||||
"
|
||||
>{{ $t('dialog.new_instance.group_access_type_plus') }}</el-radio-button
|
||||
>
|
||||
<el-radio-button
|
||||
label="public"
|
||||
:disabled="
|
||||
!hasGroupPermission(newInstanceDialog.groupRef, 'group-instance-public-create') ||
|
||||
newInstanceDialog.groupRef.privacy === 'private'
|
||||
"
|
||||
>{{ $t('dialog.new_instance.group_access_type_public') }}</el-radio-button
|
||||
>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('dialog.new_instance.region')">
|
||||
<el-radio-group v-model="newInstanceDialog.region" size="mini" @change="buildInstance">
|
||||
<el-radio-button label="US West">{{
|
||||
$t('dialog.new_instance.region_usw')
|
||||
}}</el-radio-button>
|
||||
<el-radio-button label="US East">{{
|
||||
$t('dialog.new_instance.region_use')
|
||||
}}</el-radio-button>
|
||||
<el-radio-button label="Europe">{{ $t('dialog.new_instance.region_eu') }}</el-radio-button>
|
||||
<el-radio-button label="Japan">{{ $t('dialog.new_instance.region_jp') }}</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('dialog.new_instance.content_settings')">
|
||||
<el-select
|
||||
v-model="newInstanceDialog.selectedContentSettings"
|
||||
multiple
|
||||
:placeholder="$t('dialog.new_instance.content_placeholder')"
|
||||
style="width: 100%"
|
||||
@change="buildInstance">
|
||||
<el-option-group :label="$t('dialog.new_instance.content_placeholder')">
|
||||
<el-option
|
||||
class="x-friend-item"
|
||||
value="emoji"
|
||||
:label="$t('dialog.new_instance.content_emoji')"></el-option>
|
||||
<el-option
|
||||
class="x-friend-item"
|
||||
value="stickers"
|
||||
:label="$t('dialog.new_instance.content_stickers')"></el-option>
|
||||
<el-option
|
||||
class="x-friend-item"
|
||||
value="pedestals"
|
||||
:label="$t('dialog.new_instance.content_pedestals')"></el-option>
|
||||
<el-option
|
||||
class="x-friend-item"
|
||||
value="prints"
|
||||
:label="$t('dialog.new_instance.content_prints')"></el-option>
|
||||
<el-option
|
||||
class="x-friend-item"
|
||||
value="drones"
|
||||
:label="$t('dialog.new_instance.content_drones')"></el-option>
|
||||
</el-option-group>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="newInstanceDialog.accessType === 'group'"
|
||||
:label="$t('dialog.new_instance.queueEnabled')">
|
||||
<el-checkbox v-model="newInstanceDialog.queueEnabled" @change="buildInstance"></el-checkbox>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="newInstanceDialog.accessType === 'group'"
|
||||
:label="$t('dialog.new_instance.ageGate')">
|
||||
<el-checkbox
|
||||
v-model="newInstanceDialog.ageGate"
|
||||
:disabled="
|
||||
!hasGroupPermission(newInstanceDialog.groupRef, 'group-instance-age-gated-create')
|
||||
"
|
||||
@change="buildInstance"></el-checkbox>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('dialog.new_instance.world_id')">
|
||||
<el-input
|
||||
v-model="newInstanceDialog.worldId"
|
||||
size="mini"
|
||||
@click.native="$event.target.tagName === 'INPUT' && $event.target.select()"
|
||||
@change="buildInstance"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="newInstanceDialog.accessType === 'group'"
|
||||
:label="$t('dialog.new_instance.group_id')">
|
||||
<el-select
|
||||
v-model="newInstanceDialog.groupId"
|
||||
clearable
|
||||
:placeholder="$t('dialog.new_instance.group_placeholder')"
|
||||
filterable
|
||||
style="width: 100%"
|
||||
@change="buildInstance">
|
||||
<el-option-group :label="$t('dialog.new_instance.group_placeholder')">
|
||||
<el-option
|
||||
v-for="group in API.currentUserGroups.values()"
|
||||
v-if="
|
||||
group &&
|
||||
(hasGroupPermission(group, 'group-instance-public-create') ||
|
||||
hasGroupPermission(group, 'group-instance-plus-create') ||
|
||||
hasGroupPermission(group, 'group-instance-open-create'))
|
||||
"
|
||||
:key="group.id"
|
||||
:label="group.name"
|
||||
:value="group.id"
|
||||
class="x-friend-item"
|
||||
style="height: auto; width: 478px">
|
||||
<div class="avatar">
|
||||
<img v-lazy="group.iconUrl" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span class="name" v-text="group.name"></span>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-option-group>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="
|
||||
newInstanceDialog.accessType === 'group' && newInstanceDialog.groupAccessType === 'members'
|
||||
"
|
||||
:label="$t('dialog.new_instance.roles')">
|
||||
<el-select
|
||||
v-model="newInstanceDialog.roleIds"
|
||||
multiple
|
||||
clearable
|
||||
:placeholder="$t('dialog.new_instance.role_placeholder')"
|
||||
style="width: 100%"
|
||||
@change="buildInstance">
|
||||
<el-option-group :label="$t('dialog.new_instance.role_placeholder')">
|
||||
<el-option
|
||||
v-for="role in newInstanceDialog.selectedGroupRoles"
|
||||
:key="role.id"
|
||||
class="x-friend-item"
|
||||
:label="role.name"
|
||||
:value="role.id"
|
||||
style="height: auto; width: 478px">
|
||||
<div class="detail">
|
||||
<span class="name" v-text="role.name"></span>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-option-group>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<template v-if="newInstanceDialog.instanceCreated">
|
||||
<el-form-item :label="$t('dialog.new_instance.location')">
|
||||
<el-input
|
||||
v-model="newInstanceDialog.location"
|
||||
size="mini"
|
||||
readonly
|
||||
@click.native="$event.target.tagName === 'INPUT' && $event.target.select()"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('dialog.new_instance.url')">
|
||||
<el-input v-model="newInstanceDialog.url" size="mini" readonly></el-input>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('dialog.new_instance.legacy')">
|
||||
<el-form :model="newInstanceDialog" label-width="150px">
|
||||
<el-form-item :label="$t('dialog.new_instance.access_type')">
|
||||
<el-radio-group
|
||||
v-model="newInstanceDialog.accessType"
|
||||
size="mini"
|
||||
@change="buildLegacyInstance">
|
||||
<el-radio-button label="public">{{
|
||||
$t('dialog.new_instance.access_type_public')
|
||||
}}</el-radio-button>
|
||||
<el-radio-button label="group">{{
|
||||
$t('dialog.new_instance.access_type_group')
|
||||
}}</el-radio-button>
|
||||
<el-radio-button label="friends+">{{
|
||||
$t('dialog.new_instance.access_type_friend_plus')
|
||||
}}</el-radio-button>
|
||||
<el-radio-button label="friends">{{
|
||||
$t('dialog.new_instance.access_type_friend')
|
||||
}}</el-radio-button>
|
||||
<el-radio-button label="invite+">{{
|
||||
$t('dialog.new_instance.access_type_invite_plus')
|
||||
}}</el-radio-button>
|
||||
<el-radio-button label="invite">{{
|
||||
$t('dialog.new_instance.access_type_invite')
|
||||
}}</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="newInstanceDialog.accessType === 'group'"
|
||||
:label="$t('dialog.new_instance.group_access_type')">
|
||||
<el-radio-group
|
||||
v-model="newInstanceDialog.groupAccessType"
|
||||
size="mini"
|
||||
@change="buildLegacyInstance">
|
||||
<el-radio-button label="members">{{
|
||||
$t('dialog.new_instance.group_access_type_members')
|
||||
}}</el-radio-button>
|
||||
<el-radio-button label="plus">{{
|
||||
$t('dialog.new_instance.group_access_type_plus')
|
||||
}}</el-radio-button>
|
||||
<el-radio-button label="public">{{
|
||||
$t('dialog.new_instance.group_access_type_public')
|
||||
}}</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('dialog.new_instance.region')">
|
||||
<el-radio-group v-model="newInstanceDialog.region" size="mini" @change="buildLegacyInstance">
|
||||
<el-radio-button label="US West">{{
|
||||
$t('dialog.new_instance.region_usw')
|
||||
}}</el-radio-button>
|
||||
<el-radio-button label="US East">{{
|
||||
$t('dialog.new_instance.region_use')
|
||||
}}</el-radio-button>
|
||||
<el-radio-button label="Europe">{{ $t('dialog.new_instance.region_eu') }}</el-radio-button>
|
||||
<el-radio-button label="Japan">{{ $t('dialog.new_instance.region_jp') }}</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="newInstanceDialog.accessType === 'group'"
|
||||
:label="$t('dialog.new_instance.ageGate')">
|
||||
<el-checkbox v-model="newInstanceDialog.ageGate" @change="buildInstance"></el-checkbox>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('dialog.new_instance.world_id')">
|
||||
<el-input
|
||||
v-model="newInstanceDialog.worldId"
|
||||
size="mini"
|
||||
@click.native="$event.target.tagName === 'INPUT' && $event.target.select()"
|
||||
@change="buildLegacyInstance"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('dialog.new_instance.instance_id')">
|
||||
<el-input
|
||||
v-model="newInstanceDialog.instanceName"
|
||||
:placeholder="$t('dialog.new_instance.instance_id_placeholder')"
|
||||
size="mini"
|
||||
@change="buildLegacyInstance"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="newInstanceDialog.accessType !== 'public' && newInstanceDialog.accessType !== 'group'"
|
||||
:label="$t('dialog.new_instance.instance_creator')">
|
||||
<el-select
|
||||
v-model="newInstanceDialog.userId"
|
||||
clearable
|
||||
:placeholder="$t('dialog.new_instance.instance_creator_placeholder')"
|
||||
filterable
|
||||
style="width: 100%"
|
||||
@change="buildLegacyInstance">
|
||||
<el-option-group v-if="API.currentUser" :label="$t('side_panel.me')">
|
||||
<el-option
|
||||
class="x-friend-item"
|
||||
:label="API.currentUser.displayName"
|
||||
:value="API.currentUser.id"
|
||||
style="height: auto">
|
||||
<div class="avatar" :class="userStatusClass(API.currentUser)">
|
||||
<img v-lazy="userImage(API.currentUser)" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span class="name" v-text="API.currentUser.displayName"></span>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-option-group>
|
||||
<el-option-group v-if="vipFriends.length" :label="$t('side_panel.favorite')">
|
||||
<el-option
|
||||
v-for="friend in vipFriends"
|
||||
:key="friend.id"
|
||||
class="x-friend-item"
|
||||
:label="friend.name"
|
||||
:value="friend.id"
|
||||
style="height: auto">
|
||||
<template v-if="friend.ref">
|
||||
<div class="avatar" :class="userStatusClass(friend.ref)">
|
||||
<img v-lazy="userImage(friend.ref)" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span
|
||||
class="name"
|
||||
:style="{ color: friend.ref.$userColour }"
|
||||
v-text="friend.ref.displayName"></span>
|
||||
</div>
|
||||
</template>
|
||||
<span v-else v-text="friend.id"></span>
|
||||
</el-option>
|
||||
</el-option-group>
|
||||
<el-option-group v-if="onlineFriends.length" :label="$t('side_panel.online')">
|
||||
<el-option
|
||||
v-for="friend in onlineFriends"
|
||||
:key="friend.id"
|
||||
class="x-friend-item"
|
||||
:label="friend.name"
|
||||
:value="friend.id"
|
||||
style="height: auto">
|
||||
<template v-if="friend.ref">
|
||||
<div class="avatar" :class="userStatusClass(friend.ref)">
|
||||
<img v-lazy="userImage(friend.ref)" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span
|
||||
class="name"
|
||||
:style="{ color: friend.ref.$userColour }"
|
||||
v-text="friend.ref.displayName"></span>
|
||||
</div>
|
||||
</template>
|
||||
<span v-else v-text="friend.id"></span>
|
||||
</el-option>
|
||||
</el-option-group>
|
||||
<el-option-group v-if="activeFriends.length" :label="$t('side_panel.active')">
|
||||
<el-option
|
||||
v-for="friend in activeFriends"
|
||||
:key="friend.id"
|
||||
class="x-friend-item"
|
||||
:label="friend.name"
|
||||
:value="friend.id"
|
||||
style="height: auto">
|
||||
<template v-if="friend.ref">
|
||||
<div class="avatar">
|
||||
<img v-lazy="userImage(friend.ref)" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span
|
||||
class="name"
|
||||
:style="{ color: friend.ref.$userColour }"
|
||||
v-text="friend.ref.displayName"></span>
|
||||
</div>
|
||||
</template>
|
||||
<span v-else v-text="friend.id"></span>
|
||||
</el-option>
|
||||
</el-option-group>
|
||||
<el-option-group v-if="offlineFriends.length" :label="$t('side_panel.offline')">
|
||||
<el-option
|
||||
v-for="friend in offlineFriends"
|
||||
:key="friend.id"
|
||||
class="x-friend-item"
|
||||
:label="friend.name"
|
||||
:value="friend.id"
|
||||
style="height: auto">
|
||||
<template v-if="friend.ref">
|
||||
<div class="avatar">
|
||||
<img v-lazy="userImage(friend.ref)" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span
|
||||
class="name"
|
||||
:style="{ color: friend.ref.$userColour }"
|
||||
v-text="friend.ref.displayName"></span>
|
||||
</div>
|
||||
</template>
|
||||
<span v-else v-text="friend.id"></span>
|
||||
</el-option>
|
||||
</el-option-group>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="newInstanceDialog.accessType === 'group'"
|
||||
:label="$t('dialog.new_instance.group_id')">
|
||||
<el-select
|
||||
v-model="newInstanceDialog.groupId"
|
||||
clearable
|
||||
:placeholder="$t('dialog.new_instance.group_placeholder')"
|
||||
filterable
|
||||
style="width: 100%"
|
||||
@change="buildLegacyInstance">
|
||||
<el-option-group :label="$t('dialog.new_instance.group_placeholder')">
|
||||
<el-option
|
||||
v-for="group in API.currentUserGroups.values()"
|
||||
v-if="group"
|
||||
:key="group.id"
|
||||
class="x-friend-item"
|
||||
:label="group.name"
|
||||
:value="group.id"
|
||||
style="height: auto; width: 478px">
|
||||
<div class="avatar">
|
||||
<img v-lazy="group.iconUrl" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span class="name" v-text="group.name"></span>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-option-group>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('dialog.new_instance.location')">
|
||||
<el-input
|
||||
v-model="newInstanceDialog.location"
|
||||
size="mini"
|
||||
readonly
|
||||
@click.native="$event.target.tagName === 'INPUT' && $event.target.select()"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('dialog.new_instance.url')">
|
||||
<el-input v-model="newInstanceDialog.url" size="mini" readonly></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<template v-if="newInstanceDialog.selectedTab === '0'" #footer>
|
||||
<template v-if="newInstanceDialog.instanceCreated">
|
||||
<el-button size="small" @click="copyInstanceUrl(newInstanceDialog.location)">{{
|
||||
$t('dialog.new_instance.copy_url')
|
||||
}}</el-button>
|
||||
<el-button size="small" @click="selfInvite(newInstanceDialog.location)">{{
|
||||
$t('dialog.new_instance.self_invite')
|
||||
}}</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
:disabled="
|
||||
(newInstanceDialog.accessType === 'friends' || newInstanceDialog.accessType === 'invite') &&
|
||||
newInstanceDialog.userId !== API.currentUser.id
|
||||
"
|
||||
@click="showInviteDialog(newInstanceDialog.location)"
|
||||
>{{ $t('dialog.new_instance.invite') }}</el-button
|
||||
>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="showLaunchDialog(newInstanceDialog.location, newInstanceDialog.shortName)"
|
||||
>{{ $t('dialog.new_instance.launch') }}</el-button
|
||||
>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button type="primary" size="small" @click="handleCreateNewInstance">{{
|
||||
$t('dialog.new_instance.create_instance')
|
||||
}}</el-button>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else-if="newInstanceDialog.selectedTab === '1'" #footer>
|
||||
<el-button size="small" @click="copyInstanceUrl(newInstanceDialog.location)">{{
|
||||
$t('dialog.new_instance.copy_url')
|
||||
}}</el-button>
|
||||
<el-button size="small" @click="selfInvite(newInstanceDialog.location)">{{
|
||||
$t('dialog.new_instance.self_invite')
|
||||
}}</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
:disabled="
|
||||
(newInstanceDialog.accessType === 'friends' || newInstanceDialog.accessType === 'invite') &&
|
||||
newInstanceDialog.userId !== API.currentUser.id
|
||||
"
|
||||
@click="showInviteDialog(newInstanceDialog.location)"
|
||||
>{{ $t('dialog.new_instance.invite') }}</el-button
|
||||
>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="showLaunchDialog(newInstanceDialog.location, newInstanceDialog.shortName)"
|
||||
>{{ $t('dialog.new_instance.launch') }}</el-button
|
||||
>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { groupRequest, instanceRequest } from '../../api';
|
||||
import utils from '../../classes/utils';
|
||||
import configRepository from '../../service/config';
|
||||
|
||||
export default {
|
||||
name: 'NewInstanceDialog',
|
||||
inject: [
|
||||
'API',
|
||||
'userImage',
|
||||
'userStatusClass',
|
||||
'beforeDialogClose',
|
||||
'dialogMouseDown',
|
||||
'dialogMouseUp',
|
||||
'showInviteDialog',
|
||||
'showLaunchDialog',
|
||||
'adjustDialogZ'
|
||||
],
|
||||
props: {
|
||||
vipFriends: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
onlineFriends: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
activeFriends: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
offlineFriends: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
instanceContentSettings: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
createNewInstance: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
newInstanceDialogLocationTag: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
newInstanceDialog: {
|
||||
visible: false,
|
||||
// loading: false,
|
||||
selectedTab: '0',
|
||||
instanceCreated: false,
|
||||
queueEnabled: false,
|
||||
worldId: '',
|
||||
instanceId: '',
|
||||
instanceName: '',
|
||||
userId: '',
|
||||
accessType: 'public',
|
||||
region: 'US West',
|
||||
groupRegion: '',
|
||||
groupId: '',
|
||||
groupAccessType: 'plus',
|
||||
ageGate: false,
|
||||
strict: false,
|
||||
location: '',
|
||||
shortName: '',
|
||||
url: '',
|
||||
secureOrShortName: '',
|
||||
lastSelectedGroupId: '',
|
||||
selectedGroupRoles: [],
|
||||
roleIds: [],
|
||||
groupRef: {},
|
||||
contentSettings: this.instanceContentSettings,
|
||||
selectedContentSettings: []
|
||||
}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
newInstanceDialogLocationTag(value) {
|
||||
this.initNewInstanceDialog(value);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.initializeNewInstanceDialog();
|
||||
},
|
||||
methods: {
|
||||
initNewInstanceDialog(tag) {
|
||||
if (!utils.isRealInstance(tag)) {
|
||||
return;
|
||||
}
|
||||
this.$nextTick(() => this.adjustDialogZ(this.$refs.newInstanceDialog.$el));
|
||||
const D = this.newInstanceDialog;
|
||||
const L = utils.parseLocation(tag);
|
||||
if (D.worldId === L.worldId) {
|
||||
// reopening dialog, keep last open instance
|
||||
D.visible = true;
|
||||
return;
|
||||
}
|
||||
D.worldId = L.worldId;
|
||||
D.instanceCreated = false;
|
||||
D.lastSelectedGroupId = '';
|
||||
D.selectedGroupRoles = [];
|
||||
D.groupRef = {};
|
||||
D.roleIds = [];
|
||||
D.strict = false;
|
||||
D.shortName = '';
|
||||
D.secureOrShortName = '';
|
||||
groupRequest.getGroupPermissions({ userId: this.API.currentUser.id });
|
||||
this.buildInstance();
|
||||
this.buildLegacyInstance();
|
||||
this.updateNewInstanceDialog();
|
||||
D.visible = true;
|
||||
},
|
||||
initializeNewInstanceDialog() {
|
||||
configRepository
|
||||
.getBool('instanceDialogQueueEnabled', true)
|
||||
.then((value) => (this.newInstanceDialog.queueEnabled = value));
|
||||
|
||||
configRepository
|
||||
.getString('instanceDialogInstanceName', '')
|
||||
.then((value) => (this.newInstanceDialog.instanceName = value));
|
||||
|
||||
configRepository
|
||||
.getString('instanceDialogUserId', '')
|
||||
.then((value) => (this.newInstanceDialog.userId = value));
|
||||
|
||||
configRepository
|
||||
.getString('instanceDialogAccessType', 'public')
|
||||
.then((value) => (this.newInstanceDialog.accessType = value));
|
||||
|
||||
configRepository
|
||||
.getString('instanceRegion', 'US West')
|
||||
.then((value) => (this.newInstanceDialog.region = value));
|
||||
|
||||
configRepository
|
||||
.getString('instanceDialogGroupId', '')
|
||||
.then((value) => (this.newInstanceDialog.groupId = value));
|
||||
|
||||
configRepository
|
||||
.getString('instanceDialogGroupAccessType', 'plus')
|
||||
.then((value) => (this.newInstanceDialog.groupAccessType = value));
|
||||
|
||||
configRepository
|
||||
.getBool('instanceDialogAgeGate', false)
|
||||
.then((value) => (this.newInstanceDialog.ageGate = value));
|
||||
|
||||
configRepository
|
||||
.getString('instanceDialogSelectedContentSettings', JSON.stringify(this.instanceContentSettings))
|
||||
.then((value) => (this.newInstanceDialog.selectedContentSettings = JSON.parse(value)));
|
||||
},
|
||||
saveNewInstanceDialog() {
|
||||
const {
|
||||
accessType,
|
||||
region,
|
||||
instanceName,
|
||||
userId,
|
||||
groupId,
|
||||
groupAccessType,
|
||||
queueEnabled,
|
||||
ageGate,
|
||||
selectedContentSettings
|
||||
} = this.newInstanceDialog;
|
||||
|
||||
configRepository.setString('instanceDialogAccessType', accessType);
|
||||
configRepository.setString('instanceRegion', region);
|
||||
configRepository.setString('instanceDialogInstanceName', instanceName);
|
||||
configRepository.setString('instanceDialogUserId', userId === this.API.currentUser.id ? '' : userId);
|
||||
configRepository.setString('instanceDialogGroupId', groupId);
|
||||
configRepository.setString('instanceDialogGroupAccessType', groupAccessType);
|
||||
configRepository.setBool('instanceDialogQueueEnabled', queueEnabled);
|
||||
configRepository.setBool('instanceDialogAgeGate', ageGate);
|
||||
configRepository.setString(
|
||||
'instanceDialogSelectedContentSettings',
|
||||
JSON.stringify(selectedContentSettings)
|
||||
);
|
||||
},
|
||||
newInstanceTabClick(tab) {
|
||||
if (tab === '1') {
|
||||
this.buildInstance();
|
||||
} else {
|
||||
this.buildLegacyInstance();
|
||||
}
|
||||
},
|
||||
updateNewInstanceDialog(noChanges) {
|
||||
const D = this.newInstanceDialog;
|
||||
if (D.instanceId) {
|
||||
D.location = `${D.worldId}:${D.instanceId}`;
|
||||
} else {
|
||||
D.location = D.worldId;
|
||||
}
|
||||
const L = utils.parseLocation(D.location);
|
||||
if (noChanges) {
|
||||
L.shortName = D.shortName;
|
||||
} else {
|
||||
D.shortName = '';
|
||||
}
|
||||
D.url = utils.getLaunchURL(L);
|
||||
},
|
||||
selfInvite(location) {
|
||||
const L = utils.parseLocation(location);
|
||||
if (!L.isRealInstance) {
|
||||
return;
|
||||
}
|
||||
instanceRequest
|
||||
.selfInvite({
|
||||
instanceId: L.instanceId,
|
||||
worldId: L.worldId
|
||||
})
|
||||
.then((args) => {
|
||||
this.$message({
|
||||
message: 'Self invite sent',
|
||||
type: 'success'
|
||||
});
|
||||
return args;
|
||||
});
|
||||
},
|
||||
async handleCreateNewInstance() {
|
||||
const args = await this.createNewInstance(this.newInstanceDialog.worldId, this.newInstanceDialog);
|
||||
|
||||
if (args) {
|
||||
this.newInstanceDialog.location = args.json.location;
|
||||
this.newInstanceDialog.instanceId = args.json.instanceId;
|
||||
this.newInstanceDialog.secureOrShortName = args.json.shortName || args.json.secureName;
|
||||
this.newInstanceDialog.instanceCreated = true;
|
||||
this.updateNewInstanceDialog();
|
||||
}
|
||||
},
|
||||
buildInstance() {
|
||||
const D = this.newInstanceDialog;
|
||||
D.instanceCreated = false;
|
||||
D.instanceId = '';
|
||||
D.shortName = '';
|
||||
D.secureOrShortName = '';
|
||||
if (!D.userId) {
|
||||
D.userId = this.API.currentUser.id;
|
||||
}
|
||||
if (D.groupId && D.groupId !== D.lastSelectedGroupId) {
|
||||
D.roleIds = [];
|
||||
const ref = this.API.cachedGroups.get(D.groupId);
|
||||
if (typeof ref !== 'undefined') {
|
||||
D.groupRef = ref;
|
||||
D.selectedGroupRoles = ref.roles;
|
||||
groupRequest
|
||||
.getGroupRoles({
|
||||
groupId: D.groupId
|
||||
})
|
||||
.then((args) => {
|
||||
D.lastSelectedGroupId = D.groupId;
|
||||
D.selectedGroupRoles = args.json;
|
||||
ref.roles = args.json;
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!D.groupId) {
|
||||
D.roleIds = [];
|
||||
D.groupRef = {};
|
||||
D.selectedGroupRoles = [];
|
||||
D.lastSelectedGroupId = '';
|
||||
}
|
||||
this.saveNewInstanceDialog();
|
||||
},
|
||||
buildLegacyInstance() {
|
||||
const D = this.newInstanceDialog;
|
||||
D.instanceCreated = false;
|
||||
D.shortName = '';
|
||||
D.secureOrShortName = '';
|
||||
const tags = [];
|
||||
if (D.instanceName) {
|
||||
D.instanceName = D.instanceName.replace(/[^A-Za-z0-9]/g, '');
|
||||
tags.push(D.instanceName);
|
||||
} else {
|
||||
const randValue = (99999 * Math.random() + 1).toFixed(0);
|
||||
tags.push(String(randValue).padStart(5, '0'));
|
||||
}
|
||||
if (!D.userId) {
|
||||
D.userId = this.API.currentUser.id;
|
||||
}
|
||||
const userId = D.userId;
|
||||
if (D.accessType !== 'public') {
|
||||
if (D.accessType === 'friends+') {
|
||||
tags.push(`~hidden(${userId})`);
|
||||
} else if (D.accessType === 'friends') {
|
||||
tags.push(`~friends(${userId})`);
|
||||
} else if (D.accessType === 'group') {
|
||||
tags.push(`~group(${D.groupId})`);
|
||||
tags.push(`~groupAccessType(${D.groupAccessType})`);
|
||||
} else {
|
||||
tags.push(`~private(${userId})`);
|
||||
}
|
||||
if (D.accessType === 'invite+') {
|
||||
tags.push('~canRequestInvite');
|
||||
}
|
||||
}
|
||||
if (D.accessType === 'group' && D.ageGate) {
|
||||
tags.push('~ageGate');
|
||||
}
|
||||
if (D.region === 'US West') {
|
||||
tags.push(`~region(us)`);
|
||||
} else if (D.region === 'US East') {
|
||||
tags.push(`~region(use)`);
|
||||
} else if (D.region === 'Europe') {
|
||||
tags.push(`~region(eu)`);
|
||||
} else if (D.region === 'Japan') {
|
||||
tags.push(`~region(jp)`);
|
||||
}
|
||||
if (D.accessType !== 'invite' && D.accessType !== 'friends') {
|
||||
D.strict = false;
|
||||
}
|
||||
if (D.strict) {
|
||||
tags.push('~strict');
|
||||
}
|
||||
if (D.groupId && D.groupId !== D.lastSelectedGroupId) {
|
||||
D.roleIds = [];
|
||||
const ref = this.API.cachedGroups.get(D.groupId);
|
||||
if (typeof ref !== 'undefined') {
|
||||
D.groupRef = ref;
|
||||
D.selectedGroupRoles = ref.roles;
|
||||
groupRequest
|
||||
.getGroupRoles({
|
||||
groupId: D.groupId
|
||||
})
|
||||
.then((args) => {
|
||||
D.lastSelectedGroupId = D.groupId;
|
||||
D.selectedGroupRoles = args.json;
|
||||
ref.roles = args.json;
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!D.groupId) {
|
||||
D.roleIds = [];
|
||||
D.selectedGroupRoles = [];
|
||||
D.groupRef = {};
|
||||
D.lastSelectedGroupId = '';
|
||||
}
|
||||
D.instanceId = tags.join('');
|
||||
this.updateNewInstanceDialog(false);
|
||||
this.saveNewInstanceDialog();
|
||||
},
|
||||
async copyInstanceUrl(location) {
|
||||
const L = utils.parseLocation(location);
|
||||
const args = await instanceRequest.getInstanceShortName({
|
||||
worldId: L.worldId,
|
||||
instanceId: L.instanceId
|
||||
});
|
||||
if (args.json) {
|
||||
if (args.json.shortName) {
|
||||
L.shortName = args.json.shortName;
|
||||
}
|
||||
// NOTE:
|
||||
// splitting the 'INSTANCE:SHORTNAME' event and put code here
|
||||
const resLocation = `${args.instance.worldId}:${args.instance.instanceId}`;
|
||||
if (resLocation === this.newInstanceDialog.location) {
|
||||
const shortName = args.json.shortName;
|
||||
const secureOrShortName = args.json.shortName || args.json.secureName;
|
||||
this.newInstanceDialog.shortName = shortName;
|
||||
this.newInstanceDialog.secureOrShortName = secureOrShortName;
|
||||
this.updateNewInstanceDialog(true);
|
||||
}
|
||||
}
|
||||
const newUrl = utils.getLaunchURL(L);
|
||||
this.copyToClipboard(newUrl);
|
||||
},
|
||||
async copyToClipboard(newUrl) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(newUrl);
|
||||
this.$message({
|
||||
message: 'Instance copied to clipboard',
|
||||
type: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
this.$message({
|
||||
message: 'Instance copied failed',
|
||||
type: 'error'
|
||||
});
|
||||
console.error(error.message);
|
||||
}
|
||||
},
|
||||
hasGroupPermission(ref, permission) {
|
||||
return utils.hasGroupPermission(ref, permission);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
ref="dialog"
|
||||
:before-close="beforeDialogClose"
|
||||
:visible="visible"
|
||||
:title="$t('dialog.previous_instances.info')"
|
||||
width="800px"
|
||||
:fullscreen="fullscreen"
|
||||
destroy-on-close
|
||||
@mousedown.native="dialogMouseDown"
|
||||
@mouseup.native="dialogMouseUp"
|
||||
@close="$emit('update:visible', false)">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
||||
<location :location="location.tag" style="font-size: 14px"></location>
|
||||
<el-input
|
||||
v-model="dataTable.filters[0].value"
|
||||
:placeholder="$t('dialog.previous_instances.search_placeholder')"
|
||||
style="width: 150px"
|
||||
clearable></el-input>
|
||||
</div>
|
||||
<data-tables v-loading="loading" v-bind="dataTable" style="margin-top: 10px">
|
||||
<el-table-column :label="$t('table.previous_instances.date')" prop="created_at" sortable width="110">
|
||||
<template slot-scope="scope">
|
||||
<el-tooltip placement="left">
|
||||
<template slot="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.icon')" prop="isFriend" width="70" align="center">
|
||||
<template slot-scope="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.previous_instances.display_name')" prop="displayName" sortable>
|
||||
<template slot-scope="scope">
|
||||
<span class="x-link" @click="lookupUser(scope.row)">{{ scope.row.displayName }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('table.previous_instances.time')" prop="time" width="100" sortable>
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.timer }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('table.previous_instances.count')" prop="count" width="100" sortable>
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.count }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</data-tables>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import utils from '../../../classes/utils';
|
||||
import database from '../../../service/database';
|
||||
import dayjs from 'dayjs';
|
||||
import Location from '../../Location.vue';
|
||||
|
||||
export default {
|
||||
name: 'PreviousInstancesInfoDialog',
|
||||
components: {
|
||||
Location
|
||||
},
|
||||
inject: ['adjustDialogZ', 'beforeDialogClose', 'dialogMouseDown', 'dialogMouseUp'],
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
instanceId: { type: String, required: true },
|
||||
gameLogIsFriend: { type: Function, required: true },
|
||||
gameLogIsFavorite: { type: Function, required: true },
|
||||
lookupUser: { type: Function, required: true },
|
||||
isDarkMode: { type: Boolean, required: true }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
echarts: null,
|
||||
echartsInstance: null,
|
||||
loading: false,
|
||||
location: {},
|
||||
currentTab: 'table',
|
||||
dataTable: {
|
||||
data: [],
|
||||
filters: [
|
||||
{
|
||||
prop: 'displayName',
|
||||
value: ''
|
||||
}
|
||||
],
|
||||
tableProps: {
|
||||
stripe: true,
|
||||
size: 'mini',
|
||||
defaultSort: {
|
||||
prop: 'created_at',
|
||||
order: 'descending'
|
||||
}
|
||||
},
|
||||
pageSize: 10,
|
||||
paginationProps: {
|
||||
small: true,
|
||||
layout: 'sizes,prev,pager,next,total',
|
||||
pageSizes: [10, 25, 50, 100]
|
||||
}
|
||||
},
|
||||
fullscreen: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
activityDetailData() {
|
||||
return this.dataTable.data.map((item) => ({
|
||||
displayName: item.displayName,
|
||||
joinTime: dayjs(item.created_at),
|
||||
leaveTime: dayjs(item.created_at).add(item.time, 'ms'),
|
||||
time: item.time,
|
||||
timer: item.timer
|
||||
}));
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(value) {
|
||||
if (value) {
|
||||
this.$nextTick(() => {
|
||||
this.init();
|
||||
this.refreshPreviousInstancesInfoTable();
|
||||
});
|
||||
utils.loadEcharts().then((echarts) => {
|
||||
this.echarts = echarts;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
this.adjustDialogZ(this.$refs.dialog.$el);
|
||||
this.loading = true;
|
||||
this.location = utils.parseLocation(this.instanceId);
|
||||
},
|
||||
refreshPreviousInstancesInfoTable() {
|
||||
database.getPlayersFromInstance(this.location.tag).then((data) => {
|
||||
const array = [];
|
||||
for (const entry of Array.from(data.values())) {
|
||||
entry.timer = utils.timeToText(entry.time);
|
||||
array.push(entry);
|
||||
}
|
||||
array.sort(utils.compareByCreatedAt);
|
||||
this.dataTable.data = array;
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,214 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
ref="previousInstancesUserDialog"
|
||||
:before-close="beforeDialogClose"
|
||||
:visible.sync="isVisible"
|
||||
:title="$t('dialog.previous_instances.header')"
|
||||
width="1000px"
|
||||
@mousedown.native="dialogMouseDown"
|
||||
@mouseup.native="dialogMouseUp">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
||||
<span style="font-size: 14px" v-text="previousInstancesUserDialog.userRef.displayName"></span>
|
||||
<el-input
|
||||
v-model="previousInstancesUserDialogTable.filters[0].value"
|
||||
:placeholder="$t('dialog.previous_instances.search_placeholder')"
|
||||
style="display: block; width: 150px"></el-input>
|
||||
</div>
|
||||
<data-tables v-loading="loading" v-bind="previousInstancesUserDialogTable" style="margin-top: 10px">
|
||||
<el-table-column :label="$t('table.previous_instances.date')" prop="created_at" sortable width="170">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.created_at | formatDate('long') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('table.previous_instances.world')" prop="name" sortable>
|
||||
<template slot-scope="scope">
|
||||
<location
|
||||
:location="scope.row.location"
|
||||
:hint="scope.row.worldName"
|
||||
:grouphint="scope.row.groupName"></location>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('table.previous_instances.instance_creator')" prop="location" width="170">
|
||||
<template slot-scope="scope">
|
||||
<display-name
|
||||
:userid="scope.row.$location.userId"
|
||||
:location="scope.row.$location.tag"></display-name>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('table.previous_instances.time')" prop="time" width="100" sortable>
|
||||
<template slot-scope="scope">
|
||||
<span v-text="scope.row.timer"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('table.previous_instances.action')" width="90" align="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
type="text"
|
||||
icon="el-icon-switch-button"
|
||||
size="mini"
|
||||
@click="showLaunchDialog(scope.row.location)"></el-button>
|
||||
<el-button
|
||||
type="text"
|
||||
icon="el-icon-s-data"
|
||||
size="mini"
|
||||
@click="showPreviousInstancesInfoDialog(scope.row.location)"></el-button>
|
||||
<el-button
|
||||
v-if="shiftHeld"
|
||||
style="color: #f56c6c"
|
||||
type="text"
|
||||
icon="el-icon-close"
|
||||
size="mini"
|
||||
@click="deleteGameLogUserInstance(scope.row)"></el-button>
|
||||
<el-button
|
||||
v-else
|
||||
type="text"
|
||||
icon="el-icon-close"
|
||||
size="mini"
|
||||
@click="deleteGameLogUserInstancePrompt(scope.row)"></el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</data-tables>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import utils from '../../../classes/utils';
|
||||
import database from '../../../service/database';
|
||||
import Location from '../../Location.vue';
|
||||
|
||||
export default {
|
||||
name: 'PreviousInstancesUserDialog',
|
||||
components: {
|
||||
Location
|
||||
},
|
||||
inject: [
|
||||
'beforeDialogClose',
|
||||
'dialogMouseDown',
|
||||
'dialogMouseUp',
|
||||
'adjustDialogZ',
|
||||
'showLaunchDialog',
|
||||
'showPreviousInstancesInfoDialog'
|
||||
],
|
||||
props: {
|
||||
previousInstancesUserDialog: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
visible: false,
|
||||
userRef: {},
|
||||
loading: false,
|
||||
forceUpdate: 0,
|
||||
previousInstances: [],
|
||||
previousInstancesTable: {
|
||||
data: [],
|
||||
filters: [
|
||||
{
|
||||
prop: 'displayName',
|
||||
value: ''
|
||||
}
|
||||
],
|
||||
tableProps: {
|
||||
stripe: true,
|
||||
size: 'mini',
|
||||
height: '400px'
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
shiftHeld: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
previousInstancesUserDialogTable: {
|
||||
data: [],
|
||||
filters: [
|
||||
{
|
||||
prop: 'worldName',
|
||||
value: ''
|
||||
}
|
||||
],
|
||||
tableProps: {
|
||||
stripe: true,
|
||||
size: 'mini',
|
||||
defaultSort: {
|
||||
prop: 'created_at',
|
||||
order: 'descending'
|
||||
}
|
||||
},
|
||||
pageSize: 10,
|
||||
paginationProps: {
|
||||
small: true,
|
||||
layout: 'sizes,prev,pager,next,total',
|
||||
pageSizes: [10, 25, 50, 100]
|
||||
}
|
||||
},
|
||||
loading: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isVisible: {
|
||||
get() {
|
||||
return this.previousInstancesUserDialog.visible;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:previous-instances-user-dialog', {
|
||||
...this.previousInstancesUserDialog,
|
||||
visible: value
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'previousInstancesUserDialog.openFlg'() {
|
||||
if (this.previousInstancesUserDialog.visible) {
|
||||
this.$nextTick(() => {
|
||||
this.adjustDialogZ(this.$refs.previousInstancesUserDialog.$el);
|
||||
});
|
||||
this.refreshPreviousInstancesUserTable();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
refreshPreviousInstancesUserTable() {
|
||||
this.loading = true;
|
||||
database.getpreviousInstancesByUserId(this.previousInstancesUserDialog.userRef).then((data) => {
|
||||
const array = [];
|
||||
for (const ref of data.values()) {
|
||||
ref.$location = utils.parseLocation(ref.location);
|
||||
if (ref.time > 0) {
|
||||
ref.timer = utils.timeToText(ref.time);
|
||||
} else {
|
||||
ref.timer = '';
|
||||
}
|
||||
array.push(ref);
|
||||
}
|
||||
array.sort(utils.compareByCreatedAt);
|
||||
this.previousInstancesUserDialogTable.data = array;
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
deleteGameLogUserInstance(row) {
|
||||
database.deleteGameLogInstance({
|
||||
id: this.previousInstancesUserDialog.userRef.id,
|
||||
displayName: this.previousInstancesUserDialog.userRef.displayName,
|
||||
location: row.location
|
||||
});
|
||||
utils.removeFromArray(this.previousInstancesUserDialogTable.data, row);
|
||||
},
|
||||
deleteGameLogUserInstancePrompt(row) {
|
||||
this.$confirm('Continue? Delete User From GameLog Instance', 'Confirm', {
|
||||
confirmButtonText: 'Confirm',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'info',
|
||||
callback: (action) => {
|
||||
if (action === 'confirm') {
|
||||
this.deleteGameLogUserInstance(row);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,186 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
ref="previousInstancesWorldDialog"
|
||||
:before-close="beforeDialogClose"
|
||||
:visible.sync="isVisible"
|
||||
:title="$t('dialog.previous_instances.header')"
|
||||
width="1000px"
|
||||
append-to-body
|
||||
@mousedown.native="dialogMouseDown"
|
||||
@mouseup.native="dialogMouseUp">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
||||
<span style="font-size: 14px" v-text="previousInstancesWorldDialog.worldRef.name"></span>
|
||||
<el-input
|
||||
v-model="previousInstancesWorldDialogTable.filters[0].value"
|
||||
:placeholder="$t('dialog.previous_instances.search_placeholder')"
|
||||
style="display: block; width: 150px"></el-input>
|
||||
</div>
|
||||
<data-tables v-loading="loading" v-bind="previousInstancesWorldDialogTable" style="margin-top: 10px">
|
||||
<el-table-column :label="$t('table.previous_instances.date')" prop="created_at" sortable width="170">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.created_at | formatDate('long') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('table.previous_instances.instance_name')" prop="name">
|
||||
<template slot-scope="scope">
|
||||
<location-world
|
||||
:locationobject="scope.row.$location"
|
||||
:grouphint="scope.row.groupName"
|
||||
:currentuserid="API.currentUser.id"
|
||||
@show-launch-dialog="showLaunchDialog"></location-world>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('table.previous_instances.instance_creator')" prop="location">
|
||||
<template slot-scope="scope">
|
||||
<display-name
|
||||
:userid="scope.row.$location.userId"
|
||||
:location="scope.row.$location.tag"
|
||||
:force-update-key="previousInstancesWorldDialog.forceUpdate"></display-name>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('table.previous_instances.time')" prop="time" width="100" sortable>
|
||||
<template slot-scope="scope">
|
||||
<span v-text="scope.row.timer"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('table.previous_instances.action')" width="90" align="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
type="text"
|
||||
icon="el-icon-s-data"
|
||||
size="mini"
|
||||
@click="showPreviousInstancesInfoDialog(scope.row.location)"></el-button>
|
||||
<el-button
|
||||
v-if="shiftHeld"
|
||||
style="color: #f56c6c"
|
||||
type="text"
|
||||
icon="el-icon-close"
|
||||
size="mini"
|
||||
@click="deleteGameLogWorldInstance(scope.row)"></el-button>
|
||||
<el-button
|
||||
v-else
|
||||
type="text"
|
||||
icon="el-icon-close"
|
||||
size="mini"
|
||||
@click="deleteGameLogWorldInstancePrompt(scope.row)"></el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</data-tables>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import utils from '../../../classes/utils';
|
||||
import database from '../../../service/database';
|
||||
|
||||
export default {
|
||||
name: 'PreviousInstancesWorldDialog',
|
||||
inject: [
|
||||
'API',
|
||||
'showLaunchDialog',
|
||||
'showPreviousInstancesInfoDialog',
|
||||
'adjustDialogZ',
|
||||
'beforeDialogClose',
|
||||
'dialogMouseDown',
|
||||
'dialogMouseUp'
|
||||
],
|
||||
props: {
|
||||
previousInstancesWorldDialog: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
shiftHeld: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
previousInstancesWorldDialogTable: {
|
||||
data: [],
|
||||
filters: [
|
||||
{
|
||||
prop: 'groupName',
|
||||
value: ''
|
||||
}
|
||||
],
|
||||
tableProps: {
|
||||
stripe: true,
|
||||
size: 'mini',
|
||||
defaultSort: {
|
||||
prop: 'created_at',
|
||||
order: 'descending'
|
||||
}
|
||||
},
|
||||
pageSize: 10,
|
||||
paginationProps: {
|
||||
small: true,
|
||||
layout: 'sizes,prev,pager,next,total',
|
||||
pageSizes: [10, 25, 50, 100]
|
||||
}
|
||||
},
|
||||
loading: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isVisible: {
|
||||
get() {
|
||||
return this.previousInstancesWorldDialog.visible;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:previous-instances-world-dialog', {
|
||||
...this.previousInstancesWorldDialog,
|
||||
visible: value
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'previousInstancesWorldDialog.openFlg'() {
|
||||
if (this.previousInstancesWorldDialog.visible) {
|
||||
this.$nextTick(() => {
|
||||
this.adjustDialogZ(this.$refs.previousInstancesWorldDialog.$el);
|
||||
});
|
||||
this.refreshPreviousInstancesWorldTable();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
refreshPreviousInstancesWorldTable() {
|
||||
this.loading = true;
|
||||
const D = this.previousInstancesWorldDialog;
|
||||
database.getpreviousInstancesByWorldId(D.worldRef).then((data) => {
|
||||
const array = [];
|
||||
for (const ref of data.values()) {
|
||||
ref.$location = utils.parseLocation(ref.location);
|
||||
if (ref.time > 0) {
|
||||
ref.timer = utils.timeToText(ref.time);
|
||||
} else {
|
||||
ref.timer = '';
|
||||
}
|
||||
array.push(ref);
|
||||
}
|
||||
array.sort(utils.compareByCreatedAt);
|
||||
this.previousInstancesWorldDialogTable.data = array;
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
deleteGameLogWorldInstance(row) {
|
||||
database.deleteGameLogInstanceByInstanceId({
|
||||
location: row.location
|
||||
});
|
||||
utils.removeFromArray(this.previousInstancesWorldDialogTable.data, row);
|
||||
},
|
||||
|
||||
deleteGameLogWorldInstancePrompt(row) {
|
||||
this.$confirm('Continue? Delete GameLog Instance', 'Confirm', {
|
||||
confirmButtonText: 'Confirm',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'info',
|
||||
callback: (action) => {
|
||||
if (action === 'confirm') {
|
||||
this.deleteGameLogWorldInstance(row);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
297
src/components/dialogs/WorldDialog/SetWorldTagsDialog.vue
Normal file
297
src/components/dialogs/WorldDialog/SetWorldTagsDialog.vue
Normal file
@@ -0,0 +1,297 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:before-close="beforeDialogClose"
|
||||
:visible.sync="isVisible"
|
||||
:title="$t('dialog.set_world_tags.header')"
|
||||
width="400px"
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
@mousedown.native="dialogMouseDown"
|
||||
@mouseup.native="dialogMouseUp">
|
||||
<el-checkbox v-model="setWorldTagsDialog.avatarScalingDisabled">
|
||||
{{ $t('dialog.set_world_tags.avatar_scaling_disabled') }}
|
||||
</el-checkbox>
|
||||
<br />
|
||||
<el-checkbox v-model="setWorldTagsDialog.focusViewDisabled">
|
||||
{{ $t('dialog.set_world_tags.focus_view_disabled') }}
|
||||
</el-checkbox>
|
||||
<br />
|
||||
<el-checkbox v-model="setWorldTagsDialog.debugAllowed">
|
||||
{{ $t('dialog.set_world_tags.enable_debugging') }}
|
||||
</el-checkbox>
|
||||
<div style="font-size: 12px; margin-top: 10px">{{ $t('dialog.set_world_tags.author_tags') }}<br /></div>
|
||||
<el-input
|
||||
v-model="setWorldTagsDialog.authorTags"
|
||||
type="textarea"
|
||||
size="mini"
|
||||
show-word-limit
|
||||
:autosize="{ minRows: 2, maxRows: 5 }"
|
||||
placeholder=""
|
||||
style="margin-top: 10px"></el-input>
|
||||
<div style="font-size: 12px; margin-top: 10px">{{ $t('dialog.set_world_tags.content_tags') }}<br /></div>
|
||||
<el-checkbox v-model="setWorldTagsDialog.contentHorror">
|
||||
{{ $t('dialog.set_world_tags.content_horror') }}
|
||||
</el-checkbox>
|
||||
<br />
|
||||
<el-checkbox v-model="setWorldTagsDialog.contentGore">
|
||||
{{ $t('dialog.set_world_tags.content_gore') }}
|
||||
</el-checkbox>
|
||||
<br />
|
||||
<el-checkbox v-model="setWorldTagsDialog.contentViolence">
|
||||
{{ $t('dialog.set_world_tags.content_violence') }}
|
||||
</el-checkbox>
|
||||
<br />
|
||||
<el-checkbox v-model="setWorldTagsDialog.contentAdult">
|
||||
{{ $t('dialog.set_world_tags.content_adult') }}
|
||||
</el-checkbox>
|
||||
<br />
|
||||
<el-checkbox v-model="setWorldTagsDialog.contentSex">
|
||||
{{ $t('dialog.set_world_tags.content_sex') }}
|
||||
</el-checkbox>
|
||||
<div style="font-size: 12px; margin-top: 10px">
|
||||
{{ $t('dialog.set_world_tags.default_content_settings') }}<br />
|
||||
</div>
|
||||
<el-checkbox v-model="setWorldTagsDialog.emoji">
|
||||
{{ $t('dialog.new_instance.content_emoji') }}
|
||||
</el-checkbox>
|
||||
<br />
|
||||
<el-checkbox v-model="setWorldTagsDialog.stickers">
|
||||
{{ $t('dialog.new_instance.content_stickers') }}
|
||||
</el-checkbox>
|
||||
<br />
|
||||
<el-checkbox v-model="setWorldTagsDialog.pedestals">
|
||||
{{ $t('dialog.new_instance.content_pedestals') }}
|
||||
</el-checkbox>
|
||||
<br />
|
||||
<el-checkbox v-model="setWorldTagsDialog.prints">
|
||||
{{ $t('dialog.new_instance.content_prints') }}
|
||||
</el-checkbox>
|
||||
<br />
|
||||
<el-checkbox v-model="setWorldTagsDialog.drones">
|
||||
{{ $t('dialog.new_instance.content_drones') }}
|
||||
</el-checkbox>
|
||||
<template #footer>
|
||||
<div style="display: flex">
|
||||
<el-button size="small" @click="setWorldTagsDialog.visible = false">
|
||||
{{ $t('dialog.set_world_tags.cancel') }}
|
||||
</el-button>
|
||||
<el-button type="primary" size="small" @click="saveSetWorldTagsDialog">
|
||||
{{ $t('dialog.set_world_tags.save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { worldRequest } from '../../../api';
|
||||
|
||||
export default {
|
||||
name: 'SetWorldTagsDialog',
|
||||
inject: ['beforeDialogClose', 'dialogMouseDown', 'dialogMouseUp', 'showWorldDialog'],
|
||||
props: {
|
||||
oldTags: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
isSetWorldTagsDialogVisible: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
worldId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
isWorldDialogVisible: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
setWorldTagsDialog: {
|
||||
authorTags: [],
|
||||
contentTags: [],
|
||||
debugAllowed: false,
|
||||
avatarScalingDisabled: false,
|
||||
focusViewDisabled: false,
|
||||
contentHorror: false,
|
||||
contentGore: false,
|
||||
contentViolence: false,
|
||||
contentAdult: false,
|
||||
contentSex: false,
|
||||
emoji: true,
|
||||
stickers: true,
|
||||
pedestals: true,
|
||||
prints: true,
|
||||
drones: true
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isVisible: {
|
||||
get() {
|
||||
return this.isSetWorldTagsDialogVisible;
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('update:is-set-world-tags-dialog-visible', val);
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
isSetWorldTagsDialogVisible(val) {
|
||||
if (val) {
|
||||
this.showSetWorldTagsDialog();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showSetWorldTagsDialog() {
|
||||
const D = this.setWorldTagsDialog;
|
||||
D.visible = true;
|
||||
D.debugAllowed = false;
|
||||
D.avatarScalingDisabled = false;
|
||||
D.focusViewDisabled = false;
|
||||
D.contentHorror = false;
|
||||
D.contentGore = false;
|
||||
D.contentViolence = false;
|
||||
D.contentAdult = false;
|
||||
D.contentSex = false;
|
||||
const authorTags = [];
|
||||
const contentTags = [];
|
||||
this.oldTags.forEach((tag) => {
|
||||
if (tag.startsWith('author_tag_')) {
|
||||
authorTags.unshift(tag.substring(11));
|
||||
}
|
||||
if (tag.startsWith('content_')) {
|
||||
contentTags.unshift(tag.substring(8));
|
||||
}
|
||||
switch (tag) {
|
||||
case 'content_horror':
|
||||
D.contentHorror = true;
|
||||
break;
|
||||
case 'content_gore':
|
||||
D.contentGore = true;
|
||||
break;
|
||||
case 'content_violence':
|
||||
D.contentViolence = true;
|
||||
break;
|
||||
case 'content_adult':
|
||||
D.contentAdult = true;
|
||||
break;
|
||||
case 'content_sex':
|
||||
D.contentSex = true;
|
||||
break;
|
||||
case 'debug_allowed':
|
||||
D.debugAllowed = true;
|
||||
break;
|
||||
case 'feature_avatar_scaling_disabled':
|
||||
D.avatarScalingDisabled = true;
|
||||
break;
|
||||
case 'feature_focus_view_disabled':
|
||||
D.focusViewDisabled = true;
|
||||
break;
|
||||
case 'feature_emoji_disabled':
|
||||
D.emoji = false;
|
||||
break;
|
||||
case 'feature_stickers_disabled':
|
||||
D.stickers = false;
|
||||
break;
|
||||
case 'feature_pedestals_disabled':
|
||||
D.pedestals = false;
|
||||
break;
|
||||
case 'feature_prints_disabled':
|
||||
D.prints = false;
|
||||
break;
|
||||
case 'feature_drones_disabled':
|
||||
D.drones = false;
|
||||
break;
|
||||
}
|
||||
});
|
||||
D.authorTags = authorTags.toString();
|
||||
D.contentTags = contentTags.toString();
|
||||
},
|
||||
saveSetWorldTagsDialog() {
|
||||
const D = this.setWorldTagsDialog;
|
||||
const authorTags = D.authorTags.trim().split(',');
|
||||
const contentTags = D.contentTags.trim().split(',');
|
||||
const tags = [];
|
||||
authorTags.forEach((tag) => {
|
||||
if (tag) {
|
||||
tags.unshift(`author_tag_${tag}`);
|
||||
}
|
||||
});
|
||||
// add back custom tags
|
||||
contentTags.forEach((tag) => {
|
||||
switch (tag) {
|
||||
case 'horror':
|
||||
case 'gore':
|
||||
case 'violence':
|
||||
case 'adult':
|
||||
case 'sex':
|
||||
case '':
|
||||
break;
|
||||
default:
|
||||
tags.unshift(`content_${tag}`);
|
||||
break;
|
||||
}
|
||||
});
|
||||
if (D.contentHorror) {
|
||||
tags.unshift('content_horror');
|
||||
}
|
||||
if (D.contentGore) {
|
||||
tags.unshift('content_gore');
|
||||
}
|
||||
if (D.contentViolence) {
|
||||
tags.unshift('content_violence');
|
||||
}
|
||||
if (D.contentAdult) {
|
||||
tags.unshift('content_adult');
|
||||
}
|
||||
if (D.contentSex) {
|
||||
tags.unshift('content_sex');
|
||||
}
|
||||
if (D.debugAllowed) {
|
||||
tags.unshift('debug_allowed');
|
||||
}
|
||||
if (D.avatarScalingDisabled) {
|
||||
tags.unshift('feature_avatar_scaling_disabled');
|
||||
}
|
||||
if (D.focusViewDisabled) {
|
||||
tags.unshift('feature_focus_view_disabled');
|
||||
}
|
||||
if (!D.emoji) {
|
||||
tags.unshift('feature_emoji_disabled');
|
||||
}
|
||||
if (!D.stickers) {
|
||||
tags.unshift('feature_stickers_disabled');
|
||||
}
|
||||
if (!D.pedestals) {
|
||||
tags.unshift('feature_pedestals_disabled');
|
||||
}
|
||||
if (!D.prints) {
|
||||
tags.unshift('feature_prints_disabled');
|
||||
}
|
||||
if (!D.drones) {
|
||||
tags.unshift('feature_drones_disabled');
|
||||
}
|
||||
worldRequest
|
||||
.saveWorld({
|
||||
id: this.worldId,
|
||||
tags
|
||||
})
|
||||
.then((args) => {
|
||||
this.$message({
|
||||
message: 'Tags updated',
|
||||
type: 'success'
|
||||
});
|
||||
this.$emit('update:is-set-world-tags-dialog-visible', false);
|
||||
if (this.isWorldDialogVisible) {
|
||||
this.showWorldDialog(args.json.id);
|
||||
}
|
||||
return args;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:before-close="beforeDialogClose"
|
||||
:visible.sync="isVisible"
|
||||
:title="$t('dialog.allowed_video_player_domains.header')"
|
||||
width="600px"
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
@mousedown.native="dialogMouseDown"
|
||||
@mouseup.native="dialogMouseUp">
|
||||
<div>
|
||||
<el-input
|
||||
v-for="(domain, index) in urlList"
|
||||
:key="index"
|
||||
v-model="urlList[index]"
|
||||
:value="domain"
|
||||
size="small"
|
||||
style="margin-top: 5px">
|
||||
<el-button slot="append" icon="el-icon-delete" @click="urlList.splice(index, 1)"></el-button>
|
||||
</el-input>
|
||||
<el-button size="mini" style="margin-top: 5px" @click="urlList.push('')">
|
||||
{{ $t('dialog.allowed_video_player_domains.add_domain') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
:disabled="!worldAllowedDomainsDialog.worldId"
|
||||
@click="saveWorldAllowedDomains">
|
||||
{{ $t('dialog.allowed_video_player_domains.save') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { worldRequest } from '../../../api';
|
||||
|
||||
export default {
|
||||
name: 'WorldAllowedDomainsDialog',
|
||||
inject: ['beforeDialogClose', 'dialogMouseDown', 'dialogMouseUp'],
|
||||
props: {
|
||||
worldAllowedDomainsDialog: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
urlList: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isVisible: {
|
||||
get() {
|
||||
return this.worldAllowedDomainsDialog.visible;
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('update:world-allowed-domains-dialog', {
|
||||
...this.worldAllowedDomainsDialog,
|
||||
visible: val
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'worldAllowedDomainsDialog.visible'(val) {
|
||||
if (val) {
|
||||
this.urlList = this.worldAllowedDomainsDialog.urlList;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
saveWorldAllowedDomains() {
|
||||
const D = this.worldAllowedDomainsDialog;
|
||||
worldRequest
|
||||
.saveWorld({
|
||||
id: D.worldId,
|
||||
urlList: D.urlList
|
||||
})
|
||||
.then((args) => {
|
||||
this.$message({
|
||||
message: 'Allowed Video Player Domains updated',
|
||||
type: 'success'
|
||||
});
|
||||
return args;
|
||||
});
|
||||
D.visible = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
1151
src/components/dialogs/WorldDialog/WorldDialog.vue
Normal file
1151
src/components/dialogs/WorldDialog/WorldDialog.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,234 +0,0 @@
|
||||
<template>
|
||||
<div @click="$emit('click')">
|
||||
<div class="x-friend-item">
|
||||
<template v-if="isLocalFavorite ? favorite.name : favorite.ref">
|
||||
<div class="avatar">
|
||||
<img v-lazy="smallThumbnail" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span class="name" v-text="localFavFakeRef.name"></span>
|
||||
<span class="extra" v-text="localFavFakeRef.authorName"></span>
|
||||
</div>
|
||||
<template v-if="editFavoritesMode">
|
||||
<el-dropdown trigger="click" size="mini" style="margin-left: 5px" @click.native.stop>
|
||||
<el-tooltip placement="top" :content="tooltipContent" :disabled="hideTooltips">
|
||||
<el-button type="default" icon="el-icon-back" size="mini" circle></el-button>
|
||||
</el-tooltip>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<template
|
||||
v-for="groupAPI in API.favoriteAvatarGroups"
|
||||
v-if="isLocalFavorite || groupAPI.name !== group.name">
|
||||
<el-dropdown-item
|
||||
:key="groupAPI.name"
|
||||
style="display: block; margin: 10px 0"
|
||||
:disabled="groupAPI.count >= groupAPI.capacity"
|
||||
@click.native="handleDropdownItemClick(groupAPI)">
|
||||
{{ groupAPI.displayName }} ({{ groupAPI.count }} / {{ groupAPI.capacity }})
|
||||
</el-dropdown-item>
|
||||
</template>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
<el-button v-if="!isLocalFavorite" type="text" size="mini" style="margin-left: 5px" @click.stop>
|
||||
<el-checkbox v-model="isSelected"></el-checkbox>
|
||||
</el-button>
|
||||
</template>
|
||||
<template v-else-if="!isLocalFavorite">
|
||||
<el-tooltip
|
||||
v-if="favorite.deleted"
|
||||
placement="left"
|
||||
:content="$t('view.favorite.unavailable_tooltip')">
|
||||
<i class="el-icon-warning" style="color: #f56c6c; margin-left: 5px"></i>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
v-if="favorite.ref.releaseStatus === 'private'"
|
||||
placement="left"
|
||||
:content="$t('view.favorite.private')">
|
||||
<i class="el-icon-warning" style="color: #e6a23c; margin-left: 5px"></i>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
v-if="favorite.ref.releaseStatus !== 'private' && !favorite.deleted"
|
||||
placement="left"
|
||||
:content="$t('view.favorite.select_avatar_tooltip')"
|
||||
:disabled="hideTooltips">
|
||||
<el-button
|
||||
:disabled="API.currentUser.currentAvatar === favorite.id"
|
||||
size="mini"
|
||||
icon="el-icon-check"
|
||||
circle
|
||||
style="margin-left: 5px"
|
||||
@click.stop="selectAvatarWithConfirmation"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
placement="right"
|
||||
:content="$t('view.favorite.unfavorite_tooltip')"
|
||||
:disabled="hideTooltips">
|
||||
<el-button
|
||||
v-if="shiftHeld"
|
||||
size="mini"
|
||||
icon="el-icon-close"
|
||||
circle
|
||||
style="color: #f56c6c; margin-left: 5px"
|
||||
@click.stop="deleteFavorite(favorite.id)"></el-button>
|
||||
<el-button
|
||||
v-else
|
||||
type="default"
|
||||
icon="el-icon-star-on"
|
||||
size="mini"
|
||||
circle
|
||||
style="margin-left: 5px"
|
||||
@click.stop="showFavoriteDialog('avatar', favorite.id)"></el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-tooltip
|
||||
placement="left"
|
||||
:content="$t('view.favorite.select_avatar_tooltip')"
|
||||
:disabled="hideTooltips">
|
||||
<el-button
|
||||
:disabled="API.currentUser.currentAvatar === favorite.id"
|
||||
size="mini"
|
||||
circle
|
||||
style="margin-left: 5px"
|
||||
icon="el-icon-check"
|
||||
@click.stop="selectAvatarWithConfirmation" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-tooltip
|
||||
v-if="isLocalFavorite"
|
||||
placement="right"
|
||||
:content="$t('view.favorite.unfavorite_tooltip')"
|
||||
:disabled="hideTooltips">
|
||||
<el-button
|
||||
v-if="shiftHeld"
|
||||
size="mini"
|
||||
icon="el-icon-close"
|
||||
circle
|
||||
style="color: #f56c6c; margin-left: 5px"
|
||||
@click.stop="removeLocalAvatarFavorite" />
|
||||
<el-button
|
||||
v-else
|
||||
type="default"
|
||||
icon="el-icon-star-on"
|
||||
size="mini"
|
||||
circle
|
||||
style="margin-left: 5px"
|
||||
@click.stop="showFavoriteDialog('avatar', favorite.id)"
|
||||
/></el-tooltip>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="avatar"></div>
|
||||
<div class="detail">
|
||||
<span class="name" v-text="favorite.name || favorite.id"></span>
|
||||
</div>
|
||||
<el-button
|
||||
type="text"
|
||||
icon="el-icon-close"
|
||||
size="mini"
|
||||
style="margin-left: 5px"
|
||||
@click.stop="deleteFavorite(favorite.id)"></el-button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { favoriteRequest } from '../../classes/request';
|
||||
|
||||
export default {
|
||||
name: 'FavoritesAvatarItem',
|
||||
inject: ['API', 'showFavoriteDialog'],
|
||||
props: {
|
||||
favorite: Object,
|
||||
group: [Object, String],
|
||||
editFavoritesMode: Boolean,
|
||||
shiftHeld: Boolean,
|
||||
hideTooltips: Boolean,
|
||||
isLocalFavorite: Boolean
|
||||
},
|
||||
computed: {
|
||||
isSelected: {
|
||||
get() {
|
||||
return this.favorite.$selected;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('handle-select', value);
|
||||
}
|
||||
},
|
||||
localFavFakeRef() {
|
||||
// local favorite no "ref" property
|
||||
return this.isLocalFavorite ? this.favorite : this.favorite.ref;
|
||||
},
|
||||
tooltipContent() {
|
||||
return $t(this.isLocalFavorite ? 'view.favorite.copy_tooltip' : 'view.favorite.move_tooltip');
|
||||
},
|
||||
smallThumbnail() {
|
||||
return (
|
||||
this.localFavFakeRef.thumbnailImageUrl.replace('256', '128') ||
|
||||
this.localFavFakeRef.thumbnailImageUrl
|
||||
);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
moveFavorite(ref, group, type) {
|
||||
favoriteRequest
|
||||
.deleteFavorite({
|
||||
objectId: ref.id
|
||||
})
|
||||
.then(() => {
|
||||
favoriteRequest.addFavorite({
|
||||
type,
|
||||
favoriteId: ref.id,
|
||||
tags: group.name
|
||||
});
|
||||
});
|
||||
},
|
||||
selectAvatarWithConfirmation() {
|
||||
this.$emit('select-avatar-with-confirmation', this.favorite.id);
|
||||
},
|
||||
deleteFavorite(objectId) {
|
||||
favoriteRequest.deleteFavorite({
|
||||
objectId
|
||||
});
|
||||
// FIXME: 메시지 수정
|
||||
// this.$confirm('Continue? Delete Favorite', 'Confirm', {
|
||||
// confirmButtonText: 'Confirm',
|
||||
// cancelButtonText: 'Cancel',
|
||||
// type: 'info',
|
||||
// callback: (action) => {
|
||||
// if (action === 'confirm') {
|
||||
// API.deleteFavorite({
|
||||
// objectId
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
},
|
||||
addFavoriteAvatar(groupAPI) {
|
||||
return favoriteRequest
|
||||
.addFavorite({
|
||||
type: 'avatar',
|
||||
favoriteId: this.favorite.id,
|
||||
tags: groupAPI.name
|
||||
})
|
||||
.then((args) => {
|
||||
this.$message({
|
||||
message: 'Avatar added to favorites',
|
||||
type: 'success'
|
||||
});
|
||||
|
||||
return args;
|
||||
});
|
||||
},
|
||||
handleDropdownItemClick(groupAPI) {
|
||||
if (this.isLocalFavorite) {
|
||||
this.addFavoriteAvatar(groupAPI);
|
||||
} else {
|
||||
this.moveFavorite(this.favorite.ref, groupAPI, 'avatar');
|
||||
}
|
||||
},
|
||||
removeLocalAvatarFavorite() {
|
||||
this.$emit('remove-local-avatar-favorite', this.favorite.id, this.group);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,68 +0,0 @@
|
||||
<template>
|
||||
<div @click="$emit('click')">
|
||||
<div class="x-friend-item">
|
||||
<div class="avatar">
|
||||
<img v-lazy="smallThumbnail" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span class="name" v-text="favorite.name"></span>
|
||||
<span class="extra" v-text="favorite.authorName"></span>
|
||||
</div>
|
||||
<el-tooltip placement="left" :content="$t('view.favorite.select_avatar_tooltip')" :disabled="hideTooltips">
|
||||
<el-button
|
||||
:disabled="API.currentUser.currentAvatar === favorite.id"
|
||||
size="mini"
|
||||
icon="el-icon-check"
|
||||
circle
|
||||
style="margin-left: 5px"
|
||||
@click.stop="selectAvatarWithConfirmation"></el-button>
|
||||
</el-tooltip>
|
||||
<template v-if="API.cachedFavoritesByObjectId.has(favorite.id)">
|
||||
<el-tooltip placement="right" content="Unfavorite" :disabled="hideTooltips">
|
||||
<el-button
|
||||
type="default"
|
||||
icon="el-icon-star-on"
|
||||
size="mini"
|
||||
circle
|
||||
style="margin-left: 5px"
|
||||
@click.stop="showFavoriteDialog('avatar', favorite.id)"></el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-tooltip placement="right" content="Favorite" :disabled="hideTooltips">
|
||||
<el-button
|
||||
type="default"
|
||||
icon="el-icon-star-off"
|
||||
size="mini"
|
||||
circle
|
||||
style="margin-left: 5px"
|
||||
@click.stop="showFavoriteDialog('avatar', favorite.id)"></el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'FavoritesAvatarLocalHistoryItem',
|
||||
inject: ['API', 'showFavoriteDialog'],
|
||||
props: {
|
||||
favorite: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
hideTooltips: Boolean
|
||||
},
|
||||
computed: {
|
||||
smallThumbnail() {
|
||||
return this.favorite.thumbnailImageUrl.replace('256', '128') || this.favorite.thumbnailImageUrl;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectAvatarWithConfirmation() {
|
||||
this.$emit('select-avatar-with-confirmation', this.favorite.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,400 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
||||
<div>
|
||||
<el-button size="small" @click="showAvatarExportDialog">
|
||||
{{ $t('view.favorite.export') }}
|
||||
</el-button>
|
||||
<el-button size="small" style="margin-left: 5px" @click="showAvatarImportDialog">
|
||||
{{ $t('view.favorite.import') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; font-size: 13px; margin-right: 10px">
|
||||
<span class="name" style="margin-right: 5px; line-height: 10px">
|
||||
{{ $t('view.favorite.sort_by') }}
|
||||
</span>
|
||||
<el-radio-group v-model="sortFav" style="margin-right: 12px" @change="saveSortFavoritesOption">
|
||||
<el-radio :label="false">
|
||||
{{ $t('view.settings.appearance.appearance.sort_favorite_by_name') }}
|
||||
</el-radio>
|
||||
<el-radio :label="true">
|
||||
{{ $t('view.settings.appearance.appearance.sort_favorite_by_date') }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
<el-input
|
||||
v-model="avatarFavoriteSearch"
|
||||
clearable
|
||||
size="mini"
|
||||
:placeholder="$t('view.favorite.avatars.search')"
|
||||
style="width: 200px"
|
||||
@input="searchAvatarFavorites" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="x-friend-list" style="margin-top: 10px">
|
||||
<div
|
||||
v-for="favorite in avatarFavoriteSearchResults"
|
||||
:key="favorite.id"
|
||||
style="display: inline-block; width: 300px; margin-right: 15px"
|
||||
@click="showAvatarDialog(favorite.id)">
|
||||
<div class="x-friend-item">
|
||||
<template v-if="favorite.name">
|
||||
<div class="avatar">
|
||||
<img v-lazy="favorite.thumbnailImageUrl" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span class="name" v-text="favorite.name" />
|
||||
<span class="extra" v-text="favorite.authorName" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="avatar"></div>
|
||||
<div class="detail">
|
||||
<span class="name" v-text="favorite.id" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span style="display: block; margin-top: 20px">
|
||||
{{ $t('view.favorite.avatars.vrchat_favorites') }}
|
||||
</span>
|
||||
<el-collapse style="border: 0">
|
||||
<el-collapse-item v-for="group in API.favoriteAvatarGroups" :key="group.name">
|
||||
<template slot="title">
|
||||
<span style="font-weight: bold; font-size: 14px; margin-left: 10px" v-text="group.displayName" />
|
||||
<span style="color: #909399; font-size: 12px; margin-left: 10px">
|
||||
{{ group.count }}/{{ group.capacity }}
|
||||
</span>
|
||||
<el-tooltip placement="top" :content="$t('view.favorite.rename_tooltip')" :disabled="hideTooltips">
|
||||
<el-button
|
||||
size="mini"
|
||||
icon="el-icon-edit"
|
||||
circle
|
||||
style="margin-left: 10px"
|
||||
@click.stop="changeFavoriteGroupName(group)" />
|
||||
</el-tooltip>
|
||||
<el-tooltip placement="right" :content="$t('view.favorite.clear_tooltip')" :disabled="hideTooltips">
|
||||
<el-button
|
||||
size="mini"
|
||||
icon="el-icon-delete"
|
||||
circle
|
||||
style="margin-left: 5px"
|
||||
@click.stop="clearFavoriteGroup(group)" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<div v-if="group.count" class="x-friend-list" style="margin-top: 10px">
|
||||
<FavoritesAvatarItem
|
||||
v-for="favorite in groupedByGroupKeyFavoriteAvatars[group.key]"
|
||||
:key="favorite.id"
|
||||
:favorite="favorite"
|
||||
:group="group"
|
||||
:hide-tooltips="hideTooltips"
|
||||
:shift-held="shiftHeld"
|
||||
:edit-favorites-mode="editFavoritesMode"
|
||||
style="display: inline-block; width: 300px; margin-right: 15px"
|
||||
@handle-select="favorite.$selected = $event"
|
||||
@remove-local-avatar-favorite="removeLocalAvatarFavorite"
|
||||
@select-avatar-with-confirmation="selectAvatarWithConfirmation"
|
||||
@click="showAvatarDialog(favorite.id)" />
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
style="
|
||||
padding-top: 25px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgb(144, 147, 153);
|
||||
">
|
||||
<span>No Data</span>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item>
|
||||
<template slot="title">
|
||||
<span style="font-weight: bold; font-size: 14px; margin-left: 10px">Local History</span>
|
||||
<span style="color: #909399; font-size: 12px; margin-left: 10px"
|
||||
>{{ avatarHistoryArray.length }}/100</span
|
||||
>
|
||||
<el-tooltip placement="right" content="Clear" :disabled="hideTooltips">
|
||||
<el-button
|
||||
size="mini"
|
||||
icon="el-icon-delete"
|
||||
circle
|
||||
style="margin-left: 5px"
|
||||
@click.stop="promptClearAvatarHistory"></el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<div v-if="avatarHistoryArray.length" class="x-friend-list" style="margin-top: 10px">
|
||||
<FavoritesAvatarLocalHistoryItem
|
||||
v-for="favorite in avatarHistoryArray"
|
||||
:key="favorite.id"
|
||||
style="display: inline-block; width: 300px; margin-right: 15px"
|
||||
:favorite="favorite"
|
||||
:hide-tooltips="hideTooltips"
|
||||
@select-avatar-with-confirmation="selectAvatarWithConfirmation"
|
||||
@click="showAvatarDialog(favorite.id)" />
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
style="
|
||||
padding-top: 25px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgb(144, 147, 153);
|
||||
">
|
||||
<span>No Data</span>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
<span style="display: block; margin-top: 20px">{{ $t('view.favorite.avatars.local_favorites') }}</span>
|
||||
<br />
|
||||
<el-button size="small" :disabled="!isLocalUserVrcplusSupporter" @click="promptNewLocalAvatarFavoriteGroup">
|
||||
{{ $t('view.favorite.avatars.new_group') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="!refreshingLocalFavorites"
|
||||
size="small"
|
||||
style="margin-left: 5px"
|
||||
@click="refreshLocalAvatarFavorites">
|
||||
{{ $t('view.favorite.avatars.refresh') }}
|
||||
</el-button>
|
||||
<el-button v-else size="small" style="margin-left: 5px" @click="refreshingLocalFavorites = false">
|
||||
<i class="el-icon-loading" style="margin-right: 5px"></i>
|
||||
<span>{{ $t('view.favorite.avatars.cancel_refresh') }}</span>
|
||||
</el-button>
|
||||
<el-collapse-item
|
||||
v-for="group in localAvatarFavoriteGroups"
|
||||
v-if="localAvatarFavorites[group]"
|
||||
:key="group">
|
||||
<template slot="title">
|
||||
<span :style="{ fontWeight: 'bold', fontSize: '14px', marginLeft: '10px' }">{{ group }}</span>
|
||||
<span :style="{ color: '#909399', fontSize: '12px', marginLeft: '10px' }">{{
|
||||
getLocalAvatarFavoriteGroupLength(group)
|
||||
}}</span>
|
||||
<el-tooltip placement="top" :content="$t('view.favorite.rename_tooltip')" :disabled="hideTooltips">
|
||||
<el-button
|
||||
size="mini"
|
||||
icon="el-icon-edit"
|
||||
circle
|
||||
:style="{ marginLeft: '5px' }"
|
||||
@click.stop="promptLocalAvatarFavoriteGroupRename(group)"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
placement="right"
|
||||
:content="$t('view.favorite.delete_tooltip')"
|
||||
:disabled="hideTooltips">
|
||||
<el-button
|
||||
size="mini"
|
||||
icon="el-icon-delete"
|
||||
circle
|
||||
:style="{ marginLeft: '5px' }"
|
||||
@click.stop="promptLocalAvatarFavoriteGroupDelete(group)"></el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<div v-if="localAvatarFavorites[group].length" class="x-friend-list" :style="{ marginTop: '10px' }">
|
||||
<FavoritesAvatarItem
|
||||
v-for="favorite in localAvatarFavorites[group]"
|
||||
:key="favorite.id"
|
||||
is-local-favorite
|
||||
:style="{ display: 'inline-block', width: '300px', marginRight: '15px' }"
|
||||
:favorite="favorite"
|
||||
:group="group"
|
||||
:hide-tooltips="hideTooltips"
|
||||
:shift-held="shiftHeld"
|
||||
:edit-favorites-mode="editFavoritesMode"
|
||||
@handle-select="favorite.$selected = $event"
|
||||
@remove-local-avatar-favorite="removeLocalAvatarFavorite"
|
||||
@select-avatar-with-confirmation="selectAvatarWithConfirmation"
|
||||
@click="showAvatarDialog(favorite.id)" />
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
:style="{
|
||||
paddingTop: '25px',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: 'rgb(144, 147, 153)'
|
||||
}">
|
||||
<span>No Data</span>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
<AvatarExportDialog
|
||||
:avatar-export-dialog-visible.sync="avatarExportDialogVisible"
|
||||
:favorite-avatars="favoriteAvatars"
|
||||
:local-avatar-favorite-groups="localAvatarFavoriteGroups"
|
||||
:local-avatar-favorites="localAvatarFavorites"
|
||||
:local-avatar-favorites-list="localAvatarFavoritesList" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FavoritesAvatarItem from './FavoritesAvatarItem.vue';
|
||||
import FavoritesAvatarLocalHistoryItem from './FavoritesAvatarLocalHistoryItem.vue';
|
||||
import AvatarExportDialog from '../../views/dialogs/favorites/AvatarExportDialog.vue';
|
||||
import { favoriteRequest } from '../../classes/request';
|
||||
|
||||
export default {
|
||||
name: 'FavoritesAvatarTab',
|
||||
components: { FavoritesAvatarItem, FavoritesAvatarLocalHistoryItem, AvatarExportDialog },
|
||||
inject: ['API', 'showAvatarDialog'],
|
||||
props: {
|
||||
sortFavorites: Boolean,
|
||||
hideTooltips: Boolean,
|
||||
shiftHeld: Boolean,
|
||||
editFavoritesMode: Boolean,
|
||||
avatarHistoryArray: Array,
|
||||
refreshingLocalFavorites: Boolean,
|
||||
localAvatarFavoriteGroups: Array,
|
||||
localAvatarFavorites: Object,
|
||||
favoriteAvatars: Array,
|
||||
localAvatarFavoritesList: Array
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
avatarExportDialogVisible: false,
|
||||
avatarFavoriteSearch: '',
|
||||
avatarFavoriteSearchResults: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
sortFav: {
|
||||
get() {
|
||||
return this.sortFavorites;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:sort-favorites', value);
|
||||
}
|
||||
},
|
||||
groupedByGroupKeyFavoriteAvatars() {
|
||||
const groupedByGroupKeyFavoriteAvatars = {};
|
||||
this.favoriteAvatars.forEach((avatar) => {
|
||||
if (avatar.groupKey) {
|
||||
if (!groupedByGroupKeyFavoriteAvatars[avatar.groupKey]) {
|
||||
groupedByGroupKeyFavoriteAvatars[avatar.groupKey] = [];
|
||||
}
|
||||
groupedByGroupKeyFavoriteAvatars[avatar.groupKey].push(avatar);
|
||||
}
|
||||
});
|
||||
|
||||
return groupedByGroupKeyFavoriteAvatars;
|
||||
},
|
||||
isLocalUserVrcplusSupporter() {
|
||||
return this.API.currentUser.$isVRCPlus;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getLocalAvatarFavoriteGroupLength(group) {
|
||||
const favoriteGroup = this.localAvatarFavorites[group];
|
||||
if (!favoriteGroup) {
|
||||
return 0;
|
||||
}
|
||||
return favoriteGroup.length;
|
||||
},
|
||||
searchAvatarFavorites() {
|
||||
let ref = null;
|
||||
const search = this.avatarFavoriteSearch.toLowerCase();
|
||||
if (search.length < 3) {
|
||||
this.avatarFavoriteSearchResults = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const results = [];
|
||||
for (let i = 0; i < this.localAvatarFavoriteGroups.length; ++i) {
|
||||
const group = this.localAvatarFavoriteGroups[i];
|
||||
if (!this.localAvatarFavorites[group]) {
|
||||
continue;
|
||||
}
|
||||
for (let j = 0; j < this.localAvatarFavorites[group].length; ++j) {
|
||||
ref = this.localAvatarFavorites[group][j];
|
||||
if (
|
||||
!ref ||
|
||||
typeof ref.id === 'undefined' ||
|
||||
typeof ref.name === 'undefined' ||
|
||||
typeof ref.authorName === 'undefined'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (ref.name.toLowerCase().includes(search) || ref.authorName.toLowerCase().includes(search)) {
|
||||
if (!results.some((r) => r.id === ref.id)) {
|
||||
results.push(ref);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.favoriteAvatars.length; ++i) {
|
||||
ref = this.favoriteAvatars[i].ref;
|
||||
if (
|
||||
!ref ||
|
||||
typeof ref.id === 'undefined' ||
|
||||
typeof ref.name === 'undefined' ||
|
||||
typeof ref.authorName === 'undefined'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (ref.name.toLowerCase().includes(search) || ref.authorName.toLowerCase().includes(search)) {
|
||||
if (!results.some((r) => r.id === ref.id)) {
|
||||
results.push(ref);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.avatarFavoriteSearchResults = results;
|
||||
},
|
||||
clearFavoriteGroup(ctx) {
|
||||
// FIXME: 메시지 수정
|
||||
this.$confirm('Continue? Clear Group', 'Confirm', {
|
||||
confirmButtonText: 'Confirm',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'info',
|
||||
callback: (action) => {
|
||||
if (action === 'confirm') {
|
||||
favoriteRequest.clearFavoriteGroup({
|
||||
type: ctx.type,
|
||||
group: ctx.name
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
showAvatarExportDialog() {
|
||||
this.avatarExportDialogVisible = true;
|
||||
},
|
||||
showAvatarImportDialog() {
|
||||
this.$emit('show-avatar-import-dialog');
|
||||
},
|
||||
saveSortFavoritesOption() {
|
||||
this.$emit('save-sort-favorites-option');
|
||||
},
|
||||
changeFavoriteGroupName(group) {
|
||||
this.$emit('change-favorite-group-name', group);
|
||||
},
|
||||
removeLocalAvatarFavorite(id, group) {
|
||||
this.$emit('remove-local-avatar-favorite', id, group);
|
||||
},
|
||||
selectAvatarWithConfirmation(id) {
|
||||
this.$emit('select-avatar-with-confirmation', id);
|
||||
},
|
||||
promptClearAvatarHistory() {
|
||||
this.$emit('prompt-clear-avatar-history');
|
||||
},
|
||||
promptNewLocalAvatarFavoriteGroup() {
|
||||
this.$emit('prompt-new-local-avatar-favorite-group');
|
||||
},
|
||||
refreshLocalAvatarFavorites() {
|
||||
this.$emit('refresh-local-avatar-favorites');
|
||||
},
|
||||
promptLocalAvatarFavoriteGroupRename(group) {
|
||||
this.$emit('prompt-local-avatar-favorite-group-rename', group);
|
||||
},
|
||||
promptLocalAvatarFavoriteGroupDelete(group) {
|
||||
this.$emit('prompt-local-avatar-favorite-group-delete', group);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,144 +0,0 @@
|
||||
<template>
|
||||
<div @click="$emit('click')">
|
||||
<div class="x-friend-item">
|
||||
<template v-if="favorite.ref">
|
||||
<div class="avatar" :class="userStatusClass(favorite.ref)">
|
||||
<img v-lazy="userImage(favorite.ref, true)" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span
|
||||
class="name"
|
||||
:style="{ color: favorite.ref.$userColour }"
|
||||
v-text="favorite.ref.displayName"></span>
|
||||
<location
|
||||
class="extra"
|
||||
v-if="favorite.ref.location !== 'offline'"
|
||||
:location="favorite.ref.location"
|
||||
:traveling="favorite.ref.travelingToLocation"
|
||||
:link="false"></location>
|
||||
<span v-else v-text="favorite.ref.statusDescription"></span>
|
||||
</div>
|
||||
<template v-if="editFavoritesMode">
|
||||
<el-dropdown trigger="click" size="mini" style="margin-left: 5px" @click.native.stop>
|
||||
<el-tooltip
|
||||
placement="left"
|
||||
:content="$t('view.favorite.move_tooltip')"
|
||||
:disabled="hideTooltips">
|
||||
<el-button type="default" icon="el-icon-back" size="mini" circle></el-button>
|
||||
</el-tooltip>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<template v-for="groupAPI in API.favoriteFriendGroups">
|
||||
<el-dropdown-item
|
||||
v-if="groupAPI.name !== group.name"
|
||||
:key="groupAPI.name"
|
||||
style="display: block; margin: 10px 0"
|
||||
:disabled="groupAPI.count >= groupAPI.capacity"
|
||||
@click.native="moveFavorite(favorite.ref, groupAPI, 'friend')">
|
||||
{{ groupAPI.displayName }} ({{ groupAPI.count }} / {{ groupAPI.capacity }})
|
||||
</el-dropdown-item>
|
||||
</template>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
<el-button type="text" size="mini" style="margin-left: 5px" @click.stop>
|
||||
<el-checkbox v-model="favorite.$selected"></el-checkbox>
|
||||
</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-tooltip
|
||||
placement="right"
|
||||
:content="$t('view.favorite.unfavorite_tooltip')"
|
||||
:disabled="hideTooltips">
|
||||
<el-button
|
||||
v-if="shiftHeld"
|
||||
size="mini"
|
||||
icon="el-icon-close"
|
||||
circle
|
||||
style="color: #f56c6c; margin-left: 5px"
|
||||
@click.stop="deleteFavorite(favorite.id)"></el-button>
|
||||
<el-button
|
||||
v-else
|
||||
type="default"
|
||||
icon="el-icon-star-on"
|
||||
size="mini"
|
||||
circle
|
||||
style="margin-left: 5px"
|
||||
@click.stop="showFavoriteDialog('friend', favorite.id)"></el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="avatar"></div>
|
||||
<div class="detail">
|
||||
<span v-text="favorite.name || favorite.id"></span>
|
||||
</div>
|
||||
<el-button
|
||||
type="text"
|
||||
icon="el-icon-close"
|
||||
size="mini"
|
||||
style="margin-left: 5px"
|
||||
@click.stop="deleteFavorite(favorite.id)"></el-button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Location from '../common/Location.vue';
|
||||
import { favoriteRequest } from '../../classes/request';
|
||||
export default {
|
||||
components: { Location },
|
||||
inject: ['showUserDialog', 'userImage', 'userStatusClass', 'API', 'showFavoriteDialog'],
|
||||
props: {
|
||||
favorite: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
hideTooltips: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
shiftHeld: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
group: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
editFavoritesMode: Boolean
|
||||
},
|
||||
methods: {
|
||||
moveFavorite(ref, group, type) {
|
||||
favoriteRequest
|
||||
.deleteFavorite({
|
||||
objectId: ref.id
|
||||
})
|
||||
.then(() => {
|
||||
favoriteRequest.addFavorite({
|
||||
type,
|
||||
favoriteId: ref.id,
|
||||
tags: group.name
|
||||
});
|
||||
});
|
||||
},
|
||||
deleteFavorite(objectId) {
|
||||
favoriteRequest.deleteFavorite({
|
||||
objectId
|
||||
});
|
||||
// FIXME: 메시지 수정
|
||||
// this.$confirm('Continue? Delete Favorite', 'Confirm', {
|
||||
// confirmButtonText: 'Confirm',
|
||||
// cancelButtonText: 'Cancel',
|
||||
// type: 'info',
|
||||
// callback: (action) => {
|
||||
// if (action === 'confirm') {
|
||||
// API.deleteFavorite({
|
||||
// objectId
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,142 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
||||
<div>
|
||||
<el-button size="small" @click="showFriendExportDialog">{{ $t('view.favorite.export') }}</el-button>
|
||||
<el-button size="small" style="margin-left: 5px" @click="showFriendImportDialog">{{
|
||||
$t('view.favorite.import')
|
||||
}}</el-button>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; font-size: 13px; margin-right: 10px">
|
||||
<span class="name" style="margin-right: 5px; line-height: 10px">{{ $t('view.favorite.sort_by') }}</span>
|
||||
<el-radio-group v-model="sortFav" @change="saveSortFavoritesOption">
|
||||
<el-radio :label="false">{{
|
||||
$t('view.settings.appearance.appearance.sort_favorite_by_name')
|
||||
}}</el-radio>
|
||||
<el-radio :label="true">{{
|
||||
$t('view.settings.appearance.appearance.sort_favorite_by_date')
|
||||
}}</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
<span style="display: block; margin-top: 30px">{{ $t('view.favorite.avatars.vrchat_favorites') }}</span>
|
||||
<el-collapse style="border: 0">
|
||||
<el-collapse-item v-for="group in API.favoriteFriendGroups" :key="group.name">
|
||||
<template slot="title">
|
||||
<span
|
||||
style="font-weight: bold; font-size: 14px; margin-left: 10px"
|
||||
v-text="group.displayName"></span>
|
||||
<span style="color: #909399; font-size: 12px; margin-left: 10px"
|
||||
>{{ group.count }}/{{ group.capacity }}</span
|
||||
>
|
||||
<el-tooltip placement="top" :content="$t('view.favorite.rename_tooltip')" :disabled="hideTooltips">
|
||||
<el-button
|
||||
size="mini"
|
||||
icon="el-icon-edit"
|
||||
circle
|
||||
style="margin-left: 10px"
|
||||
@click.stop="changeFavoriteGroupName(group)"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip placement="right" :content="$t('view.favorite.clear_tooltip')" :disabled="hideTooltips">
|
||||
<el-button
|
||||
size="mini"
|
||||
icon="el-icon-delete"
|
||||
circle
|
||||
style="margin-left: 5px"
|
||||
@click.stop="clearFavoriteGroup(group)"></el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<div v-if="group.count" class="x-friend-list" style="margin-top: 10px">
|
||||
<FavoritesFriendItem
|
||||
v-for="favorite in groupedByGroupKeyFavoriteFriends[group.key]"
|
||||
:key="favorite.id"
|
||||
style="display: inline-block; width: 300px; margin-right: 15px"
|
||||
:favorite="favorite"
|
||||
:edit-favorites-mode="editFavoritesMode"
|
||||
:group="group"
|
||||
@click="showUserDialog(favorite.id)" />
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
style="
|
||||
padding-top: 25px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgb(144, 147, 153);
|
||||
">
|
||||
<span>No Data</span>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
<FriendExportDialog
|
||||
:friend-export-dialog-visible.sync="friendExportDialogVisible"
|
||||
:favorite-friends="favoriteFriends" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FavoritesFriendItem from './FavoritesFriendItem.vue';
|
||||
import FriendExportDialog from '../../views/dialogs/favorites/FriendExportDialog.vue';
|
||||
import { favoriteRequest } from '../../classes/request';
|
||||
|
||||
export default {
|
||||
name: 'FavoritesFriendTab',
|
||||
components: { FriendExportDialog, FavoritesFriendItem },
|
||||
inject: ['showUserDialog', 'API'],
|
||||
props: {
|
||||
favoriteFriends: Array,
|
||||
sortFavorites: Boolean,
|
||||
hideTooltips: Boolean,
|
||||
groupedByGroupKeyFavoriteFriends: Object,
|
||||
editFavoritesMode: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
friendExportDialogVisible: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
sortFav: {
|
||||
get() {
|
||||
return this.sortFavorites;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:sort-favorites', value);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showFriendExportDialog() {
|
||||
this.friendExportDialogVisible = true;
|
||||
},
|
||||
showFriendImportDialog() {
|
||||
this.$emit('show-friend-import-dialog');
|
||||
},
|
||||
saveSortFavoritesOption() {
|
||||
this.$emit('save-sort-favorites-option');
|
||||
},
|
||||
|
||||
clearFavoriteGroup(ctx) {
|
||||
// FIXME: 메시지 수정
|
||||
this.$confirm('Continue? Clear Group', 'Confirm', {
|
||||
confirmButtonText: 'Confirm',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'info',
|
||||
callback: (action) => {
|
||||
if (action === 'confirm') {
|
||||
favoriteRequest.clearFavoriteGroup({
|
||||
type: ctx.type,
|
||||
group: ctx.name
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
changeFavoriteGroupName(group) {
|
||||
this.$emit('change-favorite-group-name', group);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,231 +0,0 @@
|
||||
<template>
|
||||
<div @click="$emit('click')" :style="{ display: 'inline-block', width: '300px', marginRight: '15px' }">
|
||||
<div class="x-friend-item">
|
||||
<template v-if="isLocalFavorite ? favorite.name : favorite.ref">
|
||||
<div class="avatar">
|
||||
<img v-lazy="smallThumbnail" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span class="name">{{ localFavFakeRef.name }}</span>
|
||||
<span v-if="localFavFakeRef.occupants" class="extra"
|
||||
>{{ localFavFakeRef.authorName }} ({{ localFavFakeRef.occupants }})</span
|
||||
>
|
||||
<span v-else class="extra">{{ localFavFakeRef.authorName }}</span>
|
||||
</div>
|
||||
<template v-if="editFavoritesMode">
|
||||
<el-dropdown trigger="click" size="mini" style="margin-left: 5px" @click.native.stop>
|
||||
<el-tooltip
|
||||
placement="left"
|
||||
:content="$t(localFavFakeRef ? 'view.favorite.copy_tooltip' : 'view.favorite.move_tooltip')"
|
||||
:disabled="hideTooltips">
|
||||
<el-button type="default" icon="el-icon-back" size="mini" circle></el-button>
|
||||
</el-tooltip>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<template v-for="groupAPI in API.favoriteWorldGroups">
|
||||
<el-dropdown-item
|
||||
v-if="isLocalFavorite || groupAPI.name !== group.name"
|
||||
:key="groupAPI.name"
|
||||
style="display: block; margin: 10px 0"
|
||||
:disabled="groupAPI.count >= groupAPI.capacity"
|
||||
@click.native="handleDropdownItemClick(groupAPI)">
|
||||
{{ groupAPI.displayName }} ({{ groupAPI.count }} / {{ groupAPI.capacity }})
|
||||
</el-dropdown-item>
|
||||
</template>
|
||||
</el-dropdown-menu>
|
||||
|
||||
<el-button v-if="!isLocalFavorite" type="text" size="mini" @click.stop style="margin-left: 5px">
|
||||
<el-checkbox v-model="isSelected"></el-checkbox>
|
||||
</el-button>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-tooltip
|
||||
v-if="!isLocalFavorite && favorite.deleted"
|
||||
placement="left"
|
||||
:content="$t('view.favorite.unavailable_tooltip')">
|
||||
<i class="el-icon-warning" style="color: #f56c6c; margin-left: 5px"></i>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
v-if="!isLocalFavorite && favorite.ref.releaseStatus === 'private'"
|
||||
placement="left"
|
||||
:content="$t('view.favorite.private')">
|
||||
<i class="el-icon-warning" style="color: #e6a23c; margin-left: 5px"></i>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
placement="left"
|
||||
:content="$t('view.favorite.self_invite_tooltip')"
|
||||
:disabled="hideTooltips">
|
||||
<el-button
|
||||
size="mini"
|
||||
icon="el-icon-message"
|
||||
style="margin-left: 5px"
|
||||
@click.stop="$emit('new-instance-self-invite', favorite.id)"
|
||||
circle></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
v-if="!isLocalFavorite"
|
||||
placement="right"
|
||||
:content="$t('view.favorite.unfavorite_tooltip')"
|
||||
:disabled="hideTooltips">
|
||||
<el-button
|
||||
v-if="shiftHeld"
|
||||
size="mini"
|
||||
icon="el-icon-close"
|
||||
circle
|
||||
style="color: #f56c6c; margin-left: 5px"
|
||||
@click.stop="deleteFavorite(favorite.id)"></el-button>
|
||||
<el-button
|
||||
v-else
|
||||
icon="el-icon-star-on"
|
||||
size="mini"
|
||||
circle
|
||||
style="margin-left: 5px"
|
||||
type="default"
|
||||
@click.stop="showFavoriteDialog('world', favorite.id)"></el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-tooltip
|
||||
v-if="isLocalFavorite"
|
||||
placement="right"
|
||||
:content="$t('view.favorite.unfavorite_tooltip')"
|
||||
:disabled="hideTooltips">
|
||||
<el-button
|
||||
v-if="shiftHeld"
|
||||
size="mini"
|
||||
icon="el-icon-close"
|
||||
circle
|
||||
style="color: #f56c6c; margin-left: 5px"
|
||||
@click.stop="$emit('remove-local-world-favorite', favorite.id, group)"></el-button>
|
||||
<el-button
|
||||
v-else
|
||||
icon="el-icon-star-on"
|
||||
size="mini"
|
||||
circle
|
||||
style="margin-left: 5px"
|
||||
type="default"
|
||||
@click.stop="showFavoriteDialog('world', favorite.id)"></el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="avatar"></div>
|
||||
<div class="detail">
|
||||
<span>{{ favorite.name || favorite.id }}</span>
|
||||
<el-tooltip
|
||||
v-if="!isLocalFavorite && favorite.deleted"
|
||||
placement="left"
|
||||
:content="$t('view.favorite.unavailable_tooltip')">
|
||||
<i class="el-icon-warning" style="color: #f56c6c; margin-left: 5px"></i>
|
||||
</el-tooltip>
|
||||
<el-button
|
||||
type="text"
|
||||
icon="el-icon-close"
|
||||
size="mini"
|
||||
style="margin-left: 5px"
|
||||
@click.stop="handleDeleteFavorite"></el-button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { favoriteRequest } from '../../classes/request';
|
||||
|
||||
export default {
|
||||
name: 'FavoritesWorldItem',
|
||||
inject: ['API', 'showFavoriteDialog'],
|
||||
props: {
|
||||
group: [Object, String],
|
||||
favorite: Object,
|
||||
editFavoritesMode: Boolean,
|
||||
hideTooltips: Boolean,
|
||||
shiftHeld: Boolean,
|
||||
isLocalFavorite: { type: Boolean, required: false }
|
||||
},
|
||||
computed: {
|
||||
isSelected: {
|
||||
get() {
|
||||
return this.favorite.$selected;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('handle-select', value);
|
||||
}
|
||||
},
|
||||
localFavFakeRef() {
|
||||
// local favorite no "ref" property
|
||||
return this.isLocalFavorite ? this.favorite : this.favorite.ref;
|
||||
},
|
||||
smallThumbnail() {
|
||||
return (
|
||||
this.localFavFakeRef.thumbnailImageUrl.replace('256', '128') ||
|
||||
this.localFavFakeRef.thumbnailImageUrl
|
||||
);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleDropdownItemClick(groupAPI) {
|
||||
if (this.isLocalFavorite) {
|
||||
this.addFavoriteWorld(this.localFavFakeRef, groupAPI, true);
|
||||
} else {
|
||||
this.moveFavorite(this.localFavFakeRef, groupAPI, 'world');
|
||||
}
|
||||
},
|
||||
handleDeleteFavorite() {
|
||||
if (this.isLocalFavorite) {
|
||||
this.$emit('remove-local-world-favorite', this.favorite.id, this.group);
|
||||
} else {
|
||||
this.deleteFavorite(this.favorite.id);
|
||||
}
|
||||
},
|
||||
moveFavorite(ref, group, type) {
|
||||
favoriteRequest
|
||||
.deleteFavorite({
|
||||
objectId: ref.id
|
||||
})
|
||||
.then(() => {
|
||||
favoriteRequest.addFavorite({
|
||||
type,
|
||||
favoriteId: ref.id,
|
||||
tags: group.name
|
||||
});
|
||||
});
|
||||
},
|
||||
deleteFavorite(objectId) {
|
||||
favoriteRequest.deleteFavorite({
|
||||
objectId
|
||||
});
|
||||
// FIXME: 메시지 수정
|
||||
// this.$confirm('Continue? Delete Favorite', 'Confirm', {
|
||||
// confirmButtonText: 'Confirm',
|
||||
// cancelButtonText: 'Cancel',
|
||||
// type: 'info',
|
||||
// callback: (action) => {
|
||||
// if (action === 'confirm') {
|
||||
// API.deleteFavorite({
|
||||
// objectId
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
},
|
||||
addFavoriteWorld(ref, group, message) {
|
||||
// wait API splitting PR Merged
|
||||
return favoriteRequest
|
||||
.addFavorite({
|
||||
type: 'world',
|
||||
favoriteId: ref.id,
|
||||
tags: group.name
|
||||
})
|
||||
.then((args) => {
|
||||
if (message) {
|
||||
this.$message({
|
||||
message: 'World added to favorites',
|
||||
type: 'success'
|
||||
});
|
||||
}
|
||||
return args;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,450 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
||||
<div>
|
||||
<el-button size="small" @click="showExportDialog">{{ $t('view.favorite.export') }}</el-button>
|
||||
<el-button size="small" style="margin-left: 5px" @click="$emit('show-world-import-dialog')">{{
|
||||
$t('view.favorite.import')
|
||||
}}</el-button>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; font-size: 13px; margin-right: 10px">
|
||||
<span class="name" style="margin-right: 5px; line-height: 10px">{{ $t('view.favorite.sort_by') }}</span>
|
||||
<el-radio-group
|
||||
v-model="sortFav"
|
||||
style="margin-right: 12px"
|
||||
@change="$emit('save-sort-favorites-option')">
|
||||
<el-radio :label="false">{{
|
||||
$t('view.settings.appearance.appearance.sort_favorite_by_name')
|
||||
}}</el-radio>
|
||||
<el-radio :label="true">{{
|
||||
$t('view.settings.appearance.appearance.sort_favorite_by_date')
|
||||
}}</el-radio>
|
||||
</el-radio-group>
|
||||
<el-input
|
||||
v-model="worldFavoriteSearch"
|
||||
clearable
|
||||
size="mini"
|
||||
:placeholder="$t('view.favorite.worlds.search')"
|
||||
style="width: 200px"
|
||||
@input="searchWorldFavorites" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="x-friend-list" style="margin-top: 10px">
|
||||
<div
|
||||
v-for="favorite in worldFavoriteSearchResults"
|
||||
:key="favorite.id"
|
||||
style="display: inline-block; width: 300px; margin-right: 15px"
|
||||
@click="showWorldDialog(favorite.id)">
|
||||
<div class="x-friend-item">
|
||||
<template v-if="favorite.name">
|
||||
<div class="avatar">
|
||||
<img v-lazy="favorite.thumbnailImageUrl" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span class="name" v-text="favorite.name"></span>
|
||||
<span v-if="favorite.occupants" class="extra"
|
||||
>{{ favorite.authorName }} ({{ favorite.occupants }})</span
|
||||
>
|
||||
<span v-else class="extra" v-text="favorite.authorName"></span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="avatar"></div>
|
||||
<div class="detail">
|
||||
<span v-text="favorite.id"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span style="display: block; margin-top: 20px">{{ $t('view.favorite.worlds.vrchat_favorites') }}</span>
|
||||
<el-collapse style="border: 0">
|
||||
<el-collapse-item v-for="group in API.favoriteWorldGroups" :key="group.name">
|
||||
<template slot="title">
|
||||
<div style="display: flex; align-items: center">
|
||||
<span
|
||||
style="font-weight: bold; font-size: 14px; margin-left: 10px"
|
||||
v-text="group.displayName" />
|
||||
<el-tag
|
||||
style="margin: 1px 0 0 5px"
|
||||
size="mini"
|
||||
:type="userFavoriteWorldsStatusForFavTab(group.visibility)"
|
||||
effect="plain"
|
||||
>{{ group.visibility.charAt(0).toUpperCase() + group.visibility.slice(1) }}</el-tag
|
||||
>
|
||||
<span style="color: #909399; font-size: 12px; margin-left: 10px"
|
||||
>{{ group.count }}/{{ group.capacity }}</span
|
||||
>
|
||||
<el-dropdown trigger="click" size="mini" style="margin-left: 10px" @click.native.stop>
|
||||
<el-tooltip
|
||||
placement="top"
|
||||
:content="$t('view.favorite.visibility_tooltip')"
|
||||
:disabled="hideTooltips">
|
||||
<el-button type="default" icon="el-icon-view" size="mini" circle />
|
||||
</el-tooltip>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item
|
||||
v-for="visibility in worldGroupVisibilityOptions"
|
||||
v-if="group.visibility !== visibility"
|
||||
:key="visibility"
|
||||
style="display: block; margin: 10px 0"
|
||||
@click.native="changeWorldGroupVisibility(group.name, visibility)"
|
||||
>{{ visibility.charAt(0).toUpperCase() + visibility.slice(1) }}</el-dropdown-item
|
||||
>
|
||||
</el-dropdown-menu>
|
||||
<el-tooltip
|
||||
placement="top"
|
||||
:content="$t('view.favorite.rename_tooltip')"
|
||||
:disabled="hideTooltips">
|
||||
<el-button
|
||||
size="mini"
|
||||
icon="el-icon-edit"
|
||||
circle
|
||||
style="margin-left: 5px"
|
||||
@click.stop="changeFavoriteGroupName(group)" />
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
placement="right"
|
||||
:content="$t('view.favorite.clear_tooltip')"
|
||||
:disabled="hideTooltips">
|
||||
<el-button
|
||||
size="mini"
|
||||
icon="el-icon-delete"
|
||||
circle
|
||||
style="margin-left: 5px"
|
||||
@click.stop="clearFavoriteGroup(group)" />
|
||||
</el-tooltip>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="group.count" class="x-friend-list" style="margin-top: 10px">
|
||||
<FavoritesWorldItem
|
||||
v-for="favorite in groupedByGroupKeyFavoriteWorlds[group.key]"
|
||||
:key="favorite.id"
|
||||
:group="group"
|
||||
:favorite="favorite"
|
||||
:edit-favorites-mode="editFavoritesMode"
|
||||
:hide-tooltips="hideTooltips"
|
||||
:shift-held="shiftHeld"
|
||||
@click="showWorldDialog(favorite.id)"
|
||||
@handle-select="favorite.$selected = $event"
|
||||
@new-instance-self-invite="newInstanceSelfInvite" />
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
style="
|
||||
padding-top: 25px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgb(144, 147, 153);
|
||||
">
|
||||
<span>No Data</span>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
<span style="display: block; margin-top: 20px">{{ $t('view.favorite.worlds.local_favorites') }}</span>
|
||||
<br />
|
||||
<el-button size="small" @click="promptNewLocalWorldFavoriteGroup">{{
|
||||
$t('view.favorite.worlds.new_group')
|
||||
}}</el-button>
|
||||
<el-button
|
||||
v-if="!refreshingLocalFavorites"
|
||||
size="small"
|
||||
style="margin-left: 5px"
|
||||
@click="$emit('refresh-local-world-favorite')"
|
||||
>{{ $t('view.favorite.worlds.refresh') }}</el-button
|
||||
>
|
||||
<el-button v-else size="small" style="margin-left: 5px" @click="refreshingLocalFavorites = false">
|
||||
<i class="el-icon-loading" style="margin-right: 5px" />
|
||||
<span>{{ $t('view.favorite.worlds.cancel_refresh') }}</span>
|
||||
</el-button>
|
||||
<el-collapse style="border: 0">
|
||||
<el-collapse-item v-for="group in localWorldFavoriteGroups" v-if="localWorldFavorites[group]" :key="group">
|
||||
<template slot="title">
|
||||
<span style="font-weight: bold; font-size: 14px; margin-left: 10px" v-text="group" />
|
||||
<span style="color: #909399; font-size: 12px; margin-left: 10px">{{
|
||||
getLocalWorldFavoriteGroupLength(group)
|
||||
}}</span>
|
||||
<el-tooltip placement="top" :content="$t('view.favorite.rename_tooltip')" :disabled="hideTooltips">
|
||||
<el-button
|
||||
size="mini"
|
||||
icon="el-icon-edit"
|
||||
circle
|
||||
style="margin-left: 10px"
|
||||
@click.stop="promptLocalWorldFavoriteGroupRename(group)" />
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
placement="right"
|
||||
:content="$t('view.favorite.delete_tooltip')"
|
||||
:disabled="hideTooltips">
|
||||
<el-button
|
||||
size="mini"
|
||||
icon="el-icon-delete"
|
||||
circle
|
||||
style="margin-left: 5px"
|
||||
@click.stop="promptLocalWorldFavoriteGroupDelete(group)" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<div v-if="localWorldFavorites[group].length" class="x-friend-list" style="margin-top: 10px">
|
||||
<FavoritesWorldItem
|
||||
v-for="favorite in localWorldFavorites[group]"
|
||||
:key="favorite.id"
|
||||
is-local-favorite
|
||||
:group="group"
|
||||
:favorite="favorite"
|
||||
:edit-favorites-mode="editFavoritesMode"
|
||||
:hide-tooltips="hideTooltips"
|
||||
:shift-held="shiftHeld"
|
||||
@click="showWorldDialog(favorite.id)"
|
||||
@new-instance-self-invite="newInstanceSelfInvite"
|
||||
@remove-local-world-favorite="removeLocalWorldFavorite" />
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
style="
|
||||
padding-top: 25px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgb(144, 147, 153);
|
||||
">
|
||||
<span>No Data</span>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
<WorldExportDialog
|
||||
:favorite-worlds="favoriteWorlds"
|
||||
:world-export-dialog-visible.sync="worldExportDialogVisible"
|
||||
:local-world-favorites="localWorldFavorites"
|
||||
:local-world-favorite-groups="localWorldFavoriteGroups"
|
||||
:local-world-favorites-list="localWorldFavoritesList" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FavoritesWorldItem from './FavoritesWorldItem.vue';
|
||||
import WorldExportDialog from '../../views/dialogs/favorites/WorldExportDialog.vue';
|
||||
import { favoriteRequest } from '../../classes/request';
|
||||
|
||||
export default {
|
||||
name: 'FavoritesWorldTab',
|
||||
components: {
|
||||
FavoritesWorldItem,
|
||||
WorldExportDialog
|
||||
},
|
||||
inject: ['API', 'showWorldDialog'],
|
||||
props: {
|
||||
sortFavorites: Boolean,
|
||||
hideTooltips: Boolean,
|
||||
favoriteWorlds: Array,
|
||||
editFavoritesMode: Boolean,
|
||||
shiftHeld: Boolean,
|
||||
refreshingLocalFavorites: Boolean,
|
||||
localWorldFavoriteGroups: Array,
|
||||
localWorldFavorites: Object,
|
||||
localWorldFavoritesList: Array
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
worldGroupVisibilityOptions: ['private', 'friends', 'public'],
|
||||
worldFavoriteSearch: '',
|
||||
worldExportDialogVisible: false,
|
||||
worldFavoriteSearchResults: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
groupedByGroupKeyFavoriteWorlds() {
|
||||
const groupedByGroupKeyFavoriteWorlds = {};
|
||||
|
||||
this.favoriteWorlds.forEach((world) => {
|
||||
if (world.groupKey) {
|
||||
if (!groupedByGroupKeyFavoriteWorlds[world.groupKey]) {
|
||||
groupedByGroupKeyFavoriteWorlds[world.groupKey] = [];
|
||||
}
|
||||
groupedByGroupKeyFavoriteWorlds[world.groupKey].push(world);
|
||||
}
|
||||
});
|
||||
|
||||
return groupedByGroupKeyFavoriteWorlds;
|
||||
},
|
||||
sortFav: {
|
||||
get() {
|
||||
return this.sortFavorites;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:sort-favorites', value);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showExportDialog() {
|
||||
this.worldExportDialogVisible = true;
|
||||
},
|
||||
|
||||
userFavoriteWorldsStatusForFavTab(visibility) {
|
||||
let style = '';
|
||||
if (visibility === 'public') {
|
||||
style = '';
|
||||
} else if (visibility === 'friends') {
|
||||
style = 'success';
|
||||
} else {
|
||||
style = 'info';
|
||||
}
|
||||
return style;
|
||||
},
|
||||
changeWorldGroupVisibility(name, visibility) {
|
||||
const params = {
|
||||
type: 'world',
|
||||
group: name,
|
||||
visibility
|
||||
};
|
||||
favoriteRequest.saveFavoriteGroup(params).then((args) => {
|
||||
this.$message({
|
||||
message: 'Group visibility changed',
|
||||
type: 'success'
|
||||
});
|
||||
return args;
|
||||
});
|
||||
},
|
||||
promptNewLocalWorldFavoriteGroup() {
|
||||
this.$prompt(
|
||||
$t('prompt.new_local_favorite_group.description'),
|
||||
$t('prompt.new_local_favorite_group.header'),
|
||||
{
|
||||
distinguishCancelAndClose: true,
|
||||
confirmButtonText: $t('prompt.new_local_favorite_group.ok'),
|
||||
cancelButtonText: $t('prompt.new_local_favorite_group.cancel'),
|
||||
inputPattern: /\S+/,
|
||||
inputErrorMessage: $t('prompt.new_local_favorite_group.input_error'),
|
||||
callback: (action, instance) => {
|
||||
if (action === 'confirm' && instance.inputValue) {
|
||||
this.$emit('new-local-world-favorite-group', instance.inputValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
promptLocalWorldFavoriteGroupRename(group) {
|
||||
this.$prompt(
|
||||
$t('prompt.local_favorite_group_rename.description'),
|
||||
$t('prompt.local_favorite_group_rename.header'),
|
||||
{
|
||||
distinguishCancelAndClose: true,
|
||||
confirmButtonText: $t('prompt.local_favorite_group_rename.save'),
|
||||
cancelButtonText: $t('prompt.local_favorite_group_rename.cancel'),
|
||||
inputPattern: /\S+/,
|
||||
inputErrorMessage: $t('prompt.local_favorite_group_rename.input_error'),
|
||||
inputValue: group,
|
||||
callback: (action, instance) => {
|
||||
if (action === 'confirm' && instance.inputValue) {
|
||||
this.$emit('rename-local-world-favorite-group', instance.inputValue, group);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
promptLocalWorldFavoriteGroupDelete(group) {
|
||||
this.$confirm(`Delete Group? ${group}`, 'Confirm', {
|
||||
confirmButtonText: 'Confirm',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'info',
|
||||
callback: (action) => {
|
||||
if (action === 'confirm') {
|
||||
this.$emit('delete-local-world-favorite-group', group);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
getLocalWorldFavoriteGroupLength(group) {
|
||||
const favoriteGroup = this.localWorldFavorites[group];
|
||||
if (!favoriteGroup) {
|
||||
return 0;
|
||||
}
|
||||
return favoriteGroup.length;
|
||||
},
|
||||
|
||||
clearFavoriteGroup(ctx) {
|
||||
// FIXME: 메시지 수정
|
||||
this.$confirm('Continue? Clear Group', 'Confirm', {
|
||||
confirmButtonText: 'Confirm',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'info',
|
||||
callback: (action) => {
|
||||
if (action === 'confirm') {
|
||||
favoriteRequest.clearFavoriteGroup({
|
||||
type: ctx.type,
|
||||
group: ctx.name
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
searchWorldFavorites(worldFavoriteSearch) {
|
||||
let ref = null;
|
||||
const search = worldFavoriteSearch.toLowerCase();
|
||||
if (search.length < 3) {
|
||||
this.worldFavoriteSearchResults = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const results = [];
|
||||
for (let i = 0; i < this.localWorldFavoriteGroups.length; ++i) {
|
||||
const group = this.localWorldFavoriteGroups[i];
|
||||
if (!this.localWorldFavorites[group]) {
|
||||
continue;
|
||||
}
|
||||
for (let j = 0; j < this.localWorldFavorites[group].length; ++j) {
|
||||
ref = this.localWorldFavorites[group][j];
|
||||
if (
|
||||
!ref ||
|
||||
typeof ref.id === 'undefined' ||
|
||||
typeof ref.name === 'undefined' ||
|
||||
typeof ref.authorName === 'undefined'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (ref.name.toLowerCase().includes(search) || ref.authorName.toLowerCase().includes(search)) {
|
||||
if (!results.some((r) => r.id === ref.id)) {
|
||||
results.push(ref);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.favoriteWorlds.length; ++i) {
|
||||
ref = this.favoriteWorlds[i].ref;
|
||||
if (
|
||||
!ref ||
|
||||
typeof ref.id === 'undefined' ||
|
||||
typeof ref.name === 'undefined' ||
|
||||
typeof ref.authorName === 'undefined'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (ref.name.toLowerCase().includes(search) || ref.authorName.toLowerCase().includes(search)) {
|
||||
if (!results.some((r) => r.id === ref.id)) {
|
||||
results.push(ref);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.worldFavoriteSearchResults = results;
|
||||
},
|
||||
changeFavoriteGroupName(group) {
|
||||
this.$emit('change-favorite-group-name', group);
|
||||
},
|
||||
newInstanceSelfInvite(event) {
|
||||
this.$emit('new-instance-self-invite', event);
|
||||
},
|
||||
|
||||
removeLocalWorldFavorite(param1, param2) {
|
||||
this.$emit('remove-local-world-favorite', param1, param2);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -1,381 +0,0 @@
|
||||
<template>
|
||||
<div class="x-friend-list" style="padding: 10px 5px">
|
||||
<div
|
||||
class="x-friend-group x-link"
|
||||
style="padding: 0 0 5px"
|
||||
@click="
|
||||
isFriendsGroupMe = !isFriendsGroupMe;
|
||||
saveFriendsGroupStates();
|
||||
">
|
||||
<i class="el-icon-arrow-right" :class="{ rotate: isFriendsGroupMe }"></i>
|
||||
<span style="margin-left: 5px">{{ $t('side_panel.me') }}</span>
|
||||
</div>
|
||||
<div v-show="isFriendsGroupMe">
|
||||
<div class="x-friend-item" @click="showUserDialog(API.currentUser.id)">
|
||||
<div class="avatar" :class="userStatusClass(API.currentUser)">
|
||||
<img v-lazy="userImage(API.currentUser)" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span class="name" :style="{ color: API.currentUser.$userColour }">{{
|
||||
API.currentUser.displayName
|
||||
}}</span>
|
||||
<location
|
||||
v-if="isGameRunning && !gameLogDisabled"
|
||||
class="extra"
|
||||
:location="lastLocation.location"
|
||||
:traveling="lastLocationDestination"
|
||||
:link="false"></location>
|
||||
<location
|
||||
v-else-if="
|
||||
isRealInstance(API.currentUser.$locationTag) ||
|
||||
isRealInstance(API.currentUser.$travelingToLocation)
|
||||
"
|
||||
class="extra"
|
||||
:location="API.currentUser.$locationTag"
|
||||
:traveling="API.currentUser.$travelingToLocation"
|
||||
:link="false">
|
||||
</location>
|
||||
<span v-else class="extra">{{ API.currentUser.statusDescription }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-show="vipFriendsDisplayNumber"
|
||||
class="x-friend-group x-link"
|
||||
@click="
|
||||
isVIPFriends = !isVIPFriends;
|
||||
saveFriendsGroupStates();
|
||||
">
|
||||
<i class="el-icon-arrow-right" :class="{ rotate: isVIPFriends }"></i>
|
||||
<span style="margin-left: 5px">
|
||||
{{ $t('side_panel.favorite') }} ―
|
||||
{{ vipFriendsDisplayNumber }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-show="isVIPFriends">
|
||||
<template v-if="isSidebarDivideByFriendGroup">
|
||||
<div v-for="group in vipFriendsDivideByGroup" :key="group[0].key">
|
||||
<transition name="el-fade-in-linear">
|
||||
<div v-show="group[0].groupName !== ''" style="margin-bottom: 3px">
|
||||
<span class="extra">{{ group[0].groupName }}</span>
|
||||
<span class="extra" style="margin-left: 5px">{{ `(${group.length})` }}</span>
|
||||
</div>
|
||||
</transition>
|
||||
<div v-if="group.length" style="margin-bottom: 10px">
|
||||
<friend-item
|
||||
v-for="friend in group"
|
||||
:key="friend.id"
|
||||
:friend="friend"
|
||||
:hide-nicknames="hideNicknames"
|
||||
@click="showUserDialog(friend.id)"
|
||||
@confirm-delete-friend="$emit('confirm-delete-friend', $event)"></friend-item>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<friend-item
|
||||
v-for="friend in vipFriendsByGroupStatus"
|
||||
v-else
|
||||
:key="friend.id"
|
||||
:friend="friend"
|
||||
:hide-nicknames="hideNicknames"
|
||||
@click="showUserDialog(friend.id)"
|
||||
@confirm-delete-friend="$emit('confirm-delete-friend', $event)">
|
||||
</friend-item>
|
||||
</div>
|
||||
|
||||
<template v-if="isSidebarGroupByInstance && friendsInSameInstance.length">
|
||||
<div class="x-friend-group x-link" @click="toggleSwitchGroupByInstanceCollapsed">
|
||||
<i class="el-icon-arrow-right" :class="{ rotate: !isSidebarGroupByInstanceCollapsed }"></i>
|
||||
<span style="margin-left: 5px"
|
||||
>{{ $t('side_panel.same_instance') }} ― {{ friendsInSameInstance.length }}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div v-show="!isSidebarGroupByInstanceCollapsed">
|
||||
<div v-for="friendArr in friendsInSameInstance" :key="friendArr[0].ref.$location.tag">
|
||||
<div style="margin-bottom: 3px">
|
||||
<location
|
||||
class="extra"
|
||||
:location="getFriendsLocations(friendArr)"
|
||||
style="display: inline"></location>
|
||||
<span class="extra" style="margin-left: 5px">{{ `(${friendArr.length})` }}</span>
|
||||
</div>
|
||||
<div v-if="friendArr && friendArr.length">
|
||||
<friend-item
|
||||
v-for="(friend, idx) in friendArr"
|
||||
:key="friend.id"
|
||||
:friend="friend"
|
||||
is-group-by-instance
|
||||
:style="{ 'margin-bottom': idx === friendArr.length - 1 ? '5px' : undefined }"
|
||||
@click="showUserDialog(friend.id)"
|
||||
@confirm-delete-friend="$emit('confirm-delete-friend', $event)">
|
||||
</friend-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div
|
||||
v-show="onlineFriendsByGroupStatus.length"
|
||||
class="x-friend-group x-link"
|
||||
@click="
|
||||
isOnlineFriends = !isOnlineFriends;
|
||||
saveFriendsGroupStates();
|
||||
">
|
||||
<i class="el-icon-arrow-right" :class="{ rotate: isOnlineFriends }"></i>
|
||||
<span style="margin-left: 5px"
|
||||
>{{ $t('side_panel.online') }} ― {{ onlineFriendsByGroupStatus.length }}</span
|
||||
>
|
||||
</div>
|
||||
<div v-show="isOnlineFriends">
|
||||
<friend-item
|
||||
v-for="friend in onlineFriendsByGroupStatus"
|
||||
:key="friend.id"
|
||||
:friend="friend"
|
||||
:hide-nicknames="hideNicknames"
|
||||
@click="showUserDialog(friend.id)"
|
||||
@confirm-delete-friend="$emit('confirm-delete-friend', $event)" />
|
||||
</div>
|
||||
<div
|
||||
v-show="activeFriends.length"
|
||||
class="x-friend-group x-link"
|
||||
@click="
|
||||
isActiveFriends = !isActiveFriends;
|
||||
saveFriendsGroupStates();
|
||||
">
|
||||
<i class="el-icon-arrow-right" :class="{ rotate: isActiveFriends }"></i>
|
||||
<span style="margin-left: 5px">{{ $t('side_panel.active') }} ― {{ activeFriends.length }}</span>
|
||||
</div>
|
||||
<div v-show="isActiveFriends">
|
||||
<friend-item
|
||||
v-for="friend in activeFriends"
|
||||
:key="friend.id"
|
||||
:friend="friend"
|
||||
:hide-nicknames="hideNicknames"
|
||||
@click="showUserDialog(friend.id)"
|
||||
@confirm-delete-friend="$emit('confirm-delete-friend', $event)"></friend-item>
|
||||
</div>
|
||||
<div
|
||||
v-show="offlineFriends.length"
|
||||
class="x-friend-group x-link"
|
||||
@click="
|
||||
isOfflineFriends = !isOfflineFriends;
|
||||
saveFriendsGroupStates();
|
||||
">
|
||||
<i class="el-icon-arrow-right" :class="{ rotate: isOfflineFriends }"></i>
|
||||
<span style="margin-left: 5px">{{ $t('side_panel.offline') }} ― {{ offlineFriends.length }}</span>
|
||||
</div>
|
||||
<div v-show="isOfflineFriends">
|
||||
<friend-item
|
||||
v-for="friend in offlineFriends"
|
||||
:key="friend.id"
|
||||
:friend="friend"
|
||||
:hide-nicknames="hideNicknames"
|
||||
@click="showUserDialog(friend.id)"
|
||||
@confirm-delete-friend="$emit('confirm-delete-friend', $event)"></friend-item>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FriendItem from './FriendItem.vue';
|
||||
import Location from '../common/Location.vue';
|
||||
import configRepository from '../../repository/config';
|
||||
import utils from '../../classes/utils';
|
||||
|
||||
export default {
|
||||
name: 'FriendsSidebar',
|
||||
components: {
|
||||
FriendItem,
|
||||
Location
|
||||
},
|
||||
inject: ['API', 'showUserDialog', 'userImage', 'userStatusClass'],
|
||||
props: {
|
||||
// settings
|
||||
isGameRunning: Boolean,
|
||||
|
||||
isSidebarDivideByFriendGroup: Boolean,
|
||||
isSidebarGroupByInstance: Boolean,
|
||||
gameLogDisabled: Boolean,
|
||||
hideNicknames: Boolean,
|
||||
isHideFriendsInSameInstance: Boolean,
|
||||
|
||||
lastLocation: Object,
|
||||
lastLocationDestination: String,
|
||||
|
||||
activeFriends: Array,
|
||||
offlineFriends: Array,
|
||||
|
||||
vipFriends: Array,
|
||||
onlineFriends: Array,
|
||||
|
||||
groupedByGroupKeyFavoriteFriends: Object
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isFriendsGroupMe: true,
|
||||
isVIPFriends: true,
|
||||
isOnlineFriends: true,
|
||||
isActiveFriends: true,
|
||||
isOfflineFriends: true,
|
||||
isSidebarGroupByInstanceCollapsed: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
friendsInSameInstance() {
|
||||
const friendsList = {};
|
||||
|
||||
const allFriends = [...this.vipFriends, ...this.onlineFriends];
|
||||
allFriends.forEach((friend) => {
|
||||
let locationTag;
|
||||
|
||||
if (friend.ref?.$location.isRealInstance) {
|
||||
locationTag = friend.ref.$location.tag;
|
||||
} else if (this.lastLocation.friendList.has(friend.id)) {
|
||||
let $location = utils.parseLocation(this.lastLocation.location);
|
||||
if ($location.isRealInstance) {
|
||||
if ($location.tag === 'private') {
|
||||
locationTag = this.lastLocation.name;
|
||||
} else {
|
||||
locationTag = $location.tag;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!locationTag) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!friendsList[locationTag]) {
|
||||
friendsList[locationTag] = [];
|
||||
}
|
||||
friendsList[locationTag].push(friend);
|
||||
});
|
||||
|
||||
const sortedFriendsList = [];
|
||||
for (const group of Object.values(friendsList)) {
|
||||
if (group.length > 1) {
|
||||
sortedFriendsList.push(group.sort((a, b) => a.ref?.$location_at - b.ref?.$location_at));
|
||||
}
|
||||
}
|
||||
|
||||
return sortedFriendsList.sort((a, b) => b.length - a.length);
|
||||
},
|
||||
onlineFriendsByGroupStatus() {
|
||||
if (
|
||||
!this.isSidebarGroupByInstance ||
|
||||
(this.isSidebarGroupByInstance && !this.isHideFriendsInSameInstance)
|
||||
) {
|
||||
return this.onlineFriends;
|
||||
}
|
||||
|
||||
const sameInstanceTag = new Set(
|
||||
this.friendsInSameInstance.flatMap((item) => item.map((friend) => friend.ref?.$location.tag))
|
||||
);
|
||||
|
||||
return this.onlineFriends.filter((item) => !sameInstanceTag.has(item.ref?.$location.tag));
|
||||
},
|
||||
vipFriendsByGroupStatus() {
|
||||
if (
|
||||
!this.isSidebarGroupByInstance ||
|
||||
(this.isSidebarGroupByInstance && !this.isHideFriendsInSameInstance)
|
||||
) {
|
||||
return this.vipFriends;
|
||||
}
|
||||
|
||||
const sameInstanceTag = new Set(
|
||||
this.friendsInSameInstance.flatMap((item) => item.map((friend) => friend.ref?.$location.tag))
|
||||
);
|
||||
|
||||
return this.vipFriends.filter((item) => !sameInstanceTag.has(item.ref?.$location.tag));
|
||||
},
|
||||
// VIP friends divide by group
|
||||
vipFriendsDivideByGroup() {
|
||||
const vipFriendsByGroup = { ...this.groupedByGroupKeyFavoriteFriends };
|
||||
const result = [];
|
||||
|
||||
for (const key in vipFriendsByGroup) {
|
||||
if (Object.hasOwn(vipFriendsByGroup, key)) {
|
||||
const groupFriends = vipFriendsByGroup[key];
|
||||
// sort groupFriends using the order of vipFriends
|
||||
// avoid unnecessary sorting
|
||||
let filteredFriends = this.vipFriends.filter((friend) =>
|
||||
groupFriends.some((item) => item.id === friend.id)
|
||||
);
|
||||
|
||||
if (filteredFriends.length > 0) {
|
||||
const groupName =
|
||||
this.API.favoriteFriendGroups.find((item) => item.key === key)?.displayName || '';
|
||||
result.push(filteredFriends.map((item) => ({ groupName, key, ...item })));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.sort((a, b) => a[0].key.localeCompare(b[0].key));
|
||||
},
|
||||
vipFriendsDisplayNumber() {
|
||||
return this.isSidebarDivideByFriendGroup
|
||||
? this.vipFriendsDivideByGroup.length
|
||||
: this.vipFriendsByGroupStatus.length;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadFriendsGroupStates();
|
||||
},
|
||||
methods: {
|
||||
saveFriendsGroupStates() {
|
||||
configRepository.setBool('VRCX_isFriendsGroupMe', this.isFriendsGroupMe);
|
||||
configRepository.setBool('VRCX_isFriendsGroupFavorites', this.isVIPFriends);
|
||||
configRepository.setBool('VRCX_isFriendsGroupOnline', this.isOnlineFriends);
|
||||
configRepository.setBool('VRCX_isFriendsGroupActive', this.isActiveFriends);
|
||||
configRepository.setBool('VRCX_isFriendsGroupOffline', this.isOfflineFriends);
|
||||
},
|
||||
async loadFriendsGroupStates() {
|
||||
this.isFriendsGroupMe = await configRepository.getBool('VRCX_isFriendsGroupMe', true);
|
||||
this.isVIPFriends = await configRepository.getBool('VRCX_isFriendsGroupFavorites', true);
|
||||
this.isOnlineFriends = await configRepository.getBool('VRCX_isFriendsGroupOnline', true);
|
||||
this.isActiveFriends = await configRepository.getBool('VRCX_isFriendsGroupActive', false);
|
||||
this.isOfflineFriends = await configRepository.getBool('VRCX_isFriendsGroupOffline', false);
|
||||
this.isSidebarGroupByInstanceCollapsed = await configRepository.getBool(
|
||||
'VRCX_sidebarGroupByInstanceCollapsed',
|
||||
false
|
||||
);
|
||||
},
|
||||
isRealInstance(locationTag) {
|
||||
return utils.isRealInstance(locationTag);
|
||||
},
|
||||
toggleSwitchGroupByInstanceCollapsed() {
|
||||
this.isSidebarGroupByInstanceCollapsed = !this.isSidebarGroupByInstanceCollapsed;
|
||||
configRepository.setBool(
|
||||
'VRCX_sidebarGroupByInstanceCollapsed',
|
||||
this.isSidebarGroupByInstanceCollapsed
|
||||
);
|
||||
},
|
||||
getFriendsLocations(friendsArr) {
|
||||
// prevent the instance title display as "Traveling".
|
||||
if (!friendsArr?.length) {
|
||||
return '';
|
||||
}
|
||||
for (const friend of friendsArr) {
|
||||
if (friend.ref?.location !== 'traveling') {
|
||||
return friend.ref.location;
|
||||
}
|
||||
if (utils.isRealInstance(friend.ref?.travelingToLocation)) {
|
||||
return friend.ref.travelingToLocation;
|
||||
}
|
||||
if (this.lastLocation.friendList.has(friend.id)) {
|
||||
return this.lastLocation.name;
|
||||
}
|
||||
}
|
||||
return friendsArr[0].ref?.location;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.x-link:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
.x-link:hover span {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
@@ -1,136 +0,0 @@
|
||||
<template>
|
||||
<div class="x-friend-list" style="padding: 10px 5px">
|
||||
<template v-for="(group, index) in groupedGroupInstances">
|
||||
<div
|
||||
:key="getGroupId(group)"
|
||||
class="x-friend-group x-link"
|
||||
:style="{ paddingTop: index === 0 ? '0px' : '10px' }">
|
||||
<div @click="toggleGroupSidebarCollapse(getGroupId(group))" style="display: flex; align-items: center">
|
||||
<i
|
||||
class="el-icon-arrow-right"
|
||||
:style="{
|
||||
transform: groupInstancesCfg[getGroupId(group)].isCollapsed ? '' : 'rotate(90deg)',
|
||||
transition: 'transform 0.3s'
|
||||
}"></i>
|
||||
<span style="margin-left: 5px">{{ group[0].group.name }} – {{ group.length }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="!groupInstancesCfg[getGroupId(group)].isCollapsed">
|
||||
<div
|
||||
v-for="ref in group"
|
||||
:key="ref.instance.id"
|
||||
class="x-friend-item"
|
||||
@click="$emit('show-group-dialog', ref.instance.ownerId)">
|
||||
<template v-if="isAgeGatedInstancesVisible || !(ref.ageGate || ref.location?.includes('~ageGate'))">
|
||||
<div class="avatar">
|
||||
<img v-lazy="getSmallGroupIconUrl(ref.group.iconUrl)" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span class="name">
|
||||
<span v-text="ref.group.name"></span>
|
||||
<span style="font-weight: normal; margin-left: 5px"
|
||||
>({{ ref.instance.userCount }}/{{ ref.instance.capacity }})</span
|
||||
>
|
||||
</span>
|
||||
<location class="extra" :location="ref.instance.location" :link="false" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Location from '../common/Location.vue';
|
||||
import utils from '../../classes/utils';
|
||||
|
||||
export default {
|
||||
name: 'GroupsSidebar',
|
||||
components: {
|
||||
Location
|
||||
},
|
||||
props: {
|
||||
groupInstances: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
groupOrder: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
isAgeGatedInstancesVisible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// temporary, sort feat not yet done
|
||||
// may be the data structure to be changed
|
||||
groupInstancesCfg: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
groupedGroupInstances() {
|
||||
const groupMap = new Map();
|
||||
|
||||
this.groupInstances.forEach((ref) => {
|
||||
const groupId = ref.group.groupId;
|
||||
if (!groupMap.has(groupId)) {
|
||||
groupMap.set(groupId, []);
|
||||
}
|
||||
groupMap.get(groupId).push(ref);
|
||||
|
||||
if (!this.groupInstancesCfg[ref.group?.groupId]) {
|
||||
this.groupInstancesCfg = {
|
||||
[ref.group.groupId]: {
|
||||
isCollapsed: false
|
||||
},
|
||||
...this.groupInstancesCfg
|
||||
};
|
||||
}
|
||||
});
|
||||
return Array.from(groupMap.values()).sort(this.sortGroupInstancesByInGame);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getSmallGroupIconUrl(url) {
|
||||
return utils.convertFileUrlToImageUrl(url);
|
||||
},
|
||||
toggleGroupSidebarCollapse(groupId) {
|
||||
this.groupInstancesCfg[groupId].isCollapsed = !this.groupInstancesCfg[groupId].isCollapsed;
|
||||
},
|
||||
showGroupDialog(ownerId) {
|
||||
this.$emit('show-group-dialog', ownerId);
|
||||
},
|
||||
getGroupId(group) {
|
||||
return group[0]?.group?.groupId || '';
|
||||
},
|
||||
sortGroupInstancesByInGame(a, b) {
|
||||
var aIndex = this.groupOrder.indexOf(a[0]?.group?.id);
|
||||
var bIndex = this.groupOrder.indexOf(b[0]?.group?.id);
|
||||
if (aIndex === -1 && bIndex === -1) {
|
||||
return 0;
|
||||
}
|
||||
if (aIndex === -1) {
|
||||
return 1;
|
||||
}
|
||||
if (bIndex === -1) {
|
||||
return -1;
|
||||
}
|
||||
return aIndex - bIndex;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.x-link:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
.x-link:hover span {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user