feat: continuous playback

This commit is contained in:
Phan An 2024-03-25 23:59:38 +01:00
parent 67766b49c1
commit d3960ffe62
21 changed files with 99 additions and 54 deletions

View file

@ -16,6 +16,7 @@ final class UserPreferences implements Arrayable, JsonSerializable
'show_album_art_overlay' => 'boolean', 'show_album_art_overlay' => 'boolean',
'lyrics_zoom_level' => 'integer', 'lyrics_zoom_level' => 'integer',
'make_uploads_public' => 'boolean', 'make_uploads_public' => 'boolean',
'continuous_playback' => 'boolean',
]; ];
private const CUSTOMIZABLE_KEYS = [ private const CUSTOMIZABLE_KEYS = [
@ -34,6 +35,7 @@ final class UserPreferences implements Arrayable, JsonSerializable
'lyrics_zoom_level', 'lyrics_zoom_level',
'visualizer', 'visualizer',
'active_extra_panel_tab', 'active_extra_panel_tab',
'continuous_playback',
]; ];
private const ALL_KEYS = self::CUSTOMIZABLE_KEYS + ['lastfm_session_key']; private const ALL_KEYS = self::CUSTOMIZABLE_KEYS + ['lastfm_session_key'];
@ -51,6 +53,7 @@ final class UserPreferences implements Arrayable, JsonSerializable
public bool $showAlbumArtOverlay, public bool $showAlbumArtOverlay,
public bool $makeUploadsPublic, public bool $makeUploadsPublic,
public bool $supportBarNoBugging, public bool $supportBarNoBugging,
public bool $continuousPlayback,
public int $lyricsZoomLevel, public int $lyricsZoomLevel,
public string $visualizer, public string $visualizer,
public ?string $activeExtraPanelTab, public ?string $activeExtraPanelTab,
@ -77,6 +80,7 @@ final class UserPreferences implements Arrayable, JsonSerializable
showAlbumArtOverlay: $data['show_album_art_overlay'] ?? true, showAlbumArtOverlay: $data['show_album_art_overlay'] ?? true,
makeUploadsPublic: $data['make_uploads_public'] ?? false, makeUploadsPublic: $data['make_uploads_public'] ?? false,
supportBarNoBugging: $data['support_bar_no_bugging'] ?? false, supportBarNoBugging: $data['support_bar_no_bugging'] ?? false,
continuousPlayback: $data['continuous_playback'] ?? false,
lyricsZoomLevel: $data['lyrics_zoom_level'] ?? 1, lyricsZoomLevel: $data['lyrics_zoom_level'] ?? 1,
visualizer: $data['visualizer'] ?? 'default', visualizer: $data['visualizer'] ?? 'default',
activeExtraPanelTab: $data['active_extra_panel_tab'] ?? null, activeExtraPanelTab: $data['active_extra_panel_tab'] ?? null,
@ -133,6 +137,7 @@ final class UserPreferences implements Arrayable, JsonSerializable
'lyrics_zoom_level' => $this->lyricsZoomLevel, 'lyrics_zoom_level' => $this->lyricsZoomLevel,
'visualizer' => $this->visualizer, 'visualizer' => $this->visualizer,
'active_extra_panel_tab' => $this->activeExtraPanelTab, 'active_extra_panel_tab' => $this->activeExtraPanelTab,
'continuous_playback' => $this->continuousPlayback,
]; ];
} }

View file

