Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c14f35c9f | ||
|
|
921e58a308 | ||
|
|
65531dc63e | ||
|
|
0b60835bbc | ||
|
|
cca897d24d | ||
|
|
ea76917264 | ||
|
|
fc824a10ae | ||
|
|
0ee9b7c442 | ||
|
|
17495276e7 | ||
|
|
2d78fd5a6f | ||
|
|
98e513e917 | ||
|
|
97eb1c65ca | ||
|
|
ba82a438dd | ||
|
|
de7b990ec5 | ||
|
|
7b90c59675 | ||
|
|
11f1a5f41a | ||
|
|
b92cd58f81 | ||
|
|
914c26b4c6 | ||
|
|
fe8939a0f6 | ||
|
|
c809ff88fa | ||
|
|
86675568a8 | ||
|
|
a8927cc78d | ||
|
|
0e8dd19ecf | ||
|
|
b32a2e79b9 | ||
|
|
a662c5809d | ||
|
|
7498720674 | ||
|
|
c8a4490b52 | ||
|
|
c4e30e6dc7 | ||
|
|
12070f14ad | ||
|
|
77a103de06 | ||
|
|
82f5805f24 | ||
|
|
f7c9ebfaa3 | ||
|
|
76cd00cc13 | ||
|
|
cb64f45b48 | ||
|
|
6751ab0b60 | ||
|
|
9827a3efc5 | ||
|
|
93b53d84cd | ||
|
|
a7794f6338 | ||
|
|
ef03e292c3 | ||
|
|
0e8c1fa3e7 | ||
|
|
af08834de7 | ||
|
|
e728806c04 | ||
|
|
3e605e7b18 | ||
|
|
d244524abc | ||
|
|
8e455e27a9 | ||
|
|
e0df1644a5 | ||
|
|
736d033ebf | ||
|
|
af3f0baa63 | ||
|
|
a438dd13dc | ||
|
|
f7b76a7abe | ||
|
|
9435af3e2a | ||
|
|
77efda1d66 | ||
|
|
fdc27b6ccd | ||
|
|
e3abd500db | ||
|
|
ee4de4126d | ||
|
|
4810ed30ae | ||
|
|
0b6593a063 | ||
|
|
b0e2dce140 | ||
|
|
c4f401923b | ||
|
|
24559a3554 | ||
|
|
5a26aed728 | ||
|
|
95773c1c23 | ||
|
|
2d01073434 | ||
|
|
1c62092d94 | ||
|
|
5e22e17dfb | ||
|
|
611da775a9 | ||
|
|
6fc3f73579 | ||
|
|
562cb99c51 | ||
|
|
67d280df84 | ||
|
|
8ccc8927a9 | ||
|
|
a21e7331cc | ||
|
|
0f0f8eb39c | ||
|
|
b64ed411b1 | ||
|
|
a155b419e1 | ||
|
|
e6177005df | ||
|
|
1c78e17565 | ||
|
|
0769bfad28 | ||
|
|
183946a943 | ||
|
|
d43ee0f6c9 | ||
|
|
eec59c218d | ||
|
|
5c1e72f8f4 | ||
|
|
d91243b1f5 | ||
|
|
83cecad280 | ||
|
|
06313f54a5 | ||
|
|
ef3ca88054 | ||
|
|
cfcf7ab5aa | ||
|
|
4cc215495a | ||
|
|
f1d14ab73f | ||
|
|
5cb8f5b93a | ||
|
|
9decf8bf88 | ||
|
|
63a7f99c53 | ||
|
|
9dd16f2e80 | ||
|
|
abe2d2a093 | ||
|
|
552d5553c0 | ||
|
|
0d33358ead | ||
|
|
908ff43be3 | ||
|
|
196e1c4615 | ||
|
|
8f321f06d0 | ||
|
|
0b338f3886 | ||
|
|
de76fc6c36 | ||
|
|
9fb0ca2dab | ||
|
|
42965dfcfa | ||
|
|
8c7b358534 | ||
|
|
d8851abf24 | ||
|
|
2c8254e50f | ||
|
|
15348f7b0e | ||
|
|
6071dda4f9 | ||
|
|
1f6416856a | ||
|
|
1370ab63d9 | ||
|
|
1e0f31ed8d | ||
|
|
fd4f284af6 | ||
|
|
51a114a699 | ||
|
|
75621ea85a | ||
|
|
e003e94e54 | ||
|
|
0d1bd6b014 | ||
|
|
9861263b08 | ||
|
|
9fa73c6e06 | ||
|
|
caada00cd9 | ||
|
|
df8d999ed7 | ||
|
|
3f94e48f12 | ||
|
|
68e57bd601 | ||
|
|
e65011cd51 | ||
|
|
b2f989b0b7 | ||
|
|
233d61d7a1 | ||
|
|
ab412c6f10 | ||
|
|
9d6b31200e | ||
|
|
a1aacad7dd | ||
|
|
6436de9f44 | ||
|
|
116782e6ed | ||
|
|
431f2efcf0 | ||
|
|
7c64da88da | ||
|
|
93eb78da91 | ||
|
|
f96e79ff49 | ||
|
|
39e6f4511f | ||
|
|
4c0d062d76 |
@@ -48,13 +48,13 @@ jobs:
|
||||
# You can remove the one that doesn't apply to your app to speed up the workflow a bit.
|
||||
|
||||
- name: install frontend dependencies
|
||||
run: cd ./crates/desktop/tauri-artcraft && yarn install # change this to npm, pnpm or bun depending on which one you use.
|
||||
run: cd ./crates/desktop/artcraft && yarn install # change this to npm, pnpm or bun depending on which one you use.
|
||||
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
projectPath: ./crates/desktop/tauri-artcraft
|
||||
projectPath: ./crates/desktop/artcraft
|
||||
tagName: app-v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version.
|
||||
releaseName: 'App v__VERSION__'
|
||||
releaseBody: 'See the assets to download this version and install.'
|
||||
|
||||
@@ -24,20 +24,22 @@ members = [
|
||||
"crates/cli/production/migration-tool",
|
||||
|
||||
# Desktop apps
|
||||
"crates/desktop/tauri-artcraft",
|
||||
"crates/desktop/tauri-realtime",
|
||||
"crates/desktop/artcraft",
|
||||
|
||||
# Shared libraries
|
||||
"crates/lib/actix_cors_configs",
|
||||
"crates/lib/actix_helpers",
|
||||
"crates/lib/api_clients/cloud_storage",
|
||||
"crates/lib/api_clients/fal_client",
|
||||
"crates/lib/api_clients/google_drive_common",
|
||||
"crates/lib/api_clients/google_sign_in",
|
||||
"crates/lib/api_clients/openai_sora_client",
|
||||
"crates/lib/api_clients/runwayml_client",
|
||||
"crates/lib/api_clients/storyteller_client",
|
||||
"crates/lib/api_clients/thumbnail_generator",
|
||||
"crates/lib/bootstrap",
|
||||
"crates/lib/bucket_paths",
|
||||
"crates/lib/build_metadata",
|
||||
"crates/lib/caching/memory_caching",
|
||||
"crates/lib/caching/memory_store",
|
||||
"crates/lib/caching/redis_caching",
|
||||
@@ -55,6 +57,7 @@ members = [
|
||||
"crates/lib/email_sender",
|
||||
"crates/lib/errors",
|
||||
"crates/lib/files/archives",
|
||||
"crates/lib/files/file_to_base64_url",
|
||||
"crates/lib/files/filesys",
|
||||
"crates/lib/files/hashing",
|
||||
"crates/lib/files/images",
|
||||
@@ -64,6 +67,7 @@ members = [
|
||||
"crates/lib/idempotency",
|
||||
"crates/lib/image_algorithms",
|
||||
"crates/lib/jobs_common",
|
||||
"crates/lib/jwt_light",
|
||||
"crates/lib/logging",
|
||||
"crates/lib/machine_learning/ml_models",
|
||||
"crates/lib/machine_learning/ml_weights_registry",
|
||||
@@ -158,15 +162,16 @@ workspace-build-acceleration = { path = "crates/workspace-build-acceleration" }
|
||||
# External libs
|
||||
# Centrally pinned versions so that we can uniformly upgrade/downgrade versions across the workspace
|
||||
# If a crate needs to use an older/newer version, the dependency should be managed in that crate's own Cargo.toml
|
||||
anyhow = "1.0.97"
|
||||
anyhow = "1.0.98"
|
||||
base64 = "0.22.1"
|
||||
chrono = { version = "0.4.40", features = ["serde"] }
|
||||
chrono = { version = "0.4.41", features = ["serde"] }
|
||||
cookie = "0.18.1"
|
||||
futures = { version = "0.3.31" }
|
||||
hex-literal = "1.0.0"
|
||||
indexmap = "2.9.0"
|
||||
log = "0.4.27"
|
||||
once_cell = "1.21.3"
|
||||
rand = "0.9.0"
|
||||
rand = "0.9.1"
|
||||
regex = "1.11.1"
|
||||
reqwest = { version = "0.12.15", features = ["json", "cookies"] }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
@@ -174,19 +179,23 @@ serde_derive = "1.0.219"
|
||||
serde_json = "1.0.140"
|
||||
serde_yaml = "0.9.1"
|
||||
sha3 = "0.10.8"
|
||||
tauri = { version = "2.4.1", features = ["unstable"] }
|
||||
tauri = { version = "2.4.1", features = ["unstable", "devtools"] } # NB: "devtools" prevents App Store distribution
|
||||
thiserror = "2.0.12"
|
||||
toml = "0.8.20"
|
||||
tokio = { version = "1.45.0", features = ["macros"] }
|
||||
toml = "0.8.22"
|
||||
url = { version = "2.5.4", features = ["serde"] }
|
||||
|
||||
# Internal libs
|
||||
build_metadata = { path = "crates/lib/build_metadata" }
|
||||
crockford = { path = "crates/lib/data/crockford" }
|
||||
enums = { path = "crates/_types/enums" }
|
||||
enums_public = { path = "crates/_types/enums_public" }
|
||||
errors = { path = "crates/lib/errors" }
|
||||
fal_client = { path = "crates/lib/api_clients/fal_client" }
|
||||
filesys = { path = "crates/lib/files/filesys" }
|
||||
idempotency = { path = "crates/lib/idempotency" }
|
||||
images = { path = "crates/lib/files/images" }
|
||||
jwt_light = { path = "crates/lib/jwt_light" }
|
||||
memory_caching = { path = "crates/lib/caching/memory_caching" }
|
||||
memory_store = { path = "crates/lib/caching/memory_store" }
|
||||
mimetypes = { path = "crates/lib/files/mimetypes" }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "tauri-artcraft"
|
||||
name = "artcraft"
|
||||
version = "0.1.0"
|
||||
description = "Artcraft"
|
||||
authors = ["you"]
|
||||
@@ -12,7 +12,7 @@ publish = false
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
name = "tauri_artcraft_app_lib"
|
||||
name = "artcraft_app_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
@@ -23,11 +23,14 @@ tauri-build = { version = "2.1.1", features = [] }
|
||||
workspace-build-acceleration = { workspace = true }
|
||||
|
||||
# Internal
|
||||
build_metadata = { workspace = true }
|
||||
crockford = { workspace = true }
|
||||
errors = { workspace = true }
|
||||
fal_client = { workspace = true }
|
||||
filesys = { workspace = true }
|
||||
memory_caching = { workspace = true }
|
||||
memory_store = { workspace = true }
|
||||
mimetypes = { workspace = true }
|
||||
openai_sora_client = { workspace = true }
|
||||
primitives = { workspace = true }
|
||||
storyteller_client = { workspace = true }
|
||||
@@ -55,10 +58,12 @@ serde_json = { workspace = true }
|
||||
serde_yaml = { workspace = true }
|
||||
tauri = { workspace = true }
|
||||
tauri-plugin-log = "2.3.1"
|
||||
tauri-plugin-upload = "2.2.1"
|
||||
tempdir = "0.3.7"
|
||||
tempfile = "3.18.0"
|
||||
tokio = { version = "1.42", features = ["rt", "rt-multi-thread", "fs"] }
|
||||
tokio-util = { version = "0.7", features = ["codec"] }
|
||||
url = { workspace = true }
|
||||
|
||||
# NB: We need to vendor this in so that we can access the cookies inside via the private API
|
||||
tauri-plugin-http = { path = "../../vendor/tauri-plugin-http" }
|
||||
@@ -66,6 +71,7 @@ tauri-plugin-http = { path = "../../vendor/tauri-plugin-http" }
|
||||
# NB: The version of the `discord-rich-presence` crate (0.2.5) last published has non-Send/Sync error
|
||||
# types and can't do threading. We need to wait for a new version to unpin from Git.
|
||||
discord-rich-presence = { git = "https://github.com/vionya/discord-rich-presence" }
|
||||
tauri-plugin-dialog = "2"
|
||||
|
||||
[dev-dependencies]
|
||||
libloading = "0.8.6"
|
||||
@@ -8,13 +8,17 @@
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"log:default",
|
||||
"upload:allow-download",
|
||||
"upload:default",
|
||||
"http:allow-fetch",
|
||||
{
|
||||
"identifier": "http:default",
|
||||
"allow": [
|
||||
{ "url": "https://*.storyteller.ai" },
|
||||
{ "url": "https://*.fakeyou.com" }
|
||||
],
|
||||
"deny": [{ "url": "https://private.tauri.app" }]
|
||||
}
|
||||
{
|
||||
"url": "https://*"
|
||||
}
|
||||
]
|
||||
},
|
||||
"dialog:default"
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 1023 B |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
@@ -0,0 +1,70 @@
|
||||
use crate::core::state::app_preferences::app_preferences_manager::AppPreferencesManager;
|
||||
use crate::core::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
|
||||
use errors::AnyhowResult;
|
||||
use log::{error, info};
|
||||
use serde_derive::Serialize;
|
||||
use tauri::{AppHandle, State};
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct GetAppPreferencesResponse {
|
||||
pub preferences: AppPreferencesPayload,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct AppPreferencesPayload {
|
||||
/// The downloads directory to use when a user downloads a file.
|
||||
pub preferred_download_directory: PreferredDownloadDirectory,
|
||||
|
||||
/// Play sounds on events.
|
||||
pub play_sounds: bool,
|
||||
|
||||
/// Key pointing to file; defined in the frontend code.
|
||||
/// 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>,
|
||||
|
||||
/// Key pointing to file; defined in the frontend code.
|
||||
pub generation_failure_sound: Option<String>,
|
||||
|
||||
/// Key pointing to file; defined in the frontend code.
|
||||
#[deprecated]
|
||||
pub generation_enqueue_sound: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_app_preferences_command(
|
||||
app_prefs: State<'_, AppPreferencesManager>,
|
||||
) -> Result<GetAppPreferencesResponse, String> {
|
||||
info!("get_app_preferences_command called");
|
||||
|
||||
let result = get_prefs(&app_prefs)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error getting app preferences: {:?}", err);
|
||||
format!("Error getting app preferences: {:?}", err)
|
||||
})?;
|
||||
|
||||
Ok(GetAppPreferencesResponse {
|
||||
preferences: result,
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_prefs(app_prefs: &AppPreferencesManager) -> AnyhowResult<AppPreferencesPayload> {
|
||||
let prefs = app_prefs.get_clone()?;
|
||||
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,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod get_app_preferences_command;
|
||||
pub mod update_app_preference_command;
|
||||
@@ -0,0 +1,122 @@
|
||||
use crate::core::state::app_preferences::app_preferences_manager::AppPreferencesManager;
|
||||
use crate::core::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use anyhow::anyhow;
|
||||
use errors::AnyhowResult;
|
||||
use log::{error, info};
|
||||
use openai_sora_client::sora_error::SoraError::AnyhowError;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tauri::{AppHandle, State};
|
||||
|
||||
/// For now, we'll only update a single preference at a time.
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdateAppPreferencesRequest {
|
||||
pub preference: PreferenceName,
|
||||
/// We'll decode this with respect to the preference value.
|
||||
pub value: Option<ValueType>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(untagged)]
|
||||
pub enum ValueType {
|
||||
Bool(bool),
|
||||
String(String),
|
||||
DownloadDirectory(PreferredDownloadDirectory),
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PreferenceName {
|
||||
PreferredDownloadDirectory,
|
||||
PlaySounds,
|
||||
EnqueueSuccessSound,
|
||||
EnqueueFailureSound,
|
||||
GenerationSuccessSound,
|
||||
GenerationFailureSound,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct UpdateAppPreferencesResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn update_app_preferences_command(
|
||||
request: UpdateAppPreferencesRequest,
|
||||
app_prefs: State<'_, AppPreferencesManager>,
|
||||
app_data_root: State<'_, AppDataRoot>,
|
||||
) -> Result<UpdateAppPreferencesResponse, String> {
|
||||
info!("update_app_preferences_command called");
|
||||
|
||||
update_prefs(request, &app_prefs, &app_data_root)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error getting app preferences: {:?}", err);
|
||||
format!("Error getting app preferences: {:?}", err)
|
||||
})?;
|
||||
|
||||
Ok(UpdateAppPreferencesResponse {
|
||||
success: true,
|
||||
})
|
||||
}
|
||||
|
||||
async fn update_prefs(
|
||||
request: UpdateAppPreferencesRequest,
|
||||
app_prefs: &AppPreferencesManager,
|
||||
app_data_root: &AppDataRoot,
|
||||
) -> AnyhowResult<()> {
|
||||
let mut prefs = app_prefs.get_clone()?;
|
||||
|
||||
info!("Value is: {:?}", request.value);
|
||||
|
||||
match request.preference {
|
||||
PreferenceName::PreferredDownloadDirectory => {
|
||||
match request.value {
|
||||
Some(ValueType::DownloadDirectory(dir)) =>
|
||||
prefs.preferred_download_directory = dir,
|
||||
_ =>
|
||||
return Err(anyhow!("Invalid value: {:?}", request.value)),
|
||||
}
|
||||
}
|
||||
PreferenceName::PlaySounds => {
|
||||
match request.value {
|
||||
Some(ValueType::Bool(val)) =>
|
||||
prefs.play_sounds = val,
|
||||
_ =>
|
||||
return Err(anyhow!("Invalid value: {:?}", request.value)),
|
||||
}
|
||||
}
|
||||
PreferenceName::EnqueueSuccessSound => {
|
||||
prefs.enqueue_success_sound = request.value
|
||||
.map(|val| string_value(&val))
|
||||
.transpose()?;
|
||||
}
|
||||
PreferenceName::EnqueueFailureSound => {
|
||||
prefs.enqueue_failure_sound = request.value
|
||||
.map(|val| string_value(&val))
|
||||
.transpose()?;
|
||||
}
|
||||
PreferenceName::GenerationSuccessSound => {
|
||||
prefs.generation_success_sound = request.value
|
||||
.map(|val| string_value(&val))
|
||||
.transpose()?;
|
||||
}
|
||||
PreferenceName::GenerationFailureSound => {
|
||||
prefs.generation_failure_sound = request.value
|
||||
.map(|val| string_value(&val))
|
||||
.transpose()?;
|
||||
}
|
||||
}
|
||||
|
||||
app_prefs.set_clone(&prefs)?;
|
||||
app_data_root.settings_dir().write_app_preferences(&prefs)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn string_value(value: &ValueType) -> AnyhowResult<String> {
|
||||
match value {
|
||||
ValueType::String(val) => Ok(val.to_string()),
|
||||
_ => Err(anyhow!("Invalid value type: {:?}", value)),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
use serde::Serialize;
|
||||
use std::error::Error;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// Easy to use Result<T, E> type.
|
||||
pub type CommandResult<SuccessPayload, ErrType, ErrPayload> =
|
||||
Result<CommandSuccessResponseWrapper<SuccessPayload>, CommandErrorResponseWrapper<ErrType, ErrPayload>>;
|
||||
|
||||
/// No inner payloads for this type. Just strings as messages.
|
||||
pub type SimpleCommandResult = CommandResult<(), (), ()>;
|
||||
|
||||
/// 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
|
||||
TooManyRequests, // 429
|
||||
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<ErrType: Serialize, ErrPayload: 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>,
|
||||
|
||||
/// Represents a "type" of error, preferably an enum without payloads.
|
||||
/// Preferably this serializes to a string.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error_type: Option<ErrType>,
|
||||
|
||||
/// Represents a full error details object or payload.
|
||||
/// This could be a structure, message, or whatever.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error_details: Option<ErrPayload>,
|
||||
}
|
||||
|
||||
|
||||
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.
|
||||
pub 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<(), ()> {
|
||||
fn from(value: &str) -> Self {
|
||||
CommandErrorResponseWrapper {
|
||||
status: CommandErrorStatus::BadRequest, // NB: Default to 500 error.
|
||||
error_message: Some(value.to_string()),
|
||||
error_type: None,
|
||||
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"}"#);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
use crate::core::state::os_platform::OsPlatform;
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
use base64::Engine;
|
||||
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
|
||||
use image::ImageReader;
|
||||
use log::info;
|
||||
use serde_derive::Serialize;
|
||||
use std::io::Cursor;
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct BuildInfoResponse {
|
||||
pub build_timestamp: DateTime<Utc>,
|
||||
pub git_commit_id: Option<String>,
|
||||
pub git_commit_short_id: Option<String>,
|
||||
pub git_commit_short_timestamp: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_build_info_command() -> Result<BuildInfoResponse, String> {
|
||||
info!("get_build_info_command called...");
|
||||
|
||||
let build_timestamp = build_metadata::build_timestamp();
|
||||
let git_commit_id = build_metadata::git_commit_id();
|
||||
let git_commit_short_id = build_metadata::git_commit_short_id();
|
||||
let git_commit_short_timestamp = build_metadata::git_commit_timestamp();
|
||||
|
||||
Ok(BuildInfoResponse {
|
||||
build_timestamp,
|
||||
git_commit_id: git_commit_id.map(|s| s.to_string()),
|
||||
git_commit_short_id: git_commit_short_id.map(|s| s.to_string()),
|
||||
git_commit_short_timestamp,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod app_preferences;
|
||||
pub mod command_response_wrapper;
|
||||
pub mod flip_image;
|
||||
pub mod get_build_info_command;
|
||||
pub mod platform_info_command;
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::state::os_platform::OsPlatform;
|
||||
use crate::core::state::os_platform::OsPlatform;
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
use base64::Engine;
|
||||
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod notification_event;
|
||||
pub mod sendable_event_error;
|
||||
pub mod sendable_event_trait;
|
||||
@@ -0,0 +1,31 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SendableEventError {
|
||||
TauriError(tauri::Error),
|
||||
AnyhowError(anyhow::Error),
|
||||
}
|
||||
|
||||
impl Error for SendableEventError {}
|
||||
|
||||
impl Display for SendableEventError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::TauriError(err) => write!(f, "SendableEventError::TauriError: {}", err),
|
||||
Self::AnyhowError(err) => write!(f, "SendableEventError::AnyhowError: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for SendableEventError {
|
||||
fn from(err: anyhow::Error) -> Self {
|
||||
SendableEventError::AnyhowError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tauri::Error> for SendableEventError {
|
||||
fn from(err: tauri::Error) -> Self {
|
||||
SendableEventError::TauriError(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
use log::info;
|
||||
use crate::core::events::sendable_event_error::SendableEventError;
|
||||
use serde::Serialize;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
|
||||
/// Implement this on a Serializable object to be able to send it as an event to the frontend.
|
||||
pub trait SendableEvent : Serialize {
|
||||
/// This is the name of the event that the frontend subscribes to.
|
||||
const FRONTEND_EVENT_NAME: &'static str;
|
||||
|
||||
/// Default implementation of send().
|
||||
/// This serializes and sends the event to the frontend.
|
||||
fn send(&self, app: &AppHandle) -> Result<(), SendableEventError> {
|
||||
info!("Sending event to frontend: {}", Self::FRONTEND_EVENT_NAME);
|
||||
let result = app.emit(Self::FRONTEND_EVENT_NAME, self);
|
||||
if let Err(err) = result {
|
||||
return Err(SendableEventError::from(err));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
pub mod commands;
|
||||
pub mod events;
|
||||
pub mod state;
|
||||
pub mod threads;
|
||||
pub mod utils;
|
||||
pub mod windows;
|
||||
@@ -0,0 +1,71 @@
|
||||
use crate::core::state::app_preferences::app_preferences_serializable::AppPreferencesSerializable;
|
||||
use crate::core::state::app_preferences::preferred_download_directory::{PreferredDownloadDirectory, SystemDownloadDirectory};
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppPreferences {
|
||||
/// The downloads directory to use when a user downloads a file.
|
||||
pub preferred_download_directory: PreferredDownloadDirectory,
|
||||
|
||||
/// Play sounds on events.
|
||||
pub play_sounds: bool,
|
||||
|
||||
/// Key pointing to file; defined in the frontend code.
|
||||
/// 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>,
|
||||
|
||||
/// Key pointing to file; defined in the frontend code.
|
||||
pub generation_failure_sound: Option<String>,
|
||||
|
||||
/// Key pointing to file; defined in the frontend code.
|
||||
#[deprecated]
|
||||
pub generation_enqueue_sound: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for AppPreferences {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
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_failure_sound: Some("crumble".to_string()),
|
||||
generation_enqueue_sound: Some("done".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppPreferences {
|
||||
pub fn load_from_file_or_default(data_root: &AppDataRoot) -> Self {
|
||||
let filename = data_root.settings_dir().get_app_preferences_path();
|
||||
if !filename.exists() {
|
||||
return Self::default();
|
||||
}
|
||||
|
||||
match AppPreferencesSerializable::load_from_file(data_root) {
|
||||
Ok(Some(serializable)) => {
|
||||
serializable.to_preferences()
|
||||
}
|
||||
Ok(None) => {
|
||||
Self::default()
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Error loading app preferences: {}", err);
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_serializable(&self) -> AppPreferencesSerializable {
|
||||
AppPreferencesSerializable::from_preferences(self)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
use crate::core::state::app_preferences::app_preferences::AppPreferences;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use memory_store::clone_cell::CloneCell;
|
||||
|
||||
pub type AppPreferencesManager = CloneCell<AppPreferences>;
|
||||
|
||||
pub fn load_app_preferences_or_default(data_root: &AppDataRoot) -> AppPreferencesManager {
|
||||
let prefs = AppPreferences::load_from_file_or_default(data_root);
|
||||
AppPreferencesManager::with_owned(prefs)
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
use crate::core::state::app_preferences::app_preferences::AppPreferences;
|
||||
use crate::core::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use errors::AnyhowResult;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
/// Vector clock versioning string rather than semver.
|
||||
const CURRENT_VERSION: &str = "1";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AppPreferencesSerializable {
|
||||
/// Versioning string.
|
||||
pub version: String,
|
||||
|
||||
/// The downloads directory to use when a user downloads a file.
|
||||
pub preferred_download_directory: Option<PreferredDownloadDirectory>,
|
||||
|
||||
/// Play sounds on events.
|
||||
pub play_sounds: Option<bool>,
|
||||
|
||||
/// Key pointing to file; defined in the frontend code.
|
||||
pub generation_success_sound: Option<String>,
|
||||
|
||||
/// Key pointing to file; defined in the frontend code.
|
||||
pub generation_failure_sound: Option<String>,
|
||||
|
||||
/// Key pointing to file; defined in the frontend code.
|
||||
pub generation_enqueue_sound: Option<String>,
|
||||
}
|
||||
|
||||
impl AppPreferencesSerializable {
|
||||
pub fn load_from_file(app_data_root: &AppDataRoot) -> AnyhowResult<Option<Self>> {
|
||||
let filename = app_data_root.settings_dir().get_app_preferences_path();
|
||||
if !filename.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let contents = std::fs::read_to_string(filename)?;
|
||||
let data: Self = serde_json::from_str(&contents)?;
|
||||
Ok(Some(data))
|
||||
}
|
||||
|
||||
pub fn from_preferences(preferences: &AppPreferences) -> Self {
|
||||
Self {
|
||||
version: CURRENT_VERSION.to_string(),
|
||||
preferred_download_directory: Some(preferences.preferred_download_directory.clone()),
|
||||
play_sounds: Some(preferences.play_sounds),
|
||||
generation_success_sound: preferences.generation_success_sound.clone(),
|
||||
generation_failure_sound: preferences.generation_failure_sound.clone(),
|
||||
generation_enqueue_sound: preferences.generation_enqueue_sound.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_preferences(&self) -> AppPreferences {
|
||||
let mut preferences = AppPreferences::default();
|
||||
|
||||
if let Some(preferred_download_directory) = &self.preferred_download_directory {
|
||||
preferences.preferred_download_directory = preferred_download_directory.clone();
|
||||
}
|
||||
|
||||
if let Some(play_sounds) = self.play_sounds {
|
||||
preferences.play_sounds = play_sounds;
|
||||
}
|
||||
|
||||
preferences.generation_success_sound = self.generation_success_sound.clone();
|
||||
preferences.generation_failure_sound = self.generation_failure_sound.clone();
|
||||
preferences.generation_enqueue_sound = self.generation_enqueue_sound.clone();
|
||||
|
||||
preferences
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod app_preferences;
|
||||
pub mod app_preferences_manager;
|
||||
pub mod app_preferences_serializable;
|
||||
pub mod preferred_download_directory;
|
||||
@@ -0,0 +1,82 @@
|
||||
use errors::AnyhowError;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
const SYSTEM_DEFAULT_SENTINEL_VALUE: &str = "system_default";
|
||||
|
||||
/// NB: The reason these are not flat is so that they serialize/deserialize nicely to/from JSON.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PreferredDownloadDirectory {
|
||||
/// The system-default downloads directory, which varies by OS.
|
||||
/// NB: Serializes as `"system_default"` in JSON.
|
||||
System(SystemDownloadDirectory),
|
||||
|
||||
/// A user-defined path
|
||||
/// NB: Serializes as `{"custom": "/path/to/dir"}` in JSON.
|
||||
Custom(PathBuf),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SystemDownloadDirectory {
|
||||
Downloads,
|
||||
Documents,
|
||||
}
|
||||
|
||||
impl FromStr for SystemDownloadDirectory {
|
||||
type Err = AnyhowError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match (s) {
|
||||
"downloads" => Ok(SystemDownloadDirectory::Downloads),
|
||||
"documents" => Ok(SystemDownloadDirectory::Documents),
|
||||
_ => Err(AnyhowError::msg(format!("Invalid system download directory: {}", s))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for PreferredDownloadDirectory {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
PreferredDownloadDirectory::System(system) => system.fmt(f),
|
||||
PreferredDownloadDirectory::Custom(path) => write!(f, "{}", path.to_string_lossy()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for SystemDownloadDirectory {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
SystemDownloadDirectory::Downloads => write!(f, "downloads"),
|
||||
SystemDownloadDirectory::Documents => write!(f, "documents"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::core::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
|
||||
use crate::core::state::app_preferences::preferred_download_directory::SystemDownloadDirectory;
|
||||
use std::str::FromStr;
|
||||
|
||||
mod json {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn to_json_system_default() {
|
||||
let val = PreferredDownloadDirectory::System(SystemDownloadDirectory::Documents);
|
||||
let val = serde_json::to_string(&val).unwrap();
|
||||
assert_eq!(&val, "{\"system\":\"documents\"}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_json_custom() {
|
||||
let val = PreferredDownloadDirectory::Custom("/tmp".into());
|
||||
let val = serde_json::to_string(&val).unwrap();
|
||||
assert_eq!(&val, "{\"custom\":\"/tmp\"}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::state::data_dir::trait_data_subdir::DataSubdir;
|
||||
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
|
||||
use chrono::{DateTime, Local};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::state::data_dir::trait_data_subdir::DataSubdir;
|
||||
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -40,4 +40,8 @@ impl AppCredentialsDir {
|
||||
pub fn get_storyteller_session_cookie_file_path(&self) -> PathBuf {
|
||||
self.path.join("artcraft_session.txt")
|
||||
}
|
||||
|
||||
pub fn get_fal_api_key_file_path(&self) -> PathBuf {
|
||||
self.path.join("fal_api_key.txt")
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
use crate::state::data_dir::app_assets_dir::AppAssetsDir;
|
||||
use crate::state::data_dir::app_credentials_dir::AppCredentialsDir;
|
||||
use crate::state::data_dir::app_downloads_dir::AppDownloadsDir;
|
||||
use crate::state::data_dir::app_weights_dir::AppWeightsDir;
|
||||
use crate::state::data_dir::temporary_dir::TemporaryDir;
|
||||
use crate::state::data_dir::trait_data_subdir::DataSubdir;
|
||||
use crate::state::expanduser::expanduser;
|
||||
use crate::state::os_platform::OsPlatform;
|
||||
use crate::core::state::data_dir::app_assets_dir::AppAssetsDir;
|
||||
use crate::core::state::data_dir::app_credentials_dir::AppCredentialsDir;
|
||||
use crate::core::state::data_dir::app_downloads_dir::AppDownloadsDir;
|
||||
use crate::core::state::data_dir::app_settings_dir::AppSettingsDir;
|
||||
use crate::core::state::data_dir::app_weights_dir::AppWeightsDir;
|
||||
use crate::core::state::data_dir::temporary_dir::TemporaryDir;
|
||||
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
|
||||
use crate::core::state::expanduser::expanduser;
|
||||
use crate::core::state::os_platform::OsPlatform;
|
||||
use anyhow::anyhow;
|
||||
use directories::UserDirs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const DEFAULT_DATA_DIR : &str = "Artcraft";
|
||||
|
||||
|
||||
/// Note: Tauri appends ".log" to the end of the filename.
|
||||
const LOG_FILE_NAME : &str = "application_debug";
|
||||
|
||||
@@ -23,6 +23,7 @@ pub struct AppDataRoot {
|
||||
assets_dir: AppAssetsDir,
|
||||
credentials_dir: AppCredentialsDir,
|
||||
downloads_dir: AppDownloadsDir,
|
||||
settings_dir: AppSettingsDir,
|
||||
weights_dir: AppWeightsDir,
|
||||
temp_dir: TemporaryDir,
|
||||
log_file_name: PathBuf,
|
||||
@@ -60,10 +61,11 @@ impl AppDataRoot {
|
||||
}
|
||||
|
||||
let assets_dir = AppAssetsDir::get_or_create_in_root_dir(&dir)?;
|
||||
let downloads_dir = AppDownloadsDir::get_or_create_in_root_dir(&dir)?;
|
||||
let credentials_dir = AppCredentialsDir::get_or_create_in_root_dir(&dir)?;
|
||||
let weights_dir = AppWeightsDir::get_or_create_in_root_dir(&dir)?;
|
||||
let downloads_dir = AppDownloadsDir::get_or_create_in_root_dir(&dir)?;
|
||||
let settings_dir = AppSettingsDir::get_or_create_in_root_dir(&dir)?;
|
||||
let temp_dir = TemporaryDir::get_or_create_in_root_dir(&dir)?;
|
||||
let weights_dir = AppWeightsDir::get_or_create_in_root_dir(&dir)?;
|
||||
let log_file_name = dir.join(LOG_FILE_NAME);
|
||||
let log_file_name_string = log_file_name
|
||||
.to_str()
|
||||
@@ -75,6 +77,7 @@ impl AppDataRoot {
|
||||
assets_dir,
|
||||
credentials_dir,
|
||||
downloads_dir,
|
||||
settings_dir,
|
||||
weights_dir,
|
||||
temp_dir,
|
||||
log_file_name,
|
||||
@@ -94,6 +97,10 @@ impl AppDataRoot {
|
||||
&self.downloads_dir
|
||||
}
|
||||
|
||||
pub fn settings_dir(&self) -> &AppSettingsDir {
|
||||
&self.settings_dir
|
||||
}
|
||||
|
||||
pub fn weights_dir(&self) -> &AppWeightsDir {
|
||||
&self.weights_dir
|
||||
}
|
||||
@@ -129,6 +136,10 @@ 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,4 +1,4 @@
|
||||
use crate::state::data_dir::trait_data_subdir::DataSubdir;
|
||||
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -0,0 +1,56 @@
|
||||
use crate::core::state::app_preferences::app_preferences::AppPreferences;
|
||||
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
|
||||
use errors::AnyhowResult;
|
||||
use serde::Serialize;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// For now, let's just keep a collection of loose json files in a settings dir.
|
||||
/// Once we know the structure and schema, we can standardize on a single json file.
|
||||
#[derive(Clone)]
|
||||
pub struct AppSettingsDir {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl DataSubdir for AppSettingsDir {
|
||||
const DIRECTORY_NAME: &'static str = "settings";
|
||||
|
||||
fn new_from<P: AsRef<Path>> (dir: P) -> Self {
|
||||
Self {
|
||||
path: dir.as_ref().to_path_buf(),
|
||||
}
|
||||
}
|
||||
|
||||
fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
impl AppSettingsDir {
|
||||
pub fn get_app_preferences_path(&self) -> PathBuf {
|
||||
self.path.join("app_preferences.json")
|
||||
}
|
||||
|
||||
pub fn write_app_preferences(&self, prefs: &AppPreferences) -> AnyhowResult<()> {
|
||||
let serializable = prefs.to_serializable();
|
||||
|
||||
let formatter = serde_json::ser::PrettyFormatter::with_indent(b" ");
|
||||
let mut buf = Vec::new();
|
||||
let mut ser = serde_json::Serializer::with_formatter(&mut buf, formatter);
|
||||
serializable.serialize(&mut ser)?;
|
||||
|
||||
let filename = self.get_app_preferences_path();
|
||||
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(filename)?;
|
||||
|
||||
file.write_all(&buf)?;
|
||||
file.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::state::data_dir::trait_data_subdir::DataSubdir;
|
||||
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -2,6 +2,7 @@ pub mod app_assets_dir;
|
||||
pub mod app_credentials_dir;
|
||||
pub mod app_data_root;
|
||||
pub mod app_downloads_dir;
|
||||
pub mod app_settings_dir;
|
||||
pub mod app_weights_dir;
|
||||
pub mod temporary_dir;
|
||||
pub mod trait_data_subdir;
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::state::data_dir::trait_data_subdir::DataSubdir;
|
||||
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tempdir::TempDir;
|
||||
use tempfile::{Builder, NamedTempFile};
|
||||
@@ -35,4 +35,17 @@ impl TemporaryDir {
|
||||
.tempfile_in(&self.path)?;
|
||||
Ok(tempfile)
|
||||
}
|
||||
|
||||
pub fn new_named_temp_file_with_extension(&self, extension: &str) -> Result<NamedTempFile, std::io::Error> {
|
||||
let extension = if extension.starts_with(".") {
|
||||
extension.to_string()
|
||||
} else {
|
||||
format!(".{}", extension)
|
||||
};
|
||||
let tempfile = Builder::new()
|
||||
.prefix("temp_")
|
||||
.suffix(&extension)
|
||||
.tempfile_in(&self.path)?;
|
||||
Ok(tempfile)
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ pub fn home_dir() -> io::Result<PathBuf> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::state::expanduser::expanduser;
|
||||
use crate::core::state::expanduser::expanduser;
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::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::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::utils::best_window_size_heuristic::best_window_size_heuristic;
|
||||
use crate::core::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 = window.inner_size()?;
|
||||
let size = best_window_size_heuristic(window)?;
|
||||
Ok(Self {
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
@@ -0,0 +1,7 @@
|
||||
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;
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::anyhow;
|
||||
use discord_rich_presence::{DiscordIpc, DiscordIpcClient};
|
||||
use errors::AnyhowResult;
|
||||
use log::info;
|
||||
use log::{debug, info};
|
||||
|
||||
/// Our Discord App ID. Not sure if this is a secret.
|
||||
const DISCORD_APP_ID : &str = "1366596912593113138";
|
||||
@@ -15,13 +15,13 @@ pub async fn discord_presence_thread() -> ! {
|
||||
}
|
||||
|
||||
async fn discord_main_loop(mut client: DiscordIpcClient) -> AnyhowResult<()> {
|
||||
info!("Connecting Discord IPC client");
|
||||
info!("(Re)-connecting Discord IPC client...");
|
||||
|
||||
client.connect()
|
||||
.map_err(|err| anyhow!("Error connecting to Discord IPC: {:?}", err))?;
|
||||
|
||||
loop {
|
||||
info!("Notifying discord...");
|
||||
debug!("Notifying discord presence API...");
|
||||
|
||||
let assets = discord_rich_presence::activity::Assets::new()
|
||||
.large_image("https://storyteller.ai/android-chrome-512x512.png")
|
||||
@@ -37,6 +37,6 @@ async fn discord_main_loop(mut client: DiscordIpcClient) -> AnyhowResult<()> {
|
||||
.assets(assets))
|
||||
.map_err(|err| anyhow!("Error setting Discord activity: {:?}", err))?;
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_millis(30_000)).await;
|
||||
tokio::time::sleep(std::time::Duration::from_millis(60_000)).await;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
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_resize_task::persist_window_resize_task;
|
||||
use crate::core::state::app_startup_time::AppStartupTime;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::main_window_position::MainWindowPosition;
|
||||
use crate::core::state::main_window_size::MainWindowSize;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use crate::core::threads::main_window_thread::persist_storyteller_cookies_task::persist_storyteller_cookies_task;
|
||||
use crate::core::threads::main_window_thread::persist_window_position_task::persist_window_position_task;
|
||||
use crate::core::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";
|
||||
|
||||
@@ -18,6 +20,7 @@ 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 {
|
||||
@@ -29,6 +32,7 @@ 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 {
|
||||
@@ -46,10 +50,12 @@ 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,3 +1,4 @@
|
||||
pub mod main_window_thread;
|
||||
pub mod persist_storyteller_cookies_task;
|
||||
pub mod persist_window_position_task;
|
||||
pub mod persist_window_resize_task;
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::state::app_startup_time::AppStartupTime;
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::state::storyteller::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use crate::threads::sora_session_login_thread::LOGIN_WINDOW_NAME;
|
||||
use crate::core::state::app_startup_time::AppStartupTime;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use anyhow::anyhow;
|
||||
use chrono::TimeDelta;
|
||||
use errors::AnyhowResult;
|
||||
@@ -0,0 +1,34 @@
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::main_window_position::MainWindowPosition;
|
||||
use crate::core::state::main_window_size::MainWindowSize;
|
||||
use crate::core::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(¤t_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(¤t_window_position)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::state::main_window_size::MainWindowSize;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::main_window_size::MainWindowSize;
|
||||
use crate::core::utils::best_window_size_heuristic::best_window_size_heuristic;
|
||||
use errors::AnyhowResult;
|
||||
use log::info;
|
||||
use memory_store::clone_slot::CloneSlot;
|
||||
@@ -12,7 +13,7 @@ pub async fn persist_window_resize_task(
|
||||
) -> AnyhowResult<()> {
|
||||
|
||||
let current_window_size = MainWindowSize::from_window(window)?;
|
||||
let current_physical_size = window.inner_size()?;
|
||||
let current_physical_size = best_window_size_heuristic(window)?;
|
||||
|
||||
let mut save_size_to_disk = true;
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod discord_presence_thread;
|
||||
pub mod main_window_thread;
|
||||
@@ -0,0 +1,25 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||