Online browser + post detail: SPA talks to e621 directly

Backend:
- POST /api/files/lookup/ reports which MD5s are already in the library

Frontend:
- e621 client extended with post/tag/favorite types and helpers: post
  search, post detail, batch posts by id, tag autocomplete, toggle
  favorite
- /online: tag search with autocomplete, post grid with rating colors
  and in-library badges, numbered pagination, page tag cloud, blacklist
  panel and filtered counts from /users/me.json, anonymous hint
- /online/view/🆔 media viewer (sample or original, video support),
  taxonomy-colored tags by category, specs sheet, favorite/unfavorite,
  description, sources, pools and parent/children thumbnails
- Shared CollapsibleSidebar extracted from the library page; Online
  added to the nav, command palette and sidebar toggle
This commit is contained in:
2026-09-17 09:00:17 -05:00
parent b6409523e6
commit 04448bf615
14 changed files with 1273 additions and 41 deletions
+31 -5
View File
@@ -1,12 +1,19 @@
import { create } from "zustand";
import { api } from "@/lib/api";
import type { E621Credentials } from "@/lib/e621";
import {
effectiveCredentials,
verifyE621Credentials,
type E621Credentials,
} from "@/lib/e621";
interface E621State {
credentials: E621Credentials | null;
blacklist: string[];
loading: boolean;
ready: boolean;
load: () => Promise<void>;
loadBlacklist: () => Promise<void>;
save: (values: {
username: string;
api_key: string;
@@ -15,17 +22,35 @@ interface E621State {
clear: () => void;
}
export const useE621 = create<E621State>((set) => ({
export const useE621 = create<E621State>((set, get) => ({
credentials: null,
blacklist: [],
loading: false,
ready: false,
load: async () => {
set({ loading: true });
try {
const credentials = await api<E621Credentials>("/api/auth/e621/");
set({ credentials, loading: false });
set({ credentials, ready: true, loading: false });
void get().loadBlacklist();
} catch {
set({ credentials: null, loading: false });
set({ credentials: null, ready: true, loading: false });
}
},
loadBlacklist: async () => {
try {
const user = await verifyE621Credentials(
effectiveCredentials(get().credentials),
);
const tags = (user.blacklisted_tags ?? "")
.split("\n")
.map((tag) => tag.trim())
.filter(Boolean);
set({ blacklist: tags });
} catch {
// Keep the last known blacklist when e621 is unreachable.
}
},
@@ -35,8 +60,9 @@ export const useE621 = create<E621State>((set) => ({
json: values,
});
set({ credentials });
void get().loadBlacklist();
return credentials;
},
clear: () => set({ credentials: null }),
clear: () => set({ credentials: null, blacklist: [], ready: false }),
}));