mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-05-01 04:33:46 +02:00
refactor: app.js (#1291)
* refactor: frontend * Fix avatar gallery sort * Update .NET dependencies * Update npm dependencies electron v37.1.0 * bulkRefreshFriends * fix dark theme * Remove crowdin * Fix config.json dialog not updating * VRCX log file fixes & add Cef log * Remove SharedVariable, fix startup * Revert init theme change * Logging date not working? Fix WinformThemer designer error * Add Cef request hander, no more escaping main page * clean * fix * fix * clean * uh * Apply thememode at startup, fixes random user colours * Split database into files * Instance info remove empty lines * Open external VRC links with VRCX * Electron fixes * fix userdialog style * ohhhh * fix store * fix store * fix: load all group members after kicking a user * fix: world dialog favorite button style * fix: Clear VRCX Cache Timer input value * clean * Fix VR overlay * Fix VR overlay 2 * Fix Discord discord rich presence for RPC worlds * Clean up age verified user tags * Fix playerList being occupied after program reload * no `this` * Fix login stuck loading * writable: false * Hide dialogs on logout * add flush sync option * rm LOGIN event * rm LOGOUT event * remove duplicate event listeners * remove duplicate event listeners * clean * remove duplicate event listeners * clean * fix theme style * fix t * clearable * clean * fix ipcEvent * Small changes * Popcorn Palace support * Remove checkActiveFriends * Clean up * Fix dragEnterCef * Block API requests when not logged in * Clear state on login & logout * Fix worldDialog instances not updating * use <script setup> * Fix avatar change event, CheckGameRunning at startup * Fix image dragging * fix * Remove PWI * fix updateLoop * add webpack-dev-server to dev environment * rm unnecessary chunks * use <script setup> * webpack-dev-server changes * use <script setup> * use <script setup> * Fix UGC text size * Split login event * t * use <script setup> * fix * Update .gitignore and enable checkJs in jsconfig * fix i18n t * use <script setup> * use <script setup> * clean * global types * fix * use checkJs for debugging * Add watchState for login watchers * fix .vue template * type fixes * rm Vue.filter * Cef v138.0.170, VC++ 2022 * Settings fixes * Remove 'USER:CURRENT' * clean up 2FA callbacks * remove userApply * rm i18n import * notification handling to use notification store methods * refactor favorite handling to use favorite store methods and clean up event emissions * refactor moderation handling to use dedicated functions for player moderation events * refactor friend handling to use dedicated functions for friend events * Fix program startup, move lang init * Fix friend state * Fix status change error * Fix user notes diff * fix * rm group event * rm auth event * rm avatar event * clean * clean * getUser * getFriends * getFavoriteWorlds, getFavoriteAvatars * AvatarGalleryUpload btn style & package.json update * Fix friend requests * Apply user * Apply world * Fix note diff * Fix VR overlay * Fixes * Update build scripts * Apply avatar * Apply instance * Apply group * update hidden VRC+ badge * Fix sameInstance "private" * fix 502/504 API errors * fix 502/504 API errors * clean * Fix friend in same instance on orange showing twice in friends list * Add back in broken friend state repair methods * add types --------- Co-authored-by: Natsumi <cmcooper123@hotmail.com>
This commit is contained in:
@@ -1,32 +1,23 @@
|
||||
<template>
|
||||
<div id="chart" class="x-container">
|
||||
<div id="chart" class="x-container" v-show="menuActiveIndex === 'charts'">
|
||||
<div class="options-container" style="margin-top: 0">
|
||||
<span class="header">{{ $t('view.charts.header') }}</span>
|
||||
<span class="header">{{ t('view.charts.header') }}</span>
|
||||
</div>
|
||||
<InstanceActivity
|
||||
:get-world-name="getWorldName"
|
||||
:is-dark-mode="isDarkMode"
|
||||
:dt-hour12="dtHour12"
|
||||
:friends-map="friendsMap"
|
||||
:local-favorite-friends="localFavoriteFriends"
|
||||
@open-previous-instance-info-dialog="$emit('open-previous-instance-info-dialog', $event)" />
|
||||
<el-backtop target="#chart" :right="30" :bottom="30"></el-backtop>
|
||||
<keep-alive>
|
||||
<InstanceActivity v-if="menuActiveIndex === 'charts'" />
|
||||
<el-backtop target="#chart" :right="30" :bottom="30"></el-backtop>
|
||||
</keep-alive>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import InstanceActivity from './components/InstanceActivity.vue';
|
||||
export default {
|
||||
name: 'ChartsTab',
|
||||
components: {
|
||||
InstanceActivity
|
||||
},
|
||||
props: {
|
||||
getWorldName: Function,
|
||||
isDarkMode: Boolean,
|
||||
dtHour12: Boolean,
|
||||
friendsMap: Map,
|
||||
localFavoriteFriends: Set
|
||||
}
|
||||
};
|
||||
import { useUiStore } from '../../stores';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const uiStore = useUiStore();
|
||||
const { menuActiveIndex } = storeToRefs(uiStore);
|
||||
</script>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,336 +2,332 @@
|
||||
<div style="width: 100%">
|
||||
<div style="height: 25px; margin-top: 60px">
|
||||
<transition name="el-fade-in-linear">
|
||||
<location
|
||||
<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>
|
||||
:location="activityDetailData[0]?.location"
|
||||
is-open-previous-instance-info-dialog />
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<div ref="activityDetailChart"></div>
|
||||
<div ref="activityDetailChartRef"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { ref, watch, computed, onDeactivated, onMounted } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import utils from '../../../classes/utils';
|
||||
import Location from '../../../components/Location.vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
export default {
|
||||
name: 'InstanceActivityDetail',
|
||||
components: {
|
||||
Location
|
||||
import { loadEcharts, timeToText } from '../../../shared/utils';
|
||||
import { useUserStore, useAppearanceSettingsStore } from '../../../stores';
|
||||
|
||||
const { isDarkMode, dtHour12 } = storeToRefs(useAppearanceSettingsStore());
|
||||
const { showUserDialog } = useUserStore();
|
||||
const { currentUser } = storeToRefs(useUserStore());
|
||||
|
||||
const props = defineProps({
|
||||
activityDetailData: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
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
|
||||
barWidth: {
|
||||
type: Number,
|
||||
required: true,
|
||||
default: 10
|
||||
}
|
||||
});
|
||||
|
||||
const activityDetailChartRef = ref(null);
|
||||
|
||||
const echarts = ref(null);
|
||||
const isLoading = ref(true);
|
||||
const echartsInstance = ref(null);
|
||||
const usersFirstActivity = ref(null);
|
||||
const resizeObserver = ref(null);
|
||||
|
||||
const startTimeStamp = computed(() => {
|
||||
return props.activityDetailData.find((item) => item.user_id === currentUser.value.id)?.joinTime.valueOf();
|
||||
});
|
||||
|
||||
const endTimeStamp = computed(() => {
|
||||
return props.activityDetailData.find((item) => item.user_id === currentUser.value.id)?.leaveTime.valueOf();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => isDarkMode.value,
|
||||
() => {
|
||||
if (echartsInstance.value) {
|
||||
echartsInstance.value.dispose();
|
||||
echartsInstance.value = null;
|
||||
initEcharts();
|
||||
}
|
||||
},
|
||||
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(
|
||||
() => dtHour12.value,
|
||||
() => {
|
||||
if (echartsInstance.value) {
|
||||
initEcharts();
|
||||
}
|
||||
},
|
||||
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();
|
||||
}
|
||||
initResizeObserver();
|
||||
|
||||
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);
|
||||
}
|
||||
onMounted(() => {
|
||||
initEcharts(true);
|
||||
});
|
||||
|
||||
this.echartsInstance.resize({
|
||||
height: chartsHeight,
|
||||
onDeactivated(() => {
|
||||
// prevent switch tab play resize animation
|
||||
resizeObserver.value.disconnect();
|
||||
});
|
||||
|
||||
function initResizeObserver() {
|
||||
resizeObserver.value = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
echartsInstance.value.resize({
|
||||
width: entry.contentRect.width,
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function initEcharts(isFirstLoad = false) {
|
||||
if (!echarts.value) {
|
||||
echarts.value = await loadEcharts();
|
||||
}
|
||||
};
|
||||
|
||||
const chartsHeight = props.activityDetailData.length * (props.barWidth + 10) + 200;
|
||||
const chartDom = activityDetailChartRef.value;
|
||||
if (!echartsInstance.value) {
|
||||
echartsInstance.value = echarts.value.init(chartDom, `${isDarkMode.value ? 'dark' : null}`, {
|
||||
height: chartsHeight,
|
||||
useDirtyRect: props.activityDetailData.length > 80
|
||||
});
|
||||
resizeObserver.value.observe(chartDom);
|
||||
}
|
||||
|
||||
echartsInstance.value.resize({
|
||||
height: chartsHeight,
|
||||
animation: {
|
||||
duration: 300
|
||||
}
|
||||
});
|
||||
|
||||
echartsInstance.value.setOption(isFirstLoad ? {} : getNewOption(), { lazyUpdate: true });
|
||||
echartsInstance.value.on('click', 'yAxis', handleClickYAxisLabel);
|
||||
|
||||
setTimeout(() => {
|
||||
isLoading.value = false;
|
||||
}, 200);
|
||||
}
|
||||
|
||||
function handleClickYAxisLabel(params) {
|
||||
const userData = usersFirstActivity.value[params.dataIndex];
|
||||
if (userData?.user_id) {
|
||||
showUserDialog(userData.user_id);
|
||||
}
|
||||
}
|
||||
|
||||
function 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 props.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() - startTimeStamp.value
|
||||
: entry.joinTime.valueOf() - startTimeStamp.value - 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);
|
||||
}
|
||||
usersFirstActivity.value = 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: props.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 = props.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 = props.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 = dtHour12.value ? 'hh:mm:ss A' : 'HH:mm:ss';
|
||||
const formattedLeftDateTime = dayjs(instanceData.leaveTime).format(format);
|
||||
const formattedJoinDateTime = dayjs(instanceData.joinTime).format(format);
|
||||
|
||||
const timeString = 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 = dtHour12.value ? '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: endTimeStamp.value - startTimeStamp.value,
|
||||
axisLine: { show: true },
|
||||
axisLabel: {
|
||||
formatter: (value) => dayjs(value + startTimeStamp.value).format(format)
|
||||
},
|
||||
splitLine: { lineStyle: { type: 'dashed' } }
|
||||
},
|
||||
series: generateSeries(),
|
||||
backgroundColor: 'rgba(0, 0, 0, 0)'
|
||||
};
|
||||
|
||||
return echartsOption;
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
echartsInstance,
|
||||
initEcharts
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -2,317 +2,222 @@
|
||||
<div v-show="menuActiveIndex === 'favorite'" class="x-container">
|
||||
<div style="font-size: 13px; position: absolute; display: flex; right: 0; z-index: 1; margin-right: 15px">
|
||||
<div v-if="editFavoritesMode" style="display: inline-block; margin-right: 10px">
|
||||
<el-button size="small" @click="clearBulkFavoriteSelection">{{ $t('view.favorite.clear') }}</el-button>
|
||||
<el-button size="small" @click="bulkCopyFavoriteSelection">{{ $t('view.favorite.copy') }}</el-button>
|
||||
<el-button size="small" @click="clearBulkFavoriteSelection">{{ t('view.favorite.clear') }}</el-button>
|
||||
<el-button size="small" @click="bulkCopyFavoriteSelection">{{ t('view.favorite.copy') }}</el-button>
|
||||
<el-button size="small" @click="showBulkUnfavoriteSelectionConfirm">{{
|
||||
$t('view.favorite.bulk_unfavorite')
|
||||
t('view.favorite.bulk_unfavorite')
|
||||
}}</el-button>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; margin-right: 10px">
|
||||
<span class="name">{{ $t('view.favorite.edit_mode') }}</span>
|
||||
<span class="name">{{ t('view.favorite.edit_mode') }}</span>
|
||||
<el-switch v-model="editFavoritesMode" style="margin-left: 5px"></el-switch>
|
||||
</div>
|
||||
<el-tooltip placement="bottom" :content="$t('view.favorite.refresh_tooltip')" :disabled="hideTooltips">
|
||||
<el-tooltip placement="bottom" :content="t('view.favorite.refresh_tooltip')" :disabled="hideTooltips">
|
||||
<el-button
|
||||
type="default"
|
||||
:loading="API.isFavoriteLoading"
|
||||
@click="
|
||||
API.refreshFavorites();
|
||||
getLocalWorldFavorites();
|
||||
"
|
||||
:loading="isFavoriteLoading"
|
||||
size="small"
|
||||
icon="el-icon-refresh"
|
||||
circle></el-button>
|
||||
circle
|
||||
@click="
|
||||
refreshFavorites();
|
||||
getLocalWorldFavorites();
|
||||
"></el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-tabs v-model="currentTabName" v-loading="API.isFavoriteLoading" type="card" style="height: 100%">
|
||||
<el-tab-pane name="friend" :label="$t('view.favorite.friends.header')" lazy>
|
||||
<el-tabs v-model="currentTabName" v-loading="isFavoriteLoading" type="card" style="height: 100%">
|
||||
<el-tab-pane name="friend" :label="t('view.favorite.friends.header')">
|
||||
<FavoritesFriendTab
|
||||
:favorite-friends="favoriteFriends"
|
||||
:sort-favorites.sync="isSortByTime"
|
||||
:hide-tooltips="hideTooltips"
|
||||
:grouped-by-group-key-favorite-friends="groupedByGroupKeyFavoriteFriends"
|
||||
:edit-favorites-mode="editFavoritesMode"
|
||||
@show-friend-import-dialog="showFriendImportDialog"
|
||||
@save-sort-favorites-option="saveSortFavoritesOption"
|
||||
@change-favorite-group-name="changeFavoriteGroupName" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane name="world" :label="$t('view.favorite.worlds.header')" lazy>
|
||||
<el-tab-pane name="world" :label="t('view.favorite.worlds.header')" lazy>
|
||||
<FavoritesWorldTab
|
||||
@show-world-import-dialog="showWorldImportDialog"
|
||||
@save-sort-favorites-option="saveSortFavoritesOption"
|
||||
@change-favorite-group-name="changeFavoriteGroupName"
|
||||
@new-instance-self-invite="newInstanceSelfInvite"
|
||||
@refresh-local-world-favorite="refreshLocalWorldFavorites"
|
||||
@delete-local-world-favorite-group="deleteLocalWorldFavoriteGroup"
|
||||
@remove-local-world-favorite="removeLocalWorldFavorite"
|
||||
@rename-local-world-favorite-group="renameLocalWorldFavoriteGroup"
|
||||
@new-local-world-favorite-group="newLocalWorldFavoriteGroup"
|
||||
:sort-favorites.sync="isSortByTime"
|
||||
:hide-tooltips="hideTooltips"
|
||||
:favorite-worlds="favoriteWorlds"
|
||||
:edit-favorites-mode="editFavoritesMode"
|
||||
:shift-held="shiftHeld"
|
||||
:refresh-local-world-favorites="refreshLocalWorldFavorites"
|
||||
:local-world-favorite-groups="localWorldFavoriteGroups"
|
||||
:local-world-favorites="localWorldFavorites"
|
||||
:local-world-favorites-list="localWorldFavoritesList" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane name="avatar" :label="$t('view.favorite.avatars.header')" lazy>
|
||||
<FavoritesAvatarTab
|
||||
:sort-favorites.sync="isSortByTime"
|
||||
:hide-tooltips="hideTooltips"
|
||||
:shift-held="shiftHeld"
|
||||
:edit-favorites-mode="editFavoritesMode"
|
||||
:avatar-history-array="avatarHistoryArray"
|
||||
:refreshing-local-favorites="refreshingLocalFavorites"
|
||||
:local-avatar-favorite-groups="localAvatarFavoriteGroups"
|
||||
:local-avatar-favorites="localAvatarFavorites"
|
||||
:favorite-avatars="favoriteAvatars"
|
||||
:local-avatar-favorites-list="localAvatarFavoritesList"
|
||||
@show-avatar-import-dialog="showAvatarImportDialog"
|
||||
@save-sort-favorites-option="saveSortFavoritesOption"
|
||||
@change-favorite-group-name="changeFavoriteGroupName"
|
||||
@remove-local-avatar-favorite="removeLocalAvatarFavorite"
|
||||
@select-avatar-with-confirmation="selectAvatarWithConfirmation"
|
||||
@prompt-clear-avatar-history="promptClearAvatarHistory"
|
||||
@prompt-new-local-avatar-favorite-group="promptNewLocalAvatarFavoriteGroup"
|
||||
@refresh-local-avatar-favorites="refreshLocalAvatarFavorites"
|
||||
@prompt-local-avatar-favorite-group-rename="promptLocalAvatarFavoriteGroupRename"
|
||||
@prompt-local-avatar-favorite-group-delete="promptLocalAvatarFavoriteGroupDelete" />
|
||||
@refresh-local-world-favorite="refreshLocalWorldFavorites" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane name="avatar" :label="t('view.favorite.avatars.header')" lazy>
|
||||
<FavoritesAvatarTab
|
||||
:hide-tooltips="hideTooltips"
|
||||
:edit-favorites-mode="editFavoritesMode"
|
||||
:refreshing-local-favorites="refreshingLocalFavorites"
|
||||
@change-favorite-group-name="changeFavoriteGroupName"
|
||||
@refresh-local-avatar-favorites="refreshLocalAvatarFavorites" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { ref, getCurrentInstance } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import * as workerTimers from 'worker-timers';
|
||||
import { avatarRequest, favoriteRequest, worldRequest } from '../../api';
|
||||
import { useAppearanceSettingsStore, useFavoriteStore, useUiStore, useAvatarStore } from '../../stores';
|
||||
import FavoritesAvatarTab from './components/FavoritesAvatarTab.vue';
|
||||
import FavoritesFriendTab from './components/FavoritesFriendTab.vue';
|
||||
import FavoritesWorldTab from './components/FavoritesWorldTab.vue';
|
||||
import FavoritesAvatarTab from './components/FavoritesAvatarTab.vue';
|
||||
import { avatarRequest, favoriteRequest, worldRequest } from '../../api';
|
||||
import * as workerTimers from 'worker-timers';
|
||||
|
||||
export default {
|
||||
name: 'FavoritesTab',
|
||||
components: {
|
||||
FavoritesFriendTab,
|
||||
FavoritesWorldTab,
|
||||
FavoritesAvatarTab
|
||||
},
|
||||
inject: ['API'],
|
||||
props: {
|
||||
menuActiveIndex: String,
|
||||
hideTooltips: Boolean,
|
||||
shiftHeld: Boolean,
|
||||
favoriteFriends: Array,
|
||||
sortFavorites: Boolean,
|
||||
groupedByGroupKeyFavoriteFriends: Object,
|
||||
favoriteWorlds: Array,
|
||||
localWorldFavoriteGroups: Array,
|
||||
localWorldFavorites: Object,
|
||||
avatarHistoryArray: Array,
|
||||
localAvatarFavoriteGroups: Array,
|
||||
localAvatarFavorites: Object,
|
||||
favoriteAvatars: Array,
|
||||
localAvatarFavoritesList: Array,
|
||||
localWorldFavoritesList: Array
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editFavoritesMode: false,
|
||||
refreshingLocalFavorites: false,
|
||||
currentTabName: 'friend'
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isSortByTime: {
|
||||
get() {
|
||||
return this.sortFavorites;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:sort-favorites', value);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showBulkUnfavoriteSelectionConfirm() {
|
||||
const elementsTicked = [];
|
||||
// check favorites type
|
||||
for (const ctx of this.favoriteFriends) {
|
||||
if (ctx.$selected) {
|
||||
elementsTicked.push(ctx.id);
|
||||
}
|
||||
}
|
||||
for (const ctx of this.favoriteWorlds) {
|
||||
if (ctx.$selected) {
|
||||
elementsTicked.push(ctx.id);
|
||||
}
|
||||
}
|
||||
for (const ctx of this.favoriteAvatars) {
|
||||
if (ctx.$selected) {
|
||||
elementsTicked.push(ctx.id);
|
||||
}
|
||||
}
|
||||
if (elementsTicked.length === 0) {
|
||||
return;
|
||||
}
|
||||
this.$confirm(
|
||||
`Are you sure you want to unfavorite ${elementsTicked.length} favorites?
|
||||
This action cannot be undone.`,
|
||||
`Delete ${elementsTicked.length} favorites?`,
|
||||
{
|
||||
confirmButtonText: 'Confirm',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'info',
|
||||
callback: (action) => {
|
||||
if (action === 'confirm') {
|
||||
this.bulkUnfavoriteSelection(elementsTicked);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
const { t } = useI18n();
|
||||
const { proxy } = getCurrentInstance();
|
||||
const { hideTooltips } = storeToRefs(useAppearanceSettingsStore());
|
||||
const {
|
||||
favoriteFriends,
|
||||
favoriteWorlds,
|
||||
favoriteAvatars,
|
||||
isFavoriteLoading,
|
||||
localAvatarFavoritesList,
|
||||
localWorldFavoritesList
|
||||
} = storeToRefs(useFavoriteStore());
|
||||
const {
|
||||
refreshFavorites,
|
||||
refreshFavoriteGroups,
|
||||
clearBulkFavoriteSelection,
|
||||
bulkCopyFavoriteSelection,
|
||||
getLocalWorldFavorites,
|
||||
handleFavoriteGroup
|
||||
} = useFavoriteStore();
|
||||
const { menuActiveIndex } = storeToRefs(useUiStore());
|
||||
const { applyAvatar } = useAvatarStore();
|
||||
|
||||
bulkUnfavoriteSelection(elementsTicked) {
|
||||
for (const id of elementsTicked) {
|
||||
favoriteRequest.deleteFavorite({
|
||||
objectId: id
|
||||
});
|
||||
}
|
||||
this.editFavoritesMode = false;
|
||||
},
|
||||
changeFavoriteGroupName(ctx) {
|
||||
this.$prompt(
|
||||
$t('prompt.change_favorite_group_name.description'),
|
||||
$t('prompt.change_favorite_group_name.header'),
|
||||
{
|
||||
distinguishCancelAndClose: true,
|
||||
cancelButtonText: $t('prompt.change_favorite_group_name.cancel'),
|
||||
confirmButtonText: $t('prompt.change_favorite_group_name.change'),
|
||||
inputPlaceholder: $t('prompt.change_favorite_group_name.input_placeholder'),
|
||||
inputValue: ctx.displayName,
|
||||
inputPattern: /\S+/,
|
||||
inputErrorMessage: $t('prompt.change_favorite_group_name.input_error'),
|
||||
callback: (action, instance) => {
|
||||
if (action === 'confirm') {
|
||||
favoriteRequest
|
||||
.saveFavoriteGroup({
|
||||
type: ctx.type,
|
||||
group: ctx.name,
|
||||
displayName: instance.inputValue
|
||||
})
|
||||
.then(() => {
|
||||
this.$message({
|
||||
message: $t('prompt.change_favorite_group_name.message.success'),
|
||||
type: 'success'
|
||||
});
|
||||
// load new group name
|
||||
this.API.refreshFavoriteGroups();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
const editFavoritesMode = ref(false);
|
||||
const refreshingLocalFavorites = ref(false);
|
||||
const currentTabName = ref('friend');
|
||||
|
||||
async refreshLocalAvatarFavorites() {
|
||||
if (this.refreshingLocalFavorites) {
|
||||
return;
|
||||
}
|
||||
this.refreshingLocalFavorites = true;
|
||||
for (const avatarId of this.localAvatarFavoritesList) {
|
||||
if (!this.refreshingLocalFavorites) {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
await avatarRequest.getAvatar({
|
||||
avatarId
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
await new Promise((resolve) => {
|
||||
workerTimers.setTimeout(resolve, 1000);
|
||||
});
|
||||
}
|
||||
this.refreshingLocalFavorites = false;
|
||||
},
|
||||
async refreshLocalWorldFavorites() {
|
||||
if (this.refreshingLocalFavorites) {
|
||||
return;
|
||||
}
|
||||
this.refreshingLocalFavorites = true;
|
||||
for (const worldId of this.localWorldFavoritesList) {
|
||||
if (!this.refreshingLocalFavorites) {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
await worldRequest.getWorld({
|
||||
worldId
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
await new Promise((resolve) => {
|
||||
workerTimers.setTimeout(resolve, 1000);
|
||||
});
|
||||
}
|
||||
this.refreshingLocalFavorites = false;
|
||||
},
|
||||
clearBulkFavoriteSelection() {
|
||||
this.$emit('clear-bulk-favorite-selection');
|
||||
},
|
||||
bulkCopyFavoriteSelection() {
|
||||
this.$emit('bulk-copy-favorite-selection', this.currentTabName);
|
||||
},
|
||||
getLocalWorldFavorites() {
|
||||
this.$emit('get-local-world-favorites');
|
||||
},
|
||||
showFriendImportDialog() {
|
||||
this.$emit('show-friend-import-dialog');
|
||||
},
|
||||
saveSortFavoritesOption() {
|
||||
this.$emit('save-sort-favorites-option');
|
||||
},
|
||||
showWorldImportDialog() {
|
||||
this.$emit('show-world-import-dialog');
|
||||
},
|
||||
newInstanceSelfInvite(worldId) {
|
||||
this.$emit('new-instance-self-invite', worldId);
|
||||
},
|
||||
deleteLocalWorldFavoriteGroup(group) {
|
||||
this.$emit('delete-local-world-favorite-group', group);
|
||||
},
|
||||
removeLocalWorldFavorite(worldId, group) {
|
||||
this.$emit('remove-local-world-favorite', worldId, group);
|
||||
},
|
||||
showAvatarImportDialog() {
|
||||
this.$emit('show-avatar-import-dialog');
|
||||
},
|
||||
removeLocalAvatarFavorite(avatarId, group) {
|
||||
this.$emit('remove-local-avatar-favorite', avatarId, 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');
|
||||
},
|
||||
promptLocalAvatarFavoriteGroupRename(group) {
|
||||
this.$emit('prompt-local-avatar-favorite-group-rename', group);
|
||||
},
|
||||
promptLocalAvatarFavoriteGroupDelete(group) {
|
||||
this.$emit('prompt-local-avatar-favorite-group-delete', group);
|
||||
},
|
||||
renameLocalWorldFavoriteGroup(inputValue, group) {
|
||||
this.$emit('rename-local-world-favorite-group', inputValue, group);
|
||||
},
|
||||
newLocalWorldFavoriteGroup(inputValue) {
|
||||
this.$emit('new-local-world-favorite-group', inputValue);
|
||||
function showBulkUnfavoriteSelectionConfirm() {
|
||||
const elementsTicked = [];
|
||||
// check favorites type
|
||||
for (const ctx of favoriteFriends.value) {
|
||||
if (ctx.$selected) {
|
||||
elementsTicked.push(ctx.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
for (const ctx of favoriteWorlds.value) {
|
||||
if (ctx.$selected) {
|
||||
elementsTicked.push(ctx.id);
|
||||
}
|
||||
}
|
||||
for (const ctx of favoriteAvatars.value) {
|
||||
if (ctx.$selected) {
|
||||
elementsTicked.push(ctx.id);
|
||||
}
|
||||
}
|
||||
if (elementsTicked.length === 0) {
|
||||
return;
|
||||
}
|
||||
proxy.$confirm(
|
||||
`Are you sure you want to unfavorite ${elementsTicked.length} favorites?
|
||||
This action cannot be undone.`,
|
||||
`Delete ${elementsTicked.length} favorites?`,
|
||||
{
|
||||
confirmButtonText: 'Confirm',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'info',
|
||||
callback: (action) => {
|
||||
if (action === 'confirm') {
|
||||
bulkUnfavoriteSelection(elementsTicked);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function bulkUnfavoriteSelection(elementsTicked) {
|
||||
for (const id of elementsTicked) {
|
||||
favoriteRequest.deleteFavorite({
|
||||
objectId: id
|
||||
});
|
||||
}
|
||||
editFavoritesMode.value = false;
|
||||
}
|
||||
function changeFavoriteGroupName(ctx) {
|
||||
proxy.$prompt(
|
||||
t('prompt.change_favorite_group_name.description'),
|
||||
t('prompt.change_favorite_group_name.header'),
|
||||
{
|
||||
distinguishCancelAndClose: true,
|
||||
cancelButtonText: t('prompt.change_favorite_group_name.cancel'),
|
||||
confirmButtonText: t('prompt.change_favorite_group_name.change'),
|
||||
inputPlaceholder: t('prompt.change_favorite_group_name.input_placeholder'),
|
||||
inputValue: ctx.displayName,
|
||||
inputPattern: /\S+/,
|
||||
inputErrorMessage: t('prompt.change_favorite_group_name.input_error'),
|
||||
callback: (action, instance) => {
|
||||
if (action === 'confirm') {
|
||||
favoriteRequest
|
||||
.saveFavoriteGroup({
|
||||
type: ctx.type,
|
||||
group: ctx.name,
|
||||
displayName: instance.inputValue
|
||||
})
|
||||
.then((args) => {
|
||||
handleFavoriteGroup({
|
||||
json: args.json,
|
||||
params: {
|
||||
favoriteGroupId: args.json.id
|
||||
}
|
||||
});
|
||||
proxy.$message({
|
||||
message: t('prompt.change_favorite_group_name.message.success'),
|
||||
type: 'success'
|
||||
});
|
||||
// load new group name
|
||||
refreshFavoriteGroups();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function refreshLocalAvatarFavorites() {
|
||||
if (refreshingLocalFavorites.value) {
|
||||
return;
|
||||
}
|
||||
refreshingLocalFavorites.value = true;
|
||||
for (const avatarId of localAvatarFavoritesList.value) {
|
||||
if (!refreshingLocalFavorites.value) {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
const args = await avatarRequest.getAvatar({
|
||||
avatarId
|
||||
});
|
||||
applyAvatar(args.json);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
await new Promise((resolve) => {
|
||||
workerTimers.setTimeout(resolve, 1000);
|
||||
});
|
||||
}
|
||||
refreshingLocalFavorites.value = false;
|
||||
}
|
||||
async function refreshLocalWorldFavorites() {
|
||||
if (refreshingLocalFavorites.value) {
|
||||
return;
|
||||
}
|
||||
refreshingLocalFavorites.value = true;
|
||||
for (const worldId of localWorldFavoritesList.value) {
|
||||
if (!refreshingLocalFavorites.value) {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
await worldRequest.getWorld({
|
||||
worldId
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
await new Promise((resolve) => {
|
||||
workerTimers.setTimeout(resolve, 1000);
|
||||
});
|
||||
}
|
||||
refreshingLocalFavorites.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</el-tooltip>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<template
|
||||
v-for="groupAPI in API.favoriteAvatarGroups"
|
||||
v-for="groupAPI in favoriteAvatarGroups"
|
||||
v-if="isLocalFavorite || groupAPI.name !== group.name">
|
||||
<el-dropdown-item
|
||||
:key="groupAPI.name"
|
||||
@@ -36,22 +36,22 @@
|
||||
<el-tooltip
|
||||
v-if="favorite.deleted"
|
||||
placement="left"
|
||||
:content="$t('view.favorite.unavailable_tooltip')">
|
||||
: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')">
|
||||
: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')"
|
||||
:content="t('view.favorite.select_avatar_tooltip')"
|
||||
:disabled="hideTooltips">
|
||||
<el-button
|
||||
:disabled="API.currentUser.currentAvatar === favorite.id"
|
||||
:disabled="currentUser.currentAvatar === favorite.id"
|
||||
size="mini"
|
||||
icon="el-icon-check"
|
||||
circle
|
||||
@@ -60,7 +60,7 @@
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
placement="right"
|
||||
:content="$t('view.favorite.unfavorite_tooltip')"
|
||||
:content="t('view.favorite.unfavorite_tooltip')"
|
||||
:disabled="hideTooltips">
|
||||
<el-button
|
||||
v-if="shiftHeld"
|
||||
@@ -82,10 +82,10 @@
|
||||
<template v-else>
|
||||
<el-tooltip
|
||||
placement="left"
|
||||
:content="$t('view.favorite.select_avatar_tooltip')"
|
||||
:content="t('view.favorite.select_avatar_tooltip')"
|
||||
:disabled="hideTooltips">
|
||||
<el-button
|
||||
:disabled="API.currentUser.currentAvatar === favorite.id"
|
||||
:disabled="currentUser.currentAvatar === favorite.id"
|
||||
size="mini"
|
||||
circle
|
||||
style="margin-left: 5px"
|
||||
@@ -96,7 +96,7 @@
|
||||
<el-tooltip
|
||||
v-if="isLocalFavorite"
|
||||
placement="right"
|
||||
:content="$t('view.favorite.unfavorite_tooltip')"
|
||||
:content="t('view.favorite.unfavorite_tooltip')"
|
||||
:disabled="hideTooltips">
|
||||
<el-button
|
||||
v-if="shiftHeld"
|
||||
@@ -139,104 +139,84 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { favoriteRequest } from '../../../api';
|
||||
import { $app } from '../../../app';
|
||||
import {
|
||||
useAppearanceSettingsStore,
|
||||
useAvatarStore,
|
||||
useFavoriteStore,
|
||||
useUiStore,
|
||||
useUserStore
|
||||
} from '../../../stores';
|
||||
|
||||
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
|
||||
const props = defineProps({
|
||||
favorite: Object,
|
||||
group: [Object, String],
|
||||
editFavoritesMode: Boolean,
|
||||
isLocalFavorite: Boolean
|
||||
});
|
||||
const emit = defineEmits(['click', 'handle-select']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { hideTooltips } = storeToRefs(useAppearanceSettingsStore());
|
||||
const { favoriteAvatarGroups } = storeToRefs(useFavoriteStore());
|
||||
const { removeLocalAvatarFavorite, showFavoriteDialog } = useFavoriteStore();
|
||||
const { selectAvatarWithConfirmation } = useAvatarStore();
|
||||
const { shiftHeld } = storeToRefs(useUiStore());
|
||||
const { currentUser } = storeToRefs(useUserStore());
|
||||
|
||||
const isSelected = computed({
|
||||
get: () => props.favorite.$selected,
|
||||
set: (value) => emit('handle-select', value)
|
||||
});
|
||||
const localFavFakeRef = computed(() => (props.isLocalFavorite ? props.favorite : props.favorite.ref));
|
||||
const tooltipContent = computed(() =>
|
||||
t(props.isLocalFavorite ? 'view.favorite.copy_tooltip' : 'view.favorite.move_tooltip')
|
||||
);
|
||||
const smallThumbnail = computed(
|
||||
() => localFavFakeRef.value.thumbnailImageUrl.replace('256', '128') || localFavFakeRef.value.thumbnailImageUrl
|
||||
);
|
||||
|
||||
function moveFavorite(ref, group, type) {
|
||||
favoriteRequest.deleteFavorite({ objectId: ref.id }).then(() => {
|
||||
favoriteRequest.addFavorite({
|
||||
type,
|
||||
favoriteId: ref.id,
|
||||
tags: group.name
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function deleteFavorite(objectId) {
|
||||
favoriteRequest.deleteFavorite({ objectId });
|
||||
}
|
||||
|
||||
function addFavoriteAvatar(groupAPI) {
|
||||
return favoriteRequest
|
||||
.addFavorite({
|
||||
type: 'avatar',
|
||||
favoriteId: props.favorite.id,
|
||||
tags: groupAPI.name
|
||||
})
|
||||
.then((args) => {
|
||||
$app.$message({
|
||||
message: 'Avatar added to favorites',
|
||||
type: 'success'
|
||||
});
|
||||
// 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;
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
function handleDropdownItemClick(groupAPI) {
|
||||
if (props.isLocalFavorite) {
|
||||
addFavoriteAvatar(groupAPI);
|
||||
} else {
|
||||
moveFavorite(props.favorite.ref, groupAPI, 'avatar');
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -8,16 +8,16 @@
|
||||
<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-tooltip placement="left" :content="t('view.favorite.select_avatar_tooltip')" :disabled="hideTooltips">
|
||||
<el-button
|
||||
:disabled="API.currentUser.currentAvatar === favorite.id"
|
||||
:disabled="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)">
|
||||
<template v-if="cachedFavoritesByObjectId.has(favorite.id)">
|
||||
<el-tooltip placement="right" content="Unfavorite" :disabled="hideTooltips">
|
||||
<el-button
|
||||
type="default"
|
||||
@@ -43,26 +43,28 @@
|
||||
</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 setup>
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { useAppearanceSettingsStore, useAvatarStore, useFavoriteStore, useUserStore } from '../../../stores';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { hideTooltips } = storeToRefs(useAppearanceSettingsStore());
|
||||
const { cachedFavoritesByObjectId } = storeToRefs(useFavoriteStore());
|
||||
const { showFavoriteDialog } = useFavoriteStore();
|
||||
const { selectAvatarWithConfirmation } = useAvatarStore();
|
||||
const { currentUser } = storeToRefs(useUserStore());
|
||||
|
||||
const props = defineProps({
|
||||
favorite: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const smallThumbnail = computed(() => {
|
||||
return props.favorite.thumbnailImageUrl.replace('256', '128') || props.favorite.thumbnailImageUrl;
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -3,29 +3,29 @@
|
||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
||||
<div>
|
||||
<el-button size="small" @click="showAvatarExportDialog">
|
||||
{{ $t('view.favorite.export') }}
|
||||
{{ t('view.favorite.export') }}
|
||||
</el-button>
|
||||
<el-button size="small" style="margin-left: 5px" @click="showAvatarImportDialog">
|
||||
{{ $t('view.favorite.import') }}
|
||||
{{ 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') }}
|
||||
{{ 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') }}
|
||||
{{ t('view.settings.appearance.appearance.sort_favorite_by_name') }}
|
||||
</el-radio>
|
||||
<el-radio :label="true">
|
||||
{{ $t('view.settings.appearance.appearance.sort_favorite_by_date') }}
|
||||
{{ 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')"
|
||||
:placeholder="t('view.favorite.avatars.search')"
|
||||
style="width: 200px"
|
||||
@input="searchAvatarFavorites" />
|
||||
</div>
|
||||
@@ -56,16 +56,16 @@
|
||||
</div>
|
||||
</div>
|
||||
<span style="display: block; margin-top: 20px">
|
||||
{{ $t('view.favorite.avatars.vrchat_favorites') }}
|
||||
{{ t('view.favorite.avatars.vrchat_favorites') }}
|
||||
</span>
|
||||
<el-collapse style="border: 0">
|
||||
<el-collapse-item v-for="group in API.favoriteAvatarGroups" :key="group.name">
|
||||
<el-collapse-item v-for="group in 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-tooltip placement="top" :content="t('view.favorite.rename_tooltip')" :disabled="hideTooltips">
|
||||
<el-button
|
||||
size="mini"
|
||||
icon="el-icon-edit"
|
||||
@@ -73,7 +73,7 @@
|
||||
style="margin-left: 10px"
|
||||
@click.stop="changeFavoriteGroupName(group)" />
|
||||
</el-tooltip>
|
||||
<el-tooltip placement="right" :content="$t('view.favorite.clear_tooltip')" :disabled="hideTooltips">
|
||||
<el-tooltip placement="right" :content="t('view.favorite.clear_tooltip')" :disabled="hideTooltips">
|
||||
<el-button
|
||||
size="mini"
|
||||
icon="el-icon-delete"
|
||||
@@ -89,12 +89,9 @@
|
||||
: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
|
||||
@@ -132,7 +129,6 @@
|
||||
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
|
||||
@@ -148,21 +144,21 @@
|
||||
<span>No Data</span>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
<span style="display: block; margin-top: 20px">{{ $t('view.favorite.avatars.local_favorites') }}</span>
|
||||
<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') }}
|
||||
{{ 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') }}
|
||||
{{ 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>
|
||||
<span>{{ t('view.favorite.avatars.cancel_refresh') }}</span>
|
||||
</el-button>
|
||||
<el-collapse-item
|
||||
v-for="group in localAvatarFavoriteGroups"
|
||||
@@ -173,7 +169,7 @@
|
||||
<span :style="{ color: '#909399', fontSize: '12px', marginLeft: '10px' }">{{
|
||||
getLocalAvatarFavoriteGroupLength(group)
|
||||
}}</span>
|
||||
<el-tooltip placement="top" :content="$t('view.favorite.rename_tooltip')" :disabled="hideTooltips">
|
||||
<el-tooltip placement="top" :content="t('view.favorite.rename_tooltip')" :disabled="hideTooltips">
|
||||
<el-button
|
||||
size="mini"
|
||||
icon="el-icon-edit"
|
||||
@@ -181,10 +177,7 @@
|
||||
:style="{ marginLeft: '5px' }"
|
||||
@click.stop="promptLocalAvatarFavoriteGroupRename(group)"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
placement="right"
|
||||
:content="$t('view.favorite.delete_tooltip')"
|
||||
:disabled="hideTooltips">
|
||||
<el-tooltip placement="right" :content="t('view.favorite.delete_tooltip')" :disabled="hideTooltips">
|
||||
<el-button
|
||||
size="mini"
|
||||
icon="el-icon-delete"
|
||||
@@ -202,11 +195,8 @@
|
||||
: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
|
||||
@@ -223,178 +213,205 @@
|
||||
</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" />
|
||||
<AvatarExportDialog :avatar-export-dialog-visible.sync="avatarExportDialogVisible" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { ref, computed, getCurrentInstance } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { favoriteRequest } from '../../../api';
|
||||
import { useAppearanceSettingsStore, useAvatarStore, useFavoriteStore, useUserStore } from '../../../stores';
|
||||
import AvatarExportDialog from '../dialogs/AvatarExportDialog.vue';
|
||||
import FavoritesAvatarItem from './FavoritesAvatarItem.vue';
|
||||
import FavoritesAvatarLocalHistoryItem from './FavoritesAvatarLocalHistoryItem.vue';
|
||||
import AvatarExportDialog from '../dialogs/AvatarExportDialog.vue';
|
||||
import { favoriteRequest } from '../../../api';
|
||||
|
||||
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
|
||||
defineProps({
|
||||
editFavoritesMode: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
avatarExportDialogVisible: false,
|
||||
avatarFavoriteSearch: '',
|
||||
avatarFavoriteSearchResults: []
|
||||
};
|
||||
refreshingLocalFavorites: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const { proxy } = getCurrentInstance();
|
||||
const emit = defineEmits(['change-favorite-group-name', 'refresh-local-avatar-favorites']);
|
||||
|
||||
const { hideTooltips, sortFavorites } = storeToRefs(useAppearanceSettingsStore());
|
||||
const { setSortFavorites } = useAppearanceSettingsStore();
|
||||
const { favoriteAvatars, favoriteAvatarGroups, localAvatarFavorites, localAvatarFavoriteGroups } =
|
||||
storeToRefs(useFavoriteStore());
|
||||
const {
|
||||
showAvatarImportDialog,
|
||||
getLocalAvatarFavoriteGroupLength,
|
||||
deleteLocalAvatarFavoriteGroup,
|
||||
renameLocalAvatarFavoriteGroup,
|
||||
newLocalAvatarFavoriteGroup,
|
||||
saveSortFavoritesOption
|
||||
} = useFavoriteStore();
|
||||
const { avatarHistoryArray } = storeToRefs(useAvatarStore());
|
||||
const { promptClearAvatarHistory, showAvatarDialog } = useAvatarStore();
|
||||
const { currentUser } = storeToRefs(useUserStore());
|
||||
const { t } = useI18n();
|
||||
|
||||
const avatarExportDialogVisible = ref(false);
|
||||
const avatarFavoriteSearch = ref('');
|
||||
const avatarFavoriteSearchResults = ref([]);
|
||||
|
||||
const sortFav = computed({
|
||||
get() {
|
||||
return sortFavorites.value;
|
||||
},
|
||||
computed: {
|
||||
sortFav: {
|
||||
get() {
|
||||
return this.sortFavorites;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:sort-favorites', value);
|
||||
set() {
|
||||
setSortFavorites();
|
||||
}
|
||||
});
|
||||
|
||||
const groupedByGroupKeyFavoriteAvatars = computed(() => {
|
||||
const groupedByGroupKeyFavoriteAvatars = {};
|
||||
favoriteAvatars.value.forEach((avatar) => {
|
||||
if (avatar.groupKey) {
|
||||
if (!groupedByGroupKeyFavoriteAvatars[avatar.groupKey]) {
|
||||
groupedByGroupKeyFavoriteAvatars[avatar.groupKey] = [];
|
||||
}
|
||||
},
|
||||
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;
|
||||
groupedByGroupKeyFavoriteAvatars[avatar.groupKey].push(avatar);
|
||||
}
|
||||
},
|
||||
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);
|
||||
}
|
||||
}
|
||||
return groupedByGroupKeyFavoriteAvatars;
|
||||
});
|
||||
|
||||
const isLocalUserVrcplusSupporter = computed(() => currentUser.value.$isVRCPlus);
|
||||
|
||||
function searchAvatarFavorites() {
|
||||
let ref = null;
|
||||
const search = avatarFavoriteSearch.value.toLowerCase();
|
||||
if (search.length < 3) {
|
||||
avatarFavoriteSearchResults.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const results = [];
|
||||
for (let i = 0; i < localAvatarFavoriteGroups.value.length; ++i) {
|
||||
const group = localAvatarFavoriteGroups.value[i];
|
||||
if (!localAvatarFavorites.value[group]) {
|
||||
continue;
|
||||
}
|
||||
for (let j = 0; j < localAvatarFavorites.value[group].length; ++j) {
|
||||
ref = localAvatarFavorites.value[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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (let i = 0; i < favoriteAvatars.value.length; ++i) {
|
||||
ref = favoriteAvatars.value[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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
avatarFavoriteSearchResults.value = results;
|
||||
}
|
||||
|
||||
function clearFavoriteGroup(ctx) {
|
||||
proxy.$confirm('Continue? Clear Group', 'Confirm', {
|
||||
confirmButtonText: 'Confirm',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'info',
|
||||
callback: (action) => {
|
||||
if (action === 'confirm') {
|
||||
favoriteRequest.clearFavoriteGroup({
|
||||
type: ctx.type,
|
||||
group: ctx.name
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showAvatarExportDialog() {
|
||||
avatarExportDialogVisible.value = true;
|
||||
}
|
||||
|
||||
function changeFavoriteGroupName(group) {
|
||||
emit('change-favorite-group-name', group);
|
||||
}
|
||||
|
||||
function promptNewLocalAvatarFavoriteGroup() {
|
||||
proxy.$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) {
|
||||
newLocalAvatarFavoriteGroup(instance.inputValue);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function refreshLocalAvatarFavorites() {
|
||||
emit('refresh-local-avatar-favorites');
|
||||
}
|
||||
|
||||
function promptLocalAvatarFavoriteGroupRename(group) {
|
||||
proxy.$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) {
|
||||
renameLocalAvatarFavoriteGroup(instance.inputValue, group);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function promptLocalAvatarFavoriteGroupDelete(group) {
|
||||
proxy.$confirm(`Delete Group? ${group}`, 'Confirm', {
|
||||
confirmButtonText: 'Confirm',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'info',
|
||||
callback: (action) => {
|
||||
if (action === 'confirm') {
|
||||
deleteLocalAvatarFavoriteGroup(group);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -10,12 +10,12 @@
|
||||
class="name"
|
||||
:style="{ color: favorite.ref.$userColour }"
|
||||
v-text="favorite.ref.displayName"></span>
|
||||
<location
|
||||
<Location
|
||||
class="extra"
|
||||
v-if="favorite.ref.location !== 'offline'"
|
||||
:location="favorite.ref.location"
|
||||
:traveling="favorite.ref.travelingToLocation"
|
||||
:link="false"></location>
|
||||
:link="false" />
|
||||
<span v-else v-text="favorite.ref.statusDescription"></span>
|
||||
</div>
|
||||
<template v-if="editFavoritesMode">
|
||||
@@ -27,7 +27,7 @@
|
||||
<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">
|
||||
<template v-for="groupAPI in favoriteFriendGroups">
|
||||
<el-dropdown-item
|
||||
v-if="groupAPI.name !== group.name"
|
||||
:key="groupAPI.name"
|
||||
@@ -82,63 +82,36 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Location from '../../../components/Location.vue';
|
||||
<script setup>
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { favoriteRequest } from '../../../api';
|
||||
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
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
}
|
||||
}
|
||||
};
|
||||
import { userImage, userStatusClass } from '../../../shared/utils';
|
||||
import { useAppearanceSettingsStore, useFavoriteStore, useUiStore } from '../../../stores';
|
||||
|
||||
defineProps({
|
||||
favorite: { type: Object, required: true },
|
||||
group: { type: Object, required: true },
|
||||
editFavoritesMode: Boolean
|
||||
});
|
||||
|
||||
defineEmits(['click']);
|
||||
|
||||
const { hideTooltips } = storeToRefs(useAppearanceSettingsStore());
|
||||
const { favoriteFriendGroups } = storeToRefs(useFavoriteStore());
|
||||
const { showFavoriteDialog } = useFavoriteStore();
|
||||
const { shiftHeld } = storeToRefs(useUiStore());
|
||||
|
||||
function moveFavorite(ref, group, type) {
|
||||
favoriteRequest.deleteFavorite({ objectId: ref.id }).then(() => {
|
||||
favoriteRequest.addFavorite({
|
||||
type,
|
||||
favoriteId: ref.id,
|
||||
tags: group.name
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function deleteFavorite(objectId) {
|
||||
favoriteRequest.deleteFavorite({ objectId });
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
</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">
|
||||
<el-collapse-item v-for="group in favoriteFriendGroups" :key="group.name">
|
||||
<template slot="title">
|
||||
<span
|
||||
style="font-weight: bold; font-size: 14px; margin-left: 10px"
|
||||
@@ -70,73 +70,67 @@
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
<FriendExportDialog
|
||||
:friend-export-dialog-visible.sync="friendExportDialogVisible"
|
||||
:favorite-friends="favoriteFriends" />
|
||||
<FriendExportDialog :friend-export-dialog-visible.sync="friendExportDialogVisible" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FavoritesFriendItem from './FavoritesFriendItem.vue';
|
||||
import FriendExportDialog from '../dialogs/FriendExportDialog.vue';
|
||||
<script setup>
|
||||
import { ref, getCurrentInstance, computed } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { favoriteRequest } from '../../../api';
|
||||
import { useAppearanceSettingsStore, useFavoriteStore, useUserStore } from '../../../stores';
|
||||
import FriendExportDialog from '../dialogs/FriendExportDialog.vue';
|
||||
import FavoritesFriendItem from './FavoritesFriendItem.vue';
|
||||
|
||||
export default {
|
||||
name: 'FavoritesFriendTab',
|
||||
components: { FriendExportDialog, FavoritesFriendItem },
|
||||
inject: ['showUserDialog', 'API'],
|
||||
props: {
|
||||
favoriteFriends: Array,
|
||||
sortFavorites: Boolean,
|
||||
hideTooltips: Boolean,
|
||||
groupedByGroupKeyFavoriteFriends: Object,
|
||||
editFavoritesMode: Boolean
|
||||
defineProps({
|
||||
editFavoritesMode: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['change-favorite-group-name']);
|
||||
|
||||
const { proxy } = getCurrentInstance();
|
||||
|
||||
const { hideTooltips, sortFavorites } = storeToRefs(useAppearanceSettingsStore());
|
||||
const { setSortFavorites } = useAppearanceSettingsStore();
|
||||
const { showUserDialog } = useUserStore();
|
||||
const { favoriteFriendGroups, groupedByGroupKeyFavoriteFriends } = storeToRefs(useFavoriteStore());
|
||||
const { showFriendImportDialog, saveSortFavoritesOption } = useFavoriteStore();
|
||||
|
||||
const friendExportDialogVisible = ref(false);
|
||||
|
||||
const sortFav = computed({
|
||||
get() {
|
||||
return sortFavorites.value;
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
friendExportDialogVisible: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
sortFav: {
|
||||
get() {
|
||||
return this.sortFavorites;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:sort-favorites', value);
|
||||
set(value) {
|
||||
setSortFavorites(value);
|
||||
}
|
||||
});
|
||||
|
||||
function showFriendExportDialog() {
|
||||
friendExportDialogVisible.value = true;
|
||||
}
|
||||
|
||||
function clearFavoriteGroup(ctx) {
|
||||
proxy.$confirm('Continue? Clear Group', 'Confirm', {
|
||||
confirmButtonText: 'Confirm',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'info',
|
||||
callback: (action) => {
|
||||
if (action === 'confirm') {
|
||||
favoriteRequest.clearFavoriteGroup({
|
||||
type: ctx.type,
|
||||
group: ctx.name
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
function changeFavoriteGroupName(group) {
|
||||
emit('change-favorite-group-name', group);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
<img v-lazy="smallThumbnail" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span class="name">{{ localFavFakeRef.name }}</span>
|
||||
<span v-if="localFavFakeRef.occupants" class="extra"
|
||||
<span class="name" v-once>{{ localFavFakeRef.name }}</span>
|
||||
<span v-if="localFavFakeRef.occupants" class="extra" v-once
|
||||
>{{ localFavFakeRef.authorName }} ({{ localFavFakeRef.occupants }})</span
|
||||
>
|
||||
<span v-else class="extra">{{ localFavFakeRef.authorName }}</span>
|
||||
<span v-else class="extra" v-once>{{ localFavFakeRef.authorName }}</span>
|
||||
</div>
|
||||
<template v-if="editFavoritesMode">
|
||||
<el-dropdown trigger="click" size="mini" style="margin-left: 5px" @click.native.stop>
|
||||
@@ -21,7 +21,7 @@
|
||||
<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">
|
||||
<template v-for="groupAPI in favoriteWorldGroups">
|
||||
<el-dropdown-item
|
||||
v-if="isLocalFavorite || groupAPI.name !== group.name"
|
||||
:key="groupAPI.name"
|
||||
@@ -59,7 +59,7 @@
|
||||
size="mini"
|
||||
icon="el-icon-message"
|
||||
style="margin-left: 5px"
|
||||
@click.stop="$emit('new-instance-self-invite', favorite.id)"
|
||||
@click.stop="newInstanceSelfInvite(favorite.id)"
|
||||
circle></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
@@ -109,7 +109,7 @@
|
||||
<template v-else>
|
||||
<div class="avatar"></div>
|
||||
<div class="detail">
|
||||
<span>{{ favorite.name || favorite.id }}</span>
|
||||
<span v-once>{{ favorite.name || favorite.id }}</span>
|
||||
<el-tooltip
|
||||
v-if="!isLocalFavorite && favorite.deleted"
|
||||
placement="left"
|
||||
@@ -128,104 +128,82 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { computed, getCurrentInstance } from 'vue';
|
||||
import { favoriteRequest } from '../../../api';
|
||||
import { useAppearanceSettingsStore, useFavoriteStore, useInviteStore, useUiStore } from '../../../stores';
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
const props = defineProps({
|
||||
group: [Object, String],
|
||||
favorite: Object,
|
||||
editFavoritesMode: Boolean,
|
||||
isLocalFavorite: { type: Boolean, default: false }
|
||||
});
|
||||
|
||||
const emit = defineEmits(['handle-select', 'remove-local-world-favorite', 'click']);
|
||||
const { proxy } = getCurrentInstance();
|
||||
|
||||
const { hideTooltips } = storeToRefs(useAppearanceSettingsStore());
|
||||
const { favoriteWorldGroups } = storeToRefs(useFavoriteStore());
|
||||
const { showFavoriteDialog } = useFavoriteStore();
|
||||
const { newInstanceSelfInvite } = useInviteStore();
|
||||
const { shiftHeld } = storeToRefs(useUiStore());
|
||||
|
||||
const isSelected = computed({
|
||||
get: () => props.favorite.$selected,
|
||||
set: (value) => emit('handle-select', value)
|
||||
});
|
||||
|
||||
const localFavFakeRef = computed(() => (props.isLocalFavorite ? props.favorite : props.favorite.ref));
|
||||
|
||||
const smallThumbnail = computed(() => {
|
||||
const url = localFavFakeRef.value.thumbnailImageUrl.replace('256', '128');
|
||||
return url || localFavFakeRef.value.thumbnailImageUrl;
|
||||
});
|
||||
|
||||
function handleDropdownItemClick(groupAPI) {
|
||||
if (props.isLocalFavorite) {
|
||||
addFavoriteWorld(localFavFakeRef.value, groupAPI, true);
|
||||
} else {
|
||||
moveFavorite(localFavFakeRef.value, groupAPI, 'world');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function handleDeleteFavorite() {
|
||||
if (props.isLocalFavorite) {
|
||||
emit('remove-local-world-favorite', props.favorite.id, props.group);
|
||||
} else {
|
||||
deleteFavorite(props.favorite.id);
|
||||
}
|
||||
}
|
||||
|
||||
function moveFavorite(refObj, group, type) {
|
||||
favoriteRequest.deleteFavorite({ objectId: refObj.id }).then(() => {
|
||||
favoriteRequest.addFavorite({
|
||||
type,
|
||||
favoriteId: refObj.id,
|
||||
tags: group.name
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function deleteFavorite(objectId) {
|
||||
favoriteRequest.deleteFavorite({ objectId });
|
||||
}
|
||||
|
||||
function addFavoriteWorld(refObj, group, message) {
|
||||
return favoriteRequest
|
||||
.addFavorite({
|
||||
type: 'world',
|
||||
favoriteId: refObj.id,
|
||||
tags: group.name
|
||||
})
|
||||
.then((args) => {
|
||||
if (message) {
|
||||
proxy.$message({ message: 'World added to favorites', type: 'success' });
|
||||
}
|
||||
return args;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -3,16 +3,13 @@
|
||||
<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')">{{
|
||||
<el-button size="small" style="margin-left: 5px" @click="showWorldImportDialog">{{
|
||||
$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-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>
|
||||
@@ -59,7 +56,7 @@
|
||||
</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">
|
||||
<el-collapse-item v-for="group in favoriteWorldGroups" :key="group.name">
|
||||
<template slot="title">
|
||||
<div style="display: flex; align-items: center">
|
||||
<span
|
||||
@@ -125,10 +122,8 @@
|
||||
: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" />
|
||||
@handle-select="favorite.$selected = $event" />
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
@@ -153,7 +148,7 @@
|
||||
v-if="!refreshingLocalFavorites"
|
||||
size="small"
|
||||
style="margin-left: 5px"
|
||||
@click="$emit('refresh-local-world-favorite')"
|
||||
@click="refreshLocalWorldFavorite"
|
||||
>{{ $t('view.favorite.worlds.refresh') }}</el-button
|
||||
>
|
||||
<el-button v-else size="small" style="margin-left: 5px" @click="refreshingLocalFavorites = false">
|
||||
@@ -196,9 +191,7 @@
|
||||
: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
|
||||
@@ -215,236 +208,247 @@
|
||||
</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" />
|
||||
<WorldExportDialog :world-export-dialog-visible.sync="worldExportDialogVisible" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FavoritesWorldItem from './FavoritesWorldItem.vue';
|
||||
import WorldExportDialog from '../dialogs/WorldExportDialog.vue';
|
||||
<script setup>
|
||||
import { computed, ref, getCurrentInstance } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { favoriteRequest } from '../../../api';
|
||||
import { useAppearanceSettingsStore, useFavoriteStore, useWorldStore } from '../../../stores';
|
||||
import WorldExportDialog from '../dialogs/WorldExportDialog.vue';
|
||||
import FavoritesWorldItem from './FavoritesWorldItem.vue';
|
||||
|
||||
export default {
|
||||
name: 'FavoritesWorldTab',
|
||||
components: {
|
||||
FavoritesWorldItem,
|
||||
WorldExportDialog
|
||||
defineProps({
|
||||
editFavoritesMode: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
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 = {};
|
||||
refreshingLocalFavorites: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
this.favoriteWorlds.forEach((world) => {
|
||||
if (world.groupKey) {
|
||||
if (!groupedByGroupKeyFavoriteWorlds[world.groupKey]) {
|
||||
groupedByGroupKeyFavoriteWorlds[world.groupKey] = [];
|
||||
}
|
||||
groupedByGroupKeyFavoriteWorlds[world.groupKey].push(world);
|
||||
}
|
||||
});
|
||||
const emit = defineEmits([
|
||||
'change-favorite-group-name',
|
||||
'save-sort-favorites-option',
|
||||
'refresh-local-world-favorite'
|
||||
]);
|
||||
|
||||
return groupedByGroupKeyFavoriteWorlds;
|
||||
},
|
||||
sortFav: {
|
||||
get() {
|
||||
return this.sortFavorites;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:sort-favorites', value);
|
||||
const { proxy } = getCurrentInstance();
|
||||
|
||||
const { t } = useI18n();
|
||||
const { hideTooltips, sortFavorites } = storeToRefs(useAppearanceSettingsStore());
|
||||
const { setSortFavorites } = useAppearanceSettingsStore();
|
||||
const { favoriteWorlds, favoriteWorldGroups, localWorldFavorites, localWorldFavoriteGroups } =
|
||||
storeToRefs(useFavoriteStore());
|
||||
const {
|
||||
showWorldImportDialog,
|
||||
getLocalWorldFavoriteGroupLength,
|
||||
deleteLocalWorldFavoriteGroup,
|
||||
renameLocalWorldFavoriteGroup,
|
||||
removeLocalWorldFavorite,
|
||||
newLocalWorldFavoriteGroup,
|
||||
handleFavoriteGroup
|
||||
} = useFavoriteStore();
|
||||
const { showWorldDialog } = useWorldStore();
|
||||
|
||||
const worldGroupVisibilityOptions = ref(['private', 'friends', 'public']);
|
||||
const worldExportDialogVisible = ref(false);
|
||||
const worldFavoriteSearch = ref('');
|
||||
const worldFavoriteSearchResults = ref([]);
|
||||
|
||||
const groupedByGroupKeyFavoriteWorlds = computed(() => {
|
||||
const groupedByGroupKeyFavoriteWorlds = {};
|
||||
|
||||
favoriteWorlds.value.forEach((world) => {
|
||||
if (world.groupKey) {
|
||||
if (!groupedByGroupKeyFavoriteWorlds[world.groupKey]) {
|
||||
groupedByGroupKeyFavoriteWorlds[world.groupKey] = [];
|
||||
}
|
||||
groupedByGroupKeyFavoriteWorlds[world.groupKey].push(world);
|
||||
}
|
||||
});
|
||||
|
||||
return groupedByGroupKeyFavoriteWorlds;
|
||||
});
|
||||
|
||||
const sortFav = computed({
|
||||
get() {
|
||||
return sortFavorites.value;
|
||||
},
|
||||
set() {
|
||||
setSortFavorites();
|
||||
}
|
||||
});
|
||||
|
||||
function showExportDialog() {
|
||||
worldExportDialogVisible.value = true;
|
||||
}
|
||||
|
||||
function userFavoriteWorldsStatusForFavTab(visibility) {
|
||||
let style = '';
|
||||
if (visibility === 'public') {
|
||||
style = '';
|
||||
} else if (visibility === 'friends') {
|
||||
style = 'success';
|
||||
} else {
|
||||
style = 'info';
|
||||
}
|
||||
return style;
|
||||
}
|
||||
|
||||
function changeWorldGroupVisibility(name, visibility) {
|
||||
const params = {
|
||||
type: 'world',
|
||||
group: name,
|
||||
visibility
|
||||
};
|
||||
favoriteRequest.saveFavoriteGroup(params).then((args) => {
|
||||
handleFavoriteGroup({
|
||||
json: args.json,
|
||||
params: {
|
||||
favoriteGroupId: args.json.id
|
||||
}
|
||||
});
|
||||
proxy.$message({
|
||||
message: 'Group visibility changed',
|
||||
type: 'success'
|
||||
});
|
||||
return args;
|
||||
});
|
||||
}
|
||||
|
||||
function promptNewLocalWorldFavoriteGroup() {
|
||||
proxy.$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) {
|
||||
newLocalWorldFavoriteGroup(instance.inputValue);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showExportDialog() {
|
||||
this.worldExportDialogVisible = true;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
userFavoriteWorldsStatusForFavTab(visibility) {
|
||||
let style = '';
|
||||
if (visibility === 'public') {
|
||||
style = '';
|
||||
} else if (visibility === 'friends') {
|
||||
style = 'success';
|
||||
} else {
|
||||
style = 'info';
|
||||
function promptLocalWorldFavoriteGroupRename(group) {
|
||||
proxy.$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) {
|
||||
renameLocalWorldFavoriteGroup(instance.inputValue, group);
|
||||
}
|
||||
}
|
||||
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'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function promptLocalWorldFavoriteGroupDelete(group) {
|
||||
proxy.$confirm(`Delete Group? ${group}`, 'Confirm', {
|
||||
confirmButtonText: 'Confirm',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'info',
|
||||
callback: (action) => {
|
||||
if (action === 'confirm') {
|
||||
deleteLocalWorldFavoriteGroup(group);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function clearFavoriteGroup(ctx) {
|
||||
proxy.$confirm('Continue? Clear Group', 'Confirm', {
|
||||
confirmButtonText: 'Confirm',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'info',
|
||||
callback: (action) => {
|
||||
if (action === 'confirm') {
|
||||
favoriteRequest.clearFavoriteGroup({
|
||||
type: ctx.type,
|
||||
group: ctx.name
|
||||
});
|
||||
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;
|
||||
function searchWorldFavorites(worldFavoriteSearch) {
|
||||
let ref = null;
|
||||
const search = worldFavoriteSearch.toLowerCase();
|
||||
if (search.length < 3) {
|
||||
worldFavoriteSearchResults.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const results = [];
|
||||
for (let i = 0; i < localWorldFavoriteGroups.value.length; ++i) {
|
||||
const group = localWorldFavoriteGroups.value[i];
|
||||
if (!localWorldFavorites.value[group]) {
|
||||
continue;
|
||||
}
|
||||
for (let j = 0; j < localWorldFavorites.value[group].length; ++j) {
|
||||
ref = localWorldFavorites.value[group][j];
|
||||
if (
|
||||
!ref ||
|
||||
typeof ref.id === 'undefined' ||
|
||||
typeof ref.name === 'undefined' ||
|
||||
typeof ref.authorName === 'undefined'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (let i = 0; i < favoriteWorlds.value.length; ++i) {
|
||||
ref = favoriteWorlds.value[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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
worldFavoriteSearchResults.value = results;
|
||||
}
|
||||
|
||||
function changeFavoriteGroupName(group) {
|
||||
emit('change-favorite-group-name', group);
|
||||
}
|
||||
|
||||
function refreshLocalWorldFavorite() {
|
||||
emit('refresh-local-world-favorite');
|
||||
}
|
||||
|
||||
function saveSortFavoritesOption() {
|
||||
emit('save-sort-favorites-option');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<safe-dialog :visible.sync="isDialogVisible" :title="$t('dialog.avatar_export.header')" width="650px">
|
||||
<safe-dialog :visible.sync="isDialogVisible" :title="t('dialog.avatar_export.header')" width="650px">
|
||||
<el-checkbox-group
|
||||
v-model="exportSelectedOptions"
|
||||
style="margin-bottom: 10px"
|
||||
@@ -26,7 +26,7 @@
|
||||
<el-dropdown-item style="display: block; margin: 10px 0" @click.native="selectAvatarExportGroup(null)">
|
||||
All Favorites
|
||||
</el-dropdown-item>
|
||||
<template v-for="groupAPI in API.favoriteAvatarGroups">
|
||||
<template v-for="groupAPI in favoriteAvatarGroups">
|
||||
<el-dropdown-item
|
||||
:key="groupAPI.name"
|
||||
style="display: block; margin: 10px 0"
|
||||
@@ -79,145 +79,152 @@
|
||||
</safe-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AvatarExportDialog',
|
||||
inject: ['API'],
|
||||
props: {
|
||||
avatarExportDialogVisible: Boolean,
|
||||
favoriteAvatars: Array,
|
||||
localAvatarFavoriteGroups: Array,
|
||||
localAvatarFavorites: Object,
|
||||
localAvatarFavoritesList: Array
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
avatarExportContent: '',
|
||||
avatarExportFavoriteGroup: null,
|
||||
avatarExportLocalFavoriteGroup: null,
|
||||
exportSelectedOptions: ['ID', 'Name'],
|
||||
exportSelectOptions: [
|
||||
{ label: 'ID', value: 'id' },
|
||||
{ label: 'Name', value: 'name' },
|
||||
{ label: 'Author ID', value: 'authorId' },
|
||||
{ label: 'Author Name', value: 'authorName' },
|
||||
{ label: 'Thumbnail', value: 'thumbnailImageUrl' }
|
||||
]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isDialogVisible: {
|
||||
get() {
|
||||
return this.avatarExportDialogVisible;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:avatar-export-dialog-visible', value);
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
avatarExportDialogVisible(visible) {
|
||||
if (visible) {
|
||||
this.showAvatarExportDialog();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showAvatarExportDialog() {
|
||||
this.avatarExportFavoriteGroup = null;
|
||||
this.avatarExportLocalFavoriteGroup = null;
|
||||
this.updateAvatarExportDialog();
|
||||
},
|
||||
handleCopyAvatarExportData(event) {
|
||||
if (event.target.tagName === 'TEXTAREA') {
|
||||
event.target.select();
|
||||
}
|
||||
navigator.clipboard
|
||||
.writeText(this.avatarExportContent)
|
||||
.then(() => {
|
||||
this.$message({
|
||||
message: 'Copied successfully!',
|
||||
type: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Copy failed:', err);
|
||||
this.$message.error('Copy failed!');
|
||||
});
|
||||
},
|
||||
updateAvatarExportDialog() {
|
||||
const formatter = function (str) {
|
||||
if (/[\x00-\x1f,"]/.test(str) === true) {
|
||||
return `"${str.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return str;
|
||||
};
|
||||
const propsForQuery = this.exportSelectOptions
|
||||
.filter((option) => this.exportSelectedOptions.includes(option.label))
|
||||
.map((option) => option.value);
|
||||
<script setup>
|
||||
import { ref, computed, watch, getCurrentInstance } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useAvatarStore, useFavoriteStore } from '../../../stores';
|
||||
|
||||
function resText(ref) {
|
||||
let resArr = [];
|
||||
propsForQuery.forEach((e) => {
|
||||
resArr.push(formatter(ref?.[e]));
|
||||
});
|
||||
return resArr.join(',');
|
||||
}
|
||||
const { t } = useI18n();
|
||||
const { proxy } = getCurrentInstance();
|
||||
|
||||
const lines = [this.exportSelectedOptions.join(',')];
|
||||
const props = defineProps({
|
||||
avatarExportDialogVisible: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
if (this.avatarExportFavoriteGroup) {
|
||||
this.API.favoriteAvatarGroups.forEach((group) => {
|
||||
if (!this.avatarExportFavoriteGroup || this.avatarExportFavoriteGroup === group) {
|
||||
this.favoriteAvatars.forEach((ref) => {
|
||||
if (group.key === ref.groupKey) {
|
||||
lines.push(resText(ref.ref));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (this.avatarExportLocalFavoriteGroup) {
|
||||
const favoriteGroup = this.localAvatarFavorites[this.avatarExportLocalFavoriteGroup];
|
||||
if (!favoriteGroup) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < favoriteGroup.length; ++i) {
|
||||
const ref = favoriteGroup[i];
|
||||
lines.push(resText(ref));
|
||||
}
|
||||
} else {
|
||||
// export all
|
||||
this.favoriteAvatars.forEach((ref) => {
|
||||
lines.push(resText(ref.ref));
|
||||
});
|
||||
for (let i = 0; i < this.localAvatarFavoritesList.length; ++i) {
|
||||
const avatarId = this.localAvatarFavoritesList[i];
|
||||
const ref = this.API.cachedAvatars.get(avatarId);
|
||||
if (typeof ref !== 'undefined') {
|
||||
lines.push(resText(ref));
|
||||
}
|
||||
}
|
||||
}
|
||||
this.avatarExportContent = lines.join('\n');
|
||||
},
|
||||
selectAvatarExportGroup(group) {
|
||||
this.avatarExportFavoriteGroup = group;
|
||||
this.avatarExportLocalFavoriteGroup = null;
|
||||
this.updateAvatarExportDialog();
|
||||
},
|
||||
selectAvatarExportLocalGroup(group) {
|
||||
this.avatarExportLocalFavoriteGroup = group;
|
||||
this.avatarExportFavoriteGroup = null;
|
||||
this.updateAvatarExportDialog();
|
||||
},
|
||||
getLocalAvatarFavoriteGroupLength(group) {
|
||||
const favoriteGroup = this.localAvatarFavorites[group];
|
||||
if (!favoriteGroup) {
|
||||
return 0;
|
||||
}
|
||||
return favoriteGroup.length;
|
||||
const emit = defineEmits(['update:avatarExportDialogVisible']);
|
||||
|
||||
const favoriteStore = useFavoriteStore();
|
||||
const {
|
||||
favoriteAvatars,
|
||||
favoriteAvatarGroups,
|
||||
localAvatarFavorites,
|
||||
localAvatarFavoritesList,
|
||||
localAvatarFavoriteGroups
|
||||
} = storeToRefs(favoriteStore);
|
||||
const { getLocalAvatarFavoriteGroupLength } = favoriteStore;
|
||||
const avatarStore = useAvatarStore();
|
||||
const { cachedAvatars } = storeToRefs(avatarStore);
|
||||
|
||||
const avatarExportContent = ref('');
|
||||
const avatarExportFavoriteGroup = ref(null);
|
||||
const avatarExportLocalFavoriteGroup = ref(null);
|
||||
const exportSelectedOptions = ref(['ID', 'Name']);
|
||||
const exportSelectOptions = ref([
|
||||
{ label: 'ID', value: 'id' },
|
||||
{ label: 'Name', value: 'name' },
|
||||
{ label: 'Author ID', value: 'authorId' },
|
||||
{ label: 'Author Name', value: 'authorName' },
|
||||
{ label: 'Thumbnail', value: 'thumbnailImageUrl' }
|
||||
]);
|
||||
|
||||
const isDialogVisible = computed({
|
||||
get() {
|
||||
return props.avatarExportDialogVisible;
|
||||
},
|
||||
set(value) {
|
||||
emit('update:avatarExportDialogVisible', value);
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.avatarExportDialogVisible,
|
||||
(value) => {
|
||||
if (value) {
|
||||
showAvatarExportDialog();
|
||||
}
|
||||
}
|
||||
};
|
||||
);
|
||||
|
||||
function showAvatarExportDialog() {
|
||||
avatarExportFavoriteGroup.value = null;
|
||||
avatarExportLocalFavoriteGroup.value = null;
|
||||
updateAvatarExportDialog();
|
||||
}
|
||||
function handleCopyAvatarExportData(event) {
|
||||
if (event.target.tagName === 'TEXTAREA') {
|
||||
event.target.select();
|
||||
}
|
||||
navigator.clipboard
|
||||
.writeText(avatarExportContent.value)
|
||||
.then(() => {
|
||||
proxy.$message({
|
||||
message: 'Copied successfully!',
|
||||
type: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Copy failed:', err);
|
||||
proxy.$message.error('Copy failed!');
|
||||
});
|
||||
}
|
||||
function updateAvatarExportDialog() {
|
||||
const formatter = function (str) {
|
||||
if (/[\x00-\x1f,"]/.test(str) === true) {
|
||||
return `"${str.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return str;
|
||||
};
|
||||
const propsForQuery = exportSelectOptions.value
|
||||
.filter((option) => exportSelectedOptions.value.includes(option.label))
|
||||
.map((option) => option.value);
|
||||
|
||||
function resText(ref) {
|
||||
let resArr = [];
|
||||
propsForQuery.forEach((e) => {
|
||||
resArr.push(formatter(ref?.[e]));
|
||||
});
|
||||
return resArr.join(',');
|
||||
}
|
||||
|
||||
const lines = [exportSelectedOptions.value.join(',')];
|
||||
|
||||
if (avatarExportFavoriteGroup.value) {
|
||||
favoriteAvatarGroups.value.forEach((group) => {
|
||||
if (!avatarExportFavoriteGroup.value || avatarExportFavoriteGroup.value === group) {
|
||||
favoriteAvatars.value.forEach((ref) => {
|
||||
if (group.key === ref.groupKey) {
|
||||
lines.push(resText(ref.ref));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (avatarExportLocalFavoriteGroup.value) {
|
||||
const favoriteGroup = localAvatarFavorites.value[avatarExportLocalFavoriteGroup.value];
|
||||
if (!favoriteGroup) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < favoriteGroup.length; ++i) {
|
||||
const ref = favoriteGroup[i];
|
||||
lines.push(resText(ref));
|
||||
}
|
||||
} else {
|
||||
// export all
|
||||
favoriteAvatars.value.forEach((ref) => {
|
||||
lines.push(resText(ref.ref));
|
||||
});
|
||||
for (let i = 0; i < localAvatarFavoritesList.value.length; ++i) {
|
||||
const avatarId = localAvatarFavoritesList.value[i];
|
||||
const ref = cachedAvatars.value.get(avatarId);
|
||||
if (typeof ref !== 'undefined') {
|
||||
lines.push(resText(ref));
|
||||
}
|
||||
}
|
||||
}
|
||||
avatarExportContent.value = lines.join('\n');
|
||||
}
|
||||
function selectAvatarExportGroup(group) {
|
||||
avatarExportFavoriteGroup.value = group;
|
||||
avatarExportLocalFavoriteGroup.value = null;
|
||||
updateAvatarExportDialog();
|
||||
}
|
||||
function selectAvatarExportLocalGroup(group) {
|
||||
avatarExportLocalFavoriteGroup.value = group;
|
||||
avatarExportFavoriteGroup.value = null;
|
||||
updateAvatarExportDialog();
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
<template>
|
||||
<safe-dialog
|
||||
ref="avatarImportDialog"
|
||||
ref="avatarImportDialogRef"
|
||||
:visible.sync="isVisible"
|
||||
:title="$t('dialog.avatar_import.header')"
|
||||
:title="t('dialog.avatar_import.header')"
|
||||
width="650px">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
||||
<div style="font-size: 12px">{{ $t('dialog.avatar_import.description') }}</div>
|
||||
<div style="font-size: 12px">{{ t('dialog.avatar_import.description') }}</div>
|
||||
<div style="display: flex; align-items: center">
|
||||
<div v-if="avatarImportDialog.progress">
|
||||
{{ $t('dialog.avatar_import.process_progress') }} {{ avatarImportDialog.progress }} /
|
||||
{{ t('dialog.avatar_import.process_progress') }} {{ avatarImportDialog.progress }} /
|
||||
{{ avatarImportDialog.progressTotal }}
|
||||
<i class="el-icon-loading" style="margin: 0 5px"></i>
|
||||
</div>
|
||||
<el-button v-if="avatarImportDialog.loading" size="small" @click="cancelAvatarImport">
|
||||
{{ $t('dialog.avatar_import.cancel') }}
|
||||
{{ t('dialog.avatar_import.cancel') }}
|
||||
</el-button>
|
||||
<el-button v-else size="small" :disabled="!avatarImportDialog.input" @click="processAvatarImportList">
|
||||
{{ $t('dialog.avatar_import.process_list') }}
|
||||
{{ t('dialog.avatar_import.process_list') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -38,12 +38,12 @@
|
||||
<i class="el-icon-arrow-down el-icon--right"></i>
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ $t('dialog.avatar_import.select_group_placeholder') }}
|
||||
{{ t('dialog.avatar_import.select_group_placeholder') }}
|
||||
<i class="el-icon-arrow-down el-icon--right"></i>
|
||||
</span>
|
||||
</el-button>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<template v-for="groupAPI in API.favoriteAvatarGroups">
|
||||
<template v-for="groupAPI in favoriteAvatarGroups">
|
||||
<el-dropdown-item
|
||||
:key="groupAPI.name"
|
||||
style="display: block; margin: 10px 0"
|
||||
@@ -63,7 +63,7 @@
|
||||
<i class="el-icon-arrow-down el-icon--right"></i>
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ $t('dialog.avatar_import.select_group_placeholder') }}
|
||||
{{ t('dialog.avatar_import.select_group_placeholder') }}
|
||||
<i class="el-icon-arrow-down el-icon--right"></i>
|
||||
</span>
|
||||
</el-button>
|
||||
@@ -88,7 +88,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<el-button size="small" @click="clearAvatarImportTable">
|
||||
{{ $t('dialog.avatar_import.clear_table') }}
|
||||
{{ t('dialog.avatar_import.clear_table') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
@@ -100,27 +100,27 @@
|
||||
!avatarImportDialog.avatarImportLocalFavoriteGroup)
|
||||
"
|
||||
@click="importAvatarImportTable">
|
||||
{{ $t('dialog.avatar_import.import') }}
|
||||
{{ t('dialog.avatar_import.import') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<span v-if="avatarImportDialog.importProgress" style="margin: 10px">
|
||||
<i class="el-icon-loading" style="margin-right: 5px"></i>
|
||||
{{ $t('dialog.avatar_import.import_progress') }}
|
||||
{{ t('dialog.avatar_import.import_progress') }}
|
||||
{{ avatarImportDialog.importProgress }}/{{ avatarImportDialog.importProgressTotal }}
|
||||
</span>
|
||||
<br />
|
||||
<template v-if="avatarImportDialog.errors">
|
||||
<el-button size="small" @click="avatarImportDialog.errors = ''">
|
||||
{{ $t('dialog.avatar_import.clear_errors') }}
|
||||
{{ t('dialog.avatar_import.clear_errors') }}
|
||||
</el-button>
|
||||
<h2 style="font-weight: bold; margin: 5px 0">
|
||||
{{ $t('dialog.avatar_import.errors') }}
|
||||
{{ t('dialog.avatar_import.errors') }}
|
||||
</h2>
|
||||
<pre style="white-space: pre-wrap; font-size: 12px" v-text="avatarImportDialog.errors"></pre>
|
||||
</template>
|
||||
<data-tables v-loading="avatarImportDialog.loading" v-bind="avatarImportTable" style="margin-top: 10px">
|
||||
<el-table-column :label="$t('table.import.image')" width="70" prop="thumbnailImageUrl">
|
||||
<el-table-column :label="t('table.import.image')" width="70" prop="thumbnailImageUrl">
|
||||
<template slot-scope="scope">
|
||||
<el-popover placement="right" height="500px" trigger="hover">
|
||||
<img slot="reference" v-lazy="scope.row.thumbnailImageUrl" class="friends-list-avatar" />
|
||||
@@ -132,21 +132,21 @@
|
||||
</el-popover>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('table.import.name')" prop="name">
|
||||
<el-table-column :label="t('table.import.name')" prop="name">
|
||||
<template slot-scope="scope">
|
||||
<span class="x-link" @click="showAvatarDialog(scope.row.id)">
|
||||
{{ scope.row.name }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('table.import.author')" width="120" prop="authorName">
|
||||
<el-table-column :label="t('table.import.author')" width="120" prop="authorName">
|
||||
<template slot-scope="scope">
|
||||
<span class="x-link" @click="showUserDialog(scope.row.authorId)">
|
||||
{{ scope.row.authorName }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('table.import.status')" width="70" prop="releaseStatus">
|
||||
<el-table-column :label="t('table.import.status')" width="70" prop="releaseStatus">
|
||||
<template slot-scope="scope">
|
||||
<span
|
||||
:style="{
|
||||
@@ -161,7 +161,7 @@
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('table.import.action')" width="90" align="right">
|
||||
<el-table-column :label="t('table.import.action')" width="90" align="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" icon="el-icon-close" size="mini" @click="deleteItemAvatarImport(scope.row)">
|
||||
</el-button>
|
||||
@@ -171,186 +171,191 @@
|
||||
</safe-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { ref, computed, watch, getCurrentInstance } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { avatarRequest, favoriteRequest } from '../../../api';
|
||||
import utils from '../../../classes/utils';
|
||||
import { adjustDialogZ, removeFromArray } from '../../../shared/utils';
|
||||
import { useAvatarStore, useFavoriteStore, useGalleryStore, useUserStore } from '../../../stores';
|
||||
|
||||
export default {
|
||||
name: 'AvatarImportDialog',
|
||||
inject: ['API', 'adjustDialogZ', 'showFullscreenImageDialog', 'showUserDialog', 'showAvatarDialog'],
|
||||
props: {
|
||||
getLocalAvatarFavoriteGroupLength: Function,
|
||||
localAvatarFavoriteGroups: Array,
|
||||
avatarImportDialogInput: String,
|
||||
avatarImportDialogVisible: Boolean
|
||||
const emit = defineEmits(['update:avatarImportDialogInput']);
|
||||
const { t } = useI18n();
|
||||
const { proxy } = getCurrentInstance();
|
||||
|
||||
const { showUserDialog } = useUserStore();
|
||||
const { favoriteAvatarGroups, avatarImportDialogInput, avatarImportDialogVisible, localAvatarFavoriteGroups } =
|
||||
storeToRefs(useFavoriteStore());
|
||||
const { addLocalAvatarFavorite, getLocalAvatarFavoriteGroupLength } = useFavoriteStore();
|
||||
const { showAvatarDialog, applyAvatar } = useAvatarStore();
|
||||
const { showFullscreenImageDialog } = useGalleryStore();
|
||||
|
||||
const avatarImportDialog = ref({
|
||||
loading: false,
|
||||
progress: 0,
|
||||
progressTotal: 0,
|
||||
input: '',
|
||||
avatarIdList: new Set(),
|
||||
errors: '',
|
||||
avatarImportFavoriteGroup: null,
|
||||
avatarImportLocalFavoriteGroup: null,
|
||||
importProgress: 0,
|
||||
importProgressTotal: 0
|
||||
});
|
||||
|
||||
const avatarImportTable = ref({
|
||||
data: [],
|
||||
tableProps: {
|
||||
stripe: true,
|
||||
size: 'mini'
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
avatarImportDialog: {
|
||||
loading: false,
|
||||
progress: 0,
|
||||
progressTotal: 0,
|
||||
input: '',
|
||||
avatarIdList: new Set(),
|
||||
errors: '',
|
||||
avatarImportFavoriteGroup: null,
|
||||
avatarImportLocalFavoriteGroup: null,
|
||||
importProgress: 0,
|
||||
importProgressTotal: 0
|
||||
},
|
||||
avatarImportTable: {
|
||||
data: [],
|
||||
tableProps: {
|
||||
stripe: true,
|
||||
size: 'mini'
|
||||
},
|
||||
layout: 'table'
|
||||
}
|
||||
};
|
||||
layout: 'table'
|
||||
});
|
||||
|
||||
const avatarImportDialogRef = ref(null);
|
||||
|
||||
const isVisible = computed({
|
||||
get() {
|
||||
return avatarImportDialogVisible.value;
|
||||
},
|
||||
computed: {
|
||||
isVisible: {
|
||||
get() {
|
||||
return this.avatarImportDialogVisible;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:avatar-import-dialog-visible', value);
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
avatarImportDialogVisible(value) {
|
||||
if (value) {
|
||||
this.adjustDialogZ(this.$refs.avatarImportDialog.$el);
|
||||
this.clearAvatarImportTable();
|
||||
this.resetAvatarImport();
|
||||
if (this.avatarImportDialogInput) {
|
||||
this.avatarImportDialog.input = this.avatarImportDialogInput;
|
||||
this.processAvatarImportList();
|
||||
this.$emit('update:avatar-import-dialog-input', '');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async processAvatarImportList() {
|
||||
const D = this.avatarImportDialog;
|
||||
D.loading = true;
|
||||
const regexAvatarId = /avtr_[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}/g;
|
||||
let match = [];
|
||||
const avatarIdList = new Set();
|
||||
while ((match = regexAvatarId.exec(D.input)) !== null) {
|
||||
avatarIdList.add(match[0]);
|
||||
}
|
||||
D.input = '';
|
||||
D.errors = '';
|
||||
D.progress = 0;
|
||||
D.progressTotal = avatarIdList.size;
|
||||
const data = Array.from(avatarIdList);
|
||||
for (let i = 0; i < data.length; ++i) {
|
||||
if (!this.isVisible) {
|
||||
this.resetAvatarImport();
|
||||
}
|
||||
if (!D.loading || !this.isVisible) {
|
||||
break;
|
||||
}
|
||||
const avatarId = data[i];
|
||||
if (!D.avatarIdList.has(avatarId)) {
|
||||
try {
|
||||
const args = await avatarRequest.getAvatar({
|
||||
avatarId
|
||||
});
|
||||
this.avatarImportTable.data.push(args.ref);
|
||||
D.avatarIdList.add(avatarId);
|
||||
} catch (err) {
|
||||
D.errors = D.errors.concat(`AvatarId: ${avatarId}\n${err}\n\n`);
|
||||
}
|
||||
}
|
||||
D.progress++;
|
||||
if (D.progress === avatarIdList.size) {
|
||||
D.progress = 0;
|
||||
}
|
||||
}
|
||||
D.loading = false;
|
||||
},
|
||||
set(value) {
|
||||
avatarImportDialogVisible.value = value;
|
||||
}
|
||||
});
|
||||
|
||||
deleteItemAvatarImport(ref) {
|
||||
utils.removeFromArray(this.avatarImportTable.data, ref);
|
||||
this.avatarImportDialog.avatarIdList.delete(ref.id);
|
||||
},
|
||||
|
||||
resetAvatarImport() {
|
||||
this.avatarImportDialog.input = '';
|
||||
this.avatarImportDialog.errors = '';
|
||||
},
|
||||
|
||||
clearAvatarImportTable() {
|
||||
this.avatarImportTable.data = [];
|
||||
this.avatarImportDialog.avatarIdList = new Set();
|
||||
},
|
||||
|
||||
selectAvatarImportGroup(group) {
|
||||
this.avatarImportDialog.avatarImportLocalFavoriteGroup = null;
|
||||
this.avatarImportDialog.avatarImportFavoriteGroup = group;
|
||||
},
|
||||
|
||||
selectAvatarImportLocalGroup(group) {
|
||||
this.avatarImportDialog.avatarImportFavoriteGroup = null;
|
||||
this.avatarImportDialog.avatarImportLocalFavoriteGroup = group;
|
||||
},
|
||||
|
||||
cancelAvatarImport() {
|
||||
this.avatarImportDialog.loading = false;
|
||||
},
|
||||
addFavoriteAvatar(ref, group, message) {
|
||||
return favoriteRequest
|
||||
.addFavorite({
|
||||
type: 'avatar',
|
||||
favoriteId: ref.id,
|
||||
tags: group.name
|
||||
})
|
||||
.then((args) => {
|
||||
if (message) {
|
||||
this.$message({
|
||||
message: 'Avatar added to favorites',
|
||||
type: 'success'
|
||||
});
|
||||
}
|
||||
return args;
|
||||
});
|
||||
},
|
||||
async importAvatarImportTable() {
|
||||
const D = this.avatarImportDialog;
|
||||
if (!D.avatarImportFavoriteGroup && !D.avatarImportLocalFavoriteGroup) {
|
||||
return;
|
||||
}
|
||||
D.loading = true;
|
||||
const data = [...this.avatarImportTable.data].reverse();
|
||||
D.importProgressTotal = data.length;
|
||||
let ref = '';
|
||||
try {
|
||||
for (let i = data.length - 1; i >= 0; i--) {
|
||||
if (!D.loading || !this.isVisible) {
|
||||
break;
|
||||
}
|
||||
ref = data[i];
|
||||
if (D.avatarImportFavoriteGroup) {
|
||||
await this.addFavoriteAvatar(ref, D.avatarImportFavoriteGroup, false);
|
||||
} else if (D.avatarImportLocalFavoriteGroup) {
|
||||
this.$emit('addLocalAvatarFavorite', ref.id, D.avatarImportLocalFavoriteGroup);
|
||||
}
|
||||
utils.removeFromArray(this.avatarImportTable.data, ref);
|
||||
D.avatarIdList.delete(ref.id);
|
||||
D.importProgress++;
|
||||
}
|
||||
} catch (err) {
|
||||
D.errors = `Name: ${ref.name}\nAvatarId: ${ref.id}\n${err}\n\n`;
|
||||
} finally {
|
||||
D.importProgress = 0;
|
||||
D.importProgressTotal = 0;
|
||||
D.loading = false;
|
||||
watch(
|
||||
() => avatarImportDialogVisible.value,
|
||||
(value) => {
|
||||
if (value) {
|
||||
adjustDialogZ(avatarImportDialogRef.value.$el);
|
||||
clearAvatarImportTable();
|
||||
resetAvatarImport();
|
||||
if (avatarImportDialogInput.value) {
|
||||
avatarImportDialog.value.input = avatarImportDialogInput.value;
|
||||
processAvatarImportList();
|
||||
emit('update:avatarImportDialogInput', '');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
);
|
||||
|
||||
async function processAvatarImportList() {
|
||||
const D = avatarImportDialog.value;
|
||||
D.loading = true;
|
||||
const regexAvatarId = /avtr_[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}/g;
|
||||
let match = [];
|
||||
const avatarIdList = new Set();
|
||||
while ((match = regexAvatarId.exec(D.input)) !== null) {
|
||||
avatarIdList.add(match[0]);
|
||||
}
|
||||
D.input = '';
|
||||
D.errors = '';
|
||||
D.progress = 0;
|
||||
D.progressTotal = avatarIdList.size;
|
||||
const data = Array.from(avatarIdList);
|
||||
for (let i = 0; i < data.length; ++i) {
|
||||
if (!isVisible.value) {
|
||||
resetAvatarImport();
|
||||
}
|
||||
if (!D.loading || !isVisible.value) {
|
||||
break;
|
||||
}
|
||||
const avatarId = data[i];
|
||||
if (!D.avatarIdList.has(avatarId)) {
|
||||
try {
|
||||
const args = await avatarRequest.getAvatar({
|
||||
avatarId
|
||||
});
|
||||
const ref = applyAvatar(args.json);
|
||||
avatarImportTable.value.data.push(ref);
|
||||
D.avatarIdList.add(avatarId);
|
||||
} catch (err) {
|
||||
D.errors = D.errors.concat(`AvatarId: ${avatarId}\n${err}\n\n`);
|
||||
}
|
||||
}
|
||||
D.progress++;
|
||||
if (D.progress === avatarIdList.size) {
|
||||
D.progress = 0;
|
||||
}
|
||||
}
|
||||
D.loading = false;
|
||||
}
|
||||
|
||||
function deleteItemAvatarImport(ref) {
|
||||
removeFromArray(avatarImportTable.value.data, ref);
|
||||
avatarImportDialog.value.avatarIdList.delete(ref.id);
|
||||
}
|
||||
|
||||
function resetAvatarImport() {
|
||||
avatarImportDialog.value.input = '';
|
||||
avatarImportDialog.value.errors = '';
|
||||
}
|
||||
|
||||
function clearAvatarImportTable() {
|
||||
avatarImportTable.value.data = [];
|
||||
avatarImportDialog.value.avatarIdList = new Set();
|
||||
}
|
||||
|
||||
function selectAvatarImportGroup(group) {
|
||||
avatarImportDialog.value.avatarImportLocalFavoriteGroup = null;
|
||||
avatarImportDialog.value.avatarImportFavoriteGroup = group;
|
||||
}
|
||||
|
||||
function selectAvatarImportLocalGroup(group) {
|
||||
avatarImportDialog.value.avatarImportFavoriteGroup = null;
|
||||
avatarImportDialog.value.avatarImportLocalFavoriteGroup = group;
|
||||
}
|
||||
|
||||
function cancelAvatarImport() {
|
||||
avatarImportDialog.value.loading = false;
|
||||
}
|
||||
function addFavoriteAvatar(ref, group, message) {
|
||||
return favoriteRequest
|
||||
.addFavorite({
|
||||
type: 'avatar',
|
||||
favoriteId: ref.id,
|
||||
tags: group.name
|
||||
})
|
||||
.then((args) => {
|
||||
if (message) {
|
||||
proxy.$message({
|
||||
message: 'Avatar added to favorites',
|
||||
type: 'success'
|
||||
});
|
||||
}
|
||||
return args;
|
||||
});
|
||||
}
|
||||
async function importAvatarImportTable() {
|
||||
const D = avatarImportDialog.value;
|
||||
if (!D.avatarImportFavoriteGroup && !D.avatarImportLocalFavoriteGroup) {
|
||||
return;
|
||||
}
|
||||
D.loading = true;
|
||||
const data = [...avatarImportTable.value.data].reverse();
|
||||
D.importProgressTotal = data.length;
|
||||
let ref = '';
|
||||
try {
|
||||
for (let i = data.length - 1; i >= 0; i--) {
|
||||
if (!D.loading || !isVisible.value) {
|
||||
break;
|
||||
}
|
||||
ref = data[i];
|
||||
if (D.avatarImportFavoriteGroup) {
|
||||
await addFavoriteAvatar(ref, D.avatarImportFavoriteGroup, false);
|
||||
} else if (D.avatarImportLocalFavoriteGroup) {
|
||||
addLocalAvatarFavorite(ref.id, D.avatarImportLocalFavoriteGroup);
|
||||
}
|
||||
removeFromArray(avatarImportTable.value.data, ref);
|
||||
D.avatarIdList.delete(ref.id);
|
||||
D.importProgress++;
|
||||
}
|
||||
} catch (err) {
|
||||
D.errors = `Name: ${ref.name}\nAvatarId: ${ref.id}\n${err}\n\n`;
|
||||
} finally {
|
||||
D.importProgress = 0;
|
||||
D.importProgressTotal = 0;
|
||||
D.loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<safe-dialog
|
||||
:visible.sync="isDialogVisible"
|
||||
class="x-dialog"
|
||||
:title="$t('dialog.friend_export.header')"
|
||||
:title="t('dialog.friend_export.header')"
|
||||
width="650px"
|
||||
destroy-on-close>
|
||||
<el-dropdown trigger="click" size="small" @click.native.stop>
|
||||
@@ -19,7 +19,7 @@
|
||||
<el-dropdown-item style="display: block; margin: 10px 0" @click.native="selectFriendExportGroup(null)">
|
||||
All Favorites
|
||||
</el-dropdown-item>
|
||||
<template v-for="groupAPI in API.favoriteFriendGroups">
|
||||
<template v-for="groupAPI in favoriteFriendGroups">
|
||||
<el-dropdown-item
|
||||
:key="groupAPI.name"
|
||||
style="display: block; margin: 10px 0"
|
||||
@@ -42,86 +42,94 @@
|
||||
</safe-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'FriendExportDialog',
|
||||
inject: ['API'],
|
||||
props: {
|
||||
friendExportDialogVisible: Boolean,
|
||||
favoriteFriends: Array
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
friendExportFavoriteGroup: null,
|
||||
friendExportContent: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isDialogVisible: {
|
||||
get() {
|
||||
return this.friendExportDialogVisible;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:friend-export-dialog-visible', value);
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
friendExportDialogVisible(value) {
|
||||
if (value) {
|
||||
this.showFriendExportDialog();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showFriendExportDialog() {
|
||||
this.friendExportFavoriteGroup = null;
|
||||
this.updateFriendExportDialog();
|
||||
},
|
||||
<script setup>
|
||||
import { ref, computed, watch, getCurrentInstance } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useFavoriteStore } from '../../../stores';
|
||||
|
||||
handleCopyFriendExportData(event) {
|
||||
if (event.target.tagName === 'TEXTAREA') {
|
||||
event.target.select();
|
||||
}
|
||||
navigator.clipboard
|
||||
.writeText(this.friendExportContent)
|
||||
.then(() => {
|
||||
this.$message({
|
||||
message: 'Copied successfully!',
|
||||
type: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Copy failed:', err);
|
||||
this.$message.error('Copy failed!');
|
||||
});
|
||||
},
|
||||
const { t } = useI18n();
|
||||
const { proxy } = getCurrentInstance();
|
||||
|
||||
updateFriendExportDialog() {
|
||||
const _ = function (str) {
|
||||
if (/[\x00-\x1f,"]/.test(str) === true) {
|
||||
return `"${str.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return str;
|
||||
};
|
||||
const lines = ['UserID,Name'];
|
||||
this.API.favoriteFriendGroups.forEach((group) => {
|
||||
if (!this.friendExportFavoriteGroup || this.friendExportFavoriteGroup === group) {
|
||||
this.favoriteFriends.forEach((ref) => {
|
||||
if (group.key === ref.groupKey) {
|
||||
lines.push(`${_(ref.id)},${_(ref.name)}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
this.friendExportContent = lines.join('\n');
|
||||
},
|
||||
const props = defineProps({
|
||||
friendExportDialogVisible: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
selectFriendExportGroup(group) {
|
||||
this.friendExportFavoriteGroup = group;
|
||||
this.updateFriendExportDialog();
|
||||
const emit = defineEmits(['update:friendExportDialogVisible']);
|
||||
|
||||
const favoriteStore = useFavoriteStore();
|
||||
const { favoriteFriends, favoriteFriendGroups } = storeToRefs(favoriteStore);
|
||||
|
||||
const friendExportFavoriteGroup = ref(null);
|
||||
const friendExportContent = ref('');
|
||||
|
||||
const isDialogVisible = computed({
|
||||
get() {
|
||||
return props.friendExportDialogVisible;
|
||||
},
|
||||
set(value) {
|
||||
emit('update:friendExportDialogVisible', value);
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.friendExportDialogVisible,
|
||||
(value) => {
|
||||
if (value) {
|
||||
showFriendExportDialog();
|
||||
}
|
||||
}
|
||||
};
|
||||
);
|
||||
|
||||
function showFriendExportDialog() {
|
||||
friendExportFavoriteGroup.value = null;
|
||||
updateFriendExportDialog();
|
||||
}
|
||||
|
||||
function handleCopyFriendExportData(event) {
|
||||
if (event.target.tagName === 'TEXTAREA') {
|
||||
event.target.select();
|
||||
}
|
||||
navigator.clipboard
|
||||
.writeText(friendExportContent.value)
|
||||
.then(() => {
|
||||
proxy.$message({
|
||||
message: 'Copied successfully!',
|
||||
type: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Copy failed:', err);
|
||||
proxy.$message.error('Copy failed!');
|
||||
});
|
||||
}
|
||||
|
||||
function updateFriendExportDialog() {
|
||||
const _ = function (str) {
|
||||
if (/[\x00-\x1f,"]/.test(str) === true) {
|
||||
return `"${str.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return str;
|
||||
};
|
||||
const lines = ['UserID,Name'];
|
||||
favoriteFriendGroups.value.forEach((group) => {
|
||||
if (!friendExportFavoriteGroup.value || friendExportFavoriteGroup.value === group) {
|
||||
favoriteFriends.value.forEach((ref) => {
|
||||
if (group.key === ref.groupKey) {
|
||||
lines.push(`${_(ref.id)},${_(ref.name)}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
friendExportContent.value = lines.join('\n');
|
||||
}
|
||||
|
||||
function selectFriendExportGroup(group) {
|
||||
friendExportFavoriteGroup.value = group;
|
||||
updateFriendExportDialog();
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
<template>
|
||||
<safe-dialog
|
||||
ref="friendImportDialog"
|
||||
ref="friendImportDialogRef"
|
||||
:visible.sync="isVisible"
|
||||
:title="$t('dialog.friend_import.header')"
|
||||
:title="t('dialog.friend_import.header')"
|
||||
width="650px">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
||||
<div style="font-size: 12px">{{ $t('dialog.friend_import.description') }}</div>
|
||||
<div style="font-size: 12px">{{ t('dialog.friend_import.description') }}</div>
|
||||
<div style="display: flex; align-items: center">
|
||||
<div v-if="friendImportDialog.progress">
|
||||
{{ $t('dialog.friend_import.process_progress') }} {{ friendImportDialog.progress }} /
|
||||
{{ t('dialog.friend_import.process_progress') }} {{ friendImportDialog.progress }} /
|
||||
{{ friendImportDialog.progressTotal }}
|
||||
<i class="el-icon-loading" style="margin: 0 5px"></i>
|
||||
</div>
|
||||
<el-button v-if="friendImportDialog.loading" size="small" @click="cancelFriendImport">
|
||||
{{ $t('dialog.friend_import.cancel') }}
|
||||
{{ t('dialog.friend_import.cancel') }}
|
||||
</el-button>
|
||||
<el-button v-else size="small" :disabled="!friendImportDialog.input" @click="processFriendImportList">
|
||||
{{ $t('dialog.friend_import.process_list') }}
|
||||
{{ t('dialog.friend_import.process_list') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -38,12 +38,12 @@
|
||||
<i class="el-icon-arrow-down el-icon--right"></i>
|
||||
</span>
|
||||
<span v-else
|
||||
>{{ $t('dialog.friend_import.select_group_placeholder') }}
|
||||
>{{ t('dialog.friend_import.select_group_placeholder') }}
|
||||
<i class="el-icon-arrow-down el-icon--right"></i
|
||||
></span>
|
||||
</el-button>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<template v-for="groupAPI in API.favoriteFriendGroups">
|
||||
<template v-for="groupAPI in favoriteFriendGroups">
|
||||
<el-dropdown-item
|
||||
:key="groupAPI.name"
|
||||
style="display: block; margin: 10px 0"
|
||||
@@ -64,7 +64,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<el-button size="small" :disabled="friendImportTable.data.length === 0" @click="clearFriendImportTable">
|
||||
{{ $t('dialog.friend_import.clear_table') }}
|
||||
{{ t('dialog.friend_import.clear_table') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
@@ -72,26 +72,26 @@
|
||||
style="margin: 5px"
|
||||
:disabled="friendImportTable.data.length === 0 || !friendImportDialog.friendImportFavoriteGroup"
|
||||
@click="importFriendImportTable">
|
||||
{{ $t('dialog.friend_import.import') }}
|
||||
{{ t('dialog.friend_import.import') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<span v-if="friendImportDialog.importProgress" style="margin: 10px">
|
||||
<i class="el-icon-loading" style="margin-right: 5px"></i>
|
||||
{{ $t('dialog.friend_import.import_progress') }} {{ friendImportDialog.importProgress }}/{{
|
||||
{{ t('dialog.friend_import.import_progress') }} {{ friendImportDialog.importProgress }}/{{
|
||||
friendImportDialog.importProgressTotal
|
||||
}}
|
||||
</span>
|
||||
<br />
|
||||
<template v-if="friendImportDialog.errors">
|
||||
<el-button size="small" @click="friendImportDialog.errors = ''">
|
||||
{{ $t('dialog.friend_import.clear_errors') }}
|
||||
{{ t('dialog.friend_import.clear_errors') }}
|
||||
</el-button>
|
||||
<h2 style="font-weight: bold; margin: 5px 0">{{ $t('dialog.friend_import.errors') }}</h2>
|
||||
<h2 style="font-weight: bold; margin: 5px 0">{{ t('dialog.friend_import.errors') }}</h2>
|
||||
<pre style="white-space: pre-wrap; font-size: 12px" v-text="friendImportDialog.errors"></pre>
|
||||
</template>
|
||||
<data-tables v-loading="friendImportDialog.loading" v-bind="friendImportTable" style="margin-top: 10px">
|
||||
<el-table-column :label="$t('table.import.image')" width="70" prop="currentAvatarThumbnailImageUrl">
|
||||
<el-table-column :label="t('table.import.image')" width="70" prop="currentAvatarThumbnailImageUrl">
|
||||
<template slot-scope="scope">
|
||||
<el-popover placement="right" height="500px" trigger="hover">
|
||||
<template slot="reference">
|
||||
@@ -105,14 +105,14 @@
|
||||
</el-popover>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('table.import.name')" prop="displayName">
|
||||
<el-table-column :label="t('table.import.name')" prop="displayName">
|
||||
<template slot-scope="scope">
|
||||
<span class="x-link" :title="scope.row.displayName" @click="showUserDialog(scope.row.id)">
|
||||
{{ scope.row.displayName }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('table.import.action')" width="90" align="right">
|
||||
<el-table-column :label="t('table.import.action')" width="90" align="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" icon="el-icon-close" size="mini" @click="deleteItemFriendImport(scope.row)">
|
||||
</el-button>
|
||||
@@ -122,175 +122,174 @@
|
||||
</safe-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import utils from '../../../classes/utils';
|
||||
<script setup>
|
||||
import { ref, computed, watch, getCurrentInstance } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { favoriteRequest, userRequest } from '../../../api';
|
||||
import { adjustDialogZ, removeFromArray, userImage, userImageFull } from '../../../shared/utils';
|
||||
import { useFavoriteStore, useGalleryStore, useUserStore } from '../../../stores';
|
||||
|
||||
export default {
|
||||
name: 'FriendImportDialog',
|
||||
inject: ['API', 'userImage', 'userImageFull', 'showFullscreenImageDialog', 'showUserDialog', 'adjustDialogZ'],
|
||||
props: {
|
||||
friendImportDialogVisible: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
friendImportDialogInput: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
}
|
||||
const { proxy } = getCurrentInstance();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const emit = defineEmits(['update:friendImportDialogInput']);
|
||||
|
||||
const { showUserDialog } = useUserStore();
|
||||
const { favoriteFriendGroups, friendImportDialogInput, friendImportDialogVisible } =
|
||||
storeToRefs(useFavoriteStore());
|
||||
const { showFullscreenImageDialog } = useGalleryStore();
|
||||
|
||||
const friendImportDialog = ref({
|
||||
loading: false,
|
||||
progress: 0,
|
||||
progressTotal: 0,
|
||||
input: '',
|
||||
userIdList: new Set(),
|
||||
errors: '',
|
||||
friendImportFavoriteGroup: null,
|
||||
importProgress: 0,
|
||||
importProgressTotal: 0
|
||||
});
|
||||
|
||||
const friendImportTable = ref({
|
||||
data: [],
|
||||
tableProps: {
|
||||
stripe: true,
|
||||
size: 'mini'
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
friendImportDialog: {
|
||||
loading: false,
|
||||
progress: 0,
|
||||
progressTotal: 0,
|
||||
input: '',
|
||||
userIdList: new Set(),
|
||||
errors: '',
|
||||
friendImportFavoriteGroup: null,
|
||||
importProgress: 0,
|
||||
importProgressTotal: 0
|
||||
},
|
||||
friendImportTable: {
|
||||
data: [],
|
||||
tableProps: {
|
||||
stripe: true,
|
||||
size: 'mini'
|
||||
},
|
||||
layout: 'table'
|
||||
}
|
||||
};
|
||||
layout: 'table'
|
||||
});
|
||||
|
||||
const friendImportDialogRef = ref(null);
|
||||
|
||||
const isVisible = computed({
|
||||
get() {
|
||||
return friendImportDialogVisible.value;
|
||||
},
|
||||
computed: {
|
||||
isVisible: {
|
||||
get() {
|
||||
return this.friendImportDialogVisible;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:friend-import-dialog-visible', value);
|
||||
set(value) {
|
||||
friendImportDialogVisible.value = value;
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => friendImportDialogVisible.value,
|
||||
(value) => {
|
||||
if (value) {
|
||||
adjustDialogZ(friendImportDialogRef.value.$el);
|
||||
clearFriendImportTable();
|
||||
resetFriendImport();
|
||||
if (friendImportDialogInput.value) {
|
||||
friendImportDialog.value.input = friendImportDialogInput.value;
|
||||
processFriendImportList();
|
||||
emit('update:friendImportDialogInput', '');
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
friendImportDialogVisible(value) {
|
||||
if (value) {
|
||||
this.adjustDialogZ(this.$refs.friendImportDialog.$el);
|
||||
this.clearFriendImportTable();
|
||||
this.resetFriendImport();
|
||||
if (this.friendImportDialogInput) {
|
||||
this.friendImportDialog.input = this.friendImportDialogInput;
|
||||
this.processFriendImportList();
|
||||
this.$emit('update:friend-import-dialog-input', '');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cancelFriendImport() {
|
||||
this.friendImportDialog.loading = false;
|
||||
},
|
||||
deleteItemFriendImport(ref) {
|
||||
utils.removeFromArray(this.friendImportTable.data, ref);
|
||||
this.friendImportDialog.userIdList.delete(ref.id);
|
||||
},
|
||||
clearFriendImportTable() {
|
||||
this.friendImportTable.data = [];
|
||||
this.friendImportDialog.userIdList = new Set();
|
||||
},
|
||||
selectFriendImportGroup(group) {
|
||||
this.friendImportDialog.friendImportFavoriteGroup = group;
|
||||
},
|
||||
async importFriendImportTable() {
|
||||
const D = this.friendImportDialog;
|
||||
D.loading = true;
|
||||
if (!D.friendImportFavoriteGroup) {
|
||||
return;
|
||||
}
|
||||
const data = [...this.friendImportTable.data].reverse();
|
||||
D.importProgressTotal = data.length;
|
||||
let ref = '';
|
||||
try {
|
||||
for (let i = data.length - 1; i >= 0; i--) {
|
||||
if (!D.loading || !this.isVisible) {
|
||||
break;
|
||||
}
|
||||
ref = data[i];
|
||||
await this.addFavoriteUser(ref, D.friendImportFavoriteGroup, false);
|
||||
utils.removeFromArray(this.friendImportTable.data, ref);
|
||||
D.userIdList.delete(ref.id);
|
||||
D.importProgress++;
|
||||
}
|
||||
} catch (err) {
|
||||
D.errors = `Name: ${ref.displayName}\nUserId: ${ref.id}\n${err}\n\n`;
|
||||
} finally {
|
||||
D.importProgress = 0;
|
||||
D.importProgressTotal = 0;
|
||||
D.loading = false;
|
||||
}
|
||||
},
|
||||
addFavoriteUser(ref, group, message) {
|
||||
return favoriteRequest
|
||||
.addFavorite({
|
||||
type: 'friend',
|
||||
favoriteId: ref.id,
|
||||
tags: group.name
|
||||
})
|
||||
.then((args) => {
|
||||
if (message) {
|
||||
this.$message({
|
||||
message: 'Friend added to favorites',
|
||||
type: 'success'
|
||||
});
|
||||
}
|
||||
return args;
|
||||
});
|
||||
},
|
||||
async processFriendImportList() {
|
||||
const D = this.friendImportDialog;
|
||||
D.loading = true;
|
||||
const regexFriendId = /usr_[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}/g;
|
||||
let match = [];
|
||||
const userIdList = new Set();
|
||||
while ((match = regexFriendId.exec(D.input)) !== null) {
|
||||
userIdList.add(match[0]);
|
||||
}
|
||||
D.input = '';
|
||||
D.errors = '';
|
||||
D.progress = 0;
|
||||
D.progressTotal = userIdList.size;
|
||||
const data = Array.from(userIdList);
|
||||
for (let i = 0; i < data.length; ++i) {
|
||||
if (!this.isVisible) {
|
||||
this.resetFriendImport();
|
||||
}
|
||||
if (!D.loading || !this.isVisible) {
|
||||
break;
|
||||
}
|
||||
const userId = data[i];
|
||||
if (!D.userIdList.has(userId)) {
|
||||
try {
|
||||
const args = await userRequest.getUser({
|
||||
userId
|
||||
});
|
||||
this.friendImportTable.data.push(args.ref);
|
||||
D.userIdList.add(userId);
|
||||
} catch (err) {
|
||||
D.errors = D.errors.concat(`UserId: ${userId}\n${err}\n\n`);
|
||||
}
|
||||
}
|
||||
D.progress++;
|
||||
if (D.progress === userIdList.size) {
|
||||
D.progress = 0;
|
||||
}
|
||||
}
|
||||
D.loading = false;
|
||||
},
|
||||
resetFriendImport() {
|
||||
this.friendImportDialog.input = '';
|
||||
this.friendImportDialog.errors = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
);
|
||||
|
||||
function cancelFriendImport() {
|
||||
friendImportDialog.value.loading = false;
|
||||
}
|
||||
function deleteItemFriendImport(ref) {
|
||||
removeFromArray(friendImportTable.value.data, ref);
|
||||
friendImportDialog.value.userIdList.delete(ref.id);
|
||||
}
|
||||
function clearFriendImportTable() {
|
||||
friendImportTable.value.data = [];
|
||||
friendImportDialog.value.userIdList = new Set();
|
||||
}
|
||||
function selectFriendImportGroup(group) {
|
||||
friendImportDialog.value.friendImportFavoriteGroup = group;
|
||||
}
|
||||
async function importFriendImportTable() {
|
||||
const D = friendImportDialog.value;
|
||||
D.loading = true;
|
||||
if (!D.friendImportFavoriteGroup) {
|
||||
return;
|
||||
}
|
||||
const data = [...friendImportTable.value.data].reverse();
|
||||
D.importProgressTotal = data.length;
|
||||
let ref = '';
|
||||
try {
|
||||
for (let i = data.length - 1; i >= 0; i--) {
|
||||
if (!D.loading || !isVisible.value) {
|
||||
break;
|
||||
}
|
||||
ref = data[i];
|
||||
await addFavoriteUser(ref, D.friendImportFavoriteGroup, false);
|
||||
removeFromArray(friendImportTable.value.data, ref);
|
||||
D.userIdList.delete(ref.id);
|
||||
D.importProgress++;
|
||||
}
|
||||
} catch (err) {
|
||||
D.errors = `Name: ${ref.displayName}\nUserId: ${ref.id}\n${err}\n\n`;
|
||||
} finally {
|
||||
D.importProgress = 0;
|
||||
D.importProgressTotal = 0;
|
||||
D.loading = false;
|
||||
}
|
||||
}
|
||||
function addFavoriteUser(ref, group, message) {
|
||||
return favoriteRequest
|
||||
.addFavorite({
|
||||
type: 'friend',
|
||||
favoriteId: ref.id,
|
||||
tags: group.name
|
||||
})
|
||||
.then((args) => {
|
||||
if (message) {
|
||||
proxy.$message({
|
||||
message: 'Friend added to favorites',
|
||||
type: 'success'
|
||||
});
|
||||
}
|
||||
return args;
|
||||
});
|
||||
}
|
||||
async function processFriendImportList() {
|
||||
const D = friendImportDialog.value;
|
||||
D.loading = true;
|
||||
const regexFriendId = /usr_[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}/g;
|
||||
let match = [];
|
||||
const userIdList = new Set();
|
||||
while ((match = regexFriendId.exec(D.input)) !== null) {
|
||||
userIdList.add(match[0]);
|
||||
}
|
||||
D.input = '';
|
||||
D.errors = '';
|
||||
D.progress = 0;
|
||||
D.progressTotal = userIdList.size;
|
||||
const data = Array.from(userIdList);
|
||||
for (let i = 0; i < data.length; ++i) {
|
||||
if (!isVisible.value) {
|
||||
resetFriendImport();
|
||||
}
|
||||
if (!D.loading || !isVisible.value) {
|
||||
break;
|
||||
}
|
||||
const userId = data[i];
|
||||
if (!D.userIdList.has(userId)) {
|
||||
try {
|
||||
const args = await userRequest.getUser({
|
||||
userId
|
||||
});
|
||||
friendImportTable.value.data.push(args.ref);
|
||||
D.userIdList.add(userId);
|
||||
} catch (err) {
|
||||
D.errors = D.errors.concat(`UserId: ${userId}\n${err}\n\n`);
|
||||
}
|
||||
}
|
||||
D.progress++;
|
||||
if (D.progress === userIdList.size) {
|
||||
D.progress = 0;
|
||||
}
|
||||
}
|
||||
D.loading = false;
|
||||
}
|
||||
function resetFriendImport() {
|
||||
friendImportDialog.value.input = '';
|
||||
friendImportDialog.value.errors = '';
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<safe-dialog :visible.sync="isDialogVisible" :title="$t('dialog.world_export.header')" width="650px">
|
||||
<safe-dialog :visible.sync="isDialogVisible" :title="t('dialog.world_export.header')" width="650px">
|
||||
<el-checkbox-group
|
||||
v-model="exportSelectedOptions"
|
||||
style="margin-bottom: 10px"
|
||||
@@ -26,7 +26,7 @@
|
||||
<el-dropdown-item style="display: block; margin: 10px 0" @click.native="selectWorldExportGroup(null)">
|
||||
None
|
||||
</el-dropdown-item>
|
||||
<template v-for="groupAPI in API.favoriteWorldGroups">
|
||||
<template v-for="groupAPI in favoriteWorldGroups">
|
||||
<el-dropdown-item
|
||||
:key="groupAPI.name"
|
||||
style="display: block; margin: 10px 0"
|
||||
@@ -81,151 +81,157 @@
|
||||
</safe-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'WorldExportDialog',
|
||||
inject: ['API'],
|
||||
props: {
|
||||
favoriteWorlds: Array,
|
||||
worldExportDialogVisible: Boolean,
|
||||
localWorldFavorites: Object,
|
||||
localWorldFavoriteGroups: Array,
|
||||
localWorldFavoritesList: Array
|
||||
<script setup>
|
||||
import { ref, computed, watch, getCurrentInstance } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useFavoriteStore, useWorldStore } from '../../../stores';
|
||||
|
||||
const props = defineProps({
|
||||
worldExportDialogVisible: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:WorldExportDialogVisible']);
|
||||
|
||||
const { t } = useI18n();
|
||||
const { proxy } = getCurrentInstance();
|
||||
|
||||
const favoriteStore = useFavoriteStore();
|
||||
const {
|
||||
favoriteWorlds,
|
||||
favoriteWorldGroups,
|
||||
localWorldFavorites,
|
||||
localWorldFavoriteGroups,
|
||||
localWorldFavoritesList
|
||||
} = storeToRefs(favoriteStore);
|
||||
const { getLocalWorldFavoriteGroupLength } = favoriteStore;
|
||||
const { cachedWorlds } = storeToRefs(useWorldStore());
|
||||
|
||||
const worldExportContent = ref('');
|
||||
const worldExportFavoriteGroup = ref(null);
|
||||
const worldExportLocalFavoriteGroup = ref(null);
|
||||
// Storage of selected filtering options for model and world export
|
||||
const exportSelectedOptions = ref(['ID', 'Name']);
|
||||
const exportSelectOptions = ref([
|
||||
{ label: 'ID', value: 'id' },
|
||||
{ label: 'Name', value: 'name' },
|
||||
{ label: 'Author ID', value: 'authorId' },
|
||||
{ label: 'Author Name', value: 'authorName' },
|
||||
{ label: 'Thumbnail', value: 'thumbnailImageUrl' }
|
||||
]);
|
||||
|
||||
const isDialogVisible = computed({
|
||||
get() {
|
||||
return props.worldExportDialogVisible;
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
worldExportContent: '',
|
||||
worldExportFavoriteGroup: null,
|
||||
worldExportLocalFavoriteGroup: null,
|
||||
// Storage of selected filtering options for model and world export
|
||||
exportSelectedOptions: ['ID', 'Name'],
|
||||
exportSelectOptions: [
|
||||
{ label: 'ID', value: 'id' },
|
||||
{ label: 'Name', value: 'name' },
|
||||
{ label: 'Author ID', value: 'authorId' },
|
||||
{ label: 'Author Name', value: 'authorName' },
|
||||
{ label: 'Thumbnail', value: 'thumbnailImageUrl' }
|
||||
]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isDialogVisible: {
|
||||
get() {
|
||||
return this.worldExportDialogVisible;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:world-export-dialog-visible', value);
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
worldExportDialogVisible(value) {
|
||||
if (value) {
|
||||
this.showWorldExportDialog();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showWorldExportDialog() {
|
||||
this.worldExportFavoriteGroup = null;
|
||||
this.worldExportLocalFavoriteGroup = null;
|
||||
this.updateWorldExportDialog();
|
||||
},
|
||||
set(value) {
|
||||
emit('update:WorldExportDialogVisible', value);
|
||||
}
|
||||
});
|
||||
|
||||
handleCopyWorldExportData(event) {
|
||||
if (event.target.tagName === 'TEXTAREA') {
|
||||
event.target.select();
|
||||
}
|
||||
navigator.clipboard
|
||||
.writeText(this.worldExportContent)
|
||||
.then(() => {
|
||||
this.$message({
|
||||
message: 'Copied successfully!',
|
||||
type: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Copy failed:', err);
|
||||
this.$message.error('Copy failed!');
|
||||
});
|
||||
},
|
||||
|
||||
updateWorldExportDialog() {
|
||||
const formatter = function (str) {
|
||||
if (/[\x00-\x1f,"]/.test(str) === true) {
|
||||
return `"${str.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return str;
|
||||
};
|
||||
|
||||
const propsForQuery = this.exportSelectOptions
|
||||
.filter((option) => this.exportSelectedOptions.includes(option.label))
|
||||
.map((option) => option.value);
|
||||
|
||||
function resText(ref) {
|
||||
let resArr = [];
|
||||
propsForQuery.forEach((e) => {
|
||||
resArr.push(formatter(ref?.[e]));
|
||||
});
|
||||
return resArr.join(',');
|
||||
}
|
||||
|
||||
const lines = [this.exportSelectedOptions.join(',')];
|
||||
|
||||
if (this.worldExportFavoriteGroup) {
|
||||
this.API.favoriteWorldGroups.forEach((group) => {
|
||||
if (this.worldExportFavoriteGroup === group) {
|
||||
this.favoriteWorlds.forEach((ref) => {
|
||||
if (group.key === ref.groupKey) {
|
||||
lines.push(resText(ref.ref));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (this.worldExportLocalFavoriteGroup) {
|
||||
const favoriteGroup = this.localWorldFavorites[this.worldExportLocalFavoriteGroup];
|
||||
if (!favoriteGroup) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < favoriteGroup.length; ++i) {
|
||||
const ref = favoriteGroup[i];
|
||||
lines.push(resText(ref));
|
||||
}
|
||||
} else {
|
||||
// export all
|
||||
this.favoriteWorlds.forEach((ref) => {
|
||||
lines.push(resText(ref.ref));
|
||||
});
|
||||
for (let i = 0; i < this.localWorldFavoritesList.length; ++i) {
|
||||
const worldId = this.localWorldFavoritesList[i];
|
||||
const ref = this.API.cachedWorlds.get(worldId);
|
||||
if (typeof ref !== 'undefined') {
|
||||
lines.push(resText(ref));
|
||||
}
|
||||
}
|
||||
}
|
||||
this.worldExportContent = lines.join('\n');
|
||||
},
|
||||
|
||||
selectWorldExportGroup(group) {
|
||||
this.worldExportFavoriteGroup = group;
|
||||
this.worldExportLocalFavoriteGroup = null;
|
||||
this.updateWorldExportDialog();
|
||||
},
|
||||
|
||||
selectWorldExportLocalGroup(group) {
|
||||
this.worldExportLocalFavoriteGroup = group;
|
||||
this.worldExportFavoriteGroup = null;
|
||||
this.updateWorldExportDialog();
|
||||
},
|
||||
getLocalWorldFavoriteGroupLength(group) {
|
||||
const favoriteGroup = this.localWorldFavorites[group];
|
||||
if (!favoriteGroup) {
|
||||
return 0;
|
||||
}
|
||||
return favoriteGroup.length;
|
||||
watch(
|
||||
() => props.worldExportDialogVisible,
|
||||
(value) => {
|
||||
if (value) {
|
||||
showWorldExportDialog();
|
||||
}
|
||||
}
|
||||
};
|
||||
);
|
||||
|
||||
function showWorldExportDialog() {
|
||||
worldExportFavoriteGroup.value = null;
|
||||
worldExportLocalFavoriteGroup.value = null;
|
||||
updateWorldExportDialog();
|
||||
}
|
||||
|
||||
function handleCopyWorldExportData(event) {
|
||||
if (event.target.tagName === 'TEXTAREA') {
|
||||
event.target.select();
|
||||
}
|
||||
navigator.clipboard
|
||||
.writeText(worldExportContent.value)
|
||||
.then(() => {
|
||||
proxy.$message({
|
||||
message: 'Copied successfully!',
|
||||
type: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Copy failed:', err);
|
||||
proxy.$message.error('Copy failed!');
|
||||
});
|
||||
}
|
||||
|
||||
function updateWorldExportDialog() {
|
||||
const formatter = function (str) {
|
||||
if (/[\x00-\x1f,"]/.test(str) === true) {
|
||||
return `"${str.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return str;
|
||||
};
|
||||
|
||||
const propsForQuery = exportSelectOptions.value
|
||||
.filter((option) => exportSelectedOptions.value.includes(option.label))
|
||||
.map((option) => option.value);
|
||||
|
||||
function resText(ref) {
|
||||
let resArr = [];
|
||||
propsForQuery.forEach((e) => {
|
||||
resArr.push(formatter(ref?.[e]));
|
||||
});
|
||||
return resArr.join(',');
|
||||
}
|
||||
|
||||
const lines = [exportSelectedOptions.value.join(',')];
|
||||
|
||||
if (worldExportFavoriteGroup.value) {
|
||||
favoriteWorldGroups.value.forEach((group) => {
|
||||
if (worldExportFavoriteGroup.value === group) {
|
||||
favoriteWorlds.value.forEach((ref) => {
|
||||
if (group.key === ref.groupKey) {
|
||||
lines.push(resText(ref.ref));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (worldExportLocalFavoriteGroup.value) {
|
||||
const favoriteGroup = localWorldFavorites.value[worldExportLocalFavoriteGroup.value];
|
||||
if (!favoriteGroup) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < favoriteGroup.length; ++i) {
|
||||
const ref = favoriteGroup[i];
|
||||
lines.push(resText(ref));
|
||||
}
|
||||
} else {
|
||||
// export all
|
||||
favoriteWorlds.value.forEach((ref) => {
|
||||
lines.push(resText(ref.ref));
|
||||
});
|
||||
for (let i = 0; i < localWorldFavoritesList.value.length; ++i) {
|
||||
const worldId = localWorldFavoritesList.value[i];
|
||||
const ref = cachedWorlds.value.get(worldId);
|
||||
if (typeof ref !== 'undefined') {
|
||||
lines.push(resText(ref));
|
||||
}
|
||||
}
|
||||
}
|
||||
worldExportContent.value = lines.join('\n');
|
||||
}
|
||||
|
||||
function selectWorldExportGroup(group) {
|
||||
worldExportFavoriteGroup.value = group;
|
||||
worldExportLocalFavoriteGroup.value = null;
|
||||
updateWorldExportDialog();
|
||||
}
|
||||
|
||||
function selectWorldExportLocalGroup(group) {
|
||||
worldExportLocalFavoriteGroup.value = group;
|
||||
worldExportFavoriteGroup.value = null;
|
||||
updateWorldExportDialog();
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
<template>
|
||||
<safe-dialog
|
||||
ref="worldImportDialog"
|
||||
ref="worldImportDialogRef"
|
||||
:visible.sync="isVisible"
|
||||
:title="$t('dialog.world_import.header')"
|
||||
:title="t('dialog.world_import.header')"
|
||||
width="650px"
|
||||
top="10vh"
|
||||
class="x-dialog">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
||||
<div style="font-size: 12px">{{ $t('dialog.world_import.description') }}</div>
|
||||
<div style="font-size: 12px">{{ t('dialog.world_import.description') }}</div>
|
||||
<div style="display: flex; align-items: center">
|
||||
<div v-if="worldImportDialog.progress">
|
||||
{{ $t('dialog.world_import.process_progress') }}
|
||||
{{ t('dialog.world_import.process_progress') }}
|
||||
{{ worldImportDialog.progress }} / {{ worldImportDialog.progressTotal }}
|
||||
<i class="el-icon-loading" style="margin: 0 5px"></i>
|
||||
</div>
|
||||
<el-button v-if="worldImportDialog.loading" size="small" @click="cancelWorldImport">
|
||||
{{ $t('dialog.world_import.cancel') }}
|
||||
{{ t('dialog.world_import.cancel') }}
|
||||
</el-button>
|
||||
<el-button v-else size="small" :disabled="!worldImportDialog.input" @click="processWorldImportList">
|
||||
{{ $t('dialog.world_import.process_list') }}
|
||||
{{ t('dialog.world_import.process_list') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -41,12 +41,12 @@
|
||||
<i class="el-icon-arrow-down el-icon--right"></i>
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ $t('dialog.world_import.select_vrchat_group_placeholder') }}
|
||||
{{ t('dialog.world_import.select_vrchat_group_placeholder') }}
|
||||
<i class="el-icon-arrow-down el-icon--right"></i>
|
||||
</span>
|
||||
</el-button>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<template v-for="groupAPI in API.favoriteWorldGroups">
|
||||
<template v-for="groupAPI in favoriteWorldGroups">
|
||||
<el-dropdown-item
|
||||
:key="groupAPI.name"
|
||||
style="display: block; margin: 10px 0"
|
||||
@@ -65,7 +65,7 @@
|
||||
<i class="el-icon-arrow-down el-icon--right"></i>
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ $t('dialog.world_import.select_local_group_placeholder') }}
|
||||
{{ t('dialog.world_import.select_local_group_placeholder') }}
|
||||
<i class="el-icon-arrow-down el-icon--right"></i>
|
||||
</span>
|
||||
</el-button>
|
||||
@@ -90,7 +90,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<el-button size="small" :disabled="worldImportTable.data.length === 0" @click="clearWorldImportTable">
|
||||
{{ $t('dialog.world_import.clear_table') }}
|
||||
{{ t('dialog.world_import.clear_table') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
@@ -102,27 +102,27 @@
|
||||
!worldImportDialog.worldImportLocalFavoriteGroup)
|
||||
"
|
||||
@click="importWorldImportTable">
|
||||
{{ $t('dialog.world_import.import') }}
|
||||
{{ t('dialog.world_import.import') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<span v-if="worldImportDialog.importProgress" style="margin: 10px">
|
||||
<i class="el-icon-loading" style="margin-right: 5px"></i>
|
||||
{{ $t('dialog.world_import.import_progress') }}
|
||||
{{ t('dialog.world_import.import_progress') }}
|
||||
{{ worldImportDialog.importProgress }}/{{ worldImportDialog.importProgressTotal }}
|
||||
</span>
|
||||
<br />
|
||||
<template v-if="worldImportDialog.errors">
|
||||
<el-button size="small" @click="worldImportDialog.errors = ''">
|
||||
{{ $t('dialog.world_import.clear_errors') }}
|
||||
{{ t('dialog.world_import.clear_errors') }}
|
||||
</el-button>
|
||||
<h2 style="font-weight: bold; margin: 5px 0">
|
||||
{{ $t('dialog.world_import.errors') }}
|
||||
{{ t('dialog.world_import.errors') }}
|
||||
</h2>
|
||||
<pre style="white-space: pre-wrap; font-size: 12px" v-text="worldImportDialog.errors"></pre>
|
||||
</template>
|
||||
<data-tables v-loading="worldImportDialog.loading" v-bind="worldImportTable" style="margin-top: 10px">
|
||||
<el-table-column :label="$t('table.import.image')" width="70" prop="thumbnailImageUrl">
|
||||
<el-table-column :label="t('table.import.image')" width="70" prop="thumbnailImageUrl">
|
||||
<template slot-scope="scope">
|
||||
<el-popover placement="right" height="500px" trigger="hover">
|
||||
<img slot="reference" v-lazy="scope.row.thumbnailImageUrl" class="friends-list-avatar" />
|
||||
@@ -134,12 +134,12 @@
|
||||
</el-popover>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('table.import.name')" prop="name">
|
||||
<el-table-column :label="t('table.import.name')" prop="name">
|
||||
<template slot-scope="scope">
|
||||
<span class="x-link" @click="showWorldDialog(scope.row.id)" v-text="scope.row.name"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('table.import.author')" width="120" prop="authorName">
|
||||
<el-table-column :label="t('table.import.author')" width="120" prop="authorName">
|
||||
<template slot-scope="scope">
|
||||
<span
|
||||
class="x-link"
|
||||
@@ -147,7 +147,7 @@
|
||||
v-text="scope.row.authorName"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('table.import.status')" width="70" prop="releaseStatus">
|
||||
<el-table-column :label="t('table.import.status')" width="70" prop="releaseStatus">
|
||||
<template slot-scope="scope">
|
||||
<span
|
||||
:style="{
|
||||
@@ -163,7 +163,7 @@
|
||||
"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('table.import.action')" width="90" align="right">
|
||||
<el-table-column :label="t('table.import.action')" width="90" align="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
type="text"
|
||||
@@ -176,185 +176,193 @@
|
||||
</safe-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { ref, watch, computed, getCurrentInstance } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { favoriteRequest, worldRequest } from '../../../api';
|
||||
import utils from '../../../classes/utils';
|
||||
import { adjustDialogZ, removeFromArray } from '../../../shared/utils';
|
||||
import { useFavoriteStore, useGalleryStore, useUserStore, useWorldStore } from '../../../stores';
|
||||
|
||||
export default {
|
||||
name: 'WorldImportDialog',
|
||||
inject: ['API', 'showFullscreenImageDialog', 'showUserDialog', 'adjustDialogZ', 'showWorldDialog'],
|
||||
props: {
|
||||
worldImportDialogVisible: Boolean,
|
||||
worldImportDialogInput: String,
|
||||
getLocalWorldFavoriteGroupLength: Function,
|
||||
localWorldFavoriteGroups: Array
|
||||
const { showUserDialog } = useUserStore();
|
||||
const { favoriteWorldGroups, worldImportDialogInput, worldImportDialogVisible, localWorldFavoriteGroups } =
|
||||
storeToRefs(useFavoriteStore());
|
||||
const { getLocalWorldFavoriteGroupLength, addLocalWorldFavorite } = useFavoriteStore();
|
||||
const { showWorldDialog } = useWorldStore();
|
||||
const { showFullscreenImageDialog } = useGalleryStore();
|
||||
|
||||
const emit = defineEmits(['update:worldImportDialogInput']);
|
||||
|
||||
const { proxy } = getCurrentInstance();
|
||||
const { t } = useI18n();
|
||||
|
||||
const worldImportDialogRef = ref(null);
|
||||
|
||||
const worldImportDialog = ref({
|
||||
loading: false,
|
||||
progress: 0,
|
||||
progressTotal: 0,
|
||||
input: '',
|
||||
worldIdList: new Set(),
|
||||
errors: '',
|
||||
worldImportFavoriteGroup: null,
|
||||
worldImportLocalFavoriteGroup: null,
|
||||
importProgress: 0,
|
||||
importProgressTotal: 0
|
||||
});
|
||||
|
||||
const worldImportTable = ref({
|
||||
data: [],
|
||||
tableProps: {
|
||||
stripe: true,
|
||||
size: 'mini'
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
worldImportDialog: {
|
||||
loading: false,
|
||||
progress: 0,
|
||||
progressTotal: 0,
|
||||
input: '',
|
||||
worldIdList: new Set(),
|
||||
errors: '',
|
||||
worldImportFavoriteGroup: null,
|
||||
worldImportLocalFavoriteGroup: null,
|
||||
importProgress: 0,
|
||||
importProgressTotal: 0
|
||||
},
|
||||
worldImportTable: {
|
||||
data: [],
|
||||
tableProps: {
|
||||
stripe: true,
|
||||
size: 'mini'
|
||||
},
|
||||
layout: 'table'
|
||||
}
|
||||
};
|
||||
layout: 'table'
|
||||
});
|
||||
|
||||
const isVisible = computed({
|
||||
get() {
|
||||
return worldImportDialogVisible.value;
|
||||
},
|
||||
computed: {
|
||||
isVisible: {
|
||||
get() {
|
||||
return this.worldImportDialogVisible;
|
||||
},
|
||||
set(visible) {
|
||||
this.$emit('update:world-import-dialog-visible', visible);
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
worldImportDialogVisible(visible) {
|
||||
if (visible) {
|
||||
this.adjustDialogZ(this.$refs.worldImportDialog.$el);
|
||||
this.clearWorldImportTable();
|
||||
this.resetWorldImport();
|
||||
if (this.worldImportDialogInput) {
|
||||
this.worldImportDialog.input = this.worldImportDialogInput;
|
||||
this.processWorldImportList();
|
||||
this.$emit('update:world-import-dialog-input', '');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
resetWorldImport() {
|
||||
this.worldImportDialog.input = '';
|
||||
this.worldImportDialog.errors = '';
|
||||
},
|
||||
async processWorldImportList() {
|
||||
const D = this.worldImportDialog;
|
||||
D.loading = true;
|
||||
const regexWorldId = /wrld_[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}/g;
|
||||
let match = [];
|
||||
const worldIdList = new Set();
|
||||
while ((match = regexWorldId.exec(D.input)) !== null) {
|
||||
worldIdList.add(match[0]);
|
||||
}
|
||||
D.input = '';
|
||||
D.errors = '';
|
||||
D.progress = 0;
|
||||
D.progressTotal = worldIdList.size;
|
||||
const data = Array.from(worldIdList);
|
||||
for (let i = 0; i < data.length; ++i) {
|
||||
if (!this.isVisible) {
|
||||
this.resetWorldImport();
|
||||
}
|
||||
if (!D.loading || !this.isVisible) {
|
||||
break;
|
||||
}
|
||||
const worldId = data[i];
|
||||
if (!D.worldIdList.has(worldId)) {
|
||||
try {
|
||||
const args = await worldRequest.getWorld({
|
||||
worldId
|
||||
});
|
||||
this.worldImportTable.data.push(args.ref);
|
||||
D.worldIdList.add(worldId);
|
||||
} catch (err) {
|
||||
D.errors = D.errors.concat(`WorldId: ${worldId}\n${err}\n\n`);
|
||||
}
|
||||
}
|
||||
D.progress++;
|
||||
if (D.progress === worldIdList.size) {
|
||||
D.progress = 0;
|
||||
}
|
||||
}
|
||||
D.loading = false;
|
||||
},
|
||||
deleteItemWorldImport(ref) {
|
||||
utils.removeFromArray(this.worldImportTable.data, ref);
|
||||
this.worldImportDialog.worldIdList.delete(ref.id);
|
||||
},
|
||||
set(visible) {
|
||||
worldImportDialogVisible.value = visible;
|
||||
}
|
||||
});
|
||||
|
||||
clearWorldImportTable() {
|
||||
this.worldImportTable.data = [];
|
||||
this.worldImportDialog.worldIdList = new Set();
|
||||
},
|
||||
|
||||
selectWorldImportGroup(group) {
|
||||
this.worldImportDialog.worldImportLocalFavoriteGroup = null;
|
||||
this.worldImportDialog.worldImportFavoriteGroup = group;
|
||||
},
|
||||
|
||||
selectWorldImportLocalGroup(group) {
|
||||
this.worldImportDialog.worldImportFavoriteGroup = null;
|
||||
this.worldImportDialog.worldImportLocalFavoriteGroup = group;
|
||||
},
|
||||
|
||||
cancelWorldImport() {
|
||||
this.worldImportDialog.loading = false;
|
||||
},
|
||||
|
||||
async importWorldImportTable() {
|
||||
const D = this.worldImportDialog;
|
||||
if (!D.worldImportFavoriteGroup && !D.worldImportLocalFavoriteGroup) {
|
||||
return;
|
||||
watch(
|
||||
() => worldImportDialogVisible.value,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
adjustDialogZ(worldImportDialogRef.value.$el);
|
||||
clearWorldImportTable();
|
||||
resetWorldImport();
|
||||
if (worldImportDialogInput.value) {
|
||||
worldImportDialog.value.input = worldImportDialogInput.value;
|
||||
processWorldImportList();
|
||||
emit('update:worldImportDialogInput', '');
|
||||
}
|
||||
D.loading = true;
|
||||
const data = [...this.worldImportTable.data].reverse();
|
||||
D.importProgressTotal = data.length;
|
||||
let ref = '';
|
||||
try {
|
||||
for (let i = data.length - 1; i >= 0; i--) {
|
||||
if (!D.loading || !this.isVisible) {
|
||||
break;
|
||||
}
|
||||
ref = data[i];
|
||||
if (D.worldImportFavoriteGroup) {
|
||||
await this.addFavoriteWorld(ref, D.worldImportFavoriteGroup, false);
|
||||
} else if (D.worldImportLocalFavoriteGroup) {
|
||||
this.$emit('addLocalWorldFavorite', ref.id, D.worldImportLocalFavoriteGroup);
|
||||
}
|
||||
utils.removeFromArray(this.worldImportTable.data, ref);
|
||||
D.worldIdList.delete(ref.id);
|
||||
D.importProgress++;
|
||||
}
|
||||
} catch (err) {
|
||||
D.errors = `Name: ${ref.name}\nWorldId: ${ref.id}\n${err}\n\n`;
|
||||
} finally {
|
||||
D.importProgress = 0;
|
||||
D.importProgressTotal = 0;
|
||||
D.loading = false;
|
||||
}
|
||||
},
|
||||
addFavoriteWorld(ref, group, message) {
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
);
|
||||
|
||||
function resetWorldImport() {
|
||||
worldImportDialog.value.input = '';
|
||||
worldImportDialog.value.errors = '';
|
||||
}
|
||||
|
||||
async function processWorldImportList() {
|
||||
const D = worldImportDialog.value;
|
||||
D.loading = true;
|
||||
const regexWorldId = /wrld_[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}/g;
|
||||
let match = [];
|
||||
const worldIdList = new Set();
|
||||
while ((match = regexWorldId.exec(D.input)) !== null) {
|
||||
worldIdList.add(match[0]);
|
||||
}
|
||||
D.input = '';
|
||||
D.errors = '';
|
||||
D.progress = 0;
|
||||
D.progressTotal = worldIdList.size;
|
||||
const data = Array.from(worldIdList);
|
||||
for (let i = 0; i < data.length; ++i) {
|
||||
if (!isVisible.value) {
|
||||
resetWorldImport();
|
||||
}
|
||||
if (!D.loading || !isVisible.value) {
|
||||
break;
|
||||
}
|
||||
const worldId = data[i];
|
||||
if (!D.worldIdList.has(worldId)) {
|
||||
try {
|
||||
const args = await worldRequest.getWorld({
|
||||
worldId
|
||||
});
|
||||
worldImportTable.value.data.push(args.ref);
|
||||
D.worldIdList.add(worldId);
|
||||
} catch (err) {
|
||||
D.errors = D.errors.concat(`WorldId: ${worldId}\n${err}\n\n`);
|
||||
}
|
||||
}
|
||||
D.progress++;
|
||||
if (D.progress === worldIdList.size) {
|
||||
D.progress = 0;
|
||||
}
|
||||
}
|
||||
D.loading = false;
|
||||
}
|
||||
|
||||
function deleteItemWorldImport(ref) {
|
||||
removeFromArray(worldImportTable.value.data, ref);
|
||||
worldImportDialog.value.worldIdList.delete(ref.id);
|
||||
}
|
||||
|
||||
function clearWorldImportTable() {
|
||||
worldImportTable.value.data = [];
|
||||
worldImportDialog.value.worldIdList = new Set();
|
||||
}
|
||||
|
||||
function selectWorldImportGroup(group) {
|
||||
worldImportDialog.value.worldImportLocalFavoriteGroup = null;
|
||||
worldImportDialog.value.worldImportFavoriteGroup = group;
|
||||
}
|
||||
|
||||
function selectWorldImportLocalGroup(group) {
|
||||
worldImportDialog.value.worldImportFavoriteGroup = null;
|
||||
worldImportDialog.value.worldImportLocalFavoriteGroup = group;
|
||||
}
|
||||
|
||||
function cancelWorldImport() {
|
||||
worldImportDialog.value.loading = false;
|
||||
}
|
||||
|
||||
async function importWorldImportTable() {
|
||||
const D = worldImportDialog.value;
|
||||
if (!D.worldImportFavoriteGroup && !D.worldImportLocalFavoriteGroup) {
|
||||
return;
|
||||
}
|
||||
D.loading = true;
|
||||
const data = [...worldImportTable.value.data].reverse();
|
||||
D.importProgressTotal = data.length;
|
||||
let ref = '';
|
||||
try {
|
||||
for (let i = data.length - 1; i >= 0; i--) {
|
||||
if (!D.loading || !isVisible.value) {
|
||||
break;
|
||||
}
|
||||
ref = data[i];
|
||||
if (D.worldImportFavoriteGroup) {
|
||||
await addFavoriteWorld(ref, D.worldImportFavoriteGroup, false);
|
||||
} else if (D.worldImportLocalFavoriteGroup) {
|
||||
addLocalWorldFavorite(ref, D.worldImportLocalFavoriteGroup);
|
||||
}
|
||||
removeFromArray(worldImportTable.value.data, ref);
|
||||
D.worldIdList.delete(ref.id);
|
||||
D.importProgress++;
|
||||
}
|
||||
} catch (err) {
|
||||
D.errors = `Name: ${ref.name}\nWorldId: ${ref.id}\n${err}\n\n`;
|
||||
} finally {
|
||||
D.importProgress = 0;
|
||||
D.importProgressTotal = 0;
|
||||
D.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function addFavoriteWorld(ref, group, message) {
|
||||
return favoriteRequest
|
||||
.addFavorite({
|
||||
type: 'world',
|
||||
favoriteId: ref.id,
|
||||
tags: group.name
|
||||
})
|
||||
.then((args) => {
|
||||
if (message) {
|
||||
proxy.$message({
|
||||
message: 'World added to favorites',
|
||||
type: 'success'
|
||||
});
|
||||
}
|
||||
return args;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
v-model="feedTable.filter"
|
||||
multiple
|
||||
clearable
|
||||
style="flex: 1; height: 40px"
|
||||
style="flex: 1"
|
||||
:placeholder="t('view.feed.filter_placeholder')"
|
||||
@change="feedTableLookup">
|
||||
<el-option
|
||||
@@ -26,7 +26,7 @@
|
||||
v-model="feedTable.search"
|
||||
:placeholder="t('view.feed.search_placeholder')"
|
||||
clearable
|
||||
style="flex: none; width: 150px; margin: 0 10px"
|
||||
style="flex: none; width: 150px; margin-left: 10px"
|
||||
@keyup.native.13="feedTableLookup"
|
||||
@change="feedTableLookup"></el-input>
|
||||
</div>
|
||||
@@ -36,10 +36,10 @@
|
||||
<template #default="scope">
|
||||
<div style="position: relative; font-size: 14px">
|
||||
<template v-if="scope.row.type === 'GPS'">
|
||||
<location
|
||||
<Location
|
||||
v-if="scope.row.previousLocation"
|
||||
:location="scope.row.previousLocation"
|
||||
style="display: inline-block"></location>
|
||||
style="display: inline-block" />
|
||||
<el-tag type="info" effect="plain" size="mini" style="margin-left: 5px">{{
|
||||
timeToText(scope.row.time)
|
||||
}}</el-tag>
|
||||
@@ -47,29 +47,29 @@
|
||||
<span style="margin-right: 5px">
|
||||
<i class="el-icon-right"></i>
|
||||
</span>
|
||||
<location
|
||||
<Location
|
||||
v-if="scope.row.location"
|
||||
:location="scope.row.location"
|
||||
:hint="scope.row.worldName"
|
||||
:grouphint="scope.row.groupName"></location>
|
||||
:grouphint="scope.row.groupName" />
|
||||
</template>
|
||||
<template v-else-if="scope.row.type === 'Offline'">
|
||||
<template v-if="scope.row.location">
|
||||
<location
|
||||
<Location
|
||||
:location="scope.row.location"
|
||||
:hint="scope.row.worldName"
|
||||
:grouphint="scope.row.groupName"></location>
|
||||
:grouphint="scope.row.groupName" />
|
||||
<el-tag type="info" effect="plain" size="mini" style="margin-left: 5px">{{
|
||||
timeToText(scope.row.time)
|
||||
}}</el-tag>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else-if="scope.row.type === 'Online'">
|
||||
<location
|
||||
<Location
|
||||
v-if="scope.row.location"
|
||||
:location="scope.row.location"
|
||||
:hint="scope.row.worldName"
|
||||
:grouphint="scope.row.groupName"></location>
|
||||
:grouphint="scope.row.groupName" />
|
||||
</template>
|
||||
<template v-else-if="scope.row.type === 'Avatar'">
|
||||
<div style="display: flex; align-items: center">
|
||||
@@ -83,12 +83,12 @@
|
||||
class="x-link"
|
||||
style="flex: none; width: 160px; height: 120px; border-radius: 4px" />
|
||||
<br />
|
||||
<avatar-info
|
||||
<AvatarInfo
|
||||
:imageurl="scope.row.previousCurrentAvatarThumbnailImageUrl"
|
||||
:userid="scope.row.userId"
|
||||
:hintownerid="scope.row.previousOwnerId"
|
||||
:hintavatarname="scope.row.previousAvatarName"
|
||||
:avatartags="scope.row.previousCurrentAvatarTags"></avatar-info>
|
||||
:avatartags="scope.row.previousCurrentAvatarTags" />
|
||||
</template>
|
||||
</div>
|
||||
<img
|
||||
@@ -110,12 +110,12 @@
|
||||
class="x-link"
|
||||
style="flex: none; width: 160px; height: 120px; border-radius: 4px" />
|
||||
<br />
|
||||
<avatar-info
|
||||
<AvatarInfo
|
||||
:imageurl="scope.row.currentAvatarThumbnailImageUrl"
|
||||
:userid="scope.row.userId"
|
||||
:hintownerid="scope.row.ownerId"
|
||||
:hintavatarname="scope.row.avatarName"
|
||||
:avatartags="scope.row.currentAvatarTags"></avatar-info>
|
||||
:avatartags="scope.row.currentAvatarTags" />
|
||||
</template>
|
||||
</div>
|
||||
<img
|
||||
@@ -175,13 +175,7 @@
|
||||
</template>
|
||||
<template v-else-if="scope.row.type === 'Bio'">
|
||||
<pre
|
||||
style="
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
line-height: 25px;
|
||||
line-height: 22px;
|
||||
"
|
||||
style="font-family: inherit; font-size: 12px; white-space: pre-wrap; line-height: 22px"
|
||||
v-html="formatDifference(scope.row.previousBio, scope.row.bio)"></pre>
|
||||
</template>
|
||||
</div>
|
||||
@@ -192,9 +186,9 @@
|
||||
<template #default="scope">
|
||||
<el-tooltip placement="right">
|
||||
<template #content>
|
||||
<span>{{ scope.row.created_at | formatDate('long') }}</span>
|
||||
<span>{{ formatDateFilter(scope.row.created_at, 'long') }}</span>
|
||||
</template>
|
||||
<span>{{ scope.row.created_at | formatDate('short') }}</span>
|
||||
<span>{{ formatDateFilter(scope.row.created_at, 'short') }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -218,18 +212,18 @@
|
||||
<el-table-column :label="t('table.feed.detail')">
|
||||
<template #default="scope">
|
||||
<template v-if="scope.row.type === 'GPS'">
|
||||
<location
|
||||
<Location
|
||||
v-if="scope.row.location"
|
||||
:location="scope.row.location"
|
||||
:hint="scope.row.worldName"
|
||||
:grouphint="scope.row.groupName"></location>
|
||||
:grouphint="scope.row.groupName" />
|
||||
</template>
|
||||
<template v-else-if="scope.row.type === 'Offline' || scope.row.type === 'Online'">
|
||||
<location
|
||||
<Location
|
||||
v-if="scope.row.location"
|
||||
:location="scope.row.location"
|
||||
:hint="scope.row.worldName"
|
||||
:grouphint="scope.row.groupName"></location>
|
||||
:grouphint="scope.row.groupName" />
|
||||
</template>
|
||||
<template v-else-if="scope.row.type === 'Status'">
|
||||
<template v-if="scope.row.statusDescription === scope.row.previousStatusDescription">
|
||||
@@ -299,12 +293,12 @@
|
||||
</template>
|
||||
</template>
|
||||
<template v-else-if="scope.row.type === 'Avatar'">
|
||||
<avatar-info
|
||||
<AvatarInfo
|
||||
:imageurl="scope.row.currentAvatarImageUrl"
|
||||
:userid="scope.row.userId"
|
||||
:hintownerid="scope.row.ownerId"
|
||||
:hintavatarname="scope.row.avatarName"
|
||||
:avatartags="scope.row.currentAvatarTags"></avatar-info>
|
||||
:avatartags="scope.row.currentAvatarTags" />
|
||||
</template>
|
||||
<template v-else-if="scope.row.type === 'Bio'">
|
||||
<span v-text="scope.row.bio"></span>
|
||||
@@ -315,41 +309,21 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'FeedTab'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import utils from '../../classes/utils';
|
||||
import Location from '../../components/Location.vue';
|
||||
import { useGalleryStore, useAppearanceSettingsStore, useUserStore, useFeedStore, useUiStore } from '../../stores';
|
||||
import { timeToText, statusClass, formatDateFilter } from '../../shared/utils';
|
||||
|
||||
const { hideTooltips } = storeToRefs(useAppearanceSettingsStore());
|
||||
const { showUserDialog } = useUserStore();
|
||||
const { feedTable } = storeToRefs(useFeedStore());
|
||||
const { feedTableLookup } = useFeedStore();
|
||||
const { menuActiveIndex } = storeToRefs(useUiStore());
|
||||
const { showFullscreenImageDialog } = useGalleryStore();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const showFullscreenImageDialog = inject('showFullscreenImageDialog');
|
||||
const statusClass = inject('statusClass');
|
||||
const showUserDialog = inject('showUserDialog');
|
||||
|
||||
defineProps({
|
||||
menuActiveIndex: {
|
||||
type: String,
|
||||
default: 'feed'
|
||||
},
|
||||
hideTooltips: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
feedTable: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['feedTableLookup']);
|
||||
|
||||
/**
|
||||
* Function that format the differences between two strings with HTML tags
|
||||
* markerStartTag and markerEndTag are optional, if emitted, the differences will be highlighted with yellow and underlined.
|
||||
@@ -464,12 +438,4 @@
|
||||
.replace(/<br>[ ]+<br>/g, '<br><br>')
|
||||
.replace(/<br> /g, '<br>');
|
||||
}
|
||||
|
||||
function feedTableLookup() {
|
||||
emit('feedTableLookup');
|
||||
}
|
||||
|
||||
function timeToText(time) {
|
||||
return utils.timeToText(time);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -2,27 +2,27 @@
|
||||
<div v-show="menuActiveIndex === 'friendList'" class="x-container">
|
||||
<div style="padding: 0 10px 0 10px">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
||||
<span class="header">{{ $t('view.friend_list.header') }}</span>
|
||||
<div style="font-size: 13px">
|
||||
<span class="header">{{ t('view.friend_list.header') }}</span>
|
||||
<div style="font-size: 13px; display: flex; align-items: center">
|
||||
<div v-if="friendsListBulkUnfriendMode" style="display: inline-block; margin-right: 10px">
|
||||
<el-button size="small" @click="showBulkUnfriendSelectionConfirm">
|
||||
{{ $t('view.friend_list.bulk_unfriend_selection') }}
|
||||
{{ t('view.friend_list.bulk_unfriend_selection') }}
|
||||
</el-button>
|
||||
<!-- el-button(size="small" @click="showBulkUnfriendAllConfirm" style="margin-right:5px") Bulk Unfriend All-->
|
||||
</div>
|
||||
<div style="display: inline-block; margin-right: 10px">
|
||||
<span class="name">{{ $t('view.friend_list.bulk_unfriend') }}</span>
|
||||
<div style="display: flex; align-items: center; margin-right: 10px">
|
||||
<span class="name">{{ t('view.friend_list.bulk_unfriend') }}</span>
|
||||
<el-switch
|
||||
v-model="friendsListBulkUnfriendMode"
|
||||
style="margin-left: 5px"
|
||||
@change="toggleFriendsListBulkUnfriendMode"></el-switch>
|
||||
</div>
|
||||
<span>{{ $t('view.friend_list.load') }}</span>
|
||||
<span>{{ t('view.friend_list.load') }}</span>
|
||||
<template v-if="friendsListLoading">
|
||||
<span style="margin-left: 5px" v-text="friendsListLoadingProgress"></span>
|
||||
<el-tooltip
|
||||
placement="top"
|
||||
:content="$t('view.friend_list.cancel_tooltip')"
|
||||
:content="t('view.friend_list.cancel_tooltip')"
|
||||
:disabled="hideTooltips">
|
||||
<el-button
|
||||
size="mini"
|
||||
@@ -35,7 +35,7 @@
|
||||
<template v-else>
|
||||
<el-tooltip
|
||||
placement="top"
|
||||
:content="$t('view.friend_list.load_tooltip')"
|
||||
:content="t('view.friend_list.load_tooltip')"
|
||||
:disabled="hideTooltips">
|
||||
<el-button
|
||||
size="mini"
|
||||
@@ -52,7 +52,7 @@
|
||||
<div style="flex: none; margin-right: 10px; display: flex; align-items: center">
|
||||
<el-tooltip
|
||||
placement="bottom"
|
||||
:content="$t('view.friend_list.favorites_only_tooltip')"
|
||||
:content="t('view.friend_list.favorites_only_tooltip')"
|
||||
:disabled="hideTooltips">
|
||||
<el-switch
|
||||
v-model="friendsListSearchFilterVIP"
|
||||
@@ -61,11 +61,10 @@
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-input
|
||||
:value="friendsListSearch"
|
||||
:placeholder="$t('view.friend_list.search_placeholder')"
|
||||
v-model="friendsListSearch"
|
||||
:placeholder="t('view.friend_list.search_placeholder')"
|
||||
clearable
|
||||
style="flex: 1"
|
||||
@input="$emit('update:friends-list-search', $event)"
|
||||
@change="friendsListSearchChange"></el-input>
|
||||
<el-select
|
||||
v-model="friendsListSearchFilters"
|
||||
@@ -73,7 +72,7 @@
|
||||
clearable
|
||||
collapse-tags
|
||||
style="flex: none; width: 200px; margin: 0 10px"
|
||||
:placeholder="$t('view.friend_list.filter_placeholder')"
|
||||
:placeholder="t('view.friend_list.filter_placeholder')"
|
||||
@change="friendsListSearchChange">
|
||||
<el-option
|
||||
v-for="type in ['Display Name', 'User Name', 'Rank', 'Status', 'Bio', 'Note', 'Memo']"
|
||||
@@ -81,7 +80,7 @@
|
||||
:label="type"
|
||||
:value="type"></el-option>
|
||||
</el-select>
|
||||
<el-tooltip placement="top" :content="$t('view.friend_list.refresh_tooltip')" :disabled="hideTooltips">
|
||||
<el-tooltip placement="top" :content="t('view.friend_list.refresh_tooltip')" :disabled="hideTooltips">
|
||||
<el-button
|
||||
type="default"
|
||||
icon="el-icon-refresh"
|
||||
@@ -109,12 +108,12 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('table.friendList.no')" width="70" prop="$friendNumber" sortable="custom">
|
||||
<el-table-column :label="t('table.friendList.no')" width="70" prop="$friendNumber" sortable="custom">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.$friendNumber ? scope.row.$friendNumber : '' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('table.friendList.avatar')" width="70" prop="photo">
|
||||
<el-table-column :label="t('table.friendList.avatar')" width="70" prop="photo">
|
||||
<template slot-scope="scope">
|
||||
<el-popover placement="right" height="500px" trigger="hover">
|
||||
<img slot="reference" v-lazy="userImage(scope.row, true)" class="friends-list-avatar" />
|
||||
@@ -127,7 +126,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('table.friendList.displayName')"
|
||||
:label="t('table.friendList.displayName')"
|
||||
min-width="140"
|
||||
prop="displayName"
|
||||
sortable
|
||||
@@ -138,11 +137,7 @@
|
||||
}}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('table.friendList.rank')"
|
||||
width="110"
|
||||
prop="$trustSortNum"
|
||||
sortable="custom">
|
||||
<el-table-column :label="t('table.friendList.rank')" width="110" prop="$trustSortNum" sortable="custom">
|
||||
<template slot-scope="scope">
|
||||
<span
|
||||
v-if="randomUserColours"
|
||||
@@ -157,7 +152,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('table.friendList.status')"
|
||||
:label="t('table.friendList.status')"
|
||||
min-width="180"
|
||||
prop="status"
|
||||
sortable
|
||||
@@ -172,7 +167,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('table.friendList.language')"
|
||||
:label="t('table.friendList.language')"
|
||||
width="110"
|
||||
prop="$languages"
|
||||
sortable
|
||||
@@ -189,7 +184,7 @@
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('table.friendList.bioLink')" width="100" prop="bioLinks">
|
||||
<el-table-column :label="t('table.friendList.bioLink')" width="100" prop="bioLinks">
|
||||
<template slot-scope="scope">
|
||||
<el-tooltip v-for="(link, index) in scope.row.bioLinks" v-if="link" :key="index">
|
||||
<template slot="content">
|
||||
@@ -209,52 +204,52 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('table.friendList.joinCount')"
|
||||
:label="t('table.friendList.joinCount')"
|
||||
width="120"
|
||||
prop="$joinCount"
|
||||
sortable></el-table-column>
|
||||
<el-table-column :label="$t('table.friendList.timeTogether')" width="140" prop="$timeSpent" sortable>
|
||||
<el-table-column :label="t('table.friendList.timeTogether')" width="140" prop="$timeSpent" sortable>
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.$timeSpent">{{ timeToText(scope.row.$timeSpent) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('table.friendList.lastSeen')"
|
||||
:label="t('table.friendList.lastSeen')"
|
||||
width="170"
|
||||
prop="$lastSeen"
|
||||
sortable
|
||||
:sort-method="(a, b) => sortAlphabetically(a, b, '$lastSeen')">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.$lastSeen | formatDate('long') }}</span>
|
||||
<span>{{ formatDateFilter(scope.row.$lastSeen, 'long') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('table.friendList.lastActivity')"
|
||||
:label="t('table.friendList.lastActivity')"
|
||||
width="170"
|
||||
prop="last_activity"
|
||||
sortable
|
||||
:sort-method="(a, b) => sortAlphabetically(a, b, 'last_activity')">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.last_activity | formatDate('long') }}</span>
|
||||
<span>{{ formatDateFilter(scope.row.last_activity, 'long') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('table.friendList.lastLogin')"
|
||||
:label="t('table.friendList.lastLogin')"
|
||||
width="170"
|
||||
prop="last_login"
|
||||
sortable
|
||||
:sort-method="(a, b) => sortAlphabetically(a, b, 'last_login')">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.last_login | formatDate('long') }}</span>
|
||||
<span>{{ formatDateFilter(scope.row.last_login, 'long') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('table.friendList.dateJoined')"
|
||||
:label="t('table.friendList.dateJoined')"
|
||||
width="120"
|
||||
prop="date_joined"
|
||||
sortable
|
||||
:sort-method="(a, b) => sortAlphabetically(a, b, 'date_joined')"></el-table-column>
|
||||
<el-table-column :label="$t('table.friendList.unfriend')" width="100" align="center">
|
||||
<el-table-column :label="t('table.friendList.unfriend')" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
type="text"
|
||||
@@ -269,251 +264,185 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { getCurrentInstance, nextTick, reactive, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { friendRequest, userRequest } from '../../api';
|
||||
import utils from '../../classes/utils';
|
||||
import { languageClass as _languageClass } from '../../composables/user/utils';
|
||||
import removeConfusables, { removeWhitespace } from '../../service/confusables';
|
||||
import { getFaviconUrl as _getFaviconUrl } from '../../composables/shared/utils';
|
||||
import {
|
||||
getFaviconUrl,
|
||||
languageClass,
|
||||
localeIncludes,
|
||||
openExternalLink,
|
||||
sortStatus,
|
||||
statusClass,
|
||||
timeToText,
|
||||
userImage,
|
||||
userImageFull,
|
||||
formatDateFilter
|
||||
} from '../../shared/utils';
|
||||
import {
|
||||
useAppearanceSettingsStore,
|
||||
useFriendStore,
|
||||
useGalleryStore,
|
||||
useSearchStore,
|
||||
useUiStore,
|
||||
useUserStore
|
||||
} from '../../stores';
|
||||
|
||||
export default {
|
||||
name: 'FriendListTab',
|
||||
inject: [
|
||||
'userImage',
|
||||
'userImageFull',
|
||||
'showFullscreenImageDialog',
|
||||
'showUserDialog',
|
||||
'statusClass',
|
||||
'openExternalLink'
|
||||
],
|
||||
props: {
|
||||
friends: {
|
||||
type: Map,
|
||||
required: true
|
||||
},
|
||||
hideTooltips: Boolean,
|
||||
randomUserColours: Boolean,
|
||||
sortStatus: Function,
|
||||
confirmDeleteFriend: Function,
|
||||
friendsListSearch: String,
|
||||
menuActiveIndex: String,
|
||||
stringComparer: Intl.Collator
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
friendsListSearchFilters: [],
|
||||
friendsListTable: {
|
||||
data: [],
|
||||
tableProps: {
|
||||
stripe: true,
|
||||
size: 'mini',
|
||||
defaultSort: {
|
||||
prop: '$friendNumber',
|
||||
order: 'descending'
|
||||
}
|
||||
},
|
||||
pageSize: 100,
|
||||
paginationProps: {
|
||||
small: true,
|
||||
layout: 'sizes,prev,pager,next,total',
|
||||
pageSizes: [50, 100, 250, 500]
|
||||
}
|
||||
},
|
||||
friendsListBulkUnfriendMode: false,
|
||||
friendsListLoading: false,
|
||||
friendsListLoadingProgress: '',
|
||||
friendsListSearchFilterVIP: false,
|
||||
// TODO
|
||||
friendsListBulkUnfriendForceUpdate: 0
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
menuActiveIndex() {
|
||||
if (this.menuActiveIndex === 'friendList') {
|
||||
requestAnimationFrame(() => {
|
||||
this.friendsListSearchChange();
|
||||
});
|
||||
const { t } = useI18n();
|
||||
|
||||
const { proxy } = getCurrentInstance();
|
||||
const $confirm = proxy.$confirm;
|
||||
const emit = defineEmits(['lookup-user']);
|
||||
|
||||
const { friends } = storeToRefs(useFriendStore());
|
||||
const { getAllUserStats, confirmDeleteFriend, handleFriendDelete } = useFriendStore();
|
||||
const { hideTooltips, randomUserColours } = storeToRefs(useAppearanceSettingsStore());
|
||||
const { showUserDialog } = useUserStore();
|
||||
const { menuActiveIndex } = storeToRefs(useUiStore());
|
||||
const { stringComparer, friendsListSearch } = storeToRefs(useSearchStore());
|
||||
const { showFullscreenImageDialog } = useGalleryStore();
|
||||
|
||||
const friendsListSearchFilters = ref([]);
|
||||
const friendsListTable = reactive({
|
||||
data: [],
|
||||
tableProps: { stripe: true, size: 'mini', defaultSort: { prop: '$friendNumber', order: 'descending' } },
|
||||
pageSize: 100,
|
||||
paginationProps: { small: true, layout: 'sizes,prev,pager,next,total', pageSizes: [50, 100, 250, 500] }
|
||||
});
|
||||
const friendsListBulkUnfriendMode = ref(false);
|
||||
const friendsListLoading = ref(false);
|
||||
const friendsListLoadingProgress = ref('');
|
||||
const friendsListSearchFilterVIP = ref(false);
|
||||
const friendsListBulkUnfriendForceUpdate = ref(0);
|
||||
|
||||
watch(menuActiveIndex, (val) => {
|
||||
if (val === 'friendList') nextTick(friendsListSearchChange);
|
||||
});
|
||||
|
||||
function friendsListSearchChange() {
|
||||
friendsListLoading.value = true;
|
||||
let query = '';
|
||||
let cleanedQuery = '';
|
||||
friendsListTable.data = [];
|
||||
let filters = friendsListSearchFilters.value.length
|
||||
? [...friendsListSearchFilters.value]
|
||||
: ['Display Name', 'Rank', 'Status', 'Bio', 'Note', 'Memo'];
|
||||
const results = [];
|
||||
if (friendsListSearch.value) {
|
||||
query = friendsListSearch.value;
|
||||
cleanedQuery = removeWhitespace(query);
|
||||
}
|
||||
for (const ctx of friends.value.values()) {
|
||||
if (!ctx.ref) continue;
|
||||
ctx.ref.$selected = ctx.ref.$selected ?? false;
|
||||
if (friendsListSearchFilterVIP.value && !ctx.isVIP) continue;
|
||||
if (query) {
|
||||
let match = false;
|
||||
if (!match && filters.includes('Display Name') && ctx.ref.displayName) {
|
||||
match =
|
||||
localeIncludes(ctx.ref.displayName, cleanedQuery, stringComparer.value) ||
|
||||
localeIncludes(removeConfusables(ctx.ref.displayName), cleanedQuery, stringComparer.value);
|
||||
}
|
||||
if (!match && filters.includes('Memo') && ctx.memo) {
|
||||
match = localeIncludes(ctx.memo, query, stringComparer.value);
|
||||
}
|
||||
if (!match && filters.includes('Note') && ctx.ref.note) {
|
||||
match = localeIncludes(ctx.ref.note, query, stringComparer.value);
|
||||
}
|
||||
if (!match && filters.includes('Bio') && ctx.ref.bio) {
|
||||
match = localeIncludes(ctx.ref.bio, query, stringComparer.value);
|
||||
}
|
||||
if (!match && filters.includes('Status') && ctx.ref.statusDescription) {
|
||||
match = localeIncludes(ctx.ref.statusDescription, query, stringComparer.value);
|
||||
}
|
||||
if (!match && filters.includes('Rank')) {
|
||||
match = String(ctx.ref.$trustLevel).toUpperCase().includes(query.toUpperCase());
|
||||
}
|
||||
if (!match) continue;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
languageClass(key) {
|
||||
return _languageClass(key);
|
||||
},
|
||||
friendsListSearchChange() {
|
||||
this.friendsListLoading = true;
|
||||
let query = '';
|
||||
let cleanedQuery = '';
|
||||
this.friendsListTable.data = [];
|
||||
let filters = [...this.friendsListSearchFilters];
|
||||
if (filters.length === 0) {
|
||||
filters = ['Display Name', 'Rank', 'Status', 'Bio', 'Note', 'Memo'];
|
||||
}
|
||||
const results = [];
|
||||
if (this.friendsListSearch) {
|
||||
query = this.friendsListSearch;
|
||||
cleanedQuery = removeWhitespace(query);
|
||||
}
|
||||
results.push(ctx.ref);
|
||||
}
|
||||
getAllUserStats();
|
||||
nextTick(() => {
|
||||
friendsListTable.data = results;
|
||||
friendsListLoading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
for (const ctx of this.friends.values()) {
|
||||
if (typeof ctx.ref === 'undefined') {
|
||||
continue;
|
||||
}
|
||||
if (typeof ctx.ref.$selected === 'undefined') {
|
||||
ctx.ref.$selected = false;
|
||||
}
|
||||
if (this.friendsListSearchFilterVIP && !ctx.isVIP) {
|
||||
continue;
|
||||
}
|
||||
if (query && filters) {
|
||||
let match = false;
|
||||
if (!match && filters.includes('Display Name') && ctx.ref.displayName) {
|
||||
match =
|
||||
utils.localeIncludes(ctx.ref.displayName, cleanedQuery, this.stringComparer) ||
|
||||
utils.localeIncludes(
|
||||
removeConfusables(ctx.ref.displayName),
|
||||
cleanedQuery,
|
||||
this.stringComparer
|
||||
);
|
||||
}
|
||||
if (!match && filters.includes('Memo') && ctx.memo) {
|
||||
match = utils.localeIncludes(ctx.memo, query, this.stringComparer);
|
||||
}
|
||||
if (!match && filters.includes('Note') && ctx.ref.note) {
|
||||
match = utils.localeIncludes(ctx.ref.note, query, this.stringComparer);
|
||||
}
|
||||
if (!match && filters.includes('Bio') && ctx.ref.bio) {
|
||||
match = utils.localeIncludes(ctx.ref.bio, query, this.stringComparer);
|
||||
}
|
||||
if (!match && filters.includes('Status') && ctx.ref.statusDescription) {
|
||||
match = utils.localeIncludes(ctx.ref.statusDescription, query, this.stringComparer);
|
||||
}
|
||||
if (!match && filters.includes('Rank')) {
|
||||
match = String(ctx.ref.$trustLevel).toUpperCase().includes(query.toUpperCase());
|
||||
}
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
results.push(ctx.ref);
|
||||
}
|
||||
this.$emit('get-all-user-stats');
|
||||
requestAnimationFrame(() => {
|
||||
this.friendsListTable.data = results;
|
||||
this.friendsListLoading = false;
|
||||
});
|
||||
},
|
||||
toggleFriendsListBulkUnfriendMode() {
|
||||
if (!this.friendsListBulkUnfriendMode) {
|
||||
this.friendsListTable.data.forEach((ref) => {
|
||||
ref.$selected = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
showBulkUnfriendSelectionConfirm() {
|
||||
const pendingUnfriendList = this.friendsListTable.data.reduce((acc, ctx) => {
|
||||
if (ctx.$selected) {
|
||||
acc.push(ctx.displayName);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
const elementsTicked = pendingUnfriendList.length;
|
||||
if (elementsTicked === 0) {
|
||||
return;
|
||||
}
|
||||
this.$confirm(
|
||||
`Are you sure you want to delete ${elementsTicked} friends?
|
||||
function toggleFriendsListBulkUnfriendMode() {
|
||||
if (!friendsListBulkUnfriendMode.value) {
|
||||
friendsListTable.data.forEach((item) => (item.$selected = false));
|
||||
}
|
||||
}
|
||||
|
||||
function showBulkUnfriendSelectionConfirm() {
|
||||
const pending = friendsListTable.data.filter((item) => item.$selected).map((item) => item.displayName);
|
||||
if (!pending.length) return;
|
||||
$confirm(
|
||||
`Are you sure you want to delete ${pending.length} friends?
|
||||
This can negatively affect your trust rank,
|
||||
This action cannot be undone.`,
|
||||
`Delete ${elementsTicked} friends?`,
|
||||
{
|
||||
confirmButtonText: 'Confirm',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'info',
|
||||
showInput: true,
|
||||
inputType: 'textarea',
|
||||
inputValue: pendingUnfriendList.join('\r\n'),
|
||||
callback: (action) => {
|
||||
if (action === 'confirm') {
|
||||
this.bulkUnfriendSelection();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
`Delete ${pending.length} friends?`,
|
||||
{
|
||||
confirmButtonText: 'Confirm',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'info',
|
||||
showInput: true,
|
||||
inputType: 'textarea',
|
||||
inputValue: pending.join('\r\n'),
|
||||
callback: (action) => action === 'confirm' && bulkUnfriendSelection()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
bulkUnfriendSelection() {
|
||||
for (const ctx of this.friendsListTable.data) {
|
||||
if (ctx.$selected) {
|
||||
friendRequest.deleteFriend({
|
||||
userId: ctx.id
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
async friendsListLoadUsers() {
|
||||
this.friendsListLoading = true;
|
||||
let i = 0;
|
||||
const toFetch = [];
|
||||
for (const ctx of this.friends.values()) {
|
||||
if (ctx.ref && !ctx.ref.date_joined) {
|
||||
toFetch.push(ctx.id);
|
||||
}
|
||||
}
|
||||
const length = toFetch.length;
|
||||
for (const userId of toFetch) {
|
||||
if (!this.friendsListLoading) {
|
||||
this.friendsListLoadingProgress = '';
|
||||
return;
|
||||
}
|
||||
i++;
|
||||
this.friendsListLoadingProgress = `${i}/${length}`;
|
||||
try {
|
||||
await userRequest.getUser({
|
||||
userId
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
this.friendsListLoadingProgress = '';
|
||||
this.friendsListLoading = false;
|
||||
},
|
||||
selectFriendsListRow(val) {
|
||||
if (val === null) {
|
||||
return;
|
||||
}
|
||||
if (!val.id) {
|
||||
this.$emit('lookup-user', val);
|
||||
return;
|
||||
}
|
||||
this.showUserDialog(val.id);
|
||||
},
|
||||
sortAlphabetically(a, b, field) {
|
||||
if (!a[field] || !b[field]) {
|
||||
return 0;
|
||||
}
|
||||
return a[field].toLowerCase().localeCompare(b[field].toLowerCase());
|
||||
},
|
||||
sortLanguages(a, b) {
|
||||
const sortedA = [];
|
||||
const sortedB = [];
|
||||
a.$languages.forEach((item) => {
|
||||
sortedA.push(item.value);
|
||||
});
|
||||
b.$languages.forEach((item) => {
|
||||
sortedB.push(item.value);
|
||||
});
|
||||
sortedA.sort();
|
||||
sortedB.sort();
|
||||
return JSON.stringify(sortedA).localeCompare(JSON.stringify(sortedB));
|
||||
},
|
||||
timeToText(val) {
|
||||
return utils.timeToText(val);
|
||||
},
|
||||
getFaviconUrl(link) {
|
||||
return _getFaviconUrl(link);
|
||||
function bulkUnfriendSelection() {
|
||||
friendsListTable.data.forEach((item) => {
|
||||
if (item.$selected)
|
||||
friendRequest.deleteFriend({ userId: item.id }).then((args) => handleFriendDelete(args));
|
||||
});
|
||||
}
|
||||
|
||||
async function friendsListLoadUsers() {
|
||||
friendsListLoading.value = true;
|
||||
let i = 0;
|
||||
const toFetch = Array.from(friends.value.values())
|
||||
.filter((ctx) => ctx.ref && !ctx.ref.date_joined)
|
||||
.map((ctx) => ctx.id);
|
||||
const total = toFetch.length;
|
||||
for (const userId of toFetch) {
|
||||
if (!friendsListLoading.value) {
|
||||
friendsListLoadingProgress.value = '';
|
||||
return;
|
||||
}
|
||||
i++;
|
||||
friendsListLoadingProgress.value = `${i}/${total}`;
|
||||
try {
|
||||
await userRequest.getUser({ userId });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
friendsListLoadingProgress.value = '';
|
||||
friendsListLoading.value = false;
|
||||
}
|
||||
|
||||
function selectFriendsListRow(val) {
|
||||
if (!val) return;
|
||||
if (!val.id) emit('lookup-user', val);
|
||||
else showUserDialog(val.id);
|
||||
}
|
||||
|
||||
function sortAlphabetically(a, b, field) {
|
||||
if (!a[field] || !b[field]) return 0;
|
||||
return a[field].toLowerCase().localeCompare(b[field].toLowerCase());
|
||||
}
|
||||
|
||||
function sortLanguages(a, b) {
|
||||
const as = a.$languages.map((i) => i.value).sort();
|
||||
const bs = b.$languages.map((i) => i.value).sort();
|
||||
return JSON.stringify(as).localeCompare(JSON.stringify(bs));
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -34,9 +34,9 @@
|
||||
<template #default="scope">
|
||||
<el-tooltip placement="right">
|
||||
<template #content>
|
||||
<span>{{ scope.row.created_at | formatDate('long') }}</span>
|
||||
<span>{{ formatDateFilter(scope.row.created_at, 'long') }}</span>
|
||||
</template>
|
||||
<span>{{ scope.row.created_at | formatDate('short') }}</span>
|
||||
<span>{{ formatDateFilter(scope.row.created_at, 'short') }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -87,42 +87,39 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'FriendLogTab'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup>
|
||||
import { getCurrentInstance, inject } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { getCurrentInstance, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import utils from '../../classes/utils';
|
||||
import configRepository from '../../service/config';
|
||||
import database from '../../service/database';
|
||||
import { database } from '../../service/database';
|
||||
import { removeFromArray, formatDateFilter } from '../../shared/utils';
|
||||
import { useAppearanceSettingsStore, useUiStore, useFriendStore, useUserStore } from '../../stores';
|
||||
|
||||
const { hideUnfriends } = storeToRefs(useAppearanceSettingsStore());
|
||||
const { showUserDialog } = useUserStore();
|
||||
const { friendLogTable } = storeToRefs(useFriendStore());
|
||||
const { shiftHeld } = storeToRefs(useUiStore());
|
||||
const { menuActiveIndex } = storeToRefs(useUiStore());
|
||||
|
||||
watch(
|
||||
() => hideUnfriends.value,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
friendLogTable.value.filters[2].value = newValue;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const { t } = useI18n();
|
||||
const { proxy } = getCurrentInstance();
|
||||
const { $confirm } = proxy;
|
||||
|
||||
const showUserDialog = inject('showUserDialog');
|
||||
|
||||
const props = defineProps({
|
||||
menuActiveIndex: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
friendLogTable: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
shiftHeld: { type: Boolean, default: false }
|
||||
});
|
||||
|
||||
function saveTableFilters() {
|
||||
configRepository.setString('VRCX_friendLogTableFilters', JSON.stringify(props.friendLogTable.filters[0].value));
|
||||
configRepository.setString('VRCX_friendLogTableFilters', JSON.stringify(friendLogTable.value.filters[0].value));
|
||||
}
|
||||
function deleteFriendLogPrompt(row) {
|
||||
$confirm('Continue? Delete Log', 'Confirm', {
|
||||
proxy.$confirm('Continue? Delete Log', 'Confirm', {
|
||||
confirmButtonText: 'Confirm',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'info',
|
||||
@@ -134,7 +131,7 @@
|
||||
});
|
||||
}
|
||||
function deleteFriendLog(row) {
|
||||
utils.removeFromArray(props.friendLogTable.data, row);
|
||||
removeFromArray(friendLogTable.value.data, row);
|
||||
database.deleteFriendLogHistory(row.rowId);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
v-model="gameLogTable.search"
|
||||
:placeholder="t('view.game_log.search_placeholder')"
|
||||
clearable
|
||||
style="flex: none; width: 150px; margin: 0 10px"
|
||||
style="flex: none; width: 150px; margin-left: 10px"
|
||||
@keyup.native.enter="gameLogTableLookup"
|
||||
@change="gameLogTableLookup"></el-input>
|
||||
</div>
|
||||
@@ -50,9 +50,9 @@
|
||||
<template #default="scope">
|
||||
<el-tooltip placement="right">
|
||||
<template #content>
|
||||
<span>{{ scope.row.created_at | formatDate('long') }}</span>
|
||||
<span>{{ formatDateFilter(scope.row.created_at, 'long') }}</span>
|
||||
</template>
|
||||
<span>{{ scope.row.created_at | formatDate('short') }}</span>
|
||||
<span>{{ formatDateFilter(scope.row.created_at, 'short') }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -99,16 +99,16 @@
|
||||
|
||||
<el-table-column :label="t('table.gameLog.detail')" prop="data">
|
||||
<template #default="scope">
|
||||
<location
|
||||
<Location
|
||||
v-if="scope.row.type === 'Location'"
|
||||
:location="scope.row.location"
|
||||
:hint="scope.row.worldName"
|
||||
:grouphint="scope.row.groupName"></location>
|
||||
<location
|
||||
:grouphint="scope.row.groupName" />
|
||||
<Location
|
||||
v-else-if="scope.row.type === 'PortalSpawn'"
|
||||
:location="scope.row.instanceId"
|
||||
:hint="scope.row.worldName"
|
||||
:grouphint="scope.row.groupName"></location>
|
||||
:grouphint="scope.row.groupName" />
|
||||
<template v-else-if="scope.row.type === 'Event'">
|
||||
<span v-text="scope.row.data"></span>
|
||||
</template>
|
||||
@@ -117,7 +117,9 @@
|
||||
</template>
|
||||
<template v-else-if="scope.row.type === 'VideoPlay'">
|
||||
<span v-if="scope.row.videoId" style="margin-right: 5px">{{ scope.row.videoId }}:</span>
|
||||
<span v-if="scope.row.videoId === 'LSMedia'" v-text="scope.row.videoName"></span>
|
||||
<span
|
||||
v-if="scope.row.videoId === 'LSMedia' || scope.row.videoId === 'PopcornPalace'"
|
||||
v-text="scope.row.videoName"></span>
|
||||
<span
|
||||
v-else-if="scope.row.videoName"
|
||||
class="x-link"
|
||||
@@ -189,82 +191,48 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'GameLogTab'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup>
|
||||
import { inject, getCurrentInstance } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { getCurrentInstance } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import utils from '../../classes/utils';
|
||||
import database from '../../service/database';
|
||||
import Location from '../../components/Location.vue';
|
||||
import { database } from '../../service/database';
|
||||
import { removeFromArray, openExternalLink, formatDateFilter } from '../../shared/utils';
|
||||
import {
|
||||
useUserStore,
|
||||
useUiStore,
|
||||
useWorldStore,
|
||||
useAppearanceSettingsStore,
|
||||
useInstanceStore,
|
||||
useGameLogStore
|
||||
} from '../../stores';
|
||||
import { useSharedFeedStore } from '../../stores';
|
||||
|
||||
const { hideTooltips } = storeToRefs(useAppearanceSettingsStore());
|
||||
const { showWorldDialog } = useWorldStore();
|
||||
const { lookupUser } = useUserStore();
|
||||
const { showPreviousInstancesInfoDialog } = useInstanceStore();
|
||||
const { menuActiveIndex, shiftHeld } = storeToRefs(useUiStore());
|
||||
const { gameLogIsFriend, gameLogIsFavorite, gameLogTableLookup } = useGameLogStore();
|
||||
const { gameLogTable } = storeToRefs(useGameLogStore());
|
||||
const { updateSharedFeed } = useSharedFeedStore();
|
||||
|
||||
const { t } = useI18n();
|
||||
const { $confirm } = getCurrentInstance().proxy;
|
||||
const { proxy } = getCurrentInstance();
|
||||
|
||||
const showWorldDialog = inject('showWorldDialog');
|
||||
const openExternalLink = inject('openExternalLink');
|
||||
const showPreviousInstancesInfoDialog = inject('showPreviousInstancesInfoDialog');
|
||||
|
||||
const props = defineProps({
|
||||
menuActiveIndex: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
gameLogTable: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
hideTooltips: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
shiftHeld: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
gameLogIsFriend: {
|
||||
type: Function,
|
||||
default: () => () => false
|
||||
},
|
||||
gameLogIsFavorite: {
|
||||
type: Function,
|
||||
default: () => () => false
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
'gameLogTableLookup',
|
||||
'gameLogIsFriend',
|
||||
'gameLogIsFavorite',
|
||||
'lookupUser',
|
||||
'updateGameLogSessionTable',
|
||||
'updateSharedFeed'
|
||||
]);
|
||||
|
||||
function gameLogTableLookup() {
|
||||
emit('gameLogTableLookup');
|
||||
}
|
||||
|
||||
function lookupUser(ref) {
|
||||
emit('lookupUser', ref);
|
||||
}
|
||||
const emit = defineEmits(['updateGameLogSessionTable']);
|
||||
|
||||
function deleteGameLogEntry(row) {
|
||||
utils.removeFromArray(props.gameLogTable.data, row);
|
||||
removeFromArray(gameLogTable.value.data, row);
|
||||
database.deleteGameLogEntry(row);
|
||||
console.log('deleteGameLogEntry', row);
|
||||
database.getGamelogDatabase().then((data) => {
|
||||
emit('updateGameLogSessionTable', data);
|
||||
emit('updateSharedFeed', true);
|
||||
updateSharedFeed(true);
|
||||
});
|
||||
}
|
||||
|
||||
function deleteGameLogEntryPrompt(row) {
|
||||
$confirm('Continue? Delete Log', 'Confirm', {
|
||||
proxy.$confirm('Continue? Delete Log', 'Confirm', {
|
||||
confirmButtonText: 'Confirm',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'info',
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
ref="loginFormRef"
|
||||
:model="loginForm"
|
||||
:rules="loginForm.rules"
|
||||
@submit.native.prevent="login()">
|
||||
@submit.native.prevent="handleLogin()">
|
||||
<el-form-item :label="t('view.login.field.username')" prop="username" required>
|
||||
<el-input
|
||||
v-model="loginForm.username"
|
||||
@@ -66,7 +66,7 @@
|
||||
<el-input
|
||||
v-model="loginForm.endpoint"
|
||||
name="endpoint"
|
||||
:placeholder="API.endpointDomainVrchat"
|
||||
:placeholder="AppGlobal.endpointDomainVrchat"
|
||||
clearable></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
@@ -77,7 +77,7 @@
|
||||
<el-input
|
||||
v-model="loginForm.websocket"
|
||||
name="websocket"
|
||||
:placeholder="API.websocketDomainVrchat"
|
||||
:placeholder="AppGlobal.websocketDomainVrchat"
|
||||
clearable></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item style="margin-top: 15px">
|
||||
@@ -149,77 +149,38 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'LoginPage'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup>
|
||||
import { inject, onBeforeUnmount, ref } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { onBeforeUnmount, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import {
|
||||
useAppearanceSettingsStore,
|
||||
useAuthStore,
|
||||
useGeneralSettingsStore,
|
||||
useVRCXUpdaterStore
|
||||
} from '../../stores';
|
||||
import { openExternalLink, userImage } from '../../shared/utils';
|
||||
import { AppGlobal } from '../../service/appConfig';
|
||||
|
||||
const { showVRCXUpdateDialog } = useVRCXUpdaterStore();
|
||||
const { hideTooltips } = storeToRefs(useAppearanceSettingsStore());
|
||||
const { loginForm, enableCustomEndpoint } = storeToRefs(useAuthStore());
|
||||
const { toggleCustomEndpoint, relogin, deleteSavedLogin, login } = useAuthStore();
|
||||
const { promptProxySettings } = useGeneralSettingsStore();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const API = inject('API');
|
||||
const openExternalLink = inject('openExternalLink');
|
||||
const userImage = inject('userImage');
|
||||
|
||||
defineProps({
|
||||
hideTooltips: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
loginForm: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
enableCustomEndpoint: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
'showVRCXUpdateDialog',
|
||||
'promptProxySettings',
|
||||
'toggleCustomEndpoint',
|
||||
'deleteSavedLogin',
|
||||
'relogin',
|
||||
'login'
|
||||
]);
|
||||
|
||||
const loginFormRef = ref(null);
|
||||
|
||||
function showVRCXUpdateDialog() {
|
||||
emit('showVRCXUpdateDialog');
|
||||
}
|
||||
|
||||
function promptProxySettings() {
|
||||
emit('promptProxySettings');
|
||||
}
|
||||
|
||||
function toggleCustomEndpoint(...args) {
|
||||
emit('toggleCustomEndpoint', args);
|
||||
}
|
||||
|
||||
function deleteSavedLogin(userId) {
|
||||
emit('deleteSavedLogin', userId);
|
||||
}
|
||||
|
||||
function relogin(user) {
|
||||
emit('relogin', user);
|
||||
}
|
||||
|
||||
function login() {
|
||||
function handleLogin() {
|
||||
if (loginFormRef.value) {
|
||||
loginFormRef.value.validate((valid) => {
|
||||
valid && emit('login');
|
||||
valid && login();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// Because v-if actually it is not required
|
||||
if (loginFormRef.value) {
|
||||
loginFormRef.value.resetFields();
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div v-show="menuActiveIndex === 'moderation'" class="x-container">
|
||||
<data-tables
|
||||
:data="tableData.data"
|
||||
:pageSize="tableData.pageSize"
|
||||
:data="playerModerationTable.data"
|
||||
:pageSize="playerModerationTable.pageSize"
|
||||
:filters="filters"
|
||||
:tableProps="tableProps"
|
||||
:paginationProps="paginationProps"
|
||||
v-loading="API.isPlayerModerationsLoading">
|
||||
v-loading="isPlayerModerationsLoading">
|
||||
<template slot="tool">
|
||||
<div class="tool-slot">
|
||||
<el-select
|
||||
@@ -15,46 +15,46 @@
|
||||
multiple
|
||||
clearable
|
||||
style="flex: 1"
|
||||
:placeholder="$t('view.moderation.filter_placeholder')">
|
||||
:placeholder="t('view.moderation.filter_placeholder')">
|
||||
<el-option
|
||||
v-for="item in moderationTypes"
|
||||
:key="item"
|
||||
:label="$t('view.moderation.filters.' + item)"
|
||||
:label="t('view.moderation.filters.' + item)"
|
||||
:value="item" />
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model="filters[1].value"
|
||||
:placeholder="$t('view.moderation.search_placeholder')"
|
||||
:placeholder="t('view.moderation.search_placeholder')"
|
||||
class="filter-input" />
|
||||
<el-tooltip
|
||||
placement="bottom"
|
||||
:content="$t('view.moderation.refresh_tooltip')"
|
||||
:content="t('view.moderation.refresh_tooltip')"
|
||||
:disabled="hideTooltips">
|
||||
<el-button
|
||||
type="default"
|
||||
:loading="API.isPlayerModerationsLoading"
|
||||
@click="API.refreshPlayerModerations()"
|
||||
:loading="isPlayerModerationsLoading"
|
||||
@click="refreshPlayerModerations()"
|
||||
icon="el-icon-refresh"
|
||||
circle />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-table-column :label="$t('table.moderation.date')" prop="created" sortable="custom" width="120">
|
||||
<el-table-column :label="t('table.moderation.date')" prop="created" sortable="custom" width="120">
|
||||
<template slot-scope="scope">
|
||||
<el-tooltip placement="right">
|
||||
<template slot="content">
|
||||
<span>{{ scope.row.created | formatDate('long') }}</span>
|
||||
<span>{{ formatDateFilter(scope.row.created, 'long') }}</span>
|
||||
</template>
|
||||
<span>{{ scope.row.created | formatDate('short') }}</span>
|
||||
<span>{{ formatDateFilter(scope.row.created, 'short') }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('table.moderation.type')" prop="type" width="100">
|
||||
<el-table-column :label="t('table.moderation.type')" prop="type" width="100">
|
||||
<template slot-scope="scope">
|
||||
<span v-text="$t('view.moderation.filters.' + scope.row.type)"></span>
|
||||
<span v-text="t('view.moderation.filters.' + scope.row.type)"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('table.moderation.source')" prop="sourceDisplayName">
|
||||
<el-table-column :label="t('table.moderation.source')" prop="sourceDisplayName">
|
||||
<template slot-scope="scope">
|
||||
<span
|
||||
class="x-link"
|
||||
@@ -62,7 +62,7 @@
|
||||
@click="showUserDialog(scope.row.sourceUserId)"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('table.moderation.target')" prop="targetDisplayName">
|
||||
<el-table-column :label="t('table.moderation.target')" prop="targetDisplayName">
|
||||
<template slot-scope="scope">
|
||||
<span
|
||||
class="x-link"
|
||||
@@ -70,9 +70,9 @@
|
||||
@click="showUserDialog(scope.row.targetUserId)"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('table.moderation.action')" width="80" align="right">
|
||||
<el-table-column :label="t('table.moderation.action')" width="80" align="right">
|
||||
<template slot-scope="scope">
|
||||
<template v-if="scope.row.sourceUserId === API.currentUser.id">
|
||||
<template v-if="scope.row.sourceUserId === currentUser.id">
|
||||
<el-button
|
||||
v-if="shiftHeld"
|
||||
style="color: #f56c6c"
|
||||
@@ -93,87 +93,85 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { getCurrentInstance, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { playerModerationRequest } from '../../api';
|
||||
import configRepository from '../../service/config.js';
|
||||
import { useUiStore, useModerationStore, useUserStore, useAppearanceSettingsStore } from '../../stores';
|
||||
import { moderationTypes } from '../../shared/constants';
|
||||
import { formatDateFilter } from '../../shared/utils';
|
||||
|
||||
export default {
|
||||
name: 'ModerationTab',
|
||||
inject: ['API', 'showUserDialog'],
|
||||
props: {
|
||||
menuActiveIndex: String,
|
||||
tableData: Object,
|
||||
shiftHeld: Boolean,
|
||||
hideTooltips: Boolean
|
||||
const { t } = useI18n();
|
||||
const { proxy } = getCurrentInstance();
|
||||
|
||||
const { hideTooltips } = storeToRefs(useAppearanceSettingsStore());
|
||||
const { showUserDialog } = useUserStore();
|
||||
const { isPlayerModerationsLoading, playerModerationTable } = storeToRefs(useModerationStore());
|
||||
const { refreshPlayerModerations, handlePlayerModerationDelete } = useModerationStore();
|
||||
const { menuActiveIndex, shiftHeld } = storeToRefs(useUiStore());
|
||||
const { currentUser } = storeToRefs(useUserStore());
|
||||
|
||||
const filters = ref([
|
||||
{
|
||||
prop: 'type',
|
||||
value: [],
|
||||
filterFn: (row, filter) => filter.value.some((v) => v === row.type)
|
||||
},
|
||||
created: async function () {
|
||||
this.filters[0].value = JSON.parse(
|
||||
await configRepository.getString('VRCX_playerModerationTableFilters', '[]')
|
||||
);
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
filters: [
|
||||
{
|
||||
prop: 'type',
|
||||
value: [],
|
||||
filterFn: (row, filter) => filter.value.some((v) => v === row.type)
|
||||
},
|
||||
{
|
||||
prop: ['sourceDisplayName', 'targetDisplayName'],
|
||||
value: ''
|
||||
}
|
||||
],
|
||||
// CONSTANTS
|
||||
moderationTypes: [
|
||||
'block',
|
||||
'unblock',
|
||||
'mute',
|
||||
'unmute',
|
||||
'interactOn',
|
||||
'interactOff',
|
||||
'muteChat',
|
||||
'unmuteChat'
|
||||
],
|
||||
tableProps: {
|
||||
stripe: true,
|
||||
size: 'mini',
|
||||
defaultSort: {
|
||||
prop: 'created',
|
||||
order: 'descending'
|
||||
}
|
||||
},
|
||||
paginationProps: {
|
||||
small: true,
|
||||
layout: 'sizes,prev,pager,next,total',
|
||||
pageSizes: [10, 15, 20, 25, 50, 100]
|
||||
}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
saveTableFilters() {
|
||||
configRepository.setString('VRCX_playerModerationTableFilters', JSON.stringify(this.filters[0].value));
|
||||
},
|
||||
deletePlayerModeration(row) {
|
||||
playerModerationRequest.deletePlayerModeration({
|
||||
moderated: row.targetUserId,
|
||||
type: row.type
|
||||
});
|
||||
},
|
||||
deletePlayerModerationPrompt(row) {
|
||||
this.$confirm(`Continue? Delete Moderation ${row.type}`, 'Confirm', {
|
||||
confirmButtonText: 'Confirm',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'info',
|
||||
callback: (action) => {
|
||||
if (action === 'confirm') {
|
||||
this.deletePlayerModeration(row);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
{
|
||||
prop: ['sourceDisplayName', 'targetDisplayName'],
|
||||
value: ''
|
||||
}
|
||||
};
|
||||
]);
|
||||
|
||||
const tableProps = ref({
|
||||
stripe: true,
|
||||
size: 'mini',
|
||||
defaultSort: {
|
||||
prop: 'created',
|
||||
order: 'descending'
|
||||
}
|
||||
});
|
||||
|
||||
const paginationProps = ref({
|
||||
small: true,
|
||||
layout: 'sizes,prev,pager,next,total',
|
||||
pageSizes: [10, 15, 20, 25, 50, 100]
|
||||
});
|
||||
|
||||
async function init() {
|
||||
filters.value[0].value = JSON.parse(
|
||||
await configRepository.getString('VRCX_playerModerationTableFilters', '[]')
|
||||
);
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
function saveTableFilters() {
|
||||
configRepository.setString('VRCX_playerModerationTableFilters', JSON.stringify(filters.value[0].value));
|
||||
}
|
||||
|
||||
async function deletePlayerModeration(row) {
|
||||
const args = await playerModerationRequest.deletePlayerModeration({
|
||||
moderated: row.targetUserId,
|
||||
type: row.type
|
||||
});
|
||||
handlePlayerModerationDelete(args);
|
||||
}
|
||||
|
||||
function deletePlayerModerationPrompt(row) {
|
||||
proxy.$confirm(`Continue? Delete Moderation ${row.type}`, 'Confirm', {
|
||||
confirmButtonText: 'Confirm',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'info',
|
||||
callback: (action) => {
|
||||
if (action === 'confirm') {
|
||||
deletePlayerModeration(row);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div v-show="menuActiveIndex === 'notification'" v-loading="API.isNotificationsLoading" class="x-container">
|
||||
<div v-show="menuActiveIndex === 'notification'" v-loading="isNotificationsLoading" class="x-container">
|
||||
<data-tables v-bind="notificationTable" ref="notificationTableRef" class="notification-table">
|
||||
<template #tool>
|
||||
<div style="margin: 0 0 10px; display: flex; align-items: center">
|
||||
@@ -45,11 +45,11 @@
|
||||
:disabled="hideTooltips">
|
||||
<el-button
|
||||
type="default"
|
||||
:loading="API.isNotificationsLoading"
|
||||
:loading="isNotificationsLoading"
|
||||
icon="el-icon-refresh"
|
||||
circle
|
||||
style="flex: none"
|
||||
@click="API.refreshNotifications()" />
|
||||
@click="refreshNotifications()" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
@@ -58,9 +58,9 @@
|
||||
<template #default="scope">
|
||||
<el-tooltip placement="right">
|
||||
<template #content>
|
||||
<span>{{ scope.row.created_at | formatDate('long') }}</span>
|
||||
<span>{{ formatDateFilter(scope.row.created_at, 'long') }}</span>
|
||||
</template>
|
||||
<span>{{ scope.row.created_at | formatDate('short') }}</span>
|
||||
<span>{{ formatDateFilter(scope.row.created_at, 'short') }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -76,7 +76,7 @@
|
||||
v-else-if="scope.row.type === 'group.queueReady' || scope.row.type === 'instance.closed'"
|
||||
placement="top">
|
||||
<template #content>
|
||||
<location
|
||||
<Location
|
||||
v-if="scope.row.location"
|
||||
:location="scope.row.location"
|
||||
:hint="scope.row.worldName"
|
||||
@@ -169,7 +169,7 @@
|
||||
<el-table-column :label="t('table.notification.message')" prop="message">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.type === 'invite'" style="display: flex">
|
||||
<location
|
||||
<Location
|
||||
v-if="scope.row.details"
|
||||
:location="scope.row.details.worldId"
|
||||
:hint="scope.row.details.worldName"
|
||||
@@ -213,7 +213,7 @@
|
||||
|
||||
<el-table-column :label="t('table.notification.action')" width="100" align="right">
|
||||
<template #default="scope">
|
||||
<template v-if="scope.row.senderUserId !== API.currentUser.id && !scope.row.$isExpired">
|
||||
<template v-if="scope.row.senderUserId !== currentUser.id && !scope.row.$isExpired">
|
||||
<template v-if="scope.row.type === 'friendRequest'">
|
||||
<el-tooltip placement="top" content="Accept" :disabled="hideTooltips">
|
||||
<el-button
|
||||
@@ -257,7 +257,11 @@
|
||||
|
||||
<template v-if="scope.row.responses">
|
||||
<template v-for="response in scope.row.responses">
|
||||
<el-tooltip placement="top" :content="response.text" :disabled="hideTooltips">
|
||||
<el-tooltip
|
||||
placement="top"
|
||||
:content="response.text"
|
||||
:disabled="hideTooltips"
|
||||
:key="response.text">
|
||||
<el-button
|
||||
v-if="response.icon === 'check'"
|
||||
type="text"
|
||||
@@ -400,85 +404,62 @@
|
||||
</data-tables>
|
||||
<SendInviteResponseDialog
|
||||
:send-invite-response-dialog="sendInviteResponseDialog"
|
||||
:send-invite-response-dialog-visible.sync="sendInviteResponseDialogVisible"
|
||||
:invite-response-message-table="inviteResponseMessageTable"
|
||||
:upload-image="uploadImage" />
|
||||
:send-invite-response-dialog-visible.sync="sendInviteResponseDialogVisible" />
|
||||
<SendInviteRequestResponseDialog
|
||||
:send-invite-response-dialog="sendInviteResponseDialog"
|
||||
:send-invite-request-response-dialog-visible.sync="sendInviteRequestResponseDialogVisible"
|
||||
:invite-request-response-message-table="inviteRequestResponseMessageTable"
|
||||
:upload-image="uploadImage" />
|
||||
:send-invite-request-response-dialog-visible.sync="sendInviteRequestResponseDialogVisible" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'NotificationTab'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup>
|
||||
import { getCurrentInstance, inject, ref } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { getCurrentInstance, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { friendRequest, inviteMessagesRequest, notificationRequest, worldRequest } from '../../api';
|
||||
import utils from '../../classes/utils';
|
||||
import { parseLocation } from '../../composables/instance/utils';
|
||||
import { convertFileUrlToImageUrl } from '../../composables/shared/utils';
|
||||
import { friendRequest, notificationRequest, worldRequest } from '../../api';
|
||||
import {
|
||||
checkCanInvite,
|
||||
convertFileUrlToImageUrl,
|
||||
escapeTag,
|
||||
formatDateFilter,
|
||||
parseLocation,
|
||||
removeFromArray
|
||||
} from '../../shared/utils';
|
||||
import configRepository from '../../service/config';
|
||||
import database from '../../service/database';
|
||||
import { database } from '../../service/database';
|
||||
import {
|
||||
useAppearanceSettingsStore,
|
||||
useGalleryStore,
|
||||
useGameStore,
|
||||
useGroupStore,
|
||||
useInviteStore,
|
||||
useLocationStore,
|
||||
useNotificationStore,
|
||||
useUiStore,
|
||||
useUserStore,
|
||||
useWorldStore
|
||||
} from '../../stores';
|
||||
import SendInviteRequestResponseDialog from './dialogs/SendInviteRequestResponseDialog.vue';
|
||||
import SendInviteResponseDialog from './dialogs/SendInviteResponseDialog.vue';
|
||||
import Location from '../../components/Location.vue';
|
||||
import Noty from 'noty';
|
||||
|
||||
const { hideTooltips } = storeToRefs(useAppearanceSettingsStore());
|
||||
const { showUserDialog } = useUserStore();
|
||||
const { showWorldDialog } = useWorldStore();
|
||||
const { showGroupDialog } = useGroupStore();
|
||||
const { lastLocation, lastLocationDestination } = storeToRefs(useLocationStore());
|
||||
const { refreshInviteMessageTableData } = useInviteStore();
|
||||
const { clearInviteImageUpload } = useGalleryStore();
|
||||
const { notificationTable, isNotificationsLoading } = storeToRefs(useNotificationStore());
|
||||
const { refreshNotifications, handleNotificationHide } = useNotificationStore();
|
||||
const { menuActiveIndex, shiftHeld } = storeToRefs(useUiStore());
|
||||
const { isGameRunning } = storeToRefs(useGameStore());
|
||||
const { showFullscreenImageDialog } = useGalleryStore();
|
||||
const { currentUser } = storeToRefs(useUserStore());
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { $confirm, $message } = getCurrentInstance().proxy;
|
||||
|
||||
const API = inject('API');
|
||||
const showWorldDialog = inject('showWorldDialog');
|
||||
const showGroupDialog = inject('showGroupDialog');
|
||||
const showUserDialog = inject('showUserDialog');
|
||||
const showFullscreenImageDialog = inject('showFullscreenImageDialog');
|
||||
const clearInviteImageUpload = inject('clearInviteImageUpload');
|
||||
|
||||
const props = defineProps({
|
||||
menuActiveIndex: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
notificationTable: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
shiftHeld: { type: Boolean, default: false },
|
||||
hideTooltips: { type: Boolean, default: false },
|
||||
lastLocation: { type: Object, default: () => ({}) },
|
||||
inviteResponseMessageTable: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
uploadImage: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
lastLocationDestination: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
isGameRunning: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
checkCanInvite: {
|
||||
type: Function,
|
||||
default: () => true
|
||||
},
|
||||
inviteRequestResponseMessageTable: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const sendInviteResponseDialog = ref({
|
||||
messageSlot: {},
|
||||
invite: {}
|
||||
@@ -491,7 +472,7 @@
|
||||
function saveTableFilters() {
|
||||
configRepository.setString(
|
||||
'VRCX_notificationTableFilters',
|
||||
JSON.stringify(props.notificationTable.filters[0].value)
|
||||
JSON.stringify(notificationTable.value.filters[0].value)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -536,7 +517,7 @@
|
||||
function showSendInviteResponseDialog(invite) {
|
||||
sendInviteResponseDialog.value.invite = invite;
|
||||
sendInviteResponseDialog.value.messageSlot = {};
|
||||
inviteMessagesRequest.refreshInviteMessageTableData('response');
|
||||
refreshInviteMessageTableData('response');
|
||||
clearInviteImageUpload();
|
||||
sendInviteResponseDialogVisible.value = true;
|
||||
}
|
||||
@@ -548,10 +529,9 @@
|
||||
type: 'info',
|
||||
callback: (action) => {
|
||||
if (action === 'confirm') {
|
||||
let currentLocation = props.lastLocation.location;
|
||||
// todo
|
||||
if (props.lastLocation.location === 'traveling') {
|
||||
currentLocation = props.lastLocationDestination;
|
||||
let currentLocation = lastLocation.value.location;
|
||||
if (lastLocation.value.location === 'traveling') {
|
||||
currentLocation = lastLocationDestination.value;
|
||||
}
|
||||
const L = parseLocation(currentLocation);
|
||||
worldRequest
|
||||
@@ -585,14 +565,14 @@
|
||||
function showSendInviteRequestResponseDialog(invite) {
|
||||
sendInviteResponseDialog.value.invite = invite;
|
||||
sendInviteResponseDialog.value.messageSlot = {};
|
||||
inviteMessagesRequest.refreshInviteMessageTableData('requestResponse');
|
||||
refreshInviteMessageTableData('requestResponse');
|
||||
clearInviteImageUpload();
|
||||
sendInviteRequestResponseDialogVisible.value = true;
|
||||
}
|
||||
|
||||
function sendNotificationResponse(notificationId, responses, responseType) {
|
||||
if (!Array.isArray(responses) || responses.length === 0) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
let responseData = '';
|
||||
for (let i = 0; i < responses.length; i++) {
|
||||
@@ -601,21 +581,41 @@
|
||||
break;
|
||||
}
|
||||
}
|
||||
return notificationRequest.sendNotificationResponse({
|
||||
const params = {
|
||||
notificationId,
|
||||
responseType,
|
||||
responseData
|
||||
});
|
||||
};
|
||||
notificationRequest
|
||||
.sendNotificationResponse(params)
|
||||
.then((json) => {
|
||||
const args = {
|
||||
json,
|
||||
params
|
||||
};
|
||||
handleNotificationHide(args);
|
||||
new Noty({
|
||||
type: 'success',
|
||||
text: escapeTag(args.json)
|
||||
}).show();
|
||||
console.log('NOTIFICATION:RESPONSE', args);
|
||||
})
|
||||
.catch((err) => {
|
||||
handleNotificationHide({ params });
|
||||
notificationRequest.hideNotificationV2(params.notificationId);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
function hideNotification(row) {
|
||||
async function hideNotification(row) {
|
||||
if (row.type === 'ignoredFriendRequest') {
|
||||
friendRequest.deleteHiddenFriendRequest(
|
||||
const args = await friendRequest.deleteHiddenFriendRequest(
|
||||
{
|
||||
notificationId: row.id
|
||||
},
|
||||
row.senderUserId
|
||||
);
|
||||
useNotificationStore().handleNotificationHide(args);
|
||||
} else {
|
||||
notificationRequest.hideNotification({
|
||||
notificationId: row.id
|
||||
@@ -637,7 +637,7 @@
|
||||
}
|
||||
|
||||
function deleteNotificationLog(row) {
|
||||
utils.removeFromArray(props.notificationTable.data, row);
|
||||
removeFromArray(notificationTable.value.data, row);
|
||||
if (row.type !== 'friendRequest' && row.type !== 'ignoredFriendRequest') {
|
||||
database.deleteNotification(row.id);
|
||||
}
|
||||
|
||||
@@ -30,24 +30,23 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getCurrentInstance, inject } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { getCurrentInstance } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { inviteMessagesRequest, notificationRequest } from '../../../api';
|
||||
import { useGalleryStore } from '../../../stores';
|
||||
|
||||
const { t } = useI18n();
|
||||
const instance = getCurrentInstance();
|
||||
const $message = instance.proxy.$message;
|
||||
|
||||
const API = inject('API');
|
||||
const galleryStore = useGalleryStore();
|
||||
const { uploadImage } = storeToRefs(galleryStore);
|
||||
|
||||
const props = defineProps({
|
||||
editAndSendInviteResponseDialog: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
uploadImage: {
|
||||
type: String
|
||||
},
|
||||
sendInviteResponseDialog: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
@@ -76,7 +75,6 @@
|
||||
throw err;
|
||||
})
|
||||
.then((args) => {
|
||||
API.$emit(`INVITE:${messageType.toUpperCase()}`, args);
|
||||
if (args.json[slot].message === I.messageSlot.message) {
|
||||
$message({
|
||||
message: "VRChat API didn't update message, try again",
|
||||
@@ -93,7 +91,7 @@
|
||||
responseSlot: slot,
|
||||
rsvp: true
|
||||
};
|
||||
if (props.uploadImage) {
|
||||
if (uploadImage.value) {
|
||||
notificationRequest
|
||||
.sendInviteResponsePhoto(params, I.invite.id)
|
||||
.catch((err) => {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
width="800px"
|
||||
append-to-body
|
||||
@close="cancelSendInviteRequestResponse">
|
||||
<template v-if="API.currentUser.$isVRCPlus">
|
||||
<template v-if="currentUser.$isVRCPlus">
|
||||
<input class="inviteImageUploadButton" type="file" accept="image/*" @change="inviteImageUpload" />
|
||||
</template>
|
||||
|
||||
@@ -43,33 +43,36 @@
|
||||
<el-button type="small" @click="cancelSendInviteRequestResponse">
|
||||
{{ t('dialog.invite_request_response_message.cancel') }}
|
||||
</el-button>
|
||||
<el-button type="small" @click="API.refreshInviteMessageTableData('requestResponse')">
|
||||
<el-button type="small" @click="refreshInviteMessageTableData('requestResponse')">
|
||||
{{ t('dialog.invite_request_response_message.refresh') }}
|
||||
</el-button>
|
||||
</template>
|
||||
<EditAndSendInviteResponseDialog
|
||||
:edit-and-send-invite-response-dialog.sync="editAndSendInviteResponseDialog"
|
||||
:upload-image="uploadImage"
|
||||
:send-invite-response-dialog.sync="sendInviteResponseDialog"
|
||||
@closeInviteDialog="closeInviteDialog" />
|
||||
<SendInviteResponseConfirmDialog
|
||||
:send-invite-response-dialog.sync="sendInviteResponseDialog"
|
||||
:upload-image="uploadImage"
|
||||
:send-invite-response-confirm-dialog="sendInviteResponseConfirmDialog"
|
||||
@closeInviteDialog="closeInviteDialog" />
|
||||
</safe-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject, ref } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { useGalleryStore, useInviteStore, useUserStore } from '../../../stores';
|
||||
import EditAndSendInviteResponseDialog from './EditAndSendInviteResponseDialog.vue';
|
||||
import SendInviteResponseConfirmDialog from './SendInviteResponseConfirmDialog.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const API = inject('API');
|
||||
const inviteImageUpload = inject('inviteImageUpload');
|
||||
const inviteStore = useInviteStore();
|
||||
const { refreshInviteMessageTableData } = inviteStore;
|
||||
const { inviteRequestResponseMessageTable } = storeToRefs(inviteStore);
|
||||
const galleryStore = useGalleryStore();
|
||||
const { inviteImageUpload } = galleryStore;
|
||||
const { currentUser } = storeToRefs(useUserStore());
|
||||
|
||||
const props = defineProps({
|
||||
sendInviteResponseDialog: {
|
||||
@@ -79,13 +82,6 @@
|
||||
sendInviteRequestResponseDialogVisible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
inviteRequestResponseMessageTable: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
uploadImage: {
|
||||
type: String
|
||||
}
|
||||
});
|
||||
|
||||
@@ -117,6 +113,10 @@
|
||||
cancelSendInviteRequestResponse();
|
||||
}
|
||||
|
||||
// function refreshInviteMessageTableData(...arg) {
|
||||
// inviteMessagesRequest.refreshInviteMessageTableData(arg);
|
||||
// }
|
||||
|
||||
function cancelSendInviteRequestResponse() {
|
||||
emit('update:sendInviteRequestResponseDialogVisible', false);
|
||||
}
|
||||
|
||||
@@ -22,22 +22,24 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { getCurrentInstance } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { notificationRequest } from '../../../api';
|
||||
import { useGalleryStore } from '../../../stores';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const instance = getCurrentInstance();
|
||||
const $message = instance.proxy.$message;
|
||||
const galleryStore = useGalleryStore();
|
||||
const { uploadImage } = storeToRefs(galleryStore);
|
||||
|
||||
const props = defineProps({
|
||||
sendInviteResponseDialog: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
uploadImage: {
|
||||
type: String
|
||||
},
|
||||
sendInviteResponseConfirmDialog: {
|
||||
type: Object,
|
||||
required: true
|
||||
@@ -48,7 +50,6 @@
|
||||
|
||||
function cancelInviteResponseConfirm() {
|
||||
emit('update:sendInviteResponseConfirmDialog', { visible: false });
|
||||
// TODO: temp fix to close dialog
|
||||
props.sendInviteResponseConfirmDialog.visible = false;
|
||||
}
|
||||
|
||||
@@ -58,7 +59,7 @@
|
||||
responseSlot: D.messageSlot.slot,
|
||||
rsvp: true
|
||||
};
|
||||
if (props.uploadImage) {
|
||||
if (uploadImage.value) {
|
||||
notificationRequest
|
||||
.sendInviteResponsePhoto(params, D.invite.id, D.messageSlot.messageType)
|
||||
.catch((err) => {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
width="800px"
|
||||
append-to-body
|
||||
@close="cancelSendInviteResponse">
|
||||
<template v-if="API.currentUser.$isVRCPlus">
|
||||
<template v-if="currentUser.$isVRCPlus">
|
||||
<input class="inviteImageUploadButton" type="file" accept="image/*" @change="inviteImageUpload" />
|
||||
</template>
|
||||
|
||||
@@ -45,33 +45,38 @@
|
||||
<el-button type="small" @click="cancelSendInviteResponse">{{
|
||||
t('dialog.invite_response_message.cancel')
|
||||
}}</el-button>
|
||||
<el-button type="small" @click="API.refreshInviteMessageTableData('response')">{{
|
||||
<el-button type="small" @click="refreshInviteMessageTableData('response')">{{
|
||||
t('dialog.invite_response_message.refresh')
|
||||
}}</el-button>
|
||||
</template>
|
||||
<EditAndSendInviteResponseDialog
|
||||
:edit-and-send-invite-response-dialog.sync="editAndSendInviteResponseDialog"
|
||||
:upload-image="uploadImage"
|
||||
:send-invite-response-dialog.sync="sendInviteResponseDialog"
|
||||
@closeInviteDialog="closeInviteDialog" />
|
||||
<SendInviteResponseConfirmDialog
|
||||
:send-invite-response-dialog.sync="sendInviteResponseDialog"
|
||||
:upload-image="uploadImage"
|
||||
:send-invite-response-confirm-dialog="sendInviteResponseConfirmDialog"
|
||||
@closeInviteDialog="closeInviteDialog" />
|
||||
</safe-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject, ref } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { useGalleryStore, useInviteStore, useUserStore } from '../../../stores';
|
||||
import EditAndSendInviteResponseDialog from './EditAndSendInviteResponseDialog.vue';
|
||||
import SendInviteResponseConfirmDialog from './SendInviteResponseConfirmDialog.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const API = inject('API');
|
||||
const inviteImageUpload = inject('inviteImageUpload');
|
||||
const inviteStore = useInviteStore();
|
||||
const { refreshInviteMessageTableData } = inviteStore;
|
||||
const { inviteResponseMessageTable } = storeToRefs(inviteStore);
|
||||
const galleryStore = useGalleryStore();
|
||||
const { inviteImageUpload } = galleryStore;
|
||||
const { currentUser } = storeToRefs(useUserStore());
|
||||
|
||||
const props = defineProps({
|
||||
sendInviteResponseDialog: {
|
||||
type: Object,
|
||||
@@ -80,13 +85,6 @@
|
||||
sendInviteResponseDialogVisible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
inviteResponseMessageTable: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
uploadImage: {
|
||||
type: String
|
||||
}
|
||||
});
|
||||
|
||||
@@ -116,7 +114,6 @@
|
||||
visible: true
|
||||
};
|
||||
}
|
||||
|
||||
function showSendInviteResponseConfirmDialog(row) {
|
||||
props.sendInviteResponseDialog.messageSlot = row;
|
||||
sendInviteResponseConfirmDialog.value.visible = true;
|
||||
|
||||
@@ -29,8 +29,8 @@
|
||||
@click="showWorldDialog(currentInstanceWorld.ref.id)">
|
||||
<i
|
||||
v-show="
|
||||
API.currentUser.$homeLocation &&
|
||||
API.currentUser.$homeLocation.worldId === currentInstanceWorld.ref.id
|
||||
currentUser.$homeLocation &&
|
||||
currentUser.$homeLocation.worldId === currentInstanceWorld.ref.id
|
||||
"
|
||||
class="el-icon-s-home"
|
||||
style="margin-right: 5px"></i>
|
||||
@@ -132,16 +132,13 @@
|
||||
</el-tag>
|
||||
</div>
|
||||
<div style="margin-top: 5px">
|
||||
<location-world
|
||||
:locationobject="currentInstanceLocation"
|
||||
:currentuserid="API.currentUser.id"
|
||||
@show-launch-dialog="showLaunchDialog"></location-world>
|
||||
<LocationWorld :locationobject="currentInstanceLocation" :currentuserid="currentUser.id" />
|
||||
<span v-if="lastLocation.playerList.size > 0" style="margin-left: 5px">
|
||||
{{ lastLocation.playerList.size }}
|
||||
<template v-if="lastLocation.friendList.size > 0"
|
||||
>({{ lastLocation.friendList.size }})</template
|
||||
>
|
||||
― <timer v-if="lastLocation.date" :epoch="lastLocation.date"></timer>
|
||||
― <Timer v-if="lastLocation.date" :epoch="lastLocation.date" />
|
||||
</span>
|
||||
</div>
|
||||
<div style="margin-top: 5px">
|
||||
@@ -175,8 +172,8 @@
|
||||
<div class="detail">
|
||||
<span class="name">{{ t('dialog.world.info.capacity') }}</span>
|
||||
<span class="extra"
|
||||
>{{ currentInstanceWorld.ref.recommendedCapacity | commaNumber }} ({{
|
||||
currentInstanceWorld.ref.capacity | commaNumber
|
||||
>{{ commaNumber(currentInstanceWorld.ref.recommendedCapacity) }} ({{
|
||||
commaNumber(currentInstanceWorld.ref.capacity)
|
||||
}})</span
|
||||
>
|
||||
</div>
|
||||
@@ -184,13 +181,15 @@
|
||||
<div class="x-friend-item" style="cursor: default">
|
||||
<div class="detail">
|
||||
<span class="name">{{ t('dialog.world.info.last_updated') }}</span>
|
||||
<span class="extra">{{ currentInstanceWorld.lastUpdated | formatDate('long') }}</span>
|
||||
<span class="extra">{{ formatDateFilter(currentInstanceWorld.lastUpdated, 'long') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="x-friend-item" style="cursor: default">
|
||||
<div class="detail">
|
||||
<span class="name">{{ t('dialog.world.info.created_at') }}</span>
|
||||
<span class="extra">{{ currentInstanceWorld.ref.created_at | formatDate('long') }}</span>
|
||||
<span class="extra">{{
|
||||
formatDateFilter(currentInstanceWorld.ref.created_at, 'long')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -245,9 +244,9 @@
|
||||
<template #default="scope">
|
||||
<el-tooltip placement="right">
|
||||
<template #content>
|
||||
<span>{{ scope.row.created_at | formatDate('long') }}</span>
|
||||
<span>{{ formatDateFilter(scope.row.created_at, 'long') }}</span>
|
||||
</template>
|
||||
<span>{{ scope.row.created_at | formatDate('short') }}</span>
|
||||
<span>{{ formatDateFilter(scope.row.created_at, 'short') }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -377,11 +376,11 @@
|
||||
v-else-if="scope.row.type === 'PortalSpawn'"
|
||||
class="x-link"
|
||||
@click="showWorldDialog(scope.row.location, scope.row.shortName)">
|
||||
<location
|
||||
<Location
|
||||
:location="scope.row.location"
|
||||
:hint="scope.row.worldName"
|
||||
:grouphint="scope.row.groupName"
|
||||
:link="false"></location>
|
||||
:link="false" />
|
||||
</span>
|
||||
<span
|
||||
v-else-if="scope.row.type === 'ChatBoxMessage'"
|
||||
@@ -445,9 +444,9 @@
|
||||
<template #default="scope">
|
||||
<el-tooltip placement="right">
|
||||
<template #content>
|
||||
<span>{{ scope.row.created_at | formatDate('long') }}</span>
|
||||
<span>{{ formatDateFilter(scope.row.created_at, 'long') }}</span>
|
||||
</template>
|
||||
<span>{{ scope.row.created_at | formatDate('short') }}</span>
|
||||
<span>{{ formatDateFilter(scope.row.created_at, 'short') }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -577,11 +576,11 @@
|
||||
v-else-if="scope.row.type === 'PortalSpawn'"
|
||||
class="x-link"
|
||||
@click="showWorldDialog(scope.row.location, scope.row.shortName)">
|
||||
<location
|
||||
<Location
|
||||
:location="scope.row.location"
|
||||
:hint="scope.row.worldName"
|
||||
:grouphint="scope.row.groupName"
|
||||
:link="false"></location>
|
||||
:link="false" />
|
||||
</span>
|
||||
<span
|
||||
v-else-if="scope.row.type === 'ChatBoxMessage'"
|
||||
@@ -665,7 +664,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('table.playerList.timer')" width="80" prop="timer" sortable>
|
||||
<template #default="scope">
|
||||
<timer :epoch="scope.row.timer"></timer>
|
||||
<Timer :epoch="scope.row.timer" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
@@ -824,138 +823,77 @@
|
||||
</div>
|
||||
<ChatboxBlacklistDialog
|
||||
:chatbox-blacklist-dialog="chatboxBlacklistDialog"
|
||||
:chatbox-user-blacklist="chatboxUserBlacklist"
|
||||
@delete-chatbox-user-blacklist="deleteChatboxUserBlacklist" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PlayerListTab'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup>
|
||||
import { inject, ref } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { languageClass } from '../../composables/user/utils';
|
||||
import configRepository from '../../service/config';
|
||||
import {
|
||||
languageClass,
|
||||
getFaviconUrl,
|
||||
openExternalLink,
|
||||
statusClass,
|
||||
userImage,
|
||||
userImageFull,
|
||||
commaNumber,
|
||||
formatDateFilter
|
||||
} from '../../shared/utils';
|
||||
import {
|
||||
useLocationStore,
|
||||
useAppearanceSettingsStore,
|
||||
usePhotonStore,
|
||||
useUserStore,
|
||||
useAvatarStore,
|
||||
useWorldStore,
|
||||
useGroupStore,
|
||||
useInstanceStore,
|
||||
useUiStore,
|
||||
useGalleryStore,
|
||||
useVrcxStore
|
||||
} from '../../stores';
|
||||
import ChatboxBlacklistDialog from './dialogs/ChatboxBlacklistDialog.vue';
|
||||
import { getFaviconUrl } from '../../composables/shared/utils';
|
||||
import { photonEventTableTypeFilterList } from '../../shared/constants';
|
||||
|
||||
const { hideTooltips, randomUserColours } = storeToRefs(useAppearanceSettingsStore());
|
||||
const {
|
||||
photonLoggingEnabled,
|
||||
photonEventIcon,
|
||||
photonEventTableTypeFilter,
|
||||
photonEventTable,
|
||||
photonEventTablePrevious,
|
||||
chatboxUserBlacklist,
|
||||
photonEventTableFilter
|
||||
} = storeToRefs(usePhotonStore());
|
||||
const { saveChatboxUserBlacklist, photonEventTableFilterChange, showUserFromPhotonId } = usePhotonStore();
|
||||
const { showUserDialog, lookupUser } = useUserStore();
|
||||
const { showAvatarDialog } = useAvatarStore();
|
||||
const { showWorldDialog } = useWorldStore();
|
||||
const { showGroupDialog } = useGroupStore();
|
||||
const { lastLocation } = storeToRefs(useLocationStore());
|
||||
const { currentInstanceLocation, currentInstanceWorld } = storeToRefs(useInstanceStore());
|
||||
const { currentInstanceUserList, getCurrentInstanceUserList } = useInstanceStore();
|
||||
const { menuActiveIndex } = storeToRefs(useUiStore());
|
||||
const { showFullscreenImageDialog } = useGalleryStore();
|
||||
const { ipcEnabled } = storeToRefs(useVrcxStore());
|
||||
const { currentUser } = storeToRefs(useUserStore());
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const API = inject('API');
|
||||
const showFullscreenImageDialog = inject('showFullscreenImageDialog');
|
||||
const showWorldDialog = inject('showWorldDialog');
|
||||
const showUserDialog = inject('showUserDialog');
|
||||
const showLaunchDialog = inject('showLaunchDialog');
|
||||
const showAvatarDialog = inject('showAvatarDialog');
|
||||
const statusClass = inject('statusClass');
|
||||
const showGroupDialog = inject('showGroupDialog');
|
||||
const openExternalLink = inject('openExternalLink');
|
||||
const userImage = inject('userImage');
|
||||
const userImageFull = inject('userImageFull');
|
||||
|
||||
const props = defineProps({
|
||||
menuActiveIndex: {
|
||||
type: String,
|
||||
default: 'playerList'
|
||||
},
|
||||
currentInstanceWorld: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
currentInstanceLocation: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
currentInstanceWorldDescriptionExpanded: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
photonLoggingEnabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
photonEventTableTypeFilter: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
photonEventTableTypeFilterList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
photonEventTableFilter: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
hideTooltips: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
ipcEnabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
photonEventIcon: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
photonEventTable: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
photonEventTablePrevious: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
currentInstanceUserList: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
chatboxUserBlacklist: {
|
||||
type: Map
|
||||
},
|
||||
randomUserColours: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
lastLocation: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
'photonEventTableFilterChange',
|
||||
'getCurrentInstanceUserList',
|
||||
'showUserFromPhotonId',
|
||||
'lookupUser'
|
||||
]);
|
||||
|
||||
const chatboxBlacklistDialog = ref({
|
||||
visible: false,
|
||||
loading: false
|
||||
});
|
||||
|
||||
function photonEventTableFilterChange(value) {
|
||||
emit('photonEventTableFilterChange', value);
|
||||
}
|
||||
const currentInstanceWorldDescriptionExpanded = ref(false);
|
||||
|
||||
function showChatboxBlacklistDialog() {
|
||||
const D = chatboxBlacklistDialog.value;
|
||||
D.visible = true;
|
||||
}
|
||||
|
||||
function showUserFromPhotonId(photonId) {
|
||||
emit('showUserFromPhotonId', photonId);
|
||||
}
|
||||
|
||||
function lookupUser(user) {
|
||||
emit('lookupUser', user);
|
||||
}
|
||||
|
||||
function selectCurrentInstanceRow(val) {
|
||||
if (val === null) {
|
||||
return;
|
||||
@@ -969,21 +907,14 @@
|
||||
}
|
||||
|
||||
async function deleteChatboxUserBlacklist(userId) {
|
||||
props.chatboxUserBlacklist.delete(userId);
|
||||
chatboxUserBlacklist.value.delete(userId);
|
||||
await saveChatboxUserBlacklist();
|
||||
emit('getCurrentInstanceUserList');
|
||||
}
|
||||
|
||||
async function saveChatboxUserBlacklist() {
|
||||
await configRepository.setString(
|
||||
'VRCX_chatboxUserBlacklist',
|
||||
JSON.stringify(Object.fromEntries(props.chatboxUserBlacklist))
|
||||
);
|
||||
getCurrentInstanceUserList();
|
||||
}
|
||||
|
||||
async function addChatboxUserBlacklist(user) {
|
||||
props.chatboxUserBlacklist.set(user.id, user.displayName);
|
||||
chatboxUserBlacklist.value.set(user.id, user.displayName);
|
||||
await saveChatboxUserBlacklist();
|
||||
emit('getCurrentInstanceUserList');
|
||||
getCurrentInstanceUserList();
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -43,19 +43,20 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import configRepository from '../../../service/config';
|
||||
import { usePhotonStore } from '../../../stores';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { chatboxUserBlacklist } = storeToRefs(usePhotonStore());
|
||||
|
||||
defineProps({
|
||||
chatboxBlacklistDialog: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
chatboxUserBlacklist: {
|
||||
type: Map,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
<template>
|
||||
<div v-if="menuActiveIndex === 'profile'" class="x-container">
|
||||
<div v-show="menuActiveIndex === 'profile'" class="x-container">
|
||||
<div class="options-container" style="margin-top: 0">
|
||||
<span class="header">{{ t('view.profile.profile.header') }}</span>
|
||||
<div class="x-friend-list" style="margin-top: 10px">
|
||||
<div class="x-friend-item" @click="showUserDialog(API.currentUser.id)">
|
||||
<div class="x-friend-item" @click="showUserDialog(currentUser.id)">
|
||||
<div class="avatar">
|
||||
<img v-lazy="userImage(API.currentUser, true)" />
|
||||
<img v-lazy="userImage(currentUser, true)" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span class="name" v-text="API.currentUser.displayName"></span>
|
||||
<span class="extra" v-text="API.currentUser.username"></span>
|
||||
<span class="name" v-text="currentUser.displayName"></span>
|
||||
<span class="extra" v-text="currentUser.username"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="x-friend-item" style="cursor: default">
|
||||
<div class="detail">
|
||||
<span class="name">{{ t('view.profile.profile.last_activity') }}</span>
|
||||
<span class="extra">{{ API.currentUser.last_activity | formatDate('long') }}</span>
|
||||
<span class="extra">{{ formatDateFilter(currentUser.last_activity, 'long') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="x-friend-item" style="cursor: default">
|
||||
<div class="detail">
|
||||
<span class="name">{{ t('view.profile.profile.two_factor') }}</span>
|
||||
<span class="extra">{{
|
||||
API.currentUser.twoFactorAuthEnabled
|
||||
currentUser.twoFactorAuthEnabled
|
||||
? t('view.profile.profile.two_factor_enabled')
|
||||
: t('view.profile.profile.two_factor_disabled')
|
||||
}}</span>
|
||||
@@ -101,12 +101,12 @@
|
||||
icon="el-icon-refresh"
|
||||
circle
|
||||
style="margin-left: 5px"
|
||||
@click="API.getConfig()"></el-button>
|
||||
@click="getConfig"></el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div class="x-friend-list" style="margin-top: 10px">
|
||||
<div
|
||||
v-for="(link, item) in API.cachedConfig.downloadUrls"
|
||||
v-for="(link, item) in cachedConfig.downloadUrls"
|
||||
:key="item"
|
||||
class="x-friend-item"
|
||||
placement="top">
|
||||
@@ -150,7 +150,7 @@
|
||||
style="margin-left: 5px"
|
||||
@click="
|
||||
inviteMessageTable.visible = true;
|
||||
refreshInviteMessageTable('message');
|
||||
refreshInviteMessageTableData('message');
|
||||
"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip placement="top" :content="t('view.profile.clear_results_tooltip')" :disabled="hideTooltips">
|
||||
@@ -204,7 +204,7 @@
|
||||
style="margin-left: 5px"
|
||||
@click="
|
||||
inviteResponseMessageTable.visible = true;
|
||||
refreshInviteMessageTable('response');
|
||||
refreshInviteMessageTableData('response');
|
||||
"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip placement="top" :content="t('view.profile.clear_results_tooltip')" :disabled="hideTooltips">
|
||||
@@ -261,7 +261,7 @@
|
||||
style="margin-left: 5px"
|
||||
@click="
|
||||
inviteRequestMessageTable.visible = true;
|
||||
refreshInviteMessageTable('request');
|
||||
refreshInviteMessageTableData('request');
|
||||
"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip placement="top" :content="t('view.profile.clear_results_tooltip')" :disabled="hideTooltips">
|
||||
@@ -318,7 +318,7 @@
|
||||
style="margin-left: 5px"
|
||||
@click="
|
||||
inviteRequestResponseMessageTable.visible = true;
|
||||
refreshInviteMessageTable('requestResponse');
|
||||
refreshInviteMessageTableData('requestResponse');
|
||||
"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip placement="top" :content="t('view.profile.clear_results_tooltip')" :disabled="hideTooltips">
|
||||
@@ -371,7 +371,7 @@
|
||||
prop="updated_at"
|
||||
sortable="custom">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.updated_at | formatDate('long') }}</span>
|
||||
<span>{{ formatDateFilter(scope.row.updated_at, 'long') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
@@ -489,92 +489,56 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ProfileTab'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup>
|
||||
import { inject, ref, getCurrentInstance } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { ref, getCurrentInstance } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { inviteMessagesRequest, miscRequest, userRequest } from '../../api';
|
||||
import utils from '../../classes/utils';
|
||||
import { parseAvatarUrl } from '../../composables/avatar/utils';
|
||||
import { authRequest, miscRequest, userRequest } from '../../api';
|
||||
import {
|
||||
parseAvatarUrl,
|
||||
buildTreeData,
|
||||
openExternalLink,
|
||||
userImage,
|
||||
parseUserUrl,
|
||||
formatDateFilter
|
||||
} from '../../shared/utils';
|
||||
import { useAuthStore } from '../../stores';
|
||||
import DiscordNamesDialog from './dialogs/DiscordNamesDialog.vue';
|
||||
import ExportFriendsListDialog from './dialogs/ExportFriendsListDialog.vue';
|
||||
import ExportAvatarsListDialog from './dialogs/ExportAvatarsListDialog.vue';
|
||||
import {
|
||||
useAppearanceSettingsStore,
|
||||
useSearchStore,
|
||||
useFriendStore,
|
||||
useUserStore,
|
||||
useAvatarStore,
|
||||
useInviteStore,
|
||||
useGalleryStore,
|
||||
useUiStore
|
||||
} from '../../stores';
|
||||
|
||||
const { friends } = storeToRefs(useFriendStore());
|
||||
const { hideTooltips } = storeToRefs(useAppearanceSettingsStore());
|
||||
const { pastDisplayNameTable, currentUser } = storeToRefs(useUserStore());
|
||||
const { showUserDialog, lookupUser, getCurrentUser } = useUserStore();
|
||||
const { showAvatarDialog } = useAvatarStore();
|
||||
const { showEditInviteMessageDialog, refreshInviteMessageTableData } = useInviteStore();
|
||||
const {
|
||||
inviteMessageTable,
|
||||
inviteResponseMessageTable,
|
||||
inviteRequestMessageTable,
|
||||
inviteRequestResponseMessageTable
|
||||
} = storeToRefs(useInviteStore());
|
||||
const { showGalleryDialog } = useGalleryStore();
|
||||
const { menuActiveIndex } = storeToRefs(useUiStore());
|
||||
const { directAccessWorld } = useSearchStore();
|
||||
const { logout } = useAuthStore();
|
||||
const { cachedConfig } = storeToRefs(useAuthStore());
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { $prompt, $message } = getCurrentInstance().proxy;
|
||||
|
||||
const API = inject('API');
|
||||
const userImage = inject('userImage');
|
||||
const showUserDialog = inject('showUserDialog');
|
||||
const showAvatarDialog = inject('showAvatarDialog');
|
||||
const showGalleryDialog = inject('showGalleryDialog');
|
||||
const openExternalLink = inject('openExternalLink');
|
||||
|
||||
const props = defineProps({
|
||||
menuActiveIndex: {
|
||||
type: String,
|
||||
default: 'profile'
|
||||
},
|
||||
hideTooltips: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
inviteMessageTable: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
visible: false,
|
||||
data: []
|
||||
})
|
||||
},
|
||||
inviteResponseMessageTable: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
visible: false,
|
||||
data: []
|
||||
})
|
||||
},
|
||||
inviteRequestMessageTable: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
visible: false,
|
||||
data: []
|
||||
})
|
||||
},
|
||||
inviteRequestResponseMessageTable: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
visible: false,
|
||||
data: []
|
||||
})
|
||||
},
|
||||
pastDisplayNameTable: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
visible: false,
|
||||
data: []
|
||||
})
|
||||
},
|
||||
friends: {
|
||||
type: Map,
|
||||
default: () => new Map()
|
||||
},
|
||||
directAccessWorld: {
|
||||
type: Function,
|
||||
default: () => {}
|
||||
},
|
||||
parseUserUrl: {
|
||||
type: Function,
|
||||
default: () => {}
|
||||
}
|
||||
});
|
||||
const emit = defineEmits(['logout', 'lookupUser', 'showEditInviteMessageDialog']);
|
||||
|
||||
const vrchatCredit = ref(null);
|
||||
const configTreeData = ref([]);
|
||||
const currentUserTreeData = ref([]);
|
||||
@@ -588,18 +552,13 @@
|
||||
|
||||
function getVisits() {
|
||||
miscRequest.getVisits().then((args) => {
|
||||
// API.$on('VISITS')
|
||||
visits.value = args.json;
|
||||
});
|
||||
}
|
||||
|
||||
function getVRChatCredits() {
|
||||
// API.$on('VRCCREDITS')
|
||||
miscRequest.getVRChatCredits().then((args) => (vrchatCredit.value = args.json?.balance));
|
||||
}
|
||||
function logout() {
|
||||
emit('logout');
|
||||
}
|
||||
|
||||
function showDiscordNamesDialog() {
|
||||
discordNamesDialogVisible.value = true;
|
||||
@@ -621,7 +580,7 @@
|
||||
inputErrorMessage: t('prompt.direct_access_username.input_error'),
|
||||
callback: (action, instance) => {
|
||||
if (action === 'confirm' && instance.inputValue) {
|
||||
emit('lookupUser', {
|
||||
lookupUser({
|
||||
displayName: instance.inputValue
|
||||
});
|
||||
}
|
||||
@@ -639,7 +598,7 @@
|
||||
if (action === 'confirm' && instance.inputValue) {
|
||||
const testUrl = instance.inputValue.substring(0, 15);
|
||||
if (testUrl === 'https://vrchat.') {
|
||||
const userId = this.parseUserUrl(instance.inputValue);
|
||||
const userId = parseUserUrl(instance.inputValue);
|
||||
if (userId) {
|
||||
showUserDialog(userId);
|
||||
} else {
|
||||
@@ -664,7 +623,7 @@
|
||||
inputErrorMessage: t('prompt.direct_access_world_id.input_error'),
|
||||
callback: (action, instance) => {
|
||||
if (action === 'confirm' && instance.inputValue) {
|
||||
if (!props.directAccessWorld(instance.inputValue)) {
|
||||
if (!directAccessWorld(instance.inputValue)) {
|
||||
$message({
|
||||
message: t('prompt.direct_access_world_id.message.error'),
|
||||
type: 'error'
|
||||
@@ -685,7 +644,7 @@
|
||||
if (action === 'confirm' && instance.inputValue) {
|
||||
const testUrl = instance.inputValue.substring(0, 15);
|
||||
if (testUrl === 'https://vrchat.') {
|
||||
const avatarId = props.parseAvatarUrl(instance.inputValue);
|
||||
const avatarId = parseAvatarUrl(instance.inputValue);
|
||||
if (avatarId) {
|
||||
showAvatarDialog(avatarId);
|
||||
} else {
|
||||
@@ -701,25 +660,21 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
function showEditInviteMessageDialog(messageType, inviteMessage) {
|
||||
emit('showEditInviteMessageDialog', messageType, inviteMessage);
|
||||
}
|
||||
function refreshInviteMessageTable(messageType) {
|
||||
inviteMessagesRequest.refreshInviteMessageTableData(messageType);
|
||||
async function getConfig() {
|
||||
await authRequest.getConfig();
|
||||
}
|
||||
async function refreshConfigTreeData() {
|
||||
await API.getConfig();
|
||||
configTreeData.value = utils.buildTreeData(API.cachedConfig);
|
||||
await getConfig();
|
||||
configTreeData.value = buildTreeData(cachedConfig.value);
|
||||
}
|
||||
async function refreshCurrentUserTreeData() {
|
||||
await API.getCurrentUser();
|
||||
currentUserTreeData.value = utils.buildTreeData(API.currentUser);
|
||||
await getCurrentUser();
|
||||
currentUserTreeData.value = buildTreeData(currentUser.value);
|
||||
}
|
||||
function getCurrentUserFeedback() {
|
||||
userRequest.getUserFeedback({ userId: API.currentUser.id }).then((args) => {
|
||||
// API.$on('USER:FEEDBACK')
|
||||
if (args.params.userId === API.currentUser.id) {
|
||||
currentUserFeedbackData.value = utils.buildTreeData(args.json);
|
||||
userRequest.getUserFeedback({ userId: currentUser.value.id }).then((args) => {
|
||||
if (args.params.userId === currentUser.value.id) {
|
||||
currentUserFeedbackData.value = buildTreeData(args.json);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -20,12 +20,13 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, inject } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
|
||||
const API = inject('API');
|
||||
import { useUserStore } from '../../../stores';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { currentUser } = storeToRefs(useUserStore());
|
||||
|
||||
const props = defineProps({
|
||||
discordNamesDialogVisible: {
|
||||
@@ -52,7 +53,7 @@
|
||||
const discordNamesContent = ref('');
|
||||
|
||||
function showDiscordNamesContent() {
|
||||
const { friends } = API.currentUser;
|
||||
const { friends } = currentUser.value;
|
||||
if (Array.isArray(friends) === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -27,29 +27,23 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, inject, getCurrentInstance } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { getCurrentInstance, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { inviteMessagesRequest } from '../../../api';
|
||||
import { useInviteStore } from '../../../stores';
|
||||
|
||||
const { t } = useI18n();
|
||||
const instance = getCurrentInstance();
|
||||
const $message = instance.proxy.$message;
|
||||
const API = inject('API');
|
||||
|
||||
const props = defineProps({
|
||||
editInviteMessageDialog: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
visible: false,
|
||||
newMessage: ''
|
||||
})
|
||||
}
|
||||
});
|
||||
const inviteStore = useInviteStore();
|
||||
const { editInviteMessageDialog } = storeToRefs(inviteStore);
|
||||
|
||||
const message = ref('');
|
||||
|
||||
watch(
|
||||
() => props.editInviteMessageDialog,
|
||||
() => editInviteMessageDialog.value,
|
||||
(newVal) => {
|
||||
if (newVal && newVal.visible) {
|
||||
message.value = newVal.newMessage;
|
||||
@@ -58,10 +52,8 @@
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
const emit = defineEmits(['update:editInviteMessageDialog']);
|
||||
|
||||
function saveEditInviteMessage() {
|
||||
const D = props.editInviteMessageDialog;
|
||||
const D = editInviteMessageDialog.value;
|
||||
D.visible = false;
|
||||
if (D.inviteMessage.message !== message.value) {
|
||||
const slot = D.inviteMessage.slot;
|
||||
@@ -75,7 +67,6 @@
|
||||
throw err;
|
||||
})
|
||||
.then((args) => {
|
||||
API.$emit(`INVITE:${messageType.toUpperCase()}`, args);
|
||||
if (args.json[slot].message === D.inviteMessage.message) {
|
||||
$message({
|
||||
message: "VRChat API didn't update message, try again",
|
||||
@@ -91,6 +82,6 @@
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
emit('update:editInviteMessageDialog', { ...props.editInviteMessageDialog, visible: false });
|
||||
editInviteMessageDialog.value.visible = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<safe-dialog :visible.sync="isVisible" :title="$t('dialog.export_own_avatars.header')" width="650px">
|
||||
<safe-dialog :visible.sync="isVisible" :title="t('dialog.export_own_avatars.header')" width="650px">
|
||||
<el-input
|
||||
v-model="exportAvatarsListCsv"
|
||||
v-loading="loading"
|
||||
@@ -13,87 +13,94 @@
|
||||
</safe-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { avatarRequest } from '../../../api';
|
||||
import { processBulk } from '../../../service/request';
|
||||
import { useAvatarStore, useUserStore } from '../../../stores';
|
||||
|
||||
export default {
|
||||
name: 'ExportAvatarsListDialog',
|
||||
inject: ['API'],
|
||||
props: {
|
||||
isExportAvatarsListDialogVisible: Boolean
|
||||
const { t } = useI18n();
|
||||
|
||||
const { cachedAvatars } = storeToRefs(useAvatarStore());
|
||||
const { applyAvatar } = useAvatarStore();
|
||||
const { currentUser } = storeToRefs(useUserStore());
|
||||
|
||||
const props = defineProps({
|
||||
isExportAvatarsListDialogVisible: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const exportAvatarsListCsv = ref('');
|
||||
const loading = ref(false);
|
||||
|
||||
const isVisible = computed({
|
||||
get() {
|
||||
return props.isExportAvatarsListDialogVisible;
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
exportAvatarsListCsv: '',
|
||||
loading: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isVisible: {
|
||||
get() {
|
||||
return this.isExportAvatarsListDialogVisible;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:is-export-avatars-list-dialog-visible', value);
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
isExportAvatarsListDialogVisible(value) {
|
||||
if (value) {
|
||||
this.initExportAvatarsListDialog();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initExportAvatarsListDialog() {
|
||||
this.loading = true;
|
||||
for (const ref of this.API.cachedAvatars.values()) {
|
||||
if (ref.authorId === this.API.currentUser.id) {
|
||||
this.API.cachedAvatars.delete(ref.id);
|
||||
}
|
||||
}
|
||||
const params = {
|
||||
n: 50,
|
||||
offset: 0,
|
||||
sort: 'updated',
|
||||
order: 'descending',
|
||||
releaseStatus: 'all',
|
||||
user: 'me'
|
||||
};
|
||||
const map = new Map();
|
||||
this.API.bulk({
|
||||
fn: avatarRequest.getAvatars,
|
||||
N: -1,
|
||||
params,
|
||||
handle: (args) => {
|
||||
for (const json of args.json) {
|
||||
const $ref = this.API.cachedAvatars.get(json.id);
|
||||
if (typeof $ref !== 'undefined') {
|
||||
map.set($ref.id, $ref);
|
||||
}
|
||||
}
|
||||
},
|
||||
done: () => {
|
||||
const avatars = Array.from(map.values());
|
||||
if (Array.isArray(avatars) === false) {
|
||||
return;
|
||||
}
|
||||
const lines = ['AvatarID,AvatarName'];
|
||||
const _ = function (str) {
|
||||
if (/[\x00-\x1f,"]/.test(str) === true) {
|
||||
return `"${str.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return str;
|
||||
};
|
||||
for (const avatar of avatars) {
|
||||
lines.push(`${_(avatar.id)},${_(avatar.name)}`);
|
||||
}
|
||||
this.exportAvatarsListCsv = lines.join('\n');
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
set(value) {
|
||||
emit('update:isExportAvatarsListDialogVisible', value);
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:isExportAvatarsListDialogVisible']);
|
||||
|
||||
watch(
|
||||
() => props.isExportAvatarsListDialogVisible,
|
||||
(value) => {
|
||||
if (value) {
|
||||
initExportAvatarsListDialog();
|
||||
}
|
||||
}
|
||||
};
|
||||
);
|
||||
|
||||
function initExportAvatarsListDialog() {
|
||||
loading.value = true;
|
||||
for (const ref of cachedAvatars.value.values()) {
|
||||
if (ref.authorId === currentUser.value.id) {
|
||||
cachedAvatars.value.delete(ref.id);
|
||||
}
|
||||
}
|
||||
const params = {
|
||||
n: 50,
|
||||
offset: 0,
|
||||
sort: 'updated',
|
||||
order: 'descending',
|
||||
releaseStatus: 'all',
|
||||
user: 'me'
|
||||
};
|
||||
const map = new Map();
|
||||
processBulk({
|
||||
fn: avatarRequest.getAvatars,
|
||||
N: -1,
|
||||
params,
|
||||
handle: (args) => {
|
||||
for (const json of args.json) {
|
||||
const ref = applyAvatar(json);
|
||||
map.set(ref.id, ref);
|
||||
}
|
||||
},
|
||||
done: () => {
|
||||
const avatars = Array.from(map.values());
|
||||
if (Array.isArray(avatars) === false) {
|
||||
return;
|
||||
}
|
||||
const lines = ['AvatarID,AvatarName'];
|
||||
const _ = function (str) {
|
||||
if (/[\x00-\x1f,"]/.test(str) === true) {
|
||||
return `"${str.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return str;
|
||||
};
|
||||
for (const avatar of avatars) {
|
||||
lines.push(`${_(avatar.id)},${_(avatar.name)}`);
|
||||
}
|
||||
exportAvatarsListCsv.value = lines.join('\n');
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<safe-dialog :title="$t('dialog.export_friends_list.header')" :visible.sync="isVisible" width="650px">
|
||||
<safe-dialog :title="t('dialog.export_friends_list.header')" :visible.sync="isVisible" width="650px">
|
||||
<el-tabs type="card">
|
||||
<el-tab-pane :label="$t('dialog.export_friends_list.csv')">
|
||||
<el-tab-pane :label="t('dialog.export_friends_list.csv')">
|
||||
<el-input
|
||||
v-model="exportFriendsListCsv"
|
||||
type="textarea"
|
||||
@@ -12,7 +12,7 @@
|
||||
style="margin-top: 15px"
|
||||
@click.native="$event.target.tagName === 'TEXTAREA' && $event.target.select()" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('dialog.export_friends_list.json')">
|
||||
<el-tab-pane :label="t('dialog.export_friends_list.json')">
|
||||
<el-input
|
||||
v-model="exportFriendsListJson"
|
||||
type="textarea"
|
||||
@@ -27,61 +27,71 @@
|
||||
</safe-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ExportFriendsListDialog',
|
||||
inject: ['API'],
|
||||
props: {
|
||||
friends: Map,
|
||||
isExportFriendsListDialogVisible: Boolean
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
import { useUserStore } from '../../../stores';
|
||||
|
||||
const props = defineProps({
|
||||
friends: {
|
||||
type: Map,
|
||||
required: true
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
exportFriendsListCsv: '',
|
||||
exportFriendsListJson: ''
|
||||
};
|
||||
isExportFriendsListDialogVisible: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
const emit = defineEmits(['update:isExportFriendsListDialogVisible']);
|
||||
|
||||
const { currentUser } = storeToRefs(useUserStore());
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const exportFriendsListCsv = ref('');
|
||||
const exportFriendsListJson = ref('');
|
||||
|
||||
const isVisible = computed({
|
||||
get() {
|
||||
return props.isExportFriendsListDialogVisible;
|
||||
},
|
||||
computed: {
|
||||
isVisible: {
|
||||
get() {
|
||||
return this.isExportFriendsListDialogVisible;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:is-export-friends-list-dialog-visible', value);
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
isExportFriendsListDialogVisible(value) {
|
||||
if (value) {
|
||||
this.initExportFriendsListDialog();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initExportFriendsListDialog() {
|
||||
const { friends } = this.API.currentUser;
|
||||
if (Array.isArray(friends) === false) {
|
||||
return;
|
||||
}
|
||||
const lines = ['UserID,DisplayName,Memo'];
|
||||
const _ = function (str) {
|
||||
if (/[\x00-\x1f,"]/.test(str) === true) {
|
||||
return `"${str.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return str;
|
||||
};
|
||||
const friendsList = [];
|
||||
for (const userId of friends) {
|
||||
const ref = this.friends.get(userId);
|
||||
const name = (typeof ref !== 'undefined' && ref.name) || '';
|
||||
const memo = (typeof ref !== 'undefined' && ref.memo.replace(/\n/g, ' ')) || '';
|
||||
lines.push(`${_(userId)},${_(name)},${_(memo)}`);
|
||||
friendsList.push(userId);
|
||||
}
|
||||
this.exportFriendsListJson = JSON.stringify({ friends: friendsList }, null, 4);
|
||||
this.exportFriendsListCsv = lines.join('\n');
|
||||
set(value) {
|
||||
emit('update:isExportFriendsListDialogVisible', value);
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.isExportFriendsListDialogVisible,
|
||||
(value) => {
|
||||
if (value) {
|
||||
initExportFriendsListDialog();
|
||||
}
|
||||
}
|
||||
};
|
||||
);
|
||||
|
||||
function initExportFriendsListDialog() {
|
||||
const { friends } = currentUser.value;
|
||||
if (Array.isArray(friends) === false) {
|
||||
return;
|
||||
}
|
||||
const lines = ['UserID,DisplayName,Memo'];
|
||||
const _ = function (str) {
|
||||
if (/[\x00-\x1f,"]/.test(str) === true) {
|
||||
return `"${str.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return str;
|
||||
};
|
||||
const friendsList = [];
|
||||
for (const userId of friends) {
|
||||
const ref = props.friends.get(userId);
|
||||
const name = (typeof ref !== 'undefined' && ref.name) || '';
|
||||
const memo = (typeof ref !== 'undefined' && ref.memo.replace(/\n/g, ' ')) || '';
|
||||
lines.push(`${_(userId)},${_(name)},${_(memo)}`);
|
||||
friendsList.push(userId);
|
||||
}
|
||||
exportFriendsListJson.value = JSON.stringify({ friends: friendsList }, null, 4);
|
||||
exportFriendsListCsv.value = lines.join('\n');
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
icon="el-icon-delete"
|
||||
circle
|
||||
style="flex: none; margin-left: 10px"
|
||||
@click="clearSearch"></el-button>
|
||||
@click="handleClearSearch"></el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-tabs ref="searchTabRef" type="card" style="margin-top: 15px" @tab-click="searchText = ''">
|
||||
@@ -55,14 +55,14 @@
|
||||
:disabled="!searchUserParams.offset"
|
||||
icon="el-icon-back"
|
||||
size="small"
|
||||
@click="moreSearchUser(-1)"
|
||||
@click="handleMoreSearchUser(-1)"
|
||||
>{{ t('view.search.prev_page') }}</el-button
|
||||
>
|
||||
<el-button
|
||||
:disabled="searchUserResults.length < 10"
|
||||
icon="el-icon-right"
|
||||
size="small"
|
||||
@click="moreSearchUser(1)"
|
||||
@click="handleMoreSearchUser(1)"
|
||||
>{{ t('view.search.next_page') }}</el-button
|
||||
>
|
||||
</el-button-group>
|
||||
@@ -81,7 +81,7 @@
|
||||
></el-button>
|
||||
<el-dropdown-menu v-slot="dropdown">
|
||||
<el-dropdown-item
|
||||
v-for="row in API.cachedConfig.dynamicWorldRows"
|
||||
v-for="row in cachedConfig.dynamicWorldRows"
|
||||
:key="row.index"
|
||||
:command="row"
|
||||
v-text="row.name"></el-dropdown-item>
|
||||
@@ -313,79 +313,51 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SearchTab'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup>
|
||||
import { inject, ref } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { groupRequest, worldRequest } from '../../api';
|
||||
import utils from '../../classes/utils';
|
||||
import { convertFileUrlToImageUrl } from '../../composables/shared/utils';
|
||||
import {
|
||||
compareByCreatedAt,
|
||||
compareByName,
|
||||
compareByUpdatedAt,
|
||||
convertFileUrlToImageUrl,
|
||||
replaceBioSymbols,
|
||||
userImage
|
||||
} from '../../shared/utils';
|
||||
import {
|
||||
useAdvancedSettingsStore,
|
||||
useAppearanceSettingsStore,
|
||||
useAuthStore,
|
||||
useAvatarProviderStore,
|
||||
useAvatarStore,
|
||||
useGroupStore,
|
||||
useSearchStore,
|
||||
useUiStore,
|
||||
useUserStore,
|
||||
useWorldStore
|
||||
} from '../../stores';
|
||||
|
||||
const { hideTooltips, randomUserColours } = storeToRefs(useAppearanceSettingsStore());
|
||||
const { avatarRemoteDatabase } = storeToRefs(useAdvancedSettingsStore());
|
||||
const { avatarRemoteDatabaseProviderList, avatarRemoteDatabaseProvider } = storeToRefs(useAvatarProviderStore());
|
||||
const { setAvatarProvider } = useAvatarProviderStore();
|
||||
const { userDialog } = storeToRefs(useUserStore());
|
||||
const { showUserDialog, refreshUserDialogAvatars } = useUserStore();
|
||||
const { showAvatarDialog, lookupAvatars } = useAvatarStore();
|
||||
const { cachedAvatars } = storeToRefs(useAvatarStore());
|
||||
const { cachedWorlds } = storeToRefs(useWorldStore());
|
||||
const { showWorldDialog } = useWorldStore();
|
||||
const { showGroupDialog, applyGroup } = useGroupStore();
|
||||
const { cachedGroups } = storeToRefs(useGroupStore());
|
||||
const { menuActiveIndex } = storeToRefs(useUiStore());
|
||||
const { searchText, searchUserResults } = storeToRefs(useSearchStore());
|
||||
const { clearSearch, moreSearchUser } = useSearchStore();
|
||||
const { cachedConfig } = storeToRefs(useAuthStore());
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const API = inject('API');
|
||||
const showUserDialog = inject('showUserDialog');
|
||||
const userImage = inject('userImage');
|
||||
const showWorldDialog = inject('showWorldDialog');
|
||||
const showAvatarDialog = inject('showAvatarDialog');
|
||||
const showGroupDialog = inject('showGroupDialog');
|
||||
|
||||
const props = defineProps({
|
||||
menuActiveIndex: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
searchText: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
searchUserResults: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
randomUserColours: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
avatarRemoteDatabaseProviderList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
avatarRemoteDatabaseProvider: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
hideTooltips: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
userDialog: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
lookupAvatars: {
|
||||
type: Function,
|
||||
default: () => () => {}
|
||||
},
|
||||
avatarRemoteDatabase: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
'clearSearch',
|
||||
'setAvatarProvider',
|
||||
'refreshUserDialogAvatars',
|
||||
'moreSearchUser',
|
||||
'update:searchText'
|
||||
]);
|
||||
|
||||
const searchTabRef = ref(null);
|
||||
|
||||
const searchUserParams = ref({});
|
||||
@@ -416,7 +388,7 @@
|
||||
return convertFileUrlToImageUrl(url);
|
||||
}
|
||||
|
||||
function clearSearch() {
|
||||
function handleClearSearch() {
|
||||
searchUserParams.value = {};
|
||||
searchWorldParams.value = {};
|
||||
searchWorldResults.value = [];
|
||||
@@ -425,11 +397,11 @@
|
||||
searchAvatarPageNum.value = 0;
|
||||
searchGroupParams.value = {};
|
||||
searchGroupResults.value = [];
|
||||
emit('clearSearch');
|
||||
clearSearch();
|
||||
}
|
||||
|
||||
function updateSearchText(text) {
|
||||
emit('update:searchText', text);
|
||||
searchText.value = text;
|
||||
}
|
||||
|
||||
function search() {
|
||||
@@ -453,16 +425,19 @@
|
||||
searchUserParams.value = {
|
||||
n: 10,
|
||||
offset: 0,
|
||||
search: props.searchText,
|
||||
search: searchText.value,
|
||||
customFields: searchUserByBio.value ? 'bio' : 'displayName',
|
||||
sort: searchUserSortByLastLoggedIn.value ? 'last_login' : 'relevance'
|
||||
};
|
||||
await moreSearchUser();
|
||||
await handleMoreSearchUser();
|
||||
}
|
||||
|
||||
async function moreSearchUser(go = null) {
|
||||
emit('moreSearchUser', go, searchUserParams.value);
|
||||
async function handleMoreSearchUser(go = null) {
|
||||
isSearchUserLoading.value = true;
|
||||
await moreSearchUser(go, searchUserParams.value);
|
||||
isSearchUserLoading.value = false;
|
||||
}
|
||||
|
||||
function searchWorld(ref) {
|
||||
searchWorldOption.value = '';
|
||||
const params = {
|
||||
@@ -508,7 +483,7 @@
|
||||
break;
|
||||
default:
|
||||
params.sort = 'relevance';
|
||||
params.search = utils.replaceBioSymbols(props.searchText);
|
||||
params.search = replaceBioSymbols(searchText.value);
|
||||
break;
|
||||
}
|
||||
params.order = ref.sortOrder || 'descending';
|
||||
@@ -548,7 +523,7 @@
|
||||
.then((args) => {
|
||||
const map = new Map();
|
||||
for (const json of args.json) {
|
||||
const ref = API.cachedWorlds.get(json.id);
|
||||
const ref = cachedWorlds.value.get(json.id);
|
||||
if (typeof ref !== 'undefined') {
|
||||
map.set(ref.id, ref);
|
||||
}
|
||||
@@ -558,13 +533,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
function setAvatarProvider(provider) {
|
||||
emit('setAvatarProvider', provider);
|
||||
}
|
||||
function refreshUserDialogAvatars(fileId) {
|
||||
emit('refreshUserDialogAvatars', fileId);
|
||||
}
|
||||
|
||||
async function searchAvatar() {
|
||||
let ref;
|
||||
isSearchAvatarLoading.value = true;
|
||||
@@ -581,10 +549,10 @@
|
||||
searchAvatarSort.value = 'name';
|
||||
}
|
||||
const avatars = new Map();
|
||||
const query = props.searchText;
|
||||
const query = searchText.value;
|
||||
const queryUpper = query.toUpperCase();
|
||||
if (!query) {
|
||||
for (ref of API.cachedAvatars.values()) {
|
||||
for (ref of cachedAvatars.value.values()) {
|
||||
switch (searchAvatarFilter.value) {
|
||||
case 'all':
|
||||
avatars.set(ref.id, ref);
|
||||
@@ -604,7 +572,7 @@
|
||||
isSearchAvatarLoading.value = false;
|
||||
} else {
|
||||
if (searchAvatarFilterRemote.value === 'all' || searchAvatarFilterRemote.value === 'local') {
|
||||
for (ref of API.cachedAvatars.values()) {
|
||||
for (ref of cachedAvatars.value.values()) {
|
||||
let match = ref.name.toUpperCase().includes(queryUpper);
|
||||
if (!match && ref.description) {
|
||||
match = ref.description.toUpperCase().includes(queryUpper);
|
||||
@@ -633,10 +601,10 @@
|
||||
}
|
||||
if (
|
||||
(searchAvatarFilterRemote.value === 'all' || searchAvatarFilterRemote.value === 'remote') &&
|
||||
props.avatarRemoteDatabase &&
|
||||
avatarRemoteDatabase.value &&
|
||||
query.length >= 3
|
||||
) {
|
||||
const data = await props.lookupAvatars('search', query);
|
||||
const data = await lookupAvatars('search', query);
|
||||
if (data && typeof data === 'object') {
|
||||
data.forEach((avatar) => {
|
||||
avatars.set(avatar.id, avatar);
|
||||
@@ -649,13 +617,13 @@
|
||||
if (searchAvatarFilterRemote.value === 'local') {
|
||||
switch (searchAvatarSort.value) {
|
||||
case 'updated':
|
||||
avatarsArray.sort(utils.compareByUpdatedAt);
|
||||
avatarsArray.sort(compareByUpdatedAt);
|
||||
break;
|
||||
case 'created':
|
||||
avatarsArray.sort(utils.compareByCreatedAt);
|
||||
avatarsArray.sort(compareByCreatedAt);
|
||||
break;
|
||||
case 'name':
|
||||
avatarsArray.sort(utils.compareByName);
|
||||
avatarsArray.sort(compareByName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -679,7 +647,7 @@
|
||||
searchGroupParams.value = {
|
||||
n: 10,
|
||||
offset: 0,
|
||||
query: utils.replaceBioSymbols(props.searchText)
|
||||
query: replaceBioSymbols(searchText.value)
|
||||
};
|
||||
await moreSearchGroup();
|
||||
}
|
||||
@@ -698,22 +666,10 @@
|
||||
isSearchGroupLoading.value = false;
|
||||
})
|
||||
.then((args) => {
|
||||
// API.$on('GROUP:SEARCH', function (args) {
|
||||
for (const json of args.json) {
|
||||
API.$emit('GROUP', {
|
||||
json,
|
||||
params: {
|
||||
groupId: json.id
|
||||
}
|
||||
});
|
||||
}
|
||||
// });
|
||||
const map = new Map();
|
||||
for (const json of args.json) {
|
||||
const ref = API.cachedGroups.get(json.id);
|
||||
if (typeof ref !== 'undefined') {
|
||||
map.set(ref.id, ref);
|
||||
}
|
||||
const ref = applyGroup(json);
|
||||
map.set(ref.id, ref);
|
||||
}
|
||||
searchGroupResults.value = Array.from(map.values());
|
||||
return args;
|
||||
|
||||
2306
src/views/Settings/Settings.vue
Normal file
2306
src/views/Settings/Settings.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,6 @@
|
||||
<el-input
|
||||
v-for="(provider, index) in avatarRemoteDatabaseProviderList"
|
||||
:key="index"
|
||||
v-model="avatarRemoteDatabaseProviderList[index]"
|
||||
:value="provider"
|
||||
size="small"
|
||||
style="margin-top: 5px"
|
||||
@@ -25,34 +24,25 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { useAvatarProviderStore } from '../../../stores';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const avatarProviderStore = useAvatarProviderStore();
|
||||
|
||||
const { avatarRemoteDatabaseProviderList } = storeToRefs(avatarProviderStore);
|
||||
const { saveAvatarProviderList, removeAvatarProvider } = avatarProviderStore;
|
||||
|
||||
defineProps({
|
||||
avatarRemoteDatabaseProviderList: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
isAvatarProviderDialogVisible: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
'update:isAvatarProviderDialogVisible',
|
||||
'update:avatarRemoteDatabaseProviderList',
|
||||
'saveAvatarProviderList',
|
||||
'removeAvatarProvider'
|
||||
]);
|
||||
|
||||
function saveAvatarProviderList() {
|
||||
emit('saveAvatarProviderList');
|
||||
}
|
||||
|
||||
function removeAvatarProvider(provider) {
|
||||
emit('removeAvatarProvider', provider);
|
||||
}
|
||||
const emit = defineEmits(['update:isAvatarProviderDialogVisible']);
|
||||
|
||||
function closeDialog() {
|
||||
emit('update:isAvatarProviderDialogVisible', false);
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
:title="t('dialog.change_log.header')"
|
||||
width="800px"
|
||||
top="5vh"
|
||||
append-to-body
|
||||
@close="closeDialog">
|
||||
<div v-if="changeLogDialog.visible" class="changelog-dialog">
|
||||
<div v-loading="!changeLogDialog.changeLog" class="changelog-dialog">
|
||||
<h2 v-text="changeLogDialog.buildName"></h2>
|
||||
<span>
|
||||
<span v-show="changeLogDialog.buildName">
|
||||
{{ t('dialog.change_log.description') }}
|
||||
<a class="x-link" @click="openExternalLink('https://www.patreon.com/Natsumi_VRCX')">Patreon</a>,
|
||||
<a class="x-link" @click="openExternalLink('https://ko-fi.com/natsumi_sama')">Ko-fi</a>.
|
||||
@@ -33,22 +34,21 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { openExternalLink } from '../../../shared/utils';
|
||||
import { useVRCXUpdaterStore } from '../../../stores';
|
||||
|
||||
const VueMarkdown = () => import('vue-markdown');
|
||||
|
||||
const VRCXUpdaterStore = useVRCXUpdaterStore();
|
||||
|
||||
const { changeLogDialog } = storeToRefs(VRCXUpdaterStore);
|
||||
|
||||
const { t } = useI18n();
|
||||
const openExternalLink = inject('openExternalLink');
|
||||
|
||||
const props = defineProps({
|
||||
changeLogDialog: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
const emit = defineEmits(['update:changeLogDialog']);
|
||||
|
||||
function closeDialog() {
|
||||
emit('update:changeLogDialog', { ...props.changeLogDialog, visible: false });
|
||||
changeLogDialog.value.visible = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<template v-if="props.photonLoggingEnabled">
|
||||
<template v-if="photonLoggingEnabled">
|
||||
<br />
|
||||
<div class="toggle-item">
|
||||
<span class="toggle-name">Photon Event Logging</span>
|
||||
@@ -59,38 +59,25 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import configRepository from '../../../service/config';
|
||||
import { feedFiltersOptions } from '../../../composables/setting/constants/feedFiltersOptions';
|
||||
import { feedFiltersOptions, sharedFeedFiltersDefaults } from '../../../shared/constants';
|
||||
import { useNotificationsSettingsStore, usePhotonStore, useSharedFeedStore } from '../../../stores';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { photonLoggingEnabled } = storeToRefs(usePhotonStore());
|
||||
const { notyFeedFiltersOptions, wristFeedFiltersOptions, photonFeedFiltersOptions } = feedFiltersOptions();
|
||||
const { sharedFeedFilters } = storeToRefs(useNotificationsSettingsStore());
|
||||
const { updateSharedFeed } = useSharedFeedStore();
|
||||
|
||||
const props = defineProps({
|
||||
feedFiltersDialogMode: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: ''
|
||||
},
|
||||
photonLoggingEnabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
sharedFeedFilters: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
noty: {},
|
||||
wrist: {}
|
||||
})
|
||||
},
|
||||
sharedFeedFiltersDefaults: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
noty: {},
|
||||
wrist: {}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
@@ -100,8 +87,8 @@
|
||||
|
||||
const currentSharedFeedFilters = computed(() => {
|
||||
return props.feedFiltersDialogMode === 'noty'
|
||||
? props.sharedFeedFilters['noty']
|
||||
: props.sharedFeedFilters['wrist'];
|
||||
? sharedFeedFilters.value['noty']
|
||||
: sharedFeedFilters.value['wrist'];
|
||||
});
|
||||
|
||||
const dialogTitle = computed(() => {
|
||||
@@ -116,23 +103,23 @@
|
||||
return props.feedFiltersDialogMode === 'noty' ? resetNotyFeedFilters : resetWristFeedFilters;
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:feedFiltersDialogMode', 'updateSharedFeed']);
|
||||
const emit = defineEmits(['update:feedFiltersDialogMode']);
|
||||
|
||||
function saveSharedFeedFilters() {
|
||||
configRepository.setString('sharedFeedFilters', JSON.stringify(props.sharedFeedFilters));
|
||||
emit('updateSharedFeed', true);
|
||||
configRepository.setString('sharedFeedFilters', JSON.stringify(sharedFeedFilters.value));
|
||||
updateSharedFeed(true);
|
||||
}
|
||||
|
||||
function resetNotyFeedFilters() {
|
||||
props.sharedFeedFilters.noty = {
|
||||
...props.sharedFeedFiltersDefaults.noty
|
||||
sharedFeedFilters.value.noty = {
|
||||
...sharedFeedFiltersDefaults.noty
|
||||
};
|
||||
saveSharedFeedFilters();
|
||||
}
|
||||
|
||||
async function resetWristFeedFilters() {
|
||||
props.sharedFeedFilters.wrist = {
|
||||
...props.sharedFeedFiltersDefaults.wrist
|
||||
sharedFeedFilters.value.wrist = {
|
||||
...sharedFeedFiltersDefaults.wrist
|
||||
};
|
||||
saveSharedFeedFilters();
|
||||
}
|
||||
|
||||
@@ -56,33 +56,28 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, inject, getCurrentInstance } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { computed, getCurrentInstance, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import configRepository from '../../../service/config';
|
||||
|
||||
const openExternalLink = inject('openExternalLink');
|
||||
const isLinux = inject('isLinux');
|
||||
import { openExternalLink } from '../../../shared/utils';
|
||||
import { useLaunchStore } from '../../../stores';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const instance = getCurrentInstance();
|
||||
const $message = instance.proxy.$message;
|
||||
|
||||
defineProps({
|
||||
isLaunchOptionsDialogVisible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:isLaunchOptionsDialogVisible']);
|
||||
const launchStore = useLaunchStore();
|
||||
const { isLaunchOptionsDialogVisible } = storeToRefs(launchStore);
|
||||
|
||||
const launchOptionsDialog = ref({
|
||||
launchArguments: '',
|
||||
vrcLaunchPathOverride: ''
|
||||
});
|
||||
|
||||
const isLinux = computed(() => LINUX);
|
||||
|
||||
function init() {
|
||||
configRepository
|
||||
.getString('launchArguments')
|
||||
@@ -125,6 +120,6 @@
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
emit('update:isLaunchOptionsDialogVisible');
|
||||
isLaunchOptionsDialogVisible.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -88,26 +88,23 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, inject } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import utils from '../../../classes/utils';
|
||||
import * as workerTimers from 'worker-timers';
|
||||
import { miscRequest } from '../../../api';
|
||||
import { removeFromArray, userImage, userImageFull } from '../../../shared/utils';
|
||||
import { useFriendStore, useGalleryStore, useUserStore } from '../../../stores';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const userImage = inject('userImage');
|
||||
const userImageFull = inject('userImageFull');
|
||||
const showUserDialog = inject('showUserDialog');
|
||||
const showFullscreenImageDialog = inject('showFullscreenImageDialog');
|
||||
const { friends } = storeToRefs(useFriendStore());
|
||||
const { showUserDialog } = useUserStore();
|
||||
const { showFullscreenImageDialog } = useGalleryStore();
|
||||
|
||||
const props = defineProps({
|
||||
isNoteExportDialogVisible: {
|
||||
type: Boolean
|
||||
},
|
||||
friends: {
|
||||
type: Map,
|
||||
default: () => new Map()
|
||||
}
|
||||
});
|
||||
|
||||
@@ -146,7 +143,7 @@
|
||||
|
||||
function updateNoteExportDialog() {
|
||||
const data = [];
|
||||
props.friends.forEach((ctx) => {
|
||||
friends.value.forEach((ctx) => {
|
||||
const newMemo = ctx.memo.replace(/[\r\n]/g, ' ');
|
||||
if (ctx.memo && ctx.ref && ctx.ref.note !== newMemo.slice(0, 256)) {
|
||||
data.push({
|
||||
@@ -174,7 +171,7 @@
|
||||
targetUserId: ctx.id,
|
||||
note: ctx.memo.slice(0, 256)
|
||||
});
|
||||
utils.removeFromArray(noteExportTable.value.data, ctx);
|
||||
removeFromArray(noteExportTable.value.data, ctx);
|
||||
progress.value++;
|
||||
await new Promise((resolve) => {
|
||||
workerTimers.setTimeout(resolve, 5000);
|
||||
@@ -195,7 +192,7 @@
|
||||
}
|
||||
|
||||
function removeFromNoteExportTable(ref) {
|
||||
utils.removeFromArray(noteExportTable.value.data, ref);
|
||||
removeFromArray(noteExportTable.value.data, ref);
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
|
||||
@@ -46,27 +46,25 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { useNotificationsSettingsStore } from '../../../stores';
|
||||
|
||||
const { t } = useI18n();
|
||||
const notificationsSettingsStore = useNotificationsSettingsStore();
|
||||
const { notificationPosition } = storeToRefs(notificationsSettingsStore);
|
||||
const { changeNotificationPosition } = notificationsSettingsStore;
|
||||
|
||||
defineProps({
|
||||
isNotificationPositionDialogVisible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
notificationPosition: {
|
||||
type: String,
|
||||
default: 'topRight'
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:isNotificationPositionDialogVisible', 'changeNotificationPosition']);
|
||||
const emit = defineEmits(['update:isNotificationPositionDialogVisible']);
|
||||
|
||||
function closeDialog() {
|
||||
emit('update:isNotificationPositionDialogVisible', false);
|
||||
}
|
||||
|
||||
function changeNotificationPosition(value) {
|
||||
emit('changeNotificationPosition', value);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { openSourceSoftwareLicenses } from '../../../composables/setting/constants/openSourceSoftwareLicenses';
|
||||
import { openSourceSoftwareLicenses } from '../../../shared/constants';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
enablePrimaryPasswordDialog.password.length === 0 ||
|
||||
enablePrimaryPasswordDialog.password !== enablePrimaryPasswordDialog.rePassword
|
||||
"
|
||||
@click="setPrimaryPassword">
|
||||
@click="handleSetPrimaryPassword()">
|
||||
{{ t('dialog.primary_password.ok') }}
|
||||
</el-button>
|
||||
</template>
|
||||
@@ -40,20 +40,18 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { useAuthStore } from '../../../stores';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
enablePrimaryPasswordDialog: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
const authStore = useAuthStore();
|
||||
const { enablePrimaryPasswordDialog } = storeToRefs(authStore);
|
||||
const { setPrimaryPassword } = authStore;
|
||||
|
||||
const emit = defineEmits(['setPrimaryPassword']);
|
||||
|
||||
function setPrimaryPassword() {
|
||||
emit('setPrimaryPassword', props.enablePrimaryPasswordDialog.password);
|
||||
props.enablePrimaryPasswordDialog.visible = false;
|
||||
function handleSetPrimaryPassword() {
|
||||
setPrimaryPassword(enablePrimaryPasswordDialog.value.password);
|
||||
enablePrimaryPasswordDialog.value.visible = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -9,13 +9,13 @@
|
||||
<div style="margin-top: 10px">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; font-size: 12px">
|
||||
<span class="name" style="margin-right: 24px">{{ t('dialog.registry_backup.auto_backup') }}</span>
|
||||
<el-switch v-model="vrcRegistryAutoBackup" @change="saveVrcRegistryAutoBackup"></el-switch>
|
||||
<el-switch v-model="vrcRegistryAutoBackup" @change="setVrcRegistryAutoBackup"></el-switch>
|
||||
</div>
|
||||
<data-tables v-bind="registryBackupTable" style="margin-top: 10px">
|
||||
<el-table-column :label="t('dialog.registry_backup.name')" prop="name"></el-table-column>
|
||||
<el-table-column :label="t('dialog.registry_backup.date')" prop="date">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.date | formatDate('long') }}</span>
|
||||
<span>{{ formatDateFilter(scope.row.date, 'long') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('dialog.registry_backup.action')" width="90" align="right">
|
||||
@@ -71,32 +71,25 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { getCurrentInstance, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import utils from '../../../classes/utils';
|
||||
import { downloadAndSaveJson } from '../../../composables/shared/utils';
|
||||
import configRepository from '../../../service/config';
|
||||
import { downloadAndSaveJson, removeFromArray, formatDateFilter } from '../../../shared/utils';
|
||||
|
||||
import { useAppearanceSettingsStore, useVrcxStore, useAdvancedSettingsStore } from '../../../stores';
|
||||
|
||||
const { hideTooltips } = storeToRefs(useAppearanceSettingsStore());
|
||||
const { backupVrcRegistry } = useVrcxStore();
|
||||
const { isRegistryBackupDialogVisible } = storeToRefs(useVrcxStore());
|
||||
const { vrcRegistryAutoBackup } = storeToRefs(useAdvancedSettingsStore());
|
||||
const { setVrcRegistryAutoBackup } = useAdvancedSettingsStore();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const instance = getCurrentInstance();
|
||||
const { $confirm, $message, $prompt } = instance.proxy;
|
||||
|
||||
const props = defineProps({
|
||||
isRegistryBackupDialogVisible: {
|
||||
type: Boolean
|
||||
},
|
||||
hideTooltips: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
backupVrcRegistry: {
|
||||
type: Function
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:isRegistryBackupDialogVisible']);
|
||||
|
||||
const registryBackupTable = ref({
|
||||
data: [],
|
||||
tableProps: {
|
||||
@@ -110,10 +103,8 @@
|
||||
layout: 'table'
|
||||
});
|
||||
|
||||
const vrcRegistryAutoBackup = ref(false);
|
||||
|
||||
watch(
|
||||
() => props.isRegistryBackupDialogVisible,
|
||||
() => isRegistryBackupDialogVisible.value,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
updateRegistryBackupDialog();
|
||||
@@ -121,23 +112,11 @@
|
||||
}
|
||||
);
|
||||
|
||||
setVrcRegistryAutoBackup();
|
||||
|
||||
function setVrcRegistryAutoBackup() {
|
||||
configRepository.getBool('VRCX_vrcRegistryAutoBackup', true).then((value) => {
|
||||
vrcRegistryAutoBackup.value = value;
|
||||
});
|
||||
}
|
||||
|
||||
async function updateRegistryBackupDialog() {
|
||||
let backupsJson = await configRepository.getString('VRCX_VRChatRegistryBackups');
|
||||
const backupsJson = await configRepository.getString('VRCX_VRChatRegistryBackups');
|
||||
registryBackupTable.value.data = JSON.parse(backupsJson || '[]');
|
||||
}
|
||||
|
||||
async function saveVrcRegistryAutoBackup() {
|
||||
await configRepository.setBool('VRCX_vrcRegistryAutoBackup', vrcRegistryAutoBackup.value);
|
||||
}
|
||||
|
||||
function restoreVrcRegistryBackup(row) {
|
||||
$confirm('Continue? Restore Backup', 'Confirm', {
|
||||
confirmButtonText: 'Confirm',
|
||||
@@ -172,7 +151,7 @@
|
||||
|
||||
async function deleteVrcRegistryBackup(row) {
|
||||
const backups = registryBackupTable.value.data;
|
||||
utils.removeFromArray(backups, row);
|
||||
removeFromArray(backups, row);
|
||||
await configRepository.setString('VRCX_VRChatRegistryBackups', JSON.stringify(backups));
|
||||
await updateRegistryBackupDialog();
|
||||
}
|
||||
@@ -197,7 +176,7 @@
|
||||
}
|
||||
|
||||
async function handleBackupVrcRegistry(name) {
|
||||
await props.backupVrcRegistry(name);
|
||||
await backupVrcRegistry(name);
|
||||
await updateRegistryBackupDialog();
|
||||
}
|
||||
|
||||
@@ -295,6 +274,6 @@
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
emit('update:isRegistryBackupDialogVisible', false);
|
||||
isRegistryBackupDialogVisible.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
>{{ t('dialog.screenshot_metadata.open_folder') }}</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="API.currentUser.$isVRCPlus && screenshotMetadataDialog.metadata.filePath"
|
||||
v-if="currentUser.$isVRCPlus && screenshotMetadataDialog.metadata.filePath"
|
||||
size="small"
|
||||
icon="el-icon-upload2"
|
||||
@click="uploadScreenshotToGallery"
|
||||
@@ -81,7 +81,7 @@
|
||||
<br />
|
||||
</template>
|
||||
<span v-if="screenshotMetadataDialog.metadata.dateTime" style="margin-right: 5px">{{
|
||||
screenshotMetadataDialog.metadata.dateTime | formatDate('long')
|
||||
formatDateFilter(screenshotMetadataDialog.metadata.dateTime, 'long')
|
||||
}}</span>
|
||||
<span
|
||||
v-if="screenshotMetadataDialog.metadata.fileResolution"
|
||||
@@ -91,11 +91,11 @@
|
||||
screenshotMetadataDialog.metadata.fileSize
|
||||
}}</el-tag>
|
||||
<br />
|
||||
<location
|
||||
<Location
|
||||
v-if="screenshotMetadataDialog.metadata.world"
|
||||
:location="screenshotMetadataDialog.metadata.world.instanceId"
|
||||
:hint="screenshotMetadataDialog.metadata.world.name" />
|
||||
<display-name
|
||||
<DisplayName
|
||||
v-if="screenshotMetadataDialog.metadata.author"
|
||||
:userid="screenshotMetadataDialog.metadata.author.id"
|
||||
:hint="screenshotMetadataDialog.metadata.author.displayName"
|
||||
@@ -162,36 +162,34 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, inject, getCurrentInstance, watch } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { getCurrentInstance, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { vrcPlusImageRequest } from '../../../api';
|
||||
import Location from '../../../components/Location.vue';
|
||||
import { useGalleryStore, useUserStore, useVrcxStore } from '../../../stores';
|
||||
import { formatDateFilter } from '../../../shared/utils';
|
||||
|
||||
const API = inject('API');
|
||||
const showFullscreenImageDialog = inject('showFullscreenImageDialog');
|
||||
const { showFullscreenImageDialog, handleGalleryImageAdd } = useGalleryStore();
|
||||
const { currentlyDroppingFile } = storeToRefs(useVrcxStore());
|
||||
const { currentUser } = storeToRefs(useUserStore());
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const instance = getCurrentInstance();
|
||||
const $message = instance.proxy.$message;
|
||||
|
||||
const userStore = useUserStore();
|
||||
const { lookupUser } = userStore;
|
||||
|
||||
const { fullscreenImageDialog } = storeToRefs(useGalleryStore());
|
||||
|
||||
const props = defineProps({
|
||||
screenshotMetadataDialog: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
currentlyDroppingFile: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
fullscreenImageDialog: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['lookupUser']);
|
||||
|
||||
watch(
|
||||
() => props.screenshotMetadataDialog.visible,
|
||||
(newVal) => {
|
||||
@@ -217,13 +215,13 @@
|
||||
};
|
||||
|
||||
function handleDrop(event) {
|
||||
if (props.currentlyDroppingFile === null) {
|
||||
if (currentlyDroppingFile.value === null) {
|
||||
return;
|
||||
}
|
||||
console.log('Dropped file into viewer: ', props.currentlyDroppingFile);
|
||||
console.log('Dropped file into viewer: ', currentlyDroppingFile.value);
|
||||
|
||||
screenshotMetadataResetSearch();
|
||||
getAndDisplayScreenshot(props.currentlyDroppingFile);
|
||||
getAndDisplayScreenshot(currentlyDroppingFile.value);
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
@@ -296,9 +294,7 @@
|
||||
vrcPlusImageRequest
|
||||
.uploadGalleryImage(base64Body)
|
||||
.then((args) => {
|
||||
// about uploadGalleryImage -> emit 'GALLERYIMAGE:ADD'
|
||||
// no need to add to the gallery logic here
|
||||
// because it refreshes when you open the gallery
|
||||
handleGalleryImageAdd(args);
|
||||
$message({
|
||||
message: t('message.gallery.uploaded'),
|
||||
type: 'success'
|
||||
@@ -393,13 +389,10 @@
|
||||
screenshotMetadataCarouselRef.value.setActiveItem(1);
|
||||
}
|
||||
|
||||
if (props.fullscreenImageDialog.visible) {
|
||||
if (fullscreenImageDialog.value.visible) {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
function lookupUser(user) {
|
||||
emit('lookupUser', user);
|
||||
}
|
||||
|
||||
function screenshotMetadataResetSearch() {
|
||||
const D = props.screenshotMetadataDialog;
|
||||
@@ -505,7 +498,7 @@
|
||||
D.metadata.dateTime = Date.parse(metadata.creationDate);
|
||||
}
|
||||
|
||||
if (props.fullscreenImageDialog?.visible) {
|
||||
if (fullscreenImageDialog.value.visible) {
|
||||
showFullscreenImageDialog(D.metadata.filePath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
:type="item.type ? item.type : 'text'"
|
||||
:min="item.min"
|
||||
:max="item.max"
|
||||
@input="refreshDialogValues"
|
||||
style="flex: 1; margin-top: 5px"
|
||||
><el-button
|
||||
v-if="item.folderBrowser"
|
||||
@@ -186,13 +187,18 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, getCurrentInstance, inject, ref, watch } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { computed, getCurrentInstance, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import {
|
||||
VRChatCameraResolutions,
|
||||
VRChatScreenshotResolutions
|
||||
} from '../../../composables/setting/constants/vrchatResolutions';
|
||||
import { getVRChatResolution } from '../../../composables/setting/utils';
|
||||
import { VRChatCameraResolutions, VRChatScreenshotResolutions } from '../../../shared/constants';
|
||||
import { getVRChatResolution, openExternalLink } from '../../../shared/utils';
|
||||
import { useAdvancedSettingsStore, useAppearanceSettingsStore, useGameStore } from '../../../stores';
|
||||
|
||||
const { hideTooltips } = storeToRefs(useAppearanceSettingsStore());
|
||||
const { VRChatUsedCacheSize, VRChatTotalCacheSize, VRChatCacheSizeLoading } = storeToRefs(useGameStore());
|
||||
const { sweepVRChatCache, getVRChatCacheSize } = useGameStore();
|
||||
const { folderSelectorDialog } = useAdvancedSettingsStore();
|
||||
const { isVRChatConfigDialogVisible } = storeToRefs(useAdvancedSettingsStore());
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -200,37 +206,6 @@
|
||||
const $confirm = instance.proxy.$confirm;
|
||||
const $message = instance.proxy.$message;
|
||||
|
||||
const openExternalLink = inject('openExternalLink');
|
||||
|
||||
const props = defineProps({
|
||||
isVRChatConfigDialogVisible: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
VRChatUsedCacheSize: {
|
||||
type: [String, Number],
|
||||
required: true
|
||||
},
|
||||
VRChatTotalCacheSize: {
|
||||
type: [String, Number],
|
||||
required: true
|
||||
},
|
||||
VRChatCacheSizeLoading: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
folderSelectorDialog: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
hideTooltips: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:isVRChatConfigDialogVisible', 'getVRChatCacheSize', 'sweepVRChatCache']);
|
||||
|
||||
const VRChatConfigFile = ref({});
|
||||
// it's a object
|
||||
const VRChatConfigList = ref({
|
||||
@@ -281,7 +256,7 @@
|
||||
const loading = ref(false);
|
||||
|
||||
watch(
|
||||
() => props.isVRChatConfigDialogVisible,
|
||||
() => isVRChatConfigDialogVisible.value,
|
||||
async (newValue) => {
|
||||
if (newValue) {
|
||||
loading.value = true;
|
||||
@@ -292,13 +267,9 @@
|
||||
);
|
||||
|
||||
const totalCacheSize = computed(() => {
|
||||
return VRChatConfigFile.value.cache_size || props.VRChatTotalCacheSize;
|
||||
return VRChatConfigFile.value.cache_size || VRChatTotalCacheSize.value;
|
||||
});
|
||||
|
||||
function getVRChatCacheSize() {
|
||||
emit('getVRChatCacheSize');
|
||||
}
|
||||
|
||||
function showDeleteAllVRChatCacheConfirm() {
|
||||
$confirm(`Continue? Delete all VRChat cache`, 'Confirm', {
|
||||
confirmButtonText: 'Confirm',
|
||||
@@ -317,15 +288,12 @@
|
||||
getVRChatCacheSize();
|
||||
}
|
||||
|
||||
function sweepVRChatCache() {
|
||||
emit('sweepVRChatCache');
|
||||
}
|
||||
|
||||
async function openConfigFolderBrowser(value) {
|
||||
const oldPath = VRChatConfigFile.value[value];
|
||||
const newPath = await props.folderSelectorDialog(oldPath);
|
||||
const newPath = await folderSelectorDialog(oldPath);
|
||||
if (newPath) {
|
||||
VRChatConfigFile.value[value] = newPath;
|
||||
refreshDialogValues();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -420,6 +388,6 @@
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
emit('update:isVRChatConfigDialogVisible', false);
|
||||
isVRChatConfigDialogVisible.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -8,13 +8,12 @@
|
||||
<div style="font-size: 12px">{{ t('dialog.youtube_api.description') }} <br /></div>
|
||||
|
||||
<el-input
|
||||
:value="youTubeApiKey"
|
||||
v-model="youTubeApiKey"
|
||||
type="textarea"
|
||||
:placeholder="t('dialog.youtube_api.placeholder')"
|
||||
maxlength="39"
|
||||
show-word-limit
|
||||
style="display: block; margin-top: 10px"
|
||||
@input="updateYouTubeApiKey">
|
||||
style="display: block; margin-top: 10px">
|
||||
</el-input>
|
||||
|
||||
<template #footer>
|
||||
@@ -33,52 +32,51 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject, getCurrentInstance } from 'vue';
|
||||
import configRepository from '../../../service/config';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { getCurrentInstance } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { openExternalLink } from '../../../shared/utils';
|
||||
import { useAdvancedSettingsStore } from '../../../stores';
|
||||
|
||||
const advancedSettingsStore = useAdvancedSettingsStore();
|
||||
|
||||
const { youTubeApiKey } = storeToRefs(advancedSettingsStore);
|
||||
|
||||
const { lookupYouTubeVideo, setYouTubeApiKey } = advancedSettingsStore;
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const instance = getCurrentInstance();
|
||||
const $message = instance.proxy.$message;
|
||||
|
||||
const openExternalLink = inject('openExternalLink');
|
||||
|
||||
const props = defineProps({
|
||||
isYouTubeApiDialogVisible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
lookupYouTubeVideo: {
|
||||
type: Function,
|
||||
default: () => {}
|
||||
},
|
||||
youTubeApiKey: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:isYouTubeApiDialogVisible', 'update:youTubeApiKey']);
|
||||
const emit = defineEmits(['update:isYouTubeApiDialogVisible']);
|
||||
|
||||
async function testYouTubeApiKey() {
|
||||
if (!props.youTubeApiKey) {
|
||||
const previousKey = youTubeApiKey.value;
|
||||
if (!youTubeApiKey.value) {
|
||||
$message({
|
||||
message: 'YouTube API key removed',
|
||||
type: 'success'
|
||||
});
|
||||
await configRepository.setString('VRCX_youtubeAPIKey', '');
|
||||
closeDialog();
|
||||
return;
|
||||
}
|
||||
const data = await props.lookupYouTubeVideo('dQw4w9WgXcQ');
|
||||
const data = await lookupYouTubeVideo('dQw4w9WgXcQ');
|
||||
if (!data) {
|
||||
updateYouTubeApiKey('');
|
||||
setYouTubeApiKey(previousKey);
|
||||
$message({
|
||||
message: 'Invalid YouTube API key',
|
||||
type: 'error'
|
||||
});
|
||||
} else {
|
||||
await configRepository.setString('VRCX_youtubeAPIKey', props.youTubeApiKey);
|
||||
setYouTubeApiKey(youTubeApiKey.value);
|
||||
$message({
|
||||
message: 'YouTube API key valid!',
|
||||
type: 'success'
|
||||
@@ -87,10 +85,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
function updateYouTubeApiKey(value) {
|
||||
emit('update:youTubeApiKey', value);
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
emit('update:isYouTubeApiDialogVisible', false);
|
||||
}
|
||||
|
||||
@@ -1,382 +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 '../../../components/FriendItem.vue';
|
||||
import Location from '../../../components/Location.vue';
|
||||
import { isRealInstance as _isRealInstance, parseLocation } from '../../../composables/instance/utils';
|
||||
import configRepository from '../../../service/config';
|
||||
|
||||
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) => {
|
||||
if (!friend.ref?.$location) {
|
||||
return;
|
||||
}
|
||||
|
||||
let locationTag = friend.ref.$location.tag;
|
||||
if (!friend.ref.$location.isRealInstance && this.lastLocation.friendList.has(friend.id)) {
|
||||
locationTag = this.lastLocation.location;
|
||||
}
|
||||
const isRealInstance = this.isRealInstance(locationTag);
|
||||
if (!isRealInstance) {
|
||||
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);
|
||||
},
|
||||
sameInstanceFriendId() {
|
||||
const sameInstanceFriendId = new Set();
|
||||
for (const item of this.friendsInSameInstance) {
|
||||
for (const friend of item) {
|
||||
if (this.isRealInstance(friend.ref?.$location.tag)) {
|
||||
sameInstanceFriendId.add(friend.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
return sameInstanceFriendId;
|
||||
},
|
||||
onlineFriendsByGroupStatus() {
|
||||
if (!this.isSidebarGroupByInstance || !this.isHideFriendsInSameInstance) {
|
||||
return this.onlineFriends;
|
||||
}
|
||||
|
||||
return this.onlineFriends.filter((item) => !this.sameInstanceFriendId.has(item.id));
|
||||
},
|
||||
vipFriendsByGroupStatus() {
|
||||
if (!this.isSidebarGroupByInstance || !this.isHideFriendsInSameInstance) {
|
||||
return this.vipFriends;
|
||||
}
|
||||
|
||||
return this.vipFriends.filter((item) => !this.sameInstanceFriendId.has(item.id));
|
||||
},
|
||||
// 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
|
||||
const filteredFriends = this.vipFriends.filter((friend) =>
|
||||
groupFriends.some((item) => {
|
||||
if (this.isSidebarGroupByInstance && this.isHideFriendsInSameInstance) {
|
||||
return item.id === friend.id && !this.sameInstanceFriendId.has(item.id);
|
||||
}
|
||||
return 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 _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;
|
||||
}
|
||||
}
|
||||
for (const friend of friendsArr) {
|
||||
if (this.isRealInstance(friend.ref?.travelingToLocation)) {
|
||||
return friend.ref.travelingToLocation;
|
||||
}
|
||||
}
|
||||
for (const friend of friendsArr) {
|
||||
if (this.lastLocation.friendList.has(friend.id)) {
|
||||
return this.lastLocation.location;
|
||||
}
|
||||
}
|
||||
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 '../../../components/Location.vue';
|
||||
import { convertFileUrlToImageUrl } from '../../../composables/shared/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 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>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div v-show="isSideBarTabShow" id="aside" class="x-aside-container">
|
||||
<div v-show="isSideBarTabShow" id="aside" class="x-aside-container" :style="{ width: `${asideWidth}px` }">
|
||||
<div style="display: flex; align-items: baseline">
|
||||
<el-select
|
||||
value=""
|
||||
@@ -10,7 +10,7 @@
|
||||
:remote-method="quickSearchRemoteMethod"
|
||||
popper-class="x-quick-search"
|
||||
style="flex: 1; padding: 10px"
|
||||
@change="$emit('quick-search-change', $event)">
|
||||
@change="quickSearchChange">
|
||||
<el-option v-for="item in quickSearchItems" :key="item.value" :value="item.value" :label="item.label">
|
||||
<div class="x-friend-item">
|
||||
<template v-if="item.ref">
|
||||
@@ -25,12 +25,12 @@
|
||||
<span v-else-if="item.ref.state === 'active'" class="extra">{{
|
||||
$t('side_panel.search_result_offline')
|
||||
}}</span>
|
||||
<location
|
||||
<Location
|
||||
v-else
|
||||
class="extra"
|
||||
:location="item.ref.location"
|
||||
:traveling="item.ref.travelingToLocation"
|
||||
:link="false"></location>
|
||||
:link="false" />
|
||||
</div>
|
||||
<img v-lazy="userImage(item.ref)" class="avatar" />
|
||||
</template>
|
||||
@@ -47,17 +47,17 @@
|
||||
size="mini"
|
||||
icon="el-icon-discover"
|
||||
circle
|
||||
@click="$emit('direct-access-paste')"></el-button>
|
||||
@click="directAccessPaste"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip placement="bottom" :content="$t('side_panel.refresh_tooltip')" :disabled="hideTooltips">
|
||||
<el-button
|
||||
type="default"
|
||||
:loading="API.isRefreshFriendsLoading"
|
||||
:loading="isRefreshFriendsLoading"
|
||||
size="mini"
|
||||
icon="el-icon-refresh"
|
||||
circle
|
||||
style="margin-right: 10px"
|
||||
@click="$emit('refresh-friends-list')"></el-button>
|
||||
@click="refreshFriendsList" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-tabs class="zero-margin-tabs" stretch style="height: calc(100% - 60px); margin-top: 5px">
|
||||
@@ -69,21 +69,7 @@
|
||||
</span>
|
||||
</template>
|
||||
<el-backtop target=".zero-margin-tabs .el-tabs__content" :bottom="20" :right="20"></el-backtop>
|
||||
<FriendsSidebar
|
||||
:hide-nicknames="hideNicknames"
|
||||
:is-game-running="isGameRunning"
|
||||
:is-sidebar-divide-by-friend-group="isSidebarDivideByFriendGroup"
|
||||
:is-sidebar-group-by-instance="isSidebarGroupByInstance"
|
||||
:game-log-disabled="gameLogDisabled"
|
||||
:last-location="lastLocation"
|
||||
:last-location-destination="lastLocationDestination"
|
||||
:active-friends="activeFriends"
|
||||
:offline-friends="offlineFriends"
|
||||
:online-friends="onlineFriends"
|
||||
:vip-friends="vipFriends"
|
||||
:is-hide-friends-in-same-instance="isHideFriendsInSameInstance"
|
||||
:grouped-by-group-key-favorite-friends="groupedByGroupKeyFavoriteFriends"
|
||||
@confirm-delete-friend="$emit('confirm-delete-friend', $event)" />
|
||||
<FriendsSidebar @confirm-delete-friend="confirmDeleteFriend" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane lazy>
|
||||
<template slot="label">
|
||||
@@ -92,61 +78,35 @@
|
||||
({{ groupInstances.length }})
|
||||
</span>
|
||||
</template>
|
||||
<GroupsSidebar
|
||||
:group-instances="groupInstances"
|
||||
:group-order="inGameGroupOrder"
|
||||
:is-age-gated-instances-visible="isAgeGatedInstancesVisible"
|
||||
@show-group-dialog="$emit('show-group-dialog', $event)" />
|
||||
<GroupsSidebar :group-instances="groupInstances" :group-order="inGameGroupOrder" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { computed } from 'vue';
|
||||
import { userImage } from '../../shared/utils';
|
||||
import {
|
||||
useAppearanceSettingsStore,
|
||||
useFriendStore,
|
||||
useGroupStore,
|
||||
useSearchStore,
|
||||
useUiStore
|
||||
} from '../../stores';
|
||||
import FriendsSidebar from './components/FriendsSidebar.vue';
|
||||
import GroupsSidebar from './components/GroupsSidebar.vue';
|
||||
import Location from '../../components/Location.vue';
|
||||
|
||||
export default {
|
||||
name: 'SideBar',
|
||||
components: {
|
||||
FriendsSidebar,
|
||||
GroupsSidebar,
|
||||
Location
|
||||
},
|
||||
inject: ['API', 'userImage'],
|
||||
props: {
|
||||
// settings
|
||||
// remove these props when have a state manager.
|
||||
hideTooltips: Boolean,
|
||||
isGameRunning: Boolean,
|
||||
isSidebarDivideByFriendGroup: Boolean,
|
||||
isSidebarGroupByInstance: Boolean,
|
||||
gameLogDisabled: Boolean,
|
||||
hideNicknames: Boolean,
|
||||
isHideFriendsInSameInstance: Boolean,
|
||||
isAgeGatedInstancesVisible: Boolean,
|
||||
const { friends, isRefreshFriendsLoading, onlineFriendCount } = storeToRefs(useFriendStore());
|
||||
const { refreshFriendsList, confirmDeleteFriend } = useFriendStore();
|
||||
const { hideTooltips, asideWidth } = storeToRefs(useAppearanceSettingsStore());
|
||||
const { menuActiveIndex } = storeToRefs(useUiStore());
|
||||
const { quickSearchRemoteMethod, quickSearchChange, directAccessPaste } = useSearchStore();
|
||||
const { quickSearchItems } = storeToRefs(useSearchStore());
|
||||
const { inGameGroupOrder, groupInstances } = storeToRefs(useGroupStore());
|
||||
|
||||
isSideBarTabShow: Boolean,
|
||||
|
||||
quickSearchRemoteMethod: Function,
|
||||
quickSearchItems: Array,
|
||||
onlineFriendCount: Number,
|
||||
friends: Map,
|
||||
|
||||
lastLocation: Object,
|
||||
lastLocationDestination: String,
|
||||
|
||||
// friends
|
||||
vipFriends: Array,
|
||||
onlineFriends: Array,
|
||||
|
||||
// no
|
||||
activeFriends: Array,
|
||||
offlineFriends: Array,
|
||||
groupInstances: Array,
|
||||
inGameGroupOrder: Array,
|
||||
groupedByGroupKeyFavoriteFriends: Object
|
||||
}
|
||||
};
|
||||
const isSideBarTabShow = computed(() => {
|
||||
return !(menuActiveIndex.value === 'friendList' || menuActiveIndex.value === 'charts');
|
||||
});
|
||||
</script>
|
||||
362
src/views/Sidebar/components/FriendsSidebar.vue
Normal file
362
src/views/Sidebar/components/FriendsSidebar.vue
Normal file
@@ -0,0 +1,362 @@
|
||||
<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(currentUser.id)">
|
||||
<div class="avatar" :class="userStatusClass(currentUser)">
|
||||
<img v-lazy="userImage(currentUser)" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span class="name" :style="{ color: currentUser.$userColour }">{{ currentUser.displayName }}</span>
|
||||
<Location
|
||||
v-if="isGameRunning && !gameLogDisabled"
|
||||
class="extra"
|
||||
:location="lastLocation.location"
|
||||
:traveling="lastLocationDestination"
|
||||
:link="false" />
|
||||
<Location
|
||||
v-else-if="
|
||||
isRealInstance(currentUser.$locationTag) || isRealInstance(currentUser.$travelingToLocation)
|
||||
"
|
||||
class="extra"
|
||||
:location="currentUser.$locationTag"
|
||||
:traveling="currentUser.$travelingToLocation"
|
||||
:link="false" />
|
||||
|
||||
<span v-else class="extra">{{ 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"
|
||||
@click="showUserDialog(friend.id)"
|
||||
@confirm-delete-friend="confirmDeleteFriend"></friend-item>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<friend-item
|
||||
v-for="friend in vipFriendsByGroupStatus"
|
||||
:key="friend.id"
|
||||
:friend="friend"
|
||||
@click="showUserDialog(friend.id)"
|
||||
@confirm-delete-friend="confirmDeleteFriend">
|
||||
</friend-item>
|
||||
</template>
|
||||
</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" />
|
||||
<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="confirmDeleteFriend">
|
||||
</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"
|
||||
@click="showUserDialog(friend.id)"
|
||||
@confirm-delete-friend="confirmDeleteFriend" />
|
||||
</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"
|
||||
@click="showUserDialog(friend.id)"
|
||||
@confirm-delete-friend="confirmDeleteFriend"></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"
|
||||
@click="showUserDialog(friend.id)"
|
||||
@confirm-delete-friend="confirmDeleteFriend"></friend-item>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import FriendItem from '../../../components/FriendItem.vue';
|
||||
import configRepository from '../../../service/config';
|
||||
import { isRealInstance, userImage, userStatusClass } from '../../../shared/utils';
|
||||
import {
|
||||
useAdvancedSettingsStore,
|
||||
useAppearanceSettingsStore,
|
||||
useFavoriteStore,
|
||||
useFriendStore,
|
||||
useGameStore,
|
||||
useLocationStore,
|
||||
useUserStore
|
||||
} from '../../../stores';
|
||||
const emit = defineEmits(['confirm-delete-friend']);
|
||||
|
||||
const { vipFriends, onlineFriends, activeFriends, offlineFriends } = storeToRefs(useFriendStore());
|
||||
const { isSidebarGroupByInstance, isHideFriendsInSameInstance, isSidebarDivideByFriendGroup } =
|
||||
storeToRefs(useAppearanceSettingsStore());
|
||||
const { gameLogDisabled } = storeToRefs(useAdvancedSettingsStore());
|
||||
const { showUserDialog } = useUserStore();
|
||||
const { favoriteFriendGroups, groupedByGroupKeyFavoriteFriends } = storeToRefs(useFavoriteStore());
|
||||
const { lastLocation, lastLocationDestination } = storeToRefs(useLocationStore());
|
||||
const { isGameRunning } = storeToRefs(useGameStore());
|
||||
const { currentUser } = storeToRefs(useUserStore());
|
||||
|
||||
const isFriendsGroupMe = ref(true);
|
||||
const isVIPFriends = ref(true);
|
||||
const isOnlineFriends = ref(true);
|
||||
const isActiveFriends = ref(false);
|
||||
const isOfflineFriends = ref(false);
|
||||
const isSidebarGroupByInstanceCollapsed = ref(false);
|
||||
|
||||
loadFriendsGroupStates();
|
||||
|
||||
const friendsInSameInstance = computed(() => {
|
||||
const friendsList = {};
|
||||
|
||||
const allFriends = [...vipFriends.value, ...onlineFriends.value];
|
||||
allFriends.forEach((friend) => {
|
||||
if (!friend.ref?.$location) {
|
||||
return;
|
||||
}
|
||||
|
||||
let locationTag = friend.ref.$location.tag;
|
||||
if (!friend.ref.$location.isRealInstance && lastLocation.value.friendList.has(friend.id)) {
|
||||
locationTag = lastLocation.value.location;
|
||||
}
|
||||
const isReal = isRealInstance(locationTag);
|
||||
if (!isReal) {
|
||||
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);
|
||||
});
|
||||
|
||||
const sameInstanceFriendId = computed(() => {
|
||||
const sameInstanceFriendId = new Set();
|
||||
for (const item of friendsInSameInstance.value) {
|
||||
for (const friend of item) {
|
||||
if (isRealInstance(friend.ref?.$location.tag) || lastLocation.value.friendList.has(friend.id)) {
|
||||
sameInstanceFriendId.add(friend.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
return sameInstanceFriendId;
|
||||
});
|
||||
|
||||
const onlineFriendsByGroupStatus = computed(() => {
|
||||
if (!isSidebarGroupByInstance.value || !isHideFriendsInSameInstance.value) {
|
||||
return onlineFriends.value;
|
||||
}
|
||||
|
||||
return onlineFriends.value.filter((item) => !sameInstanceFriendId.value.has(item.id));
|
||||
});
|
||||
|
||||
const vipFriendsByGroupStatus = computed(() => {
|
||||
if (!isSidebarGroupByInstance.value || !isHideFriendsInSameInstance.value) {
|
||||
return vipFriends.value;
|
||||
}
|
||||
|
||||
return vipFriends.value.filter((item) => !sameInstanceFriendId.value.has(item.id));
|
||||
});
|
||||
|
||||
// VIP friends divide by group
|
||||
const vipFriendsDivideByGroup = computed(() => {
|
||||
const vipFriendsByGroup = { ...groupedByGroupKeyFavoriteFriends.value };
|
||||
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
|
||||
const filteredFriends = vipFriends.value.filter((friend) =>
|
||||
groupFriends.some((item) => {
|
||||
if (isSidebarGroupByInstance.value && isHideFriendsInSameInstance.value) {
|
||||
return item.id === friend.id && !sameInstanceFriendId.value.has(item.id);
|
||||
}
|
||||
return item.id === friend.id;
|
||||
})
|
||||
);
|
||||
|
||||
if (filteredFriends.length > 0) {
|
||||
const groupName = favoriteFriendGroups.value.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));
|
||||
});
|
||||
|
||||
const vipFriendsDisplayNumber = computed(() => {
|
||||
return isSidebarDivideByFriendGroup.value
|
||||
? vipFriendsDivideByGroup.value.length
|
||||
: vipFriendsByGroupStatus.value.length;
|
||||
});
|
||||
|
||||
function saveFriendsGroupStates() {
|
||||
configRepository.setBool('VRCX_isFriendsGroupMe', isFriendsGroupMe.value);
|
||||
configRepository.setBool('VRCX_isFriendsGroupFavorites', isVIPFriends.value);
|
||||
configRepository.setBool('VRCX_isFriendsGroupOnline', isOnlineFriends.value);
|
||||
configRepository.setBool('VRCX_isFriendsGroupActive', isActiveFriends.value);
|
||||
configRepository.setBool('VRCX_isFriendsGroupOffline', isOfflineFriends.value);
|
||||
}
|
||||
|
||||
async function loadFriendsGroupStates() {
|
||||
isFriendsGroupMe.value = await configRepository.getBool('VRCX_isFriendsGroupMe', true);
|
||||
isVIPFriends.value = await configRepository.getBool('VRCX_isFriendsGroupFavorites', true);
|
||||
isOnlineFriends.value = await configRepository.getBool('VRCX_isFriendsGroupOnline', true);
|
||||
isActiveFriends.value = await configRepository.getBool('VRCX_isFriendsGroupActive', false);
|
||||
isOfflineFriends.value = await configRepository.getBool('VRCX_isFriendsGroupOffline', false);
|
||||
isSidebarGroupByInstanceCollapsed.value = await configRepository.getBool(
|
||||
'VRCX_sidebarGroupByInstanceCollapsed',
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
function toggleSwitchGroupByInstanceCollapsed() {
|
||||
isSidebarGroupByInstanceCollapsed.value = !isSidebarGroupByInstanceCollapsed.value;
|
||||
configRepository.setBool('VRCX_sidebarGroupByInstanceCollapsed', isSidebarGroupByInstanceCollapsed.value);
|
||||
}
|
||||
|
||||
function getFriendsLocations(friendsArr) {
|
||||
// prevent the instance title display as "Traveling".
|
||||
if (!friendsArr?.length) {
|
||||
return '';
|
||||
}
|
||||
for (const friend of friendsArr) {
|
||||
if (isRealInstance(friend.ref?.location)) {
|
||||
return friend.ref.location;
|
||||
}
|
||||
}
|
||||
for (const friend of friendsArr) {
|
||||
if (isRealInstance(friend.ref?.travelingToLocation)) {
|
||||
return friend.ref.travelingToLocation;
|
||||
}
|
||||
}
|
||||
for (const friend of friendsArr) {
|
||||
if (lastLocation.value.friendList.has(friend.id)) {
|
||||
return lastLocation.value.location;
|
||||
}
|
||||
}
|
||||
return friendsArr[0].ref?.location;
|
||||
}
|
||||
|
||||
function confirmDeleteFriend(friend) {
|
||||
emit('confirm-delete-friend', friend);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.x-link:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
.x-link:hover span {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
106
src/views/Sidebar/components/GroupsSidebar.vue
Normal file
106
src/views/Sidebar/components/GroupsSidebar.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<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="showGroupDialog(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 setup>
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { computed, ref } from 'vue';
|
||||
import { convertFileUrlToImageUrl } from '../../../shared/utils';
|
||||
import { useAppearanceSettingsStore, useGroupStore } from '../../../stores';
|
||||
|
||||
const { isAgeGatedInstancesVisible } = storeToRefs(useAppearanceSettingsStore());
|
||||
const { showGroupDialog, sortGroupInstancesByInGame } = useGroupStore();
|
||||
const { groupInstances } = storeToRefs(useGroupStore());
|
||||
|
||||
defineProps({
|
||||
groupOrder: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
});
|
||||
|
||||
const groupInstancesCfg = ref({});
|
||||
|
||||
const groupedGroupInstances = computed(() => {
|
||||
const groupMap = new Map();
|
||||
|
||||
groupInstances.value.forEach((ref) => {
|
||||
const groupId = ref.group.groupId;
|
||||
if (!groupMap.has(groupId)) {
|
||||
groupMap.set(groupId, []);
|
||||
}
|
||||
groupMap.get(groupId).push(ref);
|
||||
|
||||
if (!groupInstancesCfg.value[ref.group?.groupId]) {
|
||||
groupInstancesCfg.value = {
|
||||
[ref.group.groupId]: {
|
||||
isCollapsed: false
|
||||
},
|
||||
...groupInstancesCfg.value
|
||||
};
|
||||
}
|
||||
});
|
||||
return Array.from(groupMap.values()).sort(sortGroupInstancesByInGame);
|
||||
});
|
||||
|
||||
function getSmallGroupIconUrl(url) {
|
||||
return convertFileUrlToImageUrl(url);
|
||||
}
|
||||
|
||||
function toggleGroupSidebarCollapse(groupId) {
|
||||
groupInstancesCfg.value[groupId].isCollapsed = !groupInstancesCfg.value[groupId].isCollapsed;
|
||||
}
|
||||
|
||||
function getGroupId(group) {
|
||||
return group[0]?.group?.groupId || '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.x-link:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
.x-link:hover span {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user