2 Commits

Author SHA1 Message Date
d3f2375995 Complete major sheet refactoring
Now using books etc is far easier
2024-02-10 21:49:46 +01:00
4ddfd75b2a Start major sheet refactoring 2024-02-10 00:58:40 +01:00
10 changed files with 418 additions and 335 deletions

27
Cargo.lock generated
View File

@ -1756,6 +1756,12 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "rustversion"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.16" version = "1.0.16"
@ -1866,6 +1872,8 @@ dependencies = [
"relm4-components", "relm4-components",
"relm4-icons", "relm4-icons",
"sqlx", "sqlx",
"strum",
"strum_macros",
"tokio", "tokio",
"walkdir", "walkdir",
] ]
@ -2169,6 +2177,25 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strum"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "723b93e8addf9aa965ebe2d11da6d7540fa2283fcea14b3371ff055f7ba13f5f"
[[package]]
name = "strum_macros"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a3417fc93d76740d974a01654a09777cb500428cc874ca9f45edfe0c4d4cd18"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.48",
]
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "2.5.0" version = "2.5.0"

View File

@ -27,5 +27,9 @@ blake3 = "1.5.0"
dotenvy = "0.15.7" dotenvy = "0.15.7"
chrono = "0.4.33" chrono = "0.4.33"
strum = "0.26"
strum_macros = "0.26"
# strum = { version = "0.26", features = ["derive"] }
[profile.dev.package.sqlx-macros] [profile.dev.package.sqlx-macros]
opt-level = 3 opt-level = 3

View File

@ -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 sheets (id INTEGER PRIMARY KEY AUTOINCREMENT,
CREATE TABLE IF NOT EXISTS composers (id integer primary key autoincrement, name TEXT); 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);

View File

@ -3,10 +3,8 @@ use std::path::Path;
use log::debug; use log::debug;
use sqlx::{migrate::MigrateDatabase, Sqlite, SqlitePool}; use sqlx::{migrate::MigrateDatabase, Sqlite, SqlitePool};
use crate::sheet::{OrphanFile, Sheet};
pub struct Database { pub struct Database {
connection: SqlitePool, pub connection: SqlitePool,
} }
impl Database { impl Database {
@ -35,87 +33,4 @@ impl Database {
debug!("Connected to database"); debug!("Connected to database");
Ok(connection) Ok(connection)
} }
pub async fn _insert_sheet(&self, sheet: Sheet) -> sqlx::Result<()> {
sqlx::query(
"
INSERT INTO sheets (name, composer_id, path, file_size, file_hash, last_opened)
VALUES ($1, $2, $3, $4, $5, $6)
",
)
.bind(sheet.name)
.bind(sheet.composer_id)
.bind(sheet.path.to_str().unwrap().to_string())
.bind(sheet.file_size as i32)
.bind(sheet.file_hash)
.bind(sheet.last_opened.timestamp())
.execute(&self.connection)
.await
.map(|_| ())
}
pub async fn update_sheet_path(&self, sheet: &Sheet) -> sqlx::Result<()> {
sqlx::query("UPDATE sheets SET path = $1 WHERE id = $2")
.bind(sheet.path.to_str().unwrap().to_string())
.bind(sheet.id)
.execute(&self.connection)
.await
.map(|_| ())
}
pub async fn update_sheet_last_opened(&self, sheet: &Sheet) -> sqlx::Result<()> {
sqlx::query("UPDATE sheets SET last_opened = $1 WHERE id = $2")
.bind(sheet.last_opened.timestamp())
.bind(sheet.id)
.execute(&self.connection)
.await
.map(|_| ())
// TODO: check for success
}
pub async fn fetch_all_sheets(&self) -> sqlx::Result<Vec<Sheet>> {
sqlx::query_as::<_, Sheet>("SELECT * FROM sheets")
.fetch_all(&self.connection)
.await
}
pub async fn insert_orphan_file(&self, file: &OrphanFile) -> sqlx::Result<i64> {
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<Vec<OrphanFile>> {
sqlx::query_as::<_, OrphanFile>("SELECT * FROM orphan_files")
.fetch_all(&self.connection)
.await
}
} }

View File

