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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user