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

Re-enable track context menu

Kasper (Sep 26, 2024, 3:20 AM +0200) 649da60e 682d59f7

+197 -203
+15 -2
src/components/Player.svelte
··· 14 14 import { get_duration } from '../lib/helpers' 15 15 import { queue_visible, toggle_queue_visibility, queue, shuffle, repeat } from '../lib/queue' 16 16 import { is_dev, methods } from '../lib/data' 17 - import { show_track_menu } from '@/lib/menus' 18 17 import { dragged } from '@/lib/drag-drop' 19 18 import * as dragGhost from './DragGhost.svelte' 20 19 import Slider from './Slider.svelte' 20 + import { get_flattened_tracklists, handle_selected_tracks_action } from '@/lib/menus' 21 + import { ipc_renderer } from '@/lib/window' 21 22 22 23 async function playing_context_menu() { 23 24 const playing = queue.getCurrent() 24 25 if (playing) { 25 - await show_track_menu([playing.id], [0]) 26 + const action = await ipc_renderer.invoke('show_tracks_menu', { 27 + is_editable_playlist: false, 28 + queue: false, 29 + lists: get_flattened_tracklists(), 30 + }) 31 + if (action !== null) { 32 + handle_selected_tracks_action({ 33 + action, 34 + track_ids: [playing.id], 35 + all_ids: [playing.id], 36 + first_index: 0, 37 + }) 38 + } 26 39 } 27 40 } 28 41
+32 -43
src/components/Queue.svelte
··· 10 10 queue, 11 11 type QueueItem, 12 12 } from '../lib/queue' 13 - import { paths } from '../lib/data' 13 + import { add_tracks_to_playlist, paths } from '../lib/data' 14 14 import { onDestroy } from 'svelte' 15 15 import QueueItemComponent from './QueueItem.svelte' 16 16 import { new_selection } from '@/lib/selection' 17 - import { show_track_menu } from '@/lib/menus' 18 17 import { dragged } from '@/lib/drag-drop' 19 18 import { methods } from '@/lib/data' 20 19 import * as dragGhost from './DragGhost.svelte' ··· 24 23 import VirtualListBlock, { scroll_container_keydown } from './VirtualListBlock.svelte' 25 24 import { open_track_info } from './TrackInfo.svelte' 26 25 import Button from './Button.svelte' 26 + import type { SelectedTracksAction } from '@/electron/typed_ipc' 27 + import { get_flattened_tracklists, handle_selected_tracks_action } from '@/lib/menus' 27 28 28 29 let object_urls: string[] = [] 29 30 ··· 60 61 autoplay_list.scroll_to_index(i, 40) 61 62 }, 62 63 async on_context_menu() { 63 - const indexes = selection.getSelectedIndexes() 64 - const current_array = $queue.current ? [$queue.current.item] : [] 65 - const all_items = [ 66 - ...$queue.past, 67 - ...current_array, 68 - ...$queue.user_queue, 69 - ...$queue.auto_queue, 70 - ] 71 - const all_ids = all_items.map((item) => item.id) 72 - await show_track_menu(all_ids, indexes, undefined, true) 64 + const action = await ipc_renderer.invoke('show_tracks_menu', { 65 + is_editable_playlist: false, 66 + queue: true, 67 + lists: get_flattened_tracklists(), 68 + }) 69 + if (action !== null) { 70 + handle_action(action) 71 + } 73 72 }, 74 73 }) 75 74 $: selection.setMinimumIndex(show_history ? 0 : up_next_index) ··· 85 84 86 85 let queue_element: HTMLElement 87 86 88 - const track_action_unlisten = ipc_listen('selectedTracksAction', (_, action) => { 89 - let first_index = selection.findFirst() 87 + function handle_action(action: SelectedTracksAction) { 88 + const first_index = selection.findFirst() 89 + const indexes = selection.getSelectedIndexes() 90 + const track_ids = indexes.map((i) => queue.getByQueueIndex(i).id) 91 + const all_items = [ 92 + ...$queue.past, 93 + ...($queue.current ? [$queue.current.item] : []), 94 + ...$queue.user_queue, 95 + ...$queue.auto_queue, 96 + ] 97 + const all_ids = all_items.map((item) => item.id) 98 + handle_selected_tracks_action({ 99 + action, 100 + track_ids: track_ids, 101 + all_ids, 102 + first_index, 103 + }) 104 + } 90 105 91 - if (first_index === null || !queue_element.contains(document.activeElement)) { 92 - return 93 - } 94 - if (action === 'Play Next') { 95 - const indexes = selection.getSelectedIndexes() 96 - const ids = indexes.map((i) => queue.getByQueueIndex(i).id) 97 - prepend_to_user_queue(ids) 98 - } else if (action === 'Add to Queue') { 99 - const indexes = selection.getSelectedIndexes() 100 - const ids = indexes.map((i) => queue.getByQueueIndex(i).id) 101 - append_to_user_queue(ids) 102 - } else if (action === 'Get Info') { 103 - const all_items = [ 104 - ...$queue.past, 105 - ...($queue.current ? [$queue.current.item] : []), 106 - ...$queue.user_queue, 107 - ...$queue.auto_queue, 108 - ] 109 - const all_ids = all_items.map((item) => item.id) 110 - open_track_info(all_ids, first_index) 111 - } else if (action === 'revealTrackFile') { 112 - const track = methods.getTrack(queue.getByQueueIndex(first_index).id) 113 - ipc_renderer.invoke('revealTrackFile', paths.tracksDir, track.file) 114 - } else if (action === 'Remove from Playlist') { 115 - return 116 - } else if (action === 'Delete from Library') { 117 - return 118 - } else { 119 - assert_unreachable(action) 106 + const track_action_unlisten = ipc_listen('selected_tracks_action', (_, action) => { 107 + if (queue_element.contains(document.activeElement)) { 108 + handle_action(action) 120 109 } 121 110 }) 122 111 onDestroy(track_action_unlisten)
+32 -29
src/components/TrackList.svelte
··· 44 44 import Header from './Header.svelte' 45 45 import { writable } from 'svelte/store' 46 46 import { SvelteSelection } from '@/lib/selection-new' 47 - import { show_track_menu } from '@/lib/menus' 47 + import { get_flattened_tracklists, handle_selected_tracks_action } from '@/lib/menus' 48 + import type { SelectedTracksAction } from '@/electron/typed_ipc' 48 49 49 50 let tracklist_element: HTMLDivElement 50 51 ··· 69 70 groupAlbumTracks: $group_album_tracks, 70 71 }) 71 72 73 + function handle_action(action: SelectedTracksAction) { 74 + if (action === 'Remove from Playlist') { 75 + remove_from_playlist($current_playlist_id, selection.items_as_array()) 76 + return 77 + } else if (action === 'Delete from Library') { 78 + delete_tracks(selection.items_as_array()) 79 + } else { 80 + handle_selected_tracks_action({ 81 + action, 82 + track_ids: methods.get_track_ids(selection.items_as_array()), 83 + all_ids: methods.get_track_ids(tracks_page.itemIds), 84 + first_index: selection.find_first_index(), 85 + }) 86 + } 87 + } 88 + 72 89 let selection = new SvelteSelection(tracks_page.itemIds, { 73 90 scroll_to_item(i) { 74 91 tracklist_actions.scroll_to_index?.(i) 75 92 }, 76 - async on_contextmenu(selected_track_indexes) { 77 - // await show_track_menu(tracks_page.trackIds, [...selected_track_indexes], { 78 - // editable: tracks_page.playlistKind === 'playlist', 79 - // }) 93 + async on_contextmenu() { 94 + const action = await ipc_renderer.invoke('show_tracks_menu', { 95 + is_editable_playlist: tracks_page.playlistKind === 'playlist', 96 + queue: false, 97 + lists: get_flattened_tracklists(), 98 + }) 99 + if (action !== null) { 100 + handle_action(action) 101 + } 80 102 }, 81 103 }) 82 104 $: selection.update_all_items(tracks_page.itemIds) 83 105 84 - const track_action_unlisten = ipc_listen('selectedTracksAction', (_, action) => { 85 - let first_item_id = selection.find_first() 86 - if (first_item_id === undefined || !tracklist_element.contains(document.activeElement)) { 87 - return 88 - } 89 - if (action === 'Play Next') { 90 - // const selected_item_ids = Array.from(selection.items) 91 - // prepend_to_user_queue(methods.get_track_ids(selected_item_ids)) 92 - } else if (action === 'Add to Queue') { 93 - // const selected_item_ids = Array.from(selection.items) 94 - // append_to_user_queue(methods.get_track_ids(selected_item_ids)) 95 - } else if (action === 'Get Info') { 96 - // open_track_info(tracks_page.trackIds, first_index) 97 - } else if (action === 'revealTrackFile') { 98 - const { track } = methods.get_track_by_item_id(first_item_id) 99 - ipc_renderer.invoke('revealTrackFile', paths.tracksDir, track.file) 100 - } else if (action === 'Remove from Playlist') { 101 - // remove_from_playlist(params.playlist_id, item_ids) 102 - } else if (action === 'Delete from Library') { 103 - // delete_indexes(selection.items) 104 - } else { 105 - assert_unreachable(action) 106 + const track_action_unlisten = ipc_listen('selected_tracks_action', (_, action) => { 107 + if (tracklist_element.contains(document.activeElement)) { 108 + handle_action(action) 106 109 } 107 110 }) 108 111 onDestroy(track_action_unlisten) ··· 116 119 play_row(index) 117 120 } 118 121 } 119 - async function delete_indexes(indexes: number[]) { 122 + async function delete_tracks(item_ids: ItemId[]) { 120 123 // const s = $selection.count > 1 ? 's' : '' 121 124 // const result = await ipc_renderer.invoke('showMessageBox', false, { 122 125 // type: 'info', ··· 156 159 // } 157 160 } else if (check_shortcut(e, 'Backspace', { cmd_or_ctrl: true }) && $selection.size > 0) { 158 161 e.preventDefault() 159 - // delete_indexes(selection.getSelectedIndexes()) 162 + // delete_tracks(selection.getSelectedIndexes()) 160 163 } else { 161 164 selection.handle_keydown(e) 162 165 return
+44 -52
src/electron/ipc.ts
··· 1 - import { dialog, Menu, shell, BrowserWindow } from 'electron' 1 + import { dialog, Menu, shell, BrowserWindow, type MenuItemConstructorOptions } from 'electron' 2 2 import { ipc_main } from './typed_ipc' 3 3 import path from 'path' 4 4 import is from './is' ··· 33 33 } 34 34 }) 35 35 36 - ipc_main.handle('showTrackMenu', (e, options) => { 37 - let queue_menu: Electron.MenuItemConstructorOptions[] = [] 38 - if (options.queue) { 39 - queue_menu = [ 36 + ipc_main.handle('show_tracks_menu', (e, options) => { 37 + return new Promise((resolve) => { 38 + const menu = Menu.buildFromTemplate([ 40 39 { 41 40 label: 'Remove from Queue', 42 41 click: () => { 43 42 e.sender.send('context.Remove from Queue') 44 43 }, 44 + visible: options.queue, 45 + }, 46 + { type: 'separator', visible: options.queue }, 47 + { 48 + label: 'Play Next', 49 + click: () => resolve('Play Next'), 50 + }, 51 + { 52 + label: 'Add to Queue', 53 + click: () => resolve('Add to Queue'), 54 + }, 55 + { 56 + label: 'Add to Playlist', 57 + submenu: options.lists.map((item) => { 58 + return { 59 + ...item, 60 + click: () => resolve({ action: 'Add to Playlist', playlist_id: item.id }), 61 + } 62 + }), 45 63 }, 46 64 { type: 'separator' }, 47 - ] 48 - } 49 - const selected_ids = options.selectedIndexes.map((i) => options.allIds[i]) 50 - const menu = Menu.buildFromTemplate([ 51 - ...queue_menu, 52 - { 53 - label: 'Play Next', 54 - click: () => { 55 - e.sender.send('context.Play Next', selected_ids) 65 + { 66 + label: 'Get Info', 67 + click: () => resolve('Get Info'), 56 68 }, 57 - }, 58 - { 59 - label: 'Add to Queue', 60 - click: () => { 61 - e.sender.send('context.Add to Queue', selected_ids) 69 + { type: 'separator' }, 70 + { 71 + label: (() => { 72 + if (is.mac) return 'Reveal in Finder' 73 + else if (is.windows) return 'Reveal in File Explorer' 74 + else return 'Reveal in File Manager' 75 + })(), 76 + click: () => resolve('reveal_track_file'), 62 77 }, 63 - }, 64 - { 65 - label: 'Add to Playlist', 66 - submenu: options.lists.map((item) => { 67 - return { 68 - ...item, 69 - click: () => e.sender.send('context.Add to Playlist', item.id, selected_ids), 70 - } 71 - }), 72 - }, 73 - { type: 'separator' }, 74 - { 75 - label: 'Get Info', 76 - click: () => { 77 - e.sender.send('context.Get Info', options.allIds, options.selectedIndexes[0]) 78 + { type: 'separator', visible: options.is_editable_playlist === true }, 79 + { 80 + label: 'Remove from Playlist', 81 + click: () => resolve('Remove from Playlist'), 82 + visible: options.is_editable_playlist === true, 78 83 }, 79 - }, 80 - { type: 'separator' }, 81 - { 82 - label: (() => { 83 - if (is.mac) return 'Reveal in Finder' 84 - else if (is.windows) return 'Reveal in File Explorer' 85 - else return 'Reveal in File Manager' 86 - })(), 87 - click: () => e.sender.send('context.revealTrackFile', selected_ids[0]), 88 - }, 89 - { type: 'separator', visible: options.playlist?.editable === true }, 90 - { 91 - label: 'Remove from Playlist', 92 - click: () => e.sender.send('context.Remove from Playlist', options.selectedIndexes), 93 - visible: options.playlist?.editable === true, 94 - }, 95 - ]) 96 - menu.popup() 84 + ]) 85 + menu.popup({ 86 + callback: () => resolve(null), 87 + }) 88 + }) 97 89 }) 98 90 99 91 ipc_main.handle('showTracklistMenu', (e, args) => {
+6 -6
src/electron/menubar.ts
··· 101 101 label: 'Play Next', 102 102 accelerator: '', 103 103 click: () => { 104 - web_contents.send('selectedTracksAction', 'Play Next') 104 + web_contents.send('selected_tracks_action', 'Play Next') 105 105 }, 106 106 }, 107 107 { 108 108 label: 'Add to Queue', 109 109 accelerator: '', 110 110 click: () => { 111 - web_contents.send('selectedTracksAction', 'Add to Queue') 111 + web_contents.send('selected_tracks_action', 'Add to Queue') 112 112 }, 113 113 }, 114 114 { type: 'separator' }, ··· 116 116 label: 'Get Info', 117 117 accelerator: 'CmdOrCtrl+I', 118 118 click: () => { 119 - web_contents.send('selectedTracksAction', 'Get Info') 119 + web_contents.send('selected_tracks_action', 'Get Info') 120 120 }, 121 121 }, 122 122 { type: 'separator' }, ··· 128 128 })(), 129 129 accelerator: 'Shift+CmdOrCtrl+R', 130 130 click: () => { 131 - web_contents.send('selectedTracksAction', 'revealTrackFile') 131 + web_contents.send('selected_tracks_action', 'reveal_track_file') 132 132 }, 133 133 }, 134 134 { type: 'separator' }, ··· 136 136 label: 'Remove from Playlist', 137 137 accelerator: '', 138 138 click: () => { 139 - web_contents.send('selectedTracksAction', 'Remove from Playlist') 139 + web_contents.send('selected_tracks_action', 'Remove from Playlist') 140 140 }, 141 141 }, 142 142 { 143 143 label: 'Delete from Library', 144 144 accelerator: 'CmdOrCtrl+Backspace', 145 145 click: () => { 146 - web_contents.send('selectedTracksAction', 'Delete from Library') 146 + web_contents.send('selected_tracks_action', 'Delete from Library') 147 147 }, 148 148 }, 149 149 ],
+13 -17
src/electron/typed_ipc.ts
··· 6 6 IpcRendererEvent, 7 7 WebContents as ElectronWebContents, 8 8 dialog, 9 + MenuItemConstructorOptions, 9 10 } from 'electron' 10 11 import { ipcMain as electronIpcMain } from 'electron' 11 12 import type { Track, TrackID } from '../../ferrum-addon' ··· 88 89 ): Promise<ReturnType<IpcCommands[K]>> 89 90 } 90 91 92 + export type SelectedTracksAction = 93 + | 'Play Next' 94 + | 'Add to Queue' 95 + | { action: 'Add to Playlist'; playlist_id: string } 96 + | 'Get Info' 97 + | 'reveal_track_file' 98 + | 'Remove from Playlist' 99 + | 'Delete from Library' 100 + 91 101 type Events = { 92 102 readyToQuit: () => void 93 103 gonnaQuit: () => void ··· 109 119 ToggleQuickNav: () => void 110 120 'Group Album Tracks': (checked: boolean) => void 111 121 112 - selectedTracksAction: ( 113 - action: 114 - | 'Play Next' 115 - | 'Add to Queue' 116 - | 'Get Info' 117 - | 'revealTrackFile' 118 - | 'Remove from Playlist' 119 - | 'Delete from Library', 120 - ) => void 122 + selected_tracks_action: (action: SelectedTracksAction) => void 121 123 122 124 Back: () => void 123 125 Forward: () => void ··· 127 129 'context.playlist.edit': (id: TrackID) => void 128 130 'context.playlist.delete': (id: TrackID) => void 129 131 'context.Remove from Queue': () => void 130 - 'context.Play Next': (ids: TrackID[]) => void 131 - 'context.Add to Queue': (ids: TrackID[]) => void 132 - 'context.Add to Playlist': (id: string, trackIds: TrackID[]) => void 133 - 'context.revealTrackFile': (id: TrackID) => void 134 132 'context.Get Info': (allIds: TrackID[], selectedIndex: number) => void 135 133 'context.Remove from Playlist': (selectedIndexes: number[]) => void 136 134 'context.toggle_column': (item: { id: string; label: string; checked: boolean }) => void 137 135 } 138 136 139 137 export type ShowTrackMenuOptions = { 140 - allIds: string[] 141 - selectedIndexes: number[] 142 - playlist?: { editable: boolean } 143 138 lists: { label: string; enabled: boolean; id: string }[] 139 + is_editable_playlist: boolean 144 140 queue: boolean 145 141 } 146 142 ··· 155 151 options: Parameters<typeof dialog.showOpenDialog>[0], 156 152 ) => ReturnType<typeof dialog.showOpenDialog> 157 153 revealTrackFile: (...paths: string[]) => void 158 - showTrackMenu: (options: ShowTrackMenuOptions) => void 154 + show_tracks_menu: (options: ShowTrackMenuOptions) => null | SelectedTracksAction 159 155 showTracklistMenu: (options: { id: string; isFolder: boolean; isRoot: boolean }) => void 160 156 show_columns_menu: (options: { menu: Electron.MenuItemConstructorOptions[] }) => void 161 157 volume_change: (up: boolean) => void
+33 -35
src/lib/menus.ts
··· 7 7 } from '@/lib/data' 8 8 import { flatten_child_lists } from '@/lib/helpers' 9 9 import { ipc_renderer } from '@/lib/window' 10 - import type { TrackID } from '../../ferrum-addon' 10 + import type { ItemId, TrackID } from '../../ferrum-addon' 11 11 import { get } from 'svelte/store' 12 12 import { append_to_user_queue, prepend_to_user_queue } from './queue' 13 - import type { ShowTrackMenuOptions } from '@/electron/typed_ipc' 13 + import type { SelectedTracksAction, ShowTrackMenuOptions } from '@/electron/typed_ipc' 14 14 import { current_playlist_id } from '@/components/TrackList.svelte' 15 + import { open_track_info } from '@/components/TrackInfo.svelte' 15 16 16 - export async function show_track_menu( 17 - all_ids: string[], 18 - selected_indexes: number[], 19 - playlist?: { editable: boolean }, 20 - queue = false, 21 - ) { 17 + export function get_flattened_tracklists() { 22 18 const track_lists = get(track_lists_details_map) 23 - const flat = flatten_child_lists(track_lists.root, track_lists, '') 19 + return flatten_child_lists(track_lists.root, track_lists, '') 20 + } 24 21 25 - const args: ShowTrackMenuOptions = { 26 - allIds: all_ids, 27 - selectedIndexes: selected_indexes, 28 - playlist, 29 - queue, 30 - lists: flat, 22 + export function handle_selected_tracks_action({ 23 + action, 24 + track_ids, 25 + all_ids, 26 + first_index, 27 + }: { 28 + action: SelectedTracksAction 29 + track_ids: TrackID[] 30 + all_ids: TrackID[] 31 + first_index: number | null 32 + }) { 33 + console.log('hsta', track_ids.length, first_index) 34 + if (track_ids.length === 0 || first_index === null) { 35 + return 31 36 } 32 - console.log('args.allids', args.allIds) 37 + const first_track_id = track_ids[0] 33 38 34 - await ipc_renderer.invoke('showTrackMenu', args) 39 + if (action === 'Play Next') { 40 + prepend_to_user_queue(track_ids) 41 + } else if (action === 'Add to Queue') { 42 + append_to_user_queue(track_ids) 43 + } else if (action === 'Get Info') { 44 + open_track_info(all_ids, first_index) 45 + } else if (action === 'reveal_track_file') { 46 + const track = methods.getTrack(first_track_id) 47 + ipc_renderer.invoke('revealTrackFile', paths.tracksDir, track.file) 48 + } else if (typeof action === 'object' && action.action === 'Add to Playlist') { 49 + add_track_to_playlist(action.playlist_id, track_ids) 50 + } 35 51 } 36 - 37 - ipc_renderer.on('context.Play Next', (e, ids: TrackID[]) => { 38 - prepend_to_user_queue(ids) 39 - }) 40 - ipc_renderer.on('context.Add to Queue', (e, ids: TrackID[]) => { 41 - append_to_user_queue(ids) 42 - }) 43 - ipc_renderer.on('context.Add to Playlist', (e, id: TrackID, track_ids: TrackID[]) => { 44 - add_track_to_playlist(id, track_ids) 45 - }) 46 - ipc_renderer.on('context.revealTrackFile', (e, id: TrackID) => { 47 - const track = methods.getTrack(id) 48 - ipc_renderer.invoke('revealTrackFile', paths.tracksDir, track.file) 49 - }) 50 - ipc_renderer.on('context.Remove from Playlist', (e, indexes: number[]) => { 51 - const playlist_id = get(current_playlist_id) 52 - remove_from_playlist(playlist_id, indexes) 53 - })
+22 -19
src/lib/selection-new.ts
··· 52 52 } 53 53 } 54 54 55 + /** Get first selected index, or `null` if selection is empty */ 56 + find_first_index() { 57 + const item_i = this.all.findIndex((item) => this.items.has(item)) 58 + if (item_i === -1) { 59 + return null 60 + } 61 + return item_i 62 + } 63 + 64 + /** Get first selected item, or `undefined` if selection is empty */ 65 + find_first() { 66 + const item_i = this.all.find((item) => this.items.has(item)) 67 + return item_i 68 + } 69 + 70 + items_as_array() { 71 + return this.all.filter((item) => this.items.has(item)) 72 + } 73 + 55 74 #get_shift_anchor() { 56 75 if (this.shift_anchor !== null) return this.shift_anchor 57 76 else return this.last_added ··· 127 146 this.last_added = { index: to_index, item: this.all[to_index] } 128 147 } 129 148 this.shift_anchor = anchor 130 - } 131 - 132 - /** Get first selected index, or `null` if selection is empty */ 133 - find_first_index() { 134 - const item_i = this.all.findIndex((item) => this.items.has(item)) 135 - if (item_i === -1) { 136 - return null 137 - } 138 - return item_i 139 - } 140 - 141 - /** Get first selected item, or `undefined` if selection is empty */ 142 - find_first() { 143 - const item_i = this.all.find((item) => this.items.has(item)) 144 - return item_i 145 149 } 146 150 147 151 /** Replace selection with the previous index, like perssing `ArrowUp` in a list. */ ··· 314 318 find_first_index() { 315 319 return this.#selection.find_first_index() 316 320 } 317 - 318 321 find_first() { 319 322 return this.#selection.find_first() 323 + } 324 + items_as_array() { 325 + return this.#selection.items_as_array() 320 326 } 321 327 322 328 clear() { 323 329 this.#selection.clear() 324 330 this.#store.set(this.#selection.items) 325 331 } 326 - 327 332 update_all_items(all: T[]) { 328 333 this.#selection.update_all_items(all) 329 334 this.#store.set(this.#selection.items) 330 335 } 331 - 332 336 shift_select_to(to_index: number) { 333 337 this.#selection.shift_select_to(to_index) 334 338 this.#store.set(this.#selection.items) 335 339 } 336 - 337 340 mouse_down_select(e: MouseEvent, index: number) { 338 341 this.#selection.mouse_down_select(e, index) 339 342 this.#store.set(this.#selection.items)