diff --git a/assets/css/tailwind.css b/assets/css/tailwind.css index af5b092d2..cc35f0bf4 100644 --- a/assets/css/tailwind.css +++ b/assets/css/tailwind.css @@ -351,6 +351,37 @@ mark { transparent 4px ); } +/* Scan sweep across the top of the queue / match-confirm panels. The band is a + repeating gradient whose period is one track width, painted on a double-width + element, so shifting exactly one period (-50%) loops with no seam: the next + band enters the left edge as the previous one leaves the right. Colour is + currentColor, so callers pick it with text-*. */ +@keyframes tac-scan-sweep { + from { + transform: translateX(-50%); + } + to { + transform: translateX(0); + } +} +.tac-scan-sweep { + width: 200%; + background-image: repeating-linear-gradient( + 90deg, + transparent 0, + transparent 12.5%, + currentColor 25%, + transparent 37.5%, + transparent 50% + ); + animation: tac-scan-sweep 2s linear infinite; +} +@media (prefers-reduced-motion: reduce) { + .tac-scan-sweep { + animation: none; + } +} + /* One-shot pulse for clip share buttons — toast is far from the click target, so the button itself flashes amber + scales briefly. Used by MatchHighlightsReel queue/featured share, HighlightCard, and diff --git a/components/MatchOptions.vue b/components/MatchOptions.vue index 2333bd925..847627672 100644 --- a/components/MatchOptions.vue +++ b/components/MatchOptions.vue @@ -807,6 +807,64 @@ import SettingHeader from "~/components/match/SettingHeader.vue"; + + +
+
+ {{ + $t("match.options.advanced.veto_pick_timeout.label") + }} + {{ + $t( + "match.options.advanced.veto_pick_timeout.description", + ) + }} +
+ + + +
+ +
+ + + + + + + + + + + {{ + $t("match.options.advanced.veto_pick_timeout.range") + }} + +
+ +
+
+ {{ @@ -832,7 +890,9 @@ import SettingHeader from "~/components/match/SettingHeader.vue"; - {{ $t("match.options.advanced.round_restart_delay.range") }} + {{ + $t("match.options.advanced.round_restart_delay.range") + }} @@ -1175,6 +1235,7 @@ import { import { mapFields } from "~/graphql/mapGraphql"; import { useApplicationSettingsStore } from "~/stores/ApplicationSettings"; import { useAuthStore } from "~/stores/AuthStore"; +import { canSetVetoPickTimeout } from "~/utilities/setupOptions"; interface Map { id: string; @@ -1294,6 +1355,7 @@ export default { select_single_region: null as string | null, showAdvancedSettings: false, advHeight: "0px", + lastVetoPickTimeout: 0, }; }, watch: { @@ -1634,6 +1696,9 @@ export default { canSetcheckInSettings() { return useAuthStore().isRoleAbove(e_player_roles_enum.match_organizer); }, + canSetVetoPickTimeout() { + return canSetVetoPickTimeout(); + }, canSetMatchCancellation() { return useAuthStore().isRoleAbove( e_player_roles_enum.tournament_organizer, @@ -1651,8 +1716,32 @@ export default { )?.value; return val || "180"; }, + vetoPickTimeoutDefault(): number { + const val = Number.parseInt( + useApplicationSettingsStore().settings.find( + (s) => s.name === "public.veto_pick_timeout", + )?.value ?? "", + 10, + ); + return Number.isNaN(val) || val <= 0 ? 60 : val; + }, }, methods: { + toggleVetoTimer(enabled: boolean) { + if (!enabled) { + const current = this.form.values.veto_pick_timeout; + if (current > 0) { + this.lastVetoPickTimeout = current; + } + this.form.setFieldValue("veto_pick_timeout", 0); + return; + } + + this.form.setFieldValue( + "veto_pick_timeout", + this.lastVetoPickTimeout || this.vetoPickTimeoutDefault, + ); + }, animateAdvanced(open: boolean) { const wrap = this.$refs.advWrapRef as HTMLElement | undefined; if (!wrap) { diff --git a/components/draft-games/DraftClock.vue b/components/draft-games/DraftClock.vue index 1bb168f6a..2326b8ad6 100644 --- a/components/draft-games/DraftClock.vue +++ b/components/draft-games/DraftClock.vue @@ -1,5 +1,6 @@ diff --git a/components/match/MatchMapVeto.vue b/components/match/MatchMapVeto.vue index dfebd4168..dd590707e 100644 --- a/components/match/MatchMapVeto.vue +++ b/components/match/MatchMapVeto.vue @@ -1,12 +1,12 @@ @@ -735,7 +742,8 @@ async function stopGpuSession(nodeId: string) { > {{ $t("pages.gpu_nodes.steam_pool.accounts_label") }} - {{ steamAccounts.length }}/{{ gpuNodes.length }} + {{ freeSteamAccounts }}/{{ steamAccounts.length }} + {{ $t("pages.gpu_nodes.stat.available") }}
(account.claims?.length ?? 0) === 0, + ).length; + }, }, methods: { // Compact header chip for the active (post-boot) render phase only — diff --git a/pages/matches/[id]/index.vue b/pages/matches/[id]/index.vue index afb84eff2..ad1d4d901 100644 --- a/pages/matches/[id]/index.vue +++ b/pages/matches/[id]/index.vue @@ -671,6 +671,7 @@ export default { map_veto_type: true, map_veto_picking_lineup_id: true, region_veto_picking_lineup_id: true, + veto_pick_expires_at: true, connection_link: true, connection_string: true, tv_connection_string: true, diff --git a/pages/settings/application/matchmaking.vue b/pages/settings/application/matchmaking.vue index 110d4ad65..f1bb462f8 100644 --- a/pages/settings/application/matchmaking.vue +++ b/pages/settings/application/matchmaking.vue @@ -151,6 +151,32 @@ import SettingsSaveBar from "~/components/settings/SettingsSaveBar.vue"; + + + + + {{ $t("pages.settings.application.veto_pick_timeout") }} + (sec) + + {{ + $t( + "pages.settings.application.veto_pick_timeout_description", + ) + }} + + + + + +
@@ -320,6 +346,7 @@ export default { .string() .default(e_player_roles_enum.user), max_acceptable_latency: z.number().default(100), + veto_pick_timeout: z.number().min(0).max(600).default(60), }), }), ), @@ -331,7 +358,18 @@ export default { immediate: true, handler(newVal: Array<{ name: string; value: string | null }>) { for (const setting of newVal) { - if ( + if (setting.name === "public.veto_pick_timeout") { + // Kept out of the branch below: 0 is a meaningful value here (it + // disables the veto timer) and `|| 100` would swallow it. + const vetoPickTimeout = Number(setting.value); + + (this.form.setFieldValue as any)( + setting.name, + Number.isFinite(vetoPickTimeout) && vetoPickTimeout >= 0 + ? vetoPickTimeout + : 60, + ); + } else if ( setting.name === "public.max_acceptable_latency" || setting.name === "auto_cancel_duration" || setting.name === "live_match_timeout" @@ -503,6 +541,12 @@ export default { name: "live_match_timeout", value: String((this.form.values as any).live_match_timeout), }, + { + name: "public.veto_pick_timeout", + value: String( + (this.form.values as any).public.veto_pick_timeout, + ), + }, ], on_conflict: { constraint: settings_constraint.settings_pkey, diff --git a/stores/DraftGamesStore.ts b/stores/DraftGamesStore.ts index 0fe3f37ec..faeb6cb25 100644 --- a/stores/DraftGamesStore.ts +++ b/stores/DraftGamesStore.ts @@ -275,6 +275,7 @@ export const useDraftGamesStore = defineStore("draft-games", () => { map_veto_type: true, map_veto_picking_lineup_id: true, region_veto_picking_lineup_id: true, + veto_pick_expires_at: true, region_veto_picks: { type: true, region: true, @@ -289,6 +290,7 @@ export const useDraftGamesStore = defineStore("draft-games", () => { regions: true, coaches: true, check_in_setting: true, + veto_pick_timeout: true, type: true, map_pool: { id: true, diff --git a/tailwind.config.js b/tailwind.config.js index f3420e531..7832c39e6 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -139,10 +139,6 @@ module.exports = { from: { strokeDashoffset: "0" }, to: { strokeDashoffset: "calc(var(--flow-length) * -1px)" }, }, - "loading-bar": { - "0%": { transform: "translateX(-100%)" }, - "100%": { transform: "translateX(100%)" }, - }, "fade-in": { from: { opacity: "0", transform: "translateY(5px)" }, to: { opacity: "1", transform: "translateY(0)" }, @@ -174,7 +170,6 @@ module.exports = { "caret-blink": "caret-blink 1.25s ease-out infinite", "spin-smooth": "spin-smooth 1s cubic-bezier(0.4, 0, 0.2, 1) infinite", "topo-flow": "topo-flow linear infinite", - "loading-bar": "loading-bar 2s infinite", "fade-in": "fade-in 0.3s ease-out", "soft-pulse": "soft-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite", "ping-slow": "ping-slow 1.8s cubic-bezier(0.4, 0, 0.6, 1) infinite", diff --git a/utilities/match-options-validator.ts b/utilities/match-options-validator.ts index 0d3c5a2e6..c7889a781 100644 --- a/utilities/match-options-validator.ts +++ b/utilities/match-options-validator.ts @@ -16,6 +16,12 @@ export default function matchOptionsValidator( settings.find((setting) => setting.name === "public.default_models") ?.value === "true"; + const defaultVetoPickTimeout = Number.parseInt( + settings.find((setting) => setting.name === "public.veto_pick_timeout") + ?.value ?? "", + 10, + ); + return z.object({ mr: z.string().default("12"), map_veto: z.boolean().default(true), @@ -24,6 +30,15 @@ export default function matchOptionsValidator( lan: z.boolean().default(false), coaches: z.boolean().default(false), tv_delay: z.number().min(0).max(120).default(115), + veto_pick_timeout: z + .number() + .min(0) + .max(600) + .default( + Number.isNaN(defaultVetoPickTimeout) || defaultVetoPickTimeout < 0 + ? 60 + : defaultVetoPickTimeout, + ), round_restart_delay: z.number().min(0).max(60).nullable().optional(), halftime_pausematch: z.boolean().default(false), knife_round: z.boolean().default(true), diff --git a/utilities/setupOptions.ts b/utilities/setupOptions.ts index 0f3f004e0..255150191 100644 --- a/utilities/setupOptions.ts +++ b/utilities/setupOptions.ts @@ -5,6 +5,13 @@ import { } from "~/generated/zeus"; import { type FormContext } from "vee-validate"; +// Mirrors the match_options column permission: only match_organizer and above +// may write veto_pick_timeout, so anyone below has to fall through to the +// platform default instead of sending a column Hasura will reject. +export function canSetVetoPickTimeout(): boolean { + return useAuthStore().isRoleAbove(e_player_roles_enum.match_organizer); +} + export const setupOptions = ( form: FormContext, options: any, @@ -36,6 +43,7 @@ export const setupOptions = ( map_pool_id: options.map_pool.id, regions: selectedRegions, tv_delay: options.tv_delay, + veto_pick_timeout: options.veto_pick_timeout ?? 60, round_restart_delay: options.round_restart_delay ?? null, halftime_pausematch: options.halftime_pausematch ?? false, check_in_setting: options.check_in_setting, @@ -71,6 +79,7 @@ export function setupOptionsVariables( match_mode: string; map_pool_id?: string; tv_delay: number; + veto_pick_timeout: number; round_restart_delay?: number | null; halftime_pausematch?: boolean; map_pool?: { @@ -186,6 +195,11 @@ export function setupOptionsVariables( tv_delay: values.tv_delay, round_restart_delay: values.round_restart_delay ?? null, halftime_pausematch: values.halftime_pausematch ?? false, + ...(canSetVetoPickTimeout() + ? { + veto_pick_timeout: values.veto_pick_timeout, + } + : {}), ...(useAuthStore().isRoleAbove(e_player_roles_enum.tournament_organizer) ? { check_in_setting: values.check_in_setting, @@ -240,6 +254,11 @@ export function setupOptionsSetMutation(hasMapPoolId: boolean = true) { tv_delay: $("tv_delay", "Int!"), round_restart_delay: $("round_restart_delay", "Int"), halftime_pausematch: $("halftime_pausematch", "Boolean!"), + ...(canSetVetoPickTimeout() + ? { + veto_pick_timeout: $("veto_pick_timeout", "Int!"), + } + : {}), ...(useAuthStore().isRoleAbove(e_player_roles_enum.tournament_organizer) ? { check_in_setting: $("check_in_setting", "e_check_in_settings_enum!"),