mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-05-07 06:56:04 +02:00
fix: add activity store and user activity caching
This commit is contained in:
@@ -12,6 +12,7 @@
|
|||||||
<AlertDialogModal></AlertDialogModal>
|
<AlertDialogModal></AlertDialogModal>
|
||||||
<PromptDialogModal></PromptDialogModal>
|
<PromptDialogModal></PromptDialogModal>
|
||||||
<OtpDialogModal></OtpDialogModal>
|
<OtpDialogModal></OtpDialogModal>
|
||||||
|
<DatabaseUpgradeDialog></DatabaseUpgradeDialog>
|
||||||
|
|
||||||
<VRCXUpdateDialog></VRCXUpdateDialog>
|
<VRCXUpdateDialog></VRCXUpdateDialog>
|
||||||
</div>
|
</div>
|
||||||
@@ -30,6 +31,7 @@
|
|||||||
import { initNoty } from './plugins/noty';
|
import { initNoty } from './plugins/noty';
|
||||||
|
|
||||||
import AlertDialogModal from './components/ui/alert-dialog/AlertDialogModal.vue';
|
import AlertDialogModal from './components/ui/alert-dialog/AlertDialogModal.vue';
|
||||||
|
import DatabaseUpgradeDialog from './components/dialogs/DatabaseUpgradeDialog.vue';
|
||||||
import MacOSTitleBar from './components/MacOSTitleBar.vue';
|
import MacOSTitleBar from './components/MacOSTitleBar.vue';
|
||||||
import OtpDialogModal from './components/ui/dialog/OtpDialogModal.vue';
|
import OtpDialogModal from './components/ui/dialog/OtpDialogModal.vue';
|
||||||
import PromptDialogModal from './components/ui/dialog/PromptDialogModal.vue';
|
import PromptDialogModal from './components/ui/dialog/PromptDialogModal.vue';
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<AlertDialog :open="isOpen">
|
||||||
|
<AlertDialogContent
|
||||||
|
class="sm:max-w-[460px]"
|
||||||
|
@interact-outside.prevent
|
||||||
|
@escape-key-down.prevent
|
||||||
|
@pointer-down-outside.prevent
|
||||||
|
@close-auto-focus.prevent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>{{ t('message.database.upgrade_in_progress_title') }}</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
{{ description }}
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<div class="flex items-center gap-3 pt-2">
|
||||||
|
<Spinner class="h-5 w-5" />
|
||||||
|
<span class="text-sm text-muted-foreground">
|
||||||
|
{{ t('message.database.upgrade_in_progress_wait') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle
|
||||||
|
} from '@/components/ui/alert-dialog';
|
||||||
|
import { Spinner } from '@/components/ui/spinner';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
import { useVrcxStore } from '../../stores';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { databaseUpgradeState } = storeToRefs(useVrcxStore());
|
||||||
|
|
||||||
|
const isOpen = computed(() => databaseUpgradeState.value.visible);
|
||||||
|
const description = computed(() => {
|
||||||
|
if (databaseUpgradeState.value.fromVersion > 0) {
|
||||||
|
return t('message.database.upgrade_in_progress_description', {
|
||||||
|
from: databaseUpgradeState.value.fromVersion,
|
||||||
|
to: databaseUpgradeState.value.toVersion
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return t('message.database.upgrade_in_progress_initializing');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -2,10 +2,19 @@
|
|||||||
<div class="flex flex-col" style="min-height: 200px">
|
<div class="flex flex-col" style="min-height: 200px">
|
||||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
<div style="display: flex; align-items: center; justify-content: space-between">
|
||||||
<div style="display: flex; align-items: center">
|
<div style="display: flex; align-items: center">
|
||||||
<Button class="rounded-full" variant="ghost" size="icon-sm" :disabled="isLoading" @click="loadData">
|
<Button
|
||||||
|
class="rounded-full"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
:disabled="isLoading"
|
||||||
|
:title="hasRequestedLoad ? t('dialog.user.activity.refresh_hint') : t('dialog.user.activity.load')"
|
||||||
|
@click="loadData">
|
||||||
<Spinner v-if="isLoading" />
|
<Spinner v-if="isLoading" />
|
||||||
<RefreshCw v-else />
|
<RefreshCw v-else />
|
||||||
</Button>
|
</Button>
|
||||||
|
<span v-if="hasRequestedLoad && !isLoading" class="ml-2 text-xs text-muted-foreground">
|
||||||
|
{{ t('dialog.user.activity.refresh_hint') }}
|
||||||
|
</span>
|
||||||
<span v-if="filteredEventCount > 0" class="text-accent-foreground ml-1">
|
<span v-if="filteredEventCount > 0" class="text-accent-foreground ml-1">
|
||||||
{{ t('dialog.user.activity.total_events', { count: filteredEventCount }) }}
|
{{ t('dialog.user.activity.total_events', { count: filteredEventCount }) }}
|
||||||
</span>
|
</span>
|
||||||
@@ -36,11 +45,28 @@
|
|||||||
<span class="font-medium ml-1">{{ peakTimeText }}</span>
|
<span class="font-medium ml-1">{{ peakTimeText }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isLoading && !hasAnyData" class="flex items-center justify-center flex-1 mt-8">
|
<div
|
||||||
<DataTableEmpty type="nodata" />
|
v-if="!hasRequestedLoad && !isLoading && !isSessionCacheLoading"
|
||||||
|
class="flex flex-col items-center justify-center flex-1 mt-8 gap-3">
|
||||||
|
<Button variant="outline" @click="loadData">
|
||||||
|
{{ t('dialog.user.activity.load') }}
|
||||||
|
</Button>
|
||||||
|
<span class="text-xs text-muted-foreground text-center">
|
||||||
|
{{ t('dialog.user.activity.load_hint') }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="!isLoading && hasAnyData && filteredEventCount === 0"
|
v-else-if="!isLoading && !isSessionCacheLoading && !hasAnyData"
|
||||||
|
class="flex items-center justify-center flex-1 mt-8">
|
||||||
|
<DataTableEmpty type="nodata" />
|
||||||
|
</div>
|
||||||
|
<div v-if="isSessionCacheLoading" class="flex flex-col items-center justify-center flex-1 mt-8 gap-2">
|
||||||
|
<Spinner class="h-5 w-5" />
|
||||||
|
<span class="text-sm text-muted-foreground">{{ t('dialog.user.activity.preparing_data') }}</span>
|
||||||
|
<span class="text-xs text-muted-foreground">{{ t('dialog.user.activity.preparing_data_hint') }}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="!isLoading && !isSessionCacheLoading && hasAnyData && filteredEventCount === 0"
|
||||||
class="flex items-center justify-center flex-1 mt-8">
|
class="flex items-center justify-center flex-1 mt-8">
|
||||||
<span class="text-muted-foreground text-sm">{{ t('dialog.user.activity.no_data_in_period') }}</span>
|
<span class="text-muted-foreground text-sm">{{ t('dialog.user.activity.no_data_in_period') }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -192,13 +218,12 @@
|
|||||||
import { database } from '../../../services/database';
|
import { database } from '../../../services/database';
|
||||||
import configRepository from '../../../services/config';
|
import configRepository from '../../../services/config';
|
||||||
import { worldRequest } from '../../../api';
|
import { worldRequest } from '../../../api';
|
||||||
import { useAppearanceSettingsStore, useUserStore } from '../../../stores';
|
import { useActivityStore, useAppearanceSettingsStore, useUserStore } from '../../../stores';
|
||||||
import { useWorldStore } from '../../../stores/world';
|
import { useWorldStore } from '../../../stores/world';
|
||||||
import { showWorldDialog } from '../../../coordinators/worldCoordinator';
|
import { showWorldDialog } from '../../../coordinators/worldCoordinator';
|
||||||
import { timeToText } from '../../../shared/utils';
|
import { timeToText } from '../../../shared/utils';
|
||||||
|
import { useCurrentUserSessions } from '../../../composables/useCurrentUserSessions';
|
||||||
import {
|
import {
|
||||||
buildSessionsFromEvents,
|
|
||||||
buildSessionsFromGamelog,
|
|
||||||
calculateOverlapGrid,
|
calculateOverlapGrid,
|
||||||
filterSessionsByPeriod,
|
filterSessionsByPeriod,
|
||||||
findBestOverlapTime,
|
findBestOverlapTime,
|
||||||
@@ -209,15 +234,18 @@
|
|||||||
const { userDialog, currentUser } = storeToRefs(useUserStore());
|
const { userDialog, currentUser } = storeToRefs(useUserStore());
|
||||||
const { isDarkMode, weekStartsOn } = storeToRefs(useAppearanceSettingsStore());
|
const { isDarkMode, weekStartsOn } = storeToRefs(useAppearanceSettingsStore());
|
||||||
const worldStore = useWorldStore();
|
const worldStore = useWorldStore();
|
||||||
|
const sessionCache = useCurrentUserSessions();
|
||||||
|
const activityStore = useActivityStore();
|
||||||
|
|
||||||
const chartRef = ref(null);
|
const chartRef = ref(null);
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
const totalOnlineEvents = ref(0);
|
|
||||||
const hasAnyData = ref(false);
|
const hasAnyData = ref(false);
|
||||||
const peakDayText = ref('');
|
const peakDayText = ref('');
|
||||||
const peakTimeText = ref('');
|
const peakTimeText = ref('');
|
||||||
const selectedPeriod = ref('all');
|
const selectedPeriod = ref('all');
|
||||||
const filteredEventCount = ref(0);
|
const filteredEventCount = ref(0);
|
||||||
|
const isSessionCacheLoading = ref(false);
|
||||||
|
const hasRequestedLoad = ref(false);
|
||||||
|
|
||||||
const isSelf = computed(() => userDialog.value.id === currentUser.value.id);
|
const isSelf = computed(() => userDialog.value.id === currentUser.value.id);
|
||||||
const topWorlds = ref([]);
|
const topWorlds = ref([]);
|
||||||
@@ -287,6 +315,12 @@
|
|||||||
watch(() => isDarkMode.value, rebuildChart);
|
watch(() => isDarkMode.value, rebuildChart);
|
||||||
watch(locale, rebuildChart);
|
watch(locale, rebuildChart);
|
||||||
watch(weekStartsOn, rebuildChart);
|
watch(weekStartsOn, rebuildChart);
|
||||||
|
watch(
|
||||||
|
() => userDialog.value.id,
|
||||||
|
() => {
|
||||||
|
resetActivityState();
|
||||||
|
}
|
||||||
|
);
|
||||||
watch(selectedPeriod, () => {
|
watch(selectedPeriod, () => {
|
||||||
if (cachedTargetSessions.length > 0 && echartsInstance) {
|
if (cachedTargetSessions.length > 0 && echartsInstance) {
|
||||||
initChart();
|
initChart();
|
||||||
@@ -312,7 +346,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (userDialog.value.activeTab === 'Activity') {
|
if (userDialog.value.activeTab === 'Activity') {
|
||||||
loadOnlineFrequency(userDialog.value.id, 'visible-watch');
|
loadOnlineFrequency(userDialog.value.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -322,7 +356,7 @@
|
|||||||
() => userDialog.value.activeTab,
|
() => userDialog.value.activeTab,
|
||||||
(activeTab) => {
|
(activeTab) => {
|
||||||
if (activeTab === 'Activity' && userDialog.value.visible) {
|
if (activeTab === 'Activity' && userDialog.value.visible) {
|
||||||
loadOnlineFrequency(userDialog.value.id, 'active-tab-watch');
|
loadOnlineFrequency(userDialog.value.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -335,7 +369,7 @@
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (userDialog.value.visible && userDialog.value.activeTab === 'Activity') {
|
if (userDialog.value.visible && userDialog.value.activeTab === 'Activity') {
|
||||||
loadOnlineFrequency(userDialog.value.id, 'mounted');
|
loadOnlineFrequency(userDialog.value.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -436,9 +470,7 @@
|
|||||||
|
|
||||||
const filteredSessions = getFilteredSessions();
|
const filteredSessions = getFilteredSessions();
|
||||||
// Use timestamps for event count display
|
// Use timestamps for event count display
|
||||||
const filteredTs = getFilteredTimestamps();
|
filteredEventCount.value = getFilteredEventCount();
|
||||||
filteredEventCount.value = filteredTs.length;
|
|
||||||
totalOnlineEvents.value = filteredTs.length;
|
|
||||||
|
|
||||||
if (filteredSessions.length === 0) {
|
if (filteredSessions.length === 0) {
|
||||||
peakDayText.value = '';
|
peakDayText.value = '';
|
||||||
@@ -541,13 +573,14 @@
|
|||||||
echartsInstance.setOption(option, { notMerge: true });
|
echartsInstance.setOption(option, { notMerge: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
let cachedTimestamps = [];
|
|
||||||
let activeRequestId = 0;
|
let activeRequestId = 0;
|
||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
const userId = userDialog.value.id;
|
const userId = userDialog.value.id;
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
|
|
||||||
|
hasRequestedLoad.value = true;
|
||||||
|
|
||||||
if (userId !== lastLoadedUserId) {
|
if (userId !== lastLoadedUserId) {
|
||||||
selectedPeriod.value = 'all';
|
selectedPeriod.value = 'all';
|
||||||
}
|
}
|
||||||
@@ -555,59 +588,14 @@
|
|||||||
const requestId = ++activeRequestId;
|
const requestId = ++activeRequestId;
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
if (isSelf.value) {
|
const entry = await activityStore.refreshActivityCache(userId, isSelf.value, {
|
||||||
// Self: use gamelog_location for heatmap
|
notifyStart: hasAnyData.value,
|
||||||
const rows = await database.getCurrentUserOnlineSessions();
|
notifyComplete: true
|
||||||
if (requestId !== activeRequestId) return;
|
});
|
||||||
if (userDialog.value.id !== userId) return;
|
if (requestId !== activeRequestId) return;
|
||||||
|
if (userDialog.value.id !== userId) return;
|
||||||
cachedTimestamps = rows.map((r) => r.created_at);
|
hydrateFromCacheEntry(entry);
|
||||||
cachedTargetSessions = buildSessionsFromGamelog(rows);
|
await finishLoadData(userId);
|
||||||
} else {
|
|
||||||
// Friend: use feed_online_offline
|
|
||||||
const [timestamps, events] = await Promise.all([
|
|
||||||
database.getOnlineFrequencyData(userId),
|
|
||||||
database.getOnlineOfflineSessions(userId)
|
|
||||||
]);
|
|
||||||
if (requestId !== activeRequestId) return;
|
|
||||||
if (userDialog.value.id !== userId) return;
|
|
||||||
|
|
||||||
cachedTimestamps = timestamps;
|
|
||||||
cachedTargetSessions = buildSessionsFromEvents(events);
|
|
||||||
}
|
|
||||||
|
|
||||||
hasAnyData.value = cachedTimestamps.length > 0;
|
|
||||||
totalOnlineEvents.value = cachedTimestamps.length;
|
|
||||||
lastLoadedUserId = userId;
|
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
if (cachedTimestamps.length > 0) {
|
|
||||||
const filteredTs = getFilteredTimestamps();
|
|
||||||
filteredEventCount.value = filteredTs.length;
|
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
if (!echartsInstance && chartRef.value) {
|
|
||||||
echartsInstance = echarts.init(chartRef.value, isDarkMode.value ? 'dark' : null, { height: 240 });
|
|
||||||
resizeObserver = new ResizeObserver((entries) => {
|
|
||||||
for (const entry of entries) {
|
|
||||||
if (echartsInstance) {
|
|
||||||
echartsInstance.resize({
|
|
||||||
width: entry.contentRect.width
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
resizeObserver.observe(chartRef.value);
|
|
||||||
}
|
|
||||||
initChart();
|
|
||||||
} else {
|
|
||||||
peakDayText.value = '';
|
|
||||||
peakTimeText.value = '';
|
|
||||||
hasAnyData.value = false;
|
|
||||||
filteredEventCount.value = 0;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading online frequency data:', error);
|
console.error('Error loading online frequency data:', error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -615,6 +603,45 @@
|
|||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared finalization after session data is loaded (both sync and async paths).
|
||||||
|
* @param {string} userId
|
||||||
|
*/
|
||||||
|
async function finishLoadData(userId) {
|
||||||
|
hasAnyData.value = cachedTargetSessions.length > 0;
|
||||||
|
lastLoadedUserId = userId;
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
if (cachedTargetSessions.length > 0) {
|
||||||
|
filteredEventCount.value = getFilteredEventCount();
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
if (!echartsInstance && chartRef.value) {
|
||||||
|
echartsInstance = echarts.init(chartRef.value, isDarkMode.value ? 'dark' : null, { height: 240 });
|
||||||
|
resizeObserver = new ResizeObserver((entries) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (echartsInstance) {
|
||||||
|
echartsInstance.resize({
|
||||||
|
width: entry.contentRect.width
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
resizeObserver.observe(chartRef.value);
|
||||||
|
}
|
||||||
|
initChart();
|
||||||
|
} else {
|
||||||
|
peakDayText.value = '';
|
||||||
|
peakTimeText.value = '';
|
||||||
|
hasAnyData.value = false;
|
||||||
|
filteredEventCount.value = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = false;
|
||||||
|
|
||||||
if (hasAnyData.value && !isSelf.value) {
|
if (hasAnyData.value && !isSelf.value) {
|
||||||
loadOverlapData(userId);
|
loadOverlapData(userId);
|
||||||
@@ -624,21 +651,107 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFilteredTimestamps() {
|
function resetActivityState() {
|
||||||
if (selectedPeriod.value === 'all') return cachedTimestamps;
|
hasRequestedLoad.value = false;
|
||||||
|
isLoading.value = false;
|
||||||
|
isSessionCacheLoading.value = false;
|
||||||
|
hasAnyData.value = false;
|
||||||
|
peakDayText.value = '';
|
||||||
|
peakTimeText.value = '';
|
||||||
|
selectedPeriod.value = 'all';
|
||||||
|
filteredEventCount.value = 0;
|
||||||
|
hasOverlapData.value = false;
|
||||||
|
overlapPercent.value = 0;
|
||||||
|
bestOverlapTime.value = '';
|
||||||
|
isOverlapLoading.value = false;
|
||||||
|
topWorlds.value = [];
|
||||||
|
cachedTargetSessions = [];
|
||||||
|
cachedCurrentSessions = [];
|
||||||
|
lastLoadedUserId = '';
|
||||||
|
activeRequestId++;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hydrateFromCacheEntry(entry) {
|
||||||
|
cachedTargetSessions = Array.isArray(entry?.sessions) ? entry.sessions : [];
|
||||||
|
hasRequestedLoad.value = Boolean(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCachedActivity(userId) {
|
||||||
|
const entry = await activityStore.getCache(userId);
|
||||||
|
if (!entry) {
|
||||||
|
hasRequestedLoad.value = false;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userDialog.value.id !== userId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
hydrateFromCacheEntry(entry);
|
||||||
|
await finishLoadData(userId);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function scheduleAutoRefresh(userId) {
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
const entry = await activityStore.refreshActivityCache(userId, isSelf.value, {
|
||||||
|
notifyComplete: true
|
||||||
|
});
|
||||||
|
if (userDialog.value.id !== userId) return;
|
||||||
|
hydrateFromCacheEntry(entry);
|
||||||
|
await finishLoadData(userId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error auto-refreshing activity data:', error);
|
||||||
|
} finally {
|
||||||
|
if (userDialog.value.id === userId) {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFilteredEventCount() {
|
||||||
|
if (selectedPeriod.value === 'all') return cachedTargetSessions.length;
|
||||||
const days = parseInt(selectedPeriod.value, 10);
|
const days = parseInt(selectedPeriod.value, 10);
|
||||||
const cutoff = dayjs().subtract(days, 'day');
|
const cutoff = dayjs().subtract(days, 'day').valueOf();
|
||||||
return cachedTimestamps.filter((ts) => dayjs(ts).isAfter(cutoff));
|
return cachedTargetSessions.filter((session) => session.start > cutoff).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} userId
|
* @param {string} userId
|
||||||
*/
|
*/
|
||||||
function loadOnlineFrequency(userId) {
|
function loadOnlineFrequency(userId) {
|
||||||
if (lastLoadedUserId === userId && hasAnyData.value) {
|
if (lastLoadedUserId !== userId) {
|
||||||
|
resetActivityState();
|
||||||
|
}
|
||||||
|
if (!userId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
loadData();
|
void (async () => {
|
||||||
|
const cacheEntry = await loadCachedActivity(userId);
|
||||||
|
if (!cacheEntry) {
|
||||||
|
if (activityStore.isRefreshing(userId)) {
|
||||||
|
hasRequestedLoad.value = true;
|
||||||
|
isSessionCacheLoading.value = true;
|
||||||
|
try {
|
||||||
|
const entry = await activityStore.refreshActivityCache(userId, isSelf.value, {
|
||||||
|
notifyComplete: true
|
||||||
|
});
|
||||||
|
if (userDialog.value.id !== userId) return;
|
||||||
|
hydrateFromCacheEntry(entry);
|
||||||
|
await finishLoadData(userId);
|
||||||
|
} finally {
|
||||||
|
if (userDialog.value.id === userId) {
|
||||||
|
isSessionCacheLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activityStore.isExpired(cacheEntry)) {
|
||||||
|
void scheduleAutoRefresh(userId);
|
||||||
|
}
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
let easterEggTimer = null;
|
let easterEggTimer = null;
|
||||||
@@ -663,13 +776,16 @@
|
|||||||
isOverlapLoading.value = true;
|
isOverlapLoading.value = true;
|
||||||
hasOverlapData.value = false;
|
hasOverlapData.value = false;
|
||||||
try {
|
try {
|
||||||
// Target sessions already cached from loadData, only fetch current user
|
if (!sessionCache.isReady()) {
|
||||||
const currentUserRows = await database.getCurrentUserOnlineSessions();
|
sessionCache.onReady(() => loadOverlapData(userId));
|
||||||
|
sessionCache.triggerLoad();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentSessions = await sessionCache.getSessions();
|
||||||
|
|
||||||
if (userDialog.value.id !== userId) return;
|
if (userDialog.value.id !== userId) return;
|
||||||
|
|
||||||
const currentSessions = buildSessionsFromGamelog(currentUserRows);
|
|
||||||
|
|
||||||
if (cachedTargetSessions.length === 0 || currentSessions.length === 0) {
|
if (cachedTargetSessions.length === 0 || currentSessions.length === 0) {
|
||||||
hasOverlapData.value = false;
|
hasOverlapData.value = false;
|
||||||
return;
|
return;
|
||||||
@@ -738,9 +854,14 @@
|
|||||||
if (result.grid[d][h] > result.maxVal) result.maxVal = result.grid[d][h];
|
if (result.grid[d][h] > result.maxVal) result.maxVal = result.grid[d][h];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Recalculate overlap percent excluding those hours
|
const overlapSessions = computeOverlapSessions(currentSessions, targetSessions);
|
||||||
const totalGrid = result.grid.flat().reduce((a, b) => a + b, 0);
|
const overlapMs = getIncludedSessionDurationMs(overlapSessions, start, end);
|
||||||
if (totalGrid === 0) {
|
const currentMs = getIncludedSessionDurationMs(currentSessions, start, end);
|
||||||
|
const targetMs = getIncludedSessionDurationMs(targetSessions, start, end);
|
||||||
|
const minOnlineMs = Math.min(currentMs, targetMs);
|
||||||
|
result.overlapPercent =
|
||||||
|
minOnlineMs > 0 ? Math.round((overlapMs / minOnlineMs) * 100) : 0;
|
||||||
|
if (overlapMs === 0) {
|
||||||
overlapPercent.value = 0;
|
overlapPercent.value = 0;
|
||||||
bestOverlapTime.value = '';
|
bestOverlapTime.value = '';
|
||||||
return;
|
return;
|
||||||
@@ -780,6 +901,59 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function computeOverlapSessions(sessionsA, sessionsB) {
|
||||||
|
const overlapSessions = [];
|
||||||
|
let i = 0;
|
||||||
|
let j = 0;
|
||||||
|
|
||||||
|
while (i < sessionsA.length && j < sessionsB.length) {
|
||||||
|
const a = sessionsA[i];
|
||||||
|
const b = sessionsB[j];
|
||||||
|
const start = Math.max(a.start, b.start);
|
||||||
|
const end = Math.min(a.end, b.end);
|
||||||
|
if (start < end) {
|
||||||
|
overlapSessions.push({ start, end });
|
||||||
|
}
|
||||||
|
if (a.end < b.end) {
|
||||||
|
i++;
|
||||||
|
} else {
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return overlapSessions;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIncludedSessionDurationMs(sessions, startHour, endHour) {
|
||||||
|
let total = 0;
|
||||||
|
for (const session of sessions) {
|
||||||
|
let cursor = session.start;
|
||||||
|
while (cursor < session.end) {
|
||||||
|
const segmentEnd = getNextHourBoundaryMs(cursor, session.end);
|
||||||
|
if (!isHourExcluded(cursor, startHour, endHour)) {
|
||||||
|
total += segmentEnd - cursor;
|
||||||
|
}
|
||||||
|
cursor = segmentEnd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNextHourBoundaryMs(cursor, sessionEnd) {
|
||||||
|
const nextHour = new Date(cursor);
|
||||||
|
nextHour.setMinutes(0, 0, 0);
|
||||||
|
nextHour.setHours(nextHour.getHours() + 1);
|
||||||
|
return Math.min(nextHour.getTime(), sessionEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isHourExcluded(cursor, startHour, endHour) {
|
||||||
|
const hour = new Date(cursor).getHours();
|
||||||
|
if (startHour <= endHour) {
|
||||||
|
return hour >= startHour && hour < endHour;
|
||||||
|
}
|
||||||
|
return hour >= startHour || hour < endHour;
|
||||||
|
}
|
||||||
|
|
||||||
function onExcludeToggle(value) {
|
function onExcludeToggle(value) {
|
||||||
excludeHoursEnabled.value = value;
|
excludeHoursEnabled.value = value;
|
||||||
configRepository.setBool('VRCX_overlapExcludeEnabled', value);
|
configRepository.setBool('VRCX_overlapExcludeEnabled', value);
|
||||||
|
|||||||
@@ -0,0 +1,225 @@
|
|||||||
|
import { database } from '../services/database';
|
||||||
|
import {
|
||||||
|
buildSessionsFromGamelog,
|
||||||
|
ONLINE_SESSION_MERGE_GAP_MS
|
||||||
|
} from '../shared/utils/overlapCalculator';
|
||||||
|
|
||||||
|
/** @typedef {{ start: number, end: number }} Session */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module-level singleton cache for the current user's online sessions.
|
||||||
|
* Lazy-loaded on first access, then incrementally updated.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {Session[] | null} */
|
||||||
|
let cachedSessions = null;
|
||||||
|
|
||||||
|
/** @type {string[] | null} */
|
||||||
|
let cachedTimestamps = null;
|
||||||
|
|
||||||
|
/** @type {string | null} */
|
||||||
|
let lastRowCreatedAt = null;
|
||||||
|
|
||||||
|
/** @type {'idle' | 'loading' | 'ready'} */
|
||||||
|
let status = 'idle';
|
||||||
|
|
||||||
|
/** @type {Promise<void> | null} */
|
||||||
|
let loadPromise = null;
|
||||||
|
|
||||||
|
/** @type {Array<() => void>} */
|
||||||
|
const onReadyCallbacks = [];
|
||||||
|
|
||||||
|
/** @type {ReturnType<typeof setInterval> | null} */
|
||||||
|
let refreshTimer = null;
|
||||||
|
|
||||||
|
const REFRESH_INTERVAL_MS = 30 * 60 * 1000; // 30 minutes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes all onReady callbacks and clears the list.
|
||||||
|
*/
|
||||||
|
function flushCallbacks() {
|
||||||
|
const cbs = onReadyCallbacks.splice(0);
|
||||||
|
for (const cb of cbs) {
|
||||||
|
try {
|
||||||
|
cb();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('useCurrentUserSessions onReady callback error:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the periodic incremental refresh timer.
|
||||||
|
* Only starts if not already running.
|
||||||
|
*/
|
||||||
|
function startRefreshTimer() {
|
||||||
|
if (refreshTimer) return;
|
||||||
|
refreshTimer = setInterval(async () => {
|
||||||
|
if (status !== 'ready') return;
|
||||||
|
try {
|
||||||
|
await incrementalUpdate();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('useCurrentUserSessions periodic refresh error:', e);
|
||||||
|
}
|
||||||
|
}, REFRESH_INTERVAL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full load: queries all gamelog_location rows and builds sessions.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async function fullLoad() {
|
||||||
|
status = 'loading';
|
||||||
|
try {
|
||||||
|
const rows = await database.getCurrentUserOnlineSessions();
|
||||||
|
cachedTimestamps = rows.map((r) => r.created_at);
|
||||||
|
cachedSessions = buildSessionsFromGamelog(rows);
|
||||||
|
if (rows.length > 0) {
|
||||||
|
lastRowCreatedAt = rows[rows.length - 1].created_at;
|
||||||
|
}
|
||||||
|
status = 'ready';
|
||||||
|
startRefreshTimer();
|
||||||
|
flushCallbacks();
|
||||||
|
} catch (e) {
|
||||||
|
status = 'idle';
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Incremental update: only fetches rows newer than lastRowCreatedAt.
|
||||||
|
* Merges new sessions into the cached sessions array.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async function incrementalUpdate() {
|
||||||
|
if (!lastRowCreatedAt || status !== 'ready') return;
|
||||||
|
|
||||||
|
const newRows =
|
||||||
|
await database.getCurrentUserOnlineSessionsAfter(lastRowCreatedAt);
|
||||||
|
if (newRows.length === 0) return;
|
||||||
|
|
||||||
|
lastRowCreatedAt = newRows[newRows.length - 1].created_at;
|
||||||
|
cachedTimestamps.push(...newRows.map((r) => r.created_at));
|
||||||
|
|
||||||
|
const newSessions = buildSessionsFromGamelog(newRows);
|
||||||
|
if (newSessions.length === 0) return;
|
||||||
|
|
||||||
|
// Merge: if last cached session and first new session overlap or are close, merge them
|
||||||
|
if (cachedSessions.length > 0 && newSessions.length > 0) {
|
||||||
|
const last = cachedSessions[cachedSessions.length - 1];
|
||||||
|
const first = newSessions[0];
|
||||||
|
if (first.start <= last.end + ONLINE_SESSION_MERGE_GAP_MS) {
|
||||||
|
last.end = Math.max(last.end, first.end);
|
||||||
|
newSessions.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cachedSessions.push(...newSessions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the cache is ready.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function isReady() {
|
||||||
|
return status === 'ready';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the cache is currently loading.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function isLoading() {
|
||||||
|
return status === 'loading';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the cached sessions. If not loaded yet, triggers a full load.
|
||||||
|
* If already loaded, does an incremental update first.
|
||||||
|
* @returns {Promise<Session[]>}
|
||||||
|
*/
|
||||||
|
async function getSessions() {
|
||||||
|
if (status === 'ready') {
|
||||||
|
await incrementalUpdate();
|
||||||
|
return cachedSessions;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'loading') {
|
||||||
|
// Wait for existing load to complete
|
||||||
|
await loadPromise;
|
||||||
|
return cachedSessions;
|
||||||
|
}
|
||||||
|
|
||||||
|
// idle: trigger full load
|
||||||
|
loadPromise = fullLoad();
|
||||||
|
try {
|
||||||
|
await loadPromise;
|
||||||
|
return cachedSessions;
|
||||||
|
} finally {
|
||||||
|
loadPromise = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the cached timestamps (created_at strings from gamelog_location).
|
||||||
|
* Must be called after getSessions() or after onReady fires.
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
function getTimestamps() {
|
||||||
|
return cachedTimestamps || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a callback to be called when the cache becomes ready.
|
||||||
|
* If already ready, callback is invoked immediately.
|
||||||
|
* @param {() => void} callback
|
||||||
|
*/
|
||||||
|
function onReady(callback) {
|
||||||
|
if (status === 'ready') {
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onReadyCallbacks.push(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers a full load if idle, or returns the existing promise if loading.
|
||||||
|
* Does NOT block the caller — designed for fire-and-forget usage.
|
||||||
|
* Returns the promise so callers can optionally await it.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
function triggerLoad() {
|
||||||
|
if (status === 'ready') return Promise.resolve();
|
||||||
|
if (status === 'loading') return loadPromise;
|
||||||
|
|
||||||
|
loadPromise = fullLoad().finally(() => {
|
||||||
|
loadPromise = null;
|
||||||
|
});
|
||||||
|
return loadPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidates the cache and stops the refresh timer.
|
||||||
|
*/
|
||||||
|
function invalidate() {
|
||||||
|
cachedSessions = null;
|
||||||
|
cachedTimestamps = null;
|
||||||
|
lastRowCreatedAt = null;
|
||||||
|
status = 'idle';
|
||||||
|
loadPromise = null;
|
||||||
|
if (refreshTimer) {
|
||||||
|
clearInterval(refreshTimer);
|
||||||
|
refreshTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCurrentUserSessions() {
|
||||||
|
return {
|
||||||
|
isReady,
|
||||||
|
isLoading,
|
||||||
|
getSessions,
|
||||||
|
getTimestamps,
|
||||||
|
onReady,
|
||||||
|
triggerLoad,
|
||||||
|
invalidate
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1427,6 +1427,11 @@
|
|||||||
},
|
},
|
||||||
"activity": {
|
"activity": {
|
||||||
"header": "Activity",
|
"header": "Activity",
|
||||||
|
"load": "Load Activity",
|
||||||
|
"load_hint": "Load activity data from the local database when needed.",
|
||||||
|
"refresh_hint": "Rebuild activity cache",
|
||||||
|
"refresh_started": "Rebuilding activity data. Please wait...",
|
||||||
|
"refresh_complete": "Activity refresh complete",
|
||||||
"total_events": "{count} online events",
|
"total_events": "{count} online events",
|
||||||
"times_online": "times online",
|
"times_online": "times online",
|
||||||
"most_active_day": "Most active day:",
|
"most_active_day": "Most active day:",
|
||||||
@@ -1458,7 +1463,10 @@
|
|||||||
},
|
},
|
||||||
"most_visited_worlds": {
|
"most_visited_worlds": {
|
||||||
"header": "Most Visited Worlds"
|
"header": "Most Visited Worlds"
|
||||||
}
|
},
|
||||||
|
"preparing_data": "Preparing activity data...",
|
||||||
|
"preparing_data_hint": "This may take a moment on first load. You'll be notified when ready.",
|
||||||
|
"data_ready": "Activity data is ready"
|
||||||
},
|
},
|
||||||
"note_memo": {
|
"note_memo": {
|
||||||
"header": "Edit Note And Memo",
|
"header": "Edit Note And Memo",
|
||||||
@@ -2631,6 +2639,12 @@
|
|||||||
},
|
},
|
||||||
"database": {
|
"database": {
|
||||||
"upgrade_complete": "Database upgrade complete",
|
"upgrade_complete": "Database upgrade complete",
|
||||||
|
"upgrade_in_progress_title": "Database upgrade in progress",
|
||||||
|
"upgrade_in_progress_description": "Updating database from version {from} to {to}. Please do not close VRCX.",
|
||||||
|
"upgrade_in_progress_initializing": "Initializing database upgrade. Please do not close VRCX.",
|
||||||
|
"upgrade_in_progress_wait": "User actions are temporarily blocked until the upgrade finishes.",
|
||||||
|
"upgrade_failed_title": "Database upgrade failed",
|
||||||
|
"upgrade_failed_description": "Database upgrade failed. Check the console for details.",
|
||||||
"disk_space": "Please free up some disk space.",
|
"disk_space": "Please free up some disk space.",
|
||||||
"disk_error": "Please check your disk for errors."
|
"disk_error": "Please check your disk for errors."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
import { dbVars } from '../database';
|
||||||
|
|
||||||
|
import sqliteService from '../sqlite.js';
|
||||||
|
|
||||||
|
const activityCache = {
|
||||||
|
/**
|
||||||
|
* @param {string} userId
|
||||||
|
* @returns {Promise<{
|
||||||
|
* userId: string,
|
||||||
|
* updatedAt: string,
|
||||||
|
* isSelf: boolean,
|
||||||
|
* sourceLastCreatedAt: string,
|
||||||
|
* pendingSessionStartAt: number | null
|
||||||
|
* } | null>}
|
||||||
|
*/
|
||||||
|
async getActivityCacheMeta(userId) {
|
||||||
|
let row = null;
|
||||||
|
await sqliteService.execute(
|
||||||
|
(dbRow) => {
|
||||||
|
row = {
|
||||||
|
userId: dbRow[0],
|
||||||
|
updatedAt: dbRow[1],
|
||||||
|
isSelf: Boolean(dbRow[2]),
|
||||||
|
sourceLastCreatedAt: dbRow[3] || '',
|
||||||
|
pendingSessionStartAt:
|
||||||
|
typeof dbRow[4] === 'number' ? dbRow[4] : null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
`SELECT user_id, updated_at, is_self, source_last_created_at, pending_session_start_at
|
||||||
|
FROM ${dbVars.userPrefix}_activity_cache_meta
|
||||||
|
WHERE user_id = @userId`,
|
||||||
|
{ '@userId': userId }
|
||||||
|
);
|
||||||
|
return row;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} userId
|
||||||
|
* @returns {Promise<Array<{start: number, end: number}>>}
|
||||||
|
*/
|
||||||
|
async getActivityCacheSessions(userId) {
|
||||||
|
const sessions = [];
|
||||||
|
await sqliteService.execute(
|
||||||
|
(dbRow) => {
|
||||||
|
sessions.push({
|
||||||
|
start: dbRow[0],
|
||||||
|
end: dbRow[1]
|
||||||
|
});
|
||||||
|
},
|
||||||
|
`SELECT start_at, end_at
|
||||||
|
FROM ${dbVars.userPrefix}_activity_cache_sessions
|
||||||
|
WHERE user_id = @userId
|
||||||
|
ORDER BY start_at`,
|
||||||
|
{ '@userId': userId }
|
||||||
|
);
|
||||||
|
return sessions;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} userId
|
||||||
|
* @returns {Promise<{
|
||||||
|
* userId: string,
|
||||||
|
* updatedAt: string,
|
||||||
|
* isSelf: boolean,
|
||||||
|
* sourceLastCreatedAt: string,
|
||||||
|
* pendingSessionStartAt: number | null,
|
||||||
|
* sessions: Array<{start: number, end: number}>
|
||||||
|
* } | null>}
|
||||||
|
*/
|
||||||
|
async getActivityCache(userId) {
|
||||||
|
const meta = await this.getActivityCacheMeta(userId);
|
||||||
|
if (!meta) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const sessions = await this.getActivityCacheSessions(userId);
|
||||||
|
return {
|
||||||
|
...meta,
|
||||||
|
sessions
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} userId
|
||||||
|
* @returns {Promise<{start: number, end: number} | null>}
|
||||||
|
*/
|
||||||
|
async getLastActivityCacheSession(userId) {
|
||||||
|
let row = null;
|
||||||
|
await sqliteService.execute(
|
||||||
|
(dbRow) => {
|
||||||
|
row = {
|
||||||
|
start: dbRow[0],
|
||||||
|
end: dbRow[1]
|
||||||
|
};
|
||||||
|
},
|
||||||
|
`SELECT start_at, end_at
|
||||||
|
FROM ${dbVars.userPrefix}_activity_cache_sessions
|
||||||
|
WHERE user_id = @userId
|
||||||
|
ORDER BY start_at DESC
|
||||||
|
LIMIT 1`,
|
||||||
|
{ '@userId': userId }
|
||||||
|
);
|
||||||
|
return row;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{
|
||||||
|
* userId: string,
|
||||||
|
* updatedAt: string,
|
||||||
|
* isSelf: boolean,
|
||||||
|
* sourceLastCreatedAt: string,
|
||||||
|
* pendingSessionStartAt: number | null,
|
||||||
|
* sessions: Array<{start: number, end: number}>
|
||||||
|
* }} entry
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async replaceActivityCache(entry) {
|
||||||
|
await sqliteService.executeNonQuery('BEGIN');
|
||||||
|
try {
|
||||||
|
await sqliteService.executeNonQuery(
|
||||||
|
`DELETE FROM ${dbVars.userPrefix}_activity_cache_sessions WHERE user_id = @userId`,
|
||||||
|
{ '@userId': entry.userId }
|
||||||
|
);
|
||||||
|
await upsertSessions(entry.userId, entry.sessions);
|
||||||
|
await upsertMeta(entry);
|
||||||
|
await sqliteService.executeNonQuery('COMMIT');
|
||||||
|
} catch (error) {
|
||||||
|
await sqliteService.executeNonQuery('ROLLBACK');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{
|
||||||
|
* userId: string,
|
||||||
|
* updatedAt: string,
|
||||||
|
* isSelf: boolean,
|
||||||
|
* sourceLastCreatedAt: string,
|
||||||
|
* pendingSessionStartAt: number | null,
|
||||||
|
* sessions: Array<{start: number, end: number}>,
|
||||||
|
* replaceLastSession?: {start: number, end: number} | null
|
||||||
|
* }} entry
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async appendActivityCache(entry) {
|
||||||
|
await sqliteService.executeNonQuery('BEGIN');
|
||||||
|
try {
|
||||||
|
if (entry.replaceLastSession) {
|
||||||
|
await sqliteService.executeNonQuery(
|
||||||
|
`DELETE FROM ${dbVars.userPrefix}_activity_cache_sessions
|
||||||
|
WHERE user_id = @userId AND start_at = @start AND end_at = @end`,
|
||||||
|
{
|
||||||
|
'@userId': entry.userId,
|
||||||
|
'@start': entry.replaceLastSession.start,
|
||||||
|
'@end': entry.replaceLastSession.end
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await upsertSessions(entry.userId, entry.sessions);
|
||||||
|
await upsertMeta(entry);
|
||||||
|
await sqliteService.executeNonQuery('COMMIT');
|
||||||
|
} catch (error) {
|
||||||
|
await sqliteService.executeNonQuery('ROLLBACK');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{
|
||||||
|
* userId: string,
|
||||||
|
* updatedAt: string,
|
||||||
|
* isSelf: boolean,
|
||||||
|
* sourceLastCreatedAt: string,
|
||||||
|
* pendingSessionStartAt: number | null
|
||||||
|
* }} entry
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async touchActivityCacheMeta(entry) {
|
||||||
|
await upsertMeta(entry);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function upsertMeta(entry) {
|
||||||
|
await sqliteService.executeNonQuery(
|
||||||
|
`INSERT OR REPLACE INTO ${dbVars.userPrefix}_activity_cache_meta
|
||||||
|
(user_id, updated_at, is_self, source_last_created_at, pending_session_start_at)
|
||||||
|
VALUES (@user_id, @updated_at, @is_self, @source_last_created_at, @pending_session_start_at)`,
|
||||||
|
{
|
||||||
|
'@user_id': entry.userId,
|
||||||
|
'@updated_at': entry.updatedAt,
|
||||||
|
'@is_self': entry.isSelf ? 1 : 0,
|
||||||
|
'@source_last_created_at': entry.sourceLastCreatedAt || '',
|
||||||
|
'@pending_session_start_at': entry.pendingSessionStartAt
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function upsertSessions(userId, sessions = []) {
|
||||||
|
for (const session of sessions) {
|
||||||
|
await sqliteService.executeNonQuery(
|
||||||
|
`INSERT OR REPLACE INTO ${dbVars.userPrefix}_activity_cache_sessions
|
||||||
|
(user_id, start_at, end_at)
|
||||||
|
VALUES (@user_id, @start_at, @end_at)`,
|
||||||
|
{
|
||||||
|
'@user_id': userId,
|
||||||
|
'@start_at': session.start,
|
||||||
|
'@end_at': session.end
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { activityCache };
|
||||||
@@ -623,6 +623,30 @@ const feed = {
|
|||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} userId
|
||||||
|
* @param {string} afterCreatedAt
|
||||||
|
* @returns {Promise<Array<{created_at: string, type: string}>>}
|
||||||
|
*/
|
||||||
|
async getOnlineOfflineSessionsAfter(userId, afterCreatedAt) {
|
||||||
|
const data = [];
|
||||||
|
await sqliteService.execute(
|
||||||
|
(dbRow) => {
|
||||||
|
data.push({ created_at: dbRow[0], type: dbRow[1] });
|
||||||
|
},
|
||||||
|
`SELECT created_at, type FROM ${dbVars.userPrefix}_feed_online_offline
|
||||||
|
WHERE user_id = @userId
|
||||||
|
AND (type = 'Online' OR type = 'Offline')
|
||||||
|
AND created_at > @afterCreatedAt
|
||||||
|
ORDER BY created_at`,
|
||||||
|
{
|
||||||
|
'@userId': userId,
|
||||||
|
'@afterCreatedAt': afterCreatedAt
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {number} days - Number of days to look back
|
* @param {number} days - Number of days to look back
|
||||||
* @param {number} limit - Max number of worlds to return
|
* @param {number} limit - Max number of worlds to return
|
||||||
|
|||||||
@@ -1386,6 +1386,23 @@ const gameLog = {
|
|||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current user's online sessions after a given timestamp (incremental).
|
||||||
|
* @param {string} afterCreatedAt - Only return rows created after this timestamp
|
||||||
|
* @returns {Promise<Array<{created_at: string, time: number}>>}
|
||||||
|
*/
|
||||||
|
async getCurrentUserOnlineSessionsAfter(afterCreatedAt) {
|
||||||
|
const data = [];
|
||||||
|
await sqliteService.execute(
|
||||||
|
(dbRow) => {
|
||||||
|
data.push({ created_at: dbRow[0], time: dbRow[1] || 0 });
|
||||||
|
},
|
||||||
|
`SELECT created_at, time FROM gamelog_location WHERE created_at > @after ORDER BY created_at`,
|
||||||
|
{ '@after': afterCreatedAt }
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get current user's top visited worlds from gamelog_location.
|
* Get current user's top visited worlds from gamelog_location.
|
||||||
* Groups by world_id and aggregates visit count and total time.
|
* Groups by world_id and aggregates visit count and total time.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { activityCache } from './activityCache.js';
|
||||||
import { avatarFavorites } from './avatarFavorites.js';
|
import { avatarFavorites } from './avatarFavorites.js';
|
||||||
import { avatarTags } from './avatarTags.js';
|
import { avatarTags } from './avatarTags.js';
|
||||||
import { feed } from './feed.js';
|
import { feed } from './feed.js';
|
||||||
@@ -25,6 +26,7 @@ const dbVars = {
|
|||||||
|
|
||||||
const database = {
|
const database = {
|
||||||
...feed,
|
...feed,
|
||||||
|
...activityCache,
|
||||||
...gameLog,
|
...gameLog,
|
||||||
...notifications,
|
...notifications,
|
||||||
...moderation,
|
...moderation,
|
||||||
@@ -70,6 +72,15 @@ const database = {
|
|||||||
await sqliteService.executeNonQuery(
|
await sqliteService.executeNonQuery(
|
||||||
`CREATE TABLE IF NOT EXISTS ${dbVars.userPrefix}_feed_online_offline (id INTEGER PRIMARY KEY, created_at TEXT, user_id TEXT, display_name TEXT, type TEXT, location TEXT, world_name TEXT, time INTEGER, group_name TEXT)`
|
`CREATE TABLE IF NOT EXISTS ${dbVars.userPrefix}_feed_online_offline (id INTEGER PRIMARY KEY, created_at TEXT, user_id TEXT, display_name TEXT, type TEXT, location TEXT, world_name TEXT, time INTEGER, group_name TEXT)`
|
||||||
);
|
);
|
||||||
|
await sqliteService.executeNonQuery(
|
||||||
|
`CREATE TABLE IF NOT EXISTS ${dbVars.userPrefix}_activity_cache_meta (user_id TEXT PRIMARY KEY, updated_at TEXT, is_self INTEGER DEFAULT 0, source_last_created_at TEXT, pending_session_start_at INTEGER)`
|
||||||
|
);
|
||||||
|
await sqliteService.executeNonQuery(
|
||||||
|
`CREATE TABLE IF NOT EXISTS ${dbVars.userPrefix}_activity_cache_sessions (user_id TEXT NOT NULL, start_at INTEGER NOT NULL, end_at INTEGER NOT NULL, PRIMARY KEY (user_id, start_at, end_at))`
|
||||||
|
);
|
||||||
|
await sqliteService.executeNonQuery(
|
||||||
|
`CREATE INDEX IF NOT EXISTS ${dbVars.userPrefix}_activity_cache_sessions_user_start_idx ON ${dbVars.userPrefix}_activity_cache_sessions (user_id, start_at)`
|
||||||
|
);
|
||||||
await sqliteService.executeNonQuery(
|
await sqliteService.executeNonQuery(
|
||||||
`CREATE TABLE IF NOT EXISTS ${dbVars.userPrefix}_friend_log_current (user_id TEXT PRIMARY KEY, display_name TEXT, trust_level TEXT, friend_number INTEGER)`
|
`CREATE TABLE IF NOT EXISTS ${dbVars.userPrefix}_friend_log_current (user_id TEXT PRIMARY KEY, display_name TEXT, trust_level TEXT, friend_number INTEGER)`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const tableAlter = {
|
|||||||
await this.updateTableForGroupNames();
|
await this.updateTableForGroupNames();
|
||||||
await this.addFriendLogFriendNumber();
|
await this.addFriendLogFriendNumber();
|
||||||
await this.updateTableForAvatarHistory();
|
await this.updateTableForAvatarHistory();
|
||||||
|
await this.ensureActivityCacheTables();
|
||||||
// }
|
// }
|
||||||
// await sqliteService.executeNonQuery('PRAGMA user_version = 1');
|
// await sqliteService.executeNonQuery('PRAGMA user_version = 1');
|
||||||
},
|
},
|
||||||
@@ -80,6 +81,25 @@ const tableAlter = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async ensureActivityCacheTables() {
|
||||||
|
const tables = [];
|
||||||
|
await sqliteService.execute((dbRow) => {
|
||||||
|
tables.push(dbRow[0]);
|
||||||
|
}, `SELECT name FROM sqlite_schema WHERE type='table' AND name LIKE '%_feed_online_offline'`);
|
||||||
|
for (const tableName of tables) {
|
||||||
|
const userPrefix = tableName.replace(/_feed_online_offline$/, '');
|
||||||
|
await sqliteService.executeNonQuery(
|
||||||
|
`CREATE TABLE IF NOT EXISTS ${userPrefix}_activity_cache_meta (user_id TEXT PRIMARY KEY, updated_at TEXT, is_self INTEGER DEFAULT 0, source_last_created_at TEXT, pending_session_start_at INTEGER)`
|
||||||
|
);
|
||||||
|
await sqliteService.executeNonQuery(
|
||||||
|
`CREATE TABLE IF NOT EXISTS ${userPrefix}_activity_cache_sessions (user_id TEXT NOT NULL, start_at INTEGER NOT NULL, end_at INTEGER NOT NULL, PRIMARY KEY (user_id, start_at, end_at))`
|
||||||
|
);
|
||||||
|
await sqliteService.executeNonQuery(
|
||||||
|
`CREATE INDEX IF NOT EXISTS ${userPrefix}_activity_cache_sessions_user_start_idx ON ${userPrefix}_activity_cache_sessions (user_id, start_at)`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
export const ONLINE_SESSION_MERGE_GAP_MS = 5 * 60 * 1000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds online sessions from Online/Offline events.
|
* Builds online sessions from Online/Offline events.
|
||||||
* @param {Array<{created_at: string, type: string}>} events - Sorted by created_at
|
* @param {Array<{created_at: string, type: string}>} events - Sorted by created_at
|
||||||
@@ -32,7 +34,10 @@ export function buildSessionsFromEvents(events) {
|
|||||||
* @param {number} [mergeGapMs] - Merge gap threshold (default 5 min)
|
* @param {number} [mergeGapMs] - Merge gap threshold (default 5 min)
|
||||||
* @returns {Array<{start: number, end: number}>}
|
* @returns {Array<{start: number, end: number}>}
|
||||||
*/
|
*/
|
||||||
export function buildSessionsFromGamelog(rows, mergeGapMs = 5 * 60 * 1000) {
|
export function buildSessionsFromGamelog(
|
||||||
|
rows,
|
||||||
|
mergeGapMs = ONLINE_SESSION_MERGE_GAP_MS
|
||||||
|
) {
|
||||||
if (rows.length === 0) return [];
|
if (rows.length === 0) return [];
|
||||||
|
|
||||||
const rawSessions = [];
|
const rawSessions = [];
|
||||||
|
|||||||
@@ -0,0 +1,222 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { toast } from 'vue-sonner';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
import { database } from '../services/database';
|
||||||
|
import {
|
||||||
|
buildSessionsFromGamelog,
|
||||||
|
ONLINE_SESSION_MERGE_GAP_MS
|
||||||
|
} from '../shared/utils/overlapCalculator';
|
||||||
|
|
||||||
|
const ACTIVITY_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
||||||
|
const refreshJobs = new Map();
|
||||||
|
|
||||||
|
function buildSessionsAndPendingFromEvents(events, initialStart = null) {
|
||||||
|
const sessions = [];
|
||||||
|
let currentStart = initialStart;
|
||||||
|
|
||||||
|
for (const event of events) {
|
||||||
|
const ts = new Date(event.created_at).getTime();
|
||||||
|
if (event.type === 'Online') {
|
||||||
|
// Treat consecutive Online events as reconnect boundaries.
|
||||||
|
if (currentStart !== null) {
|
||||||
|
sessions.push({ start: currentStart, end: ts });
|
||||||
|
}
|
||||||
|
currentStart = ts;
|
||||||
|
} else if (event.type === 'Offline' && currentStart !== null) {
|
||||||
|
sessions.push({ start: currentStart, end: ts });
|
||||||
|
currentStart = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
pendingSessionStartAt: currentStart,
|
||||||
|
sessions
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeWithLastSession(lastSession, newSessions) {
|
||||||
|
if (!lastSession || newSessions.length === 0) {
|
||||||
|
return { replaceLastSession: null, sessions: newSessions };
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstSession = newSessions[0];
|
||||||
|
if (firstSession.start > lastSession.end + ONLINE_SESSION_MERGE_GAP_MS) {
|
||||||
|
return { replaceLastSession: null, sessions: newSessions };
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergedFirst = {
|
||||||
|
start: Math.min(lastSession.start, firstSession.start),
|
||||||
|
end: Math.max(lastSession.end, firstSession.end)
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
replaceLastSession: lastSession,
|
||||||
|
sessions: [mergedFirst, ...newSessions.slice(1)]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useActivityStore = defineStore('Activity', () => {
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
function getCache(userId) {
|
||||||
|
return database.getActivityCache(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isExpired(cacheEntry) {
|
||||||
|
if (!cacheEntry?.updatedAt) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const updatedAtMs = Date.parse(cacheEntry.updatedAt);
|
||||||
|
if (Number.isNaN(updatedAtMs)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return Date.now() - updatedAtMs >= ACTIVITY_CACHE_TTL_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRefreshing(userId) {
|
||||||
|
return refreshJobs.has(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fullRefresh(userId, isSelf) {
|
||||||
|
if (isSelf) {
|
||||||
|
const rows = await database.getCurrentUserOnlineSessions();
|
||||||
|
const sessions = buildSessionsFromGamelog(rows);
|
||||||
|
const sourceLastCreatedAt =
|
||||||
|
rows.length > 0 ? rows[rows.length - 1].created_at : '';
|
||||||
|
const entry = {
|
||||||
|
userId,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
isSelf,
|
||||||
|
sourceLastCreatedAt,
|
||||||
|
pendingSessionStartAt: null,
|
||||||
|
sessions
|
||||||
|
};
|
||||||
|
await database.replaceActivityCache(entry);
|
||||||
|
return database.getActivityCache(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const events = await database.getOnlineOfflineSessions(userId);
|
||||||
|
const { sessions, pendingSessionStartAt } = buildSessionsAndPendingFromEvents(events);
|
||||||
|
const sourceLastCreatedAt =
|
||||||
|
events.length > 0 ? events[events.length - 1].created_at : '';
|
||||||
|
const entry = {
|
||||||
|
userId,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
isSelf,
|
||||||
|
sourceLastCreatedAt,
|
||||||
|
pendingSessionStartAt,
|
||||||
|
sessions
|
||||||
|
};
|
||||||
|
await database.replaceActivityCache(entry);
|
||||||
|
return database.getActivityCache(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function incrementalRefresh(meta) {
|
||||||
|
const updatedAt = new Date().toISOString();
|
||||||
|
|
||||||
|
if (meta.isSelf) {
|
||||||
|
if (!meta.sourceLastCreatedAt) {
|
||||||
|
return fullRefresh(meta.userId, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = await database.getCurrentUserOnlineSessionsAfter(
|
||||||
|
meta.sourceLastCreatedAt
|
||||||
|
);
|
||||||
|
if (rows.length === 0) {
|
||||||
|
await database.touchActivityCacheMeta({
|
||||||
|
...meta,
|
||||||
|
updatedAt
|
||||||
|
});
|
||||||
|
return database.getActivityCache(meta.userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceLastCreatedAt = rows[rows.length - 1].created_at;
|
||||||
|
const newSessionsRaw = buildSessionsFromGamelog(rows);
|
||||||
|
const lastSession = await database.getLastActivityCacheSession(meta.userId);
|
||||||
|
const merged = mergeWithLastSession(lastSession, newSessionsRaw);
|
||||||
|
|
||||||
|
await database.appendActivityCache({
|
||||||
|
...meta,
|
||||||
|
updatedAt,
|
||||||
|
sourceLastCreatedAt,
|
||||||
|
pendingSessionStartAt: null,
|
||||||
|
sessions: merged.sessions,
|
||||||
|
replaceLastSession: merged.replaceLastSession
|
||||||
|
});
|
||||||
|
return database.getActivityCache(meta.userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!meta.sourceLastCreatedAt) {
|
||||||
|
return fullRefresh(meta.userId, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const events = await database.getOnlineOfflineSessionsAfter(
|
||||||
|
meta.userId,
|
||||||
|
meta.sourceLastCreatedAt
|
||||||
|
);
|
||||||
|
if (events.length === 0) {
|
||||||
|
await database.touchActivityCacheMeta({
|
||||||
|
...meta,
|
||||||
|
updatedAt
|
||||||
|
});
|
||||||
|
return database.getActivityCache(meta.userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { sessions, pendingSessionStartAt } = buildSessionsAndPendingFromEvents(
|
||||||
|
events,
|
||||||
|
meta.pendingSessionStartAt
|
||||||
|
);
|
||||||
|
const sourceLastCreatedAt = events[events.length - 1].created_at;
|
||||||
|
|
||||||
|
await database.appendActivityCache({
|
||||||
|
...meta,
|
||||||
|
updatedAt,
|
||||||
|
sourceLastCreatedAt,
|
||||||
|
pendingSessionStartAt,
|
||||||
|
sessions
|
||||||
|
});
|
||||||
|
return database.getActivityCache(meta.userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshActivityCache(userId, isSelf, options = {}) {
|
||||||
|
const { notifyStart = false, notifyComplete = false } = options;
|
||||||
|
|
||||||
|
const existing = refreshJobs.get(userId);
|
||||||
|
if (existing) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notifyStart) {
|
||||||
|
toast.info(t('dialog.user.activity.refresh_started'), {
|
||||||
|
position: 'bottom-center'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const job = (async () => {
|
||||||
|
const meta = await database.getActivityCacheMeta(userId);
|
||||||
|
const entry =
|
||||||
|
meta && meta.isSelf === isSelf
|
||||||
|
? await incrementalRefresh(meta)
|
||||||
|
: await fullRefresh(userId, isSelf);
|
||||||
|
if (notifyComplete) {
|
||||||
|
toast.success(t('dialog.user.activity.refresh_complete'), {
|
||||||
|
position: 'bottom-center'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
})().finally(() => {
|
||||||
|
refreshJobs.delete(userId);
|
||||||
|
});
|
||||||
|
|
||||||
|
refreshJobs.set(userId, job);
|
||||||
|
return job;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
getCache,
|
||||||
|
isExpired,
|
||||||
|
isRefreshing,
|
||||||
|
refreshActivityCache,
|
||||||
|
ttlMs: ACTIVITY_CACHE_TTL_MS
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -2,6 +2,7 @@ import { createPinia } from 'pinia';
|
|||||||
|
|
||||||
import { getSentry, isSentryOptedIn } from '../plugins';
|
import { getSentry, isSentryOptedIn } from '../plugins';
|
||||||
import { useAdvancedSettingsStore } from './settings/advanced';
|
import { useAdvancedSettingsStore } from './settings/advanced';
|
||||||
|
import { useActivityStore } from './activity';
|
||||||
import { useAppearanceSettingsStore } from './settings/appearance';
|
import { useAppearanceSettingsStore } from './settings/appearance';
|
||||||
import { useAuthStore } from './auth';
|
import { useAuthStore } from './auth';
|
||||||
import { useAvatarProviderStore } from './avatarProvider';
|
import { useAvatarProviderStore } from './avatarProvider';
|
||||||
@@ -124,6 +125,7 @@ export async function initPiniaPlugins() {
|
|||||||
export function createGlobalStores() {
|
export function createGlobalStores() {
|
||||||
return {
|
return {
|
||||||
advancedSettings: useAdvancedSettingsStore(),
|
advancedSettings: useAdvancedSettingsStore(),
|
||||||
|
activity: useActivityStore(),
|
||||||
appearanceSettings: useAppearanceSettingsStore(),
|
appearanceSettings: useAppearanceSettingsStore(),
|
||||||
discordPresenceSettings: useDiscordPresenceSettingsStore(),
|
discordPresenceSettings: useDiscordPresenceSettingsStore(),
|
||||||
generalSettings: useGeneralSettingsStore(),
|
generalSettings: useGeneralSettingsStore(),
|
||||||
@@ -186,6 +188,7 @@ export {
|
|||||||
useChartsStore,
|
useChartsStore,
|
||||||
useDashboardStore,
|
useDashboardStore,
|
||||||
useAdvancedSettingsStore,
|
useAdvancedSettingsStore,
|
||||||
|
useActivityStore,
|
||||||
useAppearanceSettingsStore,
|
useAppearanceSettingsStore,
|
||||||
useDiscordPresenceSettingsStore,
|
useDiscordPresenceSettingsStore,
|
||||||
useGeneralSettingsStore,
|
useGeneralSettingsStore,
|
||||||
|
|||||||
+19
-18
@@ -71,6 +71,11 @@ export const useVrcxStore = defineStore('Vrcx', () => {
|
|||||||
windowState: '',
|
windowState: '',
|
||||||
externalNotifierVersion: 0
|
externalNotifierVersion: 0
|
||||||
});
|
});
|
||||||
|
const databaseUpgradeState = ref({
|
||||||
|
visible: false,
|
||||||
|
fromVersion: 0,
|
||||||
|
toVersion: 0
|
||||||
|
});
|
||||||
|
|
||||||
const currentlyDroppingFile = ref(null);
|
const currentlyDroppingFile = ref(null);
|
||||||
const isRegistryBackupDialogVisible = ref(false);
|
const isRegistryBackupDialogVisible = ref(false);
|
||||||
@@ -182,15 +187,13 @@ export const useVrcxStore = defineStore('Vrcx', () => {
|
|||||||
*/
|
*/
|
||||||
async function updateDatabaseVersion() {
|
async function updateDatabaseVersion() {
|
||||||
// requires dbVars.userPrefix to be already set
|
// requires dbVars.userPrefix to be already set
|
||||||
const databaseVersion = 13;
|
const databaseVersion = 14;
|
||||||
let msgBox;
|
|
||||||
if (state.databaseVersion < databaseVersion) {
|
if (state.databaseVersion < databaseVersion) {
|
||||||
if (state.databaseVersion) {
|
databaseUpgradeState.value = {
|
||||||
msgBox = toast.warning(
|
visible: state.databaseVersion > 0,
|
||||||
'DO NOT CLOSE VRCX, database upgrade in progress...',
|
fromVersion: state.databaseVersion,
|
||||||
{ duration: Infinity, position: 'bottom-right' }
|
toVersion: databaseVersion
|
||||||
);
|
};
|
||||||
}
|
|
||||||
console.log(
|
console.log(
|
||||||
`Updating database from ${state.databaseVersion} to ${databaseVersion}...`
|
`Updating database from ${state.databaseVersion} to ${databaseVersion}...`
|
||||||
);
|
);
|
||||||
@@ -212,19 +215,16 @@ export const useVrcxStore = defineStore('Vrcx', () => {
|
|||||||
databaseVersion
|
databaseVersion
|
||||||
);
|
);
|
||||||
console.log('Database update complete.');
|
console.log('Database update complete.');
|
||||||
toast.dismiss(msgBox);
|
|
||||||
if (state.databaseVersion) {
|
|
||||||
// only display when database exists
|
|
||||||
toast.success(t('message.database.upgrade_complete'));
|
|
||||||
}
|
|
||||||
state.databaseVersion = databaseVersion;
|
state.databaseVersion = databaseVersion;
|
||||||
|
databaseUpgradeState.value.visible = false;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
toast.dismiss(msgBox);
|
databaseUpgradeState.value.visible = false;
|
||||||
toast.error(
|
await modalStore.alert({
|
||||||
'Database upgrade failed, check console for details',
|
title: t('message.database.upgrade_failed_title'),
|
||||||
{ duration: 120000 }
|
description: t('message.database.upgrade_failed_description'),
|
||||||
);
|
dismissible: false
|
||||||
|
});
|
||||||
AppApi.ShowDevTools();
|
AppApi.ShowDevTools();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -817,6 +817,7 @@ export const useVrcxStore = defineStore('Vrcx', () => {
|
|||||||
state,
|
state,
|
||||||
|
|
||||||
appStartAt,
|
appStartAt,
|
||||||
|
databaseUpgradeState,
|
||||||
proxyServer,
|
proxyServer,
|
||||||
setProxyServer,
|
setProxyServer,
|
||||||
setIpcEnabled,
|
setIpcEnabled,
|
||||||
|
|||||||
Reference in New Issue
Block a user