diff --git a/documentation/design/README.md b/documentation/design/README.md new file mode 100644 index 0000000000000000000000000000000000000000..cfad874c21a6b5bc5eb9add0548997c6a4a1e333 --- /dev/null +++ b/documentation/design/README.md @@ -0,0 +1,70 @@ +# Design + +(those are just quick notes for now, we can elaborate later) + +## Designing Clapper for the Web + +### Targeted devices + +Currently Clapper works best on a laptop, +since the UI has been developed around the presence of a relatively wide screen, +a mouse and a touch pad (for horizontal scrolling in the timeline). + +However I think we should try to better support the following environments: + +- Tiny laptop (12", 11") +- Tablet (without a mouse) +- Desktop computer with multiple screens + +(mobile has its own chapter, see below in this document) + +### Use Web APIs in priority + +We should try to use standard Web APIs (ratified by the W3C) as much as possible, with polyfills in case some browsers don't implement them yet. + +We can use experimental standards (eg. WebGPU), +but since they are unstable and not supported by all browsers, they should not be mandatory to the experience. + +## Designing Clapper for desktop + +The experience on Desktop should be similar to the one in a browser, but we can do some changes: + +### Customizing the look of the app window + +Electron offers us to hide or customize the app's window, so we should do it. + +We don't have to follow the design principles of the underlying operating system (some apps don't care eg. video games, Spotify, Slack, FLStudio, Discord..) but it may be necessary for some operations (file pickers, installer, window management etc). + +### Use the native file system + +The big benefit of running Clapper as a desktop application is that we can access the file system, meaning we can work with files of arbitrary file length (note: for this kind of file system manipulation, we will have to use extra code eg. NodeJS) + +This can also be used for performance optimization such as using temporary files, or pre-computing things and store them in a cache for the next time Clapper is opened. + +### Download additional data for local use + +By running Clapper on the user's device, we can also make it download data of arbitrary size. + +This can help with various use cases, such as running AI models locally. + +For instance the desktop app LMStudio can download models from Hugging Face. + +### Call external or embedded native libraries + +We have complete freedom to ship Clapper with embedded native tools eg. a database or a native library (eg. a C++ library to run a LLM locally). + +We could even use Python scripts with Clapper. + +Please note however that we will have to make sure anything we embed works on various operating systems (Windows/macOS/Linux) so this requires dedicated skills and maintenance. + +### Use System Notifications + +When a job is pending, finished, a software update is ready etc. + +## Clapper Design For Mobile + +Currently Clapper can *run* on mobile, but it is not designed for it. + +So things like the top menu etc will be pretty much unuseable, unless we adapt them. + + diff --git a/package-lock.json b/package-lock.json index 7d139a5cc16582c91c8dcf5375d179db59ff3a53..ff73b9cc036dcdffd43ecce7775b7d7f736cc288 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@aitube/broadway": "0.2.3", "@aitube/clap": "0.2.3", - "@aitube/clapper-services": "0.2.3-1", + "@aitube/clapper-services": "0.2.3-2", "@aitube/client": "0.2.3", "@aitube/engine": "0.2.3", "@aitube/timeline": "0.2.3", @@ -33,7 +33,7 @@ "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-collapsible": "^1.0.3", - "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", @@ -66,7 +66,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cmdk": "^0.2.1", - "comfydeploy": "^0.0.19-beta.13", + "comfydeploy": "^0.0.21", "date-fns": "^3.6.0", "dotenv": "^16.4.5", "fflate": "^0.8.2", @@ -89,6 +89,7 @@ "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.3.1", "react-drag-drop-files": "^2.3.10", + "react-error-boundary": "^4.0.13", "react-hook-consent": "^3.5.3", "react-hotkeys-hook": "^4.5.0", "react-icons": "^5.2.1", @@ -99,7 +100,7 @@ "replicate": "^0.32.0", "sharp": "0.33.4", "sonner": "^1.5.0", - "tailwind-merge": "^2.4.0", + "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", "three": "^0.164.1", "ts-node": "^10.9.2", @@ -109,7 +110,7 @@ "web-audio-beat-detector": "^8.2.12", "yaml": "^2.4.5", "zustand": "4.5.2", - "zx": "^8.1.3" + "zx": "^8.1.4" }, "devDependencies": { "@electron-forge/cli": "^7.4.0", @@ -191,9 +192,9 @@ } }, "node_modules/@aitube/clapper-services": { - "version": "0.2.3-1", - "resolved": "https://registry.npmjs.org/@aitube/clapper-services/-/clapper-services-0.2.3-1.tgz", - "integrity": "sha512-R6n9Wlte7E7bRIKh3Jf8xSZkvly2BgQ4p12OJY1rBNzgvDaUfHyFcCdCwKcBXzcDlWzGW5fjQIA7+OPeoXpC0Q==", + "version": "0.2.3-2", + "resolved": "https://registry.npmjs.org/@aitube/clapper-services/-/clapper-services-0.2.3-2.tgz", + "integrity": "sha512-Qd6Riridk4FVcTjlscxw5wsbUgojwi1wkTIjlgPluhT5J5kLyEJQL/hmT2gBDBRsB4kyieVNZiGflgXnauDENw==", "peerDependencies": { "@aitube/clap": "0.2.3", "@aitube/timeline": "0.2.3", @@ -9394,11 +9395,14 @@ } }, "node_modules/comfydeploy": { - "version": "0.0.19-beta.13", - "resolved": "https://registry.npmjs.org/comfydeploy/-/comfydeploy-0.0.19-beta.13.tgz", - "integrity": "sha512-m3sn8XCYJ9FjEhB6CoA8IYQ67CvizQuZ9vodNroiZB5BA2TakkGTK5TUj+PxmpOopdUZVhlSOVqjcB1+KQYJBQ==", - "peerDependencies": { - "zod": ">= 3" + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/comfydeploy/-/comfydeploy-0.0.21.tgz", + "integrity": "sha512-v5i6igitVWN6jmDlg35g8XyJuGsToD1sbhEZIz4nM3F6gSXZf2YjJzL/wzabsHlOtHhKsO/vdQAXuvO7/w8deQ==", + "dependencies": { + "zod": "^3.22.4" + }, + "engines": { + "node": ">=16" } }, "node_modules/comma-separated-tokens": { @@ -17745,6 +17749,17 @@ "react-dom": "^18.0.0" } }, + "node_modules/react-error-boundary": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.13.tgz", + "integrity": "sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/react-hook-consent": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/react-hook-consent/-/react-hook-consent-3.5.3.tgz", @@ -19595,9 +19610,9 @@ "dev": true }, "node_modules/tailwind-merge": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.4.0.tgz", - "integrity": "sha512-49AwoOQNKdqKPd9CViyH5wJoSKsCDjUlzL8DxuGp3P1FsGY36NJDAa18jLZcaHAUUuTj+JB8IAo8zWgBNvBF7A==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.2.tgz", + "integrity": "sha512-kjEBm+pvD+6eAwzJL2Bi+02/9LFLal1Gs61+QB7HvTfQQ0aXwC5LGT8PEt1gS0CWKktKe6ysPTAy3cBC5MeiIg==", "funding": { "type": "github", "url": "https://github.com/sponsors/dcastil" diff --git a/package.json b/package.json index d74f839fbda2daffa73bdce98b13cba6d0132c9f..195b3627f33deceb79c3b44c1cca6e420510aca4 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "dependencies": { "@aitube/broadway": "0.2.3", "@aitube/clap": "0.2.3", - "@aitube/clapper-services": "0.2.3-1", + "@aitube/clapper-services": "0.2.3-2", "@aitube/client": "0.2.3", "@aitube/engine": "0.2.3", "@aitube/timeline": "0.2.3", @@ -60,7 +60,7 @@ "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-collapsible": "^1.0.3", - "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", @@ -93,7 +93,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cmdk": "^0.2.1", - "comfydeploy": "^0.0.19-beta.13", + "comfydeploy": "^0.0.21", "date-fns": "^3.6.0", "dotenv": "^16.4.5", "fflate": "^0.8.2", @@ -116,6 +116,7 @@ "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.3.1", "react-drag-drop-files": "^2.3.10", + "react-error-boundary": "^4.0.13", "react-hook-consent": "^3.5.3", "react-hotkeys-hook": "^4.5.0", "react-icons": "^5.2.1", @@ -126,7 +127,7 @@ "replicate": "^0.32.0", "sharp": "0.33.4", "sonner": "^1.5.0", - "tailwind-merge": "^2.4.0", + "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", "three": "^0.164.1", "ts-node": "^10.9.2", @@ -136,7 +137,7 @@ "web-audio-beat-detector": "^8.2.12", "yaml": "^2.4.5", "zustand": "4.5.2", - "zx": "^8.1.3" + "zx": "^8.1.4" }, "devDependencies": { "@electron-forge/cli": "^7.4.0", diff --git a/src/app/api/resolve/providers/comfy-replicate/index.ts b/src/app/api/resolve/providers/comfy-replicate/index.ts index 49fe55b9835ec329c412f64eea8492de7e6bfac2..d291a7ad173c86bb81121c07dec0f32054a8bbaf 100644 --- a/src/app/api/resolve/providers/comfy-replicate/index.ts +++ b/src/app/api/resolve/providers/comfy-replicate/index.ts @@ -1,7 +1,11 @@ -import { ClapSegmentStatus, getClapAssetSourceType } from '@aitube/clap' +import { + ClapSegmentCategory, + ClapSegmentStatus, + getClapAssetSourceType, +} from '@aitube/clap' import { ResolveRequest } from '@aitube/clapper-services' -import { getComfyWorkflow } from '../comfyui/getComfyWorkflow' +import { getComfyWorkflow } from '../../../../../services/editors/workflow-editor/workflows/comfyui/getComfyWorkflow' import { runWorkflow } from './runWorkflow' import { TimelineSegment } from '@aitube/timeline' @@ -11,22 +15,55 @@ export async function resolveSegment( if (!request.settings.replicateApiKey) { throw new Error(`Missing API key for "Replicate.com"`) } - const workflow = getComfyWorkflow(request) const segment: TimelineSegment = request.segment - try { - segment.assetUrl = await runWorkflow({ - apiKey: request.settings.replicateApiKey, - workflow, - }) - segment.assetSourceType = getClapAssetSourceType(segment.assetUrl) - } catch (err) { - console.error(`failed to call Replicate: `, err) - segment.assetUrl = '' - segment.assetSourceType = getClapAssetSourceType(segment.assetUrl) - segment.status = ClapSegmentStatus.TO_GENERATE - // request.segment.status = ClapSegmentStatus.ERROR + if (request.segment.category === ClapSegmentCategory.STORYBOARD) { + const inputFields = + request.settings.imageGenerationWorkflow.inputFields || [] + + // since this is a random "wild" workflow, it is possible + // that the field name is a bit different + // we try to look into the workflow input fields + // to find the best match + const promptFields = [ + inputFields.find((f) => f.id === 'prompt'), // exactMatch, + inputFields.find((f) => f.id.includes('prompt')), // similarName, + inputFields.find((f) => f.type === 'string'), // similarType + ].filter((x) => typeof x !== 'undefined') + + const promptField = promptFields[0] + if (!promptField) { + throw new Error( + `this workflow doesn't seem to have a parameter called "prompt"` + ) + } + + console.log(`TODO: inject parameters into the final request object`) + const workflow = `{}` // <- the final workflow sent to Replicate + + try { + segment.assetUrl = await runWorkflow({ + apiKey: request.settings.replicateApiKey, + workflow, + }) + segment.assetSourceType = getClapAssetSourceType(segment.assetUrl) + } catch (err) { + console.error(`failed to call Replicate: `, err) + segment.assetUrl = '' + segment.assetSourceType = getClapAssetSourceType(segment.assetUrl) + segment.status = ClapSegmentStatus.TO_GENERATE + // request.segment.status = ClapSegmentStatus.ERROR + } + } else if (request.segment.category === ClapSegmentCategory.VIDEO) { + // TODO do the same for video + throw new Error( + `Clapper doesn't support ${request.segment.category} generation for provider "Comfy.icu". Please open a pull request with (working code) to solve this!` + ) + } else { + throw new Error( + `Clapper doesn't support ${request.segment.category} generation for provider "Comfy.icu". Please open a pull request with (working code) to solve this!` + ) } return segment diff --git a/src/app/api/resolve/providers/comfyui/getComfyWorkflow.ts b/src/app/api/resolve/providers/comfyui/getComfyWorkflow.ts deleted file mode 100644 index 5bedd5520c0853a07de6ae6f3e540b938d4dae49..0000000000000000000000000000000000000000 --- a/src/app/api/resolve/providers/comfyui/getComfyWorkflow.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { ClapSegmentCategory } from '@aitube/clap' -import { getVideoPrompt } from '@aitube/engine' - -import { ComfyNode, ResolveRequest } from '@aitube/clapper-services' - -// TODO move this to @aitube/engine or @aitube/engine-comfy -export function getComfyWorkflow(request: ResolveRequest) { - let comfyWorkflow = '{}' - - if (request.segment.category === ClapSegmentCategory.STORYBOARD) { - comfyWorkflow = request.settings.comfyWorkflowForImage - } else if (request.segment.category === ClapSegmentCategory.VIDEO) { - comfyWorkflow = request.settings.comfyWorkflowForVideo - } - - // parse the node array from the ComfyUI workflow - const nodes = Object.values(JSON.parse(comfyWorkflow)) as ComfyNode[] - - const visualPrompt = getVideoPrompt(request.segments, request.entities) - - const output: Record = {} - - nodes.forEach((node, i) => { - if (typeof node.inputs.text === 'string') { - if (node._meta.title.includes('Prompt')) { - node.inputs.text = visualPrompt - } - } - output[`${i}`] = node - }) - - return JSON.stringify(output) -} diff --git a/src/app/api/resolve/providers/comfyui/index.ts b/src/app/api/resolve/providers/comfyui/index.ts index 7256d85553284d8aefceda54e3bde3a38d2e67fb..9b2c3d19a03376c42528cbf6bc58837d5488ac9c 100644 --- a/src/app/api/resolve/providers/comfyui/index.ts +++ b/src/app/api/resolve/providers/comfyui/index.ts @@ -1,5 +1,6 @@ import { ResolveRequest } from '@aitube/clapper-services' import { + ClapAssetSource, ClapSegmentCategory, ClapSegmentStatus, generateSeed, @@ -7,6 +8,7 @@ import { } from '@aitube/clap' import { TimelineSegment } from '@aitube/timeline' import { + BasicCredentials, CallWrapper, ComfyApi, PromptBuilder, @@ -15,6 +17,7 @@ import { } from '@saintno/comfyui-sdk' import { getWorkflowInputValues } from '../getWorkflowInputValues' +import { decodeOutput } from '@/lib/utils/decodeOutput' export async function resolveSegment( request: ResolveRequest @@ -25,10 +28,25 @@ export async function resolveSegment( const segment: TimelineSegment = { ...request.segment } + const credentials: BasicCredentials = { + type: 'basic', + username: request.settings.comfyUiHttpAuthLogin, + password: request.settings.comfyUiHttpAuthPassword, + } + // for API doc please see: // https://github.com/tctien342/comfyui-sdk/blob/main/examples/example-t2i.ts const api = new ComfyApi( - request.settings.comfyUiApiUrl || 'http://localhost:8188' + request.settings.comfyUiApiUrl || 'http://localhost:8188', + request.settings.comfyUiClientId, + + // HTTP Auth is optional + // also in the future, we might support other things (bearer tokens?) + request.settings.comfyUiHttpAuthLogin + ? { + credentials, + } + : undefined ).init() if (request.segment.category === ClapSegmentCategory.STORYBOARD) { @@ -123,18 +141,38 @@ export async function resolveSegment( .onStart(() => console.log('Task is started')) .onPreview((blob) => console.log(blob)) .onFinished((data) => { - console.log( - data.images?.images.map((img: any) => api.getPathImage(img)) - ) + console.log('Pipeline finished') }) .onProgress((info) => console.log('Processing node', info.node, `${info.value}/${info.max}`) ) .onFailed((err) => console.log('Task is failed', err)) - const result = await pipeline.run() + const rawOutput = await pipeline.run() + + if (!rawOutput) { + throw new Error(`failed to run the pipeline (no output)`) + } + + const imagePaths = rawOutput.images?.images.map((img: any) => + api.getPathImage(img) + ) + + console.log(`imagePaths:`, imagePaths) + + const imagePath = imagePaths.at(0) + if (!imagePath) { + throw new Error(`failed to run the pipeline (no image)`) + } + + // TODO: check what the imagePath looks like exactly + const assetUrl = await decodeOutput(imagePath) + + console.log(`assetUrl:`, imagePaths) + segment.assetUrl = assetUrl + segment.assetSourceType = ClapAssetSource.DATA - console.log(`result:`, result) + // TODO: } else { throw new Error( `Clapper doesn't support ${request.segment.category} generation for provider "ComfyUI". Please open a pull request with (working code) to solve this!` diff --git a/src/app/api/resolve/providers/comfyui/temporary_demo.json b/src/app/api/resolve/providers/comfyui/temporary_demo.json deleted file mode 100644 index 589190ffc9f1dd8cd0f2575bdb63287a5e463135..0000000000000000000000000000000000000000 --- a/src/app/api/resolve/providers/comfyui/temporary_demo.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "3": { - "inputs": { - "seed": 303533494192794, - "steps": 20, - "cfg": 8, - "sampler_name": "euler", - "scheduler": "normal", - "denoise": 1, - "model": ["4", 0], - "positive": ["6", 0], - "negative": ["7", 0], - "latent_image": ["5", 0] - }, - "class_type": "KSampler", - "_meta": { - "title": "KSampler" - } - }, - "4": { - "inputs": { - "ckpt_name": "SD15/counterfeitV30_v30.safetensors" - }, - "class_type": "CheckpointLoaderSimple", - "_meta": { - "title": "Load Checkpoint" - } - }, - "5": { - "inputs": { - "width": 512, - "height": 512, - "batch_size": 1 - }, - "class_type": "EmptyLatentImage", - "_meta": { - "title": "Empty Latent Image" - } - }, - "6": { - "inputs": { - "text": "beautiful scenery nature glass bottle landscape, , purple galaxy bottle,", - "clip": ["4", 1] - }, - "class_type": "CLIPTextEncode", - "_meta": { - "title": "CLIP Text Encode (Prompt)" - } - }, - "7": { - "inputs": { - "text": "text, watermark", - "clip": ["4", 1] - }, - "class_type": "CLIPTextEncode", - "_meta": { - "title": "CLIP Text Encode (Prompt)" - } - }, - "8": { - "inputs": { - "samples": ["3", 0], - "vae": ["4", 2] - }, - "class_type": "VAEDecode", - "_meta": { - "title": "VAE Decode" - } - }, - "9": { - "inputs": { - "filename_prefix": "ComfyUI", - "images": ["8", 0] - }, - "class_type": "SaveImage", - "_meta": { - "title": "Save Image" - } - } -} diff --git a/src/app/embed/embed.tsx b/src/app/embed/embed.tsx index b32005d3d1903e80e2d872367d4046e2f36e882c..2a185eb6e9ac221fd2dd3a8a7c1e195e2e8423f9 100644 --- a/src/app/embed/embed.tsx +++ b/src/app/embed/embed.tsx @@ -23,7 +23,7 @@ export function Embed() {
diff --git a/src/app/main.tsx b/src/app/main.tsx index bafc941a906b496e0091726a064fd0e0adb9c2e0..2e66962306df689ad227076f92561818f16aa79b 100644 --- a/src/app/main.tsx +++ b/src/app/main.tsx @@ -6,6 +6,7 @@ import { useSearchParams } from 'next/navigation' import { DndProvider, useDrop } from 'react-dnd' import { HTML5Backend, NativeTypes } from 'react-dnd-html5-backend' import { UIWindowLayout } from '@aitube/clapper-services' +import { ErrorBoundary, FallbackProps } from 'react-error-boundary' import { Toaster } from '@/components/ui/sonner' import { cn } from '@/lib/utils' @@ -14,12 +15,10 @@ import { Monitor } from '@/components/monitor' import { SettingsDialog } from '@/components/settings' import { LoadingDialog } from '@/components/dialogs/loader/LoadingDialog' -import { useUI, useIO } from '@/services' import { TopBar } from '@/components/toolbars/top-bar' import { Timeline } from '@/components/core/timeline' import { ChatView } from '@/components/assistant/ChatView' import { Editors } from '@/components/editors/Editors' -import { useTheme } from '@/services/ui/useTheme' import { BottomToolbar } from '@/components/toolbars/bottom-bar' import { FruityDesktop, FruityWindow } from '@/components/windows' import { ScriptEditor } from '@/components/editors/ScriptEditor' @@ -27,7 +26,10 @@ import { SegmentEditor } from '@/components/editors/SegmentEditor' import { EntityEditor } from '@/components/editors/EntityEditor' import { WorkflowEditor } from '@/components/editors/WorkflowEditor' import { FilterEditor } from '@/components/editors/FilterEditor' + +import { useUI, useIO, useTheme } from '@/services' import { useRenderLoop } from '@/services/renderer' +import { useDynamicWorkflows } from '@/services/editors/workflow-editor/useDynamicWorkflows' type DroppableThing = { files: File[] } @@ -47,6 +49,10 @@ function MainContent() { // perform its routine even when the monitor component is hidden useRenderLoop() + // this has to be done at the root of the app, that way it can + // sync workflows even when the workflow component is hidden + useDynamicWorkflows() + const [{ isOver, canDrop }, connectFileDrop] = useDrop({ accept: [NativeTypes.FILE], drop: (item: DroppableThing): void => { @@ -229,7 +235,7 @@ function MainContent() { : 'pointer-events-none hidden', `fixed top-9 h-[calc(100vh-36px)] w-screen flex-row overflow-hidden`, `items-center justify-center`, - `bg-stone-950` + `bg-neutral-950` )} >
+

Something went wrong:

+
{error.message}
+
+ ) +} + export function Main() { return ( - + + + ) diff --git a/src/components/dialogs/iframe-warning/index.tsx b/src/components/dialogs/iframe-warning/index.tsx index 89668ca0ce5d998b420bee9ecf5b72bb0c29288d..0c07b8cccf6c96ccff2b2e98a5502f7afe223d12 100644 --- a/src/components/dialogs/iframe-warning/index.tsx +++ b/src/components/dialogs/iframe-warning/index.tsx @@ -18,7 +18,7 @@ export function IframeWarning() { return (

diff --git a/src/components/dialogs/loader/index.tsx b/src/components/dialogs/loader/index.tsx index f455146abe3faf2c284c44b9bb7359a160bf56a8..b34f78415b82e4c417821c9833a0dd686ce5c6a2 100644 --- a/src/components/dialogs/loader/index.tsx +++ b/src/components/dialogs/loader/index.tsx @@ -27,7 +27,7 @@ export function DeprecatedLoader({ )} >

Loading.. diff --git a/src/components/forms/FormSection.tsx b/src/components/forms/FormSection.tsx index 0ac12306dfb9806822af4fab4d4409c3351d51c2..359bc34ac89d919c8cb340ef005106e0ba20dde7 100644 --- a/src/components/forms/FormSection.tsx +++ b/src/components/forms/FormSection.tsx @@ -15,7 +15,7 @@ export function FormSection({

diff --git a/src/components/mobile/MobileMenu.tsx b/src/components/mobile/MobileMenu.tsx new file mode 100644 index 0000000000000000000000000000000000000000..de926383dc11c32cc1618a5d13b4465cec31ebbc --- /dev/null +++ b/src/components/mobile/MobileMenu.tsx @@ -0,0 +1,25 @@ +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, + SheetTrigger, +} from '@/components/ui/sheet' + +export function MobileMenu() { + return ( + + Open + + + Clapper + + TODO JULIAN: we need a way to display the hierarchical menu on + mobile + + + + + ) +} diff --git a/src/components/monitor/StaticPlayer/index.tsx b/src/components/monitor/StaticPlayer/index.tsx index 7c93297c9e361ba4907eaa79794da0313a17f1c8..01505eda6cb12d93aa9ab248ce5e4f3c080773d0 100644 --- a/src/components/monitor/StaticPlayer/index.tsx +++ b/src/components/monitor/StaticPlayer/index.tsx @@ -54,7 +54,7 @@ export function StaticPlayer( }, [setStaticVideoRef]) const placeholder = ( -
+
{error ? {error} : No video yet}
) @@ -85,7 +85,7 @@ export function StaticPlayer( // autoPlay // muted loop - className="h-full rounded-lg border border-stone-950 object-cover" + className="h-full rounded-lg border border-neutral-950 object-cover" style={{}} /> ) : ( diff --git a/src/components/settings/index.tsx b/src/components/settings/index.tsx index f68b66d87139258d97bfb7b6f7eccc60ad15543b..a054d503eb6255b8a744a2762de7991a17792426 100644 --- a/src/components/settings/index.tsx +++ b/src/components/settings/index.tsx @@ -67,7 +67,7 @@ export function SettingsDialog() {
-
+
{panels[showSettings]}