From d7e07fd3650ac236e2c874c57166135ab1c2c43e Mon Sep 17 00:00:00 2001 From: Chris Johnstone Date: Sun, 2 Aug 2026 17:12:23 +1200 Subject: [PATCH 1/3] feat(playback): fix transparency between playback, and mask white flashes --- src-tauri/src/app_bootstrap.rs | 7 +++ src-tauri/tauri.linux.conf.json | 3 +- src-tauri/tauri.windows.conf.json | 13 ++++ src/App.vue | 50 +++++++++++++-- src/composables/usePlaybackTransitionMask.ts | 64 ++++++++++++++++++++ src/styles/app-shell.css | 7 +++ 6 files changed, 137 insertions(+), 7 deletions(-) create mode 100644 src/composables/usePlaybackTransitionMask.ts diff --git a/src-tauri/src/app_bootstrap.rs b/src-tauri/src/app_bootstrap.rs index ea70be7..ed358b5 100644 --- a/src-tauri/src/app_bootstrap.rs +++ b/src-tauri/src/app_bootstrap.rs @@ -27,6 +27,13 @@ pub(crate) fn setup(app: &mut tauri::App) -> Result<(), Box> { .get_webview_window("main") .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Failed to get main window"))?; + // Keep the native host opaque black so there is always a backing surface + // beneath mpv. Only the webview needs transparency for the UI overlay. + #[cfg(any(target_os = "windows", target_os = "linux"))] + window + .as_ref() + .set_background_color(Some(tauri::utils::config::Color(0, 0, 0, 0)))?; + #[cfg(any(target_os = "windows", target_os = "linux"))] { let _ = window.set_decorations(false); diff --git a/src-tauri/tauri.linux.conf.json b/src-tauri/tauri.linux.conf.json index 80ac321..8f38981 100644 --- a/src-tauri/tauri.linux.conf.json +++ b/src-tauri/tauri.linux.conf.json @@ -6,7 +6,8 @@ "width": 1280, "height": 720, "decorations": true, - "transparent": true, + "transparent": false, + "backgroundColor": "#000000", "visible": true } ] diff --git a/src-tauri/tauri.windows.conf.json b/src-tauri/tauri.windows.conf.json index 8c15172..36efd75 100644 --- a/src-tauri/tauri.windows.conf.json +++ b/src-tauri/tauri.windows.conf.json @@ -1,4 +1,17 @@ { + "app": { + "windows": [ + { + "title": "Soia", + "width": 1280, + "height": 720, + "decorations": true, + "transparent": false, + "backgroundColor": "#000000", + "visible": false + } + ] + }, "bundle": { "fileAssociations": [ { diff --git a/src/App.vue b/src/App.vue index f3c9400..85c6ff4 100644 --- a/src/App.vue +++ b/src/App.vue @@ -30,6 +30,7 @@ import { usePlaylistEntriesWithProgress } from "./composables/usePlaylistEntries import { useAppStartupBindings } from "./composables/useAppStartupBindings"; import { usePlaybackSeekActions } from "./composables/usePlaybackSeekActions"; import { usePlaybackLoadingState } from "./composables/usePlaybackLoadingState"; +import { usePlaybackTransitionMask } from "./composables/usePlaybackTransitionMask"; import { usePlaybackNavigation } from "./composables/usePlaybackNavigation"; import { usePlaybackVolumePersistence } from "./composables/usePlaybackVolumePersistence"; import { usePlaylistCreationPrompt } from "./composables/usePlaylistCreationPrompt"; @@ -84,6 +85,7 @@ const { const clearNavSelectionDuringLoad = ref(false); const playbackLoadingState = usePlaybackLoadingState(); const { isLoading, loadingUrl } = playbackLoadingState; +const playbackTransitionMask = usePlaybackTransitionMask(); const playlistCreationPrompt = usePlaylistCreationPrompt(); const { persistCurrentManualWindow, @@ -107,6 +109,7 @@ const playbackFlow = usePlaybackFlow({ isInfoOpen, loadingState: playbackLoadingState, onPlaybackIntent: async () => { + await playbackTransitionMask.activateAndWaitForPaint(); await persistCurrentManualWindow(); clearNavSelectionDuringLoad.value = true; }, @@ -137,8 +140,13 @@ const { } = playbackFlow; const onStopPlaybackWithWindowRestore = async () => { - await onStopPlayback(); - await restorePersistedManualWindow(); + await playbackTransitionMask.activateAndWaitForPaint(); + try { + await onStopPlayback(); + await restorePersistedManualWindow(); + } finally { + playbackTransitionMask.clear(); + } }; const playbackNavigation = usePlaybackNavigation({ @@ -162,6 +170,11 @@ const shouldUseTransparentVideoMode = computed( player.state.media.isFileLoaded && !shouldKeepPlaybackBackgroundOpaque.value, ); +const shouldMaskPlaybackTransition = computed( + () => + playbackTransitionMask.isVisible.value && + !shouldKeepPlaybackBackgroundOpaque.value, +); const sideNavActivePanel = computed(() => isLoading.value && clearNavSelectionDuringLoad.value ? null @@ -237,8 +250,14 @@ const { schedulePointerRefresh, onStopPlayback: onStopPlaybackWithWindowRestore, playPath, - playPreviousTrack: playbackNavigation.playPreviousTrack, - playNextTrack: playbackNavigation.playNextTrack, + playPreviousTrack: async () => { + await playbackTransitionMask.activateAndWaitForPaint(); + await playbackNavigation.playPreviousTrack(); + }, + playNextTrack: async () => { + await playbackTransitionMask.activateAndWaitForPaint(); + await playbackNavigation.playNextTrack(); + }, }); const onSideNavNavigate = async ( @@ -348,9 +367,9 @@ const { hasLoadedPanel, loadActivePanel } = useAppUiPersistence({ const { onFileLoaded: onFileLoadedBase, - onPlaybackRestart, + onPlaybackRestart: onPlaybackRestartBase, onProgress, - onEndFile, + onEndFile: onEndFileBase, } = useAppPlaybackEvents({ player, @@ -365,6 +384,22 @@ const { playNextAfterEnd: playbackNavigation.playNextAfterEnd, }); +const onPlaybackRestart = () => { + onPlaybackRestartBase(); + void playbackTransitionMask.releaseAfterPlaybackRestart(); +}; + +const onEndFile = (payload: Parameters[0]) => { + if (payload.reason === "eof") { + playbackTransitionMask.activate(); + } else if (payload.reason === "error") { + // A failed file never reaches playback-restart, so it cannot release + // the transition mask through the normal success path. + playbackTransitionMask.clear(); + } + onEndFileBase(payload); +}; + const onFileLoaded = async () => { await onFileLoadedBase(); if (subtitlesDisabled.value) { @@ -400,6 +435,7 @@ useAppRuntimeBindings({ onEndFile, onSourceLoadState: ({ loading, loadingKey, error }) => { if (loading) { + playbackTransitionMask.activate(); isLoading.value = true; loadingUrl.value = loadingKey || player.state.media.url; return; @@ -412,6 +448,7 @@ useAppRuntimeBindings({ ) { return; } + playbackTransitionMask.clear(); isLoading.value = false; loadingUrl.value = ""; }, @@ -457,6 +494,7 @@ useAppStartupBindings({ class="soia-container" :class="{ 'video-mode': shouldUseTransparentVideoMode, + 'playback-transition-mask': shouldMaskPlaybackTransition, 'cursor-hidden': player.state.media.isFileLoaded && !ui.showControls.value && diff --git a/src/composables/usePlaybackTransitionMask.ts b/src/composables/usePlaybackTransitionMask.ts new file mode 100644 index 0000000..c378f02 --- /dev/null +++ b/src/composables/usePlaybackTransitionMask.ts @@ -0,0 +1,64 @@ +import { nextTick, readonly, ref } from "vue"; + +const MASK_PAINT_TIMEOUT_MS = 100; + +const waitForMaskPaint = () => + new Promise((resolve) => { + let settled = false; + const finish = () => { + if (settled) return; + settled = true; + window.clearTimeout(timeoutId); + resolve(); + }; + const timeoutId = window.setTimeout(finish, MASK_PAINT_TIMEOUT_MS); + + // The second callback runs after the browser has had an opportunity to + // paint the mask. The timeout prevents a hidden/throttled WebView from + // blocking playback indefinitely. + window.requestAnimationFrame(() => window.requestAnimationFrame(finish)); + }); + +/** + * Covers native video surfaces while MPV changes files. + * + * MPV's source-loading state ends when it accepts a load command, which can be + * earlier than the first presented frame. This mask instead spans playback + * intent/EOF through MPV_EVENT_PLAYBACK_RESTART. + */ +export const usePlaybackTransitionMask = () => { + const isVisible = ref(false); + let generation = 0; + + const activate = () => { + // Invalidate a delayed release belonging to an older transition. + generation += 1; + isVisible.value = true; + }; + + const clear = () => { + generation += 1; + isVisible.value = false; + }; + + const activateAndWaitForPaint = async () => { + activate(); + await nextTick(); + await waitForMaskPaint(); + }; + + const releaseAfterPlaybackRestart = async () => { + const releaseGeneration = generation; + await waitForMaskPaint(); + if (releaseGeneration !== generation) return; + isVisible.value = false; + }; + + return { + isVisible: readonly(isVisible), + activate, + clear, + activateAndWaitForPaint, + releaseAfterPlaybackRestart, + }; +}; diff --git a/src/styles/app-shell.css b/src/styles/app-shell.css index 0a63f2f..61b519b 100644 --- a/src/styles/app-shell.css +++ b/src/styles/app-shell.css @@ -24,6 +24,13 @@ box-shadow: none !important; } +.soia-container.playback-transition-mask { + background-color: #000 !important; + background-image: none !important; + box-shadow: none !important; + transition: none; +} + .cursor-hidden { cursor: none; } From eace06d551c42ce7918329241cf61703914dd005 Mon Sep 17 00:00:00 2001 From: FengZeng Date: Mon, 3 Aug 2026 22:02:53 +0800 Subject: [PATCH 2/3] fix(windows): prevent white flashes with themed native background --- src-tauri/Cargo.toml | 2 +- src-tauri/src/app_bootstrap.rs | 3 +- src-tauri/src/lib.rs | 8 +++ src-tauri/src/platform/default.rs | 20 ++++++- src-tauri/src/platform/windows.rs | 92 ++++++++++++++++++++++++++++++- src-tauri/tauri.linux.conf.json | 1 - src-tauri/tauri.windows.conf.json | 1 - src/App.vue | 50 ++--------------- src/styles/app-shell.css | 7 --- 9 files changed, 123 insertions(+), 61 deletions(-) diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index c6278bb..6ecc487 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -69,7 +69,7 @@ objc2-quartz-core = "0.3.1" block2 = "0.6.1" [target.'cfg(target_os = "windows")'.dependencies] -windows-sys = { version = "0.61", features = ["Win32_UI_WindowsAndMessaging"] } +windows-sys = { version = "0.61", features = ["Win32_Graphics_Gdi", "Win32_UI_WindowsAndMessaging"] } [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-updater = "2" diff --git a/src-tauri/src/app_bootstrap.rs b/src-tauri/src/app_bootstrap.rs index ed358b5..bbad5ec 100644 --- a/src-tauri/src/app_bootstrap.rs +++ b/src-tauri/src/app_bootstrap.rs @@ -27,8 +27,7 @@ pub(crate) fn setup(app: &mut tauri::App) -> Result<(), Box> { .get_webview_window("main") .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Failed to get main window"))?; - // Keep the native host opaque black so there is always a backing surface - // beneath mpv. Only the webview needs transparency for the UI overlay. + // Only the webview needs transparency for the UI overlay. #[cfg(any(target_os = "windows", target_os = "linux"))] window .as_ref() diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 3e50cde..372fcc4 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -502,6 +502,14 @@ fn show_main_window(app_handle: &tauri::AppHandle) { let _ = app_handle_for_thread.run_on_main_thread(move || { if let Some(window) = app_handle_for_show.get_webview_window(MAIN_WINDOW_LABEL) { let _ = window.show(); + #[cfg(target_os = "windows")] + if let Err(error) = crate::platform::windows::paint_native_window_background( + &window, + None, + window.theme().unwrap_or(tauri::Theme::Dark), + ) { + log::warn!("Failed to paint native main window background: {error}"); + } } }); } diff --git a/src-tauri/src/platform/default.rs b/src-tauri/src/platform/default.rs index d000e5a..9f39200 100644 --- a/src-tauri/src/platform/default.rs +++ b/src-tauri/src/platform/default.rs @@ -41,12 +41,26 @@ impl PlatformIntegration for DefaultPlatformIntegration { fn apply_window_appearance( &self, - _window: tauri::Window, + window: tauri::Window, _compact_mode: bool, _corner_radius: Option, - _theme: Option, + theme: Option, ) -> Result<(), String> { - Ok(()) + #[cfg(target_os = "windows")] + { + let system_theme = window.theme().unwrap_or(tauri::Theme::Dark); + super::windows::paint_native_window_background( + &window, + theme.as_deref(), + system_theme, + ) + } + + #[cfg(not(target_os = "windows"))] + { + let _ = (window, theme); + Ok(()) + } } fn set_window_vibrancy_visible( diff --git a/src-tauri/src/platform/windows.rs b/src-tauri/src/platform/windows.rs index 443c0e0..b293699 100644 --- a/src-tauri/src/platform/windows.rs +++ b/src-tauri/src/platform/windows.rs @@ -2,10 +2,17 @@ mod imp { use raw_window_handle::{HasWindowHandle, RawWindowHandle}; use std::sync::mpsc; + use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::{Mutex, OnceLock}; use std::time::{Duration, Instant}; use tauri::{Emitter, Manager}; use tokio::time::sleep; + use windows_sys::Win32::Graphics::Gdi::{ + CreateSolidBrush, FillRect, GetDC, InvalidateRect, ReleaseDC, UpdateWindow, + }; + use windows_sys::Win32::UI::WindowsAndMessaging::{ + GetClientRect, SetClassLongPtrW, GCLP_HBRBACKGROUND, + }; use windows_sys::Win32::UI::WindowsAndMessaging::IsZoomed; const MAIN_WINDOW_LABEL: &str = "main"; @@ -17,6 +24,87 @@ mod imp { const PIP_SCREEN_WIDTH_FACTOR: f64 = 0.30; const PIP_SCREEN_HEIGHT_FACTOR: f64 = 0.45; const PIP_MARGIN: i32 = 20; + const LIGHT_BACKGROUND_COLOR: u32 = 0x00f6f6f6; + const DARK_BACKGROUND_COLOR: u32 = 0x000f0f0f; + // Win32 COLORREF uses 0x00BBGGRR, so CSS #1e2227 becomes 0x0027221e. + const GRAPHITE_BACKGROUND_COLOR: u32 = 0x0027221e; + static CURRENT_BACKGROUND_COLOR: AtomicU32 = AtomicU32::new(0); + + pub(crate) fn paint_native_window_background( + window: &W, + theme: Option<&str>, + system_theme: tauri::Theme, + ) -> Result<(), String> { + let handle = window.window_handle().map_err(|error| error.to_string())?; + let hwnd = match handle.as_raw() { + RawWindowHandle::Win32(handle) => { + handle.hwnd.get() as usize as *mut std::ffi::c_void + } + _ => return Err("Main window does not expose a Win32 window handle".into()), + }; + + let color = match theme.map(|value| value.trim().to_ascii_lowercase()) { + Some(value) if value == "light" => LIGHT_BACKGROUND_COLOR, + Some(value) if value == "dark" => DARK_BACKGROUND_COLOR, + Some(value) if value == "graphite" => GRAPHITE_BACKGROUND_COLOR, + Some(_) => system_background_color(system_theme), + None => { + let current = CURRENT_BACKGROUND_COLOR.load(Ordering::Acquire); + if current == 0 { + system_background_color(system_theme) + } else { + current + } + } + }; + CURRENT_BACKGROUND_COLOR.store(color, Ordering::Release); + + let brush = background_brush(color); + if brush.is_null() { + return Err("Failed to create native window background brush".into()); + } + + let mut client_rect = windows_sys::Win32::Foundation::RECT::default(); + unsafe { + // The class owns this process-lifetime brush after assignment. + SetClassLongPtrW(hwnd, GCLP_HBRBACKGROUND, brush as isize); + if GetClientRect(hwnd, &mut client_rect) == 0 { + return Err("Failed to query main window client area".into()); + } + + let dc = GetDC(hwnd); + if dc.is_null() { + return Err("Failed to acquire main window device context".into()); + } + FillRect(dc, &client_rect, brush); + ReleaseDC(hwnd, dc); + + InvalidateRect(hwnd, std::ptr::null(), 1); + UpdateWindow(hwnd); + } + Ok(()) + } + + fn system_background_color(theme: tauri::Theme) -> u32 { + match theme { + tauri::Theme::Light => LIGHT_BACKGROUND_COLOR, + _ => DARK_BACKGROUND_COLOR, + } + } + + fn background_brush(color: u32) -> *mut std::ffi::c_void { + static LIGHT_BRUSH: OnceLock = OnceLock::new(); + static DARK_BRUSH: OnceLock = OnceLock::new(); + static GRAPHITE_BRUSH: OnceLock = OnceLock::new(); + + let slot = match color { + LIGHT_BACKGROUND_COLOR => &LIGHT_BRUSH, + GRAPHITE_BACKGROUND_COLOR => &GRAPHITE_BRUSH, + _ => &DARK_BRUSH, + }; + *slot.get_or_init(|| unsafe { CreateSolidBrush(color) as usize }) + as *mut std::ffi::c_void + } #[derive(Clone, Copy, Debug)] struct WindowSnapshot { @@ -383,6 +471,6 @@ mod imp { #[cfg(target_os = "windows")] pub(crate) use imp::{ - enforce_native_pip_aspect, is_native_pip_enabled, set_native_pip_enabled, - prepare_window_for_fullscreen, update_native_pip_state, + enforce_native_pip_aspect, is_native_pip_enabled, paint_native_window_background, + prepare_window_for_fullscreen, set_native_pip_enabled, update_native_pip_state, }; diff --git a/src-tauri/tauri.linux.conf.json b/src-tauri/tauri.linux.conf.json index 8f38981..f91d045 100644 --- a/src-tauri/tauri.linux.conf.json +++ b/src-tauri/tauri.linux.conf.json @@ -7,7 +7,6 @@ "height": 720, "decorations": true, "transparent": false, - "backgroundColor": "#000000", "visible": true } ] diff --git a/src-tauri/tauri.windows.conf.json b/src-tauri/tauri.windows.conf.json index 36efd75..0619be1 100644 --- a/src-tauri/tauri.windows.conf.json +++ b/src-tauri/tauri.windows.conf.json @@ -7,7 +7,6 @@ "height": 720, "decorations": true, "transparent": false, - "backgroundColor": "#000000", "visible": false } ] diff --git a/src/App.vue b/src/App.vue index 85c6ff4..f3c9400 100644 --- a/src/App.vue +++ b/src/App.vue @@ -30,7 +30,6 @@ import { usePlaylistEntriesWithProgress } from "./composables/usePlaylistEntries import { useAppStartupBindings } from "./composables/useAppStartupBindings"; import { usePlaybackSeekActions } from "./composables/usePlaybackSeekActions"; import { usePlaybackLoadingState } from "./composables/usePlaybackLoadingState"; -import { usePlaybackTransitionMask } from "./composables/usePlaybackTransitionMask"; import { usePlaybackNavigation } from "./composables/usePlaybackNavigation"; import { usePlaybackVolumePersistence } from "./composables/usePlaybackVolumePersistence"; import { usePlaylistCreationPrompt } from "./composables/usePlaylistCreationPrompt"; @@ -85,7 +84,6 @@ const { const clearNavSelectionDuringLoad = ref(false); const playbackLoadingState = usePlaybackLoadingState(); const { isLoading, loadingUrl } = playbackLoadingState; -const playbackTransitionMask = usePlaybackTransitionMask(); const playlistCreationPrompt = usePlaylistCreationPrompt(); const { persistCurrentManualWindow, @@ -109,7 +107,6 @@ const playbackFlow = usePlaybackFlow({ isInfoOpen, loadingState: playbackLoadingState, onPlaybackIntent: async () => { - await playbackTransitionMask.activateAndWaitForPaint(); await persistCurrentManualWindow(); clearNavSelectionDuringLoad.value = true; }, @@ -140,13 +137,8 @@ const { } = playbackFlow; const onStopPlaybackWithWindowRestore = async () => { - await playbackTransitionMask.activateAndWaitForPaint(); - try { - await onStopPlayback(); - await restorePersistedManualWindow(); - } finally { - playbackTransitionMask.clear(); - } + await onStopPlayback(); + await restorePersistedManualWindow(); }; const playbackNavigation = usePlaybackNavigation({ @@ -170,11 +162,6 @@ const shouldUseTransparentVideoMode = computed( player.state.media.isFileLoaded && !shouldKeepPlaybackBackgroundOpaque.value, ); -const shouldMaskPlaybackTransition = computed( - () => - playbackTransitionMask.isVisible.value && - !shouldKeepPlaybackBackgroundOpaque.value, -); const sideNavActivePanel = computed(() => isLoading.value && clearNavSelectionDuringLoad.value ? null @@ -250,14 +237,8 @@ const { schedulePointerRefresh, onStopPlayback: onStopPlaybackWithWindowRestore, playPath, - playPreviousTrack: async () => { - await playbackTransitionMask.activateAndWaitForPaint(); - await playbackNavigation.playPreviousTrack(); - }, - playNextTrack: async () => { - await playbackTransitionMask.activateAndWaitForPaint(); - await playbackNavigation.playNextTrack(); - }, + playPreviousTrack: playbackNavigation.playPreviousTrack, + playNextTrack: playbackNavigation.playNextTrack, }); const onSideNavNavigate = async ( @@ -367,9 +348,9 @@ const { hasLoadedPanel, loadActivePanel } = useAppUiPersistence({ const { onFileLoaded: onFileLoadedBase, - onPlaybackRestart: onPlaybackRestartBase, + onPlaybackRestart, onProgress, - onEndFile: onEndFileBase, + onEndFile, } = useAppPlaybackEvents({ player, @@ -384,22 +365,6 @@ const { playNextAfterEnd: playbackNavigation.playNextAfterEnd, }); -const onPlaybackRestart = () => { - onPlaybackRestartBase(); - void playbackTransitionMask.releaseAfterPlaybackRestart(); -}; - -const onEndFile = (payload: Parameters[0]) => { - if (payload.reason === "eof") { - playbackTransitionMask.activate(); - } else if (payload.reason === "error") { - // A failed file never reaches playback-restart, so it cannot release - // the transition mask through the normal success path. - playbackTransitionMask.clear(); - } - onEndFileBase(payload); -}; - const onFileLoaded = async () => { await onFileLoadedBase(); if (subtitlesDisabled.value) { @@ -435,7 +400,6 @@ useAppRuntimeBindings({ onEndFile, onSourceLoadState: ({ loading, loadingKey, error }) => { if (loading) { - playbackTransitionMask.activate(); isLoading.value = true; loadingUrl.value = loadingKey || player.state.media.url; return; @@ -448,7 +412,6 @@ useAppRuntimeBindings({ ) { return; } - playbackTransitionMask.clear(); isLoading.value = false; loadingUrl.value = ""; }, @@ -494,7 +457,6 @@ useAppStartupBindings({ class="soia-container" :class="{ 'video-mode': shouldUseTransparentVideoMode, - 'playback-transition-mask': shouldMaskPlaybackTransition, 'cursor-hidden': player.state.media.isFileLoaded && !ui.showControls.value && diff --git a/src/styles/app-shell.css b/src/styles/app-shell.css index 61b519b..0a63f2f 100644 --- a/src/styles/app-shell.css +++ b/src/styles/app-shell.css @@ -24,13 +24,6 @@ box-shadow: none !important; } -.soia-container.playback-transition-mask { - background-color: #000 !important; - background-image: none !important; - box-shadow: none !important; - transition: none; -} - .cursor-hidden { cursor: none; } From 94c4b90d8b17c71e119906421be5ec467420a631 Mon Sep 17 00:00:00 2001 From: FengZeng Date: Fri, 7 Aug 2026 20:27:21 +0800 Subject: [PATCH 3/3] fix(linux): restore transparent video surface --- src-tauri/src/app_bootstrap.rs | 2 +- src-tauri/tauri.linux.conf.json | 2 +- src/composables/usePlaybackTransitionMask.ts | 64 -------------------- 3 files changed, 2 insertions(+), 66 deletions(-) delete mode 100644 src/composables/usePlaybackTransitionMask.ts diff --git a/src-tauri/src/app_bootstrap.rs b/src-tauri/src/app_bootstrap.rs index bbad5ec..3369df0 100644 --- a/src-tauri/src/app_bootstrap.rs +++ b/src-tauri/src/app_bootstrap.rs @@ -28,7 +28,7 @@ pub(crate) fn setup(app: &mut tauri::App) -> Result<(), Box> { .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Failed to get main window"))?; // Only the webview needs transparency for the UI overlay. - #[cfg(any(target_os = "windows", target_os = "linux"))] + #[cfg(target_os = "windows")] window .as_ref() .set_background_color(Some(tauri::utils::config::Color(0, 0, 0, 0)))?; diff --git a/src-tauri/tauri.linux.conf.json b/src-tauri/tauri.linux.conf.json index f91d045..80ac321 100644 --- a/src-tauri/tauri.linux.conf.json +++ b/src-tauri/tauri.linux.conf.json @@ -6,7 +6,7 @@ "width": 1280, "height": 720, "decorations": true, - "transparent": false, + "transparent": true, "visible": true } ] diff --git a/src/composables/usePlaybackTransitionMask.ts b/src/composables/usePlaybackTransitionMask.ts deleted file mode 100644 index c378f02..0000000 --- a/src/composables/usePlaybackTransitionMask.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { nextTick, readonly, ref } from "vue"; - -const MASK_PAINT_TIMEOUT_MS = 100; - -const waitForMaskPaint = () => - new Promise((resolve) => { - let settled = false; - const finish = () => { - if (settled) return; - settled = true; - window.clearTimeout(timeoutId); - resolve(); - }; - const timeoutId = window.setTimeout(finish, MASK_PAINT_TIMEOUT_MS); - - // The second callback runs after the browser has had an opportunity to - // paint the mask. The timeout prevents a hidden/throttled WebView from - // blocking playback indefinitely. - window.requestAnimationFrame(() => window.requestAnimationFrame(finish)); - }); - -/** - * Covers native video surfaces while MPV changes files. - * - * MPV's source-loading state ends when it accepts a load command, which can be - * earlier than the first presented frame. This mask instead spans playback - * intent/EOF through MPV_EVENT_PLAYBACK_RESTART. - */ -export const usePlaybackTransitionMask = () => { - const isVisible = ref(false); - let generation = 0; - - const activate = () => { - // Invalidate a delayed release belonging to an older transition. - generation += 1; - isVisible.value = true; - }; - - const clear = () => { - generation += 1; - isVisible.value = false; - }; - - const activateAndWaitForPaint = async () => { - activate(); - await nextTick(); - await waitForMaskPaint(); - }; - - const releaseAfterPlaybackRestart = async () => { - const releaseGeneration = generation; - await waitForMaskPaint(); - if (releaseGeneration !== generation) return; - isVisible.value = false; - }; - - return { - isVisible: readonly(isVisible), - activate, - clear, - activateAndWaitForPaint, - releaseAfterPlaybackRestart, - }; -};