Compare commits

..
379 changed files with 4377 additions and 24482 deletions
@@ -17,15 +17,7 @@ pub struct AppPreferencesPayload {
/// Play sounds on events.
pub play_sounds: bool,
/// Key pointing to file; defined in the frontend code.
/// Defined for enqueue since image enqueue can be async
pub enqueue_success_sound: Option<String>,
/// Key pointing to file; defined in the frontend code.
/// Defined for enqueue since image enqueue can be async
pub enqueue_failure_sound: Option<String>,
/// Key pointing to file; defined in the frontend code.
pub generation_success_sound: Option<String>,
@@ -33,7 +25,6 @@ pub struct AppPreferencesPayload {
pub generation_failure_sound: Option<String>,
/// Key pointing to file; defined in the frontend code.
#[deprecated]
pub generation_enqueue_sound: Option<String>,
}
@@ -61,8 +52,6 @@ async fn get_prefs(app_prefs: &AppPreferencesManager) -> AnyhowResult<AppPrefere
Ok(AppPreferencesPayload {
preferred_download_directory: prefs.preferred_download_directory,
play_sounds: prefs.play_sounds,
enqueue_success_sound: prefs.enqueue_success_sound,
enqueue_failure_sound: prefs.enqueue_failure_sound,
generation_success_sound: prefs.generation_success_sound,
generation_failure_sound: prefs.generation_failure_sound,
generation_enqueue_sound: prefs.generation_enqueue_sound,
@@ -1,119 +0,0 @@
use serde::Serialize;
use std::error::Error;
use std::str::FromStr;
/// Easy to use Result<T, E> type.
pub type CommandResult<T, E> = Result<CommandSuccessResponseWrapper<T>, CommandErrorResponseWrapper<E>>;
/// Statuses for successful commands.
#[derive(Serialize, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub enum CommandSuccessStatus {
Success, // 200
}
/// Statuses for errorful commands.
#[derive(Serialize, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub enum CommandErrorStatus {
BadRequest, // 400
Unauthorized, // 401
ServerError, // 500
}
#[derive(Serialize, Debug)]
pub struct CommandSuccessResponseWrapper<T: Serialize> {
/// Both "success" and "error" types have a `status` field for the frontend.
pub status: CommandSuccessStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub success_message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub payload: Option<T>,
}
#[derive(Serialize, Debug)]
pub struct CommandErrorResponseWrapper<T: Serialize> {
/// Both "success" and "error" types have a `status` field for the frontend.
pub status: CommandErrorStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub error_message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error_details: Option<T>,
}
impl CommandSuccessResponseWrapper<()> {
pub fn empty() -> Self {
CommandSuccessResponseWrapper {
status: CommandSuccessStatus::Success,
success_message: None,
payload: None,
}
}
}
/// NB: This is a hack because of `()` (a.k.a. the "unit" type) and `Serialize` trait coherence and overlap.
/// We cannot currently implement a trait for `()` and generic `Serialize` because `()` itself implements
/// `Serialize`. Rust's `"specialization"`, which would let us account for this, is an unstable, nightly-only
/// feature. So instead we manually mark all types other than `()` with `SerializeMarker` as a hack to
/// distinguish the two.
trait SerializeMarker : Serialize {}
// NB: () is Serialize! So we can't broadly implement just `T: Serialize`.
impl From<()> for CommandSuccessResponseWrapper<()> {
fn from(_: ()) -> Self {
CommandSuccessResponseWrapper {
status: CommandSuccessStatus::Success,
success_message: None,
payload: None,
}
}
}
/// NB: Note the `"+ SerializeMarker"` bound here. This is a hack to distinguish between `()` and other types
/// for which we will manually mark as `SerializeMarker`.
impl <T> From<T> for CommandSuccessResponseWrapper<T> where T: Serialize + SerializeMarker {
fn from(val: T) -> Self {
CommandSuccessResponseWrapper {
status: CommandSuccessStatus::Success,
success_message: None,
payload: Some(val),
}
}
}
impl From<&str> for CommandSuccessResponseWrapper<String> {
fn from(msg: &str) -> Self {
CommandSuccessResponseWrapper {
status: CommandSuccessStatus::Success,
success_message: Some(msg.to_string()),
payload: None,
}
}
}
impl From<&str> for CommandErrorResponseWrapper<String> {
fn from(value: &str) -> Self {
CommandErrorResponseWrapper {
status: CommandErrorStatus::BadRequest, // NB: Default to 500 error.
error_message: Some(value.to_string()),
error_details: None,
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_empty_tuple_serialize() {
let response = super::CommandSuccessResponseWrapper::<()>::from(());
let serialized = serde_json::to_string(&response).unwrap();
// NB: This tests that the blanket "Serialize" impl doesn't produce a "null payload" field.
assert_eq!(serialized, r#"{"status":"success"}"#);
}
}
@@ -1,5 +1,4 @@
pub mod app_preferences;
pub mod command_response_wrapper;
pub mod flip_image;
pub mod platform_info_command;
pub mod sora;
@@ -1,85 +0,0 @@
use crate::commands::app_preferences::get_app_preferences_command::AppPreferencesPayload;
use crate::state::app_preferences::app_preferences_manager::AppPreferencesManager;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::sora::sora_credential_manager::SoraCredentialManager;
use crate::windows::sora_login_window::open_sora_login_window::open_sora_login_window;
use errors::AnyhowResult;
use log::{error, info};
use once_cell::sync::Lazy;
use openai_sora_client::creds::sora_credential_set::SoraCredentialSet;
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
use openai_sora_client::requests::list_media::list_media::list_media;
use serde_derive::Serialize;
use tauri::{AppHandle, State};
#[derive(Serialize)]
#[serde(rename_all = "snake_case")]
pub struct CheckSoraSessionCommand {
pub state: SoraSessionState,
pub maybe_account_email: Option<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SoraSessionState{
#[serde(rename = "not_set_up")]
NotSetUp,
#[serde(rename = "expired_or_error")]
ExpiredOrError,
#[serde(rename = "valid")]
Valid,
}
#[tauri::command]
pub async fn check_sora_session_command(
app: AppHandle,
sora_creds_manager: State<'_, SoraCredentialManager>,
) -> Result<CheckSoraSessionCommand, CheckSoraSessionCommand> {
info!("check_sora_session_command called");
match do_check(&sora_creds_manager).await {
Ok(state) => Ok(state),
Err(err) => {
error!("Error checking session state: {:?}", err);
Err(CheckSoraSessionCommand {
state: SoraSessionState::ExpiredOrError,
maybe_account_email: None,
})
}
}
}
async fn do_check(
sora_creds_manager: &SoraCredentialManager,
) -> AnyhowResult<CheckSoraSessionCommand> {
let mut creds = match sora_creds_manager.get_credentials()? {
Some(creds) => creds,
None => {
return Ok(CheckSoraSessionCommand {
state: SoraSessionState::NotSetUp,
maybe_account_email: None,
});
}
};
let upgraded = maybe_upgrade_or_renew_session(&mut creds).await?;
list_media(&creds).await?;
if upgraded {
info!("Saving upgraded session to manager and disk...");
sora_creds_manager.set_credentials(&creds)?;
sora_creds_manager.persist_all_to_disk()?;
}
let maybe_account_email = creds
.jwt_bearer_token
.map(|token| token.jwt_claims().email.clone());
Ok(CheckSoraSessionCommand {
state: SoraSessionState::Valid,
maybe_account_email,
})
}
@@ -1,5 +1,3 @@
pub mod check_sora_session_command;
pub mod open_sora_login_command;
pub mod sora_image_generation_command;
pub mod sora_image_remix_command;
pub mod sora_logout_command;
@@ -1,21 +1,31 @@
use crate::state::app_preferences::app_preferences_manager::AppPreferencesManager;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::sora::sora_credential_manager::SoraCredentialManager;
use crate::windows::sora_login_window::open_sora_login_window::open_sora_login_window;
use crate::threads::sora_session_login_thread::{LOGIN_WINDOW_NAME, SORA_LOGIN_URL};
use crate::utils::clear_all_webview_cookies::clear_all_webview_cookies;
use errors::AnyhowResult;
use log::{error, info};
use once_cell::sync::Lazy;
use tauri::{AppHandle, State};
use std::time::Duration;
use tauri::{AppHandle, Manager, Url, WebviewUrl, WebviewWindowBuilder, WindowBuilder};
use tokio::time::sleep_until;
pub const START_URL_STR: &str = "https://storyteller.ai/";
pub static START_URL: Lazy<Url> = Lazy::new(|| {
Url::parse(START_URL_STR).expect("URL should parse")
});
pub const SORA_HOMEPAGE_URL_STR: &str = "https://sora.com/";
pub static SORA_HOMEPAGE_URL : Lazy<Url> = Lazy::new(|| {
Url::parse(SORA_HOMEPAGE_URL_STR).expect("URL should parse")
});
#[tauri::command]
pub async fn open_sora_login_command(
app: AppHandle,
app_data_root: State<'_, AppDataRoot>,
sora_creds_manager: State<'_, SoraCredentialManager>,
) -> Result<String, String> {
info!("open_login_command called");
do_open_login(&app, &app_data_root, &sora_creds_manager)
do_open_login(&app)
.await
.map_err(|err| {
error!("Error opening login: {:?}", err);
@@ -25,14 +35,37 @@ pub async fn open_sora_login_command(
Ok("result".to_string())
}
async fn do_open_login(
app: &AppHandle,
app_data_root: &AppDataRoot,
sora_creds_manager: &SoraCredentialManager,
) -> AnyhowResult<()> {
async fn do_open_login(app: &AppHandle) -> AnyhowResult<()> {
info!("Building login window...");
open_sora_login_window(app, app_data_root, sora_creds_manager).await?;
let url = WebviewUrl::External(START_URL.clone());
let window = WebviewWindowBuilder::new(app, LOGIN_WINDOW_NAME, url)
//.user_agent(openai_sora_client::credentials::USER_AGENT)
.always_on_top(true)
.title("Login to OpenAI")
.center()
.resizable(true)
.visible(true)
.closable(true)
.focused(true)
.devtools(true)
.build()?;
for (name, webview) in window.webviews() {
if name != LOGIN_WINDOW_NAME {
continue;
}
clear_all_webview_cookies(&webview)?;
// NB: We're starting to get Cloudflare protection screens. Let's try to avoid.
webview.navigate(SORA_HOMEPAGE_URL.clone())?;
tokio::time::sleep(Duration::from_millis(100)).await;
webview.navigate(SORA_LOGIN_URL.clone())?;
break;
}
info!("Done.");
Ok(())
@@ -1,7 +1,3 @@
use crate::commands::command_response_wrapper::CommandResult;
use crate::events::sendable_event_trait::SendableEvent;
use crate::events::sora::sora_image_enqueue_failure_event::SoraImageEnqueueFailureEvent;
use crate::events::sora::sora_image_enqueue_success_event::SoraImageEnqueueSuccessEvent;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::data_dir::trait_data_subdir::DataSubdir;
use crate::state::sora::read_sora_credentials_from_disk::read_sora_credentials_from_disk;
@@ -32,6 +28,16 @@ use storyteller_client::media_files::get_media_file::get_media_file;
use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, Emitter, Manager, State};
use tokens::tokens::media_files::MediaFileToken;
use crate::threads::sora_task_polling_thread::SoraImageGenerationComplete;
#[derive(Debug, Clone, Serialize)]
pub struct SoraImageEnqueueFailure {
// TODO: Reason.
}
#[derive(Debug, Clone, Serialize)]
pub struct SoraImageEnqueueSuccess {
}
#[derive(Deserialize)]
pub struct SoraImageRemixCommand {
@@ -57,7 +63,7 @@ pub async fn sora_image_remix_command(
app_data_root: State<'_, AppDataRoot>,
sora_creds_manager: State<'_, SoraCredentialManager>,
sora_task_queue: State<'_, SoraTaskQueue>,
) -> CommandResult<(), String> {
) -> Result<String, String> {
info!("image_generation_command called; scene media token: {:?}, additional images: {:?}",
request.snapshot_media_token, request.maybe_additional_images);
@@ -69,24 +75,20 @@ pub async fn sora_image_remix_command(
if let Err(err) = result {
error!("error: {:?}", err);
let event = SoraImageEnqueueFailureEvent {};
let result = event.send(&app);
let result = app.emit("sora-image-enqueue-failure", SoraImageEnqueueFailure{});
if let Err(err) = result {
error!("Failed to emit event: {:?}", err);
}
return Err("there was an error".into());
return Err("there was an error".to_string());
}
let event = SoraImageEnqueueSuccessEvent {};
let result = event.send(&app);
let result = app.emit("sora-image-enqueue-success", SoraImageEnqueueSuccess{});
if let Err(err) = result {
error!("Failed to emit event: {:?}", err);
}
Ok(().into())
Ok("success".to_string())
}
pub async fn generate_image(
@@ -1,35 +0,0 @@
use crate::commands::app_preferences::get_app_preferences_command::AppPreferencesPayload;
use crate::commands::command_response_wrapper::{CommandResult, CommandSuccessResponseWrapper};
use crate::state::app_preferences::app_preferences_manager::AppPreferencesManager;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::sora::sora_credential_manager::SoraCredentialManager;
use crate::windows::sora_login_window::open_sora_login_window::open_sora_login_window;
use errors::{AnyhowError, AnyhowResult};
use log::{error, info};
use once_cell::sync::Lazy;
use openai_sora_client::creds::sora_credential_set::SoraCredentialSet;
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
use openai_sora_client::requests::list_media::list_media::list_media;
use serde_derive::Serialize;
use std::fmt::format;
use tauri::{AppHandle, State};
#[tauri::command]
pub async fn sora_logout_command(
sora_creds_manager: State<'_, SoraCredentialManager>,
) -> CommandResult<(), String> {
info!("sora_logout_command called");
if let Err(err) = sora_creds_manager.purge_credentials_from_disk() {
error!("Error purging credentials from disk: {:?}", err);
return Err("error purging credentials from disk".into());
}
if let Err(err) = sora_creds_manager.clear_credentials() {
error!("Error clearing credentials from memory: {:?}", err);
return Err("error clearing credentials from memory".into());
}
Ok(().into())
}
+1 -4
View File
@@ -1,4 +1 @@
pub mod notification_event;
pub mod sendable_event_error;
pub mod sendable_event_trait;
pub mod sora;
pub mod notification_event;
@@ -1,31 +0,0 @@
use std::error::Error;
use std::fmt::{Display, Formatter};
#[derive(Debug)]
pub enum SendableEventError {
TauriError(tauri::Error),
AnyhowError(anyhow::Error),
}
impl Error for SendableEventError {}
impl Display for SendableEventError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::TauriError(err) => write!(f, "SendableEventError::TauriError: {}", err),
Self::AnyhowError(err) => write!(f, "SendableEventError::AnyhowError: {}", err),
}
}
}
impl From<anyhow::Error> for SendableEventError {
fn from(err: anyhow::Error) -> Self {
SendableEventError::AnyhowError(err)
}
}
impl From<tauri::Error> for SendableEventError {
fn from(err: tauri::Error) -> Self {
SendableEventError::TauriError(err)
}
}
@@ -1,21 +0,0 @@
use log::info;
use crate::events::sendable_event_error::SendableEventError;
use serde::Serialize;
use tauri::{AppHandle, Emitter};
/// Implement this on a Serializable object to be able to send it as an event to the frontend.
pub trait SendableEvent : Serialize {
/// This is the name of the event that the frontend subscribes to.
const FRONTEND_EVENT_NAME: &'static str;
/// Default implementation of send().
/// This serializes and sends the event to the frontend.
fn send(&self, app: &AppHandle) -> Result<(), SendableEventError> {
info!("Sending event to frontend: {}", Self::FRONTEND_EVENT_NAME);
let result = app.emit(Self::FRONTEND_EVENT_NAME, self);
if let Err(err) = result {
return Err(SendableEventError::from(err));
}
Ok(())
}
}
@@ -1,5 +0,0 @@
pub mod sora_image_enqueue_failure_event;
pub mod sora_image_enqueue_success_event;
pub mod sora_image_generation_complete_event;
pub mod sora_image_generation_failed_event;
pub mod sora_login_success_event;
@@ -1,11 +0,0 @@
use crate::events::sendable_event_trait::SendableEvent;
use serde_derive::Serialize;
#[derive(Debug, Clone, Serialize)]
pub struct SoraImageEnqueueFailureEvent {
// TODO: Reason.
}
impl SendableEvent for SoraImageEnqueueFailureEvent {
const FRONTEND_EVENT_NAME: &'static str = "sora-image-enqueue-failure";
}
@@ -1,11 +0,0 @@
use crate::events::sendable_event_trait::SendableEvent;
use serde_derive::Serialize;
#[derive(Debug, Clone, Serialize)]
pub struct SoraImageEnqueueSuccessEvent {
// TODO: Token, etc.
}
impl SendableEvent for SoraImageEnqueueSuccessEvent {
const FRONTEND_EVENT_NAME: &'static str = "sora-image-enqueue-success";
}
@@ -1,12 +0,0 @@
use serde_derive::Serialize;
use tokens::tokens::media_files::MediaFileToken;
use crate::events::sendable_event_trait::SendableEvent;
#[derive(Debug, Clone, Serialize)]
pub struct SoraImageGenerationCompleteEvent {
pub media_file_token: MediaFileToken,
}
impl SendableEvent for SoraImageGenerationCompleteEvent {
const FRONTEND_EVENT_NAME: &'static str = "sora-image-generation-complete";
}
@@ -1,12 +0,0 @@
use serde_derive::Serialize;
use crate::events::sendable_event_trait::SendableEvent;
#[derive(Debug, Clone, Serialize)]
pub struct SoraImageGenerationFailedEvent {
pub prompt: String,
}
impl SendableEvent for SoraImageGenerationFailedEvent {
const FRONTEND_EVENT_NAME: &'static str = "sora-image-generation-failed";
}
@@ -1,11 +0,0 @@
use crate::events::sendable_event_trait::SendableEvent;
use serde_derive::Serialize;
#[derive(Debug, Clone, Serialize)]
pub struct SoraLoginSuccessEvent {
// TODO: Reason.
}
impl SendableEvent for SoraLoginSuccessEvent {
const FRONTEND_EVENT_NAME: &'static str = "sora-login-success";
}
+6 -24
View File
@@ -3,33 +3,30 @@ pub mod events;
pub mod state;
pub mod threads;
pub mod utils;
pub mod windows;
use tauri::Manager;
use crate::commands::flip_image::flip_image;
use crate::commands::app_preferences::get_app_preferences_command::get_app_preferences_command;
use crate::commands::app_preferences::update_app_preference_command::update_app_preferences_command;
use crate::commands::flip_image::flip_image;
use crate::commands::platform_info_command::platform_info_command;
use crate::commands::sora::check_sora_session_command::check_sora_session_command;
use crate::commands::sora::open_sora_login_command::open_sora_login_command;
use crate::commands::sora::sora_image_generation_command::sora_image_generation_command;
use crate::commands::sora::sora_image_remix_command::sora_image_remix_command;
use crate::commands::sora::sora_logout_command::sora_logout_command;
use crate::state::app_preferences::app_preferences_manager::load_app_preferences_or_default;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::main_window_position::MainWindowPosition;
use crate::state::main_window_size::MainWindowSize;
use crate::state::sora::sora_credential_manager::SoraCredentialManager;
use crate::state::sora::sora_task_queue::SoraTaskQueue;
use crate::state::storyteller::storyteller_credential_manager::StorytellerCredentialManager;
use crate::threads::discord_presence_thread::discord_presence_thread;
use crate::threads::main_window_thread::main_window_thread::main_window_thread;
use crate::threads::sora_session_login_thread::sora_session_login_thread;
use crate::threads::sora_task_polling_thread::sora_task_polling_thread;
use crate::utils::webview_unsafe::webview_unsafe_for_app;
use tauri_plugin_dialog;
use crate::state::app_preferences::app_preferences_manager::load_app_preferences_or_default;
use tauri_plugin_http;
use tauri_plugin_dialog;
use tauri_plugin_log::Target;
use tauri_plugin_log::TargetKind;
@@ -111,20 +108,7 @@ pub fn run() {
}
}
match MainWindowPosition::from_filesystem_configs(&app_data_root_3) {
Ok(None) => {}
Ok(Some(pos)) => {
println!("Moving window to: {:?}", pos);
let result = pos.apply_to_main_window(&app);
if let Err(err) = result {
eprintln!("Could not set window position: {:?}", err);
}
}
Err(err) => {
eprintln!("Failed to read window position from disk: {:?}", err);
}
}
tauri::async_runtime::spawn(sora_session_login_thread(app_2, app_data_root_2, sora_creds_manager_2));
tauri::async_runtime::spawn(main_window_thread(app_3, app_data_root_3, storyteller_creds_manager_2));
tauri::async_runtime::spawn(sora_task_polling_thread(app_4, app_data_root_4, sora_creds_manager_3, storyteller_creds_manager_3, sora_task_queue_2));
tauri::async_runtime::spawn(discord_presence_thread());
@@ -136,15 +120,13 @@ pub fn run() {
.manage(sora_creds_manager)
.manage(sora_task_queue)
.invoke_handler(tauri::generate_handler![
check_sora_session_command,
flip_image,
get_app_preferences_command,
update_app_preferences_command,
open_sora_login_command,
platform_info_command,
sora_image_generation_command,
sora_image_remix_command,
sora_logout_command,
update_app_preferences_command,
])
.run(tauri::generate_context!("tauri.conf.json"))
.expect("error while running tauri application");
@@ -9,14 +9,6 @@ pub struct AppPreferences {
/// Play sounds on events.
pub play_sounds: bool,
/// Key pointing to file; defined in the frontend code.
/// Defined for enqueue since image enqueue can be async
pub enqueue_success_sound: Option<String>,
/// Key pointing to file; defined in the frontend code.
/// Defined for enqueue since image enqueue can be async
pub enqueue_failure_sound: Option<String>,
/// Key pointing to file; defined in the frontend code.
pub generation_success_sound: Option<String>,
@@ -25,7 +17,6 @@ pub struct AppPreferences {
pub generation_failure_sound: Option<String>,
/// Key pointing to file; defined in the frontend code.
#[deprecated]
pub generation_enqueue_sound: Option<String>,
}
@@ -35,9 +26,7 @@ impl Default for AppPreferences {
preferred_download_directory: PreferredDownloadDirectory::System(SystemDownloadDirectory::Downloads),
play_sounds: true,
// NB: These are defined in the frontend.
enqueue_success_sound: Some("done".to_string()),
enqueue_failure_sound: Some("spike_throw".to_string()),
generation_success_sound: Some("special_flower".to_string()),
generation_success_sound: Some("flower".to_string()),
generation_failure_sound: Some("crumble".to_string()),
generation_enqueue_sound: Some("done".to_string()),
}
@@ -136,10 +136,6 @@ impl AppDataRoot {
pub fn get_window_size_config_file(&self) -> PathBuf {
self.path.join("window_size.json")
}
pub fn get_window_position_config_file(&self) -> PathBuf {
self.path.join("window_position.json")
}
}
// eg. /home/bob/artcraft, /Users/bob/artcraft, or C:\Users\Alice\artcraft
@@ -1,73 +0,0 @@
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::windows::main_window::constants::MAIN_WINDOW_NAME;
use errors::AnyhowResult;
use serde_derive::{Deserialize, Serialize};
use std::fs::OpenOptions;
use std::io::Write;
use tauri::{AppHandle, Manager, PhysicalPosition, Window};
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
pub struct MainWindowPosition {
pub x: i32,
pub y: i32,
}
impl MainWindowPosition {
pub fn new(x: i32, y: i32) -> Self {
Self { x, y }
}
pub fn from_main_window(app: &AppHandle) -> AnyhowResult<Self> {
let windows = app.windows();
windows.get(MAIN_WINDOW_NAME)
.map(|window| Self::from_window(window))
.unwrap_or_else(|| Err(anyhow::anyhow!("Main window not found")))
}
pub fn from_window(window: &Window) -> AnyhowResult<Self> {
let position = window.outer_position()?;
Ok(Self {
x: position.x,
y: position.y,
})
}
pub fn from_filesystem_configs(app_data_root: &AppDataRoot) -> AnyhowResult<Option<Self>> {
let filename = app_data_root.get_window_position_config_file();
if !filename.exists() {
return Ok(None);
}
let contents = std::fs::read_to_string(filename)?;
let pos : MainWindowPosition = serde_json::from_str(&contents)?;
Ok(Some(pos))
}
pub fn apply_to_main_window(&self, app: &AppHandle) -> AnyhowResult<()> {
let windows = app.windows();
let window = windows.get(MAIN_WINDOW_NAME)
.ok_or_else(|| anyhow::anyhow!("Main window not found"))?;
window.set_position(PhysicalPosition::new(self.x, self.y))?;
Ok(())
}
pub fn persist_to_filesystem(&self, app_data_root: &AppDataRoot) -> AnyhowResult<()> {
let filename = app_data_root.get_window_position_config_file();
let json = serde_json::to_string(self)?;
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(filename)?;
file.write_all(json.as_bytes())?;
file.flush()?;
Ok(())
}
pub fn to_physical_position(&self) -> PhysicalPosition<i32> {
PhysicalPosition::new(self.x, self.y)
}
pub fn matches_physical_position(&self, pos: &PhysicalPosition<i32>) -> bool {
self.x == pos.x && self.y == pos.y
}
}
@@ -1,12 +1,12 @@
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::utils::best_window_size_heuristic::best_window_size_heuristic;
use crate::windows::main_window::constants::MAIN_WINDOW_NAME;
use errors::AnyhowResult;
use serde_derive::{Deserialize, Serialize};
use std::fs::OpenOptions;
use std::io::Write;
use tauri::{AppHandle, Manager, PhysicalSize, Window};
const MAIN_WINDOW_NAME: &str = "main";
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
pub struct MainWindowSize {
pub width: u32,
@@ -26,7 +26,7 @@ impl MainWindowSize {
}
pub fn from_window(window: &Window) -> AnyhowResult<Self> {
let size = best_window_size_heuristic(window)?;
let size = window.inner_size()?;
Ok(Self {
width: size.width,
height: size.height,
-1
View File
@@ -2,7 +2,6 @@ pub mod app_preferences;
pub mod app_startup_time;
pub mod data_dir;
pub mod expanduser;
pub mod main_window_position;
pub mod main_window_size;
pub mod os_platform;
pub mod sora;
@@ -1,4 +1,3 @@
use std::{fs, io};
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::sora::read_sora_credentials_from_disk::read_sora_credentials_from_disk;
use crate::state::sora::sora_credential_holder::SoraCredentialHolder;
@@ -10,7 +9,6 @@ use openai_sora_client::creds::sora_sentinel::SoraSentinel;
use openai_sora_client::requests::sentinel_refresh::generate::token::generate_token;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::Path;
use openai_sora_client::creds::sora_cookies::SoraCookies;
use openai_sora_client::creds::sora_jwt_bearer_token::SoraJwtBearerToken;
@@ -47,31 +45,6 @@ impl SoraCredentialManager {
Ok(())
}
/// This is meant to be infallible.
pub fn purge_credentials_from_disk(&self) -> Result<(), io::Error> {
let creds_dir = self.app_data_root.credentials_dir();
remove_file_if_exists(creds_dir.get_sora_cookie_file_path())?;
remove_file_if_exists(creds_dir.get_sora_bearer_token_file_path())?;
remove_file_if_exists(creds_dir.get_sora_sentinel_file_path())?;
Ok(())
}
/// This is meant to be infallible.
pub fn try_purge_credentials_from_disk(&self) {
let creds_dir = self.app_data_root.credentials_dir();
if let Err(err) = fs::remove_file(creds_dir.get_sora_cookie_file_path()) {
error!("Failed to remove cookie file: {:?}", err);
}
if let Err(err) = fs::remove_file(creds_dir.get_sora_bearer_token_file_path()) {
error!("Failed to remove bearer token file: {:?}", err);
}
if let Err(err) = fs::remove_file(creds_dir.get_sora_sentinel_file_path()) {
error!("Failed to remove sentinel file: {:?}", err);
}
}
pub fn get_credentials(&self) -> AnyhowResult<Option<SoraCredentialSet>> {
self.holder.get_credentials()
}
@@ -182,11 +155,3 @@ fn persist_sentinel_to_disk(sentinel: &SoraSentinel, app_data_root: &AppDataRoot
Ok(())
}
fn remove_file_if_exists<P: AsRef<Path>>(path: P) -> Result<(), io::Error> {
let p = path.as_ref();
if p.exists() && p.is_file() {
fs::remove_file(p)?;
}
Ok(())
}
@@ -1,7 +1,7 @@
use anyhow::anyhow;
use discord_rich_presence::{DiscordIpc, DiscordIpcClient};
use errors::AnyhowResult;
use log::{debug, info};
use log::info;
/// Our Discord App ID. Not sure if this is a secret.
const DISCORD_APP_ID : &str = "1366596912593113138";
@@ -15,13 +15,13 @@ pub async fn discord_presence_thread() -> ! {
}
async fn discord_main_loop(mut client: DiscordIpcClient) -> AnyhowResult<()> {
info!("(Re)-connecting Discord IPC client...");
info!("Connecting Discord IPC client");
client.connect()
.map_err(|err| anyhow!("Error connecting to Discord IPC: {:?}", err))?;
loop {
debug!("Notifying discord presence API...");
info!("Notifying discord...");
let assets = discord_rich_presence::activity::Assets::new()
.large_image("https://storyteller.ai/android-chrome-512x512.png")
@@ -37,6 +37,6 @@ async fn discord_main_loop(mut client: DiscordIpcClient) -> AnyhowResult<()> {
.assets(assets))
.map_err(|err| anyhow!("Error setting Discord activity: {:?}", err))?;
tokio::time::sleep(std::time::Duration::from_millis(60_000)).await;
tokio::time::sleep(std::time::Duration::from_millis(30_000)).await;
}
}
@@ -1,15 +1,13 @@
use crate::state::app_startup_time::AppStartupTime;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::main_window_position::MainWindowPosition;
use crate::state::main_window_size::MainWindowSize;
use crate::state::storyteller::storyteller_credential_manager::StorytellerCredentialManager;
use crate::threads::main_window_thread::persist_storyteller_cookies_task::persist_storyteller_cookies_task;
use crate::threads::main_window_thread::persist_window_position_task::persist_window_position_task;
use crate::threads::main_window_thread::persist_window_resize_task::persist_window_resize_task;
use errors::AnyhowResult;
use log::{error, info};
use memory_store::clone_slot::CloneSlot;
use tauri::{AppHandle, Manager, Webview, Window};
use crate::state::app_startup_time::AppStartupTime;
const MAIN_WINDOW_NAME : &str = "main";
@@ -20,7 +18,6 @@ pub async fn main_window_thread(
) -> ! {
// TODO: Move these into some kind of dependency injection framework
let window_size_slot: CloneSlot<MainWindowSize> = CloneSlot::empty();
let window_pos_slot: CloneSlot<MainWindowPosition> = CloneSlot::empty();
let app_startup_time = AppStartupTime::new();
loop {
@@ -32,7 +29,6 @@ pub async fn main_window_thread(
&app_data_root,
&storyteller_creds_manager,
&window_size_slot,
&window_pos_slot,
&app_startup_time,
).await;
if let Err(err) = result {
@@ -50,12 +46,10 @@ pub async fn handle_main_window(
app_data_root: &AppDataRoot,
storyteller_creds_manager: &StorytellerCredentialManager,
window_size_slot: &CloneSlot<MainWindowSize>,
window_pos_slot: &CloneSlot<MainWindowPosition>,
app_startup_time: &AppStartupTime,
) -> AnyhowResult<()> {
loop {
log_errors(persist_window_resize_task(window, app_data_root, window_size_slot).await);
log_errors(persist_window_position_task(window, app_data_root, window_pos_slot).await);
log_errors(persist_storyteller_cookies_task(app, storyteller_creds_manager, app_startup_time).await);
tokio::time::sleep(std::time::Duration::from_millis(1_000)).await;
}
@@ -1,4 +1,3 @@
pub mod main_window_thread;
pub mod persist_storyteller_cookies_task;
pub mod persist_window_position_task;
pub mod persist_window_resize_task;
@@ -1,6 +1,7 @@
use crate::state::app_startup_time::AppStartupTime;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::storyteller::storyteller_credential_manager::StorytellerCredentialManager;
use crate::threads::sora_session_login_thread::LOGIN_WINDOW_NAME;
use anyhow::anyhow;
use chrono::TimeDelta;
use errors::AnyhowResult;
@@ -1,34 +0,0 @@
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::main_window_position::MainWindowPosition;
use crate::state::main_window_size::MainWindowSize;
use crate::utils::best_window_size_heuristic::best_window_size_heuristic;
use errors::AnyhowResult;
use log::info;
use memory_store::clone_slot::CloneSlot;
use tauri::Window;
pub async fn persist_window_position_task(
window: &Window,
app_data_root: &AppDataRoot,
window_pos_slot: &CloneSlot<MainWindowPosition>,
) -> AnyhowResult<()> {
let current_window_position = MainWindowPosition::from_window(window)?;
let current_physical_pos = window.outer_position()?;
let mut save_size_to_disk = true;
if let Ok(Some(old_size)) = window_pos_slot.get_clone() {
if old_size.matches_physical_position(&current_physical_pos) {
save_size_to_disk = false;
}
}
if save_size_to_disk {
info!("Saving window position configs to disk...");
current_window_position.persist_to_filesystem(app_data_root)?;
window_pos_slot.set_clone(&current_window_position)?;
}
Ok(())
}
@@ -1,6 +1,5 @@
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::main_window_size::MainWindowSize;
use crate::utils::best_window_size_heuristic::best_window_size_heuristic;
use errors::AnyhowResult;
use log::info;
use memory_store::clone_slot::CloneSlot;
@@ -13,7 +12,7 @@ pub async fn persist_window_resize_task(
) -> AnyhowResult<()> {
let current_window_size = MainWindowSize::from_window(window)?;
let current_physical_size = best_window_size_heuristic(window)?;
let current_physical_size = window.inner_size()?;
let mut save_size_to_disk = true;
@@ -1,3 +1,4 @@
pub mod discord_presence_thread;
pub mod main_window_thread;
pub mod sora_session_login_thread;
pub mod sora_task_polling_thread;
@@ -0,0 +1,178 @@
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::sora::sora_credential_holder::SoraCredentialHolder;
use crate::state::sora::sora_credential_manager::SoraCredentialManager;
use crate::utils::cookies::sora_webview_cookies::get_all_sora_cookies_as_string;
use crate::utils::sora::initialize_sora_jwt_bearer_token::initialize_sora_jwt_bearer_token;
use anyhow::anyhow;
use chrono::{DateTime, NaiveDateTime, TimeDelta};
use errors::AnyhowResult;
use log::{error, info, warn};
use once_cell::sync::Lazy;
use openai_sora_client::creds::sora_credential_set::SoraCredentialSet;
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
use openai_sora_client::utils::has_session_cookie::{has_session_cookie, SessionCookiePresence};
use reqwest::Url;
use std::fs;
use std::fs::{read_to_string, OpenOptions};
use std::io::Write;
use std::ops::Sub;
use tauri::{AppHandle, Manager, Webview};
pub const LOGIN_WINDOW_NAME: &str = "login_window";
pub const SORA_LOGIN_URL_STR: &str = "https://sora.com/auth/login?callback_path=/";
pub static SORA_LOGIN_URL: Lazy<Url> = Lazy::new(|| {
Url::parse(SORA_LOGIN_URL_STR).expect("URL should parse")
});
pub const SORA_ROOT_URL_STR: &str = "https://sora.com/";
pub static SORA_ROOT_URL: Lazy<Url> = Lazy::new(|| {
Url::parse(SORA_ROOT_URL_STR).expect("URL should parse")
});
pub async fn sora_session_login_thread(
app: AppHandle,
app_data_root: AppDataRoot,
sora_creds_manager: SoraCredentialManager
) -> ! {
loop {
for (window_name, webview) in app.webviews() {
if window_name == LOGIN_WINDOW_NAME {
let result = check_login_window(
&webview,
&app_data_root,
&sora_creds_manager,
).await;
if let Err(err) = result {
error!("Error checking login window: {:?}", err);
}
break;
}
}
tokio::time::sleep(std::time::Duration::from_millis(2_000)).await;
}
}
async fn check_login_window(
webview: &Webview,
app_data_root: &AppDataRoot,
sora_credential_manager: &SoraCredentialManager,
) -> AnyhowResult<()> {
clear_browsing_data_on_test_domain(webview)?;
//keep_on_task(webview)?;
extract_cookies_to_file(webview, app_data_root, sora_credential_manager).await?;
//initialize_sora_jwt_bearer_token(app_data_root).await?; // TODO: This only runs once. We need better management.
Ok(())
}
fn keep_on_task(webview: &Webview) -> AnyhowResult<()> {
let url = webview.url()?;
let hostname= url.host()
.ok_or(anyhow!("no host in url"))?
.to_string();
match hostname.as_str() {
"auth.openai.com" => {},
"openai.com" => {},
"sora.com" => {},
// Third party SSO
"accounts.google.com" => {},
"accounts.youtube.com" => {},
"login.live.com" => {},
"appleid.apple.com" => {},
_ => {
info!("Non login hostname: {}", hostname);
webview.navigate(SORA_LOGIN_URL.clone())?;
}
}
Ok(())
}
/// This is just so we have a way to clear browsing data.
fn clear_browsing_data_on_test_domain(webview: &Webview) -> AnyhowResult<()> {
let url = webview.url()?;
let hostname= url.host()
.ok_or(anyhow!("no host in url"))?
.to_string();
match hostname.as_str() {
"storyteller.ai" => {
info!("Clearing all browsing data...");
webview.clear_all_browsing_data()?;
}
_ => {}
}
Ok(())
}
// TODO(bt,2025-04-07): Heuristic to detect when logged in. Only write when logged in.
async fn extract_cookies_to_file(
webview: &Webview,
app_data_root: &AppDataRoot,
sora_credential_manager: &SoraCredentialManager,
) -> AnyhowResult<()> {
let new_cookies = get_all_sora_cookies_as_string(webview)?.trim().to_string();
if new_cookies.is_empty() {
return Ok(());
}
let session_cookie_presence = has_session_cookie(&new_cookies)
.unwrap_or_else(|err| {
error!("Failed to check for session cookie: {:?}", err);
SessionCookiePresence::MaybePresent
});
info!("Session cookies are: {:?}", session_cookie_presence);
let (should_write_cookies, should_upgrade_session) = match session_cookie_presence {
SessionCookiePresence::Present => (true, true),
SessionCookiePresence::MaybePresent => (true, false),
SessionCookiePresence::Absent => (false, false),
};
if !should_write_cookies {
return Ok(());
}
// TODO(bt): Race conditions ahead.
sora_credential_manager.clear_credentials()?;
let _r = fs::remove_file(app_data_root.get_sora_cookie_file_path());
let _r = fs::remove_file(app_data_root.get_sora_bearer_token_file_path());
let _r = fs::remove_file(app_data_root.get_sora_sentinel_file_path());
let mut new_credentials =
SoraCredentialSet::initialize_with_just_cookies_str(&new_cookies);
if should_upgrade_session {
let _upgraded = maybe_upgrade_or_renew_session(&mut new_credentials).await?;
}
sora_credential_manager.set_credentials(&new_credentials)?;
sora_credential_manager.persist_all_to_disk()?;
Ok(())
}
pub fn read_sora_cookies_from_disk(app_data_root: &AppDataRoot) -> Option<String> {
let cookie_file = app_data_root.get_sora_cookie_file_path();
if !cookie_file.exists() {
return None;
}
match read_to_string(cookie_file) {
Ok(contents) => Some(contents.trim().to_string()),
Err(err) => {
warn!("Failed to read cookie file: {:?}", err);
None
}
}
}
@@ -1,6 +1,3 @@
use crate::events::sendable_event_trait::SendableEvent;
use crate::events::sora::sora_image_generation_complete_event::SoraImageGenerationCompleteEvent;
use crate::events::sora::sora_image_generation_failed_event::SoraImageGenerationFailedEvent;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::data_dir::trait_data_subdir::DataSubdir;
use crate::state::sora::sora_credential_manager::SoraCredentialManager;
@@ -21,6 +18,16 @@ use tauri::{AppHandle, Emitter};
use tempdir::TempDir;
use tokens::tokens::media_files::MediaFileToken;
#[derive(Debug, Clone, Serialize)]
pub struct SoraImageGenerationComplete {
pub media_file_token: MediaFileToken,
}
#[derive(Debug, Clone, Serialize)]
pub struct SoraImageGenerationFailed {
pub prompt: String,
}
pub async fn sora_task_polling_thread(
app_handle: AppHandle,
app_data_root: AppDataRoot,
@@ -95,10 +102,9 @@ async fn polling_loop(
for task in failed_tasks.iter() {
if sora_task_queue.contains_key(&task.id)? {
let event = SoraImageGenerationFailedEvent {
app_handle.emit("sora-image-generation-failed", SoraImageGenerationFailed {
prompt: task.prompt.clone(),
};
event.send(&app_handle)?;
})?;
}
}
@@ -127,11 +133,9 @@ async fn polling_loop(
info!("Uploaded to API backend: {:?}", result.media_file_token);
let event = SoraImageGenerationCompleteEvent {
media_file_token: result.media_file_token,
};
event.send(&app_handle)?;
app_handle.emit("sora-image-generation-complete", SoraImageGenerationComplete {
media_file_token: result.media_file_token
})?;
}
}
@@ -142,7 +146,7 @@ async fn polling_loop(
sora_task_queue.remove_list(&succeeded_task_ids)?;
tokio::time::sleep(std::time::Duration::from_millis(6_000)).await;
tokio::time::sleep(std::time::Duration::from_millis(2_000)).await;
}
}
@@ -1,25 +0,0 @@
use tauri::{PhysicalSize, Window};
use errors::AnyhowResult;
// A typical height difference when dev tools are open:
// Window inner size = PhysicalSize { width: 3704, height: 732 }
// Window outer size = PhysicalSize { width: 3704, height: 1476 }
const HEIGHT_DIFFERENCE_THRESHOLD: u32 = 300;
/// Get the best window size
pub fn best_window_size_heuristic(window: &Window) -> AnyhowResult<PhysicalSize<u32>> {
// NB: Ordinarily we'd use inner size, as the outer size includes the window decorations.
// However, dev tools contract the inner size, and in the event they're open, we should
// prefer the outer size.
let inner = window.inner_size()?;
let outer = window.outer_size()?;
let height_delta = outer.height.saturating_sub(inner.height);
if height_delta > HEIGHT_DIFFERENCE_THRESHOLD {
// Dev tools are open, use the outer size
Ok(outer)
} else {
// Dev tools are not open, use the inner size
Ok(inner)
}
}
@@ -0,0 +1 @@
pub mod sora_webview_cookies;
@@ -0,0 +1,26 @@
use once_cell::sync::Lazy;
use reqwest::Url;
use tauri::Webview;
use tauri::webview::Cookie;
use errors::AnyhowResult;
const SORA_ROOT_COOKIE_URL_STR: &str = "https://sora.com/";
static SORA_ROOT_COOKIE_URL: Lazy<Url> = Lazy::new(|| {
Url::parse(SORA_ROOT_COOKIE_URL_STR).expect("URL should parse")
});
pub fn get_all_sora_cookies(webview: &Webview) -> AnyhowResult<Vec<Cookie>> {
let cookies = webview.cookies_for_url(SORA_ROOT_COOKIE_URL.clone())?;
Ok(cookies)
}
pub fn get_all_sora_cookies_as_string(webview: &Webview) -> AnyhowResult<String> {
let cookies = get_all_sora_cookies(webview)?;
let cookie_string = cookies
.iter()
.map(|cookie| format!("{}={}", cookie.name(), cookie.value()))
.collect::<Vec<String>>()
.join("; ");
Ok(cookie_string)
}
+2 -2
View File
@@ -1,9 +1,9 @@
pub mod best_window_size_heuristic;
pub mod clear_all_webview_cookies;
pub mod cookies;
pub mod get_url_file_extension;
pub mod image;
pub mod large_file_transfer;
pub mod object_store;
pub mod simple_http_download;
pub mod sora;
pub mod webview_unsafe;
pub mod webview_unsafe;
@@ -1,4 +0,0 @@
/// The name of the main application window.
/// This is the Artcraft application UX itself.
pub const MAIN_WINDOW_NAME : &str = "main";
@@ -1 +0,0 @@
pub mod constants;
@@ -1,2 +0,0 @@
pub mod sora_login_window;
pub mod main_window;
@@ -1,20 +0,0 @@
use anyhow::anyhow;
use log::info;
use tauri::Webview;
use errors::AnyhowResult;
// This is just so we have a way to clear browsing data while debugging / building.
pub fn clear_browsing_data_on_test_domain(webview: &Webview) -> AnyhowResult<()> {
let url = webview.url()?;
let hostname= url.host()
.ok_or(anyhow!("no host in url"))?
.to_string();
match hostname.as_str() {
"storyteller.ai" => {
info!("Clearing all browsing data...");
webview.clear_all_browsing_data()?;
}
_ => {}
}
Ok(())
}
@@ -1,28 +0,0 @@
use errors::AnyhowResult;
use once_cell::sync::Lazy;
use reqwest::Url;
use tauri::webview::Cookie;
use tauri::WebviewWindow;
const CHAT_GPT_ROOT_COOKIE_URL_STR: &str = "https://chatgpt.com";
static CHAT_GPT_ROOT_COOKIE_URL: Lazy<Url> = Lazy::new(|| {
Url::parse(CHAT_GPT_ROOT_COOKIE_URL_STR).expect("URL should parse")
});
pub fn extract_sora_webview_cookies(webview: &WebviewWindow) -> AnyhowResult<String> {
// NB(bt): Cookies were originally on sora.com, but now they're located on
// the sora.chatgpt.com / *.chatgpt.com domains.
let cookies = get_all_chatgpt_cookies(webview)?;
let cookie_string = cookies
.iter()
.map(|cookie| format!("{}={}", cookie.name(), cookie.value()))
.collect::<Vec<String>>()
.join("; ");
Ok(cookie_string)
}
fn get_all_chatgpt_cookies(webview: &WebviewWindow) -> AnyhowResult<Vec<Cookie>> {
let cookies = webview.cookies_for_url(CHAT_GPT_ROOT_COOKIE_URL.clone())?;
Ok(cookies)
}
@@ -1,28 +0,0 @@
use crate::windows::sora_login_window::open_sora_login_window::SORA_LOGIN_URL;
use anyhow::anyhow;
use errors::AnyhowResult;
use log::info;
use tauri::Webview;
// NB: Probably best not to use this.
pub fn keep_on_login_domains(webview: &Webview) -> AnyhowResult<()> {
let url = webview.url()?;
let hostname= url.host()
.ok_or(anyhow!("no host in url"))?
.to_string();
match hostname.as_str() {
"auth.openai.com" => {},
"openai.com" => {},
"sora.com" => {},
// Third party SSO
"accounts.google.com" => {},
"accounts.youtube.com" => {},
"login.live.com" => {},
"appleid.apple.com" => {},
_ => {
info!("Non login hostname: {}", hostname);
webview.navigate(SORA_LOGIN_URL.clone())?;
}
}
Ok(())
}
@@ -1,5 +0,0 @@
pub mod clear_browsing_on_test_domain;
pub mod extract_sora_webview_cookies;
pub mod keep_on_login_domains;
pub mod open_sora_login_window;
pub mod sora_login_thread;
@@ -1,84 +0,0 @@
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::sora::sora_credential_manager::SoraCredentialManager;
use crate::utils::clear_all_webview_cookies::clear_all_webview_cookies;
use crate::windows::sora_login_window::sora_login_thread::sora_login_thread;
use anyhow::anyhow;
use errors::AnyhowResult;
use once_cell::sync::Lazy;
use reqwest::Url;
use std::time::Duration;
use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder};
/// Name of the window
pub const LOGIN_WINDOW_NAME: &str = "login_window";
pub const START_URL_STR: &str = "https://google.com";
pub static START_URL: Lazy<Url> = Lazy::new(|| {
Url::parse(START_URL_STR).expect("URL should parse")
});
pub const SORA_HOMEPAGE_URL_STR: &str = "https://sora.com/";
pub static SORA_HOMEPAGE_URL : Lazy<Url> = Lazy::new(|| {
Url::parse(SORA_HOMEPAGE_URL_STR).expect("URL should parse")
});
pub const SORA_LOGIN_URL_STR: &str = "https://chatgpt.com/auth/login?next=%2Fsora%2F";
pub static SORA_LOGIN_URL: Lazy<Url> = Lazy::new(|| {
Url::parse(SORA_LOGIN_URL_STR).expect("URL should parse")
});
//pub const SORA_ROOT_URL_STR: &str = "https://sora.com/";
//
//pub static SORA_ROOT_URL: Lazy<Url> = Lazy::new(|| {
// Url::parse(SORA_ROOT_URL_STR).expect("URL should parse")
//});
pub async fn open_sora_login_window(
app: &AppHandle,
app_data_root: &AppDataRoot,
sora_creds_manager: &SoraCredentialManager,
) -> AnyhowResult<()> {
if app.get_window(LOGIN_WINDOW_NAME).is_some() {
return Err(anyhow!("Login window already open"));
}
let url = WebviewUrl::External(START_URL.clone());
let window = WebviewWindowBuilder::new(app, LOGIN_WINDOW_NAME, url)
//.user_agent(openai_sora_client::credentials::USER_AGENT)
.always_on_top(true)
.title("Login to OpenAI's Sora")
.center()
.resizable(true)
.visible(true)
.closable(true)
.min_inner_size(200.0, 800.0)
.focused(true)
.devtools(true)
.build()?;
let webview = window.get_webview(LOGIN_WINDOW_NAME)
.ok_or_else(|| anyhow!("no webview found"))?;
clear_all_webview_cookies(&webview)?;
webview.navigate(SORA_HOMEPAGE_URL.clone())?;
// NB: We're starting to get Cloudflare protection screens. Let's try to avoid.
tokio::time::sleep(Duration::from_millis(100)).await;
webview.navigate(SORA_LOGIN_URL.clone())?;
let app_handle = app.clone();
let app_data_root = app_data_root.clone();
let sora_creds_manager = sora_creds_manager.clone();
let _ = tauri::async_runtime::spawn(async move {
sora_login_thread(app_handle, app_data_root, sora_creds_manager).await;
});
Ok(())
}
@@ -1,157 +0,0 @@
use crate::events::sendable_event_trait::SendableEvent;
use crate::events::sora::sora_login_success_event::SoraLoginSuccessEvent;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::sora::sora_credential_holder::SoraCredentialHolder;
use crate::state::sora::sora_credential_manager::SoraCredentialManager;
use crate::utils::sora::initialize_sora_jwt_bearer_token::initialize_sora_jwt_bearer_token;
use crate::windows::sora_login_window::extract_sora_webview_cookies::extract_sora_webview_cookies;
use crate::windows::sora_login_window::open_sora_login_window::LOGIN_WINDOW_NAME;
use anyhow::anyhow;
use chrono::{DateTime, NaiveDateTime, TimeDelta};
use errors::AnyhowResult;
use log::{error, info, warn};
use once_cell::sync::Lazy;
use openai_sora_client::creds::sora_credential_set::SoraCredentialSet;
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
use openai_sora_client::utils::has_session_cookie::{has_session_cookie, SessionCookiePresence};
use reqwest::Url;
use std::fs;
use std::fs::{read_to_string, OpenOptions};
use std::io::Write;
use std::ops::Sub;
use tauri::{AppHandle, Manager, Webview, WebviewWindow};
pub async fn sora_login_thread(
app: AppHandle,
app_data_root: AppDataRoot,
sora_creds_manager: SoraCredentialManager
) {
loop {
let login_webview_window = match app.get_webview_window(LOGIN_WINDOW_NAME) {
Some(webview) => webview,
None => {
info!("Exit sora login thread.");
return; // NB: Only exit if we don't have the webview.
}
};
let result = check_login_window(
&app,
&login_webview_window,
&app_data_root,
&sora_creds_manager,
).await;
match result {
Err(err) => {
error!("Error checking login window: {:?}", err);
}
Ok(false) => {} // Continue iteration and try again...
Ok(true) => {
info!("Successfully saved cookies from login window. Closing.");
if let Err(err) = login_webview_window.close() {
error!("Error closing login window: {:?}", err);
}
return;
}
}
tokio::time::sleep(std::time::Duration::from_millis(2_000)).await;
}
}
/// Returns true if we can exit.
async fn check_login_window(
app_handle: &AppHandle,
webview_window: &WebviewWindow,
app_data_root: &AppDataRoot,
sora_credential_manager: &SoraCredentialManager,
) -> AnyhowResult<bool> {
/* Login flow looks like this:
1. Start: https://sora.chatgpt.com/
2. Login Start: https://chatgpt.com/auth/login?next=%2Fsora%2F [this is where we start]
3. Login Continue: https://auth.openai.com/log-in
4. SSO / Google Login (etc): https://accounts.google.com/v3/signin/challenge/pwd?[query]
5. Done / Landing: https://sora.chatgpt.com/explore
*/
/*
- Do not use credential manager until the end (we don't load old cookies!)
- Check if we're on the correct domain, if not exit? (or inverse, that we're not in login flow)
*/
let hostname = get_hostname(webview_window)?;
let mut maybe_at_destination = false;
match hostname.as_str() {
"sora.com" | // Old destination
"sora.chatgpt.com" // New destination
=> {
maybe_at_destination = true;
}
// chatgpt.com/auth is also an auth domain
"auth.openai.com" |
"accounts.google.com" |
"accounts.youtube.com" |
"login.live.com" |
"appleid.apple.com"
=> {
// NB: We're in auth flow.
info!("Sora login webview is in auth flow; hostname `{}`.", hostname);
return Ok(false)
}
_ => {}, // We just don't know...
}
let webview_cookies = extract_sora_webview_cookies(webview_window)?.trim().to_string();
let session_cookie_presence = has_session_cookie(&webview_cookies)
.unwrap_or_else(|err| {
error!("Failed to check for session cookie: {:?}", err);
SessionCookiePresence::MaybePresent
});
info!("Sora login webview is at hostname `{}`; cookie status: {:?}.", hostname, session_cookie_presence);
match session_cookie_presence {
SessionCookiePresence::Absent => {
info!("Session cookies are absent.");
return Ok(false);
}
_ => {},
}
let mut new_credentials =
SoraCredentialSet::initialize_with_just_cookies_str(&webview_cookies);
let _upgraded = maybe_upgrade_or_renew_session(&mut new_credentials).await?;
// TODO(bt): Race conditions ahead.
sora_credential_manager.clear_credentials()?;
sora_credential_manager.try_purge_credentials_from_disk();
sora_credential_manager.set_credentials(&new_credentials)?;
sora_credential_manager.persist_all_to_disk()?;
// NB: Event sent to the frontend for the login flow. We shouldn't rely on just this
// alone as it could be brittle if the events aren't caught.
let event = SoraLoginSuccessEvent {};
if let Err(err) = event.send(app_handle) {
error!("Error sending Sora login success event: {:?}", err);
}
Ok(true)
}
fn get_hostname(webview: &WebviewWindow) -> AnyhowResult<String> {
let url = webview.url()?;
let url_hostname= url.host()
.ok_or(anyhow!("no host in url"))?
.to_string();
Ok(url_hostname)
}
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
{"default":{"identifier":"default","description":"enables the default permissions","local":true,"windows":["main"],"permissions":["core:default","log:default","upload:allow-download","upload:default",{"identifier":"http:default","allow":[{"url":"https://*.storyteller.ai"},{"url":"https://*.fakeyou.com"}],"deny":[{"url":"https://private.tauri.app"}]}]}}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -7,14 +7,13 @@ use chrono::{DateTime, TimeDelta, Utc};
use errors::AnyhowResult;
use log::info;
use std::ops::Sub;
use crate::sora_error::SoraError;
const EXPIRATION_DEADLINE : TimeDelta = TimeDelta::hours(12);
/// Call this at session startup to generate the tokens for the first time.
/// Call this periodically to refresh the JWT as it nears its expiration date.
/// This will not refresh the sentinel token once it's been generated - we can use other flows to accomplish that.
pub async fn maybe_upgrade_or_renew_session(sora_credentials: &mut SoraCredentialSet) -> Result<bool, SoraError> {
pub async fn maybe_upgrade_or_renew_session(sora_credentials: &mut SoraCredentialSet) -> AnyhowResult<bool> {
let mut refresh_jwt = !sora_credentials.jwt_bearer_token.is_some();
let refresh_sentinel = !sora_credentials.sora_sentinel.is_some();
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
pub mod list_media;
@@ -1,6 +1,5 @@
pub mod bearer;
pub mod image_gen;
pub mod job;
pub mod list_media;
pub mod sentinel_refresh;
pub mod upload;
@@ -26,23 +26,10 @@ pub enum SoraError {
/// }
UnauthorizedCookieOrBearerExpired,
/// Failed to generate a bearer token despite the `200 OK` response.
/// This has been generated by sending old/expired session cookies or empty cookies.
FailedToGenerateBearer,
/// Something is wrong with the JWT bearer token.
/// This error originates on our end as we try to parse the JWT.
LocalJwtClaimsParseError(String),
/// Sora is having issues.
/// Typically served from Cloudflare
/// We preserve the message for debugging.
BadGateway(String),
/// HTTP 524 (Cloudflare-specific)
/// Cloudflare formed a TCP connection to Sora, but no payload was delivered before timeout
/// We preserve the message for debugging
CloudFlareTimeout(String),
/// Another error occurred.
OtherBadStatus(anyhow::Error),
@@ -68,12 +55,6 @@ impl Display for SoraError {
Self::NoBearerTokenAvailable => {
write!(f, "Unauthorized: no bearer token set")
}
Self::FailedToGenerateBearer => {
write!(f, "Failed to generate bearer. Probably invalid session.")
}
Self::LocalJwtClaimsParseError(message) => {
write!(f, "Local JWT claims parse error: {}", message)
}
Self::TooManyConcurrentTasks => {
write!(f, "Too many concurrent tasks. Please wait.")
}
@@ -83,9 +64,6 @@ impl Display for SoraError {
Self::BadGateway(message) => {
write!(f, "Bad Gateway; Sora is likely having issues: {:?}", message)
}
Self::CloudFlareTimeout(message) => {
write!(f, "Cloudflare Timeout (524); Sora is likely having issues: {:?}", message)
}
Self::OtherBadStatus(err) => {
write!(f, "Other error: {}", err)
}
@@ -8,33 +8,7 @@ pub async fn classify_general_http_error(response: reqwest::Response) -> SoraErr
Ok(text) => text,
Err(err) => return SoraError::ReqwestError(err),
};
/*
Old cookie -
Message: {
"error": {
"message": "Your authentication token has expired. Please try signing in again.",
"type": "invalid_request_error",
"param": null,
"code": "token_expired"
}
}
No cookie -
Message: {
"error": {
"message": "Your authentication token has expired. Please try signing in again.",
"type": "invalid_request_error",
"param": null,
"code": "token_expired"
}
}
*/
// TODO: I *think* this is just the bearer token, not the cookie.
let cookie_expired =
message.contains("Your authentication token has expired. Please try signing in again.")
|| message.contains("token_expired");
@@ -43,18 +17,8 @@ Message: {
return SoraError::UnauthorizedCookieOrBearerExpired
}
let status_code = status.as_u16();
match status_code {
502 => {
return SoraError::BadGateway(message);
}
524 => {
if message.contains("cloudflare") || message.contains("Cloudflare") {
return SoraError::CloudFlareTimeout(message);
}
}
_ => {}, // Fall-through
if status.as_u16() == 502 {
return SoraError::BadGateway(message);
}
SoraError::OtherBadStatus(anyhow::anyhow!("Upload failed with status {}: {}", status, message))
@@ -4,8 +4,7 @@ use cookie::Cookie;
use once_cell::sync::Lazy;
use errors::AnyhowResult;
pub const SESSION_COOKIE_NAME_OLD : &str = "__Secure-authjs.session-token";
pub const SESSION_COOKIE_NAME_NEW : &str = "__Secure-next-auth.session-token";
pub const SESSION_COOKIE_NAME : &str = "__Secure-authjs.session-token";
/// Cookies Sora.com mints that are not the session cookie. We can ignore these.
/// This list is very likely to change over time.
@@ -52,9 +51,7 @@ pub fn has_session_cookie(cookies_header: &str) -> AnyhowResult<SessionCookiePre
for cookie in cookies {
let cookie= cookie?;
let cookie_name = cookie.name();
if cookie_name == SESSION_COOKIE_NAME_NEW {
return Ok(SessionCookiePresence::Present);
} else if cookie_name == SESSION_COOKIE_NAME_NEW {
if cookie_name == SESSION_COOKIE_NAME {
return Ok(SessionCookiePresence::Present);
} else if !IRRELEVANT_COOKIE_NAMES.contains(cookie_name) {
unknown_cookie_count += 1;
@@ -1,13 +1,12 @@
//! Adapted from 'jwt' crate
use crate::sora_error::SoraError;
use anyhow::anyhow;
use base64::prelude::BASE64_URL_SAFE_NO_PAD;
use base64::Engine;
use base64::prelude::BASE64_URL_SAFE_NO_PAD;
use chrono::{DateTime, Utc};
use errors::AnyhowResult;
use log::warn;
use serde_json::Value;
use errors::AnyhowResult;
const SEPARATOR: &str = ".";
@@ -20,53 +19,48 @@ pub struct SoraJwtClaims {
pub email_verified: bool,
}
pub fn lightweight_sora_jwt_parse(token: &str) -> Result<SoraJwtClaims, SoraError> {
pub fn lightweight_sora_jwt_parse(token: &str) -> AnyhowResult<SoraJwtClaims> {
let [_header_str, claims_str, _signature_str] = split_components(token)?;
let claims = decode_base64(claims_str)
.map_err(|err| SoraError::LocalJwtClaimsParseError(
format!("failure to parse claims base64: {:?}", err)))?;
let claims : serde_json::Map<String, Value> = serde_json::from_str(&claims)
.map_err(|err| SoraError::LocalJwtClaimsParseError(
format!("failure to parse claims json: {:?}", err)))?;
let claims = decode_base64(claims_str)?;
let claims : serde_json::Map<String, Value> = serde_json::from_str(&claims)?;
let iat = claims.get("iat")
.map(|val| val.as_number())
.flatten()
.ok_or_else(|| jwt_error("no iat claim"))?;
.ok_or(anyhow!("no iat claim"))?;
let iat = iat.as_i64()
.ok_or_else(|| jwt_error("iat is not a number"))?;
.ok_or(anyhow!("iat is not a number"))?;
let iat = DateTime::from_timestamp(iat, 0)
.ok_or_else(|| jwt_error("iat is not a valid timestamp"))?;
.ok_or(anyhow!("iat is not a valid timestamp"))?;
let exp = claims.get("exp")
.map(|val| val.as_number())
.flatten()
.ok_or_else(|| jwt_error("no exp claim"))?;
.ok_or(anyhow!("no exp claim"))?;
let exp = exp.as_i64()
.ok_or_else(|| jwt_error("exp is not a number"))?;
.ok_or(anyhow!("exp is not a number"))?;
let exp = DateTime::from_timestamp(exp, 0)
.ok_or_else(|| jwt_error("iat is not a valid timestamp"))?;
.ok_or(anyhow!("iat is not a valid timestamp"))?;
let user_id = claims.get("https://api.openai.com/auth")
.map(|val| val.get("user_id"))
.flatten()
.map(|val| val.as_str())
.flatten()
.ok_or_else(|| jwt_error("no user_id claim"))?;
.ok_or(anyhow!("no user_id claim"))?;
let profile = claims.get("https://api.openai.com/profile")
.ok_or_else(|| jwt_error("no user_id claim"))?;
.ok_or(anyhow!("no user_id claim"))?;
let email = profile.get("email")
.map(|val| val.as_str())
.flatten()
.ok_or_else(|| jwt_error("no email claim"))?;
.ok_or(anyhow!("no email claim"))?;
let email_verified = profile.get("email_verified")
.map(|val| val.as_bool())
@@ -85,19 +79,14 @@ pub fn lightweight_sora_jwt_parse(token: &str) -> Result<SoraJwtClaims, SoraErro
})
}
fn split_components(token: &str) -> Result<[&str; 3], SoraError> {
fn split_components(token: &str) -> AnyhowResult<[&str; 3]> {
let mut components = token.split(SEPARATOR);
let header = components.next()
.ok_or_else(|| jwt_error("no header component"))?;
let claims = components.next()
.ok_or_else(|| jwt_error("no claims component"))?;
let signature = components.next()
.ok_or_else(|| jwt_error("no signature component"))?;
let header = components.next().ok_or(anyhow!("no header component"))?;
let claims = components.next().ok_or(anyhow!("no claims component"))?;
let signature = components.next().ok_or(anyhow!("no signature component"))?;
if components.next().is_some() {
return Err(jwt_error("too many components"));
return Err(anyhow!("too many components"));
}
Ok([header, claims, signature])
@@ -109,10 +98,6 @@ fn decode_base64(raw: &str) -> AnyhowResult<String> {
Ok(json_str)
}
fn jwt_error(reason: &str) -> SoraError {
SoraError::LocalJwtClaimsParseError(reason.to_string())
}
#[cfg(test)]
mod tests {
use crate::utils::lightweight_sora_jwt_parse::lightweight_sora_jwt_parse;
@@ -4,12 +4,12 @@ import { faPalette } from "@fortawesome/pro-solid-svg-icons";
import {
ToolbarButton,
ToolbarButtonProps,
} from "../../../components/features/ToolbarButton";
import { paperWrapperStyles } from "../../../components/styles";
} from "~/components/features/ToolbarButton";
import { paperWrapperStyles } from "~/components/styles";
import { ToolbarNodeButtonNames } from "./enums";
import { ToolbarNodeButtonData } from "./data";
import { ColorPicker } from "../../../components/ui/TextEditor/ColorPicker";
import { DEFAULT_PAINT_COLOR } from "../../../signals/uiEvents/toolbarMain/paintMode";
import { ColorPicker } from "~/components/ui/TextEditor/ColorPicker";
import { DEFAULT_PAINT_COLOR } from "~/signals/uiEvents/toolbarMain/paintMode";
export interface ToolbarNodeProps {
disabled?: boolean;
@@ -1,6 +1,8 @@
import { useSignals } from "@preact/signals-react/runtime";
import { AUTH_STATUS } from "~/enums";
import { authentication } from "~/signals";
import { ButtonLink } from "~/components";
import ProfileDropdown from "./ProfileDropdown";
@@ -17,14 +17,11 @@ import { downloadFileFromUrl } from "@storyteller/api";
import { TabSelector, TabItem } from "@storyteller/ui-tab-selector";
import { Signal } from "@preact/signals-react";
import { useSignals } from "@preact/signals-react/runtime";
import { setLogoutStates } from "~/signals/authentication/utilities";
function isEditorPath(path: string) {
if (path === "/") return true;
if (path === "/idealenigma/") return true;
return false;
}
interface Props {
pageName: string;
appTabIdSignal: Signal<string>;
@@ -68,13 +65,23 @@ export const TopBar = ({ pageName, appTabIdSignal, setAppTabId }: Props) => {
Back to Editor
</ButtonLink>
)}
{/* <TabSelector
<TabSelector
tabs={appTabs}
activeTab={appTabIdSignal.value}
disabled={false}
onTabChange={(tabId: string) => setAppTabId(tabId)}
className="w-fit"
/> */}
/>
{/* <Button
variant="secondary"
icon={faChevronRight}
iconClassName="text-xs"
iconFlip={true}
onClick={handleClick}
className="bg-transparent p-0 text-sm text-white/80 hover:bg-transparent hover:text-white hover:underline hover:underline-offset-2"
>
Go to 2D Canvas
</Button> */}
</div>
<div className="flex items-center justify-center gap-2 font-medium">
@@ -111,7 +118,6 @@ export const TopBar = ({ pageName, appTabIdSignal, setAppTabId }: Props) => {
<SettingsModal
isOpen={isSettingsModalOpen}
onClose={() => setIsSettingsModalOpen(false)}
globalAccountLogoutCallback={() => setLogoutStates()}
/>
<GalleryModal
+3 -3
View File
@@ -66,8 +66,8 @@ export class Configs {
break;
}
//console.debug(`Build environment type: ${environment}`);
//console.debug(`Deploy context: ${this.deployContext}`);
//console.debug(`Upload API: ${this.uploadApiVideo}`);
console.debug(`Build environment type: ${environment}`);
console.debug(`Deploy context: ${this.deployContext}`);
console.debug(`Upload API: ${this.uploadApiVideo}`);
}
}
@@ -1,97 +0,0 @@
import { MouseEventHandler, useState, useEffect } from "react";
import { Transition } from "@headlessui/react";
import { twMerge } from "tailwind-merge";
import { uiAccess } from "./signals/uiAccess";
import { dispatchUiEvents } from "./signals/uiEvents";
import { ToolbarNode } from "./components/features/ToolbarNode";
import { ToolbarNodeButtonNames } from "./components/features/ToolbarNode/enums";
import { transitionTimingStyles } from "./components/styles";
import { useSignals } from "@preact/signals-react/runtime";
export const ContextualToolbarNode = () => {
const [winSize, setWinSize] = useState<{ width: number; height: number }>({
width: window.innerWidth,
height: window.innerHeight,
});
const { isShowing, position, ...rest } = uiAccess.toolbarNode.signal.value;
const buttonsProps = Object.values(ToolbarNodeButtonNames).reduce(
(acc, buttonName) => {
acc[buttonName] = {
onClick: dispatchUiEvents.toolbarNode[buttonName],
disabled:
uiAccess.toolbarNode.signal.value.buttonStates[buttonName].disabled,
hidden:
uiAccess.toolbarNode.signal.value.buttonStates[buttonName].hidden,
active:
uiAccess.toolbarNode.signal.value.buttonStates[buttonName].active,
};
return acc;
},
{} as {
[key in ToolbarNodeButtonNames]: {
onClick: MouseEventHandler<HTMLButtonElement>;
disabled: boolean;
hidden: boolean;
active: boolean;
};
},
);
useEffect(() => {
function resizeHandler() {
setWinSize({
width: window.innerWidth,
height: window.innerHeight,
});
}
window.addEventListener("resize", resizeHandler);
return () => {
window.removeEventListener("resize", resizeHandler);
};
}, []);
const handleColorChange = (color: string) => {
// Don't do anything if no node is known to be bound
if (uiAccess.toolbarNode.signal.value.knodeIds.length < 1) {
return;
}
// Update the UI component
uiAccess.toolbarNode.setColor(color);
// Signal the engine with the node ID (just grab the first -and expected only- node)
const nodeID = uiAccess.toolbarNode.signal.value.knodeIds[0];
dispatchUiEvents.toolbarNode.setSelectedColor({ kNodeId: nodeID, color });
}
useSignals();
const color = uiAccess.toolbarNode.signal.value.color;
const boundedTopPosition = Math.min(position.y, winSize.height - 164);
return (
<Transition
as="div"
show={isShowing}
className={twMerge(
transitionTimingStyles,
"fixed -translate-x-1/2 translate-y-5 data-[closed]:opacity-0",
)}
style={{
top: boundedTopPosition,
left: position.x,
}}
>
<ToolbarNode
{...rest}
buttonsProps={buttonsProps}
locked={uiAccess.toolbarNode.signal.value.locked}
onLockClicked={(e) => {
dispatchUiEvents.toolbarNode.lock(e);
}}
color={color}
onColorChange={handleColorChange}
/>
</Transition>
);
};
@@ -1,738 +0,0 @@
import Konva from "konva";
import { uiAccess, uiEvents } from "./signals";
import { ImageNode, ShapeNode, ShapeType, TextNode, PaintNode, PreviewCopyNode } from "./KonvaApp/Nodes";
import { UndoStackManager } from "./KonvaApp/UndoRedo";
import { CommandManager, MatteBox, SceneManager } from "./KonvaApp/EngineUtitlities";
import {
NodeIsolator,
NodesManager,
NodesTranslationEventDetails,
NodeTransformationEventDetails,
NodeTransformer,
SelectionManager,
SelectionManagerEvents,
SelectorSquare,
} from "./KonvaApp/NodesManagers";
import { EngineOptions, TextNodeData, VideoNodeData } from "./KonvaApp/types";
import { AppModes, VideoResolutions } from "./KonvaApp/constants";
import { ToolbarMainButtonNames } from "./components/features/ToolbarMain/enum";
import { ToolbarNodeButtonNames } from "./components/features/ToolbarNode/enums";
import { NavigateFunction } from "react-router-dom";
import { RealTimeDrawEngine } from "./KonvaApp/RenderingPrimitives/RealTimeDrawEngine";
import { NodeColor } from "./signals/uiEvents/toolbarNode";
import { invoke } from "@tauri-apps/api/core";
import { DecodeBase64ToImage } from "./utilities/DecodeBase64ToImage";
import { EncodeImageBitmapToBase64 } from "./utilities/EncodeImageBitmapToBase64";
export interface RenderingOptions {
artstyle: string;
positivePrompt: string;
negativePrompt: string;
cinematic: boolean;
enginePreProcessing: boolean;
faceDetail: boolean;
lipSync: boolean;
upscale: boolean;
styleStrength: number;
}
export class Engine {
private navigateRef: NavigateFunction;
private appMode: AppModes = AppModes.INIT;
private boardCanvasRef: HTMLDivElement;
private stage: Konva.Stage;
private bgLayer: Konva.Layer;
private mediaLayer: Konva.Layer;
private nodeIsolationLayer: Konva.Layer;
private uiLayer: Konva.Layer;
private previewLayer: Konva.Layer;
private offScreenCanvas: OffscreenCanvas;
public realTimeDrawEngine: RealTimeDrawEngine;
private nodesManager: NodesManager;
private nodeIsolator: NodeIsolator;
private nodeTransformer: NodeTransformer;
private selectionManager: SelectionManager;
private selectorSquare: SelectorSquare;
private loadingVideosProvider: LoadingVideosProvider;
private matteBox: MatteBox;
private backgroundNode: ShapeNode | null = null;
private sceneManager: SceneManager;
public undoStackManager: UndoStackManager;
public commandManager: CommandManager;
public segmentationButtonCanBePressed: boolean = true;
// signal reference
constructor(boardCanvasRef: HTMLDivElement, options: EngineOptions) {
if (import.meta.env.DEV) {
console.log("Engine Created");
}
this.navigateRef = options.navigate;
this.appMode = AppModes.SELECT;
this.boardCanvasRef = boardCanvasRef;
this.stage = new Konva.Stage({
container: this.boardCanvasRef,
width: window.innerWidth,
height: window.innerHeight,
});
this.bgLayer = new Konva.Layer();
this.mediaLayer = new Konva.Layer();
this.previewLayer = new Konva.Layer();
this.nodeIsolationLayer = new Konva.Layer();
this.uiLayer = new Konva.Layer();
this.stage.add(this.bgLayer);
this.stage.add(this.previewLayer);
this.stage.add(this.mediaLayer);
this.stage.add(this.nodeIsolationLayer);
this.stage.add(this.uiLayer);
// Konva Transformer
this.nodeTransformer = new NodeTransformer();
this.uiLayer.add(this.nodeTransformer.getKonvaNode());
// Loading Placeholders
// Node Isolator
this.nodeIsolator = new NodeIsolator({
mediaLayerRef: this.mediaLayer,
nodeIsolationLayerRef: this.nodeIsolationLayer,
});
// core layer for all the work done.
this.offScreenCanvas = new OffscreenCanvas(0, 0);
// Collection of all Nodes
this.nodesManager = new NodesManager();
// Partial Collection of selected Nodes
this.selectionManager = new SelectionManager({
nodeTransformerRef: this.nodeTransformer,
mediaLayerRef: this.mediaLayer,
});
this.realTimeDrawEngine = new RealTimeDrawEngine({
width: VideoResolutions.SQUARE_1024.width,
height: VideoResolutions.SQUARE_1024.height,
mediaLayerRef: this.mediaLayer,
offScreenCanvas: this.offScreenCanvas,
onDraw: async (canvas, lineBounds) => {
await this.addPaintNode(canvas, lineBounds);
},
onPreviewCopy: async (image) => {
await this.addPreviewCopy(image);
},
});
// Selector Square to select Nodes
this.selectorSquare = new SelectorSquare({
captureCanvasRef: this.realTimeDrawEngine.captureCanvas,
mediaLayerRef: this.mediaLayer,
nodesManagerRef: this.nodesManager,
selectionManagerRef: this.selectionManager,
stageRef: this.stage,
});
this.uiLayer.add(this.selectorSquare.getKonvaNode());
//Collection of commands for undo-redo
this.undoStackManager = new UndoStackManager(() => {
//this.realTimeDrawEngine.render();
});
this.commandManager = new CommandManager({
mediaLayerRef: this.mediaLayer,
nodesManagerRef: this.nodesManager,
nodeTransformerRef: this.nodeTransformer,
selectionManagerRef: this.selectionManager,
renderEngineRef: this.realTimeDrawEngine,
undoStackManagerRef: this.undoStackManager,
});
// set up secene manager
this.sceneManager = new SceneManager({
navigateRef: this.navigateRef,
loadingVideosProviderRef: this.loadingVideosProvider,
mediaLayerRef: this.mediaLayer,
nodesManagerRef: this.nodesManager,
selectionManagerRef: this.selectionManager,
renderEngineRef: this.realTimeDrawEngine,
});
this.matteBox = new MatteBox({
boardCanvasSize: {
width: this.boardCanvasRef.clientWidth,
height: this.boardCanvasRef.clientHeight,
},
captureCanvasSize: this.realTimeDrawEngine.captureCanvas.getSize(),
uiLayerRef: this.uiLayer,
});
// some of the managers has events
// hence, lastly, setup these events
this.setupEventSystem();
this.setAppMode(this.appMode);
}
private setAppMode(newAppMode: AppModes) {
this.appMode = newAppMode;
switch (this.appMode) {
case AppModes.PAINT: {
console.log("APPMODE: PAINT");
this.selectorSquare.disable(); // this breaks the paint if you use it in the wrong spot
this.selectionManager.disable(); // this prevents selection.
this.realTimeDrawEngine.enablePaintMode();
uiAccess.toolbarMain.enable();
uiAccess.toolbarMain.changeButtonState(ToolbarMainButtonNames.SELECT, {
active: false,
});
uiAccess.toolbarMain.changeButtonState(ToolbarMainButtonNames.PAINT, {
active: true,
});
this.matteBox.disable();
return;
}
case AppModes.SELECT: {
console.log("APPMODE: SELECT");
this.realTimeDrawEngine.disablePaintMode();
this.selectorSquare.enable();
this.selectionManager.enable();
uiAccess.toolbarMain.enable();
uiAccess.toolbarMain.changeButtonState(ToolbarMainButtonNames.SELECT, {
active: true,
});
uiAccess.toolbarMain.changeButtonState(ToolbarMainButtonNames.PAINT, {
active: false,
});
this.matteBox.disable();
document.body.style.cursor = "default";
return;
}
case AppModes.PREVIEW: {
console.log("APPMODE: RENDER");
this.selectorSquare.disable();
this.selectionManager.disable();
uiAccess.toolbarMain.disable();
uiAccess.toolbarMain.changeButtonState(ToolbarMainButtonNames.SELECT, {
active: false,
});
this.matteBox.enable();
document.body.style.cursor = "not-allowed";
return;
}
case AppModes.RENDERING: {
console.log("APPMODE: RENDER");
this.selectorSquare.disable();
this.selectionManager.disable();
uiAccess.toolbarMain.disable();
uiAccess.toolbarMain.changeButtonState(ToolbarMainButtonNames.SELECT, {
active: false,
});
this.matteBox.enable(true);
document.body.style.cursor = "wait";
return;
}
case AppModes.INIT:
default: {
console.log("APPMODE: INIT");
this.selectorSquare.disable();
uiAccess.toolbarMain.disable();
this.selectionManager.disable();
uiAccess.toolbarMain.changeButtonState(ToolbarMainButtonNames.SELECT, {
active: false,
});
document.body.style.cursor = "wait";
this.matteBox.disable();
}
}
}
private setupEventSystem() {
// Listen to changes in container size
const resizeObserver = new ResizeObserver(() => {
this.onBoardCanvasResize();
});
resizeObserver.observe(this.boardCanvasRef);
this.onBoardCanvasResize();
// Listen to Nodes
this.selectionManager.eventTarget.addEventListener(
SelectionManagerEvents.NODES_TRANSLATIONS,
((event: CustomEvent<NodesTranslationEventDetails>) => {
//console.log("Event: SelectionManager -> Engine", event);
this.commandManager.translateNodes(event.detail);
}) as EventListener,
);
this.selectionManager.eventTarget.addEventListener(
SelectionManagerEvents.NODES_TRANSFORMATION,
((event: CustomEvent<NodeTransformationEventDetails>) => {
//console.log("Event: SelectionManager -> Engine", event);
this.commandManager.transformNodes(event.detail);
}) as EventListener,
);
// Listen to Tooolbar Node
uiEvents.toolbarNode.lock.onClick(() => {
this.commandManager.toggleLockNodes();
});
uiEvents.toolbarNode.DOWNLOAD.onClick(() => {
const nodes = this.selectionManager.getSelectedNodes();
if (nodes.size > 1) {
uiAccess.dialogError.show({
title: "Error: Download Node Content",
message:
"Please do not select more than 1 item for the Download Node Content feature, you can only download 1 item at a time",
});
return;
}
const node = nodes.values().next().value;
try {
if (node instanceof VideoNode && node.currentUrl) {
downloadURI(node.currentUrl, `Download Video Node-${node.kNode.id}`);
} else {
throw new Error();
}
} catch {
uiAccess.dialogError.show({
title: "Error: Download Node Content",
message: "This item does not have content for download.",
});
}
});
uiEvents.toolbarNode.DELETE.onClick(() =>
this.commandManager.deleteNodes(),
);
uiEvents.toolbarNode.MOVE_LAYER_DOWN.onClick(() =>
this.commandManager.moveNodesDown(),
);
uiEvents.toolbarNode.MOVE_LAYER_UP.onClick(() =>
this.commandManager.moveNodesUp(),
);
uiEvents.toolbarNode.REMOVE_BACKGROUND.onClick(() =>
this.removeBackground(),
);
// Listen to Toolbar Main
uiEvents.toolbarMain.UNDO.onClick(() => {
this.undoStackManager.undo();
});
uiEvents.toolbarMain.REDO.onClick(() => {
this.undoStackManager.redo();
//this.realTimeDrawEngine.render();
});
uiEvents.toolbarMain.SAVE.onClick(async (/*event*/) => {
//await this.realTimeDrawEngine.saveOutput(); // This works.
await invoke("open_login_command");
});
uiEvents.toolbarMain.GENERATE.onClick(async (/*event*/) => {
console.log("generate clicked");
await this.realTimeDrawEngine.generateImage();
});
uiEvents.toolbarMain.SELECT.onClick(() => {
console.log("Toolbar Main >> Select");
this.setAppMode(AppModes.SELECT);
});
uiEvents.toolbarMain.PAINT.onClick(() => {
console.log("Toolbar Main >> Paint");
this.setAppMode(AppModes.PAINT);
});
uiEvents.toolbarMain.ERASER.onClick(() => {
console.log("Toolbar Main >> Eraser");
});
uiEvents.toolbarMain.PREVIEW.onClick(async () => {
console.log("Toolbar Main >> Preview");
this.setAppMode(AppModes.PREVIEW);
await this.handlePreview();
this.setAppMode(AppModes.SELECT);
});
uiEvents.toolbarMain.DOWNLOAD.onClick(async () => {
console.log("Toolbar Main >> Render Download");
try {
this.setAppMode(AppModes.RENDERING);
} catch (error) {
// throw error to retry
uiAccess.dialogError.show({
title: "Generation Error",
message: error?.toString() || "Unknown Error",
});
this.setAppMode(AppModes.SELECT);
}
});
// Listen to other toolbars
// VideoExtraction Toolbar
// Listen to other requests coming from the UI
uiEvents.onGetStagedImage((image) => {
this.addImage(image);
});
uiEvents.onAddTextToEngine((textdata) => {
this.addText(textdata);
});
let renderTimeout: NodeJS.Timeout;
uiEvents.promptEvents.onPromptStrengthChanged(async (strength) => {
this.realTimeDrawEngine.currentStrength = strength / 100.0;
clearTimeout(renderTimeout);
renderTimeout = setTimeout(async () => {
//await this.realTimeDrawEngine.render();
}, 1000);
});
uiEvents.promptEvents.onPromptTextChanged(async (prompt) => {
this.realTimeDrawEngine.currentPrompt = prompt;
clearTimeout(renderTimeout);
renderTimeout = setTimeout(async () => {
//await this.realTimeDrawEngine.render();
}, 1000);
});
uiEvents.onAddShapeToEngine((shapeData) => {
switch (shapeData.shape) {
case "circle":
this.addShape(ShapeType.CIRCLE, 100);
break;
case "square":
this.addShape(ShapeType.SQUARE, 100);
break;
case "triangle":
this.addShape(ShapeType.TRIANGLE, 100);
break;
}
});
uiEvents.toolbarMain.onBgColorChanged((data) => {
this.realTimeDrawEngine.updateBackground(data);
});
uiEvents.toolbarNode.color.onConfirmChanged((nodeColor) => {
console.log("Color change event triggered with:", nodeColor);
if (!nodeColor) {
console.warn("nodeColor is undefined or null");
return;
}
if (!nodeColor.kNodeId) {
console.warn("nodeColor.kNodeId is undefined or null");
return;
}
this.changeNodeColor(nodeColor);
});
uiEvents.toolbarMain.onPaintColorChanged((color) => {
this.realTimeDrawEngine.paintColor = color;
});
uiEvents.toolbarMain.onEraseBrushSizeChanged((size: number) => {
this.realTimeDrawEngine.paintBrushSize = size;
});
}
removeBackground() {
const selectedNodeSet = this.selectionManager.getSelectedNodes();
if (selectedNodeSet.size !== 1) {
return; // Can only remove background from a single image at a time.
}
const [selectedNode] = selectedNodeSet;
const removeBgInvocation = async (
selectedNode: ImageNode,
callback: (
existingImageNode: ImageNode,
createdImage: ImageBitmap,
) => void,
) => {
const image: HTMLImageElement = selectedNode.kNode.image();
const base64Bitmap = await EncodeImageBitmapToBase64(image);
const base64BitmapResponse = await invoke("remove_background", {
image: base64Bitmap,
});
const createdImage = await DecodeBase64ToImage(
base64BitmapResponse as string,
);
callback(selectedNode, createdImage);
};
const _promise = removeBgInvocation(
selectedNode,
(selectedNode: ImageNode, createdImage: ImageBitmap) => {
if (!this.nodesManager.hasNode(selectedNode)) {
return;
}
this.commandManager.deleteSpecificNodes([selectedNode]);
this.addImageFromImageBitmap(createdImage);
},
);
}
disableAllButtons() {
const buttonNames = Object.values(ToolbarNodeButtonNames);
for (const name of buttonNames) {
uiAccess.toolbarNode.changeButtonState(name, { disabled: true });
}
}
async enableAllButtons() {
const buttonNames = Object.values(ToolbarNodeButtonNames);
for (const name of buttonNames) {
await uiAccess.toolbarNode.changeButtonState(name, { disabled: false });
}
}
sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
private onBoardCanvasResize() {
console.log("onBoardCanvasResize Event called");
// Print window dimensions for debugging
console.log("Window dimensions:", {
innerWidth: window.innerWidth,
innerHeight: window.innerHeight,
outerWidth: window.outerWidth,
outerHeight: window.outerHeight,
});
this.realTimeDrawEngine.updateCaptureCanvas(
window.innerWidth,
window.innerHeight,
);
this.matteBox.updateSize({
boardCanvasSize: {
width: this.boardCanvasRef.offsetWidth,
height: this.boardCanvasRef.offsetHeight,
},
});
this.uiLayer.draw();
this.stage.width(this.boardCanvasRef.offsetWidth);
this.stage.height(this.boardCanvasRef.offsetHeight);
this.stage.draw(); // Redraw the canvas
}
// Sandbox is quickly a way to test your idea.
public async sandbox() {}
public onMessage(event: MessageEvent) {
console.log("Message From Shared Worker");
console.log(event);
}
public initializeStage(sceneToken?: string) {
// load canvas that was originaly saved TODO Save manager for resharing.
uiAccess.toolbarNode.hide();
uiAccess.loadingBar.hide();
// load the scene if there's a scenetoken
if (sceneToken) {
this.sceneManager.loadScene(sceneToken);
}
this.setupStage();
// this.populateWithDebugItems();
}
public isInitialized() {
return this.stage !== null;
}
public async setupStage() {
// Frame rate inicator
// const textNode = new Konva.Text({
// x: 10,
// y: 80,
// text: "",
// fontSize: 18,
// fontFamily: "Source Sans 3",
// fill: "white",
// });
// const anim = new Konva.Animation((frame) => {
// if (frame && import.meta.env.DEV) {
// const timeDiff = frame.timeDiff;
// const frameRate = frame.frameRate;
// textNode.setText(
// `FrameTime: ${timeDiff.toFixed(0)} ms\nFrameRate: ${frameRate.toFixed(0)} fps`,
// );
// }
// }, this.mediaLayer);
// anim.start();
//this.uiLayer.add(textNode);
this.addKeyboardShortcuts();
}
private changeNodeColor(nodeColor: NodeColor) {
console.log("Changing node color:", nodeColor);
// Find the node that is selected with the id
const idSelector = "#" + nodeColor.kNodeId;
const selectedNode = this.mediaLayer.findOne(idSelector);
console.log("Selected node:", selectedNode);
console.log("ID selector:", idSelector);
console.log("Media layer:", this.mediaLayer);
// TODO: Change the color of the node
// Turns out shapes are image nodes... how do we change the image's fill??
if (selectedNode) {
// Get the shape node from the real time drawing engine's image nodes
const shapeNode = this.realTimeDrawEngine.findImageNodeById(
nodeColor.kNodeId,
);
if (shapeNode && shapeNode instanceof ShapeNode) {
// Create new shape node with updated color
const newShapeNode = new ShapeNode({
canvasPosition: this.realTimeDrawEngine.captureCanvas.position(),
canvasSize: this.realTimeDrawEngine.captureCanvas.size(),
shapeType: shapeNode.shapeType,
size: shapeNode.kNode.size(),
color: nodeColor.color,
mediaLayerRef: this.mediaLayer,
selectionManagerRef: this.selectionManager,
loaded: async () => {
//this.realTimeDrawEngine.render();
},
});
newShapeNode.kNode.position(shapeNode.kNode.position());
newShapeNode.kNode.zIndex(shapeNode.kNode.zIndex());
// Remove old node and add new one
shapeNode.kNode.destroy();
this.commandManager.createNode(newShapeNode);
}
}
}
public addText(textNodeData: TextNodeData) {
const textNode = new TextNode({
textNodeData: textNodeData,
mediaLayerRef: this.mediaLayer,
selectionManagerRef: this.selectionManager,
canvasPosition: this.realTimeDrawEngine.captureCanvas.position(),
canvasSize: this.realTimeDrawEngine.captureCanvas.size(),
});
this.commandManager.createNode(textNode);
}
public addShape(type: ShapeType, size: number, color?: string) {
const shapeNode = new ShapeNode({
canvasPosition: this.realTimeDrawEngine.captureCanvas.position(),
canvasSize: this.realTimeDrawEngine.captureCanvas.size(),
shapeType: type,
size: { width: size, height: size },
color: color,
mediaLayerRef: this.mediaLayer,
selectionManagerRef: this.selectionManager,
loaded: async () => {
//await this.realTimeDrawEngine.render();
this.setAppMode(AppModes.SELECT);
},
});
this.commandManager.createNode(shapeNode);
}
public addPreviewCopy(image: Image.Konva) {
// Start of Selection
const copyNode = new PreviewCopyNode({
image: image,
mediaLayerRef: this.mediaLayer,
selectionManagerRef: this.selectionManager,
loaded: async () => {
//this.realTimeDrawEngine.render();
this.setAppMode(AppModes.SELECT);
},
});
this.commandManager.createNode(copyNode);
}
public addImage(imageFile: File) {
const imageNode = new ImageNode({
mediaLayerRef: this.mediaLayer,
canvasPosition: this.realTimeDrawEngine.captureCanvas.position(),
canvasSize: this.realTimeDrawEngine.captureCanvas.size(),
imageFile: imageFile,
selectionManagerRef: this.selectionManager,
loaded: async () => {
//await this.realTimeDrawEngine.render();
this.setAppMode(AppModes.SELECT);
},
});
this.commandManager.createNode(imageNode);
}
public addImageFromImageBitmap(imageBitmap: ImageBitmap) {
const imageNode = new ImageNode({
mediaLayerRef: this.mediaLayer,
canvasPosition: this.realTimeDrawEngine.captureCanvas.position(),
canvasSize: this.realTimeDrawEngine.captureCanvas.size(),
imageBitmap: imageBitmap,
selectionManagerRef: this.selectionManager,
loaded: async () => {
//await this.realTimeDrawEngine.render();
this.setAppMode(AppModes.SELECT);
},
});
this.commandManager.createNode(imageNode);
}
public addPaintNode(
canvas: HTMLCanvasElement,
lineBounds: {
width: number;
height: number;
x: number;
y: number;
},
) {
const node = new PaintNode({
canvasElement: canvas,
lineBounds: lineBounds,
mediaLayerRef: this.mediaLayer,
selectionManagerRef: this.selectionManager,
loaded: async () => {
//await this.realTimeDrawEngine.render();
},
});
this.commandManager.createNode(node);
}
// Events for Undo and Redo
private addKeyboardShortcuts() {
window.addEventListener("keydown", (event) => {
if (event.ctrlKey && event.key === "z") {
this.undoStackManager.undo();
} else if (
(event.ctrlKey && event.key === "y") ||
(event.ctrlKey && event.shiftKey && event.key === "Z")
) {
this.undoStackManager.redo();
} else if (event.key === "Delete") {
this.commandManager.deleteNodes();
}
});
}
}
// TODO: Move to utilities?
function downloadURI(uri: string, name: string) {
const link = document.createElement("a");
link.download = name;
link.target = "_blank";
link.href = uri;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
@@ -1,37 +0,0 @@
import React, { createContext, useContext, useState } from "react";
import { JobContextType } from "@storyteller/common";
const JobContext = createContext<JobContextType | undefined>(undefined);
export const JobProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [jobTokens, setJobTokens] = useState<string[]>([]);
const addJobToken = (token: string) => {
setJobTokens((prevTokens) => [...prevTokens, token]);
};
const removeJobToken = (token: string) => {
setJobTokens((prevTokens) => prevTokens.filter((t) => t !== token));
};
const clearJobTokens = () => {
setJobTokens([]);
};
return (
<JobContext.Provider
value={{ jobTokens, addJobToken, removeJobToken, clearJobTokens }}
>
{children}
</JobContext.Provider>
);
};
export const useJobContext = () => {
const context = useContext(JobContext);
if (context === undefined) {
throw new Error("useJobContext must be used within a JobProvider");
}
return context;
};
@@ -1,371 +0,0 @@
import Konva from "konva";
import { uiAccess } from "../signals";
import { CommandManager, MatteBox } from "./EngineUtitlities";
import { ImageNode } from "./Nodes";
import { PaintNode } from "./Nodes/PaintNode";
import {
NodesManager,
NodesTranslationEventDetails,
NodeTransformationEventDetails,
NodeTransformer,
SelectionManager,
SelectionManagerEvents,
SelectorSquare,
} from "./NodesManagers";
import { UndoStackManager } from "./UndoRedo";
import { ToolbarMainButtonNames } from "../components/features/ToolbarMain/enum";
import { EditModes, VideoResolutions } from "./constants";
import { ToolbarNodeButtonNames } from "../components/features/ToolbarNode/enums";
export interface RenderingOptions {
artstyle: string;
positivePrompt: string;
negativePrompt: string;
cinematic: boolean;
enginePreProcessing: boolean;
faceDetail: boolean;
lipSync: boolean;
upscale: boolean;
styleStrength: number;
}
export class EditEngine {
private editMode: EditModes = EditModes.SELECT;
private boardCanvasRef: HTMLDivElement;
private stage: Konva.Stage;
private mediaLayer: Konva.Layer;
private nodeIsolationLayer: Konva.Layer;
private uiLayer: Konva.Layer;
private offScreenCanvas: OffscreenCanvas;
private realTimeDrawEngine: RealTimeDrawEngine;
private nodesManager: NodesManager;
private nodeTransformer: NodeTransformer;
private selectionManager: SelectionManager;
private selectorSquare: SelectorSquare;
private matteBox: MatteBox;
private undoStackManager: UndoStackManager;
private commandManager: CommandManager;
public segmentationButtonCanBePressed: boolean = true;
// signal reference
constructor(boardCanvasRef: HTMLDivElement) {
if (import.meta.env.DEV) {
console.log("Engine Created");
}
this.editMode = EditModes.SELECT;
this.boardCanvasRef = boardCanvasRef;
this.stage = new Konva.Stage({
container: this.boardCanvasRef,
width: window.innerWidth,
height: window.innerHeight,
});
//this.bgLayer = new Konva.Layer();
this.mediaLayer = new Konva.Layer();
this.nodeIsolationLayer = new Konva.Layer();
this.uiLayer = new Konva.Layer();
//this.stage.add(this.bgLayer);
this.stage.add(this.mediaLayer);
this.stage.add(this.nodeIsolationLayer);
this.stage.add(this.uiLayer);
// Konva Transformer
this.nodeTransformer = new NodeTransformer();
this.uiLayer.add(this.nodeTransformer.getKonvaNode());
// core layer for all the work done.
this.offScreenCanvas = new OffscreenCanvas(0, 0);
// Collection of all Nodes
this.nodesManager = new NodesManager();
// Partial Collection of selected Nodes
this.selectionManager = new SelectionManager({
nodeTransformerRef: this.nodeTransformer,
mediaLayerRef: this.mediaLayer,
});
this.realTimeDrawEngine = new RealTimeDrawEngine({
width: VideoResolutions.SQUARE_1024.width,
height: VideoResolutions.SQUARE_1024.height,
mediaLayerRef: this.mediaLayer,
offScreenCanvas: this.offScreenCanvas,
onDraw: async (canvas, lineBounds) => {
await this.addPaintNode(canvas, lineBounds);
},
});
// Selector Square to select Nodes
this.selectorSquare = new SelectorSquare({
captureCanvasRef: this.realTimeDrawEngine.captureCanvas,
mediaLayerRef: this.mediaLayer,
nodesManagerRef: this.nodesManager,
selectionManagerRef: this.selectionManager,
stageRef: this.stage,
});
this.uiLayer.add(this.selectorSquare.getKonvaNode());
//Collection of commands for undo-redo
this.undoStackManager = new UndoStackManager(() => {
this.realTimeDrawEngine.render();
});
this.commandManager = new CommandManager({
mediaLayerRef: this.mediaLayer,
nodesManagerRef: this.nodesManager,
nodeTransformerRef: this.nodeTransformer,
selectionManagerRef: this.selectionManager,
renderEngineRef: this.realTimeDrawEngine,
undoStackManagerRef: this.undoStackManager,
});
this.matteBox = new MatteBox({
boardCanvasSize: {
width: this.boardCanvasRef.clientWidth,
height: this.boardCanvasRef.clientHeight,
},
captureCanvasSize: this.realTimeDrawEngine.captureCanvas.getSize(),
uiLayerRef: this.uiLayer,
});
// some of the managers has events
// hence, lastly, setup these events
this.setupEventSystem();
this.setEditMode(this.editMode);
}
private setEditMode(newEditMode: EditModes) {
this.editMode = newEditMode;
switch (this.editMode) {
case EditModes.SELECT:
default: {
console.log("EDITMODE: SELECT");
this.realTimeDrawEngine.disablePaintMode();
this.selectorSquare.enable();
this.selectionManager.enable();
uiAccess.toolbarMain.enable();
uiAccess.toolbarMain.changeButtonState(ToolbarMainButtonNames.SELECT, {
active: true,
});
uiAccess.toolbarMain.changeButtonState(ToolbarMainButtonNames.PAINT, {
active: false,
});
this.matteBox.disable();
document.body.style.cursor = "default";
return;
}
case EditModes.EDIT: {
console.log("EDITMODE: EDIT");
this.realTimeDrawEngine.enablePaintMode();
this.selectorSquare.disable();
this.selectionManager.disable();
}
}
}
private setupEventSystem() {
// Listen to changes in container size
const resizeObserver = new ResizeObserver(() => {
this.onBoardCanvasResize();
});
resizeObserver.observe(this.boardCanvasRef);
this.onBoardCanvasResize();
// Listen to Nodes
this.selectionManager.eventTarget.addEventListener(
SelectionManagerEvents.NODES_TRANSLATIONS,
((event: CustomEvent<NodesTranslationEventDetails>) => {
//console.log("Event: SelectionManager -> Engine", event);
this.commandManager.translateNodes(event.detail);
}) as EventListener,
);
this.selectionManager.eventTarget.addEventListener(
SelectionManagerEvents.NODES_TRANSFORMATION,
((event: CustomEvent<NodeTransformationEventDetails>) => {
//console.log("Event: SelectionManager -> Engine", event);
this.commandManager.transformNodes(event.detail);
}) as EventListener,
);
// Listen to Toolbar Main
// uiEvents.toolbarMain.UNDO.onClick(() => {
// this.undoStackManager.undo();
// });
// uiEvents.toolbarMain.REDO.onClick(() => {
// this.undoStackManager.redo();
// this.realTimeDrawEngine.render();
// });
// uiEvents.toolbarMain.SAVE.onClick(async (/*event*/) => {
// await this.realTimeDrawEngine.saveOutput();
// });
// uiEvents.toolbarMain.SELECT.onClick(() => {
// console.log("Toolbar Main >> Select");
// this.setAppMode(AppModes.SELECT);
// });
// uiEvents.toolbarMain.PAINT.onClick(() => {
// console.log("Toolbar Main >> Paint");
// this.setAppMode(AppModes.PAINT);
// });
}
disableAllButtons() {
const buttonNames = Object.values(ToolbarNodeButtonNames);
for (const name of buttonNames) {
uiAccess.toolbarNode.changeButtonState(name, { disabled: true });
}
}
async enableAllButtons() {
const buttonNames = Object.values(ToolbarNodeButtonNames);
for (const name of buttonNames) {
await uiAccess.toolbarNode.changeButtonState(name, { disabled: false });
}
}
sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
private onBoardCanvasResize() {
this.realTimeDrawEngine.updateCaptureCanvas(0, 0);
console.log("onBoardCanvasResize Event called");
console.log("Board Canvas values below");
console.log(this.boardCanvasRef.offsetWidth);
console.log(this.boardCanvasRef.offsetHeight);
this.matteBox.updateSize({
boardCanvasSize: {
width: this.boardCanvasRef.offsetWidth,
height: this.boardCanvasRef.offsetHeight,
},
});
this.uiLayer.draw();
this.stage.width(this.boardCanvasRef.offsetWidth);
this.stage.height(this.boardCanvasRef.offsetHeight);
this.stage.draw(); // Redraw the canvas
}
// Sandbox is quickly a way to test your idea.
public async sandbox() {}
public onMessage(event: MessageEvent) {
console.log("Message From Shared Worker");
console.log(event);
}
public initializeStage() {
// load canvas that was originaly saved TODO Save manager for resharing.
uiAccess.toolbarNode.hide();
uiAccess.loadingBar.hide();
this.setupStage();
}
public isInitialized() {
return this.stage !== null;
}
public async setupStage() {
// Frame rate inicator
// const textNode = new Konva.Text({
// x: 10,
// y: 80,
// text: "",
// fontSize: 18,
// fontFamily: "Source Sans 3",
// fill: "white",
// });
// const anim = new Konva.Animation((frame) => {
// if (frame && import.meta.env.DEV) {
// const timeDiff = frame.timeDiff;
// const frameRate = frame.frameRate;
// textNode.setText(
// `FrameTime: ${timeDiff.toFixed(0)} ms\nFrameRate: ${frameRate.toFixed(0)} fps`,
// );
// }
// }, this.mediaLayer);
// anim.start();
// this.uiLayer.add(textNode);
this.addKeyboardShortcuts();
}
public addImage(imageFile: File) {
const imageNode = new ImageNode({
mediaLayerRef: this.mediaLayer,
canvasPosition: this.realTimeDrawEngine.captureCanvas.position(),
canvasSize: this.realTimeDrawEngine.captureCanvas.size(),
imageFile: imageFile,
selectionManagerRef: this.selectionManager,
loaded: async () => {
await this.realTimeDrawEngine.render();
this.setEditMode(EditModes.SELECT);
},
});
this.commandManager.createNode(imageNode);
}
public addImageFromImageBitmap(imageBitmap: ImageBitmap) {
const imageNode = new ImageNode({
mediaLayerRef: this.mediaLayer,
canvasPosition: this.realTimeDrawEngine.captureCanvas.position(),
canvasSize: this.realTimeDrawEngine.captureCanvas.size(),
imageBitmap: imageBitmap,
selectionManagerRef: this.selectionManager,
loaded: async () => {
await this.realTimeDrawEngine.render();
this.setEditMode(EditModes.SELECT);
},
});
this.commandManager.createNode(imageNode);
}
public addPaintNode(
canvas: HTMLCanvasElement,
lineBounds: {
width: number;
height: number;
x: number;
y: number;
},
) {
var node = new PaintNode({
canvasElement: canvas,
lineBounds: lineBounds,
mediaLayerRef: this.mediaLayer,
selectionManagerRef: this.selectionManager,
loaded: async () => {
await this.realTimeDrawEngine.render();
},
});
this.commandManager.createNode(node);
}
// Events for Undo and Redo
private addKeyboardShortcuts() {
window.addEventListener("keydown", (event) => {
if (event.ctrlKey && event.key === "z") {
this.undoStackManager.undo();
} else if (
(event.ctrlKey && event.key === "y") ||
(event.ctrlKey && event.shiftKey && event.key === "Z")
) {
this.undoStackManager.redo();
} else if (event.key === "Delete") {
this.commandManager.deleteNodes();
}
});
}
}
@@ -1,7 +0,0 @@
const ensureBase64Prefix = (base64String: string) => {
return base64String.startsWith("data:")
? base64String
: `data:image/png;base64,${base64String}`;
}
export { ensureBase64Prefix };
@@ -1,157 +0,0 @@
import Konva from "konva";
import { MediaNode, Position, RGBColor, Transformation } from "../types";
import {
CreateCommand,
DeleteCommand,
LockNodesCommand,
MoveLayerDown,
MoveLayerUp,
TransformCommand,
TranslateCommand,
UndoStackManager,
UnlockNodesCommand,
} from "../UndoRedo";
import {
NodesManager,
NodeTransformer,
SelectionManager,
} from "../NodesManagers";
import { RealTimeDrawEngine } from "../RenderingPrimitives/RealTimeDrawEngine";
interface EngineReferences {
mediaLayerRef: Konva.Layer;
nodesManagerRef: NodesManager;
nodeTransformerRef: NodeTransformer;
selectionManagerRef: SelectionManager;
renderEngineRef: RealTimeDrawEngine;
undoStackManagerRef: UndoStackManager;
}
export class CommandManager {
private mediaLayerRef: Konva.Layer;
private nodesManagerRef: NodesManager;
private nodeTransformerRef: NodeTransformer;
private selectionManagerRef: SelectionManager;
private renderEngineRef: RealTimeDrawEngine;
private undoStackManagerRef: UndoStackManager;
constructor(engineRefs: EngineReferences) {
this.mediaLayerRef = engineRefs.mediaLayerRef;
this.nodesManagerRef = engineRefs.nodesManagerRef;
this.nodeTransformerRef = engineRefs.nodeTransformerRef;
this.selectionManagerRef = engineRefs.selectionManagerRef;
this.renderEngineRef = engineRefs.renderEngineRef;
this.undoStackManagerRef = engineRefs.undoStackManagerRef;
}
createNode(node: MediaNode) {
const command = new CreateCommand({
nodes: new Set<MediaNode>([node]),
mediaLayerRef: this.mediaLayerRef,
nodesManagerRef: this.nodesManagerRef,
nodeTransformerRef: this.nodeTransformerRef,
selectionManagerRef: this.selectionManagerRef,
renderEngineRef: this.renderEngineRef,
});
this.undoStackManagerRef.executeCommand(command);
// Set the kNode's manual id to same as kNode's internal _id
// This helps identify the kNode in the Konva layer with find
node.kNode.id(node.kNode._id.toString());
//this.renderEngineRef.render();
}
deleteNodes() {
const nodes = this.selectionManagerRef.getSelectedNodes();
const command = new DeleteCommand({
nodes: nodes,
mediaLayerRef: this.mediaLayerRef,
nodesManagerRef: this.nodesManagerRef,
nodeTransformerRef: this.nodeTransformerRef,
selectionManagerRef: this.selectionManagerRef,
renderEngineRef: this.renderEngineRef,
});
this.undoStackManagerRef.executeCommand(command);
//this.renderEngineRef.render();
}
deleteSpecificNodes(nodes: MediaNode[]) {
const command = new DeleteCommand({
nodes: nodes,
mediaLayerRef: this.mediaLayerRef,
nodesManagerRef: this.nodesManagerRef,
nodeTransformerRef: this.nodeTransformerRef,
selectionManagerRef: this.selectionManagerRef,
renderEngineRef: this.renderEngineRef,
});
this.undoStackManagerRef.executeCommand(command);
//this.renderEngineRef.render();
}
toggleLockNodes() {
const nodes = this.selectionManagerRef.getSelectedNodes();
const node = nodes.values().next().value;
if (!node) {
console.log("Node Not Found for Locking");
return;
}
if (node.isLocked()) {
const command = new UnlockNodesCommand({
nodes: this.selectionManagerRef.getSelectedNodes(),
});
this.undoStackManagerRef.executeCommand(command);
} else {
const command = new LockNodesCommand({
nodes: this.selectionManagerRef.getSelectedNodes(),
});
this.undoStackManagerRef.executeCommand(command);
}
}
moveNodesUp() {
const command = new MoveLayerUp({
nodes: this.selectionManagerRef.getSelectedNodes(),
nodesManagerRef: this.nodesManagerRef,
mediaLayerRef: this.mediaLayerRef,
});
this.undoStackManagerRef.executeCommand(command);
//this.renderEngineRef.render();
}
moveNodesDown() {
const command = new MoveLayerDown({
nodes: this.selectionManagerRef.getSelectedNodes(),
nodesManagerRef: this.nodesManagerRef,
mediaLayerRef: this.mediaLayerRef,
});
this.undoStackManagerRef.executeCommand(command);
//this.renderEngineRef.render();
}
translateNodes(props: {
nodes: Set<MediaNode>;
initialPositions: Map<MediaNode, Position>;
finalPositions: Map<MediaNode, Position>;
}) {
const command = new TranslateCommand({
...props,
layerRef: this.mediaLayerRef,
});
this.undoStackManagerRef.pushCommand(command);
//this.renderEngineRef.render();
}
transformNodes(props: {
nodes: Set<MediaNode>;
initialTransformations: Map<MediaNode, Transformation[]>;
finalTransformations: Map<MediaNode, Transformation[]>;
}) {
const command = new TransformCommand({
...props,
layerRef: this.mediaLayerRef,
});
this.undoStackManagerRef.pushCommand(command);
//this.renderEngineRef.render();
}
}
@@ -1,134 +0,0 @@
import Konva from "konva";
import { Size } from "../types";
import { Colors } from "../constants";
import { uiAccess } from "../../signals/uiAccess";
export class MatteBox {
private matteBoxPoly: Konva.Line;
private lightBoxRect: Konva.Rect;
private uiLayerRef: Konva.Layer;
private boardCanvasSize: Size;
private captureCanvasSize: Size;
constructor({
boardCanvasSize,
captureCanvasSize,
uiLayerRef,
}: {
boardCanvasSize: Size;
captureCanvasSize: Size;
uiLayerRef: Konva.Layer;
}) {
this.uiLayerRef = uiLayerRef;
this.boardCanvasSize = boardCanvasSize;
this.captureCanvasSize = captureCanvasSize;
this.matteBoxPoly = new Konva.Line({
points: getMatteBoxPolygonPoints({
boardCanvasSize: this.boardCanvasSize,
captureCanvasSize: this.captureCanvasSize,
}),
fill: Colors.translucentBlack,
closed: true,
});
this.lightBoxRect = new Konva.Rect({
size: captureCanvasSize,
position: {
x: (boardCanvasSize.width - captureCanvasSize.width) / 2,
y: (boardCanvasSize.height - captureCanvasSize.height) / 2,
},
fill: Colors.transparent,
// fill: "rgba(100,0,0,0.3)",
});
}
public enable(showMagicBox?: boolean) {
this.uiLayerRef.add(this.matteBoxPoly);
this.uiLayerRef.add(this.lightBoxRect);
if (showMagicBox) {
uiAccess.magicBox.show();
}
}
public disable() {
this.matteBoxPoly.remove();
this.lightBoxRect.remove();
if (uiAccess.magicBox.isShowing()) {
uiAccess.magicBox.hide();
}
}
public updateSize({
boardCanvasSize,
captureCanvasSize,
}: {
boardCanvasSize?: Size;
captureCanvasSize?: Size;
}) {
if (boardCanvasSize) {
this.boardCanvasSize = boardCanvasSize;
}
if (captureCanvasSize) {
this.captureCanvasSize = captureCanvasSize;
}
this.matteBoxPoly.points(
getMatteBoxPolygonPoints({
boardCanvasSize: this.boardCanvasSize,
captureCanvasSize: this.captureCanvasSize,
}),
);
this.lightBoxRect.setAttrs({
size: this.captureCanvasSize,
position: {
x: (this.boardCanvasSize.width - this.captureCanvasSize.width) / 2,
y: (this.boardCanvasSize.height - this.captureCanvasSize.height) / 2,
},
});
}
}
function getMatteBoxPolygonPoints({
boardCanvasSize: board,
captureCanvasSize: capture,
}: {
boardCanvasSize: Size;
captureCanvasSize: Size;
}) {
return [
//top left
0,
0,
// top right
board.width,
0,
//bottom right
board.width,
board.height,
//bottom middle
board.width / 2,
board.height,
//canvas bottom middle,
board.width / 2,
(board.height - capture.height) / 2 + capture.height,
//canvas bottom right,
(board.width - capture.width) / 2 + capture.width,
(board.height - capture.height) / 2 + capture.height,
//canvas top right,
(board.width - capture.width) / 2 + capture.width,
(board.height - capture.height) / 2,
//canvas top left,
(board.width - capture.width) / 2,
(board.height - capture.height) / 2,
//canvas bottom left,
(board.width - capture.width) / 2,
(board.height - capture.height) / 2 + capture.height,
//canvas bottom middle,
board.width / 2,
(board.height - capture.height) / 2 + capture.height,
//bottom middle
board.width / 2,
board.height,
//bottom left
0,
board.height,
//back to top left
0,
0,
];
}
@@ -1,230 +0,0 @@
import Konva from "konva";
import { v4 as uuidv4 } from "uuid";
// TODO: USE API STORTELLE
import { MediaFilesApi, MediaUploadApi } from "~/Classes/ApiManager";
import { NodesManager, SelectionManager } from "../NodesManagers";
import {
ImageNodeData,
NodeData,
TextNodeData,
TransformationData,
VideoNodeData,
} from "../types";
import { uiAccess } from "../../signals/uiAccess";
import { NavigateFunction } from "react-router-dom";
import { NodeType } from "../Nodes/constants";
import { ImageNode, TextNode } from "../Nodes";
import { RealTimeDrawEngine as RenderEngine } from "../RenderingPrimitives/RealTimeDrawEngine";
export class SceneManager {
private navigateRef: NavigateFunction;
private mediaLayerRef: Konva.Layer;
private nodesManagerRef: NodesManager;
private selectionManagerRef: SelectionManager;
private renderEngineRef: RenderEngine;
private currentSceneToken: string | undefined;
constructor({
navigateRef,
mediaLayerRef,
nodesManagerRef,
selectionManagerRef,
renderEngineRef,
}: {
navigateRef: NavigateFunction;
mediaLayerRef: Konva.Layer;
nodesManagerRef: NodesManager;
selectionManagerRef: SelectionManager;
renderEngineRef: RenderEngine;
}) {
this.navigateRef = navigateRef;
this.mediaLayerRef = mediaLayerRef;
this.nodesManagerRef = nodesManagerRef;
this.selectionManagerRef = selectionManagerRef;
this.renderEngineRef = renderEngineRef;
}
public async saveScene() {
const mediaUploadApi = new MediaUploadApi();
const timestamp = new Date();
const sceneTitle =
"board_" + timestamp.toISOString().substring(0, 19).replace(/\D/g, "");
const saveJson = this.extractAllNodesData();
console.log("saveJson", saveJson);
const file = new File([saveJson], `${sceneTitle}`, {
type: "application/json",
});
if (!this.currentSceneToken) {
const uploadResponse = await mediaUploadApi.UploadNewScene({
blob: file,
fileName: file.name,
uuid: uuidv4(),
});
console.log(uploadResponse);
if (!uploadResponse.success || !uploadResponse.data) {
uiAccess.dialogError.show({
title: "Save New Scene Error",
message: uploadResponse.errorMessage,
});
return;
}
this.currentSceneToken = uploadResponse.data;
this.navigateRef(`/${this.currentSceneToken}`);
} else {
const uploadResponse = await mediaUploadApi.UploadSavedScene({
blob: file,
fileName: file.name,
uuid: uuidv4(),
mediaToken: this.currentSceneToken,
});
console.log(uploadResponse);
if (!uploadResponse.success || !uploadResponse.data) {
uiAccess.dialogError.show({
title: "Save Scene Error",
message: uploadResponse.errorMessage,
});
return;
}
}
}
public async loadScene(mediaFileToken: string) {
const mediaFileApi = new MediaFilesApi();
const getSceneResponse = await mediaFileApi.GetMediaFileByToken({
mediaFileToken: mediaFileToken,
});
if (!getSceneResponse.success || !getSceneResponse.data) {
uiAccess.dialogError.show({
title: "Load Scene Error",
message: getSceneResponse.errorMessage,
});
return;
}
this.currentSceneToken = mediaFileToken;
// console.log(getSceneResponse.data);
// console.log(getSceneResponse.data.public_bucket_path);
const fileResponse = await fetch(getSceneResponse.data.public_bucket_url);
if (!fileResponse.ok) {
uiAccess.dialogError.show({
title: "Load Scene Error",
message: "Failed to download Scene File",
});
return;
}
const fileBlob = await fileResponse.blob();
const loadJson: NodeData[] = await new Promise((resolve, reject) => {
const reader = new FileReader();
const readerError = () => {
uiAccess.dialogError.show({
title: "Load Scene Error",
message: "Failed to read Scene File",
});
this.navigateRef("/");
console.log("debug file Response", fileResponse);
console.log("debug file Blob", fileBlob);
reject();
};
reader.onloadend = () => {
try {
resolve(JSON.parse(reader.result as string));
} catch {
readerError();
}
};
reader.onerror = () => {
readerError();
};
reader.readAsText(fileBlob);
});
this.rebuildScene(loadJson);
}
private extractAllNodesData() {
const nodesData: NodeData[] = [];
this.nodesManagerRef.getAllNodes().forEach((node) => {
const nodeData = node.getNodeData(
this.renderEngineRef.captureCanvas.position(),
);
if (nodeData !== null) {
nodesData.push(nodeData);
}
});
nodesData.sort((a, b) => a.transform.zIndex - b.transform.zIndex);
return JSON.stringify(nodesData);
}
private rebuildScene(nodesData: NodeData[]) {
// console.log(nodesData);
nodesData.forEach((nodeDatum) => {
if (nodeDatum.type === NodeType.IMAGE && nodeDatum.imageNodeData) {
this.addImage(nodeDatum.imageNodeData, nodeDatum.transform);
} else if (nodeDatum.type === NodeType.VIDEO && nodeDatum.videoNodeData) {
this.addVideo(nodeDatum.videoNodeData, nodeDatum.transform);
} else if (nodeDatum.type === NodeType.TEXT && nodeDatum.textNodeData) {
this.addText({
textNodeData: nodeDatum.textNodeData,
transform: nodeDatum.transform,
});
}
});
}
private addImage(
imageNodeData: ImageNodeData,
transform: TransformationData,
) {
const imageNode = new ImageNode({
mediaFileUrl: imageNodeData.mediaFileUrl,
mediaLayerRef: this.mediaLayerRef,
canvasPosition: this.renderEngineRef.captureCanvas.position(),
canvasSize: this.renderEngineRef.captureCanvas.size(),
transform: transform,
selectionManagerRef: this.selectionManagerRef,
});
this.nodesManagerRef.saveNode(imageNode);
this.renderEngineRef.addNodes(imageNode);
}
private addVideo(
videoNodeData: VideoNodeData,
transform: TransformationData,
) {
const videoNode = new VideoNode({
loadingVideosProviderRef: this.loadingVideosProviderRef,
mediaLayerRef: this.mediaLayerRef,
selectionManagerRef: this.selectionManagerRef,
canvasPosition: this.renderEngineRef.captureCanvas.position(),
canvasSize: this.renderEngineRef.captureCanvas.size(),
videoNodeData: videoNodeData,
transform: transform,
});
this.nodesManagerRef.saveNode(videoNode);
this.renderEngineRef.addNodes(videoNode);
}
private addText({
textNodeData,
transform,
}: {
textNodeData: TextNodeData;
transform: TransformationData;
}) {
const textNode = new TextNode({
textNodeData: textNodeData,
transform: transform,
mediaLayerRef: this.mediaLayerRef,
canvasPosition: this.renderEngineRef.captureCanvas.position(),
canvasSize: this.renderEngineRef.captureCanvas.size(),
selectionManagerRef: this.selectionManagerRef,
});
this.nodesManagerRef.saveNode(textNode);
this.renderEngineRef.addNodes(textNode);
}
}
@@ -1,3 +0,0 @@
export { CommandManager } from "./CommandManager";
export { MatteBox } from "./MatteBox";
export { SceneManager } from "./SceneManager";
@@ -1,46 +0,0 @@
import { invoke } from "@tauri-apps/api/core";
import {
cancelGeneration,
finishGeneration,
onStartGeneration,
} from "~/signals/generation/generationSignals";
export class GenerationEngine {
private isGenerating = false;
constructor() {
console.log("GenerationEngine constructor");
this.attachEventListeners();
}
private attachEventListeners() {
onStartGeneration((prompt) => {
this.invokeGeneration(prompt);
});
}
private async invokeGeneration(prompt: string) {
if (this.isGenerating) {
return;
}
this.isGenerating = true;
console.log("Invoking generation for prompt:", prompt);
try {
const base64BitmapResponse = await invoke("text_to_image", {
prompt: prompt,
});
console.log("Generation finished with response:", base64BitmapResponse);
const base64string = base64BitmapResponse as string;
finishGeneration(base64string);
} catch (error) {
console.error("Error during image processing:", error);
cancelGeneration();
} finally {
this.isGenerating = false;
}
}
}
@@ -1,253 +0,0 @@
import Konva from "konva";
import { SelectionManager } from "../NodesManagers";
import { highlightStrokeWidth, primaryOrange } from "./constants";
import { NodeData, Position } from "../types";
import { RealTimeDrawEngine } from "../RenderingPrimitives/RealTimeDrawEngine";
import { MouseEventHandler } from "react";
import { KonvaEventObject } from "konva/lib/Node";
export type SupportedNode = Konva.Image | Konva.Group | Konva.Shape;
export abstract class BaseNode {
protected selectionManagerRef: SelectionManager;
public kNode: SupportedNode;
protected mediaLayerRef: Konva.Layer;
// Internal State members
// do not modify internal
protected isSelecting: boolean = false;
// This locks interaction when the render engine is rendering
protected isProcessing: boolean = false;
protected _isLocked: boolean = false;
protected _areBaseEventsListening: boolean = false;
protected _isKDragEventListening: boolean = false;
protected _isKTranformEventListening: boolean = false;
abstract getNodeData(captureCanvasPosition: Position): NodeData | null;
constructor({
selectionManagerRef,
mediaLayerRef,
kNode,
}: {
selectionManagerRef: SelectionManager;
mediaLayerRef: Konva.Layer;
kNode: SupportedNode;
}) {
// console.log("Node constructed");
// this.uuid = uuidv4();
this.mediaLayerRef = mediaLayerRef;
this.selectionManagerRef = selectionManagerRef;
this.kNode = kNode;
}
protected delete() {
// Do any other clean up and delete the konva node.
this.kNode.destroy();
}
public async setProcessing(val: boolean) {
this.isProcessing = val;
}
public isKEventRef() {
return this._isKDragEventListening && this._isKTranformEventListening;
}
public setIsKEventRef(flag: boolean) {
this._isKDragEventListening = flag;
this._isKTranformEventListening = flag;
this.removeListenToBaseKNodeTransformations();
this.removeListenToBaseKNodeDrags();
if (flag) {
this.listenToBaseKNodeTransformations();
this.listenToBaseKNodeDrags();
}
}
public highlight() {
// console.log("Highlight", this.kNode._id);
if (this.kNode instanceof Konva.Image || this.kNode instanceof Konva.Shape) {
this.kNode.stroke(primaryOrange);
this.kNode.strokeWidth(highlightStrokeWidth);
return;
}
if (this.kNode instanceof Konva.Group) {
const wrapperRect = this.kNode.findOne(".wrapper") as Konva.Rect;
console.log("Wrapper rect:", wrapperRect);
if (wrapperRect != null) {
wrapperRect.stroke(primaryOrange);
wrapperRect.strokeWidth(highlightStrokeWidth);
}
return;
}
if (import.meta.env.DEV) {
console.warn(
"DEV: BaseNode Highlight Error, kNode of BaseNode should be override by MediaNodes that extends kNode",
);
}
}
public unhighlight() {
// console.log("unHighlight", this.kNode._id);
if (this.kNode instanceof Konva.Image || this.kNode instanceof Konva.Shape) {
this.kNode.strokeWidth(0);
return;
}
if (this.kNode instanceof Konva.Group) {
const wrapperRect = this.kNode.findOne(".wrapper") as Konva.Rect;
console.log("Wrapper rect:", wrapperRect);
if (wrapperRect != null) {
// Issue investigate
wrapperRect.strokeWidth(0);
}
return;
}
if (import.meta.env.DEV) {
console.warn(
"DEV: BaseNode UnHighlight Error, kNode of BaseNode should be override by MediaNodes that extends kNode",
);
}
}
public moveLayerUp() {
this.kNode.moveUp();
}
public moveLayerDown() {
this.kNode.moveDown();
}
public isLocked() {
return this._isLocked;
}
public lock() {
this._isLocked = true;
this.kNode.setDraggable(false);
this.selectionManagerRef.updateNodeTransformer();
if (this.isKEventRef()) {
this.selectionManagerRef.updateContextComponents();
}
}
public unlock() {
this._isLocked = false;
this.kNode.setDraggable(true);
this.selectionManagerRef.updateNodeTransformer();
if (this.isKEventRef()) {
this.selectionManagerRef.updateContextComponents();
}
}
public listenToBaseKNode() {
if (this._areBaseEventsListening) {
return;
}
this._areBaseEventsListening = true;
const handleSelect = (isMultiSelect: boolean) => {
// console.log("handle select");
if (!isMultiSelect) {
// clear selection if not multislect
console.log("No Shift >> no multiselect");
this.selectionManagerRef.clearSelection();
}
if (this.selectionManagerRef.isNodeSelected(this)) {
this.selectionManagerRef.deselectNode(this);
return;
}
if (this.isSelecting) {
this.selectionManagerRef.selectNode(this);
return;
}
};
const handleRightClick = (e: KonvaEventObject<MouseEvent>) => {
this.selectionManagerRef.clearSelection();
this.selectionManagerRef.selectNode(this, undefined, {
show: true,
at: { x: e.evt.x, y: e.evt.y }
});
}
this.kNode.on("contextmenu", (e) => {
console.log("CONTEXT MENU", e);
e.evt.preventDefault();
handleRightClick(e);
});
this.kNode.on("mousedown", (e) => {
// https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/which
// 3: Right mouse button
// If right click, let context event handle it
if (e.evt.button === 2) {
return;
}
// Selection of Node
if (!this.selectionManagerRef.isNodeSelected(this)) {
this.isSelecting = true;
// send shift key to check for multiselect
handleSelect(e.evt.shiftKey);
}
});
this.kNode.on("mouseup", (e) => {
// https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/which
// 3: Right mouse button
// If right click, let context event handle it
if (e.evt.button === 2) {
return;
}
// just coming out of dragging or selecting mode
// no need to handle select
if (this.selectionManagerRef.isDragging() || this.isSelecting) {
this.isSelecting = false;
return;
}
// checking for multiselect, in multiselect deselection is possible
handleSelect(e.evt.shiftKey);
});
}
public removeListenToBaseKNode() {
this._areBaseEventsListening = false;
this.kNode.removeEventListener("mousedown mouseup");
}
private handleDragStart = () => {
console.log("Drag start", this.kNode._id);
if (this.isProcessing) {
return;
}
this.selectionManagerRef.dragStart();
};
private handleDragEnd = () => {
this.selectionManagerRef.dragEnd();
};
public listenToBaseKNodeDrags() {
this.kNode.on("dragstart", this.handleDragStart);
this.kNode.on("dragend", this.handleDragEnd);
}
public removeListenToBaseKNodeDrags() {
this.kNode.off("dragstart", this.handleDragStart);
this.kNode.off("dragend", this.handleDragEnd);
}
public listenToBaseKNodeTransformations() {
this.kNode.on("transformstart", (event) => {
console.log("transformstart", event.target._id);
this.selectionManagerRef.transformStart();
});
this.kNode.on("transform", () => {
this.kNode.setAttrs({
width: this.kNode.width() * this.kNode.scaleX(),
height: this.kNode.height() * this.kNode.scaleY(),
scaleX: 1,
scaleY: 1,
});
});
this.kNode.on("transformend", (event) => {
console.log("transformend", event.target._id);
this.selectionManagerRef.transformEnd();
});
}
public removeListenToBaseKNodeTransformations() {
this.kNode.removeEventListener("transformend");
}
}
@@ -1,261 +0,0 @@
import Konva from "konva";
import { MediaFilesApi, MediaUploadApi } from "~/Classes/ApiManager";
import { SelectionManager } from "../NodesManagers";
import { Position, Size, NodeData, TransformationData } from "../types";
import { NetworkedNode, UploadStatus } from "./NetworkedNode";
import { NodeType, transparent } from "./constants";
import { NodeUtilities } from "./NodeUtilities";
interface ImageNodeContructor {
canvasPosition: Position;
canvasSize: Size;
imageFile?: File;
imageBitmap?: ImageBitmap;
mediaFileToken?: string;
mediaFileUrl?: string;
transform?: TransformationData;
mediaLayerRef: Konva.Layer;
selectionManagerRef: SelectionManager;
loaded: () => Promise<void>;
}
export class ImageNode extends NetworkedNode {
public kNode: Konva.Image;
public imageSize?: Size;
private loaded!: () => Promise<void>;
constructor({
canvasPosition,
canvasSize,
imageFile,
mediaFileToken,
mediaFileUrl,
transform: existingTransform,
mediaLayerRef,
selectionManagerRef,
loaded,
imageBitmap,
}: ImageNodeContructor) {
// kNodes need to be created first to guaruntee
// that it is not undefined in parent's context
const transform = NodeUtilities.getInitialTransform({
existingTransform,
canvasPosition,
canvasSize,
});
const kNode = new Konva.Image({
image: undefined, // to do replace with placeholder
...transform,
draggable: true,
strokeScaleEnabled: false,
});
super({
selectionManagerRef: selectionManagerRef,
mediaLayerRef: mediaLayerRef,
kNode: kNode,
localFile: imageFile,
});
this.kNode = kNode;
this.loaded = loaded;
// this.imageSize = minNodeSize;
this.mediaLayerRef.add(this.kNode);
if (imageFile) {
this.loadImageFromFile({
imageFile: imageFile,
maxSize: canvasSize,
refPosition: canvasPosition,
});
return;
}
if (mediaFileUrl && transform) {
this.mediaFileToken = mediaFileToken;
this.mediaFileUrl = mediaFileUrl;
this.loadImageFromUrl(mediaFileUrl);
return;
}
if (imageBitmap) {
this.loadImageFromImageBitmap({
imageBitmap: imageBitmap,
maxSize: canvasSize,
refPosition: canvasPosition,
});
return;
}
console.log("image node creation is fucked");
}
private loadImageFromFile({
imageFile,
maxSize,
refPosition,
}: {
imageFile: File;
maxSize: Size;
refPosition: Position;
}) {
const imageComponent = new Image();
imageComponent.crossOrigin = "anonymous";
// this.setProgress({ progress: 100, status: UploadStatus.SUCCESS });
imageComponent.onload = () => {
//this.setProgress({ progress: 0, status: UploadStatus.FILE_STAGED });
this.imageSize = {
width: imageComponent.width,
height: imageComponent.height,
};
const adjustedSize = NodeUtilities.adjustNodeSizeToCanvas({
componentSize: this.imageSize,
maxSize: maxSize,
});
const centerPosition = NodeUtilities.positionNodeOnCanvasCenter({
canvasOffset: refPosition,
componentSize: adjustedSize,
maxSize: maxSize,
});
this.kNode.image(imageComponent);
this.kNode.setSize(adjustedSize);
this.kNode.setPosition(centerPosition);
this.kNode.moveToTop();
this.kNode.fill(transparent);
this.listenToBaseKNode();
this.mediaLayerRef.draw();
//this.uploadImage(imageFile);
this.loaded();
};
imageComponent.onerror = () => {
//this.setProgress({ progress: 0, status: UploadStatus.ERROR_ON_FILE });
};
imageComponent.src = URL.createObjectURL(imageFile);
}
private async loadImageFromUrl(mediaFileUrl: string) {
//this.setProgress({ progress: 75, status: UploadStatus.LOADING });
const newImage = new Image();
newImage.crossOrigin = "anonymous";
newImage.onerror = () => {
//this.setProgress({ progress: 90, status: UploadStatus.ERROR_ON_LOAD });
};
newImage.onload = () => {
// console.log("network image", newImage);
this.kNode.image(newImage);
this.kNode.draw();
//this.setProgress({ progress: 100, status: UploadStatus.SUCCESS });
};
newImage.src = mediaFileUrl;
this.listenToBaseKNode();
}
private loadImageFromImageBitmap({
imageBitmap,
maxSize,
refPosition,
}: {
imageBitmap: ImageBitmap;
maxSize: Size;
refPosition: Position;
}) {
this.imageSize = {
width: imageBitmap.width,
height: imageBitmap.height,
};
const adjustedSize = NodeUtilities.adjustNodeSizeToCanvas({
componentSize: this.imageSize,
maxSize: maxSize, // TODO
});
const centerPosition = NodeUtilities.positionNodeOnCanvasCenter({
canvasOffset: refPosition, // TODO
componentSize: adjustedSize, // TODO
maxSize: maxSize, // TODO
});
this.kNode.image(imageBitmap);
this.kNode.setSize(adjustedSize);
this.kNode.setPosition(centerPosition);
this.kNode.moveToTop();
this.kNode.fill(transparent);
this.listenToBaseKNode();
this.mediaLayerRef.draw();
//this.uploadImage(imageFile);
this.loaded();
this.listenToBaseKNode();
}
private async uploadImage(imageFile: File) {
//this.setProgress({ progress: 10, status: UploadStatus.UPLOADING });
const mediaUploadApi = new MediaUploadApi();
const uploadResponse = await mediaUploadApi.UploadImage({
blob: imageFile,
uuid: this.uuidGenerate(),
fileName: imageFile.name,
});
// console.log(uploadResponse);
if (!uploadResponse.success || !uploadResponse.data) {
this.setStatus(UploadStatus.ERROR_ON_UPLOAD, uploadResponse.errorMessage);
return;
}
this.mediaFileToken = uploadResponse.data;
this.retreiveImage(this.mediaFileToken);
}
private async retreiveImage(mediaFileToken: string) {
//this.setProgress({ progress: 50, status: UploadStatus.RETREIVING });
const mediaFileApi = new MediaFilesApi();
const mediaFileResponse = await mediaFileApi.GetMediaFileByToken({
mediaFileToken: mediaFileToken,
});
// console.log(mediaFileResponse);
if (!mediaFileResponse.success || !mediaFileResponse.data) {
this.setStatus(
UploadStatus.ERROR_ON_RETREIVE,
mediaFileResponse.errorMessage,
);
return;
}
this.mediaFileUrl = mediaFileResponse.data.media_links.cdn_url;
this.loadImageFromUrl(this.mediaFileUrl);
}
public async retry() {
if (this.mediaFileUrl) {
this.loadImageFromUrl(this.mediaFileUrl);
return;
}
if (this.mediaFileToken) {
this.retreiveImage(this.mediaFileToken);
return;
}
if (this.localFile) {
this.uploadImage(this.localFile);
return;
}
console.warn("Image Node has no data to recontruct itself!");
}
public getNodeData(canvasPostion: Position) {
if (!this.mediaFileUrl) {
console.error("Image Node can not be saved");
return null;
}
const data: NodeData = {
type: NodeType.IMAGE,
transform: {
position: {
x: this.kNode.position().x - canvasPostion.x,
y: this.kNode.position().y - canvasPostion.y,
},
size: this.kNode.size(),
rotation: this.kNode.rotation(),
scale: {
x: this.kNode.scaleX(),
y: this.kNode.scaleY(),
},
zIndex: this.kNode.getZIndex(),
},
imageNodeData: {
mediaFileUrl: this.mediaFileUrl,
mediaFileToken: this.mediaFileToken,
},
};
return data;
}
}
@@ -1,134 +0,0 @@
import Konva from "konva";
import { v4 as uuidv4 } from "uuid";
import { SelectionManager } from "../NodesManagers";
import { BaseNode, SupportedNode } from "./BaseNode";
import { Size } from "../types";
import { LoadingVideosProvider } from "../EngineUtitlities/LoadingVideosProvider";
import { NodeProgressEventDetail } from "../types/events";
export enum UploadStatus {
INIT = "init",
FILE_STAGED = "file_staged",
ERROR_ON_FILE = "error_on_file",
UPLOADING = "uploading",
ERROR_ON_UPLOAD = "error_on_upload",
RETREIVING = "retreiving",
ERROR_ON_RETREIVE = "error_on_retreive",
LOADING = "loading",
ERROR_ON_LOAD = "error_on_load",
SUCCESS = "success",
}
export abstract class NetworkedNode extends BaseNode {
public kNode: SupportedNode;
// members to deal with loading progress
public didFinishLoading: boolean = false;
protected _progress: number = 0;
protected _progressMessage?: string;
protected progressStatus: string = UploadStatus.INIT;
public progressEvent: EventTarget;
// members to deal with the file carried
protected localFile?: File;
protected mediaFileToken?: string;
protected mediaFileUrl?: string;
public mediaFileSize?: Size;
// loading screen
protected loadingVideosProviderRef?: LoadingVideosProvider;
// error and retry handling
public errorMessage?: string;
abstract retry(): void;
constructor({
kNode,
selectionManagerRef,
mediaLayerRef,
localFile,
loadingVideosProviderRef,
}: {
kNode: SupportedNode;
selectionManagerRef: SelectionManager;
mediaLayerRef: Konva.Layer;
loadingVideosProviderRef?: LoadingVideosProvider;
localFile?: File;
}) {
super({
kNode,
selectionManagerRef,
mediaLayerRef,
});
this.kNode = kNode;
this.loadingVideosProviderRef = loadingVideosProviderRef;
this.didFinishLoading = false;
this.progressEvent = new EventTarget();
this.localFile = localFile;
}
public progress() {
return this._progress;
}
public progressMessage() {
return this._progressMessage;
}
public status() {
return this.progressStatus;
}
public isError() {
const errorStatues: string[] = [
UploadStatus.ERROR_ON_FILE,
UploadStatus.ERROR_ON_UPLOAD,
UploadStatus.ERROR_ON_RETREIVE,
UploadStatus.ERROR_ON_LOAD,
];
return errorStatues.includes(this.progressStatus);
}
protected uuidGenerate() {
return uuidv4();
}
protected setStatus(newStatus: string, message?: string) {
this.progressStatus = newStatus;
this.errorMessage = message;
this.selectionManagerRef.updateContextComponents();
}
protected setProgress<S>({
progress,
name,
status,
message,
}: {
progress: number;
name?: string;
status?: S;
message?: string;
}) {
this._progress = progress;
if (message) {
this._progressMessage = message;
}
if (status && typeof status === "string") {
this.progressStatus = status;
}
if (this._progress === 100) {
this.didFinishLoading = true;
}
// TODO: change to use
this.progressEvent.dispatchEvent(
new CustomEvent<NodeProgressEventDetail<S>>(
name ? name : "GenericNodeProgress",
{
detail: {
node: this,
progress: this._progress,
message: message,
status: status,
},
},
),
);
this.selectionManagerRef.updateContextComponents();
}
}
@@ -1,158 +0,0 @@
import Konva from "konva";
import { Position, Size, TransformationData } from "../types";
import { minNodeSize, transparent } from "./constants";
export const NodeUtilities = {
adjustNodeSizeToCanvas,
downloadOffscreenCanvas,
getInitialTransform,
isAssetUrlAvailable,
positionNodeOnCanvasCenter,
printKNodeAttrs,
urlToBlob,
};
function downloadOffscreenCanvas(canvas: OffscreenCanvas) {
canvas.convertToBlob().then((blob) => {
if (blob) {
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "offscreen-canvas-image.png"; // Specify the file name
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url); // Clean up
}
});
}
function getInitialTransform({
existingTransform,
mediaFileSize,
canvasPosition,
canvasSize,
}: {
existingTransform?: TransformationData;
mediaFileSize?: Size;
canvasPosition: Position;
canvasSize: Size;
}) {
return existingTransform
? {
...existingTransform,
position: {
x: existingTransform.position.x + canvasPosition.x,
y: existingTransform.position.y + canvasPosition.y,
},
fill: transparent,
}
: {
position: positionNodeOnCanvasCenter({
canvasOffset: canvasPosition,
componentSize: mediaFileSize ?? minNodeSize,
maxSize: canvasSize,
}),
size: mediaFileSize ?? minNodeSize,
fill: "gray",
};
}
function adjustNodeSizeToCanvas({
componentSize,
maxSize,
}: {
componentSize: Size;
maxSize: Size;
}) {
const adjustedSize = {
width: componentSize.width,
height: componentSize.height,
};
if (adjustedSize.width > maxSize.width) {
const scaleDown = maxSize.width / adjustedSize.width;
adjustedSize.width = adjustedSize.width * scaleDown;
adjustedSize.height = adjustedSize.height * scaleDown;
}
if (adjustedSize.height > maxSize.height) {
const scaleDownAgain = maxSize.height / adjustedSize.height;
adjustedSize.width = adjustedSize.width * scaleDownAgain;
adjustedSize.height = adjustedSize.height * scaleDownAgain;
}
return adjustedSize;
}
function positionNodeOnCanvasCenter({
canvasOffset,
componentSize,
maxSize,
}: {
canvasOffset: Position;
componentSize: Size;
maxSize: Size;
}) {
return {
x: canvasOffset.x + maxSize.width / 2 - componentSize.width / 2,
y: canvasOffset.y + maxSize.height / 2 - componentSize.height / 2,
};
}
function printKNodeAttrs(kNode: Konva.Node) {
console.log({
position: kNode.position(),
size: kNode.size(),
scale: kNode.scale(),
rotation: kNode.rotation(),
});
}
async function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// Function to check if the URL returns a 200 status
async function checkUrl(url: string): Promise<boolean> {
try {
const response = await fetch(url);
return response.status === 200;
} catch (error) {
console.error("Error fetching URL:", error);
return false;
}
}
// wait using a while loop to display the result.
async function isAssetUrlAvailable({
url,
sleepDurationMs = 500,
totalRetries,
}: {
url: string;
sleepDurationMs?: number;
totalRetries?: number;
}): Promise<boolean> {
let retryCount = 0;
let isAvailable = false;
const totalRetriesReached = () => {
if (!totalRetries) {
return false;
}
return retryCount >= totalRetries;
};
while (!isAvailable && !totalRetriesReached()) {
isAvailable = await checkUrl(url);
if (!isAvailable) {
console.log("Asset at Url not available yet, retrying...");
retryCount = retryCount + 1;
await sleep(sleepDurationMs);
} else if (import.meta.env.DEV) {
console.log("Preview Image is available:", url);
}
}
return isAvailable;
}
async function urlToBlob(url: string): Promise<Blob> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch resource: ${response.statusText}`);
}
const blob = await response.blob();
return blob;
}
@@ -1,91 +0,0 @@
import Konva from "konva";
import { SelectionManager } from "../NodesManagers";
import { Position, NodeData } from "../types";
import { NodeType } from "./constants";
import { NetworkedNode } from "./NetworkedNode";
interface PaintNodeConstructor {
canvasElement: HTMLCanvasElement;
lineBounds: {
width: number;
height: number;
x: number;
y: number;
};
mediaLayerRef: Konva.Layer;
selectionManagerRef: SelectionManager;
loaded: () => Promise<void>;
}
export class PaintNode extends NetworkedNode {
retry(): void {
throw new Error("Method not implemented.");
}
constructor({
canvasElement,
lineBounds,
mediaLayerRef,
selectionManagerRef,
loaded,
}: PaintNodeConstructor) {
const cl = canvasElement;
// Convert circle to image
const dataURL = cl.toDataURL();
const imageObj = new Image();
imageObj.src = dataURL;
const paintShape = new Konva.Image({
x: lineBounds.x,
y: lineBounds.y,
width: lineBounds.width,
height: lineBounds.height,
image: imageObj,
listening: false, // Disable event listening to allow captureCanvas to receive events
draggable: true,
// stroke: '#000000',
// strokeWidth: 2,
// dash: [5, 5], // Creates dotted outline THIS IS FOR DEBUG
globalCompositeOperation: "source-over",
fill: "transparent",
});
imageObj.onload = () => {
this.mediaLayerRef.draw();
loaded();
};
super({
selectionManagerRef: selectionManagerRef,
mediaLayerRef: mediaLayerRef,
kNode: paintShape,
});
this.kNode.moveToTop();
this.mediaLayerRef.add(this.kNode);
this.listenToBaseKNode();
this.mediaLayerRef.draw();
}
public getNodeData(canvasPosition: Position) {
const data: NodeData = {
type: NodeType.SHAPE,
transform: {
position: {
x: this.kNode.position().x - canvasPosition.x,
y: this.kNode.position().y - canvasPosition.y,
},
size: this.kNode.size(),
rotation: this.kNode.rotation(),
scale: {
x: this.kNode.scaleX(),
y: this.kNode.scaleY(),
},
zIndex: this.kNode.getZIndex(),
},
};
return data;
}
}
@@ -1,58 +0,0 @@
import Konva from "konva";
import { SelectionManager } from "../NodesManagers";
import { Position, Size, NodeData, TransformationData } from "../types";
import { NodeType } from "./constants";
import { NodeUtilities } from "./NodeUtilities";
import { NetworkedNode } from "./NetworkedNode";
interface PreviewCopyConstructor {
image: Konva.Image;
mediaLayerRef: Konva.Layer;
selectionManagerRef: SelectionManager;
loaded: () => Promise<void>;
}
export class PreviewCopyNode extends NetworkedNode {
retry(): void {
throw new Error("Method not implemented.");
}
constructor({
image,
mediaLayerRef,
selectionManagerRef,
loaded,
}: PreviewCopyConstructor) {
// Create the actual shape inside the group
super({
selectionManagerRef: selectionManagerRef,
mediaLayerRef: mediaLayerRef,
kNode: image,
});
this.kNode.moveToTop();
this.mediaLayerRef.add(this.kNode);
this.listenToBaseKNode();
this.mediaLayerRef.draw();
}
public getNodeData(canvasPosition: Position) {
const data: NodeData = {
type: NodeType.SHAPE,
transform: {
position: {
x: this.kNode.position().x - canvasPosition.x,
y: this.kNode.position().y - canvasPosition.y,
},
size: this.kNode.size(),
rotation: this.kNode.rotation(),
scale: {
x: this.kNode.scaleX(),
y: this.kNode.scaleY(),
},
zIndex: this.kNode.getZIndex(),
},
};
return data;
}
}
@@ -1,186 +0,0 @@
import Konva from "konva";
import { SelectionManager } from "../NodesManagers";
import { BaseNode } from "./BaseNode";
import {
NodeData,
Position,
Size,
TextNodeChildrenTransformData,
TextNodeData,
TransformationData,
} from "../types";
import { NodeUtilities } from "./NodeUtilities";
import { NodeType } from "./constants";
export class PureTextNode extends BaseNode {
public kNode: Konva.Group;
public rectNode: Konva.Rect;
public textNode: Konva.Text;
private originalTextSize: Size;
private textNodeData: TextNodeData;
public didFinishLoading: boolean = true;
constructor({
textNodeData,
selectionManagerRef,
mediaLayerRef,
canvasPosition,
canvasSize,
transform: existingTransform,
textChildrenTransforms,
}: {
textNodeData: TextNodeData;
selectionManagerRef: SelectionManager;
mediaLayerRef: Konva.Layer;
canvasSize: Size;
canvasPosition: Position;
transform?: TransformationData;
textChildrenTransforms?: TextNodeChildrenTransformData;
}) {
const textNode = new Konva.Text({
...textNodeData,
x: 10,
y: 10,
width: 500,
});
const kNode = new Konva.Group({
position: NodeUtilities.positionNodeOnCanvasCenter({
canvasOffset: canvasPosition,
componentSize: {
width: textNode.width() + 20,
height: textNode.height() + 20,
},
maxSize: canvasSize,
}),
width: textNode.width() + 20,
height: textNode.height() + 20,
draggable: true,
});
const rectNode = new Konva.Rect({
x: 0,
y: 0,
width: textNode.width() + 20,
height: textNode.height() + 20,
strokeScaleEnabled: false,
name: "wrapper",
});
super({
selectionManagerRef: selectionManagerRef,
mediaLayerRef: mediaLayerRef,
kNode: kNode,
});
this.textNodeData = textNodeData;
this.kNode = kNode;
this.textNode = textNode;
this.rectNode = rectNode;
this.kNode.add(this.rectNode);
this.kNode.add(this.textNode);
if (existingTransform && textChildrenTransforms) {
this.applyTransform({
existingTransform,
textChildrenTransforms,
canvasPosition,
});
}
this.mediaLayerRef.add(this.kNode);
this.originalTextSize = this.textNode.size();
this.listenToBaseKNode();
}
public listenToBaseKNodeTransformations() {
this.kNode.on("transformstart", () => {
// console.log("transformstart", event.target._id);
this.selectionManagerRef.transformStart();
});
this.kNode.on("transform", () => {
const newBoxSize = {
width: this.kNode.width() * this.kNode.scaleX(),
height: this.kNode.height() * this.kNode.scaleY(),
scaleX: 1,
scaleY: 1,
};
this.rectNode.setAttrs(newBoxSize);
this.kNode.setAttrs(newBoxSize);
const scaleX = (newBoxSize.width - 20) / this.originalTextSize.width;
const scaleY = (newBoxSize.height - 20) / this.originalTextSize.height;
this.textNode.setAttrs({
// make sure these stay the same
x: 10,
y: 10,
size: this.originalTextSize,
//while these changes in size
scaleX: scaleX,
scaleY: scaleY,
});
});
this.kNode.on("transformend", () => {
// console.log("transformend", event.target._id);
NodeUtilities.printKNodeAttrs(this.kNode);
NodeUtilities.printKNodeAttrs(this.textNode);
this.selectionManagerRef.transformEnd(this);
});
}
public getNodeData(canvasPostion: Position) {
const data: NodeData = {
type: NodeType.TEXT,
transform: {
position: {
x: this.kNode.position().x - canvasPostion.x,
y: this.kNode.position().y - canvasPostion.y,
},
size: this.kNode.size(),
rotation: this.kNode.rotation(),
scale: {
x: this.kNode.scaleX(),
y: this.kNode.scaleY(),
},
zIndex: this.kNode.getZIndex(),
},
textNodeData: this.textNodeData,
textChildrenTransforms: {
wrapperRectTransform: {
position: this.rectNode.position(),
size: this.rectNode.size(),
rotation: this.rectNode.rotation(),
scale: {
x: this.rectNode.scaleX(),
y: this.rectNode.scaleY(),
},
zIndex: this.rectNode.getZIndex(),
},
textNodeTransform: {
position: this.textNode.position(),
size: this.textNode.size(),
rotation: this.textNode.rotation(),
scale: {
x: this.textNode.scaleX(),
y: this.textNode.scaleY(),
},
zIndex: this.textNode.getZIndex(),
},
},
};
return data;
}
private applyTransform({
canvasPosition,
existingTransform,
textChildrenTransforms,
}: {
canvasPosition: Position;
existingTransform: TransformationData;
textChildrenTransforms: TextNodeChildrenTransformData;
}) {
this.kNode.setAttrs({
...existingTransform,
position: {
x: existingTransform.position.x + canvasPosition.x,
y: existingTransform.position.y + canvasPosition.y,
},
});
this.rectNode.setAttrs(textChildrenTransforms.wrapperRectTransform);
this.textNode.setAttrs(textChildrenTransforms.textNodeTransform);
}
}
@@ -1,152 +0,0 @@
import Konva from "konva";
import { SelectionManager } from "../NodesManagers";
import { Position, Size, NodeData, TransformationData } from "../types";
import { NodeType } from "./constants";
import { NodeUtilities } from "./NodeUtilities";
import { NetworkedNode } from "./NetworkedNode";
export enum ShapeType {
CIRCLE = "circle",
SQUARE = "square",
TRIANGLE = "triangle",
}
interface ShapeNodeConstructor {
canvasPosition: Position;
canvasSize: Size;
shapeType: ShapeType;
size: Size;
color?: string; // Hex color string
transform?: TransformationData;
mediaLayerRef: Konva.Layer;
selectionManagerRef: SelectionManager;
loaded: () => Promise<void>;
}
export class ShapeNode extends NetworkedNode {
retry(): void {
throw new Error("Method not implemented.");
}
public shapeType: ShapeType;
public size: Size;
public color: string;
constructor({
canvasPosition,
canvasSize,
shapeType,
size,
color = "#2D81FF", // Default red if no color provided
transform: existingTransform,
mediaLayerRef,
selectionManagerRef,
loaded,
}: ShapeNodeConstructor) {
// Create the actual shape inside the group
const transform = NodeUtilities.getInitialTransform({
existingTransform,
mediaFileSize: size,
canvasPosition,
canvasSize,
});
let shape = null;
switch (shapeType) {
case ShapeType.CIRCLE:
shape = new Konva.Ellipse({
radiusX: size.width / 2,
radiusY: size.height / 2,
fill: color,
strokeScaleEnabled: false,
draggable: true,
});
break;
case ShapeType.SQUARE:
shape = new Konva.Rect({
width: size.width,
height: size.height,
...transform,
fill: color,
strokeScaleEnabled: false,
draggable: true,
});
break;
case ShapeType.TRIANGLE:
shape = new Konva.Shape({
width: size.width,
height: size.height,
...transform,
fill: color,
strokeScaleEnabled: false,
draggable: true,
sceneFunc: (context, shape) => {
context.beginPath();
context.moveTo(shape.width() / 2, 0);
context.lineTo(shape.width(), shape.height());
context.lineTo(0, shape.height());
context.closePath();
context.fillStrokeShape(shape);
}
});
break;
default:
throw new Error("Invalid shape type");
}
super({
selectionManagerRef: selectionManagerRef,
mediaLayerRef: mediaLayerRef,
kNode: shape,
});
this.shapeType = shapeType;
this.size = size;
this.color = color;
// Center the shape
const centerPosition = NodeUtilities.positionNodeOnCanvasCenter({
canvasOffset: canvasPosition,
componentSize: this.size,
maxSize: canvasSize,
});
// If the shape is an ellipse, the center is the origin so we need to offset it
if (shapeType === ShapeType.CIRCLE) {
centerPosition.x += size.width / 2;
centerPosition.y += size.height / 2;
}
// Add shape to group
// bring to the front
this.kNode.moveToTop();
this.kNode.setPosition(centerPosition);
this.mediaLayerRef.add(this.kNode);
this.listenToBaseKNode();
this.mediaLayerRef.draw();
}
public getNodeData(canvasPosition: Position) {
const data: NodeData = {
type: NodeType.SHAPE,
transform: {
position: {
x: this.kNode.position().x - canvasPosition.x,
y: this.kNode.position().y - canvasPosition.y,
},
size: this.kNode.size(),
rotation: this.kNode.rotation(),
scale: {
x: this.kNode.scaleX(),
y: this.kNode.scaleY(),
},
zIndex: this.kNode.getZIndex(),
},
};
return data;
}
}
@@ -1,168 +0,0 @@
import Konva from "konva";
import { SelectionManager } from "../NodesManagers";
import { BaseNode } from "./BaseNode";
import {
NodeData,
Position,
Size,
TextAlign,
TextNodeData,
TransformationData,
} from "../types";
import { NodeUtilities } from "./NodeUtilities";
import { NodeType, transparent } from "./constants";
export class TextNode extends BaseNode {
public kNode: Konva.Image;
private textNodeData: TextNodeData;
private originalTextSize?: Size;
public didFinishLoading: boolean = true;
constructor({
textNodeData,
selectionManagerRef,
mediaLayerRef,
canvasPosition,
canvasSize,
transform: existingTransform,
}: {
textNodeData: TextNodeData;
selectionManagerRef: SelectionManager;
mediaLayerRef: Konva.Layer;
canvasSize: Size;
canvasPosition: Position;
transform?: TransformationData;
}) {
const transform = NodeUtilities.getInitialTransform({
existingTransform,
canvasPosition,
canvasSize,
});
// Create a new ImageNode to house the canvas text
const kNode = new Konva.Image({
image: undefined, // will use the canvas as the image source
...transform,
draggable: true,
strokeScaleEnabled: false,
});
super({
selectionManagerRef: selectionManagerRef,
mediaLayerRef: mediaLayerRef,
kNode: kNode,
});
this.textNodeData = textNodeData;
this.kNode = kNode;
this.mediaLayerRef.add(this.kNode);
this.listenToBaseKNode();
this.loadTextImageUsingCanvas({
maxSize: canvasSize,
refPosition: canvasPosition,
existingTransform: existingTransform,
});
}
private loadTextImageUsingCanvas({
maxSize,
refPosition,
existingTransform,
}: {
maxSize: Size;
refPosition: Position;
existingTransform?: TransformationData;
}) {
// Create a temporary canvas to draw the text
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
if (context === null) {
return;
}
// calvulate and set canvas
const lines: string[] = this.textNodeData.text.split("\n");
const { fontSize, fontFamily, fontStyle, fontWeight, maxWidth, textAlign } =
this.textNodeData;
this.originalTextSize = {
width: maxWidth,
height: fontSize * lines.length * 1.5 + fontSize * 0.5,
};
canvas.width = this.originalTextSize.width;
canvas.height = this.originalTextSize.height;
// Set the font styles to match the text node
context.font = `${fontWeight} ${fontStyle} ${fontSize}px ${fontFamily}`;
context.fillStyle = this.textNodeData.color;
context.textAlign = textAlign;
context.textBaseline = "top";
// Draw the text on the canvas
lines.forEach((line, idx) => {
const yOffset = fontSize * (1.5 * idx + 0.5);
const xOffset =
textAlign === TextAlign.LEFT
? 0
: textAlign === TextAlign.CENTER
? maxWidth / 2
: maxWidth; // textAlign === TextAlign.Right
context.fillText(line, xOffset, yOffset);
});
// Convert canvas to data URL
const dataURL = canvas.toDataURL("image/png");
// Create a new Image object
const imageComponent = new Image();
imageComponent.crossOrigin = "anonymous";
imageComponent.onload = () => {
if (existingTransform) {
this.kNode.setAttrs({
image: imageComponent,
fill: transparent,
});
return;
}
const imageSize = {
width: canvas.width,
height: canvas.height,
};
const centerPosition = NodeUtilities.positionNodeOnCanvasCenter({
canvasOffset: refPosition,
componentSize: imageSize,
maxSize: maxSize,
});
this.kNode.setAttrs({
image: imageComponent,
size: imageSize,
position: centerPosition,
fill: transparent,
});
};
imageComponent.src = dataURL;
}
public getNodeData(canvasPostion: Position) {
const data: NodeData = {
type: NodeType.TEXT,
transform: {
position: {
x: this.kNode.position().x - canvasPostion.x,
y: this.kNode.position().y - canvasPostion.y,
},
size: this.kNode.size(),
rotation: this.kNode.rotation(),
scale: {
x: this.kNode.scaleX(),
y: this.kNode.scaleY(),
},
zIndex: this.kNode.getZIndex(),
},
textNodeData: this.textNodeData,
};
return data;
}
}
@@ -1,16 +0,0 @@
import { Colors } from "../constants";
export enum NodeType {
IMAGE = "image",
TEXT = "text",
VIDEO = "video",
SHAPE = "shape",
}
export const transparent = Colors.transparent;
export const primaryOrange = Colors.primaryOrange;
export const highlightStrokeWidth = 2;
export const minNodeSize = {
width: 200,
height: 200,
};
@@ -1,7 +0,0 @@
export { NodeType } from "./constants";
export { BaseNode } from "./BaseNode";
export { ImageNode } from "./ImageNode";
export { PreviewCopyNode } from "./PreviewCopy";
export { TextNode } from "./TextNode";
export { ShapeNode , ShapeType} from "./ShapeNode";
export { PaintNode } from "./PaintNode";
@@ -1,122 +0,0 @@
import Konva from "konva";
import { MediaNode, TransformationData } from "../types";
import { uiAccess } from "../../signals/uiAccess";
export class NodeIsolator {
private mediaLayerRef: Konva.Layer;
private backgroundRect: Konva.Rect;
private referenceRect: Konva.Rect;
private nodeIsolationLayerRef: Konva.Layer;
private currentNode: MediaNode | undefined;
private originalKNodeTransformation: TransformationData | undefined;
private adjustSizeFnRef: (() => void) | undefined;
constructor({
mediaLayerRef,
nodeIsolationLayerRef,
}: {
mediaLayerRef: Konva.Layer;
nodeIsolationLayerRef: Konva.Layer;
}) {
this.mediaLayerRef = mediaLayerRef;
this.nodeIsolationLayerRef = nodeIsolationLayerRef;
this.backgroundRect = new Konva.Rect({
fill: "rgba(0,0,0,0.5)",
});
this.referenceRect = new Konva.Rect({
fill: "gray",
stroke: "salmon",
strokeWidth: 10,
dash: [20, 10],
strokeScaleEnabled: false,
});
}
private adjustSizes() {
this.backgroundRect.setAttrs({
width: window.innerWidth,
height: window.innerHeight,
});
if (this.currentNode && this.originalKNodeTransformation) {
this.referenceRect.setAttrs(this.originalKNodeTransformation);
const originalSize = {
width:
this.originalKNodeTransformation.size.width *
this.originalKNodeTransformation.scale.x,
height:
this.originalKNodeTransformation.size.height *
this.originalKNodeTransformation.scale.y,
};
const maxWidth = window.innerWidth * 0.8;
const isolationResize = {
width: maxWidth,
height: (maxWidth / originalSize.width) * originalSize.height,
};
const maxHeight = window.innerHeight - 350;
if (isolationResize.height > maxHeight) {
isolationResize.height = maxHeight;
isolationResize.width =
(maxHeight / originalSize.height) * originalSize.width;
}
const isolationReposition = {
x: (window.innerWidth - isolationResize.width) / 2,
y: (maxHeight - isolationResize.height) / 2 + 100,
};
this.currentNode.kNode.setAttrs({
scale: { x: 1, y: 1 },
rotation: 0,
size: isolationResize,
position: isolationReposition,
});
}
}
public enterIsolation(node: MediaNode) {
// console.log("NodeIsolator > enterIsolation");
uiAccess.toolbarMain.disable();
this.currentNode = node;
this.adjustSizeFnRef = () => this.adjustSizes();
this.preserveKNodeTransformation(node.kNode);
this.currentNode.kNode.remove();
this.nodeIsolationLayerRef.add(this.backgroundRect);
this.nodeIsolationLayerRef.add(this.currentNode.kNode);
this.mediaLayerRef.add(this.referenceRect);
this.adjustSizeFnRef();
window.addEventListener("resize", this.adjustSizeFnRef);
}
public exitIsolation() {
// console.log("NodeIsolator > exitIsolation");
if (this.adjustSizeFnRef) {
window.removeEventListener("resize", this.adjustSizeFnRef);
this.adjustSizeFnRef = undefined;
}
if (!this.currentNode) {
console.error("NodeIsolator lost crrent node before isolation!!");
return;
}
this.currentNode.kNode.remove();
this.backgroundRect.remove();
this.referenceRect.remove();
this.mediaLayerRef.add(this.currentNode.kNode);
this.currentNode.kNode.setAttrs(this.originalKNodeTransformation);
this.currentNode = undefined;
this.originalKNodeTransformation = undefined;
uiAccess.toolbarMain.enable();
}
private preserveKNodeTransformation(kNode: Konva.Node) {
this.originalKNodeTransformation = {
position: kNode.position(),
size: kNode.size(),
rotation: kNode.rotation(),
scale: {
x: kNode.scaleX(),
y: kNode.scaleY(),
},
zIndex: kNode.zIndex(),
};
}
}

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