Skip to content

Commit 3659619

Browse files
authored
Update filebrowser.js
Added scrolling and sort options to navigation panel and editing feature for a selected file Signed-off-by: rdmitry0911 <rdmitry0911@gmail.com>
1 parent 8559119 commit 3659619

File tree

1 file changed

+322
-23
lines changed
  • applications/luci-app-filebrowser/htdocs/luci-static/resources/view/system

1 file changed

+322
-23
lines changed

applications/luci-app-filebrowser/htdocs/luci-static/resources/view/system/filebrowser.js

Lines changed: 322 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,333 @@
11
'use strict';
22
'require view';
3+
'require fs';
34
'require ui';
4-
'require form';
5+
'require dom';
56

6-
var formData = {
7-
files: {
8-
root: null,
9-
}
10-
};
7+
var currentPath = '/'; // Изначально корневой каталог
8+
var sortField = 'name'; // Поле для сортировки по умолчанию
9+
var sortDirection = 'asc'; // Направление сортировки по умолчанию (asc - по возрастанию)
1110

1211
return view.extend({
13-
render: function() {
14-
var m, s, o;
12+
load: function() {
13+
return fs.list(currentPath); // Загрузить список файлов в текущем каталоге
14+
},
1515

16-
m = new form.JSONMap(formData, _('File Browser'), '');
16+
render: function(data) {
17+
var files = data;
1718

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+
]);
1998

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);
27100

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+
});
30333

31-
handleSave: null,
32-
handleSaveApply: null,
33-
handleReset: null
34-
})

0 commit comments

Comments
 (0)