implement recent bookmarks menu item

This commit is contained in:
yggverse 2025-01-12 00:38:22 +02:00
parent c335207b28
commit 9e86e9b29f
13 changed files with 155 additions and 60 deletions

View File

@ -8,6 +8,7 @@ description = "Browser for Gemini Protocol"
keywords = ["gemini", "gemini-protocol", "gtk", "browser", "client"] keywords = ["gemini", "gemini-protocol", "gtk", "browser", "client"]
categories = ["network-programming", "gui"] categories = ["network-programming", "gui"]
repository = "https://github.com/YGGverse/Yoda" repository = "https://github.com/YGGverse/Yoda"
# homepage = "https://yggverse.github.io" # homepage = "https://yggverse.github.io"
[dependencies.adw] [dependencies.adw]
@ -51,6 +52,9 @@ version = "0.1.1"
[dependencies.libspelling] [dependencies.libspelling]
version = "0.3.0" version = "0.3.0"
[dependencies]
itertools = "0.14.0"
# development # development
[patch.crates-io] [patch.crates-io]
# ggemini = { git = "https://github.com/YGGverse/ggemini.git" } # ggemini = { git = "https://github.com/YGGverse/ggemini.git" }

View File

@ -15,7 +15,7 @@ GTK 4 / Libadwaita client written in Rust
* [x] Hotkeys * [x] Hotkeys
* [ ] Bookmarks * [ ] Bookmarks
* [ ] Browser window * [ ] Browser window
* [ ] Recent list * [x] Recent bookmarks list
* [ ] Build-in multimedia support * [ ] Build-in multimedia support
* [x] [Images](#images) * [x] [Images](#images)
* [ ] [Audio](#audio) * [ ] [Audio](#audio)

View File

@ -4,7 +4,7 @@ mod header;
mod tab; mod tab;
mod widget; mod widget;
use action::Action; use action::{Action, Position};
use header::Header; use header::Header;
use sqlite::Transaction; use sqlite::Transaction;
use tab::Tab; use tab::Tab;
@ -29,7 +29,11 @@ impl Window {
// Init components // Init components
let tab = Rc::new(Tab::new(&profile, (&browser_action, &action))); 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)); let widget = Rc::new(Widget::new(&header.widget.gobject, &tab.widget.tab_view));
// Init events // Init events
@ -110,6 +114,13 @@ impl Window {
} // @TODO rename destination method } // @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 // Init struct
Self { Self {
action, action,

View File

@ -6,6 +6,7 @@ mod find;
mod history_back; mod history_back;
mod history_forward; mod history_forward;
mod home; mod home;
mod open;
mod pin; mod pin;
mod reload; mod reload;
mod save_as; mod save_as;
@ -19,6 +20,7 @@ use find::Find;
use history_back::HistoryBack; use history_back::HistoryBack;
use history_forward::HistoryForward; use history_forward::HistoryForward;
use home::Home; use home::Home;
use open::Open;
use pin::Pin; use pin::Pin;
use reload::Reload; use reload::Reload;
use save_as::SaveAs; use save_as::SaveAs;
@ -44,6 +46,7 @@ pub struct Action {
pub history_back: Rc<HistoryBack>, pub history_back: Rc<HistoryBack>,
pub history_forward: Rc<HistoryForward>, pub history_forward: Rc<HistoryForward>,
pub home: Rc<Home>, pub home: Rc<Home>,
pub open: Rc<Open>,
pub pin: Rc<Pin>, pub pin: Rc<Pin>,
pub reload: Rc<Reload>, pub reload: Rc<Reload>,
pub save_as: Rc<SaveAs>, pub save_as: Rc<SaveAs>,
@ -67,6 +70,7 @@ impl Action {
let history_back = Rc::new(HistoryBack::new()); let history_back = Rc::new(HistoryBack::new());
let history_forward = Rc::new(HistoryForward::new()); let history_forward = Rc::new(HistoryForward::new());
let home = Rc::new(Home::new()); let home = Rc::new(Home::new());
let open = Rc::new(Open::new());
let pin = Rc::new(Pin::new()); let pin = Rc::new(Pin::new());
let reload = Rc::new(Reload::new()); let reload = Rc::new(Reload::new());
let save_as = Rc::new(SaveAs::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_back.simple_action);
simple_action_group.add_action(&history_forward.simple_action); simple_action_group.add_action(&history_forward.simple_action);
simple_action_group.add_action(&home.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(&pin.simple_action);
simple_action_group.add_action(&reload.simple_action); simple_action_group.add_action(&reload.simple_action);
simple_action_group.add_action(&save_as.simple_action); simple_action_group.add_action(&save_as.simple_action);
@ -102,6 +107,7 @@ impl Action {
history_back, history_back,
history_forward, history_forward,
home, home,
open,
pin, pin,
reload, reload,
save_as, save_as,

View File

@ -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::<String>()
.expect("Parameter does not match `String` type"),
)
})
}
}

View File

@ -13,14 +13,15 @@ pub struct Header {
} }
impl Header { impl Header {
// Construct // Constructors
pub fn new( pub fn new(
(browser_action, window_action): (&Rc<BrowserAction>, &Rc<WindowAction>), (browser_action, window_action): (&Rc<BrowserAction>, &Rc<WindowAction>),
profile: &Rc<Profile>, profile: &Rc<Profile>,
tab_view: &TabView, tab_view: &TabView,
) -> Self { ) -> Self {
// Init components // 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 // Return new struct
Self { Self {

View File

@ -26,11 +26,11 @@ impl Bar {
) -> Self { ) -> Self {
let control = Control::new(); let control = Control::new();
let tab = Tab::new(window_action, view); 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 { Self {
widget: Rc::new(Widget::new( widget: Rc::new(Widget::new(
&control.widget.gobject, &control.window_controls,
&menu.widget.gobject, &menu.menu_button,
&tab.widget.tab_bar, &tab.widget.tab_bar,
)), )),
} }

View File

@ -1,17 +1,19 @@
mod widget; use gtk::{PackType, WindowControls};
use widget::Widget;
use std::rc::Rc; const MARGIN: i32 = 4;
pub struct Control { pub struct Control {
pub widget: Rc<Widget>, pub window_controls: WindowControls,
} }
impl Control { impl Control {
// Construct // Construct
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
widget: Rc::new(Widget::new()), window_controls: WindowControls::builder()
.margin_end(MARGIN)
.side(PackType::End)
.build(),
} }
} }
} }

View File

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

View File

@ -1,22 +1,24 @@
mod widget;
use widget::Widget;
use super::{BrowserAction, Profile, WindowAction}; use super::{BrowserAction, Profile, WindowAction};
use gtk::{ use gtk::{
gio::{self}, gio::{self},
prelude::ActionExt, prelude::{ActionExt, ToVariant},
Align, MenuButton,
}; };
use std::rc::Rc; use std::rc::Rc;
// Config options
const RECENT_BOOKMARKS: usize = 50;
pub struct Menu { pub struct Menu {
pub widget: Rc<Widget>, pub menu_button: MenuButton,
} }
#[rustfmt::skip] // @TODO template builder? #[rustfmt::skip] // @TODO template builder?
impl Menu { impl Menu {
pub fn new( pub fn new(
(browser_action, window_action): (&Rc<BrowserAction>, &Rc<WindowAction>), (browser_action, window_action): (&Rc<BrowserAction>, &Rc<WindowAction>),
_profile: &Rc<Profile>, profile: &Rc<Profile>,
) -> Self { ) -> Self {
// Main // Main
let main = gio::Menu::new(); let main = gio::Menu::new();
@ -122,6 +124,12 @@ impl Menu {
main.append_submenu(Some("Page"), &main_page); 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 // Main > Tool
let main_tool = gio::Menu::new(); let main_tool = gio::Menu::new();
@ -152,7 +160,42 @@ impl Menu {
browser_action.close.simple_action.name() 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 // Result
Self { widget:Rc::new(Widget::new(&main)) } Self {
menu_button
}
} }
} }

View File

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

View File

@ -11,8 +11,8 @@ use sqlite::{Connection, Transaction};
use std::{rc::Rc, sync::RwLock}; use std::{rc::Rc, sync::RwLock};
pub struct Bookmark { pub struct Bookmark {
database: Rc<Database>, // permanent storage pub database: Rc<Database>, // permanent storage
memory: Rc<Memory>, // fast search index pub memory: Rc<Memory>, // fast search index
} }
impl Bookmark { impl Bookmark {

View File

@ -1,6 +1,7 @@
mod error; mod error;
use error::Error; use error::Error;
use itertools::Itertools;
use std::{cell::RefCell, collections::HashMap}; use std::{cell::RefCell, collections::HashMap};
/// Reduce disk usage by cache Bookmarks index in memory /// Reduce disk usage by cache Bookmarks index in memory
@ -54,4 +55,24 @@ impl Memory {
None => Err(Error::Unexpected), // @TODO None => Err(Error::Unexpected), // @TODO
} }
} }
/// Get recent requests vector sorted DESC by `ID`
pub fn recent(&self, limit: usize) -> Vec<String> {
let mut recent: Vec<String> = 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()
}
} }