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 { useTitle } from "@vueuse/core";
|
||||
import { useClockSocket } from "@/composables/useClockSocket";
|
||||
import { useLiveBadge } from "@/composables/useLiveBadge";
|
||||
|
||||
interface Props {
|
||||
eventPath: string;
|
||||
@@ -25,6 +26,7 @@ const {
|
||||
cooldownDurationSeconds,
|
||||
press,
|
||||
} = useClockSocket(props.eventPath);
|
||||
useLiveBadge("clock", connected);
|
||||
|
||||
const remainingDisplay = computed(() => {
|
||||
const totalSeconds = Math.floor(remainingMs.value / 1000);
|
||||
@@ -38,10 +40,6 @@ const onCooldown = computed(() => cooldownRemainingMs.value > 0);
|
||||
|
||||
<template>
|
||||
<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">
|
||||
{{ remainingDisplay }}
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { computed, onMounted, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useTitle } from "@vueuse/core";
|
||||
import { usePlaceSocket } from "@/composables/usePlaceSocket";
|
||||
import { useLiveBadge } from "@/composables/useLiveBadge";
|
||||
import { PALETTE } from "@/palette";
|
||||
|
||||
interface Props {
|
||||
@@ -47,6 +48,7 @@ const {
|
||||
gridHeight,
|
||||
place,
|
||||
} = usePlaceSocket(props.eventPath, drawPixel);
|
||||
useLiveBadge("place", connected);
|
||||
|
||||
const overlayWidth = computed(() => gridWidth.value * PX_PER_CELL);
|
||||
const overlayHeight = computed(() => gridHeight.value * PX_PER_CELL);
|
||||
@@ -131,10 +133,6 @@ function handleClick(event: MouseEvent) {
|
||||
|
||||
<template>
|
||||
<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}` }">
|
||||
<canvas ref="pixelCanvasRef" :width="gridWidth" :height="gridHeight"
|
||||
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 { placeApiCanvas } from '@/api'
|
||||
|
||||
@@ -108,6 +108,15 @@ export function usePlaceSocket(eventPath: string, onPixel?: OnPixel, onResize?:
|
||||
|
||||
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() {
|
||||
const response = await placeApiCanvas({ path: { event_path: eventPath } })
|
||||
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 { usePageHeaderStore } from '@/stores/pageHeader'
|
||||
import { useLiveBadge } from '@/composables/useLiveBadge'
|
||||
|
||||
interface SubmissionUpdate {
|
||||
id: string
|
||||
@@ -108,9 +108,7 @@ export function useSubmissionsWebSocket({ userSubmissions, onScopeUpdate }: Opti
|
||||
},
|
||||
})
|
||||
|
||||
// Live badge in AppHeader (reset by the router on the next navigation)
|
||||
const pageHeaderStore = usePageHeaderStore()
|
||||
watch(isConnected, (connected) => pageHeaderStore.setLive(connected), { immediate: true })
|
||||
useLiveBadge('submissions', isConnected)
|
||||
|
||||
const handleSubmissionUpdate = (submission: SubmissionUpdate) => {
|
||||
const existingIndex = userSubmissions.value.findIndex((s: any) => s.id === submission.id)
|
||||
|
||||
@@ -8,10 +8,22 @@ import { usePageHeaderStore } from "@/stores/pageHeader";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
const { t, tm, rt } = useI18n();
|
||||
const userInfo = useUser();
|
||||
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(() => {
|
||||
if (dynamicTitle.value) return dynamicTitle.value;
|
||||
@@ -47,28 +59,16 @@ const goBack = () => {
|
||||
<i class="mdi mdi-information-outline ml-1 opacity-70"></i>
|
||||
</div>
|
||||
<div tabindex="0" class="dropdown-content card card-sm bg-base-200 shadow-xl z-50 w-80 mt-2">
|
||||
<div class="card-body">
|
||||
<template v-if="live">
|
||||
<div v-if="liveHelp" class="card-body">
|
||||
<h3 class="card-title text-base">
|
||||
<i class="mdi mdi-wifi text-success"></i>
|
||||
{{ t("liveStatus.liveTitle") }}
|
||||
<i :class="['mdi', live ? 'mdi-wifi text-success' : 'mdi-wifi-off text-warning']"></i>
|
||||
{{ liveHelp.title }}
|
||||
</h3>
|
||||
<p>{{ t("liveStatus.liveIntro") }}</p>
|
||||
<ul class="list-disc pl-5 space-y-1">
|
||||
<li>{{ t("liveStatus.liveSubmissions") }}</li>
|
||||
<li>{{ t("liveStatus.liveMatches") }}</li>
|
||||
<p>{{ liveHelp.intro }}</p>
|
||||
<ul v-if="liveHelp.points.length" class="list-disc pl-5 space-y-1">
|
||||
<li v-for="point in liveHelp.points" :key="point">{{ point }}</li>
|
||||
</ul>
|
||||
<p class="text-base-content/70">{{ t("liveStatus.liveNoRefresh") }}</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>
|
||||
<p class="text-base-content/70">{{ liveHelp.outro }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -80,8 +80,6 @@
|
||||
"clock": {
|
||||
"pageTitle": "Beat the Clock",
|
||||
"header": "Beat the Clock",
|
||||
"connected": "connected",
|
||||
"reconnecting": "reconnecting…",
|
||||
"press": "PRESS",
|
||||
"nextPlayer": "▲ next player",
|
||||
"chasingYou": "▼ chasing you",
|
||||
@@ -90,8 +88,6 @@
|
||||
"place": {
|
||||
"pageTitle": "Place",
|
||||
"header": "Place",
|
||||
"connected": "connected",
|
||||
"reconnecting": "reconnecting…",
|
||||
"showGrid": "Show grid",
|
||||
"hideGrid": "Hide grid"
|
||||
},
|
||||
@@ -261,15 +257,66 @@
|
||||
"stopFailed": "Failed to stop the sync."
|
||||
},
|
||||
"liveStatus": {
|
||||
"liveTitle": "You're connected live",
|
||||
"liveIntro": "This page updates on its own, you don't need to refresh it:",
|
||||
"liveSubmissions": "Your submissions update as they're processed (uploaded files, text read from your screenshots).",
|
||||
"liveMatches": "When your match ends, you're sent back to the match list, and straight into your next match if you have only one.",
|
||||
"liveNoRefresh": "Just keep this page open between matches.",
|
||||
"offlineTitle": "Live connection lost",
|
||||
"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.",
|
||||
"offlineReconnect": "It reconnects automatically, usually within a few seconds.",
|
||||
"offlineRefresh": "Still offline after a while? Refresh the page. Anything you already submitted is saved."
|
||||
"submissions": {
|
||||
"live": {
|
||||
"title": "You're connected live",
|
||||
"intro": "This page updates on its own, you don't need to refresh it:",
|
||||
"points": [
|
||||
"Your submissions update as they're processed (uploaded files, text read from your screenshots).",
|
||||
"When your match ends, you're sent back to the match list, and straight into your next match if you have only one."
|
||||
],
|
||||
"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": {
|
||||
"notFound": {
|
||||
|
||||
@@ -2,6 +2,9 @@ import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
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
|
||||
// has fetched (e.g. "Noita Submissions — Semifinal", assembled from an API
|
||||
// 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
|
||||
// when the page has no live connection (no badge at all).
|
||||
const live = ref<boolean | null>(null);
|
||||
const liveKind = ref<LiveKind | null>(null);
|
||||
|
||||
const setTitle = (value: string) => {
|
||||
title.value = value;
|
||||
@@ -23,15 +27,17 @@ export const usePageHeaderStore = defineStore("pageHeader", () => {
|
||||
backRoute.value = value;
|
||||
};
|
||||
|
||||
const setLive = (value: boolean | null) => {
|
||||
const setLive = (value: boolean, kind: LiveKind) => {
|
||||
live.value = value;
|
||||
liveKind.value = kind;
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
title.value = "";
|
||||
backRoute.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