mirror of
https://github.com/wgh136/nysoure.git
synced 2025-12-18 08:21:14 +00:00
feat: Add tag field to file models, and enhance file upload functionality with optional fields
This commit is contained in:
@@ -258,6 +258,9 @@ export const i18nData = {
|
||||
"Survival time": "存活时间",
|
||||
"Characters": "角色",
|
||||
"Aliases (one per line)": "别名(每行一个)",
|
||||
"File Size": "文件大小",
|
||||
"Tag": "标签",
|
||||
"Optional": "可选",
|
||||
},
|
||||
},
|
||||
"zh-TW": {
|
||||
@@ -517,6 +520,11 @@ export const i18nData = {
|
||||
"Private": "私有",
|
||||
"View {count} more replies": "查看另外 {count} 條回覆",
|
||||
"Survival time": "存活時間",
|
||||
"Characters": "角色",
|
||||
"Aliases (one per line)": "別名(每行一個)",
|
||||
"File Size": "檔案大小",
|
||||
"Tag": "標籤",
|
||||
"Optional": "可選",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -123,6 +123,7 @@ export interface RFile {
|
||||
hash?: string;
|
||||
storage_name?: string;
|
||||
created_at: number; // unix timestamp
|
||||
tag?: string;
|
||||
}
|
||||
|
||||
export interface UploadingFile {
|
||||
|
||||
@@ -479,6 +479,7 @@ class Network {
|
||||
fileSize: number,
|
||||
resourceId: number,
|
||||
storageId: number,
|
||||
tag: string,
|
||||
): Promise<Response<UploadingFile>> {
|
||||
return this._callApi(() =>
|
||||
axios.post(`${this.apiBaseUrl}/files/upload/init`, {
|
||||
@@ -487,6 +488,7 @@ class Network {
|
||||
file_size: fileSize,
|
||||
resource_id: resourceId,
|
||||
storage_id: storageId,
|
||||
tag,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -529,6 +531,9 @@ class Network {
|
||||
description: string,
|
||||
resourceId: number,
|
||||
redirectUrl: string,
|
||||
fileSize: number,
|
||||
md5: string,
|
||||
tag: string,
|
||||
): Promise<Response<RFile>> {
|
||||
return this._callApi(() =>
|
||||
axios.post(`${this.apiBaseUrl}/files/redirect`, {
|
||||
@@ -536,6 +541,9 @@ class Network {
|
||||
description,
|
||||
resource_id: resourceId,
|
||||
redirect_url: redirectUrl,
|
||||
file_size: fileSize,
|
||||
md5,
|
||||
tag,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -546,6 +554,7 @@ class Network {
|
||||
description: string,
|
||||
resourceId: number,
|
||||
storageId: number,
|
||||
tag: string,
|
||||
): Promise<Response<RFile>> {
|
||||
return this._callApi(() =>
|
||||
axios.post(`${this.apiBaseUrl}/files/upload/url`, {
|
||||
@@ -554,6 +563,7 @@ class Network {
|
||||
description,
|
||||
resource_id: resourceId,
|
||||
storage_id: storageId,
|
||||
tag,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -566,11 +576,13 @@ class Network {
|
||||
fileId: string,
|
||||
filename: string,
|
||||
description: string,
|
||||
tag: string,
|
||||
): Promise<Response<RFile>> {
|
||||
return this._callApi(() =>
|
||||
axios.put(`${this.apiBaseUrl}/files/${fileId}`, {
|
||||
filename,
|
||||
description,
|
||||
tag,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
@@ -801,6 +802,11 @@ function FileTile({ file }: { file: RFile }) {
|
||||
{file.storage_name}
|
||||
</Badge>
|
||||
)}
|
||||
{file.tag && (
|
||||
<Badge className={"badge-soft badge-warning text-xs mr-2"}>
|
||||
{file.tag}
|
||||
</Badge>
|
||||
)}
|
||||
<Badge className={"badge-soft badge-info text-xs mr-2"}>
|
||||
<MdOutlineAccessTime size={16} className={"inline-block"} />
|
||||
{new Date(file.created_at * 1000).toISOString().substring(0, 10)}
|
||||
@@ -919,11 +925,72 @@ function Files({
|
||||
files: RFile[];
|
||||
resource: ResourceDetails;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [selectedTags, setSelectedTags] = useState<Set<string>>(new Set());
|
||||
|
||||
// Extract unique tags from all files
|
||||
const allTags = useMemo(() => {
|
||||
const tags = new Set<string>();
|
||||
files.forEach((file) => {
|
||||
if (file.tag) {
|
||||
tags.add(file.tag);
|
||||
}
|
||||
});
|
||||
return Array.from(tags).sort();
|
||||
}, [files]);
|
||||
|
||||
// Filter files based on selected tags
|
||||
const filteredFiles = useMemo(() => {
|
||||
if (selectedTags.size === 0) {
|
||||
return files;
|
||||
}
|
||||
return files.filter((file) => file.tag && selectedTags.has(file.tag));
|
||||
}, [files, selectedTags]);
|
||||
|
||||
const toggleTag = (tag: string) => {
|
||||
setSelectedTags((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(tag)) {
|
||||
newSet.delete(tag);
|
||||
} else {
|
||||
newSet.add(tag);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={"pt-3"}>
|
||||
{files.map((file) => {
|
||||
{allTags.length > 0 && (
|
||||
<form className="filter mb-4">
|
||||
{allTags.map((tag) => (
|
||||
<input
|
||||
key={tag}
|
||||
className="btn"
|
||||
type="checkbox"
|
||||
aria-label={tag}
|
||||
checked={selectedTags.has(tag)}
|
||||
onChange={() => toggleTag(tag)}
|
||||
/>
|
||||
))}
|
||||
{selectedTags.size > 0 && (
|
||||
<input
|
||||
className="btn btn-square"
|
||||
type="reset"
|
||||
value="×"
|
||||
onClick={() => setSelectedTags(new Set())}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
)}
|
||||
{filteredFiles.map((file) => {
|
||||
return <FileTile file={file} key={file.id}></FileTile>;
|
||||
})}
|
||||
{filteredFiles.length === 0 && selectedTags.size > 0 && (
|
||||
<div className="text-center text-base-content/60 py-8">
|
||||
{t("No files match the selected tags")}
|
||||
</div>
|
||||
)}
|
||||
<div className={"h-2"}></div>
|
||||
{(app.canUpload() || (app.allowNormalUserUpload && app.isLoggedIn())) && (
|
||||
<div className={"flex flex-row-reverse"}>
|
||||
@@ -954,6 +1021,10 @@ function CreateFileDialog({ resourceId }: { resourceId: number }) {
|
||||
const [storage, setStorage] = useState<Storage | null>(null);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [description, setDescription] = useState<string>("");
|
||||
const [tag, setTag] = useState<string>("");
|
||||
const [fileSize, setFileSize] = useState<string>("");
|
||||
const [fileSizeUnit, setFileSizeUnit] = useState<string>("MB");
|
||||
const [md5, setMd5] = useState<string>("");
|
||||
|
||||
const [fileUrl, setFileUrl] = useState<string>("");
|
||||
|
||||
@@ -985,11 +1056,38 @@ function CreateFileDialog({ resourceId }: { resourceId: number }) {
|
||||
setSubmitting(false);
|
||||
return;
|
||||
}
|
||||
let fileSizeNum = 0;
|
||||
if (fileSize) {
|
||||
const size = parseFloat(fileSize);
|
||||
if (isNaN(size)) {
|
||||
setError(t("File size must be a number"));
|
||||
setSubmitting(false);
|
||||
return;
|
||||
}
|
||||
// Convert to bytes based on unit
|
||||
switch (fileSizeUnit) {
|
||||
case "B":
|
||||
fileSizeNum = size;
|
||||
break;
|
||||
case "KB":
|
||||
fileSizeNum = size * 1024;
|
||||
break;
|
||||
case "MB":
|
||||
fileSizeNum = size * 1024 * 1024;
|
||||
break;
|
||||
case "GB":
|
||||
fileSizeNum = size * 1024 * 1024 * 1024;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const res = await network.createRedirectFile(
|
||||
filename,
|
||||
description,
|
||||
resourceId,
|
||||
redirectUrl,
|
||||
fileSizeNum,
|
||||
md5,
|
||||
tag,
|
||||
);
|
||||
if (res.success) {
|
||||
setSubmitting(false);
|
||||
@@ -1046,6 +1144,7 @@ function CreateFileDialog({ resourceId }: { resourceId: number }) {
|
||||
description,
|
||||
resourceId,
|
||||
storage.id,
|
||||
tag,
|
||||
);
|
||||
if (res.success) {
|
||||
setSubmitting(false);
|
||||
@@ -1119,15 +1218,7 @@ function CreateFileDialog({ resourceId }: { resourceId: number }) {
|
||||
<p className={"text-sm font-bold p-2"}>{t("Type")}</p>
|
||||
<form className="filter mb-2">
|
||||
<input
|
||||
className="btn btn-square"
|
||||
type="reset"
|
||||
value="×"
|
||||
onClick={() => {
|
||||
setFileType(null);
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
className="btn text-sm"
|
||||
className="btn"
|
||||
type="radio"
|
||||
name="type"
|
||||
aria-label={t("Redirect")}
|
||||
@@ -1136,7 +1227,7 @@ function CreateFileDialog({ resourceId }: { resourceId: number }) {
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
className="btn text-sm"
|
||||
className="btn"
|
||||
type="radio"
|
||||
name="type"
|
||||
aria-label={t("Upload")}
|
||||
@@ -1145,7 +1236,7 @@ function CreateFileDialog({ resourceId }: { resourceId: number }) {
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
className="btn text-sm"
|
||||
className="btn"
|
||||
type="radio"
|
||||
name="type"
|
||||
aria-label={t("File Url")}
|
||||
@@ -1153,6 +1244,14 @@ function CreateFileDialog({ resourceId }: { resourceId: number }) {
|
||||
setFileType(FileType.serverTask);
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
className="btn btn-square"
|
||||
type="reset"
|
||||
value="×"
|
||||
onClick={() => {
|
||||
setFileType(null);
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
|
||||
{fileType === FileType.redirect && (
|
||||
@@ -1183,6 +1282,45 @@ function CreateFileDialog({ resourceId }: { resourceId: number }) {
|
||||
setDescription(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
className="input w-full my-2"
|
||||
placeholder={t("Tag") + " (" + t("Optional") + ")"}
|
||||
onChange={(e) => {
|
||||
setTag(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<div className="join w-full">
|
||||
<input
|
||||
type="number"
|
||||
className="input flex-1 join-item"
|
||||
placeholder={t("File Size") + " (" + t("Optional") + ")"}
|
||||
value={fileSize}
|
||||
onChange={(e) => {
|
||||
setFileSize(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<select
|
||||
className="select w-24 join-item"
|
||||
value={fileSizeUnit}
|
||||
onChange={(e) => {
|
||||
setFileSizeUnit(e.target.value);
|
||||
}}
|
||||
>
|
||||
<option value="B">B</option>
|
||||
<option value="KB">KB</option>
|
||||
<option value="MB">MB</option>
|
||||
<option value="GB">GB</option>
|
||||
</select>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
className="input w-full my-2"
|
||||
placeholder={"MD5" + " (" + t("Optional") + ")"}
|
||||
onChange={(e) => {
|
||||
setMd5(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1239,6 +1377,14 @@ function CreateFileDialog({ resourceId }: { resourceId: number }) {
|
||||
setDescription(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
className="input w-full my-2"
|
||||
placeholder={t("Tag") + " (" + t("Optional") + ")"}
|
||||
onChange={(e) => {
|
||||
setTag(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1311,6 +1457,14 @@ function CreateFileDialog({ resourceId }: { resourceId: number }) {
|
||||
setDescription(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
className="input w-full my-2"
|
||||
placeholder={t("Tag") + " (" + t("Optional") + ")"}
|
||||
onChange={(e) => {
|
||||
setTag(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1340,6 +1494,8 @@ function UpdateFileInfoDialog({ file }: { file: RFile }) {
|
||||
|
||||
const [description, setDescription] = useState(file.description);
|
||||
|
||||
const [tag, setTag] = useState(file.tag || "");
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const reload = useContext(context);
|
||||
@@ -1349,7 +1505,7 @@ function UpdateFileInfoDialog({ file }: { file: RFile }) {
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
const res = await network.updateFile(file.id, filename, description);
|
||||
const res = await network.updateFile(file.id, filename, description, tag);
|
||||
const dialog = document.getElementById(
|
||||
`update_file_info_dialog_${file.id}`,
|
||||
) as HTMLDialogElement;
|
||||
@@ -1397,6 +1553,12 @@ function UpdateFileInfoDialog({ file }: { file: RFile }) {
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
type={"text"}
|
||||
label={t("Tag") + " (" + t("Optional") + ")"}
|
||||
value={tag}
|
||||
onChange={(e) => setTag(e.target.value)}
|
||||
/>
|
||||
<div className="modal-action">
|
||||
<form method="dialog">
|
||||
<button className="btn btn-ghost">{t("Close")}</button>
|
||||
|
||||
Reference in New Issue
Block a user