[READ-ONLY] Mirror of https://github.com/probablykasper/ferrum. Music library app for Mac, Linux and Windows ferrum.kasper.space
electron linux macos music music-library music-player napi windows
0

Configure Feed

Select the types of activity you want to include in your feed.

Keep scroll position when opening play history

Kasper (Mar 8, 2026, 7:13 AM +0100) 367b7303 4c1ccc7f

+46 -3
+1
CHANGELOG.md
··· 2 2 3 3 ## Next 4 4 - Remember queue on restart 5 + - Keep scroll position when opening play history 5 6 - Android: Add track streaming search links for Spotify and YouTube Music 6 7 7 8 ## 0.21.0 - 2026 Map 6
+45 -3
src/components/Queue.svelte
··· 15 15 import { ipc_listen, ipc_renderer } from '$lib/window' 16 16 import { check_shortcut } from '$lib/helpers' 17 17 import { fly } from 'svelte/transition' 18 + import { cubicOut } from 'svelte/easing' 18 19 import VirtualListBlock, { scroll_container_keydown } from './VirtualListBlock.svelte' 19 20 import type { SelectedTracksAction } from '$electron/typed_ipc' 20 21 import { get_flattened_tracklists, handle_selected_tracks_action } from '$lib/menus' 21 22 import { SvelteSelection } from '$lib/selection' 22 23 23 24 let object_urls: string[] = [] 25 + const entry_height = 54 24 26 25 27 onDestroy(() => { 26 28 for (let url of object_urls) { ··· 90 92 onDestroy(ipc_listen('context.Remove from Queue', remove_from_queue)) 91 93 92 94 let queue_element: HTMLElement 95 + let indicator_scroll_anim: number | null = null 96 + const indicator_scroll_duration_ms = 200 97 + function smooth_scroll_to(top: number) { 98 + const max_top = Math.max(0, queue_element.scrollHeight - queue_element.clientHeight) 99 + const target_top = Math.max(0, Math.min(top, max_top)) 100 + if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { 101 + queue_element.scrollTop = target_top 102 + return 103 + } 104 + if (indicator_scroll_anim) { 105 + cancelAnimationFrame(indicator_scroll_anim) 106 + } 107 + const start_top = queue_element.scrollTop 108 + const start_ts = performance.now() 109 + const frame = (now: number) => { 110 + const p = Math.min(1, (now - start_ts) / indicator_scroll_duration_ms) 111 + queue_element.scrollTop = start_top + (target_top - start_top) * cubicOut(p) 112 + if (p < 1) { 113 + indicator_scroll_anim = requestAnimationFrame(frame) 114 + } else { 115 + indicator_scroll_anim = null 116 + } 117 + } 118 + indicator_scroll_anim = requestAnimationFrame(frame) 119 + } 120 + onDestroy(() => { 121 + if (indicator_scroll_anim !== null) { 122 + cancelAnimationFrame(indicator_scroll_anim) 123 + indicator_scroll_anim = null 124 + } 125 + }) 93 126 94 127 function handle_action(action: SelectedTracksAction) { 95 128 const first_index = selection.find_first_index() ··· 204 237 <button 205 238 type="button" 206 239 on:click={() => { 240 + const history_rows = $queue.past.length + Number(!!$queue.current) 241 + const show = !show_history 207 242 show_history = !show_history 208 243 tick().then(() => { 244 + if (show) { 245 + const old_top = queue_element.scrollTop + history_rows * entry_height 246 + queue_element.scrollTop = old_top 247 + smooth_scroll_to(old_top - entry_height * 5) 248 + } else { 249 + queue_element.scrollTop = 0 250 + } 209 251 up_next_list?.refresh() 210 252 autoplay_list.refresh() 211 253 }) ··· 237 279 bind:this={history_list} 238 280 items={$queue.past} 239 281 get_key={(item) => item.qId} 240 - item_height={54} 282 + item_height={entry_height} 241 283 scroll_container={queue_element} 242 284 let:item 243 285 let:i={qi} ··· 320 362 bind:this={up_next_list} 321 363 items={$queue.user_queue} 322 364 get_key={(item) => item.qId} 323 - item_height={54} 365 + item_height={entry_height} 324 366 scroll_container={queue_element} 325 367 let:item 326 368 let:i ··· 361 403 bind:this={autoplay_list} 362 404 items={$queue.auto_queue} 363 405 get_key={(item) => item.qId} 364 - item_height={54} 406 + item_height={entry_height} 365 407 scroll_container={queue_element} 366 408 let:item 367 409 let:i