[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 column widths

Kasper (May 27, 2025, 9:38 AM +0200) 955bd2ba 0b7bcb19

+83 -68
+83 -68
src/components/TrackList.svelte
··· 261 261 262 262 const all_columns: ColumnRegular[] = [ 263 263 // sorted alphabetically 264 - { name: '#', prop: 'index' }, 264 + { name: '#', prop: 'index', width_px: 46 }, 265 265 // { name: 'Size', prop: 'size' }, 266 - { name: 'Album', prop: 'albumName', width: '90%' }, 267 - { name: 'Album Artist', prop: 'albumArtist', width: '90%' }, 268 - { name: 'Artist', prop: 'artist', width: '120%' }, 266 + { name: 'Album', prop: 'albumName', width_pct: 0.9 }, 267 + { name: 'Album Artist', prop: 'albumArtist', width_pct: 0.9 }, 268 + { name: 'Artist', prop: 'artist', width_pct: 1.2 }, 269 269 // { name: 'Bitrate', prop: 'bitrate' }, 270 - { name: 'BPM', prop: 'bpm' }, 271 - { name: 'Comments', prop: 'comments', width: '65%' }, 270 + { name: 'BPM', prop: 'bpm', width_px: 43 }, 271 + { name: 'Comments', prop: 'comments', width_pct: 0.65 }, 272 272 // { name: 'Compilation', prop: 'compilation' }, 273 - { name: 'Composer', prop: 'composer', width: '65%' }, 274 - { name: 'Date Added', prop: 'dateAdded' }, 273 + { name: 'Composer', prop: 'composer', width_pct: 0.65 }, 274 + { name: 'Date Added', prop: 'dateAdded', width_px: 140 }, 275 275 // { name: 'DateImported', prop: 'dateImported' }, 276 276 // { name: 'DateModified', prop: 'dateModified' }, 277 277 // { name: 'Disabled', prop: 'disabled' }, 278 278 // { name: 'DiscCount', prop: 'discCount' }, 279 279 // { name: 'DiscNum', prop: 'discNum' }, 280 280 // { name: 'Disliked', prop: 'disliked' }, 281 - { name: 'Time', prop: 'duration' }, 282 - { name: 'Genre', prop: 'genre', width: '65%' }, 283 - { name: 'Grouping', prop: 'grouping', width: '65%' }, 284 - { name: 'Image', prop: 'image' }, 281 + { name: 'Time', prop: 'duration', width_px: 50 }, 282 + { name: 'Genre', prop: 'genre', width_pct: 0.65 }, 283 + { name: 'Grouping', prop: 'grouping', width_pct: 0.65 }, 284 + { name: 'Image', prop: 'image', width_px: 28 }, 285 285 // { name: 'ImportedFrom', prop: 'importedFrom' }, 286 286 // { name: 'Liked', prop: 'liked' }, 287 - { name: 'Name', prop: 'name', width: '170%' }, 288 - { name: 'Plays', prop: 'playCount' }, 287 + { name: 'Name', prop: 'name', width_pct: 1.7 }, 288 + { name: 'Plays', prop: 'playCount', width_px: 52 }, 289 289 // { name: 'Rating', prop: 'rating' }, 290 290 // { name: 'SampleRate', prop: 'sampleRate' }, 291 - { name: 'Skips', prop: 'skipCount' }, 292 - // { name: 'Sort Album', prop: 'sortAlbumName', width: '65%' }, 293 - // { name: 'Sort Album Artist', prop: 'sortAlbumArtist', width: '65%' }, 294 - // { name: 'Sort Artist', prop: 'sortArtist', width: '65%' }, 295 - // { name: 'Sort Composer', prop: 'sortComposer', width: '65%' }, 296 - // { name: 'Sort Name', prop: 'sortName', width: '65%' }, 291 + { name: 'Skips', prop: 'skipCount', width_px: 52 }, 292 + // { name: 'Sort Album', prop: 'sortAlbumName', width_pct: 0.65 }, 293 + // { name: 'Sort Album Artist', prop: 'sortAlbumArtist', width_pct: 0.65 }, 294 + // { name: 'Sort Artist', prop: 'sortArtist', width_pct: 0.65 }, 295 + // { name: 'Sort Composer', prop: 'sortComposer', width_pct: 0.65 }, 296 + // { name: 'Sort Name', prop: 'sortName', width_pct: 0.65 }, 297 297 // { name: 'TrackCount', prop: 'trackCount' }, 298 298 // { name: 'TrackNum', prop: 'trackNum' }, 299 299 // { name: 'Volume', prop: 'volume' }, 300 - { name: 'Year', prop: 'year' }, 300 + { name: 'Year', prop: 'year', width_px: 47 }, 301 301 ] 302 302 const default_columns: ('index' | 'image' | keyof Track)[] = [ 303 303 'index', ··· 312 312 'dateAdded', 313 313 'year', 314 314 ] 315 + let container_el: HTMLElement 315 316 let columns: ColumnRegular[] = load_columns() 317 + onMount(() => (columns = load_columns())) 316 318 function load_columns(): ColumnRegular[] { 317 319 let loaded_columns = view_options.columns 318 320 if (loaded_columns.length === 0) { ··· 321 323 const columns = loaded_columns 322 324 .map((key) => all_columns.find((col) => col.prop === key)) 323 325 .filter((col) => col !== undefined) 326 + const total_fixed_width = columns.reduce((sum, col) => sum + (col.width_px || 0), 0) 327 + const total_percent_pct = columns.reduce((sum, col) => sum + (col.width_pct || 0), 0) 328 + const container_width = container_el?.clientWidth ?? total_fixed_width 329 + const total_percent_width = container_width - total_fixed_width 324 330 return columns.map((col) => { 331 + const size_pct = col.width_pct / total_percent_pct 332 + const size = (col.width_px ?? size_pct * total_percent_width) || 0 325 333 return { 326 334 ...col, 327 - cellTemplate(create_element, props, additional_data) { 328 - return create_element('div', {}, props.model[props.prop]) 335 + name: col.name === 'Image' ? '' : col.name, 336 + size, 337 + columnProperties() { 338 + const classes: Record<string, boolean> = {} 339 + classes[col.prop] = true 340 + return { 341 + class: classes, 342 + } 343 + }, 344 + cellProperties() { 345 + const classes: Record<string, boolean> = {} 346 + classes[col.prop] = true 347 + return { 348 + class: classes, 349 + } 329 350 }, 330 351 } 331 352 }) ··· 338 359 save_view_options(view_options) 339 360 } 340 361 // function on_column_context_menu() { 341 - // ipc_renderer.invoke('show_columns_menu', { 362 + // ipc_renderer.invoke(show_columns_menu', { 342 363 // menu: all_columns.map((col) => { 343 364 // return { 344 365 // id: col.key, ··· 368 389 const grid = document.createElement('revo-grid') 369 390 grid.setAttribute('theme', 'darkCompact') 370 391 grid.readonly = true 371 - grid.columns = load_columns() 392 + $: grid.columns = columns 372 393 grid.rowSize = 24 373 394 grid.style.lineHeight = '24px' 374 395 const tracks = tracks_page.itemIds 375 396 .map((item_id, i) => { 397 + const track = get_item(item_id).track 398 + if (track === null) return null 376 399 return { 377 - ...get_item(item_id).track, 400 + ...track, 378 401 index: i + 1, 379 402 odd: i % 2 === 0 ? 'odd' : null, 380 403 } ··· 434 457 subtitle="{tracks_page.itemIds.length} songs" 435 458 description={tracks_page.playlistDescription} 436 459 /> 460 + <svelte:window 461 + on:resize={() => { 462 + columns = load_columns() 463 + }} 464 + /> 437 465 <div 466 + bind:this={container_el} 438 467 class="grow" 439 468 {@attach (element) => { 440 469 element.appendChild(grid) ··· 562 591 </div> --> 563 592 564 593 <style lang="sass"> 565 - :global(revo-grid[theme=darkCompact]) 566 - --revo-grid-focused-bg: hsla(var(--hue), 70%, 46%, 1) 567 - :global(revogr-header .header-rgRow) 568 - height: 24px 569 - line-height: 24px 570 - font-size: 12px 571 - box-shadow: none 572 - font-weight: 400 573 - :global(revogr-data .rgRow) 574 - line-height: 24px 575 - font-size: 12px 576 - box-shadow: none 577 - :global(.rgRow.odd) 578 - background-color: hsla(0, 0%, 90%, 0.06) 579 - :global(revogr-header .rgHeaderCell .resizable) 580 - display: none 594 + :global revo-grid[theme=darkCompact] 595 + --revo-grid-focused-bg: hsla(var(--hue), 70%, 46%, 1) 596 + revogr-data, revogr-header 597 + .rgCell, .rgHeaderCell 598 + padding: 0 5px 599 + &:first-child 600 + padding-left: 10px 601 + &:last-child 602 + padding-right: 0px 603 + &.index, &.playCount, &.skipCount, &.duration 604 + padding-left: 0px 605 + padding-right: 10px 606 + text-align: right 607 + flex-shrink: 0 608 + .rgRow 609 + line-height: 24px 610 + font-size: 12px 611 + box-shadow: none 612 + .rgRow.odd 613 + background-color: hsla(0, 0%, 90%, 0.06) 614 + revogr-header .header-rgRow 615 + height: 24px 616 + line-height: 24px 617 + font-size: 12px 618 + box-shadow: none 619 + font-weight: 400 620 + revogr-header .rgHeaderCell .resizable 621 + display: none 581 622 // .selected 582 623 // background-color: hsla(var(--hue), 20%, 42%, 0.8) 583 624 // :global(:focus) ··· 626 667 // text-overflow: ellipsis 627 668 // padding-left: 5px 628 669 // padding-right: 5px 629 - // &:first-child 630 - // padding-left: 10px 631 - // &:last-child 632 - // padding-right: 0px 633 - // &.index, &.playCount, &.skipCount, &.duration 634 - // padding-left: 0px 635 - // padding-right: 10px 636 - // text-align: right 637 - // flex-shrink: 0 638 670 // .selected .index svg.playing-icon 639 671 // fill: var(--icon-color) 640 672 // .index 641 - // width: 46px 642 673 // svg.playing-icon 643 674 // fill: #00ffff 644 675 // width: 16px 645 676 // height: 100% 646 - // .image 647 - // width: 18px 648 - // flex-shrink: 0 649 - // box-sizing: content-box 650 - // .playCount, .skipCount 651 - // width: 52px 652 - // .duration 653 - // width: 50px 654 677 // .dateAdded 655 - // width: 140px 656 - // flex-shrink: 0 657 678 // font-variant-numeric: tabular-nums 658 - // .year 659 - // width: 0px 660 - // min-width: 47px 661 - // .bpm 662 - // width: 0px 663 - // min-width: 43px 664 679 // .drag-line 665 680 // position: absolute 666 681 // width: 100%