* Add comprehensive Chinese internationalization support - Implemented i18n framework with react-i18next for multi-language support - Added Chinese (zh) and English (en) translation files with comprehensive coverage - Localized Admin interface, authentication flows, and error messages - Translated FileManager operations and UI elements - Updated HomepageAuth component with localized authentication messages - Localized LeftSidebar navigation and host management - Added language switcher component (shown after login only) - Configured default language as English with Chinese as secondary option - Localized TOTPSetup two-factor authentication interface - Updated Docker build to include translation files - Achieved 95%+ UI localization coverage across core components Co-Authored-By: Claude <noreply@anthropic.com> * Extend Chinese localization coverage to Host Manager components - Added comprehensive translations for HostManagerHostViewer component - Localized all host management UI text including import/export features - Translated error messages and confirmation dialogs for host operations - Added translations for HostManagerHostEditor validation messages - Localized connection details, organization settings, and form labels - Fixed syntax error in FileManagerOperations component - Achieved near-complete localization of SSH host management interface - Updated placeholders and tooltips for better user guidance Co-Authored-By: Claude <noreply@anthropic.com> * Complete comprehensive Chinese localization for Termix - Added full localization support for Tunnel components (connected/disconnected states, retry messages) - Localized all tunnel status messages and connection errors - Added translations for port forwarding UI elements - Verified Server, TopNavbar, and Tab components already have complete i18n support - Achieved 99%+ localization coverage across entire application - All core UI components now fully support Chinese and English languages This completes the comprehensive internationalization effort for the Termix SSH management platform. Co-Authored-By: Claude <noreply@anthropic.com> * Localize additional Host Manager components and authentication settings - Added translations for all authentication options (Password, Key, SSH Private Key) - Localized form labels in HostManagerHostEditor (Pin Connection, Enable Terminal/Tunnel/FileManager) - Translated Upload/Update Key button states - Localized Host Viewer and Add/Edit Host tab labels - Added Chinese translations for all host management settings - Fixed duplicate translation keys in JSON files Co-Authored-By: Claude <noreply@anthropic.com> * Extend localization coverage to UI components and common strings - Added comprehensive common translations (online/offline, success/error, etc.) - Localized status indicator component with all status states - Updated FileManagerLeftSidebar toast messages for rename/delete operations - Added translations for UI elements (close, toggle sidebar, etc.) - Expanded placeholder translations for form inputs - Added Chinese translations for all new common strings - Improved consistency across component status messages Co-Authored-By: Claude <noreply@anthropic.com> * Complete Chinese localization for remaining UI components - Add comprehensive Chinese translations for Host Manager component - Translate all form labels, buttons, and descriptions - Add translations for SSH configuration warnings and instructions - Localize tunnel connection settings and port forwarding options - Localize SSH Tools panel - Translate key recording functionality - Add translations for settings and configuration options - Translate homepage welcome messages and navigation elements - Add Chinese translations for login success messages - Localize "Updates & Releases" section title - Translate sidebar "Host Manager" button - Fix translation key display issues - Remove duplicate translation keys in both language files - Ensure all components properly reference translation keys - Fix hosts.tunnelConnections key mapping This completes the full Chinese localization of the Termix application, achieving near 100% UI translation coverage while maintaining English as the default language. * Complete final Chinese localization for Host Manager tunnel configuration - Add Chinese translations for authentication UI elements - Translate "Authentication", "Password", and "Key" tab labels - Localize SSH private key and key password fields - Add translations for key type selector - Localize tunnel connection configuration descriptions - Translate retry attempts and retry interval descriptions - Add dynamic tunnel forwarding description with port parameters - Localize endpoint SSH configuration labels - Fix missing translation keys - Add "upload" translation for file upload button - Ensure all FormLabel and FormDescription elements use translation keys This completes the comprehensive Chinese localization of the entire Termix application, achieving 100% UI translation coverage. * Fix OIDC errors for "Failed to get user information" * Fix OIDC errors for "Failed to get user information" * Fix spelling error * Fix PR feedback: Improve Profile section translations and UX - Fixed password reset translations in Profile section - Moved language selector from TopNavbar to Profile page - Added profile.selectPreferredLanguage translation key - Improved user experience for language preferences * Migrate everything to alert system, update user.ts for OIDC updates. * Update env * Fix OIDC errors for "Failed to get user information" * Fix OIDC errors for "Failed to get user information" * Fix spelling error * Migrate everything to alert system, update user.ts for OIDC updates. * Translation update * Translation update * Translation update * Translate tunnels * Comment update * Update build workflow naming * Add more translations, fix user delete failing * Fix config editor erorrs causing user delete failure --------- Co-authored-by: ZacharyZcR <PayasoNorahC@protonmail.com> Co-authored-by: Claude <noreply@anthropic.com>
627 lines
28 KiB
TypeScript
627 lines
28 KiB
TypeScript
import React, {useState, useRef, useEffect} from 'react';
|
|
import {Button} from '@/components/ui/button.tsx';
|
|
import {Input} from '@/components/ui/input.tsx';
|
|
import {Card} from '@/components/ui/card.tsx';
|
|
import {Separator} from '@/components/ui/separator.tsx';
|
|
import {
|
|
Upload,
|
|
FilePlus,
|
|
FolderPlus,
|
|
Trash2,
|
|
Edit3,
|
|
X,
|
|
Check,
|
|
AlertCircle,
|
|
FileText,
|
|
Folder
|
|
} from 'lucide-react';
|
|
import {cn} from '@/lib/utils.ts';
|
|
import {useTranslation} from 'react-i18next';
|
|
|
|
interface FileManagerOperationsProps {
|
|
currentPath: string;
|
|
sshSessionId: string | null;
|
|
onOperationComplete: () => void;
|
|
onError: (error: string) => void;
|
|
onSuccess: (message: string) => void;
|
|
}
|
|
|
|
export function FileManagerOperations({
|
|
currentPath,
|
|
sshSessionId,
|
|
onOperationComplete,
|
|
onError,
|
|
onSuccess
|
|
}: FileManagerOperationsProps) {
|
|
const {t} = useTranslation();
|
|
const [showUpload, setShowUpload] = useState(false);
|
|
const [showCreateFile, setShowCreateFile] = useState(false);
|
|
const [showCreateFolder, setShowCreateFolder] = useState(false);
|
|
const [showDelete, setShowDelete] = useState(false);
|
|
const [showRename, setShowRename] = useState(false);
|
|
|
|
const [uploadFile, setUploadFile] = useState<File | null>(null);
|
|
const [newFileName, setNewFileName] = useState('');
|
|
const [newFolderName, setNewFolderName] = useState('');
|
|
const [deletePath, setDeletePath] = useState('');
|
|
const [deleteIsDirectory, setDeleteIsDirectory] = useState(false);
|
|
const [renamePath, setRenamePath] = useState('');
|
|
const [renameIsDirectory, setRenameIsDirectory] = useState(false);
|
|
const [newName, setNewName] = useState('');
|
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [showTextLabels, setShowTextLabels] = useState(true);
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
const checkContainerWidth = () => {
|
|
if (containerRef.current) {
|
|
const width = containerRef.current.offsetWidth;
|
|
setShowTextLabels(width > 240);
|
|
}
|
|
};
|
|
|
|
checkContainerWidth();
|
|
|
|
const resizeObserver = new ResizeObserver(checkContainerWidth);
|
|
if (containerRef.current) {
|
|
resizeObserver.observe(containerRef.current);
|
|
}
|
|
|
|
return () => {
|
|
resizeObserver.disconnect();
|
|
};
|
|
}, []);
|
|
|
|
const handleFileUpload = async () => {
|
|
if (!uploadFile || !sshSessionId) return;
|
|
|
|
setIsLoading(true);
|
|
try {
|
|
const content = await uploadFile.text();
|
|
const {uploadSSHFile} = await import('@/ui/main-axios.ts');
|
|
|
|
await uploadSSHFile(sshSessionId, currentPath, uploadFile.name, content);
|
|
onSuccess(t('fileManager.fileUploadedSuccessfully', { name: uploadFile.name }));
|
|
setShowUpload(false);
|
|
setUploadFile(null);
|
|
onOperationComplete();
|
|
} catch (error: any) {
|
|
onError(error?.response?.data?.error || t('fileManager.failedToUploadFile'));
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleCreateFile = async () => {
|
|
if (!newFileName.trim() || !sshSessionId) return;
|
|
|
|
setIsLoading(true);
|
|
try {
|
|
const {createSSHFile} = await import('@/ui/main-axios.ts');
|
|
|
|
await createSSHFile(sshSessionId, currentPath, newFileName.trim());
|
|
onSuccess(t('fileManager.fileCreatedSuccessfully', { name: newFileName.trim() }));
|
|
setShowCreateFile(false);
|
|
setNewFileName('');
|
|
onOperationComplete();
|
|
} catch (error: any) {
|
|
onError(error?.response?.data?.error || t('fileManager.failedToCreateFile'));
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleCreateFolder = async () => {
|
|
if (!newFolderName.trim() || !sshSessionId) return;
|
|
|
|
setIsLoading(true);
|
|
try {
|
|
const {createSSHFolder} = await import('@/ui/main-axios.ts');
|
|
|
|
await createSSHFolder(sshSessionId, currentPath, newFolderName.trim());
|
|
onSuccess(t('fileManager.folderCreatedSuccessfully', { name: newFolderName.trim() }));
|
|
setShowCreateFolder(false);
|
|
setNewFolderName('');
|
|
onOperationComplete();
|
|
} catch (error: any) {
|
|
onError(error?.response?.data?.error || t('fileManager.failedToCreateFolder'));
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleDelete = async () => {
|
|
if (!deletePath || !sshSessionId) return;
|
|
|
|
setIsLoading(true);
|
|
try {
|
|
const {deleteSSHItem} = await import('@/ui/main-axios.ts');
|
|
|
|
await deleteSSHItem(sshSessionId, deletePath, deleteIsDirectory);
|
|
onSuccess(t('fileManager.itemDeletedSuccessfully', { type: deleteIsDirectory ? t('fileManager.folder') : t('fileManager.file') }));
|
|
setShowDelete(false);
|
|
setDeletePath('');
|
|
setDeleteIsDirectory(false);
|
|
onOperationComplete();
|
|
} catch (error: any) {
|
|
onError(error?.response?.data?.error || t('fileManager.failedToDeleteItem'));
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleRename = async () => {
|
|
if (!renamePath || !newName.trim() || !sshSessionId) return;
|
|
|
|
setIsLoading(true);
|
|
try {
|
|
const {renameSSHItem} = await import('@/ui/main-axios.ts');
|
|
|
|
await renameSSHItem(sshSessionId, renamePath, newName.trim());
|
|
onSuccess(t('fileManager.itemRenamedSuccessfully', { type: renameIsDirectory ? t('fileManager.folder') : t('fileManager.file') }));
|
|
setShowRename(false);
|
|
setRenamePath('');
|
|
setRenameIsDirectory(false);
|
|
setNewName('');
|
|
onOperationComplete();
|
|
} catch (error: any) {
|
|
onError(error?.response?.data?.error || t('fileManager.failedToRenameItem'));
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const openFileDialog = () => {
|
|
fileInputRef.current?.click();
|
|
};
|
|
|
|
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = event.target.files?.[0];
|
|
if (file) {
|
|
setUploadFile(file);
|
|
}
|
|
};
|
|
|
|
const resetStates = () => {
|
|
setShowUpload(false);
|
|
setShowCreateFile(false);
|
|
setShowCreateFolder(false);
|
|
setShowDelete(false);
|
|
setShowRename(false);
|
|
setUploadFile(null);
|
|
setNewFileName('');
|
|
setNewFolderName('');
|
|
setDeletePath('');
|
|
setDeleteIsDirectory(false);
|
|
setRenamePath('');
|
|
setRenameIsDirectory(false);
|
|
setNewName('');
|
|
};
|
|
|
|
if (!sshSessionId) {
|
|
return (
|
|
<div className="p-4 text-center">
|
|
<AlertCircle className="w-8 h-8 text-muted-foreground mx-auto mb-2"/>
|
|
<p className="text-sm text-muted-foreground">{t('fileManager.connectToSsh')}</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div ref={containerRef} className="p-4 space-y-4">
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setShowUpload(true)}
|
|
className="h-10 bg-[#18181b] border-2 border-[#303032] hover:border-[#434345] hover:bg-[#2d2d30]"
|
|
title={t('fileManager.uploadFile')}
|
|
>
|
|
<Upload className={cn("w-4 h-4", showTextLabels ? "mr-2" : "")}/>
|
|
{showTextLabels && <span className="truncate">{t('fileManager.uploadFile')}</span>}
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setShowCreateFile(true)}
|
|
className="h-10 bg-[#18181b] border-2 border-[#303032] hover:border-[#434345] hover:bg-[#2d2d30]"
|
|
title={t('fileManager.newFile')}
|
|
>
|
|
<FilePlus className={cn("w-4 h-4", showTextLabels ? "mr-2" : "")}/>
|
|
{showTextLabels && <span className="truncate">{t('fileManager.newFile')}</span>}
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setShowCreateFolder(true)}
|
|
className="h-10 bg-[#18181b] border-2 border-[#303032] hover:border-[#434345] hover:bg-[#2d2d30]"
|
|
title={t('fileManager.newFolder')}
|
|
>
|
|
<FolderPlus className={cn("w-4 h-4", showTextLabels ? "mr-2" : "")}/>
|
|
{showTextLabels && <span className="truncate">{t('fileManager.newFolder')}</span>}
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setShowRename(true)}
|
|
className="h-10 bg-[#18181b] border-2 border-[#303032] hover:border-[#434345] hover:bg-[#2d2d30]"
|
|
title={t('fileManager.rename')}
|
|
>
|
|
<Edit3 className={cn("w-4 h-4", showTextLabels ? "mr-2" : "")}/>
|
|
{showTextLabels && <span className="truncate">{t('fileManager.rename')}</span>}
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setShowDelete(true)}
|
|
className="h-10 bg-[#18181b] border-2 border-[#303032] hover:border-[#434345] hover:bg-[#2d2d30] col-span-2"
|
|
title={t('fileManager.deleteItem')}
|
|
>
|
|
<Trash2 className={cn("w-4 h-4", showTextLabels ? "mr-2" : "")}/>
|
|
{showTextLabels && <span className="truncate">{t('fileManager.deleteItem')}</span>}
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="bg-[#141416] border-2 border-[#373739] rounded-md p-3">
|
|
<div className="flex items-start gap-2 text-sm">
|
|
<Folder className="w-4 h-4 text-blue-400 flex-shrink-0 mt-0.5"/>
|
|
<div className="flex-1 min-w-0">
|
|
<span className="text-muted-foreground block mb-1">{t('fileManager.currentPath')}:</span>
|
|
<span className="text-white font-mono text-xs break-all leading-relaxed">{currentPath}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Separator className="p-0.25 bg-[#303032]"/>
|
|
|
|
{showUpload && (
|
|
<Card className="bg-[#18181b] border-2 border-[#303032] p-3 sm:p-4">
|
|
<div className="flex items-start justify-between mb-3">
|
|
<div className="flex-1 min-w-0">
|
|
<h3 className="text-base sm:text-lg font-semibold text-white flex items-center gap-2 mb-1">
|
|
<Upload className="w-5 h-5 sm:w-6 sm:h-6 flex-shrink-0"/>
|
|
<span className="break-words">{t('fileManager.uploadFileTitle')}</span>
|
|
</h3>
|
|
<p className="text-xs text-muted-foreground break-words">
|
|
{t('fileManager.maxFileSize')}
|
|
</p>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setShowUpload(false)}
|
|
className="h-8 w-8 p-0 flex-shrink-0 ml-2"
|
|
>
|
|
<X className="w-4 h-4"/>
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<div className="border-2 border-dashed border-[#434345] rounded-lg p-4 text-center">
|
|
{uploadFile ? (
|
|
<div className="space-y-3">
|
|
<FileText className="w-12 h-12 text-blue-400 mx-auto"/>
|
|
<p className="text-white font-medium text-sm break-words px-2">{uploadFile.name}</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
{(uploadFile.size / 1024).toFixed(2)} KB
|
|
</p>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setUploadFile(null)}
|
|
className="w-full text-sm h-8"
|
|
>
|
|
{t('fileManager.removeFile')}
|
|
</Button>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
<Upload className="w-12 h-12 text-muted-foreground mx-auto"/>
|
|
<p className="text-white text-sm break-words px-2">{t('fileManager.clickToSelectFile')}</p>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={openFileDialog}
|
|
className="w-full text-sm h-8"
|
|
>
|
|
{t('fileManager.chooseFile')}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<input
|
|
ref={fileInputRef}
|
|
type="file"
|
|
onChange={handleFileSelect}
|
|
className="hidden"
|
|
accept="*/*"
|
|
/>
|
|
|
|
<div className="flex flex-col gap-2">
|
|
<Button
|
|
onClick={handleFileUpload}
|
|
disabled={!uploadFile || isLoading}
|
|
className="w-full text-sm h-9"
|
|
>
|
|
{isLoading ? t('fileManager.uploading') : t('fileManager.uploadFile')}
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setShowUpload(false)}
|
|
disabled={isLoading}
|
|
className="w-full text-sm h-9"
|
|
>
|
|
{t('common.cancel')}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
)}
|
|
|
|
{showCreateFile && (
|
|
<Card className="bg-[#18181b] border-2 border-[#303032] p-3 sm:p-4">
|
|
<div className="flex items-start justify-between mb-3">
|
|
<div className="flex-1 min-w-0">
|
|
<h3 className="text-base sm:text-lg font-semibold text-white flex items-center gap-2">
|
|
<FilePlus className="w-5 h-5 sm:w-6 sm:h-6 flex-shrink-0"/>
|
|
<span className="break-words">{t('fileManager.createNewFile')}</span>
|
|
</h3>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setShowCreateFile(false)}
|
|
className="h-8 w-8 p-0 flex-shrink-0 ml-2"
|
|
>
|
|
<X className="w-4 h-4"/>
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<div>
|
|
<label className="text-sm font-medium text-white mb-2 block">
|
|
{t('fileManager.fileName')}
|
|
</label>
|
|
<Input
|
|
value={newFileName}
|
|
onChange={(e) => setNewFileName(e.target.value)}
|
|
placeholder={t('placeholders.fileName')}
|
|
className="bg-[#23232a] border-2 border-[#434345] text-white text-sm"
|
|
onKeyDown={(e) => e.key === 'Enter' && handleCreateFile()}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-2">
|
|
<Button
|
|
onClick={handleCreateFile}
|
|
disabled={!newFileName.trim() || isLoading}
|
|
className="w-full text-sm h-9"
|
|
>
|
|
{isLoading ? t('fileManager.creating') : t('fileManager.createFile')}
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setShowCreateFile(false)}
|
|
disabled={isLoading}
|
|
className="w-full text-sm h-9"
|
|
>
|
|
{t('common.cancel')}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
)}
|
|
|
|
{showCreateFolder && (
|
|
<Card className="bg-[#18181b] border-2 border-[#303032] p-3">
|
|
<div className="flex items-start justify-between mb-3">
|
|
<div className="flex-1 min-w-0">
|
|
<h3 className="text-base font-semibold text-white flex items-center gap-2">
|
|
<FolderPlus className="w-6 h-6 flex-shrink-0"/>
|
|
<span className="break-words">{t('fileManager.createNewFolder')}</span>
|
|
</h3>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setShowCreateFolder(false)}
|
|
className="h-8 w-8 p-0 flex-shrink-0 ml-2"
|
|
>
|
|
<X className="w-4 h-4"/>
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<div>
|
|
<label className="text-sm font-medium text-white mb-2 block">
|
|
{t('fileManager.folderName')}
|
|
</label>
|
|
<Input
|
|
value={newFolderName}
|
|
onChange={(e) => setNewFolderName(e.target.value)}
|
|
placeholder={t('placeholders.folderName')}
|
|
className="bg-[#23232a] border-2 border-[#434345] text-white text-sm"
|
|
onKeyDown={(e) => e.key === 'Enter' && handleCreateFolder()}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-2">
|
|
<Button
|
|
onClick={handleCreateFolder}
|
|
disabled={!newFolderName.trim() || isLoading}
|
|
className="w-full text-sm h-9"
|
|
>
|
|
{isLoading ? t('fileManager.creating') : t('fileManager.createFolder')}
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setShowCreateFolder(false)}
|
|
disabled={isLoading}
|
|
className="w-full text-sm h-9"
|
|
>
|
|
{t('common.cancel')}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
)}
|
|
|
|
{showDelete && (
|
|
<Card className="bg-[#18181b] border-2 border-[#303032] p-3">
|
|
<div className="flex items-start justify-between mb-3">
|
|
<div className="flex-1 min-w-0">
|
|
<h3 className="text-base font-semibold text-white flex items-center gap-2">
|
|
<Trash2 className="w-6 h-6 text-red-400 flex-shrink-0"/>
|
|
<span className="break-words">{t('fileManager.deleteItem')}</span>
|
|
</h3>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setShowDelete(false)}
|
|
className="h-8 w-8 p-0 flex-shrink-0 ml-2"
|
|
>
|
|
<X className="w-4 h-4"/>
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<div className="bg-red-900/20 border border-red-500/30 rounded-lg p-3">
|
|
<div className="flex items-start gap-2 text-red-300">
|
|
<AlertCircle className="w-5 h-5 flex-shrink-0"/>
|
|
<span className="text-sm font-medium break-words">{t('fileManager.warningCannotUndo')}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="text-sm font-medium text-white mb-2 block">
|
|
{t('fileManager.itemPath')}
|
|
</label>
|
|
<Input
|
|
value={deletePath}
|
|
onChange={(e) => setDeletePath(e.target.value)}
|
|
placeholder={t('placeholders.fullPath')}
|
|
className="bg-[#23232a] border-2 border-[#434345] text-white text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-start gap-2">
|
|
<input
|
|
type="checkbox"
|
|
id="deleteIsDirectory"
|
|
checked={deleteIsDirectory}
|
|
onChange={(e) => setDeleteIsDirectory(e.target.checked)}
|
|
className="rounded border-[#434345] bg-[#23232a] mt-0.5 flex-shrink-0"
|
|
/>
|
|
<label htmlFor="deleteIsDirectory" className="text-sm text-white break-words">
|
|
{t('fileManager.thisIsDirectory')}
|
|
</label>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-2">
|
|
<Button
|
|
onClick={handleDelete}
|
|
disabled={!deletePath || isLoading}
|
|
variant="destructive"
|
|
className="w-full text-sm h-9"
|
|
>
|
|
{isLoading ? t('fileManager.deleting') : t('fileManager.deleteItem')}
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setShowDelete(false)}
|
|
disabled={isLoading}
|
|
className="w-full text-sm h-9"
|
|
>
|
|
{t('common.cancel')}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
)}
|
|
|
|
{showRename && (
|
|
<Card className="bg-[#18181b] border-2 border-[#303032] p-3">
|
|
<div className="flex items-start justify-between mb-3">
|
|
<div className="flex-1 min-w-0">
|
|
<h3 className="text-base font-semibold text-white flex items-center gap-2">
|
|
<Edit3 className="w-6 h-6 flex-shrink-0"/>
|
|
<span className="break-words">{t('fileManager.renameItem')}</span>
|
|
</h3>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setShowRename(false)}
|
|
className="h-8 w-8 p-0 flex-shrink-0 ml-2"
|
|
>
|
|
<X className="w-4 h-4"/>
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<div>
|
|
<label className="text-sm font-medium text-white mb-2 block">
|
|
{t('fileManager.currentPathLabel')}
|
|
</label>
|
|
<Input
|
|
value={renamePath}
|
|
onChange={(e) => setRenamePath(e.target.value)}
|
|
placeholder={t('placeholders.currentPath')}
|
|
className="bg-[#23232a] border-2 border-[#434345] text-white text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="text-sm font-medium text-white mb-2 block">
|
|
{t('fileManager.newName')}
|
|
</label>
|
|
<Input
|
|
value={newName}
|
|
onChange={(e) => setNewName(e.target.value)}
|
|
placeholder={t('placeholders.newName')}
|
|
className="bg-[#23232a] border-2 border-[#434345] text-white text-sm"
|
|
onKeyDown={(e) => e.key === 'Enter' && handleRename()}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-start gap-2">
|
|
<input
|
|
type="checkbox"
|
|
id="renameIsDirectory"
|
|
checked={renameIsDirectory}
|
|
onChange={(e) => setRenameIsDirectory(e.target.checked)}
|
|
className="rounded border-[#434345] bg-[#23232a] mt-0.5 flex-shrink-0"
|
|
/>
|
|
<label htmlFor="renameIsDirectory" className="text-sm text-white break-words">
|
|
{t('fileManager.thisIsDirectoryRename')}
|
|
</label>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-2">
|
|
<Button
|
|
onClick={handleRename}
|
|
disabled={!renamePath || !newName.trim() || isLoading}
|
|
className="w-full text-sm h-9"
|
|
>
|
|
{isLoading ? t('fileManager.renaming') : t('fileManager.renameItem')}
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setShowRename(false)}
|
|
disabled={isLoading}
|
|
className="w-full text-sm h-9"
|
|
>
|
|
{t('common.cancel')}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|