add game session tracking and display in status bar

This commit is contained in:
pa
2026-03-13 22:56:04 +09:00
parent 7b7c1b4568
commit 9b6ca42d9d
8 changed files with 361 additions and 15 deletions

View File

@@ -1,6 +1,7 @@
<template>
<div
class="shrink-0 h-[22px] flex items-center bg-sidebar border-t border-border font-mono text-xs select-none overflow-hidden"
class="shrink-0 h-[22px] flex items-center bg-sidebar border-t border-border text-xs select-none overflow-hidden"
style="font-family: var(--font-mono-cjk)"
@contextmenu.prevent>
<ContextMenu>
<ContextMenuTrigger as-child>
@@ -50,19 +51,53 @@
</div>
</TooltipWrapper>
<TooltipWrapper
v-if="!isMacOS && visibility.vrchat"
:content="
gameStore.isGameRunning ? t('status_bar.game_running') : t('status_bar.game_stopped')
"
side="top">
<div class="flex items-center gap-1 px-2 h-[22px] whitespace-nowrap border-r border-border">
<span
class="inline-block size-2 rounded-full shrink-0"
:class="gameStore.isGameRunning ? 'bg-status-online' : 'bg-status-offline-alt'" />
<span class="text-foreground text-[11px]">{{ t('status_bar.game') }}</span>
</div>
</TooltipWrapper>
<HoverCard v-if="!isMacOS && visibility.vrchat" v-model:open="gameHoverOpen" :open-delay="50" :close-delay="50">
<HoverCardTrigger as-child>
<div class="flex items-center gap-1 px-2 h-[22px] whitespace-nowrap border-r border-border">
<span
class="inline-block size-2 rounded-full shrink-0"
:class="gameStore.isGameRunning ? 'bg-status-online' : 'bg-status-offline-alt'" />
<span class="text-foreground text-[11px]">{{ t('status_bar.game') }}</span>
<span v-if="gameStore.isGameRunning" class="text-[10px] text-foreground">{{
gameSessionText
}}</span>
</div>
</HoverCardTrigger>
<HoverCardContent
v-if="gameStore.isGameRunning && userStore.currentUser.$online_for"
class="w-auto min-w-[160px] px-3 py-2"
side="top"
align="start"
:side-offset="4">
<div class="flex flex-col gap-1">
<div class="flex items-center justify-between gap-3">
<span class="text-[11px] text-muted-foreground">{{ t('status_bar.game_started_at') }}</span>
<span class="text-[11px] text-foreground">{{ gameStartedAtText }}</span>
</div>
<div class="flex items-center justify-between gap-3">
<span class="text-[11px] text-muted-foreground">{{ t('status_bar.game_session_duration') }}</span>
<span class="text-[11px] text-foreground">{{ gameSessionDetailText }}</span>
</div>
</div>
</HoverCardContent>
<HoverCardContent
v-else-if="!gameStore.isGameRunning && gameStore.lastSessionDurationMs > 0"
class="w-auto min-w-[160px] px-3 py-2"
side="top"
align="start"
:side-offset="4">
<div class="flex flex-col gap-1">
<div class="flex items-center justify-between gap-3">
<span class="text-[11px] text-muted-foreground">{{ t('status_bar.game_last_session') }}</span>
<span class="text-[11px] text-foreground">{{ lastSessionText }}</span>
</div>
<div class="flex items-center justify-between gap-3">
<span class="text-[11px] text-muted-foreground">{{ t('status_bar.game_last_offline') }}</span>
<span class="text-[11px] text-foreground">{{ lastOfflineTimeText }}</span>
</div>
</div>
</HoverCardContent>
</HoverCard>
<HoverCard v-if="visibility.servers" v-model:open="serversHoverOpen">
<HoverCardTrigger as-child>
@@ -284,8 +319,9 @@
NumberFieldIncrement,
NumberFieldInput
} from '@/components/ui/number-field';
import { useGameStore, useGeneralSettingsStore, useVrcStatusStore, useVrcxStore } from '@/stores';
import { useGameStore, useGeneralSettingsStore, useUserStore, useVrcStatusStore, useVrcxStore } from '@/stores';
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card';
import { timeToText } from '@/shared/utils';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { useIntervalFn, useNow } from '@vueuse/core';
import { TooltipWrapper } from '@/components/ui/tooltip';
@@ -315,10 +351,41 @@
const isMacOS = computed(() => navigator.platform.includes('Mac'));
const gameStore = useGameStore();
const userStore = useUserStore();
const vrcxStore = useVrcxStore();
const vrcStatusStore = useVrcStatusStore();
const generalSettingsStore = useGeneralSettingsStore();
// --- Game session timer ---
const gameHoverOpen = ref(false);
const gameSessionText = computed(() => {
if (!gameStore.isGameRunning || !userStore.currentUser.$online_for) return '';
const elapsed = now.value - userStore.currentUser.$online_for;
return elapsed > 0 ? timeToText(elapsed) : '';
});
const gameStartedAtText = computed(() => {
if (!userStore.currentUser.$online_for) return '-';
return dayjs(userStore.currentUser.$online_for).format('MM/DD HH:mm');
});
const gameSessionDetailText = computed(() => {
if (!gameStore.isGameRunning || !userStore.currentUser.$online_for) return '-';
const elapsed = now.value - userStore.currentUser.$online_for;
return elapsed > 0 ? timeToText(elapsed, true) : '-';
});
const lastSessionText = computed(() => {
if (gameStore.lastSessionDurationMs <= 0) return '-';
return timeToText(gameStore.lastSessionDurationMs);
});
const lastOfflineTimeText = computed(() => {
if (gameStore.lastOfflineAt <= 0) return '-';
return dayjs(gameStore.lastOfflineAt).format('MM/DD HH:mm');
});
// --- Servers status HoverCard ---
const serversHoverOpen = ref(false);
let serversHoverTimer = null;

View File

@@ -189,6 +189,8 @@ function mountStatusBar(storeOverrides = {}) {
Game: {
isGameRunning: false,
isSteamVRRunning: false,
lastSessionDurationMs: 0,
lastOfflineAt: 0,
...storeOverrides.Game
},
Vrcx: {
@@ -202,6 +204,12 @@ function mountStatusBar(storeOverrides = {}) {
lastStatusSummary: '',
...storeOverrides.VrcStatus
},
User: {
currentUser: {
$online_for: Date.now()
},
...storeOverrides.User
},
GeneralSettings: {
...storeOverrides.GeneralSettings
}
@@ -269,4 +277,17 @@ describe('StatusBar.vue - Servers indicator', () => {
const wrapper = mountStatusBar({ Game: { isSteamVRRunning: true } });
expect(wrapper.text()).toContain('SteamVR');
});
test('shows last game session details when game is offline and there is session data', () => {
const wrapper = mountStatusBar({
Game: {
isGameRunning: false,
lastSessionDurationMs: 3_600_000,
lastOfflineAt: new Date('2026-03-13T14:30:00Z').getTime()
}
});
expect(wrapper.text()).toContain('Last Session');
expect(wrapper.text()).toContain('Offline Since');
});
});