reorganize clone semantics, implement recently closed tabs history

This commit is contained in:
yggverse 2025-01-12 04:02:41 +02:00
parent 3682b5bf3f
commit ba68019614
17 changed files with 176 additions and 52 deletions

View File

@ -32,7 +32,7 @@ GTK 4 / Libadwaita client written in Rust
* [x] Unsupported content type downloads * [x] Unsupported content type downloads
* [ ] History * [ ] History
* [ ] Browser window * [ ] Browser window
* [ ] Recently closed * [x] Recently closed
* [ ] Recently visited * [ ] Recently visited
* [ ] Proxy * [ ] Proxy
* [ ] Session * [ ] Session

View File

@ -8,8 +8,9 @@ use std::rc::Rc;
// Config options // Config options
const RECENT_BOOKMARKS: usize = 50;
const LABEL_MAX_LENGTH: usize = 32; const LABEL_MAX_LENGTH: usize = 32;
const RECENT_BOOKMARKS: usize = 50;
const RECENTLY_CLOSED: usize = 50;
pub struct Menu { pub struct Menu {
pub menu_button: MenuButton, pub menu_button: MenuButton,
@ -131,6 +132,16 @@ impl Menu {
main.append_submenu(Some("Bookmarks"), &main_bookmarks); 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 // Main > Tool
let main_tool = gio::Menu::new(); let main_tool = gio::Menu::new();
@ -176,6 +187,7 @@ impl Menu {
let main_bookmarks = main_bookmarks.clone(); let main_bookmarks = main_bookmarks.clone();
let window_action = window_action.clone(); let window_action = window_action.clone();
move |_| { move |_| {
// Bookmarks
main_bookmarks.remove_all(); main_bookmarks.remove_all();
for request in profile.bookmark.memory.recent(RECENT_BOOKMARKS) { for request in profile.bookmark.memory.recent(RECENT_BOOKMARKS) {
let menu_item = gio::MenuItem::new(Some(&label(&request, LABEL_MAX_LENGTH)), None); 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 { // if profile.bookmark.memory.total() > RECENT_BOOKMARKS {
// @TODO // @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);
}
} }
}); });

View File

@ -15,8 +15,8 @@ use crate::app::browser::{
}; };
use crate::Profile; use crate::Profile;
use gtk::{ use gtk::{
glib::{GString, Propagation}, glib::{DateTime, GString, Propagation},
prelude::WidgetExt, prelude::{EditableExt, WidgetExt},
}; };
use sqlite::Transaction; use sqlite::Transaction;
use std::{cell::RefCell, collections::HashMap, rc::Rc}; use std::{cell::RefCell, collections::HashMap, rc::Rc};
@ -92,6 +92,7 @@ impl Tab {
widget.tab_view.connect_close_page({ widget.tab_view.connect_close_page({
let index = index.clone(); let index = index.clone();
let profile = profile.clone();
move |_, item| { move |_, item| {
// Get index ID by keyword saved // Get index ID by keyword saved
match item.keyword() { match item.keyword() {
@ -100,7 +101,14 @@ impl Tab {
panic!("Tab index can not be empty!") panic!("Tab index can not be empty!")
} }
// Cleanup HashMap index // 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!"), None => panic!("Undefined tab index!"),
} }

View File

@ -1,10 +1,11 @@
mod bookmark; mod bookmark;
mod database; mod database;
//mod history; mod history;
mod identity; mod identity;
use bookmark::Bookmark; use bookmark::Bookmark;
use database::Database; use database::Database;
use history::History;
use identity::Identity; use identity::Identity;
use gtk::glib::{user_config_dir, DateTime}; use gtk::glib::{user_config_dir, DateTime};
@ -20,6 +21,7 @@ const DB_NAME: &str = "database.sqlite3";
pub struct Profile { pub struct Profile {
pub bookmark: Rc<Bookmark>, pub bookmark: Rc<Bookmark>,
pub database: Rc<Database>, pub database: Rc<Database>,
pub history: Rc<History>,
pub identity: Rc<Identity>, pub identity: Rc<Identity>,
pub config_path: PathBuf, pub config_path: PathBuf,
} }
@ -87,7 +89,7 @@ impl Profile {
} // unlock database } // unlock database
// Init model // Init model
let database = Rc::new(Database::new(connection.clone())); let database = Rc::new(Database::build(&connection));
// Get active profile or create new one // Get active profile or create new one
let profile_id = Rc::new(match database.active().unwrap() { let profile_id = Rc::new(match database.active().unwrap() {
@ -98,11 +100,10 @@ impl Profile {
}, },
}); });
// Init bookmark component @TODO handle errors // Init components
let bookmark = Rc::new(Bookmark::new(connection.clone(), profile_id.clone())); let bookmark = Rc::new(Bookmark::build(&connection, &profile_id));
let history = Rc::new(History::build(&connection, &profile_id));
// Init identity component let identity = Rc::new(match Identity::build(&connection, &profile_id) {
let identity = Rc::new(match Identity::new(connection, profile_id) {
Ok(result) => result, Ok(result) => result,
Err(e) => todo!("{:?}", e.to_string()), Err(e) => todo!("{:?}", e.to_string()),
}); });
@ -110,8 +111,9 @@ impl Profile {
// Result // Result
Self { Self {
bookmark, bookmark,
identity,
database, database,
history,
identity,
config_path, config_path,
} }
} }

View File

@ -19,7 +19,7 @@ impl Bookmark {
// Constructors // Constructors
/// Create new `Self` /// Create new `Self`
pub fn new(connection: Rc<RwLock<Connection>>, profile_id: Rc<i64>) -> Self { pub fn build(connection: &Rc<RwLock<Connection>>, profile_id: &Rc<i64>) -> Self {
// Init children components // Init children components
let database = Rc::new(Database::new(connection, profile_id)); let database = Rc::new(Database::new(connection, profile_id));
let memory = Rc::new(Memory::new()); let memory = Rc::new(Memory::new());

View File

@ -18,10 +18,10 @@ impl Database {
// Constructors // Constructors
/// Create new `Self` /// Create new `Self`
pub fn new(connection: Rc<RwLock<Connection>>, profile_id: Rc<i64>) -> Self { pub fn new(connection: &Rc<RwLock<Connection>>, profile_id: &Rc<i64>) -> Self {
Self { Self {
connection, connection: connection.clone(),
profile_id, profile_id: profile_id.clone(),
} }
} }

View File

@ -17,8 +17,10 @@ impl Database {
// Constructors // Constructors
/// Create new `Self` /// Create new `Self`
pub fn new(connection: Rc<RwLock<Connection>>) -> Self { pub fn build(connection: &Rc<RwLock<Connection>>) -> Self {
Self { connection } Self {
connection: connection.clone(),
}
} }
// Getters // Getters

View File

@ -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> { pub struct History {
// Migrate self components pub memory: Rc<Memory>, // fast search index
if let Err(e) = database::init(tx) { }
return Err(e.to_string());
} impl History {
// Constructors
// Delegate migration to childs
// nothing yet.. /// Create new `Self`
pub fn build(_connection: &Rc<RwLock<Connection>>, _profile_id: &Rc<i64>) -> Self {
// Success // Init children components
Ok(()) let memory = Rc::new(Memory::new());
// Return new `Self`
Self { memory }
}
} }

View File

@ -56,7 +56,7 @@ pub fn select(
WHERE `profile_id` = ? AND `request` LIKE ?", 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 { Ok(Table {
id: row.get(0)?, id: row.get(0)?,
profile_id: row.get(1)?, profile_id: row.get(1)?,

View File

@ -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(),
}
}
}

View File

@ -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<HashMap<GString, i64>>,
}
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<GString> {
let mut recent: Vec<GString> = 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()
}
}

View File

@ -19,9 +19,9 @@ impl Identity {
// Constructors // Constructors
/// Create new `Self` /// Create new `Self`
pub fn new(connection: Rc<RwLock<Connection>>, profile_id: Rc<i64>) -> Result<Self, Error> { pub fn build(connection: &Rc<RwLock<Connection>>, profile_id: &Rc<i64>) -> Result<Self, Error> {
// Init identity database // 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 // Get active identity set for profile or create new one
let profile_identity_id = Rc::new(match database.active() { let profile_identity_id = Rc::new(match database.active() {
@ -36,7 +36,7 @@ impl Identity {
}); });
// Init gemini component // 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, Ok(result) => result,
Err(e) => return Err(Error::Gemini(e)), Err(e) => return Err(Error::Gemini(e)),
}); });

View File

@ -15,8 +15,10 @@ impl Database {
// Constructors // Constructors
/// Create new `Self` /// Create new `Self`
pub fn new(connection: Rc<RwLock<Connection>>) -> Self { pub fn build(connection: &Rc<RwLock<Connection>>) -> Self {
Self { connection } Self {
connection: connection.clone(),
}
} }
// Getters // Getters
@ -37,7 +39,7 @@ impl Database {
// Setters // Setters
/// Create new record in `Self` database connected /// Create new record in `Self` database connected
pub fn add(&self, profile_id: Rc<i64>, is_active: bool) -> Result<i64, Error> { pub fn add(&self, profile_id: &Rc<i64>, is_active: bool) -> Result<i64, Error> {
// Begin new transaction // Begin new transaction
let mut writable = self.connection.write().unwrap(); let mut writable = self.connection.write().unwrap();
let tx = writable.transaction()?; let tx = writable.transaction()?;
@ -80,7 +82,7 @@ pub fn init(tx: &Transaction) -> Result<usize, Error> {
) )
} }
pub fn insert(tx: &Transaction, profile_id: Rc<i64>, is_active: bool) -> Result<usize, Error> { pub fn insert(tx: &Transaction, profile_id: &Rc<i64>, is_active: bool) -> Result<usize, Error> {
tx.execute( tx.execute(
"INSERT INTO `profile_identity` ( "INSERT INTO `profile_identity` (
`profile_id`, `profile_id`,

View File

@ -29,16 +29,16 @@ impl Gemini {
// Constructors // Constructors
/// Create new `Self` /// Create new `Self`
pub fn new( pub fn build(
connection: Rc<RwLock<Connection>>, connection: &Rc<RwLock<Connection>>,
profile_identity_id: Rc<i64>, profile_identity_id: &Rc<i64>,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
// Init components // Init components
let auth = match Auth::new(connection.clone()) { let auth = match Auth::new(connection) {
Ok(auth) => Rc::new(auth), Ok(auth) => Rc::new(auth),
Err(e) => return Err(Error::Auth(e)), 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()); let memory = Rc::new(Memory::new());
// Init `Self` // Init `Self`

View File

@ -21,7 +21,7 @@ impl Auth {
// Constructors // Constructors
/// Create new `Self` /// Create new `Self`
pub fn new(connection: Rc<RwLock<Connection>>) -> Result<Self, Error> { pub fn new(connection: &Rc<RwLock<Connection>>) -> Result<Self, Error> {
// Init `Self` // Init `Self`
let this = Self { let this = Self {
database: Rc::new(Database::new(connection)), database: Rc::new(Database::new(connection)),

View File

@ -16,8 +16,10 @@ impl Database {
// Constructors // Constructors
/// Create new `Self` /// Create new `Self`
pub fn new(connection: Rc<RwLock<Connection>>) -> Self { pub fn new(connection: &Rc<RwLock<Connection>>) -> Self {
Self { connection } Self {
connection: connection.clone(),
}
} }
// Actions // Actions

View File

@ -17,10 +17,10 @@ impl Database {
// Constructors // Constructors
/// Create new `Self` /// Create new `Self`
pub fn new(connection: Rc<RwLock<Connection>>, profile_identity_id: Rc<i64>) -> Self { pub fn build(connection: &Rc<RwLock<Connection>>, profile_identity_id: &Rc<i64>) -> Self {
Self { Self {
connection, connection: connection.clone(),
profile_identity_id, profile_identity_id: profile_identity_id.clone(),
} }
} }