From 9e86e9b29fb9d71aa35cc59c901287e2cc4d55a4 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 12 Jan 2025 00:38:22 +0200 Subject: [PATCH] implement recent bookmarks menu item --- Cargo.toml | 4 ++ README.md | 2 +- src/app/browser/window.rs | 15 ++++- src/app/browser/window/action.rs | 6 ++ src/app/browser/window/action/open.rs | 44 ++++++++++++++ src/app/browser/window/header.rs | 5 +- src/app/browser/window/header/bar.rs | 6 +- src/app/browser/window/header/bar/control.rs | 12 ++-- .../window/header/bar/control/widget.rs | 17 ------ src/app/browser/window/header/bar/menu.rs | 59 ++++++++++++++++--- .../browser/window/header/bar/menu/widget.rs | 20 ------- src/profile/bookmark.rs | 4 +- src/profile/bookmark/memory.rs | 21 +++++++ 13 files changed, 155 insertions(+), 60 deletions(-) create mode 100644 src/app/browser/window/action/open.rs delete mode 100644 src/app/browser/window/header/bar/control/widget.rs delete mode 100644 src/app/browser/window/header/bar/menu/widget.rs diff --git a/Cargo.toml b/Cargo.toml index 3aec19c8..e3351988 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ description = "Browser for Gemini Protocol" keywords = ["gemini", "gemini-protocol", "gtk", "browser", "client"] categories = ["network-programming", "gui"] repository = "https://github.com/YGGverse/Yoda" + # homepage = "https://yggverse.github.io" [dependencies.adw] @@ -51,6 +52,9 @@ version = "0.1.1" [dependencies.libspelling] version = "0.3.0" +[dependencies] +itertools = "0.14.0" + # development [patch.crates-io] # ggemini = { git = "https://github.com/YGGverse/ggemini.git" } diff --git a/README.md b/README.md index 1595d397..d4e72638 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ GTK 4 / Libadwaita client written in Rust * [x] Hotkeys * [ ] Bookmarks * [ ] Browser window - * [ ] Recent list + * [x] Recent bookmarks list * [ ] Build-in multimedia support * [x] [Images](#images) * [ ] [Audio](#audio) diff --git a/src/app/browser/window.rs b/src/app/browser/window.rs index fe9a8b27..488ee2e5 100644 --- a/src/app/browser/window.rs +++ b/src/app/browser/window.rs @@ -4,7 +4,7 @@ mod header; mod tab; mod widget; -use action::Action; +use action::{Action, Position}; use header::Header; use sqlite::Transaction; use tab::Tab; @@ -29,7 +29,11 @@ impl Window { // Init components let tab = Rc::new(Tab::new(&profile, (&browser_action, &action))); - let header = Header::new((&browser_action, &action), &profile, &tab.widget.tab_view); + let header = Rc::new(Header::new( + (&browser_action, &action), + &profile, + &tab.widget.tab_view, + )); let widget = Rc::new(Widget::new(&header.widget.gobject, &tab.widget.tab_view)); // Init events @@ -110,6 +114,13 @@ impl Window { } // @TODO rename destination method }); + action.open.on_activate({ + let tab = tab.clone(); + move |_, request| { + tab.append(Position::End, Some(request), false, true, false, true); + } + }); + // Init struct Self { action, diff --git a/src/app/browser/window/action.rs b/src/app/browser/window/action.rs index 2cd45a7b..74ae9cd0 100644 --- a/src/app/browser/window/action.rs +++ b/src/app/browser/window/action.rs @@ -6,6 +6,7 @@ mod find; mod history_back; mod history_forward; mod home; +mod open; mod pin; mod reload; mod save_as; @@ -19,6 +20,7 @@ use find::Find; use history_back::HistoryBack; use history_forward::HistoryForward; use home::Home; +use open::Open; use pin::Pin; use reload::Reload; use save_as::SaveAs; @@ -44,6 +46,7 @@ pub struct Action { pub history_back: Rc, pub history_forward: Rc, pub home: Rc, + pub open: Rc, pub pin: Rc, pub reload: Rc, pub save_as: Rc, @@ -67,6 +70,7 @@ impl Action { let history_back = Rc::new(HistoryBack::new()); let history_forward = Rc::new(HistoryForward::new()); let home = Rc::new(Home::new()); + let open = Rc::new(Open::new()); let pin = Rc::new(Pin::new()); let reload = Rc::new(Reload::new()); let save_as = Rc::new(SaveAs::new()); @@ -87,6 +91,7 @@ impl Action { simple_action_group.add_action(&history_back.simple_action); simple_action_group.add_action(&history_forward.simple_action); simple_action_group.add_action(&home.simple_action); + simple_action_group.add_action(&open.simple_action); simple_action_group.add_action(&pin.simple_action); simple_action_group.add_action(&reload.simple_action); simple_action_group.add_action(&save_as.simple_action); @@ -102,6 +107,7 @@ impl Action { history_back, history_forward, home, + open, pin, reload, save_as, diff --git a/src/app/browser/window/action/open.rs b/src/app/browser/window/action/open.rs new file mode 100644 index 00000000..f10d0129 --- /dev/null +++ b/src/app/browser/window/action/open.rs @@ -0,0 +1,44 @@ +// Defaults + +use gtk::{ + gio::SimpleAction, + glib::{uuid_string_random, SignalHandlerId}, + prelude::StaticVariantType, +}; + +/// Open [SimpleAction](https://docs.gtk.org/gio/class.SimpleAction.html) +pub struct Open { + pub simple_action: SimpleAction, +} + +impl Open { + // Constructors + + /// Create new `Self` + pub fn new() -> Self { + Self { + simple_action: SimpleAction::new( + &uuid_string_random(), + Some(&String::static_variant_type()), + ), + } + } + + // Actions + + /// Formatted action connector for external implementation + pub fn on_activate( + &self, + callback: impl Fn(&SimpleAction, String) + 'static, + ) -> SignalHandlerId { + self.simple_action.connect_activate(move |this, message| { + callback( + this, + message + .expect("Variant required to call this action") + .get::() + .expect("Parameter does not match `String` type"), + ) + }) + } +} diff --git a/src/app/browser/window/header.rs b/src/app/browser/window/header.rs index 80126303..ef0a714e 100644 --- a/src/app/browser/window/header.rs +++ b/src/app/browser/window/header.rs @@ -13,14 +13,15 @@ pub struct Header { } impl Header { - // Construct + // Constructors + pub fn new( (browser_action, window_action): (&Rc, &Rc), profile: &Rc, tab_view: &TabView, ) -> Self { // Init components - let bar = Bar::new((browser_action, window_action), profile, tab_view); + let bar = Rc::new(Bar::new((browser_action, window_action), profile, tab_view)); // Return new struct Self { diff --git a/src/app/browser/window/header/bar.rs b/src/app/browser/window/header/bar.rs index c410cc8f..99ccc97c 100644 --- a/src/app/browser/window/header/bar.rs +++ b/src/app/browser/window/header/bar.rs @@ -26,11 +26,11 @@ impl Bar { ) -> Self { let control = Control::new(); let tab = Tab::new(window_action, view); - let menu = Menu::new((browser_action, window_action), profile); + let menu = Rc::new(Menu::new((browser_action, window_action), profile)); Self { widget: Rc::new(Widget::new( - &control.widget.gobject, - &menu.widget.gobject, + &control.window_controls, + &menu.menu_button, &tab.widget.tab_bar, )), } diff --git a/src/app/browser/window/header/bar/control.rs b/src/app/browser/window/header/bar/control.rs index f09dba04..c1abf399 100644 --- a/src/app/browser/window/header/bar/control.rs +++ b/src/app/browser/window/header/bar/control.rs @@ -1,17 +1,19 @@ -mod widget; -use widget::Widget; +use gtk::{PackType, WindowControls}; -use std::rc::Rc; +const MARGIN: i32 = 4; pub struct Control { - pub widget: Rc, + pub window_controls: WindowControls, } impl Control { // Construct pub fn new() -> Self { Self { - widget: Rc::new(Widget::new()), + window_controls: WindowControls::builder() + .margin_end(MARGIN) + .side(PackType::End) + .build(), } } } diff --git a/src/app/browser/window/header/bar/control/widget.rs b/src/app/browser/window/header/bar/control/widget.rs deleted file mode 100644 index 4d5301af..00000000 --- a/src/app/browser/window/header/bar/control/widget.rs +++ /dev/null @@ -1,17 +0,0 @@ -use gtk::{PackType, WindowControls}; - -pub struct Widget { - pub gobject: WindowControls, -} - -impl Widget { - // Construct - pub fn new() -> Self { - Self { - gobject: WindowControls::builder() - .side(PackType::End) - .margin_end(4) - .build(), - } - } -} diff --git a/src/app/browser/window/header/bar/menu.rs b/src/app/browser/window/header/bar/menu.rs index a9820df5..024605aa 100644 --- a/src/app/browser/window/header/bar/menu.rs +++ b/src/app/browser/window/header/bar/menu.rs @@ -1,22 +1,24 @@ -mod widget; - -use widget::Widget; - use super::{BrowserAction, Profile, WindowAction}; use gtk::{ gio::{self}, - prelude::ActionExt, + prelude::{ActionExt, ToVariant}, + Align, MenuButton, }; use std::rc::Rc; +// Config options + +const RECENT_BOOKMARKS: usize = 50; + pub struct Menu { - pub widget: Rc, + pub menu_button: MenuButton, } + #[rustfmt::skip] // @TODO template builder? impl Menu { pub fn new( (browser_action, window_action): (&Rc, &Rc), - _profile: &Rc, + profile: &Rc, ) -> Self { // Main let main = gio::Menu::new(); @@ -122,6 +124,12 @@ impl Menu { main.append_submenu(Some("Page"), &main_page); + // Main > Bookmark + // * menu items dynamically generated using profile memory pool and `set_create_popup_func` + let main_bookmarks = gio::Menu::new(); + + main.append_submenu(Some("Bookmarks"), &main_bookmarks); + // Main > Tool let main_tool = gio::Menu::new(); @@ -152,7 +160,42 @@ impl Menu { browser_action.close.simple_action.name() ))); + // Init main widget + let menu_button = MenuButton::builder() + .css_classes(["flat"]) + .icon_name("open-menu-symbolic") + .menu_model(&main) + .tooltip_text("Menu") + .valign(Align::Center) + .build(); + + // Generate dynamical menu items + menu_button.set_create_popup_func({ + let profile = profile.clone(); + let main_bookmarks = main_bookmarks.clone(); + let window_action = window_action.clone(); + move |_| { + main_bookmarks.remove_all(); + for bookmark in profile.bookmark.memory.recent(RECENT_BOOKMARKS) { + let menu_item = gio::MenuItem::new(Some(&bookmark), None); + menu_item.set_action_and_target_value(Some(&format!( + "{}.{}", + window_action.id, + window_action.open.simple_action.name() + )), Some(&bookmark.to_variant())); + + main_bookmarks.append_item(&menu_item); + } + // Show all bookmarks menu item + // if profile.bookmark.memory.total() > RECENT_BOOKMARKS { + // @TODO + // } + } + }); + // Result - Self { widget:Rc::new(Widget::new(&main)) } + Self { + menu_button + } } } diff --git a/src/app/browser/window/header/bar/menu/widget.rs b/src/app/browser/window/header/bar/menu/widget.rs deleted file mode 100644 index c940ca37..00000000 --- a/src/app/browser/window/header/bar/menu/widget.rs +++ /dev/null @@ -1,20 +0,0 @@ -use gtk::{gio::Menu, Align, MenuButton}; - -pub struct Widget { - pub gobject: MenuButton, -} - -impl Widget { - // Construct - pub fn new(model: &Menu) -> Self { - Self { - gobject: MenuButton::builder() - .css_classes(["flat"]) - .icon_name("open-menu-symbolic") - .menu_model(model) - .tooltip_text("Menu") - .valign(Align::Center) - .build(), - } - } -} diff --git a/src/profile/bookmark.rs b/src/profile/bookmark.rs index 20f64e7b..e01d710a 100644 --- a/src/profile/bookmark.rs +++ b/src/profile/bookmark.rs @@ -11,8 +11,8 @@ use sqlite::{Connection, Transaction}; use std::{rc::Rc, sync::RwLock}; pub struct Bookmark { - database: Rc, // permanent storage - memory: Rc, // fast search index + pub database: Rc, // permanent storage + pub memory: Rc, // fast search index } impl Bookmark { diff --git a/src/profile/bookmark/memory.rs b/src/profile/bookmark/memory.rs index 2d810066..acaae71d 100644 --- a/src/profile/bookmark/memory.rs +++ b/src/profile/bookmark/memory.rs @@ -1,6 +1,7 @@ mod error; use error::Error; +use itertools::Itertools; use std::{cell::RefCell, collections::HashMap}; /// Reduce disk usage by cache Bookmarks index in memory @@ -54,4 +55,24 @@ impl Memory { None => Err(Error::Unexpected), // @TODO } } + + /// Get recent requests vector sorted DESC by `ID` + 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.to_string()) + } + recent + } + + /// Get total records in memory pool + pub fn total(&self) -> usize { + self.index.borrow().len() + } }