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
|
|
@ -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> {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue