fix: move other (clock+place) live icon to top ehader too
This commit is contained in:
@@ -3,6 +3,7 @@ import { computed } from "vue";
|
|||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { useTitle } from "@vueuse/core";
|
import { useTitle } from "@vueuse/core";
|
||||||
import { useClockSocket } from "@/composables/useClockSocket";
|
import { useClockSocket } from "@/composables/useClockSocket";
|
||||||
|
import { useLiveBadge } from "@/composables/useLiveBadge";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
eventPath: string;
|
eventPath: string;
|
||||||
@@ -25,6 +26,7 @@ const {
|
|||||||
cooldownDurationSeconds,
|
cooldownDurationSeconds,
|
||||||
press,
|
press,
|
||||||
} = useClockSocket(props.eventPath);
|
} = useClockSocket(props.eventPath);
|
||||||
|
useLiveBadge("clock", connected);
|
||||||
|
|
||||||
const remainingDisplay = computed(() => {
|
const remainingDisplay = computed(() => {
|
||||||
const totalSeconds = Math.floor(remainingMs.value / 1000);
|
const totalSeconds = Math.floor(remainingMs.value / 1000);
|
||||||
@@ -38,10 +40,6 @@ const onCooldown = computed(() => cooldownRemainingMs.value > 0);
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex-1 bg-base-200 flex flex-col items-center px-4 py-8 gap-8">
|
<div class="flex-1 bg-base-200 flex flex-col items-center px-4 py-8 gap-8">
|
||||||
<span class="badge" :class="connected ? 'badge-success' : 'badge-error'">
|
|
||||||
{{ connected ? t("clock.connected") : t("clock.reconnecting") }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div class="font-bold text-7xl tabular-nums">
|
<div class="font-bold text-7xl tabular-nums">
|
||||||
{{ remainingDisplay }}
|
{{ remainingDisplay }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { computed, onMounted, ref, watch } from "vue";
|
|||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { useTitle } from "@vueuse/core";
|
import { useTitle } from "@vueuse/core";
|
||||||
import { usePlaceSocket } from "@/composables/usePlaceSocket";
|
import { usePlaceSocket } from "@/composables/usePlaceSocket";
|
||||||
|
import { useLiveBadge } from "@/composables/useLiveBadge";
|
||||||
import { PALETTE } from "@/palette";
|
import { PALETTE } from "@/palette";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -47,6 +48,7 @@ const {
|
|||||||
gridHeight,
|
gridHeight,
|
||||||
place,
|
place,
|
||||||
} = usePlaceSocket(props.eventPath, drawPixel);
|
} = usePlaceSocket(props.eventPath, drawPixel);
|
||||||
|
useLiveBadge("place", connected);
|
||||||
|
|
||||||
const overlayWidth = computed(() => gridWidth.value * PX_PER_CELL);
|
const overlayWidth = computed(() => gridWidth.value * PX_PER_CELL);
|
||||||
const overlayHeight = computed(() => gridHeight.value * PX_PER_CELL);
|
const overlayHeight = computed(() => gridHeight.value * PX_PER_CELL);
|
||||||
@@ -131,10 +133,6 @@ function handleClick(event: MouseEvent) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex-1 bg-base-200 flex flex-col items-center px-4 py-8 gap-6">
|
<div class="flex-1 bg-base-200 flex flex-col items-center px-4 py-8 gap-6">
|
||||||
<span class="badge" :class="connected ? 'badge-success' : 'badge-error'">
|
|
||||||
{{ connected ? t("place.connected") : t("place.reconnecting") }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div class="relative w-full max-w-[512px]" :style="{ aspectRatio: `${gridWidth} / ${gridHeight}` }">
|
<div class="relative w-full max-w-[512px]" :style="{ aspectRatio: `${gridWidth} / ${gridHeight}` }">
|
||||||
<canvas ref="pixelCanvasRef" :width="gridWidth" :height="gridHeight"
|
<canvas ref="pixelCanvasRef" :width="gridWidth" :height="gridHeight"
|
||||||
class="absolute inset-0 w-full h-full border border-base-300 rounded-box shadow-lg" :style="{
|
class="absolute inset-0 w-full h-full border border-base-300 rounded-box shadow-lg" :style="{
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { watch, type Ref } from "vue";
|
||||||
|
import { usePageHeaderStore, type LiveKind } from "@/stores/pageHeader";
|
||||||
|
|
||||||
|
// Shows the page's live (websocket) state as a badge mid-header, with a
|
||||||
|
// popover explaining what it means on this page (liveStatus.<kind> strings).
|
||||||
|
// Reset by the router on the next navigation.
|
||||||
|
export const useLiveBadge = (kind: LiveKind, connected: Ref<boolean>) => {
|
||||||
|
const pageHeaderStore = usePageHeaderStore();
|
||||||
|
watch(connected, (value) => pageHeaderStore.setLive(value, kind), { immediate: true });
|
||||||
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import { useWebSocket } from '@vueuse/core'
|
import { useWebSocket } from '@vueuse/core'
|
||||||
import { placeApiCanvas } from '@/api'
|
import { placeApiCanvas } from '@/api'
|
||||||
|
|
||||||
@@ -108,6 +108,15 @@ export function usePlaceSocket(eventPath: string, onPixel?: OnPixel, onResize?:
|
|||||||
|
|
||||||
const connected = computed(() => status.value === 'OPEN')
|
const connected = computed(() => status.value === 'OPEN')
|
||||||
|
|
||||||
|
// Pixels placed while disconnected never reach us: catch up by reloading
|
||||||
|
// the whole canvas on every reconnect (the first load is done on mount).
|
||||||
|
let hasConnected = false
|
||||||
|
watch(connected, (isConnected) => {
|
||||||
|
if (!isConnected) return
|
||||||
|
if (hasConnected) loadCanvas()
|
||||||
|
hasConnected = true
|
||||||
|
})
|
||||||
|
|
||||||
async function loadCanvas() {
|
async function loadCanvas() {
|
||||||
const response = await placeApiCanvas({ path: { event_path: eventPath } })
|
const response = await placeApiCanvas({ path: { event_path: eventPath } })
|
||||||
if (!response.data) return
|
if (!response.data) return
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { useWebSocket } from '@vueuse/core'
|
import { useWebSocket } from '@vueuse/core'
|
||||||
import { usePageHeaderStore } from '@/stores/pageHeader'
|
import { useLiveBadge } from '@/composables/useLiveBadge'
|
||||||
|
|
||||||
interface SubmissionUpdate {
|
interface SubmissionUpdate {
|
||||||
id: string
|
id: string
|
||||||
@@ -108,9 +108,7 @@ export function useSubmissionsWebSocket({ userSubmissions, onScopeUpdate }: Opti
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Live badge in AppHeader (reset by the router on the next navigation)
|
useLiveBadge('submissions', isConnected)
|
||||||
const pageHeaderStore = usePageHeaderStore()
|
|
||||||
watch(isConnected, (connected) => pageHeaderStore.setLive(connected), { immediate: true })
|
|
||||||
|
|
||||||
const handleSubmissionUpdate = (submission: SubmissionUpdate) => {
|
const handleSubmissionUpdate = (submission: SubmissionUpdate) => {
|
||||||
const existingIndex = userSubmissions.value.findIndex((s: any) => s.id === submission.id)
|
const existingIndex = userSubmissions.value.findIndex((s: any) => s.id === submission.id)
|
||||||
|
|||||||
@@ -8,10 +8,22 @@ import { usePageHeaderStore } from "@/stores/pageHeader";
|
|||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t } = useI18n();
|
const { t, tm, rt } = useI18n();
|
||||||
const userInfo = useUser();
|
const userInfo = useUser();
|
||||||
const pageHeaderStore = usePageHeaderStore();
|
const pageHeaderStore = usePageHeaderStore();
|
||||||
const { title: dynamicTitle, backRoute: dynamicBackRoute, live } = storeToRefs(pageHeaderStore);
|
const { title: dynamicTitle, backRoute: dynamicBackRoute, live, liveKind } = storeToRefs(pageHeaderStore);
|
||||||
|
|
||||||
|
// Popover text of the live badge, per page (liveStatus.<kind>.live/offline)
|
||||||
|
const liveHelp = computed(() => {
|
||||||
|
if (!liveKind.value) return null;
|
||||||
|
const key = `liveStatus.${liveKind.value}.${live.value ? "live" : "offline"}`;
|
||||||
|
return {
|
||||||
|
title: t(`${key}.title`),
|
||||||
|
intro: t(`${key}.intro`),
|
||||||
|
points: (tm(`${key}.points`) as unknown[]).map((point) => rt(point as Parameters<typeof rt>[0])),
|
||||||
|
outro: t(`${key}.outro`),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const title = computed(() => {
|
const title = computed(() => {
|
||||||
if (dynamicTitle.value) return dynamicTitle.value;
|
if (dynamicTitle.value) return dynamicTitle.value;
|
||||||
@@ -47,28 +59,16 @@ const goBack = () => {
|
|||||||
<i class="mdi mdi-information-outline ml-1 opacity-70"></i>
|
<i class="mdi mdi-information-outline ml-1 opacity-70"></i>
|
||||||
</div>
|
</div>
|
||||||
<div tabindex="0" class="dropdown-content card card-sm bg-base-200 shadow-xl z-50 w-80 mt-2">
|
<div tabindex="0" class="dropdown-content card card-sm bg-base-200 shadow-xl z-50 w-80 mt-2">
|
||||||
<div class="card-body">
|
<div v-if="liveHelp" class="card-body">
|
||||||
<template v-if="live">
|
|
||||||
<h3 class="card-title text-base">
|
<h3 class="card-title text-base">
|
||||||
<i class="mdi mdi-wifi text-success"></i>
|
<i :class="['mdi', live ? 'mdi-wifi text-success' : 'mdi-wifi-off text-warning']"></i>
|
||||||
{{ t("liveStatus.liveTitle") }}
|
{{ liveHelp.title }}
|
||||||
</h3>
|
</h3>
|
||||||
<p>{{ t("liveStatus.liveIntro") }}</p>
|
<p>{{ liveHelp.intro }}</p>
|
||||||
<ul class="list-disc pl-5 space-y-1">
|
<ul v-if="liveHelp.points.length" class="list-disc pl-5 space-y-1">
|
||||||
<li>{{ t("liveStatus.liveSubmissions") }}</li>
|
<li v-for="point in liveHelp.points" :key="point">{{ point }}</li>
|
||||||
<li>{{ t("liveStatus.liveMatches") }}</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
<p class="text-base-content/70">{{ t("liveStatus.liveNoRefresh") }}</p>
|
<p class="text-base-content/70">{{ liveHelp.outro }}</p>
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<h3 class="card-title text-base">
|
|
||||||
<i class="mdi mdi-wifi-off text-warning"></i>
|
|
||||||
{{ t("liveStatus.offlineTitle") }}
|
|
||||||
</h3>
|
|
||||||
<p>{{ t("liveStatus.offlineIntro") }}</p>
|
|
||||||
<p>{{ t("liveStatus.offlineReconnect") }}</p>
|
|
||||||
<p class="text-base-content/70">{{ t("liveStatus.offlineRefresh") }}</p>
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -80,8 +80,6 @@
|
|||||||
"clock": {
|
"clock": {
|
||||||
"pageTitle": "Beat the Clock",
|
"pageTitle": "Beat the Clock",
|
||||||
"header": "Beat the Clock",
|
"header": "Beat the Clock",
|
||||||
"connected": "connected",
|
|
||||||
"reconnecting": "reconnecting…",
|
|
||||||
"press": "PRESS",
|
"press": "PRESS",
|
||||||
"nextPlayer": "▲ next player",
|
"nextPlayer": "▲ next player",
|
||||||
"chasingYou": "▼ chasing you",
|
"chasingYou": "▼ chasing you",
|
||||||
@@ -90,8 +88,6 @@
|
|||||||
"place": {
|
"place": {
|
||||||
"pageTitle": "Place",
|
"pageTitle": "Place",
|
||||||
"header": "Place",
|
"header": "Place",
|
||||||
"connected": "connected",
|
|
||||||
"reconnecting": "reconnecting…",
|
|
||||||
"showGrid": "Show grid",
|
"showGrid": "Show grid",
|
||||||
"hideGrid": "Hide grid"
|
"hideGrid": "Hide grid"
|
||||||
},
|
},
|
||||||
@@ -261,15 +257,66 @@
|
|||||||
"stopFailed": "Failed to stop the sync."
|
"stopFailed": "Failed to stop the sync."
|
||||||
},
|
},
|
||||||
"liveStatus": {
|
"liveStatus": {
|
||||||
"liveTitle": "You're connected live",
|
"submissions": {
|
||||||
"liveIntro": "This page updates on its own, you don't need to refresh it:",
|
"live": {
|
||||||
"liveSubmissions": "Your submissions update as they're processed (uploaded files, text read from your screenshots).",
|
"title": "You're connected live",
|
||||||
"liveMatches": "When your match ends, you're sent back to the match list, and straight into your next match if you have only one.",
|
"intro": "This page updates on its own, you don't need to refresh it:",
|
||||||
"liveNoRefresh": "Just keep this page open between matches.",
|
"points": [
|
||||||
"offlineTitle": "Live connection lost",
|
"Your submissions update as they're processed (uploaded files, text read from your screenshots).",
|
||||||
"offlineIntro": "The page can't get updates right now, so it won't move you to your next match or show processing results on its own.",
|
"When your match ends, you're sent back to the match list, and straight into your next match if you have only one."
|
||||||
"offlineReconnect": "It reconnects automatically, usually within a few seconds.",
|
],
|
||||||
"offlineRefresh": "Still offline after a while? Refresh the page. Anything you already submitted is saved."
|
"outro": "Just keep this page open between matches."
|
||||||
|
},
|
||||||
|
"offline": {
|
||||||
|
"title": "Live connection lost",
|
||||||
|
"intro": "The page can't get updates right now:",
|
||||||
|
"points": [
|
||||||
|
"It won't move you to your next match on its own.",
|
||||||
|
"Processing results of your submissions won't show up by themselves."
|
||||||
|
],
|
||||||
|
"outro": "It reconnects automatically, usually within a few seconds. Still offline after a while? Refresh the page, anything you already submitted is saved."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"clock": {
|
||||||
|
"live": {
|
||||||
|
"title": "You're connected live",
|
||||||
|
"intro": "Everything here is real time, shared with every player:",
|
||||||
|
"points": [
|
||||||
|
"The countdown is the same for everyone, and moves as soon as someone presses.",
|
||||||
|
"Your points and rank update right after each of your presses."
|
||||||
|
],
|
||||||
|
"outro": "Presses go through this live connection, keep it green to play."
|
||||||
|
},
|
||||||
|
"offline": {
|
||||||
|
"title": "Live connection lost",
|
||||||
|
"intro": "You can't play right now:",
|
||||||
|
"points": [
|
||||||
|
"Your presses can't reach the server.",
|
||||||
|
"The countdown shown may be off, other players' presses aren't coming in."
|
||||||
|
],
|
||||||
|
"outro": "It reconnects automatically within a second or so, and your stats are refreshed when it does. Still offline? Refresh the page."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"place": {
|
||||||
|
"live": {
|
||||||
|
"title": "You're connected live",
|
||||||
|
"intro": "The canvas is shared with every player, in real time:",
|
||||||
|
"points": [
|
||||||
|
"Pixels placed by others show up as soon as they're placed.",
|
||||||
|
"Your pixels go through this live connection, then the cooldown starts."
|
||||||
|
],
|
||||||
|
"outro": "Just keep this page open, no refresh needed."
|
||||||
|
},
|
||||||
|
"offline": {
|
||||||
|
"title": "Live connection lost",
|
||||||
|
"intro": "You can't place pixels right now:",
|
||||||
|
"points": [
|
||||||
|
"Your clicks can't reach the server.",
|
||||||
|
"Pixels placed by others aren't coming in, the canvas may be out of date."
|
||||||
|
],
|
||||||
|
"outro": "It reconnects automatically within a second or so, and reloads the whole canvas to catch up. Still offline? Refresh the page."
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"errorPage": {
|
"errorPage": {
|
||||||
"notFound": {
|
"notFound": {
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import { defineStore } from "pinia";
|
|||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import type { RouteLocationRaw } from "vue-router";
|
import type { RouteLocationRaw } from "vue-router";
|
||||||
|
|
||||||
|
// Which page's explanation the live badge popover shows (liveStatus.<kind>)
|
||||||
|
export type LiveKind = "submissions" | "clock" | "place";
|
||||||
|
|
||||||
// Lets a routed page hand AppHeader a title built from data only that page
|
// Lets a routed page hand AppHeader a title built from data only that page
|
||||||
// has fetched (e.g. "Noita Submissions — Semifinal", assembled from an API
|
// has fetched (e.g. "Noita Submissions — Semifinal", assembled from an API
|
||||||
// call keyed by the scopeId route param). Cleared on every navigation so a
|
// call keyed by the scopeId route param). Cleared on every navigation so a
|
||||||
@@ -14,6 +17,7 @@ export const usePageHeaderStore = defineStore("pageHeader", () => {
|
|||||||
// Live (websocket) connection state of the page, shown mid-header; null
|
// Live (websocket) connection state of the page, shown mid-header; null
|
||||||
// when the page has no live connection (no badge at all).
|
// when the page has no live connection (no badge at all).
|
||||||
const live = ref<boolean | null>(null);
|
const live = ref<boolean | null>(null);
|
||||||
|
const liveKind = ref<LiveKind | null>(null);
|
||||||
|
|
||||||
const setTitle = (value: string) => {
|
const setTitle = (value: string) => {
|
||||||
title.value = value;
|
title.value = value;
|
||||||
@@ -23,15 +27,17 @@ export const usePageHeaderStore = defineStore("pageHeader", () => {
|
|||||||
backRoute.value = value;
|
backRoute.value = value;
|
||||||
};
|
};
|
||||||
|
|
||||||
const setLive = (value: boolean | null) => {
|
const setLive = (value: boolean, kind: LiveKind) => {
|
||||||
live.value = value;
|
live.value = value;
|
||||||
|
liveKind.value = kind;
|
||||||
};
|
};
|
||||||
|
|
||||||
const clear = () => {
|
const clear = () => {
|
||||||
title.value = "";
|
title.value = "";
|
||||||
backRoute.value = null;
|
backRoute.value = null;
|
||||||
live.value = null;
|
live.value = null;
|
||||||
|
liveKind.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
return { title, backRoute, live, setTitle, setBackRoute, setLive, clear };
|
return { title, backRoute, live, liveKind, setTitle, setBackRoute, setLive, clear };
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user