mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 06:25:41 +00:00
feat(linux): WebKitGTK wheel scroll mode (smooth default, optional linear)
feat(linux): WebKitGTK wheel scroll mode (smooth default, optional linear)
This commit is contained in:
Generated
+1
@@ -3612,6 +3612,7 @@ dependencies = [
|
||||
"thread-priority",
|
||||
"tokio",
|
||||
"url",
|
||||
"webkit2gtk",
|
||||
"windows 0.58.0",
|
||||
"zbus 5.14.0",
|
||||
]
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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<String, String> {
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -555,6 +555,8 @@ export const ruTranslation = {
|
||||
useCustomTitlebar: 'Своя строка заголовка',
|
||||
useCustomTitlebarDesc:
|
||||
'Заменить системную строку заголовка встроенной, в стиле темы приложения. Отключите, чтобы использовать родную строку GNOME/GTK и т.д.',
|
||||
linuxWebkitSmoothScroll: 'Плавное колесо (Linux)',
|
||||
linuxWebkitSmoothScrollDesc: 'Вкл — инерция. Выкл — по шагам, как в GTK.',
|
||||
discordRichPresence: 'Статус в Discord',
|
||||
discordRichPresenceDesc:
|
||||
'Показывать текущий трек в профиле и статусе Discord. Нужен запущенный клиент Discord.',
|
||||
|
||||
@@ -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 获取封面',
|
||||
|
||||
@@ -1136,6 +1136,25 @@ export default function Settings() {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{IS_LINUX && (
|
||||
<>
|
||||
<div className="settings-section-divider" />
|
||||
<div className="settings-toggle-row">
|
||||
<div>
|
||||
<div style={{ fontWeight: 500 }}>{t('settings.linuxWebkitSmoothScroll')}</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{t('settings.linuxWebkitSmoothScrollDesc')}</div>
|
||||
</div>
|
||||
<label className="toggle-switch" aria-label={t('settings.linuxWebkitSmoothScroll')}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={auth.linuxWebkitKineticScroll}
|
||||
onChange={e => auth.setLinuxWebkitKineticScroll(e.target.checked)}
|
||||
/>
|
||||
<span className="toggle-track" />
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="settings-section-divider" />
|
||||
<div className="settings-toggle-row">
|
||||
<div>
|
||||
|
||||
@@ -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<AuthState>()(
|
||||
discordTemplateState: '{album}',
|
||||
discordTemplateLargeText: '{album}',
|
||||
useCustomTitlebar: false,
|
||||
linuxWebkitKineticScroll: true,
|
||||
nowPlayingEnabled: false,
|
||||
lyricsServerFirst: true,
|
||||
enableNeteaselyrics: false,
|
||||
@@ -443,6 +448,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
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<AuthState>()(
|
||||
}
|
||||
} 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<AuthState>()(
|
||||
),
|
||||
...conflictingLegacyState,
|
||||
...lyricsSourcesMigrated,
|
||||
...wheelSmoothOneTime,
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user