import {useNavigate, useParams} from "react-router"; import { createContext, createRef, ReactElement, ReactNode, useCallback, useContext, useEffect, useRef, useState } from "react"; import {ResourceDetails, RFile, Storage, Comment} from "../network/models.ts"; import {network} from "../network/network.ts"; import showToast from "../components/toast.ts"; import Markdown from "react-markdown"; import "../markdown.css"; import Loading from "../components/loading.tsx"; import { MdAdd, MdOutlineArticle, MdOutlineComment, MdOutlineDataset, MdOutlineDelete, MdOutlineDownload, MdOutlineEdit } from "react-icons/md"; import {app} from "../app.ts"; import {uploadingManager} from "../network/uploading.ts"; import {ErrorAlert} from "../components/alert.tsx"; import {useTranslation} from "react-i18next"; import Pagination from "../components/pagination.tsx"; import showPopup, {useClosePopup} from "../components/popup.tsx"; import {Turnstile} from "@marsidev/react-turnstile"; import Button from "../components/button.tsx"; import Badge, {BadgeAccent} from "../components/badge.tsx"; import Input from "../components/input.tsx"; export default function ResourcePage() { const params = useParams() const {t} = useTranslation(); const idStr = params.id const id = idStr ? parseInt(idStr) : NaN const [resource, setResource] = useState(null) const [page, setPage] = useState(0) const reload = useCallback(async () => { if (!isNaN(id)) { setResource(null) const res = await network.getResourceDetails(id) if (res.success) { setResource(res.data!) } else { showToast({message: res.message, type: "error"}) } } }, [id]) useEffect(() => { document.title = t("Resource Details"); }, [t]) useEffect(() => { setResource(null); if (!isNaN(id)) { network.getResourceDetails(id).then((res) => { if (res.success) { setResource(res.data!) document.title = res.data!.title } else { showToast({message: res.message, type: "error"}) } }) } }, [id]) const navigate = useNavigate() if (isNaN(id)) { return
{t("Resource ID is required")}
} if (!resource) { return } return

{resource.title}

{ resource.alternativeTitles.map((e, i) => { return

{e}

}) }

{ resource.tags.map((e) => { return { navigate(`/tag/${e.name}`); }}>{e.name} }) }

{ app.isAdmin() || app.user?.id === resource.author.id ? : null }
} function DeleteResourceDialog({resourceId, uploaderId}: { resourceId: number, uploaderId?: number }) { const [isLoading, setLoading] = useState(false) const navigate = useNavigate() const {t} = useTranslation() const handleDelete = async () => { if (isLoading) { return } setLoading(true) const res = await network.deleteResource(resourceId) const dialog = document.getElementById("delete_resource_dialog") as HTMLDialogElement dialog.close() if (res.success) { showToast({message: t("Resource deleted successfully"), type: "success"}) navigate("/", {replace: true}) } else { showToast({message: res.message, type: "error"}) } setLoading(false) } if (!app.isAdmin() && app.user?.id !== uploaderId) { return <> } return <>

{t("Delete Resource")}

{t("Are you sure you want to delete the resource")}? {t("This action cannot be undone.")}

} const context = createContext<() => void>(() => { }) function Article({resource}: { resource: ResourceDetails }) { const navigate = useNavigate() return
{ console.log(props.children) if (typeof props.children === "object" && (props.children as ReactElement).type === "strong") { // @ts-ignore const child = (props.children as ReactElement).props.children.toString() as string if (child.startsWith(" { return !(s.startsWith("width") || s.startsWith("height") || s.startsWith("class") || s.startsWith("style")); }) html = splits.join(" ") return
} // @ts-ignore } else if (typeof props.children === "object" && props.children.props && 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
{img}
{second}
} } } 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
} } } return

{props.children}

}, a: ({node, ...props}) => { const href = props.href as string if (href.startsWith(window.location.origin) || href.startsWith("/")) { let path = href if (path.startsWith(window.location.origin)) { path = path.substring(window.location.origin.length) } const content = props.children?.toString() if (path.startsWith("/resources/")) { const id = path.substring("/resources/".length) for (const r of resource.related ?? []) { if (r.id.toString() === id) { return { navigate(`/resources/${r.id}`, {replace: true}) }}> {r.image && {"cover"}} {r.title} {content} } } } } return }, }}>{resource.article}
} function fileSizeToString(size: number) { if (size < 1024) { return size + "B" } else if (size < 1024 * 1024) { return (size / 1024).toFixed(2) + "KB" } else if (size < 1024 * 1024 * 1024) { return (size / 1024 / 1024).toFixed(2) + "MB" } else { return (size / 1024 / 1024 / 1024).toFixed(2) + "GB" } } function FileTile({file}: { file: RFile }) { const buttonRef = createRef() const {t} = useTranslation() return

{file.filename}

{file.description}

{file.is_redirect ? t("Redirect") : fileSizeToString(file.size)}

} function CloudflarePopup({file}: { file: RFile }) { const closePopup = useClosePopup() const [isLoading, setLoading] = useState(true) const {t} = useTranslation() return
{ isLoading ?
: null }

{t("Verifying your request")}

{ setLoading(false) }} onSuccess={(token) => { closePopup(); const link = network.getFileDownloadLink(file.id, token); window.open(link, "_blank"); }}>

