[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.

Optimize slider

Kasper (Sep 12, 2024, 2:08 AM +0200) d6baf176 04ed2db4

+65 -41
+8 -9
src/components/Player.svelte
··· 1 1 <script lang="ts"> 2 2 import { 3 3 stopped, 4 - paused, 5 4 playPause, 6 - duration, 7 - currentTime, 8 5 seek, 9 6 playingTrack, 10 7 playingId, ··· 12 9 skipToNext, 13 10 coverSrc, 14 11 volume, 12 + timeRecord, 15 13 } from '../lib/player' 16 14 import { getDuration } from '../lib/helpers' 17 15 import { queueVisible, toggleQueueVisibility, queue, shuffle, repeat } from '../lib/queue' ··· 114 112 </button> 115 113 116 114 <button class="play-pause" on:click={playPause} tabindex="-1" on:mousedown|preventDefault> 117 - {#if $paused} 115 + {#if $timeRecord.paused} 118 116 <svg 119 117 xmlns="http://www.w3.org/2000/svg" 120 118 class="parent-active-zoom" ··· 177 175 </button> 178 176 </div> 179 177 <div class="time-bar"> 180 - <small class="current-time">{getDuration($currentTime)}</small> 178 + <small class="current-time">{getDuration($timeRecord.elapsed)}</small> 181 179 <Slider 182 180 class="mx-1.5 w-full" 183 - value={$currentTime / $duration || 0} 181 + value={$timeRecord.elapsed / $timeRecord.duration || 0} 182 + growth_rate={$timeRecord.paused ? 0 : 1 / $timeRecord.duration} 184 183 max={1} 185 184 update_on_drag={false} 186 - on_apply={(value) => { 187 - seek(value * $duration) 185 + on_user_change={(value) => { 186 + seek(value * $timeRecord.duration) 188 187 }} 189 188 /> 190 - <small class="duration">{getDuration($duration)}</small> 189 + <small class="duration">{getDuration($timeRecord.duration)}</small> 191 190 </div> 192 191 </div> 193 192 <div class="right">
+34 -12
src/components/Slider.svelte
··· 1 1 <script lang="ts"> 2 + import { onDestroy } from 'svelte' 2 3 import type { HTMLBaseAttributes } from 'svelte/elements' 3 4 export let value: number 5 + /** Growth rate per second. This is for providing a smooth visual with low CPU usage. */ 6 + export let growth_rate: number = 0 4 7 export let max = 100 5 8 export let update_on_drag = true 6 - export let on_apply: (value: number) => void = () => {} 9 + export let on_user_change: (value: number) => void = () => {} 7 10 export let klass = '' 8 11 export { klass as class } 9 12 10 13 // eslint-disable-next-line @typescript-eslint/no-unused-vars 11 14 interface $$Props extends HTMLBaseAttributes { 12 15 value: number 16 + growth_rate?: number 13 17 max?: number 14 18 step?: number 15 19 class?: string 16 20 update_on_drag?: boolean 17 - on_apply?: (value: number) => void 21 + on_user_change?: (value: number) => void 18 22 } 19 23 20 24 let bar: HTMLDivElement 21 25 let dragging = false 22 26 23 27 let internal_value = value 28 + let updated_at = Date.now() 24 29 $: if (update_on_drag || !dragging) { 25 - // Only update if the difference is 0.5px+ 26 - const diff_px = Math.abs(internal_value - value) * bar?.clientWidth * devicePixelRatio 27 - if (diff_px > 0.5) { 28 - internal_value = value 29 - } else if (!bar) { 30 - internal_value = value 31 - } 30 + internal_value = value 31 + updated_at = Date.now() 32 32 } 33 33 34 34 function apply(e: MouseEvent) { 35 35 const delta = e.clientX - bar.getBoundingClientRect().left 36 36 internal_value = Math.min(max, Math.max(0, (delta / bar.clientWidth) * max)) 37 + updated_at = Date.now() 37 38 if (update_on_drag || !dragging) { 38 39 value = internal_value 39 40 } 40 41 } 42 + 43 + function update_value() { 44 + const now = Date.now() 45 + const elapsed_ms = now - updated_at 46 + internal_value = internal_value + elapsed_ms * 0.001 * growth_rate 47 + updated_at = now 48 + } 49 + 50 + let interval: ReturnType<typeof setInterval> | null = null 51 + function start_growing() { 52 + if (interval) clearInterval(interval) 53 + const secs_per_pixel = max / (bar.clientWidth * devicePixelRatio * 2 * growth_rate) 54 + interval = setInterval(update_value, secs_per_pixel * 1000) 55 + } 56 + $: if (growth_rate && bar) { 57 + start_growing() 58 + } 59 + 60 + onDestroy(() => { 61 + if (interval) clearInterval(interval) 62 + }) 41 63 </script> 42 64 43 65 <svelte:window ··· 50 72 if (dragging) { 51 73 dragging = false 52 74 apply(e) 53 - on_apply(value) 75 + on_user_change(value) 54 76 } 55 77 }} 56 78 /> 57 - <!-- We use custom mouse events because updating an <input> value causes reflow --> 79 + <!-- If we had an <input>, it would cause a reflow every time the value updates. Instead of that, we CSS and mouse events. --> 80 + <!-- We also don't use the Web Animation API, because somehow that had way higher CPU usage for me --> 58 81 <div class="slider{` ${klass}`.trimEnd()}" {...$$restProps}> 59 82 <!-- svelte-ignore a11y-no-static-element-interactions --> 60 83 <div ··· 66 89 > 67 90 <div class="pointer-events-none relative w-full rounded-full bg-gray-700" bind:this={bar}> 68 91 <div class="w-full overflow-hidden rounded-full"> 69 - <!-- I tried the Web Animation API, but somehow that resulted in higher CPU usage --> 70 92 <div 71 93 class="relative -left-full h-1 w-full rounded-full bg-gray-300 transition-colors duration-100 will-change-transform group-hover:bg-[hsl(217,100%,60%)] group-active:bg-[hsl(217,100%,60%)]" 72 94 style:translate="{(internal_value / max) * 100}%"
+23 -20
src/lib/player.ts
··· 19 19 }, 20 20 } 21 21 })() 22 - export const paused = writable(true) 23 - export const currentTime = writable(0) 24 - export const duration = writable(0) 22 + export const timeRecord = writable({ 23 + elapsed: 0, 24 + at_timestamp: Date.now(), 25 + paused: true, 26 + duration: 0, 27 + }) 28 + function updateTimeDetails() { 29 + timeRecord.update((record) => { 30 + record.elapsed = audio.currentTime 31 + record.at_timestamp = Date.now() 32 + record.paused = audio.paused 33 + record.duration = audio.duration 34 + return record 35 + }) 36 + } 25 37 export const playingTrack: Writable<Track | null> = writable(null) 26 38 export const playingId = derived(queue, () => { 27 39 const currentId = queue.getCurrent()?.id ··· 76 88 } 77 89 })() 78 90 79 - function update_time() { 80 - currentTime.set(audio.currentTime) 81 - if (!audio.paused) { 82 - requestAnimationFrame(update_time) 83 - } 84 - } 85 - 86 - audio.onplay = () => { 87 - requestAnimationFrame(update_time) 88 - } 91 + audio.onplay = updateTimeDetails 92 + audio.onpause = updateTimeDetails 93 + audio.ontimeupdate = updateTimeDetails 89 94 90 95 audio.addEventListener('error', async (e) => { 91 96 stop() ··· 111 116 112 117 function startPlayback() { 113 118 audio.play() 114 - paused.set(false) 119 + updateTimeDetails() 115 120 startTime = Date.now() 116 121 if (mediaSession) mediaSession.playbackState = 'playing' 117 122 } ··· 141 146 } 142 147 } 143 148 144 - audio.ondurationchange = () => { 145 - duration.set(audio.duration) 146 - } 149 + audio.ondurationchange = updateTimeDetails 147 150 148 151 /** Saves play time if needed */ 149 152 function resetAndSavePlayTime() { ··· 157 160 function pausePlayback() { 158 161 waitingToPlay = false 159 162 audio.pause() 160 - paused.set(true) 163 + updateTimeDetails() 161 164 resetAndSavePlayTime() 162 165 if (mediaSession) mediaSession.playbackState = 'paused' 163 166 } ··· 192 195 export function stop() { 193 196 waitingToPlay = false 194 197 audio.pause() 195 - paused.set(true) 198 + updateTimeDetails() 196 199 resetAndSavePlayTime() 197 200 stopped.set(true) 198 201 seek(0) ··· 254 257 } else { 255 258 audio.currentTime = newTime 256 259 } 257 - currentTime.set(newTime) 260 + updateTimeDetails() 258 261 } 259 262 260 263 if (navigator.mediaSession) {