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

Make columns hideable

Kasper (Sep 18, 2024, 6:31 AM +0200) f8be3c31 60a08d76

+196 -185
+176 -183
src/components/TrackList.svelte
··· 23 23 import * as dragGhost from './DragGhost.svelte' 24 24 import VirtualListBlock, { scroll_container_keydown } from './VirtualListBlock.svelte' 25 25 import { open_track_info } from './TrackInfo.svelte' 26 + import type { Track } from 'ferrum-addon/addon' 26 27 27 28 let tracklist_element: HTMLDivElement 28 29 export let params: { playlist_id: string } ··· 56 57 onDestroy(track_action_unlisten) 57 58 58 59 const sort_by = page.sort_by 59 - $: sortKey = $page.sortKey 60 + $: sort_key = $page.sortKey 60 61 61 62 ipc_renderer.on('Group Album Tracks', (_, checked) => { 62 63 page.set_group_album_tracks(checked) ··· 200 201 onMount(() => { 201 202 tracklist_actions.scroll_to_index = virtual_list.scroll_to_index 202 203 }) 204 + 205 + type Column = { 206 + name: string 207 + key: 'index' | keyof Track 208 + width?: string 209 + } 210 + const all_columns: Column[] = [ 211 + // sorted alphabetically 212 + { name: '#', key: 'index' }, 213 + // { name: 'Size', key: 'size' }, 214 + { name: 'Album', key: 'albumName', width: '90%' }, 215 + // { name: 'Album Artist', key: 'albumArtist', width: '90%' }, 216 + { name: 'Artist', key: 'artist', width: '120%' }, 217 + // { name: 'Bitrate', key: 'bitrate' }, 218 + // { name: 'Bpm', key: 'bpm' }, 219 + { name: 'Comments', key: 'comments', width: '65%' }, 220 + // { name: 'Compilation', key: 'compilation' }, 221 + // { name: 'Composer', key: 'composer' }, 222 + { name: 'Date Added', key: 'dateAdded' }, 223 + // { name: 'DateImported', key: 'dateImported' }, 224 + // { name: 'DateModified', key: 'dateModified' }, 225 + // { name: 'Disabled', key: 'disabled' }, 226 + // { name: 'DiscCount', key: 'discCount' }, 227 + // { name: 'DiscNum', key: 'discNum' }, 228 + // { name: 'Disliked', key: 'disliked' }, 229 + { name: 'Time', key: 'duration' }, 230 + { name: 'Genre', key: 'genre', width: '65%' }, 231 + // { name: 'Grouping', key: 'grouping', width: '65%' }, 232 + // { name: 'ImportedFrom', key: 'importedFrom' }, 233 + // { name: 'Liked', key: 'liked' }, 234 + { name: 'Name', key: 'name', width: '170%' }, 235 + { name: 'Plays', key: 'playCount' }, 236 + // { name: 'Rating', key: 'rating' }, 237 + // { name: 'SampleRate', key: 'sampleRate' }, 238 + // { name: 'Skips', key: 'skipCount' }, 239 + // { name: 'SortAlbumArtist', key: 'sortAlbumArtist' }, 240 + // { name: 'SortAlbumName', key: 'sortAlbumName' }, 241 + // { name: 'SortArtist', key: 'sortArtist' }, 242 + // { name: 'SortComposer', key: 'sortComposer' }, 243 + // { name: 'SortName', key: 'sortName' }, 244 + // { name: 'TrackCount', key: 'trackCount' }, 245 + // { name: 'TrackNum', key: 'trackNum' }, 246 + // { name: 'Volume', key: 'volume' }, 247 + { name: 'Year', key: 'year' }, 248 + ] 249 + let columns: Column[] = [ 250 + { name: '#', key: 'index' }, 251 + { name: 'Name', key: 'name', width: '170%' }, 252 + { name: 'Plays', key: 'playCount' }, 253 + { name: 'Time', key: 'duration' }, 254 + { name: 'Artist', key: 'artist', width: '120%' }, 255 + { name: 'Album', key: 'albumName', width: '90%' }, 256 + { name: 'Comments', key: 'comments', width: '65%' }, 257 + { name: 'Genre', key: 'genre', width: '65%' }, 258 + { name: 'Date Added', key: 'dateAdded' }, 259 + { name: 'Year', key: 'year' }, 260 + ] 261 + function on_column_context_menu() { 262 + ipc_renderer.invoke('show_columns_menu', { 263 + menu: all_columns.map((col) => { 264 + return { 265 + id: col.key, 266 + label: col.name, 267 + type: 'checkbox', 268 + checked: !!columns.find((c) => c.key === col.key), 269 + } 270 + }), 271 + }) 272 + } 273 + onDestroy( 274 + ipc_listen('context.toggle_column', (_, item) => { 275 + if (item.checked) { 276 + const column = all_columns.find((column) => column.key === item.id) 277 + if (column) { 278 + columns.push({ ...column }) 279 + columns = columns 280 + } 281 + } else { 282 + columns = columns.filter((column) => column.key !== item.id) 283 + } 284 + }), 285 + ) 203 286 </script> 204 287 205 288 <div ··· 209 292 on:dragleave={() => (drag_to_index = null)} 210 293 class:no-selection={$selection.count === 0} 211 294 > 295 + <!-- svelte-ignore a11y-interactive-supports-focus --> 212 296 <div 213 297 class="row table-header border-b border-b-slate-500/30" 214 298 class:desc={$page.sortDesc} 215 299 role="row" 300 + on:contextmenu={on_column_context_menu} 216 301 > 217 - <!-- svelte-ignore a11y-interactive-supports-focus --> 218 - <!-- svelte-ignore a11y-click-events-have-key-events --> 219 - <div 220 - class="c index" 221 - class:sort={sortKey === 'index'} 222 - on:click={() => sort_by('index')} 223 - role="button" 224 - > 225 - <span>#</span> 226 - </div> 227 - <!-- svelte-ignore a11y-interactive-supports-focus --> 228 - <!-- svelte-ignore a11y-click-events-have-key-events --> 229 - <div 230 - class="c name" 231 - class:sort={sortKey === 'name'} 232 - on:click={() => sort_by('name')} 233 - role="button" 234 - > 235 - <span>Name</span> 236 - </div> 237 - <!-- svelte-ignore a11y-interactive-supports-focus --> 238 - <!-- svelte-ignore a11y-click-events-have-key-events --> 239 - <div 240 - class="c playCount" 241 - class:sort={sortKey === 'playCount'} 242 - on:click={() => sort_by('playCount')} 243 - role="button" 244 - > 245 - <span>Plays</span> 246 - </div> 247 - <!-- svelte-ignore a11y-interactive-supports-focus --> 248 - <!-- svelte-ignore a11y-click-events-have-key-events --> 249 - <div 250 - class="c duration" 251 - class:sort={sortKey === 'duration'} 252 - on:click={() => sort_by('duration')} 253 - role="button" 254 - > 255 - <span>Time</span> 256 - </div> 257 - <!-- svelte-ignore a11y-interactive-supports-focus --> 258 - <!-- svelte-ignore a11y-click-events-have-key-events --> 259 - <div 260 - class="c artist" 261 - class:sort={sortKey === 'artist'} 262 - on:click={() => sort_by('artist')} 263 - role="button" 264 - > 265 - <span>Artist</span> 266 - </div> 267 - <!-- svelte-ignore a11y-interactive-supports-focus --> 268 - <!-- svelte-ignore a11y-click-events-have-key-events --> 269 - <div 270 - class="c albumName" 271 - class:sort={sortKey === 'albumName'} 272 - on:click={() => sort_by('albumName')} 273 - role="button" 274 - > 275 - <span>Album</span> 276 - </div> 277 - <!-- svelte-ignore a11y-interactive-supports-focus --> 278 - <!-- svelte-ignore a11y-click-events-have-key-events --> 279 - <div 280 - class="c comments" 281 - class:sort={sortKey === 'comments'} 282 - on:click={() => sort_by('comments')} 283 - role="button" 284 - > 285 - <span>Comments</span> 286 - </div> 287 - <!-- svelte-ignore a11y-interactive-supports-focus --> 288 - <!-- svelte-ignore a11y-click-events-have-key-events --> 289 - <div 290 - class="c genre" 291 - class:sort={sortKey === 'genre'} 292 - on:click={() => sort_by('genre')} 293 - role="button" 294 - > 295 - <span>Genre</span> 296 - </div> 297 - <!-- svelte-ignore a11y-interactive-supports-focus --> 298 - <!-- svelte-ignore a11y-click-events-have-key-events --> 299 - <div 300 - class="c dateAdded" 301 - class:sort={sortKey === 'dateAdded'} 302 - on:click={() => sort_by('dateAdded')} 303 - role="button" 304 - > 305 - <span>Date Added</span> 306 - </div> 307 - <!-- svelte-ignore a11y-interactive-supports-focus --> 308 - <!-- svelte-ignore a11y-click-events-have-key-events --> 309 - <div 310 - class="c year" 311 - class:sort={sortKey === 'year'} 312 - on:click={() => sort_by('year')} 313 - role="button" 314 - > 315 - <span>Year</span> 316 - </div> 302 + {#each columns as column} 303 + <!-- svelte-ignore a11y-interactive-supports-focus --> 304 + <!-- svelte-ignore a11y-click-events-have-key-events --> 305 + <div 306 + class="c {column.key}" 307 + class:sort={sort_key === column.key} 308 + style:width={column.width} 309 + on:click={() => sort_by(column.key)} 310 + role="button" 311 + > 312 + <span>{column.name}</span> 313 + </div> 314 + {/each} 317 315 </div> 318 316 <!-- svelte-ignore a11y-no-noninteractive-tabindex --> 319 317 <!-- svelte-ignore a11y-no-static-element-interactions --> ··· 353 351 class:selected={$selection.list[i] === true} 354 352 class:playing={track.id === $playing_id} 355 353 > 356 - <div class="c index"> 357 - {#if track.id === $playing_id} 358 - <svg 359 - class="playing-icon inline" 360 - xmlns="http://www.w3.org/2000/svg" 361 - height="24" 362 - viewBox="0 0 24 24" 363 - width="24" 364 - > 365 - <path d="M0 0h24v24H0z" fill="none" /> 366 - <path 367 - d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z" 368 - /> 369 - </svg> 370 - {:else} 371 - {i + 1} 372 - {/if} 373 - </div> 374 - <div class="c name">{track.name}</div> 375 - <div class="c playCount">{track.playCount || ''}</div> 376 - <div class="c duration">{track.duration ? get_duration(track.duration) : ''}</div> 377 - <div class="c artist">{track.artist}</div> 378 - <div class="c albumName">{track.albumName || ''}</div> 379 - <div class="c comments">{track.comments || ''}</div> 380 - <div class="c genre">{track.genre || ''}</div> 381 - <div class="c dateAdded">{format_date(track.dateAdded)}</div> 382 - <div class="c year">{track.year || ''}</div> 354 + {#each columns as column} 355 + <div class="c {column.key}" style:width={column.width}> 356 + {#if column.key === 'index'} 357 + {#if track.id === $playing_id} 358 + <svg 359 + class="playing-icon inline" 360 + xmlns="http://www.w3.org/2000/svg" 361 + height="24" 362 + viewBox="0 0 24 24" 363 + width="24" 364 + > 365 + <path d="M0 0h24v24H0z" fill="none" /> 366 + <path 367 + d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z" 368 + /> 369 + </svg> 370 + {:else} 371 + {i + 1} 372 + {/if} 373 + {:else if column.key === 'duration'} 374 + {track.duration ? get_duration(track.duration) : ''} 375 + {:else if column.key === 'dateAdded'} 376 + {format_date(track.dateAdded)} 377 + {:else} 378 + {track[column.key] || ''} 379 + {/if} 380 + </div> 381 + {/each} 383 382 </div> 384 383 {/if} 385 384 </VirtualListBlock> ··· 416 415 display: inline-block 417 416 &.desc .c.sort span::after 418 417 content: '▼' 419 - .row 420 - display: flex 421 - max-width: 100% 422 - $row-height: 24px 423 - height: $row-height 424 - font-size: 12px 425 - line-height: $row-height 418 + .row 419 + display: flex 420 + max-width: 100% 421 + $row-height: 24px 422 + height: $row-height 423 + font-size: 12px 424 + line-height: $row-height 425 + box-sizing: border-box 426 + position: relative 427 + &.playing.selected 428 + color: #ffffff 429 + &.playing 430 + color: #00ffff 431 + .c 432 + display: inline-block 433 + vertical-align: top 434 + width: 100% 435 + white-space: nowrap 436 + overflow: hidden 437 + text-overflow: ellipsis 438 + padding-right: 10px 439 + &:first-child 440 + padding-left: 10px 441 + box-sizing: content-box 442 + &.index, &.playCount, &.skipCount, &.duration 443 + padding-left: 0px 444 + text-align: right 426 445 box-sizing: border-box 427 - position: relative 428 - &.playing.selected 429 - color: #ffffff 430 - &.playing 431 - color: #00ffff 432 - .c 433 - display: inline-block 434 - vertical-align: top 435 - width: 100% 436 - white-space: nowrap 437 - overflow: hidden 438 - text-overflow: ellipsis 439 - padding-right: 10px 440 - &.selected .index svg.playing-icon 441 - fill: var(--icon-color) 442 - .index 443 - width: 0px 444 - min-width: 46px 445 - text-align: right 446 - svg.playing-icon 447 - fill: #00ffff 448 - width: 16px 449 - height: 100% 450 - .name 451 - width: 170% 452 - .playCount 453 - width: 0px 454 - min-width: 52px 455 - text-align: right 456 - .duration 457 - width: 0px 458 - min-width: 50px 459 - text-align: right 460 - .artist 461 - width: 120% 462 - .albumName 463 - width: 90% 464 - .genre 465 - width: 65% 466 - .comments 467 - width: 65% 468 - .dateAdded 469 - width: 130px 470 - min-width: 140px 471 - font-variant-numeric: tabular-nums 472 - .year 473 - width: 0px 474 - min-width: 47px 446 + .selected .index svg.playing-icon 447 + fill: var(--icon-color) 448 + .index 449 + width: 46px 450 + flex-shrink: 0 451 + svg.playing-icon 452 + fill: #00ffff 453 + width: 16px 454 + height: 100% 455 + .playCount, .skipCount 456 + width: 52px 457 + flex-shrink: 0 458 + .duration 459 + width: 50px 460 + flex-shrink: 0 461 + .dateAdded 462 + flex-shrink: 0 463 + width: 140px 464 + font-variant-numeric: tabular-nums 465 + .year 466 + width: 0px 467 + min-width: 47px 475 468 .drag-line 476 469 position: absolute 477 470 width: 100%
+16
src/electron/ipc.ts
··· 127 127 const menu = Menu.buildFromTemplate(menu_items) 128 128 menu.popup() 129 129 }) 130 + 131 + ipc_main.handle('show_columns_menu', (e, args) => { 132 + const menu = Menu.buildFromTemplate( 133 + args.menu.map((item) => { 134 + item.click = (item) => { 135 + e.sender.send('context.toggle_column', { 136 + id: item.id, 137 + label: item.label, 138 + checked: item.checked, 139 + }) 140 + } 141 + return item 142 + }), 143 + ) 144 + menu.popup() 145 + })
+3 -1
src/electron/typed_ipc.ts
··· 8 8 dialog, 9 9 } from 'electron' 10 10 import { ipcMain as electronIpcMain } from 'electron' 11 - import type { TrackID } from '../../ferrum-addon' 11 + import type { Track, TrackID } from '../../ferrum-addon' 12 12 13 13 type OptionalPromise<T> = T | Promise<T> 14 14 type InputMap = { ··· 133 133 'context.revealTrackFile': (id: TrackID) => void 134 134 'context.Get Info': (allIds: TrackID[], selectedIndex: number) => void 135 135 'context.Remove from Playlist': (selectedIndexes: number[]) => void 136 + 'context.toggle_column': (item: { id: string; label: string; checked: boolean }) => void 136 137 } 137 138 138 139 export type ShowTrackMenuOptions = { ··· 155 156 revealTrackFile: (...paths: string[]) => void 156 157 showTrackMenu: (options: ShowTrackMenuOptions) => void 157 158 showTracklistMenu: (options: { id: string; isFolder: boolean; isRoot: boolean }) => void 159 + show_columns_menu: (options: { menu: Electron.MenuItemConstructorOptions[] }) => void 158 160 volume_change: (up: boolean) => void 159 161 160 162 'update:Shuffle': (checked: boolean) => void
+1 -1
src/lib/data.ts
··· 238 238 methods.save() 239 239 }, 240 240 loadTags: (id: TrackID) => { 241 - call((data) => data.load_tags(id)) 241 + return call((data) => data.load_tags(id)) 242 242 }, 243 243 getImage: (index: number) => { 244 244 return call((data) => data.get_image(index))