From 766c69be2389b3e09ff90b1157ed6299e9fe6db6 Mon Sep 17 00:00:00 2001
From: yorrd <kai@adornis.de>
Date: Sat, 17 May 2025 19:48:14 +0000
Subject: [PATCH 1/2] feat: add screen capture

---
 src/client/ad-screen-recorder.ts | 127 +++++++++++++++++++++++++++++++
 src/client/main-element.ts       |   7 ++
 src/translations.json            |   3 +-
 3 files changed, 136 insertions(+), 1 deletion(-)
 create mode 100644 src/client/ad-screen-recorder.ts

diff --git a/src/client/ad-screen-recorder.ts b/src/client/ad-screen-recorder.ts
new file mode 100644
index 0000000..df4db62
--- /dev/null
+++ b/src/client/ad-screen-recorder.ts
@@ -0,0 +1,127 @@
+import { getByID } from '@adornis/baseql/operations/mongo.js';
+import { XSnackbar } from '@adornis/chemistry/elements/components/x-snackbar.js';
+import { AdornisFile } from '@adornis/file-utils/db/files.js';
+import { stateful, useState } from '@adornis/functional-lit/stateful.js';
+import { html } from 'lit';
+
+export const AdScreenRecorder = stateful(() => {
+  const [recording, setRecording] = useState(false);
+  const [mediaRecorder, setMediaRecorder] = useState<MediaRecorder | null>(null);
+  const [videoUrl, setVideoUrl] = useState<string | null>(null);
+  const [error, setError] = useState<string | null>(null);
+  const [shareUrl, setShareUrl] = useState<string | null>(null);
+  const [screenStream, setScreenStream] = useState<MediaStream | null>(null);
+
+  const stopScreenStream = (stream: MediaStream | null) => {
+    if (stream) {
+      stream.getTracks().forEach(track => track.stop());
+      setScreenStream(null);
+    }
+  };
+
+  const startRecording = async () => {
+    setError(null);
+    setVideoUrl(null);
+    setShareUrl(null);
+    try {
+      const screen = await (navigator.mediaDevices as any).getDisplayMedia({ video: true, audio: true });
+      setScreenStream(screen);
+      let mic: MediaStream | null = null;
+      try {
+        mic = await navigator.mediaDevices.getUserMedia({ audio: true });
+      } catch {}
+
+      let mixedAudio: MediaStream | null = null;
+      if (screen.getAudioTracks().length > 0 || (mic && mic.getAudioTracks().length > 0)) {
+        const ctx = new (window.AudioContext || (window as any).webkitAudioContext)();
+        const dest = ctx.createMediaStreamDestination();
+        if (screen.getAudioTracks().length > 0) {
+          ctx.createMediaStreamSource(new MediaStream(screen.getAudioTracks())).connect(dest);
+        }
+        if (mic && mic.getAudioTracks().length > 0) {
+          ctx.createMediaStreamSource(new MediaStream(mic.getAudioTracks())).connect(dest);
+        }
+        mixedAudio = dest.stream;
+      }
+
+      const tracks = [
+        ...screen.getVideoTracks(),
+        ...(mixedAudio ? mixedAudio.getAudioTracks() : [])
+      ];
+      const combined = new MediaStream(tracks);
+      let chunks: Blob[] = [];
+      const recorder = new MediaRecorder(combined, { mimeType: 'video/webm' });
+      recorder.ondataavailable = (e: BlobEvent) => {
+        if (e.data.size > 0) chunks.push(e.data);
+      };
+      recorder.onstop = async () => {
+        setRecording(false);
+        stopScreenStream(screenStream);
+        const blob = new Blob(chunks, { type: 'video/webm' });
+        setVideoUrl(URL.createObjectURL(blob));
+        if (!blob || blob.size === 0) {
+          setError('Recording failed: Blob is empty or invalid.');
+          return;
+        }
+        try {
+          const fileToUpload = new File([blob], 'screen-recording.webm', { type: blob.type || 'video/webm' });
+          const fileID = await AdornisFile.upload(fileToUpload);
+          const file = await getByID(AdornisFile, fileID)(AdornisFile.allFields);
+          setShareUrl(file?.getServeLink() ?? '');
+        } catch (err: any) {
+          setError('Error generating share link: ' + (err?.message || err));
+        }
+      };
+      setMediaRecorder(recorder);
+      recorder.start();
+      setRecording(true);
+    } catch (err: any) {
+      setError('Failed to start screen recording: ' + err.message);
+    }
+  };
+
+  const stopRecording = async () => {
+    if (mediaRecorder && mediaRecorder instanceof MediaRecorder) {
+      mediaRecorder.stop();
+      setMediaRecorder(null);
+      setRecording(false);
+      stopScreenStream(screenStream);
+    }
+  };
+
+  return html`
+    <div class="flex justify-center min-h-[75vh] w-full overflow-auto">
+      <div class="card bg-base-100 w-full max-w-lg p-8 flex flex-col items-center gap-8">
+        <div class="w-full flex flex-col items-center gap-2">
+          <div class="text-3xl font-bold mb-2">Screen Recorder</div>
+          <div class="flex gap-4 w-full justify-center">
+            <button class="btn btn-primary" ?disabled=${recording} @click=${startRecording}>Start Recording</button>
+            <button class="btn btn-secondary" ?disabled=${!recording} @click=${stopRecording}>Stop Recording</button>
+          </div>
+        </div>
+        ${error ? html`<div class="alert alert-error w-full text-center">${error}</div>` : null}
+        ${videoUrl
+          ? html`
+              <div class="flex flex-col items-center gap-3 w-full">
+                <video src="${videoUrl}" controls class="rounded-box w-full max-w-xl"></video>
+                <div class="flex gap-2 w-full justify-center">
+                  <button
+                    class="btn btn-accent flex items-center gap-2"
+                    @click=${() => {
+                      navigator.clipboard.writeText(shareUrl ?? '');
+                      XSnackbar.show('Link copied to clipboard');
+                    }}
+                  >
+                    Share Link
+                  </button>
+                  <a class="btn btn-outline" href="${shareUrl}" target="_blank" download="screen-recording.webm">
+                    Download
+                  </a>
+                </div>
+              </div>
+            `
+          : null}
+      </div>
+    </div>
+  `;
+});
diff --git a/src/client/main-element.ts b/src/client/main-element.ts
index 82dd249..0258493 100644
--- a/src/client/main-element.ts
+++ b/src/client/main-element.ts
@@ -35,6 +35,8 @@ import { html, nothing } from 'lit';
 import { version } from '../../package.json';
 import logo from '../assets/ndixadornis.png';
 import { DashboardUser } from '../db/dashboard-user.js';
