Compare commits

...
Author SHA1 Message Date
Brandon Thomas c365d65e75 write prefs to fs 2025-05-04 13:50:36 -04:00
Brandon Thomas 51f4e74954 preferences serialization and saving 2025-05-04 13:33:43 -04:00
Brandon Thomas a0abe8b9e5 get preferences endpoint 2025-05-04 13:07:06 -04:00
Brandon Thomas 54f83db986 settings struct 2025-05-04 02:11:28 -04:00
Brandon Thomas 7871338cf0 remove old settings from realtime app 2025-05-04 01:24:16 -04:00
Brandon Thomas 2285c9e32f settings dir 2025-05-04 01:12:29 -04:00
16 changed files with 407 additions and 126 deletions
@@ -0,0 +1,46 @@
use crate::state::app_preferences::app_preferences_manager::AppPreferencesManager;
use crate::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,
}
#[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,
})
}
@@ -0,0 +1,2 @@
pub mod get_app_preferences_command;
pub mod update_app_preference_command;
@@ -0,0 +1,70 @@
use crate::state::app_preferences::app_preferences_manager::AppPreferencesManager;
use crate::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
use crate::state::data_dir::app_data_root::AppDataRoot;
use errors::AnyhowResult;
use log::{error, info};
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: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PreferenceName {
PreferredDownloadDirectory,
PlaySounds,
}
#[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()?;
match request.preference {
PreferenceName::PreferredDownloadDirectory => {
prefs.preferred_download_directory = request.value.parse::<PreferredDownloadDirectory>()?;
}
PreferenceName::PlaySounds => {
prefs.play_sounds = request.value.parse::<bool>()?;
}
}
app_prefs.set_clone(&prefs)?;
app_data_root.settings_dir().write_app_preferences(&prefs)?;
Ok(())
}
@@ -1,3 +1,4 @@
pub mod app_preferences;
pub mod flip_image;
pub mod platform_info_command;
pub mod sora;
+12 -10
View File
@@ -11,17 +11,19 @@ use crate::commands::platform_info_command::platform_info_command;
use crate::commands::sora::open_sora_login_command::open_sora_login_command;
use crate::commands::sora::sora_image_generation_command::sora_image_generation_command;
use crate::commands::sora::sora_image_remix_command::sora_image_remix_command;
use crate::state::app_config::AppConfig;
use crate::commands::app_preferences::get_app_preferences_command::get_app_preferences_command;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::main_window_size::MainWindowSize;
use crate::state::sora::sora_credential_manager::SoraCredentialManager;
use crate::utils::webview_unsafe::webview_unsafe_for_app;
use crate::state::sora::sora_task_queue::SoraTaskQueue;
use crate::state::storyteller::storyteller_credential_manager::StorytellerCredentialManager;
use crate::threads::discord_presence_thread::discord_presence_thread;
use crate::threads::main_window_thread::main_window_thread::main_window_thread;
use crate::threads::sora_session_login_thread::sora_session_login_thread;
use crate::threads::sora_task_polling_thread::sora_task_polling_thread;
use crate::utils::webview_unsafe::webview_unsafe_for_app;
use crate::state::app_preferences::app_preferences_manager::load_app_preferences_or_default;
use tauri_plugin_http;
use tauri_plugin_log::Target;
use tauri_plugin_log::TargetKind;
@@ -30,14 +32,13 @@ use tauri_plugin_log::TargetKind;
pub fn run() {
// NB: Tauri wants to install the logger itself, so we can't rely on the logger crate
// until the tauri runtime begins.
println!("Loading model config...");
let config = AppConfig::init()
.expect("config should load");
let app_data_root = config.app_data_root.clone();
let app_data_root_2 = config.app_data_root.clone();
println!("Loading config...");
let app_data_root = AppDataRoot::create_default().expect("data directory should be created");
let app_data_root_2 = app_data_root.clone();
println!("Loading app preferences...");
let app_preferences = load_app_preferences_or_default(&app_data_root);
println!("Attempting to read existing artcraft credentials...");
let storyteller_creds_manager = StorytellerCredentialManager::initialize_from_disk_infallible(&app_data_root);
let storyteller_creds_manager_2 = storyteller_creds_manager.clone();
@@ -112,11 +113,12 @@ pub fn run() {
Ok(())
})
.manage(app_data_root)
.manage(config)
.manage(app_preferences)
.manage(sora_creds_manager)
.manage(sora_task_queue)
.invoke_handler(tauri::generate_handler![
flip_image,
get_app_preferences_command,
open_sora_login_command,
platform_info_command,
sora_image_generation_command,
@@ -1,58 +0,0 @@
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::os_platform::OsPlatform;
use crate::state::yaml_config::YamlConfig;
const DEFAULT_SD_IMAGE_WIDTH: usize = 1024;
const DEFAULT_SD_IMAGE_HEIGHT: usize = 1024;
/// A central place to configure app-wide details.
pub struct AppConfig {
pub image_height: usize,
pub image_width: usize,
pub scheduler_steps: usize,
pub scheduler_samples: usize,
pub seed: Option<u64>,
pub cfg_scale: Option<f64>,
/// Root location for application data.
pub app_data_root: AppDataRoot,
}
impl AppConfig {
pub fn init() -> anyhow::Result<Self> {
let yaml_configs = YamlConfig::load_from_config_file_recursive();
println!("Configs: {:?}", yaml_configs);
let os_platform = OsPlatform::get();
let default_data_root = match os_platform {
OsPlatform::Linux => yaml_configs.linux_default_data_path.as_deref(),
OsPlatform::MacOs => yaml_configs.mac_default_data_path.as_deref(),
OsPlatform::Windows => yaml_configs.windows_default_data_path.as_deref(),
};
println!("Possible user-defined data root: {:?}", default_data_root);
let app_data_root = match default_data_root {
None => AppDataRoot::create_default()?,
Some(path) => AppDataRoot::create_existing(path)?,
};
println!("App data root: {:?}", app_data_root.path());
Ok(Self {
image_height: yaml_configs.image_height.unwrap_or(DEFAULT_SD_IMAGE_HEIGHT),
image_width: yaml_configs.image_width.unwrap_or(DEFAULT_SD_IMAGE_WIDTH),
scheduler_steps: yaml_configs.scheduler_steps.unwrap_or(4),
scheduler_samples: yaml_configs.scheduler_samples.unwrap_or(15),
seed: yaml_configs.seed,
cfg_scale: yaml_configs.cfg_scale,
app_data_root,
})
}
}
@@ -0,0 +1,50 @@
use crate::state::app_preferences::app_preferences_serializable::AppPreferencesSerializable;
use crate::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
use crate::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,
}
impl Default for AppPreferences {
fn default() -> Self {
Self {
preferred_download_directory: PreferredDownloadDirectory::SystemDefault,
play_sounds: true,
}
}
}
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 {
preferred_download_directory: Some(self.preferred_download_directory.clone()),
play_sounds: Some(self.play_sounds),
}
}
}
@@ -0,0 +1,10 @@
use crate::state::app_preferences::app_preferences::AppPreferences;
use crate::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,48 @@
use crate::state::app_preferences::app_preferences::AppPreferences;
use crate::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
use crate::state::data_dir::app_data_root::AppDataRoot;
use errors::AnyhowResult;
use serde_derive::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppPreferencesSerializable {
/// 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>,
}
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 save_to_file(&self, app_data_root: &AppDataRoot) -> AnyhowResult<()> {
let filename = app_data_root.settings_dir().get_app_preferences_path();
let json = serde_json::to_string(self)?;
std::fs::write(filename, json)?;
Ok(())
}
pub fn 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
}
}
@@ -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,100 @@
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";
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum PreferredDownloadDirectory {
/// The system-default downloads directory, which varies by OS.
/// NB: Serializes as `"system_default"` in JSON.
SystemDefault,
/// A user-defined path
/// NB: Serializes as `{"custom": "/path/to/dir"}` in JSON.
Custom(PathBuf),
}
impl FromStr for PreferredDownloadDirectory {
type Err = AnyhowError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == SYSTEM_DEFAULT_SENTINEL_VALUE || s == "\"system_default\"" {
Ok(PreferredDownloadDirectory::SystemDefault)
} else {
// TODO: If it gets serialized as JSON, we may need to remove the `{"custom": ...` wrapping layer.
let path = PathBuf::from(s);
Ok(PreferredDownloadDirectory::Custom(path))
}
}
}
impl Display for PreferredDownloadDirectory {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
PreferredDownloadDirectory::SystemDefault => write!(f, "{}", SYSTEM_DEFAULT_SENTINEL_VALUE),
PreferredDownloadDirectory::Custom(path) => write!(f, "{}", path.to_string_lossy()),
}
}
}
#[cfg(test)]
mod tests {
use crate::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
use std::path::PathBuf;
use std::str::FromStr;
mod json {
use super::*;
#[test]
fn to_json_system_default() {
let val = PreferredDownloadDirectory::SystemDefault;
let val = serde_json::to_string(&val).unwrap();
assert_eq!(&val, "\"system_default\"");
}
#[test]
fn to_json_custom() {
let val = PreferredDownloadDirectory::Custom("/tmp".into());
let val = serde_json::to_string(&val).unwrap();
assert_eq!(&val, "{\"custom\":\"/tmp\"}");
}
}
mod string {
use super::*;
#[test]
fn to_string_system_default() {
let val = PreferredDownloadDirectory::SystemDefault;
let val = val.to_string();
assert_eq!(&val, "system_default");
}
#[test]
fn to_string_custom() {
let val = PreferredDownloadDirectory::Custom("/tmp/foo".into());
let val = val.to_string();
assert_eq!(&val, "/tmp/foo");
}
}
#[test]
fn from_string_system_default() {
let val = "system_default";
let val = PreferredDownloadDirectory::from_str(val).unwrap();
assert_eq!(val, PreferredDownloadDirectory::SystemDefault);
}
#[test]
fn from_string_custom() {
let val = "/tmp/foo";
let val = PreferredDownloadDirectory::from_str(val).unwrap();
assert_eq!(val, PreferredDownloadDirectory::Custom(PathBuf::from("/tmp/foo")));
}
}
@@ -1,6 +1,7 @@
use crate::state::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_settings_dir::AppSettingsDir;
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;
@@ -12,7 +13,6 @@ 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
}
@@ -0,0 +1,51 @@
use crate::state::app_preferences::app_preferences::AppPreferences;
use crate::state::data_dir::trait_data_subdir::DataSubdir;
use errors::AnyhowResult;
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 serializable = serde_json::to_string(&serializable)?;
let filename = self.get_app_preferences_path();
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(filename)?;
file.write_all(serializable.as_bytes())?;
file.flush()?;
Ok(())
}
}
@@ -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;
+2 -3
View File
@@ -1,9 +1,8 @@
pub mod app_config;
pub mod app_preferences;
pub mod app_startup_time;
pub mod data_dir;
pub mod expanduser;
pub mod main_window_size;
pub mod os_platform;
pub mod sora;
pub mod storyteller;
pub mod yaml_config;
pub mod storyteller;
@@ -1,52 +0,0 @@
use filesys::recursively_find_file_by_name::recursively_find_file_by_name;
use std::fs::File;
use std::io::BufReader;
use std::path::Path;
const CONFIG_FILENAME : &str = "app_config.yaml";
/// Configuration read or derived from a YAML config file at startup
/// This is not meant to contain any runtime config values.
/// This may not be exhaustive.
#[derive(serde::Deserialize, serde::Serialize, Clone, Debug, Default)]
pub struct YamlConfig {
pub image_height: Option<usize>,
pub image_width: Option<usize>,
pub scheduler_steps: Option<usize>,
pub scheduler_samples: Option<usize>,
pub seed: Option<u64>,
pub cfg_scale: Option<f64>,
/// Override the default location to store data on Linux
pub linux_default_data_path: Option<String>,
/// Override the default location to store data on Windows
pub windows_default_data_path: Option<String>,
/// Override the default location to store data on Mac
pub mac_default_data_path: Option<String>,
}
impl YamlConfig {
pub fn read_from_file<P: AsRef<Path>>(path: P) -> anyhow::Result<Self> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let config = serde_yaml::from_reader(reader)?;
Ok(config)
}
pub fn load_from_config_file_recursive() -> YamlConfig {
let maybe_path = recursively_find_file_by_name(CONFIG_FILENAME, "../../../", 5)
.ok()
.flatten();
if let Some(path) = maybe_path {
let path = path.canonicalize().unwrap_or(path);
println!("Attempting to load configs from: {:?}", &path);
if let Ok(config) = YamlConfig::read_from_file(path) {
return config;
}
}
println!("Couldn't load config file. Using default configs.");
YamlConfig::default()
}
}