@ -19,15 +19,20 @@ composer install
yarn install yarn install
``` ```
You can now start the development server with `yarn dev`, which is simply a wrapper around `php artisan serve`: You can now start the development server with `yarn dev`:
```bash ```bash
yarn dev $ yarn dev
$ php artisan serve
INFO Server running on [http://127.0.0.1:8000]. VITE v5.1.6 ready in 1549 ms
Press Ctrl+C to stop the server ➜ Local: http://localhost:5173/
➜ Network: use --host to expose
➜ press h + enter to show help
LARAVEL v9.52.0 plugin v1.0.2
➜ APP_URL: http://localhost:8000
``` ```
A development version of Koel should now be available at `http://localhost:8000` with full HMR support. A development version of Koel should now be available at `http://localhost:8000` with full HMR support.

View file

@ -35,6 +35,7 @@ More themes are to be added in the future, along with the ability to create your
Koel allows you to set a couple of preferences: Koel allows you to set a couple of preferences:
* Whether playing a song should trigger continuous playback of the entire playlist, album, artist, or genre
* Whether to show a notification whenever a new song starts playing * Whether to show a notification whenever a new song starts playing
* Whether to confirm before closing Koels browser tab * Whether to confirm before closing Koels browser tab
* Whether to show a translucent, blurred overlay of the current albums art * Whether to show a translucent, blurred overlay of the current albums art

View file

@ -6,6 +6,12 @@
Make uploaded songs public by default Make uploaded songs public by default
</label> </label>
</div> </div>
<div class="form-row">
<label>
<CheckBox v-model="preferences.continuous_playback" name="continuous_playback" />
Playing a song triggers continuous playback of the entire playlist, album, artist, or genre
</label>
</div>
<div v-if="isPhone" class="form-row"> <div v-if="isPhone" class="form-row">
<label> <label>
<CheckBox v-model="preferences.show_now_playing_notification" name="notify" /> <CheckBox v-model="preferences.show_now_playing_notification" name="notify" />

View file

@ -112,11 +112,11 @@ const { showErrorDialog } = useDialogBox()
const { getRouteParam, go, onScreenActivated } = useRouter() const { getRouteParam, go, onScreenActivated } = useRouter()
const albumId = ref<number>() const albumId = ref<number>()
const album = ref<Album>() const album = ref<Album | undefined>()
const songs = ref<Song[]>([]) const songs = ref<Song[]>([])
const loading = ref(false) const loading = ref(false)
let otherAlbums = ref<Album[]>() let otherAlbums = ref<Album[] | undefined>()
let info = ref<ArtistInfo | null>() let info = ref<ArtistInfo | undefined>()
const { const {
SongList, SongList,
@ -126,13 +126,14 @@ const {
showingControls, showingControls,
isPhone, isPhone,
duration, duration,
context,
sort, sort,
onPressEnter, onPressEnter,
playAll, playAll,
playSelected, playSelected,
applyFilter, applyFilter,
onScrollBreakpoint onScrollBreakpoint
} = useSongList(songs) } = useSongList(songs, { type: 'Album' })
const { SongListControls, config } = useSongListControls('Album') const { SongListControls, config } = useSongListControls('Album')
@ -169,6 +170,8 @@ watch(albumId, async id => {
songStore.fetchForAlbum(id) songStore.fetchForAlbum(id)
]) ])
context.entity = album.value
sort('track') sort('track')
} catch (error) { } catch (error) {
logger.error(error) logger.error(error)

View file

@ -87,7 +87,7 @@ const {
onPressEnter, onPressEnter,
playSelected, playSelected,
onScrollBreakpoint onScrollBreakpoint
} = useSongList(toRef(songStore.state, 'songs'), { sortable: true }) } = useSongList(toRef(songStore.state, 'songs'), { type: 'Songs' }, { sortable: true })
const { SongListControls, config } = useSongListControls('Songs') const { SongListControls, config } = useSongListControls('Songs')

View file

@ -121,6 +121,7 @@ const {
songList, songList,
showingControls, showingControls,
isPhone, isPhone,
context,
duration, duration,
sort, sort,
onPressEnter, onPressEnter,
@ -128,7 +129,7 @@ const {
playSelected, playSelected,
applyFilter, applyFilter,
onScrollBreakpoint onScrollBreakpoint
} = useSongList(songs) } = useSongList(songs, { type: 'Artist' })
const { SongListControls, config } = useSongListControls('Artist') const { SongListControls, config } = useSongListControls('Artist')
@ -157,6 +158,8 @@ watch(artistId, async id => {
artistStore.resolve(id), artistStore.resolve(id),
songStore.fetchForArtist(id) songStore.fetchForArtist(id)
]) ])
context.entity = artist.value
} catch (error) { } catch (error) {
logger.error(error) logger.error(error)
showErrorDialog('Failed to load artist. Please try again.', 'Error') showErrorDialog('Failed to load artist. Please try again.', 'Error')

View file

@ -89,7 +89,7 @@ const {
applyFilter, applyFilter,
onScrollBreakpoint, onScrollBreakpoint,
sort sort
} = useSongList(toRef(favoriteStore.state, 'songs')) } = useSongList(toRef(favoriteStore.state, 'songs'), { type: 'Favorites' })
const { SongListControls, config } = useSongListControls('Favorites') const { SongListControls, config } = useSongListControls('Favorites')

