diff --git a/tauri/Cargo.toml b/tauri/Cargo.toml index cc9b5a0c3..a9d821760 100644 --- a/tauri/Cargo.toml +++ b/tauri/Cargo.toml @@ -25,4 +25,5 @@ notify = "8.2.0" notify-debouncer-mini = "0.7.0" tauri-plugin-dialog = "2.8.2" tauri-plugin-single-instance = "2.5.3" +tauri-plugin-opener = "2" diff --git a/tauri/src/commands/mod.rs b/tauri/src/commands/mod.rs index a5f783ac8..165d1c972 100644 --- a/tauri/src/commands/mod.rs +++ b/tauri/src/commands/mod.rs @@ -12,3 +12,4 @@ pub mod queries; pub mod selection; pub mod statistics; pub mod storage; +pub mod system; diff --git a/tauri/src/commands/system.rs b/tauri/src/commands/system.rs new file mode 100644 index 000000000..f7937a8b4 --- /dev/null +++ b/tauri/src/commands/system.rs @@ -0,0 +1,18 @@ +//! What the app asks of the system, outside of the files. + +use tauri::{AppHandle, Runtime}; +use tauri_plugin_opener::OpenerExt; + +/// Opens a web page (or an email to write) with the app of the system for it: the links of the +/// page are not followed in the window of the app. Only `http`, `https` and `mailto` addresses are +/// opened. +#[tauri::command(async)] +pub fn open_url(app: AppHandle, url: String) -> Result<(), String> { + let allowed = ["http://", "https://", "mailto:"]; + if !allowed.iter().any(|scheme| url.starts_with(scheme)) { + return Err(format!("{url} is not a web address")); + } + app.opener() + .open_url(url, None::<&str>) + .map_err(|error| error.to_string()) +} diff --git a/tauri/src/lib.rs b/tauri/src/lib.rs index 6951efa22..679ab02a4 100644 --- a/tauri/src/lib.rs +++ b/tauri/src/lib.rs @@ -50,6 +50,7 @@ pub fn run() { )); register_commands(builder) .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_opener::init()) .setup(|app| { let handle = app.handle().clone(); let emit: state::Emit = std::sync::Arc::new(move |event, payload| { @@ -193,6 +194,8 @@ fn register_commands(builder: tauri::Builder) -> tauri::Bu disk::reload_files, disk::export_files, disk::list_folder, + // the system + system::open_url, ]) } diff --git a/website/src/lib/desktop/links.ts b/website/src/lib/desktop/links.ts new file mode 100644 index 000000000..ccf2ad29c --- /dev/null +++ b/website/src/lib/desktop/links.ts @@ -0,0 +1,52 @@ +import { engine } from '$lib/engine'; + +/** Where the pages of the website are, for the links of the desktop app to them. */ +const website = 'https://gpx.studio'; + +let installed = false; + +/** The address to open in the browser for a link: the pages of the website are on the website. */ +function browserURL(href: string): string { + const url = new URL(href, location.href); + return url.origin === location.origin + ? `${website}${url.pathname}${url.search}${url.hash}` + : url.href; +} + +/** + * Opens the links that leave the app in the browser of the system, as the window of the desktop app + * does not follow them: the external links, the links that open a new tab (the pages of the + * website, like the help), and what `window.open` is given. The other links are pages of the app. + * It is done once, for as long as the window is open. + */ +export function openLinksInBrowser() { + if (installed) { + return; + } + installed = true; + const onClick = (e: MouseEvent) => { + // the main button, or the middle one which opens a new tab + if (e.button > 1) { + return; + } + const link = (e.target as Element | null)?.closest?.('a[href]'); + if (!(link instanceof HTMLAnchorElement)) { + return; + } + const url = new URL(link.href, location.href); + if (url.origin === location.origin && link.target !== '_blank' && e.button === 0) { + return; + } + e.preventDefault(); + engine.openUrl(browserURL(link.href)); + }; + window.open = (url?: string | URL) => { + if (url !== undefined) { + engine.openUrl(browserURL(String(url))); + } + return null; + }; + // before the router of the app, which would follow the link + document.addEventListener('click', onClick, true); + document.addEventListener('auxclick', onClick, true); +} diff --git a/website/src/lib/engine/backend/tauri.ts b/website/src/lib/engine/backend/tauri.ts index 1bc677e1f..76422949d 100644 --- a/website/src/lib/engine/backend/tauri.ts +++ b/website/src/lib/engine/backend/tauri.ts @@ -221,6 +221,7 @@ export async function createTauriBackend(): Promise { exportFiles: (fileIds, options) => invoke('export_files', { fileIds, options }), listFolder: (path) => invoke('list_folder', { path }), + openUrl: (url) => invoke('open_url', { url }), listen: async (onEngineChanged, onWorkspaceChanged, onFilesOpened, onFolderChanged) => { await listen('files-opened', (event) => onFilesOpened(event.payload)); await listen('engine-changed', (event) => onEngineChanged(event.payload)); diff --git a/website/src/lib/engine/backend/types.ts b/website/src/lib/engine/backend/types.ts index 6cb1548c1..6333eaf77 100644 --- a/website/src/lib/engine/backend/types.ts +++ b/website/src/lib/engine/backend/types.ts @@ -89,6 +89,8 @@ export interface DesktopFiles { exportFiles(fileIds: string[], options: ExportOptions): Promise; /** The folders and GPX files in a folder of the open folder (the open folder by default). */ listFolder(path?: string): Promise; + /** Opens a web page (or an email to write) in the app of the system for it. */ + openUrl(url: string): Promise; /** * Tells when the engine changed without being asked, because the files on the disk did (the * outcome to apply), when the status of the files changed, when files were opened with the diff --git a/website/src/lib/engine/engine.ts b/website/src/lib/engine/engine.ts index b54344643..f16439170 100644 --- a/website/src/lib/engine/engine.ts +++ b/website/src/lib/engine/engine.ts @@ -617,6 +617,16 @@ class Engine { await this.enqueue(async () => this.refreshWorkspace(this.backend!)); } + /** Opens a web page in the browser of the system (the desktop app). */ + async openUrl(url: string) { + await this.ready; + try { + await this.backend!.desktop?.openUrl(url); + } catch (error) { + console.error('The page could not be opened', error); + } + } + /** The folders and GPX files in a folder of the open folder (the open folder by default). */ async listFolder(path?: string): Promise { await this.ready; diff --git a/website/src/routes/[[language]]/app/+page.svelte b/website/src/routes/[[language]]/app/+page.svelte index dcd2c9313..619c34e48 100644 --- a/website/src/routes/[[language]]/app/+page.svelte +++ b/website/src/routes/[[language]]/app/+page.svelte @@ -22,6 +22,7 @@ import { get } from 'svelte/store'; import { getURLForLanguage } from '$lib/utils'; import { inDesktopApp } from '$lib/engine/backend'; + import { openLinksInBrowser } from '$lib/desktop/links'; import { hoveredPoint, slicedStatistics } from '$lib/state/selection-statistics'; import { engine } from '$lib/engine'; import { boundsManager } from '$lib/state/bounds'; @@ -72,6 +73,7 @@ settings.connect(await engine.openStorage()); if (inDesktopApp()) { + openLinksInBrowser(); // the window of the desktop app always opens the same page, in English: it goes to the // language that was chosen last const saved = get(settings.language);