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

Merge pull request #7 from probablykasper/artists-page

authored by

Kasper and committed by
GitHub
(Sep 15, 2024, 6:35 PM +0200) 6f0ff714 310372c1

+283 -143
+7 -1
ferrum-addon/addon.d.ts
··· 3 3 4 4 /* auto-generated by NAPI-RS */ 5 5 6 + export declare function get_artists(): Array<string> 6 7 export declare function load_data(isDev: boolean, libraryPath?: string | undefined | null): void 7 8 export interface PathsJs { 8 9 libraryDir: string ··· 105 106 export const enum SpecialTrackListName { 106 107 Root = 0 107 108 } 108 - export declare function open_playlist(openPlaylistId: string): void 109 + export declare function open_playlist(openPlaylistId: string, viewAs?: ViewAs | undefined | null): void 109 110 export declare function get_page_track(index: number): Track 110 111 export declare function get_page_track_id(index: number): string 111 112 export declare function refresh_page(): void 112 113 export declare function get_page_track_ids(): Array<TrackID> 114 + export const enum ViewAs { 115 + Songs = 0, 116 + Artists = 1 117 + } 113 118 export interface PageInfo { 114 119 id: string 120 + viewAs: ViewAs 115 121 tracklist: TrackList 116 122 sortKey: string 117 123 sortDesc: boolean
+27
src-native/artists.rs
··· 1 + use crate::data::Data; 2 + use crate::data_js::get_data; 3 + use crate::library_types::Library; 4 + use napi::{Env, Result}; 5 + use std::collections::HashSet; 6 + use std::time::Instant; 7 + 8 + pub fn load_artists(library: &Library) -> HashSet<String> { 9 + let now = Instant::now(); 10 + let mut artists = HashSet::new(); 11 + for (_, track) in &library.tracks { 12 + if !artists.contains(&track.artist) { 13 + artists.insert(track.artist.clone()); 14 + } 15 + } 16 + println!("Get artists: {}ms", now.elapsed().as_millis()); 17 + return artists; 18 + } 19 + 20 + #[napi(js_name = "get_artists")] 21 + #[allow(dead_code)] 22 + pub fn get_artists(env: Env) -> Result<Vec<String>> { 23 + let data: &mut Data = get_data(&env)?; 24 + let mut artists: Vec<String> = data.artists.clone().into_iter().collect(); 25 + artists.sort(); 26 + Ok(artists) 27 + }
+10 -2
src-native/data.rs
··· 1 + use crate::artists::load_artists; 1 2 use crate::library::{load_library, Paths}; 2 3 use crate::library_types::{Library, TrackID, TrackList, TrackListID}; 3 - use crate::page::get_track_ids; 4 + use crate::page::{get_track_ids, ViewAs}; 4 5 use crate::sidebar_view::SidebarView; 5 6 use crate::sort::sort; 6 7 use crate::tracks::Tag; ··· 8 9 use atomicwrites::{AllowOverwrite, AtomicFile}; 9 10 use napi::Result; 10 11 use serde::Serialize; 12 + use std::collections::HashSet; 11 13 use std::env; 12 14 use std::io::Write; 13 15 use std::path::PathBuf; ··· 23 25 /// The visible tracks on the current page 24 26 pub page_track_ids: Option<Vec<TrackID>>, 25 27 pub open_playlist_id: TrackListID, 28 + pub view_as: ViewAs, 26 29 pub filter: String, 27 30 pub sort_key: String, 28 31 pub sort_desc: bool, 29 32 pub group_album_tracks: bool, 30 33 /// Current tag being edited 31 34 pub current_tag: Option<Tag>, 35 + pub artists: HashSet<String>, 32 36 } 33 37 34 38 impl Data { ··· 99 103 100 104 let loaded_library = load_library(&paths)?; 101 105 let loaded_cache = SidebarView::load(&paths); 106 + let artists = load_artists(&loaded_library); 102 107 103 108 let mut data = Data { 104 109 is_dev, 105 110 paths, 106 111 library: loaded_library, 112 + artists, 107 113 view_cache: loaded_cache, 108 114 open_playlist_id: "root".to_string(), 109 115 open_playlist_track_ids: vec![], 116 + view_as: ViewAs::Songs, 110 117 page_track_ids: None, 111 118 filter: "".to_string(), 112 119 sort_key: "index".to_string(), ··· 118 125 sort(&mut data, "dateAdded", true)?; 119 126 return Ok(data); 120 127 } 121 - pub fn open_playlist(&mut self, playlist_id: TrackID) -> Result<()> { 128 + pub fn open_playlist(&mut self, playlist_id: TrackID, view_as: Option<ViewAs>) -> Result<()> { 122 129 self.open_playlist_id = playlist_id; 130 + self.view_as = view_as.unwrap_or_default(); 123 131 self.open_playlist_track_ids = get_track_ids(self)?; 124 132 self.page_track_ids = None; 125 133 match self.library.get_tracklist(&self.open_playlist_id)? {
+1
src-native/lib.rs
··· 20 20 #[macro_use] 21 21 extern crate napi_derive; 22 22 23 + mod artists; 23 24 mod data; 24 25 mod data_js; 25 26 mod filter;
+15 -2
src-native/page.rs
··· 11 11 12 12 #[napi(js_name = "open_playlist")] 13 13 #[allow(dead_code)] 14 - pub fn open_playlist(open_playlist_id: String, env: Env) -> Result<()> { 14 + pub fn open_playlist(open_playlist_id: String, view_as: Option<ViewAs>, env: Env) -> Result<()> { 15 15 let data: &mut Data = get_data(&env)?; 16 - data.open_playlist(open_playlist_id) 16 + data.open_playlist(open_playlist_id, view_as) 17 17 } 18 18 19 19 #[napi(js_name = "get_page_track")] ··· 97 97 Ok(ids) 98 98 } 99 99 100 + #[napi] 101 + pub enum ViewAs { 102 + Songs, 103 + Artists, 104 + } 105 + impl Default for ViewAs { 106 + fn default() -> Self { 107 + Self::Songs 108 + } 109 + } 110 + 100 111 #[napi(object)] 101 112 pub struct PageInfo { 102 113 pub id: String, 114 + pub view_as: ViewAs, 103 115 #[napi(ts_type = "TrackList")] 104 116 pub tracklist: JsUnknown, 105 117 pub sort_key: String, ··· 115 127 116 128 Ok(PageInfo { 117 129 id: data.open_playlist_id.clone(), 130 + view_as: data.view_as, 118 131 tracklist: env.to_js_value(tracklist)?, 119 132 sort_key: data.sort_key.clone(), 120 133 sort_desc: data.sort_desc,
+1 -1
src-native/playlists.rs
··· 135 135 data.library.trackLists.remove(id); 136 136 } 137 137 if ids.contains(&data.open_playlist_id) { 138 - data.open_playlist("root".to_string())?; 138 + data.open_playlist("root".to_string(), None)?; 139 139 } 140 140 Ok(()) 141 141 }
+53 -3
src/App.svelte
··· 1 1 <script lang="ts"> 2 - import { onDestroy } from 'svelte' 2 + import { onDestroy, onMount } from 'svelte' 3 3 import { fade } from 'svelte/transition' 4 4 import TrackList from './components/TrackList.svelte' 5 5 import Player from './components/Player.svelte' ··· 9 9 import PlaylistInfoModal from './components/PlaylistInfo.svelte' 10 10 import { queueVisible } from './lib/queue' 11 11 import { ipcListen, ipcRenderer } from '@/lib/window' 12 - import { importTracks, type PlaylistInfo, methods } from './lib/data' 12 + import { 13 + importTracks, 14 + type PlaylistInfo, 15 + methods, 16 + page, 17 + isMac, 18 + view_as_songs, 19 + view_as_artists, 20 + } from './lib/data' 13 21 import { playPause } from './lib/player' 14 22 import DragGhost from './components/DragGhost.svelte' 15 23 import ItunesImport from './components/ItunesImport.svelte' ··· 17 25 import { modalCount } from './components/Modal.svelte' 18 26 import QuickNav from './components/QuickNav.svelte' 19 27 import { checkShortcut } from './lib/helpers' 28 + import ArtistList from './components/ArtistList.svelte' 29 + import { tracklist_actions } from './lib/page' 20 30 21 31 ipcRenderer.emit('appLoaded') 22 32 ··· 160 170 } 161 171 }), 162 172 ) 173 + 174 + onMount(() => { 175 + tracklist_actions.focus() 176 + }) 163 177 </script> 164 178 165 179 <svelte:window on:keydown={keydown} /> ··· 184 198 > 185 199 <div class="meat"> 186 200 <Sidebar /> 187 - <TrackList {onTrackInfo} /> 201 + <div class="flex size-full min-w-0 flex-col"> 202 + <div class="relative pt-4 px-5 pb-5"> 203 + <div 204 + class="absolute top-0 left-0 h-10 w-full" 205 + class:dragbar={$modalCount === 0 && isMac} 206 + class:queue-visible={$queueVisible} 207 + /> 208 + <h3 class="m-0 pb-0.5 text-[19px] font-medium leading-none"> 209 + {#if $page.tracklist.id === 'root'} 210 + {#if $page.viewAs === view_as_songs} 211 + Songs 212 + <div class="text-[13px] leading-4 opacity-70">{$page.length} songs</div> 213 + {:else if $page.viewAs === view_as_artists} 214 + Artists 215 + <div class="text-[13px] leading-4 opacity-70"> 216 + {page.get_artists().length} artists 217 + </div> 218 + {/if} 219 + {:else if $page.tracklist.type !== 'special'} 220 + {$page.tracklist.name} 221 + <div class="text-[13px] leading-4 opacity-70">{$page.length} songs</div> 222 + {/if} 223 + </h3> 224 + {#if 'description' in $page.tracklist && $page.tracklist.description !== ''} 225 + <div class="mt-2.5 text-sm text-[13px] opacity-70">{$page.tracklist.description}</div> 226 + {/if} 227 + </div> 228 + {#if $page.viewAs === 0} 229 + <TrackList {onTrackInfo} /> 230 + {:else} 231 + <ArtistList /> 232 + {/if} 233 + </div> 188 234 {#if $queueVisible} 189 235 <Queue {onTrackInfo} /> 190 236 {/if} ··· 262 308 height: 100% 263 309 top: 0px 264 310 left: 0px 311 + .dragbar 312 + -webkit-app-region: drag 313 + &.queue-visible 314 + width: calc(100% - var(--right-sidebar-width)) 265 315 .drag-overlay 266 316 display: flex 267 317 align-items: center
+23
src/components/ArtistList.svelte
··· 1 + <script lang="ts"> 2 + import { filter, page } from '@/lib/data' 3 + import fuzzysort from 'fuzzysort' 4 + 5 + $: all_artists = page.get_artists() 6 + $: artists = fuzzysort.go($filter, all_artists, { all: true }) 7 + </script> 8 + 9 + <div class="w-full border-b border-b-slate-500/30"> 10 + <p class="px-3">(Work in progress)</p> 11 + </div> 12 + 13 + <div class="size-full overflow-y-auto text-sm"> 14 + {#each artists as artist} 15 + <p class="block py-1 px-3 text-current"> 16 + {#if artist.target} 17 + {artist.target} 18 + {:else} 19 + Unknown Artist 20 + {/if} 21 + </p> 22 + {/each} 23 + </div>
+23 -6
src/components/QuickNav.svelte
··· 3 3 import { checkShortcut } from '../lib/helpers' 4 4 import { ipcListen } from '@/lib/window' 5 5 import fuzzysort from 'fuzzysort' 6 - import { page, trackListsDetailsMap } from '@/lib/data' 7 - import type { TrackListDetails } from '../../ferrum-addon/addon' 6 + import { page, trackListsDetailsMap, view_as_artists, view_as_songs } from '@/lib/data' 7 + import type { TrackListDetails, ViewAs } from '../../ferrum-addon/addon' 8 8 import Modal from './Modal.svelte' 9 + import { special_playlists_nav } from './Sidebar.svelte' 10 + 11 + type Result = TrackListDetails & { view_as?: ViewAs } 9 12 10 13 let value = '' 11 - let playlists: TrackListDetails[] = [] 14 + let playlists: Result[] = [] 12 15 let show = false 13 16 $: if (show) { 14 - playlists = Object.values($trackListsDetailsMap).sort((a, b) => a.name.localeCompare(b.name)) 17 + playlists = get_playlists() 18 + } 19 + function get_playlists() { 20 + const playlists: Result[] = special_playlists_nav 21 + for (const playlist of Object.values($trackListsDetailsMap)) { 22 + if (playlist.kind === 'playlist' || playlist.kind === 'folder') { 23 + playlists.push(playlist) 24 + } 25 + } 26 + playlists.push({ id: 'root', name: 'Songs', kind: 'special', view_as: view_as_songs }) 27 + playlists.push({ id: 'root', name: 'Artists', kind: 'special', view_as: view_as_artists }) 28 + return playlists 15 29 } 16 30 17 31 let filteredItems = fuzzysort.go(value, playlists, { key: 'name', all: true }) ··· 39 53 show = false 40 54 value = '' 41 55 } else if (checkShortcut(e, 'Enter')) { 42 - page.openPlaylist(filteredItems[selectedIndex].obj.id) 56 + page.openPlaylist( 57 + filteredItems[selectedIndex].obj.id, 58 + filteredItems[selectedIndex].obj.view_as ?? view_as_songs, 59 + ) 43 60 show = false 44 61 } else if (checkShortcut(e, 'ArrowUp')) { 45 62 selectedIndex-- ··· 85 102 bind:this={listItems[i]} 86 103 type="button" 87 104 on:click={() => { 88 - page.openPlaylist(item.obj.id) 105 + page.openPlaylist(item.obj.id, item.obj.view_as ?? view_as_songs) 89 106 show = false 90 107 }} 91 108 class:selected={selectedIndex === i}
+30 -14
src/components/Sidebar.svelte
··· 1 + <script lang="ts" context="module"> 2 + export const special_playlists_nav = [ 3 + { id: 'root', name: 'Songs', kind: 'special', view_as: 0 }, 4 + { id: 'root', name: 'Artists', kind: 'special', view_as: 1 }, 5 + ] 6 + </script> 7 + 1 8 <script lang="ts"> 2 9 import SidebarItems, { type SidebarItemHandle } from './SidebarItems.svelte' 3 10 import Filter from './Filter.svelte' 4 - import { isMac, trackListsDetailsMap, page, movePlaylist } from '../lib/data' 11 + import { isMac, trackListsDetailsMap, page, movePlaylist, view_as_songs } from '../lib/data' 5 12 import { ipcListen, ipcRenderer } from '../lib/window' 6 13 import { writable } from 'svelte/store' 7 14 import { onDestroy, setContext, tick } from 'svelte' 8 15 import { dragged } from '../lib/drag-drop' 9 16 import { tracklist_actions } from '@/lib/page' 10 17 11 - const special = { 12 - children: ['root'], 13 - } 14 18 let viewport: HTMLElement 15 19 const itemHandle = setContext('itemHandle', writable(null as SidebarItemHandle | null)) 16 20 17 21 onDestroy( 18 22 ipcListen('Select Previous List', () => { 19 - console.log('Select Previous List') 20 23 $itemHandle?.handleKey(new KeyboardEvent('keydown', { key: 'ArrowUp' })) 21 24 }), 22 25 ) 23 26 onDestroy( 24 27 ipcListen('Select Next List', () => { 25 - console.log('Select Next List') 26 28 $itemHandle?.handleKey(new KeyboardEvent('keydown', { key: 'ArrowDown' })) 27 29 }), 28 30 ) ··· 33 35 isFolder: false, 34 36 isRoot: true, 35 37 }) 36 - } 37 - function open(id: string) { 38 - if ($page.id !== id) page.openPlaylist(id) 39 38 } 40 39 41 40 let rootDroppable = false ··· 129 128 <div class="focuser" tabindex="0" on:focus={focuser} /> 130 129 <div class="spacer" /> 131 130 <SidebarItems 132 - trackList={special} 133 - on:selectDown={() => { 131 + parentId={null} 132 + children={special_playlists_nav} 133 + on_open={(item) => { 134 + page.openPlaylist('root', item.view_as ?? view_as_songs) 135 + }} 136 + on_select_down={() => { 134 137 if ($trackListsDetailsMap.root.children && $trackListsDetailsMap.root.children[0]) { 135 - open($trackListsDetailsMap.root.children[0]) 138 + page.openPlaylist($trackListsDetailsMap.root.children[0], view_as_songs) 136 139 } 137 140 }} 138 - parentId={null} 139 141 /> 140 142 <div class="spacer" /> 141 143 <SidebarItems 142 - trackList={{ children: $trackListsDetailsMap['root'].children || [] }} 143 144 parentId={$trackListsDetailsMap['root'].id} 145 + children={($trackListsDetailsMap['root'].children || []).map( 146 + (childId) => $trackListsDetailsMap[childId], 147 + )} 148 + on_open={(item) => { 149 + if ($page.id !== item.id) { 150 + if (item.id === 'root') { 151 + page.openPlaylist( 152 + 'root', 153 + item.view_as ?? special_playlists_nav[special_playlists_nav.length - 1].view_as, 154 + ) 155 + } else { 156 + page.openPlaylist(item.id, item.view_as ?? view_as_songs) 157 + } 158 + } 159 + }} 144 160 /> 145 161 </nav> 146 162 </div>
+65 -66
src/components/SidebarItems.svelte
··· 29 29 </script> 30 30 31 31 <script lang="ts"> 32 - import type { TrackListDetails } from '../../ferrum-addon' 32 + import type { TrackListDetails, ViewAs } from '../../ferrum-addon' 33 33 import { type Writable, writable } from 'svelte/store' 34 - import { createEventDispatcher } from 'svelte' 35 34 import { getContext } from 'svelte' 36 35 import { dragged } from '../lib/drag-drop' 37 36 import * as dragGhost from './DragGhost.svelte' 38 37 import { ipcRenderer } from '@/lib/window' 39 38 import { checkShortcut } from '@/lib/helpers' 40 39 41 - export let parentId: string | null 42 40 export let show = true 43 - export let trackList: { children: string[] } 41 + export let parentId: string | null 44 42 export let preventDrop = false 43 + export let children: (TrackListDetails & { view_as?: ViewAs })[] 45 44 46 45 export let level = 0 47 - $: childLists = trackList.children.map((childId) => { 48 - return $trackListsDetailsMap[childId] 49 - }) 50 - function open(id: string) { 51 - if ($page.id !== id) page.openPlaylist(id) 52 - } 46 + export let on_open: (item: { id: string; view_as?: ViewAs }) => void 53 47 async function tracklistContextMenu(id: string, isFolder: boolean) { 54 48 await ipcRenderer.invoke('showTracklistMenu', { id, isFolder, isRoot: false }) 55 49 } ··· 59 53 return list.children && list.children.length > 0 && $shownFolders.has(id) 60 54 } 61 55 62 - const dispatch = createEventDispatcher<{ selectDown: null }>() 63 - function selectFirst(in_id: string) { 64 - const children = $trackListsDetailsMap[in_id].children 65 - if (children && children[0]) { 66 - open(children[0]) 56 + export let on_select_down = () => {} 57 + function selectFirst(item: TrackListDetails) { 58 + const child_id = item.children?.[0] 59 + if (child_id) { 60 + on_open($trackListsDetailsMap[child_id]) 67 61 } 68 62 } 69 63 function selectLast(in_id: string) { ··· 71 65 if (children && (hasShowingChildren(in_id) || in_id === 'root')) { 72 66 selectLast(children[children.length - 1]) 73 67 } else { 74 - open(in_id) 68 + on_open($trackListsDetailsMap[in_id]) 75 69 } 76 70 } 77 71 function selectUp(i: number) { 78 - const prevId = trackList.children[i - 1] || null 72 + const prevId = children[i - 1]?.id || null 79 73 if (i === 0 && parentId) { 80 - open(parentId) 74 + on_open({ id: parentId }) 81 75 } else if (prevId && hasShowingChildren(prevId)) { 82 76 selectLast(prevId) 83 77 } else if (prevId) { 84 - open(prevId) 78 + on_open({ id: prevId }) 85 79 } 86 80 } 87 81 function selectDown(i: number) { 88 - if (hasShowingChildren(trackList.children[i])) { 89 - selectFirst(trackList.children[i]) 90 - } else if (trackList.children[i + 1]) { 91 - open(trackList.children[i + 1]) 82 + if (hasShowingChildren(children[i].id)) { 83 + selectFirst(children[i]) 84 + } else if (children[i + 1]) { 85 + console.trace() 86 + on_open(children[i + 1]) 92 87 } else { 93 - dispatch('selectDown') 88 + on_select_down() 94 89 } 95 90 } 96 91 97 92 export function handleKey(e: KeyboardEvent) { 98 - const index = trackList.children.findIndex((id) => id === $page.tracklist.id) 93 + const index = children.findIndex((child) => { 94 + if (child.id === 'root') { 95 + return child.id === $page.tracklist.id && child.view_as === $page.viewAs 96 + } else { 97 + return child.id === $page.tracklist.id 98 + } 99 + }) 99 100 if (index < 0) { 100 101 return 101 102 } ··· 103 104 if (checkShortcut(e, 'ArrowUp')) { 104 105 selectUp(index) 105 106 } else if (checkShortcut(e, 'ArrowUp', { alt: true })) { 106 - open('root') 107 + on_open({ id: 'root' }) 107 108 } else if (checkShortcut(e, 'ArrowDown', { alt: true })) { 108 109 selectLast('root') 109 110 } else if (checkShortcut(e, 'ArrowDown')) { ··· 112 113 if (selectedList.kind === 'folder' && $shownFolders.has(selectedList.id)) { 113 114 hideFolder(selectedList.id) 114 115 } else if (parentId) { 115 - open(parentId) 116 + on_open({ id: parentId }) 116 117 } 117 118 } else if (checkShortcut(e, 'ArrowRight') && selectedList.kind === 'folder') { 118 119 showFolder(selectedList.id) ··· 121 122 } 122 123 e.preventDefault() 123 124 } 124 - $: if (trackList.children.includes($page.id)) { 125 + $: if (children.find((child) => child.id === $page.id)) { 125 126 const itemHandle = getContext<Writable<SidebarItemHandle | null>>('itemHandle') 126 127 itemHandle.set({ handleKey }) 127 128 } ··· 146 147 </script> 147 148 148 149 <div class="sub" class:show> 149 - {#each childLists as childList, i} 150 - {#if childList.kind === 'folder'} 150 + {#each children as child_list, i} 151 + {#if child_list.kind === 'folder'} 151 152 <div 152 153 class="item" 153 154 style:padding-left={14 * level + 'px'} 154 - class:active={$page.id === childList.id} 155 + class:active={$page.id === child_list.id} 155 156 draggable="true" 156 - on:dragstart={(e) => onDragStart(e, childList)} 157 - class:show={$shownFolders.has(childList.id)} 157 + on:dragstart={(e) => onDragStart(e, child_list)} 158 + class:show={$shownFolders.has(child_list.id)} 158 159 class:droppable={dragPlaylistOntoIndex === i} 159 160 role="none" 160 161 on:drop={(e) => { ··· 163 164 e.dataTransfer?.types[0] === 'ferrum.playlist' && 164 165 dragged.playlist && 165 166 !preventDrop && 166 - dragged.playlist.id !== childList.id && 167 - childList.children !== undefined 167 + dragged.playlist.id !== child_list.id && 168 + child_list.children !== undefined 168 169 ) { 169 170 movePlaylist( 170 171 dragged.playlist.id, 171 172 dragged.playlist.fromFolder, 172 - childList.id, 173 - Math.max(0, childList.children.length - 1), 173 + child_list.id, 174 + Math.max(0, child_list.children.length - 1), 174 175 ) 175 176 dragPlaylistOntoIndex = null 176 177 } 177 178 }} 178 - on:mousedown={() => open(childList.id)} 179 - on:contextmenu={() => tracklistContextMenu(childList.id, true)} 179 + on:mousedown={() => on_open(child_list)} 180 + on:contextmenu={() => tracklistContextMenu(child_list.id, true)} 180 181 > 181 182 <!-- svelte-ignore a11y-click-events-have-key-events --> 182 183 <!-- svelte-ignore a11y-interactive-supports-focus --> ··· 186 187 aria-label="Arrow button" 187 188 on:mousedown|stopPropagation 188 189 on:click={() => { 189 - if ($shownFolders.has(childList.id)) { 190 - hideFolder(childList.id) 190 + if ($shownFolders.has(child_list.id)) { 191 + hideFolder(child_list.id) 191 192 } else { 192 - showFolder(childList.id) 193 + showFolder(child_list.id) 193 194 } 194 195 }} 195 196 xmlns="http://www.w3.org/2000/svg" ··· 209 210 e.dataTransfer?.types[0] === 'ferrum.playlist' && 210 211 dragged.playlist && 211 212 !preventDrop && 212 - dragged.playlist.id !== childList.id 213 + dragged.playlist.id !== child_list.id 213 214 ) { 214 215 dragPlaylistOntoIndex = i 215 216 e.preventDefault() ··· 219 220 dragPlaylistOntoIndex = null 220 221 }} 221 222 > 222 - {childList.name} 223 + {child_list.name} 223 224 </div> 224 225 </div> 225 226 <svelte:self 226 - show={$shownFolders.has(childList.id)} 227 - trackList={childList} 228 - parentId={childList.id} 227 + show={$shownFolders.has(child_list.id)} 228 + parentId={child_list.id} 229 + children={child_list.children?.map((childId) => $trackListsDetailsMap[childId]) || []} 229 230 level={level + 1} 230 - preventDrop={preventDrop || dragged.playlist?.id === childList.id} 231 - on:selectDown={() => { 232 - if (i < trackList.children.length - 1) { 233 - open(trackList.children[i + 1]) 231 + preventDrop={preventDrop || dragged.playlist?.id === child_list.id} 232 + {on_open} 233 + on_select_down={() => { 234 + if (i < children.length - 1) { 235 + on_open(children[i + 1]) 234 236 } else { 235 - dispatch('selectDown') 237 + on_select_down() 236 238 } 237 239 }} 238 240 /> 239 - {:else if childList.kind === 'playlist'} 241 + {:else if child_list.kind === 'playlist'} 240 242 <!-- svelte-ignore a11y-interactive-supports-focus --> 241 243 <div 242 244 class="item" ··· 244 246 aria-label="playlist" 245 247 style:padding-left={14 * level + 'px'} 246 248 draggable="true" 247 - on:dragstart={(e) => onDragStart(e, childList)} 248 - class:active={$page.id === childList.id} 249 - on:mousedown={() => open(childList.id)} 249 + on:dragstart={(e) => onDragStart(e, child_list)} 250 + class:active={$page.id === child_list.id} 251 + on:mousedown={() => on_open(child_list)} 250 252 class:droppable={dragTrackOntoIndex === i} 251 253 class:droppable-above={dragPlaylistOntoIndex === i && dropAbove} 252 254 class:droppable-below={dragPlaylistOntoIndex === i && !dropAbove} 253 255 on:drop={(e) => { 254 256 if (e.currentTarget && e.dataTransfer?.types[0] === 'ferrum.tracks' && dragged.tracks) { 255 - addTracksToPlaylist(childList.id, dragged.tracks.ids) 257 + addTracksToPlaylist(child_list.id, dragged.tracks.ids) 256 258 dragTrackOntoIndex = null 257 259 } else if ( 258 260 e.currentTarget && ··· 272 274 dragPlaylistOntoIndex = null 273 275 } 274 276 }} 275 - on:contextmenu={() => tracklistContextMenu(childList.id, false)} 277 + on:contextmenu={() => tracklistContextMenu(child_list.id, false)} 276 278 > 277 279 <div class="arrow" /> 278 280 <div ··· 299 301 dragPlaylistOntoIndex = null 300 302 }} 301 303 > 302 - {childList.name} 304 + {child_list.name} 303 305 </div> 304 306 </div> 305 307 {:else} ··· 308 310 class="item" 309 311 role="link" 310 312 style:padding-left={14 * level + 'px'} 311 - on:mousedown={() => open(childList.id)} 312 - class:active={$page.id === childList.id} 313 + on:mousedown={() => on_open(child_list)} 314 + class:active={$page.id === child_list.id && 315 + child_list.name === ['Songs', 'Artists'][$page.viewAs]} 313 316 > 314 317 <div class="arrow" /> 315 318 <div class="text"> 316 - {#if childList.id === 'root'} 317 - Songs 318 - {:else} 319 - {childList.name} 320 - {/if} 319 + {child_list.name} 321 320 </div> 322 321 </div> 323 322 {/if}
+7 -40
src/components/TrackList.svelte
··· 1 1 <script lang="ts"> 2 - import { 3 - page, 4 - removeFromOpenPlaylist, 5 - filter, 6 - deleteTracksInOpen, 7 - paths, 8 - isMac, 9 - } from '../lib/data' 2 + import { page, removeFromOpenPlaylist, filter, deleteTracksInOpen, paths } from '../lib/data' 10 3 import { newPlaybackInstance, playingId } from '../lib/player' 11 4 import { 12 5 getDuration, ··· 15 8 checkShortcut, 16 9 assertUnreachable, 17 10 } from '../lib/helpers' 18 - import { appendToUserQueue, prependToUserQueue, queueVisible } from '../lib/queue' 11 + import { appendToUserQueue, prependToUserQueue } from '../lib/queue' 19 12 import { selection, tracklist_actions } from '../lib/page' 20 13 import { ipcListen, ipcRenderer } from '../lib/window' 21 14 import { onDestroy, onMount } from 'svelte' 22 15 import { dragged } from '../lib/drag-drop' 23 16 import * as dragGhost from './DragGhost.svelte' 24 17 import type { TrackID } from 'ferrum-addon/addon' 25 - import { modalCount } from './Modal.svelte' 26 18 import VirtualListBlock, { scroll_container_keydown } from './VirtualListBlock.svelte' 27 19 28 20 export let onTrackInfo: (allIds: TrackID[], index: number) => void ··· 214 206 on:dragleave={() => (dragToIndex = null)} 215 207 class:no-selection={$selection.count === 0} 216 208 > 217 - <div class="relative pt-4 px-5 pb-5"> 218 - <div class:dragbar={$modalCount === 0 && isMac} class:queue-visible={$queueVisible} /> 219 - <h3 class="title pb-0.5 text-[19px] leading-none"> 220 - {#if $page.tracklist.id === 'root'} 221 - Songs 222 - {:else if $page.tracklist.type !== 'special'} 223 - {$page.tracklist.name} 224 - {/if} 225 - </h3> 226 - <div class="text-[13px] leading-4 opacity-70">{$page.length} songs</div> 227 - {#if 'description' in $page.tracklist && $page.tracklist.description !== ''} 228 - <div class="mt-2.5 text-sm text-[13px] opacity-70">{$page.tracklist.description}</div> 229 - {/if} 230 - </div> 231 - <div class="row table-header" class:desc={$page.sortDesc} role="row"> 209 + <div 210 + class="row table-header border-b border-b-slate-500/30" 211 + class:desc={$page.sortDesc} 212 + role="row" 213 + > 232 214 <!-- svelte-ignore a11y-interactive-supports-focus --> 233 215 <!-- svelte-ignore a11y-click-events-have-key-events --> 234 216 <div ··· 332 314 </div> 333 315 <!-- svelte-ignore a11y-no-noninteractive-tabindex --> 334 316 <!-- svelte-ignore a11y-no-static-element-interactions --> 335 - <!-- svelte-ignore a11y-autofocus --> 336 317 <div 337 318 bind:this={scroll_container} 338 319 class="relative h-full overflow-y-auto outline-none" 339 320 on:keydown={keydown} 340 321 on:mousedown|self={selection.clear} 341 322 tabindex="0" 342 - autofocus 343 323 on:keydown={scroll_container_keydown} 344 324 > 345 325 <!-- Using `let:item={i}` instead of `let:i` fixes drag-and-drop --> ··· 412 392 :global(:focus) 413 393 .selected 414 394 background-color: hsla(var(--hue), 70%, 46%, 1) 415 - 416 - .dragbar 417 - -webkit-app-region: drag 418 - position: absolute 419 - height: 40px 420 - width: 100% 421 - top: 0px 422 - left: 0px 423 - &.queue-visible 424 - width: calc(100% - var(--right-sidebar-width)) 425 - .title 426 - margin: 0px 427 - font-weight: 500 428 395 .tracklist 429 396 display: flex 430 397 flex-direction: column
+21 -8
src/lib/data.ts
··· 1 1 import { writable } from 'svelte/store' 2 2 import { ipcRenderer } from '@/lib/window' 3 - import type { MsSinceUnixEpoch, TrackID, TrackList, TrackListID, TrackMd } from '../../ferrum-addon' 3 + import type { 4 + MsSinceUnixEpoch, 5 + TrackID, 6 + TrackList, 7 + TrackListID, 8 + TrackMd, 9 + ViewAs, 10 + } from '../../ferrum-addon' 4 11 import { selection as pageSelection } from './page' 5 12 import { queue } from './queue' 6 13 ··· 12 19 export const ItunesImport = innerAddon.ItunesImport 13 20 14 21 call((addon) => addon.load_data(isDev, libraryPath)) 22 + 23 + export const view_as_songs: ViewAs.Songs = 0 24 + export const view_as_artists: ViewAs.Artists = 1 15 25 16 26 function getErrorMessage(err: unknown): string { 17 27 if (typeof err === 'object' && err !== null) { ··· 289 299 set, 290 300 update, 291 301 refreshIdsAndKeepSelection, 292 - openPlaylist: (id: string) => { 293 - call((data) => data.open_playlist(id)) 302 + openPlaylist(id: string, view_as: ViewAs) { 303 + call((data) => data.open_playlist(id, view_as)) 294 304 refreshIdsAndKeepSelection() 295 305 pageSelection.clear() 296 306 filter.set('') 297 307 }, 298 - sortBy: (key: string) => { 308 + sortBy(key: string) { 299 309 call((addon) => addon.sort(key, true)) 300 310 refreshIdsAndKeepSelection() 301 311 pageSelection.clear() 302 312 }, 303 - set_group_album_tracks: (value: boolean) => { 313 + set_group_album_tracks(value: boolean) { 304 314 call((addon) => addon.set_group_album_tracks(value)) 305 315 refreshIdsAndKeepSelection() 306 316 pageSelection.clear() 307 317 }, 308 - getTrack: (index: number) => { 318 + get_artists() { 319 + return call((addon) => addon.get_artists()) 320 + }, 321 + getTrack(index: number) { 309 322 return call((addon) => addon.get_page_track(index)) 310 323 }, 311 - getTrackId: (index: number) => { 324 + getTrackId(index: number) { 312 325 return call((data) => data.get_page_track_id(index)) 313 326 }, 314 - getTrackIds: () => { 327 + getTrackIds() { 315 328 return call((data) => data.get_page_track_ids()) 316 329 }, 317 330 moveTracks: (indexes: number[], toIndex: number) => {