View file

@ -70,7 +70,7 @@ const {
onPressEnter, onPressEnter,
playSelected, playSelected,
onScrollBreakpoint onScrollBreakpoint
} = useSongList(ref<Song[]>([])) } = useSongList(ref<Song[]>([]), { type: 'Genre' })
const { SongListControls, config } = useSongListControls('Genre') const { SongListControls, config } = useSongListControls('Genre')

View file

@ -101,6 +101,7 @@ const {
selectedSongs, selectedSongs,
showingControls, showingControls,
isPhone, isPhone,
context,
sortField, sortField,
onPressEnter, onPressEnter,
playAll, playAll,
@ -109,7 +110,7 @@ const {
onScrollBreakpoint, onScrollBreakpoint,
sort: baseSort, sort: baseSort,
config: listConfig config: listConfig
} = useSongList(ref<Song[] | CollaborativeSong[]>([])) } = useSongList(ref<Song[] | CollaborativeSong[]>([]), { type: 'Playlist' })
const { SongListControls, config: controlsConfig } = useSongListControls('Playlist') const { SongListControls, config: controlsConfig } = useSongListControls('Playlist')
const { removeSongsFromPlaylist } = usePlaylistManagement() const { removeSongsFromPlaylist } = usePlaylistManagement()
@ -165,6 +166,7 @@ watch(playlistId, async id => {
sortField.value = null sortField.value = null
playlist.value = playlistStore.byId(id) playlist.value = playlistStore.byId(id)
context.entity = playlist.value
// reset this config value to its default to not cause rows to be mal-rendered // reset this config value to its default to not cause rows to be mal-rendered
listConfig.collaborative = false listConfig.collaborative = false

View file

@ -79,7 +79,7 @@ const {
playSelected, playSelected,
applyFilter, applyFilter,
onScrollBreakpoint onScrollBreakpoint
} = useSongList(toRef(queueStore.state, 'songs'), { reorderable: true, sortable: false }) } = useSongList(toRef(queueStore.state, 'songs'), { type: 'Queue' }, { reorderable: true, sortable: false })
const { SongListControls, config } = useSongListControls('Queue') const { SongListControls, config } = useSongListControls('Queue')

View file

@ -67,7 +67,7 @@ const {
playSelected, playSelected,
applyFilter, applyFilter,
onScrollBreakpoint onScrollBreakpoint
} = useSongList(recentlyPlayedSongs, { sortable: false }) } = useSongList(recentlyPlayedSongs, { type: 'RecentlyPlayed' }, { sortable: false })
const { SongListControls, config } = useSongListControls('RecentlyPlayed') const { SongListControls, config } = useSongListControls('RecentlyPlayed')

View file

@ -58,7 +58,7 @@ const {
applyFilter, applyFilter,
sort, sort,
onScrollBreakpoint onScrollBreakpoint
} = useSongList(toRef(searchStore.state, 'songs')) } = useSongList(toRef(searchStore.state, 'songs'), { type: 'Search.Songs' })
const { SongListControls, config } = useSongListControls('Search.Songs') const { SongListControls, config } = useSongListControls('Search.Songs')
const decodedQ = computed(() => decodeURIComponent(q.value)) const decodedQ = computed(() => decodeURIComponent(q.value))

View file

