diff --git a/src/renderer/components/TrackList/TrackList.vue b/src/renderer/components/TrackList/TrackList.vue index 28cf263d..2b166313 100644 --- a/src/renderer/components/TrackList/TrackList.vue +++ b/src/renderer/components/TrackList/TrackList.vue @@ -141,7 +141,7 @@ export default { this.activateRadio(false); } if (this.settings.autoReplacePlaylist) { - this.playPlaylist({ tracks: this.trackDetails, source: this.source, firstIndex: index }); + this.playPlaylist({ tracks: this.trackDetails, source: this.source, start: index }); } else { this.playTrack(index); } diff --git a/src/renderer/components/TrackList/TrackListHeader.vue b/src/renderer/components/TrackList/TrackListHeader.vue index 541ff4a8..53829ead 100644 --- a/src/renderer/components/TrackList/TrackListHeader.vue +++ b/src/renderer/components/TrackList/TrackListHeader.vue @@ -20,6 +20,7 @@ import { mapActions } from 'vuex'; export default { inheritAttrs: false, props: { + /** @type {Vue.PropOptions} */ tracks: { type: Array, required: true @@ -32,6 +33,7 @@ export default { } }, computed: { + /** @returns {string} */ btnPlayText() { return `播放全部 (${this.count || this.tracks.length})`; } diff --git a/src/renderer/store/actions.js b/src/renderer/store/actions.js index c37a6718..92236c5d 100644 --- a/src/renderer/store/actions.js +++ b/src/renderer/store/actions.js @@ -424,7 +424,10 @@ export async function playTrackIndex({ state, commit, dispatch }, index) { if (state.ui.radioMode === true) { commit(types.SET_RADIO_INDEX, index); } else { - commit(types.SET_CURRENT_INDEX, index); + commit(types.SET_PLAYLIST_INDEX, index); + if (state.playlist.loopMode === LOOP_MODE.RANDOM) { + commit(types.GENERATE_RANDOM_PLAYLIST, index); + } } await dispatch('updateUiTrack'); dispatch('playAudio'); @@ -432,42 +435,47 @@ export async function playTrackIndex({ state, commit, dispatch }, index) { /** * @param {ActionContext} param0 + * @param {number} number */ -export function playNextTrack({ dispatch, getters }) { +export async function playTrackOffset({ commit, dispatch, state, getters }, payload) { const { index, list, loopMode } = getters.queue; let nextIndex; - switch (loopMode) { - case LOOP_MODE.RANDOM: - nextIndex = Math.floor(Math.random() * list.length); - break; - default: - nextIndex = (index + 1) % list.length; - break; + if (loopMode === LOOP_MODE.RANDOM) { + const { randomIndex, randomList } = state.playlist; + const nextRandomIndex = (randomIndex + randomList.length + payload) % randomList.length; + commit(types.SET_RANDOM_PLAYLIST_INDEX, nextRandomIndex); + nextIndex = randomList[nextRandomIndex]; + } else { + nextIndex = (index + list.length + payload) % list.length; } - dispatch('playTrackIndex', nextIndex); + if (state.ui.radioMode === true) { + commit(types.SET_RADIO_INDEX, nextIndex); + } else { + commit(types.SET_PLAYLIST_INDEX, nextIndex); + } + await dispatch('updateUiTrack'); + dispatch('playAudio'); } /** * @param {ActionContext} param0 */ -export function playPreviousTrack({ dispatch, getters }) { - const { index, list, loopMode } = getters.queue; - let nextIndex; - switch (loopMode) { - case LOOP_MODE.RANDOM: - nextIndex = Math.floor(Math.random() * list.length); - break; - default: - nextIndex = (index + list.length - 1) % list.length; - break; - } - dispatch('playTrackIndex', nextIndex); +export function playNextTrack({ dispatch }) { + dispatch('playTrackOffset', 1); +} + +/** + * @param {ActionContext} param0 + */ +export function playPreviousTrack({ dispatch }) { + dispatch('playTrackOffset', -1); } /** * @param {ActionContext} param0 + * @param {{ tracks: Models.Track[], source?: any, start?: number }} */ -export async function playPlaylist({ commit, dispatch, state }, { tracks, source, firstIndex = -1 }) { +export async function playPlaylist({ commit, dispatch, state }, { tracks, source, start = -1 }) { const list = []; for (const t of tracks) { if (source) { @@ -479,21 +487,25 @@ export async function playPlaylist({ commit, dispatch, state }, { tracks, source if (state.ui.radioMode === true) { commit(types.ACTIVATE_RADIO, false); } - if (firstIndex === -1 && state.playlist.loopMode === LOOP_MODE.RANDOM) { - firstIndex = Math.floor(Math.random() * list.length); - } - if (firstIndex === -1) { - firstIndex = 0; + let nextIndex; + if (start === -1) { + if (state.playlist.loopMode === LOOP_MODE.RANDOM) { + nextIndex = Math.floor(Math.random() * list.length); + commit(types.GENERATE_RANDOM_PLAYLIST, nextIndex); + } else { + nextIndex = 0; + } + } else { + nextIndex = start; } - dispatch('playTrackIndex', firstIndex); + dispatch('playTrackIndex', nextIndex); } /** * @param {ActionContext} context */ export function clearPlaylist({ commit, dispatch }) { - commit(types.SET_PLAY_LIST, []); - commit(types.SET_CURRENT_INDEX, 0); + commit(types.CLEAR_PLAY_LIST); dispatch('updateUiTrack'); } @@ -523,6 +535,9 @@ export async function restorePlaylist({ commit }) { } } commit(types.RESTORE_PLAYLIST, { index, list, loopMode }); + if (loopMode === LOOP_MODE.RANDOM) { + commit(types.GENERATE_RANDOM_PLAYLIST, index); + } } } catch (e) { console.error('restorePlaylist failed:', e); // eslint-disable-line no-console @@ -600,12 +615,13 @@ export async function checkDownloaded({ commit }, { metadata }) { * @param {ActionContext} param0 */ export function nextLoopMode({ commit, state }) { - const { loopMode } = state.playlist; + const { index, loopMode } = state.playlist; switch (loopMode) { case LOOP_MODE.LIST: commit(types.SET_LOOP_MODE_SINGLE); break; case LOOP_MODE.SINGLE: + commit(types.GENERATE_RANDOM_PLAYLIST, index); commit(types.SET_LOOP_MODE_RANDOM); break; case LOOP_MODE.RANDOM: @@ -618,23 +634,30 @@ export function nextLoopMode({ commit, state }) { * @param {ActionContext} param0 * @param {{ tracks: Models.Track[]; source?: any; index?: number }} payload */ -export function insertTrackIntoPlaylist({ commit, state }, payload) { +export function insertTrackIntoPlaylist({ commit, state, getters }, payload) { if (payload.source) { for (const t of payload.tracks) { t.source = payload.source; } } - const index = payload.index || state.playlist.index; - commit(types.INSERT_TRACK_INTO_PLAYLIST, { tracks: payload.tracks, index }); + const start = payload.index || state.playlist.index; + commit(types.INSERT_TRACK_INTO_PLAYLIST, { tracks: payload.tracks, start }); + if (getters.queue.loopMode === LOOP_MODE.RANDOM) { + commit(types.INSERT_TRACK_INTO_RANDOM_PLAYLIST, { start, count: payload.tracks.length }); + } } /** * @param {ActionContext} param0 + * @param {{ start: number; count: number }} payload */ export function removeTrackFromPlaylist({ getters, commit, dispatch }, payload) { - const playingId = getters.playing.id; + const { index, loopMode } = getters.queue; commit(types.REMOVE_TRACK_FROM_PLAYLIST, payload); - if (playingId !== getters.playing.id) { + if (loopMode === LOOP_MODE.RANDOM) { + commit(types.REMOVE_TRACK_FROM_RANDOM_PLAYLIST, payload); + } + if (index >= payload.start) { dispatch('updateUiTrack'); } } diff --git a/src/renderer/store/hooks.js b/src/renderer/store/hooks.js index 8807d908..395347d8 100644 --- a/src/renderer/store/hooks.js +++ b/src/renderer/store/hooks.js @@ -77,14 +77,16 @@ function updatePlaylistTable(mutation) { case SET_PLAY_LIST: PlaylistDb.replace(mutation.payload); break; - case INSERT_TRACK_INTO_PLAYLIST: - const { index, tracks } = mutation.payload; - PlaylistDb.insert(index, tracks); + case INSERT_TRACK_INTO_PLAYLIST: { + const { start, tracks } = mutation.payload; + PlaylistDb.insert(start, tracks); break; - case REMOVE_TRACK_FROM_PLAYLIST: + } + case REMOVE_TRACK_FROM_PLAYLIST: { const { start, count } = mutation.payload; PlaylistDb.remove(start, count); break; + } } } diff --git a/src/renderer/store/modules/playlist.js b/src/renderer/store/modules/playlist.js index 7a558c00..fc2d085a 100644 --- a/src/renderer/store/modules/playlist.js +++ b/src/renderer/store/modules/playlist.js @@ -1,3 +1,8 @@ +// @ts-check + +import range from 'lodash/range'; +import shuffle from 'lodash/shuffle'; + import * as types from '../mutation-types'; export const LOOP_MODE = { @@ -11,6 +16,9 @@ const state = { loopMode: LOOP_MODE.LIST, /** @type {Models.Track[]} */ list: [], + /** @type {number[]} */ + randomList: [], + randomIndex: 0 }; /** @@ -22,9 +30,12 @@ const mutations = { state.list = payload; }, [types.CLEAR_PLAY_LIST](state) { + state.index = 0; state.list = []; + state.randomIndex = 0; + state.randomList = []; }, - [types.SET_CURRENT_INDEX](state, payload) { + [types.SET_PLAYLIST_INDEX](state, payload) { state.index = payload; }, [types.SET_LOOP_MODE_LIST](state) { @@ -41,27 +52,74 @@ const mutations = { state.loopMode = loopMode || LOOP_MODE.LIST; state.list = list; }, - [types.INSERT_TRACK_INTO_PLAYLIST](state, { tracks, index }) { - state.list.splice(index, 0, ...tracks); - if (index <= state.index) { + [types.INSERT_TRACK_INTO_PLAYLIST](state, /** @type {{ start: number, tracks: Models.Track[] }} */ { start, tracks }) { + state.list.splice(start, 0, ...tracks); + if (start <= state.index) { // keep current track unchanged state.index += tracks.length; } }, - [types.REMOVE_TRACK_FROM_PLAYLIST](state, { start, count }) { - state.list.splice(start, count); - const end = start + count - 1; - if (start > state.index) return; - if (end < state.index) { - state.index -= count; - return; - } - // start <= index && end >= index - if (start < state.list.length) { - state.index = start; + [types.REMOVE_TRACK_FROM_PLAYLIST](state, /** @type {{ start: number, count: number }} */ { start, count }) { + const { index, list } = state; + const removed = list.splice(start, count).length; + if (index < start) { + // nothing + } else if (index >= start + removed) { + state.index -= removed; } else { - state.index = state.list.length - 1; + state.index = (start < list.length) ? start : list.length - 1; + } + }, + [types.GENERATE_RANDOM_PLAYLIST](state, /** @type {number} */ payload = 0) { + const arr = shuffle(range(state.list.length)); + state.randomIndex = arr.findIndex(v => v === payload); + state.randomList = arr; + }, + [types.SET_RANDOM_PLAYLIST_INDEX](state, /** @type {number} */ payload) { + state.randomIndex = payload; + }, + // eslint-disable-next-line no-unused-vars + [types.INSERT_TRACK_INTO_RANDOM_PLAYLIST](state, /** @type {{ start: number, count: number }} */ { start, count }) { + const { index, randomIndex, randomList } = state; + // queued track(s) should be played right after current track. + // so it's index in playlist (`index + 1`) should be instered in `randomList` after `randomIndex` + const insertPosition = randomIndex; + const seq = range(index + 1, index + 1 + count); + randomList.splice(insertPosition + 1, 0, ...seq); + const total = randomList.length; + for (let i = 0; i < total; i++) { + if (randomList[i] >= seq[0]) { + if (i <= insertPosition || insertPosition + count < i) { + randomList[i] += count; + } + } + } + }, + [types.REMOVE_TRACK_FROM_RANDOM_PLAYLIST](state, /** @type {{ start: number, count: number }} */ { start, count }) { + const { randomIndex, randomList } = state; + const kept = []; + const removed2 = []; + let offset = 0; + const total = randomList.length; + for (let i = 0; i < total; i++) { + const r = randomList[i]; + // filter out tracks in "removed" range + if (r < start || start + count <= r) { + kept.push(r); + } else { + // for each track removed before randomIndex, + // it shoud decrease by 1 + if (i < randomIndex) offset++; + removed2.push(r); + } + } + for (let i = 0; i < kept.length; i++) { + const k = kept[i]; + // every kept value shoud decrease by count of removed values that less than it + kept[i] -= removed2.filter(r => r < k).length; } + state.randomList = kept; + state.randomIndex = randomIndex - offset; } }; diff --git a/src/renderer/store/mutation-types.js b/src/renderer/store/mutation-types.js index d567c8cb..ae305032 100644 --- a/src/renderer/store/mutation-types.js +++ b/src/renderer/store/mutation-types.js @@ -42,13 +42,17 @@ export const SET_COLLECT_TRACKS = 'SET_COLLECT_TRACKS'; // Playlist export const SET_PLAY_LIST = 'SET_PLAY_LIST'; export const CLEAR_PLAY_LIST = 'CLEAR_PLAY_LIST'; -export const SET_CURRENT_INDEX = 'SET_CURRENT_INDEX'; +export const SET_PLAYLIST_INDEX = 'SET_PLAYLIST_INDEX'; export const SET_LOOP_MODE_LIST = 'SET_LOOP_MODE_LOOP'; export const SET_LOOP_MODE_SINGLE = 'SET_LOOP_MODE_SINGLE'; export const SET_LOOP_MODE_RANDOM = 'SET_LOOP_MODE_RANDOM'; export const RESTORE_PLAYLIST = 'RESTORE_PLAYLIST'; export const INSERT_TRACK_INTO_PLAYLIST = 'INSERT_TRACK_INTO_PLAYLIST'; export const REMOVE_TRACK_FROM_PLAYLIST = 'REMOVE_TRACK_FROM_PLAYLIST'; +export const GENERATE_RANDOM_PLAYLIST = 'GENERATE_RANDOM_PLAYLIST'; +export const SET_RANDOM_PLAYLIST_INDEX = 'SET_RANDOM_PLAYLIST_INDEX'; +export const INSERT_TRACK_INTO_RANDOM_PLAYLIST = 'INSERT_TRACK_INTO_RANDOM_PLAYLIST'; +export const REMOVE_TRACK_FROM_RANDOM_PLAYLIST = 'REMOVE_TRACK_FROM_RANDOM_PLAYLIST'; // Settings export const UPDATE_SETTINGS = 'UPDATE_SETTINGS';