mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-21 01:12:40 -04:00
Compare commits
7
Commits
bt-cli-tool
...
whoops
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c14f35c9f | ||
|
|
921e58a308 | ||
|
|
65531dc63e | ||
|
|
0b60835bbc | ||
|
|
cca897d24d | ||
|
|
ea76917264 | ||
|
|
fc824a10ae |
Generated
+172
-9
@@ -74,7 +74,7 @@ checksum = "b340e9cfa5b08690aae90fb61beb44e9b06f44fe3d0f93781aaa58cfba86245e"
|
||||
dependencies = [
|
||||
"actix-utils",
|
||||
"actix-web",
|
||||
"derive_more",
|
||||
"derive_more 0.99.17",
|
||||
"futures-util",
|
||||
"log",
|
||||
"once_cell",
|
||||
@@ -97,7 +97,7 @@ dependencies = [
|
||||
"brotli 6.0.0",
|
||||
"bytes 1.10.0",
|
||||
"bytestring",
|
||||
"derive_more",
|
||||
"derive_more 0.99.17",
|
||||
"encoding_rs",
|
||||
"flate2",
|
||||
"futures-core",
|
||||
@@ -140,7 +140,7 @@ dependencies = [
|
||||
"actix-utils",
|
||||
"actix-web",
|
||||
"bytes 1.10.0",
|
||||
"derive_more",
|
||||
"derive_more 0.99.17",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"httparse",
|
||||
@@ -253,7 +253,7 @@ dependencies = [
|
||||
"bytestring",
|
||||
"cfg-if 1.0.0",
|
||||
"cookie 0.16.2",
|
||||
"derive_more",
|
||||
"derive_more 0.99.17",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
@@ -322,7 +322,7 @@ dependencies = [
|
||||
"bytes 1.10.0",
|
||||
"bytestring",
|
||||
"csv",
|
||||
"derive_more",
|
||||
"derive_more 0.99.17",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http 0.2.11",
|
||||
@@ -809,6 +809,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"assertor",
|
||||
"base64 0.22.1",
|
||||
"build_metadata",
|
||||
"bytes 1.10.0",
|
||||
"chrono",
|
||||
"const_format",
|
||||
@@ -1384,6 +1385,15 @@ dependencies = [
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "1.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.70.1"
|
||||
@@ -1663,6 +1673,79 @@ dependencies = [
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "build-info"
|
||||
version = "0.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "288657edd15bfa5a7f30ca3b123c1af2c503eaf218e517fa10bc9063dbc2ad99"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"build-info-common",
|
||||
"build-info-proc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "build-info-build"
|
||||
version = "0.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3146483d5bc5081ec26f9c4e60232e770980d750b2802d6cc2563cded665cc73"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
"bincode",
|
||||
"build-info-common",
|
||||
"cargo_metadata",
|
||||
"chrono",
|
||||
"git2",
|
||||
"glob",
|
||||
"pretty_assertions",
|
||||
"rustc_version 0.4.0",
|
||||
"serde_json",
|
||||
"zstd 0.13.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "build-info-common"
|
||||
version = "0.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8524ad59f5958f37f95e66bf6c9a8fa8440c4f56c069247c44244434cfca3eb1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"derive_more 2.0.1",
|
||||
"semver 1.0.23",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "build-info-proc"
|
||||
version = "0.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b58fb02636d968e8327d84a5a256df9704ac27a1eda98429c35dbe50a278f69"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
"bincode",
|
||||
"build-info-common",
|
||||
"chrono",
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"proc-macro-error2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_json",
|
||||
"syn 2.0.87",
|
||||
"zstd 0.13.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "build_metadata"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"build-info",
|
||||
"build-info-build",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "built"
|
||||
version = "0.7.6"
|
||||
@@ -3287,6 +3370,27 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
|
||||
dependencies = [
|
||||
"derive_more-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more-impl"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "des"
|
||||
version = "0.7.0"
|
||||
@@ -3381,6 +3485,12 @@ dependencies = [
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diff"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.9.0"
|
||||
@@ -5119,6 +5229,19 @@ dependencies = [
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "git2"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2deb07a133b1520dc1a5690e9bd08950108873d7ed5de38dcc74d3b5ebffa110"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"libc",
|
||||
"libgit2-sys",
|
||||
"log",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glib"
|
||||
version = "0.18.5"
|
||||
@@ -6745,6 +6868,18 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libgit2-sys"
|
||||
version = "0.18.1+1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1dcb20f84ffcdd825c7a311ae347cce604a6f084a767dec4a4929829645290e"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"libz-sys",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
version = "0.7.4"
|
||||
@@ -6793,6 +6928,18 @@ dependencies = [
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libz-sys"
|
||||
version = "1.1.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "limitation"
|
||||
version = "0.1.1"
|
||||
@@ -9084,6 +9231,16 @@ dependencies = [
|
||||
"termtree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pretty_assertions"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
|
||||
dependencies = [
|
||||
"diff",
|
||||
"yansi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.25"
|
||||
@@ -10818,7 +10975,7 @@ checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"cssparser 0.27.2",
|
||||
"derive_more",
|
||||
"derive_more 0.99.17",
|
||||
"fxhash",
|
||||
"log",
|
||||
"matches",
|
||||
@@ -10838,7 +10995,7 @@ checksum = "4eb30575f3638fc8f6815f448d50cb1a2e255b0897985c8c59f4d37b72a07b06"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"cssparser 0.31.2",
|
||||
"derive_more",
|
||||
"derive_more 0.99.17",
|
||||
"fxhash",
|
||||
"log",
|
||||
"new_debug_unreachable",
|
||||
@@ -11789,7 +11946,7 @@ dependencies = [
|
||||
"cookies",
|
||||
"crockford",
|
||||
"datetimes",
|
||||
"derive_more",
|
||||
"derive_more 0.99.17",
|
||||
"diesel",
|
||||
"dotenv",
|
||||
"easyenv",
|
||||
@@ -15318,7 +15475,7 @@ dependencies = [
|
||||
"crossbeam-utils 0.8.20",
|
||||
"crypto-common",
|
||||
"deranged",
|
||||
"derive_more",
|
||||
"derive_more 0.99.17",
|
||||
"digest 0.10.7",
|
||||
"either",
|
||||
"flate2",
|
||||
@@ -15551,6 +15708,12 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yansi"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.7.5"
|
||||
|
||||
@@ -39,6 +39,7 @@ members = [
|
||||
"crates/lib/api_clients/thumbnail_generator",
|
||||
"crates/lib/bootstrap",
|
||||
"crates/lib/bucket_paths",
|
||||
"crates/lib/build_metadata",
|
||||
"crates/lib/caching/memory_caching",
|
||||
"crates/lib/caching/memory_store",
|
||||
"crates/lib/caching/redis_caching",
|
||||
@@ -185,6 +186,7 @@ toml = "0.8.22"
|
||||
url = { version = "2.5.4", features = ["serde"] }
|
||||
|
||||
# Internal libs
|
||||
build_metadata = { path = "crates/lib/build_metadata" }
|
||||
crockford = { path = "crates/lib/data/crockford" }
|
||||
enums = { path = "crates/_types/enums" }
|
||||
enums_public = { path = "crates/_types/enums_public" }
|
||||
|
||||
@@ -23,6 +23,7 @@ tauri-build = { version = "2.1.1", features = [] }
|
||||
workspace-build-acceleration = { workspace = true }
|
||||
|
||||
# Internal
|
||||
build_metadata = { workspace = true }
|
||||
crockford = { workspace = true }
|
||||
errors = { workspace = true }
|
||||
fal_client = { workspace = true }
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
use crate::core::state::os_platform::OsPlatform;
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
use base64::Engine;
|
||||
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
|
||||
use image::ImageReader;
|
||||
use log::info;
|
||||
use serde_derive::Serialize;
|
||||
use std::io::Cursor;
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct BuildInfoResponse {
|
||||
pub build_timestamp: DateTime<Utc>,
|
||||
pub git_commit_id: Option<String>,
|
||||
pub git_commit_short_id: Option<String>,
|
||||
pub git_commit_short_timestamp: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_build_info_command() -> Result<BuildInfoResponse, String> {
|
||||
info!("get_build_info_command called...");
|
||||
|
||||
let build_timestamp = build_metadata::build_timestamp();
|
||||
let git_commit_id = build_metadata::git_commit_id();
|
||||
let git_commit_short_id = build_metadata::git_commit_short_id();
|
||||
let git_commit_short_timestamp = build_metadata::git_commit_timestamp();
|
||||
|
||||
Ok(BuildInfoResponse {
|
||||
build_timestamp,
|
||||
git_commit_id: git_commit_id.map(|s| s.to_string()),
|
||||
git_commit_short_id: git_commit_short_id.map(|s| s.to_string()),
|
||||
git_commit_short_timestamp,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod app_preferences;
|
||||
pub mod command_response_wrapper;
|
||||
pub mod flip_image;
|
||||
pub mod get_build_info_command;
|
||||
pub mod platform_info_command;
|
||||
|
||||
@@ -6,6 +6,7 @@ use tauri::Manager;
|
||||
use crate::core::commands::app_preferences::get_app_preferences_command::get_app_preferences_command;
|
||||
use crate::core::commands::app_preferences::update_app_preference_command::update_app_preferences_command;
|
||||
use crate::core::commands::flip_image::flip_image;
|
||||
use crate::core::commands::get_build_info_command::get_build_info_command;
|
||||
use crate::core::commands::platform_info_command::platform_info_command;
|
||||
use crate::core::state::app_preferences::app_preferences_manager::load_app_preferences_or_default;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
@@ -164,6 +165,7 @@ pub fn run() {
|
||||
fal_kling_image_to_video_command,
|
||||
flip_image,
|
||||
get_app_preferences_command,
|
||||
get_build_info_command,
|
||||
get_fal_api_key_command,
|
||||
open_sora_login_command,
|
||||
platform_info_command,
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"default":{"identifier":"default","description":"enables the default permissions","local":true,"windows":["main"],"permissions":["core:default","log:default"]}}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,6 @@ authors = [
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
# NB: Public "errors" crate is name-squatted, so we're fine to use this internally.
|
||||
name = "actix_cors_configs"
|
||||
path = "src/lib.rs"
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
[package]
|
||||
name = "build_metadata"
|
||||
resolver = "2" # NB: Required by `build-info`
|
||||
edition = "2024"
|
||||
version = "0.0.1"
|
||||
authors = [
|
||||
"Brandon Thomas <bt@brand.io>",
|
||||
"Brandon Thomas <echelon@gmail.com>",
|
||||
]
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
name = "build_metadata"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[package.metadata.cargo-udeps.ignore]
|
||||
normal = ["workspace-build-acceleration"]
|
||||
|
||||
[dependencies]
|
||||
# Workspace hack (faster compile times)
|
||||
workspace-build-acceleration = { workspace = true }
|
||||
|
||||
# Internal
|
||||
# (None yet)
|
||||
|
||||
# External
|
||||
build-info = "0.0.40"
|
||||
|
||||
[dev-dependencies]
|
||||
# None yet
|
||||
|
||||
[build-dependencies]
|
||||
build-info-build = "0.0.40"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
fn main() {
|
||||
// Tell Cargo that if the given file changes, to rerun this build script.
|
||||
// TODO: Use this to test the GIT_SHA, etc. files that CI dumps into the working directory
|
||||
// println!("cargo::rerun-if-changed=Cargo.toml");
|
||||
|
||||
// Make build info available in the main crate sources.
|
||||
build_info_build::build_script();
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
use std::ops::Deref;
|
||||
use build_info::chrono::{DateTime, Utc};
|
||||
|
||||
build_info::build_info!(fn version);
|
||||
|
||||
pub fn build_timestamp() -> DateTime<Utc> {
|
||||
version().timestamp
|
||||
}
|
||||
|
||||
pub fn git_commit_id() -> Option<&'static str> {
|
||||
version().version_control
|
||||
.as_ref()
|
||||
.map(|vc| vc.git())
|
||||
.flatten()
|
||||
.map(|git| git.commit_id.deref())
|
||||
}
|
||||
|
||||
pub fn git_commit_short_id() -> Option<&'static str> {
|
||||
version().version_control
|
||||
.as_ref()
|
||||
.map(|vc| vc.git())
|
||||
.flatten()
|
||||
.map(|git| git.commit_short_id.deref())
|
||||
}
|
||||
|
||||
pub fn git_commit_timestamp() -> Option<DateTime<Utc>> {
|
||||
version().version_control
|
||||
.as_ref()
|
||||
.map(|vc| vc.git())
|
||||
.flatten()
|
||||
.map(|git| git.commit_timestamp)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::git_commit_id;
|
||||
|
||||
#[test]
|
||||
#[ignore] // Run manually for testing, not in CI
|
||||
fn test_git_commit_id() {
|
||||
assert_eq!(git_commit_id(), None);
|
||||
}
|
||||
}
|
||||
@@ -17,13 +17,13 @@ path = "src/lib.rs"
|
||||
normal = ["workspace-build-acceleration"]
|
||||
|
||||
[dependencies]
|
||||
# cargo-hakari workspace hack (faster compile times)
|
||||
workspace-build-acceleration = { workspace = true }
|
||||
|
||||
# NB: This is one of the most central crates in the monorepo and everything depends on it.
|
||||
# Consider the dependencies here wisely.
|
||||
# Ideally we have almost nothing as a dependency of this crate.
|
||||
anyhow = "1.0.86"
|
||||
|
||||
# Workspace hack (faster compile times)
|
||||
workspace-build-acceleration = { version = "0.1", path = "../../workspace-build-acceleration" }
|
||||
anyhow = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
# None yet
|
||||
|
||||
@@ -16,12 +16,12 @@ path = "src/lib.rs"
|
||||
normal = ["workspace-build-acceleration"]
|
||||
|
||||
[dependencies]
|
||||
# cargo-hakari workspace hack (faster compile times)
|
||||
workspace-build-acceleration = { workspace = true }
|
||||
|
||||
# Internal
|
||||
# (None yet)
|
||||
|
||||
# Workspace hack (faster compile times)
|
||||
workspace-build-acceleration = { version = "0.1", path = "../../workspace-build-acceleration" }
|
||||
|
||||
# External
|
||||
# (None yet)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
// TODO: Move to shared component
|
||||
interface BadgeProps {
|
||||
label: string;
|
||||
color?: string;
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Canvas, Meta } from '@storybook/blocks';
|
||||
|
||||
import * as ButtonStories from './Button.stories';
|
||||
|
||||
<Meta of={ButtonStories} />
|
||||
|
||||
# Button
|
||||
|
||||
A button denotes a possible action to be taken on behalf of the user.
|
||||
|
||||
There are three types of Button: primary, secondary, action.
|
||||
|
||||
<Canvas of={ButtonStories.Primary} />
|
||||
<Canvas of={ButtonStories.Secondary} />
|
||||
<Canvas of={ButtonStories.Action} />
|
||||
|
||||
It can be shown with an optional icon. If "iconFlip" is true, the icon shows after the "children", if false it shows before.
|
||||
|
||||
<Canvas of={ButtonStories.WithIconFlip} />
|
||||
<Canvas of={ButtonStories.WithIconNoFlip} />
|
||||
@@ -1,46 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Button } from "./Button";
|
||||
import { withActions } from "@storybook/addon-actions/decorator";
|
||||
import { faAngleLeft } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
const meta: Meta<typeof Button> = {
|
||||
component: Button,
|
||||
parameters: {
|
||||
actions: {
|
||||
handles: ["click"],
|
||||
},
|
||||
},
|
||||
decorators: [withActions],
|
||||
};
|
||||
|
||||
type Story = StoryObj<typeof Button>;
|
||||
|
||||
export const Primary: Story = {
|
||||
render: () => <Button variant="primary">Primary</Button>,
|
||||
};
|
||||
|
||||
export const Secondary: Story = {
|
||||
render: () => <Button variant="secondary">Secondary</Button>,
|
||||
};
|
||||
|
||||
export const Action: Story = {
|
||||
render: () => <Button variant="action">Action</Button>,
|
||||
};
|
||||
|
||||
export const WithIconFlip: Story = {
|
||||
render: () => (
|
||||
<Button variant="secondary" iconFlip icon={faAngleLeft}>
|
||||
Icon Flip
|
||||
</Button>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithIconNoFlip: Story = {
|
||||
render: () => (
|
||||
<Button variant="secondary" icon={faAngleLeft}>
|
||||
Icon
|
||||
</Button>
|
||||
),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
@@ -1,107 +0,0 @@
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { ButtonHTMLAttributes } from "react";
|
||||
import { faSpinnerThird } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
icon?: IconDefinition;
|
||||
iconClassName?: string;
|
||||
iconFlip?: boolean;
|
||||
htmlFor?: string;
|
||||
variant?: "primary" | "secondary" | "action";
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export const Button = ({
|
||||
icon,
|
||||
iconClassName,
|
||||
children,
|
||||
className: propsClassName,
|
||||
htmlFor,
|
||||
variant: propsVariant = "primary",
|
||||
disabled,
|
||||
iconFlip = false,
|
||||
loading,
|
||||
...rest
|
||||
}: ButtonProps) => {
|
||||
function getVariantClassNames(variant: string) {
|
||||
switch (variant) {
|
||||
case "secondary": {
|
||||
return "bg-brand-secondary hover:bg-brand-secondary-700 text-white focus-visible:outline-brand-secondary";
|
||||
}
|
||||
case "action": {
|
||||
return "bg-action hover:bg-action-900 text-white focus-visible:outline-action";
|
||||
}
|
||||
case "primary":
|
||||
default: {
|
||||
return "bg-brand-primary hover:bg-brand-primary-400 text-white focus-visible:outline-brand-primary-600";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const disabledClass = twMerge(
|
||||
disabled || loading ? "opacity-50 pointer-events-none" : "",
|
||||
);
|
||||
|
||||
const className = twMerge(
|
||||
"text-sm font-medium rounded-lg px-3 py-2 border border-transparent shadow-sm focus-visible:outline focus-visible:outline-0 focus-visible:outline-offset-0 transition-all duration-150 flex gap-2 items-center justify-center",
|
||||
getVariantClassNames(propsVariant),
|
||||
propsClassName,
|
||||
disabledClass,
|
||||
);
|
||||
|
||||
// const ButtonType = htmlFor ? "label" : "button";
|
||||
if (htmlFor) {
|
||||
return (
|
||||
<label className={className} htmlFor={htmlFor} style={rest.style}>
|
||||
{loading && !iconFlip ? (
|
||||
<FontAwesomeIcon icon={faSpinnerThird} className="animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
{icon && !iconFlip ? (
|
||||
<FontAwesomeIcon icon={icon} className={iconClassName} />
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
{children}
|
||||
{loading && iconFlip ? (
|
||||
<FontAwesomeIcon icon={faSpinnerThird} className="animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
{icon && iconFlip ? (
|
||||
<FontAwesomeIcon icon={icon} className={iconClassName} />
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<button
|
||||
className={className}
|
||||
disabled={disabled || loading}
|
||||
{...{ ...rest, htmlFor }}
|
||||
>
|
||||
{loading && !iconFlip ? (
|
||||
<FontAwesomeIcon icon={faSpinnerThird} className="animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
{icon && !iconFlip ? (
|
||||
<FontAwesomeIcon icon={icon} className={iconClassName} />
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
{children}
|
||||
{loading && iconFlip ? (
|
||||
<FontAwesomeIcon icon={faSpinnerThird} className="animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
{icon && iconFlip ? (
|
||||
<FontAwesomeIcon icon={icon} className={iconClassName} />
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./Button";
|
||||
@@ -1,81 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { TransitionDialogue, Button, ButtonProps } from "~/components";
|
||||
|
||||
type UnionedButtonProps = { label?: string } & ButtonProps;
|
||||
|
||||
interface ButtonDialoguePropsI {
|
||||
buttonProps?: UnionedButtonProps;
|
||||
confirmButtonProps?: UnionedButtonProps;
|
||||
closeButtonProps?: UnionedButtonProps;
|
||||
dialogProps?: {
|
||||
className?: string;
|
||||
};
|
||||
title?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
showClose?: boolean;
|
||||
}
|
||||
|
||||
export const ButtonDialogue = ({
|
||||
dialogProps = {},
|
||||
buttonProps: unionedButtonProps,
|
||||
confirmButtonProps,
|
||||
closeButtonProps: unionedCloseButtonProps,
|
||||
title,
|
||||
children,
|
||||
showClose = true,
|
||||
}: ButtonDialoguePropsI) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const closeModal = () => setIsOpen(false);
|
||||
const openModal = () => setIsOpen(true);
|
||||
|
||||
const { label: buttonLabel, ...buttonProps } = unionedButtonProps || {
|
||||
label: "Open",
|
||||
};
|
||||
const { label: closeButtonLabel, ...closeButtonProps } =
|
||||
unionedCloseButtonProps || { label: "Close" };
|
||||
return (
|
||||
<>
|
||||
<Button type="button" onClick={openModal} {...buttonProps}>
|
||||
{buttonLabel}
|
||||
</Button>
|
||||
|
||||
<TransitionDialogue
|
||||
title={title}
|
||||
isOpen={isOpen}
|
||||
onClose={closeModal}
|
||||
className={dialogProps.className}
|
||||
>
|
||||
<div className="mt-2 h-full">{children}</div>
|
||||
|
||||
<div className="mt-6 flex justify-end gap-2">
|
||||
{showClose && (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={closeModal}
|
||||
{...closeButtonProps}
|
||||
variant="secondary"
|
||||
>
|
||||
{closeButtonLabel}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{confirmButtonProps && (
|
||||
<Button
|
||||
type="button"
|
||||
{...confirmButtonProps}
|
||||
onClick={(e) => {
|
||||
if (confirmButtonProps.onClick) {
|
||||
confirmButtonProps.onClick(e);
|
||||
}
|
||||
closeModal();
|
||||
}}
|
||||
>
|
||||
{confirmButtonProps.label ? confirmButtonProps.label : "Confirm"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TransitionDialogue>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { ButtonDialogue } from './ButtonDialogue';
|
||||
+3
-1
@@ -1,3 +1,4 @@
|
||||
// TODO: Move to shared component.
|
||||
import { ButtonHTMLAttributes, Fragment, useState } from "react";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import {
|
||||
@@ -5,7 +6,8 @@ import {
|
||||
faChevronDown,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Button, ButtonProps, TransitionDialogue } from "~/components";
|
||||
import { TransitionDialogue } from "~/components";
|
||||
import { Button, ButtonProps } from "@storyteller/ui-button";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
type UnionedButtonProps = { label?: string } & ButtonProps;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// TODO: create a shared component.
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconProps,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Fragment, useCallback } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { DoNotShow } from "~/constants";
|
||||
import { Button } from "~/components";
|
||||
import { Button } from "@storyteller/ui-button"
|
||||
|
||||
interface Props {
|
||||
text: string;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Button } from "../Button";
|
||||
import { Button } from "@storyteller/ui-button"
|
||||
|
||||
export interface PopoverItem {
|
||||
label: string;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// TODO: DELETE
|
||||
import {
|
||||
faCircleXmark,
|
||||
faFilterList,
|
||||
faSearch,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { Button, Input, Tooltip } from "~/components";
|
||||
import { Input, Tooltip } from "~/components";
|
||||
import { Button } from "@storyteller/ui-button"
|
||||
import debounce from "lodash/debounce";
|
||||
import { useRef, useState, useEffect, useCallback } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
import { Button } from "~/components";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
interface ToggleButtonProps {
|
||||
isActive: boolean;
|
||||
icon?: IconDefinition;
|
||||
activeIcon?: IconDefinition;
|
||||
onClick: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ToggleButton = ({
|
||||
isActive,
|
||||
icon,
|
||||
activeIcon,
|
||||
onClick,
|
||||
className,
|
||||
}: ToggleButtonProps) => {
|
||||
return (
|
||||
<Button
|
||||
className={twMerge(
|
||||
"flex h-9 w-9 items-center border-2 border-transparent text-sm text-white backdrop-blur-lg transition-all",
|
||||
isActive
|
||||
? "border-white/20 bg-brand-primary/40 hover:border-white/30 hover:bg-brand-primary/40"
|
||||
: "bg-[#5F5F68]/60 hover:bg-[#5F5F68]/90",
|
||||
className,
|
||||
)}
|
||||
variant="secondary"
|
||||
icon={isActive && activeIcon ? activeIcon : icon}
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./ToggleButton";
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Button, P, H3 } from "~/components";
|
||||
import { P, H3 } from "~/components";
|
||||
import { FilterEngineCategories } from "~/enums";
|
||||
|
||||
import {
|
||||
Button
|
||||
} from "@storyteller/ui-button"
|
||||
interface Props {
|
||||
onCancel: () => void;
|
||||
onRetry: () => void;
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { Button, H6, Input, ListDropdown } from "~/components";
|
||||
import { H6, Input, ListDropdown } from "~/components";
|
||||
import {
|
||||
Button
|
||||
} from "@storyteller/ui-button"
|
||||
import { FileUploader } from "./FileUploader";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Button } from "~/components";
|
||||
import {
|
||||
Button
|
||||
} from "@storyteller/ui-button"
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { useCallback, useEffect, useState, useRef } from "react";
|
||||
import { Button, H6, Input, Label, ListDropdown } from "~/components";
|
||||
import { H6, Input, Label, ListDropdown } from "~/components";
|
||||
import {
|
||||
Button
|
||||
} from "@storyteller/ui-button"
|
||||
import { FileUploader } from "../UploadModal/FileUploader";
|
||||
import { loadPreviewOnCanvas, snapshotCanvasAsThumbnail } from "./utilities";
|
||||
import { upload3DObjects } from "./utilities/upload3DObjects";
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { Button, H6, Input } from "~/components";
|
||||
import { H6, Input } from "~/components";
|
||||
import { Button } from "@storyteller/ui-button"
|
||||
import { FileUploader } from "../UploadModal/FileUploader";
|
||||
import { uploadPlane } from "./uploadPlane";
|
||||
import { UploaderState } from "~/models";
|
||||
|
||||
+3
-2
@@ -1,8 +1,9 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { Button, H6, Input } from "~/components";
|
||||
import { H6, Input } from "~/components";
|
||||
import { Button } from "@storyteller/ui-button"
|
||||
import { FileUploader } from "../UploadModal/FileUploader";
|
||||
import { uploadPlane } from "./uploadPlane";
|
||||
|
||||
import { UploaderState } from "~/models";
|
||||
import { uploadImage } from "./uploadImage";
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
export * from "./Badge";
|
||||
export * from "./Button";
|
||||
export * from "./ButtonDialogue";
|
||||
export * from "./ButtonDropdown";
|
||||
export * from "./ButtonIcon";
|
||||
export * from "./ButtonIconSelect";
|
||||
@@ -25,7 +23,6 @@ export * from "./Select";
|
||||
export * from "./Slider";
|
||||
export * from "./Tabs";
|
||||
export * from "./Textarea";
|
||||
export * from "./ToggleButton";
|
||||
export * from "./Tooltip";
|
||||
export * from "./TransitionDialogue";
|
||||
export * from "./Typography";
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { faTriangleExclamation } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
import { Button, TransitionDialogue } from "~/components";
|
||||
import { TransitionDialogue } from "~/components";
|
||||
import { Button } from "@storyteller/ui-button"
|
||||
import {
|
||||
showErrorDialog,
|
||||
errorDialogMessage,
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import React from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faLock } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
interface PremiumLockProps {
|
||||
requiredPlan?: "any" | "plus" | "pro" | "elite";
|
||||
children: React.ReactNode;
|
||||
plural?: boolean; // if the message should be plural for multiple features
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// replace with api to check
|
||||
const sessionSubscriptions = {
|
||||
hasPaidFeatures: () => true, // if has any paid features at all
|
||||
hasActivePlusSubscription: () => false, // if has plus or not
|
||||
hasActiveProSubscription: () => false, // if has pro or not
|
||||
hasActiveEliteSubscription: () => false, // if has elite or not
|
||||
};
|
||||
|
||||
export function PremiumLock({
|
||||
requiredPlan = "plus",
|
||||
plural = false,
|
||||
children,
|
||||
className,
|
||||
}: PremiumLockProps) {
|
||||
const getPlanMessage = (plan: string) => {
|
||||
switch (plan) {
|
||||
case "any":
|
||||
return "premium";
|
||||
case "plus":
|
||||
return "premium plus";
|
||||
case "pro":
|
||||
return "premium pro";
|
||||
case "elite":
|
||||
return "premium elite";
|
||||
default:
|
||||
return "premium";
|
||||
}
|
||||
};
|
||||
|
||||
const hasAccess = () => {
|
||||
switch (requiredPlan) {
|
||||
case "any":
|
||||
return sessionSubscriptions.hasPaidFeatures();
|
||||
case "plus":
|
||||
return (
|
||||
sessionSubscriptions.hasActivePlusSubscription() ||
|
||||
sessionSubscriptions.hasActiveProSubscription() ||
|
||||
sessionSubscriptions.hasActiveEliteSubscription()
|
||||
);
|
||||
case "pro":
|
||||
return (
|
||||
sessionSubscriptions.hasActiveProSubscription() ||
|
||||
sessionSubscriptions.hasActiveEliteSubscription()
|
||||
);
|
||||
case "elite":
|
||||
return sessionSubscriptions.hasActiveEliteSubscription();
|
||||
default:
|
||||
return sessionSubscriptions.hasPaidFeatures();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasAccess() ? (
|
||||
children
|
||||
) : (
|
||||
<div
|
||||
className={twMerge(
|
||||
"relative overflow-hidden rounded-lg bg-ui-controls px-3",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="pointer-events-none pb-3 pt-10">{children}</div>
|
||||
|
||||
<div className="absolute left-0 top-0 z-10 flex h-full w-full rounded-lg border border-brand-primary/40 bg-brand-secondary/60 pl-3 text-white/90">
|
||||
<div className="mt-2 flex justify-center">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faLock} className="me-2 text-sm" />
|
||||
|
||||
<span className="text-sm font-medium">
|
||||
{plural
|
||||
? "These features require "
|
||||
: "This feature requires "}
|
||||
<Link
|
||||
to="/pricing"
|
||||
className="text-brand-primary brightness-125 transition-all hover:brightness-150"
|
||||
>
|
||||
{getPlanMessage(requiredPlan)}
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./PremiumLock";
|
||||
@@ -1,104 +0,0 @@
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faHexagonXmark,
|
||||
faTriangleExclamation,
|
||||
faXmark,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { ToastTypes } from "~/enums";
|
||||
import { toasts, deleteToast } from "~/signals";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
export type ToastDataType = {
|
||||
type: ToastTypes;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export const Toaster = () => {
|
||||
useSignals();
|
||||
const TITLES = {
|
||||
[ToastTypes.ERROR]: "Error!",
|
||||
[ToastTypes.WARNING]: "Warning!",
|
||||
[ToastTypes.SUCCESS]: "Success",
|
||||
};
|
||||
|
||||
const ICONS: Record<string, React.ReactNode> = {
|
||||
error: <FontAwesomeIcon icon={faHexagonXmark} className="text-[#FF5F5F]" />,
|
||||
warning: (
|
||||
<FontAwesomeIcon
|
||||
icon={faTriangleExclamation}
|
||||
className="text-[#FFDC5F]"
|
||||
/>
|
||||
),
|
||||
success: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
className="h-4 w-4"
|
||||
>
|
||||
<path
|
||||
opacity="1"
|
||||
d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c-9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"
|
||||
fill="#3ACA86"
|
||||
/>
|
||||
<path
|
||||
d="M369 175c-9.4 9.4-9.4 24.6 0 33.9L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c-9.4-9.4 24.6-9.4 33.9 0z"
|
||||
fill="#FFFFFF"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
};
|
||||
|
||||
const toastTypeToKey: Record<ToastTypes, string> = {
|
||||
[ToastTypes.ERROR]: "error",
|
||||
[ToastTypes.WARNING]: "warning",
|
||||
[ToastTypes.SUCCESS]: "success",
|
||||
};
|
||||
|
||||
const toastRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
const [heights, setHeights] = useState<number[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const newHeights = toastRefs.current.map((ref) => ref?.offsetHeight || 0);
|
||||
setHeights(newHeights);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [toasts.value]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{toasts.value.map((toast, index) => (
|
||||
<div
|
||||
key={toast.id}
|
||||
ref={(el) => (toastRefs.current[index] = el)}
|
||||
className="absolute z-50 w-[360px] animate-fadeIn rounded-lg border border-[#5E5E7C] bg-ui-controls p-3 shadow-xl"
|
||||
style={{
|
||||
top: heights
|
||||
.slice(0, index)
|
||||
.reduce((acc, height) => acc + height + 8, 52),
|
||||
right: 8,
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-between rounded-lg">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
{ICONS[toastTypeToKey[toast.type]]}
|
||||
<div className="text-base font-bold text-white">
|
||||
{TITLES[toast.type]}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-left text-sm text-white opacity-75">
|
||||
{toast.message}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => deleteToast(toast.id)}
|
||||
className="ml-4 text-xl font-bold text-white opacity-50"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -2,7 +2,7 @@ import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { faFilm } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Button } from "~/components";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { generateMovieId, viewMyMovies } from "~/pages/PageEnigma/signals";
|
||||
|
||||
import { activeWorkflowJobs } from "~/signals";
|
||||
|
||||
@@ -5,7 +5,9 @@ import {
|
||||
faImages,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { useLocation, useParams } from "react-router-dom";
|
||||
import { Button, ButtonLink } from "~/components";
|
||||
import { Button } from "@storyteller/ui-button"
|
||||
|
||||
import { ButtonLink } from "~/components";
|
||||
import { AuthButtons } from "./AuthButtons";
|
||||
import { SceneTitleInput } from "./SceneTitleInput";
|
||||
import { getCurrentLocationWithoutParams } from "~/utilities";
|
||||
|
||||
-160
@@ -1,160 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { faCirclePlus, faSpinnerThird } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import {
|
||||
Button,
|
||||
FileUploader,
|
||||
H2,
|
||||
TransitionDialogue,
|
||||
WaveformPlayer,
|
||||
} from "~/components";
|
||||
|
||||
import { MediaUploadApi } from "~/Classes/ApiManager/MediaUploadApi";
|
||||
import { AUDIO_FILE_TYPE, ToastTypes } from "~/enums";
|
||||
export const FILE_TYPES = Object.keys(AUDIO_FILE_TYPE);
|
||||
|
||||
import { getFileName } from "~/utilities";
|
||||
import { addToast } from "~/signals";
|
||||
|
||||
type ComponentState = {
|
||||
isOpen: boolean;
|
||||
file: File | undefined;
|
||||
audioUrl: string;
|
||||
uploadState: "init" | "staged" | "uploading" | "uploaded" | "error";
|
||||
fileToken: string | undefined;
|
||||
};
|
||||
const initialValues: ComponentState = {
|
||||
isOpen: false,
|
||||
file: undefined,
|
||||
audioUrl: "",
|
||||
uploadState: "init",
|
||||
fileToken: undefined,
|
||||
};
|
||||
|
||||
export const UploadAudioButtonDialogue = ({
|
||||
onUploaded,
|
||||
}: {
|
||||
onUploaded: (fileToken: string) => void;
|
||||
}) => {
|
||||
const [{ isOpen, file, audioUrl, uploadState, fileToken }, setState] =
|
||||
useState<ComponentState>(initialValues);
|
||||
|
||||
const closeModal = () => setState(initialValues);
|
||||
const openModal = () => setState((curr) => ({ ...curr, isOpen: true }));
|
||||
|
||||
const handleFileChange = (file: File) => {
|
||||
setState((curr) => ({
|
||||
...curr,
|
||||
file: file,
|
||||
audioUrl: URL.createObjectURL(file),
|
||||
uploadState: "staged",
|
||||
}));
|
||||
};
|
||||
const handleClear = () => {
|
||||
setState({
|
||||
...initialValues,
|
||||
isOpen: true,
|
||||
});
|
||||
};
|
||||
const submitUpload = async () => {
|
||||
if (!file) {
|
||||
//TODO: Maybe more verbose on not having file
|
||||
return;
|
||||
}
|
||||
setState((curr) => ({
|
||||
...curr,
|
||||
uploadState: "uploading",
|
||||
}));
|
||||
const mediaApi = new MediaUploadApi();
|
||||
|
||||
const request = {
|
||||
blob: file,
|
||||
fileName: getFileName(file),
|
||||
uuid: uuidv4(),
|
||||
maybe_title: getFileName(file),
|
||||
};
|
||||
const response = await mediaApi.UploadAudio(request);
|
||||
if (response.success && response.data) {
|
||||
setState((curr) => ({
|
||||
...curr,
|
||||
uploadState: "uploaded",
|
||||
fileToken: response.data,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
//handle errors
|
||||
addToast(
|
||||
ToastTypes.ERROR,
|
||||
response.errorMessage || "Unknown Error: Upload Audio",
|
||||
);
|
||||
setState((curr) => ({
|
||||
...curr,
|
||||
uploadState: "staged",
|
||||
}));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (fileToken && uploadState === "uploaded") {
|
||||
//calling the callback on a successful upload
|
||||
onUploaded(fileToken);
|
||||
}
|
||||
}, [fileToken, uploadState, onUploaded]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
className="grow py-3 text-sm font-medium"
|
||||
variant="action"
|
||||
icon={faCirclePlus}
|
||||
type="button"
|
||||
onClick={openModal}
|
||||
>
|
||||
Upload Audio
|
||||
</Button>
|
||||
<TransitionDialogue
|
||||
isOpen={isOpen}
|
||||
onClose={closeModal}
|
||||
title={<H2>Upload Audio</H2>}
|
||||
>
|
||||
<div className="flex w-full flex-col gap-3">
|
||||
<FileUploader
|
||||
file={file}
|
||||
fileTypes={FILE_TYPES}
|
||||
handleChange={handleFileChange}
|
||||
/>
|
||||
|
||||
{file && (
|
||||
<div className="flex items-center gap-3 rounded-lg bg-brand-secondary p-3">
|
||||
<div className="grow">
|
||||
<WaveformPlayer audio={audioUrl} hasPlayButton />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-3">
|
||||
<Button variant="secondary" onClick={closeModal}>
|
||||
{uploadState === "uploaded" ? "Close" : "Cancel"}
|
||||
</Button>
|
||||
{uploadState !== "uploaded" && (
|
||||
<Button
|
||||
onClick={submitUpload}
|
||||
disabled={uploadState !== "staged"}
|
||||
>
|
||||
Upload
|
||||
{uploadState === "uploading" && (
|
||||
<FontAwesomeIcon icon={faSpinnerThird} spin />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
{uploadState === "uploaded" && (
|
||||
<Button onClick={handleClear}>Upload Another</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TransitionDialogue>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { UploadAudioButtonDialogue } from "./UploadAudioButtonDialogue";
|
||||
-202
@@ -1,202 +0,0 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { FileUploader } from "react-drag-drop-files";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import {
|
||||
faCheck,
|
||||
faCircleXmark,
|
||||
faFileArrowUp,
|
||||
faFileAudio,
|
||||
faSpinnerThird,
|
||||
faXmark,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Button, ButtonIcon, P, Tooltip, WaveformPlayer } from "~/components";
|
||||
import { MediaUploadApi } from "~/Classes/ApiManager";
|
||||
import { getFileName } from "~/utilities";
|
||||
import { AUDIO_FILE_TYPE, ToastTypes } from "~/enums";
|
||||
import { addToast } from "~/signals";
|
||||
const FILE_TYPES = Object.keys(AUDIO_FILE_TYPE);
|
||||
|
||||
interface Props {
|
||||
onFileStaged?: (file: File) => void;
|
||||
onClear?: () => void;
|
||||
onFileUploaded: (token: string) => void;
|
||||
}
|
||||
|
||||
export const UploadAudioComponent = ({
|
||||
onFileStaged,
|
||||
onClear,
|
||||
onFileUploaded,
|
||||
}: Props) => {
|
||||
const [{ file, uploadState }, setState] = useState<{
|
||||
file: File | undefined;
|
||||
uploadState: "none" | "staged" | "uploading" | "uploaded" | "error";
|
||||
}>({
|
||||
file: undefined,
|
||||
uploadState: "none",
|
||||
});
|
||||
|
||||
const audioUrl = file ? URL.createObjectURL(file) : "";
|
||||
|
||||
const handleChange = (file: File) => {
|
||||
setState((curr) => ({
|
||||
...curr,
|
||||
file: file,
|
||||
uploadState: "staged",
|
||||
}));
|
||||
if (onFileStaged) {
|
||||
onFileStaged(file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
setState((curr) => ({
|
||||
...curr,
|
||||
file: undefined,
|
||||
uploadState: "none",
|
||||
uploadToken: undefined,
|
||||
}));
|
||||
if (onClear) {
|
||||
onClear();
|
||||
}
|
||||
};
|
||||
|
||||
const handleUploadFile = useCallback(
|
||||
async (file: File) => {
|
||||
setState((curr) => ({ ...curr, uploadState: "uploading" }));
|
||||
|
||||
const mediaUploadApi = new MediaUploadApi();
|
||||
const request = {
|
||||
blob: file,
|
||||
fileName: getFileName(file),
|
||||
uuid: uuidv4(),
|
||||
maybe_title: getFileName(file),
|
||||
};
|
||||
const response = await mediaUploadApi.UploadAudio(request);
|
||||
if (response.success && response.data) {
|
||||
setState((curr) => ({
|
||||
...curr,
|
||||
uploadState: "uploaded",
|
||||
}));
|
||||
onFileUploaded(response.data);
|
||||
return;
|
||||
}
|
||||
//handle errors
|
||||
addToast(
|
||||
ToastTypes.ERROR,
|
||||
response.errorMessage || "Unknown Error: Upload Audio",
|
||||
);
|
||||
setState((curr) => ({
|
||||
...curr,
|
||||
uploadState: "staged",
|
||||
}));
|
||||
},
|
||||
[onFileUploaded],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* Usage refer to https://github.com/KarimMokhtar/react-drag-drop-files */}
|
||||
<FileUploader
|
||||
handleChange={handleChange}
|
||||
name="file"
|
||||
types={FILE_TYPES}
|
||||
maxSize={50}
|
||||
>
|
||||
<DragAndDropZone file={file} />
|
||||
</FileUploader>
|
||||
|
||||
{file && (
|
||||
<>
|
||||
<div className="flex items-center gap-3 rounded-lg bg-brand-secondary p-3">
|
||||
<div className="grow">
|
||||
<WaveformPlayer audio={audioUrl} hasPlayButton />
|
||||
</div>
|
||||
<Tooltip content="Remove" position="top">
|
||||
<ButtonIcon
|
||||
icon={faXmark}
|
||||
onClick={handleClear}
|
||||
className="h-auto w-auto bg-transparent p-0 text-xl opacity-60 hover:bg-transparent hover:opacity-90"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
className="w-full py-2.5"
|
||||
variant="action"
|
||||
onClick={() => {
|
||||
if (!file) {
|
||||
//TODO: Maybe more verbose on not having file
|
||||
return false;
|
||||
}
|
||||
handleUploadFile(file);
|
||||
}}
|
||||
disabled={uploadState !== "staged"}
|
||||
icon={
|
||||
uploadState === "uploaded"
|
||||
? faCheck
|
||||
: uploadState === "error"
|
||||
? faCircleXmark
|
||||
: faFileArrowUp
|
||||
}
|
||||
>
|
||||
{uploadState === "uploaded"
|
||||
? "Uploaded"
|
||||
: uploadState === "error"
|
||||
? "Upload Error"
|
||||
: "Upload Audio"}
|
||||
{uploadState === "uploading" && (
|
||||
<FontAwesomeIcon icon={faSpinnerThird} spin />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DragAndDropZone = ({ file }: { file: File | undefined }) => {
|
||||
const fileSize =
|
||||
file && file.size >= 1024 * 1024
|
||||
? (file.size / 1024 / 1024).toFixed(2) + " MB"
|
||||
: file
|
||||
? `${Math.floor(file.size / 1024)} KB`
|
||||
: null;
|
||||
const fileName =
|
||||
file && file.name ? file.name.split(".")[0].toUpperCase() : "";
|
||||
|
||||
if (!file) {
|
||||
return (
|
||||
<div className="flex cursor-pointer items-center gap-3.5 rounded-lg border border-dashed border-[#363636] bg-brand-secondary p-3">
|
||||
<FontAwesomeIcon icon={faFileArrowUp} className="text-4xl" />
|
||||
<div className="flex flex-col gap-0">
|
||||
<P className="font-medium">
|
||||
<u>Upload a file</u> or drop it here
|
||||
</P>
|
||||
<P className="flex items-center gap-2 text-sm font-normal opacity-50">
|
||||
{FILE_TYPES.join(", ").toString()} supported
|
||||
</P>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="flex cursor-pointer items-center gap-3.5 rounded-lg border border-dashed border-[#363636] bg-brand-secondary p-3">
|
||||
<FontAwesomeIcon icon={faFileAudio} className="text-4xl" />
|
||||
<div className="flex flex-col gap-0">
|
||||
<P className="font-medium">
|
||||
{file.name.slice(0, file.name.lastIndexOf("."))}
|
||||
</P>
|
||||
<P className="flex items-center gap-2 text-sm font-normal">
|
||||
<span className="opacity-50">
|
||||
{`${fileName} file size: ${fileSize} `}
|
||||
</span>
|
||||
<u className="transition-all hover:text-white/80">Change File</u>
|
||||
</P>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { UploadAudioComponent } from "./UploadAudioComponent";
|
||||
@@ -1,8 +1,3 @@
|
||||
export * from "./ErrorDialog";
|
||||
export * from "./Toaster";
|
||||
export * from "./TopBar";
|
||||
export * from "./PremiumLock";
|
||||
|
||||
// all the Upload stuff will depend on the API Manager Signal
|
||||
export * from "./UploadAudioButtonDialogue";
|
||||
export * from "./UploadAudioComponent";
|
||||
|
||||
@@ -10,7 +10,7 @@ import "@fortawesome/fontawesome-svg-core/styles.css";
|
||||
import { config } from "@fortawesome/fontawesome-svg-core";
|
||||
import EnvironmentVariables from "~/Classes/EnvironmentVariables";
|
||||
import { pageHeight, pageWidth, persistLogin } from "~/signals";
|
||||
import { showWizard } from "~/pages/PageEnigma/Wizard/signals/wizard";
|
||||
|
||||
import { posthog } from "posthog-js";
|
||||
|
||||
config.autoAddCss = false; /* eslint-disable import/first */
|
||||
@@ -55,17 +55,9 @@ const GlobalSettingsManager = ({ env }: { env: Record<string, string> }) => {
|
||||
pageWidth.value = window.innerWidth;
|
||||
}
|
||||
|
||||
function initWizard() {
|
||||
if (showWizard.value) {
|
||||
return;
|
||||
}
|
||||
const wizard = localStorage.getItem("storyteller-wizard");
|
||||
showWizard.value = wizard ? "" : "initial";
|
||||
localStorage.setItem("storyteller-wizard", "shown");
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
initWizard();
|
||||
|
||||
setPage();
|
||||
window.addEventListener("resize", setPage);
|
||||
return () => {
|
||||
|
||||
@@ -13,7 +13,8 @@ import { AUTH_STATUS } from "~/enums";
|
||||
import { authentication, login, logout } from "~/signals";
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
import { Button, Input, ConfirmationModal } from "~/components";
|
||||
import { Input, ConfirmationModal } from "~/components";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
|
||||
import "./styles/normalize.css";
|
||||
import "./styles/tailwind.css";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { useState, useRef } from "react";
|
||||
|
||||
export interface LabeledSelectProps {
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { faArrowsRotate } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
|
||||
export interface ToggleButtonProps {
|
||||
label?: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
options?: string[];
|
||||
icons?: { [key: string]: IconDefinition };
|
||||
}
|
||||
|
||||
export const ToggleButton = ({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
options = ["1", "2", "3"],
|
||||
icons,
|
||||
}: ToggleButtonProps) => {
|
||||
const handleClick = () => {
|
||||
const currentIndex = options.indexOf(value);
|
||||
const nextIndex = (currentIndex + 1) % options.length;
|
||||
onChange(options[nextIndex]);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
icon={icons?.[value] ?? faArrowsRotate}
|
||||
variant="secondary"
|
||||
className="glass px-3 py-1.5"
|
||||
onClick={handleClick}
|
||||
>
|
||||
{label && (
|
||||
<>
|
||||
{label} <span className="opacity-60">—</span>
|
||||
</>
|
||||
)}{" "}
|
||||
{value}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
-261
@@ -1,261 +0,0 @@
|
||||
import { ImageFormat } from "./video_audio_preprocessor";
|
||||
import * as THREE from "three";
|
||||
import { FrameWorkerManager } from "./workers/frame_worker_manager";
|
||||
|
||||
import { BlobReader, BlobWriter, ZipWriter } from "@zip.js/zip.js";
|
||||
|
||||
export interface IEngineFrameBuffers {
|
||||
colorFrames: (Blob | null)[];
|
||||
clearBuffer(name: BufferType): Promise<void>;
|
||||
checkIfBufferIsEmpty(buffer: BufferType): Promise<boolean>;
|
||||
countNonNullFrames(buffer: BufferType): Promise<number>;
|
||||
}
|
||||
|
||||
export enum BufferType {
|
||||
COLOR = "colorFrames",
|
||||
}
|
||||
|
||||
export interface EngineFrameBuffersError {
|
||||
message: string;
|
||||
code: number;
|
||||
}
|
||||
|
||||
export class EngineFrameBuffers implements IEngineFrameBuffers {
|
||||
colorFrames: (Blob | null)[];
|
||||
|
||||
private numberOfFrames: number;
|
||||
public hasCache: boolean;
|
||||
public colorRenderQueue: Promise<Blob>[];
|
||||
private webWorkers: boolean;
|
||||
|
||||
public payloadZip: Blob | null;
|
||||
|
||||
public frameWorkerManager: FrameWorkerManager | undefined;
|
||||
|
||||
// for the client side zip.
|
||||
private zipFileWriter: BlobWriter | undefined;
|
||||
private zipWriter: ZipWriter<Blob> | undefined;
|
||||
|
||||
private imageType = "image/jpeg";
|
||||
constructor(width: number, height: number) {
|
||||
this.colorFrames = [];
|
||||
this.colorRenderQueue = [];
|
||||
this.payloadZip = null;
|
||||
this.hasCache = false;
|
||||
|
||||
this.zipFileWriter = undefined;
|
||||
this.zipWriter = undefined;
|
||||
|
||||
//Check for SharedWorker support
|
||||
if (typeof Worker === "undefined") {
|
||||
console.error("Web Workers is not supported in this browser.");
|
||||
this.webWorkers = false;
|
||||
} else {
|
||||
this.webWorkers = true;
|
||||
console.error("Web Workers is supported in this browser.");
|
||||
this.frameWorkerManager = new FrameWorkerManager(
|
||||
width,
|
||||
height,
|
||||
"image/png",
|
||||
);
|
||||
}
|
||||
|
||||
this.webWorkers = false;
|
||||
|
||||
this.imageType = "image/jpeg";
|
||||
|
||||
if (this.webWorkers == false) {
|
||||
this.zipFileWriter = new BlobWriter(this.imageType);
|
||||
this.zipWriter = new ZipWriter(this.zipFileWriter);
|
||||
}
|
||||
|
||||
this.numberOfFrames = 0;
|
||||
}
|
||||
|
||||
async retrieveFrame(
|
||||
rawRenderer: THREE.WebGLRenderer,
|
||||
imageFormat: ImageFormat,
|
||||
): Promise<Blob> {
|
||||
return new Promise((resolve, reject) => {
|
||||
rawRenderer.domElement.toBlob(
|
||||
(blob) => {
|
||||
if (blob) {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
//const imgData = reader.result as string;
|
||||
// if needed as a base 64
|
||||
resolve(blob);
|
||||
};
|
||||
reader.onerror = () => {
|
||||
reject({
|
||||
message: "Failed to Retrieve Frame and Serialize it.",
|
||||
code: 100,
|
||||
});
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
}
|
||||
},
|
||||
imageFormat,
|
||||
1.0,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async setRenderSurfaceSize(width: number, height: number) {
|
||||
if (this.webWorkers && this.frameWorkerManager) {
|
||||
this.frameWorkerManager.setCanvas(width, height);
|
||||
}
|
||||
}
|
||||
|
||||
async enqueueWork(
|
||||
rawRenderer: THREE.WebGLRenderer,
|
||||
scene: THREE.Scene,
|
||||
camera: THREE.PerspectiveCamera,
|
||||
): Promise<void> {
|
||||
this.numberOfFrames += 1;
|
||||
if (this.webWorkers && this.frameWorkerManager) {
|
||||
this.frameWorkerManager.render(scene, camera, this.numberOfFrames);
|
||||
} else {
|
||||
const colorRenderTask = this.retrieveFrame(rawRenderer, ImageFormat.JPEG);
|
||||
this.colorRenderQueue.push(colorRenderTask);
|
||||
}
|
||||
}
|
||||
|
||||
private downloadBlob(blob: Blob, filename: string): void {
|
||||
// Create an object URL for the Blob
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
// Create a temporary anchor element
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename; // Specify the filename
|
||||
|
||||
// Append the anchor to the document body
|
||||
document.body.appendChild(a);
|
||||
|
||||
// Trigger a click event on the anchor
|
||||
a.click();
|
||||
|
||||
// Clean up by revoking the object URL and removing the anchor element
|
||||
URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
|
||||
async equeueAudio(audio: Blob) {}
|
||||
|
||||
async collectColorFrames(final_length: number): Promise<void> {
|
||||
try {
|
||||
// webworker requires the COEP.
|
||||
if (this.webWorkers) {
|
||||
console.time("ColorQueueRenderTime Worker Time");
|
||||
if (this.webWorkers && this.frameWorkerManager) {
|
||||
console.log(`Collected Number of Frames: ${final_length}`);
|
||||
// this is sent off to video generation ...
|
||||
this.payloadZip =
|
||||
await this.frameWorkerManager.collectedFrames(final_length);
|
||||
}
|
||||
console.timeEnd("ColorQueueRenderTime Worker Time");
|
||||
} else {
|
||||
console.time("ColorQueueRenderTime");
|
||||
try {
|
||||
// going to have to zip here with the audio
|
||||
if (this.webWorkers == false) {
|
||||
console.log("Using Other Pathway.");
|
||||
this.colorFrames = await Promise.all(this.colorRenderQueue);
|
||||
console.log(this.colorFrames);
|
||||
// Check if zipFileWriter or zipWriter is undefined or null
|
||||
if (!this.zipFileWriter || !this.zipWriter) {
|
||||
console.log("Failed to Startup Zip File Writer");
|
||||
return;
|
||||
}
|
||||
|
||||
this.colorFrames.forEach(async (blob, idx) => {
|
||||
try {
|
||||
if (!blob || !this.zipFileWriter) {
|
||||
console.log("Failed to get Blob and Zip File Writer");
|
||||
return;
|
||||
}
|
||||
|
||||
//console.log(`Zipping Frame ${idx} to Zip`);
|
||||
const name = String(idx).padStart(5, "0"); // '0009'
|
||||
if (this.zipWriter) {
|
||||
await this.zipWriter.add(`${name}.jpg`, new BlobReader(blob));
|
||||
} else {
|
||||
console.error("zipWriter is undefined");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
|
||||
this.payloadZip = await this.zipWriter.close();
|
||||
//this.downloadBlob(this.payloadZip, "test.zip");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error in Collecting Color Frames", error);
|
||||
}
|
||||
console.timeEnd("ColorQueueRenderTime");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
this.hasCache = false;
|
||||
}
|
||||
|
||||
// Testing function
|
||||
public downloadImageBlob(blob: Blob, filename: string) {
|
||||
// Create a URL for the blob
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
// Create a temporary anchor element
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
|
||||
// Append the anchor to the document body
|
||||
document.body.appendChild(a);
|
||||
|
||||
// Trigger a click event on the anchor
|
||||
a.click();
|
||||
|
||||
// Remove the anchor from the document
|
||||
document.body.removeChild(a);
|
||||
|
||||
// Revoke the object URL to free up memory
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
async clearBuffer(name: BufferType): Promise<void> {
|
||||
switch (name) {
|
||||
case BufferType.COLOR:
|
||||
this.colorFrames = [];
|
||||
this.colorRenderQueue = [];
|
||||
this.payloadZip = null;
|
||||
this.numberOfFrames = 0;
|
||||
|
||||
// reset the writers
|
||||
this.zipFileWriter = new BlobWriter(this.imageType);
|
||||
this.zipWriter = new ZipWriter(this.zipFileWriter);
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new Error("Invalid buffer name");
|
||||
}
|
||||
}
|
||||
|
||||
async countNonNullFrames(buffer: BufferType): Promise<number> {
|
||||
return this[buffer].filter((frame) => frame !== null).length;
|
||||
}
|
||||
|
||||
async checkIfBufferIsEmpty(buffer: BufferType): Promise<boolean> {
|
||||
return this[buffer].every((frame) => frame === null);
|
||||
}
|
||||
|
||||
async logBufferInfo(buffer: BufferType): Promise<void> {
|
||||
const nonNullFrameCount = await this.countNonNullFrames(buffer);
|
||||
console.log(
|
||||
`Buffer Info - ${buffer}: ${nonNullFrameCount} non-null frames`,
|
||||
);
|
||||
}
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
// import { VideoAudioPreProcessor } from "app/pages/PageEnigma/Editor/VideoProcessor/video_audio_preprocessor.ts";
|
||||
|
||||
// const processor = new VideoAudioPreProcessor();
|
||||
|
||||
// // chrome://inspect/#workers debug shared worker.
|
||||
// self.addEventListener("connect", function (event: MessageEvent) {
|
||||
// const port = (event as MessageEvent & { ports: MessagePort[] }).ports[0];
|
||||
|
||||
// port.addEventListener("message", function (event: MessageEvent) {
|
||||
// console.log("Message received from main script:", event.data);
|
||||
// port.postMessage("Hello from the Shared Worker!");
|
||||
// });
|
||||
|
||||
// port.start();
|
||||
// });
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
class VideoProcessingSharedWorkerManager {
|
||||
constructor() {
|
||||
if (typeof SharedWorker !== "undefined") {
|
||||
console.log("Shared Workers are supported.");
|
||||
// You can safely create a Shared Worker here
|
||||
const worker = new SharedWorker(
|
||||
"app/pages/PageEnigma/Editor/VideoProcessor/shared_workers/video_shared_worker_manager.ts",
|
||||
{ type: "module" },
|
||||
);
|
||||
|
||||
// Get the port for communication
|
||||
const port = worker.port;
|
||||
|
||||
// Listen for messages from the worker
|
||||
port.addEventListener("message", function (event) {
|
||||
console.log("Message received from worker:", event.data);
|
||||
});
|
||||
|
||||
// Start the port
|
||||
port.start();
|
||||
|
||||
// Send a message to the worker
|
||||
port.postMessage("Hello from the main script!");
|
||||
} else {
|
||||
console.log("Shared Workers are not supported in this browser.");
|
||||
}
|
||||
}
|
||||
async enqueueWork(frames: [string]) {}
|
||||
|
||||
async completedWork() {}
|
||||
}
|
||||
-567
@@ -1,567 +0,0 @@
|
||||
import { MediaFilesApi } from "~/Classes/ApiManager/MediaFilesApi";
|
||||
import { BucketConfig } from "~/api/BucketConfig";
|
||||
// import { createFFmpeg, fetchFile, FFmpeg } from "@ffmpeg/ffmpeg";
|
||||
import { ClipUI } from "../../clips/clip_ui";
|
||||
|
||||
export interface VideoPreProcessorError {
|
||||
message: string;
|
||||
code: number;
|
||||
}
|
||||
|
||||
export interface ProcessStatus {
|
||||
message: string;
|
||||
percent: string;
|
||||
}
|
||||
|
||||
export interface StreamingProgressResponse<T> {
|
||||
success: boolean;
|
||||
errorMessage?: string;
|
||||
data?: T;
|
||||
}
|
||||
|
||||
interface VideoPreProcessorOutputProperties {
|
||||
frameRate: number;
|
||||
imageFormat: ImageFormat;
|
||||
timelineTrackLengthSeconds: number;
|
||||
}
|
||||
|
||||
export interface AudioBuffer {
|
||||
buffer: AudioClipInfo[];
|
||||
}
|
||||
|
||||
// This is needed from the audio clip
|
||||
export interface AudioClipInfo {
|
||||
media_id: string; // file location
|
||||
offset: number; // in frames
|
||||
length: number; // in frames
|
||||
}
|
||||
|
||||
export enum ImageFormat {
|
||||
JPEG = "image/jpeg",
|
||||
PNG = "image/png",
|
||||
}
|
||||
|
||||
// 44099.10498046875 for the image buffer loop and memory pressure.
|
||||
export class VideoAudioPreProcessor {
|
||||
// ffmpeg: FFmpeg;
|
||||
properties: VideoPreProcessorOutputProperties;
|
||||
onProgress: (response: StreamingProgressResponse<ProcessStatus>) => void;
|
||||
mediaFilesApi: MediaFilesApi;
|
||||
|
||||
bucketConfig: BucketConfig;
|
||||
|
||||
constructor(
|
||||
onProgress: (response: StreamingProgressResponse<ProcessStatus>) => void,
|
||||
properties: VideoPreProcessorOutputProperties = {
|
||||
frameRate: 60,
|
||||
imageFormat: ImageFormat.PNG,
|
||||
timelineTrackLengthSeconds: 7,
|
||||
},
|
||||
) {
|
||||
// this.ffmpeg = createFFmpeg({ log: true, logger: (p) => console.log(p) });
|
||||
this.properties = properties;
|
||||
this.onProgress = onProgress;
|
||||
this.mediaFilesApi = new MediaFilesApi();
|
||||
|
||||
this.bucketConfig = new BucketConfig();
|
||||
}
|
||||
|
||||
// You have to call this early on because it can take a while to load from the CDN.
|
||||
async initialize() {
|
||||
// if (this.ffmpeg.isLoaded() == false) {
|
||||
// await this.ffmpeg.load();
|
||||
// console.log("Video Generation: Loaded FFMPEG");
|
||||
// } else {
|
||||
// console.log("Video Generation: Already Loaded");
|
||||
// }
|
||||
}
|
||||
|
||||
// This retrieves by creating dom elements
|
||||
|
||||
async removeInMemoryWavs(): Promise<void> {
|
||||
try {
|
||||
// const fileNames = this.ffmpeg.FS("readdir", "/");
|
||||
// const extension = ".wav";
|
||||
// for (const fileName of fileNames) {
|
||||
// if (fileName.endsWith(extension)) {
|
||||
// await this.ffmpeg.FS("unlink", fileName);
|
||||
// }
|
||||
// }
|
||||
} catch (error) {
|
||||
const processError: VideoPreProcessorError = {
|
||||
message: "Failed to delete files from FFmpeg memory store:",
|
||||
code: 101,
|
||||
};
|
||||
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
console.error(errorMessage);
|
||||
|
||||
throw processError;
|
||||
}
|
||||
}
|
||||
async removeInMemoryImages(): Promise<void> {
|
||||
try {
|
||||
// const fileNames = this.ffmpeg.FS("readdir", "/");
|
||||
const imageFormat = this.properties.imageFormat;
|
||||
let extension = "";
|
||||
if (imageFormat == ImageFormat.JPEG) {
|
||||
extension = ".jpeg";
|
||||
} else {
|
||||
extension = ".png";
|
||||
}
|
||||
|
||||
// for (const fileName of fileNames) {
|
||||
// if (fileName.endsWith(extension)) {
|
||||
// await this.ffmpeg.FS("unlink", fileName);
|
||||
// }
|
||||
// }
|
||||
} catch (error) {
|
||||
const processError: VideoPreProcessorError = {
|
||||
message: "Failed to delete files from FFmpeg memory store:",
|
||||
code: 101,
|
||||
};
|
||||
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
console.error(errorMessage);
|
||||
|
||||
throw processError;
|
||||
}
|
||||
}
|
||||
|
||||
async removeAllVideos(): Promise<void> {
|
||||
try {
|
||||
const fileNames = this.ffmpeg.FS("readdir", "/");
|
||||
for (const fileName of fileNames) {
|
||||
if (fileName.endsWith(".mp4")) {
|
||||
await this.ffmpeg.FS("unlink", fileName);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to delete files from FFmpeg memory store:", error);
|
||||
|
||||
const processError: VideoPreProcessorError = {
|
||||
message: "Failed to delete files from FFmpeg memory store:",
|
||||
code: 102,
|
||||
};
|
||||
throw processError;
|
||||
}
|
||||
}
|
||||
|
||||
//Used in the case where the CDN does not properly return the core
|
||||
// If this fails then keep trying during the generation process.
|
||||
async preConditionCheck(): Promise<boolean> {
|
||||
if (this.ffmpeg.isLoaded() == false) {
|
||||
await this.ffmpeg.load();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async setOutputProperties(properties: VideoPreProcessorOutputProperties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
// Just Encase Clip UI Changes we have this as a proxy to keep things going as per usual
|
||||
public async audioClipsToAudioBuffer(
|
||||
audioClips: ClipUI[],
|
||||
): Promise<AudioBuffer> {
|
||||
const audioClipInfos = audioClips.map((val) => {
|
||||
return {
|
||||
media_id: val.media_id, // file location
|
||||
offset: val.offset, // in frames
|
||||
length: val.length, // in frames
|
||||
};
|
||||
});
|
||||
return { buffer: audioClipInfos };
|
||||
}
|
||||
|
||||
async combineAudioAndVideo(
|
||||
audioTrackFileName: string,
|
||||
colorVideoFileName: string,
|
||||
outputFileName: string,
|
||||
): Promise<Blob> {
|
||||
try {
|
||||
// Error ahndle properly
|
||||
await this.ffmpeg.run(
|
||||
"-i",
|
||||
`${colorVideoFileName}.mp4`,
|
||||
"-i",
|
||||
`${audioTrackFileName}.wav`,
|
||||
"-c:v",
|
||||
"copy",
|
||||
"-c:a",
|
||||
"aac",
|
||||
"-map",
|
||||
"0:v:0",
|
||||
"-map",
|
||||
"1:a:0",
|
||||
"-strict",
|
||||
"experimental",
|
||||
`${outputFileName}.mp4`,
|
||||
);
|
||||
// Upload individual videos or single color video
|
||||
const output = this.ffmpeg.FS("readFile", `${outputFileName}.mp4`);
|
||||
// Create a Blob from the output file for downloading
|
||||
const blob = new Blob([output.buffer], { type: "video/mp4" });
|
||||
return blob;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
const processError: VideoPreProcessorError = {
|
||||
message: "Failed to Combine Video with Audio.",
|
||||
code: 103,
|
||||
};
|
||||
throw processError;
|
||||
}
|
||||
}
|
||||
|
||||
private async getMediaUrlFromToken(mediaToken: string): Promise<string> {
|
||||
try {
|
||||
const response = await this.mediaFilesApi.GetMediaFileByToken({
|
||||
mediaFileToken: mediaToken,
|
||||
});
|
||||
|
||||
if (response.success && response.data) {
|
||||
this.bucketConfig.isLocalDev = false;
|
||||
const mediaUrl = this.bucketConfig.getGcsUrl(
|
||||
response.data.public_bucket_path,
|
||||
);
|
||||
|
||||
return mediaUrl;
|
||||
} else {
|
||||
const processError: VideoPreProcessorError = {
|
||||
message: "Failed to get MediaUrlFrom Token",
|
||||
code: 104,
|
||||
};
|
||||
throw processError;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// debug download mp4 from blob
|
||||
async blobToVideoDownload(blob: Blob): Promise<void> {
|
||||
if (!blob) {
|
||||
console.error("Invalid blob provided");
|
||||
return;
|
||||
}
|
||||
console.log("Creating Blob URL");
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
console.log("Blob URL created:", blobUrl);
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = blobUrl;
|
||||
link.download = "video.mp4";
|
||||
document.body.appendChild(link);
|
||||
console.log("Link appended to document");
|
||||
|
||||
link.click();
|
||||
console.log("Link clicked to trigger download");
|
||||
|
||||
document.body.removeChild(link);
|
||||
console.log("Link removed from document");
|
||||
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
console.log("Blob URL revoked");
|
||||
}
|
||||
|
||||
// Only use for debugging
|
||||
// This is to quickly grab the file from the memory
|
||||
async outputWav(fileName: string) {
|
||||
// Read the WAV file from FFmpeg's in-memory filesystem
|
||||
const output = this.ffmpeg.FS("readFile", `${fileName}.wav`);
|
||||
|
||||
// Create a Blob from the file data
|
||||
const blob = new Blob([output.buffer], { type: "audio/wav" });
|
||||
|
||||
// Create a link element and trigger download
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `${fileName}.wav`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
console.log("Download triggered");
|
||||
}
|
||||
|
||||
// Only use for debugging
|
||||
// This is to quickly grab the file from the memory
|
||||
async outputMp4(fileName: string) {
|
||||
// Read the WAV file from FFmpeg's in-memory filesystem
|
||||
const output = this.ffmpeg.FS("readFile", `${fileName}.mp4`);
|
||||
|
||||
// Create a Blob from the file data
|
||||
const blob = new Blob([output.buffer], { type: "video/mp4" });
|
||||
|
||||
// Create a link element and trigger download
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `${fileName}.wav`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
console.log("Download triggered");
|
||||
}
|
||||
|
||||
async processAudioWithBuffer(
|
||||
audioBuffer: AudioBuffer,
|
||||
audioOutputFileName: string,
|
||||
includeBlob: boolean,
|
||||
): Promise<Blob | void> {
|
||||
const outputName = `${audioOutputFileName}`;
|
||||
|
||||
try {
|
||||
// Create the samples in memory
|
||||
for (let i = 0; i < audioBuffer.buffer.length; i++) {
|
||||
const buffer: AudioClipInfo = audioBuffer.buffer[i];
|
||||
const urlPath = await this.getMediaUrlFromToken(buffer.media_id);
|
||||
const audio = await fetchFile(urlPath);
|
||||
console.log(urlPath);
|
||||
console.log(audio);
|
||||
|
||||
if (audio.length === 0) {
|
||||
console.log("audio is 0");
|
||||
|
||||
const processError: VideoPreProcessorError = {
|
||||
message: `Audio length is 0 for ${urlPath}`,
|
||||
code: 103,
|
||||
};
|
||||
throw processError;
|
||||
}
|
||||
|
||||
await this.ffmpeg.FS("writeFile", `tmp${i}.wav`, audio);
|
||||
}
|
||||
console.log("Write the Audio");
|
||||
|
||||
// Construct the FFmpeg command to mix the WAV files
|
||||
const inputArgs = [];
|
||||
const filterComplexArgs = [];
|
||||
|
||||
// Add the silent audio file and account for it below. i + 1
|
||||
inputArgs.push(
|
||||
"-f",
|
||||
"lavfi",
|
||||
"-t",
|
||||
"7",
|
||||
"-i",
|
||||
"anullsrc=r=44100:cl=stereo",
|
||||
);
|
||||
|
||||
// Add the existing audio clips
|
||||
for (let i = 0; i < audioBuffer.buffer.length; i++) {
|
||||
const endTime =
|
||||
audioBuffer.buffer[i].length / this.properties.frameRate; // allows trimming from the right
|
||||
|
||||
const offset = audioBuffer.buffer[i].offset / this.properties.frameRate; // where it will end up on the track.
|
||||
|
||||
const startTime =
|
||||
audioBuffer.buffer[i].offset / this.properties.frameRate; // allows trimming from the left eventually
|
||||
const end = endTime - startTime;
|
||||
|
||||
inputArgs.push("-i", `tmp${i}.wav`);
|
||||
// trim the audio to the end time then offset
|
||||
filterComplexArgs.push(
|
||||
`[${i + 1}:a]atrim=start=${0}:end=${end},adelay=${offset * 1000}|${offset * 1000}[a${i}];`,
|
||||
);
|
||||
|
||||
console.log("Write Audio and Offset Position");
|
||||
}
|
||||
|
||||
// Add the silent audio track to the filter complex arguments
|
||||
filterComplexArgs.push("[0:a]");
|
||||
|
||||
// Add the existing audio tracks to the filter complex arguments
|
||||
for (let i = 0; i < audioBuffer.buffer.length; i++) {
|
||||
filterComplexArgs.push(`[a${i}]`);
|
||||
}
|
||||
|
||||
// Mix the silent audio track with the existing audio tracks
|
||||
filterComplexArgs.push(`amix=inputs=${audioBuffer.buffer.length + 1}[a]`);
|
||||
|
||||
// Run the FFmpeg command to mix the WAV files
|
||||
await this.ffmpeg.run(
|
||||
...inputArgs,
|
||||
"-filter_complex",
|
||||
filterComplexArgs.join(""),
|
||||
"-map",
|
||||
"[a]",
|
||||
`${outputName}.wav`,
|
||||
);
|
||||
|
||||
this.debugListFiles();
|
||||
|
||||
if (includeBlob) {
|
||||
const blob = await this.outputWavFromMemory(`${outputName}`);
|
||||
return blob;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// Testing function
|
||||
public downloadImageBlob(blob: Blob, filename: string) {
|
||||
// Create a URL for the blob
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
// Create a temporary anchor element
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
|
||||
// Append the anchor to the document body
|
||||
document.body.appendChild(a);
|
||||
|
||||
// Trigger a click event on the anchor
|
||||
a.click();
|
||||
|
||||
// Remove the anchor from the document
|
||||
document.body.removeChild(a);
|
||||
|
||||
// Revoke the object URL to free up memory
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
async processVideoWithBuffer(
|
||||
buffer: (string | null)[],
|
||||
fileName: string,
|
||||
outFileName: string,
|
||||
imagePrefix: string, // use a different one to populate the in memory file system, if they overlap you will have problems
|
||||
): Promise<Blob> {
|
||||
try {
|
||||
const imageFormat = this.properties.imageFormat;
|
||||
let extension = "";
|
||||
|
||||
if (imageFormat == ImageFormat.JPEG) {
|
||||
extension = ".jpeg";
|
||||
} else {
|
||||
extension = ".png";
|
||||
}
|
||||
console.log(`Image Files of extension ${extension}`);
|
||||
console.log(`Buffer Length ${buffer.length}`);
|
||||
|
||||
for (let index = 0; index < buffer.length; index++) {
|
||||
const element = buffer[index];
|
||||
if (element === null) {
|
||||
continue;
|
||||
}
|
||||
// this can fail so catch any errors.
|
||||
const image = await fetchFile(element);
|
||||
|
||||
await this.ffmpeg.FS(
|
||||
"writeFile",
|
||||
`${imagePrefix}_image${index}${extension}`,
|
||||
image,
|
||||
);
|
||||
}
|
||||
|
||||
await this.ffmpeg.run(
|
||||
"-framerate",
|
||||
"" + this.properties.frameRate,
|
||||
"-i",
|
||||
`${imagePrefix}_image%d${extension}`,
|
||||
`${fileName}.mp4`,
|
||||
);
|
||||
|
||||
await this.ffmpeg.run(
|
||||
"-i",
|
||||
`${fileName}.mp4`,
|
||||
"-f",
|
||||
"lavfi",
|
||||
"-i",
|
||||
"anullsrc", // This adds a silent audio track
|
||||
"-max_muxing_queue_size",
|
||||
"999999",
|
||||
"-vf", // WE REMOVED THE BLACK FRAME IN FFMEPG INSTEAD OF THE ENGINE
|
||||
"select=gte(n\\,1)", // WE REMOVED THE BLACK FRAME IN FFMEPG INSTEAD OF THE ENGINE Applies a video filter to remove the first frame.
|
||||
// "select=gte(n\\,1),scale=1024:576",
|
||||
"-c:v",
|
||||
"libx264", // Specify video codec (optional, but recommended for MP4)
|
||||
"-c:a",
|
||||
"aac", // Specify audio codec (optional, but recommended for MP4)
|
||||
"-shortest", // Ensure output duration matches the shortest stream (video or audio)
|
||||
"-pix_fmt",
|
||||
"yuv420p",
|
||||
"-f",
|
||||
"mp4",
|
||||
`${outFileName}.mp4`,
|
||||
);
|
||||
|
||||
const blob = await this.outputMp4FromMemory(`${outFileName}`);
|
||||
return blob;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const preProcessingError: VideoPreProcessorError = {
|
||||
message: "Could Not Process Color Video.",
|
||||
code: 105,
|
||||
};
|
||||
throw preProcessingError;
|
||||
}
|
||||
}
|
||||
|
||||
// Grab the file from in memory fs and provide it as a blob
|
||||
async outputMp4FromMemory(fileName: string): Promise<Blob> {
|
||||
// Upload individual videos or single color video
|
||||
const output = this.ffmpeg.FS("readFile", `${fileName}.mp4`);
|
||||
// Create a Blob from the output file for downloading
|
||||
const blob = new Blob([output.buffer], { type: "video/mp4" });
|
||||
return blob;
|
||||
}
|
||||
|
||||
// Grab the file from in memory fs and provide it as a blob
|
||||
async outputImageFromMemory(
|
||||
fileName: string,
|
||||
imageExtension: ImageFormat = ImageFormat.JPEG,
|
||||
): Promise<Blob> {
|
||||
// Upload individual videos or single color video
|
||||
const output = this.ffmpeg.FS("readFile", `${fileName}`);
|
||||
// Create a Blob from the output file for downloading
|
||||
const blob = new Blob([output.buffer], { type: `${imageExtension}` });
|
||||
return blob;
|
||||
}
|
||||
|
||||
async outputWavFromMemory(fileName: string): Promise<Blob> {
|
||||
// Upload individual videos or single color video
|
||||
const output = this.ffmpeg.FS("readFile", `${fileName}.wav`);
|
||||
// Create a Blob from the output file for downloading
|
||||
const blob = new Blob([output.buffer], { type: "audio/wav" });
|
||||
return blob;
|
||||
}
|
||||
|
||||
// This must be called every time before a render is called
|
||||
async clear() {
|
||||
// List all files in the in-memory filesystem
|
||||
const files = this.ffmpeg.FS("readdir", "/");
|
||||
await this.deleteFilesFromFFmpegMemoryStore(files);
|
||||
}
|
||||
|
||||
async debugListFiles() {
|
||||
const files = this.ffmpeg.FS("readdir", "/");
|
||||
for (const fileName of files) {
|
||||
console.log(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteFilesFromFFmpegMemoryStore(fileNames: string[]): Promise<void> {
|
||||
for (const fileName of fileNames) {
|
||||
if (
|
||||
fileName.endsWith(".png") ||
|
||||
fileName.endsWith(".mp4") ||
|
||||
fileName.endsWith(".wav") ||
|
||||
fileName.endsWith(".jpeg")
|
||||
) {
|
||||
await this.ffmpeg.FS("unlink", fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-162
@@ -1,162 +0,0 @@
|
||||
interface Task {
|
||||
imageBitmap: ImageBitmap;
|
||||
width: number;
|
||||
height: number;
|
||||
id: number;
|
||||
}
|
||||
|
||||
import { ClipUI } from "~/pages/PageEnigma/clips/clip_ui";
|
||||
import { BlobReader, BlobWriter, ZipWriter } from "@zip.js/zip.js";
|
||||
|
||||
class FrameProcessorWorker {
|
||||
private holdingCanvas: OffscreenCanvas;
|
||||
private ctx: ImageBitmapRenderingContext | null;
|
||||
public imageType: string;
|
||||
|
||||
private zipFileWriter: BlobWriter;
|
||||
private zipWriter: ZipWriter<Blob>;
|
||||
|
||||
private decodeQueue: Task[];
|
||||
|
||||
public audio_data: ClipUI[];
|
||||
|
||||
public isProcessing: boolean;
|
||||
|
||||
public final_length: number;
|
||||
public item_count: number;
|
||||
constructor() {
|
||||
this.final_length = 0;
|
||||
this.holdingCanvas = new OffscreenCanvas(0, 0);
|
||||
this.ctx = this.holdingCanvas.getContext("bitmaprenderer");
|
||||
this.imageType = "image/jpeg";
|
||||
|
||||
// Zipping
|
||||
this.decodeQueue = [];
|
||||
this.zipFileWriter = new BlobWriter(this.imageType);
|
||||
this.zipWriter = new ZipWriter(this.zipFileWriter);
|
||||
|
||||
this.audio_data = [];
|
||||
|
||||
this.isProcessing = false;
|
||||
this.item_count = 0;
|
||||
}
|
||||
|
||||
async startZipProcessQueue(): Promise<void> {
|
||||
if (this.decodeQueue.length > 0 && this.isProcessing == false) {
|
||||
this.isProcessing = true;
|
||||
|
||||
const task = this.decodeQueue.shift();
|
||||
if (task) {
|
||||
const { imageBitmap, width, height, id } = task;
|
||||
// Proceed with processing the task
|
||||
|
||||
//console.log(`Working On an Frame Id ${id}`);
|
||||
this.holdingCanvas.width = width;
|
||||
this.holdingCanvas.height = height;
|
||||
|
||||
try {
|
||||
// Create or reuse the offscreen canvas
|
||||
this.ctx?.transferFromImageBitmap(imageBitmap);
|
||||
|
||||
// Convert the canvas content to a Blob
|
||||
const blob = await this.holdingCanvas.convertToBlob({
|
||||
quality: 1.0,
|
||||
type: this.imageType,
|
||||
});
|
||||
|
||||
// Send the Blob back to the main thread
|
||||
//console.log(`Zipping Frame ${id} to Zip`);
|
||||
const name = String(id).padStart(5, "0"); // '0009'
|
||||
await this.zipWriter.add(`${name}.jpg`, new BlobReader(blob));
|
||||
|
||||
this.item_count++;
|
||||
} catch (error) {
|
||||
console.error("Error processing image:", error);
|
||||
self.postMessage({ error: (error as Error).message }); //
|
||||
} finally {
|
||||
this.isProcessing = false;
|
||||
}
|
||||
} else {
|
||||
console.log("Queue is empty, no task to process.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
// have this function loop
|
||||
setTimeout(this.startZipProcessQueue.bind(this), 0);
|
||||
}
|
||||
|
||||
async zipFile() {
|
||||
// ensure that this queue is done before zipping
|
||||
console.log(`Waiting to Finish Decoding: ${this.decodeQueue.length}`);
|
||||
console.log(`Decoded Count: ${this.item_count}`);
|
||||
console.log(`Final Length: ${this.final_length}`);
|
||||
if (this.item_count === this.final_length) {
|
||||
console.log("Zipping");
|
||||
const result = await this.zipWriter.close();
|
||||
self.postMessage({ result: result });
|
||||
// reset the item count and final length
|
||||
this.item_count = 0;
|
||||
this.final_length = 0;
|
||||
|
||||
// Zipping
|
||||
this.decodeQueue = [];
|
||||
this.zipFileWriter = new BlobWriter(this.imageType);
|
||||
this.zipWriter = new ZipWriter(this.zipFileWriter);
|
||||
this.audio_data = [];
|
||||
this.isProcessing = false;
|
||||
|
||||
return;
|
||||
} else {
|
||||
setTimeout(this.zipFile.bind(this), 0);
|
||||
}
|
||||
}
|
||||
|
||||
public enqueueTask(
|
||||
imageBitmap: ImageBitmap,
|
||||
width: number,
|
||||
height: number,
|
||||
id: number,
|
||||
): void {
|
||||
this.decodeQueue.push({ imageBitmap, width, height, id });
|
||||
}
|
||||
}
|
||||
|
||||
const runner = new FrameProcessorWorker();
|
||||
let start = false;
|
||||
|
||||
self.addEventListener("message", async (event: MessageEvent) => {
|
||||
const {
|
||||
type,
|
||||
imageBitmap,
|
||||
width,
|
||||
height,
|
||||
frame,
|
||||
audio_data,
|
||||
zip,
|
||||
final_length,
|
||||
} = event.data;
|
||||
|
||||
// console.log(
|
||||
// `${type}: ${imageBitmap} ${width} ${height} ${frame} ${audio_data} ${zip} ${final_length}`,
|
||||
// );
|
||||
|
||||
if (start == false) {
|
||||
start = true;
|
||||
runner.imageType = type;
|
||||
await runner.startZipProcessQueue();
|
||||
}
|
||||
|
||||
if (audio_data || zip) {
|
||||
if (audio_data) {
|
||||
runner.audio_data = audio_data;
|
||||
}
|
||||
if (zip) {
|
||||
console.log(`Worker Reported Final Length: ${final_length}`);
|
||||
runner.final_length = final_length;
|
||||
await runner.zipFile();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
runner.enqueueTask(imageBitmap, width, height, frame);
|
||||
});
|
||||
-125
@@ -1,125 +0,0 @@
|
||||
import * as THREE from "three";
|
||||
// import { BlobReader, BlobWriter, ZipWriter } from "@zip.js/zip.js";
|
||||
|
||||
interface WorkerMessageEvent extends MessageEvent {
|
||||
data: {
|
||||
result: Blob;
|
||||
};
|
||||
}
|
||||
|
||||
export class FrameWorkerManager {
|
||||
private worker: Worker;
|
||||
private offscreenCanvas: OffscreenCanvas;
|
||||
private offScreenRenderer: THREE.WebGLRenderer;
|
||||
public type: string;
|
||||
|
||||
private blobResult: Blob | undefined;
|
||||
|
||||
constructor(width: number, height: number, type: string = "image/jpeg") {
|
||||
this.type = type;
|
||||
|
||||
this.worker = new Worker(
|
||||
"app/pages/PageEnigma/Editor/VideoProcessor/workers/frame_processor_worker.ts",
|
||||
{ type: "module" },
|
||||
);
|
||||
this.worker.addEventListener("message", this.handleMessage.bind(this));
|
||||
|
||||
this.offscreenCanvas = new OffscreenCanvas(width, height);
|
||||
this.offScreenRenderer = new THREE.WebGLRenderer({
|
||||
canvas: this.offscreenCanvas,
|
||||
});
|
||||
}
|
||||
|
||||
setCanvas(width: number, height: number): void {
|
||||
this.offscreenCanvas.width = width;
|
||||
this.offscreenCanvas.height = height;
|
||||
}
|
||||
|
||||
handleMessage(event: WorkerMessageEvent): void {
|
||||
const { result } = event.data;
|
||||
console.log(result);
|
||||
// zip file
|
||||
this.blobResult = result;
|
||||
}
|
||||
|
||||
async sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async collectedFrames(final_length: number): Promise<Blob> {
|
||||
// to get the zip file
|
||||
console.log(`The Last Frame Number is: ${final_length}`);
|
||||
this.worker.postMessage({
|
||||
type: this.type,
|
||||
imageBitmap: undefined,
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
frame: undefined,
|
||||
audio_data: undefined,
|
||||
zip: true,
|
||||
final_length: final_length,
|
||||
});
|
||||
|
||||
while (this.blobResult === undefined) {
|
||||
await this.sleep(1);
|
||||
}
|
||||
|
||||
const result = this.blobResult;
|
||||
this.blobResult = undefined;
|
||||
|
||||
// For testing.
|
||||
//this.downloadBlob(result, "test.zip");
|
||||
|
||||
if (!result) {
|
||||
throw Error("Failed to Compress Payload");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private downloadBlob(blob: Blob, filename: string): void {
|
||||
// Create an object URL for the Blob
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
// Create a temporary anchor element
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename; // Specify the filename
|
||||
|
||||
// Append the anchor to the document body
|
||||
document.body.appendChild(a);
|
||||
|
||||
// Trigger a click event on the anchor
|
||||
a.click();
|
||||
|
||||
// Clean up by revoking the object URL and removing the anchor element
|
||||
URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
|
||||
public terminate(): void {
|
||||
this.worker.terminate();
|
||||
console.log("Worker Terminated");
|
||||
}
|
||||
|
||||
public render(scene: THREE.Scene, camera: THREE.Camera, frame: number): void {
|
||||
this.offScreenRenderer.render(scene, camera);
|
||||
const imageBitmap = this.offscreenCanvas.transferToImageBitmap();
|
||||
this.processFrame(imageBitmap, frame);
|
||||
}
|
||||
|
||||
private processFrame(imageBitmap: ImageBitmap, frame: number): void {
|
||||
const width = this.offscreenCanvas.width;
|
||||
const height = this.offscreenCanvas.height;
|
||||
const type = this.type;
|
||||
|
||||
this.worker.postMessage({
|
||||
type,
|
||||
imageBitmap,
|
||||
width,
|
||||
height,
|
||||
audio_data: undefined,
|
||||
frame,
|
||||
zip: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -48,11 +48,7 @@ import { MediaUploadApi } from "~/Classes/ApiManager";
|
||||
import { SceneManager } from "./scene_manager_api";
|
||||
import { CustomOutlinePass } from "./CustomOutlinePass.js";
|
||||
import FindSurfaces from "./FindSurfaces.js";
|
||||
import {
|
||||
ImageFormat,
|
||||
//VideoAudioPreProcessor,
|
||||
} from "./VideoProcessor/video_audio_preprocessor";
|
||||
import { BufferType, EngineFrameBuffers } from "./VideoProcessor/engine_buffer";
|
||||
|
||||
|
||||
import Stats from "three/examples/jsm/libs/stats.module.js";
|
||||
import { CharacterAnimationEngine } from "./Engines/CharacterAnimationEngine";
|
||||
@@ -325,16 +321,9 @@ class Editor {
|
||||
|
||||
// set image type at this stage
|
||||
|
||||
const imageFormat = ImageFormat.JPEG;
|
||||
|
||||
this.renderIndex = 0;
|
||||
this.engineFrameBuffers = new EngineFrameBuffers(
|
||||
this.getRenderDimensions().width,
|
||||
this.getRenderDimensions().height,
|
||||
);
|
||||
if (this.engineFrameBuffers.frameWorkerManager) {
|
||||
this.engineFrameBuffers.frameWorkerManager.type = imageFormat;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Add helper method to convert focal length to FOV
|
||||
@@ -1252,7 +1241,7 @@ class Editor {
|
||||
console.log(`COLLECTING COLOR FRAMES COUNTED: ${this.renderIndex}`);
|
||||
await this.engineFrameBuffers.collectColorFrames(this.renderIndex);
|
||||
|
||||
await this.engineFrameBuffers.logBufferInfo(BufferType.COLOR);
|
||||
|
||||
this.renderIndex = 0;
|
||||
console.time("Stop Playback And Upload Video Time");
|
||||
await this.stopPlaybackAndUploadVideo();
|
||||
@@ -1263,7 +1252,7 @@ class Editor {
|
||||
} catch (error) {
|
||||
// don't use cache in this case.
|
||||
this.processingHasFailed = true;
|
||||
this.engineFrameBuffers.clearBuffer(BufferType.COLOR);
|
||||
|
||||
console.log(`Video Generation: ${error}`);
|
||||
}
|
||||
} // End Timeline Playing
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as THREE from "three";
|
||||
import { SceneGenereationMetaData } from "../models/sceneGenerationMetadata";
|
||||
import { StoryTellerProxyScene } from "../proxy/storyteller_proxy_scene";
|
||||
import { StoryTellerProxyTimeline } from "../proxy/storyteller_proxy_timeline";
|
||||
|
||||
import {
|
||||
setCameraAspectRatio,
|
||||
hideEditorLoader,
|
||||
@@ -53,19 +53,11 @@ export class SaveManager {
|
||||
);
|
||||
const scene_json = await proxyScene.saveToScene(this.editor.version);
|
||||
|
||||
const proxyTimeline = new StoryTellerProxyTimeline(
|
||||
this.editor.version,
|
||||
this.editor.timeline,
|
||||
this.editor.transform_engine,
|
||||
this.editor.animation_engine,
|
||||
this.editor.audio_engine,
|
||||
this.editor.lipsync_engine,
|
||||
this.editor.emotion_engine,
|
||||
);
|
||||
const timeline_json = await proxyTimeline.saveToJson();
|
||||
|
||||
|
||||
const check_sum_data = {
|
||||
scene: scene_json,
|
||||
timeline: timeline_json,
|
||||
timeline: "",
|
||||
camera_data: {
|
||||
position: this.editor.camera?.position,
|
||||
rotation: this.editor.camera?.rotation,
|
||||
@@ -89,16 +81,7 @@ export class SaveManager {
|
||||
);
|
||||
const scene_json = await proxyScene.saveToScene(this.editor.version);
|
||||
|
||||
const proxyTimeline = new StoryTellerProxyTimeline(
|
||||
this.editor.version,
|
||||
this.editor.timeline,
|
||||
this.editor.transform_engine,
|
||||
this.editor.animation_engine,
|
||||
this.editor.audio_engine,
|
||||
this.editor.lipsync_engine,
|
||||
this.editor.emotion_engine,
|
||||
);
|
||||
const timeline_json = await proxyTimeline.saveToJson();
|
||||
|
||||
|
||||
// Save all cameras data
|
||||
const camerasData = cameras.value.map((cam: Camera) => ({
|
||||
@@ -114,7 +97,7 @@ export class SaveManager {
|
||||
version: this.editor.version,
|
||||
scene: scene_json,
|
||||
...sceneGenerationMetadata,
|
||||
timeline: timeline_json,
|
||||
timeline: "",
|
||||
skybox: this.editor.activeScene.skybox,
|
||||
camera_data: {
|
||||
position: this.editor.camera?.position,
|
||||
@@ -150,17 +133,7 @@ export class SaveManager {
|
||||
);
|
||||
const scene_json = await proxyScene.saveToScene(this.editor.version);
|
||||
console.log(scene_json);
|
||||
const proxyTimeline = new StoryTellerProxyTimeline(
|
||||
this.editor.version,
|
||||
this.editor.timeline,
|
||||
this.editor.transform_engine,
|
||||
this.editor.animation_engine,
|
||||
this.editor.audio_engine,
|
||||
this.editor.lipsync_engine,
|
||||
this.editor.emotion_engine,
|
||||
);
|
||||
const timeline_json = await proxyTimeline.saveToJson();
|
||||
|
||||
|
||||
// Save all cameras data
|
||||
const camerasData = cameras.value.map((cam: Camera) => ({
|
||||
id: cam.id,
|
||||
@@ -175,7 +148,7 @@ export class SaveManager {
|
||||
version: this.editor.version,
|
||||
scene: scene_json,
|
||||
...sceneGenerationMetadata,
|
||||
timeline: timeline_json,
|
||||
timeline: "",
|
||||
skybox: this.editor.activeScene.skybox,
|
||||
camera_data: {
|
||||
position: this.editor.camera?.position,
|
||||
@@ -297,17 +270,7 @@ export class SaveManager {
|
||||
child.layers.set(1);
|
||||
});
|
||||
|
||||
const proxyTimeline = new StoryTellerProxyTimeline(
|
||||
this.editor.version,
|
||||
this.editor.timeline,
|
||||
this.editor.transform_engine,
|
||||
this.editor.animation_engine,
|
||||
this.editor.audio_engine,
|
||||
this.editor.lipsync_engine,
|
||||
this.editor.emotion_engine,
|
||||
);
|
||||
console.debug("Loading Timeline", scene_json["timeline"]);
|
||||
await proxyTimeline.loadFromJson(scene_json["timeline"]);
|
||||
|
||||
this.editor.timeline.checkEditorCanPlay();
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import { v4 as uuidv4 } from "uuid";
|
||||
import { SceneGenereationMetaData as SceneGenerationMetaData } from "~/pages/PageEnigma/models/sceneGenerationMetadata";
|
||||
import { MediaUploadApi, StudioGen2Api, VideoApi } from "~/Classes/ApiManager";
|
||||
import { globalIPAMediaToken } from "../signals";
|
||||
import { BufferType } from "./VideoProcessor/engine_buffer";
|
||||
|
||||
|
||||
// TODO THIS CLASS MAKES NO SENSE
|
||||
// Refactor so we generate all the frames first.
|
||||
@@ -98,7 +98,7 @@ export class VideoGeneration {
|
||||
console.log(`Studio recording: https://storyteller.ai/media/${mediaTokens.color}`);
|
||||
|
||||
} finally {
|
||||
await this.editor.engineFrameBuffers.clearBuffer(BufferType.COLOR);
|
||||
|
||||
}
|
||||
|
||||
return mediaTokens;
|
||||
|
||||
@@ -106,7 +106,7 @@ export const PageEnigma = ({ sceneToken }: { sceneToken?: string }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<TrackProvider>
|
||||
|
||||
<EngineProvider sceneToken={sceneToken}>
|
||||
<PageEditor />
|
||||
<DragComponent />
|
||||
@@ -119,10 +119,10 @@ export const PageEnigma = ({ sceneToken }: { sceneToken?: string }) => {
|
||||
/>
|
||||
<GenerateModals />
|
||||
<ErrorDialog />
|
||||
{/* <Wizard /> */}
|
||||
</EngineProvider>
|
||||
<EditorLoadingBar />
|
||||
|
||||
<EditorLoadingBar/>
|
||||
<Toaster offsetTop={70} offsetRight={12} />
|
||||
</TrackProvider>
|
||||
</EngineProvider>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { faAngleLeft, faFilm, faPlus } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { Button, TopBar } from "~/components";
|
||||
import { TopBar } from "~/components";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { PreviewImages } from "~/pages/PageEnigma/comps/PreviewImages";
|
||||
import { useContext, useState } from "react";
|
||||
import { EngineContext } from "~/pages/PageEnigma/contexts/EngineContext";
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
import { WizardItem } from "~/pages/PageEnigma/Wizard/Wizard";
|
||||
import { ObjectSelectionButton } from "~/pages/PageEnigma/Wizard/ObjectSelectionButton";
|
||||
import { BucketConfig } from "~/api/BucketConfig";
|
||||
import { MediaInfo } from "~/pages/PageEnigma/models";
|
||||
import { AssetType, FilterEngineCategories, ToastTypes } from "~/enums";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { MediaFilesApi } from "~/Classes/ApiManager";
|
||||
import { addToast } from "~/signals";
|
||||
import {
|
||||
backgroundWizardItems,
|
||||
currentStep,
|
||||
selectedBackground,
|
||||
} from "~/pages/PageEnigma/Wizard/signals/wizard";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { BackgroundList } from "~/pages/PageEnigma/Wizard/BackgroundList";
|
||||
|
||||
export const Background = () => {
|
||||
useSignals();
|
||||
const item = currentStep.value as WizardItem;
|
||||
const [showList, setShowList] = useState(false);
|
||||
|
||||
const responseMapping = (data: MediaInfo[]) => {
|
||||
return data
|
||||
.filter(
|
||||
(item) =>
|
||||
[
|
||||
"Sci-fi City",
|
||||
"Cherry Blossom Petals",
|
||||
"Barbie Dreamland Test",
|
||||
"Space Station",
|
||||
"graveyard_2",
|
||||
"sand tile",
|
||||
"Tiny Island",
|
||||
"dock",
|
||||
].indexOf(item.maybe_title ?? "") > -1,
|
||||
)
|
||||
.map((item) => {
|
||||
const bucketConfig = new BucketConfig();
|
||||
const itemThumb = bucketConfig.getCdnUrl(
|
||||
item.cover_image.maybe_cover_image_public_bucket_path ?? "",
|
||||
600,
|
||||
100,
|
||||
);
|
||||
return {
|
||||
colorIndex: item.cover_image.default_cover.color_index,
|
||||
imageIndex: item.cover_image.default_cover.image_index,
|
||||
media_id: item.token,
|
||||
name: item.maybe_title ?? "Unknown",
|
||||
type: AssetType.OBJECT,
|
||||
media_type: item.media_type,
|
||||
version: 1,
|
||||
...(item.cover_image.maybe_cover_image_public_bucket_path
|
||||
? {
|
||||
thumbnail: itemThumb,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const fetchFeaturedObjects = useCallback(async () => {
|
||||
const mediaFilesApi = new MediaFilesApi();
|
||||
const response = await mediaFilesApi.ListFeaturedMediaFiles({
|
||||
filter_engine_categories: [FilterEngineCategories.OBJECT],
|
||||
});
|
||||
if (response.success && response.data) {
|
||||
backgroundWizardItems.value = responseMapping(response.data);
|
||||
return;
|
||||
}
|
||||
addToast(
|
||||
ToastTypes.ERROR,
|
||||
response.errorMessage || "Unknown Error in Fetching Featured Objects",
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchFeaturedObjects();
|
||||
}, [fetchFeaturedObjects]);
|
||||
|
||||
if (showList) {
|
||||
return <BackgroundList onClose={() => setShowList(false)} />;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div>{item.label}</div>
|
||||
<ObjectSelectionButton
|
||||
onClick={() => setShowList(true)}
|
||||
label={
|
||||
selectedBackground.value ? "Selected Background" : "Select Background"
|
||||
}
|
||||
busy={!backgroundWizardItems.value}
|
||||
imageSrc={
|
||||
selectedBackground.value?.thumbnail ??
|
||||
backgroundWizardItems.value?.[0]?.thumbnail ??
|
||||
""
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,52 +0,0 @@
|
||||
import { useContext } from "react";
|
||||
import { ItemElement } from "./ItemElement";
|
||||
import Queue from "~/pages/PageEnigma/Queue/Queue";
|
||||
import { QueueNames } from "~/pages/PageEnigma/Queue";
|
||||
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
|
||||
import { objectGroup } from "~/pages/PageEnigma/signals";
|
||||
import { EngineContext } from "~/pages/PageEnigma/contexts/EngineContext";
|
||||
import {
|
||||
backgroundWizardItems,
|
||||
selectedBackground,
|
||||
} from "~/pages/PageEnigma/Wizard/signals/wizard";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const BackgroundList = ({ onClose }: Props) => {
|
||||
useSignals();
|
||||
const editorEngine = useContext(EngineContext);
|
||||
|
||||
return (
|
||||
<div className="flex h-4/5 flex-wrap gap-4 overflow-y-auto">
|
||||
{(backgroundWizardItems.value ?? []).map((item) => (
|
||||
<button
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const existingBackground = objectGroup.value.objects.find(
|
||||
(object) => object.name === selectedBackground.value?.name,
|
||||
);
|
||||
if (existingBackground) {
|
||||
editorEngine?.deleteObject(existingBackground?.object_uuid ?? "");
|
||||
}
|
||||
|
||||
selectedBackground.value = item;
|
||||
Queue.publish({
|
||||
queueName: QueueNames.TO_ENGINE,
|
||||
action: toEngineActions.ADD_OBJECT,
|
||||
data: item,
|
||||
});
|
||||
onClose();
|
||||
}}
|
||||
key={item.object_uuid}
|
||||
className="w-[144px]"
|
||||
>
|
||||
<ItemElement item={item} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,72 +0,0 @@
|
||||
import { Clip, MediaItem } from "~/pages/PageEnigma/models";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { ItemElement } from "./ItemElement";
|
||||
import Queue from "~/pages/PageEnigma/Queue/Queue";
|
||||
import { QueueNames } from "~/pages/PageEnigma/Queue";
|
||||
import { fromEngineActions } from "~/pages/PageEnigma/Queue/fromEngineActions";
|
||||
import { AssetType } from "~/enums";
|
||||
import {
|
||||
characterWizardItems,
|
||||
selectedCharacters,
|
||||
showList,
|
||||
} from "~/pages/PageEnigma/Wizard/signals/wizard";
|
||||
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
|
||||
export const CharacterList = () => {
|
||||
useSignals();
|
||||
const addingObject = useRef<MediaItem | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
Queue.subscribe(QueueNames.FROM_ENGINE, "wizard-character", (message) => {
|
||||
switch (message.action) {
|
||||
case fromEngineActions.UPDATE_CHARACTER_ID:
|
||||
if (addingObject.current) {
|
||||
const data = message.data as Clip;
|
||||
selectedCharacters.value = [
|
||||
...selectedCharacters.value,
|
||||
{
|
||||
...addingObject.current,
|
||||
version: data.version,
|
||||
type: data.type.toString() as AssetType,
|
||||
media_id: data.media_id,
|
||||
name: data.name,
|
||||
object_uuid: data.object_uuid,
|
||||
},
|
||||
];
|
||||
}
|
||||
break;
|
||||
case fromEngineActions.DELETE_OBJECT:
|
||||
selectedCharacters.value = selectedCharacters.value.filter(
|
||||
(object) =>
|
||||
object.object_uuid !== (message.data as Clip).object_uuid,
|
||||
);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex h-4/5 flex-wrap gap-4 overflow-y-auto">
|
||||
{(characterWizardItems.value ?? []).map((item) => (
|
||||
<button
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
addingObject.current = item;
|
||||
showList.value = null;
|
||||
Queue.publish({
|
||||
queueName: QueueNames.TO_ENGINE,
|
||||
action: toEngineActions.ADD_CHARACTER,
|
||||
data: item,
|
||||
});
|
||||
}}
|
||||
key={item.object_uuid}
|
||||
className="w-[144px]"
|
||||
>
|
||||
<ItemElement item={item} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,106 +0,0 @@
|
||||
import { WizardItem, WizardType } from "~/pages/PageEnigma/Wizard/Wizard";
|
||||
import { ObjectSelectionButton } from "~/pages/PageEnigma/Wizard/ObjectSelectionButton";
|
||||
import { BucketConfig } from "~/api/BucketConfig";
|
||||
import { MediaInfo } from "~/pages/PageEnigma/models";
|
||||
import { AssetType, FilterEngineCategories, ToastTypes } from "~/enums";
|
||||
import { useCallback, useContext, useEffect } from "react";
|
||||
import { MediaFilesApi } from "~/Classes/ApiManager";
|
||||
import { addToast } from "~/signals";
|
||||
import { ItemElement } from "./ItemElement";
|
||||
import { EngineContext } from "~/pages/PageEnigma/contexts/EngineContext";
|
||||
import {
|
||||
characterWizardItems,
|
||||
currentStep,
|
||||
selectedCharacters,
|
||||
} from "~/pages/PageEnigma/Wizard/signals/wizard";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { demoCharacterItems } from "~/pages/PageEnigma/signals";
|
||||
|
||||
export const Characters = () => {
|
||||
useSignals();
|
||||
const item = currentStep.value as WizardItem;
|
||||
const editorEngine = useContext(EngineContext);
|
||||
|
||||
const responseMapping = (data: MediaInfo[]) => {
|
||||
return data.map((item) => {
|
||||
const bucketConfig = new BucketConfig();
|
||||
const itemThumb = bucketConfig.getCdnUrl(
|
||||
item.cover_image.maybe_cover_image_public_bucket_path ?? "",
|
||||
600,
|
||||
100,
|
||||
);
|
||||
return {
|
||||
object_uuid: item.token,
|
||||
colorIndex: item.cover_image.default_cover.color_index,
|
||||
imageIndex: item.cover_image.default_cover.image_index,
|
||||
media_id: item.token,
|
||||
name: item.maybe_title ?? "Unknown",
|
||||
type: AssetType.OBJECT,
|
||||
media_type: item.media_type,
|
||||
version: 1,
|
||||
...(item.cover_image.maybe_cover_image_public_bucket_path
|
||||
? {
|
||||
thumbnail: itemThumb,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const fetchFeaturedCharacters = useCallback(async () => {
|
||||
const mediaFilesApi = new MediaFilesApi();
|
||||
const response = await mediaFilesApi.ListFeaturedMediaFiles({
|
||||
filter_engine_categories: [FilterEngineCategories.CHARACTER],
|
||||
});
|
||||
if (response.success && response.data) {
|
||||
characterWizardItems.value = [
|
||||
...responseMapping(response.data),
|
||||
...demoCharacterItems.value,
|
||||
];
|
||||
return;
|
||||
}
|
||||
addToast(
|
||||
ToastTypes.ERROR,
|
||||
response.errorMessage || "Unknown Error in Fetching Featured Objects",
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchFeaturedCharacters();
|
||||
}, [fetchFeaturedCharacters]);
|
||||
|
||||
console.log(characterWizardItems.value?.[0]?.thumbnail ?? "");
|
||||
const initialCharacter = characterWizardItems.value?.[0] ?? {
|
||||
imageIndex: 0,
|
||||
thumbnail: "",
|
||||
};
|
||||
const defaultThumb = `/resources/images/default-covers/${(initialCharacter.imageIndex || 0) % 24}.webp`;
|
||||
const thumbnail = initialCharacter.thumbnail
|
||||
? initialCharacter.thumbnail
|
||||
: defaultThumb;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>{item.label}</div>
|
||||
<div className="mb-2 flex flex-wrap gap-2">
|
||||
{selectedCharacters.value.map((item) => (
|
||||
<button
|
||||
key={item.object_uuid}
|
||||
className="w-[80px]"
|
||||
onClick={() => {
|
||||
editorEngine?.deleteObject(item.object_uuid!);
|
||||
}}
|
||||
>
|
||||
<ItemElement item={item} showDelete />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<ObjectSelectionButton
|
||||
onClick={() => {}}
|
||||
label="Selected Character"
|
||||
busy={!characterWizardItems.value}
|
||||
imageSrc={thumbnail}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,56 +0,0 @@
|
||||
import { MediaItem } from "~/pages/PageEnigma/models";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { SyntheticEvent } from "react";
|
||||
import { Badge } from "~/components";
|
||||
import { AssetType } from "~/enums";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faTrash } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
interface Props {
|
||||
item: MediaItem;
|
||||
showDelete?: boolean;
|
||||
}
|
||||
|
||||
export const ItemElement = ({ item, showDelete }: Props) => {
|
||||
useSignals();
|
||||
const defaultThumb = `/resources/images/default-covers/${(item.imageIndex || 0) % 24}.webp`;
|
||||
const thumbnail = item.thumbnail ? item.thumbnail : defaultThumb;
|
||||
|
||||
return (
|
||||
<div className="group relative w-full cursor-pointer select-none overflow-hidden rounded-lg transition-all duration-200 hover:brightness-110">
|
||||
{item.media_type && !showDelete && (
|
||||
<Badge
|
||||
label={
|
||||
item.type === AssetType.OBJECT
|
||||
? item.media_type.toUpperCase()
|
||||
: item.media_type === "fbx" ||
|
||||
item.media_type.toLowerCase() === "glb"
|
||||
? "Mixamo"
|
||||
: item.media_type === "pmx"
|
||||
? "MMD"
|
||||
: item.media_type.toUpperCase()
|
||||
}
|
||||
className="absolute right-0 mr-[3px] mt-[3px]"
|
||||
/>
|
||||
)}
|
||||
{showDelete && (
|
||||
<div className="absolute right-0 mr-[3px] mt-[3px] text-brand-primary">
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<img
|
||||
crossOrigin="anonymous"
|
||||
src={thumbnail}
|
||||
onError={(e: SyntheticEvent<HTMLImageElement>) => {
|
||||
e.currentTarget.src = defaultThumb;
|
||||
}}
|
||||
alt={item.name}
|
||||
className="pointer-events-none aspect-[4.5/5] w-full select-none bg-gradient-to-b from-[#CCCCCC] to-[#A0A0A0] object-cover object-center"
|
||||
/>
|
||||
<div className="pointer-events-none w-full select-none truncate bg-ui-controls px-2 py-1 text-center text-[12px] transition-all duration-200 group-hover:bg-ui-controls-button/50">
|
||||
{item.name || item.media_id}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,89 +0,0 @@
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import {
|
||||
currentStep,
|
||||
showWizard,
|
||||
} from "~/pages/PageEnigma/Wizard/signals/wizard";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faArrowRight } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { Fragment } from "react";
|
||||
|
||||
export const MainPage = () => {
|
||||
useSignals();
|
||||
const step = currentStep.value;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-3 font-bold">How would you like to start?</div>
|
||||
<div className="flex gap-5">
|
||||
{(step?.options ?? []).map((option) => (
|
||||
<Fragment key={option.value}>
|
||||
{option.label === "Remix" && (
|
||||
<button
|
||||
className={twMerge(
|
||||
"group relative flex h-[249px] w-full flex-col items-center rounded-lg border border-white/5 bg-brand-secondary/50 px-2 pt-[58px] transition-all duration-150 hover:border-brand-primary hover:bg-brand-primary/20",
|
||||
)}
|
||||
onClick={() => (showWizard.value = option.value as string)}
|
||||
>
|
||||
<svg
|
||||
width="36"
|
||||
height="33"
|
||||
viewBox="0 0 36 33"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.74336 8.12213C14.2972 3.56828 21.6551 3.54632 26.2382 8.04892L23.2219 11.058C22.7167 11.5631 22.5703 12.3172 22.8412 12.9762C23.112 13.6351 23.7563 14.0597 24.4665 14.0597H33.2154H33.8377C34.8115 14.0597 35.5949 13.2763 35.5949 12.3026V2.93133C35.5949 2.22117 35.1702 1.57689 34.5113 1.306C33.8524 1.03512 33.0983 1.18154 32.5931 1.68671L29.5475 4.73237C23.134 -1.60055 12.8037 -1.57859 6.42681 4.80558C4.64042 6.59198 3.35187 8.69319 2.56117 10.9408C2.12921 12.1635 2.77349 13.496 3.98882 13.9279C5.20416 14.3599 6.54396 13.7156 6.97591 12.5003C7.53965 10.9042 8.45481 9.40336 9.74336 8.12213ZM0.452637 20.5024V21.0589V21.1101V29.8737C0.452637 30.5839 0.877272 31.2281 1.53619 31.499C2.19511 31.7699 2.9492 31.6235 3.45437 31.1183L6.50003 28.0727C12.9135 34.4056 23.2438 34.3836 29.6207 27.9995C31.4071 26.2131 32.7029 24.1118 33.4936 21.8715C33.9256 20.6489 33.2813 19.3164 32.066 18.8844C30.8507 18.4525 29.5109 19.0968 29.0789 20.3121C28.5152 21.9081 27.6 23.409 26.3115 24.6902C21.7576 29.2441 14.3997 29.266 9.81658 24.7634L12.8256 21.7471C13.3308 21.2419 13.4772 20.4878 13.2063 19.8289C12.9354 19.17 12.2912 18.7453 11.581 18.7453H2.82474H2.77349H2.20975C1.23602 18.7453 0.452637 19.5287 0.452637 20.5024Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M17.9759 11.2148C18.6202 11.2148 19.1414 11.7361 19.1414 12.3803V14.956H21.717C22.3613 14.956 22.8825 15.4772 22.8825 16.1214C22.8825 16.7657 22.3613 17.2869 21.717 17.2869H19.1414V19.8625C19.1414 20.5068 18.6202 21.028 17.9759 21.028C17.3317 21.028 16.8104 20.5068 16.8104 19.8625V17.2869H14.2348C13.5906 17.2869 13.0693 16.7657 13.0693 16.1214C13.0693 15.4772 13.5906 14.956 14.2348 14.956H16.8104V12.3803C16.8104 11.7361 17.3317 11.2148 17.9759 11.2148Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
<div className="mt-4 text-xl font-bold">Remix</div>
|
||||
<div className="mt-4 px-8 text-sm text-white/70">
|
||||
Create a scene from an existing scene
|
||||
</div>
|
||||
<div className="absolute bottom-[8px] right-[12px] text-lg text-white opacity-50 group-hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faArrowRight} />
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{option.label === "Blank Scene" && (
|
||||
<button
|
||||
className={twMerge(
|
||||
"group relative flex h-[249px] w-full flex-col items-center rounded-lg border border-white/5 bg-brand-secondary/50 px-2 pt-[58px] transition-all duration-150 hover:border-brand-primary hover:bg-brand-primary/20",
|
||||
)}
|
||||
onClick={() => (showWizard.value = option.value as string)}
|
||||
>
|
||||
<svg
|
||||
width="26"
|
||||
height="34"
|
||||
viewBox="0 0 26 34"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M0.494629 4.18421C0.494629 1.87636 2.32929 0 4.58586 0H14.8139V8.36842C14.8139 9.52562 15.7281 10.4605 16.8595 10.4605H25.042V29.2895C25.042 31.5973 23.2073 33.4737 20.9508 33.4737H4.58586C2.32929 33.4737 0.494629 31.5973 0.494629 29.2895V4.18421ZM25.042 8.36842H16.8595V0L25.042 8.36842Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
<div className="mt-4 text-xl font-bold">Blank Scene</div>
|
||||
<div className="mt-4 px-8 text-sm text-white/70">
|
||||
If you’re already familiar with Storyteller Studio.
|
||||
</div>
|
||||
<div className="absolute bottom-[8px] right-[12px] text-lg text-white opacity-50 group-hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faArrowRight} />
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,72 +0,0 @@
|
||||
import { Clip, MediaItem } from "~/pages/PageEnigma/models";
|
||||
import { AssetType } from "~/enums";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { ItemElement } from "./ItemElement";
|
||||
import Queue from "~/pages/PageEnigma/Queue/Queue";
|
||||
import { QueueNames } from "~/pages/PageEnigma/Queue";
|
||||
import { fromEngineActions } from "~/pages/PageEnigma/Queue/fromEngineActions";
|
||||
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
|
||||
import {
|
||||
objectWizardItems,
|
||||
selectedObjects,
|
||||
showList,
|
||||
} from "~/pages/PageEnigma/Wizard/signals/wizard";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
|
||||
export const ObjectList = () => {
|
||||
useSignals();
|
||||
const addingObject = useRef<MediaItem | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
Queue.subscribe(QueueNames.FROM_ENGINE, "wizard-objects", (message) => {
|
||||
switch (message.action) {
|
||||
case fromEngineActions.ADD_OBJECT:
|
||||
if (addingObject.current) {
|
||||
const data = message.data as Clip;
|
||||
selectedObjects.value = [
|
||||
...selectedObjects.value,
|
||||
{
|
||||
...addingObject.current,
|
||||
version: data.version,
|
||||
type: data.type.toString() as AssetType,
|
||||
media_id: data.media_id,
|
||||
name: data.name,
|
||||
object_uuid: data.object_uuid,
|
||||
},
|
||||
];
|
||||
}
|
||||
break;
|
||||
case fromEngineActions.DELETE_OBJECT:
|
||||
selectedObjects.value = selectedObjects.value.filter(
|
||||
(object) =>
|
||||
object.object_uuid !== (message.data as Clip).object_uuid,
|
||||
);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex h-4/5 flex-wrap gap-4 overflow-y-auto">
|
||||
{(objectWizardItems.value ?? []).map((item) => (
|
||||
<button
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
addingObject.current = item;
|
||||
showList.value = null;
|
||||
Queue.publish({
|
||||
queueName: QueueNames.TO_ENGINE,
|
||||
action: toEngineActions.ADD_OBJECT,
|
||||
data: item,
|
||||
});
|
||||
}}
|
||||
key={item.object_uuid}
|
||||
className="w-[144px]"
|
||||
>
|
||||
<ItemElement item={item} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,43 +0,0 @@
|
||||
import { faChevronRight } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { H4, LoadingSpinner } from "~/components";
|
||||
|
||||
interface ObjectSelectionButtonProps {
|
||||
onClick: () => void;
|
||||
label: string;
|
||||
imageSrc: string;
|
||||
busy: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function ObjectSelectionButton({
|
||||
onClick,
|
||||
label,
|
||||
imageSrc,
|
||||
busy,
|
||||
disabled,
|
||||
}: ObjectSelectionButtonProps) {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<button
|
||||
className="flex w-full cursor-pointer items-center justify-between gap-3 rounded-lg border border-white/30 bg-ui-controls-button/70 p-2 pr-3 text-start transition-all hover:border-brand-primary hover:bg-ui-controls-button/60"
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
<div className="aspect-video w-20 overflow-hidden rounded-md bg-ui-controls-button/100">
|
||||
{busy ? (
|
||||
<LoadingSpinner />
|
||||
) : (
|
||||
<img crossOrigin="anonymous" src={imageSrc} alt={label} />
|
||||
)}
|
||||
</div>
|
||||
<div className="grow">
|
||||
<>
|
||||
<H4>{label}</H4>
|
||||
</>
|
||||
</div>
|
||||
<FontAwesomeIcon icon={faChevronRight} className="text-xl opacity-60" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
import { WizardItem, WizardType } from "~/pages/PageEnigma/Wizard/Wizard";
|
||||
import { ObjectSelectionButton } from "~/pages/PageEnigma/Wizard/ObjectSelectionButton";
|
||||
import { BucketConfig } from "~/api/BucketConfig";
|
||||
import { MediaInfo } from "~/pages/PageEnigma/models";
|
||||
import { AssetType, FilterEngineCategories, ToastTypes } from "~/enums";
|
||||
import { useCallback, useContext, useEffect } from "react";
|
||||
import { MediaFilesApi } from "~/Classes/ApiManager";
|
||||
import { addToast } from "~/signals";
|
||||
import { ItemElement } from "./ItemElement";
|
||||
import { EngineContext } from "~/pages/PageEnigma/contexts/EngineContext";
|
||||
import {
|
||||
currentStep,
|
||||
objectWizardItems,
|
||||
selectedObjects,
|
||||
} from "~/pages/PageEnigma/Wizard/signals/wizard";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
|
||||
export const Objects = () => {
|
||||
useSignals();
|
||||
const item = currentStep.value as WizardItem;
|
||||
const editorEngine = useContext(EngineContext);
|
||||
|
||||
const responseMapping = (data: MediaInfo[]) => {
|
||||
return data
|
||||
.filter(
|
||||
(item) =>
|
||||
[
|
||||
"Sci-fi City",
|
||||
"Cherry Blossom Petals",
|
||||
"Barbie Dreamland Test",
|
||||
"Space Station",
|
||||
"graveyard_2",
|
||||
"sand tile",
|
||||
"Tiny Island",
|
||||
"dock",
|
||||
].indexOf(item.maybe_title ?? "") === -1,
|
||||
)
|
||||
.map((item) => {
|
||||
const bucketConfig = new BucketConfig();
|
||||
const itemThumb = bucketConfig.getCdnUrl(
|
||||
item.cover_image.maybe_cover_image_public_bucket_path ?? "",
|
||||
600,
|
||||
100,
|
||||
);
|
||||
return {
|
||||
object_uuid: item.token,
|
||||
colorIndex: item.cover_image.default_cover.color_index,
|
||||
imageIndex: item.cover_image.default_cover.image_index,
|
||||
media_id: item.token,
|
||||
name: item.maybe_title ?? "Unknown",
|
||||
type: AssetType.OBJECT,
|
||||
media_type: item.media_type,
|
||||
version: 1,
|
||||
...(item.cover_image.maybe_cover_image_public_bucket_path
|
||||
? {
|
||||
thumbnail: itemThumb,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const fetchFeaturedObjects = useCallback(async () => {
|
||||
const mediaFilesApi = new MediaFilesApi();
|
||||
const response = await mediaFilesApi.ListFeaturedMediaFiles({
|
||||
filter_engine_categories: [FilterEngineCategories.OBJECT],
|
||||
});
|
||||
if (response.success && response.data) {
|
||||
objectWizardItems.value = responseMapping(response.data);
|
||||
return;
|
||||
}
|
||||
addToast(
|
||||
ToastTypes.ERROR,
|
||||
response.errorMessage || "Unknown Error in Fetching Featured Objects",
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchFeaturedObjects();
|
||||
}, [fetchFeaturedObjects]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>{item.label}</div>
|
||||
<div className="mb-2 flex flex-wrap gap-2">
|
||||
{selectedObjects.value.map((item) => (
|
||||
<button
|
||||
key={item.object_uuid}
|
||||
className="w-[80px]"
|
||||
onClick={() => {
|
||||
editorEngine?.deleteObject(item.object_uuid!);
|
||||
}}
|
||||
>
|
||||
<ItemElement item={item} showDelete />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<ObjectSelectionButton
|
||||
onClick={() => {}}
|
||||
label="Selected Object"
|
||||
busy={!objectWizardItems.value}
|
||||
imageSrc={objectWizardItems.value?.[0]?.thumbnail ?? ""}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
import { WizardItem } from "~/pages/PageEnigma/Wizard/Wizard";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { Select } from "~/components";
|
||||
import {
|
||||
currentStep,
|
||||
showWizard,
|
||||
} from "~/pages/PageEnigma/Wizard/signals/wizard";
|
||||
|
||||
export const Question = () => {
|
||||
useSignals();
|
||||
const item = currentStep.value as WizardItem;
|
||||
|
||||
return (
|
||||
<Select
|
||||
options={item.options!}
|
||||
onChange={(value) => (showWizard.value = value)}
|
||||
placeholder={item.label}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,100 +0,0 @@
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { useEffect, useState } from "react";
|
||||
import { LoadingSpinner } from "~/components";
|
||||
import { RemixVideo } from "~/pages/PageEnigma/Wizard/RemixVideo";
|
||||
const cards = [
|
||||
{
|
||||
title: "Space Station",
|
||||
defaultVideo: "/resources/videos/remix/space_station_result.mp4",
|
||||
hoverVideo: "/resources/videos/remix/space_station_raw.mp4",
|
||||
token: "m_ezkyxc4fqe6s8rmq7352zrbvgwq4w8",
|
||||
text: "",
|
||||
},
|
||||
{
|
||||
title: "Walking Down the Dock",
|
||||
defaultVideo: "/resources/videos/remix/dock_walk_result.mp4",
|
||||
hoverVideo: "/resources/videos/remix/dock_walk_raw.mp4",
|
||||
token: "m_bcmc0k8ny4meywdmdy2vn2eafeyr0r",
|
||||
text: "",
|
||||
},
|
||||
{
|
||||
title: "The Beach",
|
||||
defaultVideo: "/resources/videos/remix/beach_result.mp4",
|
||||
hoverVideo: "/resources/videos/remix/beach_raw.mp4",
|
||||
token: "m_k10hh6qvb8zfctk901avmnv2brqpyw",
|
||||
text: "",
|
||||
},
|
||||
{
|
||||
title: "The Market",
|
||||
defaultVideo: "/resources/videos/remix/market_result.mp4",
|
||||
hoverVideo: "/resources/videos/remix/market_raw.mp4",
|
||||
token: "m_pp7f78gpyd7kmxkbsp6x9vngqg12gc",
|
||||
text: "",
|
||||
},
|
||||
{
|
||||
title: "Cherry Blossom",
|
||||
defaultVideo: "/resources/videos/remix/cherry_blossom_result.mp4",
|
||||
hoverVideo: "/resources/videos/remix/cherry_blossom_raw.mp4",
|
||||
token: "m_97xhpc7dt7xnfwj7paxg94mt95dshv",
|
||||
text: "",
|
||||
},
|
||||
{
|
||||
title: "Pirate Ship",
|
||||
defaultVideo: "/resources/videos/remix/pirate_ship_result.mp4",
|
||||
hoverVideo: "/resources/videos/remix/pirate_ship_raw.mp4",
|
||||
token: "m_tz8vm3vw3xsk5z5qvpq1y9cczdn2vp",
|
||||
text: "",
|
||||
},
|
||||
{
|
||||
title: "Cute Ghost",
|
||||
defaultVideo: "/resources/videos/remix/cute_ghost_result.mp4",
|
||||
hoverVideo: "/resources/videos/remix/cute_ghost_raw.mp4",
|
||||
token: "m_6vcp2d5k2k9k0xmz6hm6r5vtr1vexx",
|
||||
text: "",
|
||||
},
|
||||
{
|
||||
title: "Japanese Schoolyard",
|
||||
defaultVideo: "/resources/videos/remix/japanese_schoolyard_result.mp4",
|
||||
hoverVideo: "/resources/videos/remix/japanese_schoolyard_raw.mp4",
|
||||
token: "m_ftp3v0jgegtp6rsck4v8x5wm4vwvr1",
|
||||
text: "",
|
||||
},
|
||||
{
|
||||
title: "Desert Walk",
|
||||
defaultVideo: "/resources/videos/remix/desert_walk_result.mp4",
|
||||
hoverVideo: "/resources/videos/remix/desert_walk_raw.mp4",
|
||||
token: "m_nhh5fym68sab83yg3a0z7hhm40p4jc",
|
||||
text: "",
|
||||
},
|
||||
];
|
||||
export const Remix = () => {
|
||||
useSignals();
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// This is a 500ms delay fix for the layout shifting
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setLoading(false);
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{loading ? (
|
||||
<div className="flex h-52 w-full items-center justify-center gap-3 font-medium">
|
||||
<LoadingSpinner />
|
||||
Loading Scenes...
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-3 gap-4 overflow-y-auto">
|
||||
{cards.map((card) => (
|
||||
<RemixVideo card={card} key={card.token} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,138 +0,0 @@
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { selectedRemixCard } from "~/pages/PageEnigma/Wizard/signals/wizard";
|
||||
import { LoadingSpinner } from "~/components";
|
||||
|
||||
export const RemixVideo = ({
|
||||
card,
|
||||
}: {
|
||||
card: {
|
||||
title: string;
|
||||
text: string;
|
||||
defaultVideo: string;
|
||||
hoverVideo: string;
|
||||
token: string;
|
||||
};
|
||||
}) => {
|
||||
useSignals();
|
||||
const [hover, setHover] = useState(false);
|
||||
const defaultVideoRef = useRef<HTMLVideoElement>(null);
|
||||
const hoverVideoRef = useRef<HTMLVideoElement>(null);
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const defaultVideo = defaultVideoRef.current;
|
||||
const hoverVideo = hoverVideoRef.current;
|
||||
|
||||
if (defaultVideo && hoverVideo) {
|
||||
const onCanPlayThrough = () => {
|
||||
if (defaultVideo.readyState >= 4 && hoverVideo.readyState >= 4) {
|
||||
setIsReady(true);
|
||||
defaultVideo.play();
|
||||
hoverVideo.play();
|
||||
}
|
||||
};
|
||||
|
||||
defaultVideo.addEventListener("canplaythrough", onCanPlayThrough);
|
||||
hoverVideo.addEventListener("canplaythrough", onCanPlayThrough);
|
||||
|
||||
return () => {
|
||||
defaultVideo.removeEventListener("canplaythrough", onCanPlayThrough);
|
||||
hoverVideo.removeEventListener("canplaythrough", onCanPlayThrough);
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (defaultVideoRef.current && hoverVideoRef.current) {
|
||||
hoverVideoRef.current.currentTime = defaultVideoRef.current.currentTime;
|
||||
setHover(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setHover(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
key={card.token}
|
||||
className={twMerge(
|
||||
"relative block aspect-video w-[352px] overflow-hidden rounded-lg border transition-all duration-150",
|
||||
card.token === selectedRemixCard.value?.token
|
||||
? "border-brand-primary"
|
||||
: "border-white/5 hover:border-white/25",
|
||||
)}
|
||||
onClick={() => (selectedRemixCard.value = card)}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<div
|
||||
className="absolute bottom-0 left-0 z-10 h-1/4 w-full object-cover"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent)",
|
||||
}}
|
||||
/>
|
||||
<video
|
||||
className="absolute left-0 top-0 h-full w-full object-cover"
|
||||
ref={defaultVideoRef}
|
||||
src={card.defaultVideo}
|
||||
crossOrigin="anonymous"
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
style={{ opacity: hover ? 0 : 1 }}
|
||||
/>
|
||||
<video
|
||||
className="absolute left-0 top-0 h-full w-full object-cover"
|
||||
ref={hoverVideoRef}
|
||||
src={card.hoverVideo}
|
||||
crossOrigin="anonymous"
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
style={{ opacity: hover ? 1 : 0 }}
|
||||
/>
|
||||
{!isReady && (
|
||||
<div className="absolute left-0 top-0 h-full w-full bg-black/10 object-cover">
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="text-md absolute bottom-[8px] left-[8px] z-10 font-medium"
|
||||
style={{
|
||||
textShadow: "0px 0px 10px rgba(0, 0, 0, 0.4)",
|
||||
}}
|
||||
>
|
||||
{card.title}
|
||||
</div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
className={`absolute right-1.5 top-1.5 h-[22px] w-[22px] transition-opacity duration-200 ease-in-out ${
|
||||
card.token === selectedRemixCard.value?.token
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
}`}
|
||||
>
|
||||
<path
|
||||
opacity="1"
|
||||
d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c-9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"
|
||||
fill="#FC6B68"
|
||||
/>
|
||||
<path
|
||||
d="M369 175c-9.4 9.4-9.4 24.6 0 33.9L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c-9.4-9.4 24.6-9.4 33.9 0z"
|
||||
fill="#FFFFFF"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<p className="mt-2 text-xs text-white/70">{card.text}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Input } from "~/components";
|
||||
import { textInput } from "~/pages/PageEnigma/Wizard/signals/wizard";
|
||||
|
||||
export const TextField = () => {
|
||||
return (
|
||||
<div>
|
||||
<Input
|
||||
value={textInput.value}
|
||||
onChange={(event) => (textInput.value = event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,177 +0,0 @@
|
||||
import { SelectOption, TransitionDialogue } from "~/components";
|
||||
import {
|
||||
currentStep,
|
||||
selectedRemixCard,
|
||||
showWizard,
|
||||
textInput,
|
||||
} from "~/pages/PageEnigma/Wizard/signals/wizard";
|
||||
import { useSignalEffect, useSignals } from "@preact/signals-react/runtime";
|
||||
import { WizardStep } from "~/pages/PageEnigma/Wizard/WizardStep";
|
||||
import { resetSceneGenerationMetadata } from "~/pages/PageEnigma/signals";
|
||||
import { CameraAspectRatio } from "~/pages/PageEnigma/enums";
|
||||
import { useContext, useMemo } from "react";
|
||||
import { EngineContext } from "~/pages/PageEnigma/contexts/EngineContext";
|
||||
import { faChevronLeft } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
export enum WizardType {
|
||||
BACKGROUND = "background",
|
||||
CHARACTER = "character",
|
||||
OBJECT = "object",
|
||||
QUESTION = "question",
|
||||
MAIN_PAGE = "main-page",
|
||||
REMIX = "remix",
|
||||
TEXT_FIELD = "text_field",
|
||||
NONE = "none",
|
||||
}
|
||||
|
||||
export interface WizardItem {
|
||||
label: string;
|
||||
type: WizardType;
|
||||
options?: SelectOption[];
|
||||
showNext?: () => boolean;
|
||||
nextTitle?: string;
|
||||
showPrior?: () => boolean;
|
||||
priorTitle?: string;
|
||||
onPrior?: string;
|
||||
nextPage?: string;
|
||||
onNext?: (() => void) | string;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export const Wizard = () => {
|
||||
useSignals();
|
||||
const editorEngine = useContext(EngineContext);
|
||||
|
||||
const wizardItems: Record<string, WizardItem> = useMemo(
|
||||
() => ({
|
||||
initial: {
|
||||
label: "Welcome to Storyteller Studio",
|
||||
type: WizardType.MAIN_PAGE,
|
||||
showPrior: () => false,
|
||||
showNext: () => false,
|
||||
options: [
|
||||
{
|
||||
label: "Remix",
|
||||
value: "remix",
|
||||
},
|
||||
{
|
||||
label: "Blank Scene",
|
||||
value: "end",
|
||||
},
|
||||
],
|
||||
width: 593,
|
||||
},
|
||||
new_scene: {
|
||||
label: "Create a New Scene",
|
||||
type: WizardType.MAIN_PAGE,
|
||||
showPrior: () => false,
|
||||
showNext: () => false,
|
||||
options: [
|
||||
{
|
||||
label: "Remix",
|
||||
value: "remix",
|
||||
},
|
||||
{
|
||||
label: "Blank Scene",
|
||||
value: "new_scene_title",
|
||||
},
|
||||
],
|
||||
width: 593,
|
||||
},
|
||||
new_scene_title: {
|
||||
label: "Add New Scene Title",
|
||||
type: WizardType.TEXT_FIELD,
|
||||
showNext: () => !!textInput.value,
|
||||
nextTitle: "Add New Scene",
|
||||
onNext: () => {
|
||||
resetSceneGenerationMetadata();
|
||||
editorEngine?.changeRenderCameraAspectRatio(
|
||||
CameraAspectRatio.HORIZONTAL_3_2,
|
||||
);
|
||||
editorEngine?.newScene(textInput.value);
|
||||
showWizard.value = "end";
|
||||
},
|
||||
priorTitle: "Cancel",
|
||||
onPrior: "new_scene",
|
||||
},
|
||||
remix: {
|
||||
label: "Choose a Scene to Remix",
|
||||
type: WizardType.REMIX,
|
||||
nextTitle: "Remix Scene",
|
||||
showNext: () => !!selectedRemixCard.value,
|
||||
priorTitle: "Cancel",
|
||||
onPrior: "initial",
|
||||
onNext: () => {
|
||||
if (editorEngine && selectedRemixCard.value) {
|
||||
editorEngine.loadScene(selectedRemixCard.value?.token);
|
||||
showWizard.value = "end";
|
||||
}
|
||||
},
|
||||
onBack: () => {
|
||||
showWizard.value = "initial";
|
||||
},
|
||||
width: 1152,
|
||||
},
|
||||
wizard: {
|
||||
label: "What kind of scene do you want?",
|
||||
type: WizardType.QUESTION,
|
||||
showNext: () => false,
|
||||
options: [
|
||||
{
|
||||
label: "Dance",
|
||||
value: "dance",
|
||||
},
|
||||
{
|
||||
label: "Monologue",
|
||||
value: "mono",
|
||||
},
|
||||
],
|
||||
},
|
||||
dance: {
|
||||
label: "Pick a background",
|
||||
type: WizardType.BACKGROUND,
|
||||
nextPage: "danceCharacter",
|
||||
},
|
||||
danceCharacter: {
|
||||
label: "Pick a character",
|
||||
type: WizardType.CHARACTER,
|
||||
nextPage: "end",
|
||||
},
|
||||
mono: {
|
||||
label: "Pick a character",
|
||||
type: WizardType.CHARACTER,
|
||||
nextPage: "end",
|
||||
},
|
||||
}),
|
||||
[editorEngine],
|
||||
);
|
||||
|
||||
useSignalEffect(() => {
|
||||
if (showWizard.value === "end") {
|
||||
return;
|
||||
}
|
||||
currentStep.value = wizardItems[showWizard.value];
|
||||
});
|
||||
|
||||
if (!currentStep.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TransitionDialogue
|
||||
isOpen={showWizard.value !== "end"}
|
||||
onClose={() => (showWizard.value = "end")}
|
||||
title={currentStep.value.label}
|
||||
width={currentStep.value.width ?? 672}
|
||||
titleIcon={showWizard.value === "remix" ? faChevronLeft : undefined}
|
||||
titleIconClassName="text-white/60 hover:text-white/80 transition-colors duration-150"
|
||||
onTitleIconClick={
|
||||
showWizard.value === "remix"
|
||||
? () => (showWizard.value = "initial")
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<WizardStep />
|
||||
</TransitionDialogue>
|
||||
);
|
||||
};
|
||||
@@ -1,74 +0,0 @@
|
||||
import { Background } from "./Background";
|
||||
import { Button } from "~/components";
|
||||
import { Objects } from "./Objects";
|
||||
import { Characters } from "./Characters";
|
||||
import {
|
||||
currentStep,
|
||||
showWizard,
|
||||
} from "~/pages/PageEnigma/Wizard/signals/wizard";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { Question } from "~/pages/PageEnigma/Wizard/Question";
|
||||
import { WizardItem, WizardType } from "~/pages/PageEnigma/Wizard/Wizard";
|
||||
import { MainPage } from "~/pages/PageEnigma/Wizard/MainPage";
|
||||
import { Remix } from "~/pages/PageEnigma/Wizard/Remix";
|
||||
import { TextField } from "~/pages/PageEnigma/Wizard/TextField";
|
||||
|
||||
export const WizardStep = () => {
|
||||
useSignals();
|
||||
const step = currentStep.value as WizardItem;
|
||||
if (!step.nextTitle) {
|
||||
step.nextTitle = "Next";
|
||||
}
|
||||
if (!step.priorTitle) {
|
||||
step.priorTitle = "Prior";
|
||||
}
|
||||
|
||||
const wizardComponents: Record<WizardType, (() => JSX.Element) | null> = {
|
||||
[WizardType.BACKGROUND]: Background,
|
||||
[WizardType.CHARACTER]: Characters,
|
||||
[WizardType.OBJECT]: Objects,
|
||||
[WizardType.QUESTION]: Question,
|
||||
[WizardType.MAIN_PAGE]: MainPage,
|
||||
[WizardType.REMIX]: Remix,
|
||||
[WizardType.TEXT_FIELD]: TextField,
|
||||
[WizardType.NONE]: null,
|
||||
};
|
||||
|
||||
const Component = wizardComponents[step.type];
|
||||
if (Component === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Component />
|
||||
<div className="flex justify-end gap-2 pt-4">
|
||||
{(!step.showPrior || step.showPrior()) && (
|
||||
<Button
|
||||
variant="action"
|
||||
onClick={() => {
|
||||
showWizard.value = step.onPrior!;
|
||||
}}
|
||||
>
|
||||
{step.priorTitle}
|
||||
</Button>
|
||||
)}
|
||||
{(!step.showNext || step.showNext()) && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (typeof step.onNext === "string") {
|
||||
showWizard.value = step.onNext;
|
||||
return;
|
||||
}
|
||||
if (step.onNext) {
|
||||
step.onNext();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{step.nextTitle}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
import { signal } from "@preact/signals-core";
|
||||
import { MediaItem } from "~/pages/PageEnigma/models";
|
||||
import { WizardItem } from "~/pages/PageEnigma/Wizard/Wizard";
|
||||
|
||||
export const showWizard = signal("");
|
||||
export const currentStep = signal<WizardItem | null>(null);
|
||||
export const textInput = signal("");
|
||||
|
||||
export const selectedAudios = signal<MediaItem[]>([]);
|
||||
export const selectedBackground = signal<MediaItem | null>(null);
|
||||
export const selectedCharacters = signal<MediaItem[]>([]);
|
||||
export const selectedObjects = signal<MediaItem[]>([]);
|
||||
|
||||
export const audioWizardItems = signal<MediaItem[] | null>(null);
|
||||
export const backgroundWizardItems = signal<MediaItem[] | null>(null);
|
||||
export const characterWizardItems = signal<MediaItem[] | null>(null);
|
||||
export const objectWizardItems = signal<MediaItem[] | null>(null);
|
||||
|
||||
export const selectedRemixCard = signal<{
|
||||
text: string;
|
||||
defaultVideo: string;
|
||||
hoverVideo: string;
|
||||
token: string;
|
||||
} | null>(null);
|
||||
@@ -1,5 +1,6 @@
|
||||
import { faCube, faMagicWandSparkles } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { Button, Tooltip } from "~/components";
|
||||
import { Tooltip } from "~/components";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { useState } from "react";
|
||||
import { AssetModal } from "./AssetModal";
|
||||
import { assetModalVisibleDuringDrag } from "../../signals";
|
||||
|
||||
+2
-1
@@ -2,7 +2,8 @@ import { TransitionDialogue } from "~/components/reusable/TransitionDialogue";
|
||||
import { faPlus, faTrashAlt } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { PopoverItem } from "~/components/reusable/Popover";
|
||||
import { Button, Input, Label, Tooltip } from "~/components";
|
||||
import { Input, Label, Tooltip } from "~/components";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { SliderV2 } from "~/components/reusable/SliderV2/SliderV2";
|
||||
import { focalLengthDragging } from "~/pages/PageEnigma/signals/camera";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
+3
-1
@@ -14,7 +14,9 @@ import {
|
||||
timelineHeight,
|
||||
} from "../../signals";
|
||||
import { EngineContext } from "~/pages/PageEnigma/contexts/EngineContext";
|
||||
import { Button, H5, InputVector } from "~/components";
|
||||
import { H5, InputVector } from "~/components";
|
||||
import { Button } from "@storyteller/ui-button"
|
||||
|
||||
import { editorState } from "~/pages/PageEnigma/signals/engine";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { EditorStates } from "~/pages/PageEnigma/enums";
|
||||
|
||||
@@ -5,7 +5,9 @@ import {
|
||||
faPlus,
|
||||
faUpRightAndDownLeftFromCenter,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { Button, ButtonIconSelect, Tooltip } from "~/components";
|
||||
import { ButtonIconSelect, Tooltip } from "~/components";
|
||||
import { Button } from "@storyteller/ui-button"
|
||||
|
||||
import { EngineContext } from "../../contexts/EngineContext";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { assetModalVisibleDuringDrag, assetModalVisible } from "../../signals";
|
||||
|
||||
+5
-10
@@ -4,7 +4,6 @@ import { useSignals, useSignalEffect } from "@preact/signals-react/runtime";
|
||||
import {
|
||||
faCheckSquare,
|
||||
faFile,
|
||||
faQuestion,
|
||||
faSquare,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
@@ -17,15 +16,14 @@ import { scene, signalScene, authentication, addToast } from "~/signals";
|
||||
import { outlinerIsShowing } from "~/pages/PageEnigma/signals/outliner/outliner";
|
||||
|
||||
import {
|
||||
ButtonDialogue,
|
||||
ButtonDropdown,
|
||||
Input,
|
||||
H4,
|
||||
Button,
|
||||
} from "~/components";
|
||||
|
||||
import {Button} from "@storyteller/ui-button";
|
||||
|
||||
import { TestFeaturesButtons } from "./TestFeaturesButtons";
|
||||
import { Help } from "./Help";
|
||||
import { LoadUserScenes } from "./LoadUserScenes";
|
||||
import { getCurrentLocationWithoutParams, isNumberString } from "~/utilities";
|
||||
import { SceneGenereationMetaData as SceneGenerationMetaData } from "~/pages/PageEnigma/models/sceneGenerationMetadata";
|
||||
@@ -42,10 +40,7 @@ import {
|
||||
} from "~/pages/PageEnigma/signals";
|
||||
import { CameraAspectRatio } from "~/pages/PageEnigma/enums";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import {
|
||||
showWizard,
|
||||
textInput,
|
||||
} from "~/pages/PageEnigma/Wizard/signals/wizard";
|
||||
|
||||
|
||||
export const ControlsTopButtons = () => {
|
||||
useSignals();
|
||||
@@ -300,7 +295,7 @@ export const ControlsTopButtons = () => {
|
||||
Outliner
|
||||
</Button>
|
||||
|
||||
<ButtonDialogue
|
||||
{/* <ButtonDialogue
|
||||
buttonProps={{
|
||||
variant: "secondary",
|
||||
label: "Help",
|
||||
@@ -320,7 +315,7 @@ export const ControlsTopButtons = () => {
|
||||
}
|
||||
>
|
||||
<Help />
|
||||
</ButtonDialogue>
|
||||
</ButtonDialogue> */}
|
||||
</div>
|
||||
<TestFeaturesButtons debug={false} />
|
||||
</div>
|
||||
|
||||
+3
-2
@@ -1,6 +1,7 @@
|
||||
import { useContext } from "react";
|
||||
import { Button } from "~/components";
|
||||
//TODO: DELETE
|
||||
|
||||
import { useContext } from "react";
|
||||
import { Button } from "@storyteller/ui-button"
|
||||
import { EngineContext } from "~/pages/PageEnigma/contexts/EngineContext";
|
||||
|
||||
export const TestFeaturesButtons = ({ debug }: { debug: boolean }) => {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { useState } from "react";
|
||||
import { MediaFile } from "~/pages/PageEnigma/models";
|
||||
import { Button, Input, Label, TransitionDialogue } from "~/components";
|
||||
import { Input, Label, TransitionDialogue } from "~/components";
|
||||
import { Button } from "@storyteller/ui-button"
|
||||
|
||||
import {
|
||||
faArrowDownToLine,
|
||||
faArrowRight,
|
||||
@@ -12,7 +14,7 @@ import {
|
||||
import SocialButton from "./SocialButton";
|
||||
import { generateMovieId, viewMyMovies } from "~/pages/PageEnigma/signals";
|
||||
import dayjs from "dayjs";
|
||||
import environmentVariables from "~/Classes/EnvironmentVariables";
|
||||
|
||||
import { downloadFile } from "~/pages/PageEnigma/comps/GenerateModals/utils/downloadFile";
|
||||
import { GetCdnOrigin } from "~/api/GetCdnOrigin";
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { TransitionDialogue } from "~/components/reusable/TransitionDialogue";
|
||||
import { faCheck } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Button, LoadingSpinner, CloseButton } from "~/components";
|
||||
import { LoadingSpinner, CloseButton } from "~/components";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { TabSelector } from "~/components/reusable/TabSelector";
|
||||
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import { LibraryModalApi } from "./LibraryModalApi";
|
||||
|
||||
@@ -17,7 +17,8 @@ import {
|
||||
faLockOpen,
|
||||
faXmark,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { Button, Input } from "~/components";
|
||||
import { Input } from "~/components";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { Transition } from "@headlessui/react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from "react";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { poseMode, showPoseControls } from "../signals/selectedMode";
|
||||
import { Button, Tooltip } from "~/components";
|
||||
import { Tooltip } from "~/components";
|
||||
import { Button } from "@storyteller/ui-button"
|
||||
import { faCheck, faPersonRunning } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
declare global {
|
||||
|
||||
+2
-1
@@ -23,7 +23,8 @@ import { pageWidth } from "~/signals";
|
||||
import { CameraAspectRatio, EditorStates } from "~/pages/PageEnigma/enums";
|
||||
import { CameraViewCanvas } from "~/pages/PageEnigma/comps/EngineCanvases";
|
||||
|
||||
import { Button, ButtonIcon, Tooltip } from "~/components";
|
||||
import { ButtonIcon, Tooltip } from "~/components";
|
||||
import { Button } from "@storyteller/ui-button"
|
||||
import { useEffect } from "react";
|
||||
|
||||
export const PreviewEngineCamera = () => {
|
||||
|
||||
+1
-2
@@ -1,9 +1,9 @@
|
||||
// TODO: Move into shared component.
|
||||
import { MediaItem } from "~/pages/PageEnigma/models";
|
||||
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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faUpDownLeftRight } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
@@ -37,7 +37,6 @@ const patchExpressionObjectType = (mediaType: string) => {
|
||||
|
||||
export const ItemElement = ({ item }: Props) => {
|
||||
useSignals();
|
||||
// const [imageError, setImageError] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="group relative w-full select-none overflow-hidden transition-all duration-200">
|
||||
|
||||
+2
-1
@@ -4,7 +4,8 @@ import { v4 as uuidv4 } from "uuid";
|
||||
import { faVolumeHigh, faChevronRight } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { H4, H6, Button, Label, Textarea } from "~/components";
|
||||
import { H4, H6, Label, Textarea } from "~/components";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
|
||||
import { TtsState } from "~/pages/PageEnigma/models/voice";
|
||||
import { AudioTabPages } from "~/pages/PageEnigma/enums";
|
||||
|
||||
+4
-3
@@ -6,9 +6,10 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { ToastTypes } from "~/enums";
|
||||
import { AudioTabPages } from "~/pages/PageEnigma/enums";
|
||||
import { V2VState } from "~/pages/PageEnigma/models/voice";
|
||||
import { VoiceConversionApi } from "~/Classes/ApiManager/VoiceConversionApi";
|
||||
import { addToast, startPollingActiveJobs } from "~/signals";
|
||||
import { H4, H6, Button, Label, UploadAudioComponent } from "~/components";
|
||||
import { VoiceConversionApi } from "~/Classes/ApiManager/VoiceConversionApi\";
|
||||
import { addToast, startPollingActiveJobs } from "~/signals\";
|
||||
import { H4, H6, Label, UploadAudioComponent } from "~/components";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
|
||||
export const PageVoicetoVoice = ({
|
||||
changePage,
|
||||
|
||||
+5
-1
@@ -13,12 +13,16 @@ import {
|
||||
ItemElements,
|
||||
} from "~/pages/PageEnigma/comps/SidePanelTabs/sharedComps";
|
||||
import {
|
||||
Button,
|
||||
FilterButtons,
|
||||
SearchFilter,
|
||||
Pagination,
|
||||
UploadModal3D,
|
||||
} from "~/components";
|
||||
|
||||
import {
|
||||
Button
|
||||
} from "@storyteller/ui-button"
|
||||
|
||||
import { isAnyStatusFetching } from "../utilities";
|
||||
import {
|
||||
useUserObjects,
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
import { faFilm } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { Button, Label } from "~/components";
|
||||
import { Label } from "~/components";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { GenerationOptions } from "~/pages/PageEnigma/models/generationOptions";
|
||||
import Queue from "~/pages/PageEnigma/Queue/Queue";
|
||||
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
|
||||
|
||||
+2
-1
@@ -22,7 +22,8 @@ import {
|
||||
faImage,
|
||||
faXmark,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { Button, Label, P, TransitionDialogue } from "~/components";
|
||||
import { Label, P, TransitionDialogue } from "~/components";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { EngineContext } from "~/pages/PageEnigma/contexts/EngineContext";
|
||||
import { MediaFilesApi } from "~/Classes/ApiManager/MediaFilesApi";
|
||||
import { BucketConfig } from "~/api/BucketConfig";
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
import { Button, Label } from "~/components";
|
||||
import { Label } from "~/components";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import {
|
||||
faArrowsRotate,
|
||||
faChevronLeft,
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { Button, TransitionDialogue } from "~/components";
|
||||
import { TransitionDialogue } from "~/components";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { StyleOptions } from "~/pages/PageEnigma/comps/SidePanelTabs/tabComps/StylizeTab/StyleOptions";
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -1,198 +0,0 @@
|
||||
import { AnimationClip } from "../clips/animation_clip";
|
||||
import { AudioClip } from "../clips/audio_clip";
|
||||
import { ClipUI } from "../clips/clip_ui";
|
||||
import { EmotionClip } from "../clips/emotion_clip";
|
||||
import { LipSyncClip } from "../clips/lipsync_clip";
|
||||
import { TransformClip } from "../clips/transform_clip";
|
||||
import { CharacterAnimationEngine } from "../Editor/Engines/CharacterAnimationEngine";
|
||||
import AudioEngine from "../Editor/Engines/audio_engine";
|
||||
import EmotionEngine from "../Editor/Engines/emotion_engine";
|
||||
import LipSyncEngine from "../Editor/Engines/lip_sync_engine";
|
||||
import { TimeLine } from "../Editor/timeline";
|
||||
import TransformEngine from "../Editor/Engines/transform_engine";
|
||||
import Ijson from "~/interfaces/Ijson";
|
||||
import { ClipGroup, ClipType } from "~/enums";
|
||||
|
||||
export class StoryTellerProxyTimeline {
|
||||
timeline: TimeLine;
|
||||
transform_engine: TransformEngine;
|
||||
animation_engine: CharacterAnimationEngine;
|
||||
audio_engine: AudioEngine;
|
||||
lipsync_engine: LipSyncEngine;
|
||||
emotion_engine: EmotionEngine;
|
||||
|
||||
constructor(
|
||||
version: number,
|
||||
timeline: TimeLine,
|
||||
transform_engine: TransformEngine,
|
||||
animation_engine: CharacterAnimationEngine,
|
||||
audio_engine: AudioEngine,
|
||||
lipsync_engine: LipSyncEngine,
|
||||
emotion_engine: EmotionEngine,
|
||||
) {
|
||||
this.timeline = timeline;
|
||||
this.transform_engine = transform_engine;
|
||||
this.animation_engine = animation_engine;
|
||||
this.audio_engine = audio_engine;
|
||||
this.lipsync_engine = lipsync_engine;
|
||||
this.emotion_engine = emotion_engine;
|
||||
}
|
||||
|
||||
private async getItemsToJson(items: Ijson[]): Promise<unknown[]> {
|
||||
const timeline_items_data: unknown[] = [];
|
||||
items.forEach((element) => { timeline_items_data.push(element.toJSON()); });
|
||||
|
||||
return timeline_items_data;
|
||||
}
|
||||
|
||||
private async getItemsDict(items: {
|
||||
[key: string]: Ijson;
|
||||
}): Promise<{ [key: string]: unknown }> {
|
||||
const timeline_items_data: { [key: string]: unknown } = {};
|
||||
for (const key in items) {
|
||||
if (Object.prototype.hasOwnProperty.call(items, key)) {
|
||||
const element = items[key];
|
||||
timeline_items_data[key] = element.toJSON();
|
||||
}
|
||||
}
|
||||
return timeline_items_data;
|
||||
}
|
||||
|
||||
public async saveToJson(): Promise<unknown> {
|
||||
const timeline_json = {
|
||||
timeline: await this.getItemsToJson(this.timeline.timeline_items),
|
||||
transform: await this.getItemsDict(this.transform_engine.clips),
|
||||
animation: this.animation_engine.toJSON(),
|
||||
audio: await this.getItemsDict(this.audio_engine.clips),
|
||||
lipsync: await this.getItemsDict(this.lipsync_engine.clips),
|
||||
emotion: await this.getItemsDict(this.emotion_engine.clips),
|
||||
};
|
||||
console.log(timeline_json);
|
||||
|
||||
return timeline_json;
|
||||
}
|
||||
|
||||
private async loadTimelineClips(timeline_clips: any[]): Promise<any[]> {
|
||||
const new_clips = [];
|
||||
if (timeline_clips) {
|
||||
for (let index = 0; index < timeline_clips.length; index++) {
|
||||
const element = timeline_clips[index];
|
||||
const clip = new ClipUI(
|
||||
element.version,
|
||||
element.type,
|
||||
element.group,
|
||||
element.name,
|
||||
element.media_id,
|
||||
element.clip_uuid,
|
||||
element.object_uuid,
|
||||
element.object_name,
|
||||
element.start_offset,
|
||||
element.ending_offset,
|
||||
element.keyframe_offset,
|
||||
element.media_file_type,
|
||||
);
|
||||
new_clips.push(clip);
|
||||
}
|
||||
}
|
||||
return new_clips;
|
||||
}
|
||||
|
||||
private async loadTransformClips(items: {
|
||||
[key: string]: any;
|
||||
}): Promise<{ [key: string]: any }> {
|
||||
const timeline_items_data: { [key: string]: any } = {};
|
||||
for (const key in items) {
|
||||
if (items.hasOwnProperty(key)) {
|
||||
const element = items[key];
|
||||
timeline_items_data[key] = new TransformClip(
|
||||
element.version,
|
||||
element.object_uuid,
|
||||
element.length,
|
||||
element.media_id,
|
||||
);
|
||||
timeline_items_data[key].keyframes = element.keyframes;
|
||||
}
|
||||
}
|
||||
return timeline_items_data;
|
||||
}
|
||||
|
||||
private async loadAudioClips(items: {
|
||||
[key: string]: any;
|
||||
}): Promise<{ [key: string]: any }> {
|
||||
const timeline_items_data: { [key: string]: any } = {};
|
||||
for (const key in items) {
|
||||
if (items.hasOwnProperty(key)) {
|
||||
const element = items[key];
|
||||
timeline_items_data[key] = new AudioClip(
|
||||
element.version,
|
||||
element.media_id,
|
||||
element.volume,
|
||||
);
|
||||
}
|
||||
}
|
||||
return timeline_items_data;
|
||||
}
|
||||
|
||||
private async loadLipsyncClips(items: {
|
||||
[key: string]: any;
|
||||
}): Promise<{ [key: string]: any }> {
|
||||
const timeline_items_data: { [key: string]: any } = {};
|
||||
for (const key in items) {
|
||||
if (items.hasOwnProperty(key)) {
|
||||
const element = items[key];
|
||||
timeline_items_data[key] = new LipSyncClip(
|
||||
element.version,
|
||||
element.media_id,
|
||||
element.volume,
|
||||
);
|
||||
}
|
||||
}
|
||||
return timeline_items_data;
|
||||
}
|
||||
|
||||
private async loadEmotionClips(items: {
|
||||
[key: string]: any;
|
||||
}): Promise<{ [key: string]: any }> {
|
||||
const timeline_items_data: { [key: string]: any } = {};
|
||||
for (const key in items) {
|
||||
if (items.hasOwnProperty(key)) {
|
||||
const element = items[key];
|
||||
timeline_items_data[key] = new EmotionClip(
|
||||
element.version,
|
||||
element.media_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
return timeline_items_data;
|
||||
}
|
||||
|
||||
public async loadFromJson(timeline: any) {
|
||||
this.timeline.timeline_items = await this.loadTimelineClips(
|
||||
timeline["timeline"],
|
||||
);
|
||||
this.transform_engine.clips = await this.loadTransformClips(
|
||||
timeline["transform"],
|
||||
);
|
||||
this.audio_engine.clips = await this.loadAudioClips(timeline["audio"]);
|
||||
this.lipsync_engine.clips = await this.loadLipsyncClips(
|
||||
timeline["lipsync"],
|
||||
);
|
||||
this.emotion_engine.clips = await this.loadEmotionClips(
|
||||
timeline["emotion"],
|
||||
);
|
||||
|
||||
// Add characters and their animations
|
||||
const animationClips = this.timeline.timeline_items.filter((clip) => { clip.group === ClipGroup.CHARACTER && clip.type === ClipType.ANIMATION })
|
||||
animationClips.forEach((clip) => {
|
||||
const characterObject = this.timeline.scene.get_object_by_uuid(clip.object_uuid);
|
||||
|
||||
if (!characterObject) {
|
||||
return
|
||||
}
|
||||
|
||||
this.animation_engine.addCharacterAnimationMedia(characterObject, clip.media_id, clip);
|
||||
});
|
||||
|
||||
this.timeline.updateUI();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user