@ -1,5 +1,6 @@
mod database; mod database;
mod sheet; mod sheet;
mod sheet_dao;
mod ui; mod ui;
use std::{ use std::{
@ -12,10 +13,10 @@ use database::Database;
use env_logger::Env; use env_logger::Env;
use log::{debug, error}; use log::{debug, error};
use relm4::RelmApp; use relm4::RelmApp;
use sheet::{OrphanFile, Sheet}; use sheet::{Pdf, Sheet};
use walkdir::WalkDir; use walkdir::WalkDir;
use crate::ui::app::AppModel; use crate::ui::app::{AppInitData, AppModel};
#[derive(Parser)] #[derive(Parser)]
#[command(author, version, about)] #[command(author, version, about)]
@ -35,38 +36,29 @@ async fn main() {
let database = Database::setup(cli.directory.join("database.sqlite")) let database = Database::setup(cli.directory.join("database.sqlite"))
.await .await
.unwrap(); .unwrap();
// database.insert_sheet(Sheet::new_debug()).await.unwrap(); let sheets = sheet_dao::fetch_all_sheets(&database).await.unwrap();
let sheets = database.fetch_all_sheets().await.unwrap();
let orphan_files = database.fetch_all_orphan_files().await.unwrap();
debug!("Validating sheets from database..."); 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 debug!("{}", validation_result.get_stats()); // TODO: handle invalidated files
for updated in validation_result.updated_sheets.iter() { for updated in validation_result.updated_sheets.iter() {
database.update_sheet_path(updated).await.unwrap(); sheet_dao::update_sheet_path(&database, updated)
} .await
for updated in validation_result.updated_orphan_files.iter() { .unwrap();
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);
} }
let mut sheets = validation_result.validated_sheets; let mut sheets = validation_result.validated_sheets;
sheets.append(&mut validation_result.updated_sheets); sheets.append(&mut validation_result.updated_sheets);
let app_init_data = AppInitData { debug!("Inserting unassigned files into orphan table...");
sheets, for unassigned in validation_result.unassigned_files {
orphans, let orphan = sheet_dao::insert_file_as_orphan(&database, unassigned)
database, .await
}; .unwrap();
sheets.push(orphan);
}
let app_init_data = AppInitData { sheets, database };
let app = RelmApp::new("de.frajul.sheet-organizer"); let app = RelmApp::new("de.frajul.sheet-organizer");
// Pass empty command line args to allow my own parsing // Pass empty command line args to allow my own parsing
@ -74,83 +66,44 @@ async fn main() {
.run_async::<AppModel>(app_init_data); .run_async::<AppModel>(app_init_data);
} }
pub struct AppInitData {
sheets: Vec<Sheet>,
orphans: Vec<OrphanFile>,
database: Database,
}
pub struct FileValidationResult { pub struct FileValidationResult {
validated_sheets: Vec<Sheet>, validated_sheets: Vec<Sheet>,
invalidated_sheets: Vec<Sheet>, invalidated_sheets: Vec<Sheet>,
updated_sheets: Vec<Sheet>, updated_sheets: Vec<Sheet>,
validated_orphan_files: Vec<OrphanFile>,
invalidated_orphan_files: Vec<OrphanFile>,
updated_orphan_files: Vec<OrphanFile>,
unassigned_files: Vec<PathBuf>, unassigned_files: Vec<PathBuf>,
} }
impl FileValidationResult { impl FileValidationResult {
fn get_stats(&self) -> String { 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_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( fn validate_sheet_files(sheets: Vec<Sheet>, dir: impl AsRef<Path>) -> FileValidationResult {
sheets: Vec<Sheet>,
orphan_files: Vec<OrphanFile>,
dir: impl AsRef<Path>,
) -> FileValidationResult {
// TODO: fix duplication
let (validated_sheets, mut invalidated_sheets): (Vec<_>, Vec<_>) = sheets let (validated_sheets, mut invalidated_sheets): (Vec<_>, Vec<_>) = sheets
.into_iter() .into_iter()
.partition(|sheet| sheet.validate_path(&sheet.path).unwrap_or(false)); .partition(|sheet| sheet.validate_own_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)
});
let mut updated_sheets = Vec::new(); let mut updated_sheets = Vec::new();
let mut updated_orphan_files = Vec::new();
let mut unassigned_files = Vec::new(); let mut unassigned_files = Vec::new();
for pdf_file in WalkDir::new(dir) // TODO: improve performance?
.into_iter() for pdf_file in find_all_pdfs_in_directory_recursive(dir) {
.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)
})
{
if let Some((i, _)) = invalidated_sheets if let Some((i, _)) = invalidated_sheets
.iter() .iter()
.enumerate() .enumerate()
.find(|(_, sheet)| sheet.validate_path(&pdf_file).unwrap_or(false)) .find(|(_, sheet)| sheet.validate_path(&pdf_file).unwrap_or(false))
{ {
let mut sheet = invalidated_sheets.remove(i); 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); updated_sheets.push(sheet);
} else if let Some((i, _)) = invalidated_orphan_files } else if !validated_sheets
.iter() .iter()
.enumerate() .any(|sheet| sheet.pdf_path_equal(&pdf_file))
.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)
{ {
unassigned_files.push(pdf_file); unassigned_files.push(pdf_file);
} }
@ -160,9 +113,19 @@ fn validate_sheet_files(
validated_sheets, validated_sheets,
invalidated_sheets, invalidated_sheets,
updated_sheets, updated_sheets,
validated_orphan_files,
invalidated_orphan_files,
updated_orphan_files,
unassigned_files, unassigned_files,
} }
} }
fn find_all_pdfs_in_directory_recursive(dir: impl AsRef<Path>) -> impl Iterator<Item = PathBuf> {
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)
})
}

