diff --git a/fe/src/__tests__/AudiobooksView.spec.ts b/fe/src/__tests__/AudiobooksView.spec.ts index 718836de9..1f4225de9 100644 --- a/fe/src/__tests__/AudiobooksView.spec.ts +++ b/fe/src/__tests__/AudiobooksView.spec.ts @@ -796,3 +796,74 @@ describe('AudiobooksView Grouping', () => { expect(wrapper.find('.series-bottom-placard').exists()).toBe(true) }) }) + +describe('AudiobooksView sort persistence', () => { + beforeEach(() => { + localStorage.clear() + const pinia = createPinia() + setActivePinia(pinia) + }) + + function ensureGlobals() { + if (typeof (globalThis as unknown as { ResizeObserver?: unknown }).ResizeObserver === 'undefined') { + ;(globalThis as unknown as Record).ResizeObserver = class { + observe() {} + disconnect() {} + } + } + if (typeof (globalThis as unknown as { WebSocket?: unknown }).WebSocket === 'undefined') { + ;(globalThis as unknown as Record).WebSocket = function () {} + } + } + + async function mountView() { + ensureGlobals() + const pinia = createPinia() + setActivePinia(pinia) + const router = createRouter({ + history: createMemoryHistory(), + routes: [ + { path: '/', name: 'home', component: { template: '
' } }, + { path: '/audiobooks', name: 'audiobooks', component: AudiobooksView }, + ], + }) + await router.push('/audiobooks') + await router.isReady().catch(() => {}) + const store = useLibraryStore() + store.audiobooks = [] as unknown as import('@/types').Audiobook[] + store.fetchLibrary = vi.fn(async () => undefined) + const wrapper = mount(AudiobooksView, { + global: { + plugins: [pinia, router], + stubs: ['BulkEditModal', 'EditAudiobookModal', 'CustomFilterModal', 'FiltersDropdown', 'CustomSelect'], + }, + }) + await new Promise((r) => setTimeout(r, 0)) + return wrapper + } + + it('persists the chosen sort to localStorage', async () => { + const wrapper = await mountView() + const vm = wrapper.vm as unknown as { groupBy: string; sortKey: string; sortOrder: string } + + vm.sortKey = 'publisher' + vm.sortOrder = 'desc' + await wrapper.vm.$nextTick() + + const saved = JSON.parse(localStorage.getItem('listenarr.sortState') || '{}') + expect(saved.books).toEqual({ key: 'publisher', order: 'desc' }) + }) + + it('restores the saved sort on mount instead of defaulting to title', async () => { + localStorage.setItem( + 'listenarr.sortState', + JSON.stringify({ books: { key: 'publisher', order: 'desc' } }), + ) + const wrapper = await mountView() + const vm = wrapper.vm as unknown as { groupBy: string; sortKey: string; sortOrder: string } + + expect(vm.groupBy).toBe('books') + expect(vm.sortKey).toBe('publisher') + expect(vm.sortOrder).toBe('desc') + }) +}) diff --git a/fe/src/views/library/AudiobooksView.vue b/fe/src/views/library/AudiobooksView.vue index 610575b98..7577fcdc3 100644 --- a/fe/src/views/library/AudiobooksView.vue +++ b/fe/src/views/library/AudiobooksView.vue @@ -900,11 +900,50 @@ const DEFAULT_SORTS = { series: { key: 'title', order: 'asc' }, } as const -const sortState = reactive({ - books: { key: 'title', order: 'asc' as 'asc' | 'desc' }, - authors: { key: 'author-last', order: 'asc' as 'asc' | 'desc' }, - series: { key: 'title', order: 'asc' as 'asc' | 'desc' }, -}) +type GroupSort = { key: string; order: 'asc' | 'desc' } +type SortStateShape = Record<'books' | 'authors' | 'series', GroupSort> + +// Persist the chosen sort (key + order, per grouping) so the library keeps the user's last +// preference across visits instead of resetting to Title every time. +const SORT_STATE_KEY = 'listenarr.sortState' + +function loadSortState(): SortStateShape { + const state: SortStateShape = { + books: { ...DEFAULT_SORTS.books }, + authors: { ...DEFAULT_SORTS.authors }, + series: { ...DEFAULT_SORTS.series }, + } + try { + const raw = localStorage.getItem(SORT_STATE_KEY) + if (raw) { + const parsed = JSON.parse(raw) + for (const group of ['books', 'authors', 'series'] as const) { + const saved = parsed?.[group] + if (saved && typeof saved.key === 'string' && (saved.order === 'asc' || saved.order === 'desc')) { + state[group] = { key: saved.key, order: saved.order } + } + } + } + } catch { + // Corrupt/unavailable storage — fall back to defaults. + } + return state +} + +const sortState = reactive(loadSortState()) + +// Persist whenever the sort changes (any grouping). +watch( + sortState, + (value) => { + try { + localStorage.setItem(SORT_STATE_KEY, JSON.stringify(value)) + } catch { + // Ignore storage write failures (private mode / quota). + } + }, + { deep: true }, +) const sortKey = computed({ get: () => sortState[groupBy.value].key,