+import './ad-screen-recorder.js';
+import { AdScreenRecorder } from './ad-screen-recorder.js';
 import './customers/ad-page-customer-list.js';
 import './profile/ad-profile.js';
 import './project-management/ad-project-management.js';
@@ -101,6 +103,11 @@ export const router = makeRouter({
     render: () => html`<ad-user-management flex></ad-user-management>`,
     metaData: { icon: 'people' },
   },
+  'screen-recorder': {
+    path: '/screen-recorder',
+    render: () => AdScreenRecorder(),
+    metaData: { icon: 'screen_share', label: 'Screen Recorder' },
+  },
   settlement: {
     path: '/settlement*',
     render: () => html`<ad-settlement flex style="width: 100%"></ad-settlement>`,
diff --git a/src/translations.json b/src/translations.json
index 7b53ab4..8c8bb0e 100644
--- a/src/translations.json
+++ b/src/translations.json
@@ -13,7 +13,8 @@
     "activity": ["Aktivität"],
     "time": ["Zeiterfassung & Urlaub"],
     "customers": ["Kunden"],
-    "faktura": ["Faktura"]
+    "faktura": ["Faktura"],
+    "screen-recorder": ["Screen Recorder"]
   },
   "field": {
     "username": ["Nutzername"],
-- 
GitLab


From 05d0817bec7e60c35aee7ac96cc69f18c207efd8 Mon Sep 17 00:00:00 2001
From: yorrd <kai@adornis.de>
Date: Sat, 17 May 2025 19:49:28 +0000
Subject: [PATCH 2/2] fix: make screen recording available to everyone

---
 src/client/main-element.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/client/main-element.ts b/src/client/main-element.ts
index 0258493..3383487 100644
--- a/src/client/main-element.ts
+++ b/src/client/main-element.ts
@@ -257,7 +257,7 @@ export const MainElement = () => {
     <x-writer-toolbar></x-writer-toolbar>
   `;
 
-  if (router.currentRoute.value?.route.name === 'knowledge-base') return content;
+  if (['knowledge-base', 'screen-recorder'].includes(router.currentRoute.value?.route.name)) return content;
 
   return authenticationGuard(
     stateful(() => content),
-- 
GitLab