sonri/src/routes/(area)/today/+page.svelte
2025-06-13 09:48:30 +02:00

189 lines
6.3 KiB
Svelte

<script lang="ts">
import { onMount } from "svelte";
import { hashBuffer } from "$lib/hash";
import { dateDiff } from "$lib/date";
import { getKeyVault, loggedIn } from "$lib/auth";
import { encrypt, uint8ToHex } from "$lib/crypto";
import { lang } from "$lib/lang";
type StatusT = "pending" | "active" | "error";
let status = $state<StatusT>("pending");
let errors = $state<string[]>([]);
let now = $state(new Date());
let recordingStart = $state<Date | null>(null);
let recordingURL = $state<string | null>(null);
// svelte-ignore non_reactive_update
let video: HTMLVideoElement, finalVideo: HTMLVideoElement;
let recorder: MediaRecorder | null = null;
onMount(() => {
if (typeof window !== 'object') return;
if (!loggedIn()) {
window.location.href = '/login';
}
});
onMount(() => {
if (typeof window !== 'object') return;
const interval = setInterval(() => {
now = new Date();
}, 100);
return () => clearInterval(interval);
});
onMount(async () => {
if (typeof window !== 'object') return;
if (!navigator || !navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
return;
}
const WIDTH = 320;
const HEIGHT = 480;
const FPS = 24;
const stream = await navigator.mediaDevices.getUserMedia({
video: { width: WIDTH, height: HEIGHT, frameRate: FPS, facingMode: 'user' },
audio: true,
});
video.srcObject = stream;
recorder = new MediaRecorder(stream, { mimeType: 'video/mp4; codecs="avc1.42001E"' });
recorder.addEventListener('dataavailable', async (e) => {
const inputBlob = new Blob([e.data], { type: 'video/mp4' });
const convertRes = await fetch('/api/convert', {
method: 'POST',
body: inputBlob,
});
if (!convertRes.ok) return;
const convertBlob = await convertRes.blob();
recordingURL = URL.createObjectURL(convertBlob);
setTimeout(() => finalVideo.scrollIntoView(), 150);
})
});
const start = () => {
recordingStart = new Date();
recorder?.start();
};
const stop = () => {
recordingStart = null;
recorder?.stop();
};
const submit = async () => {
if (!recordingURL) return;
const blob = await (await fetch(recordingURL)).blob();
const buffer = await blob.arrayBuffer();
console.log('buffer', buffer);
const vault = getKeyVault()!;
const encrypted = await encrypt(vault, new Uint8Array(buffer));
console.log('encrypted', encrypted);
const res = await fetch(`/api/today?pubkey=${uint8ToHex(vault.publicKey)}`, {
method: 'POST',
body: encrypted,
headers: {
'Content-Type': 'application/octet-stream',
},
});
if (res.ok) {
window.location.href = '/log';
}
};
</script>
<div class="min-rh-screen">
<div class="rh-screen p-4 min-h-0 grid place-items-center">
<video
autoplay muted bind:this={video}
class="opacity-0 h-full w-full min-h-0 max-w-[70vh] object-cover rounded-[40px] bg-[#303030] fade-in"
style="animation-delay: 500ms;"
>
<track kind="captions" />
</video>
<div
class="absolute top-0 left-0 right-0 p-6 m-auto w-fit scale-in"
style="animation-delay: 50ms;"
>
<div class="rounded-3xl bg-white px-3 py-1 font-serif text-center w-fit">
<h1 class="font-bold text-xl">{now.toLocaleDateString(lang(), { dateStyle: 'long' })}</h1>
<h2 class="text-sm">{now.toLocaleTimeString(lang())}</h2>
</div>
{#if !!recordingStart}
<div class="h-1"></div>
<div class="rounded-3xl bg-white px-3 py-1 font-serif flex gap-1 items-center w-fit m-auto scale-in">
<svg class="blink" xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 0 24 24" width="24px" fill="red"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zm0-5C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/></svg>
<div>
<span class="text-md">
<!-- <span class="font-bold">Recording</span> -->
<span>{dateDiff(now, recordingStart).toLocaleTimeString(lang(), { minute: 'numeric', second: 'numeric' })}</span>
</span>
</div>
</div>
{/if}
</div>
<div
class="absolute bottom-0 left-0 right-0 p-6 m-auto w-fit fade-in"
style="animation-delay: 50ms;"
>
<button
class={`font-serif px-5 py-3 rounded-3xl text-white font-bold text-2xl cursor-pointer ${recordingStart ? 'bg-[#e14c2f]' : 'bg-[#2596be]'}`}
style="transition-duration: 250ms;"
onclick={() => recordingStart ? stop() : start()}
>
{recordingStart ? 'Stop' : 'Start'}
</button>
</div>
</div>
{#if recordingURL}
<div class="h-8"></div>
<div class="p-4 min-h-0 grid place-items-center">
<video controls bind:this={finalVideo}
src={recordingURL}
class="opacity-0 h-full w-full max-w-[50vh] min-h-0 w-full object-cover rounded-[40px] bg-[#303030] fade-in"
style="aspect-ratio: 320 / 480;"
>
<track kind="captions" />
</video>
<div class="h-2"></div>
<button
class={`font-serif px-5 py-3 rounded-3xl text-white font-bold text-2xl cursor-pointer bg-[#2596be]`}
style="transition-duration: 250ms;"
onclick={submit}
>
Submit
</button>
</div>
{/if}
</div>
<!-- <canvas class="hidden" bind:this={renderCanvas}></canvas>
<canvas class="hidden" bind:this={captureCanvas}></canvas>
<video class="hidden" bind:this={sourceVideo}>
<track kind="captions" />
</video> -->
<style>
</style>