diff --git a/frontend/src/lib/filterUrlCompression.test.ts b/frontend/src/lib/filterUrlCompression.test.ts new file mode 100644 index 0000000..bea5ab5 --- /dev/null +++ b/frontend/src/lib/filterUrlCompression.test.ts @@ -0,0 +1,33 @@ +import { describe, it, expect } from 'vitest'; +import { + buildFilterSearchParams, + parseFilterParams, + compressFilters, + decompressFilters, +} from './filterUrlCompression'; + +describe('downloaded filter URL round-trip', () => { + it('round-trips an explicit on', () => { + const encoded = compressFilters({ downloaded: '1' }); + expect(encoded).not.toBeNull(); + expect(decompressFilters(encoded ?? '')?.downloaded).toBe('1'); + }); + + it('round-trips an explicit off rather than dropping it', () => { + const encoded = compressFilters({ downloaded: '0' }); + expect(encoded).not.toBeNull(); + expect(decompressFilters(encoded ?? '')?.downloaded).toBe('0'); + }); + + it('survives buildFilterSearchParams and parseFilterParams', () => { + const params = buildFilterSearchParams({ downloaded: '0', search: 'sonic' }); + const parsed = parseFilterParams(params); + expect(parsed.downloaded).toBe('0'); + expect(parsed.search).toBe('sonic'); + }); + + it('leaves downloaded undefined when absent', () => { + const parsed = parseFilterParams(new URLSearchParams()); + expect(parsed.downloaded).toBeUndefined(); + }); +}); diff --git a/frontend/src/lib/filterUrlCompression.ts b/frontend/src/lib/filterUrlCompression.ts index 1d6003c..f3482e8 100644 --- a/frontend/src/lib/filterUrlCompression.ts +++ b/frontend/src/lib/filterUrlCompression.ts @@ -18,6 +18,8 @@ export interface FilterUrlParams { sortOrder?: string; page?: number; fo?: string; // filter order + /** '1' = downloaded only, '0' = explicitly off, undefined = use the admin default */ + downloaded?: '0' | '1'; } // Filter order category abbreviations @@ -31,6 +33,7 @@ const CATEGORY_TO_ABBR: Record = { Language: 'L', Tag: 'T', Year: 'Y', + Downloaded: 'W', }; const ABBR_TO_CATEGORY: Record = Object.fromEntries( @@ -104,6 +107,7 @@ function toUrlSafeFormat(params: FilterUrlParams): string | null { if (params.tags) parts.push(`t.${escapeValue(params.tags)}`); if (params.yearFrom !== undefined) parts.push(`f.${params.yearFrom}`); if (params.yearTo !== undefined) parts.push(`e.${params.yearTo}`); + if (params.downloaded !== undefined) parts.push(`w.${params.downloaded}`); if (params.fo) parts.push(`o.${encodeFilterOrder(params.fo)}`); // Join with underscore (URL-safe) @@ -163,6 +167,9 @@ function fromUrlSafeFormat(encoded: string): FilterUrlParams | null { case 'o': params.fo = decodeFilterOrder(value); break; + case 'w': + if (value === '0' || value === '1') params.downloaded = value; + break; } } @@ -367,6 +374,12 @@ export function parseFilterParams(searchParams: URLSearchParams): FilterUrlParam sortOrder: searchParams.get('sortOrder') ?? undefined, page: safeParseInt(searchParams.get('page')), fo: searchParams.get('fo') ?? undefined, + downloaded: + searchParams.get('downloaded') === '1' + ? '1' + : searchParams.get('downloaded') === '0' + ? '0' + : undefined, }; } @@ -413,6 +426,7 @@ export function hasLegacyParams(searchParams: URLSearchParams): boolean { 'yearFrom', 'yearTo', 'fo', + 'downloaded', ]; return legacyKeys.some((key) => searchParams.has(key)); }