@ -3,7 +3,14 @@ import { expect, it } from 'vitest'
import factory from '@/__tests__/factory' import factory from '@/__tests__/factory'
import UnitTestCase from '@/__tests__/UnitTestCase' import UnitTestCase from '@/__tests__/UnitTestCase'
import { arrayify } from '@/utils' import { arrayify } from '@/utils'
import { SelectedSongsKey, SongListConfigKey, SongListSortFieldKey, SongListSortOrderKey, SongsKey } from '@/symbols' import {
SelectedSongsKey,
SongListConfigKey,
SongListContextKey,
SongListSortFieldKey,
SongListSortOrderKey,
SongsKey
} from '@/symbols'
import { screen } from '@testing-library/vue' import { screen } from '@testing-library/vue'
import SongList from './SongList.vue' import SongList from './SongList.vue'
@ -16,6 +23,9 @@ new class extends UnitTestCase {
sortable: true, sortable: true,
reorderable: true reorderable: true
}, },
context: SongListContext = {
type: 'Album',
},
selectedSongs: Song[] = [], selectedSongs: Song[] = [],
sortField: SongListSortField = 'title', sortField: SongListSortField = 'title',
sortOrder: SortOrder = 'asc' sortOrder: SortOrder = 'asc'
@ -38,10 +48,11 @@ new class extends UnitTestCase {
}, },
provide: { provide: {
[<symbol>SongsKey]: [ref(songs)], [<symbol>SongsKey]: [ref(songs)],
[<symbol>SelectedSongsKey]: [ref(selectedSongs), value => (selectedSongs = value)], [<symbol>SelectedSongsKey]: [ref(selectedSongs), (value: Song[]) => (selectedSongs = value)],
[<symbol>SongListConfigKey]: [config], [<symbol>SongListConfigKey]: [config],
[<symbol>SongListSortFieldKey]: [sortFieldRef, value => (sortFieldRef.value = value)], [<symbol>SongListContextKey]: [context],
[<symbol>SongListSortOrderKey]: [sortOrderRef, value => (sortOrderRef.value = value)] [<symbol>SongListSortFieldKey]: [sortFieldRef, (value: SongListSortField) => (sortFieldRef.value = value)],
[<symbol>SongListSortOrderKey]: [sortOrderRef, (value: SortOrder) => (sortOrderRef.value = value)]
} }
} }
}) })

View file

