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

@ -31,7 +31,7 @@ impl PartialEq for ApiClient {
}
}
// ── Response types ──
// Response types
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct MediaResponse {
@ -179,7 +179,7 @@ fn default_view() -> String {
"library".to_string()
}
fn default_page_size() -> usize {
48
50
}
fn default_view_mode() -> String {
"grid".to_string()
@ -279,7 +279,7 @@ pub struct DatabaseStatsResponse {
pub backend_name: String,
}
// ── Markdown Notes/Links Response Types ──
// Markdown notes/links response types
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct BacklinksResponse {
@ -363,6 +363,55 @@ pub struct CreateSavedSearchRequest {
pub sort_order: Option<String>,
}
// Book management response types
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct BookMetadataResponse {
pub media_id: String,
pub isbn: Option<String>,
pub isbn13: Option<String>,
pub publisher: Option<String>,
pub language: Option<String>,
pub page_count: Option<i32>,
pub publication_date: Option<String>,
pub series_name: Option<String>,
pub series_index: Option<f64>,
pub format: Option<String>,
pub authors: Vec<BookAuthorResponse>,
#[serde(default)]
pub identifiers: HashMap<String, Vec<String>>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct BookAuthorResponse {
pub name: String,
pub role: String,
pub file_as: Option<String>,
pub position: i32,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct ReadingProgressResponse {
pub media_id: String,
pub user_id: String,
pub current_page: i32,
pub total_pages: Option<i32>,
pub progress_percent: f64,
pub last_read_at: String,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct SeriesSummary {
pub name: String,
pub book_count: u64,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct AuthorSummary {
pub name: String,
pub book_count: u64,
}
impl ApiClient {
pub fn new(base_url: &str, api_key: Option<&str>) -> Self {
let mut headers = header::HeaderMap::new();
@ -402,7 +451,7 @@ impl ApiClient {
}
}
// ── Media ──
// Media
pub async fn list_media(
&self,
@ -522,7 +571,7 @@ impl ApiClient {
Ok(resp.count)
}
// ── Import ──
// Import
pub async fn import_file(&self, path: &str) -> Result<ImportResponse> {
Ok(
@ -616,7 +665,7 @@ impl ApiClient {
)
}
// ── Search ──
// Search
pub async fn search(
&self,
@ -646,7 +695,7 @@ impl ApiClient {
)
}
// ── Tags ──
// Tags
pub async fn list_tags(&self) -> Result<Vec<TagResponse>> {
Ok(
@ -730,7 +779,7 @@ impl ApiClient {
)
}
// ── Custom Fields ──
// Custom fields
pub async fn set_custom_field(
&self,
@ -762,7 +811,7 @@ impl ApiClient {
Ok(())
}
// ── Collections ──
// Collections
pub async fn list_collections(&self) -> Result<Vec<CollectionResponse>> {
Ok(
@ -862,7 +911,7 @@ impl ApiClient {
Ok(())
}
// ── Batch Operations ──
// Batch operations
pub async fn batch_tag(
&self,
@ -928,7 +977,7 @@ impl ApiClient {
.await?)
}
// ── Audit ──
// Audit
pub async fn list_audit(
&self,
@ -948,7 +997,7 @@ impl ApiClient {
)
}
// ── Scan ──
// Scan
pub async fn trigger_scan(&self) -> Result<Vec<ScanResponse>> {
Ok(
@ -977,7 +1026,7 @@ impl ApiClient {
)
}
// ── Config ──
// Config
pub async fn get_config(&self) -> Result<ConfigResponse> {
Ok(
@ -1049,7 +1098,7 @@ impl ApiClient {
)
}
// ── Database Management ──
// Database management
pub async fn database_stats(&self) -> Result<DatabaseStatsResponse> {
Ok(
@ -1086,7 +1135,180 @@ impl ApiClient {
Ok(())
}
// ── Duplicates ──
/// Download a database backup and save it to the given path.
pub async fn backup_database(&self, save_path: &str) -> Result<()> {
let bytes = self
.client
.post(self.url("/database/backup"))
.send()
.await?
.error_for_status()?
.bytes()
.await?;
tokio::fs::write(save_path, &bytes).await?;
Ok(())
}
// Books
pub async fn get_book_metadata(
&self,
media_id: &str,
) -> Result<BookMetadataResponse> {
Ok(
self
.client
.get(self.url(&format!("/books/{media_id}/metadata")))
.send()
.await?
.error_for_status()?
.json()
.await?,
)
}
pub async fn list_books(
&self,
offset: u64,
limit: u64,
author: Option<&str>,
series: Option<&str>,
) -> Result<Vec<MediaResponse>> {
let mut url = format!("/books?offset={offset}&limit={limit}");
if let Some(a) = author {
url.push_str(&format!("&author={}", urlencoding::encode(a)));
}
if let Some(s) = series {
url.push_str(&format!("&series={}", urlencoding::encode(s)));
}
Ok(
self
.client
.get(self.url(&url))
.send()
.await?
.error_for_status()?
.json()
.await?,
)
}
pub async fn list_series(&self) -> Result<Vec<SeriesSummary>> {
Ok(
self
.client
.get(self.url("/books/series"))
.send()
.await?
.error_for_status()?
.json()
.await?,
)
}
pub async fn get_series_books(
&self,
series_name: &str,
) -> Result<Vec<MediaResponse>> {
Ok(
self
.client
.get(self.url(&format!(
"/books/series/{}",
urlencoding::encode(series_name)
)))
.send()
.await?
.error_for_status()?
.json()
.await?,
)
}
pub async fn list_authors(&self) -> Result<Vec<AuthorSummary>> {
Ok(
self
.client
.get(self.url("/books/authors"))
.send()
.await?
.error_for_status()?
.json()
.await?,
)
}
pub async fn get_author_books(
&self,
author_name: &str,
) -> Result<Vec<MediaResponse>> {
Ok(
self
.client
.get(self.url(&format!(
"/books/authors/{}/books",
urlencoding::encode(author_name)
)))
.send()
.await?
.error_for_status()?
.json()
.await?,
)
}
pub async fn get_reading_progress(
&self,
media_id: &str,
) -> Result<ReadingProgressResponse> {
Ok(
self
.client
.get(self.url(&format!("/books/{media_id}/progress")))
.send()
.await?
.error_for_status()?
.json()
.await?,
)
}
pub async fn update_reading_progress(
&self,
media_id: &str,
current_page: i32,
) -> Result<()> {
self
.client
.put(self.url(&format!("/books/{media_id}/progress")))
.json(&serde_json::json!({"current_page": current_page}))
.send()
.await?
.error_for_status()?;
Ok(())
}
pub async fn get_reading_list(
&self,
status: Option<&str>,
) -> Result<Vec<MediaResponse>> {
let mut url = "/books/reading-list".to_string();
if let Some(s) = status {
url.push_str(&format!("?status={s}"));
}
Ok(
self
.client
.get(self.url(&url))
.send()
.await?
.error_for_status()?
.json()
.await?,
)
}
// Duplicates
pub async fn list_duplicates(&self) -> Result<Vec<DuplicateGroupResponse>> {
Ok(
@ -1101,7 +1323,7 @@ impl ApiClient {
)
}
// ── UI Config ──
// UI config
pub async fn update_ui_config(
&self,
@ -1120,7 +1342,7 @@ impl ApiClient {
)
}
// ── Auth ──
// Auth
pub async fn login(
&self,
@ -1223,7 +1445,7 @@ impl ApiClient {
)
}
// ── Saved Searches ──
// Saved searches
pub async fn list_saved_searches(&self) -> Result<Vec<SavedSearchResponse>> {
Ok(
@ -1272,7 +1494,7 @@ impl ApiClient {
Ok(())
}
// ── Markdown Notes/Links ──
// Markdown notes/links
/// Get backlinks (incoming links) to a media item.
pub async fn get_backlinks(&self, id: &str) -> Result<BacklinksResponse> {