Files
J621/frontend/src/App.tsx
T
JakeBreath 1c2cb8d468 Add an ephemeral similarity check page
- /similar (nav: Similar): drop a file to get the exact MD5 match, the
  perceptual matches against the library, and e621 IQDB candidates
  (auto-run for images when credentials are configured). Read-only —
  nothing enters the library.
- SimilarityCheck model + /api/similarity/ (create/list/retrieve/delete)
  with signed preview URLs and an expires_at timestamp.
- Temp files are wiped on startup (AppConfig.ready, file-only so no
  database access during initialization), lazily past
  SIMILARITY_TTL_MINUTES (default 30, env-overridable), on delete, and
  by manage.py cleanup_similarity.
- uploadFile() takes a target path; .env.example documents the TTL.
2026-09-17 18:22:11 -05:00

142 lines
3.9 KiB
TypeScript

import { useEffect, useState, type ReactNode } from "react";
import {
Navigate,
Route,
Routes,
useLocation,
useParams,
} from "react-router-dom";
import { AppShell } from "@/components/AppShell";
import { FullPageSpinner } from "@/components/ui";
import AccountPage from "@/features/account/AccountPage";
import { AgeGate } from "@/features/age/AgeGate";
import LoginPage from "@/features/auth/LoginPage";
import RegisterPage from "@/features/auth/RegisterPage";
import DetailPage from "@/features/detail/DetailPage";
import DeletePage from "@/features/library/DeletePage";
import DuplicatesPage from "@/features/library/DuplicatesPage";
import FollowedPage from "@/features/follows/FollowedPage";
import LibraryPage from "@/features/library/LibraryPage";
import Md5Redirect from "@/features/library/Md5Redirect";
import OnlinePage from "@/features/online/OnlinePage";
import PoolDetailPage from "@/features/pools/PoolDetailPage";
import PoolsPage from "@/features/pools/PoolsPage";
import SimilarPage from "@/features/similar/SimilarPage";
import UploadPage from "@/features/upload/UploadPage";
import UsersPage from "@/features/users/UsersPage";
import { isAgeVerified, markAgeVerified } from "@/lib/age";
import { useAuth } from "@/store/auth";
function RequireAuth({ children }: { children: ReactNode }) {
const user = useAuth((state) => state.user);
const location = useLocation();
if (!user) {
return <Navigate to="/login" state={{ from: location.pathname }} replace />;
}
return <>{children}</>;
}
function PostRedirect() {
const { postId = "" } = useParams();
return <Navigate to={`/detail/${postId}`} replace />;
}
export default function App() {
const ready = useAuth((state) => state.ready);
const loadUser = useAuth((state) => state.loadUser);
const [ageVerified, setAgeVerified] = useState(isAgeVerified);
useEffect(() => {
void loadUser();
}, [loadUser]);
if (!ageVerified) {
return (
<AgeGate
onEnter={() => {
markAgeVerified();
setAgeVerified(true);
}}
/>
);
}
if (!ready) {
return <FullPageSpinner />;
}
return (
<Routes>
<Route element={<AppShell />}>
<Route path="/" element={<LibraryPage />} />
<Route path="/detail/:itemId" element={<DetailPage />} />
<Route path="/view/:md5" element={<Md5Redirect />} />
<Route path="/online" element={<OnlinePage />} />
<Route path="/online/view/:postId" element={<PostRedirect />} />
<Route path="/pools" element={<PoolsPage />} />
<Route path="/pools/:poolId" element={<PoolDetailPage />} />
<Route
path="/followed"
element={
<RequireAuth>
<FollowedPage />
</RequireAuth>
}
/>
<Route
path="/duplicates"
element={
<RequireAuth>
<DuplicatesPage />
</RequireAuth>
}
/>
<Route
path="/delete"
element={
<RequireAuth>
<DeletePage />
</RequireAuth>
}
/>
<Route
path="/users"
element={
<RequireAuth>
<UsersPage />
</RequireAuth>
}
/>
<Route
path="/similar"
element={
<RequireAuth>
<SimilarPage />
</RequireAuth>
}
/>
<Route
path="/upload"
element={
<RequireAuth>
<UploadPage />
</RequireAuth>
}
/>
<Route
path="/account"
element={
<RequireAuth>
<AccountPage />
</RequireAuth>
}
/>
<Route path="*" element={<Navigate to="/" replace />} />
</Route>
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
</Routes>
);
}