tauri-app: filters, review on click, smaller video player
All checks were successful
record-daemon / Build, check and test (push) Successful in 2m4s

This commit is contained in:
2026-05-07 13:16:28 +02:00
parent ff4d865c2a
commit 2b4f3499cd
2 changed files with 124 additions and 16 deletions

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { ref, computed, onMounted } from "vue";
import { invoke } from "@tauri-apps/api/core";
import type { GameHistoryItem, TimestampedEvent, ItemInfo } from "../types/timeline";
@@ -40,11 +40,57 @@ function getVideoTimestampSecs(event: TimestampedEvent): number {
return event.video_timestamp[0];
}
// Queue filter types
type QueueFilter = "all" | "ranked_solo" | "ranked_flex" | "normals" | "aram" | "custom";
const activeFilter = ref<QueueFilter>("all");
const queueFilterOptions: { value: QueueFilter; label: string }[] = [
{ value: "all", label: "All" },
{ value: "ranked_solo", label: "Ranked Solo" },
{ value: "ranked_flex", label: "Ranked Flex" },
{ value: "normals", label: "Normals" },
{ value: "aram", label: "ARAM" },
{ value: "custom", label: "Custom" },
];
// Categorize a game into a queue filter category
function getQueueCategory(game: GameHistoryItem): QueueFilter {
const queueType = getQueueType(game);
const queueId = getQueueId(game);
// Check by queue type name first
if (queueType) {
if (queueType.includes("Ranked Solo")) return "ranked_solo";
if (queueType.includes("Ranked Flex")) return "ranked_flex";
if (queueType.includes("ARAM")) return "aram";
if (queueType.includes("Normal") || queueType.includes("Draft") || queueType.includes("Blind")) return "normals";
if (queueType.includes("Custom") || queueType.includes("Practice Tool")) return "custom";
}
// Fallback to queue ID
if (queueId !== null) {
if (queueId === 420) return "ranked_solo";
if (queueId === 440) return "ranked_flex";
if (queueId === 400 || queueId === 430) return "normals";
if (queueId === 450) return "aram";
if (queueId === 600 || queueId === 610) return "custom";
}
// Default: treat unknown as custom
return "custom";
}
const games = ref<GameHistoryItem[]>([]);
const loading = ref(true);
const error = ref<string | null>(null);
const selectedGame = ref<GameHistoryItem | null>(null);
// Filtered games based on active filter
const filteredGames = computed(() => {
if (activeFilter.value === "all") return games.value;
return games.value.filter(g => getQueueCategory(g) === activeFilter.value);
});
async function loadGameHistory() {
try {
loading.value = true;
@@ -91,6 +137,19 @@ onMounted(() => {
</button>
</header>
<!-- Queue Filters -->
<div v-if="!loading && !error && games.length > 0" class="filter-bar">
<button
v-for="opt in queueFilterOptions"
:key="opt.value"
class="filter-btn"
:class="{ active: activeFilter === opt.value }"
@click="activeFilter = opt.value"
>
{{ opt.label }}
</button>
</div>
<!-- Loading State -->
<div v-if="loading" class="loading">
<div class="spinner"></div>
@@ -110,10 +169,17 @@ onMounted(() => {
<p>Start playing to see your match history here.</p>
</div>
<!-- No results for filter -->
<div v-else-if="filteredGames.length === 0" class="empty">
<div class="empty-icon">🔍</div>
<h2>No Games Found</h2>
<p>No games match the selected filter.</p>
</div>
<!-- Game List - Single Column -->
<div v-else class="game-list">
<div
v-for="game in games"
v-for="game in filteredGames"
:key="game.recording_id"
class="game-card"
:class="{
@@ -121,7 +187,7 @@ onMounted(() => {
defeat: getGameResult(game) === 'Defeat',
terminated: getGameResult(game) === 'Terminated'
}"
@click="selectGame(game)"
@click="openReview(game)"
>
<!-- Result Banner -->
<div class="result-banner">
@@ -367,11 +433,11 @@ onMounted(() => {
</div>
<!-- Events Timeline -->
<div class="modal-section" v-if="selectedGame.events.length > 0">
<h3>Events ({{ selectedGame.events.length }})</h3>
<div class="modal-section" v-if="selectedGame.events.filter(e => e.event_type !== 'unknown').length > 0">
<h3>Events ({{ selectedGame.events.filter(e => e.event_type !== 'unknown').length }})</h3>
<div class="events-list">
<div
v-for="(event, idx) in selectedGame.events.slice(0, 10)"
v-for="(event, idx) in selectedGame.events.filter(e => e.event_type !== 'unknown').slice(0, 10)"
:key="idx"
class="event-item"
>
@@ -379,8 +445,8 @@ onMounted(() => {
<span class="event-type">{{ event.event_type }}</span>
<span class="event-desc">{{ event.description }}</span>
</div>
<div v-if="selectedGame.events.length > 10" class="events-more">
+{{ selectedGame.events.length - 10 }} more events
<div v-if="selectedGame.events.filter(e => e.event_type !== 'unknown').length > 10" class="events-more">
+{{ selectedGame.events.filter(e => e.event_type !== 'unknown').length - 10 }} more events
</div>
</div>
</div>
@@ -424,6 +490,40 @@ onMounted(() => {
color: #f0f0f0;
}
/* Filter Bar */
.filter-bar {
display: flex;
gap: 0.5rem;
padding: 0.75rem 2rem;
background: rgba(0, 0, 0, 0.15);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
overflow-x: auto;
}
.filter-btn {
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #888;
padding: 0.4rem 0.9rem;
border-radius: 20px;
cursor: pointer;
font-size: 0.8rem;
font-weight: 500;
transition: all 0.2s;
white-space: nowrap;
}
.filter-btn:hover {
background: rgba(255, 255, 255, 0.1);
color: #ccc;
}
.filter-btn.active {
background: rgba(200, 170, 110, 0.25);
border-color: #c8aa6e;
color: #c8aa6e;
}
.refresh-btn {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);

View File

@@ -65,7 +65,9 @@ const videoUrl = computed(() => {
// Events sorted by timestamp
const sortedEvents = computed(() => {
return [...props.game.events].sort((a, b) => {
return [...props.game.events]
.filter(e => e.event_type !== "unknown")
.sort((a, b) => {
const aTime = a.video_timestamp[0] + a.video_timestamp[1] / 1e9;
const bTime = b.video_timestamp[0] + b.video_timestamp[1] / 1e9;
return aTime - bTime;
@@ -900,7 +902,8 @@ onUnmounted(() => {
<style scoped>
.game-review {
min-height: 100vh;
height: 100vh;
overflow: hidden;
background: linear-gradient(180deg, #0a0a13 0%, #0f0f1a 100%);
color: #fff;
display: flex;
@@ -1026,6 +1029,7 @@ onUnmounted(() => {
.review-content {
display: flex;
flex: 1;
min-height: 0;
overflow: hidden;
}
@@ -1035,6 +1039,8 @@ onUnmounted(() => {
display: flex;
flex-direction: column;
min-width: 0;
min-height: 0;
align-items: center;
}
.video-section.with-sidebar {
@@ -1044,11 +1050,13 @@ onUnmounted(() => {
.video-container {
position: relative;
background: #000;
flex: 1;
aspect-ratio: 16/9;
max-height: 55vh;
width: min(100%, calc(55vh * 16 / 9));
display: flex;
align-items: center;
justify-content: center;
min-height: 300px;
margin: 0 auto;
}
.video-player {