From 7d75e010c73f5aeb50714161730c8585e0ca8869 Mon Sep 17 00:00:00 2001 From: Joakim Hulthe Date: Sun, 17 May 2026 13:04:51 +0200 Subject: [PATCH] Add *very* basic login flow --- Cargo.lock | 18 ++++++++++- Cargo.toml | 1 + assets/immich-logo.svg | 18 +++++++++++ src/cachemap.rs | 7 +++-- src/config.rs | 46 +++++++++++++++++++++++++++ src/main.rs | 70 ++++++++++++++++++++++++++++++++---------- src/xdg.rs | 8 +++++ ui/app-window.slint | 4 ++- ui/global.slint | 2 ++ ui/login.slint | 35 +++++++++++++++++++++ 10 files changed, 187 insertions(+), 22 deletions(-) create mode 100644 assets/immich-logo.svg create mode 100644 src/config.rs create mode 100644 src/xdg.rs create mode 100644 ui/login.slint 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); + } + } +}