View File

@ -1,49 +1,174 @@
use std::{ use std::{
cmp::Ordering,
ffi::{OsStr, OsString},
fs, fs,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use sqlx::{prelude::*, sqlite::SqliteRow};
// use sqlx::{FromRow, sqlite::SqliteRow, sqlx::Row};
use chrono::{DateTime, NaiveDateTime, Utc}; use chrono::{DateTime, NaiveDateTime, Utc};
use sqlx::database;
use strum_macros::{EnumDiscriminants, EnumIter};
use crate::{database::Database, sheet_dao};
pub trait PdfSheet {
fn get_pdf(&self) -> &Pdf;
}
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct Sheet { pub struct Sheet {
pub id: i64, pub id: i64,
pub name: String, pub last_opened: I64DateTime,
pub composer_id: i64, pub kind: SheetKind,
pub path: PathBuf,
pub file_size: u64,
pub file_hash: String,
pub last_opened: DateTime<Utc>,
} }
impl FromRow<'_, SqliteRow> for Sheet { #[derive(Debug, Clone, PartialEq, Eq, EnumDiscriminants)]
fn from_row(row: &SqliteRow) -> sqlx::Result<Self> { #[strum_discriminants(derive(EnumIter))]
Ok(Self { pub enum SheetKind {
id: row.try_get("id")?, Sheet {
name: row.try_get("name")?, pdf: Pdf,
composer_id: row.try_get("composer_id")?, name: String,
path: row.try_get::<&str, _>("path")?.into(), composer_id: i64,
file_size: row.try_get::<i64, _>("file_size")? as u64, },
file_hash: row.try_get("file_hash")?, Orphan {
last_opened: NaiveDateTime::from_timestamp_opt( pdf: Pdf,
row.try_get::<i64, _>("last_opened")?, },
0, Book {
) pdf: Pdf,
.unwrap() name: String,
.and_utc(), composer_id: i64,
sheet_ids: Vec<i64>,
},
BookSheet {
name: String,
book_id: i64,
first_page: i64,
last_page: i64,
},
}
impl PartialOrd for Sheet {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Sheet {
fn cmp(&self, other: &Self) -> Ordering {
self.last_opened.cmp(&other.last_opened)
}
}
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<Path>) -> 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<bool> {
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<Path>) -> std::io::Result<bool> {
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?
})
}
}
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)] #[derive(sqlx::FromRow)]
pub struct Composer { pub struct Composer {
pub id: i64, id: i64,
pub name: String, name: String,
} }
impl Sheet { #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct I64DateTime(pub DateTime<Utc>);
impl TryFrom<i64> for I64DateTime {
type Error = String;
fn try_from(value: i64) -> Result<Self, Self::Error> {
Ok(I64DateTime(
NaiveDateTime::from_timestamp_opt(value, 0)
.ok_or("Failed converting i64 to DateTime")?
.and_utc(),
))
}
}
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<Path>) -> std::io::Result<bool> { pub fn validate_path(&self, path: impl AsRef<Path>) -> std::io::Result<bool> {
// First compare file size since it is faster than hashing // First compare file size since it is faster than hashing
let file_size = fs::metadata(path.as_ref())?.len(); let file_size = fs::metadata(path.as_ref())?.len();
@ -59,24 +184,7 @@ impl Sheet {
} }
} }
impl OrphanFile { impl TryFrom<PathBuf> for Pdf {
// TODO: fix duplication
pub fn validate_path(&self, path: impl AsRef<Path>) -> std::io::Result<bool> {
// First compare file size since it is faster than hashing
let file_size = fs::metadata(path.as_ref())?.len();
if file_size == self.file_size {
let file_content = fs::read(path.as_ref())?;
let file_hash = blake3::hash(&file_content);
if file_hash.to_string() == self.file_hash {
return Ok(true);
}
}
Ok(false)
}
}
impl TryFrom<PathBuf> for OrphanFile {
type Error = std::io::Error; type Error = std::io::Error;
fn try_from(path: PathBuf) -> Result<Self, Self::Error> { fn try_from(path: PathBuf) -> Result<Self, Self::Error> {
@ -84,38 +192,10 @@ impl TryFrom<PathBuf> for OrphanFile {
let file_content = fs::read(path.as_path())?; let file_content = fs::read(path.as_path())?;
let file_hash = blake3::hash(&file_content).to_string(); let file_hash = blake3::hash(&file_content).to_string();
Ok(OrphanFile { Ok(Pdf {
id: -1,
path, path,
file_size, file_size,
file_hash, file_hash,
last_opened: DateTime::default(),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OrphanFile {
pub id: i64,
pub path: PathBuf,
pub file_size: u64,
pub file_hash: String,
pub last_opened: DateTime<Utc>,
}
impl FromRow<'_, SqliteRow> for OrphanFile {
fn from_row(row: &SqliteRow) -> sqlx::Result<Self> {
Ok(Self {
id: row.try_get("id")?,
path: row.try_get::<&str, _>("path")?.into(),
file_size: row.try_get::<i64, _>("file_size")? as u64,
file_hash: row.try_get("file_hash")?,
last_opened: NaiveDateTime::from_timestamp_opt(
row.try_get::<i64, _>("last_opened")?,
0,
)
.unwrap()
.and_utc(),
}) })
} }
} }

140
src/sheet_dao.rs Normal file
View File

@ -0,0 +1,140 @@
use std::path::{Path, PathBuf};
use strum::{EnumMessage, IntoEnumIterator};
use strum_macros::{EnumDiscriminants, EnumIter, EnumString};
use chrono::{DateTime, NaiveDateTime, Utc};
use sqlx::{sqlite::SqliteRow, Row, SqlitePool};
use crate::{
database::Database,
sheet::{I64DateTime, Pdf, Sheet, SheetKind, SheetKindDiscriminants},
};
pub async fn insert_file_as_orphan(
database: &Database,
file: impl AsRef<Path>,
) -> sqlx::Result<Sheet> {
let pdf = Pdf::try_from(file.as_ref().to_path_buf()).unwrap();
let last_opened = DateTime::<Utc>::default();
let result = sqlx::query(
"
INSERT INTO orphans (path, file_size, file_hash, last_opened)
VALUES ($1, $2, $3, $4)
",
)
.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
.unwrap();
let id = result.last_insert_rowid();
Ok(Sheet {
id,
last_opened: I64DateTime(last_opened),
kind: SheetKind::Orphan { pdf },
})
}
pub async fn find_path_of_book(database: &Database, book_id: &i64) -> sqlx::Result<PathBuf> {
sqlx::query("SELECT path FROM books WHERE id = $1")
.bind(book_id)
.map(|row: SqliteRow| PathBuf::try_from(row.try_get::<String, _>("path").unwrap()).unwrap())
.fetch_one(&database.connection)
.await
}
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(i64::from(&sheet.last_opened))
.bind(sheet.id)
.execute(&database.connection)
.await
.map(|_| ())
}
pub async fn fetch_all_sheets(database: &Database) -> sqlx::Result<Vec<Sheet>> {
let mut sheets: Vec<Sheet> = Vec::new();
for kind in SheetKindDiscriminants::iter() {
let table = kind.get_database_table_name();
let mut sheets_of_kind = sqlx::query(&format!("SELECT * FROM {}", table))
.map(|row: SqliteRow| Sheet {
id: row.try_get("id").unwrap(),
last_opened: I64DateTime::try_from(row.try_get::<i64, _>("last_opened").unwrap())
.unwrap(),
kind: parse_kind_from_row(kind, row).unwrap(),
})
.fetch_all(&database.connection)
.await?;
sheets.append(&mut sheets_of_kind);
}
Ok(sheets)
}
fn parse_kind_from_row(kind: SheetKindDiscriminants, row: SqliteRow) -> sqlx::Result<SheetKind> {
Ok(match kind {
SheetKindDiscriminants::Sheet => SheetKind::Sheet {
name: row.try_get("name")?,
composer_id: row.try_get("composer_id")?,
pdf: parse_pdf_from_row(&row)?,
},
SheetKindDiscriminants::Orphan => SheetKind::Orphan {
pdf: parse_pdf_from_row(&row)?,
},
SheetKindDiscriminants::Book => SheetKind::Book {
name: row.try_get("name")?,
composer_id: row.try_get("composer_id")?,
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<i64> {
s.trim()
.split(",")
.map(|s| i64::from_str_radix(s, 10).unwrap())
.collect()
}
fn parse_pdf_from_row(row: &SqliteRow) -> sqlx::Result<Pdf> {
// TODO: use get instead of try_get???
Ok(Pdf {
path: PathBuf::from(row.try_get::<String, _>("path").unwrap()),
file_size: row.try_get::<i64, _>("file_size")? as u64,
file_hash: row.try_get("file_hash")?,
})
}

View File

@ -8,8 +8,9 @@ use relm4::{
use crate::{ use crate::{
database::Database, database::Database,
ui::{mcdu::McduOutput, sheet_model::SheetModelType}, sheet::{I64DateTime, Sheet},
AppInitData, sheet_dao,
ui::mcdu::McduOutput,
}; };
use super::{ use super::{
@ -20,13 +21,18 @@ use super::{
pub struct AppModel { pub struct AppModel {
database: Database, database: Database,
mcdu: Controller<McduModel>, mcdu: Controller<McduModel>,
sheets_and_files_listing: Controller<SheetListingModel>, sheets_listing: Controller<SheetListingModel>,
} }
#[derive(Debug)] #[derive(Debug)]
pub enum AppInput { pub enum AppInput {
SearchStarted(String), SearchStarted(String),
SheetPressed(SheetModelType), SheetPressed(Sheet),
}
pub struct AppInitData {
pub sheets: Vec<Sheet>,
pub database: Database,
} }
#[relm4::component(pub, async)] #[relm4::component(pub, async)]
@ -49,7 +55,7 @@ impl AsyncComponent for AppModel {
set_orientation: gtk::Orientation::Vertical, set_orientation: gtk::Orientation::Vertical,
set_hexpand: true, set_hexpand: true,
gtk::ScrolledWindow { gtk::ScrolledWindow {
model.sheets_and_files_listing.widget(), model.sheets_listing.widget(),
set_vexpand: true, set_vexpand: true,
set_hexpand: true, set_hexpand: true,
}, },
@ -74,31 +80,19 @@ impl AsyncComponent for AppModel {
McduOutput::SearchStarted(query) => AppInput::SearchStarted(query), McduOutput::SearchStarted(query) => AppInput::SearchStarted(query),
}); });
let mut orphan_files: Vec<SheetModelType> = init_data let mut sheets = init_data.sheets;
.orphans sheets.sort_by(|a, b| a.cmp(b).reverse());
.into_iter()
.map(|orphan| SheetModelType::Orphan { orphan })
.collect();
orphan_files.sort_by(|a, b| a.cmp(b).reverse());
let mut sheets_and_files: Vec<SheetModelType> = init_data let sheets_listing = SheetListingModel::builder()
.sheets .launch(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)
.forward(sender.input_sender(), |response| { .forward(sender.input_sender(), |response| {
AppInput::SheetPressed(response.sheet_model_type) AppInput::SheetPressed(response.sheet)
}); });
let model = AppModel { let model = AppModel {
database: init_data.database, database: init_data.database,
mcdu, mcdu,
sheets_and_files_listing, sheets_listing,
}; };
let widgets = view_output!(); let widgets = view_output!();
@ -112,30 +106,19 @@ impl AsyncComponent for AppModel {
_sender: AsyncComponentSender<Self>, _sender: AsyncComponentSender<Self>,
_root: &Self::Root, _root: &Self::Root,
) { ) {
// AppInput::SheetPressed(sheet) => opener::open(sheet).unwrap(),
match message { match message {
AppInput::SearchStarted(query) => { AppInput::SearchStarted(query) => {
self.sheets_and_files_listing self.sheets_listing
.emit(SheetListingInput::Query(query.clone())); .emit(SheetListingInput::Query(query.clone()));
} }
AppInput::SheetPressed(sheet_model_type) => { AppInput::SheetPressed(sheet) => {
opener::open(sheet_model_type.get_path()).unwrap(); sheet.open_file(&self.database).await;
match sheet_model_type { // TODO: updating does not work
SheetModelType::Orphan { mut orphan } => { let mut sheet = sheet;
orphan.last_opened = Utc::now(); sheet.last_opened = I64DateTime(Utc::now());
self.database sheet_dao::update_sheet_last_opened(&self.database, &sheet)
.update_orphan_last_opened(&orphan) .await
.await .unwrap();
.unwrap();
}
SheetModelType::Sheet { mut sheet } => {
sheet.last_opened = Utc::now();
self.database
.update_sheet_last_opened(&sheet)
.await
.unwrap();
}
};
} }
} }
} }

View File

@ -4,7 +4,9 @@ use relm4::factory::FactoryVecDeque;
use relm4::RelmListBoxExt; use relm4::RelmListBoxExt;
use relm4::{gtk, ComponentParts, ComponentSender, SimpleComponent}; 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 { pub struct SheetListingModel {
sheets: FactoryVecDeque<SheetModel>, sheets: FactoryVecDeque<SheetModel>,
@ -18,12 +20,12 @@ pub enum SheetListingInput {
#[derive(Debug)] #[derive(Debug)]
pub struct SheetModelSelected { pub struct SheetModelSelected {
pub sheet_model_type: SheetModelType, pub sheet: Sheet,
} }
#[relm4::component(pub)] #[relm4::component(pub)]
impl SimpleComponent for SheetListingModel { impl SimpleComponent for SheetListingModel {
type Init = Vec<SheetModelType>; type Init = Vec<Sheet>;
type Input = SheetListingInput; type Input = SheetListingInput;
type Output = SheetModelSelected; type Output = SheetModelSelected;
@ -65,10 +67,10 @@ impl SimpleComponent for SheetListingModel {
self.sheets.broadcast(OnQueryUpdate { query }); self.sheets.broadcast(OnQueryUpdate { query });
} }
SheetListingInput::ListBoxRowClicked(index) => { SheetListingInput::ListBoxRowClicked(index) => {
let x = self.sheets.get(index as usize).unwrap(); let sheet_model = self.sheets.get(index as usize).unwrap();
sender sender
.output(SheetModelSelected { .output(SheetModelSelected {
sheet_model_type: x.sheet_model_type.clone(), sheet: sheet_model.sheet.clone(),
}) })
.unwrap(); .unwrap();
} }

View File

@ -3,47 +3,13 @@ use std::{cmp::Ordering, path::Path};
use gtk::prelude::*; use gtk::prelude::*;
use relm4::prelude::*; use relm4::prelude::*;
use crate::sheet::{OrphanFile, Sheet}; use crate::sheet::Sheet;
use super::sheet_listing::SheetListingInput; 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<Ordering> {
Some(self.cmp(other))
}
}
pub struct SheetModel { pub struct SheetModel {
pub label: String, pub label: String,
pub sheet_model_type: SheetModelType, pub sheet: Sheet,
visible: bool, visible: bool,
} }
@ -57,7 +23,7 @@ pub struct OnQueryUpdate {
#[relm4::factory(pub)] #[relm4::factory(pub)]
impl FactoryComponent for SheetModel { impl FactoryComponent for SheetModel {
type Init = SheetModelType; type Init = Sheet;
type ParentWidget = gtk::ListBox; type ParentWidget = gtk::ListBox;
type CommandOutput = (); type CommandOutput = ();
type ParentInput = SheetListingInput; type ParentInput = SheetListingInput;
@ -81,20 +47,16 @@ impl FactoryComponent for SheetModel {
} }
fn init_model(value: Self::Init, _index: &DynamicIndex, _sender: FactorySender<Self>) -> Self { fn init_model(value: Self::Init, _index: &DynamicIndex, _sender: FactorySender<Self>) -> Self {
let label = match &value { let label = match &value.kind {
SheetModelType::Sheet { sheet } => sheet.name.to_string(), crate::sheet::SheetKind::Sheet { name, .. } => name,
SheetModelType::Orphan { orphan } => orphan crate::sheet::SheetKind::Orphan { pdf } => pdf.get_name(),
.path crate::sheet::SheetKind::Book { name, .. } => name,
.file_name() crate::sheet::SheetKind::BookSheet { name, .. } => name,
.unwrap()
.to_str()
.unwrap()
.to_string(),
}; };
SheetModel { SheetModel {
label, label: label.to_string(),
sheet_model_type: value, sheet: value,
visible: true, visible: true,
} }
} }