diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e55059c..3e5b280 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,7 +15,6 @@ "masonic": "^4.1.0", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-i18next": "^15.5.1", "react-icons": "^5.5.0", "react-markdown": "^10.1.0", "react-router": "^7.5.3", @@ -29,7 +28,7 @@ "@types/react-dom": "^19.0.4", "@types/spark-md5": "^3.0.5", "@vitejs/plugin-react": "^4.3.4", - "daisyui": "^5.0.35", + "daisyui": "^5.5.5", "eslint": "^9.22.0", "eslint-config-prettier": "^10.1.5", "eslint-plugin-prettier": "^5.4.1", @@ -281,15 +280,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/runtime": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", - "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/template": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.1.tgz", @@ -2558,9 +2548,9 @@ "license": "MIT" }, "node_modules/daisyui": { - "version": "5.0.35", - "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.0.35.tgz", - "integrity": "sha512-AWi11n/x5++mps55jcwrBf0Lmip1euWY0FYcH/05SFGmoqrU7S7/aIUWaiaeqlJ5EcmEZ/7zEY73aOxMv6hcIg==", + "version": "5.5.5", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.5.5.tgz", + "integrity": "sha512-ekvI93ZkWIJoCOtDl0D2QMxnWvTejk9V5nWBqRv+7t0xjiBXqAK5U6o6JE2RPvlIC3EqwNyUoIZSdHX9MZK3nw==", "dev": true, "license": "MIT", "funding": { @@ -3618,15 +3608,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/html-parse-stringify": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", - "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", - "license": "MIT", - "dependencies": { - "void-elements": "3.1.0" - } - }, "node_modules/html-url-attributes": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", @@ -3670,38 +3651,6 @@ "url": "https://github.com/sponsors/typicode" } }, - "node_modules/i18next": { - "version": "25.1.1", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.1.1.tgz", - "integrity": "sha512-FZcp3vk3PXc8onasbsWYahfeDIWX4LkKr4vd01xeXrmqyNXlVNtVecEIw2K1o8z3xYrHMcd1bwYQub+3g7zqCw==", - "funding": [ - { - "type": "individual", - "url": "https://locize.com" - }, - { - "type": "individual", - "url": "https://locize.com/i18next.html" - }, - { - "type": "individual", - "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" - } - ], - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.26.10" - }, - "peerDependencies": { - "typescript": "^5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -5681,32 +5630,6 @@ "react": "^19.1.0" } }, - "node_modules/react-i18next": { - "version": "15.5.1", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.5.1.tgz", - "integrity": "sha512-C8RZ7N7H0L+flitiX6ASjq9p5puVJU1Z8VyL3OgM/QOMRf40BMZX+5TkpxzZVcTmOLPX5zlti4InEX5pFyiVeA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.25.0", - "html-parse-stringify": "^3.0.1" - }, - "peerDependencies": { - "i18next": ">= 23.2.3", - "react": ">= 16.8.0", - "typescript": "^5" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, "node_modules/react-icons": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", @@ -6418,7 +6341,7 @@ "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -6727,15 +6650,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/void-elements": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", - "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 07bf65d..cbaa2fa 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -35,7 +35,7 @@ "@types/react-dom": "^19.0.4", "@types/spark-md5": "^3.0.5", "@vitejs/plugin-react": "^4.3.4", - "daisyui": "^5.0.35", + "daisyui": "^5.5.5", "eslint": "^9.22.0", "eslint-config-prettier": "^10.1.5", "eslint-plugin-prettier": "^5.4.1", diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts index 542d687..6c34543 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/i18n.ts @@ -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": "可選", }, }, }; diff --git a/frontend/src/network/models.ts b/frontend/src/network/models.ts index f1e26a8..d55cb63 100644 --- a/frontend/src/network/models.ts +++ b/frontend/src/network/models.ts @@ -123,6 +123,7 @@ export interface RFile { hash?: string; storage_name?: string; created_at: number; // unix timestamp + tag?: string; } export interface UploadingFile { diff --git a/frontend/src/network/network.ts b/frontend/src/network/network.ts index 363ef98..0199da5 100644 --- a/frontend/src/network/network.ts +++ b/frontend/src/network/network.ts @@ -479,6 +479,7 @@ class Network { fileSize: number, resourceId: number, storageId: number, + tag: string, ): Promise> { 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> { 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> { 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> { return this._callApi(() => axios.put(`${this.apiBaseUrl}/files/${fileId}`, { filename, description, + tag, }), ); } diff --git a/frontend/src/pages/resource_details_page.tsx b/frontend/src/pages/resource_details_page.tsx index 882c86e..8be342c 100644 --- a/frontend/src/pages/resource_details_page.tsx +++ b/frontend/src/pages/resource_details_page.tsx @@ -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} )} + {file.tag && ( + + {file.tag} + + )} {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>(new Set()); + + // Extract unique tags from all files + const allTags = useMemo(() => { + const tags = new Set(); + 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 (
- {files.map((file) => { + {allTags.length > 0 && ( +
+ {allTags.map((tag) => ( + toggleTag(tag)} + /> + ))} + {selectedTags.size > 0 && ( + setSelectedTags(new Set())} + /> + )} +
+ )} + {filteredFiles.map((file) => { return ; })} + {filteredFiles.length === 0 && selectedTags.size > 0 && ( +
+ {t("No files match the selected tags")} +
+ )}
{(app.canUpload() || (app.allowNormalUserUpload && app.isLoggedIn())) && (
@@ -954,6 +1021,10 @@ function CreateFileDialog({ resourceId }: { resourceId: number }) { const [storage, setStorage] = useState(null); const [file, setFile] = useState(null); const [description, setDescription] = useState(""); + const [tag, setTag] = useState(""); + const [fileSize, setFileSize] = useState(""); + const [fileSizeUnit, setFileSizeUnit] = useState("MB"); + const [md5, setMd5] = useState(""); const [fileUrl, setFileUrl] = useState(""); @@ -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 }) {

{t("Type")}

{ - setFileType(null); - }} - /> - + { + setFileType(null); + }} + />
{fileType === FileType.redirect && ( @@ -1183,6 +1282,45 @@ function CreateFileDialog({ resourceId }: { resourceId: number }) { setDescription(e.target.value); }} /> + { + setTag(e.target.value); + }} + /> +
+ { + setFileSize(e.target.value); + }} + /> + +
+ { + setMd5(e.target.value); + }} + /> )} @@ -1239,6 +1377,14 @@ function CreateFileDialog({ resourceId }: { resourceId: number }) { setDescription(e.target.value); }} /> + { + setTag(e.target.value); + }} + /> )} @@ -1311,6 +1457,14 @@ function CreateFileDialog({ resourceId }: { resourceId: number }) { setDescription(e.target.value); }} /> + { + 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)} /> + setTag(e.target.value)} + />
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 023c6b9..2719e02 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -8,8 +8,8 @@ export default defineConfig({ server: { proxy: { "/api": { - target: "http://localhost:3000", - // target: "https://res.nyne.dev", + // target: "http://localhost:3000", + target: "https://nysoure.com", changeOrigin: true, }, "https://www.moyu.moe": {