Compare commits

..
Author SHA1 Message Date
bflatastic 2b176d023e updated upload flow - auto open asset modal after upload 2025-05-07 16:04:30 +07:00
bflatastic eef929c065 improved upload 2025-05-07 14:23:45 +07:00
bflatastic 91ea263b0c tab selector to have 3d first, and logic to hide tooltip when asset modal open 2025-05-07 12:42:04 +07:00
Brandon Thomas 196e1c4615 fix broken package (#585) 2025-05-06 17:54:34 -04:00
bflatastic 8f321f06d0 thumnail to always load in img tag even if there's error 2025-05-06 22:30:27 +07:00
bflatastic 0b338f3886 login logic fixed 2025-05-06 14:26:49 +07:00
BFlat de76fc6c36 login modal on 3D (#584)
- login modal is a shared component that
- finished but needs checking session if openai/sora logged in or not
- replaced video for 3d (it had a black screen at the end)
2025-05-06 13:15:05 +07:00
bflatastic 9fb0ca2dab fixed logo being squished 2025-05-06 10:36:09 +07:00
Brandon Thomas 42965dfcfa modal settings menu (#583) 2025-05-05 23:06:32 -04:00
ArEnScandTanish Manku 8c7b358534 Feature/2d integration (#581)
Co-authored-by: Tanish Manku <-h>
2025-05-05 16:00:12 -04:00
ArEnScandMichael Chung d8851abf24 Removing lower panel and timeline files too much stuff, builds and wo… (#580)
…rks fine.

Co-authored-by: Michael Chung <michaelchung@michaels-mbp-2.home>
2025-05-05 15:06:28 -04:00
bflatastic 2c8254e50f updated artcraft site to show carter's thunderbolts video 2025-05-05 22:33:49 +07:00
bflatastic 15348f7b0e draw attention to add 3d object with bouncing tooltip 2025-05-05 22:08:16 +07:00
Brandon Thomas 6071dda4f9 Settings system backend (#579) 2025-05-04 16:02:54 -04:00
105 changed files with 2186 additions and 3056 deletions
Generated
+594 -63
View File
@@ -839,6 +839,7 @@ dependencies = [
"subprocess_common",
"tauri",
"tauri-build",
"tauri-plugin-dialog",
"tauri-plugin-http",
"tauri-plugin-log",
"tauri-plugin-upload",
@@ -856,6 +857,27 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e"
[[package]]
name = "ashpd"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d43c03d9e36dd40cab48435be0b09646da362c278223ca535493877b2c1dee9"
dependencies = [
"enumflags2",
"futures-channel",
"futures-util",
"rand 0.8.5",
"raw-window-handle",
"serde",
"serde_repr",
"tokio",
"url",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"zbus",
]
[[package]]
name = "assert_float_eq"
version = "1.1.4"
@@ -871,6 +893,18 @@ dependencies = [
"num-traits",
]
[[package]]
name = "async-broadcast"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
dependencies = [
"event-listener 5.2.0",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-channel"
version = "1.9.0"
@@ -878,10 +912,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
dependencies = [
"concurrent-queue",
"event-listener",
"event-listener 2.5.3",
"futures-core",
]
[[package]]
name = "async-channel"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f2776ead772134d55b62dd45e59a79e21612d85d0af729b8b7d3967d601a62a"
dependencies = [
"concurrent-queue",
"event-listener 5.2.0",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-compression"
version = "0.4.5"
@@ -898,6 +945,84 @@ dependencies = [
"zstd-safe 7.1.0",
]
[[package]]
name = "async-io"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059"
dependencies = [
"async-lock",
"cfg-if 1.0.0",
"concurrent-queue",
"futures-io",
"futures-lite 2.6.0",
"parking",
"polling",
"rustix 0.38.44",
"slab",
"tracing",
"windows-sys 0.59.0",
]
[[package]]
name = "async-lock"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18"
dependencies = [
"event-listener 5.2.0",
"event-listener-strategy",
"pin-project-lite",
]
[[package]]
name = "async-process"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb"
dependencies = [
"async-channel 2.3.0",
"async-io",
"async-lock",
"async-signal",
"async-task",
"blocking",
"cfg-if 1.0.0",
"event-listener 5.2.0",
"futures-lite 2.6.0",
"rustix 0.38.44",
"tracing",
]
[[package]]
name = "async-recursion"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
]
[[package]]
name = "async-signal"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3"
dependencies = [
"async-io",
"async-lock",
"atomic-waker",
"cfg-if 1.0.0",
"futures-core",
"futures-io",
"rustix 0.38.44",
"signal-hook-registry",
"slab",
"windows-sys 0.59.0",
]
[[package]]
name = "async-stream"
version = "0.3.5"
@@ -944,6 +1069,12 @@ dependencies = [
"tokio",
]
[[package]]
name = "async-task"
version = "4.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]]
name = "async-trait"
version = "0.1.80"
@@ -1012,7 +1143,7 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c232177ba50b16fe7a4588495bd474a62a9e45a8e4ca6fd7d0b7ac29d164631e"
dependencies = [
"nix",
"nix 0.26.4",
"rand 0.8.5",
]
@@ -1409,6 +1540,19 @@ dependencies = [
"objc2 0.6.1",
]
[[package]]
name = "blocking"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea"
dependencies = [
"async-channel 2.3.0",
"async-task",
"futures-io",
"futures-lite 2.6.0",
"piper",
]
[[package]]
name = "blowfish"
version = "0.9.1"
@@ -1439,7 +1583,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5430e3be710b68d984d1391c854eb431a9d548640711faa54eecb1df93db91cc"
dependencies = [
"borsh-derive",
"cfg_aliases",
"cfg_aliases 0.2.1",
]
[[package]]
@@ -1997,6 +2141,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
@@ -3037,6 +3187,17 @@ dependencies = [
"serde",
]
[[package]]
name = "derivative"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "derive_arbitrary"
version = "1.3.2"
@@ -3356,6 +3517,15 @@ dependencies = [
"syn 2.0.87",
]
[[package]]
name = "dlib"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
dependencies = [
"libloading 0.8.6",
]
[[package]]
name = "dlopen2"
version = "0.7.0"
@@ -3415,6 +3585,12 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1"
[[package]]
name = "downcast-rs"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
[[package]]
name = "download-job"
version = "0.0.1"
@@ -3786,6 +3962,12 @@ dependencies = [
"cfg-if 1.0.0",
]
[[package]]
name = "endi"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf"
[[package]]
name = "enterpolation"
version = "0.2.1"
@@ -3810,6 +3992,27 @@ dependencies = [
"syn 2.0.87",
]
[[package]]
name = "enumflags2"
version = "0.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba2f4b465f5318854c6f8dd686ede6c0a9dc67d4b1ac241cf0eb51521a309147"
dependencies = [
"enumflags2_derive",
"serde",
]
[[package]]
name = "enumflags2_derive"
version = "0.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc4caf64a58d7a6d65ab00639b046ff54399a39f5f2554728895ace4b297cd79"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
]
[[package]]
name = "enums"
version = "0.0.1"
@@ -3956,6 +4159,27 @@ version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
[[package]]
name = "event-listener"
version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b5fb89194fa3cad959b833185b3063ba881dbfc7030680b314250779fb4cc91"
dependencies = [
"concurrent-queue",
"parking",
"pin-project-lite",
]
[[package]]
name = "event-listener-strategy"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
dependencies = [
"event-listener 5.2.0",
"pin-project-lite",
]
[[package]]
name = "exr"
version = "1.73.0"
@@ -4305,6 +4529,19 @@ dependencies = [
"waker-fn",
]
[[package]]
name = "futures-lite"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532"
dependencies = [
"fastrand 2.3.0",
"futures-core",
"futures-io",
"parking",
"pin-project-lite",
]
[[package]]
name = "futures-macro"
version = "0.3.30"
@@ -5299,9 +5536,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e9b187a72d63adbfba487f48095306ac823049cb504ee195541e91c7775f5ad"
dependencies = [
"anyhow",
"async-channel",
"async-channel 1.9.0",
"base64 0.13.1",
"futures-lite",
"futures-lite 1.13.0",
"http 0.2.11",
"infer 0.2.3",
"pin-project-lite",
@@ -7194,7 +7431,7 @@ dependencies = [
"gtk",
"keyboard-types",
"objc2 0.6.1",
"objc2-app-kit",
"objc2-app-kit 0.3.1",
"objc2-core-foundation",
"objc2-foundation 0.3.1",
"once_cell",
@@ -7382,6 +7619,19 @@ dependencies = [
"pin-utils",
]
[[package]]
name = "nix"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
dependencies = [
"bitflags 2.5.0",
"cfg-if 1.0.0",
"cfg_aliases 0.1.1",
"libc",
"memoffset 0.9.0",
]
[[package]]
name = "nodrop"
version = "0.1.14"
@@ -7623,6 +7873,22 @@ dependencies = [
"objc2-exception-helper",
]
[[package]]
name = "objc2-app-kit"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
dependencies = [
"bitflags 2.5.0",
"block2 0.5.1",
"libc",
"objc2 0.5.2",
"objc2-core-data 0.2.2",
"objc2-core-image 0.2.2",
"objc2-foundation 0.2.2",
"objc2-quartz-core 0.2.2",
]
[[package]]
name = "objc2-app-kit"
version = "0.3.1"
@@ -7634,10 +7900,10 @@ dependencies = [
"libc",
"objc2 0.6.1",
"objc2-cloud-kit",
"objc2-core-data",
"objc2-core-data 0.3.1",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-core-image",
"objc2-core-image 0.3.1",
"objc2-foundation 0.3.1",
"objc2-quartz-core 0.3.1",
]
@@ -7653,6 +7919,18 @@ dependencies = [
"objc2-foundation 0.3.1",
]
[[package]]
name = "objc2-core-data"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
dependencies = [
"bitflags 2.5.0",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
]
[[package]]
name = "objc2-core-data"
version = "0.3.1"
@@ -7688,6 +7966,18 @@ dependencies = [
"objc2-io-surface",
]
[[package]]
name = "objc2-core-image"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
dependencies = [
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
"objc2-metal",
]
[[package]]
name = "objc2-core-image"
version = "0.3.1"
@@ -7721,6 +8011,7 @@ checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
dependencies = [
"bitflags 2.5.0",
"block2 0.5.1",
"dispatch",
"libc",
"objc2 0.5.2",
]
@@ -7806,7 +8097,7 @@ dependencies = [
"bitflags 2.5.0",
"block2 0.6.1",
"objc2 0.6.1",
"objc2-app-kit",
"objc2-app-kit 0.3.1",
"objc2-core-foundation",
"objc2-foundation 0.3.1",
]
@@ -8117,6 +8408,16 @@ dependencies = [
"hashbrown 0.14.5",
]
[[package]]
name = "ordered-stream"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
dependencies = [
"futures-core",
"pin-project-lite",
]
[[package]]
name = "ort"
version = "1.16.3"
@@ -8127,7 +8428,6 @@ dependencies = [
"half",
"lazy_static",
"libc",
"libloading 0.7.4",
"ndarray",
"tar",
"thiserror 1.0.63",
@@ -8570,6 +8870,17 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "piper"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066"
dependencies = [
"atomic-waker",
"fastrand 2.3.0",
"futures-io",
]
[[package]]
name = "pkcs1"
version = "0.7.5"
@@ -8623,6 +8934,21 @@ dependencies = [
"miniz_oxide 0.8.4",
]
[[package]]
name = "polling"
version = "3.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f"
dependencies = [
"cfg-if 1.0.0",
"concurrent-queue",
"hermit-abi 0.4.0",
"pin-project-lite",
"rustix 0.38.44",
"tracing",
"windows-sys 0.59.0",
]
[[package]]
name = "portable-atomic"
version = "1.10.0"
@@ -9090,6 +9416,15 @@ dependencies = [
"serde",
]
[[package]]
name = "quick-xml"
version = "0.37.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
dependencies = [
"memchr",
]
[[package]]
name = "quinn"
version = "0.11.5"
@@ -9774,6 +10109,29 @@ dependencies = [
"subtle",
]
[[package]]
name = "rfd"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8af382a047821a08aa6bfc09ab0d80ff48d45d8726f7cd8e44891f7cb4a4278e"
dependencies = [
"ashpd",
"block2 0.5.1",
"glib-sys",
"gobject-sys",
"gtk-sys",
"js-sys",
"log",
"objc2 0.5.2",
"objc2-app-kit 0.2.2",
"objc2-foundation 0.2.2",
"raw-window-handle",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows-sys 0.48.0",
]
[[package]]
name = "rgb"
version = "0.8.50"
@@ -10017,15 +10375,15 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.38.34"
version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [
"bitflags 2.5.0",
"errno",
"libc",
"linux-raw-sys 0.4.14",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -10239,6 +10597,12 @@ dependencies = [
"syn 2.0.87",
]
[[package]]
name = "scoped-tls"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "scopeguard"
version = "1.2.0"
@@ -10922,7 +11286,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08"
dependencies = [
"bytemuck",
"cfg_aliases",
"cfg_aliases 0.2.1",
"core-graphics",
"foreign-types 0.5.0",
"js-sys",
@@ -11049,7 +11413,7 @@ dependencies = [
"crossbeam-queue",
"dotenvy",
"either",
"event-listener",
"event-listener 2.5.3",
"futures-channel",
"futures-core",
"futures-intrusive",
@@ -12057,7 +12421,7 @@ dependencies = [
"ndk-context",
"ndk-sys 0.6.0+11769913",
"objc2 0.6.1",
"objc2-app-kit",
"objc2-app-kit 0.3.1",
"objc2-foundation 0.3.1",
"once_cell",
"parking_lot 0.12.2",
@@ -12129,7 +12493,7 @@ dependencies = [
"mime",
"muda",
"objc2 0.6.1",
"objc2-app-kit",
"objc2-app-kit 0.3.1",
"objc2-foundation 0.3.1",
"percent-encoding",
"plist",
@@ -12236,6 +12600,24 @@ dependencies = [
"walkdir",
]
[[package]]
name = "tauri-plugin-dialog"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcaf6e5d6062423a0f711a23c2a573ccba222b6a16a9322d8499928f27e41376"
dependencies = [
"log",
"raw-window-handle",
"rfd",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"tauri-plugin-fs",
"thiserror 2.0.12",
"url",
]
[[package]]
name = "tauri-plugin-fs"
version = "2.2.1"
@@ -12323,47 +12705,6 @@ dependencies = [
"tokio-util",
]
[[package]]
name = "tauri-realtime"
version = "0.1.0"
dependencies = [
"anyhow",
"assertor",
"base64 0.22.1",
"bytes 1.10.0",
"chrono",
"const_format",
"crockford",
"cudarc",
"directories 6.0.0",
"dirs 6.0.0",
"filesys",
"futures 0.3.29",
"half",
"image",
"libloading 0.8.6",
"log",
"memory_caching",
"ml_models",
"ml_weights_registry",
"once_cell",
"ort",
"primitives",
"rand 0.8.5",
"reqwest 0.12.15",
"serde",
"serde_json",
"serde_yaml",
"serial_test",
"tauri",
"tauri-build",
"tauri-plugin-log",
"tempdir",
"tempfile",
"tokio",
"tokio-util",
]
[[package]]
name = "tauri-runtime"
version = "2.5.1"
@@ -12395,7 +12736,7 @@ dependencies = [
"jni",
"log",
"objc2 0.6.1",
"objc2-app-kit",
"objc2-app-kit 0.3.1",
"objc2-foundation 0.3.1",
"once_cell",
"percent-encoding",
@@ -12802,6 +13143,7 @@ dependencies = [
"signal-hook-registry",
"socket2",
"tokio-macros",
"tracing",
"windows-sys 0.52.0",
]
@@ -13230,7 +13572,7 @@ dependencies = [
"libappindicator",
"muda",
"objc2 0.6.1",
"objc2-app-kit",
"objc2-app-kit 0.3.1",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-foundation 0.3.1",
@@ -13363,6 +13705,17 @@ version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "uds_windows"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9"
dependencies = [
"memoffset 0.9.0",
"tempfile",
"winapi 0.3.9",
]
[[package]]
name = "ug"
version = "0.1.0"
@@ -14019,6 +14372,66 @@ dependencies = [
"web-sys",
]
[[package]]
name = "wayland-backend"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe770181423e5fc79d3e2a7f4410b7799d5aab1de4372853de3c6aa13ca24121"
dependencies = [
"cc",
"downcast-rs",
"rustix 0.38.44",
"scoped-tls",
"smallvec 1.13.2",
"wayland-sys",
]
[[package]]
name = "wayland-client"
version = "0.31.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978fa7c67b0847dbd6a9f350ca2569174974cd4082737054dbb7fbb79d7d9a61"
dependencies = [
"bitflags 2.5.0",
"rustix 0.38.44",
"wayland-backend",
"wayland-scanner",
]
[[package]]
name = "wayland-protocols"
version = "0.32.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "779075454e1e9a521794fed15886323ea0feda3f8b0fc1390f5398141310422a"
dependencies = [
"bitflags 2.5.0",
"wayland-backend",
"wayland-client",
"wayland-scanner",
]
[[package]]
name = "wayland-scanner"
version = "0.31.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484"
dependencies = [
"proc-macro2",
"quick-xml 0.37.5",
"quote",
]
[[package]]
name = "wayland-sys"
version = "0.31.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615"
dependencies = [
"dlib",
"log",
"pkg-config",
]
[[package]]
name = "web-sys"
version = "0.3.65"
@@ -14230,7 +14643,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c"
dependencies = [
"objc2 0.6.1",
"objc2-app-kit",
"objc2-app-kit 0.3.1",
"objc2-core-foundation",
"objc2-foundation 0.3.1",
"raw-window-handle",
@@ -14919,7 +15332,7 @@ dependencies = [
"libc",
"ndk 0.9.0",
"objc2 0.6.1",
"objc2-app-kit",
"objc2-app-kit 0.3.1",
"objc2-core-foundation",
"objc2-foundation 0.3.1",
"objc2-ui-kit",
@@ -14989,7 +15402,17 @@ checksum = "e105d177a3871454f754b33bb0ee637ecaaac997446375fd3e5d43a2ed00c909"
dependencies = [
"libc",
"linux-raw-sys 0.4.14",
"rustix 0.38.34",
"rustix 0.38.44",
]
[[package]]
name = "xdg-home"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6"
dependencies = [
"libc",
"windows-sys 0.59.0",
]
[[package]]
@@ -15016,6 +15439,65 @@ dependencies = [
"synstructure",
]
[[package]]
name = "zbus"
version = "4.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9ff46f2a25abd690ed072054733e0bc3157e3d4c45f41bd183dce09c2ff8ab9"
dependencies = [
"async-broadcast",
"async-process",
"async-recursion",
"async-trait",
"derivative",
"enumflags2",
"event-listener 5.2.0",
"futures-core",
"futures-sink",
"futures-util",
"hex",
"nix 0.28.0",
"ordered-stream",
"rand 0.8.5",
"serde",
"serde_repr",
"sha1 0.10.6",
"static_assertions",
"tokio",
"tracing",
"uds_windows",
"windows-sys 0.52.0",
"xdg-home",
"zbus_macros",
"zbus_names",
"zvariant",
]
[[package]]
name = "zbus_macros"
version = "4.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e0e3852c93dcdb49c9462afe67a2a468f7bd464150d866e861eaf06208633e0"
dependencies = [
"proc-macro-crate 3.1.0",
"proc-macro2",
"quote",
"regex",
"syn 1.0.109",
"zvariant_utils 1.1.0",
]
[[package]]
name = "zbus_names"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c"
dependencies = [
"serde",
"static_assertions",
"zvariant",
]
[[package]]
name = "zerocopy"
version = "0.7.33"
@@ -15268,3 +15750,52 @@ checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028"
dependencies = [
"zune-core",
]
[[package]]
name = "zvariant"
version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe"
dependencies = [
"endi",
"enumflags2",
"serde",
"static_assertions",
"url",
"zvariant_derive",
]
[[package]]
name = "zvariant_derive"
version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449"
dependencies = [
"proc-macro-crate 3.1.0",
"proc-macro2",
"quote",
"syn 2.0.87",
"zvariant_utils 2.1.0",
]
[[package]]
name = "zvariant_utils"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00bedb16a193cc12451873fee2a1bc6550225acece0e36f333e68326c73c8172"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "zvariant_utils"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
]
-1
View File
@@ -25,7 +25,6 @@ members = [
# Desktop apps
"crates/desktop/artcraft",
"crates/desktop/tauri-realtime",
# Shared libraries
"crates/lib/actix_cors_configs",
+1
View File
@@ -67,6 +67,7 @@ tauri-plugin-http = { path = "../../vendor/tauri-plugin-http" }
# NB: The version of the `discord-rich-presence` crate (0.2.5) last published has non-Send/Sync error
# types and can't do threading. We need to wait for a new version to unpin from Git.
discord-rich-presence = { git = "https://github.com/vionya/discord-rich-presence" }
tauri-plugin-dialog = "2"
[dev-dependencies]
libloading = "0.8.6"
@@ -25,6 +25,7 @@
"url": "https://private.tauri.app"
}
]
}
},
"dialog:default"
]
}
@@ -14,8 +14,18 @@ pub struct GetAppPreferencesResponse {
pub struct AppPreferencesPayload {
/// The downloads directory to use when a user downloads a file.
pub preferred_download_directory: PreferredDownloadDirectory,
/// Play sounds on events.
pub play_sounds: bool,
/// Key pointing to file; defined in the frontend code.
pub generation_success_sound: Option<String>,
/// Key pointing to file; defined in the frontend code.
pub generation_failure_sound: Option<String>,
/// Key pointing to file; defined in the frontend code.
pub generation_enqueue_sound: Option<String>,
}
@@ -42,5 +52,8 @@ async fn get_prefs(app_prefs: &AppPreferencesManager) -> AnyhowResult<AppPrefere
Ok(AppPreferencesPayload {
preferred_download_directory: prefs.preferred_download_directory,
play_sounds: prefs.play_sounds,
generation_success_sound: prefs.generation_success_sound,
generation_failure_sound: prefs.generation_failure_sound,
generation_enqueue_sound: prefs.generation_enqueue_sound,
})
}
@@ -1,8 +1,10 @@
use crate::state::app_preferences::app_preferences_manager::AppPreferencesManager;
use crate::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
use crate::state::data_dir::app_data_root::AppDataRoot;
use anyhow::anyhow;
use errors::AnyhowResult;
use log::{error, info};
use openai_sora_client::sora_error::SoraError::AnyhowError;
use serde_derive::{Deserialize, Serialize};
use tauri::{AppHandle, State};
@@ -11,7 +13,15 @@ use tauri::{AppHandle, State};
pub struct UpdateAppPreferencesRequest {
pub preference: PreferenceName,
/// We'll decode this with respect to the preference value.
pub value: String,
pub value: Option<ValueType>,
}
#[derive(Deserialize, Debug)]
#[serde(untagged)]
pub enum ValueType {
Bool(bool),
String(String),
DownloadDirectory(PreferredDownloadDirectory),
}
#[derive(Deserialize)]
@@ -19,6 +29,9 @@ pub struct UpdateAppPreferencesRequest {
pub enum PreferenceName {
PreferredDownloadDirectory,
PlaySounds,
GenerationSuccessSound,
GenerationFailureSound,
GenerationEnqueueSound,
}
#[derive(Serialize)]
@@ -26,7 +39,6 @@ pub struct UpdateAppPreferencesResponse {
pub success: bool,
}
#[tauri::command]
pub async fn update_app_preferences_command(
request: UpdateAppPreferencesRequest,
@@ -54,12 +66,39 @@ async fn update_prefs(
) -> AnyhowResult<()> {
let mut prefs = app_prefs.get_clone()?;
info!("Value is: {:?}", request.value);
match request.preference {
PreferenceName::PreferredDownloadDirectory => {
prefs.preferred_download_directory = request.value.parse::<PreferredDownloadDirectory>()?;
match request.value {
Some(ValueType::DownloadDirectory(dir)) =>
prefs.preferred_download_directory = dir,
_ =>
return Err(anyhow!("Invalid value: {:?}", request.value)),
}
}
PreferenceName::PlaySounds => {
prefs.play_sounds = request.value.parse::<bool>()?;
match request.value {
Some(ValueType::Bool(val)) =>
prefs.play_sounds = val,
_ =>
return Err(anyhow!("Invalid value: {:?}", request.value)),
}
}
PreferenceName::GenerationSuccessSound => {
prefs.generation_success_sound = request.value
.map(|val| string_value(&val))
.transpose()?;
}
PreferenceName::GenerationFailureSound => {
prefs.generation_failure_sound = request.value
.map(|val| string_value(&val))
.transpose()?;
}
PreferenceName::GenerationEnqueueSound => {
prefs.generation_enqueue_sound = request.value
.map(|val| string_value(&val))
.transpose()?;
}
}
@@ -68,3 +107,10 @@ async fn update_prefs(
Ok(())
}
fn string_value(value: &ValueType) -> AnyhowResult<String> {
match value {
ValueType::String(val) => Ok(val.to_string()),
_ => Err(anyhow!("Invalid value type: {:?}", value)),
}
}
@@ -21,13 +21,23 @@ use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
use openai_sora_client::requests::image_gen::sora_image_gen_remix::{sora_image_gen_remix, SoraImageGenRemixRequest};
use openai_sora_client::requests::upload::upload_media_from_bytes::sora_media_upload_from_bytes;
use openai_sora_client::requests::upload::upload_media_from_file::sora_media_upload_from_file;
use serde_derive::Deserialize;
use serde_derive::{Deserialize, Serialize};
use std::fs::read_to_string;
use std::io::Cursor;
use storyteller_client::media_files::get_media_file::get_media_file;
use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, Manager, State};
use tauri::{AppHandle, Emitter, Manager, State};
use tokens::tokens::media_files::MediaFileToken;
use crate::threads::sora_task_polling_thread::SoraImageGenerationComplete;
#[derive(Debug, Clone, Serialize)]
pub struct SoraImageEnqueueFailure {
// TODO: Reason.
}
#[derive(Debug, Clone, Serialize)]
pub struct SoraImageEnqueueSuccess {
}
#[derive(Deserialize)]
pub struct SoraImageRemixCommand {
@@ -48,7 +58,7 @@ pub struct SoraImageRemixCommand {
#[tauri::command]
pub async fn sora_image_remix_command(
_app: AppHandle,
app: AppHandle,
request: SoraImageRemixCommand,
app_data_root: State<'_, AppDataRoot>,
sora_creds_manager: State<'_, SoraCredentialManager>,
@@ -60,12 +70,23 @@ pub async fn sora_image_remix_command(
// TODO(bt,2025-04-24): Better error messages to caller
generate_image(request, &app_data_root, &sora_creds_manager, &sora_task_queue)
.await
.map_err(|err| {
error!("error: {:?}", err);
"there was an error".to_string()
})?;
let result = generate_image(request, &app_data_root, &sora_creds_manager, &sora_task_queue).await;
if let Err(err) = result {
error!("error: {:?}", err);
let result = app.emit("sora-image-enqueue-failure", SoraImageEnqueueFailure{});
if let Err(err) = result {
error!("Failed to emit event: {:?}", err);
}
return Err("there was an error".to_string());
}
let result = app.emit("sora-image-enqueue-success", SoraImageEnqueueSuccess{});
if let Err(err) = result {
error!("Failed to emit event: {:?}", err);
}
Ok("success".to_string())
}
+5 -1
View File
@@ -7,11 +7,12 @@ pub mod utils;
use tauri::Manager;
use crate::commands::flip_image::flip_image;
use crate::commands::app_preferences::get_app_preferences_command::get_app_preferences_command;
use crate::commands::app_preferences::update_app_preference_command::update_app_preferences_command;
use crate::commands::platform_info_command::platform_info_command;
use crate::commands::sora::open_sora_login_command::open_sora_login_command;
use crate::commands::sora::sora_image_generation_command::sora_image_generation_command;
use crate::commands::sora::sora_image_remix_command::sora_image_remix_command;
use crate::commands::app_preferences::get_app_preferences_command::get_app_preferences_command;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::main_window_size::MainWindowSize;
use crate::state::sora::sora_credential_manager::SoraCredentialManager;
@@ -25,6 +26,7 @@ use crate::utils::webview_unsafe::webview_unsafe_for_app;
use crate::state::app_preferences::app_preferences_manager::load_app_preferences_or_default;
use tauri_plugin_http;
use tauri_plugin_dialog;
use tauri_plugin_log::Target;
use tauri_plugin_log::TargetKind;
@@ -54,6 +56,7 @@ pub fn run() {
println!("Initializing backend runtime...");
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_upload::init())
.plugin(tauri_plugin_log::Builder::new()
@@ -119,6 +122,7 @@ pub fn run() {
.invoke_handler(tauri::generate_handler![
flip_image,
get_app_preferences_command,
update_app_preferences_command,
open_sora_login_command,
platform_info_command,
sora_image_generation_command,
@@ -1,5 +1,5 @@
use crate::state::app_preferences::app_preferences_serializable::AppPreferencesSerializable;
use crate::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
use crate::state::app_preferences::preferred_download_directory::{PreferredDownloadDirectory, SystemDownloadDirectory};
use crate::state::data_dir::app_data_root::AppDataRoot;
#[derive(Clone)]
@@ -9,13 +9,26 @@ pub struct AppPreferences {
/// Play sounds on events.
pub play_sounds: bool,
/// Key pointing to file; defined in the frontend code.
pub generation_success_sound: Option<String>,
/// Key pointing to file; defined in the frontend code.
pub generation_failure_sound: Option<String>,
/// Key pointing to file; defined in the frontend code.
pub generation_enqueue_sound: Option<String>,
}
impl Default for AppPreferences {
fn default() -> Self {
Self {
preferred_download_directory: PreferredDownloadDirectory::SystemDefault,
preferred_download_directory: PreferredDownloadDirectory::System(SystemDownloadDirectory::Downloads),
play_sounds: true,
// NB: These are defined in the frontend.
generation_success_sound: Some("flower".to_string()),
generation_failure_sound: Some("crumble".to_string()),
generation_enqueue_sound: Some("done".to_string()),
}
}
}
@@ -42,9 +55,6 @@ impl AppPreferences {
}
pub fn to_serializable(&self) -> AppPreferencesSerializable {
AppPreferencesSerializable {
preferred_download_directory: Some(self.preferred_download_directory.clone()),
play_sounds: Some(self.play_sounds),
}
AppPreferencesSerializable::from_preferences(self)
}
}
@@ -4,13 +4,28 @@ use crate::state::data_dir::app_data_root::AppDataRoot;
use errors::AnyhowResult;
use serde_derive::{Deserialize, Serialize};
/// Vector clock versioning string rather than semver.
const CURRENT_VERSION: &str = "1";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppPreferencesSerializable {
/// Versioning string.
pub version: String,
/// The downloads directory to use when a user downloads a file.
pub preferred_download_directory: Option<PreferredDownloadDirectory>,
/// Play sounds on events.
pub play_sounds: Option<bool>,
/// Key pointing to file; defined in the frontend code.
pub generation_success_sound: Option<String>,
/// Key pointing to file; defined in the frontend code.
pub generation_failure_sound: Option<String>,
/// Key pointing to file; defined in the frontend code.
pub generation_enqueue_sound: Option<String>,
}
impl AppPreferencesSerializable {
@@ -25,11 +40,15 @@ impl AppPreferencesSerializable {
Ok(Some(data))
}
pub fn save_to_file(&self, app_data_root: &AppDataRoot) -> AnyhowResult<()> {
let filename = app_data_root.settings_dir().get_app_preferences_path();
let json = serde_json::to_string(self)?;
std::fs::write(filename, json)?;
Ok(())
pub fn from_preferences(preferences: &AppPreferences) -> Self {
Self {
version: CURRENT_VERSION.to_string(),
preferred_download_directory: Some(preferences.preferred_download_directory.clone()),
play_sounds: Some(preferences.play_sounds),
generation_success_sound: preferences.generation_success_sound.clone(),
generation_failure_sound: preferences.generation_failure_sound.clone(),
generation_enqueue_sound: preferences.generation_enqueue_sound.clone(),
}
}
pub fn to_preferences(&self) -> AppPreferences {
@@ -43,6 +62,10 @@ impl AppPreferencesSerializable {
preferences.play_sounds = play_sounds;
}
preferences.generation_success_sound = self.generation_success_sound.clone();
preferences.generation_failure_sound = self.generation_failure_sound.clone();
preferences.generation_enqueue_sound = self.generation_enqueue_sound.clone();
preferences
}
}
@@ -6,28 +6,34 @@ use std::str::FromStr;
const SYSTEM_DEFAULT_SENTINEL_VALUE: &str = "system_default";
/// NB: The reason these are not flat is so that they serialize/deserialize nicely to/from JSON.
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum PreferredDownloadDirectory {
/// The system-default downloads directory, which varies by OS.
/// NB: Serializes as `"system_default"` in JSON.
SystemDefault,
System(SystemDownloadDirectory),
/// A user-defined path
/// NB: Serializes as `{"custom": "/path/to/dir"}` in JSON.
Custom(PathBuf),
}
impl FromStr for PreferredDownloadDirectory {
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum SystemDownloadDirectory {
Downloads,
Documents,
}
impl FromStr for SystemDownloadDirectory {
type Err = AnyhowError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == SYSTEM_DEFAULT_SENTINEL_VALUE || s == "\"system_default\"" {
Ok(PreferredDownloadDirectory::SystemDefault)
} else {
// TODO: If it gets serialized as JSON, we may need to remove the `{"custom": ...` wrapping layer.
let path = PathBuf::from(s);
Ok(PreferredDownloadDirectory::Custom(path))
match (s) {
"downloads" => Ok(SystemDownloadDirectory::Downloads),
"documents" => Ok(SystemDownloadDirectory::Documents),
_ => Err(AnyhowError::msg(format!("Invalid system download directory: {}", s))),
}
}
}
@@ -35,17 +41,25 @@ impl FromStr for PreferredDownloadDirectory {
impl Display for PreferredDownloadDirectory {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
PreferredDownloadDirectory::SystemDefault => write!(f, "{}", SYSTEM_DEFAULT_SENTINEL_VALUE),
PreferredDownloadDirectory::System(system) => system.fmt(f),
PreferredDownloadDirectory::Custom(path) => write!(f, "{}", path.to_string_lossy()),
}
}
}
impl Display for SystemDownloadDirectory {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
SystemDownloadDirectory::Downloads => write!(f, "downloads"),
SystemDownloadDirectory::Documents => write!(f, "documents"),
}
}
}
#[cfg(test)]
mod tests {
use crate::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
use std::path::PathBuf;
use crate::state::app_preferences::preferred_download_directory::SystemDownloadDirectory;
use std::str::FromStr;
mod json {
@@ -53,9 +67,9 @@ mod tests {
#[test]
fn to_json_system_default() {
let val = PreferredDownloadDirectory::SystemDefault;
let val = PreferredDownloadDirectory::System(SystemDownloadDirectory::Documents);
let val = serde_json::to_string(&val).unwrap();
assert_eq!(&val, "\"system_default\"");
assert_eq!(&val, "{\"system\":\"documents\"}");
}
#[test]
@@ -65,36 +79,4 @@ mod tests {
assert_eq!(&val, "{\"custom\":\"/tmp\"}");
}
}
mod string {
use super::*;
#[test]
fn to_string_system_default() {
let val = PreferredDownloadDirectory::SystemDefault;
let val = val.to_string();
assert_eq!(&val, "system_default");
}
#[test]
fn to_string_custom() {
let val = PreferredDownloadDirectory::Custom("/tmp/foo".into());
let val = val.to_string();
assert_eq!(&val, "/tmp/foo");
}
}
#[test]
fn from_string_system_default() {
let val = "system_default";
let val = PreferredDownloadDirectory::from_str(val).unwrap();
assert_eq!(val, PreferredDownloadDirectory::SystemDefault);
}
#[test]
fn from_string_custom() {
let val = "/tmp/foo";
let val = PreferredDownloadDirectory::from_str(val).unwrap();
assert_eq!(val, PreferredDownloadDirectory::Custom(PathBuf::from("/tmp/foo")));
}
}
@@ -1,6 +1,7 @@
use crate::state::app_preferences::app_preferences::AppPreferences;
use crate::state::data_dir::trait_data_subdir::DataSubdir;
use errors::AnyhowResult;
use serde::Serialize;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::{Path, PathBuf};
@@ -33,8 +34,12 @@ impl AppSettingsDir {
pub fn write_app_preferences(&self, prefs: &AppPreferences) -> AnyhowResult<()> {
let serializable = prefs.to_serializable();
let serializable = serde_json::to_string(&serializable)?;
let formatter = serde_json::ser::PrettyFormatter::with_indent(b" ");
let mut buf = Vec::new();
let mut ser = serde_json::Serializer::with_formatter(&mut buf, formatter);
serializable.serialize(&mut ser)?;
let filename = self.get_app_preferences_path();
let mut file = OpenOptions::new()
@@ -43,7 +48,7 @@ impl AppSettingsDir {
.truncate(true)
.open(filename)?;
file.write_all(serializable.as_bytes())?;
file.write_all(&buf)?;
file.flush()?;
Ok(())
@@ -7,10 +7,10 @@ import { SOCIAL_LINKS } from "../../config/links";
const Landing = () => {
const videos = [
{
src: "https://www.youtube.com/embed/pGn-1BKo3nY?si=q4dm0ICb6wF1JW8N",
src: "https://www.youtube.com/embed/H4NFXGMuwpY?si=U2_m5Ic1YBn6176D",
},
{
src: "https://www.youtube.com/embed/_FkKf7sECk4?si=Jx38ZGSoyYj0hObr",
src: "https://www.youtube.com/embed/pGn-1BKo3nY?si=q4dm0ICb6wF1JW8N",
},
{
src: "https://www.youtube.com/embed/7x7IZkHiGD8?si=tL8nK4CULigpfHQR",
@@ -258,6 +258,7 @@ export const PromptBox = () => {
maybe_number_of_samples: 1,
},
});
console.log("Generate response:", generateResponse);
toast.success("Please wait while we process your image.");
};
@@ -358,7 +359,6 @@ export const PromptBox = () => {
}
};
return (
<>
<Modal
-3
View File
@@ -25,9 +25,6 @@
{
"path": "../../libs/tauri-utils"
},
{
"path": "../../libs/soundboard"
},
{
"path": "./tsconfig.app.json"
},
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

@@ -1,12 +1,7 @@
import { useEffect, useState } from "react";
import {
TransitionDialogue,
LoadingDots,
Label,
CloseButton,
} from "~/components";
import { LoadingDots } from "~/components";
import { Modal } from "@storyteller/ui-modal";
import { Label } from "@storyteller/ui-label";
import { UploadAssetError } from "../UploadModal/UploadAssetError";
import { UploadSuccess } from "../UploadModal/UploadSuccess";
import { UploadFiles3D } from "./UploadFiles3D";
@@ -19,18 +14,21 @@ import {
CHARACTER_MIXAMO_FILE_TYPE,
IMAGEPLANE_FILE_TYPE,
} from "~/enums";
import { assetModalVisible } from "~/pages/PageEnigma/signals";
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
interface Props {
onClose: () => void;
onSuccess: () => void;
onSuccess: (category: FilterEngineCategories) => void;
isOpen: boolean;
title: string;
titleIcon: IconDefinition;
options?: {
fileSubtypes?: { [key: string]: string }[];
hasLength?: boolean;
hasThumbnailUpload?: boolean;
};
preselectedCategory?: FilterEngineCategories;
isSelectVisible: boolean;
}
// Map categories to their allowed file types
@@ -43,16 +41,6 @@ const categoryFileTypes: Partial<Record<FilterEngineCategories, string[]>> = {
[FilterEngineCategories.LOCATION]: Object.values(OBJECT_FILE_TYPE),
[FilterEngineCategories.CREATURE]: Object.values(OBJECT_FILE_TYPE),
};
// Map engine categories to asset modal tabs
const categoryToAssetTab: Partial<Record<FilterEngineCategories, string>> = {
[FilterEngineCategories.OBJECT]: "objects",
[FilterEngineCategories.CHARACTER]: "character",
[FilterEngineCategories.IMAGE_PLANE]: "image-planes",
[FilterEngineCategories.LOCATION]: "sets",
[FilterEngineCategories.CREATURE]: "creatures",
};
// Excluded category options for the upload modal
const categoryOptions = Object.entries(FilterEngineCategories)
.filter(
@@ -82,12 +70,19 @@ export function UploadModal3D({
onClose,
onSuccess,
title,
titleIcon,
options,
}: Props) {
preselectedCategory,
isSelectVisible,
}: Props & { preselectedCategory?: FilterEngineCategories }) {
const [uploaderState, setUploaderState] =
useState<UploaderState>(initialUploaderState);
const [selectedCategory, setSelectedCategory] =
useState<FilterEngineCategories>(FilterEngineCategories.OBJECT);
useState<FilterEngineCategories>(
preselectedCategory !== undefined
? preselectedCategory
: FilterEngineCategories.OBJECT,
);
const updateUploaderState = (newState: UploaderState) => {
setUploaderState(newState);
@@ -103,30 +98,51 @@ export function UploadModal3D({
}
}, [isOpen]);
useEffect(() => {
if (preselectedCategory !== undefined) {
setSelectedCategory(preselectedCategory);
}
}, [preselectedCategory]);
useEffect(() => {
console.log("Preselected category:", preselectedCategory);
console.log("Initial selected category:", selectedCategory);
}, [preselectedCategory, selectedCategory]);
useEffect(() => {
if (uploaderState.status === UploaderStates.success) {
onSuccess(selectedCategory);
onClose();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [uploaderState.status]);
const UploaderModalContent = () => {
switch (uploaderState.status) {
case UploaderStates.ready:
return (
<div className="space-y-4">
<div className="flex w-full flex-col">
<Label>Category</Label>
<Select
id="category-select"
options={categoryOptions}
value={selectedCategory}
onChange={(value) => {
if (
typeof value === "string" &&
Object.values(FilterEngineCategories).includes(
value as FilterEngineCategories,
)
) {
setSelectedCategory(value as FilterEngineCategories);
}
}}
placeholder="Select a category"
/>
</div>
{isSelectVisible && (
<div className="flex w-full flex-col">
<Label className="mb-1">Category</Label>
<Select
id="category-select"
options={categoryOptions}
value={selectedCategory}
onChange={(value) => {
if (
typeof value === "string" &&
Object.values(FilterEngineCategories).includes(
value as FilterEngineCategories,
)
) {
setSelectedCategory(value as FilterEngineCategories);
}
}}
placeholder="Select a category"
/>
</div>
)}
<UploadFiles3D
title={title}
engineCategory={selectedCategory}
@@ -146,31 +162,17 @@ export function UploadModal3D({
<div className="w-100 text-center opacity-50">Uploading...</div>
</>
);
case UploaderStates.success:
case UploaderStates.success: {
return (
<UploadSuccess
title="3D model"
onOk={() => {
onSuccess();
onClose();
// Store the selected category and tab in sessionStorage
const assetTab = categoryToAssetTab[selectedCategory];
if (assetTab) {
sessionStorage.setItem(
"lastUploadedCategory",
selectedCategory,
);
sessionStorage.setItem("lastUploadedTab", assetTab);
// Reopen the asset modal
assetModalVisible.value = true;
} else {
// Clear any existing stored values if we don't have a valid tab
sessionStorage.removeItem("lastUploadedCategory");
sessionStorage.removeItem("lastUploadedTab");
}
onSuccess(selectedCategory);
}}
/>
);
}
case UploaderStates.assetError:
return (
<UploadAssetError
@@ -199,15 +201,15 @@ export function UploadModal3D({
};
return (
<TransitionDialogue
<Modal
isOpen={isOpen}
onClose={onClose}
titleIcon={titleIcon}
title={title}
className="max-w-xl"
showClose={false}
showClose={true}
>
<CloseButton className="absolute right-4 top-4" onClick={onClose} />
<UploaderModalContent />
</TransitionDialogue>
</Modal>
);
}
@@ -1,7 +1,6 @@
import { useState } from "react";
import {
faChevronLeft,
faChevronRight,
faGear,
faImages,
} from "@fortawesome/pro-solid-svg-icons";
@@ -15,6 +14,9 @@ import { GalleryModal } from "@storyteller/ui-gallery-modal";
import { SettingsModal } from "@storyteller/ui-settings-modal";
import { Tooltip } from "@storyteller/ui-tooltip";
import { downloadFileFromUrl } from "@storyteller/api";
import { TabSelector, TabItem } from "@storyteller/ui-tab-selector";
import { Signal } from "@preact/signals-react";
import { useSignals } from "@preact/signals-react/runtime";
function isEditorPath(path: string) {
if (path === "/") return true;
if (path === "/idealenigma/") return true;
@@ -22,9 +24,18 @@ function isEditorPath(path: string) {
}
interface Props {
pageName: string;
appTabIdSignal: Signal<string>;
setAppTabId: (id: string) => void;
}
export const TopBar = ({ pageName }: Props) => {
const appTabs: TabItem[] = [
{ id: "3D", label: "3D" },
{ id: "2D", label: "2D" },
];
export const TopBar = ({ pageName, appTabIdSignal, setAppTabId }: Props) => {
useSignals();
const currentLocation = getCurrentLocationWithoutParams(
useLocation().pathname,
useParams(),
@@ -33,10 +44,6 @@ export const TopBar = ({ pageName }: Props) => {
const [activeLibraryTab, setActiveLibraryTab] = useState("my-media");
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
const handleClick = () => {
window.location.href = "https://storyteller-2d.netlify.app/";
};
return (
<>
<header className="fixed left-0 top-0 z-[60] w-full border-b border-white/5 bg-ui-background">
@@ -58,6 +65,13 @@ export const TopBar = ({ pageName }: Props) => {
Back to Editor
</ButtonLink>
)}
<TabSelector
tabs={appTabs}
activeTab={appTabIdSignal.value}
disabled={false}
onTabChange={(tabId: string) => setAppTabId(tabId)}
className="w-fit"
/>
{/* <Button
variant="secondary"
icon={faChevronRight}
@@ -1,14 +1,15 @@
import { SoundRegistry, SoundEffect } from "@storyteller/soundboard";
// TODO: This is maintained in two places. Here and MiscSettingsPane.
export const InstallSounds = () => {
const r = SoundRegistry.getInstance();
if (r.hasSound("fail")) {
if (r.hasSound("crumble")) {
return;
}
r.setSoundOnce("success", new SoundEffect("resources/sound/smrpg_flower.wav"));
r.setSoundOnce("bingo", new SoundEffect("resources/sound/smrpg_correct.wav"));
r.setSoundOnce("flower", new SoundEffect("resources/sound/smrpg_flower.wav"));
r.setSoundOnce("correct", new SoundEffect("resources/sound/smrpg_correct.wav"));
r.setSoundOnce("next", new SoundEffect("resources/sound/oot_dialogue_next.wav", { defaultVolume: 1.0 }));
r.setSoundOnce("done", new SoundEffect("resources/sound/oot_dialogue_done.wav", { defaultVolume: 1.0 }));
r.setSoundOnce("fail", new SoundEffect("resources/sound/smrpg_drybones_crumble.wav", { defaultVolume: 0.5 }));
r.setSoundOnce("crumble", new SoundEffect("resources/sound/smrpg_drybones_crumble.wav", { defaultVolume: 0.5 }));
(window as any).sounds = r;
}
@@ -1,5 +1,4 @@
import React, { useEffect } from "react";
import { invoke } from "@tauri-apps/api/core";
import React, { useEffect, useState } from "react";
import { useSignals } from "@preact/signals-react/runtime";
import { LoadingDots, TopBar } from "~/components";
import { Controls3D } from "./comps/Controls3D";
@@ -23,9 +22,9 @@ import { Outliner } from "./comps/Outliner";
import { CameraAspectRatio } from "./enums";
import { PromptBox } from "./comps/PromptBox";
import { OnboardingHelper } from "./comps/OnboardingHelper";
import { IsDesktopApp } from "@storyteller/tauri-utils";
import { FocalLengthDisplay } from "./comps/FocalLengthDisplay/FocalLengthDisplay";
import { DemoModal } from "@storyteller/ui-demo-modal";
import { appTabId, setAppTabId } from "~/signals/appTab";
import { LoginModal } from "@storyteller/ui-login-modal";
export const PageEditor = () => {
useSignals();
@@ -75,7 +74,11 @@ export const PageEditor = () => {
return (
<div className="w-screen">
<TopBar pageName="Edit Scene" />
<TopBar
pageName="Edit Scene"
appTabIdSignal={appTabId}
setAppTabId={setAppTabId}
/>
<OnboardingHelper />
<div
className="relative flex w-screen"
@@ -153,7 +156,7 @@ export const PageEditor = () => {
<Timeline />
</div> */}
<DemoModal
{/* <DemoModal
title="Welcome to ArtCraft 3D"
subTitle="Your 3D editor for digital art and design"
description="Set up your scene by adding objects and start bringing your ideas to life!"
@@ -166,7 +169,14 @@ export const PageEditor = () => {
console.error("This is not the Desktop app.");
}
}}
/>
/> */}
{/* <LoginModal
onClose={() => {}}
videoSrc2D="/resources/videos/artcraft-canvas-demo.mp4"
videoSrc3D="/resources/videos/artcraft-3d-demo.mp4"
openAiLogo="/resources/images/openai-logo.png"
/> */}
</div>
);
};
@@ -4,10 +4,8 @@ import { Button, TopBar } from "~/components";
import { PreviewImages } from "~/pages/PageEnigma/comps/PreviewImages";
import { useContext, useState } from "react";
import { EngineContext } from "~/pages/PageEnigma/contexts/EngineContext";
import { LowerPanel } from "~/pages/PageEnigma/comps/LowerPanel";
import { Pages } from "~/pages/PageEnigma/constants/page";
import { currentPage, pageWidth } from "~/signals";
import { Timeline } from "~/pages/PageEnigma/comps/Timeline";
import { ControlsVideo } from "~/pages/PageEnigma/comps/ControlsVideo";
import { StylizeTabPages } from "~/pages/PageEnigma/enums";
import { generateProgress, selectedArtStyle } from "~/pages/PageEnigma/signals";
@@ -20,6 +18,7 @@ import { StyleSelection } from "~/pages/PageEnigma/comps/StyleSelection";
import { StyleMoreOptions } from "~/pages/PageEnigma/comps/StyleMoreOptions/StyleMoreOptions";
import Queue, { QueueNames } from "~/pages/PageEnigma/Queue";
import { toTimelineActions } from "~/pages/PageEnigma/Queue/toTimelineActions";
import { appTabId, setAppTabId } from "~/signals/appTab";
export const PageStyling = () => {
useSignals();
@@ -61,7 +60,11 @@ export const PageStyling = () => {
return (
<div className="h-screen w-screen">
<TopBar pageName="Stylization" />
<TopBar
pageName="Stylization"
appTabIdSignal={appTabId}
setAppTabId={setAppTabId}
/>
<div className="mt-4 flex flex-col items-center gap-6">
<PreviewImages />
</div>
@@ -1,7 +1,6 @@
import { Modal } from "@storyteller/ui-modal";
import { UploadModal3D } from "~/components/reusable/UploadModal3D";
import {
faPlus,
faSearch,
faChevronLeft,
faLayerGroup,
@@ -12,13 +11,13 @@ import {
faMountainCity,
faDog,
faImage,
faUpFromLine,
} from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Tooltip } from "@storyteller/ui-tooltip";
import { Button } from "@storyteller/ui-button";
import { CloseButton } from "@storyteller/ui-close-button";
import { Input } from "@storyteller/ui-input";
import { TabSelector } from "~/components/reusable/TabSelector";
import { TabSelector } from "@storyteller/ui-tab-selector";
import React, {
useState,
useEffect,
@@ -45,6 +44,7 @@ import { isAnyStatusFetching } from "../SidePanelTabs/utilities";
type AssetTab = {
id: string;
label: string;
labelSingle?: string;
icon: typeof faLayerGroup;
engineCategory?: FilterEngineCategories;
items: MediaItem[];
@@ -81,6 +81,21 @@ const AllTabSection = ({
</div>
);
const categoryToTabIdMap: Record<FilterEngineCategories, string> = {
[FilterEngineCategories.CHARACTER]: "characters",
[FilterEngineCategories.CREATURE]: "creatures",
[FilterEngineCategories.IMAGE_PLANE]: "image-planes",
[FilterEngineCategories.LOCATION]: "sets",
[FilterEngineCategories.OBJECT]: "objects",
[FilterEngineCategories.SKYBOX]: "skybox",
[FilterEngineCategories.ANIMATION]: "all",
[FilterEngineCategories.AUDIO]: "all",
[FilterEngineCategories.EXPRESSION]: "all",
[FilterEngineCategories.SCENE]: "all",
[FilterEngineCategories.SET_DRESSING]: "all",
[FilterEngineCategories.VIDEO_PLANE]: "all",
};
export const AssetModal = () => {
useSignals();
const [activeLibraryTab, setActiveLibraryTab] = useState("library");
@@ -88,6 +103,9 @@ export const AssetModal = () => {
const [searchTerm, setSearchTerm] = useState("");
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
const searchInputRef = useRef<HTMLInputElement>(null);
const [selectedCategory, setSelectedCategory] =
useState<FilterEngineCategories | null>(null);
const [isSelectVisible, setIsSelectVisible] = useState(true);
const handleReopenChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.checked;
@@ -101,6 +119,10 @@ export const AssetModal = () => {
assetModalVisible.value = false;
};
const handleOpen = () => {
assetModalVisible.value = true;
};
useEffect(() => {
if (assetModalVisible.value) {
// Check for stored category and tab from upload
@@ -116,7 +138,10 @@ export const AssetModal = () => {
const timer = setTimeout(() => {
searchInputRef.current?.focus();
}, 100);
return () => clearTimeout(timer);
return () => {
clearTimeout(timer);
// Ensure no side effects remain
};
}
return undefined;
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -232,7 +257,8 @@ export const AssetModal = () => {
{ id: "all", label: "All", icon: faLayerGroup, items: [] },
{
id: "character",
label: "Character",
label: "Characters",
labelSingle: "Character",
icon: faUser,
engineCategory: FilterEngineCategories.CHARACTER,
items:
@@ -243,6 +269,7 @@ export const AssetModal = () => {
{
id: "objects",
label: "Objects",
labelSingle: "Object",
icon: faCube,
engineCategory: FilterEngineCategories.OBJECT,
items:
@@ -253,6 +280,7 @@ export const AssetModal = () => {
{
id: "sets",
label: "Sets",
labelSingle: "Set",
icon: faMountainCity,
engineCategory: FilterEngineCategories.LOCATION,
items:
@@ -263,6 +291,7 @@ export const AssetModal = () => {
{
id: "creatures",
label: "Creatures",
labelSingle: "Creature",
icon: faDog,
engineCategory: FilterEngineCategories.CREATURE,
items:
@@ -273,12 +302,14 @@ export const AssetModal = () => {
{
id: "skybox",
label: "Skybox",
labelSingle: "Skybox",
icon: faSun,
items: activeLibraryTab === "library" ? demoSkyboxItems.value : [],
},
{
id: "image-planes",
label: "Image Planes",
labelSingle: "Image Plane",
icon: faImage,
engineCategory: FilterEngineCategories.IMAGE_PLANE,
items:
@@ -397,11 +428,20 @@ export const AssetModal = () => {
};
const handleAddAsset = () => {
assetModalVisible.value = false;
handleClose();
setIsUploadModalOpen(true);
setSelectedCategory(null);
setIsSelectVisible(true);
};
const handleUploadSuccess = () => {
const handleAddSpecificAsset = (category: FilterEngineCategories) => {
handleClose();
setIsUploadModalOpen(true);
setSelectedCategory(category);
setIsSelectVisible(false);
};
const handleUploadSuccess = (category: FilterEngineCategories) => {
if (reopenAfterDragSignal.value) {
// Small delay to allow the modal to close and reopen
setTimeout(() => {
@@ -410,10 +450,32 @@ export const AssetModal = () => {
} else {
handleClose();
}
// Use the provided category to find the AssetTab ID
const lastUploadedTabId = categoryToTabIdMap[category] || "all";
// Close the upload modal
setTimeout(() => {
sessionStorage.setItem("lastUploadedTab", lastUploadedTabId);
console.log("AssetModal: lastUploadedTabId set to:", lastUploadedTabId);
setIsUploadModalOpen(false);
handleOpen();
}, 300);
fetchUserCharacters();
fetchUserObjects();
fetchUserSets();
fetchUserCreatures();
fetchUserImagePlanes();
};
const clearSearch = () => setSearchTerm("");
const getArticle = (word: string | undefined) => {
if (!word) return "a";
return /^[aeiou]/i.test(word) ? "an" : "a";
};
return (
<>
<Modal
@@ -429,17 +491,31 @@ export const AssetModal = () => {
<h2 className="text-[18px] font-semibold opacity-80">
3D Assets
</h2>
<Tooltip content="Upload model" position="top" delay={200}>
{/* <Tooltip content="Upload model" position="top" delay={200}>
<Button
className="h-6 w-6 rounded-full border-none bg-transparent text-white/70 transition-colors hover:bg-transparent hover:text-white/100"
className="flex h-7 w-7 items-center justify-center rounded-md border-none bg-primary/50 text-white/70 transition-colors hover:bg-primary/70 hover:text-white/100"
onClick={handleAddAsset}
>
<FontAwesomeIcon icon={faPlus} className="text-xl" />
<FontAwesomeIcon icon={faPlus} className="text-lg" />
</Button>
</Tooltip>
</Tooltip> */}
</div>
<hr className="my-2 w-full border-white/10" />
<div className="space-y-2">
<div className="flex h-full flex-col space-y-2">
<Button
variant="primary"
className={twMerge(
"w-full justify-start rounded-xl border border-white/[2%] bg-white/[4%] px-3.5 py-2.5 text-left hover:bg-white/15",
"border-primary bg-primary/70 hover:bg-primary",
)}
onClick={handleAddAsset}
>
<FontAwesomeIcon
icon={faUpFromLine}
className="mr-2 text-lg opacity-70"
/>
Upload an asset
</Button>
{assetTabs.map((tab) => (
<Button
key={tab.id}
@@ -447,7 +523,7 @@ export const AssetModal = () => {
className={twMerge(
"w-full justify-start rounded-xl border border-white/[2%] bg-white/[4%] px-3.5 py-2.5 text-left hover:bg-white/15",
activeAssetTab === tab.id &&
"border-brand-primary bg-brand-primary/10 hover:bg-brand-primary/10",
"border-primary bg-primary/10 hover:bg-primary/10",
)}
onClick={() => setActiveAssetTab(tab.id)}
>
@@ -479,7 +555,7 @@ export const AssetModal = () => {
id="reopen-after-add"
checked={reopenAfterDragSignal.value}
onChange={handleReopenChange}
className="h-4 w-4 cursor-pointer rounded-lg border-gray-300 bg-gray-700 text-brand-primary focus:ring-brand-primary"
className="h-4 w-4 cursor-pointer rounded-lg border-gray-300 bg-gray-700 text-primary focus:ring-primary"
/>
<label
htmlFor="reopen-after-add"
@@ -529,18 +605,36 @@ export const AssetModal = () => {
)}
>
{activeAssetTab !== "all" && !searchTerm && (
<div className="mb-2 flex items-center font-semibold">
<Button
variant="secondary"
className="flex items-center gap-2 border-none bg-transparent px-3 py-1.5 text-sm text-white/70 hover:bg-transparent hover:text-white/100"
onClick={() => setActiveAssetTab("all")}
>
<FontAwesomeIcon
icon={faChevronLeft}
className="text-sm font-semibold opacity-70"
/>
</Button>
{currentTab.label}
<div className="mb-2 flex items-center justify-between font-semibold">
<div className="flex items-center">
<Button
variant="secondary"
className="flex items-center gap-2 border-none bg-transparent px-3 py-1.5 text-sm text-white/70 hover:bg-transparent hover:text-white/100"
onClick={() => setActiveAssetTab("all")}
>
<FontAwesomeIcon
icon={faChevronLeft}
className="text-sm font-semibold opacity-70"
/>
</Button>
{currentTab.label}
</div>
{activeAssetTab !== "skybox" &&
activeAssetTab !== "all" && (
<Button
icon={faUpFromLine}
onClick={() =>
handleAddSpecificAsset(
currentTab.engineCategory ||
FilterEngineCategories.OBJECT,
)
}
className="border-primary bg-primary/70 px-2 py-1 text-xs transition-colors hover:bg-primary"
>
Upload {getArticle(currentTab.labelSingle)}{" "}
{currentTab.labelSingle}
</Button>
)}
</div>
)}
{renderContent()}
@@ -551,10 +645,15 @@ export const AssetModal = () => {
</div>
</Modal>
<UploadModal3D
onClose={() => setIsUploadModalOpen(false)}
onSuccess={handleUploadSuccess}
onClose={() => {
setIsUploadModalOpen(false);
}}
onSuccess={(category) => handleUploadSuccess(category)}
isOpen={isUploadModalOpen}
title="Upload 3D Asset"
title={`Upload ${selectedCategory ? (selectedCategory.toLowerCase().replace(/_/g, " ") === "location" ? "sets" : selectedCategory.toLowerCase().replace(/_/g, " ")) : "3D Asset"}`}
titleIcon={!isSelectVisible ? currentTab.icon : faUpFromLine}
preselectedCategory={selectedCategory || FilterEngineCategories.OBJECT}
isSelectVisible={isSelectVisible}
/>
</>
);
@@ -7,15 +7,39 @@ import {
} from "@fortawesome/pro-solid-svg-icons";
import { Button, ButtonIconSelect, Tooltip } from "~/components";
import { EngineContext } from "../../contexts/EngineContext";
import { useContext } from "react";
import { useContext, useEffect, useState } from "react";
import { assetModalVisibleDuringDrag, assetModalVisible } from "../../signals";
import { AssetModal } from "../AssetMenu/AssetModal";
import { selectedMode } from "../../signals/selectedMode";
import { useSignals } from "@preact/signals-react/runtime";
import { outlinerState } from "../../signals/outliner/outliner";
import { twMerge } from "tailwind-merge";
export const Controls3D = () => {
useSignals();
const editorEngine = useContext(EngineContext);
const [showEmptySceneTooltip, setShowEmptySceneTooltip] = useState(false);
useEffect(() => {
// Check if scene is empty and onboarding helper is not visible
const checkSceneEmpty = () => {
const isSceneEmpty =
outlinerState.items.value.length === 0 && !assetModalVisible.value;
setShowEmptySceneTooltip(isSceneEmpty);
};
// Initial check
checkSceneEmpty();
// Subscribe to outliner state changes
const unsubscribe = outlinerState.items.subscribe(checkSceneEmpty);
return () => {
unsubscribe();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [assetModalVisible.value]);
const handleModeChange = (value: string) => {
selectedMode.value = value;
@@ -80,23 +104,40 @@ export const Controls3D = () => {
};
return (
<div>
<>
<div className="flex justify-center">
<div className="glass rounded-b-xl p-1.5 pr-2 text-white shadow-md">
<div className="flex items-center justify-center gap-2.5">
<div className="flex items-center gap-1">
<Tooltip
content="Add 3D asset to scene (B)"
position="bottom"
delay={100}
closeOnClick
>
<Button
icon={faPlus}
className="h-9 w-9 rounded-[10px] text-lg"
onClick={handleOpenModal}
/>
</Tooltip>
<div className="relative">
{showEmptySceneTooltip && (
<div className="absolute -bottom-14 left-1/2 -translate-x-1/2 transform whitespace-nowrap">
<div className="animate-bounce rounded-lg bg-brand-primary px-4 py-2 text-sm font-medium text-white shadow-lg">
Click + to add your first 3D asset!
<div className="absolute -top-1.5 left-1/2 h-3 w-3 -translate-x-1/2 rotate-45 transform bg-brand-primary" />
</div>
</div>
)}
<Tooltip
content="Add 3D asset to scene (B)"
position="bottom"
delay={300}
closeOnClick
className={twMerge(
showEmptySceneTooltip ? "hidden" : "block",
)}
>
<Button
icon={faPlus}
className={`h-9 w-9 rounded-[10px] text-lg ${
showEmptySceneTooltip
? "border-white/80 bg-brand-primary/90"
: ""
}`}
onClick={handleOpenModal}
/>
</Tooltip>
</div>
<Tooltip
content="Create 3D model from image"
position="bottom"
@@ -124,6 +165,6 @@ export const Controls3D = () => {
</div>
<AssetModal />
</div>
</>
);
};
@@ -1,40 +0,0 @@
import React from "react";
import { useMouseEventsTimeline } from "~/pages/PageEnigma/comps/Timeline/utils/useMouseEventsTimeline";
import { dndTimelineHeight, timelineHeight } from "~/pages/PageEnigma/signals";
import useTimelineClick from "~/pages/PageEnigma/comps/Timeline/utils/useTimelineClick";
import { Pages } from "~/pages/PageEnigma/constants/page";
import { currentPage } from "~/signals";
interface LowerPanelPropsI {
children: React.ReactNode;
}
export const LowerPanel = ({ children }: LowerPanelPropsI) => {
const { onPointerDown } = useMouseEventsTimeline();
const displayHeight =
dndTimelineHeight.value > -1
? dndTimelineHeight.value
: timelineHeight.value;
const onTimelineClick = useTimelineClick(Pages.EDIT);
return (
<>
{currentPage.value === Pages.EDIT && (
<div
className="absolute z-10 h-[3px] w-full cursor-ns-resize bg-ui-panel-border"
style={{ bottom: displayHeight }}
onPointerDown={onPointerDown}
/>
)}
<div
className={["absolute bottom-0", "w-screen", "bg-ui-panel"].join(" ")}
style={{ height: displayHeight }}
onClick={onTimelineClick}
>
{children}
</div>
</>
);
};
@@ -1 +0,0 @@
export { LowerPanel } from './LowerPanel';
@@ -534,7 +534,7 @@ export const PromptBox = () => {
},
});
//console.log("response", generateResponse);
console.log("response", generateResponse);
//if (generateResponse.errorMessage) {
// handleError(response.errorMessage);
@@ -625,8 +625,6 @@ export const PromptBox = () => {
src={image.url}
alt="Reference"
className="h-full w-full rounded-lg object-cover"
// crossOrigin="anonymous"
// referrerPolicy="no-referrer"
/>
<button
onClick={() => handleRemoveReference(image.id)}
@@ -3,7 +3,7 @@ import { useSignals } from "@preact/signals-react/runtime";
import DndAsset from "~/pages/PageEnigma/DragAndDrop/DndAsset";
import { Badge } from "~/components";
import { AssetType } from "~/enums";
import { useState } from "react";
// import { useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faUpDownLeftRight } from "@fortawesome/pro-solid-svg-icons";
@@ -37,7 +37,7 @@ const patchExpressionObjectType = (mediaType: string) => {
export const ItemElement = ({ item }: Props) => {
useSignals();
const [imageError, setImageError] = useState(false);
// const [imageError, setImageError] = useState(false);
return (
<div className="group relative w-full select-none overflow-hidden transition-all duration-200">
@@ -59,16 +59,13 @@ export const ItemElement = ({ item }: Props) => {
onPointerDown={(event) => DndAsset.onPointerDown(event, item)}
style={{ cursor: "grab", pointerEvents: "auto" }}
>
{item.thumbnail && !imageError && (
<img
crossOrigin="anonymous"
referrerPolicy="no-referrer"
src={item.thumbnail}
alt={item.name}
className="h-full w-full object-cover object-center"
onError={() => setImageError(true)}
/>
)}
<img
crossOrigin="anonymous"
referrerPolicy="no-referrer"
src={item.thumbnail}
alt={item.name}
className="h-full w-full object-cover object-center"
/>
<div className="text-shadow-md absolute inset-0 flex items-center justify-center bg-brand-primary-950/50 text-[13px] font-medium text-white opacity-0 transition-opacity duration-200 group-hover:opacity-100">
<FontAwesomeIcon icon={faUpDownLeftRight} className="mr-1.5" />
@@ -1,92 +0,0 @@
import { ChangeEvent, useLayoutEffect, useState } from "react";
import { Button, Textarea, TransitionDialogue } from "~/components";
import {
addPromptTravel,
updatePromptTravel,
} from "~/pages/PageEnigma/signals/promptTravelGroup";
import { useSignals } from "@preact/signals-react/runtime";
import { Clip } from "~/pages/PageEnigma/models";
interface Props {
clip?: Clip;
openPrompt: boolean;
setOpenPrompt: (open: boolean) => void;
offset: number;
}
export const AddEditPrompt = ({
clip,
openPrompt,
setOpenPrompt,
offset,
}: Props) => {
useSignals();
const [error, setError] = useState("");
const [prompt, setPrompt] = useState("");
useLayoutEffect(() => {
if (clip) {
setPrompt(clip.name);
}
}, [clip]);
const onChangeHandlerPositive = (event: ChangeEvent<HTMLTextAreaElement>) => {
setPrompt(event.target.value);
};
const onSave = () => {
if (!prompt) {
setError("Enter a prompt before saving");
return;
}
setOpenPrompt(false);
if (!clip) {
addPromptTravel({
text: prompt,
length: 60,
offset: offset,
});
return;
}
updatePromptTravel({
id: clip.clip_uuid,
offset: clip.offset,
length: clip.length,
name: prompt,
});
};
return (
<TransitionDialogue
isOpen={openPrompt}
onClose={() => setOpenPrompt(false)}
title="Add Text Prompt"
>
<div>
<Textarea
label="Enter a Prompt"
className="w-full"
rows={3}
name="positive-prompt"
placeholder="Type here to describe your scene"
onChange={onChangeHandlerPositive}
required
value={prompt}
/>
{!!error && (
<div className="mt-2 text-sm text-brand-primary">{error}</div>
)}
<div className="mt-4 flex justify-end">
<div className="flex gap-2">
<Button variant="action" onClick={() => setOpenPrompt(false)}>
Cancel
</Button>
<Button variant="primary" onClick={onSave}>
Save
</Button>
</div>
</div>
</div>
</TransitionDialogue>
);
};
@@ -1,71 +0,0 @@
import { TrackClips } from "~/pages/PageEnigma/comps/Timeline/TrackClips";
import {
audioGroup,
audioMinimized,
fullWidth,
minimizeIconPosition,
updateAudio,
} from "~/pages/PageEnigma/signals";
import { ClipGroup } from "~/enums";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faAngleDown, faAngleUp } from "@fortawesome/pro-solid-svg-icons";
import { useSignals } from "@preact/signals-react/runtime";
export const Audio = () => {
useSignals();
const { clips } = audioGroup.value;
if (audioMinimized.value) {
return (
<div
id="track-global-audio"
className="relative flex h-[30px] items-center justify-end rounded-r-lg bg-global_audio-groupBg pr-4"
style={{ width: fullWidth.value + 16 }}
>
<button
className="absolute"
style={{
left: minimizeIconPosition.value,
}}
onClick={(event) => {
event.stopPropagation();
event.preventDefault();
audioMinimized.value = !audioMinimized.value;
}}
>
<FontAwesomeIcon icon={faAngleDown} />
</button>
</div>
);
}
return (
<div
id="track-global-audio"
className="relative block rounded-r-lg bg-global_audio-groupBg pb-2 pr-4"
style={{ width: fullWidth.value + 16 }}
>
<button
className="absolute"
style={{
left: minimizeIconPosition.value,
}}
onClick={(event) => {
event.stopPropagation();
event.preventDefault();
audioMinimized.value = !audioMinimized.value;
}}
>
<FontAwesomeIcon icon={faAngleUp} />
</button>
<div className="pt-[30px]">
<TrackClips
id={audioGroup.value.id}
clips={clips}
updateClip={updateAudio}
group={ClipGroup.GLOBAL_AUDIO}
/>
</div>
</div>
);
};
@@ -1,72 +0,0 @@
import {
cameraGroup,
cameraMinimized,
fullWidth,
minimizeIconPosition,
updateCamera,
} from "~/pages/PageEnigma/signals";
import { TrackKeyFrames } from "~/pages/PageEnigma/comps/Timeline/TrackKeyFrames";
import { useSignals } from "@preact/signals-react/runtime";
import { ClipGroup } from "~/enums";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faAngleDown, faAngleUp } from "@fortawesome/pro-solid-svg-icons";
export const Camera = () => {
useSignals();
const { keyframes } = cameraGroup.value!;
if (cameraMinimized.value) {
return (
<div
id="track-camera"
className="relative flex h-[30px] items-center justify-end rounded-r-lg bg-camera-groupBg pr-4"
style={{ width: fullWidth.value + 16 }}
>
<button
className="absolute"
style={{
left: minimizeIconPosition.value,
}}
onClick={(event) => {
event.stopPropagation();
event.preventDefault();
cameraMinimized.value = !cameraMinimized.value;
}}
>
<FontAwesomeIcon icon={faAngleDown} />
</button>
</div>
);
}
return (
<div
id="track-camera"
className="relative block rounded-r-lg bg-camera-groupBg pb-2 pr-4"
>
<div className="flex justify-end">
<button
className="absolute"
style={{
right: minimizeIconPosition.value,
}}
onClick={(event) => {
event.stopPropagation();
event.preventDefault();
cameraMinimized.value = !cameraMinimized.value;
}}
>
<FontAwesomeIcon icon={faAngleUp} />
</button>
</div>
<div className="pt-[30px]">
<TrackKeyFrames
id={cameraGroup.value.id}
keyframes={keyframes}
group={ClipGroup.CAMERA}
updateKeyframe={updateCamera}
/>
</div>
</div>
);
};
@@ -1,150 +0,0 @@
import { faAngleDown, faAngleUp } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useContext, useMemo } from "react";
import { TrackClips } from "~/pages/PageEnigma/comps/Timeline/TrackClips";
import { TrackKeyFrames } from "~/pages/PageEnigma/comps/Timeline/TrackKeyFrames";
import { CharacterTrack } from "~/pages/PageEnigma/models";
import {
filmLength,
frameTrackButtonWidthPx,
fullWidth,
minimizeIconPosition,
scale,
toggleCharacterMinimized,
updateCharacters
} from "~/pages/PageEnigma/signals";
import { ClipGroup, ClipType } from "~/enums";
import { EngineContext } from "~/pages/PageEnigma/contexts/EngineContext";
function buildUpdaters(
updateCharacters: (options: {
type: ClipType;
id: string;
length: number;
offset: number;
}) => void,
) {
function updateClipAnimations(options: {
id: string;
length: number;
offset: number;
}) {
updateCharacters({ ...options, type: ClipType.ANIMATION });
}
function updateClipPosition(options: { id: string; offset: number }) {
updateCharacters({
...options,
length: 0,
type: ClipType.TRANSFORM,
});
}
function updateClipEmotions(options: {
id: string;
length: number;
offset: number;
}) {
updateCharacters({ ...options, type: ClipType.EXPRESSION });
}
function updateClipLipSync(options: {
id: string;
length: number;
offset: number;
}) {
updateCharacters({ ...options, type: ClipType.AUDIO });
}
return {
updateClipLipSync,
updateClipPosition,
updateClipAnimations,
updateClipEmotions,
};
}
interface Props {
character: CharacterTrack;
}
export const Character = ({ character }: Props) => {
const editorEngine = useContext(EngineContext);
const {
updateClipLipSync,
updateClipPosition,
updateClipAnimations,
updateClipEmotions,
} = useMemo(() => buildUpdaters(updateCharacters), []);
const {
animationClips,
positionKeyframes,
lipSyncClips,
expressionClips,
minimized,
} = character;
if (minimized) {
return (
<div
id={`track-character-${character.object_uuid}`}
className="relative flex h-[30px] items-center justify-end rounded-r-lg bg-character-groupBg pr-4 box-content"
style={{ width: filmLength.value * 1000 * 4 * scale.value + frameTrackButtonWidthPx * 2 }}
>
<button
className="absolute"
style={{
right: minimizeIconPosition.value,
}}
onClick={(event) => {
event.stopPropagation();
event.preventDefault();
toggleCharacterMinimized(character.object_uuid);
}}
>
<FontAwesomeIcon icon={faAngleDown} />
</button>
</div>
);
}
return (
<div
id={`track-character-${character.object_uuid}`}
className="relative block rounded-r-lg bg-character-groupBg pb-2 pr-4"
>
<div className="flex h-[30px] items-center justify-end">
<button
className="absolute"
style={{
right: minimizeIconPosition.value,
}}
onClick={(event) => {
event.stopPropagation();
event.preventDefault();
toggleCharacterMinimized(character.object_uuid);
}}
>
<FontAwesomeIcon icon={faAngleUp} />
</button>
</div>
<div className="flex flex-col">
<TrackClips
id={character.object_uuid}
clips={animationClips}
updateClip={updateClipAnimations}
group={ClipGroup.CHARACTER}
type={ClipType.ANIMATION}
/>
<TrackKeyFrames
id={character.object_uuid}
keyframes={positionKeyframes}
updateKeyframe={updateClipPosition}
group={ClipGroup.CHARACTER}
/>
</div>
</div>
);
};
@@ -1,158 +0,0 @@
import { faImage, faTrashCan } from "@fortawesome/pro-solid-svg-icons";
import { ButtonIconStack } from "~/components/reusable/ButtonIconStack";
import { frameTrackButtonWidthPx } from "../../signals";
import { useCallback, useContext, useEffect, useRef, useState } from "react";
import { EngineContext } from "../../contexts/EngineContext";
import { CHARACTER_FRAME_FILE_TYPE } from "~/enums";
import { UploadModalMedia } from "~/components/reusable/UploadModalMedia";
import { UploadImageMediaModal } from "~/components/reusable/UploadModalMedia/UploadImageMediaModal";
import { MediaFilesApi } from "~/Classes/ApiManager";
import { MediaFile } from "../../models";
import { ApiResponse } from "~/Classes/ApiManager/ApiManager";
import { get_media_url } from "~/Classes/ApiHelpers";
import { twMerge } from "tailwind-merge";
import { ButtonIcon } from "~/components";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
// TODO(bt,2025-01-28): This is just a temporary flag
const TODO_REMOVE_RUN_POSE_CALCULATION = false;
export enum CharacterFrameTarget {
Start,
End
}
export interface CharacterFrameButtonProps {
target: CharacterFrameTarget;
characterId: string;
className?: string;
}
export const CharacterFrameStrings = {
[CharacterFrameTarget.Start]: "Start Frame",
[CharacterFrameTarget.End]: "End Frame"
}
export default function CharacterFrameButton(
{
target,
characterId,
className,
}: CharacterFrameButtonProps
) {
const clickable = useRef(true);
const editorEngine = useContext(EngineContext);
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
const [mediaFile, setMediaFile] = useState<string | undefined>(undefined);
const handleFrameClick = useCallback(() => {
if (!clickable.current) {
return;
}
// Make button unresponsive to clicks until modal is closed
clickable.current = false;
setIsUploadModalOpen(true);
}, [])
const unlockButton = useCallback(() => {
clickable.current = true;
setIsUploadModalOpen(false);
}, [setIsUploadModalOpen]);
const handleFrameSet = useCallback((token?: string) => {
if (!token) {
console.error("No image media file token to set as first frame");
return;
}
// TODO(brandon,2024-01-27): Please forgive me for this ugly hack. It's just
// temporary to move to integration testing quickly. Setting the media token
// to a global space so we can read from it when calling the inference API.
console.log(`Setting image media file token as first frame token: ${token}`);
(window as any).firstFrameMediaToken = token;
// TODO: Fetch the image and pass the token to the character engine
// Fetch the image and set as button bg
get_media_url(token)
.then(async (url) => {
console.debug("Frame url: ", url)
setMediaFile(url);
// TODO(brandon,2024-01-27): Please forgive me for this ugly hack. It's just
// temporary to move to integration testing quickly. Setting the media token
// to a global space so we can read from it when calling the inference API.
(window as any).firstFrameMediaUrl = url;
// TODO(brandon,2024-01-27): TEMPORARY EXPERIMENT. REMOVE ME.
// TODO(brandon,2024-01-27): TEMPORARY EXPERIMENT. REMOVE ME.
// TODO(brandon,2024-01-27): TEMPORARY EXPERIMENT. REMOVE ME.
// await testGlobalExperiment();
})
.catch((error) => {
console.error("Error fetching media file", error);
})
.finally(() => {
unlockButton();
})
}, [unlockButton]);
const handleDeleteFrame = useCallback(() => {
const character = editorEngine!.timeline.scene.get_object_by_uuid(characterId)!;
editorEngine!.animation_engine.clearStartFrame(
character,
editorEngine!.timeline.current_time,
editorEngine!.timeline.timeline_limit
);
setMediaFile(undefined);
unlockButton();
}, [unlockButton, setMediaFile, characterId, editorEngine]);
if (!mediaFile) {
return (
<>
<div className={className} style={{ minWidth: frameTrackButtonWidthPx, width: frameTrackButtonWidthPx }}>
<ButtonIconStack icon={faImage} additionalStyle="bg-character-frame" text={CharacterFrameStrings[target]} onClick={handleFrameClick} />
</div>
<UploadImageMediaModal
isOpen={isUploadModalOpen}
onClose={unlockButton}
onSuccess={handleFrameSet}
title={"Upload Character Frame Image"}
fileTypes={Object.values(CHARACTER_FRAME_FILE_TYPE)}
/>
</>
)
} else {
const classes = twMerge([
className,
"bg-character-unselected overflow-hidden"
]);
const buttonClasses = twMerge([
"absolute flex box-content top-1 right-1 w-6 h-6 rounded-md invisible group-hover:visible items-center justify-center",
"bg-gray-800 hover:bg-red text-gray-500 hover:text-gray-100 border-2 border-transparent hover:border-white",
"transition-all duration-150",
])
return (
<>
<div className={classes} style={{ minWidth: frameTrackButtonWidthPx, width: frameTrackButtonWidthPx }}>
<div className={"rounded-md w-full h-full relative group border-2 border-gray-400"}>
<img src={mediaFile} alt={CharacterFrameStrings[target]} className={"rounded-md object-cover w-full h-full"} />
<button className={buttonClasses} onClick={handleDeleteFrame}>
<FontAwesomeIcon icon={faTrashCan} size={"xs"} className={""} />
</button>
</div>
</div>
</>
)
}
}
@@ -1,16 +0,0 @@
import { characterGroup } from "~/pages/PageEnigma/signals";
import { Character } from "~/pages/PageEnigma/comps/Timeline/Character";
import { useSignals } from "@preact/signals-react/runtime";
export const Characters = () => {
useSignals();
return (
<>
{characterGroup.value.characters.map((character) => (
<div key={character.object_uuid} className="mb-1 pr-4">
<Character character={character} />
</div>
))}
</>
);
};
@@ -1,76 +0,0 @@
import {
fullWidth,
minimizeIconPosition,
objectGroup,
objectsMinimized,
selectedObject,
} from "~/pages/PageEnigma/signals";
import { useSignals } from "@preact/signals-react/runtime";
import { ObjectTrackComponent } from "~/pages/PageEnigma/comps/Timeline/ObjectTrack";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faAngleDown, faAngleUp } from "@fortawesome/pro-solid-svg-icons";
export const ObjectGroups = () => {
useSignals();
if (!objectGroup.value.objects.length) {
return null;
}
if (objectsMinimized.value) {
return (
<div
id="track-objects"
className="relative mb-4 flex h-[30px] items-center justify-end rounded-r-lg bg-object-groupBg pr-4"
style={{ width: fullWidth.value + 16 }}
>
<button
className="absolute"
style={{
left: minimizeIconPosition.value,
}}
onClick={(event) => {
event.stopPropagation();
event.preventDefault();
objectsMinimized.value = !objectsMinimized.value;
}}
>
<FontAwesomeIcon icon={faAngleDown} />
</button>
</div>
);
}
return (
<div
id="track-objects"
className="relative mb-5 block rounded-r-lg bg-object-groupBg pb-2 pr-4"
style={{ width: fullWidth.value + 16 }}
>
<button
className="absolute"
style={{
left: minimizeIconPosition.value,
}}
onClick={(event) => {
event.stopPropagation();
event.preventDefault();
objectsMinimized.value = !objectsMinimized.value;
}}
>
<FontAwesomeIcon icon={faAngleUp} />
</button>
<div className="pt-[30px]">
{objectGroup.value.objects.map((object) => (
<div
key={object.object_uuid}
className="mb-1"
id={`track-object-${selectedObject.value?.id}`}
>
<ObjectTrackComponent object={object} />
</div>
))}
</div>
</div>
);
};
@@ -1,19 +0,0 @@
import { updateObject } from "~/pages/PageEnigma/signals";
import { TrackKeyFrames } from "~/pages/PageEnigma/comps/Timeline/TrackKeyFrames";
import { ClipGroup } from "~/enums";
import { ObjectTrack } from "~/pages/PageEnigma/models";
interface Props {
object: ObjectTrack;
}
export const ObjectTrackComponent = ({ object }: Props) => {
return (
<TrackKeyFrames
id={object.object_uuid}
keyframes={object.keyframes}
updateKeyframe={updateObject}
group={ClipGroup.OBJECT}
/>
);
};
@@ -1,44 +0,0 @@
import { Link } from "react-router-dom";
import { fullWidth, timelineHeight } from "../../signals";
interface PremiumLockTimelineProps {
locked: boolean;
}
export default function PremiumLockTimeline({
locked,
}: PremiumLockTimelineProps) {
return (
<>
{locked ? (
<div className="sticky left-0 top-1/2 z-20 h-full w-full -translate-y-1/2 transform">
<div
style={{
height: timelineHeight.value,
width: fullWidth.value - 704,
}}
className="absolute top-1/2 ml-[720px] -translate-y-1/2 transform border-l border-[#43435C] bg-ui-panel/70 backdrop-blur-sm"
>
<div className="flex h-full w-full flex-col justify-center pl-12">
<div className="flex w-[400px] flex-col gap-1 rounded-lg border border-white/40 bg-ui-controls p-4 text-center shadow-lg">
<h6 className="text-md font-medium">
Your timeline is currently limited to 3 seconds.
</h6>
<p className="text-sm font-normal">
Please{" "}
<Link
to="/pricing"
className="font-medium text-brand-primary brightness-125"
>
upgrade your account
</Link>{" "}
to increase the movie render length up to 7 seconds.
</p>
</div>
</div>
</div>
</div>
) : null}
</>
);
}
@@ -1,28 +0,0 @@
import { fullWidth } from "~/pages/PageEnigma/signals";
import { useSignals } from "@preact/signals-react/runtime";
import {
promptTravelGroup,
updatePromptTravel,
} from "~/pages/PageEnigma/signals/promptTravelGroup";
import { TrackClipsPrompt } from "~/pages/PageEnigma/comps/Timeline/TrackClipsPrompt";
export const PromptTravel = () => {
useSignals();
const { clips } = promptTravelGroup.value;
return (
<div
id="track-prompt-travel"
className="bg-prompt-groupBg relative block rounded-r-lg pb-2 pr-4"
style={{ width: fullWidth.value + 16 }}
>
<div className="pt-[30px]">
<TrackClipsPrompt
id={promptTravelGroup.value.id}
clips={clips}
updateClip={updatePromptTravel}
/>
</div>
</div>
);
};
@@ -1,18 +0,0 @@
export function CameraHeader() {
return (
<>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="12"
viewBox="0 0 14 12"
fill="none">
<path
d="M3.11111 1.5C2.69855 1.5 2.30289 1.65804 2.01117 1.93934C1.71944 2.22064 1.55556 2.60218 1.55556 3C1.55556 3.39782 1.71944 3.77936 2.01117 4.06066C2.30289 4.34197 2.69855 4.5 3.11111 4.5C3.52367 4.5 3.91933 4.34197 4.21106 4.06066C4.50278 3.77936 4.66667 3.39782 4.66667 3C4.66667 2.60218 4.50278 2.22064 4.21106 1.93934C3.91933 1.65804 3.52367 1.5 3.11111 1.5ZM8.55556 6H3.11111C1.39271 6 0 4.65703 0 3C0 1.34297 1.39271 0 3.11111 0C4.28264 0 5.30347 0.623437 5.83333 1.54688C6.36319 0.623437 7.38403 0 8.55556 0C10.274 0 11.6667 1.34297 11.6667 3C11.6667 4.65703 10.274 6 8.55556 6ZM8.55556 1.5C8.143 1.5 7.74734 1.65804 7.45561 1.93934C7.16389 2.22064 7 2.60218 7 3C7 3.39782 7.16389 3.77936 7.45561 4.06066C7.74734 4.34197 8.143 4.5 8.55556 4.5C8.96811 4.5 9.36378 4.34197 9.6555 4.06066C9.94722 3.77936 10.1111 3.39782 10.1111 3C10.1111 2.60218 9.94722 2.22064 9.6555 1.93934C9.36378 1.65804 8.96811 1.5 8.55556 1.5ZM13.5698 6.07969C13.8323 6.20625 14 6.46641 14 6.75V11.25C14 11.5336 13.8347 11.7938 13.5698 11.9203C13.3049 12.0469 12.9913 12.0211 12.7556 11.85L11.2 10.725L10.8889 10.5V10.125V7.875V7.5L11.2 7.275L12.7556 6.15C12.9913 5.97891 13.3073 5.95312 13.5698 6.07969ZM1.55556 6.75H3.11111H8.55556C9.41354 6.75 10.1111 7.42266 10.1111 8.25V10.5C10.1111 11.3273 9.41354 12 8.55556 12H3.11111C2.25312 12 1.55556 11.3273 1.55556 10.5V8.25H0.777778C0.347569 8.25 0 7.91484 0 7.5C0 7.08516 0.347569 6.75 0.777778 6.75H1.55556Z"
fill="white"
/>
</svg>
Camera
</>
);
}
@@ -1,22 +0,0 @@
interface Props {
name: string;
}
export function CharacterHeader({ name }: Props) {
return (
<>
<svg
xmlns="http://www.w3.org/2000/svg"
width="11"
height="12"
viewBox="0 0 11 12"
fill="none">
<path
d="M5.5 6C6.33354 6 7.13294 5.68393 7.72234 5.12132C8.31174 4.55871 8.64286 3.79565 8.64286 3C8.64286 2.20435 8.31174 1.44129 7.72234 0.87868C7.13294 0.316071 6.33354 0 5.5 0C4.66646 0 3.86706 0.316071 3.27766 0.87868C2.68826 1.44129 2.35714 2.20435 2.35714 3C2.35714 3.79565 2.68826 4.55871 3.27766 5.12132C3.86706 5.68393 4.66646 6 5.5 6ZM4.3779 7.125C1.95938 7.125 0 8.99531 0 11.3039C0 11.6883 0.326563 12 0.729241 12H10.2708C10.6734 12 11 11.6883 11 11.3039C11 8.99531 9.04063 7.125 6.6221 7.125H4.3779Z"
fill="white"
/>
</svg>
{name}
</>
);
}
@@ -1,19 +0,0 @@
export function GlobalAudioHeader() {
return (
<>
<svg
xmlns="http://www.w3.org/2000/svg"
width="15"
height="12"
viewBox="0 0 15 12"
fill="none"
>
<path
d="M8.25 11.25C8.25 11.6648 7.91484 12 7.5 12C7.08516 12 6.75 11.6648 6.75 11.25V0.75C6.75 0.335156 7.08516 0 7.5 0C7.91484 0 8.25 0.335156 8.25 0.75V11.25ZM12.75 9.75C12.75 10.1648 12.4148 10.5 12 10.5C11.5852 10.5 11.25 10.1648 11.25 9.75V2.25C11.25 1.83516 11.5852 1.5 12 1.5C12.4148 1.5 12.75 1.83516 12.75 2.25V9.75ZM6 9C6 9.41484 5.66484 9.75 5.25 9.75C4.83516 9.75 4.5 9.41484 4.5 9V3C4.5 2.58516 4.83516 2.25 5.25 2.25C5.66484 2.25 6 2.58516 6 3V9ZM10.5 8.25C10.5 8.66484 10.1648 9 9.75 9C9.33516 9 9 8.66484 9 8.25V3.75C9 3.33516 9.33516 3 9.75 3C10.1648 3 10.5 3.33516 10.5 3.75V8.25ZM3.75 6.75C3.75 7.16484 3.41484 7.5 3 7.5C2.58516 7.5 2.25 7.16484 2.25 6.75V5.25C2.25 4.83516 2.58516 4.5 3 4.5C3.41484 4.5 3.75 4.83516 3.75 5.25V6.75ZM0 6C0 5.80109 0.0790176 5.61032 0.21967 5.46967C0.360322 5.32902 0.551088 5.25 0.75 5.25C0.948912 5.25 1.13968 5.32902 1.28033 5.46967C1.42098 5.61032 1.5 5.80109 1.5 6C1.5 6.19891 1.42098 6.38968 1.28033 6.53033C1.13968 6.67098 0.948912 6.75 0.75 6.75C0.551088 6.75 0.360322 6.67098 0.21967 6.53033C0.0790176 6.38968 0 6.19891 0 6ZM13.5 6C13.5 5.80109 13.579 5.61032 13.7197 5.46967C13.8603 5.32902 14.0511 5.25 14.25 5.25C14.4489 5.25 14.6397 5.32902 14.7803 5.46967C14.921 5.61032 15 5.80109 15 6C15 6.19891 14.921 6.38968 14.7803 6.53033C14.6397 6.67098 14.4489 6.75 14.25 6.75C14.0511 6.75 13.8603 6.67098 13.7197 6.53033C13.579 6.38968 13.5 6.19891 13.5 6Z"
fill="white"
/>
</svg>
Music
</>
);
}
@@ -1,37 +0,0 @@
import { useContext } from "react";
import { EngineContext } from "~/pages/PageEnigma/contexts/EngineContext";
import { toggleLipSyncMute } from "~/pages/PageEnigma/signals";
import { faVolume, faVolumeSlash } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { CharacterTrack } from "~/pages/PageEnigma/models";
export const LipSyncSubHeader = ({
character,
}: {
character: CharacterTrack;
}) => {
const editorEngine = useContext(EngineContext);
if (editorEngine && editorEngine.isObjectLipsync(character.object_uuid)) {
return (
<div className="mb-1 flex h-[30px] flex-col justify-center pl-[22px] text-xs font-medium text-white opacity-80">
<div className="flex gap-3">
Vocal / Speech
<button
className="text-md text-white transition-colors duration-100 hover:text-white/80"
onClick={() => toggleLipSyncMute(character.object_uuid)}
>
{character.muted ? (
<FontAwesomeIcon
icon={faVolumeSlash}
className="text-brand-primary transition-colors duration-100 hover:text-brand-primary/80"
/>
) : (
<FontAwesomeIcon icon={faVolume} />
)}
</button>
</div>
</div>
);
}
return null;
};
@@ -1,18 +0,0 @@
export function ObjectsHeader() {
return (
<>
<svg
xmlns="http://www.w3.org/2000/svg"
width="13"
height="13"
viewBox="0 0 13 13"
fill="none">
<path
d="M5.9541 0.0959457C6.30703 -0.0319819 6.69297 -0.0319819 7.04844 0.0959457L11.9234 1.85111C12.5684 2.08394 13 2.70055 13 3.39392V9.60608C13 10.2969 12.5684 10.9161 11.9209 11.1489L7.0459 12.9041C6.69297 13.032 6.30703 13.032 5.95156 12.9041L1.07656 11.1489C0.431641 10.9161 0 10.2994 0 9.60608V3.39392C0 2.70311 0.431641 2.08394 1.0791 1.85111L5.9541 0.0959457ZM6.5 1.63875L2.08965 3.22505L6.5 4.81136L10.9104 3.22505L6.5 1.63875ZM1.625 9.60608L5.6875 11.067V6.25694L1.625 4.796V9.60608ZM7.3125 11.067L11.375 9.60608V4.796L7.3125 6.2595V11.0696V11.067Z"
fill="white"
/>
</svg>
Objects
</>
);
}
@@ -1,19 +0,0 @@
export function PromptTravelHeader() {
return (
<>
<svg
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1.714 2.57098V1.71399H5.142V5.14196H4.7135C4.23947 5.14196 3.8565 5.52493 3.8565 5.99896C3.8565 6.47298 4.23947 6.85595 4.7135 6.85595H7.28449C7.75852 6.85595 8.14149 6.47298 8.14149 5.99896C8.14149 5.52493 7.75852 5.14196 7.28449 5.14196H6.85599V1.71399H10.284V2.57098C10.284 3.04501 10.667 3.42798 11.141 3.42798C11.615 3.42798 11.998 3.04501 11.998 2.57098V1.28549C11.998 0.575793 11.4222 0 10.7125 0H5.999H1.2855C0.575796 0 0 0.575793 0 1.28549V2.57098C0 3.04501 0.382972 3.42798 0.856999 3.42798C1.33103 3.42798 1.714 3.04501 1.714 2.57098ZM0.251744 8.82168C-0.0830218 9.15645 -0.0830218 9.7001 0.251744 10.0349L1.96574 11.7489C2.21213 11.9952 2.57903 12.0675 2.90041 11.9336C3.22178 11.7997 3.43068 11.4891 3.43068 11.1409V10.2839H8.56999V11.1409C8.56999 11.4864 8.77889 11.7997 9.10026 11.9336C9.42164 12.0675 9.78854 11.9926 10.0349 11.7489L11.7489 10.0349C12.0837 9.7001 12.0837 9.15645 11.7489 8.82168L10.0349 7.10769C9.78854 6.86131 9.42164 6.789 9.10026 6.9229C8.77889 7.05681 8.56999 7.36747 8.56999 7.71562V8.57262H3.428V7.71295C3.428 7.36747 3.2191 7.05413 2.89773 6.92023C2.57635 6.78632 2.20945 6.86131 1.96306 7.10502L0.249065 8.819L0.251744 8.82168Z"
fill="white"
/>
</svg>
Prompt Travel
</>
);
}
@@ -1,222 +0,0 @@
import {
audioGroup,
audioMinimized,
cameraMinimized,
characterGroup,
objectGroup,
objectsMinimized,
timelineScrollY,
toggleAudioMute,
} from "~/pages/PageEnigma/signals";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faVolume, faVolumeSlash } from "@fortawesome/pro-solid-svg-icons";
import { useSignals } from "@preact/signals-react/runtime";
import { GlobalAudioHeader } from "~/pages/PageEnigma/comps/Timeline/RowHeaders/GlobalAudioHeader";
import { CameraHeader } from "~/pages/PageEnigma/comps/Timeline/RowHeaders/CameraHeader";
import { ObjectsHeader } from "~/pages/PageEnigma/comps/Timeline/RowHeaders/ObjectsHeader";
import { CharacterHeader } from "~/pages/PageEnigma/comps/Timeline/RowHeaders/CharacterHeader";
import { LipSyncSubHeader } from "./LipSyncSubHeader";
import { MediaFileAnimationType } from "~/enums";
import { MediaFileType } from "~/pages/PageEnigma/enums";
import { currentPage } from "~/signals";
import { Pages } from "~/pages/PageEnigma/constants/page";
import { PromptTravelHeader } from "~/pages/PageEnigma/comps/Timeline/RowHeaders/PromptTravelHeader";
export const RowHeaders = () => {
useSignals();
const compressedHeaderClasses =
"flex h-[30px] items-center gap-2 pl-2 text-xs rounded-tl-lg font-medium text-white w-32";
const uncompressedHeaderClasses =
"flex h-[30px] items-center gap-2 rounded-br-lg rounded-tl-lg pl-2 text-xs font-medium text-white w-32";
if (currentPage.value === Pages.STYLE) {
return (
<div className="relative">
<div className="absolute mt-2 w-[146px]" style={{ top: -8 }}>
<div className="mb-1 h-[72px] w-full rounded-l-lg bg-prompt-groupBg">
<div className="h-[30px] text-xs font-medium text-white">
<div
className={[
uncompressedHeaderClasses,
"bg-prompt-titleBg",
].join(" ")}
>
<PromptTravelHeader />
</div>
</div>
<div className="flex h-[30px] flex-col justify-center pl-[22px] text-xs font-medium text-white opacity-80">
Text Prompt
</div>
</div>
</div>
</div>
);
}
return (
<div className="relative">
<div
className="absolute mt-2 w-[146px]"
style={{ top: timelineScrollY.value * -1 - 8 }}
>
{characterGroup.value.characters.map((character) => {
if (character.minimized) {
return (
<div
key={character.object_uuid}
className="mb-1 h-[30px] w-full rounded-l-lg bg-character-groupBg"
>
<div
className={[
compressedHeaderClasses,
"bg-character-titleBg",
].join(" ")}
>
<CharacterHeader name={character.name} />
</div>
</div>
);
}
return (
<div
key={character.object_uuid}
className="mb-1 w-full rounded-l-lg bg-character-groupBg pb-2"
>
<div className="h-[30px] text-xs font-medium text-white">
<div
className={[
uncompressedHeaderClasses,
"bg-character-titleBg",
].join(" ")}
>
<CharacterHeader name={character.name} />
</div>
</div>
<div className="mb-1 flex h-[60px] flex-col justify-center pl-[22px] text-xs font-medium text-white opacity-80">
Animation
</div>
<div className="mb-1 flex h-[30px] flex-col justify-center pl-[22px] text-xs font-medium text-white opacity-80">
Position
</div>
</div>
);
})}
{cameraMinimized.value ? (
<div className="mb-1 h-[30px] w-full rounded-l-lg bg-camera-groupBg">
<div
className={[compressedHeaderClasses, "bg-camera-titleBg"].join(
" ",
)}
>
<CameraHeader />
</div>
</div>
) : (
<div className="mb-1 h-[72px] w-full rounded-l-lg bg-camera-groupBg">
<div className="h-[30px] text-xs font-medium text-white">
<div
className={[
uncompressedHeaderClasses,
"bg-camera-titleBg",
].join(" ")}
>
<CameraHeader />
</div>
</div>
<div className="flex h-[30px] flex-col justify-center pl-[22px] text-xs font-medium text-white opacity-80">
Position
</div>
</div>
)}
{audioMinimized.value ? (
<div className="mb-1 h-[30px] w-full rounded-l-lg bg-global_audio-groupBg">
<div
className={[
compressedHeaderClasses,
"bg-global_audio-titleBg",
].join(" ")}
>
<GlobalAudioHeader />
</div>
</div>
) : (
<div className="mb-1 h-[102px] w-full rounded-l-lg bg-global_audio-groupBg">
<div className="h-[30px] text-xs font-medium text-white">
<div
className={[
uncompressedHeaderClasses,
"bg-global_audio-titleBg",
].join(" ")}
>
<GlobalAudioHeader />
</div>
</div>
<div className="flex h-[60px] flex-col justify-center pl-[22px] text-xs font-medium text-white opacity-80">
<div className="flex gap-3">
Track 1
<button
className="text-md text-white transition-colors duration-100 hover:text-white/80"
onClick={() => toggleAudioMute()}
>
{audioGroup.value.muted ? (
<FontAwesomeIcon
icon={faVolumeSlash}
className="text-brand-primary transition-colors duration-100 hover:text-brand-primary/80"
/>
) : (
<FontAwesomeIcon icon={faVolume} />
)}
</button>
</div>
</div>
</div>
)}
{objectGroup.value.objects.length > 0 && (
<>
{objectsMinimized.value ? (
<div className="h-[30px] w-full rounded-l-lg bg-object-groupBg">
<div
className={[
compressedHeaderClasses,
"bg-object-titleBg",
].join(" ")}
>
<ObjectsHeader />
</div>
</div>
) : (
<div
className="mb-1 h-[72px] w-full rounded-l-lg bg-object-groupBg"
style={{
height: `${38 + objectGroup.value.objects.length * 34}px`,
}}
>
<div className="h-[30px] text-xs font-medium text-white">
<div
className={[
uncompressedHeaderClasses,
"bg-object-titleBg",
].join(" ")}
>
<ObjectsHeader />
</div>
</div>
{objectGroup.value.objects.map((obj) => (
<div
key={obj.object_uuid}
className="mb-1 flex h-[30px] flex-col justify-center pl-[22px] text-xs font-medium text-white opacity-80"
>
{obj.name}
</div>
))}
</div>
)}
</>
)}
</div>
</div>
);
};
@@ -1,94 +0,0 @@
import {
currentTime,
scale,
timelineHeight,
timelineScrollX,
pointerScrubber,
secondaryScrubber,
frameTrackButtonWidthPx,
} from "~/pages/PageEnigma/signals";
import { useMouseEventsScrubber } from "~/pages/PageEnigma/comps/Timeline/utils/useMouseEventsScrubber";
import { useSignals } from "@preact/signals-react/runtime";
import { Pages } from "~/pages/PageEnigma/constants/page";
import { useContext, useEffect } from "react";
import { currentPage } from "~/signals";
import { EngineContext } from "~/pages/PageEnigma/contexts/EngineContext";
export const Scrubber = () => {
useSignals();
const editorEngine = useContext(EngineContext);
const { onPointerDown, time } = useMouseEventsScrubber();
const isPlaying = editorEngine?.timeline?.is_playing ?? false;
const displayTime = time === -1 ? currentTime.value : time;
useEffect(() => {
secondaryScrubber.value = displayTime;
if (currentPage.value === Pages.STYLE && isPlaying) {
return;
}
pointerScrubber.value = displayTime;
}, [displayTime, isPlaying]);
const scrollX = timelineScrollX.value;
return (
<>
{pointerScrubber.value * 4 * scale.value - scrollX + frameTrackButtonWidthPx >= 0 && (
<div
className="absolute z-[20] flex cursor-ew-resize flex-col items-center"
style={{
top: 8,
left: pointerScrubber.value * 4 * scale.value + 199 - scrollX + frameTrackButtonWidthPx,
}}
onPointerDown={onPointerDown}
>
<div>
<svg
width="14"
height="21"
viewBox="0 0 14 21"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7 19.5858L1.58578 14.1715C1.21071 13.7965 0.999999 13.2878 0.999999 12.7573L1 2C1 1.44772 1.44771 1 2 1L12 1C12.5523 1 13 1.44772 13 2L13 12.7573C13 13.2878 12.7893 13.7965 12.4142 14.1715L7 19.5858Z"
fill="white"
stroke="white"
strokeWidth="2"
/>
</svg>
</div>
<div
className="block bg-white"
style={{
width: 2,
marginTop: -5,
height: timelineHeight.value - 36,
}}
/>
</div>
)}
{secondaryScrubber.value * 4 * scale.value - scrollX >= 0 &&
currentPage.value === Pages.STYLE && (
<div
className="absolute z-[20] flex cursor-ew-resize flex-col items-center"
style={{
top: 28,
left: secondaryScrubber.value * 4 * scale.value + 204 - scrollX,
}}
onPointerDown={onPointerDown}
>
<div
className="block bg-white/80"
style={{
width: 1,
marginTop: -5,
height: timelineHeight.value - 36,
}}
/>
</div>
)}
</>
);
};
@@ -1,171 +0,0 @@
import { UIEvent, useCallback, useEffect, useRef } from "react";
import { LowerPanel } from "~/pages/PageEnigma/comps/LowerPanel";
import { Camera } from "./Camera";
import { Audio } from "./Audio";
import {
characterGroup,
filmLength,
frameTrackButtonWidthPx,
ignoreKeyDelete,
isHotkeyDisabled,
objectsMinimized,
scale,
selectedItem,
selectedObject,
sidePanelVisible,
sidePanelWidth,
timelineHeight,
timelineScrollX,
timelineScrollY,
} from "~/pages/PageEnigma/signals";
import { useSignalEffect, useSignals } from "@preact/signals-react/runtime";
import { TimerGrid } from "~/pages/PageEnigma/comps/TimerGrid/TimerGrid";
import { Scrubber } from "~/pages/PageEnigma/comps/Timeline/Scrubber";
import { Characters } from "~/pages/PageEnigma/comps/Timeline/Characters";
import { ObjectGroups } from "~/pages/PageEnigma/comps/Timeline/ObjectGroups";
import { RowHeaders } from "~/pages/PageEnigma/comps/Timeline/RowHeaders/RowHeaders";
import { currentPage, pageWidth } from "~/signals";
import { Pages } from "~/pages/PageEnigma/constants/page";
import PremiumLockTimeline from "./PremiumLockTimeline";
import { AssetType } from "~/enums";
import { PromptTravel } from "~/pages/PageEnigma/comps/Timeline/PromptTravel";
import { useOnDelete } from "~/pages/PageEnigma/comps/Timeline/utils/useOnDelete";
function scrollItem(itemId: string) {
const element = document.getElementById(itemId);
if (!element) {
return;
}
element.scrollIntoView({ behavior: "smooth", block: "nearest" });
}
export const Timeline = () => {
useSignals();
const lastSelectedObject = useRef(selectedObject.value);
if (selectedObject.value !== lastSelectedObject.current) {
lastSelectedObject.current = selectedObject.value;
switch (selectedObject.value?.type) {
case AssetType.CHARACTER:
if (
characterGroup.value.characters.some(
(character) => character.object_uuid === selectedObject.value?.id,
)
) {
scrollItem(`track-character-${selectedObject.value?.id}`);
break;
}
if (objectsMinimized.value) {
scrollItem("track-objects");
break;
}
scrollItem(`track-object-${selectedObject.value?.id}`);
break;
case AssetType.CAMERA:
scrollItem("track-camera");
break;
case AssetType.OBJECT:
if (objectsMinimized.value) {
scrollItem("track-objects");
break;
}
scrollItem(`track-object-${selectedObject.value?.id}`);
break;
}
}
const onScroll = useCallback((event: UIEvent<HTMLDivElement>) => {
timelineScrollX.value = event.currentTarget.scrollLeft;
timelineScrollY.value = event.currentTarget.scrollTop;
}, []);
useSignalEffect(() => {
timelineHeight.value = currentPage.value === Pages.EDIT ? 208 : 120;
});
const { onDeleteAsk, confirmationModal } = useOnDelete();
const onDeleteKey = useCallback(
(event: KeyboardEvent) => {
if (ignoreKeyDelete.value || isHotkeyDisabled()) {
return;
}
if (
["Backspace", "Delete"].indexOf(event.key) > -1 &&
selectedItem.value !== null
) {
onDeleteAsk(event, selectedItem.value);
}
},
[onDeleteAsk],
);
useEffect(() => {
document.addEventListener("keydown", onDeleteKey);
return () => {
document.removeEventListener("keydown", onDeleteKey);
};
}, [onDeleteKey]);
const sidebarAdjustment = sidePanelVisible.value
? sidePanelWidth.value + 84
: 84;
return (
<>
<LowerPanel>
<div
style={{
marginRight:
currentPage.value === Pages.EDIT ? sidebarAdjustment : 16,
}}
>
<TimerGrid />
<div className="flex">
<div
className="ml-[60px] mt-2 w-[144px] min-w-[144px] overflow-hidden"
style={{
height: timelineHeight.value - 36,
}}
>
<RowHeaders />
</div>
<div
className="relative mt-2 overflow-auto"
onScroll={onScroll}
style={{
width: pageWidth.value - 204,
height: timelineHeight.value - 36,
}}
>
<div className="w-fit">
{currentPage.value === Pages.EDIT ? (
<>
<PremiumLockTimeline locked={false} />
<Characters />
<div className="pb-1 pr-4">
<Camera />
</div>
<div className="pb-1 pr-8">
<Audio />
</div>
<ObjectGroups />
</>
) : (
<>
<PremiumLockTimeline locked={false} />
<PromptTravel />
</>
)}
</div>
</div>
<Scrubber />
</div>
</div>
</LowerPanel>
{confirmationModal()}
</>
);
};
@@ -1,150 +0,0 @@
import { useState } from "react";
import { ClipGroup } from "~/enums";
import DndAsset from "~/pages/PageEnigma/DragAndDrop/DndAsset";
import { Clip } from "~/pages/PageEnigma/models";
import {
canDrop,
dragHandleWidth,
dragItem,
frameTrackButtonWidthPx,
scale,
selectedItem,
unitPx,
} from "~/pages/PageEnigma/signals";
import { useMouseEventsClip } from "./utils/useMouseEventsClip";
interface Props {
min: number;
max: number;
group: ClipGroup;
clip: Clip;
updateClip: (options: { id: string; offset: number; length: number }) => void;
}
export const TrackClip = ({ clip, min, max, group, updateClip }: Props) => {
const [state, setState] = useState({
length: clip.length,
offset: clip.offset,
});
const { onPointerDown } = useMouseEventsClip(
clip,
max,
min,
updateClip,
setState,
);
const selectedClipId = (selectedItem.value as Clip | null)?.clip_uuid ?? "";
const { length, offset } = state;
const classes = [
"absolute",
clip.clip_uuid === selectedClipId
? `bg-${group}-selected border-white focus-visible:outline-0`
: `bg-${group}-clip`,
];
return (
<>
{ /* Left drag handle button */}
<button
className={[
...classes,
"rounded-l-md border border-transparent",
"block h-full cursor-ew-resize",
clip.clip_uuid === selectedClipId
? "border border-b-2 border-l-2 border-r-0 border-t-2"
: "",
].join(" ")}
style={{
width: dragHandleWidth,
left: offset * unitPx * scale.value + frameTrackButtonWidthPx,
}}
onPointerDown={(event) => onPointerDown(event, "left")}
onPointerMove={() => {
if (dragItem.value) {
canDrop.value = false;
DndAsset.notDropText = "Cannot drop onto another clip";
}
}}
onClick={(event) => {
event.stopPropagation();
event.preventDefault();
selectedItem.value = clip;
}}
>
<div className="flex h-full items-center justify-start">
<div className="ml-1.5 block h-[10px] w-[2px] bg-white opacity-60" />
</div>
</button>
{ /* Label/Move button */}
<button
className={[
...classes,
"block h-full",
clip.clip_uuid === selectedClipId
? "border border-b-2 border-l-0 border-r-0 border-t-2"
: "",
].join(" ")}
style={{
width: length * unitPx * scale.value - 2 * dragHandleWidth,
left: offset * unitPx * scale.value + dragHandleWidth + frameTrackButtonWidthPx,
cursor: "move",
}}
onPointerDown={(event) => onPointerDown(event, "drag")}
onPointerMove={() => {
if (dragItem.value) {
canDrop.value = false;
DndAsset.notDropText = "Cannot drop onto another clip";
}
}}
onClick={(event) => {
event.stopPropagation();
event.preventDefault();
selectedItem.value = clip;
}}
>
<div
className="ml-2 w-full overflow-hidden text-ellipsis whitespace-nowrap text-left text-xs text-white"
style={{ width: length * 4 * scale.value - 46 }}
>
{clip.name}
</div>
</button>
{ /* Right drag handle button */}
<button
className={[
...classes,
"rounded-r-md border border-transparent",
"block h-full cursor-ew-resize",
clip.clip_uuid === selectedClipId
? "border border-b-2 border-l-0 border-r-2 border-t-2"
: "",
].join(" ")}
style={{
width: dragHandleWidth,
left: offset * unitPx * scale.value + length * unitPx * scale.value - dragHandleWidth + frameTrackButtonWidthPx,
}}
onPointerDown={(event) => onPointerDown(event, "right")}
onPointerMove={() => {
if (dragItem.value) {
canDrop.value = false;
DndAsset.notDropText = "Cannot drop onto another clip";
}
}}
onClick={(event) => {
event.stopPropagation();
event.preventDefault();
selectedItem.value = clip;
}}
>
<div className="flex h-full items-center justify-end">
<div className="mr-1.5 block h-[10px] w-[2px] bg-white opacity-60" />
</div>
</button>
</>
);
};
@@ -1,192 +0,0 @@
import { useMouseEventsClip } from "./utils/useMouseEventsClip";
import { useState } from "react";
import {
canDrop,
dragItem,
scale,
selectedItem,
} from "~/pages/PageEnigma/signals";
import { ClipGroup } from "~/enums";
import { Clip } from "~/pages/PageEnigma/models";
import DndAsset from "~/pages/PageEnigma/DragAndDrop/DndAsset";
import { faPencil, faTrashCan } from "@fortawesome/pro-solid-svg-icons";
import { useOnDelete } from "~/pages/PageEnigma/comps/Timeline/utils/useOnDelete";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AddEditPrompt } from "~/pages/PageEnigma/comps/Timeline/AddEditPrompt";
interface Props {
min: number;
max: number;
group: ClipGroup;
clip: Clip;
updateClip: (options: {
id: string;
offset: number;
length: number;
name: string;
}) => void;
}
export const TrackClipPrompt = ({
clip,
min,
max,
group,
updateClip,
}: Props) => {
const [state, setState] = useState({
length: clip.length,
offset: clip.offset,
});
const [hover, setHover] = useState<boolean>(false);
const [openPrompt, setOpenPrompt] = useState(false);
const { onPointerDown } = useMouseEventsClip(
clip,
max,
min,
(options) => updateClip({ ...options, name: clip.name }),
setState,
);
const { confirmationModal, onDeleteAsk } = useOnDelete();
const selectedClipId = (selectedItem.value as Clip | null)?.clip_uuid ?? "";
const { length, offset } = state;
const classes = [
"absolute",
clip.clip_uuid === selectedClipId
? `bg-${group}-selected border-white focus-visible:outline-0`
: `bg-${group}-clip`,
];
return (
<>
<button
className={[
...classes,
"z-40 w-[15px] rounded-l-md border border-transparent",
"block h-full cursor-ew-resize",
clip.clip_uuid === selectedClipId
? "border border-b-2 border-l-2 border-r-0 border-t-2"
: "",
].join(" ")}
style={{
left: offset * 4 * scale.value,
}}
onPointerDown={(event) => onPointerDown(event, "left")}
onPointerMove={() => {
if (dragItem.value) {
canDrop.value = false;
DndAsset.notDropText = "Cannot drop onto another clip";
}
}}
onClick={(event) => {
event.stopPropagation();
event.preventDefault();
selectedItem.value = clip;
}}
>
<div className="flex h-full items-center justify-start">
<div className="ml-1.5 block h-[10px] w-[2px] bg-white opacity-60" />
</div>
</button>
<button
className={[
...classes,
"z-50 block h-full",
clip.clip_uuid === selectedClipId
? "border border-b-2 border-l-0 border-r-0 border-t-2"
: "",
].join(" ")}
style={{
width: length * 4 * scale.value - 30,
left: offset * 4 * scale.value + 15,
cursor: "move",
}}
onPointerOver={() => setHover(true)}
onPointerLeave={() => setHover(false)}
onPointerDown={(event) => onPointerDown(event, "drag")}
onPointerMove={() => {
if (dragItem.value) {
canDrop.value = false;
DndAsset.notDropText = "Cannot drop onto another clip";
}
}}
onClick={(event) => {
event.stopPropagation();
event.preventDefault();
selectedItem.value = clip;
}}
>
<div className="flex items-center">
<div
className="ml-2 w-full truncate text-left text-xs text-white"
style={{ width: length * 4 * scale.value - 46 }}
>
{clip.name}
</div>
{hover && (
<>
<div
className="cursor-pointer"
onClick={(event) => onDeleteAsk(event, clip)}
>
<FontAwesomeIcon
icon={faTrashCan}
className="text-sm text-white/80 hover:text-white"
/>
</div>
<div
className="cursor-pointer"
onClick={() => setOpenPrompt(true)}
>
<FontAwesomeIcon
icon={faPencil}
className="me-1 ms-2 text-sm text-white/80 hover:text-white"
/>
</div>
</>
)}
</div>
</button>
<button
className={[
...classes,
"z-50 w-[15px] rounded-r-md border border-transparent",
"block h-full cursor-ew-resize",
clip.clip_uuid === selectedClipId
? "border border-b-2 border-l-0 border-r-2 border-t-2"
: "",
].join(" ")}
style={{
left: offset * 4 * scale.value + length * 4 * scale.value - 15,
}}
onPointerDown={(event) => onPointerDown(event, "right")}
onPointerMove={() => {
if (dragItem.value) {
canDrop.value = false;
DndAsset.notDropText = "Cannot drop onto another clip";
}
}}
onClick={(event) => {
event.stopPropagation();
event.preventDefault();
selectedItem.value = clip;
}}
>
<div className="flex h-full items-center justify-end">
<div className="mr-1.5 block h-[10px] w-[2px] bg-white opacity-60" />
</div>
</button>
{confirmationModal()}
<AddEditPrompt
clip={clip}
setOpenPrompt={setOpenPrompt}
openPrompt={openPrompt}
offset={offset}
/>
</>
);
};
@@ -1,159 +0,0 @@
import { TrackClip } from "~/pages/PageEnigma/comps/Timeline/TrackClip";
import { Clip } from "~/pages/PageEnigma/models";
import { PointerEvent } from "react";
import {
canDrop,
dragItem,
filmLength,
frameTrackButtonWidthPx,
scale,
} from "~/pages/PageEnigma/signals";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowUp, faImage } from "@fortawesome/pro-solid-svg-icons";
import DndAsset from "~/pages/PageEnigma/DragAndDrop/DndAsset";
import { ClipGroup, ClipType } from "~/enums";
import { currentPage } from "~/signals";
import { Pages } from "~/pages/PageEnigma/constants/page";
import { getCanDrop } from "~/pages/PageEnigma/comps/Timeline/utils/getCanDrop";
import { ButtonIconStack } from "~/components/reusable/ButtonIconStack";
import CharacterFrameButton, { CharacterFrameTarget } from "./CharacterFrameButtons";
import { MIN_ANIM_DURATION } from "~/constants";
interface Props {
id: string;
clips: Clip[];
group: ClipGroup;
type?: ClipType;
updateClip: (options: { id: string; length: number; offset: number }) => void;
}
export const TrackClips = ({ id, clips, updateClip, group, type }: Props) => {
const trackType = (type ?? group) as ClipType;
const canDropAsset = getCanDrop({
dragType: dragItem.value?.type,
type,
group,
});
function onPointerOver() {
if (getCanDrop({ dragType: dragItem.value?.type, type, group })) {
DndAsset.dropId = id;
}
}
function onPointerMove(
event: PointerEvent<HTMLDivElement | HTMLButtonElement>,
) {
if (!getCanDrop({ dragType: dragItem.value?.type, type, group })) {
return;
}
const element = document.getElementById(`track-${trackType}-${id}`);
DndAsset.overElement = element!.getBoundingClientRect();
const track = document.getElementById(`track-${trackType}-${id}`);
if (!track) {
canDrop.value = false;
return;
}
// Now check if the clip fits
const position = track.getBoundingClientRect();
const clipOffset = Math.round(
(event.clientX - position.x - frameTrackButtonWidthPx) / 4 / scale.value,
);
if (clipOffset + MIN_ANIM_DURATION > filmLength.value * 1000) {
DndAsset.notDropText = "Not enough room to hold item";
canDrop.value = false;
return;
}
const overlap = clips.some((clip) => {
if (clipOffset === clip.offset) {
return true;
}
if (clipOffset > clip.offset && clipOffset <= clip.offset + clip.length) {
return true;
}
return (
clipOffset < clip.offset &&
clipOffset + (dragItem.value!.length ?? 0) >= clip.offset
);
});
canDrop.value = !overlap;
if (!overlap) {
DndAsset.dropOffset = clipOffset;
}
if (overlap) {
DndAsset.notDropText = "Not enough space to drop here";
}
}
return (
<div
id={`track-${trackType}-${id}`}
className={[
"relative mb-1 flex h-[60px] w-fit rounded-md overflow-hidden",
].join(" ")}
style={{ marginLeft: (group === ClipGroup.CHARACTER ? 0 : frameTrackButtonWidthPx) }}
>
{(group === ClipGroup.CHARACTER &&
<CharacterFrameButton className={"h-full rounded-l-md"} target={CharacterFrameTarget.Start} characterId={id} />
)}
<div
className={[
`bg-${group}-unselected inset-0`,
canDropAsset
? "animate-pulse bg-white/30 duration-[750ms]"
: "",
].join(" ")}
style={{ left: frameTrackButtonWidthPx, width: filmLength.value * 1000 * 4 * scale.value }}
onPointerOver={onPointerOver}
onPointerMove={onPointerMove}
>
{clips.map((clip, index) => (
<TrackClip
key={clip.clip_uuid}
min={
index > 0 ? clips[index - 1].offset + clips[index - 1].length : 0
}
max={
index < clips.length - 1
? clips[index + 1].offset
: filmLength.value * 1000
}
group={group}
updateClip={updateClip}
clip={clip}
/>
))}
</div>
{clips.length === 0 && currentPage.value === Pages.EDIT && (
<div className="prevent-select absolute flex h-full items-center gap-2 ps-2 text-xs font-medium text-white" style={{ marginLeft: frameTrackButtonWidthPx + 10 }}>
<div className="animate-bounce">
<FontAwesomeIcon icon={faArrowUp} className="text-white/80" />
</div>
<div className="text-xs text-white/80">
{type === ClipType.TRANSFORM
? "Drag and Drop TRANSFORM clip here" // This should not show
: type === ClipType.AUDIO
? "Drag and drop character speech or vocal audio clips here"
: type === ClipType.ANIMATION
? "Drag and drop character animation clips here"
: type === ClipType.EXPRESSION
? "Drag and drop facial expression animation clips here"
: type === ClipType.FAKE
? "Drag and Drop FAKE clip here" // this should not show.
: "Drag and drop uploaded music clips or sound effects from the audio tab here"}
</div>
</div>
)}
{(group === ClipGroup.CHARACTER &&
<CharacterFrameButton className={"h-full rounded-r-md"} target={CharacterFrameTarget.End} characterId={id} />
)}
</div>
);
};
@@ -1,126 +0,0 @@
import { TrackClipPrompt } from "~/pages/PageEnigma/comps/Timeline/TrackClipPrompt";
import { Clip } from "~/pages/PageEnigma/models";
import { PointerEvent, useRef, useState } from "react";
import { filmLength, scale } from "~/pages/PageEnigma/signals";
import { ClipGroup } from "~/enums";
import { twMerge } from "tailwind-merge";
import { useSignals } from "@preact/signals-react/runtime";
import { AddEditPrompt } from "~/pages/PageEnigma/comps/Timeline/AddEditPrompt";
interface Props {
id: string;
clips: Clip[];
updateClip: (options: {
id: string;
length: number;
offset: number;
name: string;
}) => void;
}
export const TrackClipsPrompt = ({ id, clips, updateClip }: Props) => {
useSignals();
const [hover, setHover] = useState(-1);
const [openPrompt, setOpenPrompt] = useState(false);
const [hasOverlap, setHasOverlap] = useState(false);
const clientX = useRef(0);
function onPointerOver(event: PointerEvent) {
const max = filmLength.value * 1000 * 4 * scale.value;
if (event.pageX < 320) {
setHover(320);
} else {
setHover(Math.min(event.pageX, max + 80));
clientX.current = event.pageX - 320;
}
}
function onPointerMove(
event: PointerEvent<HTMLDivElement | HTMLButtonElement>,
) {
if (hover === -1) {
return;
}
const max = filmLength.value * 1000 * 4 * scale.value;
const offset =
(event.pageX < 320 ? 0 : Math.min(event.pageX - 320, max + 80)) /
4 /
scale.value;
if (event.pageX < 320) {
setHover(320);
clientX.current = 0;
} else {
const offset = Math.min(event.pageX, max + 80);
setHover(offset);
clientX.current = offset - 320;
}
const overlap = clips.some((clip) => {
if (offset + 60 < clip.offset) {
return false;
}
return clip.offset + clip.length >= offset;
});
setHasOverlap(overlap);
}
return (
<div
id={`track-prompt-travel-${id}`}
className={twMerge(
"relative mb-1 block h-[30px] w-full rounded-md",
`bg-prompt-unselected`,
)}
>
<div
className={twMerge("absolute inset-0 rounded-md", "opacity-0")}
onPointerOver={onPointerOver}
onPointerMove={onPointerMove}
onPointerLeave={() => setHover(-1)}
/>
{clips.map((clip, index) => (
<TrackClipPrompt
key={clip.clip_uuid}
min={
index > 0 ? clips[index - 1].offset + clips[index - 1].length : 0
}
max={
index < clips.length - 1
? clips[index + 1].offset
: filmLength.value * 1000
}
group={ClipGroup.PROMPT_TRAVEL}
updateClip={updateClip}
clip={clip}
/>
))}
{hover > -1 && (
<button
className={twMerge(
"text- absolute h-full rounded-md",
"border border-dashed border-white/60",
"text-sm text-white/60",
hasOverlap ? "bg-action" : "bg-prompt-selected/30",
)}
style={{ width: 60 * scale.value * 4, left: hover - 320 }}
onPointerOver={onPointerOver}
onPointerMove={onPointerMove}
onPointerLeave={() => setHover(-1)}
onClick={(event) => {
event.stopPropagation();
event.preventDefault();
!hasOverlap && setOpenPrompt(true);
}}
>
{hasOverlap ? "Overlap" : "Click to add prompt"}
</button>
)}
{openPrompt && (
<AddEditPrompt
setOpenPrompt={setOpenPrompt}
openPrompt={openPrompt}
offset={clientX.current / 4 / scale.value}
/>
)}
</div>
);
};
@@ -1,46 +0,0 @@
import { Keyframe } from "~/pages/PageEnigma/models";
import { filmLength, scale, selectedItem } from "~/pages/PageEnigma/signals";
import { useMouseEventsKeyframe } from "~/pages/PageEnigma/comps/Timeline/utils/useMouseEventsKeyframe";
import { useSignals } from "@preact/signals-react/runtime";
interface Props {
keyframe: Keyframe;
updateKeyframe: (options: { id: string; offset: number }) => void;
}
export const TrackKeyFrame = ({ keyframe, updateKeyframe }: Props) => {
useSignals();
const { onPointerDown, offset } = useMouseEventsKeyframe({
keyframe,
max: filmLength.value * 1000,
min: 0,
updateKeyframe,
});
const displayOffset = offset > -1 ? offset : keyframe.offset;
const selectedKeyframeId =
(selectedItem.value as Keyframe | null)?.keyframe_uuid ?? "";
return (
<button
className={[
"top-[9px] block h-[12px] w-[12px] rotate-45 cursor-ew-resize",
"absolute",
keyframe.keyframe_uuid === selectedKeyframeId
? "bg-keyframe-selected"
: "bg-keyframe-unselected",
].join(" ")}
style={{
left: displayOffset * 4 * scale.value - 5,
}}
onPointerDown={(event) => onPointerDown(event, "drag")}
onClick={(event) => {
event.stopPropagation();
event.preventDefault();
selectedItem.value = keyframe;
}}
/>
);
};
@@ -1,88 +0,0 @@
import { Keyframe } from "~/pages/PageEnigma/models";
import { TrackKeyFrame } from "~/pages/PageEnigma/comps/Timeline/TrackKeyFrame";
import DndAsset from "~/pages/PageEnigma/DragAndDrop/DndAsset";
import { dragItem, filmLength, frameTrackButtonWidthPx, scale } from "~/pages/PageEnigma/signals";
import { AssetType, ClipGroup } from "~/enums";
interface Props {
id: string;
keyframes: Keyframe[];
title?: string;
group: ClipGroup;
toggleMute?: () => void;
muted?: boolean;
updateKeyframe: (options: {
id: string;
offset: number;
force?: boolean;
}) => void;
}
function setNotDropText({
dragType,
group,
}: {
dragType?: AssetType;
group: ClipGroup;
}) {
if (dragType === AssetType.ANIMATION) {
if (group === ClipGroup.CHARACTER) {
DndAsset.notDropText = "Cannot drag animation onto movement track";
}
if (group === ClipGroup.CAMERA) {
DndAsset.notDropText = "Cannot drag animation onto camera track";
}
if (group === ClipGroup.OBJECT) {
DndAsset.notDropText = "Cannot drag animation onto object track";
}
}
if (dragType === AssetType.AUDIO) {
if (group === ClipGroup.CHARACTER) {
DndAsset.notDropText = "Cannot drag audio onto movement track";
}
if (group === ClipGroup.CAMERA) {
DndAsset.notDropText = "Cannot drag audio onto camera track";
}
if (group === ClipGroup.OBJECT) {
DndAsset.notDropText = "Cannot drag audio onto object track";
}
}
}
export const TrackKeyFrames = ({
id,
keyframes,
updateKeyframe,
title,
group,
}: Props) => {
function onPointerMove() {
if (!DndAsset.overElement) {
const element = document.getElementById(`track-${group}-${id}`);
DndAsset.overElement = element!.getBoundingClientRect();
}
setNotDropText({ dragType: dragItem.value?.type, group });
}
return (
<div style={{ marginLeft: frameTrackButtonWidthPx, width: filmLength.value * 1000 * 4 * scale.value }}>
<div
id={`track-${group}-${id}`}
className={`relative mb-1 block h-[30px] w-full rounded-md bg-${group}-unselected`}
onPointerMove={onPointerMove}
>
{keyframes.map((keyframe) => (
<TrackKeyFrame
key={keyframe.keyframe_uuid}
updateKeyframe={updateKeyframe}
keyframe={keyframe}
/>
))}
{!!title && (
<div className="prevent-select absolute ps-2 pt-1 text-xs font-medium text-white">
{title}
</div>
)}
</div>
</div>
);
};
@@ -1 +0,0 @@
export { Timeline } from './Timeline';
@@ -1,61 +0,0 @@
import { ClipGroup, ClipType, AssetType } from "~/enums";
import DndAsset from "~/pages/PageEnigma/DragAndDrop/DndAsset";
export function getCanDrop({
dragType,
type,
group,
}: {
dragType?: AssetType;
type?: ClipType;
group: ClipGroup;
}) {
if (dragType === AssetType.ANIMATION) {
if (type === ClipType.ANIMATION) {
return true;
}
if (type === ClipType.AUDIO) {
DndAsset.notDropText =
"Cannot drag character animation onto vocals and speech track";
}
if (type === ClipType.EXPRESSION) {
DndAsset.notDropText =
"Cannot drag character animation onto facial expression track";
}
if (group === ClipGroup.GLOBAL_AUDIO) {
DndAsset.notDropText = "Cannot drag character animation onto music track";
}
if (group === ClipGroup.CAMERA) {
DndAsset.notDropText =
"Cannot drag character animation onto camera track";
}
}
if (dragType === AssetType.EXPRESSION) {
if (type === ClipType.EXPRESSION) {
return true;
}
}
if (dragType === AssetType.EXPRESSION) {
if (type === ClipType.EXPRESSION) {
return true;
}
}
if (dragType === AssetType.AUDIO) {
if (group === ClipGroup.CHARACTER && type === ClipType.AUDIO) {
return true;
}
if (group === ClipGroup.GLOBAL_AUDIO) {
return true;
}
if (group === ClipGroup.CAMERA) {
DndAsset.notDropText = "Cannot drag audio onto camera track";
}
if (type === ClipType.ANIMATION) {
DndAsset.notDropText = "Cannot drag audio onto character animation track";
}
if (type === ClipType.EXPRESSION) {
DndAsset.notDropText = "Cannot drag audio onto facial expression track";
}
}
return false;
}
@@ -1,109 +0,0 @@
import React, { Dispatch, useCallback, useEffect, useRef } from "react";
import { Clip } from "~/pages/PageEnigma/models";
import { canDrop, scale } from "~/pages/PageEnigma/signals";
import { useSignals } from "@preact/signals-react/runtime";
export const useMouseEventsClip = (
clip: Clip,
max: number,
min: number,
updateClip: (args: { id: string; offset: number; length: number }) => void,
setState: Dispatch<{ length: number; offset: number }>,
) => {
useSignals();
const currLength = useRef(clip.length);
const currOffset = useRef(clip.offset);
const initLength = useRef(clip.length);
const initOffset = useRef(clip.offset);
const isActive = useRef("");
const clientX = useRef(0);
const onPointerUp = useCallback(
(event: PointerEvent) => {
if (isActive.current) {
event.stopPropagation();
event.preventDefault();
updateClip({
id: clip.clip_uuid,
offset: Math.round(currOffset.current),
length: Math.round(currLength.current),
});
isActive.current = "";
canDrop.value = false;
}
},
[updateClip, clip.clip_uuid],
);
const onMouseMove = useCallback(
(event: MouseEvent) => {
if (isActive.current) {
event.stopPropagation();
event.preventDefault();
}
const delta = (event.clientX - clientX.current) / 4 / scale.value;
const deltaOffset = delta + initOffset.current;
if (isActive.current === "drag") {
if (deltaOffset < min) {
currOffset.current = min;
} else if (deltaOffset + currLength.current > max) {
currOffset.current = max - currLength.current;
} else {
currOffset.current = deltaOffset;
}
}
if (isActive.current === "left") {
if (
initLength.current - delta < 90 / 4 / scale.value ||
deltaOffset < min
) {
return;
}
currOffset.current = deltaOffset;
currLength.current = initLength.current - delta;
}
if (isActive.current === "right") {
if (
initLength.current + delta < 90 / 4 / scale.value ||
currOffset.current + initLength.current + delta > max
) {
return;
}
currLength.current = initLength.current + delta;
}
setState({ length: currLength.current, offset: currOffset.current });
},
[max, min, setState],
);
useEffect(() => {
currLength.current = clip.length;
currOffset.current = clip.offset;
initLength.current = clip.length;
initOffset.current = clip.offset;
}, [clip.length, clip.offset]);
useEffect(() => {
window.addEventListener("pointerup", onPointerUp);
window.addEventListener("pointermove", onMouseMove);
return () => {
window.removeEventListener("pointerup", onPointerUp);
window.removeEventListener("pointermove", onMouseMove);
};
}, [onPointerUp, onMouseMove]);
return {
onPointerDown: (
event: React.PointerEvent<HTMLButtonElement>,
type: string,
) => {
if (event.button === 0) {
event.stopPropagation();
event.preventDefault();
clientX.current = event.clientX;
isActive.current = type;
}
},
};
};
@@ -1,91 +0,0 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import { Keyframe } from "~/pages/PageEnigma/models";
import { canDrop, scale } from "~/pages/PageEnigma/signals";
import { useSignals } from "@preact/signals-react/runtime";
export const useMouseEventsKeyframe = ({
keyframe,
max,
min,
updateKeyframe,
}: {
keyframe: Keyframe;
max: number;
min: number;
updateKeyframe: (args: { id: string; offset: number }) => void;
}) => {
useSignals();
const [offset, setOffset] = useState(-1);
const currOffset = useRef(keyframe.offset);
const initOffset = useRef(keyframe.offset);
const isActive = useRef("");
const clientX = useRef(0);
const onPointerUp = useCallback(
(event: PointerEvent) => {
if (isActive.current) {
event.stopPropagation();
event.preventDefault();
updateKeyframe({
id: keyframe.keyframe_uuid,
offset: Math.round(currOffset.current),
});
isActive.current = "";
canDrop.value = false;
setOffset(-1);
}
},
[updateKeyframe, keyframe.keyframe_uuid, setOffset],
);
const onMouseMove = useCallback(
(event: MouseEvent) => {
const delta = (event.clientX - clientX.current) / 4 / scale.value;
const deltaOffset = delta + initOffset.current;
if (isActive.current === "drag") {
event.stopPropagation();
event.preventDefault();
if (deltaOffset < min) {
currOffset.current = min;
} else if (deltaOffset > max) {
currOffset.current = max;
} else {
currOffset.current = deltaOffset;
}
setOffset(currOffset.current);
}
},
[max, min, setOffset],
);
useEffect(() => {
currOffset.current = keyframe.offset;
initOffset.current = keyframe.offset;
}, [keyframe.offset]);
useEffect(() => {
window.addEventListener("pointerup", onPointerUp);
window.addEventListener("pointermove", onMouseMove);
return () => {
window.removeEventListener("pointerup", onPointerUp);
window.removeEventListener("pointermove", onMouseMove);
};
}, [onPointerUp, onMouseMove]);
return {
onPointerDown: (
event: React.PointerEvent<HTMLButtonElement>,
type: string,
) => {
if (event.button === 0) {
event.stopPropagation();
event.preventDefault();
clientX.current = event.clientX;
isActive.current = type;
setOffset(keyframe.offset);
}
},
offset,
};
};
@@ -1,88 +0,0 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import {
currentTime,
filmLength,
scale,
} from "~/pages/PageEnigma/signals";
import Queue from "~/pages/PageEnigma/Queue/Queue";
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
export const useMouseEventsScrubber = () => {
const [isActive, setIsActive] = useState(false);
const clientX = useRef(0);
const [time, setTime] = useState(-1);
const getDelta = useCallback((event: MouseEvent) => {
const max = filmLength.value * 1000;
const delta = Math.round(
(event.clientX - clientX.current) / 4 / scale.value + currentTime.value,
);
if (delta < 0) {
return 0;
}
if (delta > max) {
return max;
}
return delta;
}, []);
useEffect(() => {
const onPointerUp = () => {
if (isActive) {
currentTime.value = Math.round(time);
setIsActive(false);
setTime(-1);
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.UPDATE_TIME,
data: { currentTime: Math.round(time) },
});
}
};
const onMouseMove = (event: MouseEvent) => {
if (isActive) {
event.stopPropagation();
event.preventDefault();
const delta = getDelta(event);
setTime((oldTime) => {
if (oldTime !== delta) {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.UPDATE_TIME,
data: {
currentTime: delta,
},
});
}
return delta;
});
return;
}
};
window.addEventListener("pointerup", onPointerUp);
window.addEventListener("pointermove", onMouseMove);
return () => {
window.removeEventListener("pointerup", onPointerUp);
window.removeEventListener("pointermove", onMouseMove);
};
}, [clientX, isActive, time, getDelta]);
return {
onPointerDown: useCallback((event: React.PointerEvent<HTMLDivElement>) => {
if (event.button === 0) {
clientX.current = event.clientX;
setIsActive(true);
setTime(currentTime.value);
}
}, []),
time,
};
};
@@ -1,52 +0,0 @@
import React, { useCallback, useEffect, useState } from "react";
import { dndSidePanelWidth, sidePanelWidth } from "~/pages/PageEnigma/signals";
export const useMouseEventsSidePanel = () => {
const [isActive, setIsActive] = useState(false);
const [clientX, setClientX] = useState(0);
useEffect(() => {
const onPointerUp = () => {
if (isActive) {
sidePanelWidth.value = Math.round(dndSidePanelWidth.value);
setIsActive(false);
dndSidePanelWidth.value = -1;
}
};
const onMouseMove = (event: MouseEvent) => {
if (isActive) {
const delta = event.clientX - clientX;
event.stopPropagation();
event.preventDefault();
if (sidePanelWidth.value - delta < 240) {
return;
}
if (sidePanelWidth.value - delta > 443) {
return;
}
dndSidePanelWidth.value = sidePanelWidth.value - delta;
return;
}
};
window.addEventListener("pointerup", onPointerUp);
window.addEventListener("pointermove", onMouseMove);
return () => {
window.removeEventListener("pointerup", onPointerUp);
window.removeEventListener("pointermove", onMouseMove);
};
}, [clientX, isActive]);
return {
onPointerDown: useCallback((event: React.PointerEvent<HTMLDivElement>) => {
if (event.button === 0) {
event.stopPropagation();
setClientX(event.clientX);
setIsActive(true);
dndSidePanelWidth.value = sidePanelWidth.value;
}
}, []),
};
};
@@ -1,85 +0,0 @@
import React, { useCallback, useEffect, useState } from "react";
import {
dndTimelineHeight,
filmLength,
scale,
secondaryScrubber,
timelineHeight,
timelineScrollX,
} from "~/pages/PageEnigma/signals";
import { currentPage, pageHeight } from "~/signals";
import { Pages } from "~/pages/PageEnigma/constants/page";
import Queue from "~/pages/PageEnigma/Queue/Queue";
import { QueueNames } from "~/pages/PageEnigma/Queue";
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
export const useMouseEventsTimeline = () => {
const [isActive, setIsActive] = useState(false);
const [clientY, setClientY] = useState(0);
useEffect(() => {
const onPointerUp = (event: PointerEvent) => {
if (isActive) {
event.stopPropagation();
event.preventDefault();
timelineHeight.value = Math.round(dndTimelineHeight.value);
setIsActive(false);
dndTimelineHeight.value = -1;
}
};
const onMouseMove = (event: MouseEvent) => {
if (isActive) {
event.stopPropagation();
event.preventDefault();
const delta = event.clientY - clientY;
if (timelineHeight.value - delta < 30) {
return;
}
if (timelineHeight.value - delta > pageHeight.value * 0.5) {
return;
}
dndTimelineHeight.value = timelineHeight.value - delta;
return;
}
if (currentPage.value === Pages.STYLE) {
let newPosition =
(event.clientX - 200 + timelineScrollX.value) / 4 / scale.value;
if (newPosition < 0) {
newPosition = 0;
}
const max = filmLength.value * 1000;
if (newPosition > max) {
newPosition = max;
}
if (newPosition !== secondaryScrubber.value) {
secondaryScrubber.value = newPosition;
Queue.publish({
queueName: QueueNames.TO_TIMELINE,
action: toEngineActions.UPDATE_TIME,
data: { currentTime: newPosition },
});
}
}
};
window.addEventListener("pointerup", onPointerUp);
window.addEventListener("pointermove", onMouseMove);
return () => {
window.removeEventListener("pointerup", onPointerUp);
window.removeEventListener("pointermove", onMouseMove);
};
}, [clientY, isActive]);
return {
onPointerDown: useCallback((event: React.PointerEvent<HTMLDivElement>) => {
if (event.button === 0) {
event.stopPropagation();
dndTimelineHeight.value = timelineHeight.value;
setClientY(event.clientY);
setIsActive(true);
}
}, []),
};
};
@@ -1,80 +0,0 @@
import { MouseEvent, useCallback, useState } from "react";
import {
deleteAudioClip,
deleteCharacterClip,
deleteKeyframe,
selectedItem,
} from "~/pages/PageEnigma/signals";
import { Clip, Keyframe } from "~/pages/PageEnigma/models";
import { deletePromptTravelClip } from "~/pages/PageEnigma/signals/promptTravelGroup";
import { DoNotShow } from "~/constants";
import { ConfirmationModal } from "~/components";
import { useSignals } from "@preact/signals-react/runtime";
function getItemType(item: Clip | Keyframe | null) {
if (!item) {
return "";
}
return (item as Clip).clip_uuid ? "clip" : "keyframe";
}
export const useOnDelete = () => {
useSignals();
const [dialogOpen, setDialogOpen] = useState(false);
const [deletingItem, setDeletingItem] = useState<Clip | Keyframe | null>(
null,
);
const onDelete = useCallback((item: Clip | Keyframe) => {
if ((item as Clip).clip_uuid) {
deleteCharacterClip(item as Clip);
deleteAudioClip(item as Clip);
deletePromptTravelClip(item as Clip);
} else {
deleteKeyframe(item as Keyframe);
}
selectedItem.value = null;
}, []);
const onDeleteAsk = useCallback(
(event: KeyboardEvent | MouseEvent, item: Clip | Keyframe) => {
event.stopPropagation();
event.preventDefault();
const show = localStorage.getItem(`Delete-${getItemType(item)}`);
if (show === DoNotShow) {
onDelete(item);
return;
}
setDeletingItem(item);
setDialogOpen(true);
},
[onDelete],
);
const confirmationModal = () => {
return (
<ConfirmationModal
title={`Delete ${getItemType(deletingItem)}`}
text={`Are you sure you want to delete the selected ${getItemType(deletingItem)}?`}
open={dialogOpen}
onClose={() => setDialogOpen(false)}
onOk={() => {
onDelete(deletingItem!);
setDialogOpen(false);
}}
okText="Delete"
okColor="bg-brand-primary"
onCancel={() => setDialogOpen(false)}
canHide
/>
);
};
return {
onDelete,
onDeleteAsk,
dialogOpen,
setDialogOpen,
confirmationModal,
};
};
@@ -1,38 +0,0 @@
import React, { useCallback } from "react";
import {
currentTime,
filmLength,
frameTrackButtonWidthPx,
scale,
selectedItem,
stylizeScrollX,
timelineScrollX,
} from "~/pages/PageEnigma/signals";
import Queue from "~/pages/PageEnigma/Queue/Queue";
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
import { Pages } from "~/pages/PageEnigma/constants/page";
export default function useTimelineClick(page: Pages) {
return useCallback((event: React.PointerEvent<HTMLDivElement>) => {
const scrollX =
page === Pages.EDIT ? timelineScrollX.value : stylizeScrollX.value;
const newTime = Math.round(
(event.clientX + scrollX - 204 - frameTrackButtonWidthPx) / 4 / scale.value,
);
if (newTime < 0) {
return;
}
const max = filmLength.value * 1000;
if (newTime > max) {
return;
}
selectedItem.value = null;
currentTime.value = newTime;
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.UPDATE_TIME,
data: { currentTime: newTime },
});
}, [page]);
}
@@ -0,0 +1,12 @@
import { signal } from "@preact/signals-react";
export const appTabId = signal("3D");
export const setAppTabId = (newId: string) => {
if (newId != "2D" && newId != "3D") {
console.error("Provided app ID is not valid");
return;
}
appTabId.value = newId;
};
+9 -6
View File
@@ -54,6 +54,15 @@
{
"path": "../../libs/components/demo-modal"
},
{
"path": "../../libs/soundboard"
},
{
"path": "../../libs/components/tab-selector"
},
{
"path": "../../libs/api"
},
{
"path": "../../libs/components/tooltip"
},
@@ -65,12 +74,6 @@
},
{
"path": "../../libs/tauri-utils"
},
{
"path": "../../libs/api"
},
{
"path": "../../libs/soundboard"
}
]
}
@@ -54,7 +54,6 @@ export function DemoModal({
<video autoPlay muted loop controls={false}>
<source src={videoSrc} type="video/mp4" />
</video>
W
</div>
<Button
className="font-semibold"
@@ -0,0 +1,12 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
@@ -0,0 +1,7 @@
# @storyteller/ui-login-modal
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test @storyteller/ui-login-modal` to execute the unit tests via [Vitest](https://vitest.dev/).
@@ -0,0 +1,17 @@
{
"name": "@storyteller/ui-login-modal",
"version": "0.0.1",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"development": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
}
}
@@ -0,0 +1,2 @@
export * from './lib/login-modal';
@@ -0,0 +1,7 @@
/*
* Replace this with your own classes
*
* e.g.
* .container {
* }
*/
@@ -0,0 +1,12 @@
import { render } from '@testing-library/react';
import LoginModal from './login-modal';
describe('LoginModal', () => {
it('should render successfully', () => {
const { baseElement } = render(<LoginModal />);
expect(baseElement).toBeTruthy();
});
});
@@ -0,0 +1,273 @@
import { Button } from "@storyteller/ui-button";
import { Transition, TransitionChild } from "@headlessui/react";
import { createPortal } from "react-dom";
import { useState, useEffect } from "react";
import {
faArrowRight,
faRightToBracket,
} from "@fortawesome/pro-solid-svg-icons";
import { faDiscord } from "@fortawesome/free-brands-svg-icons";
import { IsDesktopApp } from "@storyteller/tauri-utils";
import { invoke } from "@tauri-apps/api/core";
interface LoginModalProps {
onClose: () => void;
videoSrc2D: string;
videoSrc3D: string;
openAiLogo: string;
}
export function LoginModal({
onClose,
videoSrc2D,
videoSrc3D,
openAiLogo,
}: LoginModalProps) {
const [step, setStep] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [hasSession, setHasSession] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const totalSteps = 3;
const checkSession = async () => {
// Return the current session state
return hasSession;
};
// Check session on component mount
useEffect(() => {
const initSession = async () => {
const sessionExists = await checkSession();
if (!sessionExists) {
setIsOpen(true);
setStep(1);
}
};
initSession();
}, []);
const handleClose = () => {
setIsOpen(false);
onClose();
};
const handleNext = async () => {
if (step === 2) {
setIsLoading(true);
try {
if (IsDesktopApp()) {
await invoke("open_sora_login_command");
// Add 3 second delay before setting session to true
await new Promise((resolve) => setTimeout(resolve, 3000));
setHasSession(true);
const sessionExists = await checkSession();
if (sessionExists) {
setStep(step + 1);
}
} else {
alert("Please open the desktop app to login");
// Add 3 second delay before setting session to true
await new Promise((resolve) => setTimeout(resolve, 3000));
setHasSession(true);
const sessionExists = true;
if (sessionExists) {
setStep(step + 1);
}
}
} catch (error) {
console.error("Login failed:", error);
} finally {
setIsLoading(false);
}
} else if (step < totalSteps) {
setStep(step + 1);
}
};
const handleBack = () => {
if (step > 1) setStep(step - 1);
};
// Progress bar rendering
const renderProgress = () => (
<div className="flex items-center justify-center gap-2 mb-2">
{[...Array(totalSteps)].map((_, idx) => (
<div
key={idx}
className={`h-1.5 rounded transition-all duration-300 w-16 ${
idx < step ? "bg-primary" : "bg-white/30"
}`}
/>
))}
</div>
);
// Step content rendering
const renderStepContent = () => {
switch (step) {
case 1:
return (
<div className="flex flex-col items-center justify-center h-full">
<h2 className="text-3xl font-bold mb-2 text-center">
Welcome to ArtCraft
</h2>
<p className="text-white/70 mb-6 text-center">
Here's what you can do...
</p>
<div className="grid grid-cols-2 gap-6 h-full grow">
<div className="h-full">
<div className="aspect-[4/3] w-full overflow-hidden bg-black/20 rounded-t-lg">
<video
className="object-cover w-full h-full"
autoPlay
muted
loop
controls={false}
>
<source src={videoSrc2D} type="video/mp4" />
</video>
</div>
<p className="text-center px-1.5 py-2 text-white/90 bg-black/20 rounded-b-lg font-medium text-sm">
2D Canvas
</p>
</div>
<div>
<div className="aspect-[4/3] w-full overflow-hidden bg-black/20 rounded-t-lg">
<video
className="object-cover w-full h-full"
autoPlay
muted
loop
controls={false}
>
<source src={videoSrc3D} type="video/mp4" />
</video>
</div>
<p className="text-center px-1.5 py-2 text-white/90 bg-black/20 rounded-b-lg font-medium text-sm">
3D Scene Editor
</p>
</div>
</div>
</div>
);
case 2:
return (
<div className="flex flex-col items-center justify-center h-full">
<h2 className="text-3xl font-bold mb-2 text-center">
Login with your OpenAI account
</h2>
<p className="text-white/70 mb-6 text-center px-16">
We're the application layer for images and video. To provide you
with <b>100% free service</b>, we use your existing AI accounts.
To start off, you'll need to add your OpenAI Sora account. If you
have a ChatGPT subscription, you can log in with Sora.
</p>
<img
src={openAiLogo}
alt="OpenAI Logo"
className="w-72 h-72 mx-auto grow"
/>
</div>
);
case 3:
return (
<div className="flex flex-col items-center justify-center h-full">
<h2 className="text-3xl font-bold mb-2 text-center">
Thank you for signing in!
</h2>
<p className="text-white/70 mb-6 text-center px-24">
You're all set to start creating amazing content. Join our Discord
community to connect with other creators, share your work, and get
the latest updates.
</p>
<Button
variant="secondary"
onClick={() =>
window.open("https://discord.gg/75svZP2Vje", "_blank")
}
icon={faDiscord}
className="text-md bg-[#5865F2] hover:bg-[#6a76ff] rounded-xl"
>
Join our Discord
</Button>
</div>
);
default:
return null;
}
};
return createPortal(
<Transition appear show={isOpen}>
<div className="fixed inset-0 z-[100]">
<TransitionChild
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 cursor-pointer bg-black/80" />
</TransitionChild>
<div className="fixed inset-0 flex items-center justify-center p-4">
<TransitionChild
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<div
className="relative h-[660px] max-w-4xl w-full rounded-xl bg-[#2C2C2C] text-white shadow-lg border border-white/5"
onClick={(e) => e.stopPropagation()}
>
<div className="flex flex-col gap-4 p-8 h-full">
<span className="text-sm text-center opacity-60 font-medium">
Step {step} of {totalSteps}
</span>
{renderProgress()}
{renderStepContent()}
<div className="flex items-end justify-center gap-2.5 mt-10 grow">
{step > 1 && step !== 3 ? (
<Button
variant="secondary"
onClick={handleBack}
disabled={isLoading}
>
Back
</Button>
) : null}
{step < totalSteps ? (
<Button
icon={step === 2 ? faRightToBracket : faArrowRight}
iconFlip={step !== 2}
onClick={handleNext}
loading={isLoading}
disabled={isLoading}
>
{step === 2 ? "Login with OpenAI" : "Continue"}
</Button>
) : (
<Button
icon={faArrowRight}
iconFlip={true}
onClick={handleClose}
>
Start Creating Now
</Button>
)}
</div>
</div>
</div>
</TransitionChild>
</div>
</div>
</Transition>,
document.body
);
}
export default LoginModal;
@@ -0,0 +1,13 @@
{
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../../tsconfig.base.json"
}
@@ -0,0 +1,45 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"types": [
"node",
"@nx/react/typings/cssmodule.d.ts",
"@nx/react/typings/image.d.ts",
"vite/client"
],
"rootDir": "src",
"jsx": "react-jsx",
"tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo"
},
"exclude": [
"out-tsc",
"dist",
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx",
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx"
],
"include": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.ts",
"src/**/*.tsx"
]
}
@@ -0,0 +1,34 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./out-tsc/vitest",
"types": [
"vitest/globals",
"vitest/importMeta",
"vite/client",
"node",
"vitest"
],
"jsx": "react-jsx"
},
"include": [
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}
@@ -0,0 +1,49 @@
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import dts from 'vite-plugin-dts';
import * as path from 'path';
export default defineConfig(() => ({
root: __dirname,
cacheDir: '../../../node_modules/.vite/libs/components/login-modal',
plugins: [react(), dts({ entryRoot: 'src', tsconfigPath: path.join(__dirname, 'tsconfig.lib.json') })],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
// Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode
build: {
outDir: './dist',
emptyOutDir: true,
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
lib: {
// Could also be a dictionary or array of multiple entry points.
entry: 'src/index.ts',
name: '@storyteller/ui-login-modal',
fileName: 'index',
// Change this to the formats you want to support.
// Don't forget to update your package.json as well.
formats: ['es' as const]
},
rollupOptions: {
// External packages that should not be bundled into your library.
external: ['react','react-dom','react/jsx-runtime']
},
},
test: {
watch: false,
globals: true,
environment: 'jsdom',
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
reporters: ['default'],
coverage: {
reportsDirectory: './test-output/vitest/coverage',
provider: 'v8' as const,
}
},
}));
@@ -13,5 +13,8 @@
"import": "./dist/index.js",
"default": "./dist/index.js"
}
},
"dependencies": {
"@tauri-apps/plugin-dialog": "^2.2.1"
}
}
@@ -0,0 +1,234 @@
import { useEffect, useState } from "react";
import { SoundRegistry, SoundEffect } from "@storyteller/soundboard";
import { Button } from "@storyteller/ui-button";
import { Input } from "@storyteller/ui-input";
import { AppPreferencesPayload, CustomDirectory, GetAppPreferences, SystemDirectory } from "@storyteller/tauri-api";
import { PreferenceName, UpdateAppPreferences } from "libs/tauri-api/src/lib/settings/UpdateAppPreference";
import { open } from '@tauri-apps/plugin-dialog';
import { Select, SelectValue } from "libs/components/select/src/lib/select";
// TODO: This is maintained in two places. Here and InstallSounds.
const SOUND_OPTIONS = [
{ value: "none", label: "None (Silent)" },
{ value: "flower", label: "Flower" },
{ value: "correct", label: "Correct" },
{ value: "next", label: "Next" },
{ value: "done", label: "Done" },
{ value: "crumble", label: "Crumble" },
];
interface MiscSettingsPaneProps {
}
export const MiscSettingsPane = (args: MiscSettingsPaneProps) => {
const [preferences, setPreferences] = useState<AppPreferencesPayload|undefined>(undefined);
useEffect(() => {
const fetchData = async () => {
const prefs = await GetAppPreferences();
console.log("prefs", prefs)
setPreferences(prefs.preferences);
}
fetchData();
}, []);
// NB: This might be a complex type.
const outerDownloadObject = preferences?.preferred_download_directory || {};
const downloadDirectory = ("custom" in outerDownloadObject) ? outerDownloadObject.custom as string : "";
const currentDownloadLabel = ("system" in outerDownloadObject) ? "System Download Directory" : downloadDirectory;
const playSounds = preferences?.play_sounds || false;
const successSound = orNone(preferences?.generation_success_sound); // ? preferences?.generation_success_sound : "none";
const failureSound = orNone(preferences?.generation_failure_sound); // ? preferences?.generation_failure_sound : "none";
const enqueueSound = orNone(preferences?.generation_enqueue_sound); // ? preferences?.generation_enqueue_sound : "none";
const reloadPreferences = async () => {
const prefs = await GetAppPreferences();
console.log("prefs", prefs)
setPreferences(prefs.preferences);
}
const setPlaySounds = async (checked: boolean) => {
//const value = checked ? "true" : "false";
await UpdateAppPreferences({
preference: PreferenceName.PlaySounds,
value: checked,
});
await reloadPreferences();
}
const openDirectoryPicker = async () => {
let directory = await open({
multiple: false,
directory: true,
defaultPath: downloadDirectory || undefined,
});
if (directory === null) {
return; // User dismissed the dialog choice
}
await UpdateAppPreferences({
preference: PreferenceName.PreferredDownloadDirectory,
value: {
custom: directory
} as CustomDirectory,
});
await reloadPreferences();
}
const clearDirectory = async () => {
await UpdateAppPreferences({
preference: PreferenceName.PreferredDownloadDirectory,
value: {
system: "downloads"
} as SystemDirectory,
});
await reloadPreferences();
}
const setSuccessSound = async (val: string) => {
let sendVal = val === "none" ? undefined : val;
await UpdateAppPreferences({
preference: PreferenceName.GenerationSuccessSound,
value: sendVal,
});
SoundRegistry.getInstance().playSound(val);
await reloadPreferences();
}
const setFailureSound = async (val: string) => {
let sendVal = val === "none" ? undefined : val;
await UpdateAppPreferences({
preference: PreferenceName.GenerationFailureSound,
value: sendVal,
});
SoundRegistry.getInstance().playSound(val);
await reloadPreferences();
}
const setEnqueueSound = async (val: string) => {
let sendVal = val === "none" ? undefined : val;
await UpdateAppPreferences({
preference: PreferenceName.GenerationEnqueueSound,
value: sendVal,
});
SoundRegistry.getInstance().playSound(val);
await reloadPreferences();
}
const playSound = (val?: string) => {
if (val !== undefined && val !== "none") {
SoundRegistry.getInstance().playSound(val);
}
}
return (<>
<div className="space-y-4">
<div>
<label htmlFor="download-path" className="mb-2 block">
Default Download Directory
</label>
This is where downloads are placed after downloading.
The current path is <pre>{currentDownloadLabel}</pre>
<Button
variant="primary"
className="py-1"
onClick={openDirectoryPicker}
>
Choose Directory
</Button>
<Button
variant="secondary"
className="py-1"
onClick={clearDirectory}
>
Use Default
</Button>
</div>
<div>
<label htmlFor="play-sounds" className="mb-2 block">
Play Notification Sounds for Events?
</label>
<Input
id="play-sounds"
type="checkbox"
checked={playSounds}
onChange={(e) => setPlaySounds((e.target as any).checked)}
/>
</div>
<div>
<label htmlFor="success-sound" className="mb-2 block">
Success Sound
</label>
<Select
id="success-sound"
value={successSound}
onChange={(val: SelectValue) =>
setSuccessSound(val as string)
}
options={SOUND_OPTIONS}
/>
<Button
variant="secondary"
className="py-1"
onClick={() => playSound(successSound)}
>
Play
</Button>
</div>
<div>
<label htmlFor="failure-sound" className="mb-2 block">
Failure Sound
</label>
<Select
id="failure-sound"
value={failureSound}
onChange={(val: SelectValue) =>
setFailureSound(val as string)
}
options={SOUND_OPTIONS}
/>
<Button
variant="secondary"
className="py-1"
onClick={() => playSound(failureSound)}
>
Play
</Button>
</div>
<div>
<label htmlFor="enqueue-sound" className="mb-2 block">
Enqueue Sound
</label>
<Select
id="enqueue-sound"
value={enqueueSound}
onChange={(val: SelectValue) =>
setEnqueueSound(val as string)
}
options={SOUND_OPTIONS}
/>
<Button
variant="secondary"
className="py-1"
onClick={() => playSound(enqueueSound)}
>
Play
</Button>
</div>
</div>
</>)
}
const orNone = (val: string | undefined | null) : string => {
if (!!!val) {
return "none";
}
return val;
}
@@ -11,6 +11,7 @@ import {
import { twMerge } from "tailwind-merge";
import { Input } from "@storyteller/ui-input";
import { Select, SelectValue } from "@storyteller/ui-select";
import { MiscSettingsPane } from "./MiscSettingsPane";
interface SettingsModalProps {
isOpen: boolean;
@@ -23,7 +24,7 @@ interface AccountInfo {
credits: number;
}
type SettingsSection = "accounts" | "video" | "image" | "misc";
type SettingsSection = "misc" | "accounts" | "video" | "image";
export const SettingsModal = ({ isOpen, onClose }: SettingsModalProps) => {
const [accountInfo] = useState<AccountInfo>({
@@ -33,17 +34,18 @@ export const SettingsModal = ({ isOpen, onClose }: SettingsModalProps) => {
});
const [selectedSection, setSelectedSection] =
useState<SettingsSection>("accounts");
useState<SettingsSection>("misc");
const [palApiKey, setPalApiKey] = useState("");
const [klingApiKey, setKlingApiKey] = useState("");
const [defaultVideoModel, setDefaultVideoModel] = useState("veo");
const [humanVideoProvider, setHumanVideoProvider] = useState("artcraft");
const sections = [
{ id: "misc" as const, label: "Misc", icon: faCog },
{ id: "accounts" as const, label: "Accounts", icon: faUser },
{ id: "video" as const, label: "Video", icon: faVideo },
{ id: "image" as const, label: "Image", icon: faImage },
{ id: "misc" as const, label: "Misc", icon: faCog },
];
const renderContent = () => {
@@ -157,15 +159,13 @@ export const SettingsModal = ({ isOpen, onClose }: SettingsModalProps) => {
case "image":
return (
<div>
<button className="text-blue-600">Various Image Settings...</button>
<button className="text-blue-600">Various Image Settings (TODO)...</button>
</div>
);
case "misc":
return (
<div>
<button className="text-blue-600">Other Settings...</button>
</div>
<MiscSettingsPane />
);
}
};
@@ -2,9 +2,15 @@
"files": [],
"include": [],
"references": [
{
"path": "../modal"
},
{
"path": "../select"
},
{
"path": "../../tauri-api"
},
{
"path": "../input"
},
@@ -12,7 +18,7 @@
"path": "../button"
},
{
"path": "../modal"
"path": "../../soundboard"
},
{
"path": "./tsconfig.lib.json"
@@ -43,9 +43,15 @@
"src/**/*.tsx"
],
"references": [
{
"path": "../modal/tsconfig.lib.json"
},
{
"path": "../select/tsconfig.lib.json"
},
{
"path": "../../tauri-api/tsconfig.lib.json"
},
{
"path": "../input/tsconfig.lib.json"
},
@@ -53,7 +59,7 @@
"path": "../button/tsconfig.lib.json"
},
{
"path": "../modal/tsconfig.lib.json"
"path": "../../soundboard/tsconfig.lib.json"
}
]
}
@@ -82,6 +82,7 @@ export const TabSelector: React.FC<TabSelectorProps> = ({
className={({ selected }) =>
twMerge(
"relative z-20 mx-0.5 min-w-max rounded-md border-2 border-transparent px-4 py-0.5 text-center text-sm font-semibold transition-all duration-200 ease-in-out",
"focus-visible:outline-none focus-visible:ring-0",
selected ? "text-white" : "text-gray-300 hover:text-white",
disabled ? "cursor-not-allowed opacity-60" : ""
)
+12
View File
@@ -0,0 +1,12 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
+36
View File
@@ -0,0 +1,36 @@
{
"extends": [
"plugin:@nx/react",
"../../.eslintrc.json"
],
"ignorePatterns": [
"!**/*",
"**/vite.config.*.timestamp*",
"**/vitest.config.*.timestamp*"
],
"overrides": [
{
"files": [
"*.ts",
"*.tsx",
"*.js",
"*.jsx"
],
"rules": {}
},
{
"files": [
"*.ts",
"*.tsx"
],
"rules": {}
},
{
"files": [
"*.js",
"*.jsx"
],
"rules": {}
}
]
}
+7
View File
@@ -0,0 +1,7 @@
# @frontend/tauri-api
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test @frontend/tauri-api` to execute the unit tests via [Vitest](https://vitest.dev/).
+20
View File
@@ -0,0 +1,20 @@
{
"name": "@storyteller/tauri-api",
"version": "0.0.1",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"development": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
},
"dependencies": {
"@tauri-apps/api": "^2.4.0"
}
}
+2
View File
@@ -0,0 +1,2 @@
export * from './lib/settings/GetAppPreferences.js';
@@ -0,0 +1,36 @@
import { invoke } from "@tauri-apps/api/core";
export interface GetAppPreferencesResult {
preferences: AppPreferencesPayload,
}
export interface AppPreferencesPayload {
// Preferred download directory
preferred_download_directory: PreferredDownloadDirectory,
// Play sounds on events.
play_sounds: boolean,
// Sound names to play.
// These are keys, not filenames, and are defined by the frontend.
generation_success_sound?: string,
generation_failure_sound?: string,
generation_enqueue_sound?: string,
}
export type PreferredDownloadDirectory = SystemDirectory | CustomDirectory;
export interface SystemDirectory {
// If the directory is a system directory.
system: string,
}
export interface CustomDirectory {
// If the directory is a custom user directory.
custom: string,
}
export const GetAppPreferences = async () : Promise<GetAppPreferencesResult> => {
let result = await invoke("get_app_preferences_command");
return (result as GetAppPreferencesResult);
}
@@ -0,0 +1,30 @@
import { invoke } from "@tauri-apps/api/core";
import { PreferredDownloadDirectory } from "./GetAppPreferences";
export interface UpdateAppPreferencesRequest {
preference: PreferenceName,
value: undefined | string | boolean | PreferredDownloadDirectory,
}
export enum PreferenceName {
PreferredDownloadDirectory = "preferred_download_directory",
PlaySounds = "play_sounds",
GenerationSuccessSound = "generation_success_sound",
GenerationFailureSound = "generation_failure_sound",
GenerationEnqueueSound = "generation_enqueue_sound",
}
export interface UpdateAppPreferencesResult {
success: boolean
}
export const UpdateAppPreferences = async (request: UpdateAppPreferencesRequest) : Promise<UpdateAppPreferencesResult> => {
let result = await invoke("update_app_preferences_command", {
request: {
preference: request.preference,
value: request.value,
}
});
return (result as UpdateAppPreferencesResult);
}
+1
View File
@@ -0,0 +1 @@
// This file should be used to export ONLY server-code from the library.
@@ -0,0 +1,3 @@
import { installGlobals } from '@remix-run/node';
import "@testing-library/jest-dom/matchers";
installGlobals();
+13
View File
@@ -0,0 +1,13 @@
{
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../tsconfig.base.json"
}
+48
View File
@@ -0,0 +1,48 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"types": [
"node",
"@nx/react/typings/cssmodule.d.ts",
"@nx/react/typings/image.d.ts",
"vite/client"
],
"rootDir": "src",
"jsx": "react-jsx",
"tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo"
},
"exclude": [
"out-tsc",
"dist",
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx",
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"eslint.config.js",
"eslint.config.cjs",
"eslint.config.mjs"
],
"include": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.ts",
"src/**/*.tsx"
]
}
@@ -0,0 +1,34 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./out-tsc/vitest",
"types": [
"vitest/globals",
"vitest/importMeta",
"vite/client",
"node",
"vitest"
],
"jsx": "react-jsx"
},
"include": [
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}
+49
View File
@@ -0,0 +1,49 @@
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import dts from 'vite-plugin-dts';
import * as path from 'path';
export default defineConfig(() => ({
root: __dirname,
cacheDir: '../../node_modules/.vite/libs/tauri-api',
plugins: [react(), dts({ entryRoot: 'src', tsconfigPath: path.join(__dirname, 'tsconfig.lib.json') })],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
// Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode
build: {
outDir: './dist',
emptyOutDir: true,
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
lib: {
// Could also be a dictionary or array of multiple entry points.
entry: 'src/index.ts',
name: '@frontend/tauri-api',
fileName: 'index',
// Change this to the formats you want to support.
// Don't forget to update your package.json as well.
formats: ['es' as const]
},
rollupOptions: {
// External packages that should not be bundled into your library.
external: ['react','react-dom','react/jsx-runtime']
},
},
test: {setupFiles: ['./src/test-setup.ts'],
watch: false,
globals: true,
environment: 'jsdom',
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
reporters: ['default'],
coverage: {
reportsDirectory: './test-output/vitest/coverage',
provider: 'v8' as const,
}
},
}));
+3
View File
@@ -13,5 +13,8 @@
"import": "./dist/index.js",
"default": "./dist/index.js"
}
},
"dependencies": {
"@tauri-apps/plugin-http": "^2.4.3"
}
}
@@ -0,0 +1,2 @@
export const TEST = 1;
@@ -0,0 +1,2 @@
export * from './GetAppPreferences';

Some files were not shown because too many files have changed in this diff Show More