v0.0.8 - Multi-window tab management: Open in New Window moves tabs, per-window active notebook, favicon display, Download Website + Open Link in New Window context menu items; update nostr-core to v0.1.3
This commit is contained in:
Generated
+8
-10
@@ -1957,12 +1957,13 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
|
||||
|
||||
[[package]]
|
||||
name = "nostr-core"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"base64",
|
||||
"bech32",
|
||||
"block-modes",
|
||||
"cbc",
|
||||
"chacha20",
|
||||
"chacha20poly1305",
|
||||
"chrono",
|
||||
@@ -1980,7 +1981,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nostr-nips"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"block-modes",
|
||||
@@ -2002,7 +2003,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nostr-relay"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"nostr-core",
|
||||
@@ -2020,7 +2021,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nostr-services"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
dependencies = [
|
||||
"nostr-core",
|
||||
"nostr-relay",
|
||||
@@ -2035,10 +2036,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nostr-signer"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"cbc",
|
||||
"hex",
|
||||
"libc",
|
||||
"nostr-core",
|
||||
@@ -2046,7 +2045,6 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serialport",
|
||||
"sha2",
|
||||
"thiserror 2.0.20",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -3057,7 +3055,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sovereign_browser"
|
||||
version = "0.0.7"
|
||||
version = "0.0.8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
@@ -3223,7 +3221,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.4.3",
|
||||
"getrandom 0.3.4",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ members = [
|
||||
|
||||
[package]
|
||||
name = "sovereign_browser"
|
||||
version = "0.0.7"
|
||||
version = "0.0.8"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
description = "A Linux x86 web browser built on WebKitGTK with Nostr identity"
|
||||
|
||||
+301
-65
@@ -63,6 +63,27 @@ static G_MAIN_NOTEBOOK: Lazy<Mutex<Option<glib::thread_guard::ThreadGuard<gtk::N
|
||||
static G_MAIN_WINDOW: Lazy<Mutex<Option<glib::thread_guard::ThreadGuard<gtk::Window>>>> =
|
||||
Lazy::new(|| Mutex::new(None));
|
||||
|
||||
/// The currently focused window's notebook. Updated on window focus-in so
|
||||
/// that "active" operations (new tab, close, reload, …) target the window
|
||||
/// the user is actually looking at. Falls back to the main notebook.
|
||||
/// Mirrors the C `g_active_notebook` (tab_manager.c:128).
|
||||
static G_ACTIVE_NOTEBOOK: Lazy<Mutex<Option<glib::thread_guard::ThreadGuard<gtk::Notebook>>>> =
|
||||
Lazy::new(|| Mutex::new(None));
|
||||
|
||||
/// Auxiliary (non-main) top-level windows, so we can revert the active
|
||||
/// notebook to the main window when an aux window is destroyed.
|
||||
/// Mirrors the C `g_aux_windows` (tab_manager.c:1380).
|
||||
static G_AUX_WINDOWS: Lazy<Mutex<Vec<glib::thread_guard::ThreadGuard<gtk::Window>>>> =
|
||||
Lazy::new(|| Mutex::new(Vec::new()));
|
||||
|
||||
/// Per-tab widget map: tab id → (notebook, webview). GTK widgets are not
|
||||
/// `Send`, so they are wrapped in ThreadGuards. This lets close/reload/
|
||||
/// duplicate resolve the tab's *own* notebook (which may be an auxiliary
|
||||
/// window's notebook) rather than assuming the main notebook.
|
||||
static G_TAB_WIDGETS: Lazy<
|
||||
Mutex<std::collections::HashMap<i32, (glib::thread_guard::ThreadGuard<gtk::Notebook>, glib::thread_guard::ThreadGuard<WebView>)>>,
|
||||
> = Lazy::new(|| Mutex::new(std::collections::HashMap::new()));
|
||||
|
||||
/// The custom inspector window (created on `open-window` signal, destroyed
|
||||
/// on `closed`). Stored in a ThreadGuard so it can live in a static.
|
||||
static G_INSPECTOR_WINDOW: Lazy<Mutex<Option<glib::thread_guard::ThreadGuard<gtk::Window>>>> =
|
||||
@@ -101,10 +122,25 @@ pub fn tab_manager_set_suppress_quit_on_last_tab(suppress: bool) {
|
||||
pub fn tab_manager_set_main_window(window: >k::Window, notebook: >k::Notebook) {
|
||||
*G_MAIN_NOTEBOOK.lock().unwrap() = Some(glib::thread_guard::ThreadGuard::new(notebook.clone()));
|
||||
*G_MAIN_WINDOW.lock().unwrap() = Some(glib::thread_guard::ThreadGuard::new(window.clone()));
|
||||
|
||||
// The main window starts as the active window. Focus-in reverts the
|
||||
// active notebook to the main one when the user returns to it.
|
||||
set_active_notebook(notebook);
|
||||
let notebook_focus = notebook.clone();
|
||||
window.connect_focus_in_event(move |_w, _e| {
|
||||
set_active_notebook(¬ebook_focus);
|
||||
glib::Propagation::Proceed
|
||||
});
|
||||
}
|
||||
|
||||
/// Get the active notebook (focused window's notebook, or main).
|
||||
///
|
||||
/// Mirrors the C `get_effective_notebook()` (tab_manager.c:2765): prefer the
|
||||
/// focused window's notebook, falling back to the main notebook.
|
||||
pub fn tab_manager_get_active_notebook() -> Option<gtk::Notebook> {
|
||||
if let Some(g) = G_ACTIVE_NOTEBOOK.lock().unwrap().as_ref() {
|
||||
return Some(g.get_ref().clone());
|
||||
}
|
||||
tab_manager_get_main_notebook()
|
||||
}
|
||||
|
||||
@@ -113,6 +149,49 @@ pub fn tab_manager_get_main_notebook() -> Option<gtk::Notebook> {
|
||||
G_MAIN_NOTEBOOK.lock().unwrap().as_ref().map(|g| g.get_ref().clone())
|
||||
}
|
||||
|
||||
/// Record the focused window's notebook as the active one.
|
||||
fn set_active_notebook(notebook: >k::Notebook) {
|
||||
*G_ACTIVE_NOTEBOOK.lock().unwrap() =
|
||||
Some(glib::thread_guard::ThreadGuard::new(notebook.clone()));
|
||||
}
|
||||
|
||||
/// Revert the active notebook to the main window's notebook.
|
||||
fn revert_active_notebook_to_main() {
|
||||
let main = tab_manager_get_main_notebook();
|
||||
*G_ACTIVE_NOTEBOOK.lock().unwrap() =
|
||||
main.map(glib::thread_guard::ThreadGuard::new);
|
||||
}
|
||||
|
||||
/// Register a tab's notebook + webview so close/reload/duplicate can resolve
|
||||
/// the tab's own notebook (which may belong to an auxiliary window).
|
||||
fn register_tab_widgets(tab_id: i32, notebook: >k::Notebook, webview: &WebView) {
|
||||
G_TAB_WIDGETS.lock().unwrap().insert(
|
||||
tab_id,
|
||||
(
|
||||
glib::thread_guard::ThreadGuard::new(notebook.clone()),
|
||||
glib::thread_guard::ThreadGuard::new(webview.clone()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Look up a tab's notebook by id.
|
||||
fn tab_notebook(tab_id: i32) -> Option<gtk::Notebook> {
|
||||
G_TAB_WIDGETS
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(&tab_id)
|
||||
.map(|(nb, _)| nb.get_ref().clone())
|
||||
}
|
||||
|
||||
/// Look up a tab's webview by id.
|
||||
fn tab_webview(tab_id: i32) -> Option<WebView> {
|
||||
G_TAB_WIDGETS
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(&tab_id)
|
||||
.map(|(_, wv)| wv.get_ref().clone())
|
||||
}
|
||||
|
||||
/// Get the main window.
|
||||
pub fn tab_manager_get_main_window() -> Option<gtk::Window> {
|
||||
G_MAIN_WINDOW.lock().unwrap().as_ref().map(|g| g.get_ref().clone())
|
||||
@@ -407,6 +486,10 @@ pub fn tab_manager_new_tab(notebook: >k::Notebook, ctx: &WebContext, url: Opti
|
||||
page.show_all();
|
||||
tab_label.show_all();
|
||||
|
||||
// Record the tab's notebook + webview so close/reload/duplicate can
|
||||
// resolve the tab's own notebook (aux windows have their own notebook).
|
||||
register_tab_widgets(tab_id, notebook, &webview);
|
||||
|
||||
// Apply the current chrome visibility so a tab opened while the
|
||||
// toolbars are hidden stays hidden. Mirrors the C version
|
||||
// (tab_manager.c:3235). The tab strip itself is per-notebook
|
||||
@@ -440,11 +523,23 @@ pub fn tab_manager_new_tab(notebook: >k::Notebook, ctx: &WebContext, url: Opti
|
||||
});
|
||||
|
||||
// Favicon change — populate the favicon image from the page favicon.
|
||||
// (cairo::Surface → Pixbuf conversion is not exposed in this gdk version,
|
||||
// so we keep the favicon widget but leave it empty.)
|
||||
let _favicon_clone = favicon.clone();
|
||||
webview.connect_favicon_notify(move |_wv| {
|
||||
// favicon population skipped (API limitation)
|
||||
// WebKitGTK's favicon database (enabled in web_context) fetches and
|
||||
// caches favicons; `notify::favicon` fires when one is ready and
|
||||
// `webview.favicon()` returns a cairo surface. Mirrors the C
|
||||
// `on_favicon_changed` (tab_manager.c:948).
|
||||
let favicon_clone = favicon.clone();
|
||||
webview.connect_favicon_notify(move |wv| {
|
||||
if let Some(surface) = wv.favicon() {
|
||||
if let Ok(img) = cairo::ImageSurface::try_from(surface) {
|
||||
let w = img.width();
|
||||
let h = img.height();
|
||||
if w > 0 && h > 0 {
|
||||
if let Some(pixbuf) = gdk::pixbuf_get_from_surface(&img, 0, 0, w, h) {
|
||||
favicon_clone.set_from_pixbuf(Some(&pixbuf));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Load progress.
|
||||
@@ -662,6 +757,7 @@ pub fn tab_manager_new_tab(notebook: >k::Notebook, ctx: &WebContext, url: Opti
|
||||
None
|
||||
};
|
||||
|
||||
let is_link = hit_test.context_is_link();
|
||||
if let Some(url) = url {
|
||||
let url_for_action = url.clone();
|
||||
let nb_clone = notebook_clone_ctx.clone();
|
||||
@@ -674,7 +770,7 @@ pub fn tab_manager_new_tab(notebook: >k::Notebook, ctx: &WebContext, url: Opti
|
||||
}
|
||||
});
|
||||
|
||||
let label = if hit_test.context_is_link() {
|
||||
let label = if is_link {
|
||||
"Open Link in New Tab"
|
||||
} else {
|
||||
"Open Page in New Tab"
|
||||
@@ -685,6 +781,29 @@ pub fn tab_manager_new_tab(notebook: >k::Notebook, ctx: &WebContext, url: Opti
|
||||
None,
|
||||
);
|
||||
context_menu.insert(&item, 0);
|
||||
|
||||
// For links, also add our own "Open Link in New Window" that
|
||||
// opens a real top-level window (WebKit's stock item fires the
|
||||
// `create` signal, which we route to a new *tab*). Mirrors the
|
||||
// C version (tab_manager.c:1995).
|
||||
if is_link {
|
||||
let url_for_win = url.clone();
|
||||
let nb_win = notebook_clone_ctx.clone();
|
||||
let win_action = gio::SimpleAction::new("open-in-new-window", None);
|
||||
win_action.connect_activate(move |_, _| {
|
||||
if let Some(wv) = tab_manager_get_active_webview(&nb_win) {
|
||||
if let Some(ctx) = wv.web_context() {
|
||||
tab_manager_new_window(Some(&url_for_win), &ctx);
|
||||
}
|
||||
}
|
||||
});
|
||||
let win_item = webkit2gtk::ContextMenuItem::from_gaction(
|
||||
&win_action,
|
||||
"Open Link in New Window",
|
||||
None,
|
||||
);
|
||||
context_menu.insert(&win_item, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// "Toggle Toolbars" — lets the user restore the hidden chrome
|
||||
@@ -713,15 +832,48 @@ pub fn tab_manager_new_tab(notebook: >k::Notebook, ctx: &WebContext, url: Opti
|
||||
}
|
||||
|
||||
/// Close a tab by ID.
|
||||
///
|
||||
/// The `notebook` argument is only a fallback: the tab's *own* notebook is
|
||||
/// resolved from the per-tab widget map, because a tab may live in an
|
||||
/// auxiliary window's notebook (whose page numbering differs from the main
|
||||
/// notebook). Mirrors the C `tab_manager_close_tab()` (tab_manager.c:3902).
|
||||
pub fn tab_manager_close_tab(notebook: >k::Notebook, tab_id: i32) {
|
||||
// Resolve the tab's own notebook (fall back to the passed one).
|
||||
let nb = tab_notebook(tab_id).unwrap_or_else(|| notebook.clone());
|
||||
|
||||
let mut state = G_TAB_STATE.lock().unwrap();
|
||||
if let Some(pos) = state.tabs.iter().position(|t| t.id == tab_id) {
|
||||
state.tabs.remove(pos);
|
||||
if pos < notebook.n_pages() as usize {
|
||||
notebook.remove_page(Some(pos as u32));
|
||||
// Find the page index within the tab's own notebook.
|
||||
if let Some(wv) = tab_webview(tab_id) {
|
||||
let n = nb.n_pages();
|
||||
for i in 0..n {
|
||||
if let Some(page) = nb.nth_page(Some(i)) {
|
||||
if find_webview(&page).map(|w| w == wv).unwrap_or(false) {
|
||||
nb.remove_page(Some(i));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// If no tabs remain and not suppressed, quit.
|
||||
drop(state);
|
||||
|
||||
// Drop the widget map entry.
|
||||
G_TAB_WIDGETS.lock().unwrap().remove(&tab_id);
|
||||
|
||||
// If this was an auxiliary window's last tab, close that window.
|
||||
let main_nb = tab_manager_get_main_notebook();
|
||||
let is_main = main_nb.as_ref().map(|m| m == &nb).unwrap_or(false);
|
||||
if !is_main && nb.n_pages() == 0 {
|
||||
if let Some(win) = nb.toplevel().and_then(|w| w.downcast::<gtk::Window>().ok()) {
|
||||
win.close();
|
||||
}
|
||||
}
|
||||
|
||||
// If no tabs remain anywhere and not suppressed, quit.
|
||||
let state = G_TAB_STATE.lock().unwrap();
|
||||
if state.tabs.is_empty() && !state.suppress_quit_on_last_tab {
|
||||
drop(state);
|
||||
gtk::main_quit();
|
||||
}
|
||||
}
|
||||
@@ -738,7 +890,24 @@ pub fn tab_manager_close_active() {
|
||||
}
|
||||
|
||||
/// Get the active tab ID.
|
||||
///
|
||||
/// Resolves the tab from the active notebook's current page (works across
|
||||
/// windows), falling back to the last-focused tab id. Mirrors the C
|
||||
/// `tab_manager_get_active()` (tab_manager.c:3993).
|
||||
pub fn tab_manager_get_active_tab() -> i32 {
|
||||
if let Some(nb) = tab_manager_get_active_notebook() {
|
||||
let cur = nb.current_page().unwrap_or(0);
|
||||
if let Some(page) = nb.nth_page(Some(cur)) {
|
||||
if let Some(wv) = find_webview(&page) {
|
||||
let widgets = G_TAB_WIDGETS.lock().unwrap();
|
||||
for (id, (_, w)) in widgets.iter() {
|
||||
if w.get_ref() == &wv {
|
||||
return *id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let state = G_TAB_STATE.lock().unwrap();
|
||||
state.active_tab_id
|
||||
}
|
||||
@@ -966,8 +1135,8 @@ pub fn tab_manager_open_file() {
|
||||
|
||||
/// Create a new blank window.
|
||||
pub fn tab_manager_new_window_blank() {
|
||||
// Get the context from the main window's active webview.
|
||||
if let Some(nb) = tab_manager_get_main_notebook() {
|
||||
// Get the context from the active window's active webview.
|
||||
if let Some(nb) = tab_manager_get_active_notebook() {
|
||||
if let Some(wv) = tab_manager_get_active_webview(&nb) {
|
||||
if let Some(ctx) = wv.web_context() {
|
||||
tab_manager_new_window(None, &ctx);
|
||||
@@ -979,7 +1148,11 @@ pub fn tab_manager_new_window_blank() {
|
||||
}
|
||||
|
||||
/// Create a new top-level window with a single tab.
|
||||
pub fn tab_manager_new_window(url: Option<&str>, ctx: &WebContext) {
|
||||
///
|
||||
/// Returns the new window's webview (or `None` if creation failed), so
|
||||
/// callers such as "Open in New Window" can close the original tab after
|
||||
/// the move. Mirrors the C `tab_manager_new_window()` (tab_manager.c:1263).
|
||||
pub fn tab_manager_new_window(url: Option<&str>, ctx: &WebContext) -> Option<WebView> {
|
||||
let window = gtk::Window::new(gtk::WindowType::Toplevel);
|
||||
window.set_title("sovereign browser");
|
||||
window.set_default_size(1024, 768);
|
||||
@@ -995,15 +1168,65 @@ pub fn tab_manager_new_window(url: Option<&str>, ctx: &WebContext) {
|
||||
setup_notebook_action_widgets(¬ebook);
|
||||
|
||||
// Create the first tab.
|
||||
tab_manager_new_tab(¬ebook, ctx, url);
|
||||
let tab_id = tab_manager_new_tab(¬ebook, ctx, url);
|
||||
if tab_id < 0 {
|
||||
window.close();
|
||||
return None;
|
||||
}
|
||||
let webview = tab_webview(tab_id);
|
||||
|
||||
// On destroy, do not quit the app (only the main window quits).
|
||||
window.connect_destroy(|_| {
|
||||
// no-op: aux window close does not quit
|
||||
// Focus-in updates the active notebook so "active" operations target
|
||||
// this window. Mirrors the C `on_window_focus_in` (tab_manager.c:1367).
|
||||
let notebook_focus = notebook.clone();
|
||||
window.connect_focus_in_event(move |_w, _e| {
|
||||
set_active_notebook(¬ebook_focus);
|
||||
glib::Propagation::Proceed
|
||||
});
|
||||
|
||||
// On destroy, revert the active notebook to the main window, drop this
|
||||
// window from the aux list, and remove its tabs from the global state
|
||||
// (their widgets are gone with the window). Does NOT quit the app.
|
||||
let window_weak = window.clone();
|
||||
let notebook_destroy = notebook.clone();
|
||||
window.connect_destroy(move |_| {
|
||||
let mut aux = G_AUX_WINDOWS.lock().unwrap();
|
||||
aux.retain(|g| g.get_ref() != &window_weak);
|
||||
drop(aux);
|
||||
|
||||
// Remove tabs that belonged to this window's notebook.
|
||||
let orphan_ids: Vec<i32> = {
|
||||
let widgets = G_TAB_WIDGETS.lock().unwrap();
|
||||
widgets
|
||||
.iter()
|
||||
.filter(|(_, (nb, _))| nb.get_ref() == ¬ebook_destroy)
|
||||
.map(|(id, _)| *id)
|
||||
.collect()
|
||||
};
|
||||
if !orphan_ids.is_empty() {
|
||||
let mut state = G_TAB_STATE.lock().unwrap();
|
||||
state.tabs.retain(|t| !orphan_ids.contains(&t.id));
|
||||
drop(state);
|
||||
let mut widgets = G_TAB_WIDGETS.lock().unwrap();
|
||||
for id in &orphan_ids {
|
||||
widgets.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
revert_active_notebook_to_main();
|
||||
});
|
||||
|
||||
// Register this window as an auxiliary window.
|
||||
G_AUX_WINDOWS
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(glib::thread_guard::ThreadGuard::new(window.clone()));
|
||||
|
||||
// This new window is now the active one.
|
||||
set_active_notebook(¬ebook);
|
||||
|
||||
window.show_all();
|
||||
window.present();
|
||||
webview
|
||||
}
|
||||
|
||||
/// Show the tab context menu (right-click on a tab label).
|
||||
@@ -1046,30 +1269,15 @@ fn show_tab_context_menu(notebook: >k::Notebook, tab_id: i32, event: &gdk::Eve
|
||||
menu.append(>k::SeparatorMenuItem::new());
|
||||
|
||||
let item_new_win = gtk::MenuItem::with_label("Open in New Window");
|
||||
let notebook_clone_nw = notebook.clone();
|
||||
item_new_win.connect_activate(move |_| {
|
||||
let state = G_TAB_STATE.lock().unwrap();
|
||||
let url = state.tabs.iter().find(|t| t.id == tab_id).map(|t| t.url.clone());
|
||||
drop(state);
|
||||
if let Some(wv) = tab_manager_get_active_webview(¬ebook_clone_nw) {
|
||||
if let Some(ctx) = wv.web_context() {
|
||||
tab_manager_new_window(url.as_deref(), &ctx);
|
||||
}
|
||||
}
|
||||
// Moves the tab to a new window (closes the original).
|
||||
tab_manager_open_in_new_window(tab_id);
|
||||
});
|
||||
menu.append(&item_new_win);
|
||||
|
||||
let item_dup = gtk::MenuItem::with_label("Duplicate Tab");
|
||||
let notebook_clone5 = notebook.clone();
|
||||
item_dup.connect_activate(move |_| {
|
||||
let state = G_TAB_STATE.lock().unwrap();
|
||||
let url = state.tabs.iter().find(|t| t.id == tab_id).map(|t| t.url.clone());
|
||||
drop(state);
|
||||
if let Some(wv) = tab_manager_get_active_webview(¬ebook_clone5) {
|
||||
if let Some(ctx) = wv.web_context() {
|
||||
tab_manager_new_tab(¬ebook_clone5, &ctx, url.as_deref());
|
||||
}
|
||||
}
|
||||
tab_manager_duplicate(tab_id);
|
||||
});
|
||||
menu.append(&item_dup);
|
||||
|
||||
@@ -1079,6 +1287,23 @@ fn show_tab_context_menu(notebook: >k::Notebook, tab_id: i32, event: &gdk::Eve
|
||||
});
|
||||
menu.append(&item_reload);
|
||||
|
||||
menu.append(>k::SeparatorMenuItem::new());
|
||||
|
||||
// Download Website — mirrors the C context menu (tab_manager.c:818).
|
||||
let item_dl = gtk::MenuItem::with_label("Download Website");
|
||||
item_dl.connect_activate(move |_| {
|
||||
let url = tab_webview(tab_id)
|
||||
.and_then(|wv| wv.uri().map(|u| u.to_string()))
|
||||
.unwrap_or_default();
|
||||
if !url.is_empty() {
|
||||
let _ = crate::site_downloader::site_downloader_download(
|
||||
&url,
|
||||
"/tmp/sovereign_download",
|
||||
);
|
||||
}
|
||||
});
|
||||
menu.append(&item_dl);
|
||||
|
||||
menu.show_all();
|
||||
menu.popup_at_pointer(Some(event));
|
||||
}
|
||||
@@ -1109,51 +1334,62 @@ pub fn tab_manager_close_to_right(notebook: >k::Notebook, tab_id: i32) {
|
||||
|
||||
/// Reload a specific tab by ID.
|
||||
pub fn tab_manager_reload_tab(tab_id: i32) {
|
||||
if let Some(nb) = tab_manager_get_active_notebook() {
|
||||
let n = nb.n_pages();
|
||||
for i in 0..n {
|
||||
if let Some(page) = nb.nth_page(Some(i)) {
|
||||
if let Some(wv) = find_webview(&page) {
|
||||
// Match by tab id via the state.
|
||||
let state = G_TAB_STATE.lock().unwrap();
|
||||
let matches = state.tabs.get(i as usize).map(|t| t.id == tab_id).unwrap_or(false);
|
||||
drop(state);
|
||||
if matches {
|
||||
wv.reload();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(wv) = tab_webview(tab_id) {
|
||||
wv.reload_bypass_cache();
|
||||
}
|
||||
}
|
||||
|
||||
/// Duplicate a tab by ID.
|
||||
/// Duplicate a tab by ID (opens the same URL in a new tab in the same window).
|
||||
pub fn tab_manager_duplicate(tab_id: i32) {
|
||||
let state = G_TAB_STATE.lock().unwrap();
|
||||
let url = state.tabs.iter().find(|t| t.id == tab_id).map(|t| t.url.clone());
|
||||
drop(state);
|
||||
if let Some(nb) = tab_manager_get_active_notebook() {
|
||||
if let Some(wv) = tab_manager_get_active_webview(&nb) {
|
||||
if let Some(ctx) = wv.web_context() {
|
||||
tab_manager_new_tab(&nb, &ctx, url.as_deref());
|
||||
}
|
||||
// Duplicate into the tab's own notebook (its window).
|
||||
let nb = match tab_notebook(tab_id) {
|
||||
Some(n) => n,
|
||||
None => return,
|
||||
};
|
||||
if let Some(wv) = tab_webview(tab_id) {
|
||||
if let Some(ctx) = wv.web_context() {
|
||||
tab_manager_new_tab(&nb, &ctx, url.as_deref());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Open a tab in a new window by ID.
|
||||
///
|
||||
/// This *moves* the tab: the URL is opened in a brand-new top-level window
|
||||
/// and the original tab is closed. Mirrors the C
|
||||
/// `tab_manager_open_in_new_window()` (tab_manager.c:4141).
|
||||
pub fn tab_manager_open_in_new_window(tab_id: i32) {
|
||||
let state = G_TAB_STATE.lock().unwrap();
|
||||
let url = state.tabs.iter().find(|t| t.id == tab_id).map(|t| t.url.clone());
|
||||
drop(state);
|
||||
if let Some(nb) = tab_manager_get_active_notebook() {
|
||||
if let Some(wv) = tab_manager_get_active_webview(&nb) {
|
||||
if let Some(ctx) = wv.web_context() {
|
||||
tab_manager_new_window(url.as_deref(), &ctx);
|
||||
}
|
||||
}
|
||||
// Capture the URL and the tab's own notebook before moving.
|
||||
let (url, nb) = {
|
||||
let state = G_TAB_STATE.lock().unwrap();
|
||||
let url = state.tabs.iter().find(|t| t.id == tab_id).map(|t| t.url.clone());
|
||||
(url, tab_notebook(tab_id))
|
||||
};
|
||||
let nb = match nb {
|
||||
Some(n) => n,
|
||||
None => return,
|
||||
};
|
||||
|
||||
// Get a WebContext from the tab's webview (or the notebook's active one).
|
||||
let ctx = tab_webview(tab_id)
|
||||
.and_then(|wv| wv.web_context())
|
||||
.or_else(|| tab_manager_get_active_webview(&nb).and_then(|wv| wv.web_context()));
|
||||
let ctx = match ctx {
|
||||
Some(c) => c,
|
||||
None => return,
|
||||
};
|
||||
|
||||
// Open the URL in a new window. If creation fails, keep the original tab.
|
||||
if tab_manager_new_window(url.as_deref(), &ctx).is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Close the original tab — "Open in New Window" moves the tab rather
|
||||
// than duplicating it.
|
||||
tab_manager_close_tab(&nb, tab_id);
|
||||
}
|
||||
|
||||
/// Build the hamburger menu button.
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
//! Version information for sovereign_browser
|
||||
|
||||
/// The current version of sovereign_browser (with leading 'v').
|
||||
pub const VERSION: &str = "v0.0.7";
|
||||
pub const VERSION: &str = "v0.0.8";
|
||||
|
||||
/// Major version number.
|
||||
pub const VERSION_MAJOR: u32 = 0;
|
||||
@@ -10,4 +10,4 @@ pub const VERSION_MAJOR: u32 = 0;
|
||||
pub const VERSION_MINOR: u32 = 0;
|
||||
|
||||
/// Patch version number.
|
||||
pub const VERSION_PATCH: u32 = 7;
|
||||
pub const VERSION_PATCH: u32 = 8;
|
||||
|
||||
@@ -27,6 +27,13 @@ pub fn web_context_create() -> WebContext {
|
||||
sec_mgr.register_uri_scheme_as_secure("nostr");
|
||||
}
|
||||
|
||||
// Enable the favicon database so `notify::favicon` fires and
|
||||
// `webview.favicon()` returns a surface. Without a directory the
|
||||
// database is disabled and tab favicons never appear. Mirrors the C
|
||||
// `webkit_web_context_set_favicon_database_directory()` (web_context.c:237).
|
||||
let fav_dir = std::env::temp_dir().join("sovereign_browser-favicons");
|
||||
ctx.set_favicon_database_directory(fav_dir.to_str());
|
||||
|
||||
ctx
|
||||
}
|
||||
|
||||
|
||||
+8
-1
@@ -12,7 +12,7 @@ pub fn webkit_data_get_dir() -> PathBuf {
|
||||
}
|
||||
|
||||
/// Configure the website data manager for a profile.
|
||||
pub fn webkit_data_configure(_web_context: &WebContext, pubkey_hex: &str) {
|
||||
pub fn webkit_data_configure(web_context: &WebContext, pubkey_hex: &str) {
|
||||
let data_dir = webkit_data_get_dir().join(pubkey_hex);
|
||||
std::fs::create_dir_all(&data_dir).ok();
|
||||
|
||||
@@ -24,4 +24,11 @@ pub fn webkit_data_configure(_web_context: &WebContext, pubkey_hex: &str) {
|
||||
let _manager = WebsiteDataManager::builder()
|
||||
.local_storage_directory(data_dir.join("localstorage").to_str().unwrap_or(""))
|
||||
.build();
|
||||
|
||||
// Per-user favicon database so a switched user doesn't see the previous
|
||||
// user's visited-site icons in the tab strip. Mirrors the C
|
||||
// `webkit_web_context_set_favicon_database_directory()` (web_context.c:237).
|
||||
let fav_dir = data_dir.join("favicons");
|
||||
std::fs::create_dir_all(&fav_dir).ok();
|
||||
web_context.set_favicon_database_directory(fav_dir.to_str());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user