mirror of
https://github.com/SrIzan10/helium.git
synced 2026-06-06 00:56:58 +00:00
feat: (mostly ai gen) touchscreen oriented viewer
This commit is contained in:
@@ -1,20 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
import { ref, watch } from "vue"
|
||||
import { Delete } from "lucide-vue-next"
|
||||
import {
|
||||
PinInput,
|
||||
PinInputGroup,
|
||||
PinInputSlot,
|
||||
} from "@/components/ui/pin-input"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useViewerStore } from "~/state/viewer";
|
||||
|
||||
const viewerStore = useViewerStore()
|
||||
const digits = ref<string[]>([])
|
||||
|
||||
// Sync local digits with store code
|
||||
watch(digits, (newDigits) => {
|
||||
const code = newDigits.join('')
|
||||
viewerStore.code = code
|
||||
})
|
||||
|
||||
// Also sync if store updates from elsewhere (though unlikely in this flow)
|
||||
watch(() => viewerStore.code, (newCode) => {
|
||||
if (newCode !== digits.value.join('')) {
|
||||
digits.value = newCode.split('')
|
||||
}
|
||||
})
|
||||
|
||||
const handleNumPad = (num: number) => {
|
||||
if (digits.value.length < 6) {
|
||||
digits.value = [...digits.value, num.toString()]
|
||||
}
|
||||
}
|
||||
|
||||
const handleBackspace = () => {
|
||||
digits.value = digits.value.slice(0, -1)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex flex-col items-center gap-6">
|
||||
<PinInput
|
||||
id="pin-input"
|
||||
@complete="(viewerStore.code = $event.join(''))"
|
||||
v-model="digits"
|
||||
type="number"
|
||||
>
|
||||
<PinInputGroup>
|
||||
@@ -22,8 +48,41 @@ const viewerStore = useViewerStore()
|
||||
v-for="(id, index) in 6"
|
||||
:key="id"
|
||||
:index="index"
|
||||
class="h-14 w-10 sm:h-16 sm:w-12 text-lg sm:text-xl"
|
||||
/>
|
||||
</PinInputGroup>
|
||||
</PinInput>
|
||||
|
||||
<!-- Touchscreen Numpad -->
|
||||
<div class="grid grid-cols-3 gap-3 w-full max-w-[280px]">
|
||||
<Button
|
||||
v-for="n in 9"
|
||||
:key="n"
|
||||
variant="outline"
|
||||
class="h-14 text-xl font-medium active:scale-95 transition-transform"
|
||||
@click="handleNumPad(n)"
|
||||
>
|
||||
{{ n }}
|
||||
</Button>
|
||||
|
||||
<div class="col-span-1"></div> <!-- Spacer -->
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
class="h-14 text-xl font-medium active:scale-95 transition-transform"
|
||||
@click="handleNumPad(0)"
|
||||
>
|
||||
0
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="h-14 active:scale-95 transition-transform hover:bg-destructive/10 hover:text-destructive"
|
||||
@click="handleBackspace"
|
||||
:disabled="digits.length === 0"
|
||||
>
|
||||
<Delete class="w-6 h-6" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface PresetAuthor {
|
||||
fullName: string | null;
|
||||
profileImageUrl: string | null;
|
||||
username: string | null;
|
||||
email: string | null;
|
||||
}
|
||||
|
||||
export interface PresetShareResponse {
|
||||
|
||||
@@ -160,5 +160,6 @@ export async function getPresetAuthorData(event: H3Event, presetId: string) {
|
||||
fullName: user.fullName,
|
||||
profileImageUrl: user.imageUrl,
|
||||
username: user.username,
|
||||
email: user.primaryEmailAddress?.emailAddress || null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,27 +1,84 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center justify-center gap-6 mt-10 px-4">
|
||||
<h1>helium</h1>
|
||||
<p>effortless screensharing powered by webrtc</p>
|
||||
<app-code-input />
|
||||
<div class="flex flex-col items-center justify-center gap-6 mt-10 px-4 min-h-[80vh]">
|
||||
<div v-if="!isConnected" class="flex flex-col items-center gap-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
<div class="text-center space-y-2">
|
||||
<h1 class="text-4xl font-bold tracking-tight">helium</h1>
|
||||
<p class="text-muted-foreground text-lg">effortless screensharing powered by webrtc</p>
|
||||
</div>
|
||||
|
||||
<app-code-input />
|
||||
|
||||
<NuxtLink to="/stream">
|
||||
<Button variant="link" class="text-muted-foreground hover:text-primary">
|
||||
host instead?
|
||||
</Button>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<div class="video relative w-full max-w-1/2 aspect-video">
|
||||
<div
|
||||
class="video transition-all duration-500 ease-in-out"
|
||||
:class="[
|
||||
isConnected
|
||||
? 'fixed inset-0 z-50 w-full h-full bg-black'
|
||||
: 'relative w-full max-w-3xl aspect-video rounded-xl overflow-hidden border shadow-sm bg-muted/50'
|
||||
]"
|
||||
>
|
||||
<!-- Status Overlay -->
|
||||
<div
|
||||
v-if="!isConnected"
|
||||
class="absolute inset-0 bg-black flex items-center justify-center z-10 text-white"
|
||||
class="absolute inset-0 flex items-center justify-center z-10 p-4 text-center"
|
||||
>
|
||||
{{ viewerStore.connectionStatus }}
|
||||
<div v-if="viewerStore.isDisconnected" class="space-y-4">
|
||||
<p class="text-sm font-medium text-muted-foreground">stream ended</p>
|
||||
<Button @click="handleReset" variant="outline">
|
||||
Enter another code
|
||||
</Button>
|
||||
</div>
|
||||
<div v-else-if="viewerStore.connectionStatus !== 'waiting for a code'" class="space-y-4">
|
||||
<div class="animate-spin w-8 h-8 border-4 border-primary border-t-transparent rounded-full mx-auto" />
|
||||
<p class="text-sm font-medium text-muted-foreground">{{ viewerStore.connectionStatus }}</p>
|
||||
</div>
|
||||
<p v-else class="text-muted-foreground/50 text-sm">
|
||||
enter code to join stream
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Video Feed -->
|
||||
<video
|
||||
ref="videofeedRef"
|
||||
autoplay
|
||||
playsinline
|
||||
controls
|
||||
class="bg-black w-full h-full"
|
||||
:controls="false"
|
||||
class="w-full h-full object-contain bg-black"
|
||||
@loadeddata="isConnected = true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<NuxtLink to="/stream"><Button>host instead?</Button></NuxtLink>
|
||||
<!-- Connected Controls Overlay -->
|
||||
<div
|
||||
v-if="isConnected"
|
||||
class="absolute top-0 left-0 right-0 p-4 flex justify-between items-start opacity-0 hover:opacity-100 transition-opacity bg-gradient-to-b from-black/50 to-transparent"
|
||||
>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="lg"
|
||||
class="gap-2 shadow-lg"
|
||||
@click="cleanupViewing"
|
||||
>
|
||||
<LogOut class="w-5 h-5" />
|
||||
Disconnect
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
class="gap-2 shadow-lg"
|
||||
@click="toggleFullscreen"
|
||||
>
|
||||
<Maximize class="w-5 h-5" />
|
||||
Fullscreen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -30,11 +87,14 @@ import { useWebSocket } from "@vueuse/core";
|
||||
import { useViewerStore } from "~/state/viewer";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useWebSocketUrl } from "~/composables/useWebSocketUrl";
|
||||
import { LogOut, Maximize } from "lucide-vue-next";
|
||||
|
||||
const isConnected = ref(false);
|
||||
const viewerStore = useViewerStore();
|
||||
const { code: codeRef } = storeToRefs(viewerStore);
|
||||
const wsUrl = useWebSocketUrl();
|
||||
const videofeedRef = ref<HTMLVideoElement | null>(null);
|
||||
|
||||
const { send, close: closeWebSocket } = useWebSocket(wsUrl, {
|
||||
autoReconnect: true,
|
||||
heartbeat: {
|
||||
@@ -90,7 +150,11 @@ const { send, close: closeWebSocket } = useWebSocket(wsUrl, {
|
||||
viewerStore.setConnectionStatus(
|
||||
`connection ${peerConnection.connectionState}`,
|
||||
);
|
||||
isConnected.value = false;
|
||||
// Don't set isConnected = false immediately here to avoid flickering if it's a temp glitch,
|
||||
// but usually disconnected means it's over.
|
||||
if (peerConnection.connectionState !== "connected") {
|
||||
isConnected.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -161,8 +225,6 @@ const { send, close: closeWebSocket } = useWebSocket(wsUrl, {
|
||||
},
|
||||
});
|
||||
|
||||
const videofeedRef = ref<HTMLVideoElement | null>(null);
|
||||
|
||||
const startWebRTCConnection = async () => {
|
||||
send(
|
||||
JSON.stringify({
|
||||
@@ -190,10 +252,35 @@ function cleanupViewing() {
|
||||
if (videofeedRef.value) {
|
||||
videofeedRef.value.srcObject = null;
|
||||
}
|
||||
|
||||
// Clear code
|
||||
viewerStore.code = '';
|
||||
|
||||
// Reset connection status
|
||||
viewerStore.setConnectionStatus("disconnected");
|
||||
isConnected.value = false;
|
||||
|
||||
// Exit fullscreen if active
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function toggleFullscreen() {
|
||||
if (!videofeedRef.value) return;
|
||||
|
||||
if (!document.fullscreenElement) {
|
||||
videofeedRef.value.requestFullscreen().catch((err) => {
|
||||
console.error(`Error attempting to enable fullscreen: ${err.message}`);
|
||||
});
|
||||
} else {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
viewerStore.resetDisconnected();
|
||||
viewerStore.setConnectionStatus('waiting for a code');
|
||||
}
|
||||
|
||||
// Cleanup on component unmount
|
||||
@@ -214,4 +301,4 @@ onMounted(() => {
|
||||
window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
@@ -22,6 +22,7 @@
|
||||
{{
|
||||
(response.author?.fullName ||
|
||||
response.author?.username ||
|
||||
response.author?.email ||
|
||||
"?")[0]!.toUpperCase()
|
||||
}}
|
||||
</div>
|
||||
@@ -68,7 +69,7 @@
|
||||
<div class="mt-8 flex justify-end gap-3">
|
||||
<Button variant="outline" @click="navigateTo('/')">Cancel</Button>
|
||||
<Button @click="importPreset" :disabled="isImporting">
|
||||
{{ isImporting ? 'Importing...' : 'Import Preset' }}
|
||||
{{ isImporting ? "Importing..." : "Import Preset" }}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -5,6 +5,7 @@ export const useViewerStore = defineStore('viewer', {
|
||||
code: '',
|
||||
peerConnection: null as RTCPeerConnection | null,
|
||||
connectionStatus: 'waiting for a code',
|
||||
isDisconnected: false,
|
||||
}),
|
||||
actions: {
|
||||
setCode(code: string) {
|
||||
@@ -18,6 +19,12 @@ export const useViewerStore = defineStore('viewer', {
|
||||
console.log('pinia connection status debug:', status);
|
||||
}
|
||||
this.connectionStatus = status;
|
||||
if (status === 'disconnected') {
|
||||
this.isDisconnected = true;
|
||||
}
|
||||
},
|
||||
resetDisconnected() {
|
||||
this.isDisconnected = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user