mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-05-06 22:46:06 +02:00
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:
@@ -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();
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
{{ scope.row.previousDisplayName }} <el-icon><Right /></el-icon>
|
||||
</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>
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
<span v-if="!scope.row.inCache" style="color: #aaa"
|
||||
><i class="el-icon-download"></i> </span
|
||||
><el-icon><Download /></el-icon> </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>
|
||||
|
||||
<span v-if="!scope.row.inCache" style="color: #aaa"
|
||||
><i class="el-icon-download"></i> </span
|
||||
><el-icon><Download /></el-icon> </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>
|
||||
|
||||
<span v-if="!scope.row.inCache" style="color: #aaa"
|
||||
><i class="el-icon-download"></i> </span
|
||||
><el-icon><Download /></el-icon> </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>
|
||||
|
||||
<span v-if="!scope.row.inCache" style="color: #aaa"
|
||||
><i class="el-icon-download"></i> </span
|
||||
><el-icon><Download /></el-icon> </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
@@ -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
@@ -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);
|
||||
|
||||
+250
-215
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();
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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') }} ―
|
||||
{{ t('side_panel.favorite') }} ―
|
||||
{{ 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') }} ― {{ friendsInSameInstance.length }}</span
|
||||
>{{ t('side_panel.same_instance') }} ― {{ 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') }} ― {{ onlineFriendsByGroupStatus.length }}</span
|
||||
>{{ t('side_panel.online') }} ― {{ 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') }} ― {{ activeFriends.length }}</span>
|
||||
<el-icon class="rotation-transition" :class="{ 'is-rotated': isActiveFriends }"><ArrowRight /></el-icon>
|
||||
<span style="margin-left: 5px">{{ t('side_panel.active') }} ― {{ 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') }} ― {{ offlineFriends.length }}</span>
|
||||
<el-icon class="rotation-transition" :class="{ 'is-rotated': isOfflineFriends }"><ArrowRight /></el-icon>
|
||||
<span style="margin-left: 5px">{{ t('side_panel.offline') }} ― {{ 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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user