Add *very* basic login flow
This commit is contained in:
18
Cargo.lock
generated
18
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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
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 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
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::{
|
||||
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
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 { 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 {
|
||||
|
||||
@@ -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
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