Upgrade to Vue3 and Element Plus (#1374)

* Update Vue devtools

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

* fix: i18n

* global components

* change v-deep

* upgrade vue-lazyload

* data table

* update enlint and safe-dialog

* package.json and vite.config.js

* el-icon

* el-message

* vue 2 -> vue3 migration changes

* $pinia

* dialog

* el-popover slot

* lint

* chore

* slot

* scss

* remote state access

* misc

* jsconfig

* el-button size mini -> small

* :model-value

* ElMessageBox

* datatable

* remove v-lazyload

* template #dropdown

* mini -> small

* css

* byebye hideTooltips

* use sass-embedded

* Update SQLite, remove unneeded libraries

* Fix shift remove local avatar favorites

* Electron arm64

* arm64 support

* bye pug

* f-word vite hah

* misc

* remove safe dialog component

* Add self invite to launch dialog

* Fix errors

* Icons 1

* improve localfavorite loading performance

* improve favorites world item performance

* dialog visibility changes for Element Plus

* clear element plus error

* import performance

* revert App.vue hah

* hah

* Revert "Add self invite to launch dialog"

This reverts commit 4801cfad58.

* Toggle self invite/open in-game

* Self invite on launch dialog

* el-button icon

* el-icon

* fix user dialog tab switching logic

* fix PlayerList

* Formatting changes

* More icons

* Fix friend log table

* loading margin

* fix markdown

* fix world dialog tab switching issue

* Fixes and formatting

* fix: global i18n.t export

* fix favorites world tab not working

* Create instance, displayName

* Remove group members sort by userId

* Fix loading dialog tabs on swtich

* Star

* charts console.warn

* wip: fix charts

* wip: fix charts

* wip: charts composables

* fix favorite item tooltip warning

* Fixes and formatting

* Clean up image dialogs

* Remove unused method

* Fix platform/size border

* Fix platform/size border

* $vr

* fix friendExportDialogVisible binding

* ElMessageBox and Settings

* Login formatting

* Rename VR overlay query

* Fix image popover and userdialog badges

* Formatting

* Big buttons

* Fixes, update Cef

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

* Fix z-index

* vr overlay

* vite input add theme

* defineAsyncComponent

* ISO 639-1

* fix i18n

* clean t

* Formatting, fix calendar, rotate arrows

* Show user status when user is offline

* Fix VR overlay

* fix theme and clean up

* split InstanceActivity

* tweak

* Fix VR overlay formatting

* fix scss var

* AppDebug hahahaha

* Years

* remove reactive

* improve perf

* state hah…

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

* improve perf

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

* improve perf 1

* Suppress stray errors

* fix traveling location display issue

* Fix empty instance creator

* improve friend list refresh performance

* fix main charts

* fix chart

* Fix darkmode

* Fix avatar dialog tags

---------

Co-authored-by: pa <maplenagisa@gmail.com>
This commit is contained in:
Natsumi
2025-09-12 10:45:24 +12:00
committed by GitHub
parent b233bbc299
commit 3324d0d279
249 changed files with 12948 additions and 19815 deletions
+5 -3
View File
@@ -5,17 +5,19 @@
</div>
<keep-alive>
<InstanceActivity v-if="menuActiveIndex === 'charts'" />
<el-backtop target="#chart" :right="30" :bottom="30"></el-backtop>
</keep-alive>
<el-backtop target="#chart" :right="30" :bottom="30"></el-backtop>
</div>
</template>
<script setup>
import { defineAsyncComponent } from 'vue';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n-bridge';
import InstanceActivity from './components/InstanceActivity.vue';
import { useI18n } from 'vue-i18n';
import { useUiStore } from '../../stores';
const InstanceActivity = defineAsyncComponent(() => import('./components/InstanceActivity.vue'));
const { t } = useI18n();
const uiStore = useUiStore();
+258 -466
View File
@@ -3,73 +3,85 @@
<div class="options-container instance-activity" style="margin-top: 0">
<div>
<span>{{ t('view.charts.instance_activity.header') }}</span>
<el-popover placement="bottom-start" trigger="hover" width="300">
<el-popover placement="bottom-start" trigger="hover" :width="300">
<div class="tips-popover">
<div>{{ t('view.charts.instance_activity.tips.online_time') }}</div>
<div>{{ t('view.charts.instance_activity.tips.click_Y_axis') }}</div>
<div>{{ t('view.charts.instance_activity.tips.click_instance_name') }}</div>
<div>
<i class="el-icon-warning-outline"></i
<el-icon><WarningFilled /></el-icon
><i>{{ t('view.charts.instance_activity.tips.accuracy_notice') }}</i>
</div>
</div>
<i
slot="reference"
class="el-icon-info"
style="margin-left: 5px; font-size: 12px; opacity: 0.7"></i>
<template #reference>
<el-icon style="margin-left: 5px; font-size: 12px; opacity: 0.7"><InfoFilled /></el-icon>
</template>
</el-popover>
</div>
<div>
<el-tooltip :content="t('view.charts.instance_activity.refresh')" placement="top"
><el-button icon="el-icon-refresh" circle style="margin-right: 9px" @click="reloadData"></el-button
><el-button :icon="Refresh" circle style="margin-right: 5px" @click="reloadData"></el-button
></el-tooltip>
<el-tooltip :content="t('view.charts.instance_activity.settings.header')" placement="top">
<el-popover placement="bottom" trigger="click" style="margin-right: 9px">
<div class="settings">
<el-popover placement="bottom" trigger="click" :width="250">
<div class="settings">
<div>
<span>{{ t('view.charts.instance_activity.settings.bar_width') }}</span>
<div>
<span>{{ t('view.charts.instance_activity.settings.bar_width') }}</span>
<div>
<el-slider
v-model.lazy="barWidth"
:max="50"
:min="1"
@change="changeBarWidth"></el-slider>
</div>
</div>
<div>
<span>{{ t('view.charts.instance_activity.settings.show_detail') }}</span>
<el-switch v-model="isDetailVisible" @change="changeIsDetailInstanceVisible">
</el-switch>
</div>
<div v-if="isDetailVisible">
<span>{{ t('view.charts.instance_activity.settings.show_solo_instance') }}</span>
<el-switch v-model="isSoloInstanceVisible" @change="changeIsSoloInstanceVisible">
</el-switch>
</div>
<div v-if="isDetailVisible">
<span>{{ t('view.charts.instance_activity.settings.show_no_friend_instance') }}</span>
<el-switch
v-model="isNoFriendInstanceVisible"
@change="changeIsNoFriendInstanceVisible">
</el-switch>
<el-slider
v-model.lazy="barWidth"
:max="50"
:min="1"
@change="
(value) => changeBarWidth(value, () => handleEchartsRerender())
"></el-slider>
</div>
</div>
<div>
<span>{{ t('view.charts.instance_activity.settings.show_detail') }}</span>
<el-switch
v-model="isDetailVisible"
@change="(value) => changeIsDetailInstanceVisible(value, () => handleSettingsChange())">
</el-switch>
</div>
<div v-if="isDetailVisible">
<span>{{ t('view.charts.instance_activity.settings.show_solo_instance') }}</span>
<el-switch
v-model="isSoloInstanceVisible"
@change="(value) => changeIsSoloInstanceVisible(value, () => handleSettingsChange())">
</el-switch>
</div>
<div v-if="isDetailVisible">
<span>{{ t('view.charts.instance_activity.settings.show_no_friend_instance') }}</span>
<el-switch
v-model="isNoFriendInstanceVisible"
@change="
(value) => changeIsNoFriendInstanceVisible(value, () => handleSettingsChange())
">
</el-switch>
</div>
</div>
<el-button slot="reference" icon="el-icon-setting" circle></el-button>
</el-popover>
</el-tooltip>
<el-button-group style="margin-right: 10px">
<template #reference>
<div>
<el-tooltip :content="t('view.charts.instance_activity.settings.header')" placement="top">
<el-button :icon="Setting" style="margin-right: 5px" circle></el-button>
</el-tooltip>
</div>
</template>
</el-popover>
<el-button-group style="margin-right: 5px">
<el-tooltip :content="t('view.charts.instance_activity.previous_day')" placement="top">
<el-button
icon="el-icon-arrow-left"
:icon="ArrowLeft"
:disabled="isPrevDayBtnDisabled"
@click="changeSelectedDateFromBtn(false)"></el-button>
</el-tooltip>
<el-tooltip :content="t('view.charts.instance_activity.next_day')" placement="top">
<el-button :disabled="isNextDayBtnDisabled" @click="changeSelectedDateFromBtn(true)"
><i class="el-icon-arrow-right el-icon--right"></i
><el-icon class="el-icon--right"><ArrowRight /></el-icon
></el-button>
</el-tooltip>
</el-button-group>
@@ -78,17 +90,16 @@
type="date"
:clearable="false"
align="right"
:picker-options="{
disabledDate: (time) => getDatePickerDisabledDate(time)
}"
:default-value="dayjs().toDate()"
:disabled-date="getDatePickerDisabledDate"
@change="reloadData"></el-date-picker>
</div>
</div>
<div style="position: relative">
<el-statistic :title="t('view.charts.instance_activity.online_time')">
<template #formatter>
<span :style="isDarkMode ? 'color:rgb(120,120,120)' : ''">{{ totalOnlineTime }}</span>
</template>
<el-statistic
:title="t('view.charts.instance_activity.online_time')"
:formatter="(val) => timeToText(val, true)"
:value="totalOnlineTime">
</el-statistic>
</div>
@@ -102,25 +113,33 @@
<el-divider>·</el-divider>
</div>
</transition>
<InstanceActivityDetail
v-for="arr in filteredActivityDetailData"
:key="arr[0].location + arr[0].created_at"
ref="activityDetailChartRef"
:activity-detail-data="arr"
:bar-width="barWidth" />
<template v-if="isDetailVisible && activityData.length !== 0">
<InstanceActivityDetail
v-for="arr in filteredActivityDetailData"
:key="arr[0].location + arr[0].created_at"
ref="activityDetailChartRef"
:activity-detail-data="arr"
:bar-width="barWidth" />
</template>
</div>
</template>
<script setup>
import { ref, onActivated, onDeactivated, watch, computed, onMounted, nextTick, onBeforeMount } from 'vue';
import { WarningFilled, InfoFilled, Refresh, Setting, ArrowLeft, ArrowRight } from '@element-plus/icons-vue';
import { ref, onDeactivated, watch, onMounted, onBeforeMount, onActivated, nextTick } from 'vue';
import dayjs from 'dayjs';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n-bridge';
import configRepository from '../../../service/config';
import { database } from '../../../service/database';
import { getWorldName, loadEcharts, parseLocation, timeToText } from '../../../shared/utils';
import { useI18n } from 'vue-i18n';
import { parseLocation, timeToText } from '../../../shared/utils';
import * as echarts from 'echarts';
import { useAppearanceSettingsStore, useFriendStore, useUserStore } from '../../../stores';
import InstanceActivityDetail from './InstanceActivityDetail.vue';
import { useInstanceActivitySettings } from '../composables/useInstanceActivitySettings';
import { useInstanceActivityData } from '../composables/useInstanceActivityData';
import { useActivityDataProcessor } from '../composables/useActivityDataProcessor';
import { useIntersectionObserver } from '../composables/useIntersectionObserver';
import { useChartHelpers } from '../composables/useChartHelpers';
import { useDateNavigation } from '../composables/useDateNavigation';
const appearanceSettingsStore = useAppearanceSettingsStore();
const friendStore = useFriendStore();
@@ -129,72 +148,55 @@
const { currentUser } = storeToRefs(useUserStore());
const { t } = useI18n();
// echarts and observer
const echarts = ref(null);
const {
barWidth,
isDetailVisible,
isSoloInstanceVisible,
isNoFriendInstanceVisible,
initializeSettings,
changeBarWidth,
changeIsDetailInstanceVisible,
changeIsSoloInstanceVisible,
changeIsNoFriendInstanceVisible,
handleChangeSettings
} = useInstanceActivitySettings();
const {
activityData,
activityDetailData,
allDateOfActivity,
worldNameArray,
getAllDateOfActivity,
getWorldNameData,
getActivityData
} = useInstanceActivityData();
const echartsInstance = ref(null);
const resizeObserver = ref(null);
const intersectionObservers = ref([]);
const selectedDate = ref(dayjs());
// data
const activityData = ref([]);
const activityDetailData = ref([]);
const allDateOfActivity = ref(new Set());
const worldNameArray = ref([]);
const { handleIntersectionObserver } = useIntersectionObserver();
const isLoading = ref(true);
// settings
const barWidth = ref(25);
const isDetailVisible = ref(true);
const isSoloInstanceVisible = ref(true);
const isNoFriendInstanceVisible = ref(true);
let reloadData;
const {
selectedDate,
isNextDayBtnDisabled,
isPrevDayBtnDisabled,
changeSelectedDateFromBtn,
getDatePickerDisabledDate
} = useDateNavigation(allDateOfActivity, () => reloadData());
const activityChartRef = ref(null);
const activityDetailChartRef = ref(null);
const totalOnlineTime = computed(() => {
return timeToText(
activityData.value?.reduce((acc, item) => acc + item.time, 0),
true
);
});
const { totalOnlineTime, filteredActivityDetailData } = useActivityDataProcessor(
activityData,
activityDetailData,
isDetailVisible,
isSoloInstanceVisible,
isNoFriendInstanceVisible
);
const allDateOfActivityArray = computed(() => {
return allDateOfActivity.value
? Array.from(allDateOfActivity.value)
.map((item) => dayjs(item))
.sort((a, b) => b.valueOf() - a.valueOf())
: [];
});
const isNextDayBtnDisabled = computed(() => {
return dayjs(selectedDate.value).isSameOrAfter(allDateOfActivityArray.value[0], 'day');
});
const isPrevDayBtnDisabled = computed(() => {
return dayjs(selectedDate.value).isSame(
allDateOfActivityArray.value[allDateOfActivityArray.value.length - 1],
'day'
);
});
const filteredActivityDetailData = computed(() => {
if (!isDetailVisible.value) {
return [];
}
let result = [...activityDetailData.value];
if (!isSoloInstanceVisible.value) {
result = result.filter((arr) => arr.length > 1);
}
if (!isNoFriendInstanceVisible.value) {
result = result.filter((arr) => {
// solo instance
if (arr.length === 1) {
return true;
}
return arr.some((item) => item.isFriend);
});
}
return result;
});
const { isDetailDataFiltered, findMatchingDetailData, generateYAxisLabel } = useChartHelpers();
watch(
() => isDarkMode.value,
@@ -218,7 +220,7 @@
onActivated(() => {
// first time also call activated
if (!echartsInstance.value) {
if (echartsInstance.value) {
reloadData();
}
});
@@ -230,75 +232,114 @@
}
});
function created() {
resizeObserver.value = new ResizeObserver((entries) => {
for (const entry of entries) {
echartsInstance.value.resize({
width: entry.contentRect.width,
animation: {
duration: 300
}
});
}
});
configRepository.getInt('VRCX_InstanceActivityBarWidth', 25).then((value) => {
barWidth.value = value;
});
configRepository.getBool('VRCX_InstanceActivityDetailVisible', true).then((value) => {
isDetailVisible.value = value;
});
configRepository.getBool('VRCX_InstanceActivitySoloInstanceVisible', true).then((value) => {
isSoloInstanceVisible.value = value;
});
configRepository.getBool('VRCX_InstanceActivityNoFriendInstanceVisible', true).then((value) => {
isNoFriendInstanceVisible.value = value;
});
}
onBeforeMount(() => {
// ensure created is called before mounted
created();
initializeSettings();
});
onMounted(async () => {
try {
getAllDateOfActivity();
const [echartsModule] = await Promise.all([
// lazy load echarts
loadEcharts().catch((error) => {
console.error('lazy load echarts failed', error);
return null;
}),
getActivityData()
]);
if (echartsModule) {
echarts.value = echartsModule;
}
if (echartsModule) {
initEcharts();
getWorldNameData();
} else {
isLoading.value = false;
}
await getActivityData(selectedDate, currentUser, friends, localFavoriteFriends, () =>
handleIntersectionObserver(activityDetailChartRef)
);
await getWorldNameData();
initEcharts();
} catch (error) {
console.error('error in mounted', error);
isLoading.value = false;
}
});
async function reloadData() {
reloadData = async function () {
isLoading.value = true;
await getActivityData();
getWorldNameData();
// possibility past 24:00
getAllDateOfActivity();
try {
await getActivityData(selectedDate, currentUser, friends, localFavoriteFriends, () =>
handleIntersectionObserver(activityDetailChartRef)
);
await getWorldNameData();
// possibility past 24:00
getAllDateOfActivity();
await nextTick();
if (echartsInstance.value && activityData.value.length) {
const chartsHeight = activityData.value.length * (barWidth.value + 10) + 200;
echartsInstance.value.resize({
height: chartsHeight,
animation: {
duration: 300
}
});
echartsInstance.value.setOption(getNewOption(), { notMerge: true });
} else if (echartsInstance.value) {
echartsInstance.value.clear();
}
} catch (error) {
console.error('Error in reloadData:', error);
} finally {
isLoading.value = false;
}
};
function handleYAxisLabelClick(params) {
const targetActivity = activityData.value[params?.dataIndex];
if (!targetActivity) {
console.error('handleClickYAxisLabel failed, no activity data found for index:', params?.dataIndex);
return;
}
const detailDataIdx = filteredActivityDetailData.value.findIndex((arr) => {
const sameLocation = arr[0]?.location === targetActivity.location;
const sameJoinTime = arr
.find((item) => item.user_id === currentUser.value.id)
?.joinTime.isSame(targetActivity.joinTime);
return sameLocation && sameJoinTime;
});
if (detailDataIdx === -1) {
console.error(
"handleClickYAxisLabel failed, likely current user wasn't in this instance or chart is filtered out.",
params
);
return;
}
if (activityDetailChartRef.value && activityDetailChartRef.value[detailDataIdx]) {
activityDetailChartRef.value[detailDataIdx].$el.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
} else {
console.error('handleClickYAxisLabel failed, chart ref not found at index:', detailDataIdx);
}
}
function getYAxisData() {
return worldNameArray.value.map((worldName, index) => {
const activityItem = activityData.value[index];
if (!activityItem) return worldName;
const detailData = findMatchingDetailData(activityItem, activityDetailData.value, currentUser.value);
if (!detailData) return worldName;
const shouldFilter =
isDetailVisible.value &&
isDetailDataFiltered(detailData, isSoloInstanceVisible.value, isNoFriendInstanceVisible.value);
return generateYAxisLabel(worldName, shouldFilter);
});
}
// echarts - start
function initEcharts() {
const chartsHeight = activityData.value.length * (barWidth.value + 10) + 200;
const chartDom = activityChartRef.value;
const afterInit = () => {
if (!echartsInstance.value) {
console.error('ECharts instance not initialized');
return;
}
echartsInstance.value.resize({
height: chartsHeight,
animation: {
@@ -306,67 +347,42 @@
}
});
const handleClickYAxisLabel = (params) => {
const detailDataIdx = filteredActivityDetailData.value.findIndex((arr) => {
const sameLocation = arr[0]?.location === activityData.value[params?.dataIndex]?.location;
const sameJoinTime = arr
.find((item) => item.user_id === currentUser.value.id)
?.joinTime.isSame(activityData.value[params?.dataIndex].joinTime);
return sameLocation && sameJoinTime;
});
if (detailDataIdx === -1) {
// no detail chart down below, it's hidden, so can't find instance data index
console.error("handleClickYAxisLabel failed, likely current user wasn't in this instance.", params);
} else {
activityDetailChartRef.value[detailDataIdx].$el.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
};
const handleClickYAxisLabel = handleYAxisLabelClick;
const options = activityData.value.length ? getNewOption() : {};
echartsInstance.value.off('click');
echartsInstance.value.setOption(options, { lazyUpdate: true });
echartsInstance.value.on('click', 'yAxis', handleClickYAxisLabel);
if (activityData.value.length && worldNameArray.value.length) {
const options = getNewOption();
echartsInstance.value.clear();
echartsInstance.value.setOption(options, { notMerge: true });
echartsInstance.value.on('click', 'yAxis', handleClickYAxisLabel);
} else {
echartsInstance.value.clear();
}
isLoading.value = false;
};
const initEchartsInstance = () => {
echartsInstance.value = echarts.value.init(chartDom, `${isDarkMode.value ? 'dark' : null}`, {
echartsInstance.value = echarts.init(chartDom, `${isDarkMode.value ? 'dark' : null}`, {
height: chartsHeight
});
resizeObserver.value.observe(chartDom);
};
const loadEchartsWithTimeout = () => {
const timeout = 5000;
let time = 0;
const timer = setInterval(() => {
if (echarts.value) {
initEchartsInstance();
afterInit();
clearInterval(timer);
return;
}
time += 100;
if (time >= timeout) {
clearInterval(timer);
console.error('echarts init timeout');
}
}, 100);
// resizeObserver.value = new ResizeObserver((entries) => {
// for (const entry of entries) {
// echartsInstance.value.resize({
// width: entry.contentRect.width,
// animation: {
// duration: 300
// }
// });
// }
// });
// resizeObserver.value.observe(chartDom);
};
if (!echartsInstance.value) {
if (!echarts.value) {
loadEchartsWithTimeout();
} else {
initEchartsInstance();
afterInit();
}
} else {
afterInit();
initEchartsInstance();
}
afterInit();
}
function getNewOption() {
const getTooltip = (params) => {
@@ -420,10 +436,17 @@
type: 'category',
axisLabel: {
interval: 0,
formatter: (value) => (value.length > 20 ? `${value.slice(0, 20)}...` : value)
rich: {
filtered: {
opacity: 0.4
},
normal: {
opacity: 1
}
}
},
inverse: true,
data: worldNameArray.value,
data: getYAxisData(),
triggerEvent: true
},
xAxis: {
@@ -461,7 +484,7 @@
return 0;
}
}
return item.joinTime - dayjs.tz(selectedDate.value).startOf('day');
return item.joinTime - dayjs.tz(selectedDate.value).startOf('day').valueOf();
})
},
{
@@ -485,7 +508,7 @@
if (idx === 0) {
const midnight = dayjs.tz(selectedDate.value).startOf('day');
if (midnight.isAfter(item.joinTime)) {
return item.leaveTime - dayjs.tz(midnight);
return item.leaveTime - dayjs.tz(midnight).valueOf();
}
}
return item.time;
@@ -494,258 +517,27 @@
],
backgroundColor: 'transparent'
};
return echartsOption;
}
// echarts - end
// settings - start
function changeBarWidth(value) {
barWidth.value = value;
function handleEchartsRerender() {
initEcharts();
configRepository.setInt('VRCX_InstanceActivityBarWidth', value).finally(() => {
handleChangeSettings();
});
handleSettingsChange();
}
function changeIsDetailInstanceVisible(value) {
isDetailVisible.value = value;
configRepository.setBool('VRCX_InstanceActivityDetailVisible', value).finally(() => {
handleChangeSettings();
});
}
function changeIsSoloInstanceVisible(value) {
isSoloInstanceVisible.value = value;
configRepository.setBool('VRCX_InstanceActivitySoloInstanceVisible', value).finally(() => {
handleChangeSettings();
});
}
function changeIsNoFriendInstanceVisible(value) {
isNoFriendInstanceVisible.value = value;
configRepository.setBool('VRCX_InstanceActivityNoFriendInstanceVisible', value).finally(() => {
handleChangeSettings();
});
}
function handleChangeSettings() {
function handleSettingsChange() {
handleChangeSettings(activityDetailChartRef);
if (echartsInstance.value) {
const newOptions = getNewOption();
echartsInstance.value.setOption({
yAxis: newOptions.yAxis
});
}
nextTick(() => {
if (activityDetailChartRef.value) {
activityDetailChartRef.value.forEach((child) => {
requestAnimationFrame(() => {
if (child.echartsInstance) {
child.initEcharts();
}
});
});
}
});
//rerender detail chart
}
// settings - end
// options - start
function changeSelectedDateFromBtn(isNext = false) {
if (!allDateOfActivityArray.value || allDateOfActivityArray.value.length === 0) {
return;
}
const idx = allDateOfActivityArray.value.findIndex((date) => date.isSame(selectedDate.value, 'day'));
if (idx !== -1) {
const newIdx = isNext ? idx - 1 : idx + 1;
if (newIdx >= 0 && newIdx < allDateOfActivityArray.value.length) {
selectedDate.value = allDateOfActivityArray.value[newIdx];
reloadData();
return;
}
}
selectedDate.value = isNext
? allDateOfActivityArray.value[0]
: allDateOfActivityArray.value[allDateOfActivityArray.value.length - 1];
reloadData();
}
function getDatePickerDisabledDate(time) {
if (
time > Date.now() ||
allDateOfActivityArray.value[allDateOfActivityArray.value.length - 1]
?.add(-1, 'day')
.isAfter(time, 'day') ||
!allDateOfActivity.value
) {
return true;
}
return !allDateOfActivity.value.has(dayjs(time).format('YYYY-MM-DD'));
}
// options - end
// data - start
async function getWorldNameData() {
worldNameArray.value = await Promise.all(
activityData.value.map(async (item) => {
try {
return await getWorldName(item.location);
} catch {
console.error('getWorldName failed location', item.location);
return 'Unknown world';
}
})
);
if (worldNameArray.value) {
initEcharts();
}
}
async function getAllDateOfActivity() {
const utcDateStrings = (await database.getDateOfInstanceActivity()) || [];
const uniqueDates = new Set();
for (const utcString of utcDateStrings) {
const formattedDate = dayjs.utc(utcString).tz().format('YYYY-MM-DD');
uniqueDates.add(formattedDate);
}
allDateOfActivity.value = uniqueDates;
}
async function getActivityData() {
const localStartDate = dayjs.tz(selectedDate.value).startOf('day').toISOString();
const localEndDate = dayjs.tz(selectedDate.value).endOf('day').toISOString();
const dbData = await database.getInstanceActivity(localStartDate, localEndDate);
const transformData = (item) => ({
...item,
joinTime: dayjs(item.created_at).subtract(item.time, 'millisecond'),
leaveTime: dayjs(item.created_at),
time: item.time < 0 ? 0 : item.time,
isFriend: item.user_id === currentUser.value.id ? null : friends.value.has(item.user_id),
isFavorite: item.user_id === currentUser.value.id ? null : localFavoriteFriends.value.has(item.user_id)
});
activityData.value = dbData.currentUserData.map(transformData);
const transformAndSort = (arr) => {
return arr.map(transformData).sort((a, b) => {
const timeDiff = Math.abs(a.joinTime.diff(b.joinTime, 'second'));
// recording delay, under 3s is considered the same time entry, beautify the chart
return timeDiff < 3 ? a.leaveTime - b.leaveTime : a.joinTime - b.joinTime;
});
};
const filterByLocation = (innerArray, locationSet) => {
return innerArray.every((innerObject) => locationSet.has(innerObject.location));
};
const locationSet = new Set(activityData.value.map((item) => item.location));
const preSplitActivityDetailData = Array.from(dbData.detailData.values())
.map(transformAndSort)
.filter((innerArray) => filterByLocation(innerArray, locationSet));
activityDetailData.value = handleSplitActivityDetailData(preSplitActivityDetailData, currentUser.value.id);
if (activityDetailData.value.length) {
nextTick(() => {
handleIntersectionObserver();
});
}
}
function handleSplitActivityDetailData(activityDetailData, currentUserId) {
function countTargetIdOccurrences(innerArray, targetId) {
let count = 0;
for (const obj of innerArray) {
if (obj.user_id === targetId) {
count++;
}
}
return count;
}
function areIntervalsOverlapping(objA, objB) {
const isObj1EndTimeBeforeObj2StartTime = objA.leaveTime.isBefore(objB.joinTime, 'second');
const isObj2EndTimeBeforeObj1StartTime = objB.leaveTime.isBefore(objA.joinTime, 'second');
return !(isObj1EndTimeBeforeObj2StartTime || isObj2EndTimeBeforeObj1StartTime);
}
function buildOverlapGraph(innerArray) {
const numObjects = innerArray.length;
const adjacencyList = Array.from({ length: numObjects }, () => []);
for (let i = 0; i < numObjects; i++) {
for (let j = i + 1; j < numObjects; j++) {
if (areIntervalsOverlapping(innerArray[i], innerArray[j])) {
adjacencyList[i].push(j);
adjacencyList[j].push(i);
}
}
}
return adjacencyList;
}
function depthFirstSearch(nodeIndex, visited, graph, component) {
visited[nodeIndex] = true;
component.push(nodeIndex);
for (const neighborIndex of graph[nodeIndex]) {
if (!visited[neighborIndex]) {
depthFirstSearch(neighborIndex, visited, graph, component);
}
}
}
function findConnectedComponents(graph, numNodes) {
const visited = new Array(numNodes).fill(false);
const components = [];
for (let i = 0; i < numNodes; i++) {
if (!visited[i]) {
const component = [];
depthFirstSearch(i, visited, graph, component);
components.push(component);
}
}
return components;
}
function processOuterArrayWithTargetId(outerArray, targetId) {
let i = 0;
while (i < outerArray.length) {
let currentInnerArray = outerArray[i];
let targetIdCount = countTargetIdOccurrences(currentInnerArray, targetId);
if (targetIdCount > 1) {
let graph = buildOverlapGraph(currentInnerArray);
let connectedComponents = findConnectedComponents(graph, currentInnerArray.length);
let newInnerArrays = connectedComponents.map((componentIndices) => {
return componentIndices.map((index) => currentInnerArray[index]);
});
outerArray.splice(i, 1, ...newInnerArrays);
i += newInnerArrays.length;
} else {
i += 1;
}
}
return outerArray.sort((a, b) => a[0].joinTime - b[0].joinTime);
}
return processOuterArrayWithTargetId(activityDetailData, currentUserId);
}
// data - end
// intersection observer - start
function handleIntersectionObserver() {
activityDetailChartRef.value?.forEach((child, index) => {
const observer = new IntersectionObserver((entries) => handleIntersection(index, entries));
observer.observe(child.$el);
intersectionObservers.value[index] = observer;
handleIntersectionObserver(activityDetailChartRef);
});
}
function handleIntersection(index, entries) {
if (!entries) {
console.error('handleIntersection failed');
return;
}
entries.forEach((entry) => {
if (entry.isIntersecting && activityDetailChartRef.value[index]) {
activityDetailChartRef.value[index].initEcharts();
intersectionObservers.value[index].unobserve(entry.target);
}
});
}
// intersection observer - end
</script>
<style lang="scss" scoped>
@@ -798,7 +590,7 @@
& > div:first-child {
> div {
width: 160px;
padding-left: 20px;
margin-left: 20px;
}
}
}
@@ -15,11 +15,12 @@
</template>
<script setup>
import { ref, watch, computed, onDeactivated, onMounted } from 'vue';
import { ref, watch, computed, onDeactivated, onMounted, nextTick } from 'vue';
import dayjs from 'dayjs';
import { storeToRefs } from 'pinia';
import { loadEcharts, timeToText } from '../../../shared/utils';
import { timeToText } from '../../../shared/utils';
import * as echarts from 'echarts';
import { useUserStore, useAppearanceSettingsStore } from '../../../stores';
const { isDarkMode, dtHour12 } = storeToRefs(useAppearanceSettingsStore());
@@ -40,7 +41,6 @@
const activityDetailChartRef = ref(null);
const echarts = ref(null);
const isLoading = ref(true);
const echartsInstance = ref(null);
const usersFirstActivity = ref(null);
@@ -76,8 +76,9 @@
initResizeObserver();
onMounted(() => {
initEcharts(true);
onMounted(async () => {
await nextTick();
initEcharts();
});
onDeactivated(() => {
@@ -87,45 +88,79 @@
function initResizeObserver() {
resizeObserver.value = new ResizeObserver((entries) => {
if (!echartsInstance.value) {
return;
}
for (const entry of entries) {
echartsInstance.value.resize({
width: entry.contentRect.width,
animation: {
duration: 300
}
});
try {
echartsInstance.value.resize({
width: entry.contentRect.width,
animation: {
duration: 300
}
});
} catch (error) {
console.warn('Error resizing chart:', error);
}
}
});
}
async function initEcharts(isFirstLoad = false) {
if (!echarts.value) {
echarts.value = await loadEcharts();
async function initEcharts() {
if (!activityDetailChartRef.value || !props.activityDetailData || props.activityDetailData.length === 0) {
isLoading.value = false;
return;
}
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
const afterInit = () => {
if (!echartsInstance.value) {
console.error('ECharts instance not initialized');
isLoading.value = false;
return;
}
});
echartsInstance.value.setOption(isFirstLoad ? {} : getNewOption(), { lazyUpdate: true });
echartsInstance.value.on('click', 'yAxis', handleClickYAxisLabel);
try {
echartsInstance.value.resize({
height: chartsHeight,
animation: {
duration: 300
}
});
echartsInstance.value.off('click');
const options = getNewOption();
if (options && options.series && options.series.length > 0) {
echartsInstance.value.clear();
echartsInstance.value.setOption(options, { notMerge: true });
echartsInstance.value.on('click', 'yAxis', handleClickYAxisLabel);
} else {
echartsInstance.value.clear();
}
} catch (error) {
console.error('Error in afterInit:', error);
}
setTimeout(() => {
isLoading.value = false;
}, 200);
};
const initEchartsInstance = () => {
if (!echartsInstance.value) {
echartsInstance.value = echarts.init(chartDom, `${isDarkMode.value ? 'dark' : null}`, {
height: chartsHeight,
useDirtyRect: props.activityDetailData.length > 80
});
if (resizeObserver.value) {
resizeObserver.value.observe(chartDom);
}
}
};
initEchartsInstance();
setTimeout(afterInit, 50);
}
function handleClickYAxisLabel(params) {
@@ -136,6 +171,26 @@
}
function getNewOption() {
if (!props.activityDetailData || props.activityDetailData.length === 0) {
return {
title: {
text: 'No data available',
left: 'center',
top: 'middle'
}
};
}
if (!startTimeStamp.value || !endTimeStamp.value) {
return {
title: {
text: 'Invalid timestamp data',
left: 'center',
top: 'middle'
}
};
}
// 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
@@ -318,9 +373,10 @@
splitLine: { lineStyle: { type: 'dashed' } }
},
series: generateSeries(),
backgroundColor: 'rgba(0, 0, 0, 0)'
backgroundColor: 'transparent'
};
console.log(echartsOption);
return echartsOption;
}
@@ -0,0 +1,27 @@
import { computed } from 'vue';
export function useActivityDataFilter(activityDetailData, isDetailVisible, isSoloInstanceVisible, isNoFriendInstanceVisible) {
const filteredActivityDetailData = computed(() => {
if (!isDetailVisible.value) {
return [];
}
let result = [...activityDetailData.value];
if (!isSoloInstanceVisible.value) {
result = result.filter((arr) => arr.length > 1);
}
if (!isNoFriendInstanceVisible.value) {
result = result.filter((arr) => {
// solo instance
if (arr.length === 1) {
return true;
}
return arr.some((item) => item.isFriend);
});
}
return result;
});
return {
filteredActivityDetailData
};
}
@@ -0,0 +1,38 @@
import { computed } from 'vue';
export function useActivityDataProcessor(
activityData,
activityDetailData,
isDetailVisible,
isSoloInstanceVisible,
isNoFriendInstanceVisible
) {
const totalOnlineTime = computed(() => {
return activityData.value?.reduce((acc, item) => acc + item.time, 0);
});
const filteredActivityDetailData = computed(() => {
if (!isDetailVisible.value) {
return [];
}
let result = [...activityDetailData.value];
if (!isSoloInstanceVisible.value) {
result = result.filter((arr) => arr.length > 1);
}
if (!isNoFriendInstanceVisible.value) {
result = result.filter((arr) => {
// solo instance
if (arr.length === 1) {
return true;
}
return arr.some((item) => item.isFriend);
});
}
return result;
});
return {
totalOnlineTime,
filteredActivityDetailData
};
}
@@ -0,0 +1,11 @@
import { computed } from 'vue';
export function useActivityStats(activityData) {
const totalOnlineTime = computed(() => {
return activityData.value?.reduce((acc, item) => acc + item.time, 0);
});
return {
totalOnlineTime
};
}
@@ -0,0 +1,62 @@
export function isDetailDataFiltered(
detailData,
isSoloInstanceVisible,
isNoFriendInstanceVisible
) {
if (!detailData) return false;
if (!isSoloInstanceVisible && detailData.length <= 1) {
return true;
}
if (
!isNoFriendInstanceVisible &&
detailData.length > 1 &&
!detailData.some((item) => item.isFriend)
) {
return true;
}
return false;
}
export function findMatchingDetailData(
activityItem,
activityDetailData,
currentUser
) {
if (!activityItem || !currentUser) return null;
return activityDetailData.find((arr) => {
const sameLocation = arr[0]?.location === activityItem.location;
const sameJoinTime = arr
.find((item) => item.user_id === currentUser.id)
?.joinTime.isSame(activityItem.joinTime);
return sameLocation && sameJoinTime;
});
}
export function generateYAxisLabel(worldName, isFiltered, maxLength = 20) {
const truncatedName =
worldName.length > maxLength
? `${worldName.slice(0, maxLength)}...`
: worldName;
return isFiltered
? `{filtered|${truncatedName}}`
: `{normal|${truncatedName}}`;
}
export function formatWorldName(worldName, maxLength = 20) {
return worldName.length > maxLength
? `${worldName.slice(0, maxLength)}...`
: worldName;
}
export function useChartHelpers() {
return {
isDetailDataFiltered,
findMatchingDetailData,
generateYAxisLabel,
formatWorldName
};
}
@@ -0,0 +1,82 @@
import { ref, computed } from 'vue';
import dayjs from 'dayjs';
export function useDateNavigation(allDateOfActivity, reloadData) {
const selectedDate = ref(dayjs().toDate());
const allDateOfActivityArray = computed(() => {
return allDateOfActivity.value
? Array.from(allDateOfActivity.value)
.map((item) => dayjs(item))
.sort((a, b) => b.valueOf() - a.valueOf())
: [];
});
const isNextDayBtnDisabled = computed(() => {
return dayjs(selectedDate.value).isSameOrAfter(
allDateOfActivityArray.value[0],
'day'
);
});
const isPrevDayBtnDisabled = computed(() => {
return dayjs(selectedDate.value).isSame(
allDateOfActivityArray.value[
allDateOfActivityArray.value.length - 1
],
'day'
);
});
function changeSelectedDateFromBtn(isNext = false) {
if (
!allDateOfActivityArray.value ||
allDateOfActivityArray.value.length === 0
) {
return;
}
const idx = allDateOfActivityArray.value.findIndex((date) =>
date.isSame(selectedDate.value, 'day')
);
if (idx !== -1) {
const newIdx = isNext ? idx - 1 : idx + 1;
if (newIdx >= 0 && newIdx < allDateOfActivityArray.value.length) {
selectedDate.value =
allDateOfActivityArray.value[newIdx].toDate();
reloadData();
return;
}
}
selectedDate.value = isNext
? allDateOfActivityArray.value[0].toDate()
: allDateOfActivityArray.value[
allDateOfActivityArray.value.length - 1
].toDate();
reloadData();
}
function getDatePickerDisabledDate(time) {
if (
time > Date.now() ||
allDateOfActivityArray.value[
allDateOfActivityArray.value.length - 1
]
?.add(-1, 'day')
.isAfter(time, 'day') ||
!allDateOfActivity.value
) {
return true;
}
return !allDateOfActivity.value.has(dayjs(time).format('YYYY-MM-DD'));
}
return {
selectedDate,
isNextDayBtnDisabled,
isPrevDayBtnDisabled,
changeSelectedDateFromBtn,
getDatePickerDisabledDate
};
}
@@ -0,0 +1,228 @@
import { ref, nextTick } from 'vue';
import dayjs from 'dayjs';
import { database } from '../../../service/database';
import { getWorldName } from '../../../shared/utils';
export function useInstanceActivityData() {
const activityData = ref([]);
const activityDetailData = ref([]);
const allDateOfActivity = ref(new Set());
const worldNameArray = ref([]);
async function getAllDateOfActivity() {
const utcDateStrings =
(await database.getDateOfInstanceActivity()) || [];
const uniqueDates = new Set();
for (const utcString of utcDateStrings) {
const formattedDate = dayjs
.utc(utcString)
.tz()
.format('YYYY-MM-DD');
uniqueDates.add(formattedDate);
}
allDateOfActivity.value = uniqueDates;
}
async function getWorldNameData() {
worldNameArray.value = await Promise.all(
activityData.value.map(async (item) => {
try {
return await getWorldName(item.location);
} catch {
console.error(
'getWorldName failed location',
item.location
);
return 'Unknown world';
}
})
);
}
async function getActivityData(
selectedDate,
currentUser,
friends,
localFavoriteFriends,
onActivityDetailReady
) {
const localStartDate = dayjs
.tz(selectedDate.value)
.startOf('day')
.toISOString();
const localEndDate = dayjs
.tz(selectedDate.value)
.endOf('day')
.toISOString();
const dbData = await database.getInstanceActivity(
localStartDate,
localEndDate
);
const transformData = (item) => ({
...item,
joinTime: dayjs(item.created_at).subtract(item.time, 'millisecond'),
leaveTime: dayjs(item.created_at),
time: item.time < 0 ? 0 : item.time,
isFriend:
item.user_id === currentUser.value.id
? null
: friends.value.has(item.user_id),
isFavorite:
item.user_id === currentUser.value.id
? null
: localFavoriteFriends.value.has(item.user_id)
});
activityData.value = dbData.currentUserData.map(transformData);
const transformAndSort = (arr) => {
return arr.map(transformData).sort((a, b) => {
const timeDiff = Math.abs(
a.joinTime.diff(b.joinTime, 'second')
);
// recording delay, under 3s is considered the same time entry, beautify the chart
return timeDiff < 3
? a.leaveTime - b.leaveTime
: a.joinTime - b.joinTime;
});
};
const filterByLocation = (innerArray, locationSet) => {
return innerArray.every((innerObject) =>
locationSet.has(innerObject.location)
);
};
const locationSet = new Set(
activityData.value.map((item) => item.location)
);
const preSplitActivityDetailData = Array.from(
dbData.detailData.values()
)
.map(transformAndSort)
.filter((innerArray) => filterByLocation(innerArray, locationSet));
activityDetailData.value = handleSplitActivityDetailData(
preSplitActivityDetailData,
currentUser.value.id
);
if (activityDetailData.value.length && onActivityDetailReady) {
nextTick(() => {
onActivityDetailReady();
});
}
}
function handleSplitActivityDetailData(activityDetailData, currentUserId) {
function countTargetIdOccurrences(innerArray, targetId) {
let count = 0;
for (const obj of innerArray) {
if (obj.user_id === targetId) {
count++;
}
}
return count;
}
function areIntervalsOverlapping(objA, objB) {
const isObj1EndTimeBeforeObj2StartTime = objA.leaveTime.isBefore(
objB.joinTime,
'second'
);
const isObj2EndTimeBeforeObj1StartTime = objB.leaveTime.isBefore(
objA.joinTime,
'second'
);
return !(
isObj1EndTimeBeforeObj2StartTime ||
isObj2EndTimeBeforeObj1StartTime
);
}
function buildOverlapGraph(innerArray) {
const numObjects = innerArray.length;
const adjacencyList = Array.from({ length: numObjects }, () => []);
for (let i = 0; i < numObjects; i++) {
for (let j = i + 1; j < numObjects; j++) {
if (areIntervalsOverlapping(innerArray[i], innerArray[j])) {
adjacencyList[i].push(j);
adjacencyList[j].push(i);
}
}
}
return adjacencyList;
}
function depthFirstSearch(nodeIndex, visited, graph, component) {
visited[nodeIndex] = true;
component.push(nodeIndex);
for (const neighborIndex of graph[nodeIndex]) {
if (!visited[neighborIndex]) {
depthFirstSearch(neighborIndex, visited, graph, component);
}
}
}
function findConnectedComponents(graph, numNodes) {
const visited = new Array(numNodes).fill(false);
const components = [];
for (let i = 0; i < numNodes; i++) {
if (!visited[i]) {
const component = [];
depthFirstSearch(i, visited, graph, component);
components.push(component);
}
}
return components;
}
function processOuterArrayWithTargetId(outerArray, targetId) {
let i = 0;
while (i < outerArray.length) {
let currentInnerArray = outerArray[i];
let targetIdCount = countTargetIdOccurrences(
currentInnerArray,
targetId
);
if (targetIdCount > 1) {
let graph = buildOverlapGraph(currentInnerArray);
let connectedComponents = findConnectedComponents(
graph,
currentInnerArray.length
);
let newInnerArrays = connectedComponents.map(
(componentIndices) => {
return componentIndices.map(
(index) => currentInnerArray[index]
);
}
);
outerArray.splice(i, 1, ...newInnerArrays);
i += newInnerArrays.length;
} else {
i += 1;
}
}
return outerArray.sort((a, b) => a[0].joinTime - b[0].joinTime);
}
return processOuterArrayWithTargetId(activityDetailData, currentUserId);
}
return {
activityData,
activityDetailData,
allDateOfActivity,
worldNameArray,
getAllDateOfActivity,
getWorldNameData,
getActivityData
};
}
@@ -0,0 +1,105 @@
import { ref, nextTick } from 'vue';
import configRepository from '../../../service/config';
export function useInstanceActivitySettings() {
const barWidth = ref(25);
const isDetailVisible = ref(true);
const isSoloInstanceVisible = ref(true);
const isNoFriendInstanceVisible = ref(true);
async function initializeSettings() {
try {
const [
barWidthValue,
isDetailVisibleValue,
isSoloInstanceVisibleValue,
isNoFriendInstanceVisibleValue
] = await Promise.all([
configRepository.getInt('VRCX_InstanceActivityBarWidth', 25),
configRepository.getBool(
'VRCX_InstanceActivityDetailVisible',
true
),
configRepository.getBool(
'VRCX_InstanceActivitySoloInstanceVisible',
true
),
configRepository.getBool(
'VRCX_InstanceActivityNoFriendInstanceVisible',
true
)
]);
barWidth.value = barWidthValue;
isDetailVisible.value = isDetailVisibleValue;
isSoloInstanceVisible.value = isSoloInstanceVisibleValue;
isNoFriendInstanceVisible.value = isNoFriendInstanceVisibleValue;
} catch (error) {
console.error('Failed to initialize settings:', error);
}
}
function changeBarWidth(value, onSettingsChange) {
barWidth.value = value;
configRepository
.setInt('VRCX_InstanceActivityBarWidth', value)
.finally(() => {
if (onSettingsChange) onSettingsChange();
});
}
function changeIsDetailInstanceVisible(value, onSettingsChange) {
isDetailVisible.value = value;
configRepository
.setBool('VRCX_InstanceActivityDetailVisible', value)
.finally(() => {
if (onSettingsChange) onSettingsChange();
});
}
function changeIsSoloInstanceVisible(value, onSettingsChange) {
isSoloInstanceVisible.value = value;
configRepository
.setBool('VRCX_InstanceActivitySoloInstanceVisible', value)
.finally(() => {
if (onSettingsChange) onSettingsChange();
});
}
function changeIsNoFriendInstanceVisible(value, onSettingsChange) {
isNoFriendInstanceVisible.value = value;
configRepository
.setBool('VRCX_InstanceActivityNoFriendInstanceVisible', value)
.finally(() => {
if (onSettingsChange) onSettingsChange();
});
}
function handleChangeSettings(activityDetailChartRef) {
nextTick(() => {
if (activityDetailChartRef.value) {
activityDetailChartRef.value.forEach((child) => {
requestAnimationFrame(() => {
if (child.echartsInstance) {
child.initEcharts();
}
});
});
}
});
}
return {
barWidth,
isDetailVisible,
isSoloInstanceVisible,
isNoFriendInstanceVisible,
initializeSettings,
changeBarWidth,
changeIsDetailInstanceVisible,
changeIsSoloInstanceVisible,
changeIsNoFriendInstanceVisible,
handleChangeSettings
};
}
@@ -0,0 +1,44 @@
import { ref } from 'vue';
export function useIntersectionObserver() {
const intersectionObservers = ref([]);
// intersection observer - start
function clearIntersectionObservers() {
intersectionObservers.value.forEach((observer) => {
if (observer) {
observer.disconnect();
}
});
intersectionObservers.value = [];
}
function handleIntersectionObserver(activityDetailChartRef) {
clearIntersectionObservers();
activityDetailChartRef.value?.forEach((child, index) => {
const observer = new IntersectionObserver((entries) =>
handleIntersection(index, entries, activityDetailChartRef)
);
observer.observe(child.$el);
intersectionObservers.value[index] = observer;
});
}
function handleIntersection(index, entries, activityDetailChartRef) {
if (!entries) {
console.error('handleIntersection failed');
return;
}
entries.forEach((entry) => {
if (entry.isIntersecting && activityDetailChartRef.value[index]) {
activityDetailChartRef.value[index].initEcharts();
intersectionObservers.value[index].unobserve(entry.target);
}
});
}
// intersection observer - end
return {
handleIntersectionObserver
};
}
+42 -48
View File
@@ -14,15 +14,12 @@
<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_favorites_tooltip')"
:disabled="hideTooltips">
<el-tooltip placement="bottom" :content="t('view.favorite.refresh_favorites_tooltip')">
<el-button
type="default"
:loading="isFavoriteLoading"
size="small"
icon="el-icon-refresh"
:icon="Refresh"
circle
@click="
refreshFavorites();
@@ -33,13 +30,11 @@
<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
:hide-tooltips="hideTooltips"
:edit-favorites-mode="editFavoritesMode"
@change-favorite-group-name="changeFavoriteGroupName" />
</el-tab-pane>
<el-tab-pane name="world" :label="t('view.favorite.worlds.header')" lazy>
<FavoritesWorldTab
:hide-tooltips="hideTooltips"
:edit-favorites-mode="editFavoritesMode"
:refresh-local-world-favorites="refreshLocalWorldFavorites"
@change-favorite-group-name="changeFavoriteGroupName"
@@ -47,7 +42,6 @@
</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"
@@ -58,19 +52,19 @@
</template>
<script setup>
import { ref, getCurrentInstance } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Refresh } from '@element-plus/icons-vue';
import { ref } from 'vue';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import * as workerTimers from 'worker-timers';
import { avatarRequest, favoriteRequest, worldRequest } from '../../api';
import { useAppearanceSettingsStore, useFavoriteStore, useUiStore, useAvatarStore } from '../../stores';
import { useFavoriteStore, useUiStore, useAvatarStore } from '../../stores';
import FavoritesAvatarTab from './components/FavoritesAvatarTab.vue';
import FavoritesFriendTab from './components/FavoritesFriendTab.vue';
import FavoritesWorldTab from './components/FavoritesWorldTab.vue';
const { t } = useI18n();
const { proxy } = getCurrentInstance();
const { hideTooltips } = storeToRefs(useAppearanceSettingsStore());
const {
favoriteFriends,
favoriteWorlds,
@@ -120,21 +114,22 @@
if (elementsTicked.length === 0) {
return;
}
proxy.$confirm(
ElMessageBox.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);
}
}
type: 'info'
}
);
)
.then((action) => {
if (action === 'confirm') {
bulkUnfavoriteSelection(elementsTicked);
}
})
.catch(() => {});
}
function bulkUnfavoriteSelection(elementsTicked) {
@@ -146,7 +141,7 @@
editFavoritesMode.value = false;
}
function changeFavoriteGroupName(ctx) {
proxy.$prompt(
ElMessageBox.prompt(
t('prompt.change_favorite_group_name.description'),
t('prompt.change_favorite_group_name.header'),
{
@@ -156,33 +151,32 @@
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();
});
}
}
inputErrorMessage: t('prompt.change_favorite_group_name.input_error')
}
);
)
.then(({ value }) => {
favoriteRequest
.saveFavoriteGroup({
type: ctx.type,
group: ctx.name,
displayName: value
})
.then((args) => {
handleFavoriteGroup({
json: args.json,
params: {
favoriteGroupId: args.json.id
}
});
ElMessage({
message: t('prompt.change_favorite_group_name.message.success'),
type: 'success'
});
// load new group name
refreshFavoriteGroups();
});
})
.catch(() => {});
}
function handleBulkCopyFavoriteSelection() {
@@ -3,32 +3,34 @@
<div class="x-friend-item">
<template v-if="isLocalFavorite ? favorite.name : favorite.ref">
<div class="avatar">
<img v-lazy="smallThumbnail" />
<img :src="smallThumbnail" loading="lazy" />
</div>
<div class="detail">
<span class="name" v-text="localFavFakeRef.name"></span>
<span class="extra" v-text="localFavFakeRef.authorName"></span>
</div>
<template v-if="editFavoritesMode">
<el-dropdown trigger="click" size="mini" style="margin-left: 5px" @click.native.stop>
<el-tooltip placement="top" :content="tooltipContent" :disabled="hideTooltips">
<el-button type="default" icon="el-icon-back" size="mini" circle></el-button>
</el-tooltip>
<el-dropdown-menu slot="dropdown">
<template
v-for="groupAPI in favoriteAvatarGroups"
v-if="isLocalFavorite || groupAPI.name !== group.name">
<el-dropdown-item
:key="groupAPI.name"
style="display: block; margin: 10px 0"
:disabled="groupAPI.count >= groupAPI.capacity"
@click.native="handleDropdownItemClick(groupAPI)">
{{ groupAPI.displayName }} ({{ groupAPI.count }} / {{ groupAPI.capacity }})
</el-dropdown-item>
</template>
</el-dropdown-menu>
<el-dropdown trigger="click" size="small" style="margin-left: 5px">
<div>
<el-tooltip placement="top" :content="tooltipContent">
<el-button type="default" :icon="Back" size="small" circle></el-button>
</el-tooltip>
</div>
<template #dropdown>
<el-dropdown-menu>
<template v-for="groupAPI in favoriteAvatarGroups" :key="groupAPI.name">
<el-dropdown-item
v-if="isLocalFavorite || groupAPI.name !== group.name"
style="display: block; margin: 10px 0"
:disabled="groupAPI.count >= groupAPI.capacity"
@click="handleDropdownItemClick(groupAPI)">
{{ groupAPI.displayName }} ({{ groupAPI.count }} / {{ groupAPI.capacity }})
</el-dropdown-item>
</template>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button v-if="!isLocalFavorite" type="text" size="mini" style="margin-left: 5px" @click.stop>
<el-button v-if="!isLocalFavorite" type="text" size="small" style="margin-left: 5px" @click.stop>
<el-checkbox v-model="isSelected"></el-checkbox>
</el-button>
</template>
@@ -37,79 +39,68 @@
v-if="favorite.deleted"
placement="left"
:content="t('view.favorite.unavailable_tooltip')">
<i class="el-icon-warning" style="color: #f56c6c; margin-left: 5px"></i>
<el-icon><Warning /></el-icon>
</el-tooltip>
<el-tooltip
v-if="favorite.ref.releaseStatus === 'private'"
placement="left"
:content="t('view.favorite.private')">
<i class="el-icon-warning" style="color: #e6a23c; margin-left: 5px"></i>
<el-icon><Warning /></el-icon>
</el-tooltip>
<el-tooltip
v-if="favorite.ref.releaseStatus !== 'private' && !favorite.deleted"
placement="left"
:content="t('view.favorite.select_avatar_tooltip')"
:disabled="hideTooltips">
:content="t('view.favorite.select_avatar_tooltip')">
<el-button
:disabled="currentUser.currentAvatar === favorite.id"
size="mini"
icon="el-icon-check"
size="small"
:icon="Check"
circle
style="margin-left: 5px"
@click.stop="selectAvatarWithConfirmation(favorite.id)"></el-button>
</el-tooltip>
<el-tooltip
placement="right"
:content="t('view.favorite.unfavorite_tooltip')"
:disabled="hideTooltips">
<el-tooltip placement="right" :content="t('view.favorite.unfavorite_tooltip')">
<el-button
v-if="shiftHeld"
size="mini"
icon="el-icon-close"
size="small"
:icon="Close"
circle
style="color: #f56c6c; margin-left: 5px"
@click.stop="deleteFavorite(favorite.id)"></el-button>
<el-button
v-else
type="default"
icon="el-icon-star-on"
size="mini"
:icon="Star"
size="small"
circle
style="margin-left: 5px"
@click.stop="showFavoriteDialog('avatar', favorite.id)"></el-button>
</el-tooltip>
</template>
<template v-else>
<el-tooltip
placement="left"
:content="t('view.favorite.select_avatar_tooltip')"
:disabled="hideTooltips">
<el-tooltip placement="left" :content="t('view.favorite.select_avatar_tooltip')">
<el-button
:disabled="currentUser.currentAvatar === favorite.id"
size="mini"
size="small"
circle
style="margin-left: 5px"
icon="el-icon-check"
:icon="Check"
@click.stop="selectAvatarWithConfirmation(favorite.id)" />
</el-tooltip>
</template>
<el-tooltip
v-if="isLocalFavorite"
placement="right"
:content="t('view.favorite.unfavorite_tooltip')"
:disabled="hideTooltips">
<el-tooltip v-if="isLocalFavorite" placement="right" :content="t('view.favorite.unfavorite_tooltip')">
<el-button
v-if="shiftHeld"
size="mini"
icon="el-icon-close"
size="small"
:icon="Close"
circle
style="color: #f56c6c; margin-left: 5px"
@click.stop="removeLocalAvatarFavorite(favorite.id, favoriteGroupName)" />
<el-button
v-else
type="default"
icon="el-icon-star-on"
size="mini"
:icon="Star"
size="small"
circle
style="margin-left: 5px"
@click.stop="showFavoriteDialog('avatar', favorite.id)"
@@ -123,15 +114,15 @@
<el-button
v-if="isLocalFavorite"
type="text"
icon="el-icon-close"
size="mini"
:icon="Close"
size="small"
style="margin-left: 5px"
@click.stop="removeLocalAvatarFavorite(favorite.id, favoriteGroupName)"></el-button>
<el-button
v-else
type="text"
icon="el-icon-close"
size="mini"
:icon="Close"
size="small"
style="margin-left: 5px"
@click.stop="deleteFavorite(favorite.id)"></el-button>
</template>
@@ -140,18 +131,13 @@
</template>
<script setup>
import { ElMessage } from 'element-plus';
import { Warning, Back, Check, Close, Star } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { favoriteRequest } from '../../../api';
import { $app } from '../../../app';
import {
useAppearanceSettingsStore,
useAvatarStore,
useFavoriteStore,
useUiStore,
useUserStore
} from '../../../stores';
import { useAvatarStore, useFavoriteStore, useUiStore, useUserStore } from '../../../stores';
const props = defineProps({
favorite: Object,
@@ -163,7 +149,6 @@
const { t } = useI18n();
const { hideTooltips } = storeToRefs(useAppearanceSettingsStore());
const { favoriteAvatarGroups } = storeToRefs(useFavoriteStore());
const { removeLocalAvatarFavorite, showFavoriteDialog } = useFavoriteStore();
const { selectAvatarWithConfirmation } = useAvatarStore();
@@ -211,7 +196,7 @@
tags: groupAPI.name
})
.then((args) => {
$app.$message({
ElMessage({
message: 'Avatar added to favorites',
type: 'success'
});
@@ -2,38 +2,38 @@
<div @click="$emit('click')">
<div class="x-friend-item">
<div class="avatar">
<img v-lazy="smallThumbnail" />
<img :src="smallThumbnail" loading="lazy" />
</div>
<div class="detail">
<span class="name" v-text="favorite.name"></span>
<span class="extra" v-text="favorite.authorName"></span>
</div>
<el-tooltip placement="left" :content="t('view.favorite.select_avatar_tooltip')" :disabled="hideTooltips">
<el-tooltip placement="left" :content="t('view.favorite.select_avatar_tooltip')">
<el-button
:disabled="currentUser.currentAvatar === favorite.id"
size="mini"
icon="el-icon-check"
size="small"
:icon="Check"
circle
style="margin-left: 5px"
@click.stop="selectAvatarWithConfirmation(favorite.id)"></el-button>
</el-tooltip>
<template v-if="cachedFavoritesByObjectId.has(favorite.id)">
<el-tooltip placement="right" content="Favorite" :disabled="hideTooltips">
<el-tooltip placement="right" content="Favorite">
<el-button
type="default"
icon="el-icon-star-on"
size="mini"
:icon="Star"
size="small"
circle
style="margin-left: 5px"
@click.stop="showFavoriteDialog('avatar', favorite.id)"></el-button>
</el-tooltip>
</template>
<template v-else>
<el-tooltip placement="right" content="Favorite" :disabled="hideTooltips">
<el-tooltip placement="right" content="Favorite">
<el-button
type="default"
icon="el-icon-star-off"
size="mini"
:icon="StarFilled"
size="small"
circle
style="margin-left: 5px"
@click.stop="showFavoriteDialog('avatar', favorite.id)"></el-button>
@@ -44,14 +44,14 @@
</template>
<script setup>
import { Check, Star, StarFilled } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useAppearanceSettingsStore, useAvatarStore, useFavoriteStore, useUserStore } from '../../../stores';
import { useI18n } from 'vue-i18n';
import { useAvatarStore, useFavoriteStore, useUserStore } from '../../../stores';
const { t } = useI18n();
const { hideTooltips } = storeToRefs(useAppearanceSettingsStore());
const { cachedFavoritesByObjectId } = storeToRefs(useFavoriteStore());
const { showFavoriteDialog } = useFavoriteStore();
const { selectAvatarWithConfirmation } = useAvatarStore();
@@ -64,6 +64,8 @@
}
});
defineEmits(['click']);
const smallThumbnail = computed(() => {
return props.favorite.thumbnailImageUrl.replace('256', '128') || props.favorite.thumbnailImageUrl;
});
@@ -24,7 +24,7 @@
<el-input
v-model="avatarFavoriteSearch"
clearable
size="mini"
size="small"
:placeholder="t('view.favorite.avatars.search')"
style="width: 200px"
@input="searchAvatarFavorites" />
@@ -39,7 +39,7 @@
<div class="x-friend-item">
<template v-if="favorite.name">
<div class="avatar">
<img v-lazy="favorite.thumbnailImageUrl" />
<img :src="favorite.thumbnailImageUrl" loading="lazy" />
</div>
<div class="detail">
<span class="name" v-text="favorite.name" />
@@ -60,23 +60,23 @@
</span>
<el-collapse style="border: 0">
<el-collapse-item v-for="group in favoriteAvatarGroups" :key="group.name">
<template slot="title">
<template #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')">
<el-button
size="mini"
icon="el-icon-edit"
size="small"
:icon="Edit"
circle
style="margin-left: 10px"
@click.stop="changeFavoriteGroupName(group)" />
</el-tooltip>
<el-tooltip placement="right" :content="t('view.favorite.clear_tooltip')" :disabled="hideTooltips">
<el-tooltip placement="right" :content="t('view.favorite.clear_tooltip')">
<el-button
size="mini"
icon="el-icon-delete"
size="small"
:icon="Delete"
circle
style="margin-left: 5px"
@click.stop="clearFavoriteGroup(group)" />
@@ -88,7 +88,6 @@
:key="favorite.id"
:favorite="favorite"
:group="group"
:hide-tooltips="hideTooltips"
:edit-favorites-mode="editFavoritesMode"
style="display: inline-block; width: 300px; margin-right: 15px"
@handle-select="favorite.$selected = $event"
@@ -108,15 +107,15 @@
</div>
</el-collapse-item>
<el-collapse-item>
<template slot="title">
<template #title>
<span style="font-weight: bold; font-size: 14px; margin-left: 10px">Local History</span>
<span style="color: #909399; font-size: 12px; margin-left: 10px"
>{{ avatarHistoryArray.length }}/100</span
>
<el-tooltip placement="right" content="Clear" :disabled="hideTooltips">
<el-tooltip placement="right" content="Clear">
<el-button
size="mini"
icon="el-icon-delete"
size="small"
:icon="Delete"
circle
style="margin-left: 5px"
@click.stop="promptClearAvatarHistory"></el-button>
@@ -128,7 +127,6 @@
:key="favorite.id"
style="display: inline-block; width: 300px; margin-right: 15px"
:favorite="favorite"
:hide-tooltips="hideTooltips"
@click="showAvatarDialog(favorite.id)" />
</div>
<div
@@ -157,30 +155,27 @@
{{ 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>
<el-icon class="is-loading"><Loading /></el-icon>
<span>{{ t('view.favorite.avatars.cancel_refresh') }}</span>
</el-button>
<el-collapse-item
v-for="group in localAvatarFavoriteGroups"
v-if="localAvatarFavorites[group]"
:key="group">
<template slot="title">
<el-collapse-item v-for="group in localAvatarFavoriteGroups" :key="group">
<template #title v-if="localAvatarFavorites[group]">
<span :style="{ fontWeight: 'bold', fontSize: '14px', marginLeft: '10px' }">{{ group }}</span>
<span :style="{ color: '#909399', fontSize: '12px', marginLeft: '10px' }">{{
getLocalAvatarFavoriteGroupLength(group)
}}</span>
<el-tooltip placement="top" :content="t('view.favorite.rename_tooltip')" :disabled="hideTooltips">
<el-tooltip placement="top" :content="t('view.favorite.rename_tooltip')">
<el-button
size="mini"
icon="el-icon-edit"
size="small"
:icon="Edit"
circle
:style="{ marginLeft: '5px' }"
@click.stop="promptLocalAvatarFavoriteGroupRename(group)"></el-button>
</el-tooltip>
<el-tooltip placement="right" :content="t('view.favorite.delete_tooltip')" :disabled="hideTooltips">
<el-tooltip placement="right" :content="t('view.favorite.delete_tooltip')">
<el-button
size="mini"
icon="el-icon-delete"
size="small"
:icon="Delete"
circle
:style="{ marginLeft: '5px' }"
@click.stop="promptLocalAvatarFavoriteGroupDelete(group)"></el-button>
@@ -194,7 +189,6 @@
:style="{ display: 'inline-block', width: '300px', marginRight: '15px' }"
:favorite="favorite"
:group="group"
:hide-tooltips="hideTooltips"
:edit-favorites-mode="editFavoritesMode"
@handle-select="favorite.$selected = $event"
@click="showAvatarDialog(favorite.id)" />
@@ -213,14 +207,17 @@
</div>
</el-collapse-item>
</el-collapse>
<AvatarExportDialog :avatar-export-dialog-visible.sync="avatarExportDialogVisible" />
<AvatarExportDialog v-model:avatarExportDialogVisible="avatarExportDialogVisible" />
</div>
</template>
<script setup>
import { ref, computed, getCurrentInstance } from 'vue';
import { Loading, Edit, Delete } from '@element-plus/icons-vue';
import { ElMessageBox } from 'element-plus';
import { ref, computed } from 'vue';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { favoriteRequest } from '../../../api';
import { useAppearanceSettingsStore, useAvatarStore, useFavoriteStore, useUserStore } from '../../../stores';
import AvatarExportDialog from '../dialogs/AvatarExportDialog.vue';
@@ -238,10 +235,9 @@
}
});
const { proxy } = getCurrentInstance();
const emit = defineEmits(['change-favorite-group-name', 'refresh-local-avatar-favorites']);
const { hideTooltips, sortFavorites } = storeToRefs(useAppearanceSettingsStore());
const { sortFavorites } = storeToRefs(useAppearanceSettingsStore());
const { setSortFavorites } = useAppearanceSettingsStore();
const { favoriteAvatars, favoriteAvatarGroups, localAvatarFavorites, localAvatarFavoriteGroups } =
storeToRefs(useFavoriteStore());
@@ -340,19 +336,20 @@
}
function clearFavoriteGroup(ctx) {
proxy.$confirm('Continue? Clear Group', 'Confirm', {
ElMessageBox.confirm('Continue? Clear Group', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
type: 'info'
})
.then((action) => {
if (action === 'confirm') {
favoriteRequest.clearFavoriteGroup({
type: ctx.type,
group: ctx.name
});
}
}
});
})
.catch(() => {});
}
function showAvatarExportDialog() {
@@ -364,18 +361,23 @@
}
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);
}
ElMessageBox.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')
}
});
)
.then(({ value }) => {
if (value) {
newLocalAvatarFavoriteGroup(value);
}
})
.catch(() => {});
}
function refreshLocalAvatarFavorites() {
@@ -383,7 +385,7 @@
}
function promptLocalAvatarFavoriteGroupRename(group) {
proxy.$prompt(
ElMessageBox.prompt(
t('prompt.local_favorite_group_rename.description'),
t('prompt.local_favorite_group_rename.header'),
{
@@ -392,26 +394,28 @@
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);
}
}
inputValue: group
}
);
)
.then(({ value }) => {
if (value) {
renameLocalAvatarFavoriteGroup(value, group);
}
})
.catch(() => {});
}
function promptLocalAvatarFavoriteGroupDelete(group) {
proxy.$confirm(`Delete Group? ${group}`, 'Confirm', {
ElMessageBox.confirm(`Delete Group? ${group}`, 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
type: 'info'
})
.then((action) => {
if (action === 'confirm') {
deleteLocalAvatarFavoriteGroup(group);
}
}
});
})
.catch(() => {});
}
</script>
@@ -3,7 +3,7 @@
<div class="x-friend-item">
<template v-if="favorite.ref">
<div class="avatar" :class="userStatusClass(favorite.ref)">
<img v-lazy="userImage(favorite.ref, true)" />
<img :src="userImage(favorite.ref, true)" loading="lazy" />
</div>
<div class="detail">
<span
@@ -19,47 +19,44 @@
<span v-else v-text="favorite.ref.statusDescription"></span>
</div>
<template v-if="editFavoritesMode">
<el-dropdown trigger="click" size="mini" style="margin-left: 5px" @click.native.stop>
<el-tooltip
placement="left"
:content="$t('view.favorite.move_tooltip')"
:disabled="hideTooltips">
<el-button type="default" icon="el-icon-back" size="mini" circle></el-button>
</el-tooltip>
<el-dropdown-menu slot="dropdown">
<template v-for="groupAPI in favoriteFriendGroups">
<el-dropdown-item
v-if="groupAPI.name !== group.name"
:key="groupAPI.name"
style="display: block; margin: 10px 0"
:disabled="groupAPI.count >= groupAPI.capacity"
@click.native="moveFavorite(favorite.ref, groupAPI, 'friend')">
{{ groupAPI.displayName }} ({{ groupAPI.count }} / {{ groupAPI.capacity }})
</el-dropdown-item>
</template>
</el-dropdown-menu>
<el-dropdown trigger="click" size="small" style="margin-left: 5px">
<div>
<el-tooltip placement="left" :content="t('view.favorite.move_tooltip')">
<el-button type="default" :icon="Back" size="small" circle></el-button>
</el-tooltip>
</div>
<template #dropdown>
<el-dropdown-menu>
<template v-for="groupAPI in favoriteFriendGroups" :key="groupAPI.name">
<el-dropdown-item
v-if="groupAPI.name !== group.name"
style="display: block; margin: 10px 0"
:disabled="groupAPI.count >= groupAPI.capacity"
@click="moveFavorite(favorite.ref, groupAPI, 'friend')">
{{ groupAPI.displayName }} ({{ groupAPI.count }} / {{ groupAPI.capacity }})
</el-dropdown-item>
</template>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button type="text" size="mini" style="margin-left: 5px" @click.stop>
<el-button type="text" size="small" style="margin-left: 5px" @click.stop>
<el-checkbox v-model="favorite.$selected"></el-checkbox>
</el-button>
</template>
<template v-else>
<el-tooltip
placement="right"
:content="$t('view.favorite.unfavorite_tooltip')"
:disabled="hideTooltips">
<el-tooltip placement="right" :content="t('view.favorite.unfavorite_tooltip')">
<el-button
v-if="shiftHeld"
size="mini"
icon="el-icon-close"
size="small"
:icon="Close"
circle
style="color: #f56c6c; margin-left: 5px"
@click.stop="deleteFavorite(favorite.id)"></el-button>
<el-button
v-else
type="default"
icon="el-icon-star-on"
size="mini"
:icon="Star"
size="small"
circle
style="margin-left: 5px"
@click.stop="showFavoriteDialog('friend', favorite.id)"></el-button>
@@ -73,8 +70,8 @@
</div>
<el-button
type="text"
icon="el-icon-close"
size="mini"
:icon="Close"
size="small"
style="margin-left: 5px"
@click.stop="deleteFavorite(favorite.id)"></el-button>
</template>
@@ -83,10 +80,12 @@
</template>
<script setup>
import { Back, Close, Star } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { favoriteRequest } from '../../../api';
import { userImage, userStatusClass } from '../../../shared/utils';
import { useAppearanceSettingsStore, useFavoriteStore, useUiStore } from '../../../stores';
import { useFavoriteStore, useUiStore } from '../../../stores';
defineProps({
favorite: { type: Object, required: true },
@@ -96,10 +95,10 @@
defineEmits(['click']);
const { hideTooltips } = storeToRefs(useAppearanceSettingsStore());
const { favoriteFriendGroups } = storeToRefs(useFavoriteStore());
const { showFavoriteDialog } = useFavoriteStore();
const { shiftHeld } = storeToRefs(useUiStore());
const { t } = useI18n();
function moveFavorite(ref, group, type) {
favoriteRequest.deleteFavorite({ objectId: ref.id }).then(() => {
@@ -2,45 +2,45 @@
<div>
<div style="display: flex; align-items: center; justify-content: space-between">
<div>
<el-button size="small" @click="showFriendExportDialog">{{ $t('view.favorite.export') }}</el-button>
<el-button size="small" @click="showFriendExportDialog">{{ t('view.favorite.export') }}</el-button>
<el-button size="small" style="margin-left: 5px" @click="showFriendImportDialog">{{
$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') }}</span>
<span class="name" style="margin-right: 5px; line-height: 10px">{{ t('view.favorite.sort_by') }}</span>
<el-radio-group v-model="sortFav" @change="saveSortFavoritesOption">
<el-radio :label="false">{{
$t('view.settings.appearance.appearance.sort_favorite_by_name')
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>
</div>
</div>
<span style="display: block; margin-top: 30px">{{ $t('view.favorite.avatars.vrchat_favorites') }}</span>
<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 favoriteFriendGroups" :key="group.name">
<template slot="title">
<template #title>
<span
style="font-weight: bold; font-size: 14px; margin-left: 10px"
v-text="group.displayName"></span>
<span style="color: #909399; font-size: 12px; margin-left: 10px"
>{{ group.count }}/{{ group.capacity }}</span
>
<el-tooltip placement="top" :content="$t('view.favorite.rename_tooltip')" :disabled="hideTooltips">
<el-tooltip placement="top" :content="t('view.favorite.rename_tooltip')">
<el-button
size="mini"
icon="el-icon-edit"
size="small"
:icon="Edit"
circle
style="margin-left: 10px"
@click.stop="changeFavoriteGroupName(group)"></el-button>
</el-tooltip>
<el-tooltip placement="right" :content="$t('view.favorite.clear_tooltip')" :disabled="hideTooltips">
<el-tooltip placement="right" :content="t('view.favorite.clear_tooltip')">
<el-button
size="mini"
icon="el-icon-delete"
size="small"
:icon="Delete"
circle
style="margin-left: 5px"
@click.stop="clearFavoriteGroup(group)"></el-button>
@@ -70,13 +70,17 @@
</div>
</el-collapse-item>
</el-collapse>
<FriendExportDialog :friend-export-dialog-visible.sync="friendExportDialogVisible" />
<FriendExportDialog v-model:friendExportDialogVisible="friendExportDialogVisible" />
</div>
</template>
<script setup>
import { ref, getCurrentInstance, computed } from 'vue';
import { Edit, Delete } from '@element-plus/icons-vue';
import { ElMessageBox } from 'element-plus';
import { ref, computed } from 'vue';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { favoriteRequest } from '../../../api';
import { useAppearanceSettingsStore, useFavoriteStore, useUserStore } from '../../../stores';
import FriendExportDialog from '../dialogs/FriendExportDialog.vue';
@@ -91,13 +95,12 @@
const emit = defineEmits(['change-favorite-group-name']);
const { proxy } = getCurrentInstance();
const { hideTooltips, sortFavorites } = storeToRefs(useAppearanceSettingsStore());
const { sortFavorites } = storeToRefs(useAppearanceSettingsStore());
const { setSortFavorites } = useAppearanceSettingsStore();
const { showUserDialog } = useUserStore();
const { favoriteFriendGroups, groupedByGroupKeyFavoriteFriends } = storeToRefs(useFavoriteStore());
const { showFriendImportDialog, saveSortFavoritesOption } = useFavoriteStore();
const { t } = useI18n();
const friendExportDialogVisible = ref(false);
@@ -115,19 +118,20 @@
}
function clearFavoriteGroup(ctx) {
proxy.$confirm('Continue? Clear Group', 'Confirm', {
ElMessageBox.confirm('Continue? Clear Group', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
type: 'info'
})
.then((action) => {
if (action === 'confirm') {
favoriteRequest.clearFavoriteGroup({
type: ctx.type,
group: ctx.name
});
}
}
});
})
.catch(() => {});
}
function changeFavoriteGroupName(group) {
@@ -1,57 +1,60 @@
<template>
<div @click="$emit('click')" :style="{ display: 'inline-block', width: '300px', marginRight: '15px' }">
<div class="fav-world-item" @click="$emit('click')">
<div class="x-friend-item">
<template v-if="isLocalFavorite ? favorite.name : favorite.ref">
<div class="avatar">
<img v-lazy="smallThumbnail" />
<div class="avatar" v-once>
<img :src="smallThumbnail" loading="lazy" decoding="async" fetchpriority="low" />
</div>
<div class="detail">
<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" v-once>{{ localFavFakeRef.authorName }}</span>
<div class="detail" v-once>
<span class="name">{{ localFavFakeRef.name }}</span>
<span v-if="localFavFakeRef.occupants" class="extra">
{{ localFavFakeRef.authorName }} ({{ localFavFakeRef.occupants }})
</span>
<span v-else class="extra">{{ localFavFakeRef.authorName }}</span>
</div>
<template v-if="editFavoritesMode">
<el-dropdown trigger="click" size="mini" style="margin-left: 5px" @click.native.stop>
<el-tooltip
placement="left"
:content="$t(localFavFakeRef ? 'view.favorite.copy_tooltip' : 'view.favorite.move_tooltip')"
:disabled="hideTooltips">
<el-button type="default" icon="el-icon-back" size="mini" circle></el-button>
</el-tooltip>
<el-dropdown-menu slot="dropdown">
<template v-for="groupAPI in favoriteWorldGroups">
<el-dropdown-item
v-if="isLocalFavorite || groupAPI.name !== group.name"
:key="groupAPI.name"
style="display: block; margin: 10px 0"
:disabled="groupAPI.count >= groupAPI.capacity"
@click.native="handleDropdownItemClick(groupAPI)">
{{ groupAPI.displayName }} ({{ groupAPI.count }} / {{ groupAPI.capacity }})
</el-dropdown-item>
</template>
</el-dropdown-menu>
<el-button v-if="!isLocalFavorite" type="text" size="mini" @click.stop style="margin-left: 5px">
<el-checkbox v-model="isSelected"></el-checkbox>
</el-button>
<el-dropdown trigger="click" size="small" style="margin-left: 5px">
<div>
<el-tooltip
placement="left"
:content="
t(localFavFakeRef ? 'view.favorite.copy_tooltip' : 'view.favorite.move_tooltip')
">
<el-button type="default" :icon="Back" size="small" circle></el-button>
</el-tooltip>
</div>
<template #dropdown>
<el-dropdown-menu>
<template v-for="groupAPI in favoriteWorldGroups" :key="groupAPI.name">
<el-dropdown-item
v-if="isLocalFavorite || groupAPI.name !== group.name"
style="display: block; margin: 10px 0"
:disabled="groupAPI.count >= groupAPI.capacity"
@click="handleDropdownItemClick(groupAPI)">
{{ groupAPI.displayName }} ({{ groupAPI.count }} / {{ groupAPI.capacity }})
</el-dropdown-item>
</template>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button v-if="!isLocalFavorite" type="text" size="small" @click.stop style="margin-left: 5px">
<el-checkbox v-model="isSelected"></el-checkbox>
</el-button>
</template>
<template v-else>
<el-tooltip
v-if="!isLocalFavorite && favorite.deleted"
placement="left"
:content="$t('view.favorite.unavailable_tooltip')">
<i class="el-icon-warning" style="color: #f56c6c; margin-left: 5px"></i>
:content="t('view.favorite.unavailable_tooltip')">
<el-icon><Warning /></el-icon>
</el-tooltip>
<el-tooltip
v-if="!isLocalFavorite && favorite.ref.releaseStatus === 'private'"
placement="left"
:content="$t('view.favorite.private')">
<i class="el-icon-warning" style="color: #e6a23c; margin-left: 5px"></i>
:content="t('view.favorite.private')">
<el-icon><Warning /></el-icon>
</el-tooltip>
<el-tooltip placement="left" :disabled="hideTooltips">
<el-tooltip placement="left">
<template #content>
{{
canOpenInstanceInGame()
@@ -60,8 +63,8 @@
}}
</template>
<el-button
size="mini"
icon="el-icon-message"
size="small"
:icon="Message"
style="margin-left: 5px"
@click.stop="newInstanceSelfInvite(favorite.id)"
circle></el-button>
@@ -69,41 +72,36 @@
<el-tooltip
v-if="!isLocalFavorite"
placement="right"
:content="$t('view.favorite.unfavorite_tooltip')"
:disabled="hideTooltips">
:content="t('view.favorite.unfavorite_tooltip')">
<el-button
v-if="shiftHeld"
size="mini"
icon="el-icon-close"
size="small"
:icon="Close"
circle
style="color: #f56c6c; margin-left: 5px"
@click.stop="deleteFavorite(favorite.id)"></el-button>
<el-button
v-else
icon="el-icon-star-on"
size="mini"
:icon="Star"
size="small"
circle
style="margin-left: 5px"
type="default"
@click.stop="showFavoriteDialog('world', favorite.id)"></el-button>
</el-tooltip>
</template>
<el-tooltip
v-if="isLocalFavorite"
placement="right"
:content="$t('view.favorite.unfavorite_tooltip')"
:disabled="hideTooltips">
<el-tooltip v-if="isLocalFavorite" placement="right" :content="t('view.favorite.unfavorite_tooltip')">
<el-button
v-if="shiftHeld"
size="mini"
icon="el-icon-close"
size="small"
:icon="Close"
circle
style="color: #f56c6c; margin-left: 5px"
@click.stop="$emit('remove-local-world-favorite', favorite.id, group)"></el-button>
<el-button
v-else
icon="el-icon-star-on"
size="mini"
:icon="Star"
size="small"
circle
style="margin-left: 5px"
type="default"
@@ -112,18 +110,18 @@
</template>
<template v-else>
<div class="avatar"></div>
<div class="detail">
<span v-once>{{ favorite.name || favorite.id }}</span>
<div class="detail" v-once>
<span>{{ favorite.name || favorite.id }}</span>
<el-tooltip
v-if="!isLocalFavorite && favorite.deleted"
placement="left"
:content="$t('view.favorite.unavailable_tooltip')">
<i class="el-icon-warning" style="color: #f56c6c; margin-left: 5px"></i>
:content="t('view.favorite.unavailable_tooltip')">
<el-icon><Warning /></el-icon>
</el-tooltip>
<el-button
type="text"
icon="el-icon-close"
size="mini"
:icon="Close"
size="small"
style="margin-left: 5px"
@click.stop="handleDeleteFavorite"></el-button>
</div>
@@ -133,16 +131,13 @@
</template>
<script setup>
import { ElMessage } from 'element-plus';
import { Warning, Back, Message, Close, Star } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
import { computed, getCurrentInstance } from 'vue';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { favoriteRequest } from '../../../api';
import {
useAppearanceSettingsStore,
useFavoriteStore,
useInviteStore,
useUiStore,
useGameStore
} from '../../../stores';
import { useFavoriteStore, useInviteStore, useUiStore } from '../../../stores';
const props = defineProps({
group: [Object, String],
@@ -152,14 +147,11 @@
});
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 { isGameRunning } = storeToRefs(useGameStore());
const { t } = useI18n();
const { canOpenInstanceInGame } = useInviteStore();
const isSelected = computed({
@@ -213,9 +205,17 @@
})
.then((args) => {
if (message) {
proxy.$message({ message: 'World added to favorites', type: 'success' });
ElMessage({ message: 'World added to favorites', type: 'success' });
}
return args;
});
}
</script>
<style scoped>
.fav-world-item {
display: inline-block;
width: 300px;
margin-right: 15px;
}
</style>
@@ -2,26 +2,26 @@
<div>
<div style="display: flex; align-items: center; justify-content: space-between">
<div>
<el-button size="small" @click="showExportDialog">{{ $t('view.favorite.export') }}</el-button>
<el-button size="small" @click="showExportDialog">{{ t('view.favorite.export') }}</el-button>
<el-button size="small" style="margin-left: 5px" @click="showWorldImportDialog">{{
$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') }}</span>
<span class="name" style="margin-right: 5px; line-height: 10px">{{ t('view.favorite.sort_by') }}</span>
<el-radio-group v-model="sortFav" style="margin-right: 12px" @change="saveSortFavoritesOption">
<el-radio :label="false">{{
$t('view.settings.appearance.appearance.sort_favorite_by_name')
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="worldFavoriteSearch"
clearable
size="mini"
:placeholder="$t('view.favorite.worlds.search')"
size="small"
:placeholder="t('view.favorite.worlds.search')"
style="width: 200px"
@input="searchWorldFavorites" />
</div>
@@ -35,7 +35,7 @@
<div class="x-friend-item">
<template v-if="favorite.name">
<div class="avatar">
<img v-lazy="favorite.thumbnailImageUrl" />
<img :src="favorite.thumbnailImageUrl" loading="lazy" />
</div>
<div class="detail">
<span class="name" v-text="favorite.name"></span>
@@ -54,64 +54,58 @@
</div>
</div>
</div>
<span style="display: block; margin-top: 20px">{{ $t('view.favorite.worlds.vrchat_favorites') }}</span>
<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 favoriteWorldGroups" :key="group.name">
<template slot="title">
<template #title>
<div style="display: flex; align-items: center">
<span
style="font-weight: bold; font-size: 14px; margin-left: 10px"
v-text="group.displayName" />
<el-tag
style="margin: 1px 0 0 5px"
size="mini"
size="small"
:type="userFavoriteWorldsStatusForFavTab(group.visibility)"
effect="plain"
>{{ group.visibility.charAt(0).toUpperCase() + group.visibility.slice(1) }}</el-tag
>
<span style="color: #909399; font-size: 12px; margin-left: 10px"
>{{ group.count }}/{{ group.capacity }}</span
>
<el-dropdown trigger="click" size="mini" style="margin-left: 10px" @click.native.stop>
<el-tooltip
placement="top"
:content="$t('view.favorite.visibility_tooltip')"
:disabled="hideTooltips">
<el-button type="default" icon="el-icon-view" size="mini" circle />
</el-tooltip>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item
v-for="visibility in worldGroupVisibilityOptions"
v-if="group.visibility !== visibility"
:key="visibility"
style="display: block; margin: 10px 0"
@click.native="changeWorldGroupVisibility(group.name, visibility)"
>{{ visibility.charAt(0).toUpperCase() + visibility.slice(1) }}</el-dropdown-item
>
</el-dropdown-menu>
<el-tooltip
placement="top"
:content="$t('view.favorite.rename_tooltip')"
:disabled="hideTooltips">
<el-button
size="mini"
icon="el-icon-edit"
circle
style="margin-left: 5px"
@click.stop="changeFavoriteGroupName(group)" />
</el-tooltip>
<el-tooltip
placement="right"
:content="$t('view.favorite.clear_tooltip')"
:disabled="hideTooltips">
<el-button
size="mini"
icon="el-icon-delete"
circle
style="margin-left: 5px"
@click.stop="clearFavoriteGroup(group)" />
</el-tooltip>
</el-dropdown>
><el-tooltip placement="top" :content="t('view.favorite.visibility_tooltip')">
<el-dropdown trigger="click" size="small" style="margin-left: 10px">
<el-button type="default" :icon="View" size="small" circle @click.stop />
<template #dropdown>
<el-dropdown-menu>
<template v-for="visibility in worldGroupVisibilityOptions" :key="visibility">
<el-dropdown-item
v-if="group.visibility !== visibility"
style="display: block; margin: 10px 0"
@click="changeWorldGroupVisibility(group.name, visibility)"
>{{
visibility.charAt(0).toUpperCase() + visibility.slice(1)
}}</el-dropdown-item
>
</template>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-tooltip>
<el-tooltip placement="top" :content="t('view.favorite.rename_tooltip')">
<el-button
size="small"
:icon="Edit"
circle
style="margin-left: 5px"
@click.stop="changeFavoriteGroupName(group)" />
</el-tooltip>
<el-tooltip placement="right" :content="t('view.favorite.clear_tooltip')">
<el-button
size="small"
:icon="Delete"
circle
style="margin-left: 5px"
@click.stop="clearFavoriteGroup(group)" />
</el-tooltip>
</div>
</template>
<div v-if="group.count" class="x-friend-list" style="margin-top: 10px">
@@ -121,7 +115,6 @@
:group="group"
:favorite="favorite"
:edit-favorites-mode="editFavoritesMode"
:hide-tooltips="hideTooltips"
@click="showWorldDialog(favorite.id)"
@handle-select="favorite.$selected = $event" />
</div>
@@ -139,44 +132,41 @@
</div>
</el-collapse-item>
</el-collapse>
<span style="display: block; margin-top: 20px">{{ $t('view.favorite.worlds.local_favorites') }}</span>
<span style="display: block; margin-top: 20px">{{ t('view.favorite.worlds.local_favorites') }}</span>
<br />
<el-button size="small" @click="promptNewLocalWorldFavoriteGroup">{{
$t('view.favorite.worlds.new_group')
t('view.favorite.worlds.new_group')
}}</el-button>
<el-button
v-if="!refreshingLocalFavorites"
size="small"
style="margin-left: 5px"
@click="refreshLocalWorldFavorite"
>{{ $t('view.favorite.worlds.refresh') }}</el-button
>{{ t('view.favorite.worlds.refresh') }}</el-button
>
<el-button v-else size="small" style="margin-left: 5px" @click="refreshingLocalFavorites = false">
<i class="el-icon-loading" style="margin-right: 5px" />
<span>{{ $t('view.favorite.worlds.cancel_refresh') }}</span>
<el-icon style="margin-right: 5px"><Loading /></el-icon>
<span>{{ t('view.favorite.worlds.cancel_refresh') }}</span>
</el-button>
<el-collapse style="border: 0">
<el-collapse-item v-for="group in localWorldFavoriteGroups" v-if="localWorldFavorites[group]" :key="group">
<template slot="title">
<el-collapse-item v-for="group in localWorldFavoriteGroups" :key="group">
<template #title v-if="localWorldFavorites[group]">
<span style="font-weight: bold; font-size: 14px; margin-left: 10px" v-text="group" />
<span style="color: #909399; font-size: 12px; margin-left: 10px">{{
getLocalWorldFavoriteGroupLength(group)
}}</span>
<el-tooltip placement="top" :content="$t('view.favorite.rename_tooltip')" :disabled="hideTooltips">
<el-tooltip placement="top" :content="t('view.favorite.rename_tooltip')">
<el-button
size="mini"
icon="el-icon-edit"
size="small"
:icon="Edit"
circle
style="margin-left: 10px"
@click.stop="promptLocalWorldFavoriteGroupRename(group)" />
</el-tooltip>
<el-tooltip
placement="right"
:content="$t('view.favorite.delete_tooltip')"
:disabled="hideTooltips">
<el-tooltip placement="right" :content="t('view.favorite.delete_tooltip')">
<el-button
size="mini"
icon="el-icon-delete"
size="small"
:icon="Delete"
circle
style="margin-left: 5px"
@click.stop="promptLocalWorldFavoriteGroupDelete(group)" />
@@ -190,7 +180,6 @@
:group="group"
:favorite="favorite"
:edit-favorites-mode="editFavoritesMode"
:hide-tooltips="hideTooltips"
@click="showWorldDialog(favorite.id)"
@remove-local-world-favorite="removeLocalWorldFavorite" />
</div>
@@ -208,14 +197,16 @@
</div>
</el-collapse-item>
</el-collapse>
<WorldExportDialog :world-export-dialog-visible.sync="worldExportDialogVisible" />
<WorldExportDialog v-model:worldExportDialogVisible="worldExportDialogVisible" />
</div>
</template>
<script setup>
import { computed, ref, getCurrentInstance } from 'vue';
import { View, Edit, Delete, Loading } from '@element-plus/icons-vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { computed, ref } from 'vue';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { favoriteRequest } from '../../../api';
import { useAppearanceSettingsStore, useFavoriteStore, useWorldStore } from '../../../stores';
import WorldExportDialog from '../dialogs/WorldExportDialog.vue';
@@ -238,10 +229,8 @@
'refresh-local-world-favorite'
]);
const { proxy } = getCurrentInstance();
const { t } = useI18n();
const { hideTooltips, sortFavorites } = storeToRefs(useAppearanceSettingsStore());
const { sortFavorites } = storeToRefs(useAppearanceSettingsStore());
const { setSortFavorites } = useAppearanceSettingsStore();
const { favoriteWorlds, favoriteWorldGroups, localWorldFavorites, localWorldFavoriteGroups } =
storeToRefs(useFavoriteStore());
@@ -290,15 +279,13 @@
}
function userFavoriteWorldsStatusForFavTab(visibility) {
let style = '';
if (visibility === 'public') {
style = '';
} else if (visibility === 'friends') {
style = 'success';
} else {
style = 'info';
return 'primary';
}
return style;
if (visibility === 'friends') {
return 'success';
}
return 'info';
}
function changeWorldGroupVisibility(name, visibility) {
@@ -314,7 +301,7 @@
favoriteGroupId: args.json.id
}
});
proxy.$message({
ElMessage({
message: 'Group visibility changed',
type: 'success'
});
@@ -323,22 +310,27 @@
}
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);
}
ElMessageBox.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')
}
});
)
.then(({ value }) => {
if (value) {
newLocalWorldFavoriteGroup(value);
}
})
.catch(() => {});
}
function promptLocalWorldFavoriteGroupRename(group) {
proxy.$prompt(
ElMessageBox.prompt(
t('prompt.local_favorite_group_rename.description'),
t('prompt.local_favorite_group_rename.header'),
{
@@ -347,43 +339,46 @@
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);
}
}
inputValue: group
}
);
)
.then(({ value }) => {
if (value) {
renameLocalWorldFavoriteGroup(value, group);
}
})
.catch(() => {});
}
function promptLocalWorldFavoriteGroupDelete(group) {
proxy.$confirm(`Delete Group? ${group}`, 'Confirm', {
ElMessageBox.confirm(`Delete Group? ${group}`, 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
type: 'info'
})
.then((action) => {
if (action === 'confirm') {
deleteLocalWorldFavoriteGroup(group);
}
}
});
})
.catch(() => {});
}
function clearFavoriteGroup(ctx) {
proxy.$confirm('Continue? Clear Group', 'Confirm', {
ElMessageBox.confirm('Continue? Clear Group', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
type: 'info'
})
.then((action) => {
if (action === 'confirm') {
favoriteRequest.clearFavoriteGroup({
type: ctx.type,
group: ctx.name
});
}
}
});
})
.catch(() => {});
}
function searchWorldFavorites(worldFavoriteSearch) {
@@ -1,92 +1,96 @@
<template>
<safe-dialog :visible.sync="isDialogVisible" :title="t('dialog.avatar_export.header')" width="650px">
<el-dialog v-model="isDialogVisible" :title="t('dialog.avatar_export.header')" width="650px">
<el-checkbox-group
v-model="exportSelectedOptions"
style="margin-bottom: 10px"
@change="updateAvatarExportDialog()">
<template v-for="option in exportSelectOptions">
<el-checkbox :key="option.value" :label="option.label"></el-checkbox>
<template v-for="option in exportSelectOptions" :key="option.value">
<el-checkbox :label="option.label"></el-checkbox>
</template>
</el-checkbox-group>
<el-dropdown trigger="click" size="small" @click.native.stop>
<el-button size="mini">
<el-dropdown trigger="click" size="small">
<el-button size="small">
<span v-if="avatarExportFavoriteGroup">
{{ avatarExportFavoriteGroup.displayName }} ({{ avatarExportFavoriteGroup.count }}/{{
avatarExportFavoriteGroup.capacity
}})
<i class="el-icon-arrow-down el-icon--right"></i>
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</span>
<span v-else>
All Favorites
<i class="el-icon-arrow-down el-icon--right"></i>
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</span>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item style="display: block; margin: 10px 0" @click.native="selectAvatarExportGroup(null)">
All Favorites
</el-dropdown-item>
<template v-for="groupAPI in favoriteAvatarGroups">
<el-dropdown-item
:key="groupAPI.name"
style="display: block; margin: 10px 0"
@click.native="selectAvatarExportGroup(groupAPI)">
{{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }})
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item style="display: block; margin: 10px 0" @click="selectAvatarExportGroup(null)">
All Favorites
</el-dropdown-item>
</template>
</el-dropdown-menu>
<template v-for="groupAPI in favoriteAvatarGroups" :key="groupAPI.name">
<el-dropdown-item
style="display: block; margin: 10px 0"
@click="selectAvatarExportGroup(groupAPI)">
{{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }})
</el-dropdown-item>
</template>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-dropdown trigger="click" size="small" style="margin-left: 10px" @click.native.stop>
<el-button size="mini">
<el-dropdown trigger="click" size="small" style="margin-left: 10px">
<el-button size="small">
<span v-if="avatarExportLocalFavoriteGroup">
{{ avatarExportLocalFavoriteGroup }} ({{
getLocalAvatarFavoriteGroupLength(avatarExportLocalFavoriteGroup)
}})
<i class="el-icon-arrow-down el-icon--right"></i>
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</span>
<span v-else>
Select Group
<i class="el-icon-arrow-down el-icon--right"></i>
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</span>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item
style="display: block; margin: 10px 0"
@click.native="selectAvatarExportLocalGroup(null)">
None
</el-dropdown-item>
<template v-for="group in localAvatarFavoriteGroups">
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
:key="group"
style="display: block; margin: 10px 0"
@click.native="selectAvatarExportLocalGroup(group)">
{{ group }} ({{ getLocalAvatarFavoriteGroupLength(group) }})
@click="selectAvatarExportLocalGroup(null)">
None
</el-dropdown-item>
</template>
</el-dropdown-menu>
<template v-for="group in localAvatarFavoriteGroups" :key="group">
<el-dropdown-item
style="display: block; margin: 10px 0"
@click="selectAvatarExportLocalGroup(group)">
{{ group }} ({{ getLocalAvatarFavoriteGroupLength(group) }})
</el-dropdown-item>
</template>
</el-dropdown-menu>
</template>
</el-dropdown>
<br />
<el-input
v-model="avatarExportContent"
type="textarea"
size="mini"
rows="15"
size="small"
:rows="15"
resize="none"
readonly
style="margin-top: 15px"
@click.native="handleCopyAvatarExportData"></el-input>
</safe-dialog>
@click="handleCopyAvatarExportData"></el-input>
</el-dialog>
</template>
<script setup>
import { ref, computed, watch, getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { ArrowDown } from '@element-plus/icons-vue';
import { ref, computed, watch } from 'vue';
import { ElMessage } from 'element-plus';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { useAvatarStore, useFavoriteStore } from '../../../stores';
const { t } = useI18n();
const { proxy } = getCurrentInstance();
const props = defineProps({
avatarExportDialogVisible: {
@@ -106,8 +110,7 @@
localAvatarFavoriteGroups
} = storeToRefs(favoriteStore);
const { getLocalAvatarFavoriteGroupLength } = favoriteStore;
const avatarStore = useAvatarStore();
const { cachedAvatars } = storeToRefs(avatarStore);
const { cachedAvatars } = useAvatarStore();
const avatarExportContent = ref('');
const avatarExportFavoriteGroup = ref(null);
@@ -151,7 +154,7 @@
navigator.clipboard
.writeText(avatarExportContent.value)
.then(() => {
proxy.$message({
ElMessage({
message: 'Copied successfully!',
type: 'success',
duration: 2000
@@ -159,7 +162,7 @@
})
.catch((err) => {
console.error('Copy failed:', err);
proxy.$message.error('Copy failed!');
ElMessage.error('Copy failed!');
});
}
function updateAvatarExportDialog() {
@@ -209,7 +212,7 @@
});
for (let i = 0; i < localAvatarFavoritesList.value.length; ++i) {
const avatarId = localAvatarFavoritesList.value[i];
const ref = cachedAvatars.value.get(avatarId);
const ref = cachedAvatars.get(avatarId);
if (typeof ref !== 'undefined') {
lines.push(resText(ref));
}
@@ -1,7 +1,7 @@
<template>
<safe-dialog
ref="avatarImportDialogRef"
:visible.sync="isVisible"
<el-dialog
:z-index="avatarImportDialogIndex"
v-model="isVisible"
:title="t('dialog.avatar_import.header')"
width="650px">
<div style="display: flex; align-items: center; justify-content: space-between">
@@ -10,7 +10,7 @@
<div v-if="avatarImportDialog.progress">
{{ t('dialog.avatar_import.process_progress') }} {{ avatarImportDialog.progress }} /
{{ avatarImportDialog.progressTotal }}
<i class="el-icon-loading" style="margin: 0 5px"></i>
<el-icon style="margin: 0 5px"><Loading /></el-icon>
</div>
<el-button v-if="avatarImportDialog.loading" size="small" @click="cancelAvatarImport">
{{ t('dialog.avatar_import.cancel') }}
@@ -23,60 +23,62 @@
<el-input
v-model="avatarImportDialog.input"
type="textarea"
size="mini"
rows="10"
size="small"
:rows="10"
resize="none"
style="margin-top: 10px"></el-input>
<div style="display: flex; align-items: center; justify-content: space-between; margin-top: 5px">
<div>
<el-dropdown trigger="click" size="small" @click.native.stop>
<el-button size="mini">
<el-dropdown trigger="click" size="small" style="margin-right: 5px" @click.stop>
<el-button size="small">
<span v-if="avatarImportDialog.avatarImportFavoriteGroup">
{{ avatarImportDialog.avatarImportFavoriteGroup.displayName }} ({{
avatarImportDialog.avatarImportFavoriteGroup.count
}}/{{ avatarImportDialog.avatarImportFavoriteGroup.capacity }})
<i class="el-icon-arrow-down el-icon--right"></i>
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</span>
<span v-else>
{{ t('dialog.avatar_import.select_group_placeholder') }}
<i class="el-icon-arrow-down el-icon--right"></i>
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</span>
</el-button>
<el-dropdown-menu slot="dropdown">
<template v-for="groupAPI in favoriteAvatarGroups">
<el-dropdown-item
:key="groupAPI.name"
style="display: block; margin: 10px 0"
:disabled="groupAPI.count >= groupAPI.capacity"
@click.native="selectAvatarImportGroup(groupAPI)">
{{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }})
</el-dropdown-item>
</template>
</el-dropdown-menu>
<template #dropdown>
<el-dropdown-menu>
<template v-for="groupAPI in favoriteAvatarGroups" :key="groupAPI.name">
<el-dropdown-item
style="display: block; margin: 10px 0"
:disabled="groupAPI.count >= groupAPI.capacity"
@click="selectAvatarImportGroup(groupAPI)">
{{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }})
</el-dropdown-item>
</template>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-dropdown trigger="click" size="small" style="margin: 5px" @click.native.stop>
<el-button size="mini">
<el-dropdown trigger="click" size="small">
<el-button size="small">
<span v-if="avatarImportDialog.avatarImportLocalFavoriteGroup">
{{ avatarImportDialog.avatarImportLocalFavoriteGroup }} ({{
getLocalAvatarFavoriteGroupLength(avatarImportDialog.avatarImportLocalFavoriteGroup)
}})
<i class="el-icon-arrow-down el-icon--right"></i>
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</span>
<span v-else>
{{ t('dialog.avatar_import.select_group_placeholder') }}
<i class="el-icon-arrow-down el-icon--right"></i>
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</span>
</el-button>
<el-dropdown-menu slot="dropdown">
<template v-for="group in localAvatarFavoriteGroups">
<el-dropdown-item
:key="group"
style="display: block; margin: 10px 0"
@click.native="selectAvatarImportLocalGroup(group)">
{{ group }} ({{ getLocalAvatarFavoriteGroupLength(group) }})
</el-dropdown-item>
</template>
</el-dropdown-menu>
<template #dropdown>
<el-dropdown-menu>
<template v-for="group in localAvatarFavoriteGroups" :key="group">
<el-dropdown-item
style="display: block; margin: 10px 0"
@click="selectAvatarImportLocalGroup(group)">
{{ group }} ({{ getLocalAvatarFavoriteGroupLength(group) }})
</el-dropdown-item>
</template>
</el-dropdown-menu>
</template>
</el-dropdown>
<span v-if="avatarImportDialog.avatarImportFavoriteGroup" style="margin-left: 5px">
{{ avatarImportTable.data.length }} /
@@ -105,7 +107,7 @@
</div>
</div>
<span v-if="avatarImportDialog.importProgress" style="margin: 10px">
<i class="el-icon-loading" style="margin-right: 5px"></i>
<el-icon style="margin-right: 5px"><Loading /></el-icon>
{{ t('dialog.avatar_import.import_progress') }}
{{ avatarImportDialog.importProgress }}/{{ avatarImportDialog.importProgressTotal }}
</span>
@@ -119,70 +121,73 @@
</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">
<DataTable v-loading="avatarImportDialog.loading" v-bind="avatarImportTable" style="margin-top: 10px">
<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" />
<template #default="{ row }">
<el-popover placement="right" :width="500" trigger="hover">
<template #reference>
<img :src="row.thumbnailImageUrl" class="friends-list-avatar" loading="lazy" />
</template>
<img
v-lazy="scope.row.imageUrl"
class="friends-list-avatar"
style="height: 500px; cursor: pointer"
@click="showFullscreenImageDialog(scope.row.imageUrl)" />
:src="row.imageUrl"
:class="['friends-list-avatar', 'x-popover-image']"
style="cursor: pointer"
@click="showFullscreenImageDialog(row.imageUrl)"
loading="lazy" />
</el-popover>
</template>
</el-table-column>
<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 }}
<template #default="{ row }">
<span class="x-link" @click="showAvatarDialog(row.id)">
{{ row.name }}
</span>
</template>
</el-table-column>
<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 }}
<template #default="{ row }">
<span class="x-link" @click="showUserDialog(row.authorId)">
{{ row.authorName }}
</span>
</template>
</el-table-column>
<el-table-column :label="t('table.import.status')" width="70" prop="releaseStatus">
<template slot-scope="scope">
<template #default="{ row }">
<span
:style="{
color:
scope.row.releaseStatus === 'public'
row.releaseStatus === 'public'
? '#67c23a'
: scope.row.releaseStatus === 'private'
: row.releaseStatus === 'private'
? '#f56c6c'
: undefined
}">
{{ scope.row.releaseStatus.charAt(0).toUpperCase() + scope.row.releaseStatus.slice(1) }}
{{ row.releaseStatus.charAt(0).toUpperCase() + row.releaseStatus.slice(1) }}
</span>
</template>
</el-table-column>
<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>
<template #default="{ row }">
<el-button type="text" :icon="Close" size="small" @click="deleteItemAvatarImport(row)"> </el-button>
</template>
</el-table-column>
</data-tables>
</safe-dialog>
</DataTable>
</el-dialog>
</template>
<script setup>
import { ref, computed, watch, getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { Close, Loading, ArrowDown } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import { ref, computed, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { avatarRequest, favoriteRequest } from '../../../api';
import { adjustDialogZ, removeFromArray } from '../../../shared/utils';
import { getNextDialogIndex, removeFromArray } from '../../../shared/utils';
import { useAvatarStore, useFavoriteStore, useGalleryStore, useUserStore } from '../../../stores';
const emit = defineEmits(['update:avatarImportDialogInput']);
const { t } = useI18n();
const { proxy } = getCurrentInstance();
const { showUserDialog } = useUserStore();
const { favoriteAvatarGroups, avatarImportDialogInput, avatarImportDialogVisible, localAvatarFavoriteGroups } =
storeToRefs(useFavoriteStore());
@@ -212,7 +217,7 @@
layout: 'table'
});
const avatarImportDialogRef = ref(null);
const avatarImportDialogIndex = ref(2000);
const isVisible = computed({
get() {
@@ -227,7 +232,7 @@
() => avatarImportDialogVisible.value,
(value) => {
if (value) {
adjustDialogZ(avatarImportDialogRef.value.$el);
avatarImportDialogIndex.value = getNextDialogIndex();
clearAvatarImportTable();
resetAvatarImport();
if (avatarImportDialogInput.value) {
@@ -318,7 +323,7 @@
})
.then((args) => {
if (message) {
proxy.$message({
ElMessage({
message: 'Avatar added to favorites',
type: 'success'
});
@@ -334,7 +339,7 @@
D.loading = true;
const data = [...avatarImportTable.value.data].reverse();
D.importProgressTotal = data.length;
let ref = '';
let ref = null;
try {
for (let i = data.length - 1; i >= 0; i--) {
if (!D.loading || !isVisible.value) {
@@ -351,7 +356,7 @@
D.importProgress++;
}
} catch (err) {
D.errors = `Name: ${ref.name}\nAvatarId: ${ref.id}\n${err}\n\n`;
D.errors = `Name: ${ref?.name}\nAvatarId: ${ref?.id}\n${err}\n\n`;
} finally {
D.importProgress = 0;
D.importProgressTotal = 0;
@@ -1,55 +1,60 @@
<template>
<safe-dialog
:visible.sync="isDialogVisible"
<el-dialog
v-model="isDialogVisible"
class="x-dialog"
:title="t('dialog.friend_export.header')"
width="650px"
destroy-on-close>
<el-dropdown trigger="click" size="small" @click.native.stop>
<el-button size="mini">
<el-dropdown trigger="click" size="small">
<el-button size="small">
<span v-if="friendExportFavoriteGroup">
{{ friendExportFavoriteGroup.displayName }} ({{ friendExportFavoriteGroup.count }}/{{
friendExportFavoriteGroup.capacity
}})
<i class="el-icon-arrow-down el-icon--right"></i>
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</span>
<span v-else>All Favorites <i class="el-icon-arrow-down el-icon--right"></i></span>
<span v-else
>All Favorites <el-icon class="el-icon--right"><ArrowDown /></el-icon
></span>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item style="display: block; margin: 10px 0" @click.native="selectFriendExportGroup(null)">
All Favorites
</el-dropdown-item>
<template v-for="groupAPI in favoriteFriendGroups">
<el-dropdown-item
:key="groupAPI.name"
style="display: block; margin: 10px 0"
@click.native="selectFriendExportGroup(groupAPI)">
{{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }})
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item style="display: block; margin: 10px 0" @click="selectFriendExportGroup(null)">
All Favorites
</el-dropdown-item>
</template>
</el-dropdown-menu>
<template v-for="groupAPI in favoriteFriendGroups" :key="groupAPI.name">
<el-dropdown-item
style="display: block; margin: 10px 0"
@click="selectFriendExportGroup(groupAPI)">
{{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }})
</el-dropdown-item>
</template>
</el-dropdown-menu>
</template>
</el-dropdown>
<br />
<el-input
v-model="friendExportContent"
type="textarea"
size="mini"
rows="15"
size="small"
:rows="15"
resize="none"
readonly
style="margin-top: 15px"
@click.native="handleCopyFriendExportData"></el-input>
</safe-dialog>
@click="handleCopyFriendExportData"></el-input>
</el-dialog>
</template>
<script setup>
import { ref, computed, watch, getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { ArrowDown } from '@element-plus/icons-vue';
import { ref, computed, watch } from 'vue';
import { ElMessage } from 'element-plus';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { useFavoriteStore } from '../../../stores';
const { t } = useI18n();
const { proxy } = getCurrentInstance();
const props = defineProps({
friendExportDialogVisible: {
@@ -96,7 +101,7 @@
navigator.clipboard
.writeText(friendExportContent.value)
.then(() => {
proxy.$message({
ElMessage({
message: 'Copied successfully!',
type: 'success',
duration: 2000
@@ -104,7 +109,7 @@
})
.catch((err) => {
console.error('Copy failed:', err);
proxy.$message.error('Copy failed!');
ElMessage.error('Copy failed!');
});
}
@@ -1,7 +1,7 @@
<template>
<safe-dialog
ref="friendImportDialogRef"
:visible.sync="isVisible"
<el-dialog
:z-index="friendImportDialogIndex"
v-model="isVisible"
:title="t('dialog.friend_import.header')"
width="650px">
<div style="display: flex; align-items: center; justify-content: space-between">
@@ -10,7 +10,7 @@
<div v-if="friendImportDialog.progress">
{{ t('dialog.friend_import.process_progress') }} {{ friendImportDialog.progress }} /
{{ friendImportDialog.progressTotal }}
<i class="el-icon-loading" style="margin: 0 5px"></i>
<el-icon style="margin: 0 5px"><Loading /></el-icon>
</div>
<el-button v-if="friendImportDialog.loading" size="small" @click="cancelFriendImport">
{{ t('dialog.friend_import.cancel') }}
@@ -23,36 +23,37 @@
<el-input
v-model="friendImportDialog.input"
type="textarea"
size="mini"
rows="10"
size="small"
:rows="10"
resize="none"
style="margin-top: 10px" />
<div style="display: flex; align-items: center; justify-content: space-between; margin-top: 5px">
<div>
<el-dropdown trigger="click" size="small" @click.native.stop>
<el-button size="mini">
<el-dropdown trigger="click" size="small">
<el-button size="small">
<span v-if="friendImportDialog.friendImportFavoriteGroup">
{{ friendImportDialog.friendImportFavoriteGroup.displayName }} ({{
friendImportDialog.friendImportFavoriteGroup.count
}}/{{ friendImportDialog.friendImportFavoriteGroup.capacity }})
<i class="el-icon-arrow-down el-icon--right"></i>
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</span>
<span v-else
>{{ t('dialog.friend_import.select_group_placeholder') }}
<i class="el-icon-arrow-down el-icon--right"></i
<el-icon class="el-icon--right"><ArrowDown /></el-icon
></span>
</el-button>
<el-dropdown-menu slot="dropdown">
<template v-for="groupAPI in favoriteFriendGroups">
<el-dropdown-item
:key="groupAPI.name"
style="display: block; margin: 10px 0"
:disabled="groupAPI.count >= groupAPI.capacity"
@click.native="selectFriendImportGroup(groupAPI)">
{{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }})
</el-dropdown-item>
</template>
</el-dropdown-menu>
<template #dropdown>
<el-dropdown-menu>
<template v-for="groupAPI in favoriteFriendGroups" :key="groupAPI.name">
<el-dropdown-item
style="display: block; margin: 10px 0"
:disabled="groupAPI.count >= groupAPI.capacity"
@click="selectFriendImportGroup(groupAPI)">
{{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }})
</el-dropdown-item>
</template>
</el-dropdown-menu>
</template>
</el-dropdown>
<span v-if="friendImportDialog.friendImportFavoriteGroup" style="margin-left: 5px">
{{ friendImportTable.data.length }} /
@@ -77,7 +78,7 @@
</div>
</div>
<span v-if="friendImportDialog.importProgress" style="margin: 10px">
<i class="el-icon-loading" style="margin-right: 5px"></i>
<el-icon style="margin-right: 5px"><Loading /></el-icon>
{{ t('dialog.friend_import.import_progress') }} {{ friendImportDialog.importProgress }}/{{
friendImportDialog.importProgressTotal
}}
@@ -90,47 +91,47 @@
<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">
<DataTable v-loading="friendImportDialog.loading" v-bind="friendImportTable" style="margin-top: 10px">
<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">
<img class="friends-list-avatar" :src="userImage(scope.row)" />
<template #default="{ row }">
<el-popover placement="right" :width="500" trigger="hover">
<template #reference>
<img class="friends-list-avatar" :src="userImage(row)" />
</template>
<img
class="friends-list-avatar"
:src="userImageFull(scope.row)"
style="height: 500px; cursor: pointer"
@click="showFullscreenImageDialog(userImageFull(scope.row))" />
:src="userImageFull(row)"
:class="['friends-list-avatar', 'x-popover-image']"
style="cursor: pointer"
@click="showFullscreenImageDialog(userImageFull(row))" />
</el-popover>
</template>
</el-table-column>
<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 }}
<template #default="{ row }">
<span class="x-link" :title="row.displayName" @click="showUserDialog(row.id)">
{{ row.displayName }}
</span>
</template>
</el-table-column>
<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>
<template #default="{ row }">
<el-button type="text" :icon="Close" size="small" @click="deleteItemFriendImport(row)"> </el-button>
</template>
</el-table-column>
</data-tables>
</safe-dialog>
</DataTable>
</el-dialog>
</template>
<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';
import { Close, Loading, ArrowDown } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
const { proxy } = getCurrentInstance();
import { ref, computed, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { favoriteRequest, userRequest } from '../../../api';
import { getNextDialogIndex, removeFromArray, userImage, userImageFull } from '../../../shared/utils';
import { useFavoriteStore, useGalleryStore, useUserStore } from '../../../stores';
const { t } = useI18n();
@@ -157,12 +158,12 @@
data: [],
tableProps: {
stripe: true,
size: 'mini'
size: 'small'
},
layout: 'table'
});
const friendImportDialogRef = ref(null);
const friendImportDialogIndex = ref(2000);
const isVisible = computed({
get() {
@@ -177,7 +178,7 @@
() => friendImportDialogVisible.value,
(value) => {
if (value) {
adjustDialogZ(friendImportDialogRef.value.$el);
friendImportDialogIndex.value = getNextDialogIndex();
clearFriendImportTable();
resetFriendImport();
if (friendImportDialogInput.value) {
@@ -211,7 +212,7 @@
}
const data = [...friendImportTable.value.data].reverse();
D.importProgressTotal = data.length;
let ref = '';
let ref = null;
try {
for (let i = data.length - 1; i >= 0; i--) {
if (!D.loading || !isVisible.value) {
@@ -224,7 +225,7 @@
D.importProgress++;
}
} catch (err) {
D.errors = `Name: ${ref.displayName}\nUserId: ${ref.id}\n${err}\n\n`;
D.errors = `Name: ${ref?.displayName}\nUserId: ${ref?.id}\n${err}\n\n`;
} finally {
D.importProgress = 0;
D.importProgressTotal = 0;
@@ -240,7 +241,7 @@
})
.then((args) => {
if (message) {
proxy.$message({
ElMessage({
message: 'Friend added to favorites',
type: 'success'
});
@@ -1,70 +1,70 @@
<template>
<safe-dialog :visible.sync="isDialogVisible" :title="t('dialog.world_export.header')" width="650px">
<el-dialog v-model="isDialogVisible" :title="t('dialog.world_export.header')" width="650px">
<el-checkbox-group
v-model="exportSelectedOptions"
style="margin-bottom: 10px"
@change="updateWorldExportDialog">
<template v-for="option in exportSelectOptions">
<el-checkbox :key="option.value" :label="option.label"></el-checkbox>
<template v-for="option in exportSelectOptions" :key="option.value">
<el-checkbox :label="option.label"></el-checkbox>
</template>
</el-checkbox-group>
<el-dropdown trigger="click" size="small" @click.native.stop>
<el-button size="mini">
<el-dropdown trigger="click" size="small">
<el-button size="small">
<span v-if="worldExportFavoriteGroup">
{{ worldExportFavoriteGroup.displayName }} ({{ worldExportFavoriteGroup.count }}/{{
worldExportFavoriteGroup.capacity
}})
<i class="el-icon-arrow-down el-icon--right"></i>
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</span>
<span v-else>
All Favorites
<i class="el-icon-arrow-down el-icon--right"></i>
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</span>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item style="display: block; margin: 10px 0" @click.native="selectWorldExportGroup(null)">
None
</el-dropdown-item>
<template v-for="groupAPI in favoriteWorldGroups">
<el-dropdown-item
:key="groupAPI.name"
style="display: block; margin: 10px 0"
@click.native="selectWorldExportGroup(groupAPI)">
{{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }})
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item style="display: block; margin: 10px 0" @click="selectWorldExportGroup(null)">
None
</el-dropdown-item>
</template>
</el-dropdown-menu>
<template v-for="groupAPI in favoriteWorldGroups" :key="groupAPI.name">
<el-dropdown-item
style="display: block; margin: 10px 0"
@click="selectWorldExportGroup(groupAPI)">
{{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }})
</el-dropdown-item>
</template>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-dropdown trigger="click" size="small" style="margin-left: 10px" @click.native.stop>
<el-button size="mini">
<el-dropdown trigger="click" size="small" style="margin-left: 10px">
<el-button size="small">
<span v-if="worldExportLocalFavoriteGroup">
{{ worldExportLocalFavoriteGroup }} ({{
getLocalWorldFavoriteGroupLength(worldExportLocalFavoriteGroup)
}})
<i class="el-icon-arrow-down el-icon--right"></i>
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</span>
<span v-else>
Select Group
<i class="el-icon-arrow-down el-icon--right"></i>
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</span>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item
style="display: block; margin: 10px 0"
@click.native="selectWorldExportLocalGroup(null)">
None
</el-dropdown-item>
<template v-for="group in localWorldFavoriteGroups">
<el-dropdown-item
:key="group"
style="display: block; margin: 10px 0"
@click.native="selectWorldExportLocalGroup(group)">
{{ group }} ({{ localWorldFavorites[group].length }})
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item style="display: block; margin: 10px 0" @click="selectWorldExportLocalGroup(null)">
None
</el-dropdown-item>
</template>
</el-dropdown-menu>
<template v-for="group in localWorldFavoriteGroups" :key="group">
<el-dropdown-item
style="display: block; margin: 10px 0"
@click="selectWorldExportLocalGroup(group)">
{{ group }} ({{ localWorldFavorites[group].length }})
</el-dropdown-item>
</template>
</el-dropdown-menu>
</template>
</el-dropdown>
<br />
@@ -72,18 +72,21 @@
<el-input
v-model="worldExportContent"
type="textarea"
size="mini"
rows="15"
size="small"
:rows="15"
resize="none"
readonly
style="margin-top: 15px"
@click.native="handleCopyWorldExportData"></el-input>
</safe-dialog>
@click="handleCopyWorldExportData"></el-input>
</el-dialog>
</template>
<script setup>
import { ref, computed, watch, getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { ArrowDown } from '@element-plus/icons-vue';
import { ref, computed, watch } from 'vue';
import { ElMessage } from 'element-plus';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { useFavoriteStore, useWorldStore } from '../../../stores';
@@ -97,7 +100,6 @@
const emit = defineEmits(['update:worldExportDialogVisible']);
const { t } = useI18n();
const { proxy } = getCurrentInstance();
const favoriteStore = useFavoriteStore();
const {
@@ -108,7 +110,7 @@
localWorldFavoritesList
} = storeToRefs(favoriteStore);
const { getLocalWorldFavoriteGroupLength } = favoriteStore;
const { cachedWorlds } = storeToRefs(useWorldStore());
const { cachedWorlds } = useWorldStore();
const worldExportContent = ref('');
const worldExportFavoriteGroup = ref(null);
@@ -154,7 +156,7 @@
navigator.clipboard
.writeText(worldExportContent.value)
.then(() => {
proxy.$message({
ElMessage({
message: 'Copied successfully!',
type: 'success',
duration: 2000
@@ -162,7 +164,7 @@
})
.catch((err) => {
console.error('Copy failed:', err);
proxy.$message.error('Copy failed!');
ElMessage.error('Copy failed!');
});
}
@@ -214,7 +216,7 @@
});
for (let i = 0; i < localWorldFavoritesList.value.length; ++i) {
const worldId = localWorldFavoritesList.value[i];
const ref = cachedWorlds.value.get(worldId);
const ref = cachedWorlds.get(worldId);
if (typeof ref !== 'undefined') {
lines.push(resText(ref));
}
@@ -1,7 +1,7 @@
<template>
<safe-dialog
ref="worldImportDialogRef"
:visible.sync="isVisible"
<el-dialog
:z-index="worldImportDialogIndex"
v-model="isVisible"
:title="t('dialog.world_import.header')"
width="650px"
class="x-dialog">
@@ -11,7 +11,7 @@
<div v-if="worldImportDialog.progress">
{{ t('dialog.world_import.process_progress') }}
{{ worldImportDialog.progress }} / {{ worldImportDialog.progressTotal }}
<i class="el-icon-loading" style="margin: 0 5px"></i>
<el-icon style="margin: 0 5px"><Loading /></el-icon>
</div>
<el-button v-if="worldImportDialog.loading" size="small" @click="cancelWorldImport">
{{ t('dialog.world_import.cancel') }}
@@ -24,60 +24,62 @@
<el-input
v-model="worldImportDialog.input"
type="textarea"
size="mini"
rows="10"
size="small"
:rows="10"
resize="none"
style="margin-top: 10px"></el-input>
<div style="display: flex; align-items: center; justify-content: space-between; margin-top: 5px">
<div>
<el-dropdown trigger="click" size="small" style="margin-right: 5px" @click.native.stop>
<el-button size="mini">
<el-dropdown trigger="click" size="small" style="margin-right: 5px" @click.stop>
<el-button size="small">
<span v-if="worldImportDialog.worldImportFavoriteGroup">
{{ worldImportDialog.worldImportFavoriteGroup.displayName }}
({{ worldImportDialog.worldImportFavoriteGroup.count }}/{{
worldImportDialog.worldImportFavoriteGroup.capacity
}})
<i class="el-icon-arrow-down el-icon--right"></i>
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</span>
<span v-else>
{{ t('dialog.world_import.select_vrchat_group_placeholder') }}
<i class="el-icon-arrow-down el-icon--right"></i>
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</span>
</el-button>
<el-dropdown-menu slot="dropdown">
<template v-for="groupAPI in favoriteWorldGroups">
<el-dropdown-item
:key="groupAPI.name"
style="display: block; margin: 10px 0"
:disabled="groupAPI.count >= groupAPI.capacity"
@click.native="selectWorldImportGroup(groupAPI)">
{{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }})
</el-dropdown-item>
</template>
</el-dropdown-menu>
<template #dropdown>
<el-dropdown-menu>
<template v-for="groupAPI in favoriteWorldGroups" :key="groupAPI.name">
<el-dropdown-item
style="display: block; margin: 10px 0"
:disabled="groupAPI.count >= groupAPI.capacity"
@click="selectWorldImportGroup(groupAPI)">
{{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }})
</el-dropdown-item>
</template>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-dropdown trigger="click" size="small" style="margin: 5px" @click.native.stop>
<el-button size="mini">
<el-dropdown trigger="click" size="small" @click.stop>
<el-button size="small">
<span v-if="worldImportDialog.worldImportLocalFavoriteGroup">
{{ worldImportDialog.worldImportLocalFavoriteGroup }}
({{ getLocalWorldFavoriteGroupLength(worldImportDialog.worldImportLocalFavoriteGroup) }})
<i class="el-icon-arrow-down el-icon--right"></i>
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</span>
<span v-else>
{{ t('dialog.world_import.select_local_group_placeholder') }}
<i class="el-icon-arrow-down el-icon--right"></i>
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</span>
</el-button>
<el-dropdown-menu slot="dropdown">
<template v-for="group in localWorldFavoriteGroups">
<el-dropdown-item
:key="group"
style="display: block; margin: 10px 0"
@click.native="selectWorldImportLocalGroup(group)">
{{ group }} ({{ getLocalWorldFavoriteGroupLength(group) }})
</el-dropdown-item>
</template>
</el-dropdown-menu>
<template #dropdown>
<el-dropdown-menu>
<template v-for="group in localWorldFavoriteGroups" :key="group">
<el-dropdown-item
style="display: block; margin: 10px 0"
@click="selectWorldImportLocalGroup(group)">
{{ group }} ({{ getLocalWorldFavoriteGroupLength(group) }})
</el-dropdown-item>
</template>
</el-dropdown-menu>
</template>
</el-dropdown>
<span v-if="worldImportDialog.worldImportFavoriteGroup" style="margin-left: 5px">
{{ worldImportTable.data.length }} /
@@ -106,7 +108,7 @@
</div>
</div>
<span v-if="worldImportDialog.importProgress" style="margin: 10px">
<i class="el-icon-loading" style="margin-right: 5px"></i>
<el-icon style="margin-right: 5px"><Loading /></el-icon>
{{ t('dialog.world_import.import_progress') }}
{{ worldImportDialog.importProgress }}/{{ worldImportDialog.importProgressTotal }}
</span>
@@ -120,67 +122,64 @@
</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">
<DataTable v-loading="worldImportDialog.loading" v-bind="worldImportTable" style="margin-top: 10px">
<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" />
<template #default="{ row }">
<el-popover placement="right" :width="500" trigger="hover">
<template #reference>
<img :src="row.thumbnailImageUrl" class="friends-list-avatar" loading="lazy" />
</template>
<img
v-lazy="scope.row.imageUrl"
class="friends-list-avatar"
style="height: 500px; cursor: pointer"
@click="showFullscreenImageDialog(scope.row.imageUrl)" />
:src="row.imageUrl"
:class="['friends-list-avatar', 'x-popover-image']"
style="cursor: pointer"
@click="showFullscreenImageDialog(row.imageUrl)"
loading="lazy" />
</el-popover>
</template>
</el-table-column>
<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 #default="{ row }">
<span class="x-link" @click="showWorldDialog(row.id)" v-text="row.name"></span>
</template>
</el-table-column>
<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)"
v-text="scope.row.authorName"></span>
<template #default="{ row }">
<span class="x-link" @click="showUserDialog(row.authorId)" v-text="row.authorName"></span>
</template>
</el-table-column>
<el-table-column :label="t('table.import.status')" width="70" prop="releaseStatus">
<template slot-scope="scope">
<template #default="{ row }">
<span
:style="{
color:
scope.row.releaseStatus === 'public'
row.releaseStatus === 'public'
? '#67c23a'
: scope.row.releaseStatus === 'private'
: row.releaseStatus === 'private'
? '#f56c6c'
: undefined
}"
v-text="
scope.row.releaseStatus.charAt(0).toUpperCase() + scope.row.releaseStatus.slice(1)
"></span>
v-text="row.releaseStatus.charAt(0).toUpperCase() + row.releaseStatus.slice(1)"></span>
</template>
</el-table-column>
<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="deleteItemWorldImport(scope.row)"></el-button>
<template #default="{ row }">
<el-button type="text" :icon="Close" size="small" @click="deleteItemWorldImport(row)"></el-button>
</template>
</el-table-column>
</data-tables>
</safe-dialog>
</DataTable>
</el-dialog>
</template>
<script setup>
import { ref, watch, computed, getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { Close, Loading, ArrowDown } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import { ref, watch, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { favoriteRequest, worldRequest } from '../../../api';
import { adjustDialogZ, removeFromArray } from '../../../shared/utils';
import { getNextDialogIndex, removeFromArray } from '../../../shared/utils';
import { useFavoriteStore, useGalleryStore, useUserStore, useWorldStore } from '../../../stores';
const { showUserDialog } = useUserStore();
@@ -192,10 +191,9 @@
const emit = defineEmits(['update:worldImportDialogInput']);
const { proxy } = getCurrentInstance();
const { t } = useI18n();
const worldImportDialogRef = ref(null);
const worldImportDialogIndex = ref(2000);
const worldImportDialog = ref({
loading: false,
@@ -214,7 +212,7 @@
data: [],
tableProps: {
stripe: true,
size: 'mini'
size: 'small'
},
layout: 'table'
});
@@ -232,7 +230,7 @@
() => worldImportDialogVisible.value,
(visible) => {
if (visible) {
adjustDialogZ(worldImportDialogRef.value.$el);
worldImportDialogIndex.value = getNextDialogIndex();
clearWorldImportTable();
resetWorldImport();
if (worldImportDialogInput.value) {
@@ -356,7 +354,7 @@
})
.then((args) => {
if (message) {
proxy.$message({
ElMessage({
message: 'World added to favorites',
type: 'success'
});
+60 -61
View File
@@ -2,10 +2,7 @@
<div v-show="menuActiveIndex === 'feed'" class="x-container feed">
<div style="margin: 0 0 10px; display: flex; align-items: center">
<div style="flex: none; margin-right: 10px; display: flex; align-items: center">
<el-tooltip
placement="bottom"
:content="t('view.feed.favorites_only_tooltip')"
:disabled="hideTooltips">
<el-tooltip placement="bottom" :content="t('view.feed.favorites_only_tooltip')">
<el-switch v-model="feedTable.vip" active-color="#13ce66" @change="feedTableLookup"></el-switch>
</el-tooltip>
</div>
@@ -27,11 +24,11 @@
:placeholder="t('view.feed.search_placeholder')"
clearable
style="flex: none; width: 150px; margin-left: 10px"
@keyup.native.13="feedTableLookup"
@keyup.enter="feedTableLookup"
@change="feedTableLookup"></el-input>
</div>
<data-tables v-loading="feedTable.loading" v-bind="feedTable" lazy>
<DataTable v-bind="feedTable">
<el-table-column type="expand" width="20">
<template #default="scope">
<div style="position: relative; font-size: 14px">
@@ -40,12 +37,12 @@
v-if="scope.row.previousLocation"
:location="scope.row.previousLocation"
style="display: inline-block" />
<el-tag type="info" effect="plain" size="mini" style="margin-left: 5px">{{
<el-tag type="info" effect="plain" size="small" style="margin-left: 5px">{{
timeToText(scope.row.time)
}}</el-tag>
<br />
<span style="margin-right: 5px">
<i class="el-icon-right"></i>
<el-icon><Right /></el-icon>
</span>
<Location
v-if="scope.row.location"
@@ -59,7 +56,7 @@
:location="scope.row.location"
:hint="scope.row.worldName"
:grouphint="scope.row.groupName" />
<el-tag type="info" effect="plain" size="mini" style="margin-left: 5px">{{
<el-tag type="info" effect="plain" size="small" style="margin-left: 5px">{{
timeToText(scope.row.time)
}}</el-tag>
</template>
@@ -73,56 +70,58 @@
</template>
<template v-else-if="scope.row.type === 'Avatar'">
<div style="display: flex; align-items: center">
<el-popover placement="right" width="500px" trigger="click">
<div
slot="reference"
style="display: inline-block; vertical-align: top; width: 160px">
<template v-if="scope.row.previousCurrentAvatarThumbnailImageUrl">
<img
v-lazy="scope.row.previousCurrentAvatarThumbnailImageUrl"
class="x-link"
style="flex: none; width: 160px; height: 120px; border-radius: 4px" />
<br />
<AvatarInfo
:imageurl="scope.row.previousCurrentAvatarThumbnailImageUrl"
:userid="scope.row.userId"
:hintownerid="scope.row.previousOwnerId"
:hintavatarname="scope.row.previousAvatarName"
:avatartags="scope.row.previousCurrentAvatarTags" />
</template>
</div>
<el-popover placement="right" :width="500" trigger="click">
<template #reference>
<div style="display: inline-block; vertical-align: top; width: 160px">
<template v-if="scope.row.previousCurrentAvatarThumbnailImageUrl">
<img
:src="scope.row.previousCurrentAvatarThumbnailImageUrl"
class="x-link"
style="flex: none; width: 160px; height: 120px; border-radius: 4px"
loading="lazy" />
<br />
<AvatarInfo
:imageurl="scope.row.previousCurrentAvatarThumbnailImageUrl"
:userid="scope.row.userId"
:hintownerid="scope.row.previousOwnerId"
:hintavatarname="scope.row.previousAvatarName"
:avatartags="scope.row.previousCurrentAvatarTags" />
</template>
</div>
</template>
<img
v-lazy="scope.row.previousCurrentAvatarImageUrl"
class="x-link"
style="width: 500px; height: 375px"
@click="showFullscreenImageDialog(scope.row.previousCurrentAvatarImageUrl)" />
:src="scope.row.previousCurrentAvatarImageUrl"
:class="['x-link', 'x-popover-image']"
@click="showFullscreenImageDialog(scope.row.previousCurrentAvatarImageUrl)"
loading="lazy" />
</el-popover>
<span style="position: relative; margin: 0 10px">
<i class="el-icon-right"></i>
<el-icon><Right /></el-icon>
</span>
<el-popover placement="right" width="500px" trigger="click">
<div
slot="reference"
style="display: inline-block; vertical-align: top; width: 160px">
<template v-if="scope.row.currentAvatarThumbnailImageUrl">
<img
v-lazy="scope.row.currentAvatarThumbnailImageUrl"
class="x-link"
style="flex: none; width: 160px; height: 120px; border-radius: 4px" />
<br />
<AvatarInfo
:imageurl="scope.row.currentAvatarThumbnailImageUrl"
:userid="scope.row.userId"
:hintownerid="scope.row.ownerId"
:hintavatarname="scope.row.avatarName"
:avatartags="scope.row.currentAvatarTags" />
</template>
</div>
<el-popover placement="right" :width="500" trigger="click">
<template #reference>
<div style="display: inline-block; vertical-align: top; width: 160px">
<template v-if="scope.row.currentAvatarThumbnailImageUrl">
<img
:src="scope.row.currentAvatarThumbnailImageUrl"
class="x-link"
style="flex: none; width: 160px; height: 120px; border-radius: 4px"
loading="lazy" />
<br />
<AvatarInfo
:imageurl="scope.row.currentAvatarThumbnailImageUrl"
:userid="scope.row.userId"
:hintownerid="scope.row.ownerId"
:hintavatarname="scope.row.avatarName"
:avatartags="scope.row.currentAvatarTags" />
</template>
</div>
</template>
<img
v-lazy="scope.row.currentAvatarImageUrl"
class="x-link"
style="width: 500px; height: 375px"
@click="showFullscreenImageDialog(scope.row.currentAvatarImageUrl)" />
:src="scope.row.currentAvatarImageUrl"
:class="['x-link', 'x-popover-image']"
@click="showFullscreenImageDialog(scope.row.currentAvatarImageUrl)"
loading="lazy" />
</el-popover>
</div>
</template>
@@ -148,7 +147,7 @@
<span style="margin-left: 5px" v-text="scope.row.previousStatusDescription"></span>
<br />
<span>
<i class="el-icon-right"></i>
<el-icon><Right /></el-icon>
</span>
<el-tooltip placement="top">
<template #content>
@@ -182,7 +181,7 @@
</template>
</el-table-column>
<el-table-column :label="t('table.feed.date')" prop="created_at" sortable="custom" width="120">
<el-table-column :label="t('table.feed.date')" prop="created_at" :sortable="true" width="120">
<template #default="scope">
<el-tooltip placement="right">
<template #content>
@@ -246,7 +245,7 @@
<i class="x-user-status" :class="statusClass(scope.row.previousStatus)"></i>
</el-tooltip>
<span style="margin: 0 5px">
<i class="el-icon-right"></i>
<el-icon><Right /></el-icon>
</span>
<el-tooltip placement="top">
<template #content>
@@ -305,17 +304,17 @@
</template>
</template>
</el-table-column>
</data-tables>
</DataTable>
</div>
</template>
<script setup>
import { Right } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n-bridge';
import { useGalleryStore, useAppearanceSettingsStore, useUserStore, useFeedStore, useUiStore } from '../../stores';
import { useI18n } from 'vue-i18n';
import { useGalleryStore, 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();
+75 -79
View File
@@ -20,26 +20,20 @@
<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')"
:disabled="hideTooltips">
<el-tooltip placement="top" :content="t('view.friend_list.cancel_tooltip')">
<el-button
size="mini"
icon="el-icon-loading"
size="small"
:icon="Loading"
circle
style="margin-left: 5px"
@click="friendsListLoading = false"></el-button>
</el-tooltip>
</template>
<template v-else>
<el-tooltip
placement="top"
:content="t('view.friend_list.load_tooltip')"
:disabled="hideTooltips">
<el-tooltip placement="top" :content="t('view.friend_list.load_tooltip')">
<el-button
size="mini"
icon="el-icon-refresh-left"
size="small"
:icon="RefreshLeft"
circle
style="margin-left: 5px"
@click="friendsListLoadUsers"></el-button>
@@ -50,10 +44,7 @@
<div style="margin: 10px 0 0 10px; display: flex; align-items: center">
<div style="flex: none; margin-right: 10px; display: flex; align-items: center">
<el-tooltip
placement="bottom"
:content="t('view.friend_list.favorites_only_tooltip')"
:disabled="hideTooltips">
<el-tooltip placement="bottom" :content="t('view.friend_list.favorites_only_tooltip')">
<el-switch
v-model="friendsListSearchFilterVIP"
active-color="#13ce66"
@@ -80,19 +71,19 @@
: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')">
<el-button
type="default"
icon="el-icon-refresh"
:icon="Refresh"
circle
style="flex: none"
@click="friendsListSearchChange"></el-button>
</el-tooltip>
</div>
<data-tables
<DataTable
v-loading="friendsListLoading"
v-bind="friendsListTable"
:table-props="{ height: 'calc(100vh - 170px)', size: 'mini' }"
:table-props="{ height: 'calc(100vh - 170px)', size: 'small' }"
style="margin-top: 10px; cursor: pointer"
@row-click="selectFriendsListRow">
<el-table-column
@@ -100,28 +91,31 @@
:key="friendsListBulkUnfriendForceUpdate"
width="55"
prop="$selected">
<template slot-scope="scope">
<el-button type="text" size="mini" @click.stop>
<template #default="{ row }">
<el-button type="text" size="small" @click.stop>
<el-checkbox
v-model="scope.row.$selected"
v-model="row.$selected"
@change="friendsListBulkUnfriendForceUpdate++"></el-checkbox>
</el-button>
</template>
</el-table-column>
<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>
<el-table-column :label="t('table.friendList.no')" width="70" prop="$friendNumber" :sortable="true">
<template #default="{ row }">
<span>{{ row.$friendNumber ? row.$friendNumber : '' }}</span>
</template>
</el-table-column>
<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" />
<template #default="{ row }">
<el-popover placement="right" :width="500" trigger="hover">
<template #reference>
<img :src="userImage(row, true)" class="friends-list-avatar" loading="lazy" />
</template>
<img
v-lazy="userImageFull(scope.row)"
class="friends-list-avatar"
style="height: 500px; cursor: pointer"
@click="showFullscreenImageDialog(userImageFull(scope.row))" />
:src="userImageFull(row)"
:class="['friends-list-avatar', 'x-popover-image']"
style="cursor: pointer"
@click="showFullscreenImageDialog(userImageFull(row))"
loading="lazy" />
</el-popover>
</template>
</el-table-column>
@@ -131,24 +125,20 @@
prop="displayName"
sortable
:sort-method="(a, b) => sortAlphabetically(a, b, 'displayName')">
<template slot-scope="scope">
<span :style="{ color: randomUserColours ? scope.row.$userColour : undefined }" class="name">{{
scope.row.displayName
<template #default="{ row }">
<span :style="{ color: randomUserColours ? row.$userColour : undefined }" class="name">{{
row.displayName
}}</span>
</template>
</el-table-column>
<el-table-column :label="t('table.friendList.rank')" width="110" prop="$trustSortNum" sortable="custom">
<template slot-scope="scope">
<el-table-column :label="t('table.friendList.rank')" width="110" prop="$trustSortNum" :sortable="true">
<template #default="{ row }">
<span
v-if="randomUserColours"
:class="scope.row.$trustClass"
:class="row.$trustClass"
class="name"
v-text="scope.row.$trustLevel"></span>
<span
v-else
class="name"
:style="{ color: scope.row.$userColour }"
v-text="scope.row.$trustLevel"></span>
v-text="row.$trustLevel"></span>
<span v-else class="name" :style="{ color: row.$userColour }" v-text="row.$trustLevel"></span>
</template>
</el-table-column>
<el-table-column
@@ -157,13 +147,13 @@
prop="status"
sortable
:sort-method="(a, b) => sortStatus(a.status, b.status)">
<template slot-scope="scope">
<template #default="{ row }">
<i
v-if="scope.row.status !== 'offline'"
:class="statusClass(scope.row.status)"
v-if="row.status !== 'offline'"
:class="statusClass(row.status)"
style="margin-right: 3px"
class="x-user-status"></i>
<span v-text="scope.row.statusDescription"></span>
<span v-text="row.statusDescription"></span>
</template>
</el-table-column>
<el-table-column
@@ -172,9 +162,9 @@
prop="$languages"
sortable
:sort-method="(a, b) => sortLanguages(a, b)">
<template slot-scope="scope">
<el-tooltip v-for="item in scope.row.$languages" :key="item.key" placement="top">
<template slot="content">
<template #default="{ row }">
<el-tooltip v-for="item in row.$languages" :key="item.key" placement="top">
<template #content>
<span>{{ item.value }} ({{ item.key }})</span>
</template>
<span
@@ -185,9 +175,9 @@
</template>
</el-table-column>
<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">
<template #default="{ row }">
<el-tooltip v-for="(link, index) in row.bioLinks.filter(Boolean)" :key="index">
<template #content>
<span v-text="link"></span>
</template>
<img
@@ -199,7 +189,8 @@
margin-right: 5px;
cursor: pointer;
"
@click.stop="openExternalLink(link)" />
@click.stop="openExternalLink(link)"
loading="lazy" />
</el-tooltip>
</template>
</el-table-column>
@@ -209,8 +200,8 @@
prop="$joinCount"
sortable></el-table-column>
<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 #default="{ row }">
<span v-if="row.$timeSpent">{{ timeToText(row.$timeSpent) }}</span>
</template>
</el-table-column>
<el-table-column
@@ -219,8 +210,8 @@
prop="$lastSeen"
sortable
:sort-method="(a, b) => sortAlphabetically(a, b, '$lastSeen')">
<template slot-scope="scope">
<span>{{ formatDateFilter(scope.row.$lastSeen, 'long') }}</span>
<template #default="{ row }">
<span>{{ formatDateFilter(row.$lastSeen, 'long') }}</span>
</template>
</el-table-column>
<el-table-column
@@ -229,8 +220,8 @@
prop="last_activity"
sortable
:sort-method="(a, b) => sortAlphabetically(a, b, 'last_activity')">
<template slot-scope="scope">
<span>{{ formatDateFilter(scope.row.last_activity, 'long') }}</span>
<template #default="{ row }">
<span>{{ formatDateFilter(row.last_activity, 'long') }}</span>
</template>
</el-table-column>
<el-table-column
@@ -239,8 +230,8 @@
prop="last_login"
sortable
:sort-method="(a, b) => sortAlphabetically(a, b, 'last_login')">
<template slot-scope="scope">
<span>{{ formatDateFilter(scope.row.last_login, 'long') }}</span>
<template #default="{ row }">
<span>{{ formatDateFilter(row.last_login, 'long') }}</span>
</template>
</el-table-column>
<el-table-column
@@ -250,24 +241,26 @@
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">
<template slot-scope="scope">
<template #default="{ row }">
<el-button
type="text"
icon="el-icon-close"
:icon="Close"
style="color: #f56c6c"
size="mini"
@click.stop="confirmDeleteFriend(scope.row.id)"></el-button>
size="small"
@click.stop="confirmDeleteFriend(row.id)"></el-button>
</template>
</el-table-column>
</data-tables>
</DataTable>
</div>
</div>
</template>
<script setup>
import { ElMessageBox } from 'element-plus';
import { Loading, Refresh, Close, RefreshLeft } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
import { getCurrentInstance, nextTick, reactive, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { nextTick, reactive, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { friendRequest, userRequest } from '../../api';
import removeConfusables, { removeWhitespace } from '../../service/confusables';
import {
@@ -293,13 +286,11 @@
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 { randomUserColours } = storeToRefs(useAppearanceSettingsStore());
const { showUserDialog } = useUserStore();
const { menuActiveIndex } = storeToRefs(useUiStore());
const { stringComparer, friendsListSearch } = storeToRefs(useSearchStore());
@@ -308,7 +299,7 @@
const friendsListSearchFilters = ref([]);
const friendsListTable = reactive({
data: [],
tableProps: { stripe: true, size: 'mini', defaultSort: { prop: '$friendNumber', order: 'descending' } },
tableProps: { stripe: true, size: 'small', defaultSort: { prop: '$friendNumber', order: 'descending' } },
pageSize: 100,
paginationProps: { small: true, layout: 'sizes,prev,pager,next,total', pageSizes: [50, 100, 250, 500] }
});
@@ -381,7 +372,7 @@
function showBulkUnfriendSelectionConfirm() {
const pending = friendsListTable.data.filter((item) => item.$selected).map((item) => item.displayName);
if (!pending.length) return;
$confirm(
ElMessageBox.confirm(
`Are you sure you want to delete ${pending.length} friends?
This can negatively affect your trust rank,
This action cannot be undone.`,
@@ -392,10 +383,15 @@
type: 'info',
showInput: true,
inputType: 'textarea',
inputValue: pending.join('\r\n'),
callback: (action) => action === 'confirm' && bulkUnfriendSelection()
inputValue: pending.join('\r\n')
}
);
)
.then((action) => {
if (action === 'confirm') {
bulkUnfriendSelection();
}
})
.catch(() => {});
}
function bulkUnfriendSelection() {
+54 -46
View File
@@ -1,36 +1,35 @@
<template>
<div v-show="menuActiveIndex === 'friendLog'" class="x-container">
<data-tables v-bind="friendLogTable">
<template #tool>
<div style="margin: 0 0 10px; display: flex; align-items: center">
<el-select
v-model="friendLogTable.filters[0].value"
multiple
clearable
style="flex: 1"
:placeholder="t('view.friend_log.filter_placeholder')"
@change="saveTableFilters">
<el-option
v-for="type in [
'Friend',
'Unfriend',
'FriendRequest',
'CancelFriendRequest',
'DisplayName',
'TrustLevel'
]"
:key="type"
:label="t('view.friend_log.filters.' + type)"
:value="type" />
</el-select>
<el-input
v-model="friendLogTable.filters[1].value"
:placeholder="t('view.friend_log.search_placeholder')"
style="flex: none; width: 150px; margin-left: 10px" />
</div>
</template>
<!-- 工具栏 -->
<div style="margin: 0 0 10px; display: flex; align-items: center">
<el-select
v-model="friendLogTable.filters[0].value"
multiple
clearable
style="flex: 1"
:placeholder="t('view.friend_log.filter_placeholder')"
@change="saveTableFilters">
<el-option
v-for="type in [
'Friend',
'Unfriend',
'FriendRequest',
'CancelFriendRequest',
'DisplayName',
'TrustLevel'
]"
:key="type"
:label="t('view.friend_log.filters.' + type)"
:value="type" />
</el-select>
<el-input
v-model="friendLogTable.filters[1].value"
:placeholder="t('view.friend_log.search_placeholder')"
style="flex: none; width: 150px; margin-left: 10px" />
</div>
<el-table-column :label="t('table.friendLog.date')" prop="created_at" sortable="custom" width="200">
<DataTable v-bind="friendLogTable">
<el-table-column :label="t('table.friendLog.date')" prop="created_at" :sortable="true" width="200">
<template #default="scope">
<el-tooltip placement="right">
<template #content>
@@ -50,7 +49,7 @@
<el-table-column :label="t('table.friendLog.user')" prop="displayName">
<template #default="scope">
<span v-if="scope.row.type === 'DisplayName'">
{{ scope.row.previousDisplayName }} <i class="el-icon-right"></i>&nbsp;
{{ scope.row.previousDisplayName }} <el-icon><Right /></el-icon>&nbsp;
</span>
<span
class="x-link"
@@ -59,7 +58,7 @@
v-text="scope.row.displayName || scope.row.userId"></span>
<template v-if="scope.row.type === 'TrustLevel'">
<span>
({{ scope.row.previousTrustLevel }} <i class="el-icon-right"></i>
({{ scope.row.previousTrustLevel }} <el-icon><Right /></el-icon>
{{ scope.row.trustLevel }})</span
>
</template>
@@ -72,25 +71,29 @@
v-if="shiftHeld"
style="color: #f56c6c"
type="text"
icon="el-icon-close"
size="mini"
:icon="Close"
size="small"
class="button-pd-0"
@click="deleteFriendLog(scope.row)"></el-button>
<el-button
v-else
type="text"
icon="el-icon-delete"
size="mini"
:icon="Delete"
size="small"
class="button-pd-0"
@click="deleteFriendLogPrompt(scope.row)"></el-button>
</template>
</el-table-column>
</data-tables>
</DataTable>
</div>
</template>
<script setup>
import { ElMessageBox } from 'element-plus';
import { Close, Delete, Right } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
import { getCurrentInstance, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { watch } from 'vue';
import { useI18n } from 'vue-i18n';
import configRepository from '../../service/config';
import { database } from '../../service/database';
import { removeFromArray, formatDateFilter } from '../../shared/utils';
@@ -111,25 +114,30 @@
);
const { t } = useI18n();
const { proxy } = getCurrentInstance();
function saveTableFilters() {
configRepository.setString('VRCX_friendLogTableFilters', JSON.stringify(friendLogTable.value.filters[0].value));
}
function deleteFriendLogPrompt(row) {
proxy.$confirm('Continue? Delete Log', 'Confirm', {
ElMessageBox.confirm('Continue? Delete Log', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
type: 'info'
})
.then((action) => {
if (action === 'confirm') {
deleteFriendLog(row);
}
}
});
})
.catch(() => {});
}
function deleteFriendLog(row) {
removeFromArray(friendLogTable.value.data, row);
database.deleteFriendLogHistory(row.rowId);
}
</script>
<style scoped>
.button-pd-0 {
padding: 0 !important;
}
</style>
+72 -73
View File
@@ -1,52 +1,48 @@
<template>
<div v-show="menuActiveIndex === 'gameLog'" class="x-container">
<data-tables v-loading="gameLogTable.loading" v-bind="gameLogTable" lazy>
<template #tool>
<div style="margin: 0 0 10px; display: flex; align-items: center">
<div style="flex: none; margin-right: 10px; display: flex; align-items: center">
<el-tooltip
placement="bottom"
:content="t('view.feed.favorites_only_tooltip')"
:disabled="hideTooltips">
<el-switch
v-model="gameLogTable.vip"
active-color="#13ce66"
@change="gameLogTableLookup"></el-switch>
</el-tooltip>
</div>
<el-select
v-model="gameLogTable.filter"
multiple
clearable
style="flex: 1"
:placeholder="t('view.game_log.filter_placeholder')"
@change="gameLogTableLookup">
<el-option
v-for="type in [
'Location',
'OnPlayerJoined',
'OnPlayerLeft',
'VideoPlay',
'Event',
'External',
'StringLoad',
'ImageLoad'
]"
:key="type"
:label="t('view.game_log.filters.' + type)"
:value="type"></el-option>
</el-select>
<el-input
v-model="gameLogTable.search"
:placeholder="t('view.game_log.search_placeholder')"
clearable
style="flex: none; width: 150px; margin-left: 10px"
@keyup.native.enter="gameLogTableLookup"
@change="gameLogTableLookup"></el-input>
</div>
</template>
<!-- 工具栏 -->
<div style="margin: 0 0 10px; display: flex; align-items: center">
<div style="flex: none; margin-right: 10px; display: flex; align-items: center">
<el-tooltip placement="bottom" :content="t('view.feed.favorites_only_tooltip')">
<el-switch
v-model="gameLogTable.vip"
active-color="#13ce66"
@change="gameLogTableLookup"></el-switch>
</el-tooltip>
</div>
<el-select
v-model="gameLogTable.filter"
multiple
clearable
style="flex: 1"
:placeholder="t('view.game_log.filter_placeholder')"
@change="gameLogTableLookup">
<el-option
v-for="type in [
'Location',
'OnPlayerJoined',
'OnPlayerLeft',
'VideoPlay',
'Event',
'External',
'StringLoad',
'ImageLoad'
]"
:key="type"
:label="t('view.game_log.filters.' + type)"
:value="type"></el-option>
</el-select>
<el-input
v-model="gameLogTable.search"
:placeholder="t('view.game_log.search_placeholder')"
clearable
style="flex: none; width: 150px; margin-left: 10px"
@keyup.enter="gameLogTableLookup"
@change="gameLogTableLookup"></el-input>
</div>
<el-table-column :label="t('table.gameLog.date')" prop="created_at" sortable="custom" width="120">
<DataTable v-loading="gameLogTable.loading" v-bind="gameLogTable">
<el-table-column :label="t('table.gameLog.date')" prop="created_at" :sortable="true" width="120">
<template #default="scope">
<el-tooltip placement="right">
<template #content>
@@ -59,7 +55,7 @@
<el-table-column :label="t('table.gameLog.type')" prop="type" width="120">
<template #default="scope">
<el-tooltip placement="right" :open-delay="500" :disabled="hideTooltips">
<el-tooltip placement="right" :show-after="500">
<template #content>
<span>{{ t('view.game_log.filters.' + scope.row.type) }}</span>
</template>
@@ -83,6 +79,7 @@
<span>💚</span>
</el-tooltip>
</template>
<span v-else></span>
</template>
</el-table-column>
@@ -167,47 +164,43 @@
v-if="shiftHeld"
style="color: #f56c6c"
type="text"
icon="el-icon-close"
size="mini"
:icon="Close"
size="small"
class="small-button"
@click="deleteGameLogEntry(scope.row)"></el-button>
<el-button
v-else
type="text"
icon="el-icon-delete"
size="mini"
:icon="Delete"
size="small"
class="small-button"
@click="deleteGameLogEntryPrompt(scope.row)"></el-button>
</template>
<el-tooltip placement="top" :content="t('dialog.previous_instances.info')" :disabled="hideTooltips">
<el-tooltip placement="top" :content="t('dialog.previous_instances.info')">
<el-button
v-if="scope.row.type === 'Location'"
type="text"
icon="el-icon-s-data"
size="mini"
:icon="DataLine"
size="small"
class="small-button"
@click="showPreviousInstancesInfoDialog(scope.row.location)"></el-button>
</el-tooltip>
</template>
</el-table-column>
</data-tables>
</DataTable>
</div>
</template>
<script setup>
import { ElMessageBox } from 'element-plus';
import { Close, Delete, DataLine } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
import { getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { database } from '../../service/database';
import { removeFromArray, openExternalLink, formatDateFilter } from '../../shared/utils';
import {
useUserStore,
useUiStore,
useWorldStore,
useAppearanceSettingsStore,
useInstanceStore,
useGameLogStore
} from '../../stores';
import { useUserStore, useUiStore, useWorldStore, useInstanceStore, useGameLogStore } from '../../stores';
import { useSharedFeedStore } from '../../stores';
const { hideTooltips } = storeToRefs(useAppearanceSettingsStore());
const { showWorldDialog } = useWorldStore();
const { lookupUser } = useUserStore();
const { showPreviousInstancesInfoDialog } = useInstanceStore();
@@ -217,8 +210,6 @@
const { updateSharedFeed } = useSharedFeedStore();
const { t } = useI18n();
const { proxy } = getCurrentInstance();
const emit = defineEmits(['updateGameLogSessionTable']);
function deleteGameLogEntry(row) {
@@ -232,15 +223,23 @@
}
function deleteGameLogEntryPrompt(row) {
proxy.$confirm('Continue? Delete Log', 'Confirm', {
ElMessageBox.confirm('Continue? Delete Log', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
type: 'info'
})
.then((action) => {
if (action === 'confirm') {
deleteGameLogEntry(row);
}
}
});
})
.catch(() => {});
}
</script>
<style scoped>
.small-button {
padding: 0;
height: 18px;
}
</style>
+24 -25
View File
@@ -2,19 +2,19 @@
<div v-loading="loginForm.loading" class="x-login-container">
<div class="x-login">
<div style="position: fixed; top: 0; left: 0; margin: 5px">
<el-tooltip placement="top" :content="t('view.login.updater')" :disabled="hideTooltips">
<el-tooltip placement="top" :content="t('view.login.updater')">
<el-button
type="default"
size="mini"
icon="el-icon-download"
size="small"
:icon="Download"
circle
@click="showVRCXUpdateDialog"></el-button>
</el-tooltip>
<el-tooltip placement="top" :content="t('view.login.proxy_settings')" :disabled="hideTooltips">
<el-tooltip placement="top" :content="t('view.login.proxy_settings')">
<el-button
type="default"
size="mini"
icon="el-icon-connection"
size="small"
:icon="Connection"
style="margin-left: 5px"
circle
@click="promptProxySettings"></el-button>
@@ -28,8 +28,12 @@
ref="loginFormRef"
:model="loginForm"
:rules="loginForm.rules"
@submit.native.prevent="handleLogin()">
<el-form-item :label="t('view.login.field.username')" prop="username" required>
@submit.prevent="handleLogin()">
<el-form-item
:label="t('view.login.field.username')"
prop="username"
required
style="display: block">
<el-input
v-model="loginForm.username"
name="username"
@@ -40,7 +44,7 @@
:label="t('view.login.field.password')"
prop="password"
required
style="margin-top: 10px">
style="display: block; margin-top: 10px">
<el-input
v-model="loginForm.password"
type="password"
@@ -49,7 +53,7 @@
clearable
show-password></el-input>
</el-form-item>
<el-checkbox v-model="loginForm.saveCredentials" style="margin-top: 15px">{{
<el-checkbox v-model="loginForm.saveCredentials">{{
t('view.login.field.saveCredentials')
}}</el-checkbox>
<el-checkbox
@@ -66,7 +70,7 @@
<el-input
v-model="loginForm.endpoint"
name="endpoint"
:placeholder="AppGlobal.endpointDomainVrchat"
:placeholder="AppDebug.endpointDomainVrchat"
clearable></el-input>
</el-form-item>
<el-form-item
@@ -77,10 +81,10 @@
<el-input
v-model="loginForm.websocket"
name="websocket"
:placeholder="AppGlobal.websocketDomainVrchat"
:placeholder="AppDebug.websocketDomainVrchat"
clearable></el-input>
</el-form-item>
<el-form-item style="margin-top: 15px">
<el-form-item>
<el-button native-type="submit" type="primary" style="width: 100%">{{
t('view.login.login')
}}</el-button>
@@ -108,7 +112,7 @@
class="x-friend-item"
@click="relogin(user)">
<div class="avatar">
<img v-lazy="userImage(user.user)" />
<img :src="userImage(user.user)" loading="lazy" />
</div>
<div class="detail">
<span class="name" v-text="user.user.displayName"></span>
@@ -117,8 +121,8 @@
</div>
<el-button
type="default"
size="mini"
icon="el-icon-delete"
size="small"
:icon="Delete"
style="margin-left: 10px"
circle
@click.stop="deleteSavedLogin(user.user.id)"></el-button>
@@ -150,20 +154,15 @@
</template>
<script setup>
import { Download, Delete, Connection } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
import { onBeforeUnmount, ref } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import {
useAppearanceSettingsStore,
useAuthStore,
useGeneralSettingsStore,
useVRCXUpdaterStore
} from '../../stores';
import { useI18n } from 'vue-i18n';
import { useAuthStore, useGeneralSettingsStore, useVRCXUpdaterStore } from '../../stores';
import { openExternalLink, userImage } from '../../shared/utils';
import { AppGlobal } from '../../service/appConfig';
import { AppDebug } from '../../service/appConfig';
const { showVRCXUpdateDialog } = useVRCXUpdaterStore();
const { hideTooltips } = storeToRefs(useAppearanceSettingsStore());
const { loginForm, enableCustomEndpoint } = storeToRefs(useAuthStore());
const { toggleCustomEndpoint, relogin, deleteSavedLogin, login } = useAuthStore();
const { promptProxySettings } = useGeneralSettingsStore();
+53 -56
View File
@@ -1,48 +1,45 @@
<template>
<div v-show="menuActiveIndex === 'moderation'" class="x-container">
<data-tables
<!-- 工具栏 -->
<div class="tool-slot">
<el-select
v-model="filters[0].value"
@change="saveTableFilters()"
multiple
clearable
style="flex: 1"
:placeholder="t('view.moderation.filter_placeholder')">
<el-option
v-for="item in moderationTypes"
:key="item"
:label="t('view.moderation.filters.' + item)"
:value="item" />
</el-select>
<el-input
v-model="filters[1].value"
:placeholder="t('view.moderation.search_placeholder')"
class="filter-input" />
<el-tooltip placement="bottom" :content="t('view.moderation.refresh_tooltip')">
<el-button
type="default"
:loading="isPlayerModerationsLoading"
@click="refreshPlayerModerations()"
:icon="Refresh"
circle />
</el-tooltip>
</div>
<DataTable
:data="playerModerationTable.data"
:pageSize="playerModerationTable.pageSize"
:filters="filters"
:tableProps="tableProps"
:paginationProps="paginationProps"
v-loading="isPlayerModerationsLoading">
<template slot="tool">
<div class="tool-slot">
<el-select
v-model="filters[0].value"
@change="saveTableFilters()"
multiple
clearable
style="flex: 1"
:placeholder="t('view.moderation.filter_placeholder')">
<el-option
v-for="item in moderationTypes"
:key="item"
:label="t('view.moderation.filters.' + item)"
:value="item" />
</el-select>
<el-input
v-model="filters[1].value"
:placeholder="t('view.moderation.search_placeholder')"
class="filter-input" />
<el-tooltip
placement="bottom"
:content="t('view.moderation.refresh_tooltip')"
:disabled="hideTooltips">
<el-button
type="default"
: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">
<template slot-scope="scope">
<el-table-column :label="t('table.moderation.date')" prop="created" :sortable="true" width="120">
<template #default="scope">
<el-tooltip placement="right">
<template slot="content">
<template #content>
<span>{{ formatDateFilter(scope.row.created, 'long') }}</span>
</template>
<span>{{ formatDateFilter(scope.row.created, 'short') }}</span>
@@ -50,12 +47,12 @@
</template>
</el-table-column>
<el-table-column :label="t('table.moderation.type')" prop="type" width="100">
<template slot-scope="scope">
<template #default="scope">
<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">
<template slot-scope="scope">
<template #default="scope">
<span
class="x-link"
v-text="scope.row.sourceDisplayName"
@@ -63,7 +60,7 @@
</template>
</el-table-column>
<el-table-column :label="t('table.moderation.target')" prop="targetDisplayName">
<template slot-scope="scope">
<template #default="scope">
<span
class="x-link"
v-text="scope.row.targetDisplayName"
@@ -71,31 +68,33 @@
</template>
</el-table-column>
<el-table-column :label="t('table.moderation.action')" width="80" align="right">
<template slot-scope="scope">
<template #default="scope">
<template v-if="scope.row.sourceUserId === currentUser.id">
<el-button
v-if="shiftHeld"
style="color: #f56c6c"
type="text"
icon="el-icon-close"
size="mini"
:icon="Close"
size="small"
@click="deletePlayerModeration(scope.row)"></el-button>
<el-button
v-else
type="text"
icon="el-icon-close"
size="mini"
:icon="Close"
size="small"
@click="deletePlayerModerationPrompt(scope.row)"></el-button>
</template>
</template>
</el-table-column>
</data-tables>
</DataTable>
</div>
</template>
<script setup>
import { getCurrentInstance, ref } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { ElMessageBox } from 'element-plus';
import { Refresh, Close } from '@element-plus/icons-vue';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { playerModerationRequest } from '../../api';
import configRepository from '../../service/config.js';
@@ -104,9 +103,6 @@
import { formatDateFilter } from '../../shared/utils';
const { t } = useI18n();
const { proxy } = getCurrentInstance();
const { hideTooltips } = storeToRefs(useAppearanceSettingsStore());
const { showUserDialog } = useUserStore();
const { isPlayerModerationsLoading, playerModerationTable } = storeToRefs(useModerationStore());
const { refreshPlayerModerations, handlePlayerModerationDelete } = useModerationStore();
@@ -127,7 +123,7 @@
const tableProps = ref({
stripe: true,
size: 'mini',
size: 'small',
defaultSort: {
prop: 'created',
order: 'descending'
@@ -161,16 +157,17 @@
}
function deletePlayerModerationPrompt(row) {
proxy.$confirm(`Continue? Delete Moderation ${row.type}`, 'Confirm', {
ElMessageBox.confirm(`Continue? Delete Moderation ${row.type}`, 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
type: 'info'
})
.then((action) => {
if (action === 'confirm') {
deletePlayerModeration(row);
}
}
});
})
.catch(() => {});
}
</script>
+176 -175
View File
@@ -1,61 +1,56 @@
<template>
<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">
<el-select
v-model="notificationTable.filters[0].value"
multiple
clearable
style="flex: 1"
:placeholder="t('view.notification.filter_placeholder')"
@change="saveTableFilters">
<el-option
v-for="type in [
'requestInvite',
'invite',
'requestInviteResponse',
'inviteResponse',
'friendRequest',
'ignoredFriendRequest',
'message',
'boop',
'event.announcement',
'groupChange',
'group.announcement',
'group.informative',
'group.invite',
'group.joinRequest',
'group.transfer',
'group.queueReady',
'moderation.warning.group',
'moderation.report.closed',
'instance.closed'
]"
:key="type"
:label="t('view.notification.filters.' + type)"
:value="type" />
</el-select>
<el-input
v-model="notificationTable.filters[1].value"
:placeholder="t('view.notification.search_placeholder')"
style="flex: none; width: 150px; margin: 0 10px" />
<el-tooltip
placement="bottom"
:content="t('view.notification.refresh_tooltip')"
:disabled="hideTooltips">
<el-button
type="default"
:loading="isNotificationsLoading"
icon="el-icon-refresh"
circle
style="flex: none"
@click="refreshNotifications()" />
</el-tooltip>
</div>
</template>
<div style="margin: 0 0 10px; display: flex; align-items: center">
<el-select
v-model="notificationTable.filters[0].value"
multiple
clearable
style="flex: 1"
:placeholder="t('view.notification.filter_placeholder')"
@change="saveTableFilters">
<el-option
v-for="type in [
'requestInvite',
'invite',
'requestInviteResponse',
'inviteResponse',
'friendRequest',
'ignoredFriendRequest',
'message',
'boop',
'event.announcement',
'groupChange',
'group.announcement',
'group.informative',
'group.invite',
'group.joinRequest',
'group.transfer',
'group.queueReady',
'moderation.warning.group',
'moderation.report.closed',
'instance.closed'
]"
:key="type"
:label="t('view.notification.filters.' + type)"
:value="type" />
</el-select>
<el-input
v-model="notificationTable.filters[1].value"
:placeholder="t('view.notification.search_placeholder')"
style="flex: none; width: 150px; margin: 0 10px" />
<el-tooltip placement="bottom" :content="t('view.notification.refresh_tooltip')">
<el-button
type="default"
:loading="isNotificationsLoading"
:icon="Refresh"
circle
style="flex: none"
@click="refreshNotifications()" />
</el-tooltip>
</div>
<el-table-column :label="t('table.notification.date')" prop="created_at" sortable="custom" width="120">
<DataTable v-bind="notificationTable" ref="notificationTableRef" class="notification-table">
<el-table-column :label="t('table.notification.date')" prop="created_at" :sortable="true" width="120">
<template #default="scope">
<el-tooltip placement="right">
<template #content>
@@ -89,11 +84,7 @@
@click="showWorldDialog(scope.row.location)"
v-text="t('view.notification.filters.' + scope.row.type)"></span>
</el-tooltip>
<el-tooltip
v-else-if="scope.row.link"
placement="top"
:content="scope.row.linkText"
:disabled="hideTooltips">
<el-tooltip v-else-if="scope.row.link" placement="top" :content="scope.row.linkText">
<span
class="x-link"
@click="openNotificationLink(scope.row.link)"
@@ -135,33 +126,35 @@
<el-table-column :label="t('table.notification.photo')" width="100" prop="photo">
<template #default="scope">
<template v-if="scope.row.details && scope.row.details.imageUrl">
<el-popover placement="right" width="500px" trigger="click">
<el-popover placement="right" :width="500" trigger="click">
<template #reference>
<img
class="x-link"
:src="getSmallThumbnailUrl(scope.row.details.imageUrl)"
style="flex: none; height: 50px; border-radius: 4px" />
style="flex: none; height: 50px; border-radius: 4px"
loading="lazy" />
</template>
<img
v-lazy="scope.row.details.imageUrl"
class="x-link"
style="width: 500px"
@click="showFullscreenImageDialog(scope.row.details.imageUrl)" />
:src="scope.row.details.imageUrl"
:class="['x-link', 'x-popover-image']"
@click="showFullscreenImageDialog(scope.row.details.imageUrl)"
loading="lazy" />
</el-popover>
</template>
<template v-else-if="scope.row.imageUrl">
<el-popover placement="right" width="500px" trigger="click">
<el-popover placement="right" :width="500" trigger="click">
<template #reference>
<img
class="x-link"
:src="getSmallThumbnailUrl(scope.row.imageUrl)"
style="flex: none; height: 50px; border-radius: 4px" />
style="flex: none; height: 50px; border-radius: 4px"
loading="lazy" />
</template>
<img
v-lazy="scope.row.imageUrl"
class="x-link"
style="width: 500px"
@click="showFullscreenImageDialog(scope.row.imageUrl)" />
:src="scope.row.imageUrl"
:class="['x-link', 'x-popover-image']"
@click="showFullscreenImageDialog(scope.row.imageUrl)"
loading="lazy" />
</el-popover>
</template>
</template>
@@ -178,28 +171,12 @@
:link="true" />
<br v-if="scope.row.details" />
</span>
<el-tooltip
<div
v-if="
scope.row.message &&
scope.row.message !== `This is a generated invite to ${scope.row.details?.worldName}`
"
placement="top">
<template #content>
<pre
class="extra"
style="
display: inline-block;
vertical-align: top;
font-family: inherit;
font-size: 12px;
white-space: pre-wrap;
margin: 0;
"
>{{ scope.row.message || '-' }}</pre
>
</template>
<div v-text="scope.row.message"></div>
</el-tooltip>
v-text="scope.row.message"></div>
<span
v-else-if="scope.row.details && scope.row.details.inviteMessage"
v-text="scope.row.details.inviteMessage"></span>
@@ -216,86 +193,85 @@
<template #default="scope">
<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-tooltip placement="top" content="Accept">
<el-button
type="text"
icon="el-icon-check"
:icon="Check"
style="color: #67c23a"
size="mini"
size="small"
class="button-pd-0"
@click="acceptFriendRequestNotification(scope.row)" />
</el-tooltip>
</template>
<template v-else-if="scope.row.type === 'invite'">
<el-tooltip placement="top" content="Decline with message" :disabled="hideTooltips">
<el-tooltip placement="top" content="Decline with message">
<el-button
type="text"
icon="el-icon-chat-line-square"
size="mini"
:icon="ChatLineSquare"
size="small"
class="button-pd-0"
@click="showSendInviteResponseDialog(scope.row)" />
</el-tooltip>
</template>
<template v-else-if="scope.row.type === 'requestInvite'">
<template
v-if="lastLocation.location && isGameRunning && checkCanInvite(lastLocation.location)">
<el-tooltip placement="top" content="Invite" :disabled="hideTooltips">
<el-tooltip placement="top" content="Invite">
<el-button
type="text"
icon="el-icon-check"
:icon="Check"
style="color: #67c23a"
size="mini"
size="small"
class="button-pd-0"
@click="acceptRequestInvite(scope.row)" />
</el-tooltip>
</template>
<el-tooltip placement="top" content="Decline with message" :disabled="hideTooltips">
<el-tooltip placement="top" content="Decline with message">
<el-button
type="text"
icon="el-icon-chat-line-square"
size="mini"
style="margin-left: 5px"
:icon="ChatLineSquare"
size="small"
:class="['button-pd-0', 'ml-5']"
@click="showSendInviteRequestResponseDialog(scope.row)" />
</el-tooltip>
</template>
<template v-if="scope.row.responses">
<template v-for="response in scope.row.responses">
<el-tooltip
placement="top"
:content="response.text"
:disabled="hideTooltips"
:key="response.text">
<template v-for="response in scope.row.responses" :key="response.text">
<el-tooltip placement="top" :content="response.text">
<el-button
v-if="response.icon === 'check'"
type="text"
icon="el-icon-check"
size="mini"
style="margin-left: 5px"
:icon="Check"
size="small"
:class="['button-pd-0', 'ml-5']"
@click="
sendNotificationResponse(scope.row.id, scope.row.responses, response.type)
" />
<el-button
v-else-if="response.icon === 'cancel'"
type="text"
icon="el-icon-close"
size="mini"
style="margin-left: 5px"
:icon="Close"
size="small"
:class="['button-pd-0', 'ml-5']"
@click="
sendNotificationResponse(scope.row.id, scope.row.responses, response.type)
" />
<el-button
v-else-if="response.icon === 'ban'"
type="text"
icon="el-icon-circle-close"
size="mini"
style="margin-left: 5px"
:icon="CircleClose"
size="small"
:class="['button-pd-0', 'ml-5']"
@click="
sendNotificationResponse(scope.row.id, scope.row.responses, response.type)
" />
<el-button
v-else-if="response.icon === 'bell-slash'"
type="text"
icon="el-icon-bell"
size="mini"
style="margin-left: 5px"
:icon="Bell"
size="small"
:class="['button-pd-0', 'ml-5']"
@click="
sendNotificationResponse(scope.row.id, scope.row.responses, response.type)
" />
@@ -309,18 +285,18 @@
<el-button
v-else-if="response.icon === 'reply'"
type="text"
icon="el-icon-chat-line-square"
size="mini"
style="margin-left: 5px"
:icon="ChatLineSquare"
size="small"
:class="['button-pd-0', 'ml-5']"
@click="
sendNotificationResponse(scope.row.id, scope.row.responses, response.type)
" />
<el-button
v-else
type="text"
icon="el-icon-collection-tag"
size="mini"
style="margin-left: 5px"
:icon="CollectionTag"
size="small"
:class="['button-pd-0', 'ml-5']"
@click="
sendNotificationResponse(scope.row.id, scope.row.responses, response.type)
" />
@@ -339,39 +315,41 @@
!scope.row.type.includes('moderation.') &&
!scope.row.type.includes('instance.')
">
<el-tooltip placement="top" content="Decline" :disabled="hideTooltips">
<el-tooltip placement="top" content="Decline">
<el-button
v-if="shiftHeld"
style="color: #f56c6c; margin-left: 5px"
type="text"
icon="el-icon-close"
size="mini"
:icon="Close"
size="small"
class="button-pd-0"
@click="hideNotification(scope.row)" />
<el-button
v-else
type="text"
icon="el-icon-close"
size="mini"
style="margin-left: 5px"
:icon="Close"
size="small"
:class="['button-pd-0', 'ml-5']"
@click="hideNotificationPrompt(scope.row)" />
</el-tooltip>
</template>
</template>
<template v-if="scope.row.type === 'group.queueReady'">
<el-tooltip placement="top" content="Delete log" :disabled="hideTooltips">
<el-tooltip placement="top" content="Delete log">
<el-button
v-if="shiftHeld"
style="color: #f56c6c; margin-left: 5px"
type="text"
icon="el-icon-close"
size="mini"
:icon="Close"
size="small"
class="button-pd-0"
@click="deleteNotificationLog(scope.row)" />
<el-button
v-else
type="text"
icon="el-icon-delete"
size="mini"
style="margin-left: 5px"
:icon="Delete"
size="small"
:class="['button-pd-0', 'ml-5']"
@click="deleteNotificationLogPrompt(scope.row)" />
</el-tooltip>
</template>
@@ -383,39 +361,53 @@
!scope.row.type.includes('group.') &&
!scope.row.type.includes('moderation.')
">
<el-tooltip placement="top" content="Delete log" :disabled="hideTooltips">
<el-tooltip placement="top" content="Delete log">
<el-button
v-if="shiftHeld"
style="color: #f56c6c; margin-left: 5px"
type="text"
icon="el-icon-close"
size="mini"
:icon="Close"
size="small"
class="button-pd-0"
@click="deleteNotificationLog(scope.row)" />
<el-button
v-else
type="text"
icon="el-icon-delete"
size="mini"
style="margin-left: 5px"
:icon="Delete"
size="small"
:class="['button-pd-0', 'ml-5']"
@click="deleteNotificationLogPrompt(scope.row)" />
</el-tooltip>
</template>
</template>
</el-table-column>
</data-tables>
</DataTable>
<SendInviteResponseDialog
:send-invite-response-dialog="sendInviteResponseDialog"
:send-invite-response-dialog-visible.sync="sendInviteResponseDialogVisible" />
:send-invite-response-dialog-visible="sendInviteResponseDialogVisible" />
<SendInviteRequestResponseDialog
:send-invite-response-dialog="sendInviteResponseDialog"
:send-invite-request-response-dialog-visible.sync="sendInviteRequestResponseDialogVisible" />
:send-invite-request-response-dialog-visible="sendInviteRequestResponseDialogVisible" />
</div>
</template>
<script setup>
import { ElMessage, ElMessageBox } from 'element-plus';
import {
Refresh,
Check,
ChatLineSquare,
Close,
CircleClose,
Bell,
CollectionTag,
Delete
} from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
import { getCurrentInstance, ref } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { friendRequest, notificationRequest, worldRequest } from '../../api';
import {
checkCanInvite,
@@ -428,7 +420,6 @@
import configRepository from '../../service/config';
import { database } from '../../service/database';
import {
useAppearanceSettingsStore,
useGalleryStore,
useGameStore,
useGroupStore,
@@ -443,7 +434,6 @@
import SendInviteResponseDialog from './dialogs/SendInviteResponseDialog.vue';
import Noty from 'noty';
const { hideTooltips } = storeToRefs(useAppearanceSettingsStore());
const { showUserDialog } = useUserStore();
const { showWorldDialog } = useWorldStore();
const { showGroupDialog } = useGroupStore();
@@ -459,8 +449,6 @@
const { t } = useI18n();
const { $confirm, $message } = getCurrentInstance().proxy;
const sendInviteResponseDialog = ref({
messageSlot: {},
invite: {}
@@ -511,18 +499,19 @@
function acceptFriendRequestNotification(row) {
// FIXME: 메시지 수정
$confirm('Continue? Accept Friend Request', 'Confirm', {
ElMessageBox.confirm('Continue? Accept Friend Request', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
type: 'info'
})
.then((action) => {
if (action === 'confirm') {
notificationRequest.acceptFriendRequestNotification({
notificationId: row.id
});
}
}
});
})
.catch(() => {});
}
function showSendInviteResponseDialog(invite) {
@@ -534,11 +523,12 @@
}
function acceptRequestInvite(row) {
$confirm('Continue? Send Invite', 'Confirm', {
ElMessageBox.confirm('Continue? Send Invite', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
type: 'info'
})
.then((action) => {
if (action === 'confirm') {
let currentLocation = lastLocation.value.location;
if (lastLocation.value.location === 'traveling') {
@@ -561,7 +551,7 @@
row.senderUserId
)
.then((_args) => {
$message('Invite sent');
ElMessage('Invite sent');
notificationRequest.hideNotification({
notificationId: row.id
});
@@ -569,8 +559,8 @@
});
});
}
}
});
})
.catch(() => {});
}
function showSendInviteRequestResponseDialog(invite) {
@@ -635,16 +625,17 @@
}
function hideNotificationPrompt(row) {
$confirm(`Continue? Decline ${row.type}`, 'Confirm', {
ElMessageBox.confirm(`Continue? Decline ${row.type}`, 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
type: 'info'
})
.then((action) => {
if (action === 'confirm') {
hideNotification(row);
}
}
});
})
.catch(() => {});
}
function deleteNotificationLog(row) {
@@ -655,15 +646,25 @@
}
function deleteNotificationLogPrompt(row) {
$confirm(`Continue? Delete ${row.type}`, 'Confirm', {
ElMessageBox.confirm(`Continue? Delete ${row.type}`, 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
type: 'info'
})
.then((action) => {
if (action === 'confirm') {
deleteNotificationLog(row);
}
}
});
})
.catch(() => {});
}
</script>
<style lang="scss" scoped>
.button-pd-0 {
padding: 0;
}
.ml-5 {
margin-left: 5px !important; // due to ".el-button + .el-button"
}
</style>
@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible.sync="editAndSendInviteResponseDialog.visible"
v-model="editAndSendInviteResponseDialog.visible"
:title="t('dialog.edit_send_invite_response_message.header')"
width="400px"
append-to-body>
@@ -11,7 +11,7 @@
<el-input
v-model="editAndSendInviteResponseDialog.newMessage"
type="textarea"
size="mini"
size="small"
maxlength="64"
show-word-limit
:autosize="{ minRows: 2, maxRows: 5 }"
@@ -19,26 +19,25 @@
style="margin-top: 10px">
</el-input>
<template #footer>
<el-button type="small" @click="cancelEditAndSendInviteResponse">{{
<el-button @click="cancelEditAndSendInviteResponse">{{
t('dialog.edit_send_invite_response_message.cancel')
}}</el-button>
<el-button type="primary" size="small" @click="saveEditAndSendInviteResponse">{{
t('dialog.edit_send_invite_response_message.send')
}}</el-button>
</template>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ElMessage } from 'element-plus';
import { storeToRefs } from 'pinia';
import { getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { inviteMessagesRequest, 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);
@@ -76,13 +75,13 @@
})
.then((args) => {
if (args.json[slot].message === I.messageSlot.message) {
$message({
ElMessage({
message: "VRChat API didn't update message, try again",
type: 'error'
});
throw new Error("VRChat API didn't update message, try again");
} else {
$message('Invite message updated');
ElMessage('Invite message updated');
}
return args;
});
@@ -101,7 +100,7 @@
notificationRequest.hideNotification({
notificationId: I.invite.id
});
$message({
ElMessage({
message: 'Invite response message sent',
type: 'success'
});
@@ -120,7 +119,7 @@
notificationRequest.hideNotification({
notificationId: I.invite.id
});
$message({
ElMessage({
message: 'Invite response message sent',
type: 'success'
});
@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible="sendInviteRequestResponseDialogVisible"
:model-value="sendInviteRequestResponseDialogVisible"
:title="t('dialog.invite_request_response_message.header')"
width="800px"
append-to-body
@@ -10,17 +10,17 @@
<input class="inviteImageUploadButton" type="file" accept="image/*" @change="inviteImageUpload" />
</template>
<data-tables
<DataTable
v-bind="inviteRequestResponseMessageTable"
style="margin-top: 10px; cursor: pointer"
@row-click="showSendInviteResponseConfirmDialog">
<el-table-column :label="t('table.profile.invite_messages.slot')" prop="slot" sortable="custom" width="70">
<el-table-column :label="t('table.profile.invite_messages.slot')" prop="slot" :sortable="true" width="70">
</el-table-column>
<el-table-column :label="t('table.profile.invite_messages.message')" prop="message"> </el-table-column>
<el-table-column
:label="t('table.profile.invite_messages.cool_down')"
prop="updatedAt"
sortable="custom"
:sortable="true"
width="110"
align="right">
<template #default="scope">
@@ -31,37 +31,42 @@
<template #default="scope">
<el-button
type="text"
icon="el-icon-edit"
size="mini"
:icon="Edit"
size="small"
@click.stop="showEditAndSendInviteResponseDialog(scope.row)">
</el-button>
</template>
</el-table-column>
</data-tables>
</DataTable>
<template #footer>
<el-button type="small" @click="cancelSendInviteRequestResponse">
<el-button @click="cancelSendInviteRequestResponse">
{{ t('dialog.invite_request_response_message.cancel') }}
</el-button>
<el-button type="small" @click="refreshInviteMessageTableData('requestResponse')">
<el-button @click="refreshInviteMessageTableData('requestResponse')">
{{ t('dialog.invite_request_response_message.refresh') }}
</el-button>
</template>
<EditAndSendInviteResponseDialog
:edit-and-send-invite-response-dialog.sync="editAndSendInviteResponseDialog"
:send-invite-response-dialog.sync="sendInviteResponseDialog"
:edit-and-send-invite-response-dialog="editAndSendInviteResponseDialog"
:send-invite-response-dialog="sendInviteResponseDialog"
@update:edit-and-send-invite-response-dialog="editAndSendInviteResponseDialog = $event"
@update:send-invite-response-dialog="sendInviteResponseDialog = $event"
@closeInviteDialog="closeInviteDialog" />
<SendInviteResponseConfirmDialog
:send-invite-response-dialog.sync="sendInviteResponseDialog"
:send-invite-response-dialog="sendInviteResponseDialog"
:send-invite-response-confirm-dialog="sendInviteResponseConfirmDialog"
@update:send-invite-response-dialog="sendInviteResponseDialog = $event"
@closeInviteDialog="closeInviteDialog" />
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { Edit } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { useGalleryStore, useInviteStore, useUserStore } from '../../../stores';
import EditAndSendInviteResponseDialog from './EditAndSendInviteResponseDialog.vue';
import SendInviteResponseConfirmDialog from './SendInviteResponseConfirmDialog.vue';
@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible="sendInviteResponseConfirmDialog.visible"
:model-value="sendInviteResponseConfirmDialog.visible"
:title="t('dialog.invite_response_message.header')"
width="400px"
append-to-body
@@ -11,27 +11,23 @@
</div>
<template #footer>
<el-button type="small" @click="cancelInviteResponseConfirm">{{
t('dialog.invite_response_message.cancel')
}}</el-button>
<el-button @click="cancelInviteResponseConfirm">{{ t('dialog.invite_response_message.cancel') }}</el-button>
<el-button type="primary" size="small" @click="sendInviteResponseConfirm">{{
t('dialog.invite_response_message.confirm')
}}</el-button>
</template>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ElMessage } from 'element-plus';
import { storeToRefs } from 'pinia';
import { getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { 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);
@@ -69,7 +65,7 @@
notificationRequest.hideNotification({
notificationId: D.invite.id
});
$message({
ElMessage({
message: 'Invite response photo message sent',
type: 'success'
});
@@ -88,7 +84,7 @@
notificationRequest.hideNotification({
notificationId: D.invite.id
});
$message({
ElMessage({
message: 'Invite response message sent',
type: 'success'
});
@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible="sendInviteResponseDialogVisible"
:model-value="sendInviteResponseDialogVisible"
:title="t('dialog.invite_response_message.header')"
width="800px"
append-to-body
@@ -10,20 +10,16 @@
<input class="inviteImageUploadButton" type="file" accept="image/*" @change="inviteImageUpload" />
</template>
<data-tables
<DataTable
v-bind="inviteResponseMessageTable"
style="margin-top: 10px; cursor: pointer"
@row-click="showSendInviteResponseConfirmDialog">
<el-table-column
:label="t('table.profile.invite_messages.slot')"
prop="slot"
sortable="custom"
width="70" />
<el-table-column :label="t('table.profile.invite_messages.slot')" prop="slot" :sortable="true" width="70" />
<el-table-column :label="t('table.profile.invite_messages.message')" prop="message" />
<el-table-column
:label="t('table.profile.invite_messages.cool_down')"
prop="updatedAt"
sortable="custom"
:sortable="true"
width="110"
align="right">
<template #default="scope">
@@ -34,36 +30,39 @@
<template #default="scope">
<el-button
type="text"
icon="el-icon-edit"
size="mini"
:icon="Edit"
size="small"
@click.stop="showEditAndSendInviteResponseDialog(scope.row)" />
</template>
</el-table-column>
</data-tables>
</DataTable>
<template #footer>
<el-button type="small" @click="cancelSendInviteResponse">{{
t('dialog.invite_response_message.cancel')
}}</el-button>
<el-button type="small" @click="refreshInviteMessageTableData('response')">{{
<el-button @click="cancelSendInviteResponse">{{ t('dialog.invite_response_message.cancel') }}</el-button>
<el-button @click="refreshInviteMessageTableData('response')">{{
t('dialog.invite_response_message.refresh')
}}</el-button>
</template>
<EditAndSendInviteResponseDialog
:edit-and-send-invite-response-dialog.sync="editAndSendInviteResponseDialog"
:send-invite-response-dialog.sync="sendInviteResponseDialog"
:edit-and-send-invite-response-dialog="editAndSendInviteResponseDialog"
:send-invite-response-dialog="sendInviteResponseDialog"
@update:edit-and-send-invite-response-dialog="editAndSendInviteResponseDialog = $event"
@update:send-invite-response-dialog="sendInviteResponseDialog = $event"
@closeInviteDialog="closeInviteDialog" />
<SendInviteResponseConfirmDialog
:send-invite-response-dialog.sync="sendInviteResponseDialog"
:send-invite-response-dialog="sendInviteResponseDialog"
:send-invite-response-confirm-dialog="sendInviteResponseConfirmDialog"
@update:send-invite-response-dialog="sendInviteResponseDialog = $event"
@closeInviteDialog="closeInviteDialog" />
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { Edit } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { useGalleryStore, useInviteStore, useUserStore } from '../../../stores';
import EditAndSendInviteResponseDialog from './EditAndSendInviteResponseDialog.vue';
import SendInviteResponseConfirmDialog from './SendInviteResponseConfirmDialog.vue';
+102 -86
View File
@@ -2,17 +2,19 @@
<div v-show="menuActiveIndex === 'playerList'" class="x-container" style="padding-top: 5px">
<div style="display: flex; flex-direction: column; height: 100%">
<div v-if="currentInstanceWorld.ref.id" style="display: flex">
<el-popover placement="right" width="500px" trigger="click" style="height: 120px">
<el-popover placement="right" :width="500" trigger="click" style="height: 120px">
<template #reference>
<img
:src="currentInstanceWorld.ref.thumbnailImageUrl"
class="x-link"
style="flex: none; width: 160px; height: 120px; border-radius: 4px"
loading="lazy" />
</template>
<img
slot="reference"
v-lazy="currentInstanceWorld.ref.thumbnailImageUrl"
class="x-link"
style="flex: none; width: 160px; height: 120px; border-radius: 4px" />
<img
v-lazy="currentInstanceWorld.ref.imageUrl"
class="x-link"
style="width: 500px; height: 375px"
@click="showFullscreenImageDialog(currentInstanceWorld.ref.imageUrl)" />
:src="currentInstanceWorld.ref.imageUrl"
:class="['x-link', 'x-popover-image']"
@click="showFullscreenImageDialog(currentInstanceWorld.ref.imageUrl)"
loading="lazy" />
</el-popover>
<div style="margin-left: 10px; display: flex; flex-direction: column; min-width: 320px; width: 100%">
<div>
@@ -24,16 +26,17 @@
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
line-clamp: 1;
"
@click="showWorldDialog(currentInstanceWorld.ref.id)">
<i
v-show="
<el-icon
v-if="
currentUser.$homeLocation &&
currentUser.$homeLocation.worldId === currentInstanceWorld.ref.id
"
class="el-icon-s-home"
style="margin-right: 5px"></i>
style="margin-right: 5px"
><HomeFilled
/></el-icon>
{{ currentInstanceWorld.ref.name }}
</span>
</div>
@@ -49,7 +52,7 @@
v-if="currentInstanceWorld.ref.$isLabs"
type="primary"
effect="plain"
size="mini"
size="small"
style="margin-right: 5px"
>{{ t('dialog.world.tags.labs') }}</el-tag
>
@@ -57,7 +60,7 @@
v-else-if="currentInstanceWorld.ref.releaseStatus === 'public'"
type="success"
effect="plain"
size="mini"
size="small"
style="margin-right: 5px"
>{{ t('dialog.world.tags.public') }}</el-tag
>
@@ -65,7 +68,7 @@
v-else-if="currentInstanceWorld.ref.releaseStatus === 'private'"
type="danger"
effect="plain"
size="mini"
size="small"
style="margin-right: 5px"
>{{ t('dialog.world.tags.private') }}</el-tag
>
@@ -74,13 +77,12 @@
class="x-tag-platform-pc"
type="info"
effect="plain"
size="mini"
size="small"
style="margin-right: 5px"
>PC
<span
v-if="currentInstanceWorld.bundleSizes['standalonewindows']"
class="x-grey"
style="margin-left: 5px; border-left: inherit; padding-left: 5px"
:class="['x-grey', 'x-tag-platform-pc', 'x-tag-border-left']"
>{{ currentInstanceWorld.bundleSizes['standalonewindows'].fileSize }}</span
>
</el-tag>
@@ -89,28 +91,26 @@
class="x-tag-platform-quest"
type="info"
effect="plain"
size="mini"
size="small"
style="margin-right: 5px"
>Android
<span
v-if="currentInstanceWorld.bundleSizes['android']"
class="x-grey"
style="margin-left: 5px; border-left: inherit; padding-left: 5px"
:class="['x-grey', 'x-tag-platform-quest', 'x-tag-border-left']"
>{{ currentInstanceWorld.bundleSizes['android'].fileSize }}</span
>
</el-tag>
<el-tag
v-if="currentInstanceWorld.isIOS"
v-if="currentInstanceWorld.isIos"
class="x-tag-platform-ios"
type="info"
effect="plain"
size="mini"
size="small"
style="margin-right: 5px"
>iOS
<span
v-if="currentInstanceWorld.bundleSizes['ios']"
class="x-grey"
style="margin-left: 5px; border-left: inherit; padding-left: 5px"
:class="['x-grey', 'x-tag-platform-ios', 'x-tag-border-left']"
>{{ currentInstanceWorld.bundleSizes['ios'].fileSize }}</span
>
</el-tag>
@@ -118,7 +118,7 @@
v-if="currentInstanceWorld.avatarScalingDisabled"
type="warning"
effect="plain"
size="mini"
size="small"
style="margin-right: 5px; margin-top: 5px"
>{{ t('dialog.world.tags.avatar_scaling_disabled') }}</el-tag
>
@@ -126,7 +126,7 @@
v-if="currentInstanceWorld.inCache"
type="info"
effect="plain"
size="mini"
size="small"
style="margin-right: 5px">
<span>{{ currentInstanceWorld.cacheSize }} {{ t('dialog.world.tags.cache') }}</span>
</el-tag>
@@ -160,7 +160,7 @@
!currentInstanceWorldDescriptionExpanded
"
type="text"
size="mini"
size="small"
@click="currentInstanceWorldDescriptionExpanded = true"
>{{ !currentInstanceWorldDescriptionExpanded && 'Show more' }}</el-button
>
@@ -219,10 +219,7 @@
<el-button style="margin-left: 10px" @click="showChatboxBlacklistDialog">{{
t('view.player_list.photon.chatbox_blacklist')
}}</el-button>
<el-tooltip
placement="bottom"
:content="t('view.player_list.photon.status_tooltip')"
:disabled="hideTooltips">
<el-tooltip placement="bottom" :content="t('view.player_list.photon.status_tooltip')">
<div
style="
display: inline-block;
@@ -239,7 +236,7 @@
</div>
<el-tabs type="card">
<el-tab-pane :label="t('view.player_list.photon.current')">
<data-tables v-bind="photonEventTable" style="margin-bottom: 10px">
<DataTable v-bind="photonEventTable" style="margin-bottom: 10px">
<el-table-column :label="t('table.playerList.date')" prop="created_at" width="120">
<template #default="scope">
<el-tooltip placement="right">
@@ -272,7 +269,7 @@
v-text="scope.row.avatar.name"></span>
&nbsp;
<span v-if="!scope.row.inCache" style="color: #aaa"
><i class="el-icon-download"></i>&nbsp;</span
><el-icon><Download /></el-icon>&nbsp;</span
>
<span
v-if="scope.row.avatar.releaseStatus === 'public'"
@@ -315,7 +312,7 @@
:class="statusClass(scope.row.previousStatus)"></i>
</el-tooltip>
<span>
<i class="el-icon-right"></i>
<el-icon><ArrowRight /></el-icon>
</span>
<el-tooltip placement="top">
<template #content>
@@ -357,7 +354,7 @@
@click="showGroupDialog(scope.row.previousGroupId)"
v-text="scope.row.previousGroupId"></span>
<span>
<i class="el-icon-right"></i>
<el-icon><ArrowRight /></el-icon>
</span>
<span
v-if="scope.row.groupName"
@@ -401,7 +398,7 @@
v-text="scope.row.avatar.name"></span>
&nbsp;
<span v-if="!scope.row.inCache" style="color: #aaa"
><i class="el-icon-download"></i>&nbsp;</span
><el-icon><Download /></el-icon>&nbsp;</span
>
<span
v-if="scope.row.avatar.releaseStatus === 'public'"
@@ -419,10 +416,11 @@
<el-tooltip placement="right">
<template #content>
<img
v-lazy="scope.row.imageUrl"
:src="scope.row.imageUrl"
class="friends-list-avatar"
style="height: 500px; cursor: pointer"
@click="showFullscreenImageDialog(scope.row.imageUrl)" />
@click="showFullscreenImageDialog(scope.row.imageUrl)"
loading="lazy" />
</template>
<span v-text="scope.row.fileId"></span>
</el-tooltip>
@@ -436,10 +434,10 @@
<span v-else v-text="scope.row.text"></span>
</template>
</el-table-column>
</data-tables>
</DataTable>
</el-tab-pane>
<el-tab-pane :label="t('view.player_list.photon.previous')">
<data-tables v-bind="photonEventTablePrevious" style="margin-bottom: 10px">
<DataTable v-bind="photonEventTablePrevious" style="margin-bottom: 10px">
<el-table-column :label="t('table.playerList.date')" prop="created_at" width="120">
<template #default="scope">
<el-tooltip placement="right">
@@ -472,7 +470,7 @@
v-text="scope.row.avatar.name"></span>
&nbsp;
<span v-if="!scope.row.inCache" style="color: #aaa"
><i class="el-icon-download"></i>&nbsp;</span
><el-icon><Download /></el-icon>&nbsp;</span
>
<span
v-if="scope.row.avatar.releaseStatus === 'public'"
@@ -515,7 +513,7 @@
:class="statusClass(scope.row.previousStatus)"></i>
</el-tooltip>
<span>
<i class="el-icon-right"></i>
<el-icon><ArrowRight /></el-icon>
</span>
<el-tooltip placement="top">
<template #content>
@@ -557,7 +555,7 @@
@click="showGroupDialog(scope.row.previousGroupId)"
v-text="scope.row.previousGroupId"></span>
<span>
<i class="el-icon-right"></i>
<el-icon><ArrowRight /></el-icon>
</span>
<span
v-if="scope.row.groupName"
@@ -601,7 +599,7 @@
v-text="scope.row.avatar.name"></span>
&nbsp;
<span v-if="!scope.row.inCache" style="color: #aaa"
><i class="el-icon-download"></i>&nbsp;</span
><el-icon><Download /></el-icon>&nbsp;</span
>
<span
v-if="scope.row.avatar.releaseStatus === 'public'"
@@ -619,10 +617,11 @@
<el-tooltip placement="right">
<template #content>
<img
v-lazy="scope.row.imageUrl"
:src="scope.row.imageUrl"
class="friends-list-avatar"
style="height: 500px; cursor: pointer"
@click="showFullscreenImageDialog(scope.row.imageUrl)" />
@click="showFullscreenImageDialog(scope.row.imageUrl)"
loading="lazy" />
</template>
<span v-text="scope.row.fileId"></span>
</el-tooltip>
@@ -636,28 +635,31 @@
<span v-else v-text="scope.row.text"></span>
</template>
</el-table-column>
</data-tables>
</DataTable>
</el-tab-pane>
</el-tabs>
</div>
<div class="current-instance-table">
<data-tables
<DataTable
v-bind="currentInstanceWorld.ref.id ? currentInstanceUserList : {}"
style="margin-top: 10px; cursor: pointer"
@row-click="selectCurrentInstanceRow">
<el-table-column :label="t('table.playerList.avatar')" width="70" prop="photo">
<template #default="scope">
<template v-if="userImage(scope.row.ref)">
<el-popover placement="right" height="500px" trigger="hover">
<el-popover placement="right" :width="500" trigger="hover">
<template #reference>
<img
:src="userImage(scope.row.ref)"
class="friends-list-avatar"
loading="lazy" />
</template>
<img
slot="reference"
v-lazy="userImage(scope.row.ref)"
class="friends-list-avatar" />
<img
v-lazy="userImageFull(scope.row.ref)"
class="friends-list-avatar"
style="height: 500px; cursor: pointer"
@click="showFullscreenImageDialog(userImageFull(scope.row.ref))" />
:src="userImageFull(scope.row.ref)"
:class="['friends-list-avatar', 'x-popover-image']"
style="cursor: pointer"
@click="showFullscreenImageDialog(userImageFull(scope.row.ref))"
loading="lazy" />
</el-popover>
</template>
</template>
@@ -678,8 +680,8 @@
<el-tooltip placement="left" content="Unblock chatbox messages">
<el-button
type="text"
icon="el-icon-turn-off-microphone"
size="mini"
:icon="Mute"
size="small"
style="color: red; margin-right: 5px"
@click.stop="deleteChatboxUserBlacklist(scope.row.ref.id)"></el-button>
</el-tooltip>
@@ -688,8 +690,8 @@
<el-tooltip placement="left" content="Block chatbox messages">
<el-button
type="text"
icon="el-icon-microphone"
size="mini"
:icon="Microphone"
size="small"
style="margin-right: 5px"
@click.stop="addChatboxUserBlacklist(scope.row.ref)"></el-button>
</el-tooltip>
@@ -700,7 +702,7 @@
<el-table-column
:label="t('table.playerList.icon')"
prop="isMaster"
width="80"
width="90"
align="center"
sortable
:sort-method="sortInstanceIcon">
@@ -708,34 +710,35 @@
<el-tooltip v-if="scope.row.isMaster" placement="left" content="Instance Master">
<span>👑</span>
</el-tooltip>
<el-tooltip v-if="scope.row.isModerator" placement="left" content="Moderator">
<el-tooltip v-else-if="scope.row.isModerator" placement="left" content="Moderator">
<span>⚔️</span>
</el-tooltip>
<el-tooltip v-if="scope.row.isFriend" placement="left" content="Friend">
<el-tooltip v-else-if="scope.row.isFriend" placement="left" content="Friend">
<span>💚</span>
</el-tooltip>
<el-tooltip v-if="scope.row.isBlocked" placement="left" content="Blocked">
<i class="el-icon el-icon-circle-close" style="color: red"></i>
<el-tooltip v-else-if="scope.row.isBlocked" placement="left" content="Blocked">
<el-icon style="color: red"><CircleClose /></el-icon>
</el-tooltip>
<el-tooltip v-if="scope.row.isMuted" placement="left" content="Muted">
<i class="el-icon el-icon-turn-off-microphone" style="color: orange"></i>
<el-tooltip v-else-if="scope.row.isMuted" placement="left" content="Muted">
<el-icon style="color: orange"><Mute /></el-icon>
</el-tooltip>
<el-tooltip
v-if="scope.row.isAvatarInteractionDisabled"
v-else-if="scope.row.isAvatarInteractionDisabled"
placement="left"
content="Avatar Interaction Disabled
">
<i class="el-icon el-icon-thumb" style="color: orange"></i>
<el-icon style="color: orange"><Pointer /></el-icon>
</el-tooltip>
<el-tooltip v-if="scope.row.isChatBoxMuted" placement="left" content="Chatbox Muted">
<i class="el-icon el-icon-chat-line-round" style="color: orange"></i>
<el-tooltip v-else-if="scope.row.isChatBoxMuted" placement="left" content="Chatbox Muted">
<el-icon style="color: orange"><ChatLineRound /></el-icon>
</el-tooltip>
<el-tooltip v-if="scope.row.timeoutTime" placement="left" content="Timeout">
<el-tooltip v-else-if="scope.row.timeoutTime" placement="left" content="Timeout">
<span style="color: red">🔴{{ scope.row.timeoutTime }}s</span>
</el-tooltip>
<span v-else></span>
</template>
</el-table-column>
<el-table-column :label="t('table.playerList.platform')" prop="inVRMode" width="80">
<el-table-column :label="t('table.playerList.platform')" prop="inVRMode" width="90">
<template #default="scope">
<template v-if="scope.row.ref.$platform">
<span v-if="scope.row.ref.$platform === 'standalonewindows'" style="color: #409eff"
@@ -762,7 +765,7 @@
:label="t('table.playerList.displayName')"
min-width="140"
prop="displayName"
sortable="custom">
:sortable="true">
<template #default="scope">
<span
v-if="randomUserColours"
@@ -789,7 +792,7 @@
:label="t('table.playerList.rank')"
width="110"
prop="$trustSortNum"
sortable="custom">
:sortable="true">
<template #default="scope">
<span
class="name"
@@ -813,7 +816,9 @@
<el-table-column :label="t('table.playerList.bioLink')" width="100" prop="ref.bioLinks">
<template #default="scope">
<div style="display: flex; align-items: center">
<el-tooltip v-for="(link, index) in scope.row.ref.bioLinks" v-if="link" :key="index">
<el-tooltip
v-for="(link, index) in scope.row.ref.bioLinks?.filter(Boolean)"
:key="index">
<template #content>
<span v-text="link"></span>
</template>
@@ -826,7 +831,8 @@
margin-right: 5px;
cursor: pointer;
"
@click.stop="openExternalLink(link)" />
@click.stop="openExternalLink(link)"
loading="lazy" />
</el-tooltip>
</div>
</template>
@@ -836,7 +842,7 @@
<span v-text="scope.row.ref.note"></span>
</template>
</el-table-column>
</data-tables>
</DataTable>
</div>
</div>
<ChatboxBlacklistDialog
@@ -846,9 +852,19 @@
</template>
<script setup>
import {
Mute,
Microphone,
Download,
ArrowRight,
HomeFilled,
CircleClose,
Pointer,
ChatLineRound
} from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import {
languageClass,
getFaviconUrl,
@@ -873,9 +889,9 @@
useVrcxStore
} from '../../stores';
import ChatboxBlacklistDialog from './dialogs/ChatboxBlacklistDialog.vue';
import { photonEventTableTypeFilterList } from '../../shared/constants';
import { photonEventTableTypeFilterList } from '../../shared/constants/photon';
const { hideTooltips, randomUserColours } = storeToRefs(useAppearanceSettingsStore());
const { randomUserColours } = storeToRefs(useAppearanceSettingsStore());
const {
photonLoggingEnabled,
photonEventIcon,
@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible.sync="chatboxBlacklistDialog.visible"
v-model="chatboxBlacklistDialog.visible"
:title="t('dialog.chatbox_blacklist.header')"
width="600px">
<div v-if="chatboxBlacklistDialog.visible" v-loading="chatboxBlacklistDialog.loading">
@@ -15,7 +15,7 @@
@change="saveChatboxBlacklist">
<template #append>
<el-button
icon="el-icon-delete"
:icon="Delete"
@click="
chatboxBlacklist.splice(index, 1);
saveChatboxBlacklist();
@@ -23,7 +23,7 @@
</el-button>
</template>
</el-input>
<el-button size="mini" style="margin-top: 5px" @click="chatboxBlacklist.push('')">
<el-button size="small" style="margin-top: 5px" @click="chatboxBlacklist.push('')">
{{ t('dialog.chatbox_blacklist.add_item') }}
</el-button>
<br />
@@ -39,13 +39,15 @@
<span>{{ user[1] }}</span>
</el-tag>
</div>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { Delete } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import configRepository from '../../../service/config';
import { usePhotonStore } from '../../../stores';
+139 -131
View File
@@ -5,7 +5,7 @@
<div class="x-friend-list" style="margin-top: 10px">
<div class="x-friend-item" @click="showUserDialog(currentUser.id)">
<div class="avatar">
<img v-lazy="userImage(currentUser, true)" />
<img :src="userImage(currentUser, true)" loading="lazy" />
</div>
<div class="detail">
<span class="name" v-text="currentUser.displayName"></span>
@@ -40,35 +40,35 @@
size="small"
type="danger"
plain
icon="el-icon-switch-button"
:icon="SwitchButton"
style="margin-left: 0; margin-right: 5px; margin-top: 10px"
@click="logout()"
>{{ t('view.profile.profile.logout') }}</el-button
>
<el-button
size="small"
icon="el-icon-picture-outline"
:icon="Picture"
style="margin-left: 0; margin-right: 5px; margin-top: 10px"
@click="showGalleryDialog()"
>{{ t('view.profile.profile.manage_gallery_inventory_icon') }}</el-button
>
<el-button
size="small"
icon="el-icon-chat-dot-round"
:icon="ChatDotRound"
style="margin-left: 0; margin-right: 5px; margin-top: 10px"
@click="showDiscordNamesDialog()"
>{{ t('view.profile.profile.discord_names') }}</el-button
>
<el-button
size="small"
icon="el-icon-printer"
:icon="Printer"
style="margin-left: 0; margin-right: 5px; margin-top: 10px"
@click="showExportFriendsListDialog()"
>{{ t('view.profile.profile.export_friend_list') }}</el-button
>
<el-button
size="small"
icon="el-icon-user"
:icon="User"
style="margin-left: 0; margin-right: 5px; margin-top: 10px"
@click="showExportAvatarsListDialog()"
>{{ t('view.profile.profile.export_own_avatars') }}</el-button
@@ -94,11 +94,11 @@
<div class="options-container">
<div class="header-bar">
<span class="header">{{ t('view.profile.vrc_sdk_downloads.header') }}</span>
<el-tooltip placement="top" :content="t('view.profile.refresh_tooltip')" :disabled="hideTooltips">
<el-tooltip placement="top" :content="t('view.profile.refresh_tooltip')">
<el-button
type="default"
size="mini"
icon="el-icon-refresh"
size="small"
:icon="Refresh"
circle
style="margin-left: 5px"
@click="getConfig"></el-button>
@@ -141,11 +141,11 @@
<div class="options-container">
<div class="header-bar">
<span class="header">{{ t('view.profile.invite_messages') }}</span>
<el-tooltip placement="top" :content="t('view.profile.refresh_tooltip')" :disabled="hideTooltips">
<el-tooltip placement="top" :content="t('view.profile.refresh_tooltip')">
<el-button
type="default"
size="mini"
icon="el-icon-refresh"
size="small"
:icon="Refresh"
circle
style="margin-left: 5px"
@click="
@@ -153,27 +153,27 @@
refreshInviteMessageTableData('message');
"></el-button>
</el-tooltip>
<el-tooltip placement="top" :content="t('view.profile.clear_results_tooltip')" :disabled="hideTooltips">
<el-tooltip placement="top" :content="t('view.profile.clear_results_tooltip')">
<el-button
type="default"
size="mini"
icon="el-icon-delete"
size="small"
:icon="Delete"
circle
style="margin-left: 5px"
@click="inviteMessageTable.visible = false"></el-button>
</el-tooltip>
</div>
<data-tables v-if="inviteMessageTable.visible" v-bind="inviteMessageTable" style="margin-top: 10px">
<DataTable v-if="inviteMessageTable.visible" v-bind="inviteMessageTable" style="margin-top: 10px">
<el-table-column
:label="t('table.profile.invite_messages.slot')"
prop="slot"
sortable="custom"
:sortable="true"
width="70"></el-table-column>
<el-table-column :label="t('table.profile.invite_messages.message')" prop="message"></el-table-column>
<el-table-column
:label="t('table.profile.invite_messages.cool_down')"
prop="updatedAt"
sortable="custom"
:sortable="true"
width="110"
align="right">
<template #default="scope">
@@ -184,22 +184,22 @@
<template #default="scope">
<el-button
type="text"
icon="el-icon-edit"
size="mini"
:icon="Edit"
size="small"
@click="showEditInviteMessageDialog('message', scope.row)"></el-button>
</template>
</el-table-column>
</data-tables>
</DataTable>
</div>
<div class="options-container">
<div class="header-bar">
<span class="header">{{ t('view.profile.invite_response_messages') }}</span>
<el-tooltip placement="top" :content="t('view.profile.refresh_tooltip')" :disabled="hideTooltips">
<el-tooltip placement="top" :content="t('view.profile.refresh_tooltip')">
<el-button
type="default"
size="mini"
icon="el-icon-refresh"
size="small"
:icon="Refresh"
circle
style="margin-left: 5px"
@click="
@@ -207,30 +207,30 @@
refreshInviteMessageTableData('response');
"></el-button>
</el-tooltip>
<el-tooltip placement="top" :content="t('view.profile.clear_results_tooltip')" :disabled="hideTooltips">
<el-tooltip placement="top" :content="t('view.profile.clear_results_tooltip')">
<el-button
type="default"
size="mini"
icon="el-icon-delete"
size="small"
:icon="Delete"
circle
style="margin-left: 5px"
@click="inviteResponseMessageTable.visible = false"></el-button>
</el-tooltip>
</div>
<data-tables
<DataTable
v-if="inviteResponseMessageTable.visible"
v-bind="inviteResponseMessageTable"
style="margin-top: 10px">
<el-table-column
:label="t('table.profile.invite_messages.slot')"
prop="slot"
sortable="custom"
:sortable="true"
width="70"></el-table-column>
<el-table-column :label="t('table.profile.invite_messages.message')" prop="message"></el-table-column>
<el-table-column
:label="t('table.profile.invite_messages.cool_down')"
prop="updatedAt"
sortable="custom"
:sortable="true"
width="110"
align="right">
<template #default="scope">
@@ -241,22 +241,22 @@
<template #default="scope">
<el-button
type="text"
icon="el-icon-edit"
size="mini"
:icon="Edit"
size="small"
@click="showEditInviteMessageDialog('response', scope.row)"></el-button>
</template>
</el-table-column>
</data-tables>
</DataTable>
</div>
<div class="options-container">
<div class="header-bar">
<span class="header">{{ t('view.profile.invite_request_messages') }}</span>
<el-tooltip placement="top" :content="t('view.profile.refresh_tooltip')" :disabled="hideTooltips">
<el-tooltip placement="top" :content="t('view.profile.refresh_tooltip')">
<el-button
type="default"
size="mini"
icon="el-icon-refresh"
size="small"
:icon="Refresh"
circle
style="margin-left: 5px"
@click="
@@ -264,30 +264,30 @@
refreshInviteMessageTableData('request');
"></el-button>
</el-tooltip>
<el-tooltip placement="top" :content="t('view.profile.clear_results_tooltip')" :disabled="hideTooltips">
<el-tooltip placement="top" :content="t('view.profile.clear_results_tooltip')">
<el-button
type="default"
size="mini"
icon="el-icon-delete"
size="small"
:icon="Delete"
circle
style="margin-left: 5px"
@click="inviteRequestMessageTable.visible = false"></el-button>
</el-tooltip>
</div>
<data-tables
<DataTable
v-if="inviteRequestMessageTable.visible"
v-bind="inviteRequestMessageTable"
style="margin-top: 10px">
<el-table-column
:label="t('table.profile.invite_messages.slot')"
prop="slot"
sortable="custom"
:sortable="true"
width="70"></el-table-column>
<el-table-column :label="t('table.profile.invite_messages.message')" prop="message"></el-table-column>
<el-table-column
:label="t('table.profile.invite_messages.cool_down')"
prop="updatedAt"
sortable="custom"
:sortable="true"
width="110"
align="right">
<template #default="scope">
@@ -298,22 +298,22 @@
<template #default="scope">
<el-button
type="text"
icon="el-icon-edit"
size="mini"
:icon="Edit"
size="small"
@click="showEditInviteMessageDialog('request', scope.row)"></el-button>
</template>
</el-table-column>
</data-tables>
</DataTable>
</div>
<div class="options-container">
<div class="header-bar">
<span class="header">{{ t('view.profile.invite_request_response_messages') }}</span>
<el-tooltip placement="top" :content="t('view.profile.refresh_tooltip')" :disabled="hideTooltips">
<el-tooltip placement="top" :content="t('view.profile.refresh_tooltip')">
<el-button
type="default"
size="mini"
icon="el-icon-refresh"
size="small"
:icon="Refresh"
circle
style="margin-left: 5px"
@click="
@@ -321,30 +321,30 @@
refreshInviteMessageTableData('requestResponse');
"></el-button>
</el-tooltip>
<el-tooltip placement="top" :content="t('view.profile.clear_results_tooltip')" :disabled="hideTooltips">
<el-tooltip placement="top" :content="t('view.profile.clear_results_tooltip')">
<el-button
type="default"
size="mini"
icon="el-icon-delete"
size="small"
:icon="Delete"
circle
style="margin-left: 5px"
@click="inviteRequestResponseMessageTable.visible = false"></el-button>
</el-tooltip>
</div>
<data-tables
<DataTable
v-if="inviteRequestResponseMessageTable.visible"
v-bind="inviteRequestResponseMessageTable"
style="margin-top: 10px">
<el-table-column
:label="t('table.profile.invite_messages.slot')"
prop="slot"
sortable="custom"
:sortable="true"
width="70"></el-table-column>
<el-table-column :label="t('table.profile.invite_messages.message')" prop="message"></el-table-column>
<el-table-column
:label="t('table.profile.invite_messages.cool_down')"
prop="updatedAt"
sortable="custom"
:sortable="true"
width="110"
align="right">
<template #default="scope">
@@ -355,21 +355,21 @@
<template #default="scope">
<el-button
type="text"
icon="el-icon-edit"
size="mini"
:icon="Edit"
size="small"
@click="showEditInviteMessageDialog('requestResponse', scope.row)"></el-button>
</template>
</el-table-column>
</data-tables>
</DataTable>
</div>
<div class="options-container">
<span class="header">{{ t('view.profile.past_display_names') }}</span>
<data-tables v-bind="pastDisplayNameTable" style="margin-top: 10px">
<DataTable v-bind="pastDisplayNameTable" style="margin-top: 10px">
<el-table-column
:label="t('table.profile.previous_display_name.date')"
prop="updated_at"
sortable="custom">
:sortable="true">
<template #default="scope">
<span>{{ formatDateFilter(scope.row.updated_at, 'long') }}</span>
</template>
@@ -377,26 +377,26 @@
<el-table-column
:label="t('table.profile.previous_display_name.name')"
prop="displayName"></el-table-column>
</data-tables>
</DataTable>
</div>
<div class="options-container">
<div class="header-bar">
<span class="header">{{ t('view.profile.config_json') }}</span>
<el-tooltip placement="top" :content="t('view.profile.refresh_tooltip')" :disabled="hideTooltips">
<el-tooltip placement="top" :content="t('view.profile.refresh_tooltip')">
<el-button
type="default"
size="mini"
icon="el-icon-refresh"
size="small"
:icon="Refresh"
circle
style="margin-left: 5px"
@click="refreshConfigTreeData()"></el-button>
</el-tooltip>
<el-tooltip placement="top" :content="t('view.profile.clear_results_tooltip')" :disabled="hideTooltips">
<el-tooltip placement="top" :content="t('view.profile.clear_results_tooltip')">
<el-button
type="default"
size="mini"
icon="el-icon-delete"
size="small"
:icon="Delete"
circle
style="margin-left: 5px"
@click="configTreeData = []"></el-button>
@@ -415,20 +415,20 @@
<div class="options-container">
<div class="header-bar">
<span class="header">{{ t('view.profile.current_user_json') }}</span>
<el-tooltip placement="top" :content="t('view.profile.refresh_tooltip')" :disabled="hideTooltips">
<el-tooltip placement="top" :content="t('view.profile.refresh_tooltip')">
<el-button
type="default"
size="mini"
icon="el-icon-refresh"
size="small"
:icon="Refresh"
circle
style="margin-left: 5px"
@click="refreshCurrentUserTreeData()"></el-button>
</el-tooltip>
<el-tooltip placement="top" :content="t('view.profile.clear_results_tooltip')" :disabled="hideTooltips">
<el-tooltip placement="top" :content="t('view.profile.clear_results_tooltip')">
<el-button
type="default"
size="mini"
icon="el-icon-delete"
size="small"
:icon="Delete"
circle
style="margin-left: 5px"
@click="currentUserTreeData = []"></el-button>
@@ -450,20 +450,20 @@
<div class="options-container">
<div class="header-bar">
<span class="header">{{ t('view.profile.feedback') }}</span>
<el-tooltip placement="top" :content="t('view.profile.refresh_tooltip')" :disabled="hideTooltips">
<el-tooltip placement="top" :content="t('view.profile.refresh_tooltip')">
<el-button
type="default"
size="mini"
icon="el-icon-refresh"
size="small"
:icon="Refresh"
circle
style="margin-left: 5px"
@click="getCurrentUserFeedback()"></el-button>
</el-tooltip>
<el-tooltip placement="top" :content="t('view.profile.clear_results_tooltip')" :disabled="hideTooltips">
<el-tooltip placement="top" :content="t('view.profile.clear_results_tooltip')">
<el-button
type="default"
size="mini"
icon="el-icon-delete"
size="small"
:icon="Delete"
circle
style="margin-left: 5px"
@click="currentUserFeedbackData = []"></el-button>
@@ -481,18 +481,23 @@
</template>
</el-tree>
</div>
<DiscordNamesDialog :discord-names-dialog-visible.sync="discordNamesDialogVisible" :friends="friends" />
<DiscordNamesDialog :discord-names-dialog-visible="discordNamesDialogVisible" :friends="friends" />
<ExportFriendsListDialog
:is-export-friends-list-dialog-visible.sync="isExportFriendsListDialogVisible"
:friends="friends" />
<ExportAvatarsListDialog :is-export-avatars-list-dialog-visible.sync="isExportAvatarsListDialogVisible" />
:is-export-friends-list-dialog-visible="isExportFriendsListDialogVisible"
:friends="friends"
@update:isExportFriendsListDialogVisible="isExportFriendsListDialogVisible = $event" />
<ExportAvatarsListDialog :is-export-avatars-list-dialog-visible="isExportAvatarsListDialogVisible" />
</div>
</template>
<script setup>
import { ElMessage, ElMessageBox } from 'element-plus';
import { SwitchButton, Picture, User, Refresh, Delete, Edit, ChatDotRound, Printer } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
import { ref, getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { authRequest, miscRequest, userRequest } from '../../api';
import {
parseAvatarUrl,
@@ -507,18 +512,15 @@
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();
@@ -536,8 +538,6 @@
const { t } = useI18n();
const { $prompt, $message } = getCurrentInstance().proxy;
const vrchatCredit = ref(null);
const configTreeData = ref([]);
const currentUserTreeData = ref([]);
@@ -576,96 +576,104 @@
isExportAvatarsListDialogVisible.value = true;
}
function promptUsernameDialog() {
$prompt(t('prompt.direct_access_username.description'), t('prompt.direct_access_username.header'), {
ElMessageBox.prompt(t('prompt.direct_access_username.description'), t('prompt.direct_access_username.header'), {
distinguishCancelAndClose: true,
confirmButtonText: t('prompt.direct_access_username.ok'),
cancelButtonText: t('prompt.direct_access_username.cancel'),
inputPattern: /\S+/,
inputErrorMessage: t('prompt.direct_access_username.input_error'),
callback: (action, instance) => {
if (action === 'confirm' && instance.inputValue) {
inputErrorMessage: t('prompt.direct_access_username.input_error')
})
.then(({ value }) => {
if (value) {
lookupUser({
displayName: instance.inputValue
displayName: value
});
}
}
});
})
.catch(() => {});
}
function promptUserIdDialog() {
$prompt(t('prompt.direct_access_user_id.description'), t('prompt.direct_access_user_id.header'), {
ElMessageBox.prompt(t('prompt.direct_access_user_id.description'), t('prompt.direct_access_user_id.header'), {
distinguishCancelAndClose: true,
confirmButtonText: t('prompt.direct_access_user_id.ok'),
cancelButtonText: t('prompt.direct_access_user_id.cancel'),
inputPattern: /\S+/,
inputErrorMessage: t('prompt.direct_access_user_id.input_error'),
callback: (action, instance) => {
instance.inputValue = instance.inputValue.trim();
if (action === 'confirm' && instance.inputValue) {
const testUrl = instance.inputValue.substring(0, 15);
inputErrorMessage: t('prompt.direct_access_user_id.input_error')
})
.then(({ value }) => {
if (value) {
const trimmedValue = value.trim();
const testUrl = trimmedValue.substring(0, 15);
if (testUrl === 'https://vrchat.') {
const userId = parseUserUrl(instance.inputValue);
const userId = parseUserUrl(trimmedValue);
if (userId) {
showUserDialog(userId);
} else {
$message({
ElMessage({
message: t('prompt.direct_access_user_id.message.error'),
type: 'error'
});
}
} else {
showUserDialog(instance.inputValue);
showUserDialog(trimmedValue);
}
}
}
});
})
.catch(() => {});
}
function promptWorldDialog() {
$prompt(t('prompt.direct_access_world_id.description'), t('prompt.direct_access_world_id.header'), {
ElMessageBox.prompt(t('prompt.direct_access_world_id.description'), t('prompt.direct_access_world_id.header'), {
distinguishCancelAndClose: true,
confirmButtonText: t('prompt.direct_access_world_id.ok'),
cancelButtonText: t('prompt.direct_access_world_id.cancel'),
inputPattern: /\S+/,
inputErrorMessage: t('prompt.direct_access_world_id.input_error'),
callback: (action, instance) => {
instance.inputValue = instance.inputValue.trim();
if (action === 'confirm' && instance.inputValue) {
if (!directAccessWorld(instance.inputValue)) {
$message({
inputErrorMessage: t('prompt.direct_access_world_id.input_error')
})
.then(({ value }) => {
if (value) {
const trimmedValue = value.trim();
if (!directAccessWorld(trimmedValue)) {
ElMessage({
message: t('prompt.direct_access_world_id.message.error'),
type: 'error'
});
}
}
}
});
})
.catch(() => {});
}
function promptAvatarDialog() {
$prompt(t('prompt.direct_access_avatar_id.description'), t('prompt.direct_access_avatar_id.header'), {
distinguishCancelAndClose: true,
confirmButtonText: t('prompt.direct_access_avatar_id.ok'),
cancelButtonText: t('prompt.direct_access_avatar_id.cancel'),
inputPattern: /\S+/,
inputErrorMessage: t('prompt.direct_access_avatar_id.input_error'),
callback: (action, instance) => {
instance.inputValue = instance.inputValue.trim();
if (action === 'confirm' && instance.inputValue) {
const testUrl = instance.inputValue.substring(0, 15);
ElMessageBox.prompt(
t('prompt.direct_access_avatar_id.description'),
t('prompt.direct_access_avatar_id.header'),
{
distinguishCancelAndClose: true,
confirmButtonText: t('prompt.direct_access_avatar_id.ok'),
cancelButtonText: t('prompt.direct_access_avatar_id.cancel'),
inputPattern: /\S+/,
inputErrorMessage: t('prompt.direct_access_avatar_id.input_error')
}
)
.then(({ value }) => {
if (value) {
const trimmedValue = value.trim();
const testUrl = trimmedValue.substring(0, 15);
if (testUrl === 'https://vrchat.') {
const avatarId = parseAvatarUrl(instance.inputValue);
const avatarId = parseAvatarUrl(trimmedValue);
if (avatarId) {
showAvatarDialog(avatarId);
} else {
$message({
ElMessage({
message: t('prompt.direct_access_avatar_id.message.error'),
type: 'error'
});
}
} else {
showAvatarDialog(instance.inputValue);
showAvatarDialog(trimmedValue);
}
}
}
});
})
.catch(() => {});
}
async function getConfig() {
await authRequest.getConfig();
@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible="discordNamesDialogVisible"
:model-value="discordNamesDialogVisible"
:title="t('dialog.discord_names.header')"
width="650px"
@close="closeDialog">
@@ -11,18 +11,18 @@
<el-input
v-model="discordNamesContent"
type="textarea"
size="mini"
rows="15"
size="small"
:rows="15"
resize="none"
readonly
style="margin-top: 15px" />
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { storeToRefs } from 'pinia';
import { ref, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { useUserStore } from '../../../stores';
const { t } = useI18n();
@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible="editInviteMessageDialog.visible"
:model-value="editInviteMessageDialog.visible"
:title="t('dialog.edit_invite_message.header')"
width="400px"
@close="closeDialog">
@@ -10,7 +10,7 @@
<el-input
v-model="message"
type="textarea"
size="mini"
size="small"
maxlength="64"
show-word-limit
:autosize="{ minRows: 2, maxRows: 5 }"
@@ -18,25 +18,23 @@
style="margin-top: 10px"></el-input>
</div>
<template #footer>
<el-button type="small" @click="closeDialog">{{ $t('dialog.edit_invite_message.cancel') }}</el-button>
<el-button @click="closeDialog">{{ t('dialog.edit_invite_message.cancel') }}</el-button>
<el-button type="primary" size="small" @click="saveEditInviteMessage">{{
t('dialog.edit_invite_message.save')
}}</el-button>
</template>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ElMessage } from 'element-plus';
import { storeToRefs } from 'pinia';
import { getCurrentInstance, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { inviteMessagesRequest } from '../../../api';
import { useInviteStore } from '../../../stores';
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const inviteStore = useInviteStore();
const { editInviteMessageDialog } = storeToRefs(inviteStore);
@@ -68,13 +66,13 @@
})
.then((args) => {
if (args.json[slot].message === D.inviteMessage.message) {
$message({
ElMessage({
message: "VRChat API didn't update message, try again",
type: 'error'
});
throw new Error("VRChat API didn't update message, try again");
} else {
$message.success('Invite message updated');
ElMessage.success('Invite message updated');
}
return args;
});
@@ -1,21 +1,21 @@
<template>
<safe-dialog :visible.sync="isVisible" :title="t('dialog.export_own_avatars.header')" width="650px">
<el-dialog v-model="isVisible" :title="t('dialog.export_own_avatars.header')" width="650px">
<el-input
v-model="exportAvatarsListCsv"
v-loading="loading"
type="textarea"
size="mini"
rows="15"
size="small"
:rows="15"
resize="none"
readonly
style="margin-top: 15px"
@click.native="$event.target.tagName === 'TEXTAREA' && $event.target.select()" />
</safe-dialog>
@click="$event.target.tagName === 'TEXTAREA' && $event.target.select()" />
</el-dialog>
</template>
<script setup>
import { ref, computed, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { avatarRequest } from '../../../api';
import { processBulk } from '../../../service/request';
@@ -23,8 +23,7 @@
const { t } = useI18n();
const { cachedAvatars } = storeToRefs(useAvatarStore());
const { applyAvatar } = useAvatarStore();
const { applyAvatar, cachedAvatars } = useAvatarStore();
const { currentUser } = storeToRefs(useUserStore());
const props = defineProps({
@@ -59,9 +58,9 @@
function initExportAvatarsListDialog() {
loading.value = true;
for (const ref of cachedAvatars.value.values()) {
for (const ref of cachedAvatars.values()) {
if (ref.authorId === currentUser.value.id) {
cachedAvatars.value.delete(ref.id);
cachedAvatars.delete(ref.id);
}
}
const params = {
@@ -1,35 +1,35 @@
<template>
<safe-dialog :title="t('dialog.export_friends_list.header')" :visible.sync="isVisible" width="650px">
<el-dialog :title="t('dialog.export_friends_list.header')" v-model="isVisible" width="650px">
<el-tabs type="card">
<el-tab-pane :label="t('dialog.export_friends_list.csv')">
<el-input
v-model="exportFriendsListCsv"
type="textarea"
size="mini"
rows="15"
size="small"
:rows="15"
resize="none"
readonly
style="margin-top: 15px"
@click.native="$event.target.tagName === 'TEXTAREA' && $event.target.select()" />
@click="$event.target.tagName === 'TEXTAREA' && $event.target.select()" />
</el-tab-pane>
<el-tab-pane :label="t('dialog.export_friends_list.json')">
<el-input
v-model="exportFriendsListJson"
type="textarea"
size="mini"
rows="15"
size="small"
:rows="15"
resize="none"
readonly
style="margin-top: 15px"
@click.native="$event.target.tagName === 'TEXTAREA' && $event.target.select()" />
@click="$event.target.tagName === 'TEXTAREA' && $event.target.select()" />
</el-tab-pane>
</el-tabs>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ref, computed, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { useUserStore } from '../../../stores';
+117 -126
View File
@@ -2,15 +2,15 @@
<div v-show="menuActiveIndex === 'search'" class="x-container">
<div style="margin: 0 0 10px; display: flex; align-items: center">
<el-input
:value="searchText"
:model-value="searchText"
:placeholder="t('view.search.search_placeholder')"
style="flex: 1"
@input="updateSearchText"
@keyup.native.13="search"></el-input>
<el-tooltip placement="bottom" :content="t('view.search.clear_results_tooltip')" :disabled="hideTooltips">
@keyup.enter="search"></el-input>
<el-tooltip placement="bottom" :content="t('view.search.clear_results_tooltip')">
<el-button
type="default"
icon="el-icon-delete"
:icon="Delete"
circle
style="flex: none; margin-left: 10px"
@click="handleClearSearch"></el-button>
@@ -30,37 +30,35 @@
:key="user.id"
class="x-friend-item"
@click="showUserDialog(user.id)">
<template>
<div class="avatar">
<img v-lazy="userImage(user, true)" />
</div>
<div class="detail">
<span class="name" v-text="user.displayName"></span>
<span
v-if="randomUserColours"
class="extra"
:class="user.$trustClass"
v-text="user.$trustLevel"></span>
<span
v-else
class="extra"
:style="{ color: user.$userColour }"
v-text="user.$trustLevel"></span>
</div>
</template>
<div class="avatar">
<img :src="userImage(user, true)" loading="lazy" />
</div>
<div class="detail">
<span class="name" v-text="user.displayName"></span>
<span
v-if="randomUserColours"
class="extra"
:class="user.$trustClass"
v-text="user.$trustLevel"></span>
<span
v-else
class="extra"
:style="{ color: user.$userColour }"
v-text="user.$trustLevel"></span>
</div>
</div>
</div>
<el-button-group v-if="searchUserResults.length" style="margin-top: 15px">
<el-button
:disabled="!searchUserParams.offset"
icon="el-icon-back"
:icon="Back"
size="small"
@click="handleMoreSearchUser(-1)"
>{{ t('view.search.prev_page') }}</el-button
>
<el-button
:disabled="searchUserResults.length < 10"
icon="el-icon-right"
:icon="Right"
size="small"
@click="handleMoreSearchUser(1)"
>{{ t('view.search.next_page') }}</el-button
@@ -77,15 +75,17 @@
style="margin-bottom: 15px"
@command="(row) => searchWorld(row)">
<el-button size="small"
>{{ t('view.search.world.category') }} <i class="el-icon-arrow-down el-icon--right"></i
>{{ t('view.search.world.category') }} <el-icon class="el-icon--right"><ArrowDown /></el-icon
></el-button>
<el-dropdown-menu v-slot="dropdown">
<el-dropdown-item
v-for="row in cachedConfig.dynamicWorldRows"
:key="row.index"
:command="row"
v-text="row.name"></el-dropdown-item>
</el-dropdown-menu>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="row in cachedConfig.dynamicWorldRows"
:key="row.index"
:command="row"
v-text="row.name"></el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-checkbox v-model="searchWorldLabs" style="margin-left: 10px">{{
t('view.search.world.community_lab')
@@ -96,31 +96,29 @@
:key="world.id"
class="x-friend-item"
@click="showWorldDialog(world.id)">
<template>
<div class="avatar">
<img v-lazy="world.thumbnailImageUrl" />
</div>
<div class="detail">
<span class="name" v-text="world.name"></span>
<span v-if="world.occupants" class="extra"
>{{ world.authorName }} ({{ world.occupants }})</span
>
<span v-else class="extra" v-text="world.authorName"></span>
</div>
</template>
<div class="avatar">
<img :src="world.thumbnailImageUrl" loading="lazy" />
</div>
<div class="detail">
<span class="name" v-text="world.name"></span>
<span v-if="world.occupants" class="extra"
>{{ world.authorName }} ({{ world.occupants }})</span
>
<span v-else class="extra" v-text="world.authorName"></span>
</div>
</div>
</div>
<el-button-group v-if="searchWorldResults.length" style="margin-top: 15px">
<el-button
:disabled="!searchWorldParams.offset"
icon="el-icon-back"
:icon="Back"
size="small"
@click="moreSearchWorld(-1)"
>{{ t('view.search.prev_page') }}</el-button
>
<el-button
:disabled="searchWorldResults.length < 10"
icon="el-icon-right"
:icon="Right"
size="small"
@click="moreSearchWorld(1)"
>{{ t('view.search.next_page') }}</el-button
@@ -136,34 +134,33 @@
<el-dropdown
v-if="avatarRemoteDatabaseProviderList.length > 1"
trigger="click"
size="mini"
size="small"
style="margin-right: 5px"
@click.native.stop>
@click.stop>
<el-button size="small"
>{{ t('view.search.avatar.search_provider') }}
<i class="el-icon-arrow-down el-icon--right"></i
<el-icon class="el-icon--right"><ArrowDown /></el-icon
></el-button>
<el-dropdown-menu v-slot="dropdown">
<el-dropdown-item
v-for="provider in avatarRemoteDatabaseProviderList"
:key="provider"
@click.native="setAvatarProvider(provider)">
<i
v-if="provider === avatarRemoteDatabaseProvider"
class="el-icon-check el-icon--left"></i>
{{ provider }}
</el-dropdown-item>
</el-dropdown-menu>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="provider in avatarRemoteDatabaseProviderList"
:key="provider"
@click="setAvatarProvider(provider)">
<el-icon v-if="provider === avatarRemoteDatabaseProvider" class="el-icon--left"
><Check
/></el-icon>
{{ provider }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-tooltip
placement="bottom"
:content="t('view.search.avatar.refresh_tooltip')"
:disabled="hideTooltips">
<el-tooltip placement="bottom" :content="t('view.search.avatar.refresh_tooltip')">
<el-button
type="default"
:loading="userDialog.isAvatarsLoading"
size="mini"
icon="el-icon-refresh"
size="small"
:icon="Refresh"
circle
@click="refreshUserDialogAvatars"></el-button>
</el-tooltip>
@@ -176,7 +173,7 @@
<div style="display: flex; align-items: center">
<el-radio-group
v-model="searchAvatarFilter"
size="mini"
size="small"
style="margin: 5px; display: block"
@change="searchAvatar">
<el-radio label="all">{{ t('view.search.avatar.all') }}</el-radio>
@@ -186,7 +183,7 @@
<el-divider direction="vertical"></el-divider>
<el-radio-group
v-model="searchAvatarFilterRemote"
size="mini"
size="small"
style="margin: 5px; display: block"
@change="searchAvatar">
<el-radio label="all">{{ t('view.search.avatar.all') }}</el-radio>
@@ -201,7 +198,7 @@
<el-radio-group
v-model="searchAvatarSort"
:disabled="searchAvatarFilterRemote !== 'local'"
size="mini"
size="small"
style="margin: 5px; display: block"
@change="searchAvatar">
<el-radio label="name">{{ t('view.search.avatar.sort_name') }}</el-radio>
@@ -215,33 +212,31 @@
:key="avatar.id"
class="x-friend-item"
@click="showAvatarDialog(avatar.id)">
<template>
<div class="avatar">
<img v-if="avatar.thumbnailImageUrl" v-lazy="avatar.thumbnailImageUrl" />
<img v-else-if="avatar.imageUrl" v-lazy="avatar.imageUrl" />
</div>
<div class="detail">
<span class="name" v-text="avatar.name"></span>
<span
v-if="avatar.releaseStatus === 'public'"
class="extra"
style="color: #67c23a"
v-text="avatar.releaseStatus"></span>
<span
v-else-if="avatar.releaseStatus === 'private'"
class="extra"
style="color: #f56c6c"
v-text="avatar.releaseStatus"></span>
<span v-else class="extra" v-text="avatar.releaseStatus"></span>
<span class="extra" v-text="avatar.authorName"></span>
</div>
</template>
<div class="avatar">
<img v-if="avatar.thumbnailImageUrl" :src="avatar.thumbnailImageUrl" loading="lazy" />
<img v-else-if="avatar.imageUrl" :src="avatar.imageUrl" loading="lazy" />
</div>
<div class="detail">
<span class="name" v-text="avatar.name"></span>
<span
v-if="avatar.releaseStatus === 'public'"
class="extra"
style="color: #67c23a"
v-text="avatar.releaseStatus"></span>
<span
v-else-if="avatar.releaseStatus === 'private'"
class="extra"
style="color: #f56c6c"
v-text="avatar.releaseStatus"></span>
<span v-else class="extra" v-text="avatar.releaseStatus"></span>
<span class="extra" v-text="avatar.authorName"></span>
</div>
</div>
</div>
<el-button-group v-if="searchAvatarPage.length" style="margin-top: 15px">
<el-button
:disabled="!searchAvatarPageNum"
icon="el-icon-back"
:icon="Back"
size="small"
@click="moreSearchAvatar(-1)"
>{{ t('view.search.prev_page') }}</el-button
@@ -251,7 +246,7 @@
searchAvatarResults.length < 10 ||
(searchAvatarPageNum + 1) * 10 >= searchAvatarResults.length
"
icon="el-icon-right"
:icon="Right"
size="small"
@click="moreSearchAvatar(1)"
>{{ t('view.search.next_page') }}</el-button
@@ -268,41 +263,39 @@
:key="group.id"
class="x-friend-item"
@click="showGroupDialog(group.id)">
<template>
<div class="avatar">
<img v-lazy="getSmallThumbnailUrl(group.iconUrl)" />
</div>
<div class="detail">
<span class="name">
<span v-text="group.name"></span>
<span style="margin-left: 5px; font-weight: normal">({{ group.memberCount }})</span>
<span
style="
margin-left: 5px;
color: #909399;
font-weight: normal;
font-family: monospace;
font-size: 12px;
"
>{{ group.shortCode }}.{{ group.discriminator }}</span
>
</span>
<span class="extra" v-text="group.description"></span>
</div>
</template>
<div class="avatar">
<img :src="getSmallThumbnailUrl(group.iconUrl)" loading="lazy" />
</div>
<div class="detail">
<span class="name">
<span v-text="group.name"></span>
<span style="margin-left: 5px; font-weight: normal">({{ group.memberCount }})</span>
<span
style="
margin-left: 5px;
color: #909399;
font-weight: normal;
font-family: monospace;
font-size: 12px;
"
>{{ group.shortCode }}.{{ group.discriminator }}</span
>
</span>
<span class="extra" v-text="group.description"></span>
</div>
</div>
</div>
<el-button-group v-if="searchGroupResults.length" style="margin-top: 15px">
<el-button
:disabled="!searchGroupParams.offset"
icon="el-icon-back"
:icon="Back"
size="small"
@click="moreSearchGroup(-1)"
>{{ t('view.search.prev_page') }}</el-button
>
<el-button
:disabled="searchGroupResults.length < 10"
icon="el-icon-right"
:icon="Right"
size="small"
@click="moreSearchGroup(1)"
>{{ t('view.search.next_page') }}</el-button
@@ -314,9 +307,10 @@
</template>
<script setup>
import { Delete, Back, Right, Refresh, ArrowDown, Check } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { groupRequest, worldRequest } from '../../api';
import {
compareByCreatedAt,
@@ -339,18 +333,15 @@
useWorldStore
} from '../../stores';
const { hideTooltips, randomUserColours } = storeToRefs(useAppearanceSettingsStore());
const { 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 { showAvatarDialog, lookupAvatars, cachedAvatars } = useAvatarStore();
const { cachedWorlds, showWorldDialog } = useWorldStore();
const { showGroupDialog, applyGroup } = useGroupStore();
const { cachedGroups } = storeToRefs(useGroupStore());
const { menuActiveIndex } = storeToRefs(useUiStore());
const { searchText, searchUserResults } = storeToRefs(useSearchStore());
const { clearSearch, moreSearchUser } = useSearchStore();
@@ -523,7 +514,7 @@
.then((args) => {
const map = new Map();
for (const json of args.json) {
const ref = cachedWorlds.value.get(json.id);
const ref = cachedWorlds.get(json.id);
if (typeof ref !== 'undefined') {
map.set(ref.id, ref);
}
@@ -552,7 +543,7 @@
const query = searchText.value;
const queryUpper = query.toUpperCase();
if (!query) {
for (ref of cachedAvatars.value.values()) {
for (ref of cachedAvatars.values()) {
switch (searchAvatarFilter.value) {
case 'all':
avatars.set(ref.id, ref);
@@ -572,7 +563,7 @@
isSearchAvatarLoading.value = false;
} else {
if (searchAvatarFilterRemote.value === 'all' || searchAvatarFilterRemote.value === 'local') {
for (ref of cachedAvatars.value.values()) {
for (ref of cachedAvatars.values()) {
let match = ref.name.toUpperCase().includes(queryUpper);
if (!match && ref.description) {
match = ref.description.toUpperCase().includes(queryUpper);
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible="isAvatarProviderDialogVisible"
:model-value="isAvatarProviderDialogVisible"
:title="t('dialog.avatar_database_provider.header')"
width="600px"
@close="closeDialog">
@@ -13,19 +13,20 @@
size="small"
style="margin-top: 5px"
@change="saveAvatarProviderList">
<el-button slot="append" icon="el-icon-delete" @click="removeAvatarProvider(provider)"></el-button>
<el-button :icon="Delete" @click="removeAvatarProvider(provider)"></el-button>
</el-input>
<el-button size="mini" style="margin-top: 5px" @click="avatarRemoteDatabaseProviderList.push('')">
<el-button size="small" style="margin-top: 5px" @click="avatarRemoteDatabaseProviderList.push('')">
{{ t('dialog.avatar_database_provider.add_provider') }}
</el-button>
</div>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { Delete } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { useAvatarProviderStore } from '../../../stores';
const { t } = useI18n();
+38 -12
View File
@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible="changeLogDialog.visible"
:model-value="changeLogDialog.visible"
:title="t('dialog.change_log.header')"
width="800px"
append-to-body
@@ -13,32 +13,35 @@
<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>.
</span>
<vue-markdown
:source="changeLogDialog.changeLog"
:linkify="false"
style="height: 62vh; overflow-y: auto; margin-top: 10px"></vue-markdown>
<VueShowdown
:markdown="changeLogDialog.changeLog"
flavor="github"
:options="showdownOptions"
@click="handleLinkClick"
style="height: 62vh; overflow-y: auto; margin-top: 10px" />
</div>
<template #footer>
<el-button type="small" @click="openExternalLink('https://github.com/vrcx-team/VRCX/releases')">
<el-button @click="openExternalLink('https://github.com/vrcx-team/VRCX/releases')">
{{ t('dialog.change_log.github') }}
</el-button>
<el-button type="small" @click="openExternalLink('https://patreon.com/Natsumi_VRCX')">
<el-button @click="openExternalLink('https://patreon.com/Natsumi_VRCX')">
{{ t('dialog.change_log.donate') }}
</el-button>
<el-button type="small" @click="closeDialog">
<el-button @click="closeDialog">
{{ t('dialog.change_log.close') }}
</el-button>
</template>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { openExternalLink } from '../../../shared/utils';
import { useVRCXUpdaterStore } from '../../../stores';
import { defineAsyncComponent } from 'vue';
const VueMarkdown = () => import('vue-markdown');
const VueShowdown = defineAsyncComponent(() => import('vue-showdown').then((module) => module.VueShowdown));
const VRCXUpdaterStore = useVRCXUpdaterStore();
@@ -46,9 +49,32 @@
const { t } = useI18n();
const showdownOptions = {
emoji: true,
openLinksInNewWindow: false,
simplifiedAutoLink: true,
excludeTrailingPunctuationFromURLs: true,
literalMidWordUnderscores: true,
strikethrough: true,
tables: true,
tablesHeaderId: false,
ghCodeBlocks: true,
tasklists: true
};
function closeDialog() {
changeLogDialog.value.visible = false;
}
function handleLinkClick(event) {
const target = event.target.closest('a');
if (target && target.href) {
event.preventDefault();
event.stopPropagation();
openExternalLink(target.href);
}
}
</script>
<style>
@@ -1,6 +1,6 @@
<template>
<safe-dialog
:visible="!!feedFiltersDialogMode"
<el-dialog
:model-value="!!feedFiltersDialogMode"
:title="dialogTitle"
width="550px"
destroy-on-close
@@ -14,12 +14,14 @@
placement="top"
style="margin-left: 5px"
:content="setting.tooltip">
<i :class="setting.tooltipIcon || 'el-icon-info'"></i> </el-tooltip
></span>
<el-icon v-if="setting.tooltipWarning"><Warning /></el-icon>
<el-icon v-else><InfoFilled /></el-icon>
</el-tooltip>
</span>
<el-radio-group
v-model="currentSharedFeedFilters[setting.key]"
size="mini"
size="small"
@change="saveSharedFeedFilters">
<el-radio-button v-for="option in setting.options" :key="option.label" :label="option.label">
{{ t(option.textKey) }}
@@ -36,7 +38,7 @@
<span class="toggle-name">{{ setting.name }}</span>
<el-radio-group
v-model="currentSharedFeedFilters[setting.key]"
size="mini"
size="small"
@change="saveSharedFeedFilters">
<el-radio-button v-for="option in setting.options" :key="option.label" :label="option.label">
{{ t(option.textKey) }}
@@ -54,13 +56,14 @@
t('dialog.shared_feed_filters.close')
}}</el-button>
</template>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { InfoFilled, Warning } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import configRepository from '../../../service/config';
import { feedFiltersOptions, sharedFeedFiltersDefaults } from '../../../shared/constants';
import { useNotificationsSettingsStore, usePhotonStore, useSharedFeedStore } from '../../../stores';
@@ -1,14 +1,14 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible="isLaunchOptionsDialogVisible"
:model-value="isLaunchOptionsDialogVisible"
:title="t('dialog.launch_options.header')"
width="600px"
@close="closeDialog">
<div style="font-size: 12px">
{{ t('dialog.launch_options.description') }} <br />
{{ t('dialog.launch_options.example') }}
<el-tag size="mini"
<el-tag size="small"
>--fps=144 --enable-debug-gui --enable-sdk-log-levels --enable-udon-debug-logging
</el-tag>
</div>
@@ -16,7 +16,7 @@
<el-input
v-model="launchOptionsDialog.launchArguments"
type="textarea"
size="mini"
size="small"
show-word-limit
:autosize="{ minRows: 2, maxRows: 5 }"
placeholder=""
@@ -52,22 +52,20 @@
</el-button>
</div>
</template>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ElMessage } from 'element-plus';
import { storeToRefs } from 'pinia';
import { computed, getCurrentInstance, ref } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import configRepository from '../../../service/config';
import { openExternalLink } from '../../../shared/utils';
import { useLaunchStore } from '../../../stores';
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const launchStore = useLaunchStore();
const { isLaunchOptionsDialogVisible } = storeToRefs(launchStore);
@@ -105,14 +103,14 @@
D.vrcLaunchPathOverride.endsWith('.exe') &&
!D.vrcLaunchPathOverride.endsWith('launch.exe')
) {
$message({
ElMessage({
message: 'Invalid path, you must enter VRChat folder or launch.exe',
type: 'error'
});
return;
}
configRepository.setString('vrcLaunchPathOverride', D.vrcLaunchPathOverride);
$message({
ElMessage({
message: 'Updated launch options',
type: 'success'
});
@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible="isNotificationPositionDialogVisible"
:model-value="isNotificationPositionDialogVisible"
:title="t('dialog.notification_position.header')"
width="400px"
@close="closeDialog">
@@ -23,7 +23,7 @@
d="M291.89,5A3.11,3.11,0,0,1,295,8.11V160.64a3.11,3.11,0,0,1-3.11,3.11H8.11A3.11,3.11,0,0,1,5,160.64V8.11A3.11,3.11,0,0,1,8.11,5H291.89m0-5H8.11A8.11,8.11,0,0,0,0,8.11V160.64a8.11,8.11,0,0,0,8.11,8.11H291.89a8.11,8.11,0,0,0,8.11-8.11V8.11A8.11,8.11,0,0,0,291.89,0Z" />
<rect style="fill: #c4c4c4" x="5" y="5" width="290" height="158.75" rx="2.5" />
</svg>
<el-radio-group :value="notificationPosition" size="mini" @input="changeNotificationPosition">
<el-radio-group :model-value="notificationPosition" size="small" @change="changeNotificationPosition">
<el-radio label="topLeft" style="margin: 0; position: absolute; left: 35px; top: 120px"></el-radio>
<el-radio label="top" style="margin: 0; position: absolute; left: 195px; top: 120px"></el-radio>
<el-radio label="topRight" style="margin: 0; position: absolute; right: 25px; top: 120px"></el-radio>
@@ -42,12 +42,12 @@
</el-button>
</div>
</template>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { useNotificationsSettingsStore } from '../../../stores';
const { t } = useI18n();
@@ -1,10 +1,11 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible="ossDialog"
:model-value="ossDialog"
:title="t('dialog.open_source.header')"
width="650px"
@close="closeDialog">
@close="closeDialog"
destroy-on-close>
<div v-once style="height: 350px; overflow: hidden scroll; word-break: break-all">
<div>
<span>{{ t('dialog.open_source.description') }}</span>
@@ -15,12 +16,12 @@
<pre style="font-size: 12px; white-space: pre-line">{{ lib.licenseText }}</pre>
</div>
</div>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { useI18n } from 'vue-i18n-bridge';
import { openSourceSoftwareLicenses } from '../../../shared/constants';
import { useI18n } from 'vue-i18n';
import { openSourceSoftwareLicenses } from '../../../shared/constants/ossLicenses';
const { t } = useI18n();
@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible.sync="enablePrimaryPasswordDialog.visible"
v-model="enablePrimaryPasswordDialog.visible"
:before-close="enablePrimaryPasswordDialog.beforeClose"
:close-on-click-modal="false"
:title="t('dialog.primary_password.header')"
@@ -10,7 +10,7 @@
v-model="enablePrimaryPasswordDialog.password"
:placeholder="t('dialog.primary_password.password_placeholder')"
type="password"
size="mini"
size="small"
maxlength="32"
show-password
autofocus>
@@ -20,7 +20,7 @@
:placeholder="t('dialog.primary_password.re_input_placeholder')"
type="password"
style="margin-top: 5px"
size="mini"
size="small"
maxlength="32"
show-password>
</el-input>
@@ -36,12 +36,12 @@
{{ t('dialog.primary_password.ok') }}
</el-button>
</template>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { useAuthStore } from '../../../stores';
const { t } = useI18n();
@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible="isRegistryBackupDialogVisible"
:model-value="isRegistryBackupDialogVisible"
:title="t('dialog.registry_backup.header')"
width="600px"
@close="closeDialog"
@@ -22,7 +22,7 @@
<span class="name" style="margin-right: 24px">{{ t('dialog.registry_backup.ask_to_restore') }}</span>
<el-switch v-model="vrcRegistryAskRestore" @change="setVrcRegistryAskRestore"></el-switch>
</div>
<data-tables v-bind="registryBackupTable" style="margin-top: 10px">
<DataTable 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">
@@ -31,39 +31,33 @@
</el-table-column>
<el-table-column :label="t('dialog.registry_backup.action')" width="90" align="right">
<template #default="scope">
<el-tooltip
placement="top"
:content="t('dialog.registry_backup.restore')"
:disabled="hideTooltips">
<el-tooltip placement="top" :content="t('dialog.registry_backup.restore')">
<el-button
type="text"
icon="el-icon-upload2"
size="mini"
:icon="Upload"
size="small"
class="button-pd-0"
@click="restoreVrcRegistryBackup(scope.row)"></el-button>
</el-tooltip>
<el-tooltip
placement="top"
:content="t('dialog.registry_backup.save_to_file')"
:disabled="hideTooltips">
<el-tooltip placement="top" :content="t('dialog.registry_backup.save_to_file')">
<el-button
type="text"
icon="el-icon-download"
size="mini"
:icon="Download"
size="small"
class="button-pd-0"
@click="saveVrcRegistryBackupToFile(scope.row)"></el-button>
</el-tooltip>
<el-tooltip
placement="top"
:content="t('dialog.registry_backup.delete')"
:disabled="hideTooltips">
<el-tooltip placement="top" :content="t('dialog.registry_backup.delete')">
<el-button
type="text"
icon="el-icon-delete"
size="mini"
:icon="Delete"
size="small"
class="button-pd-0"
@click="deleteVrcRegistryBackup(scope.row)"></el-button>
</el-tooltip>
</template>
</el-table-column>
</data-tables>
</DataTable>
<div style="display: flex; align-items: center; justify-content: space-between; margin-top: 10px">
<el-button type="danger" size="small" @click="deleteVrcRegistry">{{
t('dialog.registry_backup.reset')
@@ -78,19 +72,20 @@
</div>
</div>
</div>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ElMessage, ElMessageBox } from 'element-plus';
import { Upload, Download, Delete } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
import { getCurrentInstance, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import configRepository from '../../../service/config';
import { downloadAndSaveJson, removeFromArray, formatDateFilter } from '../../../shared/utils';
import { useAppearanceSettingsStore, useVrcxStore, useAdvancedSettingsStore } from '../../../stores';
import { useVrcxStore, useAdvancedSettingsStore } from '../../../stores';
const { hideTooltips } = storeToRefs(useAppearanceSettingsStore());
const { backupVrcRegistry } = useVrcxStore();
const { isRegistryBackupDialogVisible } = storeToRefs(useVrcxStore());
const { vrcRegistryAutoBackup, vrcRegistryAskRestore } = storeToRefs(useAdvancedSettingsStore());
@@ -98,14 +93,11 @@
const { t } = useI18n();
const instance = getCurrentInstance();
const { $confirm, $message, $prompt } = instance.proxy;
const registryBackupTable = ref({
data: [],
tableProps: {
stripe: true,
size: 'mini',
size: 'small',
defaultSort: {
prop: 'date',
order: 'descending'
@@ -129,31 +121,32 @@
}
function restoreVrcRegistryBackup(row) {
$confirm('Continue? Restore Backup', 'Confirm', {
ElMessageBox.confirm('Continue? Restore Backup', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'warning',
callback: (action) => {
type: 'warning'
})
.then((action) => {
if (action !== 'confirm') {
return;
}
const data = JSON.stringify(row.data);
AppApi.SetVRChatRegistry(data)
.then(() => {
$message({
ElMessage({
message: 'VRC registry settings restored',
type: 'success'
});
})
.catch((e) => {
console.error(e);
$message({
ElMessage({
message: `Failed to restore VRC registry settings, check console for full error: ${e}`,
type: 'error'
});
});
}
});
})
.catch(() => {});
}
function saveVrcRegistryBackupToFile(row) {
@@ -168,22 +161,23 @@
}
function deleteVrcRegistry() {
$confirm('Continue? Delete VRC Registry Settings', 'Confirm', {
ElMessageBox.confirm('Continue? Delete VRC Registry Settings', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'warning',
callback: (action) => {
type: 'warning'
})
.then((action) => {
if (action !== 'confirm') {
return;
}
AppApi.DeleteVRChatRegistryFolder().then(() => {
$message({
ElMessage({
message: 'VRC registry settings deleted',
type: 'success'
});
});
}
});
})
.catch(() => {});
}
async function handleBackupVrcRegistry(name) {
@@ -192,16 +186,16 @@
}
async function promptVrcRegistryBackupName() {
const name = await $prompt('Enter a name for the backup', 'Backup Name', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
inputPattern: /\S+/,
inputErrorMessage: 'Name is required',
inputValue: 'Backup'
});
if (name.action === 'confirm') {
await handleBackupVrcRegistry(name.value);
}
try {
const { value } = await ElMessageBox.prompt('Enter a name for the backup', 'Backup Name', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
inputPattern: /\S+/,
inputErrorMessage: 'Name is required',
inputValue: 'Backup'
});
await handleBackupVrcRegistry(value);
} catch (error) {}
}
async function openJsonFileSelectorDialogElectron() {
@@ -260,20 +254,20 @@
}
AppApi.SetVRChatRegistry(json)
.then(() => {
$message({
ElMessage({
message: 'VRC registry settings restored',
type: 'success'
});
})
.catch((e) => {
console.error(e);
$message({
ElMessage({
message: `Failed to restore VRC registry settings, check console for full error: ${e}`,
type: 'error'
});
});
} catch {
$message({
ElMessage({
message: 'Invalid JSON',
type: 'error'
});
@@ -288,3 +282,9 @@
isRegistryBackupDialogVisible.value = false;
}
</script>
<style scoped>
.button-pd-0 {
padding: 0;
}
</style>
@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible="isVRChatConfigDialogVisible"
:model-value="isVRChatConfigDialogVisible"
:title="t('dialog.config_json.header')"
width="420px"
@close="closeDialog">
@@ -16,12 +16,12 @@
<span>/</span>
<span v-text="totalCacheSize"></span>
<span>GB</span>
<el-tooltip placement="top" :content="t('dialog.config_json.refresh')" :disabled="hideTooltips">
<el-tooltip placement="top" :content="t('dialog.config_json.refresh')">
<el-button
type="default"
:loading="VRChatCacheSizeLoading"
size="small"
icon="el-icon-refresh"
:icon="Refresh"
circle
style="margin-left: 5px"
@click="getVRChatCacheSize"></el-button>
@@ -32,7 +32,7 @@
<el-button
size="small"
style="margin-left: 5px"
icon="el-icon-delete"
:icon="Delete"
@click="showDeleteAllVRChatCacheConfirm"
>{{ t('dialog.config_json.delete_cache') }}</el-button
>
@@ -40,13 +40,9 @@
<div style="margin-top: 10px">
<span style="margin-right: 5px">{{ t('dialog.config_json.delete_old_cache') }}</span>
<el-button
size="small"
style="margin-left: 5px"
icon="el-icon-folder-delete"
@click="sweepVRChatCache"
>{{ t('dialog.config_json.sweep_cache') }}</el-button
>
<el-button size="small" style="margin-left: 5px" :icon="FolderDelete" @click="sweepVRChatCache">{{
t('dialog.config_json.sweep_cache')
}}</el-button>
</div>
<div v-for="(item, value) in VRChatConfigList" :key="value" style="display: block; margin-top: 10px">
@@ -55,19 +51,20 @@
<el-input
v-model="VRChatConfigFile[value]"
:placeholder="item.default"
size="mini"
size="small"
: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"
slot="append"
size="mini"
icon="el-icon-folder-opened"
@click="openConfigFolderBrowser(value)"></el-button
></el-input>
style="flex: 1; margin-top: 5px">
<template #append>
<el-button
v-if="item.folderBrowser"
size="small"
:icon="FolderOpened"
@click="openConfigFolderBrowser(value)"></el-button>
</template>
</el-input>
</div>
</div>
@@ -82,7 +79,7 @@
<el-button size="small">
<span>
<span v-text="getVRChatCameraResolution()"></span>
<i class="el-icon-arrow-down el-icon--right"></i>
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</span>
</el-button>
<template #dropdown>
@@ -107,7 +104,7 @@
<el-button size="small">
<span>
<span v-text="getVRChatSpoutResolution()"></span>
<i class="el-icon-arrow-down el-icon--right"></i>
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</span>
</el-button>
<template #dropdown>
@@ -135,7 +132,7 @@
<el-button size="small">
<span>
<span v-text="getVRChatScreenshotResolution()"></span>
<i class="el-icon-arrow-down el-icon--right"></i>
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</span>
</el-button>
<template #dropdown>
@@ -182,18 +179,19 @@
</div>
</div>
</template>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ElMessage, ElMessageBox } from 'element-plus';
import { Refresh, Delete, FolderOpened, FolderDelete, ArrowDown } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
import { computed, getCurrentInstance, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { VRChatCameraResolutions, VRChatScreenshotResolutions } from '../../../shared/constants';
import { getVRChatResolution, openExternalLink } from '../../../shared/utils';
import { useAdvancedSettingsStore, useAppearanceSettingsStore, useGameStore } from '../../../stores';
import { useAdvancedSettingsStore, useGameStore } from '../../../stores';
const { hideTooltips } = storeToRefs(useAppearanceSettingsStore());
const { VRChatUsedCacheSize, VRChatTotalCacheSize, VRChatCacheSizeLoading } = storeToRefs(useGameStore());
const { sweepVRChatCache, getVRChatCacheSize } = useGameStore();
const { folderSelectorDialog } = useAdvancedSettingsStore();
@@ -201,10 +199,6 @@
const { t } = useI18n();
const instance = getCurrentInstance();
const $confirm = instance.proxy.$confirm;
const $message = instance.proxy.$message;
const VRChatConfigFile = ref({});
// it's a object
const VRChatConfigList = ref({
@@ -270,16 +264,17 @@
});
function showDeleteAllVRChatCacheConfirm() {
$confirm(`Continue? Delete all VRChat cache`, 'Confirm', {
ElMessageBox.confirm(`Continue? Delete all VRChat cache`, 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
type: 'info'
})
.then((action) => {
if (action === 'confirm') {
deleteAllVRChatCache();
}
}
});
})
.catch(() => {});
}
async function deleteAllVRChatCache() {
@@ -374,7 +369,7 @@
const parsedConfig = JSON.parse(config);
VRChatConfigFile.value = { ...VRChatConfigFile.value, ...parsedConfig };
} catch {
$message({
ElMessage({
message: 'Invalid JSON in config.json',
type: 'error'
});
@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible="isYouTubeApiDialogVisible"
:model-value="isYouTubeApiDialogVisible"
:title="t('dialog.youtube_api.header')"
width="400px"
@close="closeDialog">
@@ -28,13 +28,13 @@
</el-button>
</div>
</template>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ElMessage } from 'element-plus';
import { storeToRefs } from 'pinia';
import { getCurrentInstance } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import { openExternalLink } from '../../../shared/utils';
import { useAdvancedSettingsStore } from '../../../stores';
@@ -46,10 +46,7 @@
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const props = defineProps({
defineProps({
isYouTubeApiDialogVisible: {
type: Boolean,
default: false
@@ -61,7 +58,7 @@
async function testYouTubeApiKey() {
const previousKey = youTubeApiKey.value;
if (!youTubeApiKey.value) {
$message({
ElMessage({
message: 'YouTube API key removed',
type: 'success'
});
@@ -71,13 +68,13 @@
const data = await lookupYouTubeVideo('dQw4w9WgXcQ');
if (!data) {
setYouTubeApiKey(previousKey);
$message({
ElMessage({
message: 'Invalid YouTube API key',
type: 'error'
});
} else {
setYouTubeApiKey(youTubeApiKey.value);
$message({
ElMessage({
message: 'YouTube API key valid!',
type: 'success'
});
+18 -22
View File
@@ -2,9 +2,8 @@
<div v-show="isSideBarTabShow" id="aside" class="x-aside-container" :style="{ width: `${asideWidth}px` }">
<div style="display: flex; align-items: baseline">
<el-select
value=""
clearable
:placeholder="$t('side_panel.search_placeholder')"
:placeholder="t('side_panel.search_placeholder')"
filterable
remote
:remote-method="quickSearchRemoteMethod"
@@ -20,10 +19,10 @@
}}</span>
<span v-if="!item.ref.isFriend" class="extra"></span>
<span v-else-if="item.ref.state === 'offline'" class="extra">{{
$t('side_panel.search_result_active')
t('side_panel.search_result_active')
}}</span>
<span v-else-if="item.ref.state === 'active'" class="extra">{{
$t('side_panel.search_result_offline')
t('side_panel.search_result_offline')
}}</span>
<Location
v-else
@@ -32,29 +31,24 @@
:traveling="item.ref.travelingToLocation"
:link="false" />
</div>
<img v-lazy="userImage(item.ref)" class="avatar" />
<img :src="userImage(item.ref)" class="avatar" loading="lazy" />
</template>
<span v-else>
{{ $t('side_panel.search_result_more') }}
{{ t('side_panel.search_result_more') }}
<span style="font-weight: bold">{{ item.label }}</span>
</span>
</div>
</el-option>
</el-select>
<el-tooltip placement="bottom" :content="$t('side_panel.direct_access_tooltip')" :disabled="hideTooltips">
<el-button
type="default"
size="mini"
icon="el-icon-discover"
circle
@click="directAccessPaste"></el-button>
<el-tooltip placement="bottom" :content="t('side_panel.direct_access_tooltip')">
<el-button type="default" size="small" :icon="Compass" circle @click="directAccessPaste"></el-button>
</el-tooltip>
<el-tooltip placement="bottom" :content="$t('side_panel.refresh_tooltip')" :disabled="hideTooltips">
<el-tooltip placement="bottom" :content="t('side_panel.refresh_tooltip')">
<el-button
type="default"
:loading="isRefreshFriendsLoading"
size="mini"
icon="el-icon-refresh"
size="small"
:icon="Refresh"
circle
style="margin-right: 10px"
@click="refreshFriendsList" />
@@ -62,8 +56,8 @@
</div>
<el-tabs class="zero-margin-tabs" stretch style="height: calc(100% - 60px); margin-top: 5px">
<el-tab-pane>
<template slot="label">
<span>{{ $t('side_panel.friends') }}</span>
<template #label>
<span>{{ t('side_panel.friends') }}</span>
<span style="color: #909399; font-size: 12px; margin-left: 10px">
({{ onlineFriendCount }}/{{ friends.size }})
</span>
@@ -72,8 +66,8 @@
<FriendsSidebar @confirm-delete-friend="confirmDeleteFriend" />
</el-tab-pane>
<el-tab-pane lazy>
<template slot="label">
<span>{{ $t('side_panel.groups') }}</span>
<template #label>
<span>{{ t('side_panel.groups') }}</span>
<span style="color: #909399; font-size: 12px; margin-left: 10px">
({{ groupInstances.length }})
</span>
@@ -85,9 +79,10 @@
</template>
<script setup>
import { ref } from 'vue';
import { Refresh, Compass } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { userImage } from '../../shared/utils';
import {
useAppearanceSettingsStore,
@@ -101,11 +96,12 @@
const { friends, isRefreshFriendsLoading, onlineFriendCount } = storeToRefs(useFriendStore());
const { refreshFriendsList, confirmDeleteFriend } = useFriendStore();
const { hideTooltips, asideWidth } = storeToRefs(useAppearanceSettingsStore());
const { asideWidth } = storeToRefs(useAppearanceSettingsStore());
const { menuActiveIndex } = storeToRefs(useUiStore());
const { quickSearchRemoteMethod, quickSearchChange, directAccessPaste } = useSearchStore();
const { quickSearchItems } = storeToRefs(useSearchStore());
const { inGameGroupOrder, groupInstances } = storeToRefs(useGroupStore());
const { t } = useI18n();
const isSideBarTabShow = computed(() => {
return !(menuActiveIndex.value === 'friendList' || menuActiveIndex.value === 'charts');
+24 -13
View File
@@ -7,13 +7,13 @@
isFriendsGroupMe = !isFriendsGroupMe;
saveFriendsGroupStates();
">
<i class="el-icon-arrow-right" :class="{ rotate: isFriendsGroupMe }"></i>
<span style="margin-left: 5px">{{ $t('side_panel.me') }}</span>
<el-icon class="rotation-transition" :class="{ 'is-rotated': isFriendsGroupMe }"><ArrowRight /></el-icon>
<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)" />
<img :src="userImage(currentUser)" loading="lazy" />
</div>
<div class="detail">
<span class="name" :style="{ color: currentUser.$userColour }">{{ currentUser.displayName }}</span>
@@ -43,9 +43,9 @@
isVIPFriends = !isVIPFriends;
saveFriendsGroupStates();
">
<i class="el-icon-arrow-right" :class="{ rotate: isVIPFriends }"></i>
<el-icon class="rotation-transition" :class="{ 'is-rotated': isVIPFriends }"><ArrowRight /></el-icon>
<span style="margin-left: 5px">
{{ $t('side_panel.favorite') }} &horbar;
{{ t('side_panel.favorite') }} &horbar;
{{ vipFriendsDisplayNumber }}
</span>
</div>
@@ -81,9 +81,11 @@
<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>
<el-icon class="rotation-transition" :class="{ 'is-rotated': !isSidebarGroupByInstanceCollapsed }"
><ArrowRight
/></el-icon>
<span style="margin-left: 5px"
>{{ $t('side_panel.same_instance') }} &horbar; {{ friendsInSameInstance.length }}</span
>{{ t('side_panel.same_instance') }} &horbar; {{ friendsInSameInstance.length }}</span
>
</div>
@@ -114,9 +116,9 @@
isOnlineFriends = !isOnlineFriends;
saveFriendsGroupStates();
">
<i class="el-icon-arrow-right" :class="{ rotate: isOnlineFriends }"></i>
<el-icon class="rotation-transition" :class="{ 'is-rotated': isOnlineFriends }"><ArrowRight /></el-icon>
<span style="margin-left: 5px"
>{{ $t('side_panel.online') }} &horbar; {{ onlineFriendsByGroupStatus.length }}</span
>{{ t('side_panel.online') }} &horbar; {{ onlineFriendsByGroupStatus.length }}</span
>
</div>
<div v-show="isOnlineFriends">
@@ -134,8 +136,8 @@
isActiveFriends = !isActiveFriends;
saveFriendsGroupStates();
">
<i class="el-icon-arrow-right" :class="{ rotate: isActiveFriends }"></i>
<span style="margin-left: 5px">{{ $t('side_panel.active') }} &horbar; {{ activeFriends.length }}</span>
<el-icon class="rotation-transition" :class="{ 'is-rotated': isActiveFriends }"><ArrowRight /></el-icon>
<span style="margin-left: 5px">{{ t('side_panel.active') }} &horbar; {{ activeFriends.length }}</span>
</div>
<div v-show="isActiveFriends">
<friend-item
@@ -152,8 +154,8 @@
isOfflineFriends = !isOfflineFriends;
saveFriendsGroupStates();
">
<i class="el-icon-arrow-right" :class="{ rotate: isOfflineFriends }"></i>
<span style="margin-left: 5px">{{ $t('side_panel.offline') }} &horbar; {{ offlineFriends.length }}</span>
<el-icon class="rotation-transition" :class="{ 'is-rotated': isOfflineFriends }"><ArrowRight /></el-icon>
<span style="margin-left: 5px">{{ t('side_panel.offline') }} &horbar; {{ offlineFriends.length }}</span>
</div>
<div v-show="isOfflineFriends">
<friend-item
@@ -167,8 +169,10 @@
</template>
<script setup>
import { ArrowRight } from '@element-plus/icons-vue';
import { computed, ref } from 'vue';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import FriendItem from '../../../components/FriendItem.vue';
import configRepository from '../../../service/config';
import { isRealInstance, userImage, userStatusClass } from '../../../shared/utils';
@@ -182,6 +186,7 @@
useUserStore
} from '../../../stores';
const emit = defineEmits(['confirm-delete-friend']);
const { t } = useI18n();
const { vipFriends, onlineFriends, activeFriends, offlineFriends } = storeToRefs(useFriendStore());
const { isSidebarGroupByInstance, isHideFriendsInSameInstance, isSidebarDivideByFriendGroup } =
@@ -359,4 +364,10 @@
.x-link:hover span {
text-decoration: underline;
}
.is-rotated {
transform: rotate(90deg);
}
.rotation-transition {
transition: transform 0.2s ease-in-out;
}
</style>
+17 -12
View File
@@ -1,17 +1,15 @@
<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' }">
<template v-for="(group, index) in groupedGroupInstances" :key="getGroupId(group)">
<div 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>
<el-icon
class="rotation-transition"
:class="{
'is-rotated': !groupInstancesCfg[getGroupId(group)].isCollapsed
}"
><ArrowRight
/></el-icon>
<span style="margin-left: 5px">{{ group[0].group.name }} {{ group.length }}</span>
</div>
</div>
@@ -24,7 +22,7 @@
@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)" />
<img :src="getSmallGroupIconUrl(ref.group.iconUrl)" loading="lazy" />
</div>
<div class="detail">
<span class="name">
@@ -43,6 +41,7 @@
</template>
<script setup>
import { ArrowRight } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
import { computed, ref } from 'vue';
import { convertFileUrlToImageUrl } from '../../../shared/utils';
@@ -103,4 +102,10 @@
.x-link:hover span {
text-decoration: underline;
}
.is-rotated {
transform: rotate(90deg);
}
.rotation-transition {
transition: transform 0.2s ease-in-out;
}
</style>
+38 -20
View File
@@ -1,12 +1,14 @@
<template>
<div id="chart" class="x-container" v-show="menuActiveIndex === 'tools'">
<div id="chart" class="x-container" v-show="isShowToolsTab">
<div class="options-container" style="margin-top: 0">
<span class="header">Tools</span>
<div class="tool-categories">
<div class="tool-category">
<div class="category-header" @click="toggleCategory('group')">
<i class="el-icon-arrow-right" :class="{ rotate: !categoryCollapsed['group'] }"></i>
<el-icon class="rotation-transition" :class="{ 'is-rotated': !categoryCollapsed['group'] }"
><ArrowRight
/></el-icon>
<span class="category-title">Group</span>
</div>
<div class="tools-grid" v-show="!categoryCollapsed['group']">
@@ -26,7 +28,9 @@
<div class="tool-category">
<div class="category-header" @click="toggleCategory('image')">
<i class="el-icon-arrow-right" :class="{ rotate: !categoryCollapsed['image'] }"></i>
<el-icon class="rotation-transition" :class="{ 'is-rotated': !categoryCollapsed['image'] }"
><ArrowRight
/></el-icon>
<span class="category-title">Image</span>
</div>
<div class="tools-grid" v-show="!categoryCollapsed['image']">
@@ -57,7 +61,9 @@
<div class="tool-category">
<div class="category-header" @click="toggleCategory('user')">
<i class="el-icon-arrow-right" :class="{ rotate: !categoryCollapsed['user'] }"></i>
<el-icon class="rotation-transition" :class="{ 'is-rotated': !categoryCollapsed['user'] }"
><ArrowRight
/></el-icon>
<span class="category-title">User</span>
</div>
<div class="tools-grid" v-show="!categoryCollapsed['user']">
@@ -76,26 +82,31 @@
</div>
</div>
</div>
<GroupCalendarDialog :visible="isGroupCalendarDialogVisible" @close="isGroupCalendarDialogVisible = false" />
<ScreenshotMetadataDialog
:isScreenshotMetadataDialogVisible="isScreenshotMetadataDialogVisible"
@close="isScreenshotMetadataDialogVisible = false" />
<NoteExportDialog
:isNoteExportDialogVisible="isNoteExportDialogVisible"
@close="isNoteExportDialogVisible = false" />
<template v-if="isShowToolsTab">
<GroupCalendarDialog
:visible="isGroupCalendarDialogVisible"
@close="isGroupCalendarDialogVisible = false" />
<ScreenshotMetadataDialog
:isScreenshotMetadataDialogVisible="isScreenshotMetadataDialogVisible"
@close="isScreenshotMetadataDialogVisible = false" />
<NoteExportDialog
:isNoteExportDialogVisible="isNoteExportDialogVisible"
@close="isNoteExportDialogVisible = false" />
<GalleryDialog />
</template>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { ArrowRight } from '@element-plus/icons-vue';
import { ref, defineAsyncComponent, computed } from 'vue';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n-bridge';
import { useUiStore, useGalleryStore } from '../../stores';
import GroupCalendarDialog from './dialogs/GroupCalendarDialog.vue';
import ScreenshotMetadataDialog from './dialogs/ScreenshotMetadataDialog.vue';
import NoteExportDialog from './dialogs/NoteExportDialog.vue';
const { t } = useI18n();
const GroupCalendarDialog = defineAsyncComponent(() => import('./dialogs/GroupCalendarDialog.vue'));
const ScreenshotMetadataDialog = defineAsyncComponent(() => import('./dialogs/ScreenshotMetadataDialog.vue'));
const NoteExportDialog = defineAsyncComponent(() => import('./dialogs/NoteExportDialog.vue'));
const GalleryDialog = defineAsyncComponent(() => import('./dialogs/GalleryDialog.vue'));
const uiStore = useUiStore();
const { showGalleryDialog } = useGalleryStore();
@@ -111,6 +122,8 @@
const isScreenshotMetadataDialogVisible = ref(false);
const isNoteExportDialogVisible = ref(false);
const isShowToolsTab = computed(() => menuActiveIndex.value === 'tools');
const showGroupCalendarDialog = () => {
isGroupCalendarDialogVisible.value = true;
};
@@ -159,6 +172,7 @@
}
.category-title {
margin-left: 5px;
font-size: 16px;
font-weight: 600;
color: var(--el-color-primary);
@@ -185,7 +199,7 @@
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
::v-deep .el-card__body {
:deep(.el-card__body) {
overflow: visible;
}
@@ -230,13 +244,17 @@
}
}
::v-deep .el-card {
:deep(.el-card) {
border-radius: 8px;
width: 100%;
overflow: visible;
}
.rotate {
.is-rotated {
transform: rotate(90deg);
}
.rotation-transition {
transition: transform 0.2s ease-in-out;
}
</style>
@@ -6,7 +6,7 @@
<div v-if="showGroupName" class="event-group-name" @click="onGroupClick">
{{ groupName }}
</div>
<el-popover placement="right" width="500" trigger="hover">
<el-popover placement="right" :width="500" trigger="hover">
<el-descriptions :title="event.title" size="small" :column="2" class="event-title-popover">
<template #extra>
<div>
@@ -28,9 +28,11 @@
}}</el-descriptions-item>
<el-descriptions-item label="Description">{{ event.description }}</el-descriptions-item>
</el-descriptions>
<div class="event-title-content" slot="reference" @click="onGroupClick">
{{ event.title }}
</div>
<template #reference>
<div class="event-title-content" @click="onGroupClick">
{{ event.title }}
</div>
</template>
</el-popover>
</div>
<div class="event-info">
@@ -43,19 +45,18 @@
</div>
</div>
<div v-if="isFollowing" class="following-badge">
<i class="el-icon-check"></i>
<el-icon><Check /></el-icon>
</div>
</el-card>
</template>
<script setup>
import { Check } from '@element-plus/icons-vue';
import { computed } from 'vue';
import dayjs from 'dayjs';
import { storeToRefs } from 'pinia';
import { useGroupStore } from '../../../stores';
const { cachedGroups } = storeToRefs(useGroupStore());
const { showGroupDialog } = useGroupStore();
const { cachedGroups, showGroupDialog } = useGroupStore();
const props = defineProps({
event: {
@@ -87,13 +88,13 @@
if (props.event.imageUrl) {
return props.event.imageUrl;
} else {
return cachedGroups.value.get(props.event.ownerId)?.bannerUrl || '';
return cachedGroups.get(props.event.ownerId)?.bannerUrl || '';
}
});
const groupName = computed(() => {
if (!props.event) return '';
return cachedGroups.value.get(props.event.ownerId)?.name || '';
return cachedGroups.get(props.event.ownerId)?.name || '';
});
const formattedTime = computed(() => {
@@ -131,7 +132,7 @@
flex: 0 0 280px;
max-width: 280px;
}
::v-deep .el-card__body {
:deep(.el-card__body) {
overflow: visible;
}
.banner {
@@ -213,7 +214,7 @@
}
}
}
::v-deep .el-card {
:deep(.el-card) {
border-radius: 8px;
width: 100%;
overflow: visible;
File diff suppressed because it is too large Load Diff
+29 -22
View File
@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible="visible"
:model-value="visible"
:title="t('dialog.group_calendar.header')"
:show-close="false"
width="90vw"
@@ -10,9 +10,9 @@
<template #title>
<div class="dialog-title-container">
<span>{{ t('dialog.group_calendar.header') }}</span>
<!-- <el-button @click="toggleViewMode" type="primary" size="small" class="view-toggle-btn">
<el-button @click="toggleViewMode" type="primary" size="small" class="view-toggle-btn">
{{ viewMode === 'timeline' ? 'List View' : 'Calendar View' }}
</el-button> -->
</el-button>
</div>
</template>
<div class="top-content">
@@ -41,23 +41,23 @@
<div class="calendar-container">
<el-calendar v-model="selectedDay" v-loading="isLoading">
<template #dateCell="{ date }">
<template #date-cell="{ data }">
<div class="date">
<div
class="calendar-date-content"
:class="{
'has-events': filteredCalendar[formatDateKey(date)]?.length
'has-events': filteredCalendar[formatDateKey(data.date)]?.length
}">
{{ dayjs(date).utc().format('D') }}
{{ dayjs(data.date).format('D') }}
<div
v-if="filteredCalendar[formatDateKey(date)]?.length"
v-if="filteredCalendar[formatDateKey(data.date)]?.length"
class="calendar-event-badge"
:class="
followingCalendarDate[formatDateKey(date)]
followingCalendarDate[formatDateKey(data.date)]
? 'has-following'
: 'no-following'
">
{{ filteredCalendar[formatDateKey(date)]?.length }}
{{ filteredCalendar[formatDateKey(data.date)]?.length }}
</div>
</div>
</div>
@@ -72,7 +72,7 @@
placeholder="Search groups or events..."
clearable
size="small"
prefix-icon="el-icon-search"
prefix-:icon="Search"
class="search-input" />
</div>
@@ -80,9 +80,11 @@
<div v-if="filteredGroupEvents.length" class="groups-container">
<div v-for="group in filteredGroupEvents" :key="group.groupId" class="group-row">
<div class="group-header" @click="toggleGroup(group.groupId)">
<i
class="el-icon-arrow-right"
:class="{ rotate: !groupCollapsed[group.groupId] }"></i>
<el-icon
class="el-icon-arrow-right rotation-transition"
:class="{ rotate: !groupCollapsed[group.groupId] }"
><ArrowRight
/></el-icon>
{{ group.groupName }}
</div>
<div class="events-row" v-show="!groupCollapsed[group.groupId]">
@@ -103,20 +105,20 @@
</div>
</transition>
</div>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ArrowRight } from '@element-plus/icons-vue';
import { ref, watch, computed } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import dayjs from 'dayjs';
import { groupRequest } from '../../../api';
import { useGroupStore } from '../../../stores';
import GroupCalendarEventCard from '../components/GroupCalendarEventCard.vue';
import { replaceBioSymbols } from '../../../shared/utils';
const { cachedGroups } = storeToRefs(useGroupStore());
const { cachedGroups } = useGroupStore();
const { t } = useI18n();
@@ -133,7 +135,7 @@
const followingCalendar = ref([]);
const selectedDay = ref(new Date());
const isLoading = ref(false);
const viewMode = ref('grid');
const viewMode = ref('timeline');
const searchQuery = ref('');
const groupCollapsed = ref({});
@@ -306,7 +308,7 @@
function getGroupName(event) {
if (!event) return '';
return cachedGroups.value.get(event.ownerId)?.name || '';
return cachedGroups.get(event.ownerId)?.name || '';
}
async function getCalendarData() {
@@ -357,7 +359,7 @@
<style lang="scss" scoped>
.x-dialog {
::v-deep .el-dialog {
:deep(.el-dialog) {
max-height: 750px;
.el-dialog__body {
height: 680px;
@@ -369,7 +371,7 @@
overflow: hidden;
.timeline-view {
.timeline-container {
::v-deep .el-timeline {
:deep(.el-timeline) {
width: 100%;
height: 100%;
min-width: 200px;
@@ -469,6 +471,7 @@
}
.calendar-container {
width: 609px;
height: 100%;
flex-shrink: 0;
}
}
@@ -540,4 +543,8 @@
.rotate {
transform: rotate(90deg);
}
.rotation-transition {
transition: transform 0.2s ease-in-out;
}
</style>
+28 -24
View File
@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible="isNoteExportDialogVisible"
:model-value="isNoteExportDialogVisible"
:title="t('dialog.note_export.header')"
width="1000px"
@close="closeDialog">
@@ -26,7 +26,7 @@
{{ t('dialog.note_export.cancel') }}
</el-button>
<span v-if="loading" style="margin: 10px">
<i class="el-icon-loading" style="margin-right: 5px"></i>
<el-icon style="margin-right: 5px"><Loading /></el-icon>
{{ t('dialog.note_export.progress') }} {{ progress }}/{{ progressTotal }}
</span>
@@ -40,57 +40,61 @@
<pre style="white-space: pre-wrap; font-size: 12px" v-text="errors"></pre>
</template>
<data-tables v-loading="loading" v-bind="noteExportTable" style="margin-top: 10px">
<DataTable v-loading="loading" v-bind="noteExportTable" style="margin-top: 10px">
<el-table-column :label="t('table.import.image')" width="70" prop="currentAvatarThumbnailImageUrl">
<template slot-scope="scope">
<el-popover placement="right" height="500px" trigger="hover">
<img slot="reference" v-lazy="userImage(scope.row.ref)" class="friends-list-avatar" />
<template #default="{ row }">
<el-popover placement="right" :width="500" trigger="hover">
<template #reference>
<img :src="userImage(row.ref)" class="friends-list-avatar" loading="lazy" />
</template>
<img
v-lazy="userImageFull(scope.row.ref)"
class="friends-list-avatar"
style="height: 500px; cursor: pointer"
@click="showFullscreenImageDialog(userImageFull(scope.row.ref))" />
:src="userImageFull(row.ref)"
:class="['friends-list-avatar', 'x-popover-image']"
style="cursor: pointer"
loading="lazy"
@click="showFullscreenImageDialog(userImageFull(row.ref))" />
</el-popover>
</template>
</el-table-column>
<el-table-column :label="t('table.import.name')" width="170" prop="name">
<template slot-scope="scope">
<span class="x-link" @click="showUserDialog(scope.row.id)" v-text="scope.row.name"></span>
<template #default="{ row }">
<span class="x-link" @click="showUserDialog(row.id)" v-text="row.name"></span>
</template>
</el-table-column>
<el-table-column :label="t('table.import.note')" prop="memo">
<template slot-scope="scope">
<template #default="{ row }">
<el-input
v-model="scope.row.memo"
v-model="row.memo"
type="textarea"
maxlength="256"
show-word-limit
:rows="2"
:autosize="{ minRows: 1, maxRows: 10 }"
size="mini"
size="small"
resize="none"></el-input>
</template>
</el-table-column>
<el-table-column :label="t('table.import.skip_export')" width="90" align="right">
<template slot-scope="scope">
<template #default="{ row }">
<el-button
type="text"
icon="el-icon-close"
size="mini"
@click="removeFromNoteExportTable(scope.row)"></el-button>
:icon="Close"
size="small"
@click="removeFromNoteExportTable(row)"></el-button>
</template>
</el-table-column>
</data-tables>
</safe-dialog>
</DataTable>
</el-dialog>
</template>
<script setup>
import { Close, Loading } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
import { ref, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { useI18n } from 'vue-i18n';
import * as workerTimers from 'worker-timers';
import { miscRequest } from '../../../api';
import { removeFromArray, userImage, userImageFull } from '../../../shared/utils';
@@ -112,7 +116,7 @@
data: [],
tableProps: {
stripe: true,
size: 'mini'
size: 'small'
},
layout: 'table'
});
@@ -1,7 +1,7 @@
<template>
<safe-dialog
<el-dialog
class="x-dialog"
:visible="isScreenshotMetadataDialogVisible"
:model-value="isScreenshotMetadataDialogVisible"
:title="t('dialog.screenshot_metadata.header')"
width="1050px"
@close="closeDialog">
@@ -16,35 +16,35 @@
}}</span>
<br />
<br />
<el-button size="small" icon="el-icon-folder-opened" @click="getAndDisplayScreenshotFromFile">{{
<el-button size="small" :icon="FolderOpened" @click="getAndDisplayScreenshotFromFile">{{
t('dialog.screenshot_metadata.browse')
}}</el-button>
<el-button size="small" icon="el-icon-picture-outline" @click="getAndDisplayLastScreenshot">{{
<el-button size="small" :icon="Picture" @click="getAndDisplayLastScreenshot">{{
t('dialog.screenshot_metadata.last_screenshot')
}}</el-button>
<el-button
size="small"
icon="el-icon-copy-document"
:icon="CopyDocument"
@click="copyImageToClipboard(screenshotMetadataDialog.metadata.filePath)"
>{{ t('dialog.screenshot_metadata.copy_image') }}</el-button
>
<el-button
size="small"
icon="el-icon-folder"
:icon="Folder"
@click="openImageFolder(screenshotMetadataDialog.metadata.filePath)"
>{{ t('dialog.screenshot_metadata.open_folder') }}</el-button
>
<el-button
v-if="currentUser.$isVRCPlus && screenshotMetadataDialog.metadata.filePath"
size="small"
icon="el-icon-upload2"
:icon="Upload"
@click="uploadScreenshotToGallery"
>{{ t('dialog.screenshot_metadata.upload') }}</el-button
>
<el-button
v-if="screenshotMetadataDialog.metadata.filePath"
size="small"
icon="el-icon-delete"
:icon="Delete"
@click="deleteMetadata(screenshotMetadataDialog.metadata.filePath)"
>{{ t('dialog.screenshot_metadata.delete_metadata') }}</el-button
>
@@ -93,7 +93,7 @@
v-if="screenshotMetadataDialog.metadata.fileResolution"
style="margin-right: 5px"
v-text="screenshotMetadataDialog.metadata.fileResolution"></span>
<el-tag v-if="screenshotMetadataDialog.metadata.fileSize" type="info" effect="plain" size="mini">{{
<el-tag v-if="screenshotMetadataDialog.metadata.fileSize" type="info" effect="plain" size="small">{{
screenshotMetadataDialog.metadata.fileSize
}}</el-tag>
<br />
@@ -117,35 +117,23 @@
style="margin-top: 10px"
@change="screenshotMetadataCarouselChange">
<el-carousel-item>
<span placement="top" width="700px" trigger="click">
<img
slot="reference"
class="x-link"
:src="screenshotMetadataDialog.metadata.previousFilePath"
style="width: 100%; height: 100%; object-fit: contain" />
</span>
<img
class="x-link"
:src="screenshotMetadataDialog.metadata.previousFilePath"
style="width: 100%; height: 100%; object-fit: contain" />
</el-carousel-item>
<el-carousel-item>
<span
placement="top"
width="700px"
trigger="click"
@click="showFullscreenImageDialog(screenshotMetadataDialog.metadata.filePath)">
<img
slot="reference"
class="x-link"
:src="screenshotMetadataDialog.metadata.filePath"
style="width: 100%; height: 100%; object-fit: contain" />
</span>
<img
class="x-link"
:src="screenshotMetadataDialog.metadata.filePath"
style="width: 100%; height: 100%; object-fit: contain"
@click="showFullscreenImageDialog(screenshotMetadataDialog.metadata.filePath)" />
</el-carousel-item>
<el-carousel-item>
<span placement="top" width="700px" trigger="click">
<img
slot="reference"
class="x-link"
:src="screenshotMetadataDialog.metadata.nextFilePath"
style="width: 100%; height: 100%; object-fit: contain" />
</span>
<img
class="x-link"
:src="screenshotMetadataDialog.metadata.nextFilePath"
style="width: 100%; height: 100%; object-fit: contain" />
</el-carousel-item>
</el-carousel>
<br />
@@ -164,13 +152,15 @@
<br />
</span>
</div>
</safe-dialog>
</el-dialog>
</template>
<script setup>
import { ElMessage } from 'element-plus';
import { FolderOpened, Picture, CopyDocument, Folder, Upload, Delete } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
import { getCurrentInstance, reactive, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n-bridge';
import { reactive, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { vrcPlusImageRequest } from '../../../api';
import { useGalleryStore, useUserStore, useVrcxStore } from '../../../stores';
import { formatDateFilter } from '../../../shared/utils';
@@ -181,9 +171,6 @@
const { t } = useI18n();
const instance = getCurrentInstance();
const $message = instance.proxy.$message;
const userStore = useUserStore();
const { lookupUser } = userStore;
@@ -286,7 +273,7 @@
return;
}
AppApi.CopyImageToClipboard(path).then(() => {
$message({
ElMessage({
message: 'Image copied to clipboard',
type: 'success'
});
@@ -297,7 +284,7 @@
return;
}
AppApi.OpenFolderAndSelectItem(path).then(() => {
$message({
ElMessage({
message: 'Opened image folder',
type: 'success'
});
@@ -309,13 +296,13 @@
}
AppApi.DeleteScreenshotMetadata(path).then((result) => {
if (!result) {
$message({
ElMessage({
message: t('message.screenshot_metadata.delete_failed'),
type: 'error'
});
return;
}
$message({
ElMessage({
message: t('message.screenshot_metadata.deleted'),
type: 'success'
});
@@ -326,7 +313,7 @@
function uploadScreenshotToGallery() {
const D = screenshotMetadataDialog;
if (D.metadata.fileSizeBytes > 10000000) {
$message({
ElMessage({
message: t('message.file.too_large'),
type: 'error'
});
@@ -339,7 +326,7 @@
.uploadGalleryImage(base64Body)
.then((args) => {
handleGalleryImageAdd(args);
$message({
ElMessage({
message: t('message.gallery.uploaded'),
type: 'success'
});
@@ -350,7 +337,7 @@
});
})
.catch((err) => {
$message({
ElMessage({
message: t('message.gallery.failed'),
type: 'error'
});