diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 67da2ed..39707c8 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -136,6 +136,9 @@ pub fn run() { if let Err(error) = macos::allow_media_autoplay(&window) { log::warn!(target: "launcher", "{error}"); } + if let Err(error) = macos::activate_and_focus_window(&window) { + log::warn!(target: "launcher", "Could not focus launcher window on startup: {error}"); + } } Ok(()) @@ -235,6 +238,9 @@ pub fn run() { if let Err(error) = macos::allow_media_autoplay(&window) { log::warn!(target: "launcher", "{error}"); } + if let Err(error) = macos::activate_and_focus_window(&window) { + log::warn!(target: "launcher", "Could not focus launcher window on ready: {error}"); + } } presence::handle_run_event(app, &event); diff --git a/src-tauri/src/macos.rs b/src-tauri/src/macos.rs index a6d5294..a657b85 100644 --- a/src-tauri/src/macos.rs +++ b/src-tauri/src/macos.rs @@ -96,6 +96,14 @@ pub fn allow_media_autoplay(window: &tauri::WebviewWindow) -> Result<(), String> use objc2::msg_send; use objc2::runtime::AnyObject; + unsafe extern "C" { + fn object_getInstanceVariable( + obj: *mut AnyObject, + name: *const std::ffi::c_char, + out_value: *mut *mut std::ffi::c_void, + ) -> *const std::ffi::c_void; + } + window .with_webview(|webview| unsafe { let webview = webview.inner() as *mut AnyObject; @@ -103,7 +111,18 @@ pub fn allow_media_autoplay(window: &tauri::WebviewWindow) -> Result<(), String> return; } - let configuration: *mut AnyObject = msg_send![webview, configuration]; + // -[WKWebView configuration] returns a copy. To affect live playback, + // access the internal `_configuration` ivar directly when available. + let mut raw_config: *mut std::ffi::c_void = std::ptr::null_mut(); + let ivar_name = c"_configuration"; + let ivar = object_getInstanceVariable(webview, ivar_name.as_ptr(), &mut raw_config); + let configuration: *mut AnyObject = if !ivar.is_null() && !raw_config.is_null() { + log::debug!(target: "launcher", "Accessing live WKWebView._configuration ivar"); + raw_config as *mut AnyObject + } else { + msg_send![webview, configuration] + }; + if configuration.is_null() { return; } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 3832826..917701a 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -21,7 +21,8 @@ "decorations": false, "resizable": false, "maximizable": false, - "shadow": true + "shadow": true, + "focus": true } ], "withGlobalTauri": true, diff --git a/src/app.css b/src/app.css index 951bf04..23dfe7c 100644 --- a/src/app.css +++ b/src/app.css @@ -172,17 +172,21 @@ [role="button"]:not([disabled]) { cursor: pointer; } - - /* Suppress browser and WebKit native media controls and overlay play buttons on theme videos */ - video::-webkit-media-controls, - video::-webkit-media-controls-enclosure, - video::-webkit-media-controls-panel, - video::-webkit-media-controls-play-button, - video::-webkit-media-controls-start-playback-button, - video::-webkit-media-controls-overlay-play-button { - display: none !important; - -webkit-appearance: none !important; - opacity: 0 !important; - pointer-events: none !important; - } +} + +/* Suppress browser and WebKit native media controls and overlay play buttons on theme videos */ +video::-webkit-media-controls, +video::-webkit-media-controls-enclosure, +video::-webkit-media-controls-panel, +video::-webkit-media-controls-play-button, +video::-webkit-media-controls-start-playback-button, +video::-webkit-media-controls-overlay-play-button, +video::-webkit-media-controls-overlay-enclosure { + display: none !important; + -webkit-appearance: none !important; + opacity: 0 !important; + visibility: hidden !important; + pointer-events: none !important; + width: 0 !important; + height: 0 !important; } diff --git a/src/lib/components/ui/titlebar/titlebar.svelte b/src/lib/components/ui/titlebar/titlebar.svelte index 831d210..41a7ccd 100644 --- a/src/lib/components/ui/titlebar/titlebar.svelte +++ b/src/lib/components/ui/titlebar/titlebar.svelte @@ -27,57 +27,55 @@
- {#if $custom_theme_audio_playing} - {#if $custom_theme_audio_playing} -
-
-
+ {#if $custom_theme_audio_playing || ($active_custom_theme && $active_custom_theme.name !== 'Default')} +
+
+
- {#if $custom_theme_volume > 0.7} - - {:else if $custom_theme_volume >= 0.15} - - {:else if $custom_theme_volume > 0} - - {:else} - - {/if} -
+ {#if $custom_theme_volume > 0.7} + + {:else if $custom_theme_volume >= 0.15} + + {:else if $custom_theme_volume > 0} + + {:else} + + {/if} +
+ + - {/if} +
{/if}
void; @@ -193,48 +194,29 @@ export const clearThemeMediaCache = () => { objectUrls.clear(); }; -const unmutingArmed = new WeakSet(); - -/** Restore full sound as soon as the user interacts with the launcher. */ -const armUnmuteOnInteraction = (node: HTMLMediaElement, targetVolume: number) => { - if (unmutingArmed.has(node)) return; - unmutingArmed.add(node); - - const unmute = () => { - window.removeEventListener('pointerdown', unmute, true); - window.removeEventListener('keydown', unmute, true); - window.removeEventListener('click', unmute, true); - unmutingArmed.delete(node); - if (node.isConnected) { - node.muted = false; - node.volume = targetVolume; - if (node.paused) { - void node.play().catch(() => {}); - } - } - }; - - window.addEventListener('pointerdown', unmute, true); - window.addEventListener('keydown', unmute, true); - window.addEventListener('click', unmute, true); -}; - /** Retry playback on the first click or key press after an autoplay rejection. */ const armAutoplayRetry = (node: HTMLMediaElement) => { if (autoplayArmed.has(node)) return; autoplayArmed.add(node); const retry = () => { - document.removeEventListener('pointerdown', retry, true); - document.removeEventListener('keydown', retry, true); - document.removeEventListener('click', retry, true); + window.removeEventListener('pointerdown', retry, true); + window.removeEventListener('keydown', retry, true); + window.removeEventListener('click', retry, true); + window.removeEventListener('focus', retry, true); autoplayArmed.delete(node); - if (node.isConnected) void node.play().catch(() => {}); + if (node.isConnected) { + node.muted = false; + const targetVolume = get(custom_theme_volume); + if (targetVolume > 0) node.volume = targetVolume; + void node.play().catch(() => {}); + } }; - document.addEventListener('pointerdown', retry, true); - document.addEventListener('keydown', retry, true); - document.addEventListener('click', retry, true); + window.addEventListener('pointerdown', retry, true); + window.addEventListener('keydown', retry, true); + window.addEventListener('click', retry, true); + window.addEventListener('focus', retry, true); }; export const playThemeMedia = (node: HTMLMediaElement) => { @@ -246,38 +228,28 @@ export const playThemeMedia = (node: HTMLMediaElement) => { node.removeAttribute('controls'); } - void node - .play() - .then(() => { - console.debug('Theme media playing', { - element: node.tagName, - muted: node.muted, - volume: node.volume, - source: node.currentSrc, - }); - }) - .catch((error) => { - console.warn('Theme media autoplay was blocked', error); - if (node instanceof HTMLVideoElement && !node.muted) { - // Fallback: Start muted immediately so the background video plays - // automatically without freezing or showing native play buttons, - // and unmute on the first user interaction. - const targetVolume = node.volume > 0 ? node.volume : 0.15; - node.muted = true; - void node - .play() - .then(() => { - console.debug('Theme video started playing muted after autoplay block'); - armUnmuteOnInteraction(node, targetVolume); - }) - .catch((mutedError) => { - console.warn('Theme video muted autoplay was also blocked', mutedError); - armAutoplayRetry(node); - }); - } else { + node.muted = false; + const currentVolume = get(custom_theme_volume); + if (node.volume === 0 && currentVolume > 0) { + node.volume = currentVolume; + } + + const playPromise = node.play(); + if (playPromise !== undefined) { + playPromise + .then(() => { + console.debug('Theme media playing', { + element: node.tagName, + muted: node.muted, + volume: node.volume, + source: node.currentSrc, + }); + }) + .catch((error) => { + console.warn('Theme media autoplay was blocked', error); armAutoplayRetry(node); - } - }); + }); + } }; /** @@ -291,13 +263,7 @@ const loadMediaSource = (node: HTMLMediaElement, source: string) => { mediaCleanups.delete(node); const previousTime = Number.isFinite(node.currentTime) ? node.currentTime : 0; - const wasPlaying = !node.paused; - const needsPreparation = - node instanceof HTMLVideoElement && isMac && loopbackThemePattern.test(source); - - // Keep the last frame visible while the first H.264 encode runs instead of - // letting WebKit struggle through the software VP9 track. - if (needsPreparation) node.pause(); + const wasPlaying = !node.paused || node.autoplay; void themeMediaUrl(source, node instanceof HTMLVideoElement).then((url) => { if (mediaTokens.get(node) !== token || !node.isConnected) return; @@ -324,7 +290,7 @@ const loadMediaSource = (node: HTMLMediaElement, source: string) => { } } - if (wasPlaying || node.autoplay) playThemeMedia(node); + playThemeMedia(node); }; const onError = () => { @@ -336,7 +302,7 @@ const loadMediaSource = (node: HTMLMediaElement, source: string) => { if (node.src !== source) { node.src = source; node.load(); - if (wasPlaying || node.autoplay) playThemeMedia(node); + playThemeMedia(node); } }; @@ -652,14 +618,18 @@ export function smoothThemeMedia(node: HTMLElement) { media.removeAttribute('controls'); } - playThemeMedia(media); - const source = media.getAttribute('src') ?? media.querySelector('source[src]')?.getAttribute('src') ?? undefined; - if (!source) return; - if (!isThemeAssetUrl(source) && !loopbackThemePattern.test(source)) return; + if (!source) { + playThemeMedia(media); + return; + } + if (!isThemeAssetUrl(source) && !loopbackThemePattern.test(source)) { + playThemeMedia(media); + return; + } if (lastSource.get(media) === source) return; lastSource.set(media, source); diff --git a/src/lib/themes.ts b/src/lib/themes.ts index 1749037..15186da 100644 --- a/src/lib/themes.ts +++ b/src/lib/themes.ts @@ -445,6 +445,8 @@ export const loadTheme = async (theme: Theme, themeContainer: HTMLElement, volum // Ensure all media elements mounted by v1 themes start playback automatically themeContainer.querySelectorAll('video, audio').forEach((media) => { + media.muted = false; + media.volume = volume; playThemeMedia(media); }); }; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index f4c6b59..5211e2c 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -267,12 +267,13 @@ export function trackMediaContainer(node: HTMLElement) { function update() { custom_theme_audio_playing.set( - Array.from(media).some((el) => !el.paused && !(el instanceof HTMLVideoElement && el.muted)) + Array.from(media).some((el) => !el.paused) ); } function register(el: HTMLMediaElement) { media.add(el); + update(); const handler = () => update(); el.addEventListener('play', handler); @@ -280,6 +281,7 @@ export function trackMediaContainer(node: HTMLElement) { return () => { media.delete(el); + update(); el.removeEventListener('play', handler); el.removeEventListener('pause', handler); }; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index e38a91c..dbc8b8f 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -277,9 +277,12 @@ const last_theme = localThemes.find((t) => t.name === lastThemeName); const canLoadLastTheme = Boolean(last_theme && isThemeTrusted(last_theme)); + const initialVolume = await config_theme_volume.get(0.15); + custom_theme_volume.set(initialVolume); + if (!last_theme || !canLoadLastTheme) { await config.value('theme').set('Default'); - loadTheme(defaultTheme, $custom_theme_container!, await config_theme_volume.get(0.15)); + loadTheme(defaultTheme, $custom_theme_container!, initialVolume); if (last_theme && !canLoadLastTheme) { sileo.error({ title: 'Theme needs approval', @@ -288,10 +291,10 @@ }); } } else { - loadTheme(last_theme, $custom_theme_container!, await config_theme_volume.get(0.15)); + loadTheme(last_theme, $custom_theme_container!, initialVolume); } - custom_theme_volume.set(await config_theme_volume.get(0.15)); + void invoke('reveal_window').catch(() => {}); patch.set(await config_patching.get(true)); customCursor.set(await config_custom_cursor.get(true)); cursorSmoothening.set(await config_cursor_smoothening.get(true)); @@ -381,6 +384,23 @@ + {@html ``} {#if $active_custom_theme?.definition}