Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
Commit
β’
59f9b88
1
Parent(s):
dd3ca23
adding some menus
Browse filesThis view is limited to 50 files because it contains too many changes. Β
See raw diff
- package-lock.json +55 -13
- package.json +3 -1
- src/app/DEPRECATED_main.txt +143 -0
- src/app/layout.tsx +8 -4
- src/app/main.tsx +22 -108
- src/app/page.tsx +1 -5
- src/app/{globals.css β styles/globals.css} +0 -0
- src/app/{react-reflex-custom.css β styles/react-reflex-custom.css} +6 -6
- src/app/{react-reflex.css β styles/react-reflex.css} +0 -0
- src/components/form/form-dir.tsx +58 -0
- src/components/form/form-field.tsx +29 -0
- src/components/form/form-file.tsx +58 -0
- src/components/form/form-input.tsx +108 -0
- src/components/form/form-label.tsx +14 -0
- src/components/form/form-radio.tsx +36 -0
- src/components/form/form-section.tsx +24 -0
- src/components/form/form-select.tsx +67 -0
- src/components/interface/loader/index.tsx +39 -0
- src/components/interface/static-video/index.tsx +1 -1
- src/components/interface/system-menu/index.tsx +236 -0
- src/components/interface/timeline/index.tsx +1 -1
- src/components/interface/top-bar/index.tsx +30 -0
- src/components/interface/top-menu/file/index.tsx +72 -0
- src/components/interface/top-menu/index.tsx +20 -0
- src/components/interface/top-menu/rendering/index.tsx +141 -0
- src/components/interface/top-menu/view/index.tsx +98 -0
- src/components/settings/SettingsSectionRendering.tsx +19 -0
- src/components/settings/index.tsx +57 -0
- src/components/ui/menubar.tsx +3 -3
- src/components/ui/scroll-area.tsx +48 -0
- src/lib/core/DEPRECATED_getSettings.txt +26 -0
- src/{components/interface/settings/constants.tsx β lib/core/DEPRECATED_localStorageKeys.txt} +0 -0
- src/lib/core/constants.ts +4 -0
- src/lib/core/getDefaultSettings.ts +3 -3
- src/lib/hooks/index.ts +6 -0
- src/lib/hooks/useClapFilePicker.ts +37 -0
- src/lib/hooks/useFullscreenStatus.ts +78 -0
- src/lib/hooks/useRequestAnimationFrame.ts +1 -2
- src/lib/hooks/useScreenplayFilePicker.ts +41 -0
- src/lib/utils/getValidBoolean.ts +9 -0
- src/lib/utils/getValidNumber.ts +10 -0
- src/lib/utils/getValidString.ts +8 -0
- src/lib/utils/index.ts +9 -1
- src/lib/utils/isValidNumber.ts +3 -0
- src/lib/utils/parseComfyVendor.ts +28 -0
- src/lib/utils/parseRenderingStrategy.ts +31 -0
- src/server/comfy/{formatStoryboardWorkflow.ts β getComfyWorkflow.ts} +15 -4
- src/server/comfy/index.ts +7 -4
- src/server/comfy/replicate.ts +1 -0
- src/settings/rendering/getDefaultSettingsRendering.ts +15 -0
package-lock.json
CHANGED
@@ -10,7 +10,7 @@
|
|
10 |
"dependencies": {
|
11 |
"@aitube/clap": "0.0.24",
|
12 |
"@aitube/engine": "0.0.15",
|
13 |
-
"@aitube/timeline": "0.0.
|
14 |
"@huggingface/hub": "^0.15.0",
|
15 |
"@radix-ui/react-accordion": "^1.1.2",
|
16 |
"@radix-ui/react-avatar": "^1.0.4",
|
@@ -22,6 +22,7 @@
|
|
22 |
"@radix-ui/react-label": "^2.0.2",
|
23 |
"@radix-ui/react-menubar": "^1.0.4",
|
24 |
"@radix-ui/react-popover": "^1.0.7",
|
|
|
25 |
"@radix-ui/react-select": "^2.0.0",
|
26 |
"@radix-ui/react-separator": "^1.0.3",
|
27 |
"@radix-ui/react-slider": "^1.1.2",
|
@@ -59,6 +60,7 @@
|
|
59 |
"react-dom": "^18.3.1",
|
60 |
"react-drag-drop-files": "^2.3.10",
|
61 |
"react-hook-consent": "^3.5.3",
|
|
|
62 |
"react-icons": "^5.2.1",
|
63 |
"react-reflex": "^4.2.6",
|
64 |
"replicate": "^0.30.1",
|
@@ -97,9 +99,9 @@
|
|
97 |
}
|
98 |
},
|
99 |
"node_modules/@aitube/timeline": {
|
100 |
-
"version": "0.0.
|
101 |
-
"resolved": "https://registry.npmjs.org/@aitube/timeline/-/timeline-0.0.
|
102 |
-
"integrity": "sha512
|
103 |
"dependencies": {
|
104 |
"date-fns": "^3.6.0",
|
105 |
"react-virtualized-auto-sizer": "^1.0.24"
|
@@ -610,9 +612,9 @@
|
|
610 |
}
|
611 |
},
|
612 |
"node_modules/@eslint-community/regexpp": {
|
613 |
-
"version": "4.10.
|
614 |
-
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.
|
615 |
-
"integrity": "sha512-
|
616 |
"engines": {
|
617 |
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
|
618 |
}
|
@@ -2130,6 +2132,37 @@
|
|
2130 |
}
|
2131 |
}
|
2132 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2133 |
"node_modules/@radix-ui/react-select": {
|
2134 |
"version": "2.0.0",
|
2135 |
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.0.0.tgz",
|
@@ -2715,9 +2748,9 @@
|
|
2715 |
}
|
2716 |
},
|
2717 |
"node_modules/@react-three/fiber": {
|
2718 |
-
"version": "8.16.
|
2719 |
-
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.16.
|
2720 |
-
"integrity": "sha512-
|
2721 |
"dependencies": {
|
2722 |
"@babel/runtime": "^7.17.8",
|
2723 |
"@types/react-reconciler": "^0.26.7",
|
@@ -4436,9 +4469,9 @@
|
|
4436 |
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
|
4437 |
},
|
4438 |
"node_modules/electron-to-chromium": {
|
4439 |
-
"version": "1.4.
|
4440 |
-
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.
|
4441 |
-
"integrity": "sha512-
|
4442 |
},
|
4443 |
"node_modules/emoji-regex": {
|
4444 |
"version": "9.2.2",
|
@@ -7349,6 +7382,15 @@
|
|
7349 |
"react-dom": ">=16.8.0"
|
7350 |
}
|
7351 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7352 |
"node_modules/react-icons": {
|
7353 |
"version": "5.2.1",
|
7354 |
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.2.1.tgz",
|
|
|
10 |
"dependencies": {
|
11 |
"@aitube/clap": "0.0.24",
|
12 |
"@aitube/engine": "0.0.15",
|
13 |
+
"@aitube/timeline": "0.0.8",
|
14 |
"@huggingface/hub": "^0.15.0",
|
15 |
"@radix-ui/react-accordion": "^1.1.2",
|
16 |
"@radix-ui/react-avatar": "^1.0.4",
|
|
|
22 |
"@radix-ui/react-label": "^2.0.2",
|
23 |
"@radix-ui/react-menubar": "^1.0.4",
|
24 |
"@radix-ui/react-popover": "^1.0.7",
|
25 |
+
"@radix-ui/react-scroll-area": "^1.0.5",
|
26 |
"@radix-ui/react-select": "^2.0.0",
|
27 |
"@radix-ui/react-separator": "^1.0.3",
|
28 |
"@radix-ui/react-slider": "^1.1.2",
|
|
|
60 |
"react-dom": "^18.3.1",
|
61 |
"react-drag-drop-files": "^2.3.10",
|
62 |
"react-hook-consent": "^3.5.3",
|
63 |
+
"react-hotkeys-hook": "^4.5.0",
|
64 |
"react-icons": "^5.2.1",
|
65 |
"react-reflex": "^4.2.6",
|
66 |
"replicate": "^0.30.1",
|
|
|
99 |
}
|
100 |
},
|
101 |
"node_modules/@aitube/timeline": {
|
102 |
+
"version": "0.0.8",
|
103 |
+
"resolved": "https://registry.npmjs.org/@aitube/timeline/-/timeline-0.0.8.tgz",
|
104 |
+
"integrity": "sha512-+Eqhdr2tGCU46r9mJ+H/so+QB6BObqerI5j3oG2iSaMSalFIgP1K8zuigMhN/FzKkESF3ORY77ciZnZupcmBDA==",
|
105 |
"dependencies": {
|
106 |
"date-fns": "^3.6.0",
|
107 |
"react-virtualized-auto-sizer": "^1.0.24"
|
|
|
612 |
}
|
613 |
},
|
614 |
"node_modules/@eslint-community/regexpp": {
|
615 |
+
"version": "4.10.1",
|
616 |
+
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.1.tgz",
|
617 |
+
"integrity": "sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==",
|
618 |
"engines": {
|
619 |
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
|
620 |
}
|
|
|
2132 |
}
|
2133 |
}
|
2134 |
},
|
2135 |
+
"node_modules/@radix-ui/react-scroll-area": {
|
2136 |
+
"version": "1.0.5",
|
2137 |
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.0.5.tgz",
|
2138 |
+
"integrity": "sha512-b6PAgH4GQf9QEn8zbT2XUHpW5z8BzqEc7Kl11TwDrvuTrxlkcjTD5qa/bxgKr+nmuXKu4L/W5UZ4mlP/VG/5Gw==",
|
2139 |
+
"dependencies": {
|
2140 |
+
"@babel/runtime": "^7.13.10",
|
2141 |
+
"@radix-ui/number": "1.0.1",
|
2142 |
+
"@radix-ui/primitive": "1.0.1",
|
2143 |
+
"@radix-ui/react-compose-refs": "1.0.1",
|
2144 |
+
"@radix-ui/react-context": "1.0.1",
|
2145 |
+
"@radix-ui/react-direction": "1.0.1",
|
2146 |
+
"@radix-ui/react-presence": "1.0.1",
|
2147 |
+
"@radix-ui/react-primitive": "1.0.3",
|
2148 |
+
"@radix-ui/react-use-callback-ref": "1.0.1",
|
2149 |
+
"@radix-ui/react-use-layout-effect": "1.0.1"
|
2150 |
+
},
|
2151 |
+
"peerDependencies": {
|
2152 |
+
"@types/react": "*",
|
2153 |
+
"@types/react-dom": "*",
|
2154 |
+
"react": "^16.8 || ^17.0 || ^18.0",
|
2155 |
+
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
2156 |
+
},
|
2157 |
+
"peerDependenciesMeta": {
|
2158 |
+
"@types/react": {
|
2159 |
+
"optional": true
|
2160 |
+
},
|
2161 |
+
"@types/react-dom": {
|
2162 |
+
"optional": true
|
2163 |
+
}
|
2164 |
+
}
|
2165 |
+
},
|
2166 |
"node_modules/@radix-ui/react-select": {
|
2167 |
"version": "2.0.0",
|
2168 |
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.0.0.tgz",
|
|
|
2748 |
}
|
2749 |
},
|
2750 |
"node_modules/@react-three/fiber": {
|
2751 |
+
"version": "8.16.8",
|
2752 |
+
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.16.8.tgz",
|
2753 |
+
"integrity": "sha512-Lc8fjATtvQEfSd8d5iKdbpHtRm/aPMeFj7jQvp6TNHfpo8IQTW3wwcE1ZMrGGoUH+w2mnyS+0MK1NLPLnuzGkQ==",
|
2754 |
"dependencies": {
|
2755 |
"@babel/runtime": "^7.17.8",
|
2756 |
"@types/react-reconciler": "^0.26.7",
|
|
|
4469 |
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
|
4470 |
},
|
4471 |
"node_modules/electron-to-chromium": {
|
4472 |
+
"version": "1.4.789",
|
4473 |
+
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.789.tgz",
|
4474 |
+
"integrity": "sha512-0VbyiaXoT++Fi2vHGo2ThOeS6X3vgRCWrjPeO2FeIAWL6ItiSJ9BqlH8LfCXe3X1IdcG+S0iLoNaxQWhfZoGzQ=="
|
4475 |
},
|
4476 |
"node_modules/emoji-regex": {
|
4477 |
"version": "9.2.2",
|
|
|
7382 |
"react-dom": ">=16.8.0"
|
7383 |
}
|
7384 |
},
|
7385 |
+
"node_modules/react-hotkeys-hook": {
|
7386 |
+
"version": "4.5.0",
|
7387 |
+
"resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.5.0.tgz",
|
7388 |
+
"integrity": "sha512-Samb85GSgAWFQNvVt3PS90LPPGSf9mkH/r4au81ZP1yOIFayLC3QAvqTgGtJ8YEDMXtPmaVBs6NgipHO6h4Mug==",
|
7389 |
+
"peerDependencies": {
|
7390 |
+
"react": ">=16.8.1",
|
7391 |
+
"react-dom": ">=16.8.1"
|
7392 |
+
}
|
7393 |
+
},
|
7394 |
"node_modules/react-icons": {
|
7395 |
"version": "5.2.1",
|
7396 |
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.2.1.tgz",
|
package.json
CHANGED
@@ -12,7 +12,7 @@
|
|
12 |
"dependencies": {
|
13 |
"@aitube/clap": "0.0.24",
|
14 |
"@aitube/engine": "0.0.15",
|
15 |
-
"@aitube/timeline": "0.0.
|
16 |
"@huggingface/hub": "^0.15.0",
|
17 |
"@radix-ui/react-accordion": "^1.1.2",
|
18 |
"@radix-ui/react-avatar": "^1.0.4",
|
@@ -24,6 +24,7 @@
|
|
24 |
"@radix-ui/react-label": "^2.0.2",
|
25 |
"@radix-ui/react-menubar": "^1.0.4",
|
26 |
"@radix-ui/react-popover": "^1.0.7",
|
|
|
27 |
"@radix-ui/react-select": "^2.0.0",
|
28 |
"@radix-ui/react-separator": "^1.0.3",
|
29 |
"@radix-ui/react-slider": "^1.1.2",
|
@@ -61,6 +62,7 @@
|
|
61 |
"react-dom": "^18.3.1",
|
62 |
"react-drag-drop-files": "^2.3.10",
|
63 |
"react-hook-consent": "^3.5.3",
|
|
|
64 |
"react-icons": "^5.2.1",
|
65 |
"react-reflex": "^4.2.6",
|
66 |
"replicate": "^0.30.1",
|
|
|
12 |
"dependencies": {
|
13 |
"@aitube/clap": "0.0.24",
|
14 |
"@aitube/engine": "0.0.15",
|
15 |
+
"@aitube/timeline": "0.0.8",
|
16 |
"@huggingface/hub": "^0.15.0",
|
17 |
"@radix-ui/react-accordion": "^1.1.2",
|
18 |
"@radix-ui/react-avatar": "^1.0.4",
|
|
|
24 |
"@radix-ui/react-label": "^2.0.2",
|
25 |
"@radix-ui/react-menubar": "^1.0.4",
|
26 |
"@radix-ui/react-popover": "^1.0.7",
|
27 |
+
"@radix-ui/react-scroll-area": "^1.0.5",
|
28 |
"@radix-ui/react-select": "^2.0.0",
|
29 |
"@radix-ui/react-separator": "^1.0.3",
|
30 |
"@radix-ui/react-slider": "^1.1.2",
|
|
|
62 |
"react-dom": "^18.3.1",
|
63 |
"react-drag-drop-files": "^2.3.10",
|
64 |
"react-hook-consent": "^3.5.3",
|
65 |
+
"react-hotkeys-hook": "^4.5.0",
|
66 |
"react-icons": "^5.2.1",
|
67 |
"react-reflex": "^4.2.6",
|
68 |
"replicate": "^0.30.1",
|
src/app/DEPRECATED_main.txt
ADDED
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import React, { useEffect, useState } from "react"
|
4 |
+
import { FileUploader } from "react-drag-drop-files"
|
5 |
+
import {
|
6 |
+
ReflexContainer,
|
7 |
+
ReflexSplitter,
|
8 |
+
ReflexElement
|
9 |
+
} from 'react-reflex'
|
10 |
+
import { parseClap } from "@aitube/clap"
|
11 |
+
import { ClapTimeline, useTimelineState } from "@aitube/timeline"
|
12 |
+
|
13 |
+
import { Toaster } from "@/components/ui/sonner"
|
14 |
+
import { cn } from "@/lib/utils"
|
15 |
+
import { TooltipProvider } from "@/components/ui/tooltip"
|
16 |
+
import { useQueryStringParams } from "@/lib/hooks/useQueryStringParams"
|
17 |
+
import { StaticVideo } from "@/components/interface/static-video"
|
18 |
+
import { RenderClap } from "@/components/interface/render-clap"
|
19 |
+
import { Timeline } from "@/components/interface/timeline"
|
20 |
+
import { TopBar } from "@/components/interface/top-bar"
|
21 |
+
|
22 |
+
export function Main() {
|
23 |
+
const { clapUrl } = useQueryStringParams({
|
24 |
+
// clapUrl: `/samples/test.clap`,
|
25 |
+
// clapUrl: `/samples/Afterglow%20v10%20X%20Rewrite%20Bryan%20E.%20Harris%202023.clap`,
|
26 |
+
clapUrl: '',
|
27 |
+
})
|
28 |
+
|
29 |
+
const [isImporting, setIsImporting] = useState(false)
|
30 |
+
const clap = useTimelineState(s => s.clap)
|
31 |
+
const setClap = useTimelineState(s => s.setClap)
|
32 |
+
const isLoading = useTimelineState(s => s.isLoading)
|
33 |
+
const isEmpty = useTimelineState(s => s.isEmpty)
|
34 |
+
|
35 |
+
const isBusy = isLoading || isImporting
|
36 |
+
|
37 |
+
const handleChange = async (file: File) => {
|
38 |
+
if (file.name.endsWith("clap")) {
|
39 |
+
setIsImporting(true)
|
40 |
+
try {
|
41 |
+
const clap = await parseClap(file)
|
42 |
+
await setClap(clap)
|
43 |
+
setTimeout(() => {
|
44 |
+
setIsImporting(false)
|
45 |
+
}, 500)
|
46 |
+
} catch (err) {
|
47 |
+
console.error(err)
|
48 |
+
setIsImporting(false)
|
49 |
+
}
|
50 |
+
} else {
|
51 |
+
setIsImporting(true)
|
52 |
+
try {
|
53 |
+
const res = await fetch("https://jbilcke-hf-broadway-api.hf.space", {
|
54 |
+
method: "POST",
|
55 |
+
headers: { 'Content-Type': 'text/plain' },
|
56 |
+
body: await file.text(),
|
57 |
+
})
|
58 |
+
const blob = await res.blob()
|
59 |
+
const clap = await parseClap(blob)
|
60 |
+
await setClap(clap)
|
61 |
+
setTimeout(() => {
|
62 |
+
setIsImporting(false)
|
63 |
+
}, 1000)
|
64 |
+
} catch (err) {
|
65 |
+
console.error(err)
|
66 |
+
setIsImporting(false)
|
67 |
+
}
|
68 |
+
}
|
69 |
+
};
|
70 |
+
|
71 |
+
useEffect(() => {
|
72 |
+
(async () => {
|
73 |
+
if (!clapUrl) {
|
74 |
+
console.log("No clap URL provided")
|
75 |
+
return
|
76 |
+
}
|
77 |
+
setIsImporting(true)
|
78 |
+
const res = await fetch(clapUrl)
|
79 |
+
const blob = await res.blob()
|
80 |
+
const clap = await parseClap(blob)
|
81 |
+
await setClap(clap)
|
82 |
+
setTimeout(() => {
|
83 |
+
setIsImporting(false)
|
84 |
+
}, 1000)
|
85 |
+
})()
|
86 |
+
}, [clapUrl])
|
87 |
+
|
88 |
+
return (
|
89 |
+
<TooltipProvider>
|
90 |
+
<div className={cn(`
|
91 |
+
fixed
|
92 |
+
flex flex-col
|
93 |
+
w-screen h-screen
|
94 |
+
overflow-hidden
|
95 |
+
items-center justify-center
|
96 |
+
bg-[rgb(58,54,50)]
|
97 |
+
`
|
98 |
+
)}
|
99 |
+
>
|
100 |
+
<TopBar />
|
101 |
+
<div className="flex flex-row flex-grow w-full overflow-hidden">
|
102 |
+
{clap
|
103 |
+
?
|
104 |
+
<ReflexContainer orientation="horizontal">
|
105 |
+
<ReflexElement>
|
106 |
+
<RenderClap />
|
107 |
+
</ReflexElement>
|
108 |
+
<ReflexSplitter />
|
109 |
+
<ReflexElement className={isLoading ? 'opacity-0' : 'opacity-100'}>
|
110 |
+
<Timeline />
|
111 |
+
</ReflexElement>
|
112 |
+
</ReflexContainer>
|
113 |
+
:
|
114 |
+
<FileUploader
|
115 |
+
handleChange={handleChange}
|
116 |
+
name="file" types={["txt", "clap"]}>
|
117 |
+
<div className={cn(`
|
118 |
+
flex
|
119 |
+
w-screen h-screen
|
120 |
+
overflow-hidden
|
121 |
+
items-center justify-center
|
122 |
+
cursor-pointer
|
123 |
+
`, isBusy ? 'animate-pulse' : '')}
|
124 |
+
style={{
|
125 |
+
backgroundImage: "repeating-radial-gradient( circle at 0 0, transparent 0, #000000 7px ), repeating-linear-gradient( #34353655, #343536 )"
|
126 |
+
}}><p
|
127 |
+
className="text-stone-100 font-sans font-thin text-[3vw]"
|
128 |
+
style={{ textShadow: "#000 1px 0 3px" }}
|
129 |
+
>
|
130 |
+
{isBusy ? 'Loading..' :
|
131 |
+
<span className="flex flex-center justify-center text-center w-full">
|
132 |
+
Click to import a screenplay (.txt)<br/>
|
133 |
+
or an OpenClap file (.clap)</span>}
|
134 |
+
</p>
|
135 |
+
</div>
|
136 |
+
</FileUploader>
|
137 |
+
}
|
138 |
+
<Toaster />
|
139 |
+
</div>
|
140 |
+
</div>
|
141 |
+
</TooltipProvider>
|
142 |
+
);
|
143 |
+
}
|
src/app/layout.tsx
CHANGED
@@ -1,7 +1,8 @@
|
|
1 |
import { cn } from '@/lib/utils'
|
2 |
-
|
3 |
-
import
|
4 |
-
import "./react-reflex
|
|
|
5 |
|
6 |
import type { Metadata } from 'next'
|
7 |
import { inter } from './fonts'
|
@@ -22,7 +23,10 @@ export default function RootLayout({
|
|
22 |
`h-full w-full overflow-none dark`,
|
23 |
inter.className
|
24 |
)}
|
25 |
-
style={{
|
|
|
|
|
|
|
26 |
{children}
|
27 |
</body>
|
28 |
</html>
|
|
|
1 |
import { cn } from '@/lib/utils'
|
2 |
+
|
3 |
+
import './styles/globals.css'
|
4 |
+
import "./styles/react-reflex.css"
|
5 |
+
import "./styles/react-reflex-custom.css"
|
6 |
|
7 |
import type { Metadata } from 'next'
|
8 |
import { inter } from './fonts'
|
|
|
23 |
`h-full w-full overflow-none dark`,
|
24 |
inter.className
|
25 |
)}
|
26 |
+
style={{
|
27 |
+
overscrollBehaviorX: "none",
|
28 |
+
backgroundImage: "repeating-radial-gradient( circle at 0 0, transparent 0, #000000 7px ), repeating-linear-gradient( #34353655, #343536 )"
|
29 |
+
}}>
|
30 |
{children}
|
31 |
</body>
|
32 |
</html>
|
src/app/main.tsx
CHANGED
@@ -1,138 +1,52 @@
|
|
1 |
"use client"
|
2 |
|
3 |
import React, { useEffect, useState } from "react"
|
4 |
-
import { FileUploader } from "react-drag-drop-files"
|
5 |
import {
|
6 |
ReflexContainer,
|
7 |
ReflexSplitter,
|
8 |
ReflexElement
|
9 |
} from 'react-reflex'
|
10 |
-
import { parseClap } from "@aitube/clap"
|
11 |
-
import { ClapTimeline, useTimelineState } from "@aitube/timeline"
|
12 |
|
13 |
import { Toaster } from "@/components/ui/sonner"
|
14 |
import { cn } from "@/lib/utils"
|
15 |
import { TooltipProvider } from "@/components/ui/tooltip"
|
16 |
-
import { useQueryStringParams } from "@/lib/hooks/useQueryStringParams"
|
17 |
-
import { StaticVideo } from "@/components/interface/static-video"
|
18 |
import { RenderClap } from "@/components/interface/render-clap"
|
19 |
import { Timeline } from "@/components/interface/timeline"
|
|
|
|
|
20 |
|
21 |
export function Main() {
|
22 |
-
const { clapUrl } = useQueryStringParams({
|
23 |
-
// clapUrl: `/samples/test.clap`,
|
24 |
-
// clapUrl: `/samples/Afterglow%20v10%20X%20Rewrite%20Bryan%20E.%20Harris%202023.clap`,
|
25 |
-
clapUrl: '',
|
26 |
-
})
|
27 |
-
|
28 |
-
const [isImporting, setIsImporting] = useState(false)
|
29 |
-
const clap = useTimelineState(s => s.clap)
|
30 |
-
const setClap = useTimelineState(s => s.setClap)
|
31 |
-
const isLoading = useTimelineState(s => s.isLoading)
|
32 |
const isEmpty = useTimelineState(s => s.isEmpty)
|
33 |
-
|
34 |
-
const isBusy = isLoading || isImporting
|
35 |
-
|
36 |
-
const handleChange = async (file: File) => {
|
37 |
-
if (file.name.endsWith("clap")) {
|
38 |
-
setIsImporting(true)
|
39 |
-
try {
|
40 |
-
const clap = await parseClap(file)
|
41 |
-
setClap(clap)
|
42 |
-
setTimeout(() => {
|
43 |
-
setIsImporting(false)
|
44 |
-
}, 500)
|
45 |
-
} catch (err) {
|
46 |
-
console.error(err)
|
47 |
-
setIsImporting(false)
|
48 |
-
}
|
49 |
-
} else {
|
50 |
-
setIsImporting(true)
|
51 |
-
try {
|
52 |
-
const res = await fetch("https://jbilcke-hf-broadway-api.hf.space", {
|
53 |
-
method: "POST",
|
54 |
-
headers: { 'Content-Type': 'text/plain' },
|
55 |
-
body: await file.text(),
|
56 |
-
})
|
57 |
-
const blob = await res.blob()
|
58 |
-
const clap = await parseClap(blob)
|
59 |
-
setClap(clap)
|
60 |
-
setTimeout(() => {
|
61 |
-
setIsImporting(false)
|
62 |
-
}, 1000)
|
63 |
-
} catch (err) {
|
64 |
-
console.error(err)
|
65 |
-
setIsImporting(false)
|
66 |
-
}
|
67 |
-
}
|
68 |
-
};
|
69 |
-
|
70 |
-
useEffect(() => {
|
71 |
-
(async () => {
|
72 |
-
if (!clapUrl) {
|
73 |
-
console.log("No clap URL provided")
|
74 |
-
return
|
75 |
-
}
|
76 |
-
setIsImporting(true)
|
77 |
-
const res = await fetch(clapUrl)
|
78 |
-
const blob = await res.blob()
|
79 |
-
const clap = await parseClap(blob)
|
80 |
-
setClap(clap)
|
81 |
-
setTimeout(() => {
|
82 |
-
setIsImporting(false)
|
83 |
-
}, 1000)
|
84 |
-
})()
|
85 |
-
}, [clapUrl])
|
86 |
-
|
87 |
return (
|
88 |
<TooltipProvider>
|
89 |
<div className={cn(`
|
90 |
fixed
|
91 |
-
flex
|
92 |
-
w-screen h-screen
|
93 |
-
overflow-hidden
|
94 |
-
items-center justify-center
|
95 |
-
bg-[rgb(58,54,50)]
|
96 |
-
`
|
97 |
-
)}
|
98 |
-
>
|
99 |
-
{clap
|
100 |
-
?
|
101 |
-
<ReflexContainer orientation="horizontal">
|
102 |
-
<ReflexElement>
|
103 |
-
<RenderClap />
|
104 |
-
</ReflexElement>
|
105 |
-
<ReflexSplitter />
|
106 |
-
<ReflexElement className={isLoading ? 'opacity-0' : 'opacity-100'}>
|
107 |
-
<Timeline />
|
108 |
-
</ReflexElement>
|
109 |
-
</ReflexContainer>
|
110 |
-
:
|
111 |
-
<FileUploader
|
112 |
-
handleChange={handleChange}
|
113 |
-
name="file" types={["txt", "clap"]}>
|
114 |
-
<div className={cn(`
|
115 |
-
flex
|
116 |
w-screen h-screen
|
117 |
overflow-hidden
|
118 |
items-center justify-center
|
119 |
-
|
120 |
-
|
121 |
style={{
|
122 |
backgroundImage: "repeating-radial-gradient( circle at 0 0, transparent 0, #000000 7px ), repeating-linear-gradient( #34353655, #343536 )"
|
123 |
-
}}
|
124 |
-
className="text-stone-100 font-sans font-thin text-[3vw]"
|
125 |
-
style={{ textShadow: "#000 1px 0 3px" }}
|
126 |
>
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
136 |
</div>
|
137 |
</TooltipProvider>
|
138 |
);
|
|
|
1 |
"use client"
|
2 |
|
3 |
import React, { useEffect, useState } from "react"
|
|
|
4 |
import {
|
5 |
ReflexContainer,
|
6 |
ReflexSplitter,
|
7 |
ReflexElement
|
8 |
} from 'react-reflex'
|
|
|
|
|
9 |
|
10 |
import { Toaster } from "@/components/ui/sonner"
|
11 |
import { cn } from "@/lib/utils"
|
12 |
import { TooltipProvider } from "@/components/ui/tooltip"
|
|
|
|
|
13 |
import { RenderClap } from "@/components/interface/render-clap"
|
14 |
import { Timeline } from "@/components/interface/timeline"
|
15 |
+
import { TopBar } from "@/components/interface/top-bar"
|
16 |
+
import { useTimelineState } from "@aitube/timeline"
|
17 |
|
18 |
export function Main() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
19 |
const isEmpty = useTimelineState(s => s.isEmpty)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
20 |
return (
|
21 |
<TooltipProvider>
|
22 |
<div className={cn(`
|
23 |
fixed
|
24 |
+
flex flex-col
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
25 |
w-screen h-screen
|
26 |
overflow-hidden
|
27 |
items-center justify-center
|
28 |
+
text-stone-900/90 dark:text-stone-100/90
|
29 |
+
`)}
|
30 |
style={{
|
31 |
backgroundImage: "repeating-radial-gradient( circle at 0 0, transparent 0, #000000 7px ), repeating-linear-gradient( #34353655, #343536 )"
|
32 |
+
}}
|
|
|
|
|
33 |
>
|
34 |
+
<TopBar />
|
35 |
+
<div className={cn(
|
36 |
+
`flex flex-row flex-grow w-full overflow-hidden`,
|
37 |
+
isEmpty ? "opacity-0" : "opacity-100"
|
38 |
+
)}>
|
39 |
+
<ReflexContainer orientation="horizontal">
|
40 |
+
<ReflexElement>
|
41 |
+
<RenderClap />
|
42 |
+
</ReflexElement>
|
43 |
+
<ReflexSplitter />
|
44 |
+
<ReflexElement>
|
45 |
+
<Timeline />
|
46 |
+
</ReflexElement>
|
47 |
+
</ReflexContainer>
|
48 |
+
<Toaster />
|
49 |
+
</div>
|
50 |
</div>
|
51 |
</TooltipProvider>
|
52 |
);
|
src/app/page.tsx
CHANGED
@@ -27,11 +27,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
|
27 |
})(window,document,'script','dataLayer','GTM-K98T8ZFZ');`}</Script>
|
28 |
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-K98T8ZFZ"
|
29 |
height="0" width="0" style={{ display: "none", visibility: "hidden" }}></iframe></noscript>
|
30 |
-
<main
|
31 |
-
``,
|
32 |
-
`text-stone-900/90 dark:text-stone-100/90`,
|
33 |
-
`bg-stone-500`,
|
34 |
-
)}>
|
35 |
{isLoaded && <Main />}
|
36 |
</main>
|
37 |
</>
|
|
|
27 |
})(window,document,'script','dataLayer','GTM-K98T8ZFZ');`}</Script>
|
28 |
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-K98T8ZFZ"
|
29 |
height="0" width="0" style={{ display: "none", visibility: "hidden" }}></iframe></noscript>
|
30 |
+
<main>
|
|
|
|
|
|
|
|
|
31 |
{isLoaded && <Main />}
|
32 |
</main>
|
33 |
</>
|
src/app/{globals.css β styles/globals.css}
RENAMED
File without changes
|
src/app/{react-reflex-custom.css β styles/react-reflex-custom.css}
RENAMED
@@ -10,29 +10,29 @@ with some custom colors and sizes
|
|
10 |
|
11 |
@layer components {
|
12 |
body .reflex-container > .reflex-splitter {
|
13 |
-
@apply transition-all duration-200 ease-in-out bg-
|
14 |
}
|
15 |
|
16 |
body .reflex-container > .reflex-splitter.active,
|
17 |
body .reflex-container > .reflex-splitter:hover {
|
18 |
-
@apply transition-all duration-200 ease-in-out bg-
|
19 |
}
|
20 |
|
21 |
body .horizontal > .reflex-splitter {
|
22 |
-
@apply h-[2px] bg-
|
23 |
}
|
24 |
|
25 |
body .reflex-container.horizontal > .reflex-splitter:hover,
|
26 |
body .reflex-container.horizontal > .reflex-splitter.active {
|
27 |
-
@apply bg-
|
28 |
}
|
29 |
|
30 |
body .reflex-container.vertical > .reflex-splitter {
|
31 |
-
@apply w-[2px] bg-
|
32 |
}
|
33 |
|
34 |
body .reflex-container.vertical > .reflex-splitter:hover,
|
35 |
body .reflex-container.vertical > .reflex-splitter.active {
|
36 |
-
@apply bg-
|
37 |
}
|
38 |
}
|
|
|
10 |
|
11 |
@layer components {
|
12 |
body .reflex-container > .reflex-splitter {
|
13 |
+
@apply transition-all duration-200 ease-in-out bg-stone-800;
|
14 |
}
|
15 |
|
16 |
body .reflex-container > .reflex-splitter.active,
|
17 |
body .reflex-container > .reflex-splitter:hover {
|
18 |
+
@apply transition-all duration-200 ease-in-out bg-stone-400;
|
19 |
}
|
20 |
|
21 |
body .horizontal > .reflex-splitter {
|
22 |
+
@apply h-[2px] bg-stone-800 border-t-stone-900 border-b-stone-800;
|
23 |
}
|
24 |
|
25 |
body .reflex-container.horizontal > .reflex-splitter:hover,
|
26 |
body .reflex-container.horizontal > .reflex-splitter.active {
|
27 |
+
@apply h-[2px] bg-stone-400 border-t-stone-400 border-b-stone-800;
|
28 |
}
|
29 |
|
30 |
body .reflex-container.vertical > .reflex-splitter {
|
31 |
+
@apply w-[2px] bg-stone-800 border-l-stone-800 border-r-stone-800;
|
32 |
}
|
33 |
|
34 |
body .reflex-container.vertical > .reflex-splitter:hover,
|
35 |
body .reflex-container.vertical > .reflex-splitter.active {
|
36 |
+
@apply w-[2px] bg-stone-400 border-l-stone-400 border-r-stone-400;
|
37 |
}
|
38 |
}
|
src/app/{react-reflex.css β styles/react-reflex.css}
RENAMED
File without changes
|
src/components/form/form-dir.tsx
ADDED
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ChangeEvent, useMemo } from "react"
|
2 |
+
|
3 |
+
import { cn } from "@/lib/utils"
|
4 |
+
|
5 |
+
import { Input } from "../ui/input"
|
6 |
+
|
7 |
+
import { FormField } from "./form-field"
|
8 |
+
|
9 |
+
export function FormDir({
|
10 |
+
label,
|
11 |
+
className,
|
12 |
+
placeholder,
|
13 |
+
disabled,
|
14 |
+
onChange,
|
15 |
+
horizontal,
|
16 |
+
accept,
|
17 |
+
}: {
|
18 |
+
label?: string
|
19 |
+
className?: string
|
20 |
+
placeholder?: string
|
21 |
+
disabled?: boolean
|
22 |
+
onChange?: (files: File[]) => void
|
23 |
+
horizontal?: boolean
|
24 |
+
accept?: string
|
25 |
+
}) {
|
26 |
+
|
27 |
+
const handleChange = useMemo(() => (event: ChangeEvent<HTMLInputElement>) => {
|
28 |
+
if (disabled) {
|
29 |
+
return
|
30 |
+
}
|
31 |
+
if (!onChange) {
|
32 |
+
return
|
33 |
+
}
|
34 |
+
|
35 |
+
const files = Array.from(event.currentTarget.files || [])
|
36 |
+
onChange(files)
|
37 |
+
}, [])
|
38 |
+
|
39 |
+
return (
|
40 |
+
<FormField
|
41 |
+
label={
|
42 |
+
`${label}:`
|
43 |
+
}
|
44 |
+
horizontal={horizontal}
|
45 |
+
>
|
46 |
+
<Input
|
47 |
+
placeholder={`${placeholder || ""}`}
|
48 |
+
className={cn(`w-full md:w-52 lg:w-56 xl:w-64 font-normal text-base`, className)}
|
49 |
+
disabled={disabled}
|
50 |
+
onChange={handleChange}
|
51 |
+
type="file"
|
52 |
+
{...{directory: ""} as any} // saw it in stack overflow, but the type isn't recognized here.. hmm
|
53 |
+
webkitdirectory=""
|
54 |
+
accept={accept}
|
55 |
+
/>
|
56 |
+
</FormField>
|
57 |
+
)
|
58 |
+
}
|
src/components/form/form-field.tsx
ADDED
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ReactNode } from "react"
|
2 |
+
|
3 |
+
import { cn } from "@/lib/utils"
|
4 |
+
|
5 |
+
import { FormLabel } from "./form-label"
|
6 |
+
|
7 |
+
export function FormField({ label, children, className, horizontal = false }: {
|
8 |
+
label?: string
|
9 |
+
children?: ReactNode
|
10 |
+
className?: string
|
11 |
+
horizontal?: boolean
|
12 |
+
}) {
|
13 |
+
return (
|
14 |
+
<div className={cn(
|
15 |
+
`flex flex-col space-y-3`,
|
16 |
+
`text-base font-thin text-gray-400`,
|
17 |
+
horizontal ? '' : 'w-full',
|
18 |
+
)}>
|
19 |
+
{label && <FormLabel>{label}</FormLabel>}
|
20 |
+
<div className={cn(
|
21 |
+
`flex`,
|
22 |
+
horizontal ? '' : 'w-full',
|
23 |
+
className
|
24 |
+
)}>
|
25 |
+
{children}
|
26 |
+
</div>
|
27 |
+
</div>
|
28 |
+
)
|
29 |
+
}
|
src/components/form/form-file.tsx
ADDED
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ChangeEvent, useMemo, useRef } from "react"
|
2 |
+
|
3 |
+
import { cn } from "@/lib/utils"
|
4 |
+
|
5 |
+
import { Input } from "../ui/input"
|
6 |
+
|
7 |
+
import { FormField } from "./form-field"
|
8 |
+
|
9 |
+
export function FormFile({
|
10 |
+
label,
|
11 |
+
className,
|
12 |
+
placeholder,
|
13 |
+
disabled,
|
14 |
+
onChange,
|
15 |
+
horizontal,
|
16 |
+
accept
|
17 |
+
}: {
|
18 |
+
label?: string
|
19 |
+
className?: string
|
20 |
+
placeholder?: string
|
21 |
+
disabled?: boolean
|
22 |
+
onChange?: (files: File[]) => void
|
23 |
+
horizontal?: boolean
|
24 |
+
accept?: string
|
25 |
+
}) {
|
26 |
+
const ref = useRef<HTMLInputElement>(null)
|
27 |
+
|
28 |
+
const handleChange = useMemo(() => (event: ChangeEvent<HTMLInputElement>) => {
|
29 |
+
if (disabled) {
|
30 |
+
return
|
31 |
+
}
|
32 |
+
if (!onChange) {
|
33 |
+
return
|
34 |
+
}
|
35 |
+
|
36 |
+
const files = Array.from(event.currentTarget.files || [])
|
37 |
+
onChange(files)
|
38 |
+
}, [])
|
39 |
+
|
40 |
+
return (
|
41 |
+
<FormField
|
42 |
+
label={
|
43 |
+
`${label}:`
|
44 |
+
}
|
45 |
+
horizontal={horizontal}
|
46 |
+
>
|
47 |
+
<Input
|
48 |
+
ref={ref}
|
49 |
+
placeholder={`${placeholder || ""}`}
|
50 |
+
className={cn(`w-full md:w-52 lg:w-56 xl:w-64 font-normal text-base`, className)}
|
51 |
+
disabled={disabled}
|
52 |
+
onChange={handleChange}
|
53 |
+
type="file"
|
54 |
+
accept={accept}
|
55 |
+
/>
|
56 |
+
</FormField>
|
57 |
+
)
|
58 |
+
}
|
src/components/form/form-input.tsx
ADDED
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ChangeEvent, HTMLInputTypeAttribute, useMemo, useRef } from "react"
|
2 |
+
|
3 |
+
import { cn, getValidNumber, isValidNumber } from "@/lib/utils"
|
4 |
+
|
5 |
+
import { Input } from "../ui/input"
|
6 |
+
|
7 |
+
import { FormField } from "./form-field"
|
8 |
+
|
9 |
+
export function FormInput<T>({
|
10 |
+
label,
|
11 |
+
className,
|
12 |
+
placeholder,
|
13 |
+
value,
|
14 |
+
minValue,
|
15 |
+
maxValue,
|
16 |
+
defaultValue,
|
17 |
+
disabled,
|
18 |
+
onChange,
|
19 |
+
horizontal,
|
20 |
+
type,
|
21 |
+
// ...props
|
22 |
+
}: {
|
23 |
+
label?: string
|
24 |
+
className?: string
|
25 |
+
placeholder?: string
|
26 |
+
value?: T
|
27 |
+
minValue?: T
|
28 |
+
maxValue?: T
|
29 |
+
defaultValue?: T
|
30 |
+
disabled?: boolean
|
31 |
+
onChange?: (newValue: T) => void
|
32 |
+
horizontal?: boolean
|
33 |
+
type?: HTMLInputTypeAttribute
|
34 |
+
}
|
35 |
+
// & Omit<ComponentProps<typeof Input>, "value" | "defaultValue" | "placeholder" | "type" | "className" | "disabled" | "onChange">
|
36 |
+
// & ComponentProps<typeof Input>
|
37 |
+
) {
|
38 |
+
|
39 |
+
const isNumberInput =
|
40 |
+
typeof defaultValue === "number"
|
41 |
+
|| typeof value === "number"
|
42 |
+
|
43 |
+
const isTextInput =
|
44 |
+
typeof defaultValue === "string"
|
45 |
+
|| typeof value === "number"
|
46 |
+
|
47 |
+
// we try to use the type provided by the user if possible,
|
48 |
+
// otherwise we "guess" it
|
49 |
+
const inputType: HTMLInputTypeAttribute = type ||
|
50 |
+
(
|
51 |
+
isNumberInput ? "number"
|
52 |
+
: isTextInput ? "text"
|
53 |
+
: "text"
|
54 |
+
)
|
55 |
+
|
56 |
+
const ref = useRef<HTMLInputElement>(null)
|
57 |
+
|
58 |
+
const handleChange = useMemo(() => (event: ChangeEvent<HTMLInputElement>) => {
|
59 |
+
if (disabled) {
|
60 |
+
return
|
61 |
+
}
|
62 |
+
if (!onChange) {
|
63 |
+
return
|
64 |
+
}
|
65 |
+
|
66 |
+
const rawStringValue = `${event.currentTarget.value || ""}`
|
67 |
+
|
68 |
+
// this could be refactorer maybe
|
69 |
+
if (isNumberInput) {
|
70 |
+
const validMinValue: number = minValue && isValidNumber(minValue) ? (minValue as number) : 0
|
71 |
+
const validMaxValue: number = maxValue && isValidNumber(maxValue) ? (maxValue as number) : 1
|
72 |
+
const validDefaultValue: number = defaultValue && isValidNumber(defaultValue) ? (defaultValue as number) : 0
|
73 |
+
|
74 |
+
const numericValue = getValidNumber(
|
75 |
+
rawStringValue,
|
76 |
+
validMinValue,
|
77 |
+
validMaxValue,
|
78 |
+
validDefaultValue,
|
79 |
+
)
|
80 |
+
onChange(numericValue as T)
|
81 |
+
} else {
|
82 |
+
onChange(rawStringValue as T)
|
83 |
+
}
|
84 |
+
}, [isNumberInput, isTextInput, minValue, maxValue, defaultValue])
|
85 |
+
|
86 |
+
return (
|
87 |
+
<FormField
|
88 |
+
label={
|
89 |
+
`${label}:`
|
90 |
+
}
|
91 |
+
horizontal={horizontal}
|
92 |
+
>
|
93 |
+
<Input
|
94 |
+
ref={ref}
|
95 |
+
placeholder={`${placeholder || defaultValue || ""}`}
|
96 |
+
className={cn(`w-full md:w-52 lg:w-56 xl:w-64 font-normal text-base`, className)}
|
97 |
+
disabled={disabled}
|
98 |
+
onChange={handleChange}
|
99 |
+
// {...props}
|
100 |
+
type={inputType}
|
101 |
+
value={`${value || defaultValue}`}
|
102 |
+
|
103 |
+
// since we are controlling the element with value=*, we should not use defaultValue=*
|
104 |
+
// defaultValue={`${defaultValue || ""}`}
|
105 |
+
/>
|
106 |
+
</FormField>
|
107 |
+
)
|
108 |
+
}
|
src/components/form/form-label.tsx
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ReactNode } from "react"
|
2 |
+
|
3 |
+
import { cn } from "@/lib/utils"
|
4 |
+
|
5 |
+
export function FormLabel({ children, className }: {
|
6 |
+
children?: ReactNode
|
7 |
+
className?: string
|
8 |
+
}) {
|
9 |
+
return (
|
10 |
+
<label className={cn(`text-sm font-normal text-gray-300/70`, className)}>{
|
11 |
+
children
|
12 |
+
}</label>
|
13 |
+
)
|
14 |
+
}
|
src/components/form/form-radio.tsx
ADDED
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { MdOutlineCheckCircle, MdRadioButtonUnchecked } from "react-icons/md"
|
2 |
+
|
3 |
+
import { cn } from "@/lib/utils"
|
4 |
+
|
5 |
+
import { FormField } from "./form-field"
|
6 |
+
|
7 |
+
export function FormRadio({ label, className, selected, items, horizontal }: {
|
8 |
+
label?: string
|
9 |
+
className?: string
|
10 |
+
selected?: string
|
11 |
+
items?: { name: string; label: string; disabled?: boolean }[]
|
12 |
+
horizontal?: boolean
|
13 |
+
}) {
|
14 |
+
return (
|
15 |
+
<FormField
|
16 |
+
label={label}
|
17 |
+
className={cn(`flex-row space-x-5`, className)}
|
18 |
+
horizontal={horizontal}>
|
19 |
+
{items?.map(item => (
|
20 |
+
<div key={item.name} className={cn(
|
21 |
+
`flex flex-row items-center space-x-2`,
|
22 |
+
selected === item.name
|
23 |
+
? `text-gray-200 font-normal`
|
24 |
+
: (
|
25 |
+
item.disabled ? `text-gray-600 font-normal` : `text-gray-400 font-normal`
|
26 |
+
)
|
27 |
+
)}>
|
28 |
+
{selected === item.name
|
29 |
+
? <MdOutlineCheckCircle className="text-lg" />
|
30 |
+
: <MdRadioButtonUnchecked className="text-lg" />}
|
31 |
+
<span>{item.label}</span>
|
32 |
+
</div>
|
33 |
+
))}
|
34 |
+
</FormField>
|
35 |
+
)
|
36 |
+
}
|
src/components/form/form-section.tsx
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ReactNode } from "react"
|
2 |
+
|
3 |
+
import { cn } from "@/lib/utils"
|
4 |
+
|
5 |
+
export function FormSection({ label, children, className, horizontal }: {
|
6 |
+
label?: string
|
7 |
+
children?: ReactNode
|
8 |
+
className?: string
|
9 |
+
horizontal?: boolean
|
10 |
+
}) {
|
11 |
+
return (
|
12 |
+
<div className={cn(`flex flex-col space-y-4`)}>
|
13 |
+
<h2 className="text-2xl font-thin pb-2 text-gray-200">{label}</h2>
|
14 |
+
<div className={cn(
|
15 |
+
"flex",
|
16 |
+
horizontal
|
17 |
+
? "flex-row space-x-3 justify-start"
|
18 |
+
: "flex-col space-y-6"
|
19 |
+
)}>
|
20 |
+
{children}
|
21 |
+
</div>
|
22 |
+
</div>
|
23 |
+
)
|
24 |
+
}
|
src/components/form/form-select.tsx
ADDED
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
import { cn } from "@/lib/utils"
|
3 |
+
|
4 |
+
import { FormField } from "./form-field"
|
5 |
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"
|
6 |
+
|
7 |
+
export function FormSelect<T>({
|
8 |
+
label,
|
9 |
+
className,
|
10 |
+
selectedItemId,
|
11 |
+
selectedItemLabel,
|
12 |
+
defaultItemId,
|
13 |
+
defaultItemLabel,
|
14 |
+
items = [],
|
15 |
+
onSelect,
|
16 |
+
horizontal,
|
17 |
+
}: {
|
18 |
+
label?: string
|
19 |
+
className?: string
|
20 |
+
selectedItemId?: string
|
21 |
+
selectedItemLabel?: string
|
22 |
+
defaultItemId?: string
|
23 |
+
defaultItemLabel?: string
|
24 |
+
items?: {
|
25 |
+
id: string
|
26 |
+
label: string
|
27 |
+
disabled?: boolean
|
28 |
+
value: T
|
29 |
+
}[]
|
30 |
+
onSelect?: (value?: T) => void
|
31 |
+
horizontal?: boolean
|
32 |
+
}) {
|
33 |
+
|
34 |
+
return (
|
35 |
+
<FormField
|
36 |
+
label={
|
37 |
+
typeof defaultItemId !== "undefined" && typeof defaultItemLabel !== "undefined"
|
38 |
+
? `${label} (defaults to ${defaultItemLabel || "N.A."}):`
|
39 |
+
: `${label}:`
|
40 |
+
}
|
41 |
+
className={cn(``, className)}
|
42 |
+
horizontal={horizontal}>
|
43 |
+
<Select
|
44 |
+
onValueChange={(newSelectedItemId: string) => {
|
45 |
+
if (!onSelect) {
|
46 |
+
return
|
47 |
+
}
|
48 |
+
const selectedItem = items.find(item => item.id === newSelectedItemId)
|
49 |
+
onSelect(selectedItem?.value)
|
50 |
+
}}
|
51 |
+
defaultValue={selectedItemId}>
|
52 |
+
<SelectTrigger className="w-full md:w-52 lg:w-56 xl:w-64 font-normal text-base">
|
53 |
+
<SelectValue placeholder={selectedItemLabel} />
|
54 |
+
</SelectTrigger>
|
55 |
+
<SelectContent>
|
56 |
+
{items?.map(item =>
|
57 |
+
<SelectItem
|
58 |
+
key={item.id}
|
59 |
+
value={item.id}>{
|
60 |
+
item.label
|
61 |
+
}</SelectItem>
|
62 |
+
)}
|
63 |
+
</SelectContent>
|
64 |
+
</Select>
|
65 |
+
</FormField>
|
66 |
+
)
|
67 |
+
}
|
src/components/interface/loader/index.tsx
ADDED
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { cn } from "@/lib/utils"
|
2 |
+
|
3 |
+
export function Loader({
|
4 |
+
isLoading = false,
|
5 |
+
className = ""
|
6 |
+
}: {
|
7 |
+
isLoading?: boolean
|
8 |
+
className?: string
|
9 |
+
}) {
|
10 |
+
// TODO: read our global state
|
11 |
+
|
12 |
+
return (
|
13 |
+
<div className={cn(`
|
14 |
+
fixed
|
15 |
+
z-50
|
16 |
+
flex
|
17 |
+
w-screen h-screen
|
18 |
+
top-0 left-0 right-0 bottom-0
|
19 |
+
p-0 m-0
|
20 |
+
overflow-hidden
|
21 |
+
items-center justify-center
|
22 |
+
|
23 |
+
backdrop-blur-lg
|
24 |
+
`,
|
25 |
+
isLoading
|
26 |
+
? 'opacity-100 pointer-events-auto'
|
27 |
+
: 'pointer-events-none opacity-0',
|
28 |
+
className)}
|
29 |
+
style={{
|
30 |
+
// backgroundImage: "repeating-radial-gradient( circle at 0 0, transparent 0, #000000 7px ), repeating-linear-gradient( #34353655, #343536 )"
|
31 |
+
}}>
|
32 |
+
<p
|
33 |
+
className={cn("text-stone-100 font-sans font-thin text-[3vw]")}
|
34 |
+
style={{ textShadow: "#000 1px 0 3px" }}>
|
35 |
+
<span className="">Loading..</span>
|
36 |
+
</p>
|
37 |
+
</div>
|
38 |
+
)
|
39 |
+
}
|
src/components/interface/static-video/index.tsx
CHANGED
@@ -3,7 +3,7 @@
|
|
3 |
import React, { ReactNode, useEffect, useRef } from "react"
|
4 |
|
5 |
import { useTimelineState } from "@aitube/timeline"
|
6 |
-
import useRequestAnimationFrame from "@/lib/hooks/useRequestAnimationFrame"
|
7 |
|
8 |
export function StaticVideo({
|
9 |
video = "",
|
|
|
3 |
import React, { ReactNode, useEffect, useRef } from "react"
|
4 |
|
5 |
import { useTimelineState } from "@aitube/timeline"
|
6 |
+
import { useRequestAnimationFrame } from "@/lib/hooks/useRequestAnimationFrame"
|
7 |
|
8 |
export function StaticVideo({
|
9 |
video = "",
|
src/components/interface/system-menu/index.tsx
ADDED
@@ -0,0 +1,236 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import * as MenubarPrimitive from "@radix-ui/react-menubar"
|
5 |
+
import { Check, ChevronRight, Circle } from "lucide-react"
|
6 |
+
|
7 |
+
import { cn } from "@/lib/utils"
|
8 |
+
|
9 |
+
const MenubarMenu = MenubarPrimitive.Menu
|
10 |
+
|
11 |
+
const MenubarGroup = MenubarPrimitive.Group
|
12 |
+
|
13 |
+
const MenubarPortal = MenubarPrimitive.Portal
|
14 |
+
|
15 |
+
const MenubarSub = MenubarPrimitive.Sub
|
16 |
+
|
17 |
+
const MenubarRadioGroup = MenubarPrimitive.RadioGroup
|
18 |
+
|
19 |
+
const Menubar = React.forwardRef<
|
20 |
+
React.ElementRef<typeof MenubarPrimitive.Root>,
|
21 |
+
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
|
22 |
+
>(({ className, ...props }, ref) => (
|
23 |
+
<MenubarPrimitive.Root
|
24 |
+
ref={ref}
|
25 |
+
className={cn(
|
26 |
+
"flex h-9 items-center space-x-1 rounded-md border-bottom border-gray-800 bg-white p-1 dark:border-gray-800 dark:bg-stone-800",
|
27 |
+
className
|
28 |
+
)}
|
29 |
+
{...props}
|
30 |
+
/>
|
31 |
+
))
|
32 |
+
Menubar.displayName = MenubarPrimitive.Root.displayName
|
33 |
+
|
34 |
+
const MenubarTrigger = React.forwardRef<
|
35 |
+
React.ElementRef<typeof MenubarPrimitive.Trigger>,
|
36 |
+
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
|
37 |
+
>(({ className, ...props }, ref) => (
|
38 |
+
<MenubarPrimitive.Trigger
|
39 |
+
ref={ref}
|
40 |
+
className={cn(
|
41 |
+
"flex cursor-default select-none items-center rounded-sm px-3 py-1 text-sm font-normal outline-none focus:bg-stone-100 focus:text-gray-900 data-[state=open]:bg-stone-100 data-[state=open]:text-gray-900 dark:focus:bg-stone-700 dark:focus:text-gray-50 dark:data-[state=open]:bg-stone-700 dark:data-[state=open]:text-gray-50",
|
42 |
+
className
|
43 |
+
)}
|
44 |
+
{...props}
|
45 |
+
/>
|
46 |
+
))
|
47 |
+
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName
|
48 |
+
|
49 |
+
const MenubarSubTrigger = React.forwardRef<
|
50 |
+
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
|
51 |
+
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
|
52 |
+
inset?: boolean
|
53 |
+
}
|
54 |
+
>(({ className, inset, children, ...props }, ref) => (
|
55 |
+
<MenubarPrimitive.SubTrigger
|
56 |
+
ref={ref}
|
57 |
+
className={cn(
|
58 |
+
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-stone-100 focus:text-gray-900 data-[state=open]:bg-stone-100 data-[state=open]:text-gray-900 dark:focus:bg-stone-700 dark:focus:text-gray-50 dark:data-[state=open]:bg-stone-800 dark:data-[state=open]:text-gray-100",
|
59 |
+
inset && "pl-8",
|
60 |
+
className
|
61 |
+
)}
|
62 |
+
{...props}
|
63 |
+
>
|
64 |
+
{children}
|
65 |
+
<ChevronRight className="ml-auto h-4 w-4" />
|
66 |
+
</MenubarPrimitive.SubTrigger>
|
67 |
+
))
|
68 |
+
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName
|
69 |
+
|
70 |
+
const MenubarSubContent = React.forwardRef<
|
71 |
+
React.ElementRef<typeof MenubarPrimitive.SubContent>,
|
72 |
+
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
|
73 |
+
>(({ className, ...props }, ref) => (
|
74 |
+
<MenubarPrimitive.SubContent
|
75 |
+
ref={ref}
|
76 |
+
className={cn(
|
77 |
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border-transparent bg-white p-1 text-gray-950 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-gray-700 dark:bg-stone-800 dark:text-gray-100",
|
78 |
+
className
|
79 |
+
)}
|
80 |
+
{...props}
|
81 |
+
/>
|
82 |
+
))
|
83 |
+
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName
|
84 |
+
|
85 |
+
const MenubarContent = React.forwardRef<
|
86 |
+
React.ElementRef<typeof MenubarPrimitive.Content>,
|
87 |
+
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
|
88 |
+
>(
|
89 |
+
(
|
90 |
+
{ className, align = "start", alignOffset = -4, sideOffset = 8, ...props },
|
91 |
+
ref
|
92 |
+
) => (
|
93 |
+
<MenubarPrimitive.Portal>
|
94 |
+
<MenubarPrimitive.Content
|
95 |
+
ref={ref}
|
96 |
+
align={align}
|
97 |
+
alignOffset={alignOffset}
|
98 |
+
sideOffset={sideOffset}
|
99 |
+
className={cn(
|
100 |
+
"z-50 min-w-[12rem] overflow-hidden rounded-md border border-gray-200 bg-white p-1 text-gray-950 shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-gray-700 dark:bg-stone-800 dark:text-gray-100",
|
101 |
+
className
|
102 |
+
)}
|
103 |
+
{...props}
|
104 |
+
/>
|
105 |
+
</MenubarPrimitive.Portal>
|
106 |
+
)
|
107 |
+
)
|
108 |
+
MenubarContent.displayName = MenubarPrimitive.Content.displayName
|
109 |
+
|
110 |
+
const MenubarItem = React.forwardRef<
|
111 |
+
React.ElementRef<typeof MenubarPrimitive.Item>,
|
112 |
+
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
|
113 |
+
inset?: boolean
|
114 |
+
}
|
115 |
+
>(({ className, inset, ...props }, ref) => (
|
116 |
+
<MenubarPrimitive.Item
|
117 |
+
ref={ref}
|
118 |
+
className={cn(
|
119 |
+
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-stone-100 focus:text-gray-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-stone-700 dark:focus:text-gray-50",
|
120 |
+
inset && "pl-8",
|
121 |
+
className
|
122 |
+
)}
|
123 |
+
{...props}
|
124 |
+
/>
|
125 |
+
))
|
126 |
+
MenubarItem.displayName = MenubarPrimitive.Item.displayName
|
127 |
+
|
128 |
+
const MenubarCheckboxItem = React.forwardRef<
|
129 |
+
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
|
130 |
+
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
|
131 |
+
>(({ className, children, checked, ...props }, ref) => (
|
132 |
+
<MenubarPrimitive.CheckboxItem
|
133 |
+
ref={ref}
|
134 |
+
className={cn(
|
135 |
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-stone-100 focus:text-gray-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-stone-700 dark:focus:text-gray-100",
|
136 |
+
className
|
137 |
+
)}
|
138 |
+
checked={checked}
|
139 |
+
{...props}
|
140 |
+
>
|
141 |
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
142 |
+
<MenubarPrimitive.ItemIndicator>
|
143 |
+
<Check className="h-4 w-4" />
|
144 |
+
</MenubarPrimitive.ItemIndicator>
|
145 |
+
</span>
|
146 |
+
{children}
|
147 |
+
</MenubarPrimitive.CheckboxItem>
|
148 |
+
))
|
149 |
+
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName
|
150 |
+
|
151 |
+
const MenubarRadioItem = React.forwardRef<
|
152 |
+
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
|
153 |
+
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
|
154 |
+
>(({ className, children, ...props }, ref) => (
|
155 |
+
<MenubarPrimitive.RadioItem
|
156 |
+
ref={ref}
|
157 |
+
className={cn(
|
158 |
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-stone-100 focus:text-gray-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-stone-700 dark:focus:text-gray-100",
|
159 |
+
className
|
160 |
+
)}
|
161 |
+
{...props}
|
162 |
+
>
|
163 |
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
164 |
+
<MenubarPrimitive.ItemIndicator>
|
165 |
+
<Circle className="h-2 w-2 fill-current" />
|
166 |
+
</MenubarPrimitive.ItemIndicator>
|
167 |
+
</span>
|
168 |
+
{children}
|
169 |
+
</MenubarPrimitive.RadioItem>
|
170 |
+
))
|
171 |
+
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName
|
172 |
+
|
173 |
+
const MenubarLabel = React.forwardRef<
|
174 |
+
React.ElementRef<typeof MenubarPrimitive.Label>,
|
175 |
+
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
|
176 |
+
inset?: boolean
|
177 |
+
}
|
178 |
+
>(({ className, inset, ...props }, ref) => (
|
179 |
+
<MenubarPrimitive.Label
|
180 |
+
ref={ref}
|
181 |
+
className={cn(
|
182 |
+
"px-2 py-1.5 text-sm font-semibold",
|
183 |
+
inset && "pl-8",
|
184 |
+
className
|
185 |
+
)}
|
186 |
+
{...props}
|
187 |
+
/>
|
188 |
+
))
|
189 |
+
MenubarLabel.displayName = MenubarPrimitive.Label.displayName
|
190 |
+
|
191 |
+
const MenubarSeparator = React.forwardRef<
|
192 |
+
React.ElementRef<typeof MenubarPrimitive.Separator>,
|
193 |
+
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
|
194 |
+
>(({ className, ...props }, ref) => (
|
195 |
+
<MenubarPrimitive.Separator
|
196 |
+
ref={ref}
|
197 |
+
className={cn("-mx-1 my-1 h-px bg-stone-100 dark:bg-stone-700", className)}
|
198 |
+
{...props}
|
199 |
+
/>
|
200 |
+
))
|
201 |
+
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName
|
202 |
+
|
203 |
+
const MenubarShortcut = ({
|
204 |
+
className,
|
205 |
+
...props
|
206 |
+
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
207 |
+
return (
|
208 |
+
<span
|
209 |
+
className={cn(
|
210 |
+
"ml-auto text-xs tracking-widest text-gray-500 dark:text-gray-400",
|
211 |
+
className
|
212 |
+
)}
|
213 |
+
{...props}
|
214 |
+
/>
|
215 |
+
)
|
216 |
+
}
|
217 |
+
MenubarShortcut.displayname = "MenubarShortcut"
|
218 |
+
|
219 |
+
export {
|
220 |
+
Menubar,
|
221 |
+
MenubarMenu,
|
222 |
+
MenubarTrigger,
|
223 |
+
MenubarContent,
|
224 |
+
MenubarItem,
|
225 |
+
MenubarSeparator,
|
226 |
+
MenubarLabel,
|
227 |
+
MenubarCheckboxItem,
|
228 |
+
MenubarRadioGroup,
|
229 |
+
MenubarRadioItem,
|
230 |
+
MenubarPortal,
|
231 |
+
MenubarSubContent,
|
232 |
+
MenubarSubTrigger,
|
233 |
+
MenubarGroup,
|
234 |
+
MenubarSub,
|
235 |
+
MenubarShortcut,
|
236 |
+
}
|
src/components/interface/timeline/index.tsx
CHANGED
@@ -7,7 +7,7 @@ export function Timeline() {
|
|
7 |
<ClapTimeline
|
8 |
showFPS={false}
|
9 |
className={cn(
|
10 |
-
|
11 |
)}
|
12 |
/>
|
13 |
)
|
|
|
7 |
<ClapTimeline
|
8 |
showFPS={false}
|
9 |
className={cn(
|
10 |
+
"bg-[rgb(58,54,50)]"
|
11 |
)}
|
12 |
/>
|
13 |
)
|
src/components/interface/top-bar/index.tsx
ADDED
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ClapProject } from "@aitube/clap"
|
2 |
+
import { useTimelineState } from "@aitube/timeline"
|
3 |
+
|
4 |
+
import { cn } from "@/lib/utils"
|
5 |
+
|
6 |
+
import { TopMenu } from "../top-menu"
|
7 |
+
|
8 |
+
|
9 |
+
export function TopBar() {
|
10 |
+
const clap: ClapProject = useTimelineState(s => s.clap)
|
11 |
+
|
12 |
+
return (
|
13 |
+
<div className={cn(
|
14 |
+
`flex flex-row`,
|
15 |
+
`w-full h-10`,
|
16 |
+
`bg-stone-900 items-center`,
|
17 |
+
`border-b`,
|
18 |
+
`border-b-stone-700`,
|
19 |
+
)}>
|
20 |
+
<TopMenu />
|
21 |
+
<div className={cn(
|
22 |
+
`flex flex-row flex-grow`,
|
23 |
+
`items-center justify-center`,
|
24 |
+
`text-xs text-stone-300`
|
25 |
+
)}>
|
26 |
+
{clap?.meta?.title || "Untitled"}
|
27 |
+
</div>
|
28 |
+
</div>
|
29 |
+
)
|
30 |
+
}
|
src/components/interface/top-menu/file/index.tsx
ADDED
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
import { useEffect } from "react"
|
3 |
+
import { useTimelineState } from "@aitube/timeline"
|
4 |
+
import { useHotkeys } from "react-hotkeys-hook"
|
5 |
+
import { parseClap } from "@aitube/clap"
|
6 |
+
|
7 |
+
import { MenubarContent, MenubarItem, MenubarMenu, MenubarSeparator, MenubarShortcut, MenubarTrigger } from "@/components/ui/menubar"
|
8 |
+
import { useClapFilePicker, useQueryStringParams, useScreenplayFilePicker } from "@/lib/hooks"
|
9 |
+
import { Loader } from "../../loader"
|
10 |
+
|
11 |
+
export function TopMenuFile() {
|
12 |
+
const { clapUrl } = useQueryStringParams({
|
13 |
+
// clapUrl: `/samples/test.clap`,
|
14 |
+
// clapUrl: `/samples/Afterglow%20v10%20X%20Rewrite%20Bryan%20E.%20Harris%202023.clap`,
|
15 |
+
clapUrl: '',
|
16 |
+
})
|
17 |
+
|
18 |
+
const isTimelineLoading: boolean = useTimelineState(s => s.isLoading)
|
19 |
+
const setClap = useTimelineState(s => s.setClap)
|
20 |
+
const saveClapAs = useTimelineState(s => s.saveClapAs)
|
21 |
+
|
22 |
+
const clapPicker = useClapFilePicker()
|
23 |
+
const screenplayPicker = useScreenplayFilePicker()
|
24 |
+
|
25 |
+
const isLoading = isTimelineLoading || clapPicker.isLoading || screenplayPicker.isLoading
|
26 |
+
|
27 |
+
// const setShowSettings = useInterfaceSettings(s => s.setShowSettings)
|
28 |
+
useHotkeys('ctrl+o', () => clapPicker.openFilePicker(), { preventDefault: true }, [])
|
29 |
+
useHotkeys('meta+o', () => clapPicker.openFilePicker(), { preventDefault: true }, [])
|
30 |
+
useHotkeys('ctrl+s', () => saveClapAs({ embedded: true }), { preventDefault: true }, [])
|
31 |
+
useHotkeys('meta+s', () => saveClapAs({ embedded: true }), { preventDefault: true }, [])
|
32 |
+
|
33 |
+
useEffect(() => {
|
34 |
+
(async () => {
|
35 |
+
if (!clapUrl) {
|
36 |
+
console.log("No clap URL provided")
|
37 |
+
return
|
38 |
+
}
|
39 |
+
const res = await fetch(clapUrl)
|
40 |
+
const blob = await res.blob()
|
41 |
+
const clap = await parseClap(blob)
|
42 |
+
await setClap(clap)
|
43 |
+
})()
|
44 |
+
}, [clapUrl])
|
45 |
+
|
46 |
+
return (
|
47 |
+
<>
|
48 |
+
<MenubarMenu>
|
49 |
+
<MenubarTrigger>File</MenubarTrigger>
|
50 |
+
<MenubarContent>
|
51 |
+
<MenubarItem onClick={() => {
|
52 |
+
clapPicker.openFilePicker()
|
53 |
+
}}>
|
54 |
+
Open project (.clap)<MenubarShortcut>βO</MenubarShortcut>
|
55 |
+
</MenubarItem>
|
56 |
+
<MenubarItem onClick={() => {
|
57 |
+
saveClapAs({ embedded: true })
|
58 |
+
}}>
|
59 |
+
Save project (.clap)<MenubarShortcut>βS</MenubarShortcut>
|
60 |
+
</MenubarItem>
|
61 |
+
<MenubarSeparator />
|
62 |
+
<MenubarItem onClick={() => {
|
63 |
+
screenplayPicker.openFilePicker()
|
64 |
+
}}>
|
65 |
+
Import screenplay (.txt)
|
66 |
+
</MenubarItem>
|
67 |
+
</MenubarContent>
|
68 |
+
</MenubarMenu>
|
69 |
+
<Loader isLoading={isLoading} />
|
70 |
+
</>
|
71 |
+
)
|
72 |
+
}
|
src/components/interface/top-menu/index.tsx
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Menubar } from "@/components/ui/menubar"
|
2 |
+
|
3 |
+
import { TopMenuFile } from "./file"
|
4 |
+
import { TopMenuView } from "./view"
|
5 |
+
import { TopMenuRendering } from "./rendering"
|
6 |
+
|
7 |
+
export function TopMenu() {
|
8 |
+
return (
|
9 |
+
<Menubar>
|
10 |
+
<TopMenuFile />
|
11 |
+
{/*
|
12 |
+
<TopMenuEdit />
|
13 |
+
<TopMenuPlayback />
|
14 |
+
<TopMenuAssistant />
|
15 |
+
*/}
|
16 |
+
<TopMenuRendering />
|
17 |
+
{/*<TopMenuView />*/}
|
18 |
+
</Menubar>
|
19 |
+
)
|
20 |
+
}
|
src/components/interface/top-menu/rendering/index.tsx
ADDED
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import { useEffect } from "react"
|
4 |
+
|
5 |
+
import {
|
6 |
+
MenubarCheckboxItem,
|
7 |
+
MenubarContent,
|
8 |
+
MenubarItem,
|
9 |
+
MenubarMenu,
|
10 |
+
MenubarSeparator,
|
11 |
+
MenubarSub,
|
12 |
+
MenubarSubContent,
|
13 |
+
MenubarSubTrigger,
|
14 |
+
MenubarTrigger
|
15 |
+
} from "@/components/ui/menubar"
|
16 |
+
import { useSettingsRendering } from "@/settings/rendering"
|
17 |
+
import { RenderingStrategy } from "@/types"
|
18 |
+
|
19 |
+
export function TopMenuRendering() {
|
20 |
+
|
21 |
+
const storyboardRenderingStrategy = useSettingsRendering((s) => s.storyboardRenderingStrategy)
|
22 |
+
const setStoryboardRenderingStrategy = useSettingsRendering((s) => s.setStoryboardRenderingStrategy)
|
23 |
+
|
24 |
+
const videoRenderingStrategy = useSettingsRendering((s) => s.videoRenderingStrategy)
|
25 |
+
const setVideoRenderingStrategy = useSettingsRendering((s) => s.setVideoRenderingStrategy)
|
26 |
+
|
27 |
+
const labels = {
|
28 |
+
[RenderingStrategy.ON_DEMAND]: "Render on click",
|
29 |
+
[RenderingStrategy.ON_SCREEN_ONLY]: "Render visible items",
|
30 |
+
[RenderingStrategy.ON_SCREEN_THEN_SURROUNDING]: "Render visible + surrounding items",
|
31 |
+
[RenderingStrategy.ON_SCREEN_THEN_ALL]: "Full rendering (for GPU-rich people)"
|
32 |
+
}
|
33 |
+
|
34 |
+
return (
|
35 |
+
<MenubarMenu>
|
36 |
+
<MenubarTrigger>Rendering</MenubarTrigger>
|
37 |
+
<MenubarContent>
|
38 |
+
|
39 |
+
<MenubarSub>
|
40 |
+
<MenubarSubTrigger>Storyboards generation</MenubarSubTrigger>
|
41 |
+
<MenubarSubContent>
|
42 |
+
<MenubarCheckboxItem
|
43 |
+
disabled
|
44 |
+
checked={storyboardRenderingStrategy === RenderingStrategy.ON_DEMAND}
|
45 |
+
onClick={(e) => {
|
46 |
+
setStoryboardRenderingStrategy(RenderingStrategy.ON_DEMAND)
|
47 |
+
e.stopPropagation()
|
48 |
+
e.preventDefault()
|
49 |
+
return false
|
50 |
+
}}>
|
51 |
+
{labels[RenderingStrategy.ON_DEMAND]}
|
52 |
+
</MenubarCheckboxItem>
|
53 |
+
<MenubarCheckboxItem
|
54 |
+
disabled
|
55 |
+
checked={storyboardRenderingStrategy === RenderingStrategy.ON_SCREEN_ONLY}
|
56 |
+
onClick={(e) => {
|
57 |
+
setStoryboardRenderingStrategy(RenderingStrategy.ON_SCREEN_ONLY)
|
58 |
+
e.stopPropagation()
|
59 |
+
e.preventDefault()
|
60 |
+
return false
|
61 |
+
}}>
|
62 |
+
{labels[RenderingStrategy.ON_SCREEN_ONLY]}
|
63 |
+
</MenubarCheckboxItem>
|
64 |
+
<MenubarCheckboxItem
|
65 |
+
disabled
|
66 |
+
checked={storyboardRenderingStrategy === RenderingStrategy.ON_SCREEN_THEN_SURROUNDING}
|
67 |
+
onClick={(e) => {
|
68 |
+
setStoryboardRenderingStrategy(RenderingStrategy.ON_SCREEN_THEN_SURROUNDING)
|
69 |
+
e.stopPropagation()
|
70 |
+
e.preventDefault()
|
71 |
+
return false
|
72 |
+
}}>
|
73 |
+
{labels[RenderingStrategy.ON_SCREEN_THEN_SURROUNDING]}
|
74 |
+
</MenubarCheckboxItem>
|
75 |
+
<MenubarCheckboxItem
|
76 |
+
disabled
|
77 |
+
checked={storyboardRenderingStrategy === RenderingStrategy.ON_SCREEN_THEN_ALL}
|
78 |
+
onClick={(e) => {
|
79 |
+
setStoryboardRenderingStrategy(RenderingStrategy.ON_SCREEN_THEN_ALL)
|
80 |
+
e.stopPropagation()
|
81 |
+
e.preventDefault()
|
82 |
+
return false
|
83 |
+
}}>
|
84 |
+
{labels[RenderingStrategy.ON_SCREEN_THEN_ALL]}
|
85 |
+
</MenubarCheckboxItem>
|
86 |
+
</MenubarSubContent>
|
87 |
+
</MenubarSub>
|
88 |
+
|
89 |
+
<MenubarSub>
|
90 |
+
<MenubarSubTrigger>Videos generation</MenubarSubTrigger>
|
91 |
+
<MenubarSubContent>
|
92 |
+
<MenubarCheckboxItem
|
93 |
+
disabled
|
94 |
+
checked={videoRenderingStrategy === RenderingStrategy.ON_DEMAND}
|
95 |
+
onClick={(e) => {
|
96 |
+
setVideoRenderingStrategy(RenderingStrategy.ON_DEMAND)
|
97 |
+
e.stopPropagation()
|
98 |
+
e.preventDefault()
|
99 |
+
return false
|
100 |
+
}}>
|
101 |
+
{labels[RenderingStrategy.ON_DEMAND]}
|
102 |
+
</MenubarCheckboxItem>
|
103 |
+
<MenubarCheckboxItem
|
104 |
+
disabled
|
105 |
+
checked={videoRenderingStrategy === RenderingStrategy.ON_SCREEN_ONLY}
|
106 |
+
onClick={(e) => {
|
107 |
+
setVideoRenderingStrategy(RenderingStrategy.ON_SCREEN_ONLY)
|
108 |
+
e.stopPropagation()
|
109 |
+
e.preventDefault()
|
110 |
+
return false
|
111 |
+
}}>
|
112 |
+
{labels[RenderingStrategy.ON_SCREEN_ONLY]}
|
113 |
+
</MenubarCheckboxItem>
|
114 |
+
<MenubarCheckboxItem
|
115 |
+
disabled
|
116 |
+
checked={videoRenderingStrategy === RenderingStrategy.ON_SCREEN_THEN_SURROUNDING}
|
117 |
+
onClick={(e) => {
|
118 |
+
setVideoRenderingStrategy(RenderingStrategy.ON_SCREEN_THEN_SURROUNDING)
|
119 |
+
e.stopPropagation()
|
120 |
+
e.preventDefault()
|
121 |
+
return false
|
122 |
+
}}>
|
123 |
+
{labels[RenderingStrategy.ON_SCREEN_THEN_SURROUNDING]}
|
124 |
+
</MenubarCheckboxItem>
|
125 |
+
<MenubarCheckboxItem
|
126 |
+
disabled
|
127 |
+
checked={videoRenderingStrategy === RenderingStrategy.ON_SCREEN_THEN_ALL}
|
128 |
+
onClick={(e) => {
|
129 |
+
setVideoRenderingStrategy(RenderingStrategy.ON_SCREEN_THEN_ALL)
|
130 |
+
e.stopPropagation()
|
131 |
+
e.preventDefault()
|
132 |
+
return false
|
133 |
+
}}>
|
134 |
+
{labels[RenderingStrategy.ON_SCREEN_THEN_ALL]}
|
135 |
+
</MenubarCheckboxItem>
|
136 |
+
</MenubarSubContent>
|
137 |
+
</MenubarSub>
|
138 |
+
</MenubarContent>
|
139 |
+
</MenubarMenu>
|
140 |
+
)
|
141 |
+
}
|
src/components/interface/top-menu/view/index.tsx
ADDED
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import { useEffect } from "react"
|
4 |
+
|
5 |
+
import {
|
6 |
+
MenubarCheckboxItem,
|
7 |
+
MenubarContent,
|
8 |
+
MenubarMenu,
|
9 |
+
MenubarSeparator,
|
10 |
+
MenubarTrigger
|
11 |
+
} from "@/components/ui/menubar"
|
12 |
+
import { useFullscreenStatus } from "@/lib/hooks"
|
13 |
+
import { useSettingsView } from "@/settings/view"
|
14 |
+
|
15 |
+
export function TopMenuView() {
|
16 |
+
const [isFullscreen, setFullscreen, ref] = useFullscreenStatus()
|
17 |
+
|
18 |
+
// we want the whole body to become fullscreen
|
19 |
+
// TODO: use pointer lock, to prevent the mouse from going up
|
20 |
+
useEffect(() => {
|
21 |
+
if (typeof window !== "undefined") {
|
22 |
+
ref.current = document.body
|
23 |
+
}
|
24 |
+
}, [])
|
25 |
+
|
26 |
+
const showTimeline = useSettingsView((s) => s.showTimeline)
|
27 |
+
const setShowTimeline = useSettingsView((s) => s.setShowTimeline)
|
28 |
+
|
29 |
+
const showExplorer = useSettingsView((s) => s.showExplorer)
|
30 |
+
const setShowExplorer = useSettingsView((s) => s.setShowExplorer)
|
31 |
+
|
32 |
+
const showChat = useSettingsView((s) => s.showChat)
|
33 |
+
const setShowChat = useSettingsView((s) => s.setShowChat)
|
34 |
+
|
35 |
+
const showVideoPlayer = useSettingsView((s) => s.showVideoPlayer)
|
36 |
+
const setShowVideoPlayer = useSettingsView((s) => s.setShowVideoPlayer)
|
37 |
+
|
38 |
+
return (
|
39 |
+
<MenubarMenu>
|
40 |
+
<MenubarTrigger>View</MenubarTrigger>
|
41 |
+
<MenubarContent>
|
42 |
+
<MenubarCheckboxItem
|
43 |
+
checked={isFullscreen}
|
44 |
+
onClick={(e) => {
|
45 |
+
// currently isFullscreen is a bit buggy and might not reflect the correct value
|
46 |
+
// setFullscreen(!isFullscreen)
|
47 |
+
|
48 |
+
// so to be sure we use setFullscreen in "toggle" mode
|
49 |
+
// (ie. we don't pass a boolean, so it will act as a current value switch)
|
50 |
+
setFullscreen()
|
51 |
+
|
52 |
+
e.stopPropagation()
|
53 |
+
e.preventDefault()
|
54 |
+
return false
|
55 |
+
}}>
|
56 |
+
Toggle fullscreen
|
57 |
+
</MenubarCheckboxItem>
|
58 |
+
<MenubarSeparator />
|
59 |
+
<MenubarCheckboxItem
|
60 |
+
checked={showTimeline}
|
61 |
+
onClick={(e) => {
|
62 |
+
setShowTimeline(!showTimeline)
|
63 |
+
e.stopPropagation()
|
64 |
+
e.preventDefault()
|
65 |
+
return false
|
66 |
+
}}
|
67 |
+
>Show timeline</MenubarCheckboxItem>
|
68 |
+
<MenubarCheckboxItem
|
69 |
+
checked={showExplorer}
|
70 |
+
onClick={(e) => {
|
71 |
+
setShowExplorer(!showExplorer)
|
72 |
+
e.stopPropagation()
|
73 |
+
e.preventDefault()
|
74 |
+
return false
|
75 |
+
}}
|
76 |
+
>Show asset explorer</MenubarCheckboxItem>
|
77 |
+
<MenubarCheckboxItem
|
78 |
+
checked={showChat}
|
79 |
+
onClick={(e) => {
|
80 |
+
setShowChat(!showChat)
|
81 |
+
e.stopPropagation()
|
82 |
+
e.preventDefault()
|
83 |
+
return false
|
84 |
+
}}
|
85 |
+
>Show chat assistant</MenubarCheckboxItem>
|
86 |
+
<MenubarCheckboxItem
|
87 |
+
checked={showVideoPlayer}
|
88 |
+
onClick={(e) => {
|
89 |
+
setShowVideoPlayer(!showVideoPlayer)
|
90 |
+
e.stopPropagation()
|
91 |
+
e.preventDefault()
|
92 |
+
return false
|
93 |
+
}}
|
94 |
+
>Show video player</MenubarCheckboxItem>
|
95 |
+
</MenubarContent>
|
96 |
+
</MenubarMenu>
|
97 |
+
)
|
98 |
+
}
|
src/components/settings/SettingsSectionRendering.tsx
ADDED
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { FormSection } from "@/components/form/form-section"
|
2 |
+
import { useSettingsRendering } from "@/settings/rendering"
|
3 |
+
|
4 |
+
export function SettingsSectionRendering() {
|
5 |
+
|
6 |
+
const comfyUiApiVendor = useSettingsRendering(s => s.comfyUiApiVendor)
|
7 |
+
const comfyUiApiKey = useSettingsRendering(s => s.comfyUiApiKey)
|
8 |
+
const storyboardRenderingStrategy = useSettingsRendering(s => s.storyboardRenderingStrategy)
|
9 |
+
const videoRenderingStrategy = useSettingsRendering(s => s.videoRenderingStrategy)
|
10 |
+
const maxNbAssetsToGenerateInParallel = useSettingsRendering(s => s.maxNbAssetsToGenerateInParallel)
|
11 |
+
|
12 |
+
return (
|
13 |
+
<div className="flex flex-col space-y-6 justify-between">
|
14 |
+
<FormSection label="TODO">
|
15 |
+
<p>TODO</p>
|
16 |
+
</FormSection>
|
17 |
+
</div>
|
18 |
+
)
|
19 |
+
}
|
src/components/settings/index.tsx
ADDED
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useState, useTransition } from "react"
|
2 |
+
|
3 |
+
import { Button } from "@/components/ui/button"
|
4 |
+
import { Dialog, DialogContent, DialogFooter } from "@/components/ui/dialog"
|
5 |
+
import { ScrollArea } from "@/components/ui/scroll-area"
|
6 |
+
import { cn } from "@/lib/utils"
|
7 |
+
import { useSettingsView } from "@/settings/view"
|
8 |
+
|
9 |
+
import { SettingsSectionRendering } from "./SettingsSectionRendering"
|
10 |
+
|
11 |
+
export function SettingsDialog() {
|
12 |
+
|
13 |
+
const showSettings = useSettingsView(s => s.showSettings)
|
14 |
+
const setShowSettings = useSettingsView(s => s.setShowSettings)
|
15 |
+
const [_isPending, startTransition] = useTransition()
|
16 |
+
|
17 |
+
const panels = {
|
18 |
+
rendering: <SettingsSectionRendering />,
|
19 |
+
}
|
20 |
+
|
21 |
+
const panelLabels = {
|
22 |
+
rendering: "Rendering",
|
23 |
+
} as any
|
24 |
+
|
25 |
+
const [configPanel, setConfigPanel] = useState<keyof typeof panels>("rendering")
|
26 |
+
|
27 |
+
return (
|
28 |
+
<Dialog open={showSettings} onOpenChange={setShowSettings}>
|
29 |
+
<DialogContent className={cn(
|
30 |
+
// DialogContent comes with some hardcoded values so we need to override them
|
31 |
+
`w-[95w] md:w-[85vw] max-w-6xl h-[80%]`,
|
32 |
+
`flex flex-row`
|
33 |
+
)}>
|
34 |
+
<ScrollArea className="flex flex-col h-full w-44">
|
35 |
+
<div className="flex flex-col items-end">
|
36 |
+
{Object.keys(panels).map(key => (
|
37 |
+
<Button
|
38 |
+
key={key}
|
39 |
+
variant="ghost"
|
40 |
+
className="flex flex-col capitalize w-full items-end text-right text-md"
|
41 |
+
onClick={() => setConfigPanel(key as keyof typeof panels)}>{panelLabels[key]}</Button>
|
42 |
+
))}
|
43 |
+
</div>
|
44 |
+
</ScrollArea>
|
45 |
+
|
46 |
+
<div className="flex flex-col h-full flex-grow justify-between max-w-[calc(100%-200px)]">
|
47 |
+
<ScrollArea className="flex flex-row h-full">
|
48 |
+
{panels[configPanel]}
|
49 |
+
</ScrollArea>
|
50 |
+
<DialogFooter className="text-gray-800">
|
51 |
+
<Button onClick={() => { setShowSettings(false) }}>Close</Button>
|
52 |
+
</DialogFooter>
|
53 |
+
</div>
|
54 |
+
</DialogContent>
|
55 |
+
</Dialog>
|
56 |
+
)
|
57 |
+
}
|
src/components/ui/menubar.tsx
CHANGED
@@ -23,7 +23,7 @@ const Menubar = React.forwardRef<
|
|
23 |
<MenubarPrimitive.Root
|
24 |
ref={ref}
|
25 |
className={cn(
|
26 |
-
"flex h-10 items-center space-x-1 rounded-md border border-stone-200 bg-white p-1 dark:border-stone-
|
27 |
className
|
28 |
)}
|
29 |
{...props}
|
@@ -74,7 +74,7 @@ const MenubarSubContent = React.forwardRef<
|
|
74 |
<MenubarPrimitive.SubContent
|
75 |
ref={ref}
|
76 |
className={cn(
|
77 |
-
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-stone-200 bg-white p-1 text-stone-
|
78 |
className
|
79 |
)}
|
80 |
{...props}
|
@@ -97,7 +97,7 @@ const MenubarContent = React.forwardRef<
|
|
97 |
alignOffset={alignOffset}
|
98 |
sideOffset={sideOffset}
|
99 |
className={cn(
|
100 |
-
"z-50 min-w-[12rem] overflow-hidden rounded-md border border-stone-200 bg-white p-1 text-stone-
|
101 |
className
|
102 |
)}
|
103 |
{...props}
|
|
|
23 |
<MenubarPrimitive.Root
|
24 |
ref={ref}
|
25 |
className={cn(
|
26 |
+
"flex h-10 items-center space-x-1 rounded-md border border-stone-200 bg-white p-1 dark:border-stone-900 dark:bg-stone-900",
|
27 |
className
|
28 |
)}
|
29 |
{...props}
|
|
|
74 |
<MenubarPrimitive.SubContent
|
75 |
ref={ref}
|
76 |
className={cn(
|
77 |
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-stone-200 bg-white p-1 text-stone-900 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-stone-800 dark:bg-stone-900 dark:text-stone-50",
|
78 |
className
|
79 |
)}
|
80 |
{...props}
|
|
|
97 |
alignOffset={alignOffset}
|
98 |
sideOffset={sideOffset}
|
99 |
className={cn(
|
100 |
+
"z-50 min-w-[12rem] overflow-hidden rounded-md border border-stone-200 bg-white p-1 text-stone-900 shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-stone-800 dark:bg-stone-900 dark:text-stone-50",
|
101 |
className
|
102 |
)}
|
103 |
{...props}
|
src/components/ui/scroll-area.tsx
ADDED
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
5 |
+
|
6 |
+
import { cn } from "@/lib/utils"
|
7 |
+
|
8 |
+
const ScrollArea = React.forwardRef<
|
9 |
+
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
10 |
+
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
11 |
+
>(({ className, children, ...props }, ref) => (
|
12 |
+
<ScrollAreaPrimitive.Root
|
13 |
+
ref={ref}
|
14 |
+
className={cn("relative overflow-hidden", className)}
|
15 |
+
{...props}
|
16 |
+
>
|
17 |
+
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
18 |
+
{children}
|
19 |
+
</ScrollAreaPrimitive.Viewport>
|
20 |
+
<ScrollBar />
|
21 |
+
<ScrollAreaPrimitive.Corner />
|
22 |
+
</ScrollAreaPrimitive.Root>
|
23 |
+
))
|
24 |
+
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
25 |
+
|
26 |
+
const ScrollBar = React.forwardRef<
|
27 |
+
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
28 |
+
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
29 |
+
>(({ className, orientation = "vertical", ...props }, ref) => (
|
30 |
+
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
31 |
+
ref={ref}
|
32 |
+
orientation={orientation}
|
33 |
+
className={cn(
|
34 |
+
"flex touch-none select-none transition-colors",
|
35 |
+
orientation === "vertical" &&
|
36 |
+
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
37 |
+
orientation === "horizontal" &&
|
38 |
+
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
39 |
+
className
|
40 |
+
)}
|
41 |
+
{...props}
|
42 |
+
>
|
43 |
+
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-stone-200 dark:bg-stone-800" />
|
44 |
+
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
45 |
+
))
|
46 |
+
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
47 |
+
|
48 |
+
export { ScrollArea, ScrollBar }
|
src/lib/core/DEPRECATED_getSettings.txt
ADDED
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { getValidNumber } from "@aitube/clap"
|
2 |
+
|
3 |
+
import { ComfyVendor, Settings } from "@/types"
|
4 |
+
import { localStorageKeys } from "@/components/interface/settings/constants"
|
5 |
+
|
6 |
+
import { getValidString, parseRenderingStrategy } from "../utils"
|
7 |
+
|
8 |
+
import { getDefaultSettings } from "./getDefaultSettings"
|
9 |
+
import { HARD_LIMIT_NB_MAX_ASSETS_TO_GENERATE_IN_PARALLEL } from "./constants"
|
10 |
+
|
11 |
+
export function getSettings(): Settings {
|
12 |
+
const defaultSettings = getDefaultSettings()
|
13 |
+
try {
|
14 |
+
return {
|
15 |
+
comfyVendor: getValidString(localStorage?.getItem?.(localStorageKeys.comfyVendor), defaultSettings.comfyVendor) as ComfyVendor,
|
16 |
+
comfyApiKey: getValidString(localStorage?.getItem?.(localStorageKeys.comfyApiKey), defaultSettings.comfyApiKey),
|
17 |
+
storyboardGenerationStrategy: parseRenderingStrategy(localStorage?.getItem?.(localStorageKeys.storyboardGenerationStrategy), defaultSettings.storyboardGenerationStrategy),
|
18 |
+
videoGenerationStrategy: parseRenderingStrategy(localStorage?.getItem?.(localStorageKeys.videoGenerationStrategy), defaultSettings.videoGenerationStrategy),
|
19 |
+
maxNbAssetsToGenerateInParallel: getValidNumber(localStorage?.getItem?.(localStorageKeys.maxNbAssetsToGenerateInParallel), 1, HARD_LIMIT_NB_MAX_ASSETS_TO_GENERATE_IN_PARALLEL, defaultSettings.maxNbAssetsToGenerateInParallel),
|
20 |
+
}
|
21 |
+
} catch (err) {
|
22 |
+
return {
|
23 |
+
...defaultSettings
|
24 |
+
}
|
25 |
+
}
|
26 |
+
}
|
src/{components/interface/settings/constants.tsx β lib/core/DEPRECATED_localStorageKeys.txt}
RENAMED
File without changes
|
src/lib/core/constants.ts
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
// do we really want a high value here?
|
3 |
+
// who is seriously ready to spawn 32 GPUs in parallels for this?
|
4 |
+
export const HARD_LIMIT_NB_MAX_ASSETS_TO_GENERATE_IN_PARALLEL = 32
|
src/lib/core/getDefaultSettings.ts
CHANGED
@@ -1,12 +1,12 @@
|
|
1 |
-
import {
|
2 |
|
3 |
export function getDefaultSettings(): Settings {
|
4 |
return {
|
5 |
comfyVendor: ComfyVendor.CUSTOM,
|
6 |
comfyApiKey: "",
|
7 |
|
8 |
-
storyboardGenerationStrategy:
|
9 |
-
videoGenerationStrategy:
|
10 |
|
11 |
maxNbAssetsToGenerateInParallel: 1,
|
12 |
}
|
|
|
1 |
+
import { RenderingStrategy, ComfyVendor, Settings } from "@/types"
|
2 |
|
3 |
export function getDefaultSettings(): Settings {
|
4 |
return {
|
5 |
comfyVendor: ComfyVendor.CUSTOM,
|
6 |
comfyApiKey: "",
|
7 |
|
8 |
+
storyboardGenerationStrategy: RenderingStrategy.ON_DEMAND,
|
9 |
+
videoGenerationStrategy: RenderingStrategy.ON_DEMAND,
|
10 |
|
11 |
maxNbAssetsToGenerateInParallel: 1,
|
12 |
}
|
src/lib/hooks/index.ts
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export { useDebounce } from "./useDebounce"
|
2 |
+
export { useClapFilePicker } from "./useClapFilePicker"
|
3 |
+
export { useFullscreenStatus } from "./useFullscreenStatus"
|
4 |
+
export { useQueryStringParams } from "./useQueryStringParams"
|
5 |
+
export { useRequestAnimationFrame } from "./useRequestAnimationFrame"
|
6 |
+
export { useScreenplayFilePicker } from "./useScreenplayFilePicker"
|
src/lib/hooks/useClapFilePicker.ts
ADDED
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useEffect, useState } from "react"
|
2 |
+
import { useFilePicker } from "use-file-picker"
|
3 |
+
import { parseClap } from "@aitube/clap"
|
4 |
+
import { useTimelineState } from "@aitube/timeline"
|
5 |
+
|
6 |
+
|
7 |
+
export function useClapFilePicker() {
|
8 |
+
const setClap = useTimelineState(s => s.setClap)
|
9 |
+
const [isLoading, setIsLoading] = useState(false)
|
10 |
+
|
11 |
+
const { openFilePicker, filesContent, loading } = useFilePicker({
|
12 |
+
accept: '.clap',
|
13 |
+
readAs: "ArrayBuffer"
|
14 |
+
})
|
15 |
+
|
16 |
+
const fileData = filesContent[0]
|
17 |
+
|
18 |
+
useEffect(() => {
|
19 |
+
const fn = async () => {
|
20 |
+
if (fileData?.name) {
|
21 |
+
try {
|
22 |
+
setIsLoading(true)
|
23 |
+
const blob = new Blob([fileData.content])
|
24 |
+
const clap = await parseClap(blob)
|
25 |
+
await setClap(clap)
|
26 |
+
} catch (err) {
|
27 |
+
console.error("failed to load the Clap file:", err)
|
28 |
+
} finally {
|
29 |
+
setIsLoading(false)
|
30 |
+
}
|
31 |
+
}
|
32 |
+
}
|
33 |
+
fn()
|
34 |
+
}, [fileData?.name])
|
35 |
+
|
36 |
+
return { openFilePicker, filesContent, fileData, isLoading: loading || isLoading }
|
37 |
+
}
|
src/lib/hooks/useFullscreenStatus.ts
ADDED
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import React, { useState, useLayoutEffect, useRef, MutableRefObject } from 'react'
|
4 |
+
|
5 |
+
interface FullscreenElement {
|
6 |
+
fullscreenElement?: Element;
|
7 |
+
mozFullScreenElement?: Element;
|
8 |
+
msFullscreenElement?: Element;
|
9 |
+
webkitFullscreenElement?: Element;
|
10 |
+
}
|
11 |
+
|
12 |
+
declare global {
|
13 |
+
interface Document extends FullscreenElement {}
|
14 |
+
}
|
15 |
+
|
16 |
+
export function useFullscreenStatus(): [boolean, (requestedValue?: boolean) => void, MutableRefObject<Element | null>] {
|
17 |
+
const elRef = useRef<Element | null>(null);
|
18 |
+
const [isFullscreen, setIsFullscreen] = useState(
|
19 |
+
typeof document !== "undefined"
|
20 |
+
? Boolean((document as FullscreenElement)[getBrowserFullscreenElementProp()])
|
21 |
+
: false
|
22 |
+
);
|
23 |
+
|
24 |
+
const setFullscreen = (maybeValue?: boolean) => {
|
25 |
+
if (!elRef.current) return;
|
26 |
+
|
27 |
+
const isFullScreen = typeof document !== "undefined" ? Boolean((document as FullscreenElement)[getBrowserFullscreenElementProp()]) : false
|
28 |
+
|
29 |
+
const shouldBeFullScreen = typeof maybeValue === "boolean" ? maybeValue : !isFullScreen
|
30 |
+
|
31 |
+
// nothing to do
|
32 |
+
if (isFullScreen === shouldBeFullScreen) {
|
33 |
+
return
|
34 |
+
}
|
35 |
+
|
36 |
+
const operation = shouldBeFullScreen
|
37 |
+
? elRef.current.requestFullscreen()
|
38 |
+
: (typeof document !== "undefined" ? document.exitFullscreen() : Promise.resolve(true))
|
39 |
+
|
40 |
+
operation.then(() => {
|
41 |
+
setIsFullscreen(shouldBeFullScreen);
|
42 |
+
})
|
43 |
+
.catch(() => {
|
44 |
+
if (typeof document !== "undefined") {
|
45 |
+
// make sure we grab a fresh value here
|
46 |
+
const isFullScreen = Boolean((document as FullscreenElement)[getBrowserFullscreenElementProp()])
|
47 |
+
setIsFullscreen(isFullScreen);
|
48 |
+
}
|
49 |
+
});
|
50 |
+
};
|
51 |
+
|
52 |
+
useLayoutEffect(() => {
|
53 |
+
if (typeof document !== "undefined") {
|
54 |
+
document.onfullscreenchange = () =>
|
55 |
+
setIsFullscreen(Boolean((document as FullscreenElement)[getBrowserFullscreenElementProp()]));
|
56 |
+
|
57 |
+
return () => {
|
58 |
+
document.onfullscreenchange = null;
|
59 |
+
};
|
60 |
+
}
|
61 |
+
}, []);
|
62 |
+
|
63 |
+
return [isFullscreen, setFullscreen, elRef];
|
64 |
+
}
|
65 |
+
|
66 |
+
function getBrowserFullscreenElementProp(): keyof FullscreenElement {
|
67 |
+
if (typeof document.fullscreenElement !== "undefined") {
|
68 |
+
return "fullscreenElement";
|
69 |
+
} else if (typeof document.mozFullScreenElement !== "undefined") {
|
70 |
+
return "mozFullScreenElement";
|
71 |
+
} else if (typeof document.msFullscreenElement !== "undefined") {
|
72 |
+
return "msFullscreenElement";
|
73 |
+
} else if (typeof document.webkitFullscreenElement !== "undefined") {
|
74 |
+
return "webkitFullscreenElement";
|
75 |
+
} else {
|
76 |
+
throw new Error("fullscreenElement is not supported by this browser");
|
77 |
+
}
|
78 |
+
}
|
src/lib/hooks/useRequestAnimationFrame.ts
CHANGED
@@ -12,7 +12,7 @@ type Config = {
|
|
12 |
*
|
13 |
* @description keep your `nextAnimationFrameHandler` as simple and performant as possible with the least amount of dependencies and transformations. It will be called frequently and this can lead to bad UX
|
14 |
*/
|
15 |
-
const useRequestAnimationFrame = (
|
16 |
nextAnimationFrameHandler: (progress: number) => void,
|
17 |
{
|
18 |
duration = Number.POSITIVE_INFINITY,
|
@@ -60,4 +60,3 @@ const useRequestAnimationFrame = (
|
|
60 |
}, [shouldAnimate])
|
61 |
}
|
62 |
|
63 |
-
export default useRequestAnimationFrame
|
|
|
12 |
*
|
13 |
* @description keep your `nextAnimationFrameHandler` as simple and performant as possible with the least amount of dependencies and transformations. It will be called frequently and this can lead to bad UX
|
14 |
*/
|
15 |
+
export const useRequestAnimationFrame = (
|
16 |
nextAnimationFrameHandler: (progress: number) => void,
|
17 |
{
|
18 |
duration = Number.POSITIVE_INFINITY,
|
|
|
60 |
}, [shouldAnimate])
|
61 |
}
|
62 |
|
|
src/lib/hooks/useScreenplayFilePicker.ts
ADDED
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useEffect, useState } from "react"
|
2 |
+
import { useTimelineState } from "@aitube/timeline"
|
3 |
+
import { parseClap } from "@aitube/clap"
|
4 |
+
import { useFilePicker } from "use-file-picker"
|
5 |
+
|
6 |
+
export function useScreenplayFilePicker() {
|
7 |
+
const setClap = useTimelineState(s => s.setClap)
|
8 |
+
const [isLoading, setIsLoading] = useState(false)
|
9 |
+
|
10 |
+
const { openFilePicker, filesContent, loading } = useFilePicker({
|
11 |
+
accept: '.txt',
|
12 |
+
readAs: "Text"
|
13 |
+
})
|
14 |
+
|
15 |
+
const fileData = filesContent[0]
|
16 |
+
|
17 |
+
useEffect(() => {
|
18 |
+
const fn = async () => {
|
19 |
+
if (fileData?.name) {
|
20 |
+
try {
|
21 |
+
setIsLoading(true)
|
22 |
+
const res = await fetch("https://jbilcke-hf-broadway-api.hf.space", {
|
23 |
+
method: "POST",
|
24 |
+
headers: { 'Content-Type': 'text/plain' },
|
25 |
+
body: fileData.content,
|
26 |
+
})
|
27 |
+
const blob = await res.blob()
|
28 |
+
const clap = await parseClap(blob)
|
29 |
+
await setClap(clap)
|
30 |
+
} catch (err) {
|
31 |
+
console.error("failed to import the screenplay:", err)
|
32 |
+
} finally {
|
33 |
+
setIsLoading(false)
|
34 |
+
}
|
35 |
+
}
|
36 |
+
}
|
37 |
+
fn()
|
38 |
+
}, [fileData?.name])
|
39 |
+
|
40 |
+
return { openFilePicker, filesContent, fileData, isLoading: loading || isLoading }
|
41 |
+
}
|
src/lib/utils/getValidBoolean.ts
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export const getValidBoolean = (something: any, defaultValue: boolean) => {
|
2 |
+
if (typeof something === "boolean") {
|
3 |
+
return something
|
4 |
+
}
|
5 |
+
|
6 |
+
const strValue = `${something || defaultValue}`.toLowerCase()
|
7 |
+
|
8 |
+
return strValue === "true" || strValue === "1" || strValue === "on"
|
9 |
+
}
|
src/lib/utils/getValidNumber.ts
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export const getValidNumber = (something: any, minValue: number, maxValue: number, defaultValue: number) => {
|
2 |
+
const strValue = `${something || defaultValue}`
|
3 |
+
const numValue = Number(strValue)
|
4 |
+
const isValid = !isNaN(numValue) && isFinite(numValue)
|
5 |
+
if (!isValid) {
|
6 |
+
return defaultValue
|
7 |
+
}
|
8 |
+
return Math.max(minValue, Math.min(maxValue, numValue))
|
9 |
+
|
10 |
+
}
|
src/lib/utils/getValidString.ts
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export function getValidString(something: any, defaultValue: string) {
|
2 |
+
const strValue = `${something || defaultValue}`
|
3 |
+
try {
|
4 |
+
return JSON.parse(strValue)
|
5 |
+
} catch (err) {
|
6 |
+
return strValue
|
7 |
+
}
|
8 |
+
}
|
src/lib/utils/index.ts
CHANGED
@@ -1 +1,9 @@
|
|
1 |
-
export { cn } from "./cn"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export { cn } from "./cn"
|
2 |
+
export { debounceAsync } from "./debounceAsync"
|
3 |
+
export { debounceSync } from "./debounceSync"
|
4 |
+
export { getValidBoolean } from "./getValidBoolean"
|
5 |
+
export { getValidNumber } from "./getValidNumber"
|
6 |
+
export { getValidString } from "./getValidString"
|
7 |
+
export { isValidNumber } from "./isValidNumber"
|
8 |
+
export { parseComfyVendor } from "./parseComfyVendor"
|
9 |
+
export { parseRenderingStrategy } from "./parseRenderingStrategy"
|
src/lib/utils/isValidNumber.ts
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
export function isValidNumber(input?: any) {
|
2 |
+
return typeof input === "number" && !isNaN(input) && isFinite(input)
|
3 |
+
}
|
src/lib/utils/parseComfyVendor.ts
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ComfyVendor } from "@/types"
|
2 |
+
|
3 |
+
export function parseComfyVendor(input: any, defaultVendor?: ComfyVendor): ComfyVendor {
|
4 |
+
|
5 |
+
let unknownString = `${input || ""}`.trim()
|
6 |
+
|
7 |
+
// the "normal" case
|
8 |
+
if (Object.values(ComfyVendor).includes(unknownString as ComfyVendor)) {
|
9 |
+
return unknownString as ComfyVendor
|
10 |
+
}
|
11 |
+
|
12 |
+
let vendor: ComfyVendor = defaultVendor || ComfyVendor.NONE
|
13 |
+
|
14 |
+
unknownString = unknownString.toLowerCase()
|
15 |
+
|
16 |
+
if (unknownString === "none" || unknownString === "undefined" || unknownString === "") {
|
17 |
+
vendor = ComfyVendor.NONE
|
18 |
+
}
|
19 |
+
else if (unknownString === "huggingface" || unknownString === "hugging_face") {
|
20 |
+
vendor = ComfyVendor.HUGGINGFACE
|
21 |
+
}
|
22 |
+
else if (unknownString === "replicate") {
|
23 |
+
vendor = ComfyVendor.REPLICATE
|
24 |
+
} else {
|
25 |
+
vendor = ComfyVendor.NONE
|
26 |
+
}
|
27 |
+
return vendor
|
28 |
+
}
|
src/lib/utils/parseRenderingStrategy.ts
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { RenderingStrategy } from "@/types"
|
2 |
+
|
3 |
+
export function parseRenderingStrategy(input: any, defaultStrategy?: RenderingStrategy): RenderingStrategy {
|
4 |
+
|
5 |
+
let unknownString = `${input || ""}`.trim()
|
6 |
+
|
7 |
+
// the "normal" case
|
8 |
+
if (Object.values(RenderingStrategy).includes(unknownString as RenderingStrategy)) {
|
9 |
+
return unknownString as RenderingStrategy
|
10 |
+
}
|
11 |
+
|
12 |
+
let strategy: RenderingStrategy = defaultStrategy || RenderingStrategy.ON_DEMAND
|
13 |
+
|
14 |
+
unknownString = unknownString.toLowerCase()
|
15 |
+
|
16 |
+
if (unknownString === "on_demand") {
|
17 |
+
strategy = RenderingStrategy.ON_DEMAND
|
18 |
+
}
|
19 |
+
else if (unknownString === "on_screen_only") {
|
20 |
+
strategy = RenderingStrategy.ON_SCREEN_ONLY
|
21 |
+
}
|
22 |
+
else if (unknownString === "on_screen_then_surrounding") {
|
23 |
+
strategy = RenderingStrategy.ON_SCREEN_THEN_SURROUNDING
|
24 |
+
}
|
25 |
+
else if (unknownString === "on_screen_then_all") {
|
26 |
+
strategy = RenderingStrategy.ON_SCREEN_THEN_ALL
|
27 |
+
} else {
|
28 |
+
strategy = RenderingStrategy.ON_DEMAND
|
29 |
+
}
|
30 |
+
return strategy
|
31 |
+
}
|
src/server/comfy/{formatStoryboardWorkflow.ts β getComfyWorkflow.ts}
RENAMED
@@ -1,12 +1,23 @@
|
|
|
|
1 |
import { getVideoPrompt } from "@aitube/engine"
|
2 |
|
3 |
import { ComfyNode, RenderRequest } from "@/types"
|
4 |
|
5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6 |
// parse the node array from the ComfyUI workflow
|
7 |
-
const nodes = Object.values(JSON.parse(
|
8 |
|
9 |
-
const
|
10 |
request.segments,
|
11 |
request.entities
|
12 |
)
|
@@ -14,7 +25,7 @@ export function formatStoryboardWorkflow(request: RenderRequest) {
|
|
14 |
for (const node of nodes) {
|
15 |
if (typeof node.inputs.text === "string") {
|
16 |
if (node._meta.title.includes("Prompt")) {
|
17 |
-
node.inputs.text =
|
18 |
}
|
19 |
}
|
20 |
}
|
|
|
1 |
+
import { ClapSegmentCategory } from "@aitube/clap"
|
2 |
import { getVideoPrompt } from "@aitube/engine"
|
3 |
|
4 |
import { ComfyNode, RenderRequest } from "@/types"
|
5 |
|
6 |
+
// TODO move this to @aitube/engine or @aitube/engine-comfy
|
7 |
+
export function getComfyWorkflow(request: RenderRequest) {
|
8 |
+
|
9 |
+
let comfyWorkflow = "{}"
|
10 |
+
|
11 |
+
if (request.segment.category === ClapSegmentCategory.STORYBOARD) {
|
12 |
+
comfyWorkflow = request.storyboardWorkflow
|
13 |
+
} else if (request.segment.category === ClapSegmentCategory.VIDEO) {
|
14 |
+
comfyWorkflow = request.videoWorkflow
|
15 |
+
}
|
16 |
+
|
17 |
// parse the node array from the ComfyUI workflow
|
18 |
+
const nodes = Object.values(JSON.parse(comfyWorkflow)) as ComfyNode[]
|
19 |
|
20 |
+
const visualPrompt = getVideoPrompt(
|
21 |
request.segments,
|
22 |
request.entities
|
23 |
)
|
|
|
25 |
for (const node of nodes) {
|
26 |
if (typeof node.inputs.text === "string") {
|
27 |
if (node._meta.title.includes("Prompt")) {
|
28 |
+
node.inputs.text = visualPrompt
|
29 |
}
|
30 |
}
|
31 |
}
|
src/server/comfy/index.ts
CHANGED
@@ -4,16 +4,19 @@ import { RenderRequest } from "@/types"
|
|
4 |
|
5 |
import { run as runWithReplicate } from "./replicate"
|
6 |
import { run as runWithHuggingFace } from "./huggingface"
|
7 |
-
import {
|
8 |
|
9 |
-
|
|
|
|
|
10 |
|
11 |
-
const workflow =
|
12 |
|
13 |
// TODO support Hugging Face as well
|
14 |
// const await runWithHuggingFace({
|
|
|
15 |
const result = await runWithReplicate({
|
16 |
-
apiKey: request.
|
17 |
workflow,
|
18 |
})
|
19 |
|
|
|
4 |
|
5 |
import { run as runWithReplicate } from "./replicate"
|
6 |
import { run as runWithHuggingFace } from "./huggingface"
|
7 |
+
import { getComfyWorkflow } from "./getComfyWorkflow"
|
8 |
|
9 |
+
// TODO: at some point in the future we will
|
10 |
+
// move src/server/comfy to @aitube/engine
|
11 |
+
export async function render(request: RenderRequest): Promise<string> {
|
12 |
|
13 |
+
const workflow = getComfyWorkflow(request)
|
14 |
|
15 |
// TODO support Hugging Face as well
|
16 |
// const await runWithHuggingFace({
|
17 |
+
|
18 |
const result = await runWithReplicate({
|
19 |
+
apiKey: request.comfyUiApiKey,
|
20 |
workflow,
|
21 |
})
|
22 |
|
src/server/comfy/replicate.ts
CHANGED
@@ -12,6 +12,7 @@ export async function run({
|
|
12 |
|
13 |
const replicate = new Replicate({ auth: apiKey })
|
14 |
|
|
|
15 |
const cogId = "fofr/any-comfyui-workflow:74f12621dc9f9b7cdca50d03941b8ddb3a368d7f5a1bb16fb7e1b87f05d96bf5"
|
16 |
|
17 |
const output = await replicate.run(cogId, {
|
|
|
12 |
|
13 |
const replicate = new Replicate({ auth: apiKey })
|
14 |
|
15 |
+
// https://replicate.com/fofr/any-comfyui-workflow
|
16 |
const cogId = "fofr/any-comfyui-workflow:74f12621dc9f9b7cdca50d03941b8ddb3a368d7f5a1bb16fb7e1b87f05d96bf5"
|
17 |
|
18 |
const output = await replicate.run(cogId, {
|
src/settings/rendering/getDefaultSettingsRendering.ts
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ComfyVendor, RenderingStrategy } from "@/types"
|
2 |
+
|
3 |
+
import { SettingsRenderingState } from "./types"
|
4 |
+
|
5 |
+
export function getDefaultSettingsRendering(): SettingsRenderingState {
|
6 |
+
const state: SettingsRenderingState = {
|
7 |
+
comfyUiApiVendor: ComfyVendor.NONE,
|
8 |
+
comfyUiApiKey: "",
|
9 |
+
storyboardRenderingStrategy: RenderingStrategy.ON_DEMAND,
|
10 |
+
videoRenderingStrategy: RenderingStrategy.ON_DEMAND,
|
11 |
+
maxNbAssetsToGenerateInParallel: 1,
|
12 |
+
}
|
13 |
+
|
14 |
+
return state
|
15 |
+
}
|