diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 891f0033..b79eb752 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1489,6 +1489,16 @@ fn is_tiling_wm() -> bool { false } +/// Tauri command: returns true when WEBKIT_DISABLE_COMPOSITING_MODE=1 is set. +/// The frontend uses this to apply a CSS class that swaps out GPU-only effects +/// (backdrop-filter, CSS filter, mask-image) for software-friendly equivalents. +#[tauri::command] +fn no_compositing_mode() -> bool { + std::env::var("WEBKIT_DISABLE_COMPOSITING_MODE") + .map(|v| v == "1") + .unwrap_or(false) +} + /// Tauri command: lets the frontend know whether we're running under a tiling /// WM so it can decide whether to render the custom TitleBar component. #[tauri::command] @@ -1683,6 +1693,7 @@ pub fn run() { greet, exit_app, set_window_decorations, + no_compositing_mode, is_tiling_wm_cmd, register_global_shortcut, unregister_global_shortcut, diff --git a/src/App.tsx b/src/App.tsx index e975b0bb..6a22d950 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -88,6 +88,13 @@ function AppShell() { invoke('is_tiling_wm_cmd').then(setIsTilingWm).catch(() => {}); }, []); + useEffect(() => { + if (!IS_LINUX) return; + invoke('no_compositing_mode').then(noComp => { + if (noComp) document.documentElement.classList.add('no-compositing'); + }).catch(() => {}); + }, []); + useEffect(() => { const win = getCurrentWindow(); // Check initial state (e.g. app launched maximised / already fullscreen). diff --git a/src/components/FullscreenPlayer.tsx b/src/components/FullscreenPlayer.tsx index 09fa3465..7ec51aea 100644 --- a/src/components/FullscreenPlayer.tsx +++ b/src/components/FullscreenPlayer.tsx @@ -355,7 +355,9 @@ export default function FullscreenPlayer({ onClose }: FullscreenPlayerProps) { }, [currentTrack?.artistId]); const portraitUrl = artistBgUrl || resolvedCoverUrl; - const showFullscreenLyrics = useAuthStore(s => s.showFullscreenLyrics); + const showFullscreenLyrics = useAuthStore(s => s.showFullscreenLyrics); + const showFsArtistPortrait = useAuthStore(s => s.showFsArtistPortrait); + const fsPortraitDim = useAuthStore(s => s.fsPortraitDim); // Pre-fetch next track's 300px cover into the IndexedDB cache. // Selector returns only the coverArt id, so it only re-runs on actual changes. @@ -419,7 +421,10 @@ export default function FullscreenPlayer({ onClose }: FullscreenPlayerProps) { aria-label={t('player.fullscreen')} data-idle={isIdle} onMouseMove={handleMouseMove} - style={dynamicAccent ? { '--dynamic-fs-accent': dynamicAccent } as React.CSSProperties : undefined} + style={{ + ...(dynamicAccent ? { '--dynamic-fs-accent': dynamicAccent } : {}), + '--fs-portrait-dim': String(fsPortraitDim / 100), + } as React.CSSProperties} > {/* Layer 0 — animated dark mesh gradient (real divs = will-change possible) */} @@ -429,7 +434,7 @@ export default function FullscreenPlayer({ onClose }: FullscreenPlayerProps) { {/* Layer 1 — artist portrait, right half, object-fit: contain */} - + {showFsArtistPortrait && } {/* Layer 2 — horizontal scrim: dark left → transparent right */} +
+
+ +

{t('settings.fsPlayerSection')}

+
+
+
+
+
{t('settings.fsShowArtistPortrait')}
+
{t('settings.fsShowArtistPortraitDesc')}
+
+ +
+ {auth.showFsArtistPortrait && ( +
+
+ {t('settings.fsPortraitDim')} + {auth.fsPortraitDim}% +
+ auth.setFsPortraitDim(parseInt(e.target.value, 10))} + className="ui-scale-slider" + /> +
+ )} +
+
+
diff --git a/src/store/authStore.ts b/src/store/authStore.ts index 8853095c..19618490 100644 --- a/src/store/authStore.ts +++ b/src/store/authStore.ts @@ -67,6 +67,9 @@ interface AuthState { enableNeteaselyrics: boolean; lyricsSources: LyricsSourceConfig[]; showFullscreenLyrics: boolean; + showFsArtistPortrait: boolean; + /** Portrait dimming 0–100 (percent), applied as CSS rgba alpha */ + fsPortraitDim: number; showChangelogOnUpdate: boolean; lastSeenChangelogVersion: string; @@ -188,6 +191,8 @@ interface AuthState { setEnableNeteaselyrics: (v: boolean) => void; setLyricsSources: (sources: LyricsSourceConfig[]) => void; setShowFullscreenLyrics: (v: boolean) => void; + setShowFsArtistPortrait: (v: boolean) => void; + setFsPortraitDim: (v: number) => void; setShowChangelogOnUpdate: (v: boolean) => void; setLastSeenChangelogVersion: (v: string) => void; setSeekbarStyle: (v: SeekbarStyle) => void; @@ -278,6 +283,8 @@ export const useAuthStore = create()( enableNeteaselyrics: false, lyricsSources: DEFAULT_LYRICS_SOURCES, showFullscreenLyrics: true, + showFsArtistPortrait: true, + fsPortraitDim: 28, showChangelogOnUpdate: true, lastSeenChangelogVersion: '', seekbarStyle: 'waveform', @@ -396,6 +403,8 @@ export const useAuthStore = create()( setEnableNeteaselyrics: (v: boolean) => set({ enableNeteaselyrics: v }), setLyricsSources: (sources) => set({ lyricsSources: sources }), setShowFullscreenLyrics: (v: boolean) => set({ showFullscreenLyrics: v }), + setShowFsArtistPortrait: (v: boolean) => set({ showFsArtistPortrait: v }), + setFsPortraitDim: (v: number) => set({ fsPortraitDim: v }), setShowChangelogOnUpdate: (v) => set({ showChangelogOnUpdate: v }), setLastSeenChangelogVersion: (v) => set({ lastSeenChangelogVersion: v }), diff --git a/src/styles/components.css b/src/styles/components.css index cbdf1a35..8fd74394 100644 --- a/src/styles/components.css +++ b/src/styles/components.css @@ -3119,6 +3119,17 @@ display: block; } +/* Subtle darkening overlay — plain div paint, no GPU cost. + Alpha driven by --fs-portrait-dim (set via inline style on .fs-player). */ +.fs-portrait-wrap::after { + content: ''; + position: absolute; + inset: 0; + z-index: 1; + pointer-events: none; + background: rgba(0, 0, 0, var(--fs-portrait-dim, 0.28)); +} + /* ── Horizontal scrim — dark left edge for text legibility ── */ .fs-scrim { position: absolute; @@ -3440,6 +3451,8 @@ align-items: center; overflow: hidden; white-space: normal; + overflow-wrap: break-word; + word-break: break-word; font-size: 2vh; line-height: 1.4; color: rgba(255, 255, 255, 0.22); @@ -3469,6 +3482,59 @@ } +/* ── no-compositing overrides ───────────────────────────────────────────────── + Applied only when WEBKIT_DISABLE_COMPOSITING_MODE=1 is set (Arch Linux etc). + Replaces GPU-only effects (backdrop-filter, CSS filter, mask-image) with + software-friendly equivalents so the fullscreen player stays responsive. */ + +/* fs-player: remove will-change — without GPU compositor it only wastes RAM */ +html.no-compositing .fs-player { + will-change: auto; +} + +/* Close button: replace frosted-glass blur with flat semi-opaque background */ +html.no-compositing .fs-close { + backdrop-filter: none; + background: rgba(0, 0, 0, 0.45); +} + +/* Portrait: remove CSS filter (drop-shadow) and will-change */ +html.no-compositing .fs-portrait { + filter: none; + will-change: auto; +} + +/* Portrait wrap: replace mask-image fade with a pseudo-element overlay gradient. + pointer-events: none on wrap means the overlay never blocks clicks. */ +html.no-compositing .fs-portrait-wrap { + -webkit-mask-image: none; + mask-image: none; +} +html.no-compositing .fs-portrait-wrap::before { + content: ''; + position: absolute; + inset: 0; + z-index: 1; + pointer-events: none; + background: linear-gradient( + to right, + #06060e 0%, + rgba(6, 6, 14, 0.4) 6%, + transparent 16% + ); +} + +/* Lyrics overlay: just remove the fade — no replacement needed */ +html.no-compositing .fs-lyrics-overlay { + -webkit-mask-image: none; + mask-image: none; +} + +/* Lyrics rail: remove will-change */ +html.no-compositing .fs-lyrics-rail { + will-change: auto; +} + /* Chat */ .chat-popup { position: absolute;