Compare commits

..

3 Commits

Author SHA1 Message Date
31b9fb5d45 fix: unused import 2025-12-10 20:54:21 +08:00
116efcdf93 feat: cover 2025-12-10 20:50:48 +08:00
9ad8d9d7e9 feat: update home page select 2025-12-10 20:32:41 +08:00
7 changed files with 98 additions and 23 deletions

View File

@@ -156,8 +156,8 @@ export const i18nData = {
"Cloudflare Turnstile Secret Key": "Cloudflare Turnstile 密钥",
"If the cloudflare turnstile keys are not empty, the turnstile will be used for register and download.":
"如果设置了 Cloudflare Turnstile 密钥,将在注册和下载时启用验证",
"The first image will be used as the cover image":
"第一张图片将用作封面图片",
"You can select a cover image using the radio button in the Cover column":
"您可以使用封面列中的单选按钮选择封面图片",
"Please enter a search keyword": "请输入搜索关键词",
"Searching...": "搜索中...",
"Create Tag": "创建标签",
@@ -425,8 +425,8 @@ export const i18nData = {
"Cloudflare Turnstile Secret Key": "Cloudflare Turnstile 密鑰",
"If the cloudflare turnstile keys are not empty, the turnstile will be used for register and download.":
"如果設置了 Cloudflare Turnstile 密鑰,將在註冊和下載時啟用驗證",
"The first image will be used as the cover image":
"第一張圖片將用作封面圖片",
"You can select a cover image using the radio button in the Cover column":
"您可以使用封面列中的單選按鈕選擇封面圖片",
"Please enter a search keyword": "請輸入搜尋關鍵字",
"Searching...": "搜尋中...",
"Create Tag": "創建標籤",

View File

@@ -48,6 +48,7 @@ export interface CreateResourceParams {
tags: number[];
article: string;
images: number[];
cover_id?: number;
gallery: number[];
gallery_nsfw: number[];
characters: CharacterParams[];
@@ -94,6 +95,7 @@ export interface ResourceDetails {
releaseDate?: string;
tags: Tag[];
images: Image[];
coverId?: number;
files: RFile[];
author: User;
views: number;

View File

@@ -29,6 +29,7 @@ export default function EditResourcePage() {
const [tags, setTags] = useState<Tag[]>([]);
const [article, setArticle] = useState<string>("");
const [images, setImages] = useState<number[]>([]);
const [coverId, setCoverId] = useState<number | undefined>(undefined);
const [links, setLinks] = useState<{ label: string; url: string }[]>([]);
const [galleryImages, setGalleryImages] = useState<number[]>([]);
const [galleryNsfw, setGalleryNsfw] = useState<number[]>([]);
@@ -59,6 +60,7 @@ export default function EditResourcePage() {
setTags(data.tags);
setArticle(data.article);
setImages(data.images.map((i) => i.id));
setCoverId(data.coverId);
setLinks(data.links ?? []);
setGalleryImages(data.gallery ?? []);
setGalleryNsfw(data.galleryNsfw ?? []);
@@ -106,6 +108,7 @@ export default function EditResourcePage() {
tags: tags.map((tag) => tag.id),
article: article,
images: images,
cover_id: coverId,
links: links,
gallery: galleryImages,
gallery_nsfw: galleryNsfw,
@@ -328,7 +331,7 @@ export default function EditResourcePage() {
"Images will not be displayed automatically, you need to reference them in the description",
)}
</p>
<p>{t("The first image will be used as the cover image")}</p>
<p>{t("You can select a cover image using the radio button in the Cover column")}</p>
</div>
</div>
<div
@@ -339,6 +342,7 @@ export default function EditResourcePage() {
<tr>
<td>{t("Preview")}</td>
<td>{"Markdown"}</td>
<td>{t("Cover")}</td>
<td>{t("Gallery")}</td>
<td>{"Nsfw"}</td>
<td>{t("Action")}</td>
@@ -368,6 +372,15 @@ export default function EditResourcePage() {
<MdContentCopy />
</button>
</td>
<td>
<input
type="radio"
name="cover"
className="radio radio-accent"
checked={coverId === image}
onChange={() => setCoverId(image)}
/>
</td>
<td>
<input
type="checkbox"
@@ -409,6 +422,9 @@ export default function EditResourcePage() {
const newImages = [...images];
newImages.splice(index, 1);
setImages(newImages);
if (coverId === id) {
setCoverId(undefined);
}
network.deleteImage(id);
}}
>

View File

@@ -5,7 +5,6 @@ import { app } from "../app.ts";
import { Resource, RSort, Statistics } from "../network/models.ts";
import { useTranslation } from "../utils/i18n";
import { useAppContext } from "../components/AppContext.tsx";
import Select from "../components/select.tsx";
import { useNavigate } from "react-router";
import { useNavigator } from "../components/navigator.tsx";
import {
@@ -40,8 +39,18 @@ export default function HomePage() {
<>
<HomeHeader />
<div className={"flex pt-4 px-4 items-center"}>
<Select
values={[
<select
value={order}
className="select select-primary max-w-72"
onChange={(e) => {
const order = parseInt(e.target.value);
setOrder(order);
if (appContext) {
appContext.set("home_page_order", order);
}
}}
>
{[
t("Time Ascending"),
t("Time Descending"),
t("Views Ascending"),
@@ -50,15 +59,12 @@ export default function HomePage() {
t("Downloads Descending"),
t("Release Date Ascending"),
t("Release Date Descending"),
]}
current={order}
onSelected={(index) => {
setOrder(index);
if (appContext) {
appContext.set("home_page_order", index);
}
}}
/>
].map((label, idx) => (
<option key={idx} value={idx}>
{label}
</option>
))}
</select>
</div>
<ResourcesView
key={`home_page_${order}`}

View File

@@ -28,6 +28,7 @@ export default function PublishPage() {
const [tags, setTags] = useState<Tag[]>([]);
const [article, setArticle] = useState<string>("");
const [images, setImages] = useState<number[]>([]);
const [coverId, setCoverId] = useState<number | undefined>(undefined);
const [links, setLinks] = useState<{ label: string; url: string }[]>([]);
const [galleryImages, setGalleryImages] = useState<number[]>([]);
const [galleryNsfw, setGalleryNsfw] = useState<number[]>([]);
@@ -48,6 +49,7 @@ export default function PublishPage() {
setTags(data.tags || []);
setArticle(data.article || "");
setImages(data.images || []);
setCoverId(data.cover_id || undefined);
setLinks(data.links || []);
setGalleryImages(data.gallery || []);
setGalleryNsfw(data.gallery_nsfw || []);
@@ -64,6 +66,7 @@ export default function PublishPage() {
tags: tags,
article: article,
images: images,
cover_id: coverId,
links: links,
gallery: galleryImages,
gallery_nsfw: galleryNsfw,
@@ -73,7 +76,7 @@ export default function PublishPage() {
const dataString = JSON.stringify(data);
localStorage.setItem("publish_data", dataString);
}
}, [altTitles, article, images, tags, title, links, galleryImages, galleryNsfw, characters, releaseDate]);
}, [altTitles, article, images, coverId, tags, title, links, galleryImages, galleryNsfw, characters, releaseDate]);
const navigate = useNavigate();
const { t } = useTranslation();
@@ -120,6 +123,7 @@ export default function PublishPage() {
tags: tags.map((tag) => tag.id),
article: article,
images: images,
cover_id: coverId,
links: links,
gallery: galleryImages,
gallery_nsfw: galleryNsfw,
@@ -344,7 +348,7 @@ export default function PublishPage() {
"Images will not be displayed automatically, you need to reference them in the description",
)}
</p>
<p>{t("The first image will be used as the cover image")}</p>
<p>{t("You can select a cover image using the radio button in the Cover column")}</p>
</div>
</div>
<div
@@ -355,7 +359,8 @@ export default function PublishPage() {
<tr>
<td>{t("Preview")}</td>
<td>{"Markdown"}</td>
<td>{"Gallery"}</td>
<td>{t("Cover")}</td>
<td>{t("Gallery")}</td>
<td>{"Nsfw"}</td>
<td>{t("Action")}</td>
</tr>
@@ -384,6 +389,15 @@ export default function PublishPage() {
<MdContentCopy />
</button>
</td>
<td>
<input
type="radio"
name="cover"
className="radio radio-accent"
checked={coverId === image}
onChange={() => setCoverId(image)}
/>
</td>
<td>
<input
type="checkbox"
@@ -425,6 +439,9 @@ export default function PublishPage() {
const newImages = [...images];
newImages.splice(index, 1);
setImages(newImages);
if (coverId === id) {
setCoverId(undefined);
}
network.deleteImage(id);
}}
>

View File

@@ -14,8 +14,9 @@ type Resource struct {
ReleaseDate *time.Time
Article string
Images []Image `gorm:"many2many:resource_images;"`
Tags []Tag `gorm:"many2many:resource_tags;"`
Files []File `gorm:"foreignKey:ResourceID"`
CoverID *uint
Tags []Tag `gorm:"many2many:resource_tags;"`
Files []File `gorm:"foreignKey:ResourceID"`
UserID uint
User User
Views uint
@@ -52,6 +53,7 @@ type ResourceDetailView struct {
ReleaseDate *time.Time `json:"releaseDate,omitempty"`
Tags []TagView `json:"tags"`
Images []ImageView `json:"images"`
CoverID *uint `json:"coverId,omitempty"`
Files []FileView `json:"files"`
Author UserView `json:"author"`
Views uint `json:"views"`
@@ -78,7 +80,18 @@ func (r *Resource) ToView() ResourceView {
}
var image *ImageView
if len(r.Images) > 0 {
if r.CoverID != nil {
// Use the cover image if specified
for _, img := range r.Images {
if img.ID == *r.CoverID {
v := img.ToView()
image = &v
break
}
}
}
// If no cover is set or cover image not found, use the first image
if image == nil && len(r.Images) > 0 {
v := r.Images[0].ToView()
image = &v
}
@@ -122,6 +135,7 @@ func (r *Resource) ToDetailView() ResourceDetailView {
ReleaseDate: r.ReleaseDate,
Tags: tags,
Images: images,
CoverID: r.CoverID,
Files: files,
Author: r.User.ToView(),
Views: r.Views,

View File

@@ -34,6 +34,7 @@ type ResourceParams struct {
Tags []uint `json:"tags"`
Article string `json:"article"`
Images []uint `json:"images"`
CoverID *uint `json:"cover_id"`
Gallery []uint `json:"gallery"`
GalleryNsfw []uint `json:"gallery_nsfw"`
Characters []CharacterParams `json:"characters"`
@@ -110,6 +111,14 @@ func CreateResource(uid uint, params *ResourceParams) (uint, error) {
}
date = &parsedDate
}
// Validate CoverID if provided
var coverID *uint
if params.CoverID != nil && *params.CoverID != 0 {
if !slices.Contains(params.Images, *params.CoverID) {
return 0, model.NewRequestError("Cover ID must be one of the resource images")
}
coverID = params.CoverID
}
r := model.Resource{
Title: params.Title,
AlternativeTitles: params.AlternativeTitles,
@@ -117,6 +126,7 @@ func CreateResource(uid uint, params *ResourceParams) (uint, error) {
Links: params.Links,
ReleaseDate: date,
Images: images,
CoverID: coverID,
Tags: tags,
UserID: uid,
Gallery: gallery,
@@ -548,11 +558,21 @@ func UpdateResource(uid, rid uint, params *ResourceParams) error {
date = &parsedDate
}
// Validate CoverID if provided
var coverID *uint
if params.CoverID != nil && *params.CoverID != 0 {
if !slices.Contains(params.Images, *params.CoverID) {
return model.NewRequestError("Cover ID must be one of the resource images")
}
coverID = params.CoverID
}
r.Title = params.Title
r.AlternativeTitles = params.AlternativeTitles
r.Article = params.Article
r.Links = params.Links
r.ReleaseDate = date
r.CoverID = coverID
r.Gallery = gallery
r.GalleryNsfw = nsfw
r.Characters = characters