Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions fe/src/__tests__/AudiobooksView.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>).ResizeObserver = class {
observe() {}
disconnect() {}
}
}
if (typeof (globalThis as unknown as { WebSocket?: unknown }).WebSocket === 'undefined') {
;(globalThis as unknown as Record<string, unknown>).WebSocket = function () {}
}
}

async function mountView() {
ensureGlobals()
const pinia = createPinia()
setActivePinia(pinia)
const router = createRouter({
history: createMemoryHistory(),
routes: [
{ path: '/', name: 'home', component: { template: '<div />' } },
{ 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')
})
})
49 changes: 44 additions & 5 deletions fe/src/views/library/AudiobooksView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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<SortStateShape>(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,
Expand Down