Add *very* basic login flow

This commit is contained in:
2026-05-17 13:04:51 +02:00
parent 0d28c4172c
commit 7d75e010c7
10 changed files with 187 additions and 22 deletions

18
Cargo.lock generated
View File

@@ -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"

View File

@@ -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"

18
assets/immich-logo.svg Normal file
View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generator: Adobe Illustrator 28.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Flower" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 792 792" style="enable-background:new 0 0 792 792;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FA2921;}
.st1{fill:#ED79B5;}
.st2{fill:#FFB400;}
.st3{fill:#1E83F7;}
.st4{fill:#18C249;}
</style>
<g id="Flower_00000077325900055813483940000000694823054982625702_">
<path class="st0" d="M375.48,267.63c38.64,34.21,69.78,70.87,89.82,105.42c34.42-61.56,57.42-134.71,57.71-181.3 c0-0.33,0-0.63,0-0.91c0-68.94-68.77-95.77-128.01-95.77s-128.01,26.83-128.01,95.77c0,0.94,0,2.2,0,3.72 C300.01,209.24,339.15,235.47,375.48,267.63z"/>
<path class="st1" d="M164.7,455.63c24.15-26.87,61.2-55.99,103.01-80.61c44.48-26.18,88.97-44.47,128.02-52.84 c-47.91-51.76-110.37-96.24-154.6-110.91c-0.31-0.1-0.6-0.19-0.86-0.28c-65.57-21.3-112.34,35.81-130.64,92.15 c-18.3,56.34-14.04,130.04,51.53,151.34C162.05,454.77,163.25,455.16,164.7,455.63z"/>
<path class="st2" d="M681.07,302.19c-18.3-56.34-65.07-113.45-130.64-92.15c-0.9,0.29-2.1,0.68-3.54,1.15 c-3.75,35.93-16.6,81.27-35.96,125.76c-20.59,47.32-45.84,88.27-72.51,118c69.18,13.72,145.86,12.98,190.26-1.14 c0.31-0.1,0.6-0.2,0.86-0.28C695.11,432.22,699.37,358.52,681.07,302.19z"/>
<path class="st3" d="M336.54,510.71c-11.15-50.39-14.8-98.36-10.7-138.08c-64.03,29.57-125.63,75.23-153.26,112.76 c-0.19,0.26-0.37,0.51-0.53,0.73c-40.52,55.78-0.66,117.91,47.27,152.72c47.92,34.82,119.33,53.54,159.86-2.24 c0.56-0.76,1.3-1.78,2.19-3.01C363.28,602.32,347.02,558.08,336.54,510.71z"/>
<path class="st4" d="M617.57,482.52c-35.33,7.54-82.42,9.33-130.72,4.66c-51.37-4.96-98.11-16.32-134.63-32.5 c8.33,70.03,32.73,142.73,59.88,180.6c0.19,0.26,0.37,0.51,0.53,0.73c40.52,55.78,111.93,37.06,159.86,2.24 c47.92-34.82,87.79-96.95,47.27-152.72C619.2,484.77,618.46,483.75,617.57,482.52z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -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<V>: Send + Sync + 'static {
type Key: ToString + FromStr;
@@ -86,9 +88,8 @@ where
serialize: fn(&K, &V) -> Vec<u8>,
deserialize: fn(&K, &[u8]) -> anyhow::Result<V>,
) -> anyhow::Result<Self> {
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:?}"
))?;

46
src/config.rs Normal file
View File

@@ -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<ImmichLogin>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImmichLogin {
pub url: String,
pub api_key: String,
}
impl Config {
pub async fn load() -> anyhow::Result<Self> {
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:?}"))
}
}

View File

@@ -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<M: Send + 'static>
// 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::<ui::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::<ui::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<Api>, scroll: f32) {

8
src/xdg.rs Normal file
View File

@@ -0,0 +1,8 @@
use std::sync::LazyLock;
use xdg::BaseDirectories;
use crate::CRATE_NAME;
pub static BASE_DIRECTORIES: LazyLock<BaseDirectories> =
LazyLock::new(|| BaseDirectories::with_prefix(CRATE_NAME));

View File

@@ -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 {

View File

@@ -1,6 +1,7 @@
import { ImageBucket, ImagePreview } from "types.slint";
export global Global {
in-out property <bool> logged-in: false;
in-out property <length> min-image-size: 160px;
in-out property <length> image-margin: 2px;
in-out property <ImagePreview> previewed-image;
@@ -12,6 +13,7 @@ export global Global {
in property <length> timeline-height;
in property <length> timeline-width;
in property <length> timeline-scroll;
callback login-api-key(url: string, api_key: string);
callback set-timeline-width(length);
callback timeline-scrolled(length);
}

35
ui/login.slint Normal file
View File

@@ -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);
}
}
}