feat: Add file color coding in file manager
- Files: blue (text-blue-400) - Folders: red (text-red-400) - Symlinks: green (text-green-400) - Add toggle in User Profile settings - Store preference in localStorage - Support i18n (en, zh) - Can be disabled via settings - Matches dark theme color scheme
This commit is contained in:
@@ -1479,6 +1479,8 @@
|
|||||||
"local": "Local",
|
"local": "Local",
|
||||||
"external": "External (OIDC)",
|
"external": "External (OIDC)",
|
||||||
"selectPreferredLanguage": "Select your preferred language for the interface",
|
"selectPreferredLanguage": "Select your preferred language for the interface",
|
||||||
|
"fileColorCoding": "File Color Coding",
|
||||||
|
"fileColorCodingDesc": "Color-code files by type: folders (red), files (blue), symlinks (green)",
|
||||||
"currentPassword": "Current Password",
|
"currentPassword": "Current Password",
|
||||||
"passwordChangedSuccess": "Password changed successfully! Please log in again.",
|
"passwordChangedSuccess": "Password changed successfully! Please log in again.",
|
||||||
"failedToChangePassword": "Failed to change password. Please check your current password and try again."
|
"failedToChangePassword": "Failed to change password. Please check your current password and try again."
|
||||||
|
|||||||
@@ -1442,6 +1442,8 @@
|
|||||||
"local": "本地",
|
"local": "本地",
|
||||||
"external": "外部 (OIDC)",
|
"external": "外部 (OIDC)",
|
||||||
"selectPreferredLanguage": "选择您的界面首选语言",
|
"selectPreferredLanguage": "选择您的界面首选语言",
|
||||||
|
"fileColorCoding": "文件颜色编码",
|
||||||
|
"fileColorCodingDesc": "按类型对文件进行颜色编码:文件夹(红色)、文件(蓝色)、符号链接(绿色)",
|
||||||
"currentPassword": "当前密码",
|
"currentPassword": "当前密码",
|
||||||
"passwordChangedSuccess": "密码修改成功!请重新登录。",
|
"passwordChangedSuccess": "密码修改成功!请重新登录。",
|
||||||
"failedToChangePassword": "修改密码失败。请检查您当前的密码并重试。"
|
"failedToChangePassword": "修改密码失败。请检查您当前的密码并重试。"
|
||||||
|
|||||||
@@ -96,15 +96,33 @@ interface FileManagerGridProps {
|
|||||||
onNewFolder?: () => void;
|
onNewFolder?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFileIcon = (file: FileItem, viewMode: "grid" | "list" = "grid") => {
|
const getFileTypeColor = (file: FileItem): string => {
|
||||||
const iconClass = viewMode === "grid" ? "w-8 h-8" : "w-6 h-6";
|
const colorEnabled = localStorage.getItem("fileColorCoding") !== "false";
|
||||||
|
if (!colorEnabled) {
|
||||||
|
return "text-muted-foreground";
|
||||||
|
}
|
||||||
|
|
||||||
if (file.type === "directory") {
|
if (file.type === "directory") {
|
||||||
return <Folder className={`${iconClass} text-muted-foreground`} />;
|
return "text-red-400";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.type === "link") {
|
if (file.type === "link") {
|
||||||
return <FileSymlink className={`${iconClass} text-muted-foreground`} />;
|
return "text-green-400";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "text-blue-400";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFileIcon = (file: FileItem, viewMode: "grid" | "list" = "grid") => {
|
||||||
|
const iconClass = viewMode === "grid" ? "w-8 h-8" : "w-6 h-6";
|
||||||
|
const colorClass = getFileTypeColor(file);
|
||||||
|
|
||||||
|
if (file.type === "directory") {
|
||||||
|
return <Folder className={`${iconClass} ${colorClass}`} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.type === "link") {
|
||||||
|
return <FileSymlink className={`${iconClass} ${colorClass}`} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ext = file.name.split(".").pop()?.toLowerCase();
|
const ext = file.name.split(".").pop()?.toLowerCase();
|
||||||
@@ -113,30 +131,30 @@ const getFileIcon = (file: FileItem, viewMode: "grid" | "list" = "grid") => {
|
|||||||
case "txt":
|
case "txt":
|
||||||
case "md":
|
case "md":
|
||||||
case "readme":
|
case "readme":
|
||||||
return <FileText className={`${iconClass} text-muted-foreground`} />;
|
return <FileText className={`${iconClass} ${colorClass}`} />;
|
||||||
case "png":
|
case "png":
|
||||||
case "jpg":
|
case "jpg":
|
||||||
case "jpeg":
|
case "jpeg":
|
||||||
case "gif":
|
case "gif":
|
||||||
case "bmp":
|
case "bmp":
|
||||||
case "svg":
|
case "svg":
|
||||||
return <FileImage className={`${iconClass} text-muted-foreground`} />;
|
return <FileImage className={`${iconClass} ${colorClass}`} />;
|
||||||
case "mp4":
|
case "mp4":
|
||||||
case "avi":
|
case "avi":
|
||||||
case "mkv":
|
case "mkv":
|
||||||
case "mov":
|
case "mov":
|
||||||
return <FileVideo className={`${iconClass} text-muted-foreground`} />;
|
return <FileVideo className={`${iconClass} ${colorClass}`} />;
|
||||||
case "mp3":
|
case "mp3":
|
||||||
case "wav":
|
case "wav":
|
||||||
case "flac":
|
case "flac":
|
||||||
case "ogg":
|
case "ogg":
|
||||||
return <FileAudio className={`${iconClass} text-muted-foreground`} />;
|
return <FileAudio className={`${iconClass} ${colorClass}`} />;
|
||||||
case "zip":
|
case "zip":
|
||||||
case "tar":
|
case "tar":
|
||||||
case "gz":
|
case "gz":
|
||||||
case "rar":
|
case "rar":
|
||||||
case "7z":
|
case "7z":
|
||||||
return <Archive className={`${iconClass} text-muted-foreground`} />;
|
return <Archive className={`${iconClass} ${colorClass}`} />;
|
||||||
case "js":
|
case "js":
|
||||||
case "ts":
|
case "ts":
|
||||||
case "jsx":
|
case "jsx":
|
||||||
@@ -150,7 +168,7 @@ const getFileIcon = (file: FileItem, viewMode: "grid" | "list" = "grid") => {
|
|||||||
case "rb":
|
case "rb":
|
||||||
case "go":
|
case "go":
|
||||||
case "rs":
|
case "rs":
|
||||||
return <Code className={`${iconClass} text-muted-foreground`} />;
|
return <Code className={`${iconClass} ${colorClass}`} />;
|
||||||
case "json":
|
case "json":
|
||||||
case "xml":
|
case "xml":
|
||||||
case "yaml":
|
case "yaml":
|
||||||
@@ -159,9 +177,9 @@ const getFileIcon = (file: FileItem, viewMode: "grid" | "list" = "grid") => {
|
|||||||
case "ini":
|
case "ini":
|
||||||
case "conf":
|
case "conf":
|
||||||
case "config":
|
case "config":
|
||||||
return <Settings className={`${iconClass} text-muted-foreground`} />;
|
return <Settings className={`${iconClass} ${colorClass}`} />;
|
||||||
default:
|
default:
|
||||||
return <File className={`${iconClass} text-muted-foreground`} />;
|
return <File className={`${iconClass} ${colorClass}`} />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
TabsTrigger,
|
TabsTrigger,
|
||||||
} from "@/components/ui/tabs.tsx";
|
} from "@/components/ui/tabs.tsx";
|
||||||
import { Separator } from "@/components/ui/separator.tsx";
|
import { Separator } from "@/components/ui/separator.tsx";
|
||||||
|
import { Switch } from "@/components/ui/switch.tsx";
|
||||||
import { User, Shield, AlertCircle } from "lucide-react";
|
import { User, Shield, AlertCircle } from "lucide-react";
|
||||||
import { TOTPSetup } from "@/ui/desktop/user/TOTPSetup.tsx";
|
import { TOTPSetup } from "@/ui/desktop/user/TOTPSetup.tsx";
|
||||||
import {
|
import {
|
||||||
@@ -87,6 +88,9 @@ export function UserProfile({ isTopbarOpen = true }: UserProfileProps) {
|
|||||||
const [deletePassword, setDeletePassword] = useState("");
|
const [deletePassword, setDeletePassword] = useState("");
|
||||||
const [deleteLoading, setDeleteLoading] = useState(false);
|
const [deleteLoading, setDeleteLoading] = useState(false);
|
||||||
const [deleteError, setDeleteError] = useState<string | null>(null);
|
const [deleteError, setDeleteError] = useState<string | null>(null);
|
||||||
|
const [fileColorCoding, setFileColorCoding] = useState<boolean>(
|
||||||
|
localStorage.getItem("fileColorCoding") !== "false"
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchUserInfo();
|
fetchUserInfo();
|
||||||
@@ -128,6 +132,13 @@ export function UserProfile({ isTopbarOpen = true }: UserProfileProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleFileColorCodingToggle = (enabled: boolean) => {
|
||||||
|
setFileColorCoding(enabled);
|
||||||
|
localStorage.setItem("fileColorCoding", enabled.toString());
|
||||||
|
// Trigger a re-render by dispatching a custom event
|
||||||
|
window.dispatchEvent(new Event("fileColorCodingChanged"));
|
||||||
|
};
|
||||||
|
|
||||||
const handleDeleteAccount = async (e: React.FormEvent) => {
|
const handleDeleteAccount = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setDeleteLoading(true);
|
setDeleteLoading(true);
|
||||||
@@ -325,6 +336,23 @@ export function UserProfile({ isTopbarOpen = true }: UserProfileProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 pt-6 border-t border-dark-border">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<Label className="text-gray-300">
|
||||||
|
{t("profile.fileColorCoding")}
|
||||||
|
</Label>
|
||||||
|
<p className="text-sm text-gray-400 mt-1">
|
||||||
|
{t("profile.fileColorCodingDesc")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={fileColorCoding}
|
||||||
|
onCheckedChange={handleFileColorCodingToggle}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 pt-6 border-t border-dark-border">
|
<div className="mt-6 pt-6 border-t border-dark-border">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
Reference in New Issue
Block a user