open links in browser

This commit is contained in:
vcoppe committed 2026-10-17 19:57:50 +02:00
1 parent 8d6885d58b
commit 358253b7a9
9 files changed
+90

No files matched your search

+1
View File
@@ -25,4 +25,5 @@ notify = "8.2.0"
notify-debouncer-mini = "0.7.0" notify-debouncer-mini = "0.7.0"
tauri-plugin-dialog = "2.8.2" tauri-plugin-dialog = "2.8.2"
tauri-plugin-single-instance = "2.5.3" tauri-plugin-single-instance = "2.5.3"
tauri-plugin-opener = "2"
+1
View File
@@ -12,3 +12,4 @@ pub mod queries;
pub mod selection; pub mod selection;
pub mod statistics; pub mod statistics;
pub mod storage; pub mod storage;
pub mod system;
+18
View File
@@ -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<R: Runtime>(app: AppHandle<R>, 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())
}
+3
View File
@@ -50,6 +50,7 @@ pub fn run() {
)); ));
register_commands(builder) register_commands(builder)
.plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_opener::init())
.setup(|app| { .setup(|app| {
let handle = app.handle().clone(); let handle = app.handle().clone();
let emit: state::Emit = std::sync::Arc::new(move |event, payload| { let emit: state::Emit = std::sync::Arc::new(move |event, payload| {
@@ -193,6 +194,8 @@ fn register_commands<R: tauri::Runtime>(builder: tauri::Builder<R>) -> tauri::Bu
disk::reload_files, disk::reload_files,
disk::export_files, disk::export_files,
disk::list_folder, disk::list_folder,
// the system
system::open_url,
]) ])
} }
+52
View File
@@ -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);
}
+1
View File
@@ -221,6 +221,7 @@ export async function createTauriBackend(): Promise<Backend> {
exportFiles: (fileIds, options) => exportFiles: (fileIds, options) =>
invoke<boolean>('export_files', { fileIds, options }), invoke<boolean>('export_files', { fileIds, options }),
listFolder: (path) => invoke<FolderEntry[]>('list_folder', { path }), listFolder: (path) => invoke<FolderEntry[]>('list_folder', { path }),
openUrl: (url) => invoke('open_url', { url }),
listen: async (onEngineChanged, onWorkspaceChanged, onFilesOpened, onFolderChanged) => { listen: async (onEngineChanged, onWorkspaceChanged, onFilesOpened, onFolderChanged) => {
await listen<Outcome>('files-opened', (event) => onFilesOpened(event.payload)); await listen<Outcome>('files-opened', (event) => onFilesOpened(event.payload));
await listen<Outcome>('engine-changed', (event) => onEngineChanged(event.payload)); await listen<Outcome>('engine-changed', (event) => onEngineChanged(event.payload));
+2
View File
@@ -89,6 +89,8 @@ export interface DesktopFiles {
exportFiles(fileIds: string[], options: ExportOptions): Promise<boolean>; exportFiles(fileIds: string[], options: ExportOptions): Promise<boolean>;
/** The folders and GPX files in a folder of the open folder (the open folder by default). */ /** The folders and GPX files in a folder of the open folder (the open folder by default). */
listFolder(path?: string): Promise<FolderEntry[]>; listFolder(path?: string): Promise<FolderEntry[]>;
/** Opens a web page (or an email to write) in the app of the system for it. */
openUrl(url: string): Promise<void>;
/** /**
* Tells when the engine changed without being asked, because the files on the disk did (the * 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 * outcome to apply), when the status of the files changed, when files were opened with the
+10
View File
@@ -617,6 +617,16 @@ class Engine {
await this.enqueue(async () => this.refreshWorkspace(this.backend!)); 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). */ /** The folders and GPX files in a folder of the open folder (the open folder by default). */
async listFolder(path?: string): Promise<FolderEntry[]> { async listFolder(path?: string): Promise<FolderEntry[]> {
await this.ready; await this.ready;
@@ -22,6 +22,7 @@
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import { getURLForLanguage } from '$lib/utils'; import { getURLForLanguage } from '$lib/utils';
import { inDesktopApp } from '$lib/engine/backend'; import { inDesktopApp } from '$lib/engine/backend';
import { openLinksInBrowser } from '$lib/desktop/links';
import { hoveredPoint, slicedStatistics } from '$lib/state/selection-statistics'; import { hoveredPoint, slicedStatistics } from '$lib/state/selection-statistics';
import { engine } from '$lib/engine'; import { engine } from '$lib/engine';
import { boundsManager } from '$lib/state/bounds'; import { boundsManager } from '$lib/state/bounds';
@@ -72,6 +73,7 @@
settings.connect(await engine.openStorage()); settings.connect(await engine.openStorage());
if (inDesktopApp()) { if (inDesktopApp()) {
openLinksInBrowser();
// the window of the desktop app always opens the same page, in English: it goes to the // the window of the desktop app always opens the same page, in English: it goes to the
// language that was chosen last // language that was chosen last
const saved = get(settings.language); const saved = get(settings.language);