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

Finish new selection implementation

Kasper (Sep 26, 2024, 3:20 AM +0200) 90f81eaf c3bfc04d

+257 -223
+36 -36
src/components/TrackList.svelte
··· 27 27 import Cover from './Cover.svelte' 28 28 import Header from './Header.svelte' 29 29 import { writable } from 'svelte/store' 30 - import { new_selection } from '@/lib/selection-new' 30 + import { SvelteSelection } from '@/lib/selection-new' 31 31 32 32 let tracklist_element: HTMLDivElement 33 33 ··· 47 47 }) 48 48 $: track_indexes = tracks_page.trackIds.map((_, i) => i) 49 49 50 - let selection = new_selection({ 50 + let selection = new SvelteSelection(track_indexes, { 51 51 scroll_to_item(i) { 52 52 tracklist_actions.scroll_to_index?.(i) 53 53 }, ··· 107 107 // } 108 108 } 109 109 async function keydown(e: KeyboardEvent) { 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() 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.size > 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.size > 0) { 136 + e.preventDefault() 137 + // delete_indexes(selection.getSelectedIndexes()) 138 + } else { 139 + selection.handle_keydown(e) 140 + return 141 + } 142 + e.preventDefault() 143 143 } 144 144 145 145 function play_row(index: number) { ··· 463 463 on:drop={drop_handler} 464 464 on:dragend={drag_end_handler} 465 465 class:odd={i % 2 === 0} 466 - class:selected={$selection.has(track.id)} 466 + class:selected={$selection.has(i)} 467 467 class:playing={track.id === $playing_id} 468 468 > 469 469 {#each columns as column}
+221 -187
src/lib/selection-new.ts
··· 1 - import { writable, type Updater } from 'svelte/store' 1 + import { writable, type Updater, type Writable } from 'svelte/store' 2 + import { check_mouse_shortcut, check_shortcut } from './helpers' 2 3 3 4 type SelectionOptions = { 4 5 scroll_to_item: (index: number) => void 5 6 on_context_menu: () => void 6 7 } 7 8 8 - export class Selection<T> { 9 + class Selection<T> { 9 10 /** Currently selected items */ 10 - selection = new Set<T>() 11 - selection_store = writable(this.selection) 12 - subscribe = this.selection_store.subscribe 11 + items = new Set<T>() 13 12 /** Full list of items that can be selected */ 14 - all: T[] = [] 13 + all: T[] 15 14 /** The last added index */ 16 15 last_added: { index: number; item: T } | null = null 17 16 /** An anchor index for shift selection. */ 18 17 shift_anchor: { index: number; item: T } | null = null 18 + /** Whether the user is current mouseup is a click or a selection update */ 19 + possible_row_click = false 20 + 19 21 scroll_to_item: SelectionOptions['scroll_to_item'] 20 22 on_context_menu: SelectionOptions['on_context_menu'] 21 23 22 - constructor(options: SelectionOptions) { 24 + constructor(all_items: T[], options: SelectionOptions) { 25 + this.all = all_items 23 26 this.scroll_to_item = options.scroll_to_item 24 27 this.on_context_menu = options.on_context_menu 25 28 } 26 29 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 30 clear() { 38 - this.selection = new Set() 31 + this.items = new Set() 39 32 this.last_added = null 40 33 this.shift_anchor = null 41 34 } ··· 45 38 update_all_items(all: T[]) { 46 39 let new_selection = new Set<T>() 47 40 for (const item of all) { 48 - if (this.selection.has(item)) { 41 + if (this.items.has(item)) { 49 42 new_selection.add(item) 50 43 } 51 44 } 52 45 this.all = all 53 - this.selection = new_selection 54 - if (this.last_added !== null && !this.selection.has(this.last_added.item)) { 46 + this.items = new_selection 47 + if (this.last_added !== null && !this.items.has(this.last_added.item)) { 55 48 this.last_added = null 56 49 } 57 - if (this.shift_anchor !== null && !this.selection.has(this.shift_anchor.item)) { 50 + if (this.shift_anchor !== null && !this.items.has(this.shift_anchor.item)) { 58 51 this.shift_anchor = null 59 52 } 60 53 } 61 54 62 - get_shift_anchor() { 55 + #get_shift_anchor() { 63 56 if (this.shift_anchor !== null) return this.shift_anchor 64 57 else return this.last_added 65 58 } 66 59 67 60 add_index(index: number) { 68 - this.selection.add(this.all[index]) 61 + this.items.add(this.all[index]) 69 62 this.last_added = { index, item: this.all[index] } 63 + this.shift_anchor = null 70 64 } 71 65 72 - add_range(from_index: number, to_index: number) { 66 + #add_index_in_shift_mode(index: number) { 67 + this.items.add(this.all[index]) 68 + this.last_added = { index, item: this.all[index] } 69 + } 70 + 71 + #add_index_range_in_shift_mode(from_index: number, to_index: number) { 73 72 // Direction here determines this.last_added 74 73 if (from_index < to_index) { 75 74 for (let i = from_index; i <= to_index; i++) { 76 - this.add_index(i) 75 + this.#add_index_in_shift_mode(i) 77 76 } 78 77 } else { 79 78 for (let i = from_index; i >= to_index; i--) { 80 - this.add_index(i) 79 + this.#add_index_in_shift_mode(i) 81 80 } 82 81 } 83 82 } 84 83 85 - remove_range(from_i: number, to_i: number) { 84 + #remove_range_in_shift_mode(from_i: number, to_i: number) { 86 85 if (from_i < to_i) { 87 86 for (let i = from_i; i <= to_i; i++) { 88 - this.selection.delete(this.all[i]) 87 + this.items.delete(this.all[i]) 89 88 } 90 89 } else { 91 90 for (let i = from_i; i >= to_i; i--) { 92 - this.selection.delete(this.all[i]) 91 + this.items.delete(this.all[i]) 93 92 } 94 93 } 95 94 } 96 95 97 96 /** Shift-select to index */ 98 97 shift_select_to(to_index: number) { 99 - const anchor = this.get_shift_anchor() 98 + const anchor = this.#get_shift_anchor() 100 99 const last_added = this.last_added 101 100 if (last_added === null || anchor === null) { 102 - return this.selection 101 + return this.items 103 102 } 104 103 105 104 if (anchor.index < to_index) { 106 105 if (to_index < last_added.index) { 107 106 // Retract selection closer to anchor 108 - this.remove_range(to_index + 1, last_added.index) 107 + this.#remove_range_in_shift_mode(to_index + 1, last_added.index) 109 108 } else if (last_added.index < anchor.index) { 110 109 // 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) 110 + this.#remove_range_in_shift_mode(anchor.index - 1, last_added.index) 111 + this.#add_index_range_in_shift_mode(anchor.index, to_index) 113 112 } else { 114 - this.add_range(last_added.index, to_index) 113 + this.#add_index_range_in_shift_mode(last_added.index, to_index) 115 114 } 116 115 this.last_added = { index: to_index, item: this.all[to_index] } 117 116 } else { 118 117 if (to_index > last_added.index) { 119 118 // Retract selection closer to anchor 120 - this.remove_range(to_index - 1, last_added.index) 119 + this.#remove_range_in_shift_mode(to_index - 1, last_added.index) 121 120 } else if (last_added.index > anchor.index) { 122 121 // 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) 122 + this.#remove_range_in_shift_mode(anchor.index + 1, last_added.index) 123 + this.#add_index_range_in_shift_mode(anchor.index, to_index) 125 124 } else { 126 - this.add_range(last_added.index, to_index) 125 + this.#add_index_range_in_shift_mode(last_added.index, to_index) 127 126 } 128 127 this.last_added = { index: to_index, item: this.all[to_index] } 129 128 } ··· 132 131 133 132 /** Get first selected index, or `null` if selection is empty */ 134 133 find_first_index() { 135 - const item_i = this.all.findIndex((item) => this.selection.has(item)) 134 + const item_i = this.all.findIndex((item) => this.items.has(item)) 136 135 if (item_i === -1) { 137 136 return null 138 137 } ··· 141 140 142 141 /** Replace selection with the previous index, like perssing `ArrowUp` in a list. */ 143 142 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 - // }) 143 + if (this.items.size === 0) { 144 + this.add_index(this.all.length - 1) 145 + } else if (this.last_added !== null) { 146 + const prev_index = this.last_added.index - 1 147 + this.clear() 148 + this.add_index(Math.max(prev_index, 0)) 149 + } 155 150 } 156 151 157 152 /** Replace selection with the previous index, like perssing `ArrowDown` in a list. */ 158 153 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 - // }) 154 + if (this.items.size === 0) { 155 + this.add_index(0) 156 + } else if (this.last_added !== null) { 157 + const next_index = this.last_added.index + 1 158 + this.clear() 159 + this.add_index(Math.min(next_index, this.all.length - 1)) 160 + } 169 161 } 170 162 /** Expand or shrink selection backwards (shift+up) */ 171 163 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 - // }) 164 + const anchor = this.#get_shift_anchor() 165 + this.shift_anchor = anchor 166 + if (anchor === null || this.last_added === null) { 167 + return 168 + } 169 + if (this.last_added.index <= anchor.index) { 170 + // add prev to selection 171 + for (let i = this.last_added.index; i >= 0; i--) { 172 + if (!this.items.has(this.all[i])) { 173 + this.#add_index_in_shift_mode(i) 174 + return 175 + } 176 + } 177 + } else { 178 + // remove first from selection 179 + this.items.delete(this.last_added.item) 180 + this.last_added = { 181 + index: this.last_added.index - 1, 182 + item: this.all[this.last_added.index - 1], 183 + } 184 + } 193 185 } 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 - // }) 186 + /** Expand or shrink selection forwards (shift+down) */ 187 + shift_select_forward() { 188 + const anchor = this.#get_shift_anchor() 189 + this.shift_anchor = anchor 190 + if (anchor === null || this.last_added === null) { 191 + return 192 + } 193 + if (this.last_added.index >= anchor.index) { 194 + // add next to selection 195 + for (let i = this.last_added.index; i < this.all.length; i++) { 196 + if (!this.items.has(this.all[i])) { 197 + this.#add_index_in_shift_mode(i) 198 + return 199 + } 200 + } 201 + } else { 202 + // remove last from selection 203 + this.items.delete(this.last_added.item) 204 + this.last_added = { 205 + index: this.last_added.index + 1, 206 + item: this.all[this.last_added.index + 1], 207 + } 208 + } 220 209 } 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 - // }) 210 + #toggle(index: number) { 211 + if (this.items.has(this.all[index])) { 212 + if (this.last_added && this.last_added.item === this.all[index]) { 213 + this.last_added = null 214 + } 215 + this.items.delete(this.all[index]) 216 + } else { 217 + this.add_index(index) 218 + } 219 + this.shift_anchor = null 234 220 } 235 221 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 - // } 222 + mouse_down_select(e: MouseEvent, index: number) { 223 + const is_selected = this.items.has(this.all[index]) 224 + if (check_mouse_shortcut(e) && !is_selected) { 225 + this.clear() 226 + this.add_index(index) 227 + } else if (check_mouse_shortcut(e, { cmd_or_ctrl: true }) && !is_selected) { 228 + this.add_index(index) 229 + } else if (check_mouse_shortcut(e, { shift: true })) { 230 + this.shift_select_to(index) 231 + } 232 + } 247 233 248 234 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) 235 + if (e.button !== 0) { 236 + return 237 + } 238 + if (this.items.has(this.all[index])) { 239 + this.possible_row_click = true 240 + } 241 + this.mouse_down_select(e, index) 256 242 } 257 243 handle_contextmenu(e: MouseEvent, index: number) { 258 - // mouse_down_select(e, index) 259 - // options.on_context_menu() 244 + this.mouse_down_select(e, index) 245 + this.on_context_menu() 260 246 } 261 247 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 248 + if (this.possible_row_click && e.button === 0) { 249 + if (check_mouse_shortcut(e)) { 250 + this.clear() 251 + this.add_index(index) 252 + } else if (check_mouse_shortcut(e, { cmd_or_ctrl: true })) { 253 + this.#toggle(index) 254 + } 255 + } 256 + this.possible_row_click = false 271 257 } 272 258 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() 259 + if (check_shortcut(e, 'Escape')) { 260 + this.clear() 261 + } else if (check_shortcut(e, 'A', { cmd_or_ctrl: true })) { 262 + this.#add_index_range_in_shift_mode(0, this.all.length - 1) 263 + } else if (check_shortcut(e, 'ArrowUp')) { 264 + this.go_backward() 265 + this.scroll_to_item(this.last_added?.index || 0) 266 + } else if (check_shortcut(e, 'ArrowUp', { shift: true })) { 267 + this.shift_select_backward() 268 + this.scroll_to_item(this.last_added?.index || 0) 269 + } else if (check_shortcut(e, 'ArrowUp', { alt: true })) { 270 + this.clear() 271 + this.add_index(0) 272 + this.scroll_to_item(0) 273 + } else if (check_shortcut(e, 'ArrowUp', { shift: true, alt: true })) { 274 + this.shift_select_to(0) 275 + this.scroll_to_item(this.last_added?.index || 0) 276 + } else if (check_shortcut(e, 'ArrowDown')) { 277 + this.go_forward() 278 + this.scroll_to_item(this.last_added?.index || 0) 279 + } else if (check_shortcut(e, 'ArrowDown', { shift: true })) { 280 + this.shift_select_forward() 281 + this.scroll_to_item(this.last_added?.index || 0) 282 + } else if (check_shortcut(e, 'ArrowDown', { alt: true })) { 283 + this.clear() 284 + this.add_index(this.all.length - 1) 285 + this.scroll_to_item(this.last_added?.index || 0) 286 + } else if (check_shortcut(e, 'ArrowDown', { shift: true, alt: true })) { 287 + this.shift_select_to(this.all.length - 1) 288 + this.scroll_to_item(this.last_added?.index || 0) 289 + } else { 290 + return 291 + } 292 + e.preventDefault() 307 293 } 308 294 } 309 295 310 - export function new_selection(options: SelectionOptions) { 311 - return new Selection(options) 296 + export class SvelteSelection<T> { 297 + readonly selection: Selection<T> 298 + readonly store: Writable<Set<T>> 299 + readonly subscribe: typeof this.store.subscribe 300 + constructor(all_items: T[], options: SelectionOptions) { 301 + this.selection = new Selection(all_items, options) 302 + this.store = writable(this.selection.items) 303 + this.subscribe = this.store.subscribe 304 + } 305 + 306 + clear() { 307 + this.selection.clear() 308 + this.store.set(this.selection.items) 309 + } 310 + 311 + update_all_items(all: T[]) { 312 + this.selection.update_all_items(all) 313 + this.store.set(this.selection.items) 314 + } 315 + 316 + shift_select_to(to_index: number) { 317 + this.selection.shift_select_to(to_index) 318 + this.store.set(this.selection.items) 319 + } 320 + 321 + find_first_index() { 322 + this.selection.find_first_index() 323 + this.store.set(this.selection.items) 324 + } 325 + 326 + mouse_down_select(e: MouseEvent, index: number) { 327 + this.selection.mouse_down_select(e, index) 328 + this.store.set(this.selection.items) 329 + } 330 + handle_mouse_down(e: MouseEvent, index: number) { 331 + this.selection.handle_mouse_down(e, index) 332 + this.store.set(this.selection.items) 333 + } 334 + handle_contextmenu(e: MouseEvent, index: number) { 335 + this.selection.handle_contextmenu(e, index) 336 + this.store.set(this.selection.items) 337 + } 338 + handle_click(e: MouseEvent, index: number) { 339 + this.selection.handle_click(e, index) 340 + this.store.set(this.selection.items) 341 + } 342 + handle_keydown(e: KeyboardEvent) { 343 + this.selection.handle_keydown(e) 344 + this.store.set(this.selection.items) 345 + } 312 346 }