feat: add embed for place game
This commit is contained in:
@@ -75,6 +75,18 @@ def place_home(request: HttpRequest, event_path: str):
|
|||||||
return render(request, "app.html")
|
return render(request, "app.html")
|
||||||
|
|
||||||
|
|
||||||
|
# Deliberately not behind @login_required: this is meant to be dropped into
|
||||||
|
# an <iframe> (an OBS browser source, a stream overlay) that can't do the
|
||||||
|
# CAS SSO login flow. It's read-only anyway - placing a pixel still requires
|
||||||
|
# a logged-in websocket connection (see place/consumers.py) - so the only
|
||||||
|
# thing this exposes to anonymous viewers is the live canvas itself, same as
|
||||||
|
# watching the non-embed page already allows.
|
||||||
|
@require_game_enabled(path="place")
|
||||||
|
def place_embed(request: HttpRequest, event_path: str):
|
||||||
|
get_object_or_404(Event, path=event_path)
|
||||||
|
return render(request, "app.html")
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
path("cas/login/", SimpleCASLoginView.as_view(), name="cas_ng_login"),
|
path("cas/login/", SimpleCASLoginView.as_view(), name="cas_ng_login"),
|
||||||
@@ -85,6 +97,7 @@ urlpatterns = [
|
|||||||
path("<slug:event_path>/noita", noita_home, name="noita.home"),
|
path("<slug:event_path>/noita", noita_home, name="noita.home"),
|
||||||
path("<slug:event_path>/clock", clock_home, name="clock.home"),
|
path("<slug:event_path>/clock", clock_home, name="clock.home"),
|
||||||
path("<slug:event_path>/place", place_home, name="place.home"),
|
path("<slug:event_path>/place", place_home, name="place.home"),
|
||||||
|
path("<slug:event_path>/place/embed", place_embed, name="place.embed"),
|
||||||
path(
|
path(
|
||||||
"<slug:event_path>/submissions/<slug:game_path>",
|
"<slug:event_path>/submissions/<slug:game_path>",
|
||||||
submission_home,
|
submission_home,
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted } from "vue";
|
import { computed, onMounted } from "vue";
|
||||||
import { RouterView } from "vue-router";
|
import { RouterView, useRoute } from "vue-router";
|
||||||
import AppHeader from "@/layout/AppHeader.vue";
|
import AppHeader from "@/layout/AppHeader.vue";
|
||||||
import AppFooter from "@/layout/AppFooter.vue";
|
import AppFooter from "@/layout/AppFooter.vue";
|
||||||
import { useUserStore } from "@/stores/user";
|
import { useUserStore } from "@/stores/user";
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
const route = useRoute();
|
||||||
|
// Embed routes (e.g. place/embed) are meant to be dropped into an <iframe>
|
||||||
|
// as-is - the header/footer chrome would just eat into the fixed space a
|
||||||
|
// stream overlay or OBS browser source gives it.
|
||||||
|
const isEmbed = computed(() => route.meta.embed === true);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
userStore.loadUserData();
|
userStore.loadUserData();
|
||||||
@@ -14,10 +19,10 @@ onMounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen flex flex-col">
|
<div class="min-h-screen flex flex-col">
|
||||||
<AppHeader />
|
<AppHeader v-if="!isEmbed" />
|
||||||
<main class="flex-1 flex flex-col">
|
<main class="flex-1 flex flex-col">
|
||||||
<RouterView />
|
<RouterView />
|
||||||
</main>
|
</main>
|
||||||
<AppFooter />
|
<AppFooter v-if="!isEmbed" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref } from "vue";
|
||||||
|
import { usePlaceSocket } from "@/composables/usePlaceSocket";
|
||||||
|
import { PALETTE } from "@/palette";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
eventPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
// Read-only: fed straight into an <iframe> elsewhere (a stream overlay, a
|
||||||
|
// README, another site), so there's no header, palette, or click handling
|
||||||
|
// here - just the live canvas. Reachable without login (see place_embed in
|
||||||
|
// urls.py) since an OBS browser source can't do the CAS SSO login flow.
|
||||||
|
const canvasRef = ref<HTMLCanvasElement | null>(null);
|
||||||
|
let ctx: CanvasRenderingContext2D | null = null;
|
||||||
|
|
||||||
|
function drawPixel(x: number, y: number, colorIndex: number) {
|
||||||
|
if (!ctx) return;
|
||||||
|
ctx.fillStyle = PALETTE[colorIndex];
|
||||||
|
ctx.fillRect(x, y, 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { connected, gridWidth, gridHeight } = usePlaceSocket(props.eventPath, drawPixel);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
ctx = canvasRef.value?.getContext("2d") ?? null;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-screen h-screen bg-base-200 flex items-center justify-center overflow-hidden">
|
||||||
|
<span class="badge badge-xs absolute top-2 left-2 opacity-70"
|
||||||
|
:class="connected ? 'badge-success' : 'badge-error'" />
|
||||||
|
<canvas ref="canvasRef" :width="gridWidth" :height="gridHeight" :style="{
|
||||||
|
width: 'min(100vw, 100vh)',
|
||||||
|
aspectRatio: `${gridWidth} / ${gridHeight}`,
|
||||||
|
imageRendering: 'pixelated',
|
||||||
|
}" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -5,6 +5,7 @@ import Market from "@/Market.vue";
|
|||||||
import Noita from "@/Noita.vue";
|
import Noita from "@/Noita.vue";
|
||||||
import OpusMagnum from "@/OpusMagnum.vue";
|
import OpusMagnum from "@/OpusMagnum.vue";
|
||||||
import Place from "@/Place.vue";
|
import Place from "@/Place.vue";
|
||||||
|
import PlaceEmbed from "@/PlaceEmbed.vue";
|
||||||
import Submissions from "@/Submissions.vue";
|
import Submissions from "@/Submissions.vue";
|
||||||
import SubmissionsSelect from "@/SubmissionsSelect.vue";
|
import SubmissionsSelect from "@/SubmissionsSelect.vue";
|
||||||
import { usePageHeaderStore } from "@/stores/pageHeader";
|
import { usePageHeaderStore } from "@/stores/pageHeader";
|
||||||
@@ -16,6 +17,9 @@ declare module "vue-router" {
|
|||||||
// usePageHeaderStore() instead and leave this out.
|
// usePageHeaderStore() instead and leave this out.
|
||||||
titleKey?: string;
|
titleKey?: string;
|
||||||
getBackRoute?: (route: RouteLocationNormalizedLoaded) => RouteLocationRaw;
|
getBackRoute?: (route: RouteLocationNormalizedLoaded) => RouteLocationRaw;
|
||||||
|
// Skips AppHeader/AppFooter (see App.vue) - for pages meant to be
|
||||||
|
// dropped into an <iframe> as-is, like place/embed.
|
||||||
|
embed?: boolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +72,13 @@ export const router = createRouter({
|
|||||||
props: true,
|
props: true,
|
||||||
meta: { titleKey: "place.header", getBackRoute: eventHome },
|
meta: { titleKey: "place.header", getBackRoute: eventHome },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/:eventPath/place/embed",
|
||||||
|
name: "place-embed",
|
||||||
|
component: PlaceEmbed,
|
||||||
|
props: true,
|
||||||
|
meta: { embed: true },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/:eventPath/submissions/:gamePath",
|
path: "/:eventPath/submissions/:gamePath",
|
||||||
name: "submissions-select",
|
name: "submissions-select",
|
||||||
|
|||||||
Reference in New Issue
Block a user