various: simplify code; work on security and performance
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I9a5114addcab5fbff430ab2b919b83466a6a6964
This commit is contained in:
parent
016841b200
commit
c4adc4e3e0
75 changed files with 12921 additions and 358 deletions
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue