nsfw images

This commit is contained in:
2025-11-02 19:51:33 +08:00
parent a7f1bcb365
commit 853647edbc
6 changed files with 91 additions and 9 deletions

View File

@@ -48,6 +48,7 @@ export interface CreateResourceParams {
article: string;
images: number[];
gallery: number[];
gallery_nsfw: number[];
}
export interface Image {
@@ -86,6 +87,7 @@ export interface ResourceDetails {
comments: number;
related: Resource[];
gallery: number[];
galleryNsfw: number[];
}
export interface Storage {

View File

@@ -29,6 +29,7 @@ export default function EditResourcePage() {
const [images, setImages] = useState<number[]>([]);
const [links, setLinks] = useState<{ label: string; url: string }[]>([]);
const [galleryImages, setGalleryImages] = useState<number[]>([]);
const [galleryNsfw, setGalleryNsfw] = useState<number[]>([]);
const [error, setError] = useState<string | null>(null);
const [isSubmitting, setSubmitting] = useState(false);
const [isLoading, setLoading] = useState(true);
@@ -57,6 +58,7 @@ export default function EditResourcePage() {
setImages(data.images.map((i) => i.id));
setLinks(data.links ?? []);
setGalleryImages(data.gallery ?? []);
setGalleryNsfw(data.galleryNsfw ?? []);
setLoading(false);
} else {
showToast({ message: t("Failed to load resource"), type: "error" });
@@ -101,6 +103,7 @@ export default function EditResourcePage() {
images: images,
links: links,
gallery: galleryImages,
gallery_nsfw: galleryNsfw,
});
if (res.success) {
setSubmitting(false);
@@ -322,6 +325,7 @@ export default function EditResourcePage() {
<td>{t("Preview")}</td>
<td>{"Markdown"}</td>
<td>{t("Gallery")}</td>
<td>{"Nsfw"}</td>
<td>{t("Action")}</td>
</tr>
</thead>
@@ -365,6 +369,22 @@ export default function EditResourcePage() {
}}
/>
</td>
<td>
<input
type="checkbox"
className="checkbox checkbox-accent"
checked={galleryNsfw.includes(image)}
onChange={(e) => {
if (e.target.checked) {
setGalleryNsfw((prev) => [...prev, image]);
} else {
setGalleryNsfw((prev) =>
prev.filter((id) => id !== image),
);
}
}}
/>
</td>
<td>
<button
className={"btn btn-square"}

View File

@@ -28,6 +28,7 @@ export default function PublishPage() {
const [images, setImages] = useState<number[]>([]);
const [links, setLinks] = useState<{ label: string; url: string }[]>([]);
const [galleryImages, setGalleryImages] = useState<number[]>([]);
const [galleryNsfw, setGalleryNsfw] = useState<number[]>([]);
const [error, setError] = useState<string | null>(null);
const [isSubmitting, setSubmitting] = useState(false);
@@ -108,6 +109,7 @@ export default function PublishPage() {
images: images,
links: links,
gallery: galleryImages,
gallery_nsfw: galleryNsfw,
});
if (res.success) {
localStorage.removeItem("publish_data");
@@ -332,6 +334,7 @@ export default function PublishPage() {
<td>{t("Preview")}</td>
<td>{"Markdown"}</td>
<td>{"Gallery"}</td>
<td>{"Nsfw"}</td>
<td>{t("Action")}</td>
</tr>
</thead>
@@ -375,6 +378,22 @@ export default function PublishPage() {
}}
/>
</td>
<td>
<input
type="checkbox"
className="checkbox checkbox-accent"
checked={galleryNsfw.includes(image)}
onChange={(e) => {
if (e.target.checked) {
setGalleryNsfw((prev) => [...prev, image]);
} else {
setGalleryNsfw((prev) =>
prev.filter((id) => id !== image),
);
}
}}
/>
</td>
<td>
<button
className={"btn btn-square"}

View File

@@ -255,12 +255,12 @@ export default function ResourcePage() {
</div>
</div>
<div className="w-96 md:w-md lg:w-lg xl:w-xl p-4 hidden sm:flex items-center justify-center">
<Gallery images={resource.gallery} />
<Gallery images={resource.gallery} nsfw={resource.galleryNsfw} />
</div>
</div>
<div className="w-full p-4 flex sm:hidden items-center justify-center">
<Gallery images={resource.gallery} />
<Gallery images={resource.gallery} nsfw={resource.galleryNsfw} />
</div>
<div
@@ -357,18 +357,20 @@ function Tags({ tags }: { tags: Tag[] }) {
tagsMap.get(type)?.push(tag);
}
const compactMode = tags.length > 10;
return (
<>
{Array.from(tagsMap.entries()).map(([type, tags]) => (
<p key={type} className={"px-4"}>
<Badge className="shadow-xs" key={type}>
<Badge className="shadow-xs mr-0.5" key={type}>
{type == "" ? t("Other") : type}
</Badge>
{tags.map((tag) => (
<Badge
key={tag.name}
className={
"m-1 cursor-pointer badge-soft badge-primary shadow-xs"
`${compactMode ? "m-0.5" : "m-1"} cursor-pointer badge-soft badge-primary shadow-xs`
}
onClick={() => {
navigate(`/tag/${encodeURIComponent(tag.name)}`);
@@ -1879,7 +1881,7 @@ function CollectionSelector({
);
}
function Gallery({ images }: { images: number[] }) {
function Gallery({ images, nsfw }: { images: number[], nsfw: number[] }) {
const [currentIndex, setCurrentIndex] = useState(0);
const [direction, setDirection] = useState(0); // 方向1=向右,-1=向左
const [isHovered, setIsHovered] = useState(false);
@@ -1919,6 +1921,10 @@ function Gallery({ images }: { images: number[] }) {
setCurrentIndex(index);
};
if (nsfw == null) {
nsfw = [];
}
return (
<div
className="relative w-full overflow-hidden rounded-xl bg-base-100-tr82 shadow-sm"
@@ -1930,10 +1936,8 @@ function Gallery({ images }: { images: number[] }) {
<div ref={containerRef} className="w-full h-full relative">
{width > 0 && (
<AnimatePresence initial={false} custom={direction} mode="sync">
<motion.img
<motion.div
key={currentIndex}
src={network.getImageUrl(images[currentIndex])}
alt={`Gallery image ${currentIndex + 1}`}
className="absolute w-full h-full object-contain"
variants={{
enter: (dir: number) => ({
@@ -1952,7 +1956,12 @@ function Gallery({ images }: { images: number[] }) {
animate="center"
exit="exit"
custom={direction}
/>
>
<GalleryImage
src={network.getImageUrl(images[currentIndex])}
nfsw={nsfw.includes(images[currentIndex])}
/>
</motion.div>
</AnimatePresence>
)}
</div>
@@ -1999,3 +2008,29 @@ function Gallery({ images }: { images: number[] }) {
</div>
);
}
function GalleryImage({src, nfsw}: {src: string, nfsw: boolean}) {
const [show, setShow] = useState(!nfsw);
return (
<div className="relative w-full h-full">
<img
src={src}
alt=""
className={`w-full h-full object-contain transition-all duration-300 ${!show ? 'blur-xl' : ''}`}
/>
{!show && (
<>
<div className="absolute inset-0 bg-base-content/20 cursor-pointer" onClick={() => {
setShow(true)
}} />
<div className="absolute top-4 left-4">
<Badge className="badge-error shadow-lg">
NSFW
</Badge>
</div>
</>
)}
</div>
);
}

View File

@@ -22,6 +22,7 @@ type Resource struct {
Comments uint
ModifiedTime time.Time
Gallery []uint `gorm:"serializer:json"`
GalleryNsfw []uint `gorm:"serializer:json"`
}
type Link struct {
@@ -54,6 +55,7 @@ type ResourceDetailView struct {
Comments uint `json:"comments"`
Related []ResourceView `json:"related"`
Gallery []uint `json:"gallery"`
GalleryNsfw []uint `json:"galleryNsfw"`
}
func (r *Resource) ToView() ResourceView {
@@ -107,5 +109,6 @@ func (r *Resource) ToDetailView() ResourceDetailView {
Downloads: r.Downloads,
Comments: r.Comments,
Gallery: r.Gallery,
GalleryNsfw: r.GalleryNsfw,
}
}

View File

@@ -28,6 +28,7 @@ type ResourceParams struct {
Article string `json:"article"`
Images []uint `json:"images"`
Gallery []uint `json:"gallery"`
GalleryNsfw []uint `json:"gallery_nsfw"`
}
func CreateResource(uid uint, params *ResourceParams) (uint, error) {
@@ -64,6 +65,7 @@ func CreateResource(uid uint, params *ResourceParams) (uint, error) {
Tags: tags,
UserID: uid,
Gallery: params.Gallery,
GalleryNsfw: params.GalleryNsfw,
}
if r, err = dao.CreateResource(r); err != nil {
return 0, err
@@ -455,6 +457,7 @@ func EditResource(uid, rid uint, params *ResourceParams) error {
r.Article = params.Article
r.Links = params.Links
r.Gallery = params.Gallery
r.GalleryNsfw = params.GalleryNsfw
images := make([]model.Image, len(params.Images))
for i, id := range params.Images {