|
1 | 1 | 'use strict'; |
2 | 2 | 'require view'; |
| 3 | +'require fs'; |
3 | 4 | 'require ui'; |
4 | | -'require form'; |
| 5 | +'require dom'; |
5 | 6 |
|
6 | | -var formData = { |
7 | | - files: { |
8 | | - root: null, |
9 | | - } |
10 | | -}; |
| 7 | +var currentPath = '/'; // Изначально корневой каталог |
| 8 | +var sortField = 'name'; // Поле для сортировки по умолчанию |
| 9 | +var sortDirection = 'asc'; // Направление сортировки по умолчанию (asc - по возрастанию) |
11 | 10 |
|
12 | 11 | return view.extend({ |
13 | | - render: function() { |
14 | | - var m, s, o; |
| 12 | + load: function() { |
| 13 | + return fs.list(currentPath); // Загрузить список файлов в текущем каталоге |
| 14 | + }, |
15 | 15 |
|
16 | | - m = new form.JSONMap(formData, _('File Browser'), ''); |
| 16 | + render: function(data) { |
| 17 | + var files = data; |
17 | 18 |
|
18 | | - s = m.section(form.NamedSection, 'files', 'files'); |
| 19 | + // Создаем контейнер для вкладок |
| 20 | + var viewContainer = E('div', {}, [ |
| 21 | + E('h2', {}, _('File Browser: ') + currentPath), |
| 22 | + E('style', {}, |
| 23 | + /* Скрываем встроенные кнопки Apply, Reset и ненужную кнопку Save */ |
| 24 | + '.cbi-button-apply, .cbi-button-reset, .cbi-button-save:not(.custom-save-button) { display: none !important; }' + |
| 25 | + /* Убираем фон и границы под кнопками, но оставляем сами кнопки, добавляем отступы */ |
| 26 | + '.cbi-page-actions { background: none !important; border: none !important; padding: 10px 0 !important; margin: 0 !important; }' + |
| 27 | + /* Убираем контуры и фон элемента cbi-tabmenu */ |
| 28 | + '.cbi-tabmenu { background: none !important; border: none !important; height: 0 !important; margin: 0 !important; padding: 0 !important; }' + |
| 29 | + /* Добавляем отступ для области прокрутки */ |
| 30 | + '#file-list-container { margin-top: 30px !important; max-height: 400px; overflow-y: auto; }' + |
| 31 | + /* Добавляем отступ для редактора */ |
| 32 | + '#content-editor { margin-top: 30px !important; }' + |
| 33 | + /* Делаем размер редактора по вертикали динамическим */ |
| 34 | + '#editor-container textarea { height: calc(100vh - 300px) !important; max-height: 500px !important; width: 100% !important; }' + |
| 35 | + /* Выровнять заголовки колонок по левому краю */ |
| 36 | + 'th { text-align: left !important; }' + |
| 37 | + /* Выровнять содержимое ячеек по левому краю, если это необходимо */ |
| 38 | + 'td { text-align: left !important; }' + |
| 39 | + /* Подсвечиваем всю строку при наведении курсора */ |
| 40 | + 'tr:hover { background-color: #f0f0f0 !important; }' + |
| 41 | + /* Закрепляем заголовки колонок при прокрутке */ |
| 42 | + 'thead th { position: sticky; top: 0; background-color: #fff; z-index: 1; }' + |
| 43 | + /* Стили для кнопок действий */ |
| 44 | + '.download-button { color: green; cursor: pointer; margin-left: 5px; }' + |
| 45 | + '.delete-button { color: red; cursor: pointer; margin-left: 5px; }' + |
| 46 | + /* Стиль для символических ссылок */ |
| 47 | + '.symlink { color: green; }' + |
| 48 | + /* Область прокрутки для списка файлов */ |
| 49 | + '#file-list-container { max-height: 400px; overflow-y: auto; }' + |
| 50 | + 'th { cursor: pointer; }' + |
| 51 | + /* Кнопки Upload */ |
| 52 | + '.action-button { margin-right: 10px; cursor: pointer; }' |
| 53 | + ), |
| 54 | + E('div', { |
| 55 | + 'class': 'cbi-tabcontainer', |
| 56 | + 'id': 'tab-group' |
| 57 | + }, [ |
| 58 | + E('ul', { 'class': 'cbi-tabmenu' }, [ |
| 59 | + E('li', { 'class': 'cbi-tab cbi-tab-active', 'id': 'tab-filebrowser' }, [ |
| 60 | + E('a', { 'href': '#', 'click': this.switchToTab.bind(this, 'filebrowser') }, _('File Browser')) |
| 61 | + ]), |
| 62 | + E('li', { 'class': 'cbi-tab', 'id': 'tab-editor' }, [ |
| 63 | + E('a', { 'href': '#', 'click': this.switchToTab.bind(this, 'editor') }, _('Editor')) |
| 64 | + ]) |
| 65 | + ]), |
| 66 | + E('div', { 'class': 'cbi-tabcontainer-content' }, [ |
| 67 | + E('div', { 'id': 'content-filebrowser', 'class': 'cbi-tab', 'style': 'display:block;' }, [ |
| 68 | + // Область прокрутки для таблицы файлов |
| 69 | + E('div', { 'id': 'file-list-container' }, [ |
| 70 | + E('table', { 'class': 'table' }, [ |
| 71 | + E('thead', {}, [ |
| 72 | + E('tr', {}, [ |
| 73 | + E('th', { 'click': this.sortBy.bind(this, 'name') }, _('Name')), |
| 74 | + E('th', { 'click': this.sortBy.bind(this, 'type') }, _('Type')), |
| 75 | + E('th', { 'click': this.sortBy.bind(this, 'size') }, _('Size')), |
| 76 | + E('th', { 'click': this.sortBy.bind(this, 'mtime') }, _('Last Modified')), |
| 77 | + E('th', {}, _('Actions')) |
| 78 | + ]) |
| 79 | + ]), |
| 80 | + E('tbody', { 'id': 'file-list' }) |
| 81 | + ]) |
| 82 | + ]), |
| 83 | + // Область действий: Upload |
| 84 | + E('div', { 'class': 'cbi-page-actions' }, [ |
| 85 | + E('button', { |
| 86 | + 'class': 'btn action-button', |
| 87 | + 'click': this.handleUploadClick.bind(this) |
| 88 | + }, _('Upload File')) |
| 89 | + ]) |
| 90 | + ]), |
| 91 | + E('div', { 'id': 'content-editor', 'class': 'cbi-tab', 'style': 'display:none;' }, [ |
| 92 | + E('p', {}, _('Select a file from the list to edit it here.')), |
| 93 | + E('div', { 'id': 'editor-container' }) // Здесь будет редактор файлов |
| 94 | + ]) |
| 95 | + ]) |
| 96 | + ]) |
| 97 | + ]); |
19 | 98 |
|
20 | | - o = s.option(form.FileUpload, 'root', ''); |
21 | | - o.root_directory = '/'; |
22 | | - o.browser = true; |
23 | | - o.show_hidden = true; |
24 | | - o.enable_upload = true; |
25 | | - o.enable_remove = true; |
26 | | - o.enable_download = true; |
| 99 | + this.loadFileList(currentPath); |
27 | 100 |
|
28 | | - return m.render(); |
29 | | - }, |
| 101 | + ui.tabs.initTabGroup(viewContainer.lastElementChild.childNodes); |
| 102 | + return viewContainer; |
| 103 | + }, |
| 104 | + |
| 105 | + switchToTab: function(tab) { |
| 106 | + document.getElementById('content-filebrowser').style.display = (tab === 'filebrowser') ? 'block' : 'none'; |
| 107 | + document.getElementById('content-editor').style.display = (tab === 'editor') ? 'block' : 'none'; |
| 108 | + |
| 109 | + document.getElementById('tab-filebrowser').classList.toggle('cbi-tab-active', tab === 'filebrowser'); |
| 110 | + document.getElementById('tab-editor').classList.toggle('cbi-tab-active', tab === 'editor'); |
| 111 | + }, |
| 112 | + |
| 113 | + handleUploadClick: function(ev) { |
| 114 | + var uploadInput = document.getElementById('file-upload'); |
| 115 | + if (!uploadInput) { |
| 116 | + uploadInput = document.createElement('input'); |
| 117 | + uploadInput.type = 'file'; |
| 118 | + uploadInput.style.display = 'none'; |
| 119 | + uploadInput.id = 'file-upload'; |
| 120 | + document.body.appendChild(uploadInput); |
| 121 | + } |
| 122 | + |
| 123 | + uploadInput.click(); |
| 124 | + |
| 125 | + uploadInput.onchange = function() { |
| 126 | + var file = uploadInput.files[0]; |
| 127 | + if (file) { |
| 128 | + // Проверка размера файла (например, 10 MB) |
| 129 | + var maxFileSize = 10 * 1024 * 1024; // 10 MB |
| 130 | + if (file.size > maxFileSize) { |
| 131 | + ui.addNotification(null, E('p', _('File size exceeds the maximum allowed size of 10 MB.')), 'error'); |
| 132 | + return; |
| 133 | + } |
| 134 | + |
| 135 | + var reader = new FileReader(); |
| 136 | + reader.onload = function(e) { |
| 137 | + var content = e.target.result; |
| 138 | + var filePath = currentPath.endsWith('/') ? currentPath + file.name : currentPath + '/' + file.name; |
| 139 | + |
| 140 | + // Используем fs.write для записи файла |
| 141 | + fs.write(filePath, content).then(function() { |
| 142 | + ui.addNotification(null, E('p', _('File uploaded successfully.')), 'info'); |
| 143 | + this.loadFileList(currentPath); |
| 144 | + }.bind(this)).catch(function(err) { |
| 145 | + ui.addNotification(null, E('p', _('Failed to upload file: %s').format(err.message))); |
| 146 | + }); |
| 147 | + }.bind(this); |
| 148 | + reader.onerror = function() { |
| 149 | + ui.addNotification(null, E('p', _('Failed to read the file.'))); |
| 150 | + }; |
| 151 | + reader.readAsText(file); // Используем readAsText для текстовых файлов |
| 152 | + } |
| 153 | + }.bind(this); |
| 154 | + }, |
| 155 | + |
| 156 | + loadFileList: function(path) { |
| 157 | + fs.list(path).then(function(files) { |
| 158 | + var fileList = document.getElementById('file-list'); |
| 159 | + fileList.innerHTML = ''; |
| 160 | + |
| 161 | + // Сортировка файлов |
| 162 | + files.sort(this.compareFiles.bind(this)); |
| 163 | + |
| 164 | + if (path !== '/') { |
| 165 | + var parentPath = path.substring(0, path.lastIndexOf('/')) || '/'; |
| 166 | + var listItemUp = E('tr', {}, [ |
| 167 | + E('td', { colspan: 5 }, [ |
| 168 | + E('a', { |
| 169 | + 'href': '#', |
| 170 | + 'click': function() { |
| 171 | + this.handleDirectoryClick(parentPath); |
| 172 | + }.bind(this) |
| 173 | + }, '.. (Parent Directory)') |
| 174 | + ]) |
| 175 | + ]); |
| 176 | + fileList.appendChild(listItemUp); |
| 177 | + } |
| 178 | + |
| 179 | + files.forEach(function(file) { |
| 180 | + var listItem; |
| 181 | + |
| 182 | + if (file.type === 'directory') { |
| 183 | + // Создание строки для директории |
| 184 | + listItem = E('tr', {}, [ |
| 185 | + E('td', {}, [ |
| 186 | + E('a', { |
| 187 | + 'href': '#', |
| 188 | + 'style': 'color:blue;', |
| 189 | + 'click': function() { |
| 190 | + this.handleDirectoryClick(path.endsWith('/') ? path + file.name : path + '/' + file.name); |
| 191 | + }.bind(this) |
| 192 | + }, file.name) |
| 193 | + ]), |
| 194 | + E('td', {}, _('Directory')), |
| 195 | + E('td', {}, '-'), |
| 196 | + E('td', {}, new Date(file.mtime * 1000).toLocaleString()), |
| 197 | + E('td', {}, [ |
| 198 | + E('span', { 'class': 'delete-button', 'click': this.handleDeleteFile.bind(this, path.endsWith('/') ? path + file.name : path + '/' + file.name) }, '🗑️') |
| 199 | + ]) // Без кнопки download для директорий |
| 200 | + ]); |
| 201 | + } else if (file.type === 'file') { |
| 202 | + // Создание строки для обычного файла |
| 203 | + listItem = E('tr', {}, [ |
| 204 | + E('td', {}, [ |
| 205 | + E('a', { |
| 206 | + 'href': '#', |
| 207 | + 'style': 'color:black;', |
| 208 | + 'click': function() { |
| 209 | + this.handleFileClick(path.endsWith('/') ? path + file.name : path + '/' + file.name); |
| 210 | + }.bind(this) |
| 211 | + }, file.name) |
| 212 | + ]), |
| 213 | + E('td', {}, _('File')), |
| 214 | + E('td', {}, this.formatFileSize(file.size)), |
| 215 | + E('td', {}, new Date(file.mtime * 1000).toLocaleString()), |
| 216 | + E('td', {}, [ |
| 217 | + E('span', { 'class': 'delete-button', 'click': this.handleDeleteFile.bind(this, path.endsWith('/') ? path + file.name : path + '/' + file.name) }, '🗑️'), |
| 218 | + E('span', { 'class': 'download-button', 'click': this.handleDownloadFile.bind(this, path.endsWith('/') ? path + file.name : path + '/' + file.name) }, '⬇️') // Кнопка download для файлов |
| 219 | + ]) |
| 220 | + ]); |
| 221 | + } |
| 222 | + |
| 223 | + fileList.appendChild(listItem); |
| 224 | + }.bind(this)); |
| 225 | + }.bind(this)).catch(function(err) { |
| 226 | + ui.addNotification(null, E('p', _('Failed to load file list: %s').format(err.message))); |
| 227 | + }); |
| 228 | + }, |
| 229 | + |
| 230 | + formatFileSize: function(size) { |
| 231 | + if (size == null || size === '-') return '-'; |
| 232 | + var i = Math.floor(Math.log(size) / Math.log(1024)); |
| 233 | + return (size / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i]; |
| 234 | + }, |
| 235 | + |
| 236 | + sortBy: function(field) { |
| 237 | + if (sortField === field) { |
| 238 | + sortDirection = sortDirection === 'asc' ? 'desc' : 'asc'; |
| 239 | + } else { |
| 240 | + sortField = field; |
| 241 | + sortDirection = 'asc'; |
| 242 | + } |
| 243 | + this.loadFileList(currentPath); |
| 244 | + }, |
| 245 | + |
| 246 | + compareFiles: function(a, b) { |
| 247 | + var valueA = a[sortField] || ''; |
| 248 | + var valueB = b[sortField] || ''; |
| 249 | + |
| 250 | + if (sortField === 'name') { |
| 251 | + valueA = valueA.toLowerCase(); |
| 252 | + valueB = valueB.toLowerCase(); |
| 253 | + } |
| 254 | + |
| 255 | + if (sortDirection === 'asc') { |
| 256 | + return valueA > valueB ? 1 : (valueA < valueB ? -1 : 0); |
| 257 | + } else { |
| 258 | + return valueA < valueB ? 1 : (valueA > valueB ? -1 : 0); |
| 259 | + } |
| 260 | + }, |
| 261 | + |
| 262 | + handleDirectoryClick: function(newPath) { |
| 263 | + currentPath = newPath || '/'; |
| 264 | + document.querySelector('h2').textContent = _('File Browser: ') + currentPath; |
| 265 | + this.loadFileList(currentPath); |
| 266 | + }, |
| 267 | + |
| 268 | + handleFileClick: function(filePath) { |
| 269 | + fs.read(filePath).then(function(content) { |
| 270 | + var editorContainer = document.getElementById('editor-container'); |
| 271 | + editorContainer.innerHTML = ''; |
| 272 | + |
| 273 | + var editor = E('div', {}, [ |
| 274 | + E('h3', {}, _('Editing: ') + filePath), |
| 275 | + E('textarea', { |
| 276 | + 'style': 'width:100%;height:80vh;', |
| 277 | + 'rows': 20 |
| 278 | + }, [content != null ? content : '']), |
| 279 | + E('div', { 'class': 'cbi-page-actions' }, [ |
| 280 | + E('button', { |
| 281 | + 'class': 'btn cbi-button-save custom-save-button', |
| 282 | + 'click': this.handleSaveFile.bind(this, filePath) |
| 283 | + }, _('Save')) |
| 284 | + ]) |
| 285 | + ]); |
| 286 | + |
| 287 | + editorContainer.appendChild(editor); |
| 288 | + |
| 289 | + this.switchToTab('editor'); |
| 290 | + }.bind(this)).catch(function(err) { |
| 291 | + ui.addNotification(null, E('p', _('Failed to open file: %s').format(err.message))); |
| 292 | + }); |
| 293 | + }, |
| 294 | + |
| 295 | + handleDownloadFile: function(filePath) { |
| 296 | + // Чтение содержимого файла с помощью fs.read |
| 297 | + fs.read(filePath).then(function(content) { |
| 298 | + var blob = new Blob([content], { type: 'application/octet-stream' }); |
| 299 | + var downloadLink = document.createElement('a'); |
| 300 | + downloadLink.href = URL.createObjectURL(blob); |
| 301 | + downloadLink.download = filePath.split('/').pop(); |
| 302 | + document.body.appendChild(downloadLink); |
| 303 | + downloadLink.click(); |
| 304 | + document.body.removeChild(downloadLink); |
| 305 | + }.bind(this)).catch(function(err) { |
| 306 | + ui.addNotification(null, E('p', _('Failed to download file: %s').format(err.message))); |
| 307 | + }); |
| 308 | + }, |
| 309 | + |
| 310 | + handleDeleteFile: function(filePath) { |
| 311 | + if (confirm(_('Are you sure you want to delete this file or directory?'))) { |
| 312 | + // Используем fs.remove для удаления файла или директории |
| 313 | + fs.remove(filePath).then(function() { |
| 314 | + ui.addNotification(null, E('p', _('File or directory deleted successfully.')), 'info'); |
| 315 | + this.loadFileList(currentPath); |
| 316 | + }.bind(this)).catch(function(err) { |
| 317 | + ui.addNotification(null, E('p', _('Failed to delete file or directory: %s').format(err.message))); |
| 318 | + }); |
| 319 | + } |
| 320 | + }, |
| 321 | + |
| 322 | + handleSaveFile: function(filePath) { |
| 323 | + var content = document.querySelector('textarea').value; |
| 324 | + // Используем fs.write для записи содержимого файла |
| 325 | + fs.write(filePath, content).then(function() { |
| 326 | + ui.addNotification(null, E('p', _('File saved successfully.')), 'info'); |
| 327 | + this.loadFileList(currentPath); |
| 328 | + }.bind(this)).catch(function(err) { |
| 329 | + ui.addNotification(null, E('p', _('Failed to save file: %s').format(err.message))); |
| 330 | + }); |
| 331 | + } |
| 332 | +}); |
30 | 333 |
|
31 | | - handleSave: null, |
32 | | - handleSaveApply: null, |
33 | | - handleReset: null |
34 | | -}) |
|
0 commit comments