- Selection model: click=select, dblclick=open, Ctrl+click toggle,
Shift+click range select, Ctrl+A, Esc to clear
- Keyboard shortcuts: Enter, Ctrl+Enter, F2, Backspace (navigate up),
Delete/Backspace (delete with confirm)
- Rename modal replaces prompt() with validation via backend ValidateName
- Context menu: Open, Open External, Show in Folder, Rename, Duplicate,
Copy, Cut, Delete — all with SVG icons and Russian labels
- Backend security: vaultPath/absPathSafe helpers prevent path traversal,
validateName rejects .. / \ null bytes empty overlong names
- MoveNode auto-renames on name conflict (copy style)
- Duplicate uses (copy) (copy 2) suffix pattern
- Russian localization: all file type labels, preview messages, tooltips
- FilePreviewModal: fixed broken {/if} tag
125 lines
5.5 KiB
JavaScript
125 lines
5.5 KiB
JavaScript
export function formatFileSize(bytes) {
|
|
if (bytes == null || bytes < 0) return '—'
|
|
if (bytes === 0) return '0 B'
|
|
const units = ['B', 'KB', 'MB', 'GB']
|
|
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1)
|
|
const val = bytes / Math.pow(1024, i)
|
|
return (i === 0 ? val.toFixed(0) : val.toFixed(1)) + ' ' + units[i]
|
|
}
|
|
|
|
const mimeLabels = {
|
|
'image/jpeg': 'Изображение JPEG',
|
|
'image/png': 'Изображение PNG',
|
|
'image/gif': 'Изображение GIF',
|
|
'image/webp': 'Изображение WebP',
|
|
'image/svg+xml': 'Изображение SVG',
|
|
'image/bmp': 'Изображение BMP',
|
|
'image/tiff': 'Изображение TIFF',
|
|
'image/avif': 'Изображение AVIF',
|
|
'application/pdf': 'PDF документ',
|
|
'application/msword': 'Документ Word',
|
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'Документ Word',
|
|
'application/vnd.ms-excel': 'Таблица Excel',
|
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'Таблица Excel',
|
|
'application/vnd.ms-powerpoint': 'Презентация PowerPoint',
|
|
'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'Презентация PowerPoint',
|
|
'application/zip': 'ZIP архив',
|
|
'application/gzip': 'GZIP архив',
|
|
'application/x-tar': 'TAR архив',
|
|
'application/x-7z-compressed': '7z архив',
|
|
'application/x-rar-compressed': 'RAR архив',
|
|
'text/plain': 'Текстовый файл',
|
|
'text/html': 'HTML файл',
|
|
'text/css': 'CSS файл',
|
|
'text/javascript': 'JavaScript файл',
|
|
'application/json': 'JSON файл',
|
|
'application/xml': 'XML файл',
|
|
'application/x-yaml': 'YAML файл',
|
|
'application/octet-stream': 'Бинарный файл',
|
|
'application/x-msdos-program': 'Исполняемый файл',
|
|
'inode/directory': 'Папка',
|
|
}
|
|
|
|
export function formatMimeType(mime) {
|
|
if (!mime) return 'Неизвестно'
|
|
return mimeLabels[mime] || mime
|
|
}
|
|
|
|
export function formatFileType(item) {
|
|
if (item.type === 'folder') return 'Папка'
|
|
const mime = (item.mime || '').toLowerCase()
|
|
if (mimeLabels[mime]) return mimeLabels[mime]
|
|
const name = (item.name || '').toLowerCase()
|
|
const ext = name.split('.').pop()
|
|
if (ext) return ext.toUpperCase()
|
|
return 'Файл'
|
|
}
|
|
|
|
export function getFileKind(item) {
|
|
if (item.type === 'folder') return 'folder'
|
|
const mime = (item.mime || '').toLowerCase()
|
|
if (mime.startsWith('image/')) return 'image'
|
|
if (mime.startsWith('video/')) return 'video'
|
|
if (mime.startsWith('audio/')) return 'audio'
|
|
if (mime.startsWith('text/')) return 'text'
|
|
if (mime.includes('pdf')) return 'pdf'
|
|
if (mime.includes('word') || mime.includes('document')) return 'document'
|
|
if (mime.includes('spreadsheet') || mime.includes('excel')) return 'spreadsheet'
|
|
if (mime.includes('presentation') || mime.includes('powerpoint')) return 'presentation'
|
|
if (mime.includes('zip') || mime.includes('tar') || mime.includes('gzip') || mime.includes('rar') || mime.includes('7z') || mime.includes('compress')) return 'archive'
|
|
if (mime.includes('json') || mime.includes('xml') || mime.includes('yaml') || mime.includes('javascript') || mime.includes('css') || mime.includes('html')) return 'code'
|
|
const name = (item.name || '').toLowerCase()
|
|
const ext = name.split('.').pop()
|
|
const codeExts = ['js','ts','jsx','tsx','vue','svelte','py','rs','go','c','cpp','h','hpp','java','kt','swift','rb','php','pl','sh','bash','zsh','fish','yml','yaml','json','xml','toml','ini','cfg','conf','md','markdown','css','scss','less','sass','sql','graphql','proto','gradle','cmake','makefile','dockerfile','env','gitignore']
|
|
if (codeExts.includes(ext)) return 'code'
|
|
return 'generic'
|
|
}
|
|
|
|
const imageMimes = ['image/jpeg','image/png','image/gif','image/webp','image/bmp','image/tiff','image/avif','image/svg+xml']
|
|
const textMimes = ['text/plain','text/html','text/css','text/javascript','application/json','application/xml','application/x-yaml','text/x-shellscript']
|
|
const codeNames = ['txt','log','conf','ini','yaml','yml','json','xml','csv','sh','py','js','ts','css','html','md','markdown','cfg']
|
|
|
|
const imageExts = ['jpg','jpeg','png','gif','webp','bmp','tiff','tif','avif','svg']
|
|
|
|
export function canPreviewFile(item) {
|
|
if (item.type === 'folder') return false
|
|
const mime = (item.mime || '').toLowerCase()
|
|
const name = (item.name || '').toLowerCase()
|
|
const ext = name.split('.').pop()
|
|
if (imageMimes.includes(mime) || imageExts.includes(ext)) return true
|
|
if (mime.includes('pdf')) return true
|
|
if (textMimes.includes(mime) || codeNames.includes(ext)) return true
|
|
return false
|
|
}
|
|
|
|
export function isImageFile(item) {
|
|
const mime = (item.mime || '').toLowerCase()
|
|
const name = (item.name || '').toLowerCase()
|
|
const ext = name.split('.').pop()
|
|
return imageMimes.includes(mime) || imageExts.includes(ext)
|
|
}
|
|
|
|
export function isTextFile(item) {
|
|
const mime = (item.mime || '').toLowerCase()
|
|
const name = (item.name || '').toLowerCase()
|
|
const ext = name.split('.').pop()
|
|
return textMimes.includes(mime) || (codeNames.includes(ext) && ext !== 'md' && ext !== 'markdown')
|
|
}
|
|
|
|
export function isPdfFile(item) {
|
|
return (item.mime || '').toLowerCase().includes('pdf')
|
|
}
|
|
|
|
export function isMarkdownFile(item) {
|
|
const name = (item.name || '').toLowerCase()
|
|
return name.endsWith('.md') || name.endsWith('.markdown')
|
|
}
|
|
|
|
export function needsBase64Preview(item) {
|
|
return isImageFile(item) || isPdfFile(item)
|
|
}
|
|
|
|
export function needsTextPreview(item) {
|
|
return isTextFile(item) || isMarkdownFile(item)
|
|
}
|