diff --git a/src/app.rs b/src/app.rs index 1c1a50d4..470ae7d7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -23,7 +23,7 @@ impl App { // Constructors /// Build new `Self` - pub fn build(profile: Profile) -> Self { + pub fn build(profile: Profile) -> Result { // Init GTK let application = Application::builder() .application_id(APPLICATION_ID) @@ -76,54 +76,56 @@ impl App { let browser = browser.clone(); let profile = profile.clone(); move |_| { - // Init writable connection - match profile.database.connection.write() { - Ok(mut connection) => { - // Create transaction - match connection.transaction() { - Ok(transaction) => { - match database::select(&transaction) { - Ok(records) => { - // Cleanup previous session records - for record in records { - match database::delete(&transaction, record.id) { + match profile.save() { + Ok(_) => match profile.database.connection.write() { + Ok(mut connection) => { + // Create transaction + match connection.transaction() { + Ok(transaction) => { + match database::select(&transaction) { + Ok(records) => { + // Cleanup previous session records + for record in records { + match database::delete(&transaction, record.id) { + Ok(_) => { + // Delegate clean action to childs + if let Err(e) = + browser.clean(&transaction, record.id) + { + todo!("{e}") + } + } + Err(e) => todo!("{e}"), + } + } + + // Save current session to DB + match database::insert(&transaction) { Ok(_) => { - // Delegate clean action to childs - if let Err(e) = - browser.clean(&transaction, record.id) - { + // Delegate save action to childs + if let Err(e) = browser.save( + &transaction, + database::last_insert_id(&transaction), + ) { todo!("{e}") } } Err(e) => todo!("{e}"), } } - - // Save current session to DB - match database::insert(&transaction) { - Ok(_) => { - // Delegate save action to childs - if let Err(e) = browser.save( - &transaction, - database::last_insert_id(&transaction), - ) { - todo!("{e}") - } - } - Err(e) => todo!("{e}"), - } + Err(e) => todo!("{e}"), } - Err(e) => todo!("{e}"), - } - // Confirm changes - if let Err(e) = transaction.commit() { - todo!("{e}") + // Confirm changes + if let Err(e) = transaction.commit() { + todo!("{e}") + } } + Err(e) => todo!("{e}"), } - Err(e) => todo!("{e}"), } - } + Err(e) => todo!("{e}"), + }, Err(e) => todo!("{e}"), } } @@ -254,10 +256,10 @@ impl App { } // Return activated App struct - Self { + Ok(Self { profile: profile.clone(), application, - } + }) } // Actions diff --git a/src/main.rs b/src/main.rs index 4f95bb08..66006441 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,9 +14,12 @@ fn main() -> ExitCode { } match Profile::init() { - Ok(profile) => match App::build(profile).run() { - Ok(app) => return app, - Err(e) => eprintln!("Failed to initialize application: {e}"), + Ok(profile) => match App::build(profile) { + Ok(app) => match app.run() { + Ok(run) => return run, + Err(e) => eprintln!("Failed to run application: {e}"), + }, + Err(e) => eprintln!("Failed to build application: {e}"), }, Err(e) => eprintln!("Failed to initialize profile: {e}"), } diff --git a/src/profile.rs b/src/profile.rs index 062127b4..5bcc7835 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -79,7 +79,7 @@ impl Profile { // Init components let bookmark = Rc::new(Bookmark::build(&connection, &profile_id)?); - let history = Rc::new(History::build(&connection, &profile_id)); + let history = Rc::new(History::build(&connection, &profile_id)?); let search = Rc::new(Search::build(&connection, &profile_id)?); let identity = Rc::new(Identity::build(&connection, &profile_id)?); @@ -93,6 +93,12 @@ impl Profile { config_path, }) } + + // Actions + + pub fn save(&self) -> Result<()> { + self.history.save() + } } pub fn migrate(tx: &Transaction) -> Result<()> { @@ -103,8 +109,7 @@ pub fn migrate(tx: &Transaction) -> Result<()> { bookmark::migrate(tx)?; identity::migrate(tx)?; search::migrate(tx)?; - // @TODO not in use yet - // history::migrate(tx)?; + history::migrate(tx)?; // Success Ok(()) diff --git a/src/profile/bookmark/database.rs b/src/profile/bookmark/database.rs index e3c4f28a..feb35adb 100644 --- a/src/profile/bookmark/database.rs +++ b/src/profile/bookmark/database.rs @@ -63,7 +63,8 @@ pub fn init(tx: &Transaction) -> Result { `request` TEXT NOT NULL, `title` TEXT NULL, - FOREIGN KEY (`profile_id`) REFERENCES `profile` (`id`) + FOREIGN KEY (`profile_id`) REFERENCES `profile` (`id`), + UNIQUE (`profile_id`, `request`) )", [], )?) diff --git a/src/profile/history.rs b/src/profile/history.rs index 57f1e19b..31e06ed5 100644 --- a/src/profile/history.rs +++ b/src/profile/history.rs @@ -1,14 +1,17 @@ -// mod database; +mod database; mod item; mod memory; +use anyhow::Result; +use database::Database; use gtk::glib::GString; -use item::Item; +use item::{Event, Item}; use memory::Memory; -use sqlite::Connection; +use sqlite::{Connection, Transaction}; use std::{cell::RefCell, rc::Rc, sync::RwLock}; pub struct History { + database: Database, // permanent storage memory: RefCell, // fast search index } @@ -16,12 +19,35 @@ impl History { // Constructors /// Create new `Self` - pub fn build(_connection: &Rc>, _profile_id: &Rc) -> Self { + pub fn build(connection: &Rc>, profile_id: &Rc) -> Result { // Init children components + let database = Database::build(connection, profile_id); let memory = RefCell::new(Memory::new()); + for item in database.records(None, None)? { + memory.borrow_mut().add(item) + } + // Return new `Self` - Self { memory } + Ok(Self { database, memory }) + } + + // Actions + + pub fn save(&self) -> Result<()> { + for item in self.memory.borrow().items() { + if !item.is_saved { + match item.id { + Some(_) => { + self.database.update(item)?; + } + None => { + self.database.add(item)?; + } + } + } + } + Ok(()) } // Actions @@ -30,7 +56,14 @@ impl History { pub fn open(&self, request: GString, title: Option) { let mut memory = self.memory.borrow_mut(); if !memory.open(&request) { - memory.add(Item::init(request, title)) + memory.add(Item { + id: None, + request, + title, + opened: Event::new(), + closed: None, + is_saved: false, + }) } } @@ -56,3 +89,16 @@ impl History { self.memory.borrow().contains_request(request, limit) } } + +// Tools + +pub fn migrate(tx: &Transaction) -> Result<()> { + // Migrate self components + database::init(tx)?; + + // Delegate migration to childs + // nothing yet.. + + // Success + Ok(()) +} diff --git a/src/profile/history/database.rs b/src/profile/history/database.rs new file mode 100644 index 00000000..2adf7c8a --- /dev/null +++ b/src/profile/history/database.rs @@ -0,0 +1,183 @@ +use super::{item::Event, Item}; +use anyhow::Result; +use gtk::glib::DateTime; +use sqlite::{Connection, Transaction}; +use std::{rc::Rc, sync::RwLock}; + +pub struct Database { + connection: Rc>, + profile_id: Rc, // multi-profile relationship +} + +impl Database { + // Constructors + + /// Create new `Self` + pub fn build(connection: &Rc>, profile_id: &Rc) -> Self { + Self { + connection: connection.clone(), + profile_id: profile_id.clone(), + } + } + + // Getters + + /// Get history records from database with optional filter by `request` + pub fn records(&self, request: Option<&str>, title: Option<&str>) -> Result> { + let readable = self.connection.read().unwrap(); // @TODO + let tx = readable.unchecked_transaction()?; + select(&tx, *self.profile_id, request, title) + } + + // Actions + + /// Create new history record in database + /// * return last insert ID on success + pub fn add(&self, item: &Item) -> Result { + let mut writable = self.connection.write().unwrap(); // @TODO + let tx = writable.transaction()?; + let id = insert(&tx, *self.profile_id, item)?; + tx.commit()?; + Ok(id) + } + + pub fn update(&self, item: &Item) -> Result { + let mut writable = self.connection.write().unwrap(); // @TODO + let tx = writable.transaction()?; + let affected = update(&tx, *self.profile_id, item)?; + tx.commit()?; + Ok(affected) + } +} + +// Low-level DB API + +pub fn init(tx: &Transaction) -> Result { + Ok(tx.execute( + "CREATE TABLE IF NOT EXISTS `profile_history` + ( + `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + `profile_id` INTEGER NOT NULL, + `opened_time` INTEGER NOT NULL, + `opened_count` INTEGER NOT NULL, + `closed_time` INTEGER NULL, + `closed_count` INTEGER NULL, + `request` TEXT NOT NULL, + `title` TEXT NULL, + + FOREIGN KEY (`profile_id`) REFERENCES `profile` (`id`), + UNIQUE (`profile_id`, `request`) + )", + [], + )?) +} + +pub fn insert(tx: &Transaction, profile_id: i64, item: &Item) -> Result { + tx.execute( + "INSERT INTO `profile_history` ( + `profile_id`, + `opened_time`, + `opened_count`, + `closed_time`, + `closed_count`, + `request`, + `title` + ) VALUES (?, ?, ?, ?, ?, ?, ?)", + ( + profile_id, + item.opened.time.to_unix(), + item.opened.count as i64, + item.closed.as_ref().map(|closed| closed.time.to_unix()), + item.closed.as_ref().map(|closed| closed.count as i64), + item.request.as_str(), + item.title.as_deref(), + ), + )?; + Ok(tx.last_insert_rowid()) +} + +pub fn update(tx: &Transaction, profile_id: i64, item: &Item) -> Result { + Ok(tx.execute( + "UPDATE `profile_history` + SET `opened_time` = ?, + `opened_count` = ?, + `closed_time` = ?, + `closed_count` = ?, + `request` = ?, + `title` = ? + WHERE `id` = ? AND `profile_id` = ?", + ( + item.opened.time.to_unix(), + item.opened.count as i64, + item.closed.as_ref().map(|closed| closed.time.to_unix()), + item.closed.as_ref().map(|closed| closed.count as i64), + item.request.as_str(), + item.title.as_deref(), + item.id.unwrap(), + profile_id, + ), + )?) +} + +pub fn select( + tx: &Transaction, + profile_id: i64, + request: Option<&str>, + title: Option<&str>, +) -> Result> { + let mut stmt = tx.prepare( + "SELECT + `id`, + `profile_id`, + `opened_time`, + `opened_count`, + `closed_time`, + `closed_count`, + `request`, + `title` + FROM `profile_history` + WHERE `profile_id` = ? AND (`request` LIKE ? OR `title` LIKE ?)", + )?; + + let result = stmt.query_map( + (profile_id, request.unwrap_or("%"), title.unwrap_or("%")), + |row| { + Ok(Item { + id: row.get(0)?, + //profile_id: row.get(1)?, + opened: Event { + time: DateTime::from_unix_local(row.get(2)?).unwrap(), + count: row.get(3)?, + }, + closed: closed(row.get(4)?, row.get(5)?), + request: row.get::<_, String>(6)?.into(), + title: row.get::<_, Option>(7)?.map(|s| s.into()), + is_saved: true, + }) + }, + )?; + + let mut items = Vec::new(); + + for record in result { + let item = record?; + items.push(item); + } + + Ok(items) +} + +// Tools + +fn closed(time: Option, count: Option) -> Option { + if let Some(t) = time { + if let Some(c) = count { + return Some(Event { + time: DateTime::from_unix_local(t).unwrap(), + count: c as usize, + }); + } + panic!() + } + None +} diff --git a/src/profile/history/item.rs b/src/profile/history/item.rs index 6eae228f..815a4f9d 100644 --- a/src/profile/history/item.rs +++ b/src/profile/history/item.rs @@ -1,6 +1,6 @@ -mod event; +pub mod event; +pub use event::Event; -use event::Event; use gtk::glib::GString; #[derive(Clone)] @@ -18,25 +18,17 @@ pub struct Item { /// Collect `Item` close events /// * used in recently closed pages menu and history page pub closed: Option, + /// Mark in-memory `Item` as saved + /// * used for database update (e.g. on app close) + pub is_saved: bool, } impl Item { - // Constructors - pub fn init(request: GString, title: Option) -> Self { - Self { - id: None, - request, - title, - opened: Event::new(), - closed: None, - } - } - - // Actions pub fn open(&mut self) { - self.opened.pulse() + self.opened.pulse(); + self.is_saved = false } pub fn close(&mut self) { @@ -44,5 +36,6 @@ impl Item { Some(ref mut closed) => closed.pulse(), None => self.closed = Some(Event::new()), } + self.is_saved = false } } diff --git a/src/profile/history/memory.rs b/src/profile/history/memory.rs index 2edabcf8..7b5188b6 100644 --- a/src/profile/history/memory.rs +++ b/src/profile/history/memory.rs @@ -21,9 +21,9 @@ impl Memory { /// Update `opened` time for given `request` /// * return `false` if the `request` not found in memory index pub fn open(&mut self, request: &str) -> bool { - for record in &mut self.0 { - if record.request == request { - record.open(); + for item in &mut self.0 { + if item.request == request { + item.open(); return true; } } @@ -32,9 +32,9 @@ impl Memory { /// Update `closed` time for given `request` pub fn close(&mut self, request: &str) { - for record in &mut self.0 { - if record.request == request { - record.close(); + for item in &mut self.0 { + if item.request == request { + item.close(); return; } } @@ -102,6 +102,10 @@ impl Memory { } items } + + pub fn items(&self) -> &Vec { + &self.0 + } } impl Default for Memory {