Add a Pools browser backed by e621's /pools endpoint

- /pools: search by name, category/active filters, sort options and
  pagination per the OpenAPI spec, with covers taken from each pool's
  first post in one batched post call; blacklisted covers fall back to a
  placeholder and deleted pools get an archive marker.
- /pools/<id>: DText description, post grid kept in the pool's own order
  with chunked loading, in-library badges, a blacklist reveal toggle and
  a Follow pool button wired into the follows API.
- e621 client gains fetchPools/fetchPool; Pools nav entry added.
This commit is contained in:
2026-09-17 14:26:20 -05:00
parent c396c368dc
commit 0a7dc5a991
7 changed files with 817 additions and 0 deletions
+62
View File
@@ -316,6 +316,68 @@ export async function fetchPostsByMd5(
return data.posts;
}
export interface E621Pool {
id: number;
name: string;
created_at: string;
updated_at: string;
creator_id: number;
creator_name: string;
description: string;
is_active: boolean;
category: "collection" | "series";
post_ids: number[];
post_count: number;
}
export type E621PoolOrder =
| "id_desc"
| "id_asc"
| "name"
| "created_at"
| "post_count";
export async function fetchPools(
credentials: E621Credentials,
options: {
name?: string;
category?: string;
isActive?: string;
order?: E621PoolOrder;
page?: number;
limit?: number;
signal?: AbortSignal;
} = {},
): Promise<E621Pool[]> {
const data = await e621Request<unknown>(credentials, "/pools.json", {
params: {
limit: options.limit ?? 48,
page: options.page,
"search[order]": options.order ?? "id_desc",
"search[name_matches]": options.name || undefined,
"search[category]": options.category || undefined,
"search[is_active]": options.isActive || undefined,
},
signal: options.signal,
});
// The spec says a bare array; tolerate a wrapped payload too.
if (Array.isArray(data)) return data as E621Pool[];
const wrapped = (data as { pools?: E621Pool[] })?.pools;
return Array.isArray(wrapped) ? wrapped : [];
}
export async function fetchPool(
credentials: E621Credentials,
poolId: number,
signal?: AbortSignal,
): Promise<E621Pool> {
const data = await e621Request<unknown>(credentials, `/pools/${poolId}.json`, {
signal,
});
const wrapped = (data as { pool?: E621Pool })?.pool;
return (wrapped ?? (data as E621Pool)) as E621Pool;
}
export interface E621IqdbResult {
post_id: number | null;
score: number | null;