diff --git a/README.md b/README.md index d4e72638..c7e282a8 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ GTK 4 / Libadwaita client written in Rust * [x] Unsupported content type downloads * [ ] History * [ ] Browser window - * [ ] Recently closed + * [x] Recently closed * [ ] Recently visited * [ ] Proxy * [ ] Session diff --git a/src/app/browser/window/header/bar/menu.rs b/src/app/browser/window/header/bar/menu.rs index ff4dd352..65b76b72 100644 --- a/src/app/browser/window/header/bar/menu.rs +++ b/src/app/browser/window/header/bar/menu.rs @@ -8,8 +8,9 @@ use std::rc::Rc; // Config options -const RECENT_BOOKMARKS: usize = 50; const LABEL_MAX_LENGTH: usize = 32; +const RECENT_BOOKMARKS: usize = 50; +const RECENTLY_CLOSED: usize = 50; pub struct Menu { pub menu_button: MenuButton, @@ -131,6 +132,16 @@ impl Menu { main.append_submenu(Some("Bookmarks"), &main_bookmarks); + // Main > History + let main_history = gio::Menu::new(); + + // Main > History > Recently closed + // * menu items dynamically generated using profile memory pool and `set_create_popup_func` + let main_history_closed = gio::Menu::new(); + main_history.append_submenu(Some("Closed tabs"), &main_history_closed); + + main.append_submenu(Some("History"), &main_history); + // Main > Tool let main_tool = gio::Menu::new(); @@ -176,6 +187,7 @@ impl Menu { let main_bookmarks = main_bookmarks.clone(); let window_action = window_action.clone(); move |_| { + // Bookmarks main_bookmarks.remove_all(); for request in profile.bookmark.memory.recent(RECENT_BOOKMARKS) { let menu_item = gio::MenuItem::new(Some(&label(&request, LABEL_MAX_LENGTH)), None); @@ -191,6 +203,19 @@ impl Menu { // if profile.bookmark.memory.total() > RECENT_BOOKMARKS { // @TODO // } + + // History + main_history_closed.remove_all(); + for request in profile.history.memory.closed.recent(RECENTLY_CLOSED) { + let menu_item = gio::MenuItem::new(Some(&label(&request, LABEL_MAX_LENGTH)), None); + menu_item.set_action_and_target_value(Some(&format!( + "{}.{}", + window_action.id, + window_action.open.simple_action.name() + )), Some(&request.to_variant())); + + main_history_closed.append_item(&menu_item); + } } }); diff --git a/src/app/browser/window/tab.rs b/src/app/browser/window/tab.rs index a9f2beb7..a930c730 100644 --- a/src/app/browser/window/tab.rs +++ b/src/app/browser/window/tab.rs @@ -15,8 +15,8 @@ use crate::app::browser::{ }; use crate::Profile; use gtk::{ - glib::{GString, Propagation}, - prelude::WidgetExt, + glib::{DateTime, GString, Propagation}, + prelude::{EditableExt, WidgetExt}, }; use sqlite::Transaction; use std::{cell::RefCell, collections::HashMap, rc::Rc}; @@ -92,6 +92,7 @@ impl Tab { widget.tab_view.connect_close_page({ let index = index.clone(); + let profile = profile.clone(); move |_, item| { // Get index ID by keyword saved match item.keyword() { @@ -100,7 +101,14 @@ impl Tab { panic!("Tab index can not be empty!") } // Cleanup HashMap index - index.borrow_mut().remove(&id); + if let Some(item) = index.borrow_mut().remove(&id) { + // Add history record into profile memory pool + // * this action allows to recover recently closed tab (e.g. from the main menu) + profile.history.memory.closed.add( + item.page.navigation.request.widget.entry.text(), + DateTime::now_local().unwrap().to_unix(), + ); + } } None => panic!("Undefined tab index!"), } diff --git a/src/profile.rs b/src/profile.rs index 6405a965..2bc27e12 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -1,10 +1,11 @@ mod bookmark; mod database; -//mod history; +mod history; mod identity; use bookmark::Bookmark; use database::Database; +use history::History; use identity::Identity; use gtk::glib::{user_config_dir, DateTime}; @@ -20,6 +21,7 @@ const DB_NAME: &str = "database.sqlite3"; pub struct Profile { pub bookmark: Rc, pub database: Rc, + pub history: Rc, pub identity: Rc, pub config_path: PathBuf, } @@ -87,7 +89,7 @@ impl Profile { } // unlock database // Init model - let database = Rc::new(Database::new(connection.clone())); + let database = Rc::new(Database::build(&connection)); // Get active profile or create new one let profile_id = Rc::new(match database.active().unwrap() { @@ -98,11 +100,10 @@ impl Profile { }, }); - // Init bookmark component @TODO handle errors - let bookmark = Rc::new(Bookmark::new(connection.clone(), profile_id.clone())); - - // Init identity component - let identity = Rc::new(match Identity::new(connection, profile_id) { + // Init components + let bookmark = Rc::new(Bookmark::build(&connection, &profile_id)); + let history = Rc::new(History::build(&connection, &profile_id)); + let identity = Rc::new(match Identity::build(&connection, &profile_id) { Ok(result) => result, Err(e) => todo!("{:?}", e.to_string()), }); @@ -110,8 +111,9 @@ impl Profile { // Result Self { bookmark, - identity, database, + history, + identity, config_path, } } diff --git a/src/profile/bookmark.rs b/src/profile/bookmark.rs index e01d710a..cce2fb41 100644 --- a/src/profile/bookmark.rs +++ b/src/profile/bookmark.rs @@ -19,7 +19,7 @@ impl Bookmark { // Constructors /// Create new `Self` - pub fn new(connection: Rc>, profile_id: Rc) -> Self { + pub fn build(connection: &Rc>, profile_id: &Rc) -> Self { // Init children components let database = Rc::new(Database::new(connection, profile_id)); let memory = Rc::new(Memory::new()); diff --git a/src/profile/bookmark/database.rs b/src/profile/bookmark/database.rs index f91e2900..02fe36fd 100644 --- a/src/profile/bookmark/database.rs +++ b/src/profile/bookmark/database.rs @@ -18,10 +18,10 @@ impl Database { // Constructors /// Create new `Self` - pub fn new(connection: Rc>, profile_id: Rc) -> Self { + pub fn new(connection: &Rc>, profile_id: &Rc) -> Self { Self { - connection, - profile_id, + connection: connection.clone(), + profile_id: profile_id.clone(), } } diff --git a/src/profile/database.rs b/src/profile/database.rs index b86dc078..641d7277 100644 --- a/src/profile/database.rs +++ b/src/profile/database.rs @@ -17,8 +17,10 @@ impl Database { // Constructors /// Create new `Self` - pub fn new(connection: Rc>) -> Self { - Self { connection } + pub fn build(connection: &Rc>) -> Self { + Self { + connection: connection.clone(), + } } // Getters diff --git a/src/profile/history.rs b/src/profile/history.rs index ac00d38d..c289dedc 100644 --- a/src/profile/history.rs +++ b/src/profile/history.rs @@ -1,18 +1,24 @@ -mod database; +// mod database; +mod memory; -use sqlite::Transaction; +use memory::Memory; -// Tools +use sqlite::Connection; +use std::{rc::Rc, sync::RwLock}; -pub fn migrate(tx: &Transaction) -> Result<(), String> { - // Migrate self components - if let Err(e) = database::init(tx) { - return Err(e.to_string()); - } - - // Delegate migration to childs - // nothing yet.. - - // Success - Ok(()) +pub struct History { + pub memory: Rc, // fast search index +} + +impl History { + // Constructors + + /// Create new `Self` + pub fn build(_connection: &Rc>, _profile_id: &Rc) -> Self { + // Init children components + let memory = Rc::new(Memory::new()); + + // Return new `Self` + Self { memory } + } } diff --git a/src/profile/history/database.rs b/src/profile/history/database.rs index fede868c..51af4663 100644 --- a/src/profile/history/database.rs +++ b/src/profile/history/database.rs @@ -56,7 +56,7 @@ pub fn select( WHERE `profile_id` = ? AND `request` LIKE ?", )?; - let result = stmt.query_map((profile_id, request.unwrap_or("%")), |row| { + let result = stmt.query_map((profile_id, request.unwrap_or("%".to_string())), |row| { Ok(Table { id: row.get(0)?, profile_id: row.get(1)?, diff --git a/src/profile/history/memory.rs b/src/profile/history/memory.rs new file mode 100644 index 00000000..b15f1b35 --- /dev/null +++ b/src/profile/history/memory.rs @@ -0,0 +1,24 @@ +mod closed; +use closed::Closed; + +/// Reduce disk usage by cache Bookmarks index in memory +pub struct Memory { + pub closed: Closed, +} + +impl Default for Memory { + fn default() -> Self { + Self::new() + } +} + +impl Memory { + // Constructors + + /// Create new `Self` + pub fn new() -> Self { + Self { + closed: Closed::new(), + } + } +} diff --git a/src/profile/history/memory/closed.rs b/src/profile/history/memory/closed.rs new file mode 100644 index 00000000..68f96603 --- /dev/null +++ b/src/profile/history/memory/closed.rs @@ -0,0 +1,53 @@ +use gtk::glib::GString; +use itertools::Itertools; +use std::{cell::RefCell, collections::HashMap}; + +/// Reduce disk usage by cache Bookmarks index in memory +pub struct Closed { + index: RefCell>, +} + +impl Default for Closed { + fn default() -> Self { + Self::new() + } +} + +impl Closed { + // Constructors + + /// Create new `Self` + pub fn new() -> Self { + Self { + index: RefCell::new(HashMap::new()), + } + } + + // Actions + + /// Add new record + /// * replace with new one if the record already exist + pub fn add(&self, request: GString, unix_timestamp: i64) { + self.index.borrow_mut().insert(request, unix_timestamp); + } + + /// Get recent requests vector sorted by `ID` DESC + pub fn recent(&self, limit: usize) -> Vec { + let mut recent: Vec = Vec::new(); + for (request, _) in self + .index + .borrow() + .iter() + .sorted_by(|a, b| Ord::cmp(&b.1, &a.1)) + .take(limit) + { + recent.push(request.clone()) + } + recent + } + + /// Get records total + pub fn total(&self) -> usize { + self.index.borrow().len() + } +} diff --git a/src/profile/identity.rs b/src/profile/identity.rs index cbfcfb45..d17bc921 100644 --- a/src/profile/identity.rs +++ b/src/profile/identity.rs @@ -19,9 +19,9 @@ impl Identity { // Constructors /// Create new `Self` - pub fn new(connection: Rc>, profile_id: Rc) -> Result { + pub fn build(connection: &Rc>, profile_id: &Rc) -> Result { // Init identity database - let database = Rc::new(Database::new(connection.clone())); + let database = Rc::new(Database::build(connection)); // Get active identity set for profile or create new one let profile_identity_id = Rc::new(match database.active() { @@ -36,7 +36,7 @@ impl Identity { }); // Init gemini component - let gemini = Rc::new(match Gemini::new(connection, profile_identity_id) { + let gemini = Rc::new(match Gemini::build(connection, &profile_identity_id) { Ok(result) => result, Err(e) => return Err(Error::Gemini(e)), }); diff --git a/src/profile/identity/database.rs b/src/profile/identity/database.rs index fc522505..3c85081a 100644 --- a/src/profile/identity/database.rs +++ b/src/profile/identity/database.rs @@ -15,8 +15,10 @@ impl Database { // Constructors /// Create new `Self` - pub fn new(connection: Rc>) -> Self { - Self { connection } + pub fn build(connection: &Rc>) -> Self { + Self { + connection: connection.clone(), + } } // Getters @@ -37,7 +39,7 @@ impl Database { // Setters /// Create new record in `Self` database connected - pub fn add(&self, profile_id: Rc, is_active: bool) -> Result { + pub fn add(&self, profile_id: &Rc, is_active: bool) -> Result { // Begin new transaction let mut writable = self.connection.write().unwrap(); let tx = writable.transaction()?; @@ -80,7 +82,7 @@ pub fn init(tx: &Transaction) -> Result { ) } -pub fn insert(tx: &Transaction, profile_id: Rc, is_active: bool) -> Result { +pub fn insert(tx: &Transaction, profile_id: &Rc, is_active: bool) -> Result { tx.execute( "INSERT INTO `profile_identity` ( `profile_id`, diff --git a/src/profile/identity/gemini.rs b/src/profile/identity/gemini.rs index b10cc417..73b153e5 100644 --- a/src/profile/identity/gemini.rs +++ b/src/profile/identity/gemini.rs @@ -29,16 +29,16 @@ impl Gemini { // Constructors /// Create new `Self` - pub fn new( - connection: Rc>, - profile_identity_id: Rc, + pub fn build( + connection: &Rc>, + profile_identity_id: &Rc, ) -> Result { // Init components - let auth = match Auth::new(connection.clone()) { + let auth = match Auth::new(connection) { Ok(auth) => Rc::new(auth), Err(e) => return Err(Error::Auth(e)), }; - let database = Rc::new(Database::new(connection, profile_identity_id.clone())); + let database = Rc::new(Database::build(connection, profile_identity_id)); let memory = Rc::new(Memory::new()); // Init `Self` diff --git a/src/profile/identity/gemini/auth.rs b/src/profile/identity/gemini/auth.rs index 26ecd2cd..1479d68d 100644 --- a/src/profile/identity/gemini/auth.rs +++ b/src/profile/identity/gemini/auth.rs @@ -21,7 +21,7 @@ impl Auth { // Constructors /// Create new `Self` - pub fn new(connection: Rc>) -> Result { + pub fn new(connection: &Rc>) -> Result { // Init `Self` let this = Self { database: Rc::new(Database::new(connection)), diff --git a/src/profile/identity/gemini/auth/database.rs b/src/profile/identity/gemini/auth/database.rs index e0708226..47190a5a 100644 --- a/src/profile/identity/gemini/auth/database.rs +++ b/src/profile/identity/gemini/auth/database.rs @@ -16,8 +16,10 @@ impl Database { // Constructors /// Create new `Self` - pub fn new(connection: Rc>) -> Self { - Self { connection } + pub fn new(connection: &Rc>) -> Self { + Self { + connection: connection.clone(), + } } // Actions diff --git a/src/profile/identity/gemini/database.rs b/src/profile/identity/gemini/database.rs index ba251a6c..08e44c35 100644 --- a/src/profile/identity/gemini/database.rs +++ b/src/profile/identity/gemini/database.rs @@ -17,10 +17,10 @@ impl Database { // Constructors /// Create new `Self` - pub fn new(connection: Rc>, profile_identity_id: Rc) -> Self { + pub fn build(connection: &Rc>, profile_identity_id: &Rc) -> Self { Self { - connection, - profile_identity_id, + connection: connection.clone(), + profile_identity_id: profile_identity_id.clone(), } }