Upgrade to Vue3 and Element Plus (#1374)

* Update Vue devtools

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

* fix: i18n

* global components

* change v-deep

* upgrade vue-lazyload

* data table

* update enlint and safe-dialog

* package.json and vite.config.js

* el-icon

* el-message

* vue 2 -> vue3 migration changes

* $pinia

* dialog

* el-popover slot

* lint

* chore

* slot

* scss

* remote state access

* misc

* jsconfig

* el-button size mini -> small

* :model-value

* ElMessageBox

* datatable

* remove v-lazyload

* template #dropdown

* mini -> small

* css

* byebye hideTooltips

* use sass-embedded

* Update SQLite, remove unneeded libraries

* Fix shift remove local avatar favorites

* Electron arm64

* arm64 support

* bye pug

* f-word vite hah

* misc

* remove safe dialog component

* Add self invite to launch dialog

* Fix errors

* Icons 1

* improve localfavorite loading performance

* improve favorites world item performance

* dialog visibility changes for Element Plus

* clear element plus error

* import performance

* revert App.vue hah

* hah

* Revert "Add self invite to launch dialog"

This reverts commit 4801cfad58.

* Toggle self invite/open in-game

* Self invite on launch dialog

* el-button icon

* el-icon

* fix user dialog tab switching logic

* fix PlayerList

* Formatting changes

* More icons

* Fix friend log table

* loading margin

* fix markdown

* fix world dialog tab switching issue

* Fixes and formatting

* fix: global i18n.t export

* fix favorites world tab not working

* Create instance, displayName

* Remove group members sort by userId

* Fix loading dialog tabs on swtich

* Star

* charts console.warn

* wip: fix charts

* wip: fix charts

* wip: charts composables

* fix favorite item tooltip warning

* Fixes and formatting

* Clean up image dialogs

* Remove unused method

* Fix platform/size border

* Fix platform/size border

* $vr

* fix friendExportDialogVisible binding

* ElMessageBox and Settings

* Login formatting

* Rename VR overlay query

* Fix image popover and userdialog badges

* Formatting

* Big buttons

* Fixes, update Cef

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

* Fix z-index

* vr overlay

* vite input add theme

* defineAsyncComponent

* ISO 639-1

* fix i18n

* clean t

* Formatting, fix calendar, rotate arrows

* Show user status when user is offline

* Fix VR overlay

* fix theme and clean up

* split InstanceActivity

* tweak

* Fix VR overlay formatting

* fix scss var

* AppDebug hahahaha

* Years

* remove reactive

* improve perf

* state hah…

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

* improve perf

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

* improve perf 1

* Suppress stray errors

* fix traveling location display issue

* Fix empty instance creator

* improve friend list refresh performance

* fix main charts

* fix chart

* Fix darkmode

* Fix avatar dialog tags

---------

Co-authored-by: pa <maplenagisa@gmail.com>
This commit is contained in:
Natsumi
2025-09-12 10:45:24 +12:00
committed by GitHub
parent b233bbc299
commit 3324d0d279
249 changed files with 12948 additions and 19815 deletions
@@ -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
};
}