Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions main/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ router.beforeEach(async () => {
setupHooks();
initialNavigation(state);

// Setup playtime event listeners
const { setupEventListeners } = usePlaytime();
setupEventListeners();

useHead({
title: "Drop",
});
Expand Down
5 changes: 4 additions & 1 deletion main/components/HeaderQueueWidget.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ const props = defineProps<{ object?: QueueState["queue"][0] }>();
/>
<div
v-if="props.object?.progress"
class="transition-all absolute left-0 top-0 bottom-0 bg-blue-600 z-10"
:class="[
'transition-all duration-500 absolute left-0 top-0 bottom-0 z-10',
props.object.status === 'Validating' ? 'bg-green-600' : 'bg-blue-600'
]"
:style="{ width: `${props.object.progress * 99 + 1}%` }"
/>
</NuxtLink>
Expand Down
53 changes: 53 additions & 0 deletions main/components/PlaytimeDisplay.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<template>
<div v-if="stats" class="flex flex-col gap-1">
<!-- Main playtime display -->
<div class="flex items-center gap-2">
<ClockIcon class="w-5 h-5 text-zinc-400" />
<span class="text-base text-zinc-300 font-medium">
{{ formatPlaytime(stats.totalPlaytimeSeconds) }} played
</span>
<span v-if="isActive && showActiveIndicator" class="text-sm 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-7">
<div>{{ stats.sessionCount }} session{{ stats.sessionCount !== 1 ? 's' : '' }}</div>
<div v-if="stats.sessionCount > 0">
Avg: {{ formatPlaytime(stats.averageSessionLength) }} per session
</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-5 h-5" />
<span class="text-base">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;
showActiveIndicator?: boolean;
}

const props = withDefaults(defineProps<Props>(), {
isActive: false,
showDetails: false,
showWhenEmpty: true,
showActiveIndicator: true,
});

const { formatPlaytime } = usePlaytime();
</script>
163 changes: 163 additions & 0 deletions main/components/PlaytimeStats.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<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="space-y-4">
<!-- Top row: Total Playtime and Sessions -->
<div 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>
</div>

<!-- Bottom row: First Played and Last Played -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- First Played -->
<div v-if="stats.firstPlayed" 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 v-if="stats.lastPlayed" 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>
</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 } = usePlaytime();

// Date formatting functions
const formatDate = (dateString: string) => {
try {
const date = new Date(dateString);
if (isNaN(date.getTime())) {
console.warn('Invalid date received:', dateString);
return 'Unknown date';
}
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
} catch (error) {
console.error('Error formatting date:', dateString, error);
return 'Unknown date';
}
};

const formatRelativeTime = (dateString: string) => {
try {
const date = new Date(dateString);
if (isNaN(date.getTime())) {
return 'Unknown time';
}

const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));

if (diffDays === 0) {
return 'Today';
} else if (diffDays === 1) {
return 'Yesterday';
} else if (diffDays < 7) {
return `${diffDays} days ago`;
} else if (diffDays < 30) {
const weeks = Math.floor(diffDays / 7);
return `${weeks} week${weeks > 1 ? 's' : ''} ago`;
} else if (diffDays < 365) {
const months = Math.floor(diffDays / 30);
return `${months} month${months > 1 ? 's' : ''} ago`;
} else {
const years = Math.floor(diffDays / 365);
return `${years} year${years > 1 ? 's' : ''} ago`;
}
} catch (error) {
console.error('Error formatting relative time:', dateString, error);
return 'Unknown time';
}
};
</script>
7 changes: 6 additions & 1 deletion main/composables/downloads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,9 @@ listen("update_stats", (event) => {
stats.value = event.payload as StatsState;
});

export const useDownloadHistory = () => useState<Array<number>>('history', () => []);
export type SpeedHistoryEntry = {
speed: number;
isValidating: boolean;
};

export const useDownloadHistory = () => useState<Array<SpeedHistoryEntry>>('history', () => []);
Loading