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

WIP move to item IDs

Kasper (Sep 26, 2024, 3:20 AM +0200) c3bfc04d 40c08185

+631 -362
+3 -3
ferrum-addon/addon.d.ts
··· 145 145 /** Returns the deleted track lists, including folder children */ 146 146 export declare function delete_track_list(id: string): void 147 147 export declare function add_tracks_to_playlist(playlistId: string, trackIds: Array<string>): void 148 - export declare function playlist_filter_duplicates(playlistId: string, ids: Array<string>): Array<TrackID> 149 - export declare function remove_from_playlist(playlistId: string, itemIds: Array<ItemId>): void 150 - export declare function delete_tracks(itemIds: Array<ItemId>): void 148 + export declare function playlist_filter_duplicates(playlistId: TrackID, ids: Array<string>): Array<TrackID> 149 + export declare function remove_from_playlist(playlistId: TrackID, itemIds: Array<ItemId>): void 150 + export declare function delete_tracks_with_item_ids(itemIds: Array<ItemId>): void 151 151 export declare function new_playlist(name: string, description: string, isFolder: boolean, parentId: string): void 152 152 export declare function update_playlist(id: string, name: string, description: string): void 153 153 export declare function move_playlist(id: string, fromId: string, toId: string, toIndex: number): void
+1
ferrum-addon/main.d.ts
··· 4 4 export * from './addon.d' 5 5 6 6 declare module 'ferrum-addon/addon' { 7 + export type ItemId = number 7 8 export type TrackID = string 8 9 export type TrackListID = string 9 10 export type PercentInteger = number
+4 -4
src-native/playlists.rs
··· 158 158 159 159 #[napi(js_name = "playlist_filter_duplicates")] 160 160 #[allow(dead_code)] 161 - pub fn filter_duplicates(playlist_id: String, ids: Vec<String>, env: Env) -> Result<Vec<TrackID>> { 161 + pub fn filter_duplicates(playlist_id: TrackID, ids: Vec<String>, env: Env) -> Result<Vec<TrackID>> { 162 162 let data: &mut Data = get_data(&env)?; 163 163 let mut track_ids: HashSet<String> = HashSet::from_iter(ids); 164 164 let playlist = match data.library.get_tracklist_mut(&playlist_id)? { ··· 176 176 177 177 #[napi(js_name = "remove_from_playlist")] 178 178 #[allow(dead_code)] 179 - pub fn remove_from_playlist(playlist_id: String, item_ids: Vec<ItemId>, env: Env) -> Result<()> { 179 + pub fn remove_from_playlist(playlist_id: TrackID, item_ids: Vec<ItemId>, env: Env) -> Result<()> { 180 180 let data: &mut Data = get_data(&env)?; 181 181 let playlist = match data.library.get_tracklist_mut(&playlist_id)? { 182 182 TrackList::Playlist(playlist) => playlist, ··· 217 217 } 218 218 } 219 219 220 - #[napi(js_name = "delete_tracks")] 220 + #[napi(js_name = "delete_tracks_with_item_ids")] 221 221 #[allow(dead_code)] 222 - pub fn delete_tracks_with_item_id(item_ids: Vec<ItemId>, env: Env) -> Result<()> { 222 + pub fn delete_tracks_with_item_ids(item_ids: Vec<ItemId>, env: Env) -> Result<()> { 223 223 let data: &mut Data = get_data(&env)?; 224 224 let library = &mut data.library; 225 225 let track_ids = get_track_ids_from_item_ids(&item_ids);
+1 -39
src/App.svelte
··· 9 9 import PlaylistInfoModal from './components/PlaylistInfo.svelte' 10 10 import { queue_visible } from './lib/queue' 11 11 import { ipc_listen, ipc_renderer } from '@/lib/window' 12 - import { 13 - import_tracks, 14 - type PlaylistInfo, 15 - methods, 16 - page, 17 - is_mac, 18 - view_as_songs, 19 - view_as_artists, 20 - } from './lib/data' 12 + import { import_tracks, type PlaylistInfo, methods } from './lib/data' 21 13 import { play_pause } from './lib/player' 22 14 import DragGhost from './components/DragGhost.svelte' 23 15 import ItunesImport from './components/ItunesImport.svelte' ··· 198 190 <div class="meat"> 199 191 <Sidebar /> 200 192 <div class="flex size-full min-w-0 flex-col"> 201 - <div class="relative pt-4 px-5 pb-5"> 202 - <div 203 - class="absolute top-0 left-0 h-10 w-full" 204 - class:dragbar={$modal_count === 0 && is_mac} 205 - class:queue-visible={$queue_visible} 206 - /> 207 - <h3 class="m-0 pb-0.5 text-[19px] font-medium leading-none"> 208 - {#if $page.tracklist.id === 'root'} 209 - {#if $page.viewAs === view_as_songs} 210 - Songs 211 - <div class="text-[13px] leading-4 opacity-70">{$page.length} songs</div> 212 - {:else if $page.viewAs === view_as_artists} 213 - Artists 214 - <div class="text-[13px] leading-4 opacity-70"> 215 - {page.get_artists().length} artists 216 - </div> 217 - {/if} 218 - {:else if $page.tracklist.type !== 'special'} 219 - {$page.tracklist.name} 220 - <div class="text-[13px] leading-4 opacity-70">{$page.length} songs</div> 221 - {/if} 222 - </h3> 223 - {#if 'description' in $page.tracklist && $page.tracklist.description !== ''} 224 - <div class="mt-2.5 text-sm text-[13px] opacity-70">{$page.tracklist.description}</div> 225 - {/if} 226 - </div> 227 193 <Route route="/playlist/:playlist_id" component={TrackList} /> 228 194 <Route route="/artists" component={ArtistList} /> 229 195 </div> ··· 304 270 height: 100% 305 271 top: 0px 306 272 left: 0px 307 - .dragbar 308 - -webkit-app-region: drag 309 - &.queue-visible 310 - width: calc(100% - var(--right-sidebar-width)) 311 273 .drag-overlay 312 274 display: flex 313 275 align-items: center
+4 -2
src/components/ArtistList.svelte
··· 1 1 <script lang="ts"> 2 - import { filter, page } from '@/lib/data' 2 + import { filter, get_artists } from '@/lib/data' 3 3 import fuzzysort from 'fuzzysort' 4 + import Header from './Header.svelte' 4 5 5 - $: all_artists = page.get_artists() 6 + $: all_artists = get_artists() 6 7 $: artists = fuzzysort.go($filter, all_artists, { all: true }) 7 8 </script> 8 9 10 + <Header title="Artists" subtitle="{all_artists.length} artists" description={undefined} /> 9 11 <div class="w-full border-b border-b-slate-500/30"> 10 12 <p class="px-3">(Work in progress)</p> 11 13 </div>
+33
src/components/Header.svelte
··· 1 + <script lang="ts"> 2 + import { is_mac } from '@/lib/data' 3 + import { modal_count } from './Modal.svelte' 4 + import { queue_visible } from '@/lib/queue' 5 + 6 + export let title: string 7 + export let subtitle: string 8 + export let description: string | undefined 9 + </script> 10 + 11 + <div class="relative pt-4 px-5 pb-5"> 12 + <div 13 + class="absolute top-0 left-0 h-10 w-full" 14 + class:dragbar={$modal_count === 0 && is_mac} 15 + class:queue-visible={$queue_visible} 16 + /> 17 + <h3 class="m-0 pb-0.5 text-[19px] font-medium leading-none"> 18 + {title} 19 + <div class="text-[13px] leading-4 opacity-70">{subtitle}</div> 20 + </h3> 21 + {#if description} 22 + <div class="mt-2.5 text-sm text-[13px] opacity-70">{description}</div> 23 + {/if} 24 + </div> 25 + 26 + <style> 27 + .dragbar { 28 + -webkit-app-region: drag; 29 + } 30 + .queue-visible { 31 + width: calc(100% - var(--right-sidebar-width)); 32 + } 33 + </style>
+8 -8
src/components/ItunesImport.svelte
··· 1 1 <script lang="ts"> 2 - import { ItunesImport, paths, call, methods, page, track_lists_details_map } from '@/lib/data' 2 + import { ItunesImport, paths, call } from '@/lib/data' 3 3 import { ipc_renderer } from '@/lib/window' 4 4 import type { ImportStatus } from 'ferrum-addon/addon' 5 5 import Button from './Button.svelte' 6 6 import Modal from './Modal.svelte' 7 - import { selection as pageSelection } from '@/lib/page' 7 + // import { selection as pageSelection } from '@/lib/page' 8 8 9 9 export let cancel: () => void 10 10 let itunes_import = ItunesImport.new() ··· 34 34 } 35 35 } 36 36 async function finish() { 37 - itunes_import.finish() 38 - methods.save() 39 - page.refresh_ids_and_keep_selection() 40 - pageSelection.clear() 41 - track_lists_details_map.refresh() 42 - cancel() 37 + // itunes_import.finish() 38 + // methods.save() 39 + // page.refresh_ids_and_keep_selection() 40 + // pageSelection.clear() 41 + // track_lists_details_map.refresh() 42 + // cancel() 43 43 } 44 44 async function submit() { 45 45 if (stage === 'select') {
+2 -2
src/components/QueueItem.svelte
··· 1 1 <script lang="ts"> 2 - import { methods, paths, track_metadata_updated } from '@/lib/data' 2 + import { methods, paths, tracks_updated } from '@/lib/data' 3 3 import type { Track } from '../../ferrum-addon' 4 4 import { join_paths } from '@/lib/window' 5 5 6 6 export let id: string 7 7 8 8 let track: Track 9 - $: $track_metadata_updated, (track = methods.getTrack(id)) 9 + $: $tracks_updated, (track = methods.getTrack(id)) 10 10 11 11 $: src = 12 12 'trackimg:?path=' +
+3 -3
src/components/Sidebar.svelte
··· 8 8 <script lang="ts"> 9 9 import SidebarItems, { type SidebarItemHandle } from './SidebarItems.svelte' 10 10 import Filter from './Filter.svelte' 11 - import { is_mac, track_lists_details_map, page, move_playlist } from '../lib/data' 11 + import { is_mac, track_lists_details_map, move_playlist } from '../lib/data' 12 12 import { ipc_listen, ipc_renderer } from '../lib/window' 13 13 import { writable } from 'svelte/store' 14 14 import { onDestroy, setContext, tick } from 'svelte' 15 15 import { dragged } from '../lib/drag-drop' 16 16 import { tracklist_actions } from '@/lib/page' 17 17 import { navigate } from '@/lib/router' 18 + import { current_playlist_id } from './TrackList.svelte' 18 19 19 20 let viewport: HTMLElement 20 21 const item_handle = setContext('itemHandle', writable(null as SidebarItemHandle | null)) ··· 61 62 62 63 let content_element: HTMLDivElement 63 64 64 - $: page_id = $page.id 65 - $: page_id, scroll_to_active() 65 + $: $current_playlist_id, scroll_to_active() 66 66 async function scroll_to_active() { 67 67 await tick() 68 68 const active = content_element?.querySelector('.active')
+2 -2
src/components/SidebarItems.svelte
··· 1 1 <script lang="ts" context="module"> 2 2 import { 3 3 track_lists_details_map, 4 - page, 5 4 methods, 6 5 add_track_to_playlist, 7 6 move_playlist, ··· 38 37 import { ipc_renderer } from '@/lib/window' 39 38 import { check_shortcut } from '@/lib/helpers' 40 39 import { navigate, url } from '@/lib/router' 40 + import { current_playlist_id } from './TrackList.svelte' 41 41 42 42 export let show = true 43 43 export let parent_path: string | null ··· 98 98 return 99 99 } 100 100 101 - const selected_list = $track_lists_details_map[$page.tracklist.id] 101 + const selected_list = $track_lists_details_map[$current_playlist_id] 102 102 if (check_shortcut(e, 'ArrowUp')) { 103 103 select_up(index) 104 104 } else if (check_shortcut(e, 'ArrowUp', { alt: true })) {
+151 -122
src/components/TrackList.svelte
··· 1 + <script lang="ts" context="module"> 2 + export let current_playlist_id = writable('') 3 + export const sort_key = writable('index') 4 + export const sort_desc = writable(true) 5 + export const group_album_tracks = writable(true) 6 + </script> 7 + 1 8 <script lang="ts"> 2 - import { 3 - page, 4 - remove_from_open_playlist, 5 - filter, 6 - delete_tracks_in_open, 7 - paths, 8 - view_as_songs, 9 - methods, 10 - } from '../lib/data' 9 + import { filter, paths, methods, track_lists_details_map, move_tracks } from '../lib/data' 11 10 import { new_playback_instance, playing_id } from '../lib/player' 12 11 import { 13 12 get_duration, ··· 17 16 assert_unreachable, 18 17 } from '../lib/helpers' 19 18 import { append_to_user_queue, prepend_to_user_queue } from '../lib/queue' 20 - import { selection, tracklist_actions } from '../lib/page' 19 + import { tracklist_actions } from '../lib/page' 21 20 import { ipc_listen, ipc_renderer } from '../lib/window' 22 21 import { onDestroy, onMount } from 'svelte' 23 22 import { dragged } from '../lib/drag-drop' ··· 26 25 import { open_track_info } from './TrackInfo.svelte' 27 26 import type { Track } from 'ferrum-addon/addon' 28 27 import Cover from './Cover.svelte' 28 + import Header from './Header.svelte' 29 + import { writable } from 'svelte/store' 30 + import { new_selection } from '@/lib/selection-new' 29 31 30 32 let tracklist_element: HTMLDivElement 33 + 31 34 export let params: { playlist_id: string } 32 35 33 - $: page.open_playlist(params.playlist_id, view_as_songs) 36 + $: { 37 + $current_playlist_id = params.playlist_id 38 + } 39 + $: tracklist = $track_lists_details_map[params.playlist_id] 40 + 41 + $: tracks_page = methods.get_tracks_page({ 42 + playlistId: params.playlist_id, 43 + filterQuery: $filter, 44 + sortKey: $sort_key, 45 + sortDesc: $sort_desc, 46 + groupAlbumTracks: $group_album_tracks, 47 + }) 48 + $: track_indexes = tracks_page.trackIds.map((_, i) => i) 49 + 50 + let selection = new_selection({ 51 + scroll_to_item(i) { 52 + tracklist_actions.scroll_to_index?.(i) 53 + }, 54 + async on_context_menu() { 55 + // const indexes = selection.getSelectedIndexes() 56 + // const ids = page.get_track_ids() 57 + // await show_track_menu(ids, indexes, { editable: get(page).tracklist.type === 'playlist' }) 58 + }, 59 + }) 60 + $: selection.update_all_items(track_indexes) 34 61 35 62 const track_action_unlisten = ipc_listen('selectedTracksAction', (_, action) => { 36 - let first_index = selection.findFirst() 37 - if (first_index === null || !tracklist_element.contains(document.activeElement)) { 38 - return 39 - } 40 - if (action === 'Play Next') { 41 - const ids = selection.getSelectedIndexes().map((i) => page.get_track_id(i)) 42 - prepend_to_user_queue(ids) 43 - } else if (action === 'Add to Queue') { 44 - const ids = selection.getSelectedIndexes().map((i) => page.get_track_id(i)) 45 - append_to_user_queue(ids) 46 - } else if (action === 'Get Info') { 47 - open_track_info(page.get_track_ids(), first_index) 48 - } else if (action === 'revealTrackFile') { 49 - const track = page.get_track(first_index) 50 - ipc_renderer.invoke('revealTrackFile', paths.tracksDir, track.file) 51 - } else if (action === 'Remove from Playlist') { 52 - remove_from_open_playlist(selection.getSelectedIndexes()) 53 - } else if (action === 'Delete from Library') { 54 - delete_indexes(selection.getSelectedIndexes()) 55 - } else { 56 - assert_unreachable(action) 57 - } 63 + // let first_index = selection.findFirst() 64 + // if (first_index === null || !tracklist_element.contains(document.activeElement)) { 65 + // return 66 + // } 67 + // if (action === 'Play Next') { 68 + // const ids = selection.getSelectedIndexes().map((i) => page.get_track_id(i)) 69 + // prepend_to_user_queue(ids) 70 + // } else if (action === 'Add to Queue') { 71 + // const ids = selection.getSelectedIndexes().map((i) => page.get_track_id(i)) 72 + // append_to_user_queue(ids) 73 + // } else if (action === 'Get Info') { 74 + // open_track_info(page.get_track_ids(), first_index) 75 + // } else if (action === 'revealTrackFile') { 76 + // const track = page.get_track(first_index) 77 + // ipc_renderer.invoke('revealTrackFile', paths.tracksDir, track.file) 78 + // } else if (action === 'Remove from Playlist') { 79 + // remove_from_open_playlist(selection.getSelectedIndexes()) 80 + // } else if (action === 'Delete from Library') { 81 + // delete_indexes(selection.getSelectedIndexes()) 82 + // } else { 83 + // assert_unreachable(action) 84 + // } 58 85 }) 59 86 onDestroy(track_action_unlisten) 60 87 61 - const sort_by = page.sort_by 62 - $: sort_key = $page.sortKey 63 - 64 88 ipc_renderer.on('Group Album Tracks', (_, checked) => { 65 - page.set_group_album_tracks(checked) 89 + group_album_tracks.set(checked) 66 90 }) 67 91 68 92 function double_click(e: MouseEvent, index: number) { ··· 71 95 } 72 96 } 73 97 async function delete_indexes(indexes: number[]) { 74 - const s = $selection.count > 1 ? 's' : '' 75 - const result = await ipc_renderer.invoke('showMessageBox', false, { 76 - type: 'info', 77 - message: `Delete ${$selection.count} song${s} from library?`, 78 - buttons: [`Delete Song${s}`, 'Cancel'], 79 - defaultId: 0, 80 - }) 81 - if (result.response === 0) { 82 - delete_tracks_in_open(indexes) 83 - } 98 + // const s = $selection.count > 1 ? 's' : '' 99 + // const result = await ipc_renderer.invoke('showMessageBox', false, { 100 + // type: 'info', 101 + // message: `Delete ${$selection.count} song${s} from library?`, 102 + // buttons: [`Delete Song${s}`, 'Cancel'], 103 + // defaultId: 0, 104 + // }) 105 + // if (result.response === 0) { 106 + // delete_tracks_in_open(indexes) 107 + // } 84 108 } 85 109 async function keydown(e: KeyboardEvent) { 86 - if (check_shortcut(e, 'Enter')) { 87 - let first_index = selection.findFirst() 88 - if (first_index !== null) { 89 - play_row(first_index) 90 - } else if (!$playing_id) { 91 - play_row(0) 92 - } 93 - } else if ( 94 - check_shortcut(e, 'Backspace') && 95 - $selection.count > 0 && 96 - !$filter && 97 - $page.tracklist.type === 'playlist' 98 - ) { 99 - e.preventDefault() 100 - const s = $selection.count > 1 ? 's' : '' 101 - const result = ipc_renderer.invoke('showMessageBox', false, { 102 - type: 'info', 103 - message: `Remove ${$selection.count} song${s} from the list?`, 104 - buttons: ['Remove Song' + s, 'Cancel'], 105 - defaultId: 0, 106 - }) 107 - const indexes = selection.getSelectedIndexes() 108 - if ((await result).response === 0) { 109 - remove_from_open_playlist(indexes) 110 - } 111 - } else if (check_shortcut(e, 'Backspace', { cmd_or_ctrl: true }) && $selection.count > 0) { 112 - e.preventDefault() 113 - delete_indexes(selection.getSelectedIndexes()) 114 - } else { 115 - selection.handleKeyDown(e) 116 - return 117 - } 118 - e.preventDefault() 110 + // if (check_shortcut(e, 'Enter')) { 111 + // let first_index = selection.findFirst() 112 + // if (first_index !== null) { 113 + // play_row(first_index) 114 + // } else if (!$playing_id) { 115 + // play_row(0) 116 + // } 117 + // } else if ( 118 + // check_shortcut(e, 'Backspace') && 119 + // $selection.count > 0 && 120 + // !$filter && 121 + // tracklist.kind === 'playlist' 122 + // ) { 123 + // e.preventDefault() 124 + // const s = $selection.count > 1 ? 's' : '' 125 + // const result = ipc_renderer.invoke('showMessageBox', false, { 126 + // type: 'info', 127 + // message: `Remove ${$selection.count} song${s} from the list?`, 128 + // buttons: ['Remove Song' + s, 'Cancel'], 129 + // defaultId: 0, 130 + // }) 131 + // const indexes = selection.getSelectedIndexes() 132 + // if ((await result).response === 0) { 133 + // remove_from_open_playlist(indexes) 134 + // } 135 + // } else if (check_shortcut(e, 'Backspace', { cmd_or_ctrl: true }) && $selection.count > 0) { 136 + // e.preventDefault() 137 + // delete_indexes(selection.getSelectedIndexes()) 138 + // } else { 139 + // selection.handleKeyDown(e) 140 + // return 141 + // } 142 + // e.preventDefault() 119 143 } 120 144 121 145 function play_row(index: number) { 122 - new_playback_instance(page.get_track_ids(), index) 146 + // new_playback_instance(page.get_track_ids(), index) 123 147 } 124 148 125 149 let drag_line: HTMLElement 126 150 let drag_indexes: number[] = [] 127 151 function on_drag_start(e: DragEvent) { 128 - if (e.dataTransfer) { 129 - drag_indexes = [] 130 - for (let i = 0; i < $selection.list.length; i++) { 131 - if ($selection.list[i]) { 132 - drag_indexes.push(i) 133 - } 134 - } 135 - e.dataTransfer.effectAllowed = 'move' 136 - if (drag_indexes.length === 1) { 137 - const track = page.get_track(drag_indexes[0]) 138 - dragGhost.set_inner_text(track.artist + ' - ' + track.name) 139 - } else { 140 - dragGhost.set_inner_text(drag_indexes.length + ' items') 141 - } 142 - dragged.tracks = { 143 - ids: drag_indexes.map((i) => page.get_track_id(i)), 144 - playlist_indexes: drag_indexes, 145 - } 146 - e.dataTransfer.setDragImage(dragGhost.drag_el, 0, 0) 147 - e.dataTransfer.setData('ferrum.tracks', '') 148 - } 152 + // if (e.dataTransfer) { 153 + // drag_indexes = [] 154 + // for (let i = 0; i < $selection.list.length; i++) { 155 + // if ($selection.list[i]) { 156 + // drag_indexes.push(i) 157 + // } 158 + // } 159 + // e.dataTransfer.effectAllowed = 'move' 160 + // if (drag_indexes.length === 1) { 161 + // const track = page.get_track(drag_indexes[0]) 162 + // dragGhost.set_inner_text(track.artist + ' - ' + track.name) 163 + // } else { 164 + // dragGhost.set_inner_text(drag_indexes.length + ' items') 165 + // } 166 + // dragged.tracks = { 167 + // ids: drag_indexes.map((i) => page.get_track_id(i)), 168 + // playlist_indexes: drag_indexes, 169 + // } 170 + // e.dataTransfer.setDragImage(dragGhost.drag_el, 0, 0) 171 + // e.dataTransfer.setData('ferrum.tracks', '') 172 + // } 149 173 } 150 174 let drag_to_index: null | number = null 151 175 function on_drag_over(e: DragEvent, index: number) { 152 - if ( 153 - !$page.sortDesc || 154 - $page.sortKey !== 'index' || 155 - $filter || 156 - $page.tracklist.type !== 'playlist' 157 - ) { 176 + if (!$sort_desc || $sort_key !== 'index' || $filter || tracklist.kind !== 'playlist') { 158 177 drag_to_index = null 159 178 return 160 179 } ··· 180 199 } 181 200 async function drop_handler() { 182 201 if (drag_to_index !== null) { 183 - page.move_tracks(drag_indexes, drag_to_index) 202 + move_tracks(params.playlist_id, drag_indexes, drag_to_index) 184 203 drag_to_index = null 185 204 } 186 205 } ··· 190 209 191 210 function get_item(index: number) { 192 211 try { 193 - const track = page.get_track(index) 194 - return { ...track, id: page.get_track_id(index) } 212 + const track_id = tracks_page.trackIds[index] 213 + const track = methods.getTrack(track_id) 214 + return { ...track, id: track_id } 195 215 } catch (_) { 196 216 return null 197 217 } ··· 199 219 200 220 let virtual_list: VirtualListBlock<number> 201 221 202 - $: if ($page && virtual_list) { 222 + $: if (virtual_list) { 203 223 virtual_list.refresh() 204 224 } 205 225 ··· 358 378 } 359 379 </script> 360 380 381 + {#if tracklist.id === 'root'} 382 + <Header title="Songs" subtitle="{tracks_page.trackIds.length} songs" description={undefined} /> 383 + {:else} 384 + <Header 385 + title={tracklist.name} 386 + subtitle="{tracks_page.trackIds.length} songs" 387 + description={tracks_page.playlistDescription} 388 + /> 389 + {/if} 361 390 <div 362 391 bind:this={tracklist_element} 363 392 class="tracklist h-full" 364 393 role="table" 365 394 on:dragleave={() => (drag_to_index = null)} 366 - class:no-selection={$selection.count === 0} 367 395 > 368 396 <!-- svelte-ignore a11y-interactive-supports-focus --> 369 397 <div 370 398 class="row table-header border-b border-b-slate-500/30" 371 - class:desc={$page.sortDesc} 399 + class:desc={$sort_desc} 372 400 role="row" 373 401 on:contextmenu={on_column_context_menu} 374 402 on:dragleave={() => (col_drag_to_index = null)} ··· 379 407 <!-- svelte-ignore a11y-click-events-have-key-events --> 380 408 <div 381 409 class="c {column.key}" 382 - class:sort={sort_key === column.key} 410 + class:sort={$sort_key === column.key} 383 411 style:width={column.width} 384 412 role="button" 385 - on:click={() => sort_by(column.key)} 413 + on:click={() => { 414 + sort_key.set(column.key) 415 + }} 386 416 draggable="true" 387 417 on:dragstart={(e) => on_col_drag_start(e, i)} 388 418 on:dragend={col_drag_end_handler} ··· 408 438 tabindex="0" 409 439 on:keydown={scroll_container_keydown} 410 440 > 411 - <!-- Using `let:item={i}` instead of `let:i` fixes drag-and-drop --> 412 441 <VirtualListBlock 413 442 bind:this={virtual_list} 414 - items={Array.from({ length: $page.length }).map((_, i) => i)} 443 + items={track_indexes} 415 444 item_height={24} 416 445 {scroll_container} 417 446 let:item={i} ··· 425 454 class="row" 426 455 role="row" 427 456 on:dblclick={(e) => double_click(e, i)} 428 - on:mousedown={(e) => selection.handleMouseDown(e, i)} 429 - on:contextmenu={(e) => selection.handleContextMenu(e, i)} 430 - on:click={(e) => selection.handleClick(e, i)} 457 + on:mousedown={(e) => selection.handle_mouse_down(e, i)} 458 + on:contextmenu={(e) => selection.handle_contextmenu(e, i)} 459 + on:click={(e) => selection.handle_click(e, i)} 431 460 draggable="true" 432 461 on:dragstart={on_drag_start} 433 462 on:dragover={(e) => on_drag_over(e, i)} 434 463 on:drop={drop_handler} 435 464 on:dragend={drag_end_handler} 436 465 class:odd={i % 2 === 0} 437 - class:selected={$selection.list[i] === true} 466 + class:selected={$selection.has(track.id)} 438 467 class:playing={track.id === $playing_id} 439 468 > 440 469 {#each columns as column}
+103 -157
src/lib/data.ts
··· 5 5 TrackID, 6 6 TrackList, 7 7 TrackListID, 8 + ItemId, 8 9 TrackMd, 10 + TracksPageOptions, 9 11 ViewAs, 10 12 ViewOptions, 11 13 } from '../../ferrum-addon' 12 - import { selection as pageSelection } from './page' 13 - import { queue } from './queue' 14 14 15 15 export const is_dev = window.is_dev 16 16 export const local_data_path = window.local_data_path ··· 66 66 return result 67 67 } 68 68 } catch (err) { 69 - console.log('errorPopup') 70 - 69 + console.error('errorPopup:', err) 71 70 error_popup(err) 72 71 throw err 73 72 } ··· 89 88 track_ids: TrackID[], 90 89 check_duplicates = true, 91 90 ) { 92 - if (check_duplicates) { 93 - const filtered_ids = call((addon) => addon.playlist_filter_duplicates(playlist_id, track_ids)) 94 - const duplicates = track_ids.length - filtered_ids.length 95 - 96 - if (duplicates > 0) { 97 - const result = await ipc_renderer.invoke('showMessageBox', false, { 98 - type: 'question', 99 - message: 'Already added', 100 - detail: 101 - duplicates > 1 102 - ? `${duplicates} songs are already in this playlist` 103 - : `${duplicates} song is already in this playlist`, 104 - buttons: ['Add anyway', 'Cancel', 'Skip'], 105 - defaultId: 0, 106 - }) 107 - if (result.response === 1) { 108 - return 109 - } else if (result.response === 2) { 110 - track_ids = filtered_ids 111 - } 112 - } 113 - } 114 - if (track_ids.length >= 1) { 115 - call((addon) => addon.add_tracks_to_playlist(playlist_id, track_ids)) 116 - if (page.get().tracklist.id === playlist_id) { 117 - page.refresh_ids_and_keep_selection() 118 - } 119 - methods.save() 120 - } 91 + // if (check_duplicates) { 92 + // const filtered_ids = call((addon) => addon.playlist_filter_duplicates(playlist_id, track_ids)) 93 + // const duplicates = track_ids.length - filtered_ids.length 94 + // if (duplicates > 0) { 95 + // const result = await ipc_renderer.invoke('showMessageBox', false, { 96 + // type: 'question', 97 + // message: 'Already added', 98 + // detail: 99 + // duplicates > 1 100 + // ? `${duplicates} songs are already in this playlist` 101 + // : `${duplicates} song is already in this playlist`, 102 + // buttons: ['Add anyway', 'Cancel', 'Skip'], 103 + // defaultId: 0, 104 + // }) 105 + // if (result.response === 1) { 106 + // return 107 + // } else if (result.response === 2) { 108 + // track_ids = filtered_ids 109 + // } 110 + // } 111 + // } 112 + // if (track_ids.length >= 1) { 113 + // call((addon) => addon.add_tracks_to_playlist(playlist_id, track_ids)) 114 + // if (page.get().tracklist.id === playlist_id) { 115 + // page.refresh_ids_and_keep_selection() 116 + // } 117 + // methods.save() 118 + // } 121 119 } 122 - export function remove_from_open_playlist(indexes: number[]) { 123 - call((addon) => addon.remove_from_open_playlist(indexes)) 124 - page.refresh_ids_and_keep_selection() 125 - pageSelection.clear() 126 - methods.save() 120 + export function remove_from_playlist(playlist_id: TrackListID, item_ids: ItemId[]) { 121 + // call((addon) => addon.remove_from_playlist(playlist_id, item_ids)) 122 + // page.refresh_ids_and_keep_selection() 123 + // pageSelection.clear() 124 + // methods.save() 127 125 } 128 - export function delete_tracks_in_open(indexes: number[]) { 129 - call((addon) => addon.delete_tracks_in_open(indexes)) 130 - page.refresh_ids_and_keep_selection() 131 - pageSelection.clear() 132 - queue.removeDeleted() 133 - methods.save() 126 + export function delete_tracks_with_item_ids(item_ids: ItemId[]) { 127 + // call((addon) => addon.delete_tracks_with_item_ids(item_ids)) 128 + // page.refresh_ids_and_keep_selection() 129 + // pageSelection.clear() 130 + // queue.removeDeleted() 131 + // methods.save() 134 132 } 135 133 export type PlaylistInfo = { 136 134 name: string ··· 146 144 methods.save() 147 145 } 148 146 export function update_playlist(id: string, name: string, description: string) { 149 - call((addon) => addon.update_playlist(id, name, description)) 150 - track_lists_details_map.refresh() 151 - page.refresh_ids_and_keep_selection() 152 - methods.save() 147 + // call((addon) => addon.update_playlist(id, name, description)) 148 + // track_lists_details_map.refresh() 149 + // page.refresh_ids_and_keep_selection() 150 + // methods.save() 153 151 } 154 152 export function move_playlist( 155 153 id: TrackListID, ··· 165 163 export const paths = call((addon) => addon.get_paths()) 166 164 167 165 export async function import_tracks(paths: string[]) { 168 - let err_state = null 169 - const now = Date.now() 170 - for (const path of paths) { 171 - try { 172 - inner_addon.import_file(path, now) 173 - } catch (err) { 174 - if (err_state === 'skip') continue 175 - const result = await ipc_renderer.invoke('showMessageBox', false, { 176 - type: 'error', 177 - message: 'Error importing track ' + path, 178 - detail: get_error_message(err), 179 - buttons: err_state ? ['OK', 'Skip all errors'] : ['OK'], 180 - defaultId: 0, 181 - }) 182 - if (result.response === 1) err_state = 'skip' 183 - else err_state = 'skippable' 184 - } 185 - } 186 - page.refresh_ids_and_keep_selection() 187 - pageSelection.clear() 188 - methods.save() 166 + // let err_state = null 167 + // const now = Date.now() 168 + // for (const path of paths) { 169 + // try { 170 + // inner_addon.import_file(path, now) 171 + // } catch (err) { 172 + // if (err_state === 'skip') continue 173 + // const result = await ipc_renderer.invoke('showMessageBox', false, { 174 + // type: 'error', 175 + // message: 'Error importing track ' + path, 176 + // detail: get_error_message(err), 177 + // buttons: err_state ? ['OK', 'Skip all errors'] : ['OK'], 178 + // defaultId: 0, 179 + // }) 180 + // if (result.response === 1) err_state = 'skip' 181 + // else err_state = 'skippable' 182 + // } 183 + // } 184 + // page.refresh_ids_and_keep_selection() 185 + // pageSelection.clear() 186 + // methods.save() 189 187 } 190 188 191 189 export const methods = { ··· 194 192 }, 195 193 getTrack: (id: TrackID) => { 196 194 return call((data) => data.get_track(id)) 195 + }, 196 + get_tracks_page: (options: TracksPageOptions) => { 197 + return call((data) => data.get_tracks_page(options)) 197 198 }, 198 199 trackExists: (id: TrackID) => { 199 200 return call((data) => data.track_exists(id)) ··· 202 203 return call((data) => data.get_track_list(id)) as TrackList 203 204 }, 204 205 deleteTrackList: (id: TrackListID) => { 205 - call((data) => data.delete_track_list(id)) 206 - page.refresh_ids_and_keep_selection() 207 - pageSelection.clear() 208 - track_lists_details_map.refresh() 209 - methods.save() 206 + // console.log('deleteTL') 207 + // call((data) => data.delete_track_list(id)) 208 + // console.log('id ', '===', ' current_playlist_id') 209 + // if (id === current_playlist_id) { 210 + // navigate('/playlist/root') 211 + // } 212 + // page.refresh_ids_and_keep_selection() 213 + // pageSelection.clear() 214 + // track_lists_details_map.refresh() 215 + // methods.save() 210 216 }, 211 217 save: () => { 212 218 return call((addon) => addon.save()) 213 219 }, 214 220 addPlay: (id: TrackID) => { 215 - call((data) => data.add_play(id)) 216 - page.refresh_ids_and_keep_selection() 217 - methods.save() 221 + // call((data) => data.add_play(id)) 222 + // page.refresh_ids_and_keep_selection() 223 + // methods.save() 218 224 }, 219 225 addSkip: (id: TrackID) => { 220 - call((data) => data.add_skip(id)) 221 - page.refresh_ids_and_keep_selection() 222 - methods.save() 226 + // call((data) => data.add_skip(id)) 227 + // page.refresh_ids_and_keep_selection() 228 + // methods.save() 223 229 }, 224 230 addPlayTime: (id: TrackID, start_time: MsSinceUnixEpoch, duration_ms: number) => { 225 - call((data) => data.add_play_time(id, start_time, duration_ms)) 226 - page.refresh_ids_and_keep_selection() 227 - methods.save() 231 + // call((data) => data.add_play_time(id, start_time, duration_ms)) 232 + // page.refresh_ids_and_keep_selection() 233 + // methods.save() 228 234 }, 229 235 readCoverAsync(id: TrackID, index: number) { 230 236 return inner_addon.read_cover_async(id, index).catch((error) => { ··· 233 239 }) 234 240 }, 235 241 updateTrackInfo: (id: TrackID, md: TrackMd) => { 236 - call((data) => data.update_track_info(id, md)) 237 - track_metadata_updated.emit() 238 - page.refresh_ids_and_keep_selection() 239 - methods.save() 242 + // call((data) => data.update_track_info(id, md)) 243 + // tracks_updated.emit() 244 + // page.refresh_ids_and_keep_selection() 245 + // methods.save() 240 246 }, 241 247 loadTags: (id: TrackID) => { 242 248 return call((data) => data.load_tags(id)) ··· 267 273 }, 268 274 } 269 275 270 - export const filter = (() => { 271 - const { subscribe, set } = writable('') 272 - return { 273 - subscribe: subscribe, 274 - set: (query: string) => { 275 - call((data) => data.filter_open_playlist(query)) 276 - page.set(page.get()) 277 - pageSelection.clear() 278 - set(query) 279 - }, 280 - } 281 - })() 276 + export const filter = writable('') 282 277 283 278 function create_refresh_store() { 284 279 const store = writable(0) ··· 289 284 }, 290 285 } 291 286 } 292 - export const track_metadata_updated = create_refresh_store() 293 - 294 - export const page = (() => { 295 - function get() { 296 - const info = call((addon) => addon.get_page_info()) 297 - return info 298 - } 299 - function refresh_ids_and_keep_selection() { 300 - call((addon) => addon.refresh_page()) 301 - set(get()) 302 - } 287 + export const tracks_updated = create_refresh_store() 303 288 304 - const { subscribe, set, update } = writable(get()) 305 - return { 306 - subscribe, 307 - get, 308 - set, 309 - update, 310 - refresh_ids_and_keep_selection: refresh_ids_and_keep_selection, 311 - open_playlist(id: string, view_as: ViewAs) { 312 - call((data) => data.open_playlist(id, view_as)) 313 - refresh_ids_and_keep_selection() 314 - pageSelection.clear() 315 - filter.set('') 316 - }, 317 - sort_by(key: string) { 318 - call((addon) => addon.sort(key, true)) 319 - refresh_ids_and_keep_selection() 320 - pageSelection.clear() 321 - }, 322 - set_group_album_tracks(value: boolean) { 323 - call((addon) => addon.set_group_album_tracks(value)) 324 - refresh_ids_and_keep_selection() 325 - pageSelection.clear() 326 - }, 327 - get_artists() { 328 - return call((addon) => addon.get_artists()) 329 - }, 330 - get_track(index: number) { 331 - return call((addon) => addon.get_page_track(index)) 332 - }, 333 - get_track_id(index: number) { 334 - return call((data) => data.get_page_track_id(index)) 335 - }, 336 - get_track_ids() { 337 - return call((data) => data.get_page_track_ids()) 338 - }, 339 - move_tracks: (indexes: number[], to_index: number) => { 340 - const new_selection = call((data) => data.move_tracks(indexes, to_index)) 341 - call((data) => data.refresh_page()) 342 - refresh_ids_and_keep_selection() 343 - pageSelection.clear() 344 - for (let i = new_selection.from; i <= new_selection.to; i++) { 345 - pageSelection.add(i) 346 - } 347 - methods.save() 348 - }, 349 - } 350 - })() 289 + export function get_artists() { 290 + return call((addon) => addon.get_artists()) 291 + } 292 + export function move_tracks(playlist_id: TrackListID, indexes: number[], to_index: number) { 293 + call((data) => data.move_tracks(playlist_id, indexes, to_index)) 294 + tracks_updated.emit() 295 + methods.save() 296 + }
+4 -2
src/lib/menus.ts
··· 2 2 add_track_to_playlist, 3 3 methods, 4 4 paths, 5 - remove_from_open_playlist, 5 + remove_from_playlist, 6 6 track_lists_details_map, 7 7 } from '@/lib/data' 8 8 import { flatten_child_lists } from '@/lib/helpers' ··· 11 11 import { get } from 'svelte/store' 12 12 import { append_to_user_queue, prepend_to_user_queue } from './queue' 13 13 import type { ShowTrackMenuOptions } from '@/electron/typed_ipc' 14 + import { current_playlist_id } from '@/components/TrackList.svelte' 14 15 15 16 export async function show_track_menu( 16 17 all_id: string[], ··· 45 46 ipc_renderer.invoke('revealTrackFile', paths.tracksDir, track.file) 46 47 }) 47 48 ipc_renderer.on('context.Remove from Playlist', (e, indexes: number[]) => { 48 - remove_from_open_playlist(indexes) 49 + const playlist_id = get(current_playlist_id) 50 + remove_from_playlist(playlist_id, indexes) 49 51 })
-18
src/lib/page.ts
··· 1 - import { get } from 'svelte/store' 2 - import { page } from './data' 3 - import { show_track_menu } from './menus' 4 - import { new_selection } from './selection' 5 - 6 1 export const tracklist_actions = { 7 2 scroll_to_index(_index: number) {}, 8 3 focus() { ··· 10 5 if (el instanceof HTMLElement) el.focus() 11 6 }, 12 7 } 13 - export const selection = new_selection({ 14 - get_item_count() { 15 - return get(page).length 16 - }, 17 - scroll_to_item(i) { 18 - tracklist_actions.scroll_to_index?.(i) 19 - }, 20 - async on_context_menu() { 21 - const indexes = selection.getSelectedIndexes() 22 - const ids = page.get_track_ids() 23 - await show_track_menu(ids, indexes, { editable: get(page).tracklist.type === 'playlist' }) 24 - }, 25 - })
+312
src/lib/selection-new.ts
··· 1 + import { writable, type Updater } from 'svelte/store' 2 + 3 + type SelectionOptions = { 4 + scroll_to_item: (index: number) => void 5 + on_context_menu: () => void 6 + } 7 + 8 + export class Selection<T> { 9 + /** Currently selected items */ 10 + selection = new Set<T>() 11 + selection_store = writable(this.selection) 12 + subscribe = this.selection_store.subscribe 13 + /** Full list of items that can be selected */ 14 + all: T[] = [] 15 + /** The last added index */ 16 + last_added: { index: number; item: T } | null = null 17 + /** An anchor index for shift selection. */ 18 + shift_anchor: { index: number; item: T } | null = null 19 + scroll_to_item: SelectionOptions['scroll_to_item'] 20 + on_context_menu: SelectionOptions['on_context_menu'] 21 + 22 + constructor(options: SelectionOptions) { 23 + this.scroll_to_item = options.scroll_to_item 24 + this.on_context_menu = options.on_context_menu 25 + } 26 + 27 + set(new_selection: Set<T>) { 28 + this.selection = new_selection 29 + this.selection_store.set(new_selection) 30 + } 31 + 32 + update(updater: Updater<Set<T>>) { 33 + this.selection = updater(this.selection) 34 + this.selection_store.set(this.selection) 35 + } 36 + 37 + clear() { 38 + this.selection = new Set() 39 + this.last_added = null 40 + this.shift_anchor = null 41 + } 42 + 43 + /** Update the list of items that can be selected. 44 + * Items that no longer exist are de-selected. */ 45 + update_all_items(all: T[]) { 46 + let new_selection = new Set<T>() 47 + for (const item of all) { 48 + if (this.selection.has(item)) { 49 + new_selection.add(item) 50 + } 51 + } 52 + this.all = all 53 + this.selection = new_selection 54 + if (this.last_added !== null && !this.selection.has(this.last_added.item)) { 55 + this.last_added = null 56 + } 57 + if (this.shift_anchor !== null && !this.selection.has(this.shift_anchor.item)) { 58 + this.shift_anchor = null 59 + } 60 + } 61 + 62 + get_shift_anchor() { 63 + if (this.shift_anchor !== null) return this.shift_anchor 64 + else return this.last_added 65 + } 66 + 67 + add_index(index: number) { 68 + this.selection.add(this.all[index]) 69 + this.last_added = { index, item: this.all[index] } 70 + } 71 + 72 + add_range(from_index: number, to_index: number) { 73 + // Direction here determines this.last_added 74 + if (from_index < to_index) { 75 + for (let i = from_index; i <= to_index; i++) { 76 + this.add_index(i) 77 + } 78 + } else { 79 + for (let i = from_index; i >= to_index; i--) { 80 + this.add_index(i) 81 + } 82 + } 83 + } 84 + 85 + remove_range(from_i: number, to_i: number) { 86 + if (from_i < to_i) { 87 + for (let i = from_i; i <= to_i; i++) { 88 + this.selection.delete(this.all[i]) 89 + } 90 + } else { 91 + for (let i = from_i; i >= to_i; i--) { 92 + this.selection.delete(this.all[i]) 93 + } 94 + } 95 + } 96 + 97 + /** Shift-select to index */ 98 + shift_select_to(to_index: number) { 99 + const anchor = this.get_shift_anchor() 100 + const last_added = this.last_added 101 + if (last_added === null || anchor === null) { 102 + return this.selection 103 + } 104 + 105 + if (anchor.index < to_index) { 106 + if (to_index < last_added.index) { 107 + // Retract selection closer to anchor 108 + this.remove_range(to_index + 1, last_added.index) 109 + } else if (last_added.index < anchor.index) { 110 + // New shift selection is on the other side of anchor 111 + this.remove_range(anchor.index - 1, last_added.index) 112 + this.add_range(anchor.index, to_index) 113 + } else { 114 + this.add_range(last_added.index, to_index) 115 + } 116 + this.last_added = { index: to_index, item: this.all[to_index] } 117 + } else { 118 + if (to_index > last_added.index) { 119 + // Retract selection closer to anchor 120 + this.remove_range(to_index - 1, last_added.index) 121 + } else if (last_added.index > anchor.index) { 122 + // New shift selection is on the other side of anchor 123 + this.remove_range(anchor.index + 1, last_added.index) 124 + this.add_range(anchor.index, to_index) 125 + } else { 126 + this.add_range(last_added.index, to_index) 127 + } 128 + this.last_added = { index: to_index, item: this.all[to_index] } 129 + } 130 + this.shift_anchor = anchor 131 + } 132 + 133 + /** Get first selected index, or `null` if selection is empty */ 134 + find_first_index() { 135 + const item_i = this.all.findIndex((item) => this.selection.has(item)) 136 + if (item_i === -1) { 137 + return null 138 + } 139 + return item_i 140 + } 141 + 142 + /** Replace selection with the previous index, like perssing `ArrowUp` in a list. */ 143 + go_backward() { 144 + // selection.clear() 145 + // store.update((selection) => { 146 + // if (selection.count === 0) { 147 + // addIndex(selection, maxIndex) 148 + // } else if (selection.lastAdded !== null) { 149 + // const newIndex = selection.lastAdded - 1 150 + // selection = createEmpty() 151 + // addIndex(selection, Math.max(0, newIndex)) 152 + // } 153 + // return selection 154 + // }) 155 + } 156 + 157 + /** Replace selection with the previous index, like perssing `ArrowDown` in a list. */ 158 + go_forward() { 159 + // store.update((selection) => { 160 + // if (selection.count === 0) { 161 + // addIndex(selection, 0) 162 + // } else if (selection.lastAdded !== null) { 163 + // const newIndex = selection.lastAdded + 1 164 + // selection = createEmpty() 165 + // addIndex(selection, Math.min(newIndex, maxIndex)) 166 + // } 167 + // return selection 168 + // }) 169 + } 170 + /** Expand or shrink selection backwards (shift+up) */ 171 + shift_select_backward() { 172 + // store.update((selection) => { 173 + // const anchor = getShiftAnchor(selection) 174 + // selection.shiftAnchor = anchor 175 + // if (anchor === null || selection.lastAdded === null) { 176 + // return selection 177 + // } 178 + // if (selection.lastAdded <= anchor) { 179 + // // add prev to selection 180 + // for (let i = selection.lastAdded; i >= 0; i--) { 181 + // if (selection.list[i] !== true) { 182 + // addIndex(selection, i) 183 + // return selection 184 + // } 185 + // } 186 + // } else { 187 + // // remove first from selection 188 + // remove(selection, selection.lastAdded) 189 + // selection.lastAdded -= 1 190 + // } 191 + // return selection 192 + // }) 193 + } 194 + /** 195 + * Expand or shrink selection forwards (shift+down). 196 + * - `maxIndex`: The maximum index to expand to 197 + */ 198 + shift_select_forward(maxIndex: number) { 199 + // store.update((selection) => { 200 + // const anchor = getShiftAnchor(selection) 201 + // selection.shiftAnchor = anchor 202 + // if (anchor === null || selection.lastAdded === null) { 203 + // return selection 204 + // } 205 + // if (selection.lastAdded >= anchor) { 206 + // // add next to selection 207 + // for (let i = selection.lastAdded; i <= maxIndex; i++) { 208 + // if (selection.list[i] !== true) { 209 + // addIndex(selection, i) 210 + // return selection 211 + // } 212 + // } 213 + // } else { 214 + // // remove last from selection 215 + // remove(selection, selection.lastAdded) 216 + // selection.lastAdded += 1 217 + // } 218 + // return selection 219 + // }) 220 + } 221 + toggle(index: number) { 222 + // store.update((selection) => { 223 + // if (selection.list[index]) { 224 + // if (selection.lastAdded === index) { 225 + // selection.lastAdded = null 226 + // } 227 + // remove(selection, index) 228 + // } else { 229 + // addIndex(selection, index) 230 + // } 231 + // selection.shiftAnchor = null 232 + // return selection 233 + // }) 234 + } 235 + 236 + // mouse_down_select(e: MouseEvent, index: number) { 237 + // const isSelected = store.get().list[index] 238 + // if (check_mouse_shortcut(e) && !isSelected) { 239 + // selection.clear() 240 + // selection.add(index) 241 + // } else if (check_mouse_shortcut(e, { cmd_or_ctrl: true }) && !isSelected) { 242 + // selection.add(index) 243 + // } else if (check_mouse_shortcut(e, { shift: true })) { 244 + // selection.shift_select_to(index) 245 + // } 246 + // } 247 + 248 + handle_mouse_down(e: MouseEvent, index: number) { 249 + // if (e.button !== 0) { 250 + // return 251 + // } 252 + // if (store.get().list[index]) { 253 + // possible_row_click = true 254 + // } 255 + // mouse_down_select(e, index) 256 + } 257 + handle_contextmenu(e: MouseEvent, index: number) { 258 + // mouse_down_select(e, index) 259 + // options.on_context_menu() 260 + } 261 + handle_click(e: MouseEvent, index: number) { 262 + // if (possible_row_click && e.button === 0) { 263 + // if (check_mouse_shortcut(e)) { 264 + // selection.clear() 265 + // selection.add(index) 266 + // } else if (check_mouse_shortcut(e, { cmd_or_ctrl: true })) { 267 + // selection.toggle(index) 268 + // } 269 + // } 270 + // possible_row_click = false 271 + } 272 + handle_keydown(e: KeyboardEvent) { 273 + // if (check_shortcut(e, 'Escape')) { 274 + // selection.clear() 275 + // } else if (check_shortcut(e, 'A', { cmd_or_ctrl: true })) { 276 + // selection.add(0, options.getItemCount() - 1) 277 + // } else if (check_shortcut(e, 'ArrowUp')) { 278 + // selection.goBackward(options.getItemCount() - 1) 279 + // options.scroll_to_item(store.get().lastAdded || 0) 280 + // } else if (check_shortcut(e, 'ArrowUp', { shift: true })) { 281 + // selection.shiftSelectBackward() 282 + // options.scroll_to_item(store.get().lastAdded || 0) 283 + // } else if (check_shortcut(e, 'ArrowUp', { alt: true })) { 284 + // selection.clear() 285 + // selection.add(0) 286 + // options.scroll_to_item(0) 287 + // } else if (check_shortcut(e, 'ArrowUp', { shift: true, alt: true })) { 288 + // selection.shiftSelectTo(0) 289 + // options.scroll_to_item(store.get().lastAdded || 0) 290 + // } else if (check_shortcut(e, 'ArrowDown')) { 291 + // selection.goForward(options.getItemCount() - 1) 292 + // options.scroll_to_item(store.get().lastAdded || 0) 293 + // } else if (check_shortcut(e, 'ArrowDown', { shift: true })) { 294 + // selection.shiftSelectForward(options.getItemCount() - 1) 295 + // options.scroll_to_item(store.get().lastAdded || 0) 296 + // } else if (check_shortcut(e, 'ArrowDown', { alt: true })) { 297 + // selection.clear() 298 + // selection.add(options.getItemCount() - 1) 299 + // options.scroll_to_item(store.get().lastAdded || 0) 300 + // } else if (check_shortcut(e, 'ArrowDown', { shift: true, alt: true })) { 301 + // selection.shiftSelectTo(options.getItemCount() - 1) 302 + // options.scroll_to_item(store.get().lastAdded || 0) 303 + // } else { 304 + // return 305 + // } 306 + // e.preventDefault() 307 + } 308 + } 309 + 310 + export function new_selection(options: SelectionOptions) { 311 + return new Selection(options) 312 + }