Initial commit
This commit is contained in:
+205
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* Shared types + URL helpers for the multi-criteria filter bar.
|
||||
*/
|
||||
|
||||
export type FilterTabKey = "actresses" | "studios" | "series" | "genres" | "collections" | "tags" | "categories";
|
||||
export type FilterMode = "or" | "and";
|
||||
|
||||
export type WatchedState = "all" | "watched" | "unwatched";
|
||||
export type RatedState = "all" | "rated" | "unrated";
|
||||
export type PresenceState = "all" | "has" | "missing";
|
||||
|
||||
export interface FilterStatus {
|
||||
watched: WatchedState;
|
||||
rated: RatedState;
|
||||
collection: PresenceState;
|
||||
tags: PresenceState;
|
||||
video: PresenceState;
|
||||
}
|
||||
|
||||
export type MarkState = "all" | "vip" | "favorite" | "owned" | "unmarked";
|
||||
export type MarkOption = "vip" | "favorite" | "owned" | "unmarked";
|
||||
export const MARK_OPTIONS: MarkOption[] = ["vip", "favorite", "owned", "unmarked"];
|
||||
|
||||
export interface FilterCriteria {
|
||||
ids: Record<FilterTabKey, number[]>;
|
||||
mode: Record<FilterTabKey, FilterMode>;
|
||||
status: FilterStatus;
|
||||
// Empty array == "ALL" (no mark filter). Otherwise the OR of the listed marks.
|
||||
marks: MarkOption[];
|
||||
}
|
||||
|
||||
export const EMPTY_STATUS: FilterStatus = {
|
||||
watched: "all",
|
||||
rated: "all",
|
||||
collection: "all",
|
||||
tags: "all",
|
||||
video: "all",
|
||||
};
|
||||
|
||||
export const EMPTY_CRITERIA: FilterCriteria = {
|
||||
ids: { actresses: [], studios: [], series: [], genres: [], collections: [], tags: [], categories: [] },
|
||||
mode: { actresses: "or", studios: "or", series: "or", genres: "or", collections: "or", tags: "or", categories: "or" },
|
||||
status: EMPTY_STATUS,
|
||||
marks: [],
|
||||
};
|
||||
|
||||
export type StatusAxisKey = keyof FilterStatus;
|
||||
|
||||
const TAB_KEYS: FilterTabKey[] = ["actresses", "studios", "series", "genres", "collections", "tags", "categories"];
|
||||
// Studios and series can't AND (a cover has at most one).
|
||||
const SUPPORTS_AND: Record<FilterTabKey, boolean> = {
|
||||
actresses: true,
|
||||
studios: false,
|
||||
series: false,
|
||||
genres: true,
|
||||
collections: true,
|
||||
tags: true,
|
||||
categories: true,
|
||||
};
|
||||
|
||||
export function tabSupportsAnd(k: FilterTabKey): boolean {
|
||||
return SUPPORTS_AND[k];
|
||||
}
|
||||
|
||||
export function parseFilterCriteria(sp: Record<string, string | string[] | undefined>): FilterCriteria {
|
||||
const c: FilterCriteria = {
|
||||
ids: { actresses: [], studios: [], series: [], genres: [], collections: [], tags: [], categories: [] },
|
||||
mode: { actresses: "or", studios: "or", series: "or", genres: "or", collections: "or", tags: "or", categories: "or" },
|
||||
status: { ...EMPTY_STATUS },
|
||||
marks: [],
|
||||
};
|
||||
for (const k of TAB_KEYS) {
|
||||
const raw = sp[k];
|
||||
if (typeof raw === "string" && raw.trim()) {
|
||||
c.ids[k] = raw.split(",").map((x) => Number(x)).filter((n) => Number.isFinite(n) && n > 0);
|
||||
}
|
||||
const m = sp[`${k}_mode`];
|
||||
if (typeof m === "string" && m.toLowerCase() === "and" && SUPPORTS_AND[k]) c.mode[k] = "and";
|
||||
}
|
||||
|
||||
// Watched axis. Accept both new (?watched=watched|unwatched) and legacy (?watched=1, ?unwatched=1).
|
||||
const watched = sp.watched;
|
||||
if (typeof watched === "string") {
|
||||
if (watched === "watched" || watched === "1") c.status.watched = "watched";
|
||||
else if (watched === "unwatched") c.status.watched = "unwatched";
|
||||
}
|
||||
if (sp.unwatched === "1") c.status.watched = "unwatched";
|
||||
|
||||
const rated = sp.rated;
|
||||
if (typeof rated === "string") {
|
||||
if (rated === "rated" || rated === "1") c.status.rated = "rated";
|
||||
else if (rated === "unrated") c.status.rated = "unrated";
|
||||
}
|
||||
if (sp.unrated === "1") c.status.rated = "unrated";
|
||||
|
||||
const collection = sp.collection;
|
||||
if (typeof collection === "string") {
|
||||
if (collection === "has") c.status.collection = "has";
|
||||
else if (collection === "missing") c.status.collection = "missing";
|
||||
}
|
||||
if (sp.uncollected === "1") c.status.collection = "missing";
|
||||
|
||||
const tagsStatus = sp.tags_status;
|
||||
if (typeof tagsStatus === "string") {
|
||||
if (tagsStatus === "has") c.status.tags = "has";
|
||||
else if (tagsStatus === "missing") c.status.tags = "missing";
|
||||
}
|
||||
if (sp.untagged === "1") c.status.tags = "missing";
|
||||
|
||||
const video = sp.video;
|
||||
if (typeof video === "string") {
|
||||
if (video === "has") c.status.video = "has";
|
||||
else if (video === "missing") c.status.video = "missing";
|
||||
}
|
||||
|
||||
// marks: comma-separated list. Legacy `?mark=vip` (singular) is also honored
|
||||
// so old bookmarked URLs keep working.
|
||||
const marksRaw = (typeof sp.marks === "string" && sp.marks) || (typeof sp.mark === "string" ? sp.mark : "");
|
||||
if (marksRaw) {
|
||||
const wanted = marksRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
||||
const valid = new Set<MarkOption>(MARK_OPTIONS);
|
||||
const out: MarkOption[] = [];
|
||||
const seen = new Set<MarkOption>();
|
||||
for (const w of wanted) {
|
||||
if (valid.has(w as MarkOption) && !seen.has(w as MarkOption)) {
|
||||
out.push(w as MarkOption);
|
||||
seen.add(w as MarkOption);
|
||||
}
|
||||
}
|
||||
c.marks = out;
|
||||
}
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
/** Apply a `FilterCriteria` onto a `URLSearchParams`. Existing keys are overwritten. */
|
||||
export function writeFilterCriteria(sp: URLSearchParams, c: FilterCriteria): void {
|
||||
for (const k of TAB_KEYS) {
|
||||
if (c.ids[k].length === 0) sp.delete(k);
|
||||
else sp.set(k, c.ids[k].join(","));
|
||||
if (c.mode[k] === "and" && SUPPORTS_AND[k]) sp.set(`${k}_mode`, "and");
|
||||
else sp.delete(`${k}_mode`);
|
||||
}
|
||||
// Always write canonical status keys; clear the legacy ones so they don't fight us.
|
||||
if (c.status.watched === "all") sp.delete("watched"); else sp.set("watched", c.status.watched);
|
||||
if (c.status.rated === "all") sp.delete("rated"); else sp.set("rated", c.status.rated);
|
||||
if (c.status.collection === "all") sp.delete("collection"); else sp.set("collection", c.status.collection);
|
||||
if (c.status.tags === "all") sp.delete("tags_status"); else sp.set("tags_status", c.status.tags);
|
||||
if (c.status.video === "all") sp.delete("video"); else sp.set("video", c.status.video);
|
||||
sp.delete("unwatched");
|
||||
sp.delete("unrated");
|
||||
sp.delete("uncollected");
|
||||
sp.delete("untagged");
|
||||
// Single canonical key: `marks=vip,favorite`. Drop legacy `mark` if present.
|
||||
sp.delete("mark");
|
||||
if (c.marks.length === 0) sp.delete("marks");
|
||||
else sp.set("marks", c.marks.join(","));
|
||||
}
|
||||
|
||||
export function totalSelected(c: FilterCriteria): number {
|
||||
let n = 0;
|
||||
for (const k of TAB_KEYS) n += c.ids[k].length;
|
||||
return n;
|
||||
}
|
||||
|
||||
export function totalStatusActive(c: FilterCriteria): number {
|
||||
let n = 0;
|
||||
if (c.status.watched !== "all") n++;
|
||||
if (c.status.rated !== "all") n++;
|
||||
if (c.status.collection !== "all") n++;
|
||||
if (c.status.tags !== "all") n++;
|
||||
if (c.status.video !== "all") n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
export function anyActive(c: FilterCriteria): boolean {
|
||||
return totalSelected(c) > 0 || totalStatusActive(c) > 0 || c.marks.length > 0;
|
||||
}
|
||||
|
||||
/** Translate the tri-state status into the boolean flags consumed by listImages / libraryLetterCounts. */
|
||||
export function statusToFlags(s: FilterStatus): {
|
||||
watched?: boolean;
|
||||
unwatched?: boolean;
|
||||
rated?: boolean;
|
||||
unrated?: boolean;
|
||||
hasCollection?: boolean;
|
||||
uncollected?: boolean;
|
||||
hasTags?: boolean;
|
||||
untagged?: boolean;
|
||||
hasVideo?: boolean;
|
||||
noVideo?: boolean;
|
||||
} {
|
||||
return {
|
||||
watched: s.watched === "watched",
|
||||
unwatched: s.watched === "unwatched",
|
||||
rated: s.rated === "rated",
|
||||
unrated: s.rated === "unrated",
|
||||
hasCollection: s.collection === "has",
|
||||
uncollected: s.collection === "missing",
|
||||
hasTags: s.tags === "has",
|
||||
untagged: s.tags === "missing",
|
||||
hasVideo: s.video === "has",
|
||||
noVideo: s.video === "missing",
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user