@ -90,11 +90,11 @@
@click="onClick(item, $event)" @click="onClick(item, $event)"
@dragleave="onDragLeave" @dragleave="onDragLeave"
@dragstart="onDragStart(item, $event)" @dragstart="onDragStart(item, $event)"
@dragenter.prevent="onDragEnter"
@dragover.prevent="onDragOver" @dragover.prevent="onDragOver"
@drop.prevent="onDrop(item, $event)" @drop.prevent="onDrop(item, $event)"
@dragend.prevent="onDragEnd" @dragend.prevent="onDragEnd"
@contextmenu.prevent="openContextMenu(item, $event)" @contextmenu.prevent="openContextMenu(item, $event)"
@play="onSongPlay(item.song)"
/> />
</VirtualScroller> </VirtualScroller>
</div> </div>
@ -106,10 +106,13 @@ import isMobile from 'ismobilejs'
import { faCaretDown, faCaretUp } from '@fortawesome/free-solid-svg-icons' import { faCaretDown, faCaretUp } from '@fortawesome/free-solid-svg-icons'
import { computed, nextTick, onMounted, Ref, ref, watch } from 'vue' import { computed, nextTick, onMounted, Ref, ref, watch } from 'vue'
import { eventBus, requireInjection } from '@/utils' import { eventBus, requireInjection } from '@/utils'
import { preferenceStore, queueStore } from '@/stores'
import { useDraggable, useDroppable } from '@/composables' import { useDraggable, useDroppable } from '@/composables'
import { playbackService } from '@/services'
import { import {
SelectedSongsKey, SelectedSongsKey,
SongListConfigKey, SongListConfigKey,
SongListContextKey,
SongListFilterKeywordsKey, SongListFilterKeywordsKey,
SongListSortFieldKey, SongListSortFieldKey,
SongListSortOrderKey, SongListSortOrderKey,
@ -137,6 +140,7 @@ const [selectedSongs, setSelectedSongs] = requireInjection<[Ref<Song[]>, Closure
const [sortField, setSortField] = requireInjection<[Ref<SongListSortField>, Closure]>(SongListSortFieldKey) const [sortField, setSortField] = requireInjection<[Ref<SongListSortField>, Closure]>(SongListSortFieldKey)
const [sortOrder, setSortOrder] = requireInjection<[Ref<SortOrder>, Closure]>(SongListSortOrderKey) const [sortOrder, setSortOrder] = requireInjection<[Ref<SortOrder>, Closure]>(SongListSortOrderKey)
const [config] = requireInjection<[Partial<SongListConfig>]>(SongListConfigKey, [{}]) const [config] = requireInjection<[Partial<SongListConfig>]>(SongListConfigKey, [{}])
const [context] = requireInjection<[SongListContext]>(SongListContextKey)
const filterKeywords = requireInjection(SongListFilterKeywordsKey, ref('')) const filterKeywords = requireInjection(SongListFilterKeywordsKey, ref(''))
@ -145,6 +149,12 @@ const lastSelectedRow = ref<SongRow>()
const sortFields = ref<SongListSortField[]>([]) const sortFields = ref<SongListSortField[]>([])
const songRows = ref<SongRow[]>([]) const songRows = ref<SongRow[]>([])
const shouldTriggerContinuousPlayback = computed(() => {
return preferenceStore.continuous_playback
&& typeof context.type !== 'undefined'
&& ['Playlist', 'Album', 'Artist', 'Genre'].includes(context.type)
})
watch( watch(
songRows, songRows,
() => setSelectedSongs(songRows.value.filter(({ selected }) => selected).map(({ song }) => song)), () => setSelectedSongs(songRows.value.filter(({ selected }) => selected).map(({ song }) => song)),
@ -293,7 +303,6 @@ const onDragOver = throttle((event: DragEvent) => {
if (!config.reorderable) return if (!config.reorderable) return
if (acceptsDrop(event)) { if (acceptsDrop(event)) {
// console.log(event)
const target = event.target as HTMLElement const target = event.target as HTMLElement
const rect = target.getBoundingClientRect() const rect = target.getBoundingClientRect()
const midPoint = rect.top + rect.height / 2 const midPoint = rect.top + rect.height / 2
@ -304,17 +313,6 @@ const onDragOver = throttle((event: DragEvent) => {
return false return false
}, 50) }, 50)
const onDragEnter = (event: DragEvent) => {
if (!config.reorderable) return
if (acceptsDrop(event)) {
// ;(event.target as HTMLElement).closest('.song-item')?.classList.add('droppable')
}
return false
}
const onDrop = (row: SongRow, event: DragEvent) => { const onDrop = (row: SongRow, event: DragEvent) => {
if (!config.reorderable || !getDroppedData(event) || !selectedSongs.value.length) { if (!config.reorderable || !getDroppedData(event) || !selectedSongs.value.length) {
wrapper.value?.classList.remove('dragging') wrapper.value?.classList.remove('dragging')
@ -365,6 +363,16 @@ const openContextMenu = async (row: SongRow, event: MouseEvent) => {
eventBus.emit('SONG_CONTEXT_MENU_REQUESTED', event, selectedSongs.value) eventBus.emit('SONG_CONTEXT_MENU_REQUESTED', event, selectedSongs.value)
} }
const onSongPlay = (song: Song) => {
if (shouldTriggerContinuousPlayback.value) {
queueStore.replaceQueueWith(getAllSongsWithSort())
} else {
queueStore.queueIfNotQueued(song)
}
playbackService.play(song)
}
defineExpose({ defineExpose({
getAllSongsWithSort getAllSongsWithSort
}) })

View file

@ -1,7 +1,5 @@
import { expect, it } from 'vitest' import { expect, it } from 'vitest'
import factory from '@/__tests__/factory' import factory from '@/__tests__/factory'
import { queueStore } from '@/stores'
import { playbackService } from '@/services'
import { screen } from '@testing-library/vue' import { screen } from '@testing-library/vue'
import UnitTestCase from '@/__tests__/UnitTestCase' import UnitTestCase from '@/__tests__/UnitTestCase'
import SongListItem from './SongListItem.vue' import SongListItem from './SongListItem.vue'
@ -41,15 +39,10 @@ new class extends UnitTestCase {
expect(html()).toMatchSnapshot() expect(html()).toMatchSnapshot()
}) })
it('plays on double click', async () => { it('emits play event on double click', async () => {
const queueMock = this.mock(queueStore, 'queueIfNotQueued') const { emitted } = this.renderComponent()
const playMock = this.mock(playbackService, 'play')
this.renderComponent()
await this.user.dblClick(screen.getByTestId('song-item')) await this.user.dblClick(screen.getByTestId('song-item'))
expect(emitted().play).toBeTruthy()
expect(queueMock).toHaveBeenCalledWith(row.song)
expect(playMock).toHaveBeenCalledWith(row.song)
}) })
} }
} }

View file

@ -41,8 +41,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { faSquareUpRight } from '@fortawesome/free-solid-svg-icons' import { faSquareUpRight } from '@fortawesome/free-solid-svg-icons'
import { computed, toRefs } from 'vue' import { computed, toRefs } from 'vue'
import { playbackService } from '@/services'
import { queueStore } from '@/stores'
import { requireInjection, secondsToHis } from '@/utils' import { requireInjection, secondsToHis } from '@/utils'
import { useAuthorization, useKoelPlus } from '@/composables' import { useAuthorization, useKoelPlus } from '@/composables'
import { SongListConfigKey } from '@/symbols' import { SongListConfigKey } from '@/symbols'
@ -60,19 +58,16 @@ const { isPlus } = useKoelPlus()
const props = defineProps<{ item: SongRow }>() const props = defineProps<{ item: SongRow }>()
const { item } = toRefs(props) const { item } = toRefs(props)
const emit = defineEmits<{ (e: 'play', song: Song): void }>()
const song = computed<Song | CollaborativeSong>(() => item.value.song) const song = computed<Song | CollaborativeSong>(() => item.value.song)
const playing = computed(() => ['Playing', 'Paused'].includes(song.value.playback_state!)) const playing = computed(() => ['Playing', 'Paused'].includes(song.value.playback_state!))
const external = computed(() => isPlus.value && song.value.owner_id !== currentUser.value?.id) const external = computed(() => isPlus.value && song.value.owner_id !== currentUser.value?.id)
const fmtLength = secondsToHis(song.value.length) const fmtLength = secondsToHis(song.value.length)
const collaborator = computed<Pick<User, 'name' | 'avatar'>>(() => { const collaborator = computed<Pick<User, 'name' | 'avatar'>>(() => (song.value as CollaborativeSong).collaboration.user)
return (song.value as CollaborativeSong).collaboration.user;
})
const play = () => { const play = () => emit('play', song.value)
queueStore.queueIfNotQueued(song.value)
playbackService.play(song.value)
}
</script> </script>
<style lang="scss"> <style lang="scss">

View file

@ -9,6 +9,7 @@ import { useRouter } from '@/composables'
import { import {
SelectedSongsKey, SelectedSongsKey,
SongListConfigKey, SongListConfigKey,
SongListContextKey,
SongListFilterKeywordsKey, SongListFilterKeywordsKey,
SongListSortFieldKey, SongListSortFieldKey,
SongListSortOrderKey, SongListSortOrderKey,
@ -21,10 +22,12 @@ import ThumbnailStack from '@/components/ui/ThumbnailStack.vue'
export const useSongList = ( export const useSongList = (
songs: Ref<Song[]>, songs: Ref<Song[]>,
context: SongListContext = {},
config: Partial<SongListConfig> = { sortable: true, reorderable: false, collaborative: false, hasCustomSort: false } config: Partial<SongListConfig> = { sortable: true, reorderable: false, collaborative: false, hasCustomSort: false }
) => { ) => {
const filterKeywords = ref('') const filterKeywords = ref('')
config = reactive(config) config = reactive(config)
context = reactive(context)
const { isCurrentScreen, go } = useRouter() const { isCurrentScreen, go } = useRouter()
const songList = ref<InstanceType<typeof SongList>>() const songList = ref<InstanceType<typeof SongList>>()
@ -111,6 +114,7 @@ export const useSongList = (
provideReadonly(SongsKey, songs, false) provideReadonly(SongsKey, songs, false)
provideReadonly(SelectedSongsKey, selectedSongs, false) provideReadonly(SelectedSongsKey, selectedSongs, false)
provideReadonly(SongListConfigKey, config) provideReadonly(SongListConfigKey, config)
provideReadonly(SongListContextKey, context)
provideReadonly(SongListSortFieldKey, sortField) provideReadonly(SongListSortFieldKey, sortField)
provideReadonly(SongListSortOrderKey, sortOrder) provideReadonly(SongListSortOrderKey, sortOrder)
@ -122,6 +126,7 @@ export const useSongList = (
ThumbnailStack, ThumbnailStack,
songs, songs,
config, config,
context,
headerLayout, headerLayout,
sortField, sortField,
sortOrder, sortOrder,

View file

@ -20,7 +20,8 @@ export const defaultPreferences: UserPreferences = {
theme: null, theme: null,
visualizer: 'default', visualizer: 'default',
active_extra_panel_tab: null, active_extra_panel_tab: null,
make_uploads_public: false make_uploads_public: false,
continuous_playback: false
} }
const preferenceStore = { const preferenceStore = {

View file

@ -18,5 +18,6 @@ export const SongListConfigKey: ReadonlyInjectionKey<Partial<SongListConfig>> =
export const SongListSortFieldKey: ReadonlyInjectionKey<Ref<SongListSortField>> = Symbol('SongListSortField') export const SongListSortFieldKey: ReadonlyInjectionKey<Ref<SongListSortField>> = Symbol('SongListSortField')
export const SongListSortOrderKey: ReadonlyInjectionKey<Ref<SortOrder>> = Symbol('SongListSortOrder') export const SongListSortOrderKey: ReadonlyInjectionKey<Ref<SortOrder>> = Symbol('SongListSortOrder')
export const SongListFilterKeywordsKey: InjectionKey<Ref<string>> = Symbol('SongListFilterKeywords') export const SongListFilterKeywordsKey: InjectionKey<Ref<string>> = Symbol('SongListFilterKeywords')
export const SongListContextKey: InjectionKey<Ref<SongListContext>> = Symbol('SongListContext')
export const ModalContextKey: InjectionKey<Ref<Record<string, any>>> = Symbol('ModalContext') export const ModalContextKey: InjectionKey<Ref<Record<string, any>>> = Symbol('ModalContext')

View file

@ -268,6 +268,7 @@ interface UserPreferences extends Record<string, any> {
show_now_playing_notification: boolean show_now_playing_notification: boolean
repeat_mode: RepeatMode repeat_mode: RepeatMode
confirm_before_closing: boolean confirm_before_closing: boolean
continuous_playback: boolean
equalizer: EqualizerPreset, equalizer: EqualizerPreset,
artists_view_mode: ArtistAlbumViewMode | null, artists_view_mode: ArtistAlbumViewMode | null,
albums_view_mode: ArtistAlbumViewMode | null, albums_view_mode: ArtistAlbumViewMode | null,
@ -406,6 +407,11 @@ interface SongListConfig {
hasCustomSort: boolean hasCustomSort: boolean
} }
type SongListContext = {
entity?: Playlist | Album | Artist | Genre,
type?: Extract<ScreenName, 'Songs' | 'Album' | 'Artist' | 'Playlist' | 'Favorites' | 'RecentlyPlayed' | 'Queue' | 'Genre' | 'Search.Songs'>
}
type SongListSortField = type SongListSortField =
keyof Pick<Song, 'track' | 'disc' | 'title' | 'album_name' | 'length' | 'artist_name' | 'created_at'> keyof Pick<Song, 'track' | 'disc' | 'title' | 'album_name' | 'length' | 'artist_name' | 'created_at'>
| 'position' | 'position'