diff --git a/Cargo.lock b/Cargo.lock
index cf93e0e..397c4d5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2473,6 +2473,7 @@ dependencies = [
"thumbhash",
"tikv-jemallocator",
"tokio",
+ "toml 1.1.2+spec-1.1.0",
"tracing",
"tracing-subscriber",
"xdg",
@@ -4798,7 +4799,7 @@ dependencies = [
"regex",
"serde_json",
"tar",
- "toml",
+ "toml 0.9.12+spec-1.1.0",
]
[[package]]
@@ -5454,6 +5455,21 @@ dependencies = [
"winnow 0.7.15",
]
+[[package]]
+name = "toml"
+version = "1.1.2+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
+dependencies = [
+ "indexmap",
+ "serde_core",
+ "serde_spanned",
+ "toml_datetime 1.1.1+spec-1.1.0",
+ "toml_parser",
+ "toml_writer",
+ "winnow 1.0.2",
+]
+
[[package]]
name = "toml_datetime"
version = "0.7.5+spec-1.1.0"
diff --git a/Cargo.toml b/Cargo.toml
index 47c684a..c35b82b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -21,6 +21,7 @@ tracing = "0.1.44"
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
xdg = "3.0.0"
tikv-jemallocator = "0.6"
+toml = "1.1.2"
[dependencies.slint]
version = "1.16.1"
diff --git a/assets/immich-logo.svg b/assets/immich-logo.svg
new file mode 100644
index 0000000..9c99381
--- /dev/null
+++ b/assets/immich-logo.svg
@@ -0,0 +1,18 @@
+
+
+
\ No newline at end of file
diff --git a/src/cachemap.rs b/src/cachemap.rs
index dffa99f..d1bf8bd 100644
--- a/src/cachemap.rs
+++ b/src/cachemap.rs
@@ -15,6 +15,8 @@ use either::Either;
use futures::{FutureExt, future::WeakShared};
use tokio::{fs, task::JoinHandle};
+use crate::xdg::BASE_DIRECTORIES;
+
pub trait Fetcher: Send + Sync + 'static {
type Key: ToString + FromStr;
@@ -86,9 +88,8 @@ where
serialize: fn(&K, &V) -> Vec,
deserialize: fn(&K, &[u8]) -> anyhow::Result,
) -> anyhow::Result {
- let crate_name = env!("CARGO_CRATE_NAME");
- let data_dir = xdg::BaseDirectories::new()
- .create_cache_directory(format!("{crate_name}/thumbnails"))
+ let data_dir = BASE_DIRECTORIES
+ .create_cache_directory("thumbnails")
.context(anyhow!(
"Failed to create XDG data folder for {cache_name:?}"
))?;
diff --git a/src/config.rs b/src/config.rs
new file mode 100644
index 0000000..eccfbd3
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,46 @@
+use anyhow::{Context, anyhow, bail};
+use serde::{Deserialize, Serialize};
+use tokio::fs;
+
+use crate::xdg::BASE_DIRECTORIES;
+
+const CONFIG_FILE: &str = "config.toml";
+
+#[derive(Default, Debug, Clone, Serialize, Deserialize)]
+pub struct Config {
+ pub immich: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ImmichLogin {
+ pub url: String,
+ pub api_key: String,
+}
+
+impl Config {
+ pub async fn load() -> anyhow::Result {
+ let Some(config_path) = BASE_DIRECTORIES.get_config_file(CONFIG_FILE) else {
+ bail!("No config file exists")
+ };
+
+ let config = fs::read_to_string(&config_path)
+ .await
+ .context(anyhow!("Failed to read config file at {config_path:?}"))?;
+
+ toml::from_str(&config).context(anyhow!(
+ "Failed to deserialize config file at {config_path:?}"
+ ))
+ }
+
+ pub async fn save(&self) -> anyhow::Result<()> {
+ let config_path = BASE_DIRECTORIES
+ .place_config_file(CONFIG_FILE)
+ .context(anyhow!("Failed to create config folder"))?;
+
+ let config = toml::to_string_pretty(self)?;
+
+ fs::write(&config_path, config)
+ .await
+ .context(anyhow!("Failed to write config file to {config_path:?}"))
+ }
+}
diff --git a/src/main.rs b/src/main.rs
index 2721362..73046ca 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -13,6 +13,7 @@ use tracing::Level;
use crate::{
api::{Api, TimeBucketKey},
+ config::Config,
ui::{AppWindow, ImageBucket},
};
@@ -20,22 +21,20 @@ use crate::{
#[global_allocator]
static ALLOCATOR: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
-mod api;
+pub mod api;
pub mod cachemap;
-mod thumbhash;
+pub mod config;
+pub mod thumbhash;
+pub mod xdg;
+
+pub const CRATE_NAME: &str = env!("CARGO_CRATE_NAME");
mod ui {
slint::include_modules!();
}
#[derive(clap::Parser)]
-struct Opt {
- #[clap(long, env = "IMMICH_BASE_URL")]
- pub immich_base_url: String,
-
- #[clap(long, env = "IMMICH_API_KEY")]
- pub immich_api_key: String,
-}
+struct Opt {}
// enum ApiReq
// where
@@ -59,7 +58,7 @@ struct Opt {
// }
fn main() -> anyhow::Result<()> {
- let opt = Opt::parse();
+ let _opt = Opt::parse();
tracing_subscriber::fmt()
.with_max_level(Level::DEBUG)
@@ -68,12 +67,53 @@ fn main() -> anyhow::Result<()> {
let runtime = tokio::runtime::Runtime::new().unwrap();
let _rt_guard = runtime.enter();
- let immich_config =
- immich_sdk::Config::new(opt.immich_base_url).with_api_key(opt.immich_api_key);
- let api_ = Api::new(immich_sdk::Client::new(immich_config).unwrap());
+ let config_ = runtime
+ .block_on(Config::load())
+ .inspect_err(|e| tracing::debug!("{e}"))
+ .map(Arc::new)
+ .unwrap_or_default();
let app = ui::AppWindow::new()?;
let global = app.global::();
+
+ let config = Arc::clone(&config_);
+ let app_weak = app.as_weak();
+ global.on_login_api_key(move |url, api_key| {
+ tracing::debug!("url: {url}, api_key: {api_key}");
+
+ let mut config = config.as_ref().clone();
+ let immich_config = config::ImmichLogin {
+ url: url.to_string(),
+ api_key: api_key.to_string(),
+ };
+ config.immich = Some(immich_config.clone());
+
+ tokio::spawn(async move {
+ if let Err(e) = config.save().await {
+ tracing::error!("{e}");
+ }
+ });
+
+ let _ = app_weak.upgrade_in_event_loop(move |app| {
+ start_api(&app, &immich_config);
+ });
+ });
+
+ if let Some(immich) = &config_.immich {
+ start_api(&app, immich);
+ }
+
+ app.run()?;
+
+ Ok(())
+}
+
+fn start_api(app: &AppWindow, immich: &config::ImmichLogin) {
+ let immich_config = immich_sdk::Config::new(&immich.url).with_api_key(&immich.api_key);
+ let api_ = Api::new(immich_sdk::Client::new(immich_config).unwrap());
+
+ let global = app.global::();
+ global.set_logged_in(true);
global.set_image_buckets(ModelRc::new(VecModel::default()));
let app_weak = app.as_weak();
@@ -130,10 +170,6 @@ fn main() -> anyhow::Result<()> {
calculate_timeline_visibility(&app, api, scroll);
});
});
-
- app.run()?;
-
- Ok(())
}
fn calculate_timeline_visibility(app: &AppWindow, api: Arc, scroll: f32) {
diff --git a/src/xdg.rs b/src/xdg.rs
new file mode 100644
index 0000000..2136426
--- /dev/null
+++ b/src/xdg.rs
@@ -0,0 +1,8 @@
+use std::sync::LazyLock;
+
+use xdg::BaseDirectories;
+
+use crate::CRATE_NAME;
+
+pub static BASE_DIRECTORIES: LazyLock =
+ LazyLock::new(|| BaseDirectories::with_prefix(CRATE_NAME));
diff --git a/ui/app-window.slint b/ui/app-window.slint
index 758cfde..1b854f3 100644
--- a/ui/app-window.slint
+++ b/ui/app-window.slint
@@ -1,6 +1,7 @@
import { AboutSlint, Button, Palette, HorizontalBox, ScrollView } from "std-widgets.slint";
import { Timeline } from "timeline.slint";
import { ImageViewer } from "image-viewer.slint";
+import { LoginView } from "login.slint";
import { Global } from "global.slint";
export { Global }
@@ -34,7 +35,8 @@ export component AppWindow inherits Window {
Header {}
- Timeline {}
+ if !Global.logged-in: LoginView {}
+ if Global.logged-in: Timeline {}
}
if Global.previewed-image.asset-id != "" : ImageViewer {
diff --git a/ui/global.slint b/ui/global.slint
index 1000be6..d0df554 100644
--- a/ui/global.slint
+++ b/ui/global.slint
@@ -1,6 +1,7 @@
import { ImageBucket, ImagePreview } from "types.slint";
export global Global {
+ in-out property logged-in: false;
in-out property min-image-size: 160px;
in-out property image-margin: 2px;
in-out property previewed-image;
@@ -12,6 +13,7 @@ export global Global {
in property timeline-height;
in property timeline-width;
in property timeline-scroll;
+ callback login-api-key(url: string, api_key: string);
callback set-timeline-width(length);
callback timeline-scrolled(length);
}
diff --git a/ui/login.slint b/ui/login.slint
new file mode 100644
index 0000000..f06ea78
--- /dev/null
+++ b/ui/login.slint
@@ -0,0 +1,35 @@
+import { TextEdit, LineEdit, Button } from "std-widgets.slint";
+import { Global } from "global.slint";
+
+export component LoginView inherits VerticalLayout {
+ padding: 16px;
+ alignment: center;
+ spacing: 8px;
+
+ HorizontalLayout {
+ alignment: center;
+ Image {
+ width: 128px;
+ height: self.width;
+ source: @image-url("../assets/immich-logo.svg");
+ }
+ }
+
+ url := LineEdit {
+ placeholder-text: "immich url";
+ height: 40px;
+ }
+
+ api-key := LineEdit {
+ placeholder-text: "immich api key";
+ height: 40px;
+ }
+
+ Button {
+ text: "Login";
+ height: 40px;
+ clicked => {
+ Global.login-api-key(url.text, api-key.text);
+ }
+ }
+}