{t("Please check your network if the verification takes too long or the captcha does not appear.")}

} function Files({files, resourceID}: { files: RFile[], resourceID: number }) { return
{ files.map((file) => { return }) }
{ app.canUpload() &&
}
} enum FileType { redirect = "redirect", upload = "upload", serverTask = "server_task" } function CreateFileDialog({resourceId}: { resourceId: number }) { const {t} = useTranslation(); const [isLoading, setLoading] = useState(false) const storages = useRef(null) const mounted = useRef(true) const [fileType, setFileType] = useState(null) const [filename, setFilename] = useState("") const [redirectUrl, setRedirectUrl] = useState("") const [storage, setStorage] = useState(null) const [file, setFile] = useState(null) const [description, setDescription] = useState("") const [fileUrl, setFileUrl] = useState("") const reload = useContext(context) const [isSubmitting, setSubmitting] = useState(false) const [error, setError] = useState(null) useEffect(() => { mounted.current = true return () => { mounted.current = false } }, []); const submit = async () => { if (isSubmitting) { return } if (!fileType) { setError(t("Please select a file type")) return } setSubmitting(true) if (fileType === FileType.redirect) { if (!redirectUrl || !filename || !description) { setError(t("Please fill in all fields")); setSubmitting(false); return; } const res = await network.createRedirectFile(filename, description, resourceId, redirectUrl); if (res.success) { setSubmitting(false) const dialog = document.getElementById("upload_dialog") as HTMLDialogElement dialog.close() showToast({message: t("File created successfully"), type: "success"}) reload() } else { setError(res.message) setSubmitting(false) } } else if (fileType === FileType.upload) { if (!file || !storage) { setError(t("Please select a file and storage")) setSubmitting(false) return } const res = await uploadingManager.addTask(file, resourceId, storage.id, description, () => { if (mounted.current) { reload(); } }); if (res.success) { setSubmitting(false) const dialog = document.getElementById("upload_dialog") as HTMLDialogElement dialog.close() showToast({message: t("Successfully create uploading task."), type: "success"}) } else { setError(res.message) setSubmitting(false) } } else if (fileType === FileType.serverTask) { if (!fileUrl || !filename || !storage) { setError(t("Please fill in all fields")); setSubmitting(false); return; } const res = await network.createServerDownloadTask(fileUrl, filename, description, resourceId, storage.id); if (res.success) { setSubmitting(false) const dialog = document.getElementById("upload_dialog") as HTMLDialogElement dialog.close() showToast({message: t("File created successfully"), type: "success"}) reload() } else { setError(res.message) setSubmitting(false) } } } return <>

{t("Create File")}

{t("Type")}

{ setFileType(null); }}/> { setFileType(FileType.redirect); }}/> { setFileType(FileType.upload); }}/> { setFileType(FileType.serverTask); }}/>
{ fileType === FileType.redirect && <>

{t("User who click the file will be redirected to the URL")}

{ setFilename(e.target.value) }}/> { setRedirectUrl(e.target.value) }}/> { setDescription(e.target.value) }}/> } { fileType === FileType.upload && <>

{t("Upload a file to server, then the file will be moved to the selected storage.")}

{ if (e.target.files) { setFile(e.target.files[0]) } }}/> { setDescription(e.target.value) }}/> } { fileType === FileType.serverTask && <>

{t("Provide a file url for the server to download, and the file will be moved to the selected storage.")}

{ setFilename(e.target.value) }}/> { setFileUrl(e.target.value) }}/> { setDescription(e.target.value) }}/> } {error && }
} function UpdateFileInfoDialog({file}: { file: RFile }) { const [isLoading, setLoading] = useState(false) const [filename, setFilename] = useState(file.filename) const [description, setDescription] = useState(file.description) const {t} = useTranslation() const reload = useContext(context) const handleUpdate = async () => { if (isLoading) { return } setLoading(true) const res = await network.updateFile(file.id, filename, description); const dialog = document.getElementById(`update_file_info_dialog_${file.id}`) as HTMLDialogElement dialog.close() if (res.success) { showToast({message: t("File info updated successfully"), type: "success"}) reload() } else { showToast({message: res.message, type: "error"}) } setLoading(false) } if (!app.isAdmin() && app.user?.id !== file.user_id) { return <> } return <>

{t("Update File Info")}

setFilename(e.target.value)}/> setDescription(e.target.value)}/>
} function Comments({resourceId}: { resourceId: number }) { const [page, setPage] = useState(1); const [maxPage, setMaxPage] = useState(0); const [listKey, setListKey] = useState(0); const [commentContent, setCommentContent] = useState(""); const [isLoading, setLoading] = useState(false); const {t} = useTranslation(); const reload = useCallback(() => { setPage(1); setMaxPage(0); setListKey(prev => prev + 1); }, []) const sendComment = async () => { if (isLoading) { return; } if (commentContent === "") { showToast({message: t("Comment content cannot be empty"), type: "error"}); return; } setLoading(true); const res = await network.createComment(resourceId, commentContent); if (res.success) { setCommentContent(""); showToast({message: t("Comment created successfully"), type: "success"}); reload(); } else { showToast({message: res.message, type: "error"}); } setLoading(false); } return