@@ -7,7 +7,11 @@ import {
77 ChevronRight ,
88 ChevronLeft ,
99 PlayCircle ,
10+ RefreshCw ,
11+ Loader2 ,
1012} from " lucide-vue-next" ;
13+ import { toast } from " @/components/ui/toast" ;
14+ import { generateMutation } from " ~/graphql/graphqlGen" ;
1115import {
1216 Tooltip ,
1317 TooltipContent ,
@@ -71,19 +75,49 @@ import cleanMapName from "~/utilities/cleanMapName";
7175 class="text-xs px-2 py-0.5 backdrop-blur-sm"
7276 >{{ $t("match.decider") }}</Badge
7377 >
74- <Tooltip v-if =" hasDemo " >
78+ <Tooltip v-if =" hasDemo && canWatchDemo && hasDemoMetadata " >
7579 <TooltipTrigger as-child>
7680 <Button
7781 size="xs"
7882 variant="ghost"
7983 class="h-6 w-6 p-0 text-white/70 hover:text-white "
80- @click .stop =" openDemoWatcher "
84+ @click .stop =" openDemoWatcher () "
8185 >
8286 <PlayCircle class="w-4 h-4" />
8387 </Button >
8488 </TooltipTrigger >
8589 <TooltipContent >Watch demo</TooltipContent >
8690 </Tooltip >
91+ <Tooltip v-else-if =" hasDemo && canParseDemo " >
92+ <TooltipTrigger as-child>
93+ <Button
94+ size="xs"
95+ variant="ghost"
96+ class="h-6 w-6 p-0 text-white/70 hover:text-white disabled:opacity-50 disabled:cursor-not-allowed "
97+ :disabled =" isParsingDemo "
98+ @click .stop =" parseDemo ()"
99+ >
100+ <Loader2 v-if =" isParsingDemo " class="w-4 h-4 animate-spin" />
101+ <RefreshCw v-else class="w-4 h-4" />
102+ </Button >
103+ </TooltipTrigger >
104+ <TooltipContent >
105+ {{ isParsingDemo ? "Parsing demo…" : "Parse demo metadata" }}
106+ </TooltipContent >
107+ </Tooltip >
108+ <Tooltip v-else-if =" hasDemo && canWatchDemo " >
109+ <TooltipTrigger as-child>
110+ <Button
111+ size="xs"
112+ variant="ghost"
113+ class="h-6 w-6 p-0 text-white/70 opacity-50 cursor-not-allowed"
114+ disabled
115+ >
116+ <PlayCircle class="w-4 h-4" />
117+ </Button >
118+ </TooltipTrigger >
119+ <TooltipContent >Demo metadata has not been parsed</TooltipContent >
120+ </Tooltip >
87121 <template v-if =" matchMap .demos_download_url " >
88122 <a target =" _blank" :href =" matchMap.demos_download_url" @click.stop >
89123 <Button
@@ -203,7 +237,12 @@ import cleanMapName from "~/utilities/cleanMapName";
203237</template >
204238
205239<script lang="ts">
206- import { e_match_status_enum , e_veto_pick_types_enum } from " ~/generated/zeus" ;
240+ import {
241+ e_match_status_enum ,
242+ e_player_roles_enum ,
243+ e_veto_pick_types_enum ,
244+ } from " ~/generated/zeus" ;
245+ import { useAuthStore } from " ~/stores/AuthStore" ;
207246
208247export default {
209248 emits: [" open-stats" ],
@@ -221,6 +260,11 @@ export default {
221260 default: false ,
222261 },
223262 },
263+ data() {
264+ return {
265+ isParsingDemo: false ,
266+ };
267+ },
224268 computed: {
225269 canOpenStats() {
226270 if (this .matchMap .status === e_match_status_enum .Scheduled ) {
@@ -229,14 +273,25 @@ export default {
229273 return (this .match .options ?.best_of ?? 1 ) > 1 ;
230274 },
231275 hasDemo() {
232- // Either the per-map aggregate (demos_total_size > 0) or the
233- // demos relation has at least one row. Mirrors the v-if guards
234- // already used by the download button below.
235276 return (
236277 !! this .matchMap .demos_total_size ||
237278 (this .matchMap .demos ?.length ?? 0 ) > 0
238279 );
239280 },
281+ canWatchDemo() {
282+ return (
283+ this .match .is_organizer ||
284+ useAuthStore ().isRoleAbove (e_player_roles_enum .streamer )
285+ );
286+ },
287+ hasDemoMetadata() {
288+ return (this .matchMap .demos ?? []).some (
289+ (d ) => !! d .metadata_parsed_at && !! d .total_ticks ,
290+ );
291+ },
292+ canParseDemo() {
293+ return useAuthStore ().isAdmin ;
294+ },
240295 showTeamPatch() {
241296 return (
242297 ! this .isDecider || this .matchMap .status === e_match_status_enum .Live
@@ -268,6 +323,29 @@ export default {
268323 },
269324 },
270325 methods: {
326+ async parseDemo() {
327+ if (this .isParsingDemo ) return ;
328+ this .isParsingDemo = true ;
329+ try {
330+ await this .$apollo .mutate ({
331+ mutation: generateMutation ({
332+ reparseDemo: [
333+ { match_map_id: this .matchMap .id },
334+ { success: true },
335+ ],
336+ }),
337+ });
338+ toast ({ title: " Demo parsed" });
339+ } catch (error ) {
340+ toast ({
341+ title: " Failed to parse demo" ,
342+ description: (error as Error )?.message ,
343+ variant: " destructive" ,
344+ });
345+ } finally {
346+ this .isParsingDemo = false ;
347+ }
348+ },
271349 openDemoWatcher() {
272350 // Pop the demo watcher into a dedicated window. The popup owns
273351 // the session lifecycle: closing the window kills the session
0 commit comments