From b28e0401ab7e24a47ff105267f3f6b6349694119 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Tue, 15 Jul 2025 10:38:32 +0800 Subject: [PATCH] feat(explorer): custom icon with Iconify --- public/locales/en-US/dashboard.json | 2 + public/locales/ja-JP/dashboard.json | 2 + public/locales/zh-CN/dashboard.json | 2 + public/locales/zh-TW/dashboard.json | 2 + .../Admin/FileSystem/Icons/FileIconList.tsx | 347 ++++++++++++------ src/component/Common/SizeInput.tsx | 2 +- .../FileManager/Explorer/FileTypeIcon.tsx | 36 +- 7 files changed, 283 insertions(+), 110 deletions(-) diff --git a/public/locales/en-US/dashboard.json b/public/locales/en-US/dashboard.json index 5a5c691..80d3bad 100644 --- a/public/locales/en-US/dashboard.json +++ b/public/locales/en-US/dashboard.json @@ -112,6 +112,8 @@ "retryDelayDes": "Initial delay time (seconds) for task retries." }, "settings": { + "imageUrl": "Image URL", + "iconifyName": "Iconify icon name", "oidc": "OpenID Connect (OIDC)", "oidcDes": "OpenID Connect (OIDC) is an open authentication protocol for identity verification between different systems. After creating an application in a third-party identity platform, please add <0>{{url}} to the \"Redirect URI\" field. For more details, please refer to the <1>documentation.", "clientID": "Client ID", diff --git a/public/locales/ja-JP/dashboard.json b/public/locales/ja-JP/dashboard.json index d3dcd26..de0d1bc 100644 --- a/public/locales/ja-JP/dashboard.json +++ b/public/locales/ja-JP/dashboard.json @@ -112,6 +112,8 @@ "retryDelayDes": "タスク再試行の初期遅延時間(秒)" }, "settings": { + "imageUrl": "画像 URL", + "iconifyName": "Iconify アイコン名", "oidc": "OpenID Connect (OIDC)", "oidcDes": "OpenID Connect (OIDC) は、異なるシステム間で認証を行うためのオープンな認証プロトコルです。サードパーティーのアイデンティティプラットフォームでアプリケーションを作成したら、<0>{{url}} を「リダイレクト URI」に追加してください。詳細は<1>公式ドキュメントを参照してください。", "clientID": "クライアント ID", diff --git a/public/locales/zh-CN/dashboard.json b/public/locales/zh-CN/dashboard.json index 9b600a5..914ae6e 100644 --- a/public/locales/zh-CN/dashboard.json +++ b/public/locales/zh-CN/dashboard.json @@ -112,6 +112,8 @@ "retryDelayDes": "任务重试的初始延迟时间(秒)。" }, "settings": { + "imageUrl": "图片 URL", + "iconifyName": "Iconify 图标名", "oidc": "OpenID Connect (OIDC)", "oidcDes": "OpenID Connect (OIDC) 是一种开放的认证协议,用于在不同的系统之间进行身份验证。在第三方身份平台中创建应用后,请将 <0>{{url}} 添加到 “重定向 URI” 中。详情请参考 <1>官方文档。", "clientID": "客户端 ID", diff --git a/public/locales/zh-TW/dashboard.json b/public/locales/zh-TW/dashboard.json index 18a2856..670b315 100644 --- a/public/locales/zh-TW/dashboard.json +++ b/public/locales/zh-TW/dashboard.json @@ -112,6 +112,8 @@ "retryDelayDes": "任務重試的初始延遲時間(秒)。" }, "settings": { + "imageUrl": "圖片 URL", + "iconifyName": "Iconify 圖標名", "oidc": "OpenID Connect (OIDC)", "oidcDes": "OpenID Connect (OIDC) 是一種開放的認證身份驗證協定,用於在不同的系統之間進行身份驗證。在第三方身份平台中創建應用後,請將 <0>{{url}} 添加到 “重定向 URI” 中。詳細請參閱 <1>官方文檔。", "clientID": "客戶端 ID", diff --git a/src/component/Admin/FileSystem/Icons/FileIconList.tsx b/src/component/Admin/FileSystem/Icons/FileIconList.tsx index da101d1..8a50f61 100644 --- a/src/component/Admin/FileSystem/Icons/FileIconList.tsx +++ b/src/component/Admin/FileSystem/Icons/FileIconList.tsx @@ -1,14 +1,28 @@ -import { Box, IconButton, Table, TableBody, TableContainer, TableHead, TableRow } from "@mui/material"; -import { useTheme } from "@mui/material/styles"; +import { Icon } from "@iconify/react/dist/iconify.js"; +import { + Box, + IconButton, + InputAdornment, + ListItemText, + Table, + TableBody, + TableContainer, + TableHead, + TableRow, + Typography, +} from "@mui/material"; +import { styled, useTheme } from "@mui/material/styles"; import { memo, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { DenseFilledTextField, + DenseSelect, NoWrapCell, NoWrapTableCell, SecondaryButton, StyledTableContainerPaper, } from "../../../Common/StyledComponents.tsx"; +import { SquareMenuItem } from "../../../FileManager/ContextMenu/ContextMenu.tsx"; import { builtInIcons, FileTypeIconSetting } from "../../../FileManager/Explorer/FileTypeIcon.tsx"; import Add from "../../../Icons/Add.tsx"; import Dismiss from "../../../Icons/Dismiss.tsx"; @@ -19,6 +33,20 @@ export interface FileIconListProps { onChange: (value: string) => void; } +export enum IconType { + Image = "imageUrl", + Iconify = "iconifyName", +} + +const StyledDenseSelect = styled(DenseSelect)(() => ({ + "& .MuiFilledInput-input": { + "&:focus": { + backgroundColor: "initial", + }, + }, + backgroundColor: "initial", +})); + const IconPreview = ({ icon }: { icon: FileTypeIconSetting }) => { const theme = useTheme(); const IconComponent = useMemo(() => { @@ -45,6 +73,12 @@ const IconPreview = ({ icon }: { icon: FileTypeIconSetting }) => { /> ); } + + // Handle iconify icons + if (icon.iconify) { + return ; + } + return ( { const [inputCache, setInputCache] = useState<{ [key: number]: string | undefined; }>({}); + const [iconUrlCache, setIconUrlCache] = useState<{ + [key: number]: string | undefined; + }>({}); + const [iconTypeCache, setIconTypeCache] = useState<{ + [key: number]: IconType | undefined; + }>({}); + return ( {configParsed?.length > 0 && ( @@ -71,123 +112,215 @@ const FileIconList = memo(({ config, onChange }: FileIconListProps) => { {t("settings.icon")} - {t("settings.iconUrl")} + {t("settings.iconUrl")} {t("settings.iconColor")} {t("settings.iconColorDark")} - {t("settings.exts")} + {t("settings.exts")} - {configParsed.map((r, i) => ( - - - - - - {!r.icon ? ( + {configParsed.map((r, i) => { + const currentIconType = + iconTypeCache[i] ?? (r.img ? IconType.Image : r.iconify ? IconType.Iconify : IconType.Image); + const currentIconUrl = + iconUrlCache[i] ?? (currentIconType === IconType.Image ? r.img : r.iconify) ?? ""; + + return ( + + + + + + {!r.icon ? ( + { + const newConfig = [...configParsed]; + const updatedItem = { ...r }; + + if (currentIconType === IconType.Image) { + updatedItem.img = currentIconUrl; + updatedItem.iconify = undefined; + } else { + updatedItem.iconify = currentIconUrl; + updatedItem.img = undefined; + } + + newConfig[i] = updatedItem; + onChange(JSON.stringify(newConfig)); + + setIconUrlCache({ + ...iconUrlCache, + [i]: undefined, + }); + }} + onChange={(e) => + setIconUrlCache({ + ...iconUrlCache, + [i]: e.target.value, + }) + } + slotProps={{ + input: { + startAdornment: ( + + { + const newType = e.target.value as IconType; + setIconTypeCache({ + ...iconTypeCache, + [i]: newType, + }); + + // Clear the URL cache when switching types + setIconUrlCache({ + ...iconUrlCache, + [i]: "", + }); + + // Update the config immediately + const newConfig = [...configParsed]; + const updatedItem = { ...r }; + + if (newType === IconType.Image) { + updatedItem.img = ""; + updatedItem.iconify = undefined; + } else { + updatedItem.iconify = ""; + updatedItem.img = undefined; + } + + newConfig[i] = updatedItem; + onChange(JSON.stringify(newConfig)); + }} + renderValue={(value) => ( + {t(`settings.${value}`)} + )} + size={"small"} + variant="filled" + > + + + {t(`settings.${IconType.Image}`)} + + + + + {t(`settings.${IconType.Iconify}`)} + + + + + ), + }, + }} + /> + ) : ( + t("settings.builtinIcon") + )} + + + {!r.icon && !r.iconify ? ( + "-" + ) : ( + + onChange( + JSON.stringify([ + ...configParsed.slice(0, i), + { + ...r, + color: color, + }, + ...configParsed.slice(i + 1), + ]), + ) + } + /> + )} + + + {!r.icon && !r.iconify ? ( + "-" + ) : ( + + onChange( + JSON.stringify([ + ...configParsed.slice(0, i), + { + ...r, + color_dark: color, + }, + ...configParsed.slice(i + 1), + ]), + ) + } + /> + )} + + { + onChange( + JSON.stringify([ + ...configParsed.slice(0, i), + { + ...r, + exts: inputCache[i]?.split(",") ?? r.exts, + }, + ...configParsed.slice(i + 1), + ]), + ); + setInputCache({ + ...inputCache, + [i]: undefined, + }); + }} onChange={(e) => - onChange( - JSON.stringify([ - ...configParsed.slice(0, i), - { ...r, img: e.target.value as string }, - ...configParsed.slice(i + 1), - ]), - ) + setInputCache({ + ...inputCache, + [i]: e.target.value, + }) } /> - ) : ( - t("settings.builtinIcon") - )} - - - {!r.icon ? ( - "-" - ) : ( - - onChange( - JSON.stringify([ - ...configParsed.slice(0, i), - { - ...r, - color: color, - }, - ...configParsed.slice(i + 1), - ]), - ) - } - /> - )} - - - {!r.icon ? ( - "-" - ) : ( - - onChange( - JSON.stringify([ - ...configParsed.slice(0, i), - { - ...r, - color_dark: color, - }, - ...configParsed.slice(i + 1), - ]), - ) - } - /> - )} - - - { - onChange( - JSON.stringify([ - ...configParsed.slice(0, i), - { - ...r, - exts: inputCache[i]?.split(",") ?? r.exts, - }, - ...configParsed.slice(i + 1), - ]), - ); - setInputCache({ - ...inputCache, - [i]: undefined, - }); - }} - onChange={(e) => - setInputCache({ - ...inputCache, - [i]: e.target.value, - }) - } - /> - - - {!r.icon && ( - onChange(JSON.stringify(configParsed.filter((_, index) => index !== i)))} - size={"small"} - > - - - )} - - - ))} + + + {!r.icon && ( + onChange(JSON.stringify(configParsed.filter((_, index) => index !== i)))} + size={"small"} + > + + + )} + + + ); + })} diff --git a/src/component/Common/SizeInput.tsx b/src/component/Common/SizeInput.tsx index 1d1f7a9..3ac98bf 100644 --- a/src/component/Common/SizeInput.tsx +++ b/src/component/Common/SizeInput.tsx @@ -42,7 +42,7 @@ export interface SizeInputProps { allowZero?: boolean; } -const StyledSelect = styled(Select)(() => ({ +export const StyledSelect = styled(Select)(() => ({ "& .MuiFilledInput-input": { paddingTop: "5px", "&:focus": { diff --git a/src/component/FileManager/Explorer/FileTypeIcon.tsx b/src/component/FileManager/Explorer/FileTypeIcon.tsx index 1778de4..9d0c5fa 100644 --- a/src/component/FileManager/Explorer/FileTypeIcon.tsx +++ b/src/component/FileManager/Explorer/FileTypeIcon.tsx @@ -1,3 +1,4 @@ +import { Icon as IconifyIcon } from "@iconify/react/dist/iconify.js"; import { Android } from "@mui/icons-material"; import { Box, SvgIconProps, useTheme } from "@mui/material"; import SvgIcon from "@mui/material/SvgIcon/SvgIcon"; @@ -41,6 +42,7 @@ export interface FileTypeIconProps extends SvgIconProps { export interface FileTypeIconSetting { exts: string[]; icon?: string; + iconify?: string; img?: string; color?: string; color_dark?: string; @@ -87,6 +89,15 @@ interface TypeIcon { reverseDarkMode?: boolean; } +interface IconComponentProps { + icon?: typeof SvgIcon | ((props: SvgIconProps) => JSX.Element); + color?: string; + color_dark?: string; + isDefault?: boolean; + img?: string; + iconify?: string; +} + const FileTypeIcon = ({ name, fileType, @@ -99,7 +110,7 @@ const FileTypeIcon = ({ }: FileTypeIconProps) => { const theme = useTheme(); const iconOptions = useAppSelector((state) => state.siteConfig.explorer.typed?.icons) as ExpandedIconSettings; - const IconComponent = useMemo(() => { + const IconComponent: IconComponentProps = useMemo(() => { if (fileType === 1) { return notLoaded ? { icon: FolderOutlined } : { icon: Folder }; } @@ -109,7 +120,7 @@ const FileTypeIcon = ({ if (fileSuffix && iconOptions) { const options = iconOptions[fileSuffix]; if (options) { - const { icon, color, color_dark, img } = options; + const { icon, color, color_dark, img, iconify } = options; if (icon) { return { icon: builtInIcons[icon], @@ -120,6 +131,12 @@ const FileTypeIcon = ({ return { img, }; + } else if (iconify) { + return { + iconify, + color, + color_dark, + }; } } } @@ -152,6 +169,21 @@ const FileTypeIcon = ({ {...rest} /> ); + } else if (IconComponent.iconify) { + return ( + //@ts-ignore + + ); } else { return ( //@ts-ignore