Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/kasper9n/ferrum. Music library app for Mac, Linux and Windows ferrum.kasper.space
electron linux macos music music-library music-player napi windows
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388import type { TrackID } from 'ferrum-addon'import { get, writable } from 'svelte/store'import { getter_writable } from './helpers'import { ipc_renderer } from './window'import { track_exists } from '$lib/data.svelte'import quit from './quit'
export const queue_visible = writable(false)export function toggle_queue_visibility() { queue_visible.update((v) => !v)}
export type QueueItem = { qId: number id: TrackID non_shuffle_pos?: number}let last_qid = -1function new_queue_item(id: TrackID): QueueItem { last_qid++ return { qId: last_qid, id, }}
/** * If `hasCurrent` is true, the current track is past[length-1] */export type Queue = { past: QueueItem[] current: { item: QueueItem from_auto_queue: boolean } | null user_queue: QueueItem[] auto_queue: QueueItem[]}export const queue = (() => { const store = getter_writable({ past: [], user_queue: [], current: null, auto_queue: [], } as Queue) return { subscribe: store.subscribe, set: store.set, update: store.update, get: store.get, getCurrent: get_current, getQueueLength: get_queue_length, getByQueueIndex: get_by_queue_index, prependToUserQueue: prepend_to_user_queue, appendToUserQueue: append_to_user_queue, removeIndexes: remove_indexes, removeDeleted: remove_deleted, }})()
let queue_file_path = ''let queue_loaded = falseconst MAX_HISTORY = 1000const SAVE_DEBOUNCE_MS = 1000let pending_save_timeout: ReturnType<typeof setTimeout> | null = null
function limit_history(past: QueueItem[]) { if (past.length <= MAX_HISTORY) return past return past.slice(past.length - MAX_HISTORY)}
function save_queue_state_now() { if (!queue_loaded) return return window.addon .save_queue_state( { past: queue.get().past, current: queue.get().current ?? undefined, user_queue: queue.get().user_queue, auto_queue: queue.get().auto_queue, last_qid, shuffle: get(shuffle), repeat: repeat.get(), }, queue_file_path, ) .catch(() => {})}function save_queue_state() { if (!queue_loaded) return if (pending_save_timeout) { clearTimeout(pending_save_timeout) } pending_save_timeout = setTimeout(() => { pending_save_timeout = null save_queue_state_now() }, SAVE_DEBOUNCE_MS)}
quit.set_handler('queue-state', async () => { if (pending_save_timeout) { clearTimeout(pending_save_timeout) pending_save_timeout = null } await save_queue_state_now()})
export function init_queue_persistence(file_path: string) { if (queue_loaded) return queue_file_path = file_path
const saved = window.addon.load_queue_state(queue_file_path) if (saved) { const past = saved.past.filter((item) => track_exists(item.id)) const current = saved.current && track_exists(saved.current.item.id) ? saved.current : null const user_queue = saved.user_queue.filter((item) => track_exists(item.id)) const auto_queue = saved.auto_queue.filter((item) => track_exists(item.id)) if (current) { past.push(current.item) } const limited_past = limit_history(past)
last_qid = Math.max( -1, saved.last_qid, ...limited_past.map((item) => item.qId), ...user_queue.map((item) => item.qId), ...auto_queue.map((item) => item.qId), ) repeat.set(saved.repeat) shuffle.set(saved.shuffle) queue.set({ past: limited_past, current: null, user_queue, auto_queue }) }
queue.subscribe(save_queue_state) queue_loaded = true}
/** Fisher-Yates shuffle */function shuffle_array<T>(array: Array<T>) { let current_index = array.length, random_index
// While there remain elements to shuffle. while (current_index !== 0) { // Pick a remaining element. random_index = Math.floor(Math.random() * current_index) current_index--
// And swap it with the current element. ;[array[current_index], array[random_index]] = [array[random_index], array[current_index]] }
return array}
export const shuffle = writable(false)function apply_shuffle(shuffle: boolean) { queue.update((q) => { if (shuffle) { for (let i = 0; i < q.auto_queue.length; i++) { q.auto_queue[i].non_shuffle_pos = i } q.auto_queue = shuffle_array(q.auto_queue) return q } else { const old_items: Partial<QueueItem[]> = [] const new_items: QueueItem[] = [] for (const item of q.auto_queue) { if (item.non_shuffle_pos !== undefined) { old_items[item.non_shuffle_pos] = item } else { new_items.push(item) } } const old_items_clean = old_items.filter((i) => i !== undefined) as QueueItem[] q.auto_queue = new_items.concat(old_items_clean) return q } })}shuffle.subscribe(($shuffle) => { apply_shuffle($shuffle) ipc_renderer.invoke('update:Shuffle', $shuffle) save_queue_state()})ipc_renderer.on('Shuffle', () => { shuffle.update((value) => !value)})
export const repeat = getter_writable(false)ipc_renderer.on('Repeat', () => { repeat.update((value) => !value)})repeat.subscribe(($repeat) => { ipc_renderer.invoke('update:Repeat', $repeat) save_queue_state()})
export function get_current() { return queue.get().current?.item ?? null}export function get_queue_length() { const { past, current, user_queue, auto_queue } = queue.get() return past.length + Number(!!current) + user_queue.length + auto_queue.length}export function get_by_queue_index(index: number) { const { past, current, user_queue, auto_queue } = queue.get() if (index < past.length) { return past[index] } index -= past.length if (current && index === 0) { return current.item } index -= Number(!!current) if (index < user_queue.length) { return user_queue[index] } index -= user_queue.length return auto_queue[index]}
export function prepend_to_user_queue(track_ids: TrackID[]) { const items = track_ids.map(new_queue_item) queue.update((q) => { q.user_queue.unshift(...items) return q })}export function append_to_user_queue(track_ids: TrackID[]) { const items = track_ids.map(new_queue_item) queue.update((q) => { q.user_queue.push(...items) return q })}
export function clear_user_queue() { queue.update((q) => { q.user_queue = [] return q })}
export function move_indexes(indexes: number[], new_index: number, to_user_queue: boolean) { const items: QueueItem[] = [] queue.update((q) => { // Sort descending. We need to remove the last indexes first to not mess up the indexes for (const index of indexes.sort((a, b) => b - a)) { const removed_item = remove_index(q, index) if (removed_item) { items.push(removed_item) if (index < new_index) { new_index-- } } else { items.push(new_queue_item(get_by_queue_index(index).id)) } } return q }) // We sorted the indexes descending, so now reverse them return insert_items(items.reverse(), new_index, to_user_queue)}
function insert_items(items: QueueItem[], index: number, to_user_queue: boolean) { queue.update((q) => { const user_queue_index = q.past.length + Number(!!q.current) index -= user_queue_index
if (index < q.user_queue.length || to_user_queue) { q.user_queue.splice(index, 0, ...items) return q } index -= q.user_queue.length q.auto_queue.splice(index, 0, ...items) return q }) return { from: index, to: index + items.length, }}
export function insert_ids(ids: TrackID[], index: number, bias_top = false) { return insert_items(ids.map(new_queue_item), index, bias_top)}
function remove_index(q: Queue, index: number): QueueItem | null { const up_next_index = q.past.length + Number(!!q.current) if (index < up_next_index) { return null } index -= up_next_index if (index < q.user_queue.length) { const [removed] = q.user_queue.splice(index, 1) return removed } index -= q.user_queue.length const [removed] = q.auto_queue.splice(index, 1) return removed}
export function remove_indexes(indexes: number[]) { queue.update((q) => { // we need to remove the last indexes first to not mess up the indexes for (const index of indexes.sort((a, b) => a - b).reverse()) { remove_index(q, index) } return q })}export function remove_deleted() { const q = queue.get() const past = q.past.filter((qi) => track_exists(qi.id)) const current = q.current && track_exists(q.current.item.id) ? q.current : null const user_queue = q.user_queue.filter((qi) => track_exists(qi.id)) const auto_queue = q.auto_queue.filter((qi) => track_exists(qi.id)) if ( past.length !== q.past.length || current !== null || user_queue.length !== q.user_queue.length || auto_queue.length !== q.auto_queue.length ) { queue.set({ past, current, user_queue, auto_queue }) }}
export function next() { const q = queue.get() if (q.current) { q.past.push(q.current.item) q.past = limit_history(q.past) } if (q.user_queue.length) { q.current = { item: q.user_queue.shift()!, from_auto_queue: false, } } else if (q.auto_queue.length) { if (repeat.get() && q.current) { q.auto_queue.push(new_queue_item(q.current.item.id)) } q.current = { item: q.auto_queue.shift()!, from_auto_queue: true, } } else { q.current = null } queue.set(q)}export function prev() { const q = queue.get() if (q.past.length) { if (q.current) { q.user_queue.unshift(q.current.item) } q.current = { item: q.past.pop()!, from_auto_queue: false, } queue.set(q) }}
export function set_new_queue(new_ids: TrackID[], new_current_index: number) { const auto_queue = new_ids.splice(new_current_index + 1) const current = new_ids.pop() queue.update((q) => { if (q.current) { q.past.push(q.current.item) q.past = limit_history(q.past) } q.current = current ? { item: new_queue_item(current), from_auto_queue: true, } : null q.auto_queue = auto_queue.map(new_queue_item) return q }) queue.removeDeleted() apply_shuffle(get(shuffle))}