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:
parent
66861b8a20
commit
adaab9de21
8 changed files with 1116 additions and 28 deletions
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue