diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index a510e862..5d508499 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3612,6 +3612,7 @@ dependencies = [ "thread-priority", "tokio", "url", + "webkit2gtk", "windows 0.58.0", "zbus 5.14.0", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 8ecb638a..cf881ceb 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -54,6 +54,8 @@ libc = "0.2" [target.'cfg(target_os = "linux")'.dependencies] zbus = { version = "5.9", default-features = false, features = ["blocking-api"] } +# Match wry/tauri’s WebKitGTK stack — used only to turn off kinetic wheel scrolling. +webkit2gtk = { version = "2.0", default-features = false, features = ["v2_40"] } [target.'cfg(windows)'.dependencies] windows = { version = "0.58", features = [ diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ff5d900e..9c31577c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -90,6 +90,35 @@ fn set_window_decorations(enabled: bool, app_handle: tauri::AppHandle) { } } +/// WebKitGTK: `enable-smooth-scrolling` also drives deferred / kinetic wheel scrolling. +#[cfg(target_os = "linux")] +fn linux_webkit_apply_smooth_scrolling(win: &tauri::WebviewWindow, enabled: bool) -> Result<(), String> { + win.with_webview(move |platform| { + use webkit2gtk::{SettingsExt, WebViewExt}; + if let Some(settings) = platform.inner().settings() { + settings.set_enable_smooth_scrolling(enabled); + } + }) + .map_err(|e| e.to_string()) +} + +/// Called from the frontend settings toggle (Linux); no-op on other platforms. +#[tauri::command] +fn set_linux_webkit_smooth_scrolling(enabled: bool, app_handle: tauri::AppHandle) -> Result<(), String> { + #[cfg(target_os = "linux")] + { + use tauri::Manager; + if let Some(win) = app_handle.get_webview_window("main") { + linux_webkit_apply_smooth_scrolling(&win, enabled)?; + } + } + #[cfg(not(target_os = "linux"))] + { + let _ = (enabled, app_handle); + } + Ok(()) +} + /// Authenticate with Navidrome's own REST API and return a Bearer token. async fn navidrome_token(server_url: &str, username: &str, password: &str) -> Result { @@ -2578,6 +2607,7 @@ pub fn run() { cli_publish_server_list, cli_publish_search_results, set_window_decorations, + set_linux_webkit_smooth_scrolling, no_compositing_mode, is_tiling_wm_cmd, register_global_shortcut, diff --git a/src/App.tsx b/src/App.tsx index 85423850..8598b4a8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -139,6 +139,7 @@ function AppShell() { const activeServerId = useAuthStore(s => s.activeServerId); const setMusicFolders = useAuthStore(s => s.setMusicFolders); const useCustomTitlebar = useAuthStore(s => s.useCustomTitlebar); + const linuxWebkitKineticScroll = useAuthStore(s => s.linuxWebkitKineticScroll); const setEntityRatingSupport = useAuthStore(s => s.setEntityRatingSupport); const offlineAlbums = useOfflineStore(s => s.albums); const hasOfflineContent = Object.values(offlineAlbums).some(a => a.serverId === serverId); @@ -151,6 +152,11 @@ function AppShell() { invoke('set_window_decorations', { enabled }).catch(() => {}); }, [useCustomTitlebar, isTilingWm]); + useEffect(() => { + if (!IS_LINUX) return; + invoke('set_linux_webkit_smooth_scrolling', { enabled: linuxWebkitKineticScroll }).catch(() => {}); + }, [linuxWebkitKineticScroll]); + useEffect(() => { if (!isLoggedIn || !activeServerId) return; const serverAtStart = activeServerId; diff --git a/src/locales/de.ts b/src/locales/de.ts index eff57a06..72103ad3 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -538,6 +538,8 @@ export const deTranslation = { discordRichPresenceDesc: 'Zeigt den aktuell gespielten Titel im Discord-Profil an. Discord muss dafür geöffnet sein.', useCustomTitlebar: 'Eigene Titelleiste', useCustomTitlebarDesc: 'Ersetzt die System-Titelleiste durch eine eingebaute, die zum App-Theme passt. Deaktivieren, um die native GNOME/GTK-Titelleiste zu verwenden.', + linuxWebkitSmoothScroll: 'Sanftes Mausrad (Linux)', + linuxWebkitSmoothScrollDesc: 'An: mit Nachlauf. Aus: zeilenweise wie in GTK-Apps.', discordAppleCovers: 'Cover über Apple Music für Discord laden', discordAppleCoversDesc: 'Sendet Künstler- und Albumname an die Apple-Such-API, um Cover für dein Discord-Profil zu finden. Standardmäßig aus Datenschutzgründen deaktiviert.', discordOptions: 'Erweiterte Discord-Optionen', diff --git a/src/locales/en.ts b/src/locales/en.ts index 3aba9ca9..21f85ae6 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -540,6 +540,8 @@ export const enTranslation = { discordRichPresenceDesc: 'Show the currently playing track on your Discord profile. Requires Discord to be running.', useCustomTitlebar: 'Custom title bar', useCustomTitlebarDesc: 'Replace the system title bar with a built-in one that matches the app theme. Disable to use the native GNOME/GTK title bar.', + linuxWebkitSmoothScroll: 'Smooth wheel (Linux)', + linuxWebkitSmoothScrollDesc: 'On: inertial scroll. Off: line-by-line, GTK-style.', discordAppleCovers: 'Fetch covers from Apple Music for Discord', discordAppleCoversDesc: 'Sends the artist and album name to Apple\'s search API to find cover art for your Discord profile. Disabled by default for privacy.', discordOptions: 'Advanced Discord options', diff --git a/src/locales/es.ts b/src/locales/es.ts index 8975b61a..8ef28c9f 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -541,6 +541,8 @@ export const esTranslation = { discordRichPresenceDesc: 'Muestra la pista actual en tu perfil de Discord. Requiere que Discord esté ejecutándose.', useCustomTitlebar: 'Barra de título personalizada', useCustomTitlebarDesc: 'Reemplaza la barra de título del sistema con una integrada que coincide con el tema de la app. Desactiva para usar la barra nativa de GNOME/GTK.', + linuxWebkitSmoothScroll: 'Rueda suave (Linux)', + linuxWebkitSmoothScrollDesc: 'Activado: inercia. Desactivado: pasos por línea (estilo GTK).', discordAppleCovers: 'Obtener portadas de Apple Music para Discord', discordAppleCoversDesc: 'Envía el artista y nombre del álbum a la API de búsqueda de Apple para encontrar portadas para tu perfil de Discord. Desactivado por defecto por privacidad.', discordOptions: 'Opciones avanzadas de Discord', diff --git a/src/locales/fr.ts b/src/locales/fr.ts index 99ebf083..a76b77a1 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -534,6 +534,8 @@ export const frTranslation = { showTrayIconDesc: 'Affiche l\'icône Psysonic dans la zone de notification / barre des menus.', minimizeToTray: 'Réduire dans la barre système', minimizeToTrayDesc: 'Lors de la fermeture, Psysonic continue de fonctionner dans la barre système au lieu de se fermer.', + linuxWebkitSmoothScroll: 'Molette fluide (Linux)', + linuxWebkitSmoothScrollDesc: 'Activé : inertie. Désactivé : pas à la ligne, style GTK.', discordRichPresence: 'Discord Rich Presence', discordRichPresenceDesc: 'Affiche le titre en cours de lecture sur votre profil Discord. Discord doit être ouvert.', discordAppleCovers: 'Récupérer les pochettes via Apple Music pour Discord', diff --git a/src/locales/nb.ts b/src/locales/nb.ts index d9ad62c7..9341e03c 100644 --- a/src/locales/nb.ts +++ b/src/locales/nb.ts @@ -533,6 +533,8 @@ export const nbTranslation = { showArtistImagesDesc: 'Last inn og vis artistbilder i artistoversikten. Denne er deaktivert som standard, for å redusere disk-I/O og nettverksbelastningen på store biblioteker.', minimizeToTray: 'Minimer til oppgavelinjen', minimizeToTrayDesc: 'Når vinduet lukkes, vil Psysonic bli kjørende i oppgavelinjen fremfor å bli avsluttet.', + linuxWebkitSmoothScroll: 'Mykt musehjul (Linux)', + linuxWebkitSmoothScrollDesc: 'På: treg rull med etterslep. Av: trinnvis som i GTK.', discordRichPresence: 'Discord Rich Presence', discordRichPresenceDesc: 'Vis sporet som spilles i din Discord-profil. Krever at Discord kjører.', discordAppleCovers: 'Hent covere fra Apple Music til Discord', diff --git a/src/locales/nl.ts b/src/locales/nl.ts index 60d4537f..9bbdf0e0 100644 --- a/src/locales/nl.ts +++ b/src/locales/nl.ts @@ -533,6 +533,8 @@ export const nlTranslation = { showTrayIconDesc: 'Toont het Psysonic-pictogram in het systeemvak / de menubalk.', minimizeToTray: 'Minimaliseren naar systeemvak', minimizeToTrayDesc: 'Bij het sluiten van het venster blijft Psysonic actief in het systeemvak in plaats van af te sluiten.', + linuxWebkitSmoothScroll: 'Vloeiend muiswiel (Linux)', + linuxWebkitSmoothScrollDesc: 'Aan: traag naloop. Uit: regel voor regel, GTK-stijl.', discordRichPresence: 'Discord Rich Presence', discordRichPresenceDesc: 'Toont het huidige nummer op je Discord-profiel. Discord moet daarvoor geopend zijn.', discordAppleCovers: 'Hoezen ophalen via Apple Music voor Discord', diff --git a/src/locales/ru.ts b/src/locales/ru.ts index 354820df..8edc7cf3 100644 --- a/src/locales/ru.ts +++ b/src/locales/ru.ts @@ -555,6 +555,8 @@ export const ruTranslation = { useCustomTitlebar: 'Своя строка заголовка', useCustomTitlebarDesc: 'Заменить системную строку заголовка встроенной, в стиле темы приложения. Отключите, чтобы использовать родную строку GNOME/GTK и т.д.', + linuxWebkitSmoothScroll: 'Плавное колесо (Linux)', + linuxWebkitSmoothScrollDesc: 'Вкл — инерция. Выкл — по шагам, как в GTK.', discordRichPresence: 'Статус в Discord', discordRichPresenceDesc: 'Показывать текущий трек в профиле и статусе Discord. Нужен запущенный клиент Discord.', diff --git a/src/locales/zh.ts b/src/locales/zh.ts index c89cac84..dc8865c6 100644 --- a/src/locales/zh.ts +++ b/src/locales/zh.ts @@ -529,6 +529,8 @@ export const zhTranslation = { showTrayIconDesc: '在系统通知区域 / 菜单栏显示 Psysonic 图标。', minimizeToTray: '最小化到托盘', minimizeToTrayDesc: '关闭窗口时,Psysonic 将继续在系统托盘中运行,而不是退出。', + linuxWebkitSmoothScroll: '滚轮平滑(Linux)', + linuxWebkitSmoothScrollDesc: '开:惯性滚动。关:逐行,类似 GTK。', discordRichPresence: 'Discord Rich Presence', discordRichPresenceDesc: '在 Discord 个人资料上显示当前播放的曲目。需要 Discord 处于运行状态。', discordAppleCovers: '通过 Apple Music 为 Discord 获取封面', diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 2ad153b9..74ea7018 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -1136,6 +1136,25 @@ export default function Settings() { )} + {IS_LINUX && ( + <> +
+
+
+
{t('settings.linuxWebkitSmoothScroll')}
+
{t('settings.linuxWebkitSmoothScrollDesc')}
+
+ +
+ + )}
diff --git a/src/store/authStore.ts b/src/store/authStore.ts index 7e246846..f779fa21 100644 --- a/src/store/authStore.ts +++ b/src/store/authStore.ts @@ -8,6 +8,7 @@ import { type SubsonicServerIdentity, } from '../utils/subsonicServerIdentity'; import { usePlayerStore } from './playerStore'; +import { IS_LINUX } from '../utils/platform'; export interface ServerProfile { id: string; @@ -65,6 +66,8 @@ interface AuthState { discordTemplateState: string; discordTemplateLargeText: string; useCustomTitlebar: boolean; + /** Linux WebKitGTK: smooth wheel on when true; off only after explicit opt-out in Settings. */ + linuxWebkitKineticScroll: boolean; nowPlayingEnabled: boolean; lyricsServerFirst: boolean; enableNeteaselyrics: boolean; @@ -209,6 +212,7 @@ interface AuthState { setDiscordTemplateState: (v: string) => void; setDiscordTemplateLargeText: (v: string) => void; setUseCustomTitlebar: (v: boolean) => void; + setLinuxWebkitKineticScroll: (v: boolean) => void; setNowPlayingEnabled: (v: boolean) => void; setLyricsServerFirst: (v: boolean) => void; setEnableNeteaselyrics: (v: boolean) => void; @@ -314,6 +318,7 @@ export const useAuthStore = create()( discordTemplateState: '{album}', discordTemplateLargeText: '{album}', useCustomTitlebar: false, + linuxWebkitKineticScroll: true, nowPlayingEnabled: false, lyricsServerFirst: true, enableNeteaselyrics: false, @@ -443,6 +448,7 @@ export const useAuthStore = create()( setDiscordTemplateState: (v) => set({ discordTemplateState: v }), setDiscordTemplateLargeText: (v) => set({ discordTemplateLargeText: v }), setUseCustomTitlebar: (v) => set({ useCustomTitlebar: v }), + setLinuxWebkitKineticScroll: (v) => set({ linuxWebkitKineticScroll: v }), setNowPlayingEnabled: (v) => set({ nowPlayingEnabled: v }), setLyricsServerFirst: (v: boolean) => set({ lyricsServerFirst: v }), setEnableNeteaselyrics: (v: boolean) => set({ enableNeteaselyrics: v }), @@ -630,6 +636,20 @@ export const useAuthStore = create()( } } catch { /* ignore */ } + // One-time: older builds could persist smooth=false as the default. Force smooth on once + // so updates do not leave users on discrete scrolling; after this flag exists, only an + // explicit toggle in Settings may turn it off (persisted in psysonic-auth). + const wheelSmoothMigrationKey = 'psysonic-linux-webkit-smooth-v1'; + let wheelSmoothOneTime: { linuxWebkitKineticScroll?: boolean } = {}; + if (IS_LINUX) { + try { + if (!localStorage.getItem(wheelSmoothMigrationKey)) { + wheelSmoothOneTime = { linuxWebkitKineticScroll: true }; + localStorage.setItem(wheelSmoothMigrationKey, '1'); + } + } catch { /* ignore */ } + } + useAuthStore.setState({ mixMinRatingSong: clampMixFilterMinStars(state.mixMinRatingSong as number), mixMinRatingAlbum: clampMixFilterMinStars(state.mixMinRatingAlbum as number), @@ -639,6 +659,7 @@ export const useAuthStore = create()( ), ...conflictingLegacyState, ...lyricsSourcesMigrated, + ...wheelSmoothOneTime, }); }, }