From d3f2375995a3a46306b13073b6b8213186831150 Mon Sep 17 00:00:00 2001 From: Julian Mutter Date: Sat, 10 Feb 2024 21:49:46 +0100 Subject: [PATCH] Complete major sheet refactoring Now using books etc is far easier --- db-migrations/0_creation.sql | 13 +- src/database.rs | 88 +------------- src/main.rs | 116 ++++++------------ src/sheet.rs | 230 ++++++++++++++++------------------- src/sheet_dao.rs | 136 +++++++++++++-------- src/ui/app.rs | 69 ++++------- src/ui/sheet_listing.rs | 12 +- src/ui/sheet_model.rs | 58 ++------- 8 files changed, 285 insertions(+), 437 deletions(-) diff --git a/db-migrations/0_creation.sql b/db-migrations/0_creation.sql index b56b5fd..4e8bc6e 100644 --- a/db-migrations/0_creation.sql +++ b/db-migrations/0_creation.sql @@ -1,4 +1,11 @@ -CREATE TABLE IF NOT EXISTS sheets (id integer primary key autoincrement, name TEXT, composer_id integer, path TEXT, file_size INTEGER, file_hash TEXT, last_opened INTEGER); -CREATE TABLE IF NOT EXISTS composers (id integer primary key autoincrement, name TEXT); +CREATE TABLE IF NOT EXISTS sheets (id INTEGER PRIMARY KEY AUTOINCREMENT, + last_opened INTEGER, name TEXT, composer_id INTEGER, path TEXT, file_size INTEGER, file_hash TEXT); +CREATE TABLE IF NOT EXISTS orphans (id INTEGER PRIMARY KEY AUTOINCREMENT, + last_opened INTEGER, path TEXT, file_size INTEGER, file_hash TEXT); +CREATE TABLE IF NOT EXISTS books (id INTEGER PRIMARY KEY AUTOINCREMENT, + last_opened INTEGER, name TEXT, composer_id INTEGER, sheet_ids TEXT, path TEXT, file_size INTEGER, file_hash TEXT); +CREATE TABLE IF NOT EXISTS booksheets (id INTEGER PRIMARY KEY AUTOINCREMENT, + last_opened INTEGER, name TEXT, book_id INTEGER, first_page INTEGER, last_page INTEGER); -CREATE TABLE IF NOT EXISTS orphan_files (id integer primary key autoincrement, path TEXT, file_size INTEGER, file_hash TEXT, last_opened INTEGER); + +CREATE TABLE IF NOT EXISTS composers (id INTEGER primary key autoincrement, name TEXT); diff --git a/src/database.rs b/src/database.rs index 15d1225..95fd18b 100644 --- a/src/database.rs +++ b/src/database.rs @@ -1,12 +1,10 @@ use std::path::Path; use log::debug; -use sqlx::{migrate::MigrateDatabase, sqlite::SqliteRow, Executor, Sqlite, SqlitePool}; - -use crate::sheet::{EnumSheet, Sheet}; +use sqlx::{migrate::MigrateDatabase, Sqlite, SqlitePool}; pub struct Database { - connection: SqlitePool, + pub connection: SqlitePool, } impl Database { @@ -35,86 +33,4 @@ impl Database { debug!("Connected to database"); Ok(connection) } - - pub async fn insert_sheet(&self, sheet: &EnumSheet) -> sqlx::Result<()> { - sheet - .insert_to_database_query() - .execute(&self.connection) - .await - .map(|_| ()) - } - - pub async fn update_sheet_path(&self, sheet: &EnumSheet) -> sqlx::Result<()> { - sheet - .update_path_in_database_query() - .execute(&self.connection) - .await - .map(|_| ()) - } - - pub async fn update_sheet_last_opened(&self, sheet: &EnumSheet) -> sqlx::Result<()> { - sheet - .update_last_opened_in_database_query() - .execute(&self.connection) - .await - .map(|_| ()) - // TODO: check for success - } - - pub async fn fetch_all_sheets(&self) -> sqlx::Result> { - let mut stream = sqlx::query("SELECT * FROM users") - .map(|row: SqliteRow| {}) - .fetch(&mut conn); - sqlx::query_as::<_, Sheet>("SELECT * FROM sheets") - .fetch_all(&self.connection) - .await - } - - pub async fn insert_orphan_file(&self, file: &OrphanFile) -> sqlx::Result { - sqlx::query( - " - INSERT INTO orphan_files (path, file_size, file_hash, last_opened) - VALUES ($1, $2, $3, $4) - ", - ) - .bind(file.path.to_str().unwrap().to_string()) - .bind(file.file_size as i32) - .bind(file.file_hash.clone()) - .bind(file.last_opened.timestamp()) - .execute(&self.connection) - .await - .map(|result| result.last_insert_rowid()) - } - - pub async fn update_orphan_file_path(&self, orphan: &OrphanFile) -> sqlx::Result<()> { - sqlx::query("UPDATE orphan_files SET path = $1 WHERE id = $2") - .bind(orphan.path.to_str().unwrap().to_string()) - .bind(orphan.id) - .execute(&self.connection) - .await - .map(|_| ()) - } - - pub async fn update_orphan_last_opened(&self, orphan: &OrphanFile) -> sqlx::Result<()> { - sqlx::query("UPDATE orphan_files SET last_opened = $1 WHERE id = $2") - .bind(orphan.last_opened.timestamp()) - .bind(orphan.id) - .execute(&self.connection) - .await - .map(|_| ()) - // TODO: check for success - } - - pub async fn fetch_all_orphan_files(&self) -> sqlx::Result> { - sqlx::query_as::<_, OrphanFile>("SELECT * FROM orphan_files") - .fetch_all(&self.connection) - .await - } - - pub fn get_executor(&self) -> E - where - E: Executor, - { - self.connection - } } diff --git a/src/main.rs b/src/main.rs index 00a3cca..972da14 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,10 +13,10 @@ use database::Database; use env_logger::Env; use log::{debug, error}; use relm4::RelmApp; -use sheet::Sheet; +use sheet::{Pdf, Sheet}; use walkdir::WalkDir; -use crate::ui::app::AppModel; +use crate::ui::app::{AppInitData, AppModel}; #[derive(Parser)] #[command(author, version, about)] @@ -36,38 +36,29 @@ async fn main() { let database = Database::setup(cli.directory.join("database.sqlite")) .await .unwrap(); - // database.insert_sheet(Sheet::new_debug()).await.unwrap(); - let sheets = database.fetch_all_sheets().await.unwrap(); - let orphan_files = database.fetch_all_orphan_files().await.unwrap(); + let sheets = sheet_dao::fetch_all_sheets(&database).await.unwrap(); debug!("Validating sheets from database..."); - let mut validation_result = validate_sheet_files(sheets, orphan_files, &cli.directory); + let mut validation_result = validate_sheet_files(sheets, &cli.directory); debug!("{}", validation_result.get_stats()); // TODO: handle invalidated files for updated in validation_result.updated_sheets.iter() { - database.update_sheet_path(updated).await.unwrap(); - } - for updated in validation_result.updated_orphan_files.iter() { - database.update_orphan_file_path(updated).await.unwrap(); - } - - let mut orphans = validation_result.validated_orphan_files; - orphans.append(&mut validation_result.updated_orphan_files); - debug!("Inserting unassigned files into orphan table..."); - for unassigned in validation_result.unassigned_files { - let mut orphan = OrphanFile::try_from(unassigned).unwrap(); - let id = database.insert_orphan_file(&orphan).await.unwrap(); - orphan.id = id; - orphans.push(orphan); + sheet_dao::update_sheet_path(&database, updated) + .await + .unwrap(); } let mut sheets = validation_result.validated_sheets; sheets.append(&mut validation_result.updated_sheets); - let app_init_data = AppInitData { - sheets, - orphans, - database, - }; + debug!("Inserting unassigned files into orphan table..."); + for unassigned in validation_result.unassigned_files { + let orphan = sheet_dao::insert_file_as_orphan(&database, unassigned) + .await + .unwrap(); + sheets.push(orphan); + } + + let app_init_data = AppInitData { sheets, database }; let app = RelmApp::new("de.frajul.sheet-organizer"); // Pass empty command line args to allow my own parsing @@ -75,83 +66,44 @@ async fn main() { .run_async::(app_init_data); } -pub struct AppInitData { - sheets: Vec, - orphans: Vec, - database: Database, -} - pub struct FileValidationResult { validated_sheets: Vec, invalidated_sheets: Vec, updated_sheets: Vec, - validated_orphan_files: Vec, - invalidated_orphan_files: Vec, - updated_orphan_files: Vec, - unassigned_files: Vec, } impl FileValidationResult { fn get_stats(&self) -> String { - format!("Validated sheets: {}\nInvalidated sheets: {}\nUpdated sheets: {}\nValidated orphan_files: {}\nInvalidated orphan_files: {}\nUpdated orphan_files: {}\nUnassigned files: {}", + format!("Validated sheets: {}\nInvalidated sheets: {}\nUpdated sheets: {}\nUnassigned files: {}", self.validated_sheets.len(), self.invalidated_sheets.len(), self.updated_sheets.len(), - self.validated_orphan_files.len(), self.invalidated_orphan_files.len(), self.updated_orphan_files.len(), self.unassigned_files.len()) + self.unassigned_files.len()) } } -fn validate_sheet_files( - sheets: Vec, - orphan_files: Vec, - dir: impl AsRef, -) -> FileValidationResult { - // TODO: fix duplication +fn validate_sheet_files(sheets: Vec, dir: impl AsRef) -> FileValidationResult { let (validated_sheets, mut invalidated_sheets): (Vec<_>, Vec<_>) = sheets .into_iter() - .partition(|sheet| sheet.validate_path(&sheet.path).unwrap_or(false)); - let (validated_orphan_files, mut invalidated_orphan_files): (Vec<_>, Vec<_>) = - orphan_files.into_iter().partition(|orphan_file| { - orphan_file - .validate_path(&orphan_file.path) - .unwrap_or(false) - }); + .partition(|sheet| sheet.validate_own_path().unwrap_or(false)); let mut updated_sheets = Vec::new(); - let mut updated_orphan_files = Vec::new(); let mut unassigned_files = Vec::new(); - for pdf_file in WalkDir::new(dir) - .into_iter() - .filter_map(|e| e.ok()) - .filter(|file| file.file_type().is_file()) - .map(|file| file.into_path()) - .filter(|path| { - path.extension() - .map(|s| s.to_string_lossy().to_ascii_lowercase() == "pdf") - .unwrap_or(false) - }) - { + // TODO: improve performance? + for pdf_file in find_all_pdfs_in_directory_recursive(dir) { if let Some((i, _)) = invalidated_sheets .iter() .enumerate() .find(|(_, sheet)| sheet.validate_path(&pdf_file).unwrap_or(false)) { let mut sheet = invalidated_sheets.remove(i); - sheet.path = pdf_file; + let new_pdf = Pdf::try_from(pdf_file).unwrap(); + sheet.update_pdf_file(new_pdf); updated_sheets.push(sheet); - } else if let Some((i, _)) = invalidated_orphan_files + } else if !validated_sheets .iter() - .enumerate() - .find(|(_, orphan_file)| orphan_file.validate_path(&pdf_file).unwrap_or(false)) - { - let mut orphan_file = invalidated_orphan_files.remove(i); - orphan_file.path = pdf_file; - updated_orphan_files.push(orphan_file); - } else if !validated_sheets.iter().any(|sheet| sheet.path == pdf_file) - && !validated_orphan_files - .iter() - .any(|orphan| orphan.path == pdf_file) + .any(|sheet| sheet.pdf_path_equal(&pdf_file)) { unassigned_files.push(pdf_file); } @@ -161,9 +113,19 @@ fn validate_sheet_files( validated_sheets, invalidated_sheets, updated_sheets, - validated_orphan_files, - invalidated_orphan_files, - updated_orphan_files, unassigned_files, } } + +fn find_all_pdfs_in_directory_recursive(dir: impl AsRef) -> impl Iterator { + WalkDir::new(dir) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|file| file.file_type().is_file()) + .map(|file| file.into_path()) + .filter(|path| { + path.extension() + .map(|s| s.to_string_lossy().to_ascii_lowercase() == "pdf") + .unwrap_or(false) + }) +} diff --git a/src/sheet.rs b/src/sheet.rs index 45865e8..9f92a3b 100644 --- a/src/sheet.rs +++ b/src/sheet.rs @@ -1,104 +1,133 @@ use std::{ cmp::Ordering, + ffi::{OsStr, OsString}, fs, path::{Path, PathBuf}, }; use chrono::{DateTime, NaiveDateTime, Utc}; -use sqlx::{prelude::FromRow, sqlite::SqliteRow, QueryBuilder}; -use strum_macros::{EnumDiscriminants, EnumIter, EnumMessage}; +use sqlx::database; +use strum_macros::{EnumDiscriminants, EnumIter}; + +use crate::{database::Database, sheet_dao}; pub trait PdfSheet { fn get_pdf(&self) -> &Pdf; } -#[derive(PartialEq, Eq, PartialOrd)] -pub struct EnumSheet { +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Sheet { pub id: i64, pub last_opened: I64DateTime, pub kind: SheetKind, } -#[derive(Debug, EnumDiscriminants)] -#[strum_discriminants(derive(EnumIter, EnumMessage))] +#[derive(Debug, Clone, PartialEq, Eq, EnumDiscriminants)] +#[strum_discriminants(derive(EnumIter))] pub enum SheetKind { - #[strum_discriminants(strum(message = "sheets"))] // Message is the sqlite table name Sheet { pdf: Pdf, name: String, composer_id: i64, }, - #[strum_discriminants(strum(message = "orphans"))] // Message is the sqlite table name - Orphan { pdf: Pdf }, - #[strum_discriminants(strum(message = "books"))] // Message is the sqlite table name + Orphan { + pdf: Pdf, + }, Book { pdf: Pdf, name: String, composer_id: i64, sheet_ids: Vec, }, - #[strum_discriminants(strum(message = "booksheets"))] // Message is the sqlite table name BookSheet { + name: String, book_id: i64, first_page: i64, last_page: i64, }, } -pub enum SheetKindTable { - Sheet, +impl PartialOrd for Sheet { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } } -pub trait AnySheet: Ord {} - -#[derive(sqlx::FromRow, PartialEq, Eq)] -pub struct Sheet { - id: i64, - #[sqlx(flatten)] - pdf: Pdf, - #[sqlx(try_from = "i64")] - last_opened: I64DateTime, - name: String, - composer_id: i64, +impl Ord for Sheet { + fn cmp(&self, other: &Self) -> Ordering { + self.last_opened.cmp(&other.last_opened) + } } -#[derive(sqlx::FromRow, Debug, Clone, PartialEq, Eq)] -pub struct Orphan { - id: i64, - #[sqlx(flatten)] - pdf: Pdf, - #[sqlx(try_from = "i64")] - last_opened: I64DateTime, +impl Sheet { + pub async fn open_file(&self, database: &Database) { + let path = match &self.kind { + SheetKind::Sheet { pdf, .. } => pdf.path.clone(), + SheetKind::Orphan { pdf } => pdf.path.clone(), + SheetKind::Book { pdf, .. } => pdf.path.clone(), + SheetKind::BookSheet { book_id, .. } => sheet_dao::find_path_of_book(database, book_id) + .await + .unwrap(), + }; + opener::open(path).unwrap(); + } + + pub fn update_pdf_file(&mut self, new_pdf: Pdf) -> std::io::Result<()> { + match &mut self.kind { + SheetKind::Sheet { pdf, .. } => *pdf = new_pdf, + SheetKind::Orphan { pdf, .. } => *pdf = new_pdf, + SheetKind::Book { pdf, .. } => *pdf = new_pdf, + SheetKind::BookSheet { .. } => {} // TODO: find better solution! + }; + Ok(()) + } + + pub fn pdf_path_equal(&self, path: impl AsRef) -> bool { + match &self.kind { + SheetKind::Sheet { pdf, .. } => pdf.path == path.as_ref(), + SheetKind::Orphan { pdf, .. } => pdf.path == path.as_ref(), + SheetKind::Book { pdf, .. } => pdf.path == path.as_ref(), + SheetKind::BookSheet { .. } => false, // TODO: find better solution! + } + } + + pub fn validate_own_path(&self) -> std::io::Result { + Ok(match &self.kind { + SheetKind::Sheet { pdf, .. } => pdf.validate_path(&pdf.path)?, + SheetKind::Orphan { pdf, .. } => pdf.validate_path(&pdf.path)?, + SheetKind::Book { pdf, .. } => pdf.validate_path(&pdf.path)?, + SheetKind::BookSheet { book_id, .. } => true, // TODO: better solution? + }) + } + + pub fn try_get_path(&self) -> Option<&Path> { + match &self.kind { + SheetKind::Sheet { pdf, .. } => Some(&pdf.path), + SheetKind::Orphan { pdf, .. } => Some(&pdf.path), + SheetKind::Book { pdf, .. } => Some(&pdf.path), + SheetKind::BookSheet { .. } => None, + } + } + + pub fn validate_path(&self, path: impl AsRef) -> std::io::Result { + Ok(match &self.kind { + SheetKind::Sheet { pdf, .. } => pdf.validate_path(path)?, + SheetKind::Orphan { pdf, .. } => pdf.validate_path(path)?, + SheetKind::Book { pdf, .. } => pdf.validate_path(path)?, + SheetKind::BookSheet { book_id, .. } => true, // TODO: better solution? + }) + } } -#[derive(sqlx::FromRow, Debug, Clone, PartialEq, Eq)] -pub struct Book { - id: i64, - #[sqlx(flatten)] - pdf: Pdf, - #[sqlx(try_from = "i64")] - last_opened: I64DateTime, - name: String, - composer_id: i64, - sheet_ids: Vec, -} - -#[derive(sqlx::FromRow, Debug, Clone, PartialEq, Eq)] -pub struct BookSheet { - id: i64, - #[sqlx(try_from = "i64")] - last_opened: I64DateTime, - book_id: i64, - first_page: i64, - last_page: i64, -} - -#[derive(sqlx::FromRow, Debug, Clone, PartialEq, Eq)] -pub struct Pdf { - #[sqlx(try_from = "String")] - path: PathBuf, - file_size: u64, - file_hash: String, +impl SheetKindDiscriminants { + pub fn get_database_table_name(&self) -> &str { + match self { + SheetKindDiscriminants::Sheet => "sheets", + SheetKindDiscriminants::Orphan => "orphans", + SheetKindDiscriminants::Book => "books", + SheetKindDiscriminants::BookSheet => "booksheets", + } + } } #[derive(sqlx::FromRow)] @@ -107,8 +136,8 @@ pub struct Composer { name: String, } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct I64DateTime(DateTime); +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct I64DateTime(pub DateTime); impl TryFrom for I64DateTime { type Error = String; @@ -122,13 +151,24 @@ impl TryFrom for I64DateTime { } } -impl From for i64 { - fn from(value: I64DateTime) -> Self { +impl From<&I64DateTime> for i64 { + fn from(value: &I64DateTime) -> Self { value.0.timestamp() } } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Pdf { + pub path: PathBuf, + pub file_size: u64, + pub file_hash: String, +} + impl Pdf { + pub fn get_name(&self) -> &str { + self.path.file_name().unwrap().to_str().unwrap() + } + pub fn validate_path(&self, path: impl AsRef) -> std::io::Result { // First compare file size since it is faster than hashing let file_size = fs::metadata(path.as_ref())?.len(); @@ -159,67 +199,3 @@ impl TryFrom for Pdf { }) } } - -impl PdfSheet for Sheet { - fn get_pdf(&self) -> &Pdf { - &self.pdf - } -} - -impl PdfSheet for Orphan { - fn get_pdf(&self) -> &Pdf { - &self.pdf - } -} - -impl PdfSheet for Book { - fn get_pdf(&self) -> &Pdf { - &self.pdf - } -} - -impl AnySheet for Sheet {} - -impl Ord for EnumSheet { - fn cmp(&self, other: &Self) -> Ordering { - self.last_opened.cmp(other.last_opened) - } -} - -impl EnumSheet { - pub fn update_path_in_database_query(&self) -> String { - todo!() - // sqlx::query("UPDATE sheets SET path = $1 WHERE id = $2") - // .bind(sheet.path.to_str().unwrap().to_string()) - // .bind(sheet.id) - } - pub fn update_last_opened_in_database_query(&self) -> String { - // sqlx::query("UPDATE sheets SET last_opened = $1 WHERE id = $2") - // .bind(sheet.last_opened.timestamp()) - // .bind(sheet.id) - todo!() - } - pub fn insert_to_database_query(&self) -> String { - todo!() - // sqlx::query( - // " - // INSERT INTO orphan_files (path, file_size, file_hash, last_opened) - // VALUES ($1, $2, $3, $4) - // ", - // ) - // .bind(file.path.to_str().unwrap().to_string()) - // .bind(file.file_size as i32) - // .bind(file.file_hash.clone()) - // .bind(file.last_opened.timestamp()) - } - - pub fn get_database_table_name(&self) -> &str { - todo!() - } -} - -// impl PartialOrd for EnumSheet { -// fn partial_cmp(&self, other: &Self) -> Option { -// Some(self.cmp(other)) -// } -// } diff --git a/src/sheet_dao.rs b/src/sheet_dao.rs index 25ca420..064a455 100644 --- a/src/sheet_dao.rs +++ b/src/sheet_dao.rs @@ -1,72 +1,95 @@ -use chrono::NaiveDateTime; -use sqlx::{sqlite::SqliteRow, SqlitePool}; +use std::path::{Path, PathBuf}; +use strum::{EnumMessage, IntoEnumIterator}; +use strum_macros::{EnumDiscriminants, EnumIter, EnumString}; -use crate::sheet::{EnumSheet, Pdf, SheetKind, SheetKindDiscriminants}; +use chrono::{DateTime, NaiveDateTime, Utc}; +use sqlx::{sqlite::SqliteRow, Row, SqlitePool}; -pub async fn insert_sheet(connection: SqlitePool, sheet: &EnumSheet) -> sqlx::Result<()> { - let table = sheet.kind.into::().get_message(); - sqlx::query(&format!( +use crate::{ + database::Database, + sheet::{I64DateTime, Pdf, Sheet, SheetKind, SheetKindDiscriminants}, +}; + +pub async fn insert_file_as_orphan( + database: &Database, + file: impl AsRef, +) -> sqlx::Result { + let pdf = Pdf::try_from(file.as_ref().to_path_buf()).unwrap(); + let last_opened = DateTime::::default(); + + let result = sqlx::query( " - INSERT INTO {} (path, file_size, file_hash, last_opened) + INSERT INTO orphans (path, file_size, file_hash, last_opened) VALUES ($1, $2, $3, $4) ", - table, - )) - .bind(sheet.pdf.path.to_str().unwrap().to_string()) - .bind(sheet.pdf.file_size as i32) - .bind(sheet.pdf.file_hash.clone()) - .bind(sheet.last_opened.timestamp()) - .execute(&mut connection) + ) + .bind(pdf.path.to_str().unwrap().to_string()) + .bind(pdf.file_size as i32) + .bind(pdf.file_hash.clone()) + .bind(last_opened.timestamp()) + .execute(&database.connection) .await - .map(|_| ()) + .unwrap(); + + let id = result.last_insert_rowid(); + + Ok(Sheet { + id, + last_opened: I64DateTime(last_opened), + kind: SheetKind::Orphan { pdf }, + }) } -pub async fn update_sheet_path(connection: SqlitePool, sheet: &EnumSheet) -> sqlx::Result<()> { - let table = sheet.kind.into::().get_message(); - sqlx::query(&format!("UPDATE {} SET path = $1 WHERE id = $2", table)) - .bind(sheet.kind.pdf.path.to_str().unwrap().to_string()) - .bind(sheet.id) - .execute(&mut connection) +pub async fn find_path_of_book(database: &Database, book_id: &i64) -> sqlx::Result { + sqlx::query("SELECT path FROM books WHERE id = $1") + .bind(book_id) + .map(|row: SqliteRow| PathBuf::try_from(row.try_get::("path").unwrap()).unwrap()) + .fetch_one(&database.connection) .await - .map(|_| ()) - // TODO: check for success } -pub async fn update_sheet_last_opened( - connection: SqlitePool, - sheet: &EnumSheet, -) -> sqlx::Result<()> { - let table = sheet.kind.into::().get_message(); +pub async fn update_sheet_path(database: &Database, sheet: &Sheet) -> sqlx::Result<()> { + if let Some(path) = sheet.try_get_path() { + let sheet_kind = SheetKindDiscriminants::from(&sheet.kind); + let table = sheet_kind.get_database_table_name(); + return sqlx::query(&format!("UPDATE {} SET path = $1 WHERE id = $2", table)) + .bind(path.to_str().unwrap().to_string()) + .bind(sheet.id) + .execute(&database.connection) + .await + .map(|_| ()); + } + Ok(()) // TODO: error on else? +} + +pub async fn update_sheet_last_opened(database: &Database, sheet: &Sheet) -> sqlx::Result<()> { + let sheet_kind = SheetKindDiscriminants::from(&sheet.kind); + let table = sheet_kind.get_database_table_name(); sqlx::query(&format!( "UPDATE {} SET last_opened = $1 WHERE id = $2", table )) - .bind(sheet.last_opened.timestamp()) + .bind(i64::from(&sheet.last_opened)) .bind(sheet.id) - .execute(&mut connection) + .execute(&database.connection) .await .map(|_| ()) - // TODO: check for success } -pub async fn fetch_all_sheets(&connection: SqlitePool) -> sqlx::Result> { - let mut sheets: Vec = Vec::new(); +pub async fn fetch_all_sheets(database: &Database) -> sqlx::Result> { + let mut sheets: Vec = Vec::new(); for kind in SheetKindDiscriminants::iter() { - let table = kind.get_message(); + let table = kind.get_database_table_name(); let mut sheets_of_kind = sqlx::query(&format!("SELECT * FROM {}", table)) - .map(|row: SqliteRow| EnumSheet { - id: row.try_get("id")?, - last_opened: NaiveDateTime::from_timestamp_opt( - row.try_get::("last_opened")?, - 0, - ) - .unwrap() - .and_utc(), - kind: parse_kind_from_row(kind, row), + .map(|row: SqliteRow| Sheet { + id: row.try_get("id").unwrap(), + last_opened: I64DateTime::try_from(row.try_get::("last_opened").unwrap()) + .unwrap(), + kind: parse_kind_from_row(kind, row).unwrap(), }) - .fetch_all(&mut connection) + .fetch_all(&database.connection) .await?; sheets.append(&mut sheets_of_kind); @@ -80,21 +103,38 @@ fn parse_kind_from_row(kind: SheetKindDiscriminants, row: SqliteRow) -> sqlx::Re SheetKindDiscriminants::Sheet => SheetKind::Sheet { name: row.try_get("name")?, composer_id: row.try_get("composer_id")?, - pdf: Pdf::from_row(row)?, + pdf: parse_pdf_from_row(&row)?, }, SheetKindDiscriminants::Orphan => SheetKind::Orphan { - pdf: Pdf::from_row(row)?, + pdf: parse_pdf_from_row(&row)?, }, SheetKindDiscriminants::Book => SheetKind::Book { name: row.try_get("name")?, composer_id: row.try_get("composer_id")?, - pdf: Pdf::from_row(row)?, - sheet_ids: todo!(), + pdf: parse_pdf_from_row(&row)?, + sheet_ids: sheet_ids_from_string(row.try_get("sheet_ids").unwrap()), }, SheetKindDiscriminants::BookSheet => SheetKind::BookSheet { + name: row.try_get("name")?, book_id: row.try_get("book_id")?, first_page: row.try_get("first_page")?, last_page: row.try_get("last_page")?, }, }) } + +fn sheet_ids_from_string(s: String) -> Vec { + s.trim() + .split(",") + .map(|s| i64::from_str_radix(s, 10).unwrap()) + .collect() +} + +fn parse_pdf_from_row(row: &SqliteRow) -> sqlx::Result { + // TODO: use get instead of try_get??? + Ok(Pdf { + path: PathBuf::from(row.try_get::("path").unwrap()), + file_size: row.try_get::("file_size")? as u64, + file_hash: row.try_get("file_hash")?, + }) +} diff --git a/src/ui/app.rs b/src/ui/app.rs index a36333e..ec48322 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -8,8 +8,9 @@ use relm4::{ use crate::{ database::Database, - ui::{mcdu::McduOutput, sheet_model::SheetModelType}, - AppInitData, + sheet::{I64DateTime, Sheet}, + sheet_dao, + ui::mcdu::McduOutput, }; use super::{ @@ -20,13 +21,18 @@ use super::{ pub struct AppModel { database: Database, mcdu: Controller, - sheets_and_files_listing: Controller, + sheets_listing: Controller, } #[derive(Debug)] pub enum AppInput { SearchStarted(String), - SheetPressed(SheetModelType), + SheetPressed(Sheet), +} + +pub struct AppInitData { + pub sheets: Vec, + pub database: Database, } #[relm4::component(pub, async)] @@ -49,7 +55,7 @@ impl AsyncComponent for AppModel { set_orientation: gtk::Orientation::Vertical, set_hexpand: true, gtk::ScrolledWindow { - model.sheets_and_files_listing.widget(), + model.sheets_listing.widget(), set_vexpand: true, set_hexpand: true, }, @@ -74,31 +80,19 @@ impl AsyncComponent for AppModel { McduOutput::SearchStarted(query) => AppInput::SearchStarted(query), }); - let mut orphan_files: Vec = init_data - .orphans - .into_iter() - .map(|orphan| SheetModelType::Orphan { orphan }) - .collect(); - orphan_files.sort_by(|a, b| a.cmp(b).reverse()); + let mut sheets = init_data.sheets; + sheets.sort_by(|a, b| a.cmp(b).reverse()); - let mut sheets_and_files: Vec = init_data - .sheets - .into_iter() - .map(|sheet| SheetModelType::Sheet { sheet }) - .chain(orphan_files) - .collect(); - sheets_and_files.sort_by(|a, b| a.cmp(b).reverse()); - - let sheets_and_files_listing = SheetListingModel::builder() - .launch(sheets_and_files) + let sheets_listing = SheetListingModel::builder() + .launch(sheets) .forward(sender.input_sender(), |response| { - AppInput::SheetPressed(response.sheet_model_type) + AppInput::SheetPressed(response.sheet) }); let model = AppModel { database: init_data.database, mcdu, - sheets_and_files_listing, + sheets_listing, }; let widgets = view_output!(); @@ -112,30 +106,19 @@ impl AsyncComponent for AppModel { _sender: AsyncComponentSender, _root: &Self::Root, ) { - // AppInput::SheetPressed(sheet) => opener::open(sheet).unwrap(), match message { AppInput::SearchStarted(query) => { - self.sheets_and_files_listing + self.sheets_listing .emit(SheetListingInput::Query(query.clone())); } - AppInput::SheetPressed(sheet_model_type) => { - opener::open(sheet_model_type.get_path()).unwrap(); - match sheet_model_type { - SheetModelType::Orphan { mut orphan } => { - orphan.last_opened = Utc::now(); - self.database - .update_orphan_last_opened(&orphan) - .await - .unwrap(); - } - SheetModelType::Sheet { mut sheet } => { - sheet.last_opened = Utc::now(); - self.database - .update_sheet_last_opened(&sheet) - .await - .unwrap(); - } - }; + AppInput::SheetPressed(sheet) => { + sheet.open_file(&self.database).await; + // TODO: updating does not work + let mut sheet = sheet; + sheet.last_opened = I64DateTime(Utc::now()); + sheet_dao::update_sheet_last_opened(&self.database, &sheet) + .await + .unwrap(); } } } diff --git a/src/ui/sheet_listing.rs b/src/ui/sheet_listing.rs index 7395703..cf0fc8a 100644 --- a/src/ui/sheet_listing.rs +++ b/src/ui/sheet_listing.rs @@ -4,7 +4,9 @@ use relm4::factory::FactoryVecDeque; use relm4::RelmListBoxExt; use relm4::{gtk, ComponentParts, ComponentSender, SimpleComponent}; -use super::sheet_model::{OnQueryUpdate, SheetModel, SheetModelType}; +use crate::sheet::Sheet; + +use super::sheet_model::{OnQueryUpdate, SheetModel}; pub struct SheetListingModel { sheets: FactoryVecDeque, @@ -18,12 +20,12 @@ pub enum SheetListingInput { #[derive(Debug)] pub struct SheetModelSelected { - pub sheet_model_type: SheetModelType, + pub sheet: Sheet, } #[relm4::component(pub)] impl SimpleComponent for SheetListingModel { - type Init = Vec; + type Init = Vec; type Input = SheetListingInput; type Output = SheetModelSelected; @@ -65,10 +67,10 @@ impl SimpleComponent for SheetListingModel { self.sheets.broadcast(OnQueryUpdate { query }); } SheetListingInput::ListBoxRowClicked(index) => { - let x = self.sheets.get(index as usize).unwrap(); + let sheet_model = self.sheets.get(index as usize).unwrap(); sender .output(SheetModelSelected { - sheet_model_type: x.sheet_model_type.clone(), + sheet: sheet_model.sheet.clone(), }) .unwrap(); } diff --git a/src/ui/sheet_model.rs b/src/ui/sheet_model.rs index c5a4e25..c616567 100644 --- a/src/ui/sheet_model.rs +++ b/src/ui/sheet_model.rs @@ -3,47 +3,13 @@ use std::{cmp::Ordering, path::Path}; use gtk::prelude::*; use relm4::prelude::*; -use crate::sheet::{OrphanFile, Sheet}; +use crate::sheet::Sheet; use super::sheet_listing::SheetListingInput; -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum SheetModelType { - Sheet { sheet: Sheet }, - Orphan { orphan: OrphanFile }, -} - -impl SheetModelType { - pub fn get_path(&self) -> &Path { - match self { - SheetModelType::Sheet { sheet } => sheet.path.as_path(), - SheetModelType::Orphan { orphan } => orphan.path.as_path(), - } - } -} -impl Ord for SheetModelType { - fn cmp(&self, other: &Self) -> Ordering { - let self_last_opened = match self { - SheetModelType::Sheet { sheet } => sheet.last_opened, - SheetModelType::Orphan { orphan } => orphan.last_opened, - }; - let other_last_opened = match other { - SheetModelType::Sheet { sheet } => sheet.last_opened, - SheetModelType::Orphan { orphan } => orphan.last_opened, - }; - self_last_opened.cmp(&other_last_opened) - } -} - -impl PartialOrd for SheetModelType { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - pub struct SheetModel { pub label: String, - pub sheet_model_type: SheetModelType, + pub sheet: Sheet, visible: bool, } @@ -57,7 +23,7 @@ pub struct OnQueryUpdate { #[relm4::factory(pub)] impl FactoryComponent for SheetModel { - type Init = SheetModelType; + type Init = Sheet; type ParentWidget = gtk::ListBox; type CommandOutput = (); type ParentInput = SheetListingInput; @@ -81,20 +47,16 @@ impl FactoryComponent for SheetModel { } fn init_model(value: Self::Init, _index: &DynamicIndex, _sender: FactorySender) -> Self { - let label = match &value { - SheetModelType::Sheet { sheet } => sheet.name.to_string(), - SheetModelType::Orphan { orphan } => orphan - .path - .file_name() - .unwrap() - .to_str() - .unwrap() - .to_string(), + let label = match &value.kind { + crate::sheet::SheetKind::Sheet { name, .. } => name, + crate::sheet::SheetKind::Orphan { pdf } => pdf.get_name(), + crate::sheet::SheetKind::Book { name, .. } => name, + crate::sheet::SheetKind::BookSheet { name, .. } => name, }; SheetModel { - label, - sheet_model_type: value, + label: label.to_string(), + sheet: value, visible: true, } }