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

Fix playlist dragging

Kasper (Jan 13, 2026, 5:21 AM +0100) a4fa55ec 87f6a071

+48 -37
+3
CHANGELOG.md
··· 1 1 # Changelog 2 2 3 + ## Next 4 + - Fix playlist dragging 5 + 3 6 ## 0.20.0 - 2025 Oct 4 4 7 - Add audio visualiser (thanks to help from @zachwinter) 5 8 - Add filtering for specific fields in search, like `artist:apashe` or `album:"mirai sekai"`. You can also right-click a column header to add a filter. Filtering by artist also finds featured artists, remixers etc in the song title.
+6 -7
src/components/Sidebar.svelte
··· 1 1 <script lang="ts" context="module"> 2 2 export const special_playlists_nav = [ 3 - { id: 'root', name: 'Songs', kind: 'special', path: '/playlist/root' }, 3 + { id: 'root', name: 'Songs', kind: 'special' }, 4 4 // { id: 'root', name: 'Artists', kind: 'special', path: '/artists' }, 5 5 ] 6 6 </script> ··· 140 140 <div class="focuser" tabindex="0" on:focus={focuser}></div> 141 141 <div class="spacer"></div> 142 142 <SidebarItems 143 - parent_path={null} 143 + parent_id={null} 144 144 children={special_playlists_nav} 145 145 on_select_down={() => { 146 146 if ($track_lists_details_map.root.children && $track_lists_details_map.root.children[0]) { ··· 150 150 /> 151 151 <div class="spacer"></div> 152 152 <SidebarItems 153 - parent_path="/playlist/root" 154 - children={($track_lists_details_map['root'].children || []).map((child_id) => ({ 155 - path: '/playlist/' + child_id, 156 - ...$track_lists_details_map[child_id], 157 - }))} 153 + parent_id="root" 154 + children={($track_lists_details_map['root'].children || []).map( 155 + (child_id) => $track_lists_details_map[child_id], 156 + )} 158 157 /> 159 158 </nav> 160 159 </div>
+39 -30
src/components/SidebarItems.svelte
··· 38 38 import { check_shortcut } from '$lib/helpers' 39 39 import { navigate, url_pathname } from '$lib/router' 40 40 import { current_playlist_id } from './TrackList.svelte' 41 + import { tracklist_actions } from '$lib/page' 41 42 42 43 export let show = true 43 - export let parent_path: string | null 44 + export let parent_id: string | null 44 45 export let prevent_drop = false 45 - type Child = TrackListDetails & { path: string } 46 + type Child = TrackListDetails 46 47 export let children: Child[] 47 48 48 49 export let level = 0 ··· 72 73 } 73 74 function select_up(i: number) { 74 75 const prev = children[i - 1] || null 75 - if (i === 0 && parent_path) { 76 - navigate(parent_path) 76 + if (i === 0 && parent_id) { 77 + navigate(`/playlist/${parent_id}`) 77 78 } else if (prev && has_showing_children(prev.id)) { 78 79 select_last(prev.id) 79 80 } else if (prev) { 80 - navigate(prev.path) 81 + navigate(`/playlist/${prev.id}`) 81 82 } 82 83 } 83 84 function select_down(i: number) { 84 85 if (has_showing_children(children[i].id)) { 85 86 select_first(children[i]) 86 87 } else if (children[i + 1]) { 87 - navigate(children[i + 1].path) 88 + navigate(`/playlist/${children[i + 1].id}`) 88 89 } else { 89 90 on_select_down() 90 91 } ··· 92 93 93 94 export function handle_key(e: KeyboardEvent) { 94 95 const index = children.findIndex((child) => { 95 - return child.path === $url_pathname 96 + return `/playlist/${child.id}` === $url_pathname 96 97 }) 97 98 if (index < 0) { 98 99 return ··· 110 111 } else if (check_shortcut(e, 'ArrowLeft')) { 111 112 if (selected_list.kind === 'folder' && $shown_folders.includes(selected_list.id)) { 112 113 hide_folder(selected_list.id) 113 - } else if (parent_path) { 114 - navigate(parent_path) 114 + } else if (parent_id) { 115 + navigate(`/playlist/${parent_id}`) 115 116 } 116 117 } else if (check_shortcut(e, 'ArrowRight') && selected_list.kind === 'folder') { 117 118 show_folder(selected_list.id) ··· 120 121 } 121 122 e.preventDefault() 122 123 } 123 - $: if (children.find((child) => child.path === $url_pathname)) { 124 + $: if (children.find((child) => `/playlist/${child.id}` === $url_pathname)) { 124 125 const item_handle = getContext<Writable<SidebarItemHandle | null>>('itemHandle') 125 126 item_handle.set({ handleKey: handle_key }) 126 127 } ··· 130 131 let drag_playlist_onto_index = null as number | null 131 132 132 133 function on_drag_start(e: DragEvent, tracklist: TrackListDetails) { 133 - if (e.dataTransfer && tracklist.kind !== 'special' && parent_path) { 134 + if (e.dataTransfer && tracklist.kind !== 'special' && parent_id) { 135 + e.dataTransfer.clearData() 134 136 e.dataTransfer.effectAllowed = 'move' 135 137 dragGhost.set_inner_text(tracklist.name) 136 138 dragged.playlist = { 137 139 id: tracklist.id, 138 - from_folder: parent_path, 140 + from_folder: parent_id, 139 141 level, 140 142 } 141 143 e.dataTransfer.setDragImage(dragGhost.drag_el, 0, 0) ··· 144 146 } 145 147 </script> 146 148 147 - <div class="sub" class:show> 149 + <!-- Don't propagate mousedown, to prevent dragging from breaking --> 150 + <!-- svelte-ignore a11y_no_static_element_interactions --> 151 + <div 152 + class="sub" 153 + class:show 154 + on:mousedown|stopPropagation={() => { 155 + requestAnimationFrame(() => { 156 + tracklist_actions.focus() 157 + }) 158 + }} 159 + > 148 160 {#each children as child_list, i} 149 161 {#if child_list.kind === 'folder'} 150 162 <a ··· 152 164 tabindex="-1" 153 165 class="item rounded-r-[5px] outline-none" 154 166 style:padding-left={14 * level + 'px'} 155 - class:active={child_list.path === $url_pathname} 167 + class:active={`/playlist/${child_list.id}` === $url_pathname} 156 168 draggable="true" 157 169 on:dragstart={(e) => on_drag_start(e, child_list)} 158 170 class:show={$shown_folders.includes(child_list.id)} ··· 175 187 drag_playlist_onto_index = null 176 188 } 177 189 }} 178 - on:mousedown={() => navigate(child_list.path)} 190 + on:mousedown={() => navigate(`/playlist/${child_list.id}`)} 179 191 on:contextmenu={() => tracklist_context_menu(child_list.id, true)} 180 192 > 181 - <!-- svelte-ignore a11y-click-events-have-key-events --> 182 193 <!-- svelte-ignore a11y-interactive-supports-focus --> 183 194 <svg 184 195 class="arrow" 185 196 role="button" 186 197 aria-label="Arrow button" 187 - on:mousedown|stopPropagation 188 - on:click={() => { 198 + on:mousedown|stopPropagation={() => { 189 199 if ($shown_folders.includes(child_list.id)) { 190 200 hide_folder(child_list.id) 191 201 } else { ··· 224 234 </a> 225 235 <Self 226 236 show={$shown_folders.includes(child_list.id)} 227 - parent_path={child_list.path} 228 - children={child_list.children?.map((child_id) => ({ 229 - path: '/playlist/' + child_id, 230 - ...$track_lists_details_map[child_id], 231 - })) || []} 237 + parent_id={child_list.id} 238 + children={($track_lists_details_map[child_list.id].children || []).map( 239 + (child_id) => $track_lists_details_map[child_id], 240 + )} 232 241 level={level + 1} 233 242 prevent_drop={prevent_drop || dragged.playlist?.id === child_list.id} 234 243 on_select_down={() => { 235 244 if (i < children.length - 1) { 236 - navigate(children[i + 1].path) 245 + navigate(`/playlist/${children[i + 1].id}`) 237 246 } else { 238 247 on_select_down() 239 248 } ··· 249 258 style:padding-left={14 * level + 'px'} 250 259 draggable="true" 251 260 on:dragstart={(e) => on_drag_start(e, child_list)} 252 - class:active={child_list.path === $url_pathname} 253 - on:mousedown={() => navigate(child_list.path)} 261 + class:active={`/playlist/${child_list.id}` === $url_pathname} 262 + on:mousedown={() => navigate(`/playlist/${child_list.id}`)} 254 263 class:droppable={drag_track_onto_index === i} 255 264 class:droppable-above={drag_playlist_onto_index === i && drop_above} 256 265 class:droppable-below={drag_playlist_onto_index === i && !drop_above} ··· 263 272 e.dataTransfer?.types[0] === 'ferrum.playlist' && 264 273 dragged.playlist && 265 274 !prevent_drop && 266 - parent_path !== null 275 + parent_id !== null 267 276 ) { 268 277 const rect = e.currentTarget.getBoundingClientRect() 269 278 drop_above = e.pageY < rect.bottom - rect.height / 2 270 279 move_playlist( 271 280 dragged.playlist.id, 272 281 dragged.playlist.from_folder, 273 - parent_path, 282 + parent_id, 274 283 drop_above ? i : i + 1, 275 284 ) 276 285 drag_playlist_onto_index = null ··· 312 321 tabindex="-1" 313 322 class="item rounded-r-[5px]" 314 323 style:padding-left={14 * level + 'px'} 315 - on:mousedown={() => navigate(child_list.path)} 316 - class:active={child_list.path === $url_pathname} 324 + on:mousedown|preventDefault={() => navigate(`/playlist/${child_list.id}`)} 325 + class:active={`/playlist/${child_list.id}` === $url_pathname} 317 326 > 318 327 <div class="arrow"></div> 319 328 <div class="text">