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