pinakes-ui: add book management component and reading progress display

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I877f0856ac5392266a9ba4f607a8d73c6a6a6964
This commit is contained in:
raf 2026-03-08 00:42:38 +03:00
commit adaab9de21
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
8 changed files with 1116 additions and 28 deletions

View file

@ -10,6 +10,7 @@ use dioxus_free_icons::{
icons::fa_solid_icons::{
FaArrowRightFromBracket,
FaBook,
FaBookOpen,
FaChartBar,
FaChevronLeft,
FaChevronRight,
@ -43,6 +44,7 @@ use crate::{
client::*,
components::{
audit,
books,
collections,
database,
detail,
@ -69,6 +71,7 @@ enum View {
Detail,
Tags,
Collections,
Books,
Audit,
Import,
Duplicates,
@ -87,6 +90,7 @@ impl View {
Self::Detail => "Detail",
Self::Tags => "Tags",
Self::Collections => "Collections",
Self::Books => "Books",
Self::Audit => "Audit Log",
Self::Import => "Import",
Self::Duplicates => "Duplicates",
@ -112,7 +116,7 @@ pub fn App() -> Element {
let mut media_list = use_signal(Vec::<MediaResponse>::new);
let mut media_total_count = use_signal(|| 0u64);
let mut media_page = use_signal(|| 0u64);
let mut media_page_size = use_signal(|| 48u64);
let mut media_page_size = use_signal(|| 50u64);
let mut media_sort = use_signal(|| "created_at_desc".to_string());
let mut search_results = use_signal(Vec::<MediaResponse>::new);
let mut search_total = use_signal(|| 0u64);
@ -134,6 +138,23 @@ pub fn App() -> Element {
let mut preview_total_size = use_signal(|| 0u64);
let mut viewing_collection = use_signal(|| Option::<String>::None);
let mut collection_members = use_signal(Vec::<MediaResponse>::new);
// Phase 4A: Book management
let mut books_list = use_signal(Vec::<MediaResponse>::new);
let mut books_series_list =
use_signal(Vec::<crate::client::SeriesSummary>::new);
let mut books_authors_list =
use_signal(Vec::<crate::client::AuthorSummary>::new);
let mut books_series_detail = use_signal(Vec::<MediaResponse>::new);
let mut books_author_detail = use_signal(Vec::<MediaResponse>::new);
let mut viewing_series = use_signal(|| Option::<String>::None);
let mut viewing_author = use_signal(|| Option::<String>::None);
let mut books_reading_list = use_signal(Vec::<MediaResponse>::new);
let mut detail_book_metadata =
use_signal(|| Option::<crate::client::BookMetadataResponse>::None);
let mut detail_reading_progress =
use_signal(|| Option::<crate::client::ReadingProgressResponse>::None);
let mut server_connected = use_signal(|| false);
let mut server_checking = use_signal(|| true);
let mut loading = use_signal(|| true);
@ -587,6 +608,27 @@ pub fn App() -> Element {
span { class: "nav-item-text", "Collections" }
span { class: "nav-badge", "{collections_list.read().len()}" }
}
button {
class: if *current_view.read() == View::Books { "nav-item active" } else { "nav-item" },
onclick: {
let client = client.read().clone();
move |_| {
current_view.set(View::Books);
viewing_series.set(None);
viewing_author.set(None);
let client = client.clone();
spawn(async move {
if let Ok(items) = client.list_books(0, 50, None, None).await {
books_list.set(items);
}
});
}
},
span { class: "nav-icon",
NavIcon { icon: FaBookOpen }
}
span { class: "nav-item-text", "Books" }
}
}
div { class: "nav-section",
@ -886,6 +928,17 @@ pub fn App() -> Element {
Ok(item) => {
let mtags = client.get_media_tags(&id).await.unwrap_or_default();
media_tags.set(mtags);
// Fetch book metadata for document types
let is_document = item.media_type == "document";
if is_document {
let bm = client.get_book_metadata(&id).await.ok();
let rp = client.get_reading_progress(&id).await.ok();
detail_book_metadata.set(bm);
detail_reading_progress.set(rp);
} else {
detail_book_metadata.set(None);
detail_reading_progress.set(None);
}
selected_media.set(Some(item));
current_view.set(View::Detail);
}
@ -1090,6 +1143,13 @@ pub fn App() -> Element {
if let Ok(item) = client.get_media(&id).await {
let mtags = client.get_media_tags(&id).await.unwrap_or_default();
media_tags.set(mtags);
if item.media_type == "document" {
detail_book_metadata.set(client.get_book_metadata(&id).await.ok());
detail_reading_progress.set(client.get_reading_progress(&id).await.ok());
} else {
detail_book_metadata.set(None);
detail_reading_progress.set(None);
}
selected_media.set(Some(item));
current_view.set(View::Detail);
}
@ -1476,6 +1536,25 @@ pub fn App() -> Element {
});
}
},
book_metadata: detail_book_metadata.read().clone(),
reading_progress: detail_reading_progress.read().clone(),
on_update_reading_progress: {
let client = client.read().clone();
move |(media_id, page): (String, i32)| {
let client = client.clone();
spawn(async move {
match client.update_reading_progress(&media_id, page).await {
Ok(()) => {
if let Ok(rp) = client.get_reading_progress(&media_id).await {
detail_reading_progress.set(Some(rp));
}
show_toast("Reading progress updated".into(), false);
}
Err(e) => show_toast(format!("Failed to update progress: {e}"), true),
}
});
}
},
}
},
None => rsx! {
@ -1618,6 +1697,13 @@ pub fn App() -> Element {
if let Ok(item) = client.get_media(&id).await {
let mtags = client.get_media_tags(&id).await.unwrap_or_default();
media_tags.set(mtags);
if item.media_type == "document" {
detail_book_metadata.set(client.get_book_metadata(&id).await.ok());
detail_reading_progress.set(client.get_reading_progress(&id).await.ok());
} else {
detail_book_metadata.set(None);
detail_reading_progress.set(None);
}
selected_media.set(Some(item));
current_view.set(View::Detail);
}
@ -1669,6 +1755,13 @@ pub fn App() -> Element {
if let Ok(item) = client.get_media(&id).await {
let mtags = client.get_media_tags(&id).await.unwrap_or_default();
media_tags.set(mtags);
if item.media_type == "document" {
detail_book_metadata.set(client.get_book_metadata(&id).await.ok());
detail_reading_progress.set(client.get_reading_progress(&id).await.ok());
} else {
detail_book_metadata.set(None);
detail_reading_progress.set(None);
}
selected_media.set(Some(item));
current_view.set(View::Detail);
}
@ -2181,13 +2274,140 @@ pub fn App() -> Element {
}
},
on_backup: {
move |_path: String| {
show_toast("Backup not yet implemented on server".into(), false);
let client = client.read().clone();
move |path: String| {
let client = client.clone();
spawn(async move {
match client.backup_database(&path).await {
Ok(()) => {
show_toast(format!("Backup saved to: {path}"), false);
},
Err(e) => show_toast(format!("Backup failed: {e}"), true),
}
});
}
},
}
}
}
View::Books => {
rsx! {
books::Books {
books: books_list.read().clone(),
series_list: books_series_list.read().clone(),
authors_list: books_authors_list.read().clone(),
series_books: books_series_detail.read().clone(),
author_books: books_author_detail.read().clone(),
reading_list: books_reading_list.read().clone(),
viewing_series: viewing_series.read().clone(),
viewing_author: viewing_author.read().clone(),
on_select: {
let client = client.read().clone();
move |id: String| {
let client = client.clone();
spawn(async move {
match client.get_media(&id).await {
Ok(item) => {
let mtags = client.get_media_tags(&id).await.unwrap_or_default();
media_tags.set(mtags);
if item.media_type == "document" {
detail_book_metadata.set(client.get_book_metadata(&id).await.ok());
detail_reading_progress.set(client.get_reading_progress(&id).await.ok());
} else {
detail_book_metadata.set(None);
detail_reading_progress.set(None);
}
selected_media.set(Some(item));
current_view.set(View::Detail);
}
Err(e) => show_toast(format!("Failed to load: {e}"), true),
}
});
}
},
on_load_books: {
let client = client.read().clone();
move |(offset, limit, author, series): (u64, u64, Option<String>, Option<String>)| {
let client = client.clone();
spawn(async move {
match client.list_books(offset, limit, author.as_deref(), series.as_deref()).await {
Ok(items) => books_list.set(items),
Err(e) => show_toast(format!("Failed to load books: {e}"), true),
}
});
}
},
on_load_series: {
let client = client.read().clone();
move |_| {
let client = client.clone();
spawn(async move {
match client.list_series().await {
Ok(series) => books_series_list.set(series),
Err(e) => show_toast(format!("Failed to load series: {e}"), true),
}
});
}
},
on_load_authors: {
let client = client.read().clone();
move |_| {
let client = client.clone();
spawn(async move {
match client.list_authors().await {
Ok(authors) => books_authors_list.set(authors),
Err(e) => show_toast(format!("Failed to load authors: {e}"), true),
}
});
}
},
on_load_reading_list: {
let client = client.read().clone();
move |status: Option<String>| {
let client = client.clone();
spawn(async move {
match client.get_reading_list(status.as_deref()).await {
Ok(items) => books_reading_list.set(items),
Err(e) => show_toast(format!("Failed to load reading list: {e}"), true),
}
});
}
},
on_view_series: {
let client = client.read().clone();
move |name: String| {
viewing_series.set(Some(name.clone()));
let client = client.clone();
spawn(async move {
match client.get_series_books(&name).await {
Ok(items) => books_series_detail.set(items),
Err(e) => show_toast(format!("Failed to load series books: {e}"), true),
}
});
}
},
on_view_author: {
let client = client.read().clone();
move |name: String| {
viewing_author.set(Some(name.clone()));
let client = client.clone();
spawn(async move {
match client.get_author_books(&name).await {
Ok(items) => books_author_detail.set(items),
Err(e) => show_toast(format!("Failed to load author books: {e}"), true),
}
});
}
},
on_back_to_list: move |_| {
viewing_series.set(None);
viewing_author.set(None);
books_series_detail.set(Vec::new());
books_author_detail.set(Vec::new());
},
}
}
}
View::Duplicates => {
rsx! {
duplicates::Duplicates {
@ -2435,6 +2655,13 @@ pub fn App() -> Element {
if let Ok(mtags) = client.get_media_tags(&media_id).await {
media_tags.set(mtags);
}
if media.media_type == "document" {
detail_book_metadata.set(client.get_book_metadata(&media_id).await.ok());
detail_reading_progress.set(client.get_reading_progress(&media_id).await.ok());
} else {
detail_book_metadata.set(None);
detail_reading_progress.set(None);
}
selected_media.set(Some(media));
current_view.set(View::Detail);
}