Compare commits

...

7 Commits

7 changed files with 184 additions and 150 deletions

View File

@@ -78,6 +78,7 @@ export default function Gallery({
<GalleryFullscreen <GalleryFullscreen
dialogRef={dialogRef} dialogRef={dialogRef}
images={images} images={images}
nsfw={nsfw}
currentIndex={currentIndex} currentIndex={currentIndex}
direction={direction} direction={direction}
goToPrevious={goToPrevious} goToPrevious={goToPrevious}
@@ -124,7 +125,7 @@ export default function Gallery({
> >
<GalleryImage <GalleryImage
src={network.getImageUrl(images[currentIndex])} src={network.getImageUrl(images[currentIndex])}
nfsw={nsfw.includes(currentIndex)} nfsw={nsfw.includes(images[currentIndex])}
/> />
</motion.div> </motion.div>
</AnimatePresence> </AnimatePresence>
@@ -188,6 +189,7 @@ export default function Gallery({
function GalleryFullscreen({ function GalleryFullscreen({
dialogRef, dialogRef,
images, images,
nsfw,
currentIndex, currentIndex,
direction, direction,
goToPrevious, goToPrevious,
@@ -197,6 +199,7 @@ function GalleryFullscreen({
}: { }: {
dialogRef: React.RefObject<HTMLDialogElement | null>; dialogRef: React.RefObject<HTMLDialogElement | null>;
images: number[]; images: number[];
nsfw: number[];
currentIndex: number; currentIndex: number;
direction: number; direction: number;
goToPrevious: () => void; goToPrevious: () => void;
@@ -237,10 +240,12 @@ function GalleryFullscreen({
if (dialogRef.current?.open) { if (dialogRef.current?.open) {
window.addEventListener("mousemove", handleMouseMove); window.addEventListener("mousemove", handleMouseMove);
window.addEventListener("touchstart", handleMouseMove);
} }
return () => { return () => {
window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("touchstart", handleMouseMove);
if (hideTimeoutRef.current) { if (hideTimeoutRef.current) {
clearTimeout(hideTimeoutRef.current); clearTimeout(hideTimeoutRef.current);
} }
@@ -316,7 +321,7 @@ function GalleryFullscreen({
<img <img
src={network.getImageUrl(images[currentIndex])} src={network.getImageUrl(images[currentIndex])}
alt="" alt=""
className="w-full h-full object-contain rounded-xl" className="w-full h-full object-contain rounded-xl select-none"
/> />
</motion.div> </motion.div>
</AnimatePresence> </AnimatePresence>
@@ -360,8 +365,8 @@ function GalleryFullscreen({
key={index} key={index}
className={`flex-shrink-0 w-16 h-16 rounded-lg overflow-hidden transition-all ${ className={`flex-shrink-0 w-16 h-16 rounded-lg overflow-hidden transition-all ${
index === currentIndex index === currentIndex
? "ring-2 ring-primary scale-110" ? "ring-2 ring-primary scale-110 "
: "opacity-60 hover:opacity-100" : `${nsfw.includes(imageId) ? "blur-sm hover:blur-none" : "opacity-60 hover:opacity-100"}`
}`} }`}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
@@ -373,7 +378,7 @@ function GalleryFullscreen({
<img <img
src={network.getResampledImageUrl(imageId)} src={network.getResampledImageUrl(imageId)}
alt={`Thumbnail ${index + 1}`} alt={`Thumbnail ${index + 1}`}
className="w-full h-full object-cover" className={`w-full h-full object-cover select-none`}
/> />
</button> </button>
))} ))}
@@ -405,7 +410,7 @@ function GalleryImage({ src, nfsw }: { src: string; nfsw: boolean }) {
<img <img
src={src} src={src}
alt="" alt=""
className={`w-full h-full object-contain transition-all duration-300 ${!show ? "blur-xl" : ""}`} className={`w-full h-full object-cover transition-all duration-300 ${!show ? "blur-xl" : ""}`}
/> />
{!show && ( {!show && (
<> <>

View File

@@ -261,6 +261,7 @@ export const i18nData = {
"File Size": "文件大小", "File Size": "文件大小",
"Tag": "标签", "Tag": "标签",
"Optional": "可选", "Optional": "可选",
"Download": "下载",
}, },
}, },
"zh-TW": { "zh-TW": {
@@ -525,6 +526,7 @@ export const i18nData = {
"File Size": "檔案大小", "File Size": "檔案大小",
"Tag": "標籤", "Tag": "標籤",
"Optional": "可選", "Optional": "可選",
"Download": "下載",
}, },
}, },
}; };

View File

@@ -187,7 +187,7 @@ function PinnedResourcesCarousel({
</div> </div>
</div> </div>
{resources.length > 1 && ( {resources.length > 1 && (
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 flex gap-2 z-10"> <div className="absolute bottom-2 left-1/2 transform -translate-x-1/2 flex gap-2 z-10">
{resources.map((_, index) => ( {resources.map((_, index) => (
<button <button
key={index} key={index}

View File

@@ -465,141 +465,141 @@ function DeleteResourceDialog({
); );
} }
const context = createContext<() => void>(() => {}); const context = createContext<() => void>(() => { });
function Article({ resource }: { resource: ResourceDetails }) { function Article({ resource }: { resource: ResourceDetails }) {
return ( return (
<> <>
<article> <article>
<Markdown <Markdown
remarkPlugins={[remarkGfm]} remarkPlugins={[remarkGfm]}
components={{ components={{
p: ({ node, ...props }) => { p: ({ node, ...props }) => {
if ( if (
typeof props.children === "object" && typeof props.children === "object" &&
(props.children as ReactElement).type === "strong" (props.children as ReactElement).type === "strong"
) { ) {
// @ts-ignore
const child = (
props.children as ReactElement
).props.children.toString() as string;
if (child.startsWith("<iframe")) {
// @ts-ignore // @ts-ignore
let html = child; const child = (
let splits = html.split(" "); props.children as ReactElement
splits = splits.filter((s: string) => { ).props.children.toString() as string;
return !( if (child.startsWith("<iframe")) {
s.startsWith("width") ||
s.startsWith("height") ||
s.startsWith("class") ||
s.startsWith("style")
);
});
html = splits.join(" ");
return (
<div
className={`w-full my-3 max-w-xl rounded-xl overflow-clip ${html.includes("youtube") ? "aspect-video" : "h-48 sm:h-64"}`}
dangerouslySetInnerHTML={{
__html: html,
}}
></div>
);
}
} else if (
typeof props.children === "object" &&
// @ts-ignore
props.children?.props &&
// @ts-ignore
props.children?.props.href
) {
const a = props.children as ReactElement;
const childProps = a.props as any;
const href = childProps.href as string;
// @ts-ignore
if (childProps.children?.length === 2) {
// @ts-ignore
const first = childProps.children[0] as ReactNode;
// @ts-ignore
const second = childProps.children[1] as ReactNode;
if (
typeof first === "object" &&
(typeof second === "string" || typeof second === "object")
) {
const img = first as ReactElement;
// @ts-ignore // @ts-ignore
if (img.type === "img") { let html = child;
return ( let splits = html.split(" ");
<a splits = splits.filter((s: string) => {
className={ return !(
"inline-block card shadow bg-base-100 no-underline hover:shadow-md transition-shadow my-2" s.startsWith("width") ||
} s.startsWith("height") ||
target={"_blank"} s.startsWith("class") ||
href={href} s.startsWith("style")
> );
<figure className={"max-h-96 min-w-48 min-h-24"}> });
{img} html = splits.join(" ");
</figure> return (
<div className={"card-body text-base-content text-lg"}> <div
<div className={"flex items-center"}> className={`w-full my-3 max-w-xl rounded-xl overflow-clip ${html.includes("youtube") ? "aspect-video" : "h-48 sm:h-64"}`}
<span className={"flex-1"}>{second}</span> dangerouslySetInnerHTML={{
<span> __html: html,
<MdOutlineOpenInNew size={24} /> }}
</span> ></div>
);
}
} else if (
typeof props.children === "object" &&
// @ts-ignore
props.children?.props &&
// @ts-ignore
props.children?.props.href
) {
const a = props.children as ReactElement;
const childProps = a.props as any;
const href = childProps.href as string;
// @ts-ignore
if (childProps.children?.length === 2) {
// @ts-ignore
const first = childProps.children[0] as ReactNode;
// @ts-ignore
const second = childProps.children[1] as ReactNode;
if (
typeof first === "object" &&
(typeof second === "string" || typeof second === "object")
) {
const img = first as ReactElement;
// @ts-ignore
if (img.type === "img") {
return (
<a
className={
"inline-block card shadow bg-base-100 no-underline hover:shadow-md transition-shadow my-2"
}
target={"_blank"}
href={href}
>
<figure className={"max-h-96 min-w-48 min-h-24"}>
{img}
</figure>
<div className={"card-body text-base-content text-lg"}>
<div className={"flex items-center"}>
<span className={"flex-1"}>{second}</span>
<span>
<MdOutlineOpenInNew size={24} />
</span>
</div>
</div> </div>
</div> </a>
</a> );
}
}
}
if (href.startsWith("https://store.steampowered.com/app/")) {
const appId = href
.substring("https://store.steampowered.com/app/".length)
.split("/")[0];
if (!Number.isNaN(Number(appId))) {
return (
<div className={"max-w-xl h-52 sm:h-48 my-2"}>
<iframe
className={"scheme-light"}
src={`https://store.steampowered.com/widget/${appId}/`}
></iframe>
</div>
); );
} }
} }
} }
if (href.startsWith("https://store.steampowered.com/app/")) { return <p {...props}>{props.children}</p>;
const appId = href },
.substring("https://store.steampowered.com/app/".length) a: ({ node, ...props }) => {
.split("/")[0]; const href = props.href as string;
if (!Number.isNaN(Number(appId))) { const origin = window.location.origin;
return (
<div className={"max-w-xl h-52 sm:h-48 my-2"}>
<iframe
className={"scheme-light"}
src={`https://store.steampowered.com/widget/${appId}/`}
></iframe>
</div>
);
}
}
}
return <p {...props}>{props.children}</p>;
},
a: ({ node, ...props }) => {
const href = props.href as string;
const origin = window.location.origin;
if (href.startsWith(origin) || href.startsWith("/")) { if (href.startsWith(origin) || href.startsWith("/")) {
let path = href; let path = href;
if (path.startsWith(origin)) { if (path.startsWith(origin)) {
path = path.substring(origin.length); path = path.substring(origin.length);
} }
const content = props.children?.toString(); const content = props.children?.toString();
if (path.startsWith("/resources/")) { if (path.startsWith("/resources/")) {
const id = path.substring("/resources/".length); const id = path.substring("/resources/".length);
for (const r of resource.related ?? []) { for (const r of resource.related ?? []) {
if (r.id.toString() === id) { if (r.id.toString() === id) {
return <RelatedResourceCard r={r} content={content} />; return <RelatedResourceCard r={r} content={content} />;
}
} }
} }
} }
}
return <a target={"_blank"} {...props}></a>; return <a target={"_blank"} {...props}></a>;
}, },
}} }}
> >
{resource.article.replaceAll("\n", " \n")} {resource.article.replaceAll("\n", " \n")}
</Markdown> </Markdown>
</article> </article>
<div className="border-b border-base-300 h-8"></div> <div className="border-b border-base-300 h-8"></div>
<Characters characters={resource.characters} /> <Characters characters={resource.characters} />
</> </>
); );
} }
@@ -898,27 +898,18 @@ function CloudflarePopup({ file }: { file: RFile }) {
<h3 className={"font-bold m-2"}> <h3 className={"font-bold m-2"}>
{downloadToken ? t("Verification successful") : t("Verifying your request")} {downloadToken ? t("Verification successful") : t("Verifying your request")}
</h3> </h3>
{!downloadToken && ( <div className={"h-20 w-full"}>
<> <Turnstile
<div className={"h-20 w-full"}> siteKey={app.cloudflareTurnstileSiteKey!}
<Turnstile onWidgetLoad={() => {
siteKey={app.cloudflareTurnstileSiteKey!} setLoading(false);
onWidgetLoad={() => { }}
setLoading(false); onSuccess={(token) => {
}} setDownloadToken(token);
onSuccess={(token) => { }}
setDownloadToken(token); ></Turnstile>
}} </div>
></Turnstile> {downloadToken ? (
</div>
<p className={"text-xs text-base-content/80 m-2"}>
{t(
"Please check your network if the verification takes too long or the captcha does not appear.",
)}
</p>
</>
)}
{downloadToken && (
<div className="p-2"> <div className="p-2">
<a <a
href={network.getFileDownloadLink(file.id, downloadToken)} href={network.getFileDownloadLink(file.id, downloadToken)}
@@ -932,7 +923,11 @@ function CloudflarePopup({ file }: { file: RFile }) {
{t("Download")} {t("Download")}
</a> </a>
</div> </div>
)} ) : <p className={"text-xs text-base-content/80 m-2"}>
{t(
"Please check your network if the verification takes too long or the captcha does not appear.",
)}
</p>}
</div> </div>
); );
} }
@@ -2109,7 +2104,7 @@ function CharacterCard({ character }: { character: CharacterParams }) {
alt={character.name} alt={character.name}
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />
<div className="absolute bottom-1 left-1 right-1 px-1 py-1 border border-base-100/40 rounded-lg bg-base-100/60"> <div className="absolute bottom-1 left-1 right-1 px-1 py-1 border border-base-100/40 rounded-lg bg-base-100/60">
<h4 className="font-semibold text-sm leading-tight line-clamp border border-transparent"> <h4 className="font-semibold text-sm leading-tight line-clamp border border-transparent">
{character.name} {character.name}
@@ -2123,7 +2118,7 @@ function CharacterCard({ character }: { character: CharacterParams }) {
) : null ) : null
} }
</h4> </h4>
{character.cv && ( {character.cv && (
<button <button
onClick={handleCVClick} onClick={handleCVClick}

View File

@@ -19,6 +19,8 @@ func main() {
Format: "[${ip}]:${port} ${status} - ${method} ${path}\n", Format: "[${ip}]:${port} ${status} - ${method} ${path}\n",
})) }))
app.Use(middleware.UnsupportedRegionMiddleware)
app.Use(middleware.ErrorHandler) app.Use(middleware.ErrorHandler)
app.Use(middleware.RealUserMiddleware) app.Use(middleware.RealUserMiddleware)

View File

@@ -32,6 +32,7 @@ func FrontendMiddleware(c fiber.Ctx) error {
} }
if _, err := os.Stat(file); path == "/" || os.IsNotExist(err) { if _, err := os.Stat(file); path == "/" || os.IsNotExist(err) {
c.Set("Cache-Control", "no-cache")
return serveIndexHtml(c) return serveIndexHtml(c)
} else { } else {
c.Set("Cache-Control", "public, max-age=31536000, immutable") c.Set("Cache-Control", "public, max-age=31536000, immutable")

View File

@@ -0,0 +1,29 @@
package middleware
import (
"strings"
"github.com/gofiber/fiber/v3"
)
func UnsupportedRegionMiddleware(c fiber.Ctx) error {
path := string(c.Request().URI().Path())
// Skip static file requests
if strings.Contains(path, ".") {
return c.Next()
}
// Skip API requests
if strings.HasPrefix(path, "/api") {
return c.Next()
}
if string(c.Request().Header.Peek("Unsupported-Region")) == "true" {
// Return a 403 Forbidden response with an empty html for unsupported regions
c.Response().Header.Add("Content-Type", "text/html")
c.Status(fiber.StatusForbidden)
return c.SendString("<html></html>")
}
return c.Next()
}