mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-10 16:07:21 +00:00
Fixed internal links opening in the system browser and limited the right-click menu (#1171)
This commit is contained in:
1 parent
c37393817b
commit
bbf4dc2231
8 files changed
+374
-31
No files matched your search
@@ -40,6 +40,7 @@
|
||||
- Fixed Semantic Search leaving out passages of your data sources which did not fit into a single search. The AI now gets every passage it finds, page by page, and a passage too long for a whole page is shortened instead of left out.
|
||||
- Fixed chats with data sources failing when you switched off Semantic Search, because AI Studio sent more passages along with your message than the AI model can read. AI Studio now sends only as many passages as fit, taking turns among your data sources, and tells you with each answer when some passages had to be left out.
|
||||
- Fixed the AI search in your mailboxes leaving out mails which did not fit into a single search. The AI now gets every mail it finds, page by page.
|
||||
- Fixed pages of AI Studio, such as those behind the icons of the navigation bar, opening in your web browser when you clicked them while holding Shift or Ctrl. Such clicks now do nothing, while links to websites still open in your browser, and now also from the AppImage on Linux. The right-click menu now appears only in text fields and on selected text, so it no longer offers entries such as opening a link in a new window.
|
||||
- Upgraded several libraries to improve security.
|
||||
- Upgraded Tauri to v2.12.5.
|
||||
- Upgraded MudBlazor to v9.12.0.
|
||||
|
||||
@@ -42,6 +42,34 @@ the window as an HttpOnly session cookie before the window opens the app (`chang
|
||||
`APP_HOST` and in `Program.cs` together, since no test compares them. `AppAccessRuntimeAgreementTests`
|
||||
only checks that both sides use the same cookie name and environment variable.
|
||||
|
||||
**Only the runtime lets links leave the window.** AI Studio shows a single window, and a browser outside
|
||||
it cannot show the app, since it lacks the session cookie. Therefore:
|
||||
|
||||
- `should_open_in_system_browser` in `app_window.rs` alone decides which address goes to the system: a
|
||||
website, another local server, or a `mailto:` or `tel:` link, but never an address of the app. The
|
||||
handlers for navigations and for new windows both ask it.
|
||||
- On Linux, `open_in_system_browser` hands such an address to the desktop portal, as the Flatpak does
|
||||
anyway; the opener plugin only steps in when there is no portal. Started from an AppImage, xdg-open
|
||||
inherits the environment of the AppImage, and the browser stays closed without any error.
|
||||
- The handler for new windows always denies the window. Only a window which `setup` builds itself can
|
||||
have that handler, which is why `tauri.conf.json` sets `"create": false`.
|
||||
- Keep the link script of the opener plugin switched off (`open_js_links_on_click(false)`): it opens
|
||||
every link clicked while holding Shift or Ctrl in the system browser, links of the app included. The
|
||||
shell plugin has a link script as well, which cannot be switched off and opens every link with a
|
||||
target the same way.
|
||||
- `link_guard.js` takes every click which would open a link somewhere else away from both scripts and
|
||||
from the page. On a link of the app, the click does nothing; on any other link, it goes on to the
|
||||
webview, whose request for a new window or a navigation reaches the runtime. Therefore, give a link
|
||||
with a target no click handler of its own: no script of the page sees its click.
|
||||
- No script of the page may open or start anything, so `capabilities/default.json` grants the opener
|
||||
and shell plugins nothing. The runtime calls both from Rust, which needs no permission, e.g., to
|
||||
start the .NET server in `dotnet.rs`.
|
||||
|
||||
**The right-click menu shows only in text fields and on selected text,** never on a link: its other
|
||||
entries, such as going back, reloading, or opening a link in a new window, make no sense in the app.
|
||||
`context_menu_guard.js` hides it everywhere else, in every frame, but only in release builds, since
|
||||
development builds need the menu to open the developer tools.
|
||||
|
||||
**Runtime API handlers never block.** All calls of the .NET app share one HTTP/2 connection, and the
|
||||
task driving it may wait on exactly the Tokio worker which a blocking handler occupies, so a single
|
||||
blocking handler can hold up the whole app. Therefore:
|
||||
|
||||
@@ -12,18 +12,6 @@
|
||||
],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"updater:default",
|
||||
"opener:default",
|
||||
"shell:allow-open",
|
||||
{
|
||||
"identifier": "shell:allow-spawn",
|
||||
"allow": [
|
||||
{
|
||||
"name": "mindworkAIStudioServer",
|
||||
"sidecar": true,
|
||||
"args": true
|
||||
}
|
||||
]
|
||||
}
|
||||
"updater:default"
|
||||
]
|
||||
}
|
||||
+225
-17
@@ -14,8 +14,8 @@ use pdfium_render::prelude::Pdfium;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{DragDropEvent,RunEvent, Manager, WindowEvent};
|
||||
use tauri::path::PathResolver;
|
||||
use tauri::webview::PageLoadEvent;
|
||||
use tauri::WebviewWindow;
|
||||
use tauri::webview::{NewWindowResponse, PageLoadEvent};
|
||||
use tauri::{WebviewWindow, WebviewWindowBuilder};
|
||||
use tauri::PhysicalPosition;
|
||||
use tauri_plugin_updater::{UpdaterExt, Update};
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
@@ -121,26 +121,27 @@ pub fn start_tauri(tauri_context: tauri::Context<tauri::Wry>) {
|
||||
}))
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
//
|
||||
// The link script of the opener plugin opens every link clicked while holding Shift or
|
||||
// Ctrl in the system browser, links of the app included. Our own script replaces it,
|
||||
// see link_guard.js:
|
||||
//
|
||||
.plugin(tauri_plugin_opener::Builder::new().open_js_links_on_click(false).build())
|
||||
.plugin(
|
||||
tauri::plugin::Builder::<tauri::Wry, ()>::new("external-link-handler")
|
||||
.js_init_script(include_str!("link_guard.js"))
|
||||
.on_navigation(|webview, url| {
|
||||
if !should_open_in_system_browser(webview, url) {
|
||||
let current_url = webview.url().ok();
|
||||
if !should_open_in_system_browser(url, approved_app_url().as_ref(), current_url.as_ref()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
match webview.app_handle().opener().open_url(url.as_str(), None::<&str>) {
|
||||
Ok(_) => {
|
||||
info!(Source = "Tauri"; "Opening external URL in system browser: {url}");
|
||||
},
|
||||
Err(error) => {
|
||||
error!(Source = "Tauri"; "Failed to open external URL '{url}' in system browser: {error}");
|
||||
},
|
||||
}
|
||||
open_in_system_browser(webview.app_handle(), url);
|
||||
false
|
||||
})
|
||||
.build(),
|
||||
)
|
||||
.plugin(context_menu_guard())
|
||||
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.on_page_load(|webview, payload| {
|
||||
@@ -160,12 +161,42 @@ pub fn start_tauri(tauri_context: tauri::Context<tauri::Wry>) {
|
||||
})
|
||||
.setup(move |app| {
|
||||
|
||||
// Get the main window:
|
||||
let window = app.get_webview_window("main").expect("Failed to get main window.");
|
||||
//
|
||||
// We create the main window ourselves rather than letting Tauri create it from the
|
||||
// configuration, where "create" is false therefore. Only a window built here can get a
|
||||
// handler for new windows; neither the configuration nor a plugin can set one.
|
||||
//
|
||||
// AI Studio shows a single window. Whatever asks for another one opens nothing in the
|
||||
// app: a link with a target, a click with Shift or the middle mouse button, or
|
||||
// window.open. An address outside the app goes to the system browser instead, after the
|
||||
// same check as a navigation. The window shows the app as soon as a link can be clicked,
|
||||
// so the approved address of the app decides, and the current address is not needed:
|
||||
//
|
||||
let window_config = app.config().app.windows.iter()
|
||||
.find(|config| config.label == "main")
|
||||
.expect("The configuration lacks the main window.");
|
||||
|
||||
let new_window_app_handle = app.handle().clone();
|
||||
let window = WebviewWindowBuilder::from_config(app.handle(), window_config)
|
||||
.expect("Failed to read the configuration of the main window.")
|
||||
.on_new_window(move |url, _| {
|
||||
if should_open_in_system_browser(&url, approved_app_url().as_ref(), None) {
|
||||
open_in_system_browser(&new_window_app_handle, &url);
|
||||
} else {
|
||||
info!(Source = "Tauri"; "Denied a new window for '{url}'.");
|
||||
}
|
||||
|
||||
NewWindowResponse::Deny
|
||||
})
|
||||
.build()
|
||||
.expect("Failed to create the main window.");
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
register_linux_permission_request_handler(&window);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
disable_link_previews(&window);
|
||||
|
||||
// Register a callback for window events, such as file drops. We have to use
|
||||
// this handler in addition to the app event handler, because file drop events
|
||||
// are only available in the window event handler (is a bug, cf. https://github.com/tauri-apps/tauri/issues/14338):
|
||||
@@ -338,6 +369,28 @@ fn should_allow_audio_capture(
|
||||
.is_some_and(|(approved, current)| same_origin(approved, current))
|
||||
}
|
||||
|
||||
/// Switches off the preview which macOS shows when somebody force-clicks a link or looks it up.
|
||||
/// wry switches it on, and Tauri offers no setting for it. The preview would show a page of the app
|
||||
/// outside the window, and a click on it may open the page in the system browser, which cannot
|
||||
/// show it; see link_guard.js.
|
||||
#[cfg(target_os = "macos")]
|
||||
fn disable_link_previews(window: &WebviewWindow) {
|
||||
use objc2::msg_send;
|
||||
use objc2::runtime::{AnyObject, Bool};
|
||||
|
||||
if let Err(error) = window.with_webview(|platform_webview| {
|
||||
let webview = platform_webview.inner().cast::<AnyObject>();
|
||||
|
||||
// Safety: on macOS, the handle is the WKWebView of the window, and Tauri calls this closure
|
||||
// on the main thread, as AppKit requires:
|
||||
unsafe {
|
||||
let _: () = msg_send![webview, setAllowsLinkPreview: Bool::NO];
|
||||
}
|
||||
}) {
|
||||
error!(Source = "Tauri"; "Failed to switch off the link previews: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn register_linux_permission_request_handler(window: &WebviewWindow) {
|
||||
if let Err(error) = window.with_webview(|platform_webview| {
|
||||
@@ -386,7 +439,30 @@ fn register_linux_permission_request_handler(window: &WebviewWindow) {
|
||||
}
|
||||
}
|
||||
|
||||
fn should_open_in_system_browser<R: tauri::Runtime>(webview: &tauri::Webview<R>, url: &tauri::Url) -> bool {
|
||||
/// Limits the right-click menu to text fields and selected text in every frame, see
|
||||
/// context_menu_guard.js. Development builds keep the menu, since it opens the developer tools,
|
||||
/// which Tauri offers in these builds only.
|
||||
fn context_menu_guard() -> tauri::plugin::TauriPlugin<tauri::Wry> {
|
||||
let plugin = tauri::plugin::Builder::<tauri::Wry, ()>::new("context-menu-guard");
|
||||
if cfg!(debug_assertions) {
|
||||
return plugin.build();
|
||||
}
|
||||
|
||||
plugin.js_init_script_on_all_frames(include_str!("context_menu_guard.js")).build()
|
||||
}
|
||||
|
||||
/// The address of the app, once the window shows it.
|
||||
fn approved_app_url() -> Option<tauri::Url> {
|
||||
APPROVED_APP_URL.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
/// Decides whether an address leaves the window for the system: a website for the browser, or a
|
||||
/// `mailto:` or `tel:` link for the mail or phone app. Navigations and requests for new windows
|
||||
/// both ask here, so that one rule decides what leaves the window.
|
||||
///
|
||||
/// An address of the app never leaves: a browser outside the window has no session cookie, see
|
||||
/// app_access.rs. Another local server, such as a local LLM, leaves once the window shows the app.
|
||||
fn should_open_in_system_browser(url: &tauri::Url, approved_app_url: Option<&tauri::Url>, current_url: Option<&tauri::Url>) -> bool {
|
||||
match url.scheme() {
|
||||
"mailto" | "tel" => return true,
|
||||
"http" | "https" => {},
|
||||
@@ -397,7 +473,7 @@ fn should_open_in_system_browser<R: tauri::Runtime>(webview: &tauri::Webview<R>,
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(approved_app_url) = APPROVED_APP_URL.lock().unwrap().as_ref() {
|
||||
if let Some(approved_app_url) = approved_app_url {
|
||||
if same_origin(approved_app_url, url) {
|
||||
return false;
|
||||
}
|
||||
@@ -407,13 +483,76 @@ fn should_open_in_system_browser<R: tauri::Runtime>(webview: &tauri::Webview<R>,
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(current_url) = webview.url() && same_origin(¤t_url, url) {
|
||||
if current_url.is_some_and(|current_url| same_origin(current_url, url)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
!is_local_host(url.host_str())
|
||||
}
|
||||
|
||||
/// Hands an address outside the app to the system, see `should_open_in_system_browser`.
|
||||
///
|
||||
/// On Linux, the desktop portal opens it, as it does for the Flatpak anyway. The opener plugin
|
||||
/// starts the xdg-open of the system with the environment of AI Studio, which an AppImage points
|
||||
/// at its own GTK and GIO modules: the browser then stays closed, and no error tells why. The
|
||||
/// opener plugin remains for systems without a portal.
|
||||
fn open_in_system_browser<R: tauri::Runtime>(app_handle: &tauri::AppHandle<R>, url: &tauri::Url) {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use crate::file_actions::LinuxPortalError;
|
||||
|
||||
let app_handle = app_handle.clone();
|
||||
let url = url.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
match open_url_with_linux_portal(&url).await {
|
||||
Ok(()) => {
|
||||
info!(Source = "Tauri"; "Opening external URL through the desktop portal: {url}");
|
||||
},
|
||||
Err(LinuxPortalError::RequestFailed(error)) => {
|
||||
error!(Source = "Tauri"; "Failed to open external URL '{url}' through the desktop portal: {error}");
|
||||
},
|
||||
Err(LinuxPortalError::Unavailable(error)) => {
|
||||
warn!(Source = "Tauri"; "The desktop portal cannot open external URL '{url}', so the opener plugin opens it: {error}");
|
||||
open_with_opener(&app_handle, &url);
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
open_with_opener(app_handle, url);
|
||||
}
|
||||
|
||||
/// Opens an address outside the app through the desktop portal, see `open_in_system_browser`.
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn open_url_with_linux_portal(url: &tauri::Url) -> Result<(), crate::file_actions::LinuxPortalError> {
|
||||
use ashpd::desktop::open_uri::OpenFileRequest;
|
||||
use crate::file_actions::LinuxPortalError;
|
||||
|
||||
let uri = ashpd::Uri::parse(url.as_str())
|
||||
.map_err(|error| LinuxPortalError::RequestFailed(format!("The desktop portal cannot take the address: {error}")))?;
|
||||
|
||||
let request = OpenFileRequest::default()
|
||||
.send_uri(&uri)
|
||||
.await
|
||||
.map_err(|error| LinuxPortalError::Unavailable(format!("Desktop portal invocation failed: {error}")))?;
|
||||
|
||||
request.response()
|
||||
.map_err(|error| LinuxPortalError::RequestFailed(format!("Desktop portal request failed: {error}")))
|
||||
}
|
||||
|
||||
/// Opens an address outside the app with the opener plugin, see `open_in_system_browser`.
|
||||
fn open_with_opener<R: tauri::Runtime>(app_handle: &tauri::AppHandle<R>, url: &tauri::Url) {
|
||||
match app_handle.opener().open_url(url.as_str(), None::<&str>) {
|
||||
Ok(_) => {
|
||||
info!(Source = "Tauri"; "Opening external URL in system browser: {url}");
|
||||
},
|
||||
Err(error) => {
|
||||
error!(Source = "Tauri"; "Failed to open external URL '{url}' in system browser: {error}");
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Our event API endpoint for Tauri events. We try to send an endless stream of events to the client.
|
||||
/// If no events are available for a certain time, we send a ping event to keep the connection alive.
|
||||
/// When the client disconnects, the stream is closed. But we try to not lose events in between.
|
||||
@@ -1365,6 +1504,75 @@ mod tests {
|
||||
assert!(!is_local_http_url(&url));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_url_never_opens_in_system_browser() {
|
||||
let approved = tauri::Url::parse("http://127.0.0.1:12345/").unwrap();
|
||||
for address in [
|
||||
"http://127.0.0.1:12345/",
|
||||
"http://127.0.0.1:12345/chat",
|
||||
"http://127.0.0.1:12345/settings?section=providers",
|
||||
"http://127.0.0.1:12345/about#licenses",
|
||||
] {
|
||||
let url = tauri::Url::parse(address).unwrap();
|
||||
|
||||
assert!(!should_open_in_system_browser(&url, Some(&approved), None), "{address}");
|
||||
assert!(!should_open_in_system_browser(&url, Some(&approved), Some(&approved)), "{address}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_url_stays_in_window_before_it_is_approved() {
|
||||
let current = tauri::Url::parse("http://127.0.0.1:12345/").unwrap();
|
||||
let url = tauri::Url::parse("http://127.0.0.1:12345/chat").unwrap();
|
||||
|
||||
assert!(!should_open_in_system_browser(&url, None, None));
|
||||
assert!(!should_open_in_system_browser(&url, None, Some(¤t)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_website_opens_in_system_browser() {
|
||||
let approved = tauri::Url::parse("http://127.0.0.1:12345/").unwrap();
|
||||
let url = tauri::Url::parse("https://example.com/docs").unwrap();
|
||||
|
||||
assert!(should_open_in_system_browser(&url, Some(&approved), None));
|
||||
assert!(should_open_in_system_browser(&url, None, None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mail_and_phone_links_open_in_system() {
|
||||
let approved = tauri::Url::parse("http://127.0.0.1:12345/").unwrap();
|
||||
for address in ["mailto:someone@example.com", "tel:+490000000"] {
|
||||
let url = tauri::Url::parse(address).unwrap();
|
||||
|
||||
assert!(should_open_in_system_browser(&url, Some(&approved), None), "{address}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn other_local_server_opens_in_system_browser_once_app_is_approved() {
|
||||
let approved = tauri::Url::parse("http://127.0.0.1:12345/").unwrap();
|
||||
let url = tauri::Url::parse("http://localhost:11434/").unwrap();
|
||||
|
||||
assert!(should_open_in_system_browser(&url, Some(&approved), None));
|
||||
assert!(!should_open_in_system_browser(&url, None, None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn other_schemes_never_open_in_system_browser() {
|
||||
let approved = tauri::Url::parse("http://127.0.0.1:12345/").unwrap();
|
||||
for address in [
|
||||
"file:///etc/passwd",
|
||||
"javascript:alert(1)",
|
||||
"data:text/html,hello",
|
||||
"tauri://localhost/index.html",
|
||||
"https://tauri.localhost/index.html",
|
||||
] {
|
||||
let url = tauri::Url::parse(address).unwrap();
|
||||
|
||||
assert!(!should_open_in_system_browser(&url, Some(&approved), None), "{address}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_capture_is_allowed_for_exact_approved_app_origin() {
|
||||
let approved = tauri::Url::parse("http://localhost:12345/").unwrap();
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// Shows the right-click menu of the webview only where it helps: in text fields, where it offers
|
||||
// pasting and spelling suggestions, and on selected text, where it offers copying. Elsewhere, its
|
||||
// entries make no sense in the app, such as going back, reloading, printing, or opening a link in
|
||||
// a new window. On a link, the menu never shows, not even within a selection.
|
||||
//
|
||||
// The runtime adds this script to every frame of the window, but only in release builds:
|
||||
// development builds keep the menu, since it opens the developer tools. See runtime/AGENTS.md.
|
||||
//
|
||||
(() => {
|
||||
const TEXT_INPUT_TYPES = ['text', 'search', 'email', 'url', 'tel', 'password', 'number'];
|
||||
|
||||
const isTextField = element =>
|
||||
element instanceof HTMLTextAreaElement
|
||||
|| (element instanceof HTMLInputElement && TEXT_INPUT_TYPES.includes(element.type))
|
||||
|| (element instanceof HTMLElement && element.isContentEditable);
|
||||
|
||||
const isOnSelection = event => {
|
||||
const selection = window.getSelection();
|
||||
if (!selection || selection.isCollapsed || selection.toString().trim() === '')
|
||||
return false;
|
||||
|
||||
for (let index = 0; index < selection.rangeCount; index++)
|
||||
for (const rect of selection.getRangeAt(index).getClientRects())
|
||||
if (event.clientX >= rect.left && event.clientX <= rect.right && event.clientY >= rect.top && event.clientY <= rect.bottom)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
window.addEventListener('contextmenu', event => {
|
||||
const path = event.composedPath();
|
||||
if (path.some(node => node instanceof HTMLAnchorElement && node.hasAttribute('href'))) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
const target = path.find(node => node instanceof Element);
|
||||
if (target && isTextField(target))
|
||||
return;
|
||||
|
||||
if (isOnSelection(event))
|
||||
return;
|
||||
|
||||
event.preventDefault();
|
||||
}, true);
|
||||
})();
|
||||
@@ -1139,7 +1139,7 @@ fn xdg_open_fallback_path(target: &FileManagerTarget) -> &Path {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
enum LinuxPortalError {
|
||||
pub(crate) enum LinuxPortalError {
|
||||
Unavailable(String),
|
||||
RequestFailed(String),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
//
|
||||
// Keeps the links of the app inside its window and hands every other link to the runtime. The
|
||||
// runtime adds this script to the main frame of the window, where it runs before any script of the
|
||||
// page; see "Only the runtime lets links leave the window" in runtime/AGENTS.md.
|
||||
//
|
||||
// This script acts on a click which would open a link somewhere else: a click while holding Shift,
|
||||
// Ctrl, Cmd, or Alt, a click with another mouse button than the main one, or a click on a link with
|
||||
// a target such as _blank.
|
||||
//
|
||||
// On a link of the app, such a click does nothing. A browser outside the window cannot show the
|
||||
// app anyway, since it lacks the session cookie, and the webview would load the whole app anew.
|
||||
//
|
||||
// On any other link, the click goes on to the webview, which asks the runtime for a new window or
|
||||
// a navigation. The runtime opens the link in the system browser instead, on Linux through the
|
||||
// desktop portal. No script of the page sees such a click: the link script of the shell plugin
|
||||
// would open every link with a target itself, links of the app included, and through an xdg-open
|
||||
// which fails in an AppImage.
|
||||
//
|
||||
// Dragging a link of the app does nothing either, since dropping it onto a browser would open it
|
||||
// there.
|
||||
//
|
||||
(() => {
|
||||
const TARGETS_OF_THIS_WINDOW = ['', '_self', '_top', '_parent'];
|
||||
|
||||
const findLink = event =>
|
||||
event.composedPath().find(node => node instanceof HTMLAnchorElement && node.hasAttribute('href')) ?? null;
|
||||
|
||||
// Where a link leads: 'app', 'outside', or null for an address the webview cannot read.
|
||||
const destinationOf = link => {
|
||||
try {
|
||||
return new URL(link.href, location.href).origin === location.origin ? 'app' : 'outside';
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const opensElsewhere = (event, link) =>
|
||||
event.button !== 0
|
||||
|| event.shiftKey
|
||||
|| event.ctrlKey
|
||||
|| event.metaKey
|
||||
|| event.altKey
|
||||
|| !TARGETS_OF_THIS_WINDOW.includes(link.target.toLowerCase());
|
||||
|
||||
const handleClick = event => {
|
||||
const link = findLink(event);
|
||||
if (!link || !opensElsewhere(event, link))
|
||||
return;
|
||||
|
||||
switch (destinationOf(link)) {
|
||||
case 'app':
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
break;
|
||||
|
||||
case 'outside':
|
||||
event.stopImmediatePropagation();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('click', handleClick, true);
|
||||
window.addEventListener('auxclick', handleClick, true);
|
||||
window.addEventListener('dragstart', event => {
|
||||
const link = findLink(event);
|
||||
if (link && destinationOf(link) === 'app')
|
||||
event.preventDefault();
|
||||
}, true);
|
||||
})();
|
||||
@@ -61,6 +61,8 @@
|
||||
|
||||
"windows": [
|
||||
{
|
||||
"label": "main",
|
||||
"create": false,
|
||||
"fullscreen": false,
|
||||
"resizable": true,
|
||||
"title": "MindWork AI Studio",
|
||||
|
||||
Reference in new issue
Block a user