Add *very* basic login flow
This commit is contained in:
18
Cargo.lock
generated
18
Cargo.lock
generated
@@ -2473,6 +2473,7 @@ dependencies = [
|
|||||||
"thumbhash",
|
"thumbhash",
|
||||||
"tikv-jemallocator",
|
"tikv-jemallocator",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"toml 1.1.2+spec-1.1.0",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"xdg",
|
"xdg",
|
||||||
@@ -4798,7 +4799,7 @@ dependencies = [
|
|||||||
"regex",
|
"regex",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tar",
|
"tar",
|
||||||
"toml",
|
"toml 0.9.12+spec-1.1.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5454,6 +5455,21 @@ dependencies = [
|
|||||||
"winnow 0.7.15",
|
"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]]
|
[[package]]
|
||||||
name = "toml_datetime"
|
name = "toml_datetime"
|
||||||
version = "0.7.5+spec-1.1.0"
|
version = "0.7.5+spec-1.1.0"
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ tracing = "0.1.44"
|
|||||||
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
|
||||||
xdg = "3.0.0"
|
xdg = "3.0.0"
|
||||||
tikv-jemallocator = "0.6"
|
tikv-jemallocator = "0.6"
|
||||||
|
toml = "1.1.2"
|
||||||
|
|
||||||
[dependencies.slint]
|
[dependencies.slint]
|
||||||
version = "1.16.1"
|
version = "1.16.1"
|
||||||
|
|||||||
18
assets/immich-logo.svg
Normal file
18
assets/immich-logo.svg
Normal 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 |
@@ -15,6 +15,8 @@ use either::Either;
|
|||||||
use futures::{FutureExt, future::WeakShared};
|
use futures::{FutureExt, future::WeakShared};
|
||||||
use tokio::{fs, task::JoinHandle};
|
use tokio::{fs, task::JoinHandle};
|
||||||
|
|
||||||
|
use crate::xdg::BASE_DIRECTORIES;
|
||||||
|
|
||||||
pub trait Fetcher<V>: Send + Sync + 'static {
|
pub trait Fetcher<V>: Send + Sync + 'static {
|
||||||
type Key: ToString + FromStr;
|
type Key: ToString + FromStr;
|
||||||
|
|
||||||
@@ -86,9 +88,8 @@ where
|
|||||||
serialize: fn(&K, &V) -> Vec<u8>,
|
serialize: fn(&K, &V) -> Vec<u8>,
|
||||||
deserialize: fn(&K, &[u8]) -> anyhow::Result<V>,
|
deserialize: fn(&K, &[u8]) -> anyhow::Result<V>,
|
||||||
) -> anyhow::Result<Self> {
|
) -> anyhow::Result<Self> {
|
||||||
let crate_name = env!("CARGO_CRATE_NAME");
|
let data_dir = BASE_DIRECTORIES
|
||||||
let data_dir = xdg::BaseDirectories::new()
|
.create_cache_directory("thumbnails")
|
||||||
.create_cache_directory(format!("{crate_name}/thumbnails"))
|
|
||||||
.context(anyhow!(
|
.context(anyhow!(
|
||||||
"Failed to create XDG data folder for {cache_name:?}"
|
"Failed to create XDG data folder for {cache_name:?}"
|
||||||
))?;
|
))?;
|
||||||
|
|||||||
46
src/config.rs
Normal file
46
src/config.rs
Normal 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:?}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
70
src/main.rs
70
src/main.rs
@@ -13,6 +13,7 @@ use tracing::Level;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{Api, TimeBucketKey},
|
api::{Api, TimeBucketKey},
|
||||||
|
config::Config,
|
||||||
ui::{AppWindow, ImageBucket},
|
ui::{AppWindow, ImageBucket},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -20,22 +21,20 @@ use crate::{
|
|||||||
#[global_allocator]
|
#[global_allocator]
|
||||||
static ALLOCATOR: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
|
static ALLOCATOR: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
|
||||||
|
|
||||||
mod api;
|
pub mod api;
|
||||||
pub mod cachemap;
|
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 {
|
mod ui {
|
||||||
slint::include_modules!();
|
slint::include_modules!();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(clap::Parser)]
|
#[derive(clap::Parser)]
|
||||||
struct Opt {
|
struct Opt {}
|
||||||
#[clap(long, env = "IMMICH_BASE_URL")]
|
|
||||||
pub immich_base_url: String,
|
|
||||||
|
|
||||||
#[clap(long, env = "IMMICH_API_KEY")]
|
|
||||||
pub immich_api_key: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
// enum ApiReq<M: Send + 'static>
|
// enum ApiReq<M: Send + 'static>
|
||||||
// where
|
// where
|
||||||
@@ -59,7 +58,7 @@ struct Opt {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
fn main() -> anyhow::Result<()> {
|
||||||
let opt = Opt::parse();
|
let _opt = Opt::parse();
|
||||||
|
|
||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt()
|
||||||
.with_max_level(Level::DEBUG)
|
.with_max_level(Level::DEBUG)
|
||||||
@@ -68,12 +67,53 @@ fn main() -> anyhow::Result<()> {
|
|||||||
let runtime = tokio::runtime::Runtime::new().unwrap();
|
let runtime = tokio::runtime::Runtime::new().unwrap();
|
||||||
let _rt_guard = runtime.enter();
|
let _rt_guard = runtime.enter();
|
||||||
|
|
||||||
let immich_config =
|
let config_ = runtime
|
||||||
immich_sdk::Config::new(opt.immich_base_url).with_api_key(opt.immich_api_key);
|
.block_on(Config::load())
|
||||||
let api_ = Api::new(immich_sdk::Client::new(immich_config).unwrap());
|
.inspect_err(|e| tracing::debug!("{e}"))
|
||||||
|
.map(Arc::new)
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
let app = ui::AppWindow::new()?;
|
let app = ui::AppWindow::new()?;
|
||||||
let global = app.global::<ui::Global>();
|
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()));
|
global.set_image_buckets(ModelRc::new(VecModel::default()));
|
||||||
|
|
||||||
let app_weak = app.as_weak();
|
let app_weak = app.as_weak();
|
||||||
@@ -130,10 +170,6 @@ fn main() -> anyhow::Result<()> {
|
|||||||
calculate_timeline_visibility(&app, api, scroll);
|
calculate_timeline_visibility(&app, api, scroll);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.run()?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn calculate_timeline_visibility(app: &AppWindow, api: Arc<Api>, scroll: f32) {
|
fn calculate_timeline_visibility(app: &AppWindow, api: Arc<Api>, scroll: f32) {
|
||||||
|
|||||||
8
src/xdg.rs
Normal file
8
src/xdg.rs
Normal 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));
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { AboutSlint, Button, Palette, HorizontalBox, ScrollView } from "std-widgets.slint";
|
import { AboutSlint, Button, Palette, HorizontalBox, ScrollView } from "std-widgets.slint";
|
||||||
import { Timeline } from "timeline.slint";
|
import { Timeline } from "timeline.slint";
|
||||||
import { ImageViewer } from "image-viewer.slint";
|
import { ImageViewer } from "image-viewer.slint";
|
||||||
|
import { LoginView } from "login.slint";
|
||||||
|
|
||||||
import { Global } from "global.slint";
|
import { Global } from "global.slint";
|
||||||
export { Global }
|
export { Global }
|
||||||
@@ -34,7 +35,8 @@ export component AppWindow inherits Window {
|
|||||||
|
|
||||||
Header {}
|
Header {}
|
||||||
|
|
||||||
Timeline {}
|
if !Global.logged-in: LoginView {}
|
||||||
|
if Global.logged-in: Timeline {}
|
||||||
}
|
}
|
||||||
|
|
||||||
if Global.previewed-image.asset-id != "" : ImageViewer {
|
if Global.previewed-image.asset-id != "" : ImageViewer {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ImageBucket, ImagePreview } from "types.slint";
|
import { ImageBucket, ImagePreview } from "types.slint";
|
||||||
|
|
||||||
export global Global {
|
export global Global {
|
||||||
|
in-out property <bool> logged-in: false;
|
||||||
in-out property <length> min-image-size: 160px;
|
in-out property <length> min-image-size: 160px;
|
||||||
in-out property <length> image-margin: 2px;
|
in-out property <length> image-margin: 2px;
|
||||||
in-out property <ImagePreview> previewed-image;
|
in-out property <ImagePreview> previewed-image;
|
||||||
@@ -12,6 +13,7 @@ export global Global {
|
|||||||
in property <length> timeline-height;
|
in property <length> timeline-height;
|
||||||
in property <length> timeline-width;
|
in property <length> timeline-width;
|
||||||
in property <length> timeline-scroll;
|
in property <length> timeline-scroll;
|
||||||
|
callback login-api-key(url: string, api_key: string);
|
||||||
callback set-timeline-width(length);
|
callback set-timeline-width(length);
|
||||||
callback timeline-scrolled(length);
|
callback timeline-scrolled(length);
|
||||||
}
|
}
|
||||||
|
|||||||
35
ui/login.slint
Normal file
35
ui/login.slint
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user