tauri-app: filters, review on click, smaller video player
All checks were successful
record-daemon / Build, check and test (push) Successful in 2m4s
All checks were successful
record-daemon / Build, check and test (push) Successful in 2m4s
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from "vue";
|
import { ref, computed, onMounted } from "vue";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import type { GameHistoryItem, TimestampedEvent, ItemInfo } from "../types/timeline";
|
import type { GameHistoryItem, TimestampedEvent, ItemInfo } from "../types/timeline";
|
||||||
|
|
||||||
@@ -40,11 +40,57 @@ function getVideoTimestampSecs(event: TimestampedEvent): number {
|
|||||||
return event.video_timestamp[0];
|
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 games = ref<GameHistoryItem[]>([]);
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const error = ref<string | null>(null);
|
const error = ref<string | null>(null);
|
||||||
const selectedGame = ref<GameHistoryItem | 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() {
|
async function loadGameHistory() {
|
||||||
try {
|
try {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
@@ -91,6 +137,19 @@ onMounted(() => {
|
|||||||
</button>
|
</button>
|
||||||
</header>
|
</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 -->
|
<!-- Loading State -->
|
||||||
<div v-if="loading" class="loading">
|
<div v-if="loading" class="loading">
|
||||||
<div class="spinner"></div>
|
<div class="spinner"></div>
|
||||||
@@ -110,10 +169,17 @@ onMounted(() => {
|
|||||||
<p>Start playing to see your match history here.</p>
|
<p>Start playing to see your match history here.</p>
|
||||||
</div>
|
</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 -->
|
<!-- Game List - Single Column -->
|
||||||
<div v-else class="game-list">
|
<div v-else class="game-list">
|
||||||
<div
|
<div
|
||||||
v-for="game in games"
|
v-for="game in filteredGames"
|
||||||
:key="game.recording_id"
|
:key="game.recording_id"
|
||||||
class="game-card"
|
class="game-card"
|
||||||
:class="{
|
:class="{
|
||||||
@@ -121,7 +187,7 @@ onMounted(() => {
|
|||||||
defeat: getGameResult(game) === 'Defeat',
|
defeat: getGameResult(game) === 'Defeat',
|
||||||
terminated: getGameResult(game) === 'Terminated'
|
terminated: getGameResult(game) === 'Terminated'
|
||||||
}"
|
}"
|
||||||
@click="selectGame(game)"
|
@click="openReview(game)"
|
||||||
>
|
>
|
||||||
<!-- Result Banner -->
|
<!-- Result Banner -->
|
||||||
<div class="result-banner">
|
<div class="result-banner">
|
||||||
@@ -367,11 +433,11 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Events Timeline -->
|
<!-- Events Timeline -->
|
||||||
<div class="modal-section" v-if="selectedGame.events.length > 0">
|
<div class="modal-section" v-if="selectedGame.events.filter(e => e.event_type !== 'unknown').length > 0">
|
||||||
<h3>Events ({{ selectedGame.events.length }})</h3>
|
<h3>Events ({{ selectedGame.events.filter(e => e.event_type !== 'unknown').length }})</h3>
|
||||||
<div class="events-list">
|
<div class="events-list">
|
||||||
<div
|
<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"
|
:key="idx"
|
||||||
class="event-item"
|
class="event-item"
|
||||||
>
|
>
|
||||||
@@ -379,8 +445,8 @@ onMounted(() => {
|
|||||||
<span class="event-type">{{ event.event_type }}</span>
|
<span class="event-type">{{ event.event_type }}</span>
|
||||||
<span class="event-desc">{{ event.description }}</span>
|
<span class="event-desc">{{ event.description }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedGame.events.length > 10" class="events-more">
|
<div v-if="selectedGame.events.filter(e => e.event_type !== 'unknown').length > 10" class="events-more">
|
||||||
+{{ selectedGame.events.length - 10 }} more events
|
+{{ selectedGame.events.filter(e => e.event_type !== 'unknown').length - 10 }} more events
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -424,6 +490,40 @@ onMounted(() => {
|
|||||||
color: #f0f0f0;
|
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 {
|
.refresh-btn {
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: rgba(255, 255, 255, 0.1);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
|||||||
@@ -65,11 +65,13 @@ const videoUrl = computed(() => {
|
|||||||
|
|
||||||
// Events sorted by timestamp
|
// Events sorted by timestamp
|
||||||
const sortedEvents = computed(() => {
|
const sortedEvents = computed(() => {
|
||||||
return [...props.game.events].sort((a, b) => {
|
return [...props.game.events]
|
||||||
const aTime = a.video_timestamp[0] + a.video_timestamp[1] / 1e9;
|
.filter(e => e.event_type !== "unknown")
|
||||||
const bTime = b.video_timestamp[0] + b.video_timestamp[1] / 1e9;
|
.sort((a, b) => {
|
||||||
return aTime - bTime;
|
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;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get event position on timeline (0-100%)
|
// Get event position on timeline (0-100%)
|
||||||
@@ -900,7 +902,8 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.game-review {
|
.game-review {
|
||||||
min-height: 100vh;
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
background: linear-gradient(180deg, #0a0a13 0%, #0f0f1a 100%);
|
background: linear-gradient(180deg, #0a0a13 0%, #0f0f1a 100%);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1026,6 +1029,7 @@ onUnmounted(() => {
|
|||||||
.review-content {
|
.review-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1035,6 +1039,8 @@ onUnmounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-section.with-sidebar {
|
.video-section.with-sidebar {
|
||||||
@@ -1044,11 +1050,13 @@ onUnmounted(() => {
|
|||||||
.video-container {
|
.video-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: #000;
|
background: #000;
|
||||||
flex: 1;
|
aspect-ratio: 16/9;
|
||||||
|
max-height: 55vh;
|
||||||
|
width: min(100%, calc(55vh * 16 / 9));
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-height: 300px;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-player {
|
.video-player {
|
||||||
|
|||||||
Reference in New Issue
Block a user