From e71bec425944f2947b47350aa1349b8f27bf2685 Mon Sep 17 00:00:00 2001 From: DaVinciLord Date: Mon, 27 Apr 2026 16:40:32 +0200 Subject: [PATCH] fix(tauri): Fix Linux Tauri Popout Windows Handle WebKitGTK window.open requests natively so popout windows work in the Tauri app on Linux. Add a localized error message for cases where no popout window can be created. --- src-tauri/src/constants.rs | 7 ++++ src-tauri/src/main.rs | 57 ++++++++++++++++++++++++-- src-tauri/tauri.conf.json | 9 +--- src/localization/chinese-simplified.ts | 2 + src/localization/dutch.ts | 2 + src/localization/english.ts | 2 + src/localization/french.ts | 2 + src/localization/german.ts | 2 + src/localization/index.ts | 1 + src/localization/italian.ts | 2 + src/localization/japanese.ts | 2 + src/localization/korean.ts | 2 + src/localization/polish.ts | 2 + src/localization/portuguese-brazil.ts | 2 + src/localization/portuguese.ts | 2 + src/localization/russian.ts | 2 + src/localization/spanish.ts | 2 + src/ui/LiveSplit.tsx | 14 ++++--- 18 files changed, 97 insertions(+), 17 deletions(-) create mode 100644 src-tauri/src/constants.rs diff --git a/src-tauri/src/constants.rs b/src-tauri/src/constants.rs new file mode 100644 index 000000000..69f4a49a8 --- /dev/null +++ b/src-tauri/src/constants.rs @@ -0,0 +1,7 @@ +pub const MAIN_WINDOW_LABEL: &str = "main"; +pub const POPOUT_WINDOW_LABEL_PREFIX: &str = "popout"; +pub const APP_TITLE: &str = "LiveSplit One"; +pub const INDEX_HTML: &str = "index.html"; +pub const ABOUT_BLANK: &str = "about:blank"; +pub const MAIN_WINDOW_WIDTH: f64 = 850.0; +pub const MAIN_WINDOW_HEIGHT: f64 = 750.0; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 2e7c59c8e..d7792b8b5 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -4,7 +4,10 @@ use std::{ borrow::Cow, future::Future, str::FromStr, - sync::{Arc, RwLock}, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, RwLock, + }, }; use livesplit_core::{ @@ -13,7 +16,15 @@ use livesplit_core::{ networking::server_protocol::Command, HotkeyConfig, HotkeySystem, TimeSpan, TimingMethod, }; -use tauri::{Emitter, Manager, WebviewWindow}; +use tauri::{ + webview::NewWindowResponse, Emitter, Manager, WebviewUrl, WebviewWindow, WebviewWindowBuilder, +}; + +mod constants; +use constants::{ + ABOUT_BLANK, APP_TITLE, INDEX_HTML, MAIN_WINDOW_HEIGHT, MAIN_WINDOW_LABEL, MAIN_WINDOW_WIDTH, + POPOUT_WINDOW_LABEL_PREFIX, +}; struct State { hotkey_system: RwLock>>, @@ -214,7 +225,47 @@ fn main() { window: RwLock::new(None), }) .setup(move |app| { - let main_window = app.webview_windows().values().next().unwrap().clone(); + let app_handle = app.handle().clone(); + let popout_counter = Arc::new(AtomicUsize::new(0)); + let popout_counter_for_handler = popout_counter.clone(); + + let main_window = WebviewWindowBuilder::new( + app, + MAIN_WINDOW_LABEL, + WebviewUrl::App(INDEX_HTML.into()), + ) + .title(APP_TITLE) + .inner_size(MAIN_WINDOW_WIDTH, MAIN_WINDOW_HEIGHT) + .use_https_scheme(true) + // WebKitGTK can return `null` for the browser-managed `window.open` + // flow. Creating the requested window here keeps the frontend code + // portable while giving Tauri full control over Linux popouts. + .on_new_window(move |_url, features| { + let label = format!( + "{POPOUT_WINDOW_LABEL_PREFIX}-{}", + popout_counter_for_handler.fetch_add(1, Ordering::Relaxed) + ); + let builder = WebviewWindowBuilder::new( + &app_handle, + label, + WebviewUrl::External(ABOUT_BLANK.parse().unwrap()), + ) + .window_features(features) + .on_document_title_changed(|window, title| { + let _ = window.set_title(&title); + }) + .title(APP_TITLE); + + match builder.build() { + Ok(window) => NewWindowResponse::Create { window }, + Err(err) => { + eprintln!("failed to create popout window: {err}"); + NewWindowResponse::Deny + } + } + }) + .build()?; + app.state::() .window .write() diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 96cb84ca8..7089b001c 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -20,14 +20,7 @@ "plugins": {}, "app": { "withGlobalTauri": true, - "windows": [ - { - "title": "LiveSplit One", - "width": 850, - "height": 750, - "useHttpsScheme": true - } - ], + "windows": [], "security": { "csp": null } diff --git a/src/localization/chinese-simplified.ts b/src/localization/chinese-simplified.ts index 30d6f22e5..fb3ce4b8f 100644 --- a/src/localization/chinese-simplified.ts +++ b/src/localization/chinese-simplified.ts @@ -114,6 +114,8 @@ export function resolveChineseSimplified(text: Label): string { return "游戏时间"; case Label.PopOut: return "弹出窗口"; + case Label.PopOutFailed: + return "无法打开新窗口。"; case Label.About: return "关于"; case Label.Back: diff --git a/src/localization/dutch.ts b/src/localization/dutch.ts index e01fd0c01..31b354713 100644 --- a/src/localization/dutch.ts +++ b/src/localization/dutch.ts @@ -114,6 +114,8 @@ export function resolveDutch(text: Label): string { return "Speltijd"; case Label.PopOut: return "Pop‑out"; + case Label.PopOutFailed: + return "Kan geen nieuw venster openen."; case Label.About: return "Over"; case Label.Back: diff --git a/src/localization/english.ts b/src/localization/english.ts index 9c7fe671f..2c5b4766d 100644 --- a/src/localization/english.ts +++ b/src/localization/english.ts @@ -114,6 +114,8 @@ export function resolveEnglish(text: Label): string { return "Game Time"; case Label.PopOut: return "Pop Out"; + case Label.PopOutFailed: + return "Failed to open a new window."; case Label.About: return "About"; case Label.Back: diff --git a/src/localization/french.ts b/src/localization/french.ts index 5d5d9474a..90b8f8976 100644 --- a/src/localization/french.ts +++ b/src/localization/french.ts @@ -114,6 +114,8 @@ export function resolveFrench(text: Label): string { return "Temps de jeu"; case Label.PopOut: return "Fenêtre détachée"; + case Label.PopOutFailed: + return "Impossible d’ouvrir une nouvelle fenêtre."; case Label.About: return "À propos"; case Label.Back: diff --git a/src/localization/german.ts b/src/localization/german.ts index 13be887f8..cc7052f37 100644 --- a/src/localization/german.ts +++ b/src/localization/german.ts @@ -114,6 +114,8 @@ export function resolveGerman(text: Label): string { return "Spielzeit"; case Label.PopOut: return "Pop‑out"; + case Label.PopOutFailed: + return "Neues Fenster konnte nicht geöffnet werden."; case Label.About: return "Über"; case Label.Back: diff --git a/src/localization/index.ts b/src/localization/index.ts index 4b6bb7bff..0d018730c 100644 --- a/src/localization/index.ts +++ b/src/localization/index.ts @@ -71,6 +71,7 @@ export enum Label { RealTime, GameTime, PopOut, + PopOutFailed, About, Back, AboutVersionPrefix, diff --git a/src/localization/italian.ts b/src/localization/italian.ts index 4a52ecb61..8004dd6b2 100644 --- a/src/localization/italian.ts +++ b/src/localization/italian.ts @@ -114,6 +114,8 @@ export function resolveItalian(text: Label): string { return "Tempo di gioco"; case Label.PopOut: return "Finestra separata"; + case Label.PopOutFailed: + return "Impossibile aprire una nuova finestra."; case Label.About: return "Informazioni"; case Label.Back: diff --git a/src/localization/japanese.ts b/src/localization/japanese.ts index e018fa239..cd50172dd 100644 --- a/src/localization/japanese.ts +++ b/src/localization/japanese.ts @@ -114,6 +114,8 @@ export function resolveJapanese(text: Label): string { return "ゲーム時間"; case Label.PopOut: return "ポップアウト"; + case Label.PopOutFailed: + return "新しいウィンドウを開けませんでした。"; case Label.About: return "このアプリについて"; case Label.Back: diff --git a/src/localization/korean.ts b/src/localization/korean.ts index c38966c52..39d6bdca5 100644 --- a/src/localization/korean.ts +++ b/src/localization/korean.ts @@ -114,6 +114,8 @@ export function resolveKorean(text: Label): string { return "게임 시간"; case Label.PopOut: return "팝아웃"; + case Label.PopOutFailed: + return "새 창을 열지 못했습니다."; case Label.About: return "정보"; case Label.Back: diff --git a/src/localization/polish.ts b/src/localization/polish.ts index d3f7285b7..ccca70820 100644 --- a/src/localization/polish.ts +++ b/src/localization/polish.ts @@ -114,6 +114,8 @@ export function resolvePolish(text: Label): string { return "Czas gry"; case Label.PopOut: return "Otwórz w oknie"; + case Label.PopOutFailed: + return "Nie udało się otworzyć nowego okna."; case Label.About: return "O programie"; case Label.Back: diff --git a/src/localization/portuguese-brazil.ts b/src/localization/portuguese-brazil.ts index 97b0ed3e1..c653e5c1d 100644 --- a/src/localization/portuguese-brazil.ts +++ b/src/localization/portuguese-brazil.ts @@ -114,6 +114,8 @@ export function resolveBrazilianPortuguese(text: Label): string { return "Tempo de jogo"; case Label.PopOut: return "Pop‑out"; + case Label.PopOutFailed: + return "Não foi possível abrir uma nova janela."; case Label.About: return "Sobre"; case Label.Back: diff --git a/src/localization/portuguese.ts b/src/localization/portuguese.ts index 89662eabb..7b465dec5 100644 --- a/src/localization/portuguese.ts +++ b/src/localization/portuguese.ts @@ -114,6 +114,8 @@ export function resolvePortuguese(text: Label): string { return "Tempo de jogo"; case Label.PopOut: return "Pop‑out"; + case Label.PopOutFailed: + return "Não foi possível abrir uma nova janela."; case Label.About: return "Sobre"; case Label.Back: diff --git a/src/localization/russian.ts b/src/localization/russian.ts index 8985b563f..5b822d45c 100644 --- a/src/localization/russian.ts +++ b/src/localization/russian.ts @@ -114,6 +114,8 @@ export function resolveRussian(text: Label): string { return "Игровое время"; case Label.PopOut: return "Окно отдельно"; + case Label.PopOutFailed: + return "Не удалось открыть новое окно."; case Label.About: return "О программе"; case Label.Back: diff --git a/src/localization/spanish.ts b/src/localization/spanish.ts index 80aafce1c..993519b50 100644 --- a/src/localization/spanish.ts +++ b/src/localization/spanish.ts @@ -114,6 +114,8 @@ export function resolveSpanish(text: Label): string { return "Tiempo de juego"; case Label.PopOut: return "Ventana emergente"; + case Label.PopOutFailed: + return "No se pudo abrir una nueva ventana."; case Label.About: return "Acerca de"; case Label.Back: diff --git a/src/ui/LiveSplit.tsx b/src/ui/LiveSplit.tsx index eb87b8e42..5cc7414e8 100644 --- a/src/ui/LiveSplit.tsx +++ b/src/ui/LiveSplit.tsx @@ -1290,12 +1290,11 @@ async function openPopupWindow( // It's fine if it fails, as not all browsers support this. } - const childWindow = window.open( - "", - "_blank", - `popup,width=${width},height=${height}`, - ); - return childWindow; + try { + return window.open("about:blank", "_blank"); + } catch { + return null; + } } async function popOut( @@ -1307,6 +1306,9 @@ async function popOut( ) { const childWindow = await openPopupWindow(width, height); if (!childWindow) { + toast.error( + `${resolve(Label.PopOut, generalSettings.lang)}: ${resolve(Label.PopOutFailed, generalSettings.lang)}`, + ); return; }