diff --git a/data/resources/icons/library-symbolic.svg b/data/resources/icons/library-symbolic.svg new file mode 100644 index 0000000..4fdb7f8 --- /dev/null +++ b/data/resources/icons/library-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/data/resources/meson.build b/data/resources/meson.build index f13646b..03f009a 100644 --- a/data/resources/meson.build +++ b/data/resources/meson.build @@ -1,5 +1,6 @@ blueprints = custom_target('blueprints', input: files( + 'ui/bookmarks.blp', 'ui/window.blp', 'ui/shortcuts.blp', 'ui/input_page.blp', diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml index 778c73a..d935220 100644 --- a/data/resources/resources.gresource.xml +++ b/data/resources/resources.gresource.xml @@ -3,9 +3,14 @@ ui/shortcuts.ui + ui/bookmarks.ui ui/window.ui ui/input_page.ui ui/download_page.ui ui/tab.ui + + + icons/library-symbolic.svg + diff --git a/data/resources/ui/bookmarks_window.blp b/data/resources/ui/bookmarks_window.blp new file mode 100644 index 0000000..e3473ae --- /dev/null +++ b/data/resources/ui/bookmarks_window.blp @@ -0,0 +1,77 @@ +using Gtk 4.0; +using Adw 1; + +template $GeopardBookmarksWindow : Adw.Window { + title: _("Bookmarks"); + default-height: 400; + default-width: 500; + height-request: 290; + width-request: 360; + modal: true; + + Adw.ToastOverlay toast_overlay { + Adw.ToolbarView { + [top] + Adw.HeaderBar { + centering-policy: strict; + + [end] + Gtk.ToggleButton search_button { + visible: false; // TODO: Remove + icon-name: "edit-find-symbolic"; + tooltip-text: _("Search"); + } + + [end] + Gtk.Button select_items_button { + visible: false; // TODO: Remove + icon-name: "selection-mode-symbolic"; + tooltip-text: _("Select Items"); + } + } + + content: Gtk.Stack stack { + transition-type: crossfade; + + Gtk.StackPage { + name: "no_bookmarks_page"; + + child: Adw.StatusPage { + icon-name: "starred-symbolic"; + title: _("No Bookmarks"); + description: _("Bookmarked sites will appear here"); + }; + } + + Gtk.StackPage { + name: "bookmarks_page"; + + child: Gtk.ScrolledWindow { + child: Gtk.Box { + orientation: vertical; + margin-top: 6; + margin-bottom: 6; + margin-start: 6; + margin-end: 6; + hexpand: true; + vexpand: true; + + Adw.Clamp { + maximum-size: 1024; + + child: Gtk.ListBox bookmarks_list { + activate-on-single-click: true; + selection-mode: none; + + styles [ + "boxed-list" + ] + }; + } + }; + }; + } + }; + } + } +} diff --git a/data/resources/ui/window.blp b/data/resources/ui/window.blp index 7a06ec3..9463252 100644 --- a/data/resources/ui/window.blp +++ b/data/resources/ui/window.blp @@ -84,6 +84,13 @@ template $GeopardWindow: Adw.ApplicationWindow { primary: true; } + [end] + Gtk.Button bookmarks_button { + icon-name: "library-symbolic"; + action-name: "win.show-bookmarks"; + tooltip-text: _("Bookmarks"); + } + [end] Gtk.Button desktop_tab_overview_btn { icon-name: "view-grid-symbolic"; @@ -171,6 +178,13 @@ template $GeopardWindow: Adw.ApplicationWindow { action-name: "overview.open"; } + [end] + Gtk.Button { + icon-name: "library-symbolic"; + action-name: "win.show-bookmarks"; + tooltip-text: _("Bookmarks"); + } + [end] Gtk.Button { icon-name: "system-search-symbolic"; @@ -191,6 +205,7 @@ template $GeopardWindow: Adw.ApplicationWindow { next_box.visible: false; refresh_btn.visible: false; tab_new_btn.visible: false; + bookmarks_button.visible: false; desktop_tab_overview_btn.visible: false; toolbar_view.reveal-bottom-bars: true; tab_bar_revealer.reveal-child: false; diff --git a/src/bookmarks.rs b/src/bookmarks.rs new file mode 100644 index 0000000..472edd4 --- /dev/null +++ b/src/bookmarks.rs @@ -0,0 +1,156 @@ +use std::collections::BTreeMap; +use std::path::Path; + +use anyhow::{Context, Ok}; +use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; +use toml; + +// todo!(bookmarks): replace bookmarks.bookmarks.insert() with bookmarks.insert_bookmark() +pub static DEFAULT_BOOKMARKS: Lazy = Lazy::new(|| { + let mut bookmarks = Bookmarks::default(); + + bookmarks.bookmarks.insert( + 1.to_string(), + BookmarkBuilder::new() + .title("Gemini Project") + .url("gemini://geminiprotocol.net") + .build(), + ); + + bookmarks.bookmarks.insert( + 2.to_string(), + BookmarkBuilder::new() + .title("Spacewalk aggregator") + .url("gemini://rawtext.club:1965/~sloum/spacewalk.gmi") + .build(), + ); + + bookmarks.bookmarks.insert( + 3.to_string(), + BookmarkBuilder::new() + .title("About Geopard + help") + .url("about:help") + .build(), + ); + + bookmarks +}); + +#[derive(Clone, Default, Serialize, Deserialize, Debug)] +pub struct Bookmark { + title: String, + description: Option, + url: String, +} + +#[derive(Clone, Default, Debug)] +pub struct BookmarkBuilder { + title: String, + description: Option, + url: String, +} + +#[derive(Clone, Default, Serialize, Deserialize, Debug)] +pub struct Bookmarks { + #[serde(rename = "bookmark")] + pub bookmarks: BTreeMap, +} + +impl BookmarkBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn title(mut self, title: &str) -> Self { + self.title = String::from(title); + self + } + + pub fn description(mut self, description: Option<&str>) -> Self { + match description { + Some(desc) => self.description = Some(String::from(desc)), + None => self.description = None, + } + self + } + + pub fn url(mut self, url: &str) -> Self { + self.url = String::from(url); + self + } + + pub fn build(self) -> Bookmark { + Bookmark { + title: self.title, + description: self.description, + url: self.url, + } + } +} + +impl Bookmark { + pub fn title(&self) -> String { + self.title.clone() + } + + pub fn set_title(&mut self, title: &str) { + self.title = String::from(title); + } + + pub fn description(&self) -> Option { + self.description.as_ref().cloned() + } + + pub fn set_description(&mut self, description: &str) { + self.description = Some(String::from(description)); + } + + pub fn url(&self) -> String { + self.url.clone() + } + + pub fn set_url(&mut self, url: &str) { + self.url = String::from(url); + } +} + +//todo!(bookmarks): Add from_gmi() method for migrations +impl Bookmarks { + pub async fn from_file(&self, path: &Path) -> anyhow::Result { + let file_str = async_fs::read_to_string(path) + .await + .context("Reading bookmarks file")?; + + let bookmarks = toml::from_str(&file_str)?; + + Ok(bookmarks) + } + + pub async fn to_file(&self, path: &Path) -> anyhow::Result<()> { + let toml = toml::to_string(self)?; + + async_fs::write(path, toml) + .await + .context("Writting data to bookmarks file")?; + + Ok(()) + } + + //todo!(bookmarks): key must be the biggest current key + 1 + pub fn insert_bookmark(&mut self, bookmark: Bookmark) { + self.bookmarks.insert(1.to_string(), bookmark); + } + + pub fn update_bookmark(&mut self, key: u32, new_bookmark: Bookmark) { + self.bookmarks.insert(key.to_string(), new_bookmark); + } + + pub fn remove_bookmark(&mut self, key: u32) { + if self.bookmarks.is_empty() { + return; + } + + self.bookmarks.remove(&key.to_string()); + } +} diff --git a/src/common/mod.rs b/src/common/mod.rs index 51159be..aab39c6 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -1,6 +1,5 @@ use gtk::glib; use once_cell::sync::Lazy; -use url::Url; pub static DOWNLOAD_PATH: Lazy = Lazy::new(|| { let mut download_path = glib::user_special_dir(glib::UserDirectory::Downloads) @@ -23,37 +22,20 @@ pub static KNOWN_HOSTS_PATH: Lazy = pub static CONFIG_DIR_PATH: Lazy = Lazy::new(|| glib::user_config_dir().join("geopard")); -pub static BOOKMARK_FILE_PATH: Lazy = +pub static OLD_BOOKMARK_FILE_PATH: Lazy = Lazy::new(|| DATA_DIR_PATH.join("bookmarks.gemini")); +pub static BOOKMARK_FILE_PATH: Lazy = + Lazy::new(|| DATA_DIR_PATH.join("bookmarks.toml")); + pub static SETTINGS_FILE_PATH: Lazy = Lazy::new(|| CONFIG_DIR_PATH.join("config.toml")); pub static HISTORY_FILE_PATH: Lazy = Lazy::new(|| DATA_DIR_PATH.join("history.gemini")); -pub static DEFAULT_BOOKMARKS: &str = r"# Bookmarks - -This is a gemini file where you can put all your bookmarks. -You can even edit this file in a text editor. That's how you -should remove bookmarks. - -## Default bookmarks - -=> gemini://geminiprotocol.net Gemini project -=> gemini://rawtext.club:1965/~sloum/spacewalk.gmi Spacewalk aggregator -=> about:help About geopard + help - -## Custom bookmarks - -"; - pub const STREAMABLE_EXTS: [&str; 8] = ["mp3", "mp4", "webm", "opus", "wav", "ogg", "mkv", "flac"]; -pub fn bookmarks_url() -> Url { - Url::parse(&format!("file://{}", BOOKMARK_FILE_PATH.to_str().unwrap())).unwrap() -} - pub fn glibctx() -> glib::MainContext { glib::MainContext::default() } diff --git a/src/main.rs b/src/main.rs index 00f4d30..989930b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ #[rustfmt::skip] mod build_config; +mod bookmarks; mod common; mod config; mod lossy_text_read; @@ -13,15 +14,13 @@ use std::{env, process}; use anyhow::Context; use async_fs::File; -use common::bookmarks_url; use futures::prelude::*; use gtk::gio; use gtk::prelude::*; use log::error; use crate::common::{ - BOOKMARK_FILE_PATH, CONFIG_DIR_PATH, DATA_DIR_PATH, DEFAULT_BOOKMARKS, HISTORY_FILE_PATH, - SETTINGS_FILE_PATH, + BOOKMARK_FILE_PATH, CONFIG_DIR_PATH, DATA_DIR_PATH, HISTORY_FILE_PATH, SETTINGS_FILE_PATH, }; async fn read_config() -> anyhow::Result { @@ -29,6 +28,11 @@ async fn read_config() -> anyhow::Result { .context("Reading config file") } +async fn read_bookmarks() -> anyhow::Result { + let bookmarks = bookmarks::Bookmarks::default(); + Ok(bookmarks.from_file(&BOOKMARK_FILE_PATH).await?) +} + async fn create_dir_if_not_exists(path: &std::path::Path) -> anyhow::Result<()> { if !path.exists() { async_fs::create_dir_all(path) @@ -60,10 +64,11 @@ async fn init_file_if_not_exists( async fn create_base_files() -> anyhow::Result<()> { let default_config = toml::to_string(&*config::DEFAULT_CONFIG).unwrap(); + let default_bookmarks = toml::to_string(&*bookmarks::DEFAULT_BOOKMARKS).unwrap(); create_dir_if_not_exists(&DATA_DIR_PATH).await?; create_dir_if_not_exists(&CONFIG_DIR_PATH).await?; - init_file_if_not_exists(&BOOKMARK_FILE_PATH, Some(DEFAULT_BOOKMARKS.as_bytes())).await?; + init_file_if_not_exists(&BOOKMARK_FILE_PATH, Some(default_bookmarks.as_bytes())).await?; init_file_if_not_exists(&HISTORY_FILE_PATH, None).await?; init_file_if_not_exists(&SETTINGS_FILE_PATH, Some(default_config.as_bytes())).await?; @@ -109,17 +114,23 @@ fn main() { read_config().await.unwrap() }); + let bookmarks = futures::executor::block_on(async { read_bookmarks().await.unwrap() }); + let windows = Rc::new(RefCell::new(vec![])); - application + //todo!(main): Modify to open URLs instead of files (issue #50) + /*application .connect_activate(move |app| app.open(&[gio::File::for_uri(bookmarks_url().as_str())], "")); + */ - application.connect_open(move |app, files, _| { - let window = widgets::Window::new(app, config.clone()); + application.connect_activate(move |app| { + let window = widgets::Window::new(app, config.clone(), bookmarks.clone()); window.present(); windows.borrow_mut().push(window.clone()); - for f in files { + gtk::prelude::WidgetExt::activate_action(&window, "win.new-tab", None).unwrap(); + + /*for f in files { gtk::prelude::WidgetExt::activate_action(&window, "win.new-empty-tab", None).unwrap(); gtk::prelude::WidgetExt::activate_action( &window, @@ -127,7 +138,7 @@ fn main() { Some(&f.uri().to_variant()), ) .unwrap(); - } + }*/ }); application.set_accels_for_action("win.previous", &["Left", "KP_Left"]); diff --git a/src/widgets/bookmarks_window.rs b/src/widgets/bookmarks_window.rs new file mode 100644 index 0000000..7795775 --- /dev/null +++ b/src/widgets/bookmarks_window.rs @@ -0,0 +1,135 @@ +use std::cell::RefCell; +use std::sync::OnceLock; + +use adw::prelude::*; +use adw::subclass::prelude::*; + +use glib::subclass::{InitializingObject, Signal}; +use gtk::{ + glib::{self, clone, Object}, + CompositeTemplate, +}; + +use crate::bookmarks; + +pub mod imp { + use super::*; + + #[derive(CompositeTemplate, Default)] + #[template(resource = "/com/ranfdev/Geopard/ui/bookmarks_window.ui")] + pub struct BookmarksWindow { + #[template_child] + pub toast_overlay: TemplateChild, + #[template_child] + pub stack: TemplateChild, + #[template_child] + pub bookmarks_list: TemplateChild, + pub(crate) bookmarks: RefCell, + } + + #[glib::object_subclass] + impl ObjectSubclass for BookmarksWindow { + const NAME: &'static str = "GeopardBookmarksWindow"; + type Type = super::BookmarksWindow; + type ParentType = adw::Window; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + } + + fn instance_init(obj: &InitializingObject) { + obj.init_template(); + } + } + impl ObjectImpl for BookmarksWindow { + fn signals() -> &'static [Signal] { + static SIGNALS: OnceLock> = OnceLock::new(); + SIGNALS.get_or_init(|| { + vec![Signal::builder("open-bookmark-url") + .param_types([str::static_type()]) + .build()] + }) + } + + fn constructed(&self) { + self.parent_constructed(); + } + } + impl WidgetImpl for BookmarksWindow {} + impl WindowImpl for BookmarksWindow {} + impl AdwWindowImpl for BookmarksWindow {} +} + +glib::wrapper! { + pub struct BookmarksWindow(ObjectSubclass) + @extends adw::Window, gtk::Window, gtk::Widget; +} + +impl BookmarksWindow { + pub fn new(app: >k::Application, bookmarks: bookmarks::Bookmarks) -> Self { + let this = Object::builder::() + .property("application", app) + .build(); + let imp = this.imp(); + imp.bookmarks.replace(bookmarks); + + this.setup(); + + this + } + + fn setup(&self) { + let imp = self.imp(); + // TODO: Set to bookmarks_page if there's at least one bookmark + imp.stack.set_visible_child_name("bookmarks_page"); + + let bookmarks_map = imp.bookmarks.borrow().clone().bookmarks; + + for (_, bookmark) in bookmarks_map.iter() { + self.add_row(&bookmark.title(), &bookmark.url()); + } + } + + // TODO: create_new_row -> adw::ActionRow + fn add_row(&self, title: &str, url: &str) { + let imp = self.imp(); + let title = title.to_string(); + let url = url.to_string(); + + let check_button = gtk::CheckButton::builder() + .visible(false) + .css_classes(vec!["selection-mode"]) + .valign(gtk::Align::Center) + .build(); + + let copy_button = gtk::Button::builder() + .icon_name("edit-copy-symbolic") + .tooltip_text("Copy URL") + .css_classes(vec!["flat"]) + .valign(gtk::Align::Center) + .build(); + + copy_button.connect_clicked(clone!(@weak imp => move |_| { + imp.toast_overlay.add_toast(adw::Toast::new("Copied to clipboard")); + })); + + let row = adw::ActionRow::builder() + .activatable(true) + .title(&title) + .subtitle(&url) + .build(); + row.add_prefix(&check_button); + row.add_suffix(©_button); + + row.connect_activated(clone!(@weak self as this => move |_| { + this.on_row_activated(&url); + })); + + imp.bookmarks_list.append(&row); + } + + fn on_row_activated(&self, url: &str) { + let imp = self.imp().obj(); + imp.emit_by_name::<()>("open-bookmark-url", &[&url]); + } +} diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 03a0952..fc37e5f 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -1,3 +1,4 @@ +mod bookmarks_window; mod pages; #[allow(clippy::await_holding_refcell_ref)] mod tab; diff --git a/src/widgets/pages/hypertext.rs b/src/widgets/pages/hypertext.rs index 7175542..bfafa46 100644 --- a/src/widgets/pages/hypertext.rs +++ b/src/widgets/pages/hypertext.rs @@ -195,7 +195,10 @@ pub mod imp { .param_types([SignalType::from(glib::types::Type::STRING)]) .build(), Signal::builder("open-in-new-tab") - .param_types([SignalType::from(glib::types::Type::STRING)]) + .param_types([ + SignalType::from(glib::types::Type::STRING), + SignalType::from(glib::types::Type::BOOL), + ]) .build(), Signal::builder("open-background-tab") .param_types([SignalType::from(glib::types::Type::STRING)]) diff --git a/src/widgets/window.rs b/src/widgets/window.rs index f43b410..74d41c9 100644 --- a/src/widgets/window.rs +++ b/src/widgets/window.rs @@ -4,19 +4,17 @@ use std::marker::PhantomData; use adw::prelude::*; use adw::subclass::application_window::AdwApplicationWindowImpl; -use anyhow::Context; use config::APP_ID; -use futures::prelude::*; -use glib::{clone, Properties}; +use glib::{clone, closure_local, Properties}; use gtk::subclass::prelude::*; use gtk::{gdk, gio, glib, CompositeTemplate, TemplateChild}; use log::{error, info, warn}; use url::Url; -use crate::common::{bookmarks_url, glibctx, BOOKMARK_FILE_PATH}; use crate::session_provider::SessionProvider; +use crate::widgets::bookmarks_window::BookmarksWindow; use crate::widgets::tab::{HistoryItem, HistoryStatus, Tab}; -use crate::{build_config, config, self_action}; +use crate::{bookmarks, build_config, config, self_action}; const ZOOM_CHANGE_FACTOR: f64 = 1.15; const ZOOM_MAX_FACTOR: f64 = 5.0; @@ -73,6 +71,7 @@ pub mod imp { #[template_child] pub(crate) main_menu_button: TemplateChild, pub(crate) config: RefCell, + pub(crate) bookmarks: RefCell, pub(crate) progress_animation: RefCell>, pub(crate) binded_tab_properties: RefCell>, #[property(get, set)] @@ -171,12 +170,17 @@ glib::wrapper! { } impl Window { - pub fn new(app: &adw::Application, config: config::Config) -> Self { + pub fn new( + app: &adw::Application, + config: config::Config, + bookmarks: bookmarks::Bookmarks, + ) -> Self { let this: Self = glib::Object::builder::() .property("application", app) .build(); let imp = this.imp(); imp.config.replace(config); + imp.bookmarks.replace(bookmarks); imp.zoom.borrow_mut().value = 1.0; this.setup_css_providers(); @@ -217,7 +221,8 @@ impl Window { self_action!(self, "reload", reload); self_action!(self, "new-tab", new_tab); self_action!(self, "new-empty-tab", new_empty_tab); - self_action!(self, "show-bookmarks", show_bookmarks); + self_action!(self, "show-bookmarks", present_bookmarks); + //todo!(window): Make it show "New Bookmark" popover self_action!(self, "bookmark-current", bookmark_current); self_action!(self, "close-tab", close_tab); self_action!(self, "focus-url-bar", focus_url_bar); @@ -245,7 +250,7 @@ impl Window { let act_open_in_new_tab = gio::SimpleAction::new("open-in-new-tab", Some(glib::VariantTy::STRING)); act_open_in_new_tab.connect_activate( - clone!(@weak self as this => move |_,v| this.open_in_new_tab(v.unwrap().get::().unwrap().as_str())), + clone!(@weak self as this => move |_,v| this.open_in_new_tab(v.unwrap().get::().unwrap().as_str(), false)), ); self.add_action(&act_open_in_new_tab); @@ -383,7 +388,7 @@ impl Window { clone!(@weak self as this => @default-return false, move |_, value, _, _| { if let Ok(files) = value.get::() { for f in files.files() { - this.open_in_new_tab(&format!("file://{}", f.path().unwrap().to_str().unwrap())); + this.open_in_new_tab(&format!("file://{}", f.path().unwrap().to_str().unwrap()), false); } } false @@ -563,21 +568,21 @@ impl Window { }; } fn new_tab(&self) { - self.show_bookmarks(); + //todo!(window): Use user preference to determine what to show in new tab + self.new_empty_tab(); self.focus_url_bar(); } + fn new_empty_tab(&self) { let imp = self.imp(); let p = self.add_tab(); + + p.set_title("Blank Tab"); imp.tab_view.set_selected_page(&p); + self.focus_url_bar(); } - fn show_bookmarks(&self) { - let imp = self.imp(); - let p = self.add_tab(); - imp.tab_view.set_selected_page(&p); - self.inner_tab(&p).spawn_open_url(bookmarks_url()); - } + fn close_tab(&self) { let imp = self.imp(); imp.tab_view @@ -588,22 +593,6 @@ impl Window { self.imp().url_bar.grab_focus(); } - async fn append_bookmark(url: &str) -> anyhow::Result<()> { - let mut file = async_fs::OpenOptions::new() - .write(true) - .append(true) - .open(&*BOOKMARK_FILE_PATH) - .await - .context("Opening bookmark.gemini")?; - - let line_to_write = format!("=> {}\n", url); - file.write_all(line_to_write.as_bytes()) - .await - .context("Writing url to favourite.gemini")?; - - file.flush().await?; - Ok(()) - } fn current_tab(&self) -> Tab { let imp = self.imp(); imp.tab_view @@ -626,7 +615,13 @@ impl Window { let imp = self.imp(); let url = imp.url_bar.text().to_string(); - glibctx().spawn_local(clone!(@weak imp => async move { + //todo!(window): Remove later + info!("{} saved to bookmarks", url); + imp.toast_overlay + .add_toast(adw::Toast::new("Page added to bookmarks")); + + //todo!(window): Replace with new bookmarks logic + /*glibctx().spawn_local(clone!(@weak imp => async move { match Self::append_bookmark(&url).await { Ok(_) => { info!("{} saved to bookmarks", url); @@ -637,7 +632,7 @@ impl Window { imp.toast_overlay.add_toast(adw::Toast::new("Failed to bookmark this page")); }, } - })); + }));*/ } fn open_omni(&self, v: &str) { let url = Url::parse(v).or_else(|_| { @@ -659,9 +654,15 @@ impl Window { Err(e) => error!("Failed to parse url: {:?}", e), } } - fn open_in_new_tab(&self, v: &str) { + fn open_in_new_tab(&self, v: &str, select_page: bool) { + let imp = self.imp(); let w = self.add_tab(); let url = Url::parse(v); + + if select_page { + imp.tab_view.set_selected_page(&w); + } + match url { Ok(url) => self.inner_tab(&w).spawn_open_url(url), Err(e) => error!("Failed to parse url: {:?}", e), @@ -730,6 +731,24 @@ impl Window { fn present_shortcuts(&self) { gtk::Builder::from_resource("/com/ranfdev/Geopard/ui/shortcuts.ui"); } + + fn present_bookmarks(&self) { + let imp = self.imp(); + let bookmarks = + BookmarksWindow::new(&self.application().unwrap(), imp.bookmarks.borrow().clone()); + bookmarks.set_transient_for(Some(self)); + + bookmarks.connect_closure( + "open-bookmark-url", + false, + closure_local!(@watch self as this => move |_: BookmarksWindow, url: &str| { + this.open_in_new_tab(url, true); + }), + ); + + bookmarks.present(); + } + fn present_about(&self) { let about = adw::AboutDialog::builder() .application_icon(build_config::APP_ID)