Compare commits

..
Author SHA1 Message Date
Michael Chung e4bbafeae6 Used Aspect Ratio to fix.
Prompt box handles both web and desktop.
2025-04-21 16:58:02 -04:00
1712 changed files with 23636 additions and 48323 deletions
-63
View File
@@ -1,63 +0,0 @@
name: 'publish'
on:
push:
branches:
- tauri-prod-release
# This workflow will trigger on each push to the `release` branch to create or update a GitHub release, build your app, and upload the artifacts to the release.
jobs:
publish-tauri:
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
- platform: 'macos-latest' # for Arm based macs (M1 and above).
args: '--target aarch64-apple-darwin'
# - platform: 'macos-latest' # for Intel based macs.
# args: '--target x86_64-apple-darwin'
# - platform: 'ubuntu-22.04' # for Tauri v1 you could replace this with ubuntu-20.04.
# args: ''
# - platform: 'windows-latest'
# args: ''
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- name: setup node
uses: actions/setup-node@v4
with:
node-version: lts/*
- name: install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
# Those targets are only used on macos runners so it's in an `if` to slightly speed up windows and linux builds.
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- name: install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-22.04' # This must match the platform value defined above.
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
# webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2.
# 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/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/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.'
releaseDraft: true
prerelease: false
args: ${{ matrix.args }}
-13
View File
@@ -1,13 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<codeStyleSettings language="Rust">
<option name="RIGHT_MARGIN" value="180" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="ALIGN_MULTILINE_PARAMETERS_IN_CALLS" value="false" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>
-5
View File
@@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>
@@ -1,12 +0,0 @@
{
"db_name": "MySQL",
"query": "\nUPDATE generic_inference_jobs\nSET\n status = \"complete_success\",\n on_success_result_entity_type = ?,\n on_success_result_entity_token = ?,\n failure_reason = NULL,\n internal_debugging_failure_reason = NULL,\n success_execution_millis = ?,\n success_inference_execution_millis = ?,\n retry_at = NULL,\n successfully_completed_at = NOW()\nWHERE token = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 5
},
"nullable": []
},
"hash": "2bd7a5b886c9624ae48533ddac409cfca0668f30ab1f7016c412876b5904168b"
}
Generated
+322 -1348
View File
File diff suppressed because it is too large Load Diff
+9 -37
View File
@@ -24,24 +24,20 @@ members = [
"crates/cli/production/migration-tool",
# Desktop apps
"crates/desktop/artcraft",
"crates/desktop/tauri-artcraft",
"crates/desktop/tauri-realtime",
# 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",
"crates/lib/concurrency",
"crates/lib/cookies",
@@ -57,7 +53,6 @@ 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",
@@ -67,7 +62,6 @@ 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",
@@ -122,7 +116,6 @@ members = [
"crates/vendor/candle/candle-transformers",
"crates/vendor/candle/tensor-tools",
"crates/vendor/limitation",
"crates/vendor/tauri-plugin-http",
# NB(bt,2023-12-12): Newrelic keeps breaking with upgrades. Kill for now.
#"crates/vendor/newrelic-telemetry-sdk-rust",
@@ -153,25 +146,18 @@ publish = false
# - tauri-runtime-wry
# - tauri-utils
#
# Broken on Windows:
# - core-foundation
# - core-foundation-sys
#
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.98"
anyhow = "1.0.97"
base64 = "0.22.1"
chrono = { version = "0.4.41", features = ["serde"] }
cookie = "0.18.1"
futures = { version = "0.3.31" }
chrono = { version = "0.4.40", features = ["serde"] }
hex-literal = "1.0.0"
indexmap = "2.9.0"
log = "0.4.27"
once_cell = "1.21.3"
rand = "0.9.1"
rand = "0.9.0"
regex = "1.11.1"
reqwest = { version = "0.12.15", features = ["json", "cookies"] }
serde = { version = "1.0.219", features = ["derive"] }
@@ -179,29 +165,15 @@ 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", "devtools"] } # NB: "devtools" prevents App Store distribution
thiserror = "2.0.12"
tokio = { version = "1.45.0", features = ["macros"] }
toml = "0.8.22"
url = { version = "2.5.4", features = ["serde"] }
thiserror = "2"
toml = "0.7.4"
url = "2.5.4"
# 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" }
shared_service_components = { path = "crates/lib/shared_service_components" }
openai_sora_client = { path = "crates/lib/api_clients/openai_sora_client" }
primitives = { path = "crates/lib/data/primitives" }
shared_service_components = { path = "crates/lib/shared_service_components" }
storyteller_client = { path = "crates/lib/api_clients/storyteller_client" }
subprocess_common = { path = "crates/lib/subprocess_common" }
tokens = { path = "crates/_types/tokens" }
@@ -28,12 +28,11 @@ pub enum MediaFileOriginCategory {
DeviceApi,
/// From Storyteller Studio Engine
#[deprecated(note="This db field should only denote file provenance, not the product.")]
#[serde(rename = "studio")]
StorytellerStudio,
/// From Storyteller Studio Engine
#[deprecated(note = "DO NOT USE. Originally deprecated in favor of `StorytellerStudio`.")]
#[deprecated(note = "Use StorytellerStudio instead")]
StoryEngine,
}
@@ -39,10 +39,6 @@ pub enum MediaFileOriginProductCategory {
#[serde(rename = "vst")]
VideoStyleTransfer,
/// Image Studio
#[serde(rename = "image_studio")]
ImageStudio,
/// Storyteller Studio
#[serde(rename = "studio")]
StorytellerStudio,
@@ -91,7 +87,6 @@ impl MediaFileOriginProductCategory {
Self::FaceFusion => "face_fusion",
Self::FaceMirror => "face_mirror",
Self::VideoStyleTransfer => "vst",
Self::ImageStudio => "image_studio",
Self::StorytellerStudio => "studio",
Self::TextToSpeech => "tts",
Self::VoiceConversion => "voice_conversion",
@@ -110,7 +105,6 @@ impl MediaFileOriginProductCategory {
"face_fusion" => Ok(Self::FaceFusion),
"face_mirror" => Ok(Self::FaceMirror),
"vst" => Ok(Self::VideoStyleTransfer),
"image_studio" => Ok(Self::ImageStudio),
"studio" => Ok(Self::StorytellerStudio),
"tts" => Ok(Self::TextToSpeech),
"voice_conversion" => Ok(Self::VoiceConversion),
@@ -132,7 +126,6 @@ impl MediaFileOriginProductCategory {
Self::FaceFusion,
Self::FaceMirror,
Self::VideoStyleTransfer,
Self::ImageStudio,
Self::StorytellerStudio,
Self::TextToSpeech,
Self::VoiceConversion,
@@ -160,7 +153,6 @@ mod tests {
assert_serialization(MediaFileOriginProductCategory::FaceFusion, "face_fusion");
assert_serialization(MediaFileOriginProductCategory::FaceMirror, "face_mirror");
assert_serialization(MediaFileOriginProductCategory::VideoStyleTransfer, "vst");
assert_serialization(MediaFileOriginProductCategory::ImageStudio, "image_studio");
assert_serialization(MediaFileOriginProductCategory::StorytellerStudio, "studio");
assert_serialization(MediaFileOriginProductCategory::TextToSpeech, "tts");
assert_serialization(MediaFileOriginProductCategory::VoiceConversion, "voice_conversion");
@@ -178,7 +170,6 @@ mod tests {
assert_eq!(MediaFileOriginProductCategory::FaceFusion.to_str(), "face_fusion");
assert_eq!(MediaFileOriginProductCategory::FaceMirror.to_str(), "face_mirror");
assert_eq!(MediaFileOriginProductCategory::VideoStyleTransfer.to_str(), "vst");
assert_eq!(MediaFileOriginProductCategory::ImageStudio.to_str(), "image_studio");
assert_eq!(MediaFileOriginProductCategory::StorytellerStudio.to_str(), "studio");
assert_eq!(MediaFileOriginProductCategory::TextToSpeech.to_str(), "tts");
assert_eq!(MediaFileOriginProductCategory::VoiceConversion.to_str(), "voice_conversion");
@@ -196,7 +187,6 @@ mod tests {
assert_eq!(MediaFileOriginProductCategory::from_str("face_fusion").unwrap(), MediaFileOriginProductCategory::FaceFusion);
assert_eq!(MediaFileOriginProductCategory::from_str("face_mirror").unwrap(), MediaFileOriginProductCategory::FaceMirror);
assert_eq!(MediaFileOriginProductCategory::from_str("vst").unwrap(), MediaFileOriginProductCategory::VideoStyleTransfer);
assert_eq!(MediaFileOriginProductCategory::from_str("image_studio").unwrap(), MediaFileOriginProductCategory::ImageStudio);
assert_eq!(MediaFileOriginProductCategory::from_str("studio").unwrap(), MediaFileOriginProductCategory::StorytellerStudio);
assert_eq!(MediaFileOriginProductCategory::from_str("tts").unwrap(), MediaFileOriginProductCategory::TextToSpeech);
assert_eq!(MediaFileOriginProductCategory::from_str("voice_conversion").unwrap(), MediaFileOriginProductCategory::VoiceConversion);
@@ -210,13 +200,12 @@ mod tests {
#[test]
fn all_variants() {
let mut variants = MediaFileOriginProductCategory::all_variants();
assert_eq!(variants.len(), 14);
assert_eq!(variants.len(), 13);
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::Unknown));
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::FaceAnimator));
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::FaceFusion));
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::FaceMirror));
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::VideoStyleTransfer));
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::ImageStudio));
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::StorytellerStudio));
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::TextToSpeech));
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::VoiceConversion));
@@ -1,24 +0,0 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "enables the default permissions",
"windows": [
"main"
],
"permissions": [
"core:default",
"log:default",
"upload:allow-download",
"upload:default",
"http:allow-fetch",
{
"identifier": "http:default",
"allow": [
{
"url": "https://*"
}
]
},
"dialog:default"
]
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1023 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

@@ -1,70 +0,0 @@
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,
})
}
@@ -1,2 +0,0 @@
pub mod get_app_preferences_command;
pub mod update_app_preference_command;
@@ -1,122 +0,0 @@
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)),
}
}
@@ -1,132 +0,0 @@
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"}"#);
}
}
@@ -1,35 +0,0 @@
use crate::core::state::os_platform::OsPlatform;
use base64::prelude::BASE64_STANDARD;
use base64::Engine;
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
use image::ImageReader;
use log::info;
use serde_derive::Serialize;
use std::io::Cursor;
use chrono::{DateTime, Utc};
#[derive(Debug, Serialize)]
pub struct BuildInfoResponse {
pub build_timestamp: DateTime<Utc>,
pub git_commit_id: Option<String>,
pub git_commit_short_id: Option<String>,
pub git_commit_short_timestamp: Option<DateTime<Utc>>,
}
#[tauri::command]
pub fn get_build_info_command() -> Result<BuildInfoResponse, String> {
info!("get_build_info_command called...");
let build_timestamp = build_metadata::build_timestamp();
let git_commit_id = build_metadata::git_commit_id();
let git_commit_short_id = build_metadata::git_commit_short_id();
let git_commit_short_timestamp = build_metadata::git_commit_timestamp();
Ok(BuildInfoResponse {
build_timestamp,
git_commit_id: git_commit_id.map(|s| s.to_string()),
git_commit_short_id: git_commit_short_id.map(|s| s.to_string()),
git_commit_short_timestamp,
})
}
@@ -1,5 +0,0 @@
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,63 +0,0 @@
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;
#[derive(Debug, Serialize)]
pub struct PlatformInfoResponse {
pub os_platform: DetectedOs,
pub webview_runtime: WebviewRuntime,
}
#[derive(Copy, Clone, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DetectedOs {
Windows,
#[serde(rename = "macos")]
MacOS,
Linux,
Unknown,
}
#[derive(Copy, Clone, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum WebviewRuntime {
#[serde(rename = "webkit_safari")]
WebkitSafari,
#[serde(rename = "webkit_gtk")]
WebkitGtk,
#[serde(rename = "edge_webview_2")]
EdgeWebview2,
Unknown,
}
#[tauri::command]
pub fn platform_info_command() -> Result<PlatformInfoResponse, String> {
info!("platform_info_command called...");
let os_platform = match OsPlatform::maybe_get() {
Some(OsPlatform::Linux) => DetectedOs::Linux,
Some(OsPlatform::MacOs) => DetectedOs::MacOS,
Some(OsPlatform::Windows) => DetectedOs::Windows,
None => DetectedOs::Unknown,
};
// These are the webviews that Tauri uses on each OS:
// https://tauri.app/reference/webview-versions/
let webview_runtime = match os_platform {
DetectedOs::Windows => WebviewRuntime::EdgeWebview2,
DetectedOs::MacOS => WebviewRuntime::WebkitSafari,
DetectedOs::Linux => WebviewRuntime::WebkitGtk,
DetectedOs::Unknown => WebviewRuntime::Unknown,
};
Ok(PlatformInfoResponse {
os_platform,
webview_runtime,
})
}
@@ -1,3 +0,0 @@
pub mod notification_event;
pub mod sendable_event_error;
pub mod sendable_event_trait;
@@ -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::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(())
}
}
-6
View File
@@ -1,6 +0,0 @@
pub mod commands;
pub mod events;
pub mod state;
pub mod threads;
pub mod utils;
pub mod windows;
@@ -1,71 +0,0 @@
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)
}
}
@@ -1,10 +0,0 @@
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)
}
@@ -1,71 +0,0 @@
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
}
}
@@ -1,4 +0,0 @@
pub mod app_preferences;
pub mod app_preferences_manager;
pub mod app_preferences_serializable;
pub mod preferred_download_directory;
@@ -1,82 +0,0 @@
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,22 +0,0 @@
use chrono::{DateTime, TimeDelta, Utc};
pub struct AppStartupTime {
startup_time: DateTime<Utc>,
}
impl AppStartupTime {
pub fn new() -> Self {
Self {
startup_time: Utc::now(),
}
}
pub fn get_startup_time(&self) -> DateTime<Utc> {
self.startup_time
}
pub fn time_delta_since(&self) -> TimeDelta {
let now = Utc::now();
now.signed_duration_since(self.startup_time)
}
}
@@ -1,36 +0,0 @@
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use chrono::{DateTime, Local};
use std::path::{Path, PathBuf};
#[derive(Clone)]
pub struct AppAssetsDir {
path: PathBuf,
}
impl DataSubdir for AppAssetsDir {
const DIRECTORY_NAME: &'static str = "assets";
fn new_from<P: AsRef<Path>>(dir: P) -> Self {
Self {
path: dir.as_ref().to_path_buf(),
}
}
fn path(&self) -> &Path {
&self.path
}
}
impl AppAssetsDir {
/// Return the current date asset directory
/// If it doesn't exist, create it.
pub fn make_or_get_current_date_dir(&self) -> anyhow::Result<PathBuf> {
let format: DateTime<Local> = Local::now();
let date_directory_name = format.format("%Y-%d-%m").to_string();
let full_path = self.path.join(date_directory_name);
if !full_path.exists() {
std::fs::create_dir(&full_path)?;
}
Ok(full_path)
}
}
@@ -1,47 +0,0 @@
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use std::path::{Path, PathBuf};
#[derive(Clone)]
pub struct AppCredentialsDir {
path: PathBuf,
}
impl DataSubdir for AppCredentialsDir {
const DIRECTORY_NAME: &'static str = "credentials";
fn new_from<P: AsRef<Path>> (dir: P) -> Self {
Self {
path: dir.as_ref().to_path_buf(),
}
}
fn path(&self) -> &Path {
&self.path
}
}
impl AppCredentialsDir {
pub fn get_sora_cookie_file_path(&self) -> PathBuf {
self.path.join("sora_cookies.txt")
}
pub fn get_sora_bearer_token_file_path(&self) -> PathBuf {
self.path.join("sora_bearer_token.txt")
}
pub fn get_sora_sentinel_file_path(&self) -> PathBuf {
self.path.join("sora_sentinel.txt")
}
pub fn get_storyteller_avt_cookie_file_path(&self) -> PathBuf {
self.path.join("artcraft_avt.txt")
}
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,151 +0,0 @@
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";
/// The path to the application data directory, which includes "asset" and "weights" data.
#[derive(Clone)]
pub struct AppDataRoot {
path: PathBuf,
assets_dir: AppAssetsDir,
credentials_dir: AppCredentialsDir,
downloads_dir: AppDownloadsDir,
settings_dir: AppSettingsDir,
weights_dir: AppWeightsDir,
temp_dir: TemporaryDir,
log_file_name: PathBuf,
log_file_name_string: String,
}
impl AppDataRoot {
pub fn create_default() -> anyhow::Result<Self> {
let directory = get_default_data_dir()?;
Self::create_existing(directory)
}
pub fn create_existing<P: AsRef<Path>>(dir: P) -> anyhow::Result<Self> {
let mut dir = dir.as_ref().to_path_buf();
match OsPlatform::get() {
OsPlatform::Linux | OsPlatform::MacOs => {
if let Some(d) = dir.as_os_str().to_str() {
dir = expanduser(d)?;
}
},
OsPlatform::Windows => {}
}
if !dir.is_dir() {
println!("Creating directory {:?}", dir);
std::fs::create_dir_all(&dir)?;
}
match dir.canonicalize() {
Ok(d) => dir = d,
Err(err) => {
println!("Error canonicalizing {:?}: {}", dir, err);
}
}
let assets_dir = AppAssetsDir::get_or_create_in_root_dir(&dir)?;
let credentials_dir = AppCredentialsDir::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()
.ok_or(anyhow!("couldn't convert log path to str"))?
.to_string();
Ok(Self {
path: dir,
assets_dir,
credentials_dir,
downloads_dir,
settings_dir,
weights_dir,
temp_dir,
log_file_name,
log_file_name_string,
})
}
pub fn assets_dir(&self) -> &AppAssetsDir {
&self.assets_dir
}
pub fn credentials_dir(&self) -> &AppCredentialsDir {
&self.credentials_dir
}
pub fn downloads_dir(&self) -> &AppDownloadsDir {
&self.downloads_dir
}
pub fn settings_dir(&self) -> &AppSettingsDir {
&self.settings_dir
}
pub fn weights_dir(&self) -> &AppWeightsDir {
&self.weights_dir
}
pub fn temp_dir(&self) -> &TemporaryDir {
&self.temp_dir
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn log_file_name(&self) -> &Path {
&self.log_file_name
}
pub fn log_file_name_str(&self) -> &str {
&self.log_file_name_string
}
pub fn get_sora_cookie_file_path(&self) -> PathBuf {
self.credentials_dir().get_sora_cookie_file_path()
}
pub fn get_sora_bearer_token_file_path(&self) -> PathBuf {
self.credentials_dir().get_sora_bearer_token_file_path()
}
pub fn get_sora_sentinel_file_path(&self) -> PathBuf {
self.credentials_dir().get_sora_sentinel_file_path()
}
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
fn get_default_data_dir() -> anyhow::Result<PathBuf> {
Ok(UserDirs::new()
.ok_or_else(|| anyhow!("could not determine user home directory"))?
.home_dir()
.join(DEFAULT_DATA_DIR))
}
@@ -1,21 +0,0 @@
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use std::path::{Path, PathBuf};
#[derive(Clone)]
pub struct AppDownloadsDir {
path: PathBuf,
}
impl DataSubdir for AppDownloadsDir {
const DIRECTORY_NAME: &'static str = "downloads";
fn new_from<P: AsRef<Path>>(dir: P) -> Self {
Self {
path: dir.as_ref().to_path_buf(),
}
}
fn path(&self) -> &Path {
&self.path
}
}
@@ -1,56 +0,0 @@
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,21 +0,0 @@
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use std::path::{Path, PathBuf};
#[derive(Clone)]
pub struct AppWeightsDir {
path: PathBuf,
}
impl DataSubdir for AppWeightsDir {
const DIRECTORY_NAME: &'static str = "weights";
fn new_from<P: AsRef<Path>>(dir: P) -> Self {
Self {
path: dir.as_ref().to_path_buf(),
}
}
fn path(&self) -> &Path {
&self.path
}
}
@@ -1,8 +0,0 @@
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,51 +0,0 @@
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use std::path::{Path, PathBuf};
use tempdir::TempDir;
use tempfile::{Builder, NamedTempFile};
#[derive(Clone)]
pub struct TemporaryDir {
path: PathBuf,
}
impl DataSubdir for TemporaryDir {
const DIRECTORY_NAME: &'static str = "temp";
fn new_from<P: AsRef<Path>>(dir: P) -> Self {
Self {
path: dir.as_ref().to_path_buf(),
}
}
fn path(&self) -> &Path {
&self.path
}
}
impl TemporaryDir {
pub fn with_prefix(&self, prefix: &str) -> anyhow::Result<TempDir> {
let tempdir = TempDir::new_in(&self.path, prefix)?;
Ok(tempdir)
}
pub fn new_named_temp_file(&self) -> anyhow::Result<NamedTempFile> {
let tempfile = Builder::new()
.prefix("temp_")
.suffix(".bin")
.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)
}
}
@@ -1,31 +0,0 @@
use std::path::Path;
pub trait DataSubdir : Sized {
const DIRECTORY_NAME: &'static str;
fn new_from<P: AsRef<Path>>(dir: P) -> Self;
fn path(&self) -> &Path;
fn get_or_create_in_root_dir<P: AsRef<Path>>(root_dir: P) -> anyhow::Result<Self> {
Self::get_or_create_dir(root_dir.as_ref().join(Self::DIRECTORY_NAME))
}
fn get_or_create_dir<P: AsRef<Path>>(dir: P) -> anyhow::Result<Self> {
let mut dir = dir.as_ref().to_path_buf();
match dir.canonicalize() {
Ok(d) => dir = d,
Err(err) => {
println!("Error canonicalizing {:?}: {}", dir, err);
}
}
if !dir.exists() {
println!("Creating directory {:?}", dir);
std::fs::create_dir(&dir)?;
}
Ok(Self::new_from(dir))
}
}
@@ -1,73 +0,0 @@
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,74 +0,0 @@
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};
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
pub struct MainWindowSize {
pub width: u32,
pub height: u32,
}
impl MainWindowSize {
pub fn new(width: u32, height: u32) -> Self {
Self { width, height }
}
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 size = best_window_size_heuristic(window)?;
Ok(Self {
width: size.width,
height: size.height,
})
}
pub fn from_filesystem_configs(app_data_root: &AppDataRoot) -> AnyhowResult<Option<Self>> {
let filename = app_data_root.get_window_size_config_file();
if !filename.exists() {
return Ok(None);
}
let contents = std::fs::read_to_string(filename)?;
let size: MainWindowSize = serde_json::from_str(&contents)?;
Ok(Some(size))
}
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_size(PhysicalSize::new(self.width, self.height))?;
Ok(())
}
pub fn persist_to_filesystem(&self, app_data_root: &AppDataRoot) -> AnyhowResult<()> {
let filename = app_data_root.get_window_size_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_size(&self) -> PhysicalSize<u32> {
PhysicalSize::new(self.width, self.height)
}
pub fn matches_physical_size(&self, size: &PhysicalSize<u32>) -> bool {
self.width == size.width && self.height == size.height
}
}
@@ -1,7 +0,0 @@
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,24 +0,0 @@
#[derive(Copy,Clone,Eq,PartialEq,Debug)]
pub enum OsPlatform {
Linux,
MacOs,
Windows,
}
impl OsPlatform {
pub fn maybe_get() -> Option<Self> {
// Totally scientific.
// https://doc.rust-lang.org/std/env/consts/constant.OS.html
match std::env::consts::OS {
"linux" => Some(Self::Linux),
"windows" => Some(Self::Windows),
"macos" | "apple" => Some(Self::MacOs),
_ => None,
}
}
pub fn get() -> Self {
Self::maybe_get().unwrap_or(Self::Linux)
}
}
@@ -1,42 +0,0 @@
use anyhow::anyhow;
use discord_rich_presence::{DiscordIpc, DiscordIpcClient};
use errors::AnyhowResult;
use log::{debug, info};
/// Our Discord App ID. Not sure if this is a secret.
const DISCORD_APP_ID : &str = "1366596912593113138";
pub async fn discord_presence_thread() -> ! {
loop {
let client = DiscordIpcClient::new(DISCORD_APP_ID);
let _err = discord_main_loop(client).await;
tokio::time::sleep(std::time::Duration::from_millis(10_000)).await;
}
}
async fn discord_main_loop(mut client: DiscordIpcClient) -> AnyhowResult<()> {
info!("(Re)-connecting Discord IPC client...");
client.connect()
.map_err(|err| anyhow!("Error connecting to Discord IPC: {:?}", err))?;
loop {
debug!("Notifying discord presence API...");
let assets = discord_rich_presence::activity::Assets::new()
.large_image("https://storyteller.ai/android-chrome-512x512.png")
.large_text("https://GetArtCraft.com");
//.small_image("https://storyteller.ai/android-chrome-192x192.png")
//.small_text("small text");
client.set_activity(discord_rich_presence::activity::Activity::new()
//.state("Anyone can make arts!")
.state("GetArtCraft.com")
.details("Making Arts with ArtCraft")
.activity_type(discord_rich_presence::activity::ActivityType::Playing)
.assets(assets))
.map_err(|err| anyhow!("Error setting Discord activity: {:?}", err))?;
tokio::time::sleep(std::time::Duration::from_millis(60_000)).await;
}
}
@@ -1,68 +0,0 @@
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};
const MAIN_WINDOW_NAME : &str = "main";
pub async fn main_window_thread(
app: AppHandle,
app_data_root: AppDataRoot,
storyteller_creds_manager: StorytellerCredentialManager,
) -> ! {
// 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 {
for (window_name, window) in app.windows() {
if window_name == MAIN_WINDOW_NAME {
let result = handle_main_window(
&app,
&window,
&app_data_root,
&storyteller_creds_manager,
&window_size_slot,
&window_pos_slot,
&app_startup_time,
).await;
if let Err(err) = result {
error!("Error handling main window: {:?}", err);
}
}
}
tokio::time::sleep(std::time::Duration::from_millis(1_000)).await;
}
}
pub async fn handle_main_window(
app: &AppHandle,
window: &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;
}
}
pub fn log_errors<T>(result: AnyhowResult<T>) {
if let Err(err) = result {
error!("Error persisting window size: {:?}", err);
}
}
@@ -1,4 +0,0 @@
pub mod main_window_thread;
pub mod persist_storyteller_cookies_task;
pub mod persist_window_position_task;
pub mod persist_window_resize_task;
@@ -1,113 +0,0 @@
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;
use log::{debug, error, info};
use once_cell::sync::Lazy;
use reqwest::Url;
use std::fs;
use std::sync::LockResult;
use storyteller_client::credentials::storyteller_avt_cookie::StorytellerAvtCookie;
use storyteller_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
use storyteller_client::credentials::storyteller_session_cookie::StorytellerSessionCookie;
use tauri::{AppHandle, Manager, Webview, Window};
use tauri_plugin_http::reqwest_cookie_store::CookieStore;
use tauri_plugin_http::Http;
const MAIN_WEBVIEW_NAME: &str = "main";
const STORYTELLER_ROOT_COOKIE_URL_STR: &str = "https://api.storyteller.ai/";
static STORYTELLER_ROOT_COOKIE_URL: Lazy<Url> = Lazy::new(|| {
Url::parse(STORYTELLER_ROOT_COOKIE_URL_STR).expect("URL should parse")
});
const AVT_COOKIE_NAME : &str = "visitor";
const SESSION_COOKIE_NAME : &str = "session";
/// There's some kind of race condition that we need to wait for before inspecting cookies.
const RACE_CONDITION_WAIT_TIME: TimeDelta = TimeDelta::milliseconds(5000);
pub async fn persist_storyteller_cookies_task(
app: &AppHandle,
storyteller_credential_manager: &StorytellerCredentialManager,
app_startup_time: &AppStartupTime,
) -> AnyhowResult<()> {
//if app_startup_time.time_delta_since() < RACE_CONDITION_WAIT_TIME {
// // NB: There's an issue when the "main window thread" inquires about the
// // webview cookies shortly after app startup. When it attempts to dump the
// // cookies within a few milliseconds of app launch, it deadlocks and the
// // app never launches correctly. The entire webview goes blank and the app
// // freezes. This hack seems to fix the problem.
// return Ok(())
//}
let maybe_http = app.try_state::<Http>();
let http = match maybe_http {
None => {
error!("No HTTP plugin found");
return Err(anyhow!("No HTTP plugin found"));
}
Some(http) => http,
};
match http.cookies_jar.store.lock() {
Err(err) => {
error!("Failed to lock cookie jar: {:?}", err);
return Err(anyhow!("Failed to lock cookie jar"));
}
Ok(cookie_jar) => {
sync_tauri_credentials(&cookie_jar, storyteller_credential_manager)?;
}
}
Ok(())
}
fn sync_tauri_credentials(
cookie_store: &CookieStore,
storyteller_credential_manager: &StorytellerCredentialManager,
) -> AnyhowResult<()> {
let current_http_plugin_credentials = get_credentials_from_cookie_store(cookie_store)?;
let mut replace_credentials = true;
let maybe_old_credentials = storyteller_credential_manager.get_credentials()?;
if let Some(old_credentials) = maybe_old_credentials {
if old_credentials.equals(&current_http_plugin_credentials) {
replace_credentials = false;
}
}
if replace_credentials {
info!("Syncing ArtCraft credentials ...");
storyteller_credential_manager.set_credentials(&current_http_plugin_credentials)?;
// NB: tauri-plugin-http stores the credentials on disk, so we can defer to that for now.
//storyteller_credential_manager.persist_all_to_disk()?;
}
Ok(())
}
pub fn get_credentials_from_cookie_store(cookie_jar: &CookieStore) -> AnyhowResult<StorytellerCredentialSet> {
let mut avt_cookie = None;
let mut session_cookie = None;
for cookie in cookie_jar.iter_unexpired() {
if let Some(avt) = StorytellerAvtCookie::maybe_from_cookie(&cookie) {
avt_cookie = Some(avt);
} else if let Some(session) = StorytellerSessionCookie::maybe_from_cookie(&cookie) {
session_cookie = Some(session);
}
}
Ok(StorytellerCredentialSet::initialize(
avt_cookie,
session_cookie,
))
}
@@ -1,34 +0,0 @@
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(&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,33 +0,0 @@
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;
use tauri::Window;
pub async fn persist_window_resize_task(
window: &Window,
app_data_root: &AppDataRoot,
window_size_slot: &CloneSlot<MainWindowSize>,
) -> AnyhowResult<()> {
let current_window_size = MainWindowSize::from_window(window)?;
let current_physical_size = best_window_size_heuristic(window)?;
let mut save_size_to_disk = true;
if let Ok(Some(old_size)) = window_size_slot.get_clone() {
if old_size.matches_physical_size(&current_physical_size) {
save_size_to_disk = false;
}
}
if save_size_to_disk {
info!("Saving window size configs to disk...");
current_window_size.persist_to_filesystem(app_data_root)?;
window_size_slot.set_clone(&current_window_size)?;
}
Ok(())
}
@@ -1,2 +0,0 @@
pub mod discord_presence_thread;
pub mod main_window_thread;
@@ -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)
}
}
@@ -1,29 +0,0 @@
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::utils::get_url_file_extension::get_url_file_extension;
use crate::core::utils::simple_http_download_to_tempfile::simple_http_download_to_tempfile;
use errors::AnyhowResult;
use storyteller_client::media_files::get_media_file::get_media_file;
use storyteller_client::utils::api_host::ApiHost;
use tempfile::NamedTempFile;
use tokens::tokens::media_files::MediaFileToken;
// TODO: Better more concrete error handling
pub async fn download_media_file_to_temp_dir(app_data_root: &AppDataRoot, token: &MediaFileToken) -> AnyhowResult<NamedTempFile> {
let response = get_media_file(&ApiHost::Storyteller, token).await?;
let media_file_url = &response.media_file.media_links.cdn_url;
let extension_with_dot = get_url_file_extension(media_file_url)
.map(|ext| format!(".{}", ext))
.unwrap_or_else(|| ".png".to_string()); // TODO: Better default extension passed by caller as heuristic.
//let filename = format!("{}{}", response.media_file.token.as_str(), extension_with_dot);
//let filename = app_data_root.downloads_dir().path().join(&filename);
let mut file = app_data_root.temp_dir().new_named_temp_file_with_extension(&extension_with_dot)?;
simple_http_download_to_tempfile(&media_file_url, &mut file).await?;
Ok(file) // NB: Must return TempFile to not drop / delete it
}
@@ -1,20 +0,0 @@
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use errors::AnyhowResult;
use std::io::Write;
use tempfile::NamedTempFile;
use url::Url;
pub async fn download_url_to_temp_dir(url: &str, app_data_root: &AppDataRoot) -> AnyhowResult<NamedTempFile> {
let url = Url::parse(&url)?;
let response = reqwest::get(url.clone()).await?;
let response_bytes = response.bytes().await?;
let ext = url.path().split(".").last().unwrap_or("png");
let mut temp_file = app_data_root.temp_dir().new_named_temp_file_with_extension(ext)?;
temp_file.write_all(&response_bytes)?;
Ok(temp_file)
}
@@ -1,35 +0,0 @@
use std::path::Path;
use reqwest::Url;
pub fn get_url_file_extension(url: &Url) -> Option<&str> {
Path::new(url.path())
.extension()
.filter(|ext| !ext.is_empty())
.and_then(|ext| ext.to_str())
}
#[cfg(test)]
mod tests {
use reqwest::Url;
#[test]
fn extension_exists() {
let url = Url::parse("https://example.com/image.png").unwrap();
let extension = super::get_url_file_extension(&url);
assert_eq!(extension, Some("png"));
}
#[test]
fn extension_does_not_exist() {
let url = Url::parse("https://example.com/foo").unwrap();
let extension = super::get_url_file_extension(&url);
assert_eq!(extension, None);
}
#[test]
fn period_not_in_path() {
let url = Url::parse("https://example.com/invalid.period/foo").unwrap();
let extension = super::get_url_file_extension(&url);
assert_eq!(extension, None);
}
}

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