mirror of
https://github.com/Drop-OSS/drop-app.git
synced 2026-07-24 17:03:14 +10:00
feat(library): implement playtime tracking on the frontend
This commit is contained in:
@@ -44,6 +44,10 @@ router.beforeEach(async () => {
|
|||||||
setupHooks();
|
setupHooks();
|
||||||
initialNavigation(state);
|
initialNavigation(state);
|
||||||
|
|
||||||
|
// Setup playtime event listeners
|
||||||
|
const { setupEventListeners } = usePlaytime();
|
||||||
|
setupEventListeners();
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: "Drop",
|
title: "Drop",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="stats" class="flex flex-col gap-1">
|
||||||
|
<!-- Main playtime display -->
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<ClockIcon class="w-4 h-4 text-zinc-400" />
|
||||||
|
<span class="text-sm text-zinc-300">
|
||||||
|
{{ formatPlaytime(stats.totalPlaytimeSeconds) }}
|
||||||
|
</span>
|
||||||
|
<span v-if="isActive" class="text-xs text-green-400 font-medium">
|
||||||
|
• Playing
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Additional details when expanded -->
|
||||||
|
<div v-if="showDetails" class="text-xs text-zinc-400 space-y-1 ml-6">
|
||||||
|
<div>{{ stats.sessionCount }} session{{ stats.sessionCount !== 1 ? 's' : '' }}</div>
|
||||||
|
<div v-if="stats.sessionCount > 0">
|
||||||
|
Avg: {{ formatPlaytime(stats.averageSessionLength) }} per session
|
||||||
|
</div>
|
||||||
|
<div>Last played {{ formatRelativeTime(stats.lastPlayed) }}</div>
|
||||||
|
<div v-if="stats.currentSessionDuration">
|
||||||
|
Current session: {{ formatPlaytime(stats.currentSessionDuration) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- No playtime data -->
|
||||||
|
<div v-else-if="showWhenEmpty" class="flex items-center gap-2 text-zinc-500">
|
||||||
|
<ClockIcon class="w-4 h-4" />
|
||||||
|
<span class="text-sm">Never played</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ClockIcon } from "@heroicons/vue/20/solid";
|
||||||
|
import type { GamePlaytimeStats } from "~/types";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
stats: GamePlaytimeStats | null;
|
||||||
|
isActive?: boolean;
|
||||||
|
showDetails?: boolean;
|
||||||
|
showWhenEmpty?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
isActive: false,
|
||||||
|
showDetails: false,
|
||||||
|
showWhenEmpty: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { formatPlaytime, formatRelativeTime } = usePlaytime();
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
<template>
|
||||||
|
<div class="bg-zinc-800/50 rounded-lg p-4 space-y-4">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<ChartBarIcon class="w-5 h-5 text-zinc-400" />
|
||||||
|
<h3 class="text-lg font-semibold text-zinc-100">Playtime Statistics</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="stats" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<!-- Total Playtime -->
|
||||||
|
<div class="bg-zinc-700/50 rounded-lg p-3">
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<ClockIcon class="w-4 h-4 text-blue-400" />
|
||||||
|
<span class="text-sm font-medium text-zinc-300">Total Playtime</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-2xl font-bold text-zinc-100">
|
||||||
|
{{ formatDetailedPlaytime(stats.totalPlaytimeSeconds) }}
|
||||||
|
</div>
|
||||||
|
<div v-if="stats.currentSessionDuration" class="text-xs text-green-400 mt-1">
|
||||||
|
+{{ formatPlaytime(stats.currentSessionDuration) }} this session
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sessions -->
|
||||||
|
<div class="bg-zinc-700/50 rounded-lg p-3">
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<PlayIcon class="w-4 h-4 text-green-400" />
|
||||||
|
<span class="text-sm font-medium text-zinc-300">Sessions</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-2xl font-bold text-zinc-100">
|
||||||
|
{{ stats.sessionCount }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-zinc-400 mt-1">
|
||||||
|
Avg: {{ formatPlaytime(stats.averageSessionLength) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- First Played -->
|
||||||
|
<div class="bg-zinc-700/50 rounded-lg p-3">
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<CalendarIcon class="w-4 h-4 text-purple-400" />
|
||||||
|
<span class="text-sm font-medium text-zinc-300">First Played</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-lg font-semibold text-zinc-100">
|
||||||
|
{{ formatDate(stats.firstPlayed) }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-zinc-400 mt-1">
|
||||||
|
{{ formatRelativeTime(stats.firstPlayed) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Last Played -->
|
||||||
|
<div class="bg-zinc-700/50 rounded-lg p-3">
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<ClockIcon class="w-4 h-4 text-orange-400" />
|
||||||
|
<span class="text-sm font-medium text-zinc-300">Last Played</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-lg font-semibold text-zinc-100">
|
||||||
|
{{ formatDate(stats.lastPlayed) }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-zinc-400 mt-1">
|
||||||
|
{{ formatRelativeTime(stats.lastPlayed) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- No stats available -->
|
||||||
|
<div v-else class="text-center py-8">
|
||||||
|
<ClockIcon class="w-12 h-12 text-zinc-600 mx-auto mb-3" />
|
||||||
|
<p class="text-zinc-400">No playtime data available</p>
|
||||||
|
<p class="text-sm text-zinc-500 mt-1">Statistics will appear after you start playing</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Current session indicator -->
|
||||||
|
<div v-if="isActive && stats" class="border-t border-zinc-700 pt-3">
|
||||||
|
<div class="flex items-center gap-2 text-green-400">
|
||||||
|
<div class="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
|
||||||
|
<span class="text-sm font-medium">Currently playing</span>
|
||||||
|
<span v-if="stats.currentSessionDuration" class="text-xs text-zinc-400">
|
||||||
|
{{ formatPlaytime(stats.currentSessionDuration) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
ChartBarIcon,
|
||||||
|
ClockIcon,
|
||||||
|
PlayIcon,
|
||||||
|
CalendarIcon
|
||||||
|
} from "@heroicons/vue/20/solid";
|
||||||
|
import type { GamePlaytimeStats } from "~/types";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
stats: GamePlaytimeStats | null;
|
||||||
|
isActive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
isActive: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { formatPlaytime, formatDetailedPlaytime, formatRelativeTime } = usePlaytime();
|
||||||
|
|
||||||
|
const formatDate = (timestamp: string): string => {
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
return date.toLocaleDateString(undefined, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { listen } from "@tauri-apps/api/event";
|
||||||
|
import type {
|
||||||
|
GamePlaytimeStats,
|
||||||
|
PlaytimeUpdateEvent,
|
||||||
|
PlaytimeSessionStartEvent,
|
||||||
|
PlaytimeSessionEndEvent
|
||||||
|
} from "~/types";
|
||||||
|
|
||||||
|
export const usePlaytime = () => {
|
||||||
|
const playtimeStats = useState<Record<string, GamePlaytimeStats>>('playtime-stats', () => ({}));
|
||||||
|
const activeSessions = useState<Set<string>>('active-sessions', () => new Set());
|
||||||
|
|
||||||
|
// Fetch playtime stats for a specific game
|
||||||
|
const fetchGamePlaytime = async (gameId: string): Promise<GamePlaytimeStats | null> => {
|
||||||
|
try {
|
||||||
|
const stats = await invoke<GamePlaytimeStats | null>("fetch_game_playtime", { gameId });
|
||||||
|
if (stats) {
|
||||||
|
playtimeStats.value[gameId] = stats;
|
||||||
|
}
|
||||||
|
return stats;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to fetch playtime for game ${gameId}:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch all playtime stats
|
||||||
|
const fetchAllPlaytimeStats = async (): Promise<Record<string, GamePlaytimeStats>> => {
|
||||||
|
try {
|
||||||
|
const stats = await invoke<Record<string, GamePlaytimeStats>>("fetch_all_playtime_stats");
|
||||||
|
playtimeStats.value = stats;
|
||||||
|
return stats;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch all playtime stats:", error);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if a session is active
|
||||||
|
const isSessionActive = async (gameId: string): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
return await invoke<boolean>("is_playtime_session_active", { gameId });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to check session status for game ${gameId}:`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get all active sessions
|
||||||
|
const getActiveSessions = async (): Promise<string[]> => {
|
||||||
|
try {
|
||||||
|
const sessions = await invoke<string[]>("get_active_playtime_sessions");
|
||||||
|
activeSessions.value = new Set(sessions);
|
||||||
|
return sessions;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to get active sessions:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format playtime duration
|
||||||
|
const formatPlaytime = (seconds: number): string => {
|
||||||
|
if (seconds < 60) {
|
||||||
|
return `${seconds}s`;
|
||||||
|
} else if (seconds < 3600) {
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
return `${minutes}m`;
|
||||||
|
} else {
|
||||||
|
const hours = Math.floor(seconds / 3600);
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60);
|
||||||
|
if (minutes === 0) {
|
||||||
|
return `${hours}h`;
|
||||||
|
}
|
||||||
|
return `${hours}h ${minutes}m`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format detailed playtime
|
||||||
|
const formatDetailedPlaytime = (seconds: number): string => {
|
||||||
|
if (seconds < 60) {
|
||||||
|
return `${seconds} seconds`;
|
||||||
|
} else if (seconds < 3600) {
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const remainingSeconds = seconds % 60;
|
||||||
|
if (remainingSeconds === 0) {
|
||||||
|
return `${minutes} minutes`;
|
||||||
|
}
|
||||||
|
return `${minutes} minutes, ${remainingSeconds} seconds`;
|
||||||
|
} else {
|
||||||
|
const hours = Math.floor(seconds / 3600);
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60);
|
||||||
|
if (minutes === 0) {
|
||||||
|
return `${hours} hours`;
|
||||||
|
}
|
||||||
|
return `${hours} hours, ${minutes} minutes`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format relative time (e.g., "2 hours ago")
|
||||||
|
const formatRelativeTime = (timestamp: string): string => {
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
const now = new Date();
|
||||||
|
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
|
||||||
|
|
||||||
|
if (diffInSeconds < 60) {
|
||||||
|
return "Just now";
|
||||||
|
} else if (diffInSeconds < 3600) {
|
||||||
|
const minutes = Math.floor(diffInSeconds / 60);
|
||||||
|
return `${minutes} minute${minutes !== 1 ? 's' : ''} ago`;
|
||||||
|
} else if (diffInSeconds < 86400) {
|
||||||
|
const hours = Math.floor(diffInSeconds / 3600);
|
||||||
|
return `${hours} hour${hours !== 1 ? 's' : ''} ago`;
|
||||||
|
} else if (diffInSeconds < 604800) {
|
||||||
|
const days = Math.floor(diffInSeconds / 86400);
|
||||||
|
return `${days} day${days !== 1 ? 's' : ''} ago`;
|
||||||
|
} else {
|
||||||
|
return date.toLocaleDateString();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get playtime stats for a game (from cache or fetch)
|
||||||
|
const getGamePlaytime = async (gameId: string): Promise<GamePlaytimeStats | null> => {
|
||||||
|
if (playtimeStats.value[gameId]) {
|
||||||
|
return playtimeStats.value[gameId];
|
||||||
|
}
|
||||||
|
return await fetchGamePlaytime(gameId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Setup event listeners
|
||||||
|
const setupEventListeners = () => {
|
||||||
|
// Listen for general playtime updates
|
||||||
|
listen<PlaytimeUpdateEvent>("playtime_update", (event) => {
|
||||||
|
const { gameId, stats, isActive } = event.payload;
|
||||||
|
playtimeStats.value[gameId] = stats;
|
||||||
|
|
||||||
|
if (isActive) {
|
||||||
|
activeSessions.value.add(gameId);
|
||||||
|
} else {
|
||||||
|
activeSessions.value.delete(gameId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for session start events
|
||||||
|
listen<PlaytimeSessionStartEvent>("playtime_session_start", (event) => {
|
||||||
|
const { gameId } = event.payload;
|
||||||
|
activeSessions.value.add(gameId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for session end events
|
||||||
|
listen<PlaytimeSessionEndEvent>("playtime_session_end", (event) => {
|
||||||
|
const { gameId } = event.payload;
|
||||||
|
activeSessions.value.delete(gameId);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Setup game-specific event listeners
|
||||||
|
const setupGameEventListeners = (gameId: string) => {
|
||||||
|
listen<PlaytimeUpdateEvent>(`playtime_update/${gameId}`, (event) => {
|
||||||
|
const { stats, isActive } = event.payload;
|
||||||
|
playtimeStats.value[gameId] = stats;
|
||||||
|
|
||||||
|
if (isActive) {
|
||||||
|
activeSessions.value.add(gameId);
|
||||||
|
} else {
|
||||||
|
activeSessions.value.delete(gameId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
listen<PlaytimeSessionStartEvent>(`playtime_session_start/${gameId}`, () => {
|
||||||
|
activeSessions.value.add(gameId);
|
||||||
|
});
|
||||||
|
|
||||||
|
listen<PlaytimeSessionEndEvent>(`playtime_session_end/${gameId}`, () => {
|
||||||
|
activeSessions.value.delete(gameId);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
playtimeStats: readonly(playtimeStats),
|
||||||
|
activeSessions: readonly(activeSessions),
|
||||||
|
fetchGamePlaytime,
|
||||||
|
fetchAllPlaytimeStats,
|
||||||
|
isSessionActive,
|
||||||
|
getActiveSessions,
|
||||||
|
formatPlaytime,
|
||||||
|
formatDetailedPlaytime,
|
||||||
|
formatRelativeTime,
|
||||||
|
getGamePlaytime,
|
||||||
|
setupEventListeners,
|
||||||
|
setupGameEventListeners,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -18,10 +18,19 @@
|
|||||||
<div class="relative z-10">
|
<div class="relative z-10">
|
||||||
<div class="px-8 pb-4">
|
<div class="px-8 pb-4">
|
||||||
<h1
|
<h1
|
||||||
class="text-5xl text-zinc-100 font-bold font-display drop-shadow-lg mb-8"
|
class="text-5xl text-zinc-100 font-bold font-display drop-shadow-lg mb-4"
|
||||||
>
|
>
|
||||||
{{ game.mName }}
|
{{ game.mName }}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
<!-- Playtime Display -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<PlaytimeDisplay
|
||||||
|
:stats="gamePlaytime"
|
||||||
|
:is-active="isPlaytimeActive"
|
||||||
|
:show-details="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-row gap-x-4 items-stretch mb-8">
|
<div class="flex flex-row gap-x-4 items-stretch mb-8">
|
||||||
<!-- Do not add scale animations to this: https://stackoverflow.com/a/35683068 -->
|
<!-- Do not add scale animations to this: https://stackoverflow.com/a/35683068 -->
|
||||||
@@ -60,6 +69,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
|
<!-- Playtime Statistics -->
|
||||||
|
<PlaytimeStats
|
||||||
|
:stats="gamePlaytime"
|
||||||
|
:is-active="isPlaytimeActive"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="bg-zinc-800/50 rounded-xl p-6 backdrop-blur-sm">
|
<div class="bg-zinc-800/50 rounded-xl p-6 backdrop-blur-sm">
|
||||||
<h2 class="text-xl font-display font-semibold text-zinc-100 mb-4">
|
<h2 class="text-xl font-display font-semibold text-zinc-100 mb-4">
|
||||||
Game Images
|
Game Images
|
||||||
@@ -528,6 +543,19 @@ const currentImageIndex = ref(0);
|
|||||||
|
|
||||||
const configureModalOpen = ref(false);
|
const configureModalOpen = ref(false);
|
||||||
|
|
||||||
|
// Playtime tracking
|
||||||
|
const {
|
||||||
|
getGamePlaytime,
|
||||||
|
setupGameEventListeners,
|
||||||
|
activeSessions
|
||||||
|
} = usePlaytime();
|
||||||
|
|
||||||
|
const gamePlaytime = ref(await getGamePlaytime(id));
|
||||||
|
const isPlaytimeActive = computed(() => activeSessions.value.has(id));
|
||||||
|
|
||||||
|
// Setup playtime event listeners for this game
|
||||||
|
setupGameEventListeners(id);
|
||||||
|
|
||||||
async function installFlow() {
|
async function installFlow() {
|
||||||
installFlowOpen.value = true;
|
installFlowOpen.value = true;
|
||||||
versionOptions.value = undefined;
|
versionOptions.value = undefined;
|
||||||
|
|||||||
@@ -94,3 +94,37 @@ export type Settings = {
|
|||||||
maxDownloadThreads: number;
|
maxDownloadThreads: number;
|
||||||
forceOffline: boolean;
|
forceOffline: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GamePlaytimeStats = {
|
||||||
|
gameId: string;
|
||||||
|
totalPlaytimeSeconds: number;
|
||||||
|
sessionCount: number;
|
||||||
|
firstPlayed: string;
|
||||||
|
lastPlayed: string;
|
||||||
|
averageSessionLength: number;
|
||||||
|
currentSessionDuration?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PlaytimeSession = {
|
||||||
|
gameId: string;
|
||||||
|
startTime: string;
|
||||||
|
sessionId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PlaytimeUpdateEvent = {
|
||||||
|
gameId: string;
|
||||||
|
stats: GamePlaytimeStats;
|
||||||
|
isActive: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PlaytimeSessionStartEvent = {
|
||||||
|
gameId: string;
|
||||||
|
startTime: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PlaytimeSessionEndEvent = {
|
||||||
|
gameId: string;
|
||||||
|
sessionDurationSeconds: number;
|
||||||
|
totalPlaytimeSeconds: number;
|
||||||
|
sessionCount: number;
|
||||||
|
};
|
||||||
@@ -51,13 +51,14 @@ pub struct PlaytimeStats {
|
|||||||
|
|
||||||
impl From<GamePlaytimeStats> for PlaytimeStats {
|
impl From<GamePlaytimeStats> for PlaytimeStats {
|
||||||
fn from(stats: GamePlaytimeStats) -> Self {
|
fn from(stats: GamePlaytimeStats) -> Self {
|
||||||
|
let average_length = stats.average_session_length();
|
||||||
Self {
|
Self {
|
||||||
game_id: stats.game_id,
|
game_id: stats.game_id,
|
||||||
total_playtime_seconds: stats.total_playtime_seconds,
|
total_playtime_seconds: stats.total_playtime_seconds,
|
||||||
session_count: stats.session_count,
|
session_count: stats.session_count,
|
||||||
first_played: stats.first_played,
|
first_played: stats.first_played,
|
||||||
last_played: stats.last_played,
|
last_played: stats.last_played,
|
||||||
average_session_length: stats.average_session_length(),
|
average_session_length: average_length,
|
||||||
current_session_duration: None,
|
current_session_duration: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user