various: simplify code; work on security and performance

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I9a5114addcab5fbff430ab2b919b83466a6a6964
This commit is contained in:
raf 2026-02-02 17:32:11 +03:00
commit c4adc4e3e0
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
75 changed files with 12921 additions and 358 deletions

View file

@ -48,7 +48,7 @@ pub fn App() -> Element {
let base_url =
std::env::var("PINAKES_SERVER_URL").unwrap_or_else(|_| "http://localhost:3000".into());
let api_key = std::env::var("PINAKES_API_KEY").ok();
let client = use_signal(|| ApiClient::new(&base_url, api_key.as_deref()));
let mut client = use_signal(|| ApiClient::new(&base_url, api_key.as_deref()));
let server_url = use_signal(|| base_url.clone());
let mut current_view = use_signal(|| View::Library);
@ -103,10 +103,13 @@ pub fn App() -> Element {
// Auth state
let mut auth_required = use_signal(|| false);
let mut current_user = use_signal(|| Option::<UserInfoResponse>::None);
let _login_error = use_signal(|| Option::<String>::None);
let _login_loading = use_signal(|| false);
let mut login_error = use_signal(|| Option::<String>::None);
let mut login_loading = use_signal(|| false);
let mut auto_play_media = use_signal(|| false);
// Import state for UI feedback
let mut import_in_progress = use_signal(|| false);
// Check auth on startup
let client_auth = client.read().clone();
use_effect(move || {
@ -117,10 +120,16 @@ pub fn App() -> Element {
current_user.set(Some(user));
auth_required.set(false);
}
Err(_) => {
// Check if server has accounts enabled by trying login endpoint
// If we get a 401 on /auth/me, accounts may be enabled
auth_required.set(false); // Will be set to true if needed
Err(e) => {
// Check if this is an auth error (401) vs network error
let err_str = e.to_string();
if err_str.contains("401")
|| err_str.contains("unauthorized")
|| err_str.contains("Unauthorized")
{
auth_required.set(true);
}
// For network errors, don't require auth (server offline state handles this)
}
}
// Load UI config
@ -255,6 +264,33 @@ pub fn App() -> Element {
}
};
// Login handler for auth flow
let on_login_submit = {
move |(username, password): (String, String)| {
let login_client = client.read().clone();
spawn(async move {
login_loading.set(true);
login_error.set(None);
match login_client.login(&username, &password).await {
Ok(resp) => {
// Update the signal with a new client that has the token set
client.write().set_token(&resp.token);
current_user.set(Some(UserInfoResponse {
username: resp.username,
role: resp.role,
}));
auth_required.set(false);
}
Err(e) => {
login_error.set(Some(format!("Login failed: {e}")));
}
}
login_loading.set(false);
});
}
};
let view_title = use_memo(move || current_view.read().title());
let _total_pages = use_memo(move || {
let ps = *media_page_size.read();
@ -265,8 +301,15 @@ pub fn App() -> Element {
rsx! {
style { {styles::CSS} }
// Phase 7.1: Keyboard shortcuts
div { class: "app",
if *auth_required.read() {
crate::components::login::Login {
on_login: on_login_submit,
error: login_error.read().clone(),
loading: *login_loading.read(),
}
} else {
// Phase 7.1: Keyboard shortcuts
div { class: "app",
tabindex: "0",
onkeydown: {
move |evt: KeyboardEvent| {
@ -316,7 +359,7 @@ pub fn App() -> Element {
}
},
span { class: "nav-icon", "\u{25a6}" }
"Library"
span { class: "nav-item-text", "Library" }
// Phase 7.2: Badge
span { class: "nav-badge", "{media_total_count}" }
}
@ -324,7 +367,7 @@ pub fn App() -> Element {
class: if *current_view.read() == View::Search { "nav-item active" } else { "nav-item" },
onclick: move |_| current_view.set(View::Search),
span { class: "nav-icon", "\u{2315}" }
"Search"
span { class: "nav-item-text", "Search" }
}
button {
class: if *current_view.read() == View::Import { "nav-item active" } else { "nav-item" },
@ -341,7 +384,7 @@ pub fn App() -> Element {
}
},
span { class: "nav-icon", "\u{2912}" }
"Import"
span { class: "nav-item-text", "Import" }
}
}
@ -357,7 +400,7 @@ pub fn App() -> Element {
}
},
span { class: "nav-icon", "\u{2605}" }
"Tags"
span { class: "nav-item-text", "Tags" }
// Phase 7.2: Badge
span { class: "nav-badge", "{tags_list.read().len()}" }
}
@ -373,7 +416,7 @@ pub fn App() -> Element {
}
},
span { class: "nav-icon", "\u{2630}" }
"Collections"
span { class: "nav-item-text", "Collections" }
// Phase 7.2: Badge
span { class: "nav-badge", "{collections_list.read().len()}" }
}
@ -391,7 +434,7 @@ pub fn App() -> Element {
}
},
span { class: "nav-icon", "\u{2637}" }
"Audit"
span { class: "nav-item-text", "Audit" }
}
button {
class: if *current_view.read() == View::Duplicates { "nav-item active" } else { "nav-item" },
@ -408,7 +451,7 @@ pub fn App() -> Element {
}
},
span { class: "nav-icon", "\u{2261}" }
"Duplicates"
span { class: "nav-item-text", "Duplicates" }
}
button {
class: if *current_view.read() == View::Settings { "nav-item active" } else { "nav-item" },
@ -425,7 +468,7 @@ pub fn App() -> Element {
}
},
span { class: "nav-icon", "\u{2699}" }
"Settings"
span { class: "nav-item-text", "Settings" }
}
button {
class: if *current_view.read() == View::Database { "nav-item active" } else { "nav-item" },
@ -442,7 +485,7 @@ pub fn App() -> Element {
}
},
span { class: "nav-icon", "\u{2750}" }
"Database"
span { class: "nav-item-text", "Database" }
}
}
@ -1142,6 +1185,7 @@ pub fn App() -> Element {
tags: tags_list.read().clone(),
collections: collections_list.read().clone(),
scan_progress: scan_progress.read().clone(),
is_importing: *import_in_progress.read(),
on_import_file: {
let client = client.read().clone();
let refresh_media = refresh_media.clone();
@ -1150,6 +1194,7 @@ pub fn App() -> Element {
let client = client.clone();
let refresh_media = refresh_media.clone();
let refresh_tags = refresh_tags.clone();
import_in_progress.set(true);
spawn(async move {
if tag_ids.is_empty() && new_tags.is_empty() && col_id.is_none() {
match client.import_file(&path).await {
@ -1179,6 +1224,7 @@ pub fn App() -> Element {
Err(e) => show_toast(format!("Import failed: {e}"), true),
}
}
import_in_progress.set(false);
});
}
},
@ -1190,8 +1236,8 @@ pub fn App() -> Element {
let client = client.clone();
let refresh_media = refresh_media.clone();
let refresh_tags = refresh_tags.clone();
import_in_progress.set(true);
spawn(async move {
show_toast("Importing directory...".into(), false);
match client.import_directory(&path, &tag_ids, &new_tags, col_id.as_deref()).await {
Ok(resp) => {
show_toast(
@ -1208,6 +1254,7 @@ pub fn App() -> Element {
}
Err(e) => show_toast(format!("Directory import failed: {e}"), true),
}
import_in_progress.set(false);
});
}
},
@ -1218,8 +1265,8 @@ pub fn App() -> Element {
move |_| {
let client = client.clone();
let refresh_media = refresh_media.clone();
import_in_progress.set(true);
spawn(async move {
show_toast("Scanning...".into(), false);
match client.trigger_scan().await {
Ok(_results) => {
// Poll scan status until done
@ -1242,6 +1289,7 @@ pub fn App() -> Element {
}
Err(e) => show_toast(format!("Scan failed: {e}"), true),
}
import_in_progress.set(false);
});
}
},
@ -1253,8 +1301,9 @@ pub fn App() -> Element {
let client = client.clone();
let refresh_media = refresh_media.clone();
let refresh_tags = refresh_tags.clone();
let file_count = paths.len();
import_in_progress.set(true);
spawn(async move {
show_toast(format!("Importing {} files...", paths.len()), false);
match client.batch_import(&paths, &tag_ids, &new_tags, col_id.as_deref()).await {
Ok(resp) => {
show_toast(
@ -1269,8 +1318,9 @@ pub fn App() -> Element {
preview_files.set(Vec::new());
preview_total_size.set(0);
}
Err(e) => show_toast(format!("Batch import failed: {e}"), true),
Err(e) => show_toast(format!("Batch import failed ({file_count} files): {e}"), true),
}
import_in_progress.set(false);
});
}
},
@ -1556,6 +1606,7 @@ pub fn App() -> Element {
}
}
}
} // end else (auth not required)
// Phase 1.4: Toast queue - show up to 3 stacked from bottom
div { class: "toast-container",