mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-03 00:05:41 -05:00
Compare commits
32
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95a6e9f19d | ||
|
|
7433096a53 | ||
|
|
d43ee0f6c9 | ||
|
|
e76aea2c12 | ||
|
|
0e111c4233 | ||
|
|
1b71199bf2 | ||
|
|
bc2e44efdf | ||
|
|
eec59c218d | ||
|
|
5c1e72f8f4 | ||
|
|
d91243b1f5 | ||
|
|
83cecad280 | ||
|
|
06313f54a5 | ||
|
|
ef3ca88054 | ||
|
|
cfcf7ab5aa | ||
|
|
4cc215495a | ||
|
|
f1d14ab73f | ||
|
|
5cb8f5b93a | ||
|
|
9decf8bf88 | ||
|
|
63a7f99c53 | ||
|
|
9dd16f2e80 | ||
|
|
abe2d2a093 | ||
|
|
552d5553c0 | ||
|
|
0d33358ead | ||
|
|
908ff43be3 | ||
|
|
196e1c4615 | ||
|
|
8f321f06d0 | ||
|
|
0b338f3886 | ||
|
|
de76fc6c36 | ||
|
|
9fb0ca2dab | ||
|
|
42965dfcfa | ||
|
|
8c7b358534 | ||
|
|
d8851abf24 |
Generated
+594
-63
@@ -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",
|
||||
]
|
||||
|
||||
@@ -25,7 +25,6 @@ members = [
|
||||
|
||||
# Desktop apps
|
||||
"crates/desktop/artcraft",
|
||||
"crates/desktop/tauri-realtime",
|
||||
|
||||
# Shared libraries
|
||||
"crates/lib/actix_cors_configs",
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
+50
-4
@@ -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)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
use crate::commands::app_preferences::get_app_preferences_command::AppPreferencesPayload;
|
||||
use crate::state::app_preferences::app_preferences_manager::AppPreferencesManager;
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::state::sora::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::windows::sora_login_window::open_sora_login_window::open_sora_login_window;
|
||||
use errors::AnyhowResult;
|
||||
use log::{error, info};
|
||||
use once_cell::sync::Lazy;
|
||||
use openai_sora_client::creds::sora_credential_set::SoraCredentialSet;
|
||||
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
|
||||
use openai_sora_client::requests::list_media::list_media::list_media;
|
||||
use serde_derive::Serialize;
|
||||
use tauri::{AppHandle, State};
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct CheckSoraSessionCommand {
|
||||
pub state: SoraSessionState,
|
||||
pub maybe_account_email: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SoraSessionState{
|
||||
#[serde(rename = "not_set_up")]
|
||||
NotSetUp,
|
||||
#[serde(rename = "expired_or_error")]
|
||||
ExpiredOrError,
|
||||
#[serde(rename = "valid")]
|
||||
Valid,
|
||||
}
|
||||
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn check_sora_session_command(
|
||||
app: AppHandle,
|
||||
sora_creds_manager: State<'_, SoraCredentialManager>,
|
||||
) -> Result<CheckSoraSessionCommand, CheckSoraSessionCommand> {
|
||||
info!("check_sora_session_command called");
|
||||
|
||||
match do_check(&sora_creds_manager).await {
|
||||
Ok(state) => Ok(state),
|
||||
Err(err) => {
|
||||
error!("Error checking session state: {:?}", err);
|
||||
Err(CheckSoraSessionCommand {
|
||||
state: SoraSessionState::ExpiredOrError,
|
||||
maybe_account_email: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn do_check(
|
||||
sora_creds_manager: &SoraCredentialManager,
|
||||
) -> AnyhowResult<CheckSoraSessionCommand> {
|
||||
|
||||
let mut creds = match sora_creds_manager.get_credentials()? {
|
||||
Some(creds) => creds,
|
||||
None => {
|
||||
return Ok(CheckSoraSessionCommand {
|
||||
state: SoraSessionState::NotSetUp,
|
||||
maybe_account_email: None,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let upgraded = maybe_upgrade_or_renew_session(&mut creds).await?;
|
||||
|
||||
list_media(&creds).await?;
|
||||
|
||||
if upgraded {
|
||||
info!("Saving upgraded session to manager and disk...");
|
||||
sora_creds_manager.set_credentials(&creds)?;
|
||||
sora_creds_manager.persist_all_to_disk()?;
|
||||
}
|
||||
|
||||
let maybe_account_email = creds
|
||||
.jwt_bearer_token
|
||||
.map(|token| token.jwt_claims().email.clone());
|
||||
|
||||
Ok(CheckSoraSessionCommand {
|
||||
state: SoraSessionState::Valid,
|
||||
maybe_account_email,
|
||||
})
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod check_sora_session_command;
|
||||
pub mod open_sora_login_command;
|
||||
pub mod sora_image_generation_command;
|
||||
pub mod sora_image_remix_command;
|
||||
|
||||
@@ -1,31 +1,21 @@
|
||||
use crate::threads::sora_session_login_thread::{LOGIN_WINDOW_NAME, SORA_LOGIN_URL};
|
||||
use crate::utils::clear_all_webview_cookies::clear_all_webview_cookies;
|
||||
use crate::state::app_preferences::app_preferences_manager::AppPreferencesManager;
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::state::sora::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::windows::sora_login_window::open_sora_login_window::open_sora_login_window;
|
||||
use errors::AnyhowResult;
|
||||
use log::{error, info};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::time::Duration;
|
||||
use tauri::{AppHandle, Manager, Url, WebviewUrl, WebviewWindowBuilder, WindowBuilder};
|
||||
use tokio::time::sleep_until;
|
||||
|
||||
pub const START_URL_STR: &str = "https://storyteller.ai/";
|
||||
|
||||
pub static START_URL: Lazy<Url> = Lazy::new(|| {
|
||||
Url::parse(START_URL_STR).expect("URL should parse")
|
||||
});
|
||||
|
||||
pub const SORA_HOMEPAGE_URL_STR: &str = "https://sora.com/";
|
||||
|
||||
pub static SORA_HOMEPAGE_URL : Lazy<Url> = Lazy::new(|| {
|
||||
Url::parse(SORA_HOMEPAGE_URL_STR).expect("URL should parse")
|
||||
});
|
||||
use tauri::{AppHandle, State};
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn open_sora_login_command(
|
||||
app: AppHandle,
|
||||
app_data_root: State<'_, AppDataRoot>,
|
||||
sora_creds_manager: State<'_, SoraCredentialManager>,
|
||||
) -> Result<String, String> {
|
||||
info!("open_login_command called");
|
||||
|
||||
do_open_login(&app)
|
||||
do_open_login(&app, &app_data_root, &sora_creds_manager)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error opening login: {:?}", err);
|
||||
@@ -35,37 +25,14 @@ pub async fn open_sora_login_command(
|
||||
Ok("result".to_string())
|
||||
}
|
||||
|
||||
async fn do_open_login(app: &AppHandle) -> AnyhowResult<()> {
|
||||
async fn do_open_login(
|
||||
app: &AppHandle,
|
||||
app_data_root: &AppDataRoot,
|
||||
sora_creds_manager: &SoraCredentialManager,
|
||||
) -> AnyhowResult<()> {
|
||||
info!("Building login window...");
|
||||
|
||||
let url = WebviewUrl::External(START_URL.clone());
|
||||
|
||||
let window = WebviewWindowBuilder::new(app, LOGIN_WINDOW_NAME, url)
|
||||
//.user_agent(openai_sora_client::credentials::USER_AGENT)
|
||||
.always_on_top(true)
|
||||
.title("Login to OpenAI")
|
||||
.center()
|
||||
.resizable(true)
|
||||
.visible(true)
|
||||
.closable(true)
|
||||
.focused(true)
|
||||
.devtools(true)
|
||||
.build()?;
|
||||
|
||||
for (name, webview) in window.webviews() {
|
||||
if name != LOGIN_WINDOW_NAME {
|
||||
continue;
|
||||
}
|
||||
|
||||
clear_all_webview_cookies(&webview)?;
|
||||
|
||||
// NB: We're starting to get Cloudflare protection screens. Let's try to avoid.
|
||||
webview.navigate(SORA_HOMEPAGE_URL.clone())?;
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
|
||||
webview.navigate(SORA_LOGIN_URL.clone())?;
|
||||
break;
|
||||
}
|
||||
open_sora_login_window(app, app_data_root, sora_creds_manager).await?;
|
||||
|
||||
info!("Done.");
|
||||
Ok(())
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
use crate::events::sendable_event_trait::SendableEvent;
|
||||
use crate::events::sora::sora_image_enqueue_failure_event::SoraImageEnqueueFailureEvent;
|
||||
use crate::events::sora::sora_image_enqueue_success_event::SoraImageEnqueueSuccessEvent;
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::state::data_dir::trait_data_subdir::DataSubdir;
|
||||
use crate::state::sora::read_sora_credentials_from_disk::read_sora_credentials_from_disk;
|
||||
@@ -21,12 +24,12 @@ 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;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -48,7 +51,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 +63,27 @@ 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 event = SoraImageEnqueueFailureEvent {};
|
||||
let result = event.send(&app);
|
||||
|
||||
if let Err(err) = result {
|
||||
error!("Failed to emit event: {:?}", err);
|
||||
}
|
||||
|
||||
return Err("there was an error".to_string());
|
||||
}
|
||||
|
||||
let event = SoraImageEnqueueSuccessEvent {};
|
||||
let result = event.send(&app);
|
||||
|
||||
if let Err(err) = result {
|
||||
error!("Failed to emit event: {:?}", err);
|
||||
}
|
||||
|
||||
Ok("success".to_string())
|
||||
}
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
pub mod notification_event;
|
||||
pub mod notification_event;
|
||||
pub mod sendable_event_error;
|
||||
pub mod sendable_event_trait;
|
||||
pub mod sora;
|
||||
@@ -0,0 +1,31 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SendableEventError {
|
||||
TauriError(tauri::Error),
|
||||
AnyhowError(anyhow::Error),
|
||||
}
|
||||
|
||||
impl Error for SendableEventError {}
|
||||
|
||||
impl Display for SendableEventError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::TauriError(err) => write!(f, "SendableEventError::TauriError: {}", err),
|
||||
Self::AnyhowError(err) => write!(f, "SendableEventError::AnyhowError: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for SendableEventError {
|
||||
fn from(err: anyhow::Error) -> Self {
|
||||
SendableEventError::AnyhowError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tauri::Error> for SendableEventError {
|
||||
fn from(err: tauri::Error) -> Self {
|
||||
SendableEventError::TauriError(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
use log::info;
|
||||
use crate::events::sendable_event_error::SendableEventError;
|
||||
use serde::Serialize;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
|
||||
/// Implement this on a Serializable object to be able to send it as an event to the frontend.
|
||||
pub trait SendableEvent : Serialize {
|
||||
/// This is the name of the event that the frontend subscribes to.
|
||||
const FRONTEND_EVENT_NAME: &'static str;
|
||||
|
||||
/// Default implementation of send().
|
||||
/// This serializes and sends the event to the frontend.
|
||||
fn send(&self, app: &AppHandle) -> Result<(), SendableEventError> {
|
||||
info!("Sending event to frontend: {}", Self::FRONTEND_EVENT_NAME);
|
||||
let result = app.emit(Self::FRONTEND_EVENT_NAME, self);
|
||||
if let Err(err) = result {
|
||||
return Err(SendableEventError::from(err));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod sora_image_enqueue_failure_event;
|
||||
pub mod sora_image_enqueue_success_event;
|
||||
pub mod sora_image_generation_complete_event;
|
||||
pub mod sora_image_generation_failed_event;
|
||||
pub mod sora_login_success_event;
|
||||
@@ -0,0 +1,11 @@
|
||||
use crate::events::sendable_event_trait::SendableEvent;
|
||||
use serde_derive::Serialize;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct SoraImageEnqueueFailureEvent {
|
||||
// TODO: Reason.
|
||||
}
|
||||
|
||||
impl SendableEvent for SoraImageEnqueueFailureEvent {
|
||||
const FRONTEND_EVENT_NAME: &'static str = "sora-image-enqueue-failure";
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
use crate::events::sendable_event_trait::SendableEvent;
|
||||
use serde_derive::Serialize;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct SoraImageEnqueueSuccessEvent {
|
||||
// TODO: Token, etc.
|
||||
}
|
||||
|
||||
impl SendableEvent for SoraImageEnqueueSuccessEvent {
|
||||
const FRONTEND_EVENT_NAME: &'static str = "sora-image-enqueue-success";
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
use serde_derive::Serialize;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use crate::events::sendable_event_trait::SendableEvent;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct SoraImageGenerationCompleteEvent {
|
||||
pub media_file_token: MediaFileToken,
|
||||
}
|
||||
|
||||
impl SendableEvent for SoraImageGenerationCompleteEvent {
|
||||
const FRONTEND_EVENT_NAME: &'static str = "sora-image-generation-complete";
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
use serde_derive::Serialize;
|
||||
use crate::events::sendable_event_trait::SendableEvent;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct SoraImageGenerationFailedEvent {
|
||||
pub prompt: String,
|
||||
}
|
||||
|
||||
impl SendableEvent for SoraImageGenerationFailedEvent {
|
||||
const FRONTEND_EVENT_NAME: &'static str = "sora-image-generation-failed";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
use crate::events::sendable_event_trait::SendableEvent;
|
||||
use serde_derive::Serialize;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct SoraLoginSuccessEvent {
|
||||
// TODO: Reason.
|
||||
}
|
||||
|
||||
impl SendableEvent for SoraLoginSuccessEvent {
|
||||
const FRONTEND_EVENT_NAME: &'static str = "sora-login-success";
|
||||
}
|
||||
@@ -3,15 +3,19 @@ pub mod events;
|
||||
pub mod state;
|
||||
pub mod threads;
|
||||
pub mod utils;
|
||||
pub mod windows;
|
||||
|
||||
use tauri::Manager;
|
||||
|
||||
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::flip_image::flip_image;
|
||||
use crate::commands::platform_info_command::platform_info_command;
|
||||
use crate::commands::sora::check_sora_session_command::check_sora_session_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::app_preferences::app_preferences_manager::load_app_preferences_or_default;
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::state::main_window_size::MainWindowSize;
|
||||
use crate::state::sora::sora_credential_manager::SoraCredentialManager;
|
||||
@@ -19,11 +23,10 @@ use crate::state::sora::sora_task_queue::SoraTaskQueue;
|
||||
use crate::state::storyteller::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use crate::threads::discord_presence_thread::discord_presence_thread;
|
||||
use crate::threads::main_window_thread::main_window_thread::main_window_thread;
|
||||
use crate::threads::sora_session_login_thread::sora_session_login_thread;
|
||||
use crate::threads::sora_task_polling_thread::sora_task_polling_thread;
|
||||
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_dialog;
|
||||
use tauri_plugin_http;
|
||||
use tauri_plugin_log::Target;
|
||||
use tauri_plugin_log::TargetKind;
|
||||
@@ -54,6 +57,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()
|
||||
@@ -105,7 +109,6 @@ pub fn run() {
|
||||
}
|
||||
}
|
||||
|
||||
tauri::async_runtime::spawn(sora_session_login_thread(app_2, app_data_root_2, sora_creds_manager_2));
|
||||
tauri::async_runtime::spawn(main_window_thread(app_3, app_data_root_3, storyteller_creds_manager_2));
|
||||
tauri::async_runtime::spawn(sora_task_polling_thread(app_4, app_data_root_4, sora_creds_manager_3, storyteller_creds_manager_3, sora_task_queue_2));
|
||||
tauri::async_runtime::spawn(discord_presence_thread());
|
||||
@@ -117,12 +120,14 @@ pub fn run() {
|
||||
.manage(sora_creds_manager)
|
||||
.manage(sora_task_queue)
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
check_sora_session_command,
|
||||
flip_image,
|
||||
get_app_preferences_command,
|
||||
open_sora_login_command,
|
||||
platform_info_command,
|
||||
sora_image_generation_command,
|
||||
sora_image_remix_command,
|
||||
update_app_preferences_command,
|
||||
])
|
||||
.run(tauri::generate_context!("tauri.conf.json"))
|
||||
.expect("error while running tauri application");
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::anyhow;
|
||||
use discord_rich_presence::{DiscordIpc, DiscordIpcClient};
|
||||
use errors::AnyhowResult;
|
||||
use log::info;
|
||||
use log::{debug, info};
|
||||
|
||||
/// Our Discord App ID. Not sure if this is a secret.
|
||||
const DISCORD_APP_ID : &str = "1366596912593113138";
|
||||
@@ -15,13 +15,13 @@ pub async fn discord_presence_thread() -> ! {
|
||||
}
|
||||
|
||||
async fn discord_main_loop(mut client: DiscordIpcClient) -> AnyhowResult<()> {
|
||||
info!("Connecting Discord IPC client");
|
||||
info!("(Re)-connecting Discord IPC client...");
|
||||
|
||||
client.connect()
|
||||
.map_err(|err| anyhow!("Error connecting to Discord IPC: {:?}", err))?;
|
||||
|
||||
loop {
|
||||
info!("Notifying discord...");
|
||||
debug!("Notifying discord presence API...");
|
||||
|
||||
let assets = discord_rich_presence::activity::Assets::new()
|
||||
.large_image("https://storyteller.ai/android-chrome-512x512.png")
|
||||
@@ -37,6 +37,6 @@ async fn discord_main_loop(mut client: DiscordIpcClient) -> AnyhowResult<()> {
|
||||
.assets(assets))
|
||||
.map_err(|err| anyhow!("Error setting Discord activity: {:?}", err))?;
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_millis(30_000)).await;
|
||||
tokio::time::sleep(std::time::Duration::from_millis(60_000)).await;
|
||||
}
|
||||
}
|
||||
-1
@@ -1,7 +1,6 @@
|
||||
use crate::state::app_startup_time::AppStartupTime;
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::state::storyteller::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use crate::threads::sora_session_login_thread::LOGIN_WINDOW_NAME;
|
||||
use anyhow::anyhow;
|
||||
use chrono::TimeDelta;
|
||||
use errors::AnyhowResult;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
pub mod discord_presence_thread;
|
||||
pub mod main_window_thread;
|
||||
pub mod sora_session_login_thread;
|
||||
pub mod sora_task_polling_thread;
|
||||
@@ -1,178 +0,0 @@
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::state::sora::sora_credential_holder::SoraCredentialHolder;
|
||||
use crate::state::sora::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::utils::cookies::sora_webview_cookies::get_all_sora_cookies_as_string;
|
||||
use crate::utils::sora::initialize_sora_jwt_bearer_token::initialize_sora_jwt_bearer_token;
|
||||
use anyhow::anyhow;
|
||||
use chrono::{DateTime, NaiveDateTime, TimeDelta};
|
||||
use errors::AnyhowResult;
|
||||
use log::{error, info, warn};
|
||||
use once_cell::sync::Lazy;
|
||||
use openai_sora_client::creds::sora_credential_set::SoraCredentialSet;
|
||||
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
|
||||
use openai_sora_client::utils::has_session_cookie::{has_session_cookie, SessionCookiePresence};
|
||||
use reqwest::Url;
|
||||
use std::fs;
|
||||
use std::fs::{read_to_string, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::ops::Sub;
|
||||
use tauri::{AppHandle, Manager, Webview};
|
||||
|
||||
pub const LOGIN_WINDOW_NAME: &str = "login_window";
|
||||
|
||||
pub const SORA_LOGIN_URL_STR: &str = "https://sora.com/auth/login?callback_path=/";
|
||||
|
||||
pub static SORA_LOGIN_URL: Lazy<Url> = Lazy::new(|| {
|
||||
Url::parse(SORA_LOGIN_URL_STR).expect("URL should parse")
|
||||
});
|
||||
|
||||
pub const SORA_ROOT_URL_STR: &str = "https://sora.com/";
|
||||
|
||||
pub static SORA_ROOT_URL: Lazy<Url> = Lazy::new(|| {
|
||||
Url::parse(SORA_ROOT_URL_STR).expect("URL should parse")
|
||||
});
|
||||
|
||||
pub async fn sora_session_login_thread(
|
||||
app: AppHandle,
|
||||
app_data_root: AppDataRoot,
|
||||
sora_creds_manager: SoraCredentialManager
|
||||
) -> ! {
|
||||
loop {
|
||||
for (window_name, webview) in app.webviews() {
|
||||
if window_name == LOGIN_WINDOW_NAME {
|
||||
let result = check_login_window(
|
||||
&webview,
|
||||
&app_data_root,
|
||||
&sora_creds_manager,
|
||||
).await;
|
||||
if let Err(err) = result {
|
||||
error!("Error checking login window: {:?}", err);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(2_000)).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_login_window(
|
||||
webview: &Webview,
|
||||
app_data_root: &AppDataRoot,
|
||||
sora_credential_manager: &SoraCredentialManager,
|
||||
) -> AnyhowResult<()> {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
clear_browsing_data_on_test_domain(webview)?;
|
||||
//keep_on_task(webview)?;
|
||||
extract_cookies_to_file(webview, app_data_root, sora_credential_manager).await?;
|
||||
//initialize_sora_jwt_bearer_token(app_data_root).await?; // TODO: This only runs once. We need better management.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn keep_on_task(webview: &Webview) -> AnyhowResult<()> {
|
||||
let url = webview.url()?;
|
||||
let hostname= url.host()
|
||||
.ok_or(anyhow!("no host in url"))?
|
||||
.to_string();
|
||||
match hostname.as_str() {
|
||||
"auth.openai.com" => {},
|
||||
"openai.com" => {},
|
||||
"sora.com" => {},
|
||||
// Third party SSO
|
||||
"accounts.google.com" => {},
|
||||
"accounts.youtube.com" => {},
|
||||
"login.live.com" => {},
|
||||
"appleid.apple.com" => {},
|
||||
_ => {
|
||||
info!("Non login hostname: {}", hostname);
|
||||
webview.navigate(SORA_LOGIN_URL.clone())?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This is just so we have a way to clear browsing data.
|
||||
fn clear_browsing_data_on_test_domain(webview: &Webview) -> AnyhowResult<()> {
|
||||
let url = webview.url()?;
|
||||
let hostname= url.host()
|
||||
.ok_or(anyhow!("no host in url"))?
|
||||
.to_string();
|
||||
match hostname.as_str() {
|
||||
"storyteller.ai" => {
|
||||
info!("Clearing all browsing data...");
|
||||
webview.clear_all_browsing_data()?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO(bt,2025-04-07): Heuristic to detect when logged in. Only write when logged in.
|
||||
async fn extract_cookies_to_file(
|
||||
webview: &Webview,
|
||||
app_data_root: &AppDataRoot,
|
||||
sora_credential_manager: &SoraCredentialManager,
|
||||
) -> AnyhowResult<()> {
|
||||
let new_cookies = get_all_sora_cookies_as_string(webview)?.trim().to_string();
|
||||
|
||||
if new_cookies.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let session_cookie_presence = has_session_cookie(&new_cookies)
|
||||
.unwrap_or_else(|err| {
|
||||
error!("Failed to check for session cookie: {:?}", err);
|
||||
SessionCookiePresence::MaybePresent
|
||||
});
|
||||
|
||||
info!("Session cookies are: {:?}", session_cookie_presence);
|
||||
|
||||
let (should_write_cookies, should_upgrade_session) = match session_cookie_presence {
|
||||
SessionCookiePresence::Present => (true, true),
|
||||
SessionCookiePresence::MaybePresent => (true, false),
|
||||
SessionCookiePresence::Absent => (false, false),
|
||||
};
|
||||
|
||||
if !should_write_cookies {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// TODO(bt): Race conditions ahead.
|
||||
|
||||
sora_credential_manager.clear_credentials()?;
|
||||
|
||||
let _r = fs::remove_file(app_data_root.get_sora_cookie_file_path());
|
||||
let _r = fs::remove_file(app_data_root.get_sora_bearer_token_file_path());
|
||||
let _r = fs::remove_file(app_data_root.get_sora_sentinel_file_path());
|
||||
|
||||
let mut new_credentials =
|
||||
SoraCredentialSet::initialize_with_just_cookies_str(&new_cookies);
|
||||
|
||||
if should_upgrade_session {
|
||||
let _upgraded = maybe_upgrade_or_renew_session(&mut new_credentials).await?;
|
||||
}
|
||||
|
||||
sora_credential_manager.set_credentials(&new_credentials)?;
|
||||
sora_credential_manager.persist_all_to_disk()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read_sora_cookies_from_disk(app_data_root: &AppDataRoot) -> Option<String> {
|
||||
let cookie_file = app_data_root.get_sora_cookie_file_path();
|
||||
if !cookie_file.exists() {
|
||||
return None;
|
||||
}
|
||||
|
||||
match read_to_string(cookie_file) {
|
||||
Ok(contents) => Some(contents.trim().to_string()),
|
||||
Err(err) => {
|
||||
warn!("Failed to read cookie file: {:?}", err);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
use crate::events::sendable_event_trait::SendableEvent;
|
||||
use crate::events::sora::sora_image_generation_complete_event::SoraImageGenerationCompleteEvent;
|
||||
use crate::events::sora::sora_image_generation_failed_event::SoraImageGenerationFailedEvent;
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::state::data_dir::trait_data_subdir::DataSubdir;
|
||||
use crate::state::sora::sora_credential_manager::SoraCredentialManager;
|
||||
@@ -18,16 +21,6 @@ use tauri::{AppHandle, Emitter};
|
||||
use tempdir::TempDir;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct SoraImageGenerationComplete {
|
||||
pub media_file_token: MediaFileToken,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct SoraImageGenerationFailed {
|
||||
pub prompt: String,
|
||||
}
|
||||
|
||||
pub async fn sora_task_polling_thread(
|
||||
app_handle: AppHandle,
|
||||
app_data_root: AppDataRoot,
|
||||
@@ -102,9 +95,10 @@ async fn polling_loop(
|
||||
|
||||
for task in failed_tasks.iter() {
|
||||
if sora_task_queue.contains_key(&task.id)? {
|
||||
app_handle.emit("sora-image-generation-failed", SoraImageGenerationFailed {
|
||||
let event = SoraImageGenerationFailedEvent {
|
||||
prompt: task.prompt.clone(),
|
||||
})?;
|
||||
};
|
||||
event.send(&app_handle)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,9 +127,11 @@ async fn polling_loop(
|
||||
|
||||
info!("Uploaded to API backend: {:?}", result.media_file_token);
|
||||
|
||||
app_handle.emit("sora-image-generation-complete", SoraImageGenerationComplete {
|
||||
media_file_token: result.media_file_token
|
||||
})?;
|
||||
let event = SoraImageGenerationCompleteEvent {
|
||||
media_file_token: result.media_file_token,
|
||||
};
|
||||
|
||||
event.send(&app_handle)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +142,7 @@ async fn polling_loop(
|
||||
|
||||
sora_task_queue.remove_list(&succeeded_task_ids)?;
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_millis(2_000)).await;
|
||||
tokio::time::sleep(std::time::Duration::from_millis(6_000)).await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pub mod sora_webview_cookies;
|
||||
@@ -1,26 +0,0 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use reqwest::Url;
|
||||
use tauri::Webview;
|
||||
use tauri::webview::Cookie;
|
||||
use errors::AnyhowResult;
|
||||
|
||||
const SORA_ROOT_COOKIE_URL_STR: &str = "https://sora.com/";
|
||||
|
||||
static SORA_ROOT_COOKIE_URL: Lazy<Url> = Lazy::new(|| {
|
||||
Url::parse(SORA_ROOT_COOKIE_URL_STR).expect("URL should parse")
|
||||
});
|
||||
|
||||
pub fn get_all_sora_cookies(webview: &Webview) -> AnyhowResult<Vec<Cookie>> {
|
||||
let cookies = webview.cookies_for_url(SORA_ROOT_COOKIE_URL.clone())?;
|
||||
Ok(cookies)
|
||||
}
|
||||
|
||||
pub fn get_all_sora_cookies_as_string(webview: &Webview) -> AnyhowResult<String> {
|
||||
let cookies = get_all_sora_cookies(webview)?;
|
||||
let cookie_string = cookies
|
||||
.iter()
|
||||
.map(|cookie| format!("{}={}", cookie.name(), cookie.value()))
|
||||
.collect::<Vec<String>>()
|
||||
.join("; ");
|
||||
Ok(cookie_string)
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
pub mod clear_all_webview_cookies;
|
||||
pub mod cookies;
|
||||
pub mod get_url_file_extension;
|
||||
pub mod image;
|
||||
pub mod large_file_transfer;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pub mod sora_login_window;
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
use anyhow::anyhow;
|
||||
use log::info;
|
||||
use tauri::Webview;
|
||||
use errors::AnyhowResult;
|
||||
|
||||
// This is just so we have a way to clear browsing data while debugging / building.
|
||||
pub fn clear_browsing_data_on_test_domain(webview: &Webview) -> AnyhowResult<()> {
|
||||
let url = webview.url()?;
|
||||
let hostname= url.host()
|
||||
.ok_or(anyhow!("no host in url"))?
|
||||
.to_string();
|
||||
match hostname.as_str() {
|
||||
"storyteller.ai" => {
|
||||
info!("Clearing all browsing data...");
|
||||
webview.clear_all_browsing_data()?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
use errors::AnyhowResult;
|
||||
use once_cell::sync::Lazy;
|
||||
use reqwest::Url;
|
||||
use tauri::webview::Cookie;
|
||||
use tauri::WebviewWindow;
|
||||
|
||||
const CHAT_GPT_ROOT_COOKIE_URL_STR: &str = "https://chatgpt.com";
|
||||
|
||||
static CHAT_GPT_ROOT_COOKIE_URL: Lazy<Url> = Lazy::new(|| {
|
||||
Url::parse(CHAT_GPT_ROOT_COOKIE_URL_STR).expect("URL should parse")
|
||||
});
|
||||
|
||||
pub fn extract_sora_webview_cookies(webview: &WebviewWindow) -> AnyhowResult<String> {
|
||||
// NB(bt): Cookies were originally on sora.com, but now they're located on
|
||||
// the sora.chatgpt.com / *.chatgpt.com domains.
|
||||
let cookies = get_all_chatgpt_cookies(webview)?;
|
||||
let cookie_string = cookies
|
||||
.iter()
|
||||
.map(|cookie| format!("{}={}", cookie.name(), cookie.value()))
|
||||
.collect::<Vec<String>>()
|
||||
.join("; ");
|
||||
Ok(cookie_string)
|
||||
}
|
||||
|
||||
fn get_all_chatgpt_cookies(webview: &WebviewWindow) -> AnyhowResult<Vec<Cookie>> {
|
||||
let cookies = webview.cookies_for_url(CHAT_GPT_ROOT_COOKIE_URL.clone())?;
|
||||
Ok(cookies)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
use crate::windows::sora_login_window::open_sora_login_window::SORA_LOGIN_URL;
|
||||
use anyhow::anyhow;
|
||||
use errors::AnyhowResult;
|
||||
use log::info;
|
||||
use tauri::Webview;
|
||||
|
||||
// NB: Probably best not to use this.
|
||||
pub fn keep_on_login_domains(webview: &Webview) -> AnyhowResult<()> {
|
||||
let url = webview.url()?;
|
||||
let hostname= url.host()
|
||||
.ok_or(anyhow!("no host in url"))?
|
||||
.to_string();
|
||||
match hostname.as_str() {
|
||||
"auth.openai.com" => {},
|
||||
"openai.com" => {},
|
||||
"sora.com" => {},
|
||||
// Third party SSO
|
||||
"accounts.google.com" => {},
|
||||
"accounts.youtube.com" => {},
|
||||
"login.live.com" => {},
|
||||
"appleid.apple.com" => {},
|
||||
_ => {
|
||||
info!("Non login hostname: {}", hostname);
|
||||
webview.navigate(SORA_LOGIN_URL.clone())?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod clear_browsing_on_test_domain;
|
||||
pub mod extract_sora_webview_cookies;
|
||||
pub mod keep_on_login_domains;
|
||||
pub mod open_sora_login_window;
|
||||
pub mod sora_login_thread;
|
||||
@@ -0,0 +1,84 @@
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::state::sora::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::utils::clear_all_webview_cookies::clear_all_webview_cookies;
|
||||
use crate::windows::sora_login_window::sora_login_thread::sora_login_thread;
|
||||
use anyhow::anyhow;
|
||||
use errors::AnyhowResult;
|
||||
use once_cell::sync::Lazy;
|
||||
use reqwest::Url;
|
||||
use std::time::Duration;
|
||||
use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder};
|
||||
|
||||
/// Name of the window
|
||||
pub const LOGIN_WINDOW_NAME: &str = "login_window";
|
||||
|
||||
pub const START_URL_STR: &str = "https://google.com";
|
||||
|
||||
pub static START_URL: Lazy<Url> = Lazy::new(|| {
|
||||
Url::parse(START_URL_STR).expect("URL should parse")
|
||||
});
|
||||
|
||||
pub const SORA_HOMEPAGE_URL_STR: &str = "https://sora.com/";
|
||||
|
||||
pub static SORA_HOMEPAGE_URL : Lazy<Url> = Lazy::new(|| {
|
||||
Url::parse(SORA_HOMEPAGE_URL_STR).expect("URL should parse")
|
||||
});
|
||||
|
||||
pub const SORA_LOGIN_URL_STR: &str = "https://chatgpt.com/auth/login?next=%2Fsora%2F";
|
||||
|
||||
pub static SORA_LOGIN_URL: Lazy<Url> = Lazy::new(|| {
|
||||
Url::parse(SORA_LOGIN_URL_STR).expect("URL should parse")
|
||||
});
|
||||
|
||||
//pub const SORA_ROOT_URL_STR: &str = "https://sora.com/";
|
||||
//
|
||||
//pub static SORA_ROOT_URL: Lazy<Url> = Lazy::new(|| {
|
||||
// Url::parse(SORA_ROOT_URL_STR).expect("URL should parse")
|
||||
//});
|
||||
|
||||
pub async fn open_sora_login_window(
|
||||
app: &AppHandle,
|
||||
app_data_root: &AppDataRoot,
|
||||
sora_creds_manager: &SoraCredentialManager,
|
||||
) -> AnyhowResult<()> {
|
||||
if app.get_window(LOGIN_WINDOW_NAME).is_some() {
|
||||
return Err(anyhow!("Login window already open"));
|
||||
}
|
||||
|
||||
let url = WebviewUrl::External(START_URL.clone());
|
||||
|
||||
let window = WebviewWindowBuilder::new(app, LOGIN_WINDOW_NAME, url)
|
||||
//.user_agent(openai_sora_client::credentials::USER_AGENT)
|
||||
.always_on_top(true)
|
||||
.title("Login to OpenAI's Sora")
|
||||
.center()
|
||||
.resizable(true)
|
||||
.visible(true)
|
||||
.closable(true)
|
||||
.min_inner_size(200.0, 800.0)
|
||||
.focused(true)
|
||||
.devtools(true)
|
||||
.build()?;
|
||||
|
||||
let webview = window.get_webview(LOGIN_WINDOW_NAME)
|
||||
.ok_or_else(|| anyhow!("no webview found"))?;
|
||||
|
||||
clear_all_webview_cookies(&webview)?;
|
||||
|
||||
webview.navigate(SORA_HOMEPAGE_URL.clone())?;
|
||||
|
||||
// NB: We're starting to get Cloudflare protection screens. Let's try to avoid.
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
|
||||
webview.navigate(SORA_LOGIN_URL.clone())?;
|
||||
|
||||
let app_handle = app.clone();
|
||||
let app_data_root = app_data_root.clone();
|
||||
let sora_creds_manager = sora_creds_manager.clone();
|
||||
|
||||
let _ = tauri::async_runtime::spawn(async move {
|
||||
sora_login_thread(app_handle, app_data_root, sora_creds_manager).await;
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
use crate::events::sendable_event_trait::SendableEvent;
|
||||
use crate::events::sora::sora_login_success_event::SoraLoginSuccessEvent;
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::state::sora::sora_credential_holder::SoraCredentialHolder;
|
||||
use crate::state::sora::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::utils::sora::initialize_sora_jwt_bearer_token::initialize_sora_jwt_bearer_token;
|
||||
use crate::windows::sora_login_window::extract_sora_webview_cookies::extract_sora_webview_cookies;
|
||||
use crate::windows::sora_login_window::open_sora_login_window::LOGIN_WINDOW_NAME;
|
||||
use anyhow::anyhow;
|
||||
use chrono::{DateTime, NaiveDateTime, TimeDelta};
|
||||
use errors::AnyhowResult;
|
||||
use log::{error, info, warn};
|
||||
use once_cell::sync::Lazy;
|
||||
use openai_sora_client::creds::sora_credential_set::SoraCredentialSet;
|
||||
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
|
||||
use openai_sora_client::utils::has_session_cookie::{has_session_cookie, SessionCookiePresence};
|
||||
use reqwest::Url;
|
||||
use std::fs;
|
||||
use std::fs::{read_to_string, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::ops::Sub;
|
||||
use tauri::{AppHandle, Manager, Webview, WebviewWindow};
|
||||
|
||||
pub async fn sora_login_thread(
|
||||
app: AppHandle,
|
||||
app_data_root: AppDataRoot,
|
||||
sora_creds_manager: SoraCredentialManager
|
||||
) {
|
||||
loop {
|
||||
let login_webview_window = match app.get_webview_window(LOGIN_WINDOW_NAME) {
|
||||
Some(webview) => webview,
|
||||
None => {
|
||||
info!("Exit sora login thread.");
|
||||
return; // NB: Only exit if we don't have the webview.
|
||||
}
|
||||
};
|
||||
|
||||
let result = check_login_window(
|
||||
&app,
|
||||
&login_webview_window,
|
||||
&app_data_root,
|
||||
&sora_creds_manager,
|
||||
).await;
|
||||
|
||||
match result {
|
||||
Err(err) => {
|
||||
error!("Error checking login window: {:?}", err);
|
||||
}
|
||||
Ok(false) => {} // Continue iteration and try again...
|
||||
Ok(true) => {
|
||||
info!("Successfully saved cookies from login window. Closing.");
|
||||
if let Err(err) = login_webview_window.close() {
|
||||
error!("Error closing login window: {:?}", err);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_millis(2_000)).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if we can exit.
|
||||
async fn check_login_window(
|
||||
app_handle: &AppHandle,
|
||||
webview_window: &WebviewWindow,
|
||||
app_data_root: &AppDataRoot,
|
||||
sora_credential_manager: &SoraCredentialManager,
|
||||
) -> AnyhowResult<bool> {
|
||||
|
||||
/* Login flow looks like this:
|
||||
|
||||
1. Start: https://sora.chatgpt.com/
|
||||
2. Login Start: https://chatgpt.com/auth/login?next=%2Fsora%2F [this is where we start]
|
||||
3. Login Continue: https://auth.openai.com/log-in
|
||||
4. SSO / Google Login (etc): https://accounts.google.com/v3/signin/challenge/pwd?[query]
|
||||
5. Done / Landing: https://sora.chatgpt.com/explore
|
||||
*/
|
||||
|
||||
/*
|
||||
- Do not use credential manager until the end (we don't load old cookies!)
|
||||
- Check if we're on the correct domain, if not exit? (or inverse, that we're not in login flow)
|
||||
*/
|
||||
|
||||
let hostname = get_hostname(webview_window)?;
|
||||
|
||||
let mut maybe_at_destination = false;
|
||||
|
||||
match hostname.as_str() {
|
||||
"sora.com" | // Old destination
|
||||
"sora.chatgpt.com" // New destination
|
||||
=> {
|
||||
maybe_at_destination = true;
|
||||
}
|
||||
// chatgpt.com/auth is also an auth domain
|
||||
"auth.openai.com" |
|
||||
"accounts.google.com" |
|
||||
"accounts.youtube.com" |
|
||||
"login.live.com" |
|
||||
"appleid.apple.com"
|
||||
=> {
|
||||
// NB: We're in auth flow.
|
||||
info!("Sora login webview is in auth flow; hostname `{}`.", hostname);
|
||||
return Ok(false)
|
||||
}
|
||||
_ => {}, // We just don't know...
|
||||
}
|
||||
|
||||
let webview_cookies = extract_sora_webview_cookies(webview_window)?.trim().to_string();
|
||||
|
||||
let session_cookie_presence = has_session_cookie(&webview_cookies)
|
||||
.unwrap_or_else(|err| {
|
||||
error!("Failed to check for session cookie: {:?}", err);
|
||||
SessionCookiePresence::MaybePresent
|
||||
});
|
||||
|
||||
info!("Sora login webview is at hostname `{}`; cookie status: {:?}.", hostname, session_cookie_presence);
|
||||
|
||||
match session_cookie_presence {
|
||||
SessionCookiePresence::Absent => {
|
||||
info!("Session cookies are absent.");
|
||||
return Ok(false);
|
||||
}
|
||||
_ => {},
|
||||
}
|
||||
|
||||
let mut new_credentials =
|
||||
SoraCredentialSet::initialize_with_just_cookies_str(&webview_cookies);
|
||||
|
||||
let _upgraded = maybe_upgrade_or_renew_session(&mut new_credentials).await?;
|
||||
|
||||
// TODO(bt): Race conditions ahead.
|
||||
|
||||
sora_credential_manager.clear_credentials()?;
|
||||
|
||||
let creds_dir = app_data_root.credentials_dir();
|
||||
|
||||
let _r = fs::remove_file(creds_dir.get_sora_cookie_file_path());
|
||||
let _r = fs::remove_file(creds_dir.get_sora_bearer_token_file_path());
|
||||
let _r = fs::remove_file(creds_dir.get_sora_sentinel_file_path());
|
||||
|
||||
sora_credential_manager.set_credentials(&new_credentials)?;
|
||||
sora_credential_manager.persist_all_to_disk()?;
|
||||
|
||||
// NB: Event sent to the frontend for the login flow. We shouldn't rely on just this
|
||||
// alone as it could be brittle if the events aren't caught.
|
||||
let event = SoraLoginSuccessEvent {};
|
||||
|
||||
if let Err(err) = event.send(app_handle) {
|
||||
error!("Error sending Sora login success event: {:?}", err);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn get_hostname(webview: &WebviewWindow) -> AnyhowResult<String> {
|
||||
let url = webview.url()?;
|
||||
let url_hostname= url.host()
|
||||
.ok_or(anyhow!("no host in url"))?
|
||||
.to_string();
|
||||
Ok(url_hostname)
|
||||
}
|
||||
+2
-1
@@ -7,13 +7,14 @@ use chrono::{DateTime, TimeDelta, Utc};
|
||||
use errors::AnyhowResult;
|
||||
use log::info;
|
||||
use std::ops::Sub;
|
||||
use crate::sora_error::SoraError;
|
||||
|
||||
const EXPIRATION_DEADLINE : TimeDelta = TimeDelta::hours(12);
|
||||
|
||||
/// Call this at session startup to generate the tokens for the first time.
|
||||
/// Call this periodically to refresh the JWT as it nears its expiration date.
|
||||
/// This will not refresh the sentinel token once it's been generated - we can use other flows to accomplish that.
|
||||
pub async fn maybe_upgrade_or_renew_session(sora_credentials: &mut SoraCredentialSet) -> AnyhowResult<bool> {
|
||||
pub async fn maybe_upgrade_or_renew_session(sora_credentials: &mut SoraCredentialSet) -> Result<bool, SoraError> {
|
||||
let mut refresh_jwt = !sora_credentials.jwt_bearer_token.is_some();
|
||||
let refresh_sentinel = !sora_credentials.sora_sentinel.is_some();
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
pub mod list_media;
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod bearer;
|
||||
pub mod image_gen;
|
||||
pub mod job;
|
||||
pub mod list_media;
|
||||
pub mod sentinel_refresh;
|
||||
pub mod upload;
|
||||
@@ -26,10 +26,23 @@ pub enum SoraError {
|
||||
/// }
|
||||
UnauthorizedCookieOrBearerExpired,
|
||||
|
||||
/// Failed to generate a bearer token despite the `200 OK` response.
|
||||
/// This has been generated by sending old/expired session cookies or empty cookies.
|
||||
FailedToGenerateBearer,
|
||||
|
||||
/// Something is wrong with the JWT bearer token.
|
||||
/// This error originates on our end as we try to parse the JWT.
|
||||
LocalJwtClaimsParseError(String),
|
||||
|
||||
/// Sora is having issues.
|
||||
/// Typically served from Cloudflare
|
||||
/// We preserve the message for debugging.
|
||||
BadGateway(String),
|
||||
|
||||
/// HTTP 524 (Cloudflare-specific)
|
||||
/// Cloudflare formed a TCP connection to Sora, but no payload was delivered before timeout
|
||||
/// We preserve the message for debugging
|
||||
CloudFlareTimeout(String),
|
||||
|
||||
/// Another error occurred.
|
||||
OtherBadStatus(anyhow::Error),
|
||||
@@ -55,6 +68,12 @@ impl Display for SoraError {
|
||||
Self::NoBearerTokenAvailable => {
|
||||
write!(f, "Unauthorized: no bearer token set")
|
||||
}
|
||||
Self::FailedToGenerateBearer => {
|
||||
write!(f, "Failed to generate bearer. Probably invalid session.")
|
||||
}
|
||||
Self::LocalJwtClaimsParseError(message) => {
|
||||
write!(f, "Local JWT claims parse error: {}", message)
|
||||
}
|
||||
Self::TooManyConcurrentTasks => {
|
||||
write!(f, "Too many concurrent tasks. Please wait.")
|
||||
}
|
||||
@@ -64,6 +83,9 @@ impl Display for SoraError {
|
||||
Self::BadGateway(message) => {
|
||||
write!(f, "Bad Gateway; Sora is likely having issues: {:?}", message)
|
||||
}
|
||||
Self::CloudFlareTimeout(message) => {
|
||||
write!(f, "Cloudflare Timeout (524); Sora is likely having issues: {:?}", message)
|
||||
}
|
||||
Self::OtherBadStatus(err) => {
|
||||
write!(f, "Other error: {}", err)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,33 @@ pub async fn classify_general_http_error(response: reqwest::Response) -> SoraErr
|
||||
Ok(text) => text,
|
||||
Err(err) => return SoraError::ReqwestError(err),
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
Old cookie -
|
||||
|
||||
Message: {
|
||||
"error": {
|
||||
"message": "Your authentication token has expired. Please try signing in again.",
|
||||
"type": "invalid_request_error",
|
||||
"param": null,
|
||||
"code": "token_expired"
|
||||
}
|
||||
}
|
||||
|
||||
No cookie -
|
||||
|
||||
Message: {
|
||||
"error": {
|
||||
"message": "Your authentication token has expired. Please try signing in again.",
|
||||
"type": "invalid_request_error",
|
||||
"param": null,
|
||||
"code": "token_expired"
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// TODO: I *think* this is just the bearer token, not the cookie.
|
||||
let cookie_expired =
|
||||
message.contains("Your authentication token has expired. Please try signing in again.")
|
||||
|| message.contains("token_expired");
|
||||
@@ -17,8 +43,18 @@ pub async fn classify_general_http_error(response: reqwest::Response) -> SoraErr
|
||||
return SoraError::UnauthorizedCookieOrBearerExpired
|
||||
}
|
||||
|
||||
if status.as_u16() == 502 {
|
||||
return SoraError::BadGateway(message);
|
||||
let status_code = status.as_u16();
|
||||
|
||||
match status_code {
|
||||
502 => {
|
||||
return SoraError::BadGateway(message);
|
||||
}
|
||||
524 => {
|
||||
if message.contains("cloudflare") || message.contains("Cloudflare") {
|
||||
return SoraError::CloudFlareTimeout(message);
|
||||
}
|
||||
}
|
||||
_ => {}, // Fall-through
|
||||
}
|
||||
|
||||
SoraError::OtherBadStatus(anyhow::anyhow!("Upload failed with status {}: {}", status, message))
|
||||
|
||||
@@ -4,7 +4,8 @@ use cookie::Cookie;
|
||||
use once_cell::sync::Lazy;
|
||||
use errors::AnyhowResult;
|
||||
|
||||
pub const SESSION_COOKIE_NAME : &str = "__Secure-authjs.session-token";
|
||||
pub const SESSION_COOKIE_NAME_OLD : &str = "__Secure-authjs.session-token";
|
||||
pub const SESSION_COOKIE_NAME_NEW : &str = "__Secure-next-auth.session-token";
|
||||
|
||||
/// Cookies Sora.com mints that are not the session cookie. We can ignore these.
|
||||
/// This list is very likely to change over time.
|
||||
@@ -51,7 +52,9 @@ pub fn has_session_cookie(cookies_header: &str) -> AnyhowResult<SessionCookiePre
|
||||
for cookie in cookies {
|
||||
let cookie= cookie?;
|
||||
let cookie_name = cookie.name();
|
||||
if cookie_name == SESSION_COOKIE_NAME {
|
||||
if cookie_name == SESSION_COOKIE_NAME_NEW {
|
||||
return Ok(SessionCookiePresence::Present);
|
||||
} else if cookie_name == SESSION_COOKIE_NAME_NEW {
|
||||
return Ok(SessionCookiePresence::Present);
|
||||
} else if !IRRELEVANT_COOKIE_NAMES.contains(cookie_name) {
|
||||
unknown_cookie_count += 1;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
//! Adapted from 'jwt' crate
|
||||
|
||||
use crate::sora_error::SoraError;
|
||||
use anyhow::anyhow;
|
||||
use base64::Engine;
|
||||
use base64::prelude::BASE64_URL_SAFE_NO_PAD;
|
||||
use base64::Engine;
|
||||
use chrono::{DateTime, Utc};
|
||||
use errors::AnyhowResult;
|
||||
use log::warn;
|
||||
use serde_json::Value;
|
||||
use errors::AnyhowResult;
|
||||
|
||||
const SEPARATOR: &str = ".";
|
||||
|
||||
@@ -19,48 +20,53 @@ pub struct SoraJwtClaims {
|
||||
pub email_verified: bool,
|
||||
}
|
||||
|
||||
pub fn lightweight_sora_jwt_parse(token: &str) -> AnyhowResult<SoraJwtClaims> {
|
||||
pub fn lightweight_sora_jwt_parse(token: &str) -> Result<SoraJwtClaims, SoraError> {
|
||||
let [_header_str, claims_str, _signature_str] = split_components(token)?;
|
||||
|
||||
let claims = decode_base64(claims_str)?;
|
||||
let claims : serde_json::Map<String, Value> = serde_json::from_str(&claims)?;
|
||||
let claims = decode_base64(claims_str)
|
||||
.map_err(|err| SoraError::LocalJwtClaimsParseError(
|
||||
format!("failure to parse claims base64: {:?}", err)))?;
|
||||
|
||||
let claims : serde_json::Map<String, Value> = serde_json::from_str(&claims)
|
||||
.map_err(|err| SoraError::LocalJwtClaimsParseError(
|
||||
format!("failure to parse claims json: {:?}", err)))?;
|
||||
|
||||
let iat = claims.get("iat")
|
||||
.map(|val| val.as_number())
|
||||
.flatten()
|
||||
.ok_or(anyhow!("no iat claim"))?;
|
||||
.ok_or_else(|| jwt_error("no iat claim"))?;
|
||||
|
||||
let iat = iat.as_i64()
|
||||
.ok_or(anyhow!("iat is not a number"))?;
|
||||
.ok_or_else(|| jwt_error("iat is not a number"))?;
|
||||
|
||||
let iat = DateTime::from_timestamp(iat, 0)
|
||||
.ok_or(anyhow!("iat is not a valid timestamp"))?;
|
||||
.ok_or_else(|| jwt_error("iat is not a valid timestamp"))?;
|
||||
|
||||
let exp = claims.get("exp")
|
||||
.map(|val| val.as_number())
|
||||
.flatten()
|
||||
.ok_or(anyhow!("no exp claim"))?;
|
||||
.ok_or_else(|| jwt_error("no exp claim"))?;
|
||||
|
||||
let exp = exp.as_i64()
|
||||
.ok_or(anyhow!("exp is not a number"))?;
|
||||
.ok_or_else(|| jwt_error("exp is not a number"))?;
|
||||
|
||||
let exp = DateTime::from_timestamp(exp, 0)
|
||||
.ok_or(anyhow!("iat is not a valid timestamp"))?;
|
||||
.ok_or_else(|| jwt_error("iat is not a valid timestamp"))?;
|
||||
|
||||
let user_id = claims.get("https://api.openai.com/auth")
|
||||
.map(|val| val.get("user_id"))
|
||||
.flatten()
|
||||
.map(|val| val.as_str())
|
||||
.flatten()
|
||||
.ok_or(anyhow!("no user_id claim"))?;
|
||||
.ok_or_else(|| jwt_error("no user_id claim"))?;
|
||||
|
||||
let profile = claims.get("https://api.openai.com/profile")
|
||||
.ok_or(anyhow!("no user_id claim"))?;
|
||||
.ok_or_else(|| jwt_error("no user_id claim"))?;
|
||||
|
||||
let email = profile.get("email")
|
||||
.map(|val| val.as_str())
|
||||
.flatten()
|
||||
.ok_or(anyhow!("no email claim"))?;
|
||||
.ok_or_else(|| jwt_error("no email claim"))?;
|
||||
|
||||
let email_verified = profile.get("email_verified")
|
||||
.map(|val| val.as_bool())
|
||||
@@ -79,14 +85,19 @@ pub fn lightweight_sora_jwt_parse(token: &str) -> AnyhowResult<SoraJwtClaims> {
|
||||
})
|
||||
}
|
||||
|
||||
fn split_components(token: &str) -> AnyhowResult<[&str; 3]> {
|
||||
fn split_components(token: &str) -> Result<[&str; 3], SoraError> {
|
||||
let mut components = token.split(SEPARATOR);
|
||||
let header = components.next().ok_or(anyhow!("no header component"))?;
|
||||
let claims = components.next().ok_or(anyhow!("no claims component"))?;
|
||||
let signature = components.next().ok_or(anyhow!("no signature component"))?;
|
||||
let header = components.next()
|
||||
.ok_or_else(|| jwt_error("no header component"))?;
|
||||
|
||||
let claims = components.next()
|
||||
.ok_or_else(|| jwt_error("no claims component"))?;
|
||||
|
||||
let signature = components.next()
|
||||
.ok_or_else(|| jwt_error("no signature component"))?;
|
||||
|
||||
if components.next().is_some() {
|
||||
return Err(anyhow!("too many components"));
|
||||
return Err(jwt_error("too many components"));
|
||||
}
|
||||
|
||||
Ok([header, claims, signature])
|
||||
@@ -98,6 +109,10 @@ fn decode_base64(raw: &str) -> AnyhowResult<String> {
|
||||
Ok(json_str)
|
||||
}
|
||||
|
||||
fn jwt_error(reason: &str) -> SoraError {
|
||||
SoraError::LocalJwtClaimsParseError(reason.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::utils::lightweight_sora_jwt_parse::lightweight_sora_jwt_parse;
|
||||
|
||||
Binary file not shown.
@@ -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
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
Binary file not shown.
+64
-62
@@ -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,8 +1,6 @@
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
|
||||
import { AUTH_STATUS } from "~/enums";
|
||||
import { authentication } from "~/signals";
|
||||
|
||||
import { ButtonLink } from "~/components";
|
||||
import ProfileDropdown from "./ProfileDropdown";
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -66,8 +66,8 @@ export class Configs {
|
||||
break;
|
||||
}
|
||||
|
||||
console.debug(`Build environment type: ${environment}`);
|
||||
console.debug(`Deploy context: ${this.deployContext}`);
|
||||
console.debug(`Upload API: ${this.uploadApiVideo}`);
|
||||
//console.debug(`Build environment type: ${environment}`);
|
||||
//console.debug(`Deploy context: ${this.deployContext}`);
|
||||
//console.debug(`Upload API: ${this.uploadApiVideo}`);
|
||||
}
|
||||
}
|
||||
@@ -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("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("flower", new SoundEffect("resources/sound/smrpg_flower.wav", { defaultVolume: 0.1 }));
|
||||
r.setSoundOnce("correct", new SoundEffect("resources/sound/smrpg_correct.wav", { defaultVolume: 0.3 }));
|
||||
r.setSoundOnce("next", new SoundEffect("resources/sound/oot_dialogue_next.wav", { defaultVolume: 0.2 }));
|
||||
r.setSoundOnce("done", new SoundEffect("resources/sound/oot_dialogue_done.wav", { defaultVolume: 0.2 }));
|
||||
r.setSoundOnce("crumble", new SoundEffect("resources/sound/smrpg_drybones_crumble.wav", { defaultVolume: 0.1 }));
|
||||
(window as any).sounds = r;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { LoadingDots, TopBar } from "~/components";
|
||||
import { Controls3D } from "./comps/Controls3D";
|
||||
@@ -23,9 +22,16 @@ 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";
|
||||
import {
|
||||
disableHotkeyInput,
|
||||
enableHotkeyInput,
|
||||
DomLevels,
|
||||
} from "./signals/hotkeys";
|
||||
import { AUTH_STATUS } from "../../enums/authentication";
|
||||
import { authentication } from "../../signals/authentication/authentication";
|
||||
|
||||
export const PageEditor = () => {
|
||||
useSignals();
|
||||
@@ -75,7 +81,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,19 +163,22 @@ export const PageEditor = () => {
|
||||
<Timeline />
|
||||
</div> */}
|
||||
|
||||
<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!"
|
||||
videoSrc="/resources/videos/artcraft-3d-demo.mp4"
|
||||
buttonText="Sign in to OpenAI to get started"
|
||||
buttonOnClick={async () => {
|
||||
if (IsDesktopApp()) {
|
||||
await invoke("open_sora_login_command");
|
||||
<LoginModal
|
||||
onClose={() => {}}
|
||||
videoSrc2D="/resources/videos/artcraft-canvas-demo.mp4"
|
||||
videoSrc3D="/resources/videos/artcraft-3d-demo.mp4"
|
||||
openAiLogo="/resources/images/openai-logo.png"
|
||||
onOpenChange={(isOpen: boolean) => {
|
||||
if (isOpen) {
|
||||
disableHotkeyInput(DomLevels.DIALOGUE);
|
||||
} else {
|
||||
console.error("This is not the Desktop app.");
|
||||
enableHotkeyInput(DomLevels.DIALOGUE);
|
||||
}
|
||||
}}
|
||||
onArtCraftAuthSuccess={(userInfo: any) => {
|
||||
authentication.status.value = AUTH_STATUS.LOGGED_IN;
|
||||
authentication.userInfo.value = userInfo;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ErrorDialog, LoadingDots } from "~/components";
|
||||
import { GenerateModals } from "~/pages/PageEnigma/comps/GenerateModals/GenerateModals";
|
||||
import { Toaster } from "@storyteller/ui-toaster";
|
||||
import { EditorLoadingBar } from "./comps/EditorLoadingBar";
|
||||
import { Wizard } from "~/pages/PageEnigma/Wizard/Wizard";
|
||||
// import { Wizard } from "~/pages/PageEnigma/Wizard/Wizard";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { useEffect, useState } from "react";
|
||||
import * as gpu from "detect-gpu";
|
||||
@@ -23,8 +23,8 @@ import {
|
||||
showPrecisionSelector,
|
||||
} from "./signals/precisionSelectorMenu";
|
||||
import { InstallSounds } from "~/pages/PageEnigma/InstallSounds";
|
||||
import { InstallImageGenerationFailure } from "~/pages/PageEnigma/tauri_events/SoraImageGenerationFailure";
|
||||
import { InstallImageGenerationSuccess } from "~/pages/PageEnigma/tauri_events/SoraImageGenerationSuccess";
|
||||
import { InstallImageGenerationFailure } from "~/pages/PageEnigma/TauriEvents/SoraImageGenerationFailure";
|
||||
import { InstallImageGenerationSuccess } from "~/pages/PageEnigma/TauriEvents/SoraImageGenerationSuccess";
|
||||
|
||||
export const PageEnigma = ({ sceneToken }: { sceneToken?: string }) => {
|
||||
useSignals();
|
||||
@@ -91,6 +91,7 @@ export const PageEnigma = ({ sceneToken }: { sceneToken?: string }) => {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
console.log("installing event listeners");
|
||||
InstallSounds();
|
||||
InstallImageGenerationSuccess();
|
||||
InstallImageGenerationFailure();
|
||||
@@ -116,7 +117,7 @@ export const PageEnigma = ({ sceneToken }: { sceneToken?: string }) => {
|
||||
/>
|
||||
<GenerateModals />
|
||||
<ErrorDialog />
|
||||
<Wizard />
|
||||
{/* <Wizard /> */}
|
||||
</EngineProvider>
|
||||
<EditorLoadingBar />
|
||||
<Toaster offsetTop={70} offsetRight={12} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { SoundRegistry, SoundEffect } from "@storyteller/soundboard";
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { AppPreferencesPayload, CustomDirectory, GetAppPreferences, SystemDirectory } from "@storyteller/tauri-api";
|
||||
import { toast } from "@storyteller/ui-toaster";
|
||||
|
||||
type SoraImageGenerationFailed = {
|
||||
prompt: string,
|
||||
};
|
||||
|
||||
export const InstallImageGenerationFailure = () => {
|
||||
listen<SoraImageGenerationFailed>('sora-image-generation-failed', async (event) => {
|
||||
console.log("sora-image-generation-failed event", event);
|
||||
const prefs = await GetAppPreferences();
|
||||
console.log("prefs", prefs);
|
||||
const soundName = prefs.preferences?.generation_failure_sound;
|
||||
console.log("soundName", soundName);
|
||||
if (soundName !== undefined) {
|
||||
console.log("playing sound", soundName);
|
||||
const registry = SoundRegistry.getInstance();
|
||||
registry.playSound(soundName);
|
||||
}
|
||||
toast.error("Image generation failed!");
|
||||
});
|
||||
}
|
||||
|
||||
export const ImageGenerationFailure = () => {
|
||||
}
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { SoundRegistry, SoundEffect } from "@storyteller/soundboard";
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { AppPreferencesPayload, CustomDirectory, GetAppPreferences, SystemDirectory } from "@storyteller/tauri-api";
|
||||
import { toast } from "@storyteller/ui-toaster";
|
||||
|
||||
type ImageGenerationSuccess = {
|
||||
media_file_token: string,
|
||||
};
|
||||
|
||||
export const InstallImageGenerationSuccess = () => {
|
||||
listen<ImageGenerationSuccess>('sora-image-generation-complete', async (event) => {
|
||||
console.log("sora-image-generation-complete event", event);
|
||||
const prefs = await GetAppPreferences();
|
||||
console.log("prefs", prefs);
|
||||
const soundName = prefs.preferences?.generation_success_sound;
|
||||
console.log("soundName", soundName);
|
||||
if (soundName !== undefined) {
|
||||
console.log("playing sound", soundName);
|
||||
const registry = SoundRegistry.getInstance();
|
||||
registry.playSound(soundName);
|
||||
}
|
||||
toast.success("Image generated!");
|
||||
});
|
||||
}
|
||||
|
||||
export const ImageGenerationSuccess = () => {
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -23,7 +23,8 @@ export const Controls3D = () => {
|
||||
useEffect(() => {
|
||||
// Check if scene is empty and onboarding helper is not visible
|
||||
const checkSceneEmpty = () => {
|
||||
const isSceneEmpty = outlinerState.items.value.length === 0;
|
||||
const isSceneEmpty =
|
||||
outlinerState.items.value.length === 0 && !assetModalVisible.value;
|
||||
|
||||
setShowEmptySceneTooltip(isSceneEmpty);
|
||||
};
|
||||
@@ -37,7 +38,8 @@ export const Controls3D = () => {
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [assetModalVisible.value]);
|
||||
|
||||
const handleModeChange = (value: string) => {
|
||||
selectedMode.value = value;
|
||||
@@ -119,7 +121,7 @@ export const Controls3D = () => {
|
||||
<Tooltip
|
||||
content="Add 3D asset to scene (B)"
|
||||
position="bottom"
|
||||
delay={100}
|
||||
delay={300}
|
||||
closeOnClick
|
||||
className={twMerge(
|
||||
showEmptySceneTooltip ? "hidden" : "block",
|
||||
|
||||
@@ -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)}
|
||||
|
||||
+9
-12
@@ -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>
|
||||
);
|
||||
};
|
||||
-158
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
-44
@@ -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>
|
||||
);
|
||||
};
|
||||
-18
@@ -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
|
||||
</>
|
||||
);
|
||||
}
|
||||
-22
@@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
-19
@@ -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
|
||||
</>
|
||||
);
|
||||
}
|
||||
-37
@@ -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;
|
||||
};
|
||||
-18
@@ -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
|
||||
</>
|
||||
);
|
||||
}
|
||||
-19
@@ -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
|
||||
</>
|
||||
);
|
||||
}
|
||||
-222
@@ -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;
|
||||
}
|
||||
-109
@@ -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;
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
-91
@@ -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,
|
||||
};
|
||||
};
|
||||
-88
@@ -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,
|
||||
};
|
||||
};
|
||||
-52
@@ -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;
|
||||
}
|
||||
}, []),
|
||||
};
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user