0
0
mirror of https://github.com/openwrt/luci.git synced 2025-01-31 06:01:47 +00:00
Dmitry R 9f74f0069a luci-app-filemanager: Editing hex files improvements
- Now it uses fs.read_direct() to retrieve the file content
 - Now it opens non-text files in hex Editor by default
 - Now the 'Toggle to ASCII mode' button is disabled in hex Editor if the file is non-text.

Signed-off-by: Dmitry R <rdmitry0911@gmail.com>
2025-01-05 14:04:29 +00:00

2906 lines
88 KiB
JavaScript

'use strict';
'require view';
'require fs';
'require ui';
'require dom';
'require rpc';
'require view.system.filemanager.md as md';
'require view.system.filemanager.md_help as md_help';
'require view.system.filemanager.HexEditor as HE';
function pop(a, message, timeout, severity) {
ui.addNotification(a, message, timeout, severity)
}
// Initialize global variables
var currentPath = '/'; // Current path in the filesystem
var selectedItems = new Set(); // Set of selected files/directories
var sortField = 'name'; // Field to sort files by
var sortDirection = 'asc'; // Sort direction (ascending/descending)
var configFilePath = '/etc/config/filemanager'; // Path to the configuration file
// Initialize drag counter
var dragCounter = 0;
// Configuration object to store interface settings
var config = {
// Column widths in the file table
columnWidths: {
'name': 150,
'type': 100,
'size': 100,
'mtime': 150,
'actions': 100
},
// Minimum column widths
columnMinWidths: {
'name': 100,
'type': 80,
'size': 80,
'mtime': 120,
'actions': 80
},
// Maximum column widths
columnMaxWidths: {
'name': 300,
'type': 200,
'size': 200,
'mtime': 300,
'actions': 200
},
// Padding and window sizes
padding: 10,
paddingMin: 5,
paddingMax: 20,
currentDirectory: '/', // Current directory
windowSizes: {
width: 800,
height: 400
},
editorContainerSizes: {
text: {
width: 850,
height: 550
},
hex: {
width: 850,
height: 550
}
},
otherSettings: {} // Additional settings
};
// Function to upload a file to the server
function uploadFile(filename, filedata, onProgress) {
return new Promise(function(resolve, reject) {
var formData = new FormData();
formData.append('sessionid', rpc.getSessionID()); // Add session ID
formData.append('filename', filename); // File name including path
formData.append('filedata', filedata); // File data
var xhr = new XMLHttpRequest();
xhr.open('POST', L.env.cgi_base + '/cgi-upload', true); // Configure the request
// Monitor upload progress
xhr.upload.onprogress = function(event) {
if (event.lengthComputable && onProgress) {
var percent = (event.loaded / event.total) * 100;
onProgress(percent); // Call the progress callback with percentage
}
};
// Handle request completion
xhr.onload = function() {
if (xhr.status === 200) {
resolve(xhr.responseText); // Upload successful
} else {
reject(new Error(xhr.statusText)); // Upload error
}
};
// Handle network errors
xhr.onerror = function() {
reject(new Error('Network error'));
};
xhr.send(formData); // Send the request
});
}
// Function to load settings from the configuration file
function parseKeyValuePairs(input, delimiter, callback) {
const pairs = input.split(',');
pairs.forEach((pair) => {
const [key, value] = pair.split(delimiter);
if (key && value) callback(key.trim(), value.trim());
});
}
async function loadConfig() {
try {
const content = await fs.read(configFilePath);
const lines = content.trim().split('\n');
lines.forEach((line) => {
if (!line.includes('option')) return;
const splitLines = line.split('option').filter(Boolean);
splitLines.forEach((subline) => {
const formattedLine = "option " + subline.trim();
const match = formattedLine.match(/^option\s+(\S+)\s+'([^']+)'$/);
if (!match) return;
const [, key, value] = match;
switch (key) {
case 'columnWidths':
case 'columnMinWidths':
case 'columnMaxWidths':
parseKeyValuePairs(value, ':', (k, v) => {
config[key] = config[key] || {};
config[key][k] = parseInt(v, 10);
});
break;
case 'currentDirectory':
config.currentDirectory = value;
break;
case 'windowSizes':
parseKeyValuePairs(value, ':', (k, v) => {
config.windowSizes = config.windowSizes || {};
const sizeValue = parseInt(v, 10);
if (!isNaN(sizeValue)) {
config.windowSizes[k] = sizeValue;
}
});
break;
case 'editorContainerSizes':
parseKeyValuePairs(value, ':', (mode, sizeStr) => {
const [widthStr, heightStr] = sizeStr.split('x');
const width = parseInt(widthStr, 10);
const height = parseInt(heightStr, 10);
if (!isNaN(width) && !isNaN(height)) {
config.editorContainerSizes[mode] = {
width: width,
height: height
};
}
});
break;
default:
config[key] = value;
}
});
});
} catch (err) {
console.error('Failed to load config: ' + err.message);
}
}
// Function to save settings to the configuration file
function saveConfig() {
// Before saving, ensure sizes are valid
['text', 'hex'].forEach(function(mode) {
var sizes = config.editorContainerSizes[mode];
if (!sizes || isNaN(sizes.width) || isNaN(sizes.height) || sizes.width <= 0 || sizes.height <= 0) {
// Use default sizes if invalid
config.editorContainerSizes[mode] = {
width: 850,
height: 550
};
}
});
var configLines = ['config filemanager',
'\toption columnWidths \'' + Object.keys(config.columnWidths).map(function(field) {
return field + ':' + config.columnWidths[field];
}).join(',') + '\'',
'\toption columnMinWidths \'' + Object.keys(config.columnMinWidths).map(function(field) {
return field + ':' + config.columnMinWidths[field];
}).join(',') + '\'',
'\toption columnMaxWidths \'' + Object.keys(config.columnMaxWidths).map(function(field) {
return field + ':' + config.columnMaxWidths[field];
}).join(',') + '\'',
'\toption padding \'' + config.padding + '\'',
'\toption paddingMin \'' + config.paddingMin + '\'',
'\toption paddingMax \'' + config.paddingMax + '\'',
'\toption currentDirectory \'' + config.currentDirectory + '\'',
'\toption windowSizes \'' + Object.keys(config.windowSizes).map(function(key) {
return key + ':' + config.windowSizes[key];
}).join(',') + '\'',
'\toption editorContainerSizes \'' + Object.keys(config.editorContainerSizes).map(function(mode) {
var sizes = config.editorContainerSizes[mode];
return mode + ':' + sizes.width + 'x' + sizes.height;
}).join(',') + '\''
];
// Add additional settings
Object.keys(config.otherSettings).forEach(function(key) {
configLines.push('\toption ' + key + ' \'' + config.otherSettings[key] + '\'');
});
var configContent = configLines.join('\n') + '\n';
// Write settings to file
return fs.write(configFilePath, configContent).then(function() {
return Promise.resolve();
}).catch(function(err) {
return Promise.reject(new Error('Failed to save configuration: ' + err.message));
});
}
// Function to correctly join paths
function joinPath(path, name) {
return path.endsWith('/') ? path + name : path + '/' + name;
}
// Function to convert symbolic permissions to numeric format
function symbolicToNumeric(permissions) {
var specialPerms = 0;
var permMap = {
'r': 4,
'w': 2,
'x': 1,
'-': 0
};
var numeric = '';
for (var i = 0; i < permissions.length; i += 3) {
var subtotal = 0;
for (var j = 0; j < 3; j++) {
var char = permissions[i + j];
if (char === 's' || char === 'S') {
// Special setuid and setgid bits
if (i === 0) {
specialPerms += 4;
} else if (i === 3) {
specialPerms += 2;
}
subtotal += permMap['x'];
} else if (char === 't' || char === 'T') {
// Special sticky bit
if (i === 6) {
specialPerms += 1;
}
subtotal += permMap['x'];
} else {
subtotal += permMap[char] !== undefined ? permMap[char] : 0;
}
}
numeric += subtotal.toString();
}
if (specialPerms > 0) {
numeric = specialPerms.toString() + numeric;
}
return numeric;
}
// Function to get a list of files in a directory
function getFileList(path) {
return fs.exec('/bin/ls', ['-lA', '--full-time', path]).then(function(res) {
if (res.code !== 0) {
var errorMessage = res.stderr ? res.stderr.trim() : 'Unknown error';
return Promise.reject(new Error('Failed to list directory: ' + errorMessage));
}
var stdout = res.stdout || '';
var lines = stdout.trim().split('\n');
var files = [];
lines.forEach(function(line) {
if (line.startsWith('total') || !line.trim()) return;
// Parse the output line from 'ls' command
var parts = line.match(/^([\-dl])[rwx\-]{2}[rwx\-Ss]{1}[rwx\-]{2}[rwx\-Ss]{1}[rwx\-]{2}[rwx\-Tt]{1}\s+\d+\s+(\S+)\s+(\S+)\s+(\d+)\s+([\d\-]+\s+[\d\:\.]{8,12}\s+[+-]\d{4})\s+(.+)$/);
if (!parts || parts.length < 7) {
console.warn('Failed to parse line:', line);
return;
}
var typeChar = parts[1];
var permissions = line.substring(0, 10);
var owner = parts[2];
var group = parts[3];
var size = parseInt(parts[4], 10);
var dateStr = parts[5];
var name = parts[6];
var type = '';
var target = null;
if (typeChar === 'd') {
type = 'directory'; // Directory
} else if (typeChar === '-') {
type = 'file'; // File
} else if (typeChar === 'l') {
type = 'symlink'; // Symbolic link
var linkParts = name.split(' -> ');
name = linkParts[0];
target = linkParts[1] || '';
} else {
type = 'unknown'; // Unknown type
}
var mtime = Date.parse(dateStr);
if (type === 'symlink' && target && size === 4096) {
size = -1; // Size for symlinks may be incorrect
}
files.push({
name: name,
type: type,
size: size,
mtime: mtime / 1000,
owner: owner,
group: group,
permissions: permissions.substring(1),
numericPermissions: symbolicToNumeric(permissions.substring(1)),
target: target
});
});
return files;
});
}
// Function to insert CSS styles into the document
function insertCss(cssContent) {
var styleElement = document.createElement('style');
styleElement.type = 'text/css';
styleElement.appendChild(document.createTextNode(cssContent));
document.head.appendChild(styleElement);
}
// CSS styles for the file manager interface
var cssContent = `
.cbi-button-apply, .cbi-button-reset, .cbi-button-save:not(.custom-save-button) {
display: none !important;
}
.cbi-page-actions {
background: none !important;
border: none !important;
padding: ${config.padding}px 0 !important;
margin: 0 !important;
display: flex;
justify-content: flex-start;
margin-top: 10px;
}
.cbi-tabmenu {
background: none !important;
border: none !important;
margin: 0 !important;
padding: 0 !important;
}
.cbi-tabmenu li {
display: inline-block;
margin-right: 10px;
}
#file-list-container {
margin-top: 30px !important;
overflow: auto;
border: 1px solid #ccc;
padding: 0;
min-width: 600px;
position: relative;
resize: both;
}
#file-list-container.drag-over {
border: 2px dashed #00BFFF;
background-color: rgba(0, 191, 255, 0.1);
}
/* Add extra space to the left of the Name and Type columns */
.table th:nth-child(1), .table td:nth-child(1), /* Name column */
.table th:nth-child(2), .table td:nth-child(2) { /* Type column */
padding-left: 5px; /* Adjust this value for the desired spacing */
}
/* Add extra space to the right of the Size column */
.table th:nth-child(3), .table td:nth-child(3) { /* Size column */
padding-right: 5px; /* Adjust this value for the desired spacing */
}
/* Add extra space to the left of the Size column header */
.table th:nth-child(3) { /* Size column header */
padding-left: 15px; /* Adjust this value for the desired spacing */
}
#drag-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 191, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: #00BFFF;
z-index: 10;
pointer-events: none;
}
#content-editor {
margin-top: 30px !important;
}
.editor-container {
display: flex;
flex-direction: column;
resize: both;
overflow: hidden;
}
.editor-content {
flex: 1;
display: flex;
overflow: hidden;
}
.line-numbers {
width: 50px;
background-color: #f0f0f0;
text-align: right;
padding-right: 5px;
user-select: none;
border-right: 1px solid #ccc;
overflow: hidden;
flex-shrink: 0;
-ms-overflow-style: none; /* Hide scrollbar in IE и Edge */
scrollbar-width: none; /* Hide scrollbar in Firefox */
}
.line-numbers::-webkit-scrollbar {
display: none; /* Hide scrollbar in Chrome, Safari и Opera */
}
.line-numbers div {
font-family: monospace;
font-size: 14px;
line-height: 1.2em;
height: 1.2em;
}
#editor-message {
font-size: 18px;
font-weight: bold;
}
#editor-textarea {
flex: 1;
resize: none;
border: none;
font-family: monospace;
font-size: 14px;
line-height: 1.2em;
padding: 0;
margin: 0;
overflow: auto;
box-sizing: border-box;
}
#editor-textarea, .line-numbers {
overflow-y: scroll;
}
th {
text-align: left !important;
position: sticky;
top: 0;
border-right: 1px solid #ddd;
box-sizing: border-box;
padding-right: 30px;
white-space: nowrap;
min-width: 100px;
background-color: #fff;
z-index: 2;
}
td {
text-align: left !important;
border-right: 1px solid #ddd;
box-sizing: border-box;
white-space: nowrap;
min-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
}
tr:hover {
background-color: #f0f0f0 !important;
}
.download-button {
color: green;
cursor: pointer;
margin-left: 5px;
}
.delete-button {
color: red;
cursor: pointer;
margin-left: 5px;
}
.edit-button {
color: blue;
cursor: pointer;
margin-left: 5px;
}
.duplicate-button {
color: orange;
cursor: pointer;
margin-left: 5px;
}
.symlink {
color: green;
}
.status-link {
color: blue;
text-decoration: underline;
cursor: pointer;
}
.action-button {
margin-right: 10px;
cursor: pointer;
}
.size-cell {
text-align: right;
font-family: monospace;
box-sizing: border-box;
white-space: nowrap;
display: flex;
justify-content: flex-end;
align-items: center;
}
.size-number {
display: inline-block;
width: 8ch;
text-align: right;
}
.size-unit {
display: inline-block;
width: 4ch;
text-align: right;
margin-left: 0.5ch;
}
.table {
table-layout: fixed;
border-collapse: collapse;
white-space: nowrap;
width: 100%;
}
.table th:nth-child(3), .table td:nth-child(3) {
width: 100px;
min-width: 100px;
max-width: 500px;
}
.table th:nth-child(3) + th, .table td:nth-child(3) + td {
padding-left: 10px;
}
.resizer {
position: absolute;
right: 0;
top: 0;
width: 5px;
height: 100%;
cursor: col-resize;
user-select: none;
z-index: 3;
}
.resizer::after {
content: "";
position: absolute;
right: 2px;
top: 0;
width: 1px;
height: 100%;
background: #aaa;
}
#file-list-container.resizable {
resize: both;
overflow: auto;
}
.sort-button {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
background: none;
border: 1px solid #ccc; /* Add a visible border */
color: #fff; /* White text color for better contrast on dark backgrounds */
cursor: pointer;
padding: 2px 5px; /* Add padding for better clickability */
font-size: 12px; /* Set font size */
border-radius: 4px; /* Rounded corners for a better appearance */
background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent black background */
transition: background-color 0.3s, color 0.3s; /* Smooth transition effects for hover */
}
.sort-button:hover {
background-color: #fff; /* Change background to white on hover */
color: #000; /* Change text color to black on hover */
border-color: #fff; /* White border on hover */
}
.sort-button:focus {
outline: none;
}
#status-bar {
margin-top: 10px;
padding: 10px;
background-color: #f9f9f9;
border: 1px solid #ccc;
min-height: 40px;
display: flex;
align-items: center;
justify-content: space-between;
}
#status-info {
font-weight: bold;
display: flex;
align-items: center;
}
#status-progress {
width: 50%;
}
.cbi-progressbar {
width: 100%;
background-color: #e0e0e0;
border-radius: 5px;
overflow: hidden;
height: 10px;
}
.cbi-progressbar div {
height: 100%;
background-color: #76c7c0;
width: 0%;
transition: width 0.2s;
}
.file-manager-header {
display: flex;
align-items: center;
}
.file-manager-header h2 {
margin: 0;
}
.file-manager-header input {
margin-left: 10px;
width: 100%;
max-width: 700px;
font-size: 18px;
}
.file-manager-header button {
margin-left: 10px;
font-size: 18px;
}
.directory-link {
/* Choose a color with good contrast or let the theme decide */
color: #00BFFF; /* DeepSkyBlue */
font-weight: bold;
}
.file-link {
color: inherit; /* Use the default text color */
}
`;
// Main exported view module
return view.extend({
editorMode: 'text',
hexEditorInstance: null,
// Method called when the view is loaded
load: function() {
var self = this;
return loadConfig().then(function() {
currentPath = config.currentDirectory || '/';
return getFileList(currentPath); // Load the file list for the current directory
});
},
// Method to render the interface
render: function(data) {
var self = this;
insertCss(cssContent); // Insert CSS styles
// insertCss(hexeditCssContent); // Insert hexedit CSS styles
var viewContainer = E('div', {
'id': 'file-manager-container'
}, [
// File Manager Header
E('div', {
'class': 'file-manager-header'
}, [
E('h2', {}, _('File Manager: ')),
E('input', {
'type': 'text',
'id': 'path-input',
'value': currentPath,
'style': 'margin-left: 10px;',
'keydown': function(event) {
if (event.key === 'Enter') {
self.handleGoButtonClick(); // Trigger directory navigation on Enter
}
}
}),
E('button', {
'id': 'go-button',
'click': this.handleGoButtonClick.bind(this),
'style': 'margin-left: 10px;'
}, _('Go'))
]),
// Tab Panels
E('div', {
'class': 'cbi-tabcontainer',
'id': 'tab-group'
}, [
E('ul', {
'class': 'cbi-tabmenu'
}, [
E('li', {
'class': 'cbi-tab cbi-tab-active',
'id': 'tab-filemanager'
}, [
E('a', {
'href': '#',
'click': this.switchToTab.bind(this, 'filemanager')
}, _('File Manager'))
]),
E('li', {
'class': 'cbi-tab',
'id': 'tab-editor'
}, [
E('a', {
'href': '#',
'click': this.switchToTab.bind(this, 'editor')
}, _('Editor'))
]),
E('li', {
'class': 'cbi-tab',
'id': 'tab-settings'
}, [
E('a', {
'href': '#',
'click': this.switchToTab.bind(this, 'settings')
}, _('Settings'))
]),
// Help Tab
E('li', {
'class': 'cbi-tab',
'id': 'tab-help'
}, [
E('a', {
'href': '#',
'click': this.switchToTab.bind(this, 'help')
}, _('Help'))
])
])
]),
// Tab Contents
E('div', {
'class': 'cbi-tabcontainer-content'
}, [
// File Manager Content
E('div', {
'id': 'content-filemanager',
'class': 'cbi-tab',
'style': 'display:block;'
}, [
// File List Container with Drag-and-Drop
(function() {
// Create the container for the file list and drag-and-drop functionality
var fileListContainer = E('div', {
'id': 'file-list-container',
'class': 'resizable',
'style': 'width: ' + config.windowSizes.width + 'px; height: ' + config.windowSizes.height + 'px;'
}, [
E('table', {
'class': 'table',
'id': 'file-table'
}, [
E('thead', {}, [
E('tr', {}, [
E('th', {
'data-field': 'name'
}, [
_('Name'),
E('button', {
'class': 'sort-button',
'data-field': 'name',
'title': _('Sort by Name')
}, '↕'),
E('div', {
'class': 'resizer'
})
]),
E('th', {
'data-field': 'type'
}, [
_('Type'),
E('button', {
'class': 'sort-button',
'data-field': 'type',
'title': _('Sort by Type')
}, '↕'),
E('div', {
'class': 'resizer'
})
]),
E('th', {
'data-field': 'size'
}, [
_('Size'),
E('button', {
'class': 'sort-button',
'data-field': 'size',
'title': _('Sort by Size')
}, '↕'),
E('div', {
'class': 'resizer'
})
]),
E('th', {
'data-field': 'mtime'
}, [
_('Last Modified'),
E('button', {
'class': 'sort-button',
'data-field': 'mtime',
'title': _('Sort by Last Modified')
}, '↕'),
E('div', {
'class': 'resizer'
})
]),
E('th', {}, [
E('input', {
'type': 'checkbox',
'id': 'select-all-checkbox',
'style': 'margin-right: 5px;',
'change': this.handleSelectAllChange.bind(this),
'click': this.handleSelectAllClick.bind(this)
}),
_('Actions')
])
])
]),
E('tbody', {
'id': 'file-list'
})
]),
E('div', {
'id': 'drag-overlay',
'style': 'display:none;'
}, _('Drop files here to upload'))
]);
// Attach drag-and-drop event listeners
fileListContainer.addEventListener('dragenter', this.handleDragEnter.bind(this));
fileListContainer.addEventListener('dragover', this.handleDragOver.bind(this));
fileListContainer.addEventListener('dragleave', this.handleDragLeave.bind(this));
fileListContainer.addEventListener('drop', this.handleDrop.bind(this));
return fileListContainer;
}).call(this), // Ensure 'this' context is preserved
// Status Bar
E('div', {
'id': 'status-bar'
}, [
E('div', {
'id': 'status-info'
}, _('No file selected.')),
E('div', {
'id': 'status-progress'
})
]),
// Page Actions
E('div', {
'class': 'cbi-page-actions'
}, [
E('button', {
'class': 'btn action-button',
'click': this.handleUploadClick.bind(this)
}, _('Upload File')),
E('button', {
'class': 'btn action-button',
'click': this.handleMakeDirectoryClick.bind(this)
}, _('Create Folder')),
E('button', {
'class': 'btn action-button',
'click': this.handleCreateFileClick.bind(this)
}, _('Create File')),
E('button', {
'id': 'delete-selected-button',
'class': 'btn action-button',
'style': 'display: none;',
'click': this.handleDeleteSelected.bind(this)
}, _('Delete Selected'))
])
]),
// Editor Content
E('div', {
'id': 'content-editor',
'class': 'cbi-tab',
'style': 'display:none;'
}, [
E('p', {
'id': 'editor-message'
}, _('Select a file from the list to edit it here.')),
E('div', {
'id': 'editor-container'
})
]),
// Help Content
E('div', {
'id': 'content-help',
'class': 'cbi-tab',
'style': 'display:none; padding: 10px; overflow:auto; width: 650px; height: 600px; resize: both; border: 1px solid #ccc; box-sizing: border-box;'
}, [
// The content will be dynamically inserted by renderHelp()
]),
// Settings Content
E('div', {
'id': 'content-settings',
'class': 'cbi-tab',
'style': 'display:none;'
}, [
E('div', {
'style': 'margin-top: 20px;'
}, [
E('h3', {}, _('Interface Settings')),
E('div', {
'id': 'settings-container'
}, [
E('form', {
'id': 'settings-form'
}, [
E('div', {}, [
E('label', {}, _('Window Width:')),
E('input', {
'type': 'number',
'id': 'window-width-input',
'value': config.windowSizes.width,
'style': 'width:100%; margin-bottom:10px;'
})
]),
E('div', {}, [
E('label', {}, _('Window Height:')),
E('input', {
'type': 'number',
'id': 'window-height-input',
'value': config.windowSizes.height,
'style': 'width:100%; margin-bottom:10px;'
})
]),
E('div', {}, [
E('label', {}, _('Text Editor Width:')),
E('input', {
'type': 'number',
'id': 'editor-text-width-input',
'value': config.editorContainerSizes.text.width,
'style': 'width:100%; margin-bottom:10px;'
})
]),
E('div', {}, [
E('label', {}, _('Text Editor Height:')),
E('input', {
'type': 'number',
'id': 'editor-text-height-input',
'value': config.editorContainerSizes.text.height,
'style': 'width:100%; margin-bottom:10px;'
})
]),
E('div', {}, [
E('label', {}, _('Hex Editor Width:')),
E('input', {
'type': 'number',
'id': 'editor-hex-width-input',
'value': config.editorContainerSizes.hex.width,
'style': 'width:100%; margin-bottom:10px;'
})
]),
E('div', {}, [
E('label', {}, _('Hex Editor Height:')),
E('input', {
'type': 'number',
'id': 'editor-hex-height-input',
'value': config.editorContainerSizes.hex.height,
'style': 'width:100%; margin-bottom:10px;'
})
]),
E('div', {}, [
E('label', {}, _('Column Widths (format: name:width,type:width,...):')),
E('input', {
'type': 'text',
'id': 'column-widths-input',
'value': Object.keys(config.columnWidths).map(function(field) {
return field + ':' + config.columnWidths[field];
}).join(','),
'style': 'width:100%; margin-bottom:10px;'
})
]),
E('div', {}, [
E('label', {}, _('Column Min Widths (format: name:minWidth,type:minWidth,...):')),
E('input', {
'type': 'text',
'id': 'column-min-widths-input',
'value': Object.keys(config.columnMinWidths).map(function(field) {
return field + ':' + config.columnMinWidths[field];
}).join(','),
'style': 'width:100%; margin-bottom:10px;'
})
]),
E('div', {}, [
E('label', {}, _('Column Max Widths (format: name:maxWidth,type:maxWidth,...):')),
E('input', {
'type': 'text',
'id': 'column-max-widths-input',
'value': Object.keys(config.columnMaxWidths).map(function(field) {
return field + ':' + config.columnMaxWidths[field];
}).join(','),
'style': 'width:100%; margin-bottom:10px;'
})
]),
E('div', {}, [
E('label', {}, _('Padding:')),
E('input', {
'type': 'number',
'id': 'padding-input',
'value': config.padding,
'style': 'width:100%; margin-bottom:10px;'
})
]),
E('div', {}, [
E('label', {}, _('Padding Min:')),
E('input', {
'type': 'number',
'id': 'padding-min-input',
'value': config.paddingMin,
'style': 'width:100%; margin-bottom:10px;'
})
]),
E('div', {}, [
E('label', {}, _('Padding Max:')),
E('input', {
'type': 'number',
'id': 'padding-max-input',
'value': config.paddingMax,
'style': 'width:100%; margin-bottom:10px;'
})
]),
E('div', {}, [
E('label', {}, _('Current Directory:')),
E('input', {
'type': 'text',
'id': 'current-directory-input',
'value': config.currentDirectory,
'style': 'width:100%; margin-bottom:10px;'
})
]),
E('div', {
'class': 'cbi-page-actions'
}, [
E('button', {
'class': 'btn cbi-button-save custom-save-button',
'click': this.handleSaveSettings.bind(this)
}, _('Save'))
])
])
])
])
])
])
]);
// Add event listeners
var sortButtons = viewContainer.querySelectorAll('.sort-button[data-field]');
sortButtons.forEach(function(button) {
button.addEventListener('click', function(event) {
event.preventDefault();
var field = button.getAttribute('data-field');
if (field) {
self.sortBy(field); // Sort the file list by the selected field
}
});
});
// Load the file list and initialize resizable columns
this.loadFileList(currentPath).then(function() {
self.initResizableColumns();
var fileListContainer = document.getElementById('file-list-container');
if (fileListContainer && typeof ResizeObserver !== 'undefined') {
// Initialize ResizeObserver only once
if (!self.fileListResizeObserver) {
self.fileListResizeObserver = new ResizeObserver(function(entries) {
for (var entry of entries) {
var newWidth = entry.contentRect.width;
var newHeight = entry.contentRect.height;
// Update config only if newWidth and newHeight are greater than 0
if (newWidth > 0 && newHeight > 0) {
config.windowSizes.width = newWidth;
config.windowSizes.height = newHeight;
}
}
});
self.fileListResizeObserver.observe(fileListContainer);
}
}
});
return viewContainer;
},
// Handler for the "Select All" checkbox click
handleSelectAllClick: function(ev) {
if (ev.altKey) {
ev.preventDefault(); // Prevent the default checkbox behavior
this.handleInvertSelection();
} else {
// Proceed with normal click handling; the 'change' event will be triggered
}
},
// Function to invert selection
handleInvertSelection: function() {
var allCheckboxes = document.querySelectorAll('.select-checkbox');
allCheckboxes.forEach(function(checkbox) {
checkbox.checked = !checkbox.checked;
var filePath = checkbox.getAttribute('data-file-path');
if (checkbox.checked) {
selectedItems.add(filePath);
} else {
selectedItems.delete(filePath);
}
});
// Update the "Select All" checkbox state
this.updateSelectAllCheckbox();
// Update the "Delete Selected" button visibility
this.updateDeleteSelectedButton();
},
/**
* Switches the active tab in the interface and performs necessary actions based on the selected tab.
*
* @param {string} tab - The identifier of the tab to switch to ('filemanager', 'editor', 'settings', or 'help').
*/
switchToTab: function(tab) {
// Retrieve the content containers for each tab
var fileManagerContent = document.getElementById('content-filemanager');
var editorContent = document.getElementById('content-editor');
var settingsContent = document.getElementById('content-settings');
var helpContent = document.getElementById('content-help');
// Retrieve the tab elements
var tabFileManager = document.getElementById('tab-filemanager');
var tabEditor = document.getElementById('tab-editor');
var tabSettings = document.getElementById('tab-settings');
var tabHelp = document.getElementById('tab-help');
// Ensure all necessary elements are present
if (fileManagerContent && editorContent && settingsContent && helpContent && tabFileManager && tabEditor && tabSettings && tabHelp) {
// Display the selected tab's content and hide the others
fileManagerContent.style.display = (tab === 'filemanager') ? 'block' : 'none';
editorContent.style.display = (tab === 'editor') ? 'block' : 'none';
settingsContent.style.display = (tab === 'settings') ? 'block' : 'none';
helpContent.style.display = (tab === 'help') ? 'block' : 'none';
// Update the active tab's styling
tabFileManager.className = (tab === 'filemanager') ? 'cbi-tab cbi-tab-active' : 'cbi-tab';
tabEditor.className = (tab === 'editor') ? 'cbi-tab cbi-tab-active' : 'cbi-tab';
tabSettings.className = (tab === 'settings') ? 'cbi-tab cbi-tab-active' : 'cbi-tab';
tabHelp.className = (tab === 'help') ? 'cbi-tab cbi-tab-active' : 'cbi-tab';
// Perform actions based on the selected tab
if (tab === 'filemanager') {
// Reload and display the updated file list when the File Manager tab is activated
this.loadFileList(currentPath)
.then(() => {
// Initialize resizable columns after successfully loading the file list
this.initResizableColumns();
})
.catch((err) => {
// Display an error notification if loading the file list fails
pop(null, E('p', _('Failed to update file list: %s').format(err.message)), 'error');
});
} else if (tab === 'settings') {
// Load and display settings when the Settings tab is activated
this.loadSettings();
} else if (tab === 'help') {
// Render the Help content when the Help tab is activated
this.renderHelp();
}
// No additional actions are required for the Editor tab in this context
}
},
/**
* Renders the Help content by converting Markdown to HTML and inserting it into the Help container.
*/
renderHelp: function() {
var self = this;
// Convert Markdown to HTML
var helpContentHTML = md.parseMarkdown(md_help.helpContentMarkdown);
// Get the Help content container
var helpContent = document.getElementById('content-help');
if (helpContent) {
// Insert the converted HTML into the Help container
helpContent.innerHTML = helpContentHTML;
// Initialize resizable functionality for the Help window
self.initResizableHelp();
} else {
console.error('Help content container not found.');
pop(null, E('p', _('Failed to render Help content: Container not found.')), 'error');
}
},
/**
* Initializes the resizable functionality for the Help window.
*/
initResizableHelp: function() {
var helpContent = document.getElementById('content-help');
if (helpContent) {
// Set initial dimensions
helpContent.style.width = '700px';
helpContent.style.height = '600px';
helpContent.style.resize = 'both';
helpContent.style.overflow = 'auto';
helpContent.style.border = '1px solid #ccc';
helpContent.style.padding = '10px';
helpContent.style.boxSizing = 'border-box';
// Optional: Add a drag handle for better user experience
/*
var dragHandle = E('div', {
'class': 'resize-handle',
'style': 'width: 10px; height: 10px; background: #ccc; position: absolute; bottom: 0; right: 0; cursor: se-resize;'
});
helpContent.appendChild(dragHandle);
*/
} else {
console.error('Help content container not found for resizing.');
}
},
// Handler for the "Go" button click to navigate to a directory
handleGoButtonClick: function() {
// Logic to navigate to the specified directory and update the file list
var self = this;
var pathInput = document.getElementById('path-input');
if (pathInput) {
var newPath = pathInput.value.trim() || '/';
fs.stat(newPath).then(function(stat) {
if (stat.type === 'directory') {
currentPath = newPath;
pathInput.value = currentPath;
self.loadFileList(currentPath).then(function() {
self.initResizableColumns();
});
} else {
pop(null, E('p', _('The specified path does not appear to be a directory.')), 'error');
}
}).catch(function(err) {
pop(null, E('p', _('Failed to access the specified path: %s').format(err.message)), 'error');
});
}
},
// Handler for dragging files over the drop zone
handleDragEnter: function(event) {
event.preventDefault();
event.stopPropagation();
dragCounter++;
var fileListContainer = document.getElementById('file-list-container');
var dragOverlay = document.getElementById('drag-overlay');
if (fileListContainer && dragOverlay) {
fileListContainer.classList.add('drag-over');
dragOverlay.style.display = 'flex';
}
},
// Handler for when files are over the drop zone
handleDragOver: function(event) {
event.preventDefault();
event.stopPropagation();
event.dataTransfer.dropEffect = 'copy'; // Indicate copy action
},
// Handler for leaving the drop zone
handleDragLeave: function(event) {
event.preventDefault();
event.stopPropagation();
dragCounter--;
if (dragCounter === 0) {
var fileListContainer = document.getElementById('file-list-container');
var dragOverlay = document.getElementById('drag-overlay');
if (fileListContainer && dragOverlay) {
fileListContainer.classList.remove('drag-over');
dragOverlay.style.display = 'none';
}
}
},
// Handler for dropping files into the drop zone
handleDrop: function(event) {
event.preventDefault();
event.stopPropagation();
dragCounter = 0; // Reset counter
var self = this;
var files = event.dataTransfer.files;
var fileListContainer = document.getElementById('file-list-container');
var dragOverlay = document.getElementById('drag-overlay');
if (fileListContainer && dragOverlay) {
fileListContainer.classList.remove('drag-over');
dragOverlay.style.display = 'none';
}
if (files.length > 0) {
self.uploadFiles(files);
}
},
// Handler for uploading a file
handleUploadClick: function(ev) {
var self = this;
var fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.multiple = true; // Allow selecting multiple files
fileInput.style.display = 'none';
document.body.appendChild(fileInput);
fileInput.onchange = function(event) {
var files = event.target.files;
if (!files || files.length === 0) {
pop(null, E('p', _('No file selected.')), 'error');
return;
}
self.uploadFiles(files); // Use the shared upload function
};
fileInput.click();
},
uploadFiles: function(files) {
var self = this;
var directoryPath = currentPath;
var statusInfo = document.getElementById('status-info');
var statusProgress = document.getElementById('status-progress');
var totalFiles = files.length;
var uploadedFiles = 0;
function uploadNextFile(index) {
if (index >= totalFiles) {
self.loadFileList(currentPath).then(function() {
self.initResizableColumns();
});
return;
}
var file = files[index];
var fullFilePath = joinPath(directoryPath, file.name);
if (statusInfo) {
statusInfo.textContent = _('Uploading: "%s"...').format(file.name);
}
if (statusProgress) {
statusProgress.innerHTML = '';
var progressBarContainer = E('div', {
'class': 'cbi-progressbar',
'title': '0%'
}, [E('div', {
'style': 'width:0%'
})]);
statusProgress.appendChild(progressBarContainer);
}
uploadFile(fullFilePath, file, function(percent) {
if (statusProgress) {
var progressBar = statusProgress.querySelector('.cbi-progressbar div');
if (progressBar) {
progressBar.style.width = percent.toFixed(2) + '%';
statusProgress.querySelector('.cbi-progressbar').setAttribute('title', percent.toFixed(2) + '%');
}
}
}).then(function() {
if (statusProgress) {
statusProgress.innerHTML = '';
}
if (statusInfo) {
statusInfo.textContent = _('File "%s" uploaded successfully.').format(file.name);
}
pop(null, E('p', _('File "%s" uploaded successfully.').format(file.name)), 5000, 'info');
uploadedFiles++;
uploadNextFile(index + 1);
}).catch(function(err) {
if (statusProgress) {
statusProgress.innerHTML = '';
}
if (statusInfo) {
statusInfo.textContent = _('Upload failed for file "%s": %s').format(file.name, err.message);
}
pop(null, E('p', _('Upload failed for file "%s": %s').format(file.name, err.message)), 'error');
uploadNextFile(index + 1);
});
}
uploadNextFile(0);
},
// Handler for creating a directory
handleMakeDirectoryClick: function(ev) {
// Logic to create a new directory
var self = this;
var statusInfo = document.getElementById('status-info');
var statusProgress = document.getElementById('status-progress');
if (statusInfo && statusProgress) {
statusInfo.innerHTML = '';
statusProgress.innerHTML = '';
var dirNameInput = E('input', {
'type': 'text',
'placeholder': _('Directory Name'),
'style': 'margin-right: 10px;'
});
var saveButton = E('button', {
'class': 'btn',
'disabled': true,
'click': function() {
self.createDirectory(dirNameInput.value);
}
}, _('Save'));
dirNameInput.addEventListener('input', function() {
if (dirNameInput.value.trim()) {
saveButton.disabled = false;
} else {
saveButton.disabled = true;
}
});
statusInfo.appendChild(E('span', {}, _('Create Directory: ')));
statusInfo.appendChild(dirNameInput);
statusProgress.appendChild(saveButton);
}
},
// Function to create a directory
createDirectory: function(dirName) {
// Execute the 'mkdir' command and update the interface
var self = this;
var trimmedDirName = dirName.trim();
var dirPath = joinPath(currentPath, trimmedDirName);
fs.exec('mkdir', [dirPath]).then(function(res) {
if (res.code !== 0) {
return Promise.reject(new Error(res.stderr.trim()));
}
pop(null, E('p', _('Directory "%s" created successfully.').format(trimmedDirName)), 5000, 'info');
self.loadFileList(currentPath).then(function() {
self.initResizableColumns();
});
var statusInfo = document.getElementById('status-info');
var statusProgress = document.getElementById('status-progress');
if (statusInfo) statusInfo.textContent = _('No directory selected.');
if (statusProgress) statusProgress.innerHTML = '';
}).catch(function(err) {
pop(null, E('p', _('Failed to create directory "%s": %s').format(trimmedDirName, err.message)), 'error');
});
},
// Handler for creating a file
handleCreateFileClick: function(ev) {
// Logic to create a new file
var self = this;
var statusInfo = document.getElementById('status-info');
var statusProgress = document.getElementById('status-progress');
if (statusInfo && statusProgress) {
statusInfo.innerHTML = '';
statusProgress.innerHTML = '';
var fileNameInput = E('input', {
'type': 'text',
'placeholder': _('File Name'),
'style': 'margin-right: 10px;'
});
var createButton = E('button', {
'class': 'btn',
'disabled': true,
'click': function() {
self.createFile(fileNameInput.value);
}
}, _('Create'));
fileNameInput.addEventListener('input', function() {
if (fileNameInput.value.trim()) {
createButton.disabled = false;
} else {
createButton.disabled = true;
}
});
statusInfo.appendChild(E('span', {}, _('Create File: ')));
statusInfo.appendChild(fileNameInput);
statusProgress.appendChild(createButton);
}
},
// Function to create a file
createFile: function(fileName) {
// Execute the 'touch' command and update the interface
var self = this;
var trimmedFileName = fileName.trim();
var filePath = joinPath(currentPath, trimmedFileName);
fs.exec('touch', [filePath]).then(function(res) {
if (res.code !== 0) {
return Promise.reject(new Error(res.stderr.trim()));
}
pop(null, E('p', _('File "%s" created successfully.').format(trimmedFileName)), 5000, 'info');
self.loadFileList(currentPath).then(function() {
self.initResizableColumns();
});
var statusInfo = document.getElementById('status-info');
var statusProgress = document.getElementById('status-progress');
if (statusInfo) statusInfo.textContent = _('No file selected.');
if (statusProgress) statusProgress.innerHTML = '';
}).catch(function(err) {
pop(null, E('p', _('Failed to create file "%s": %s').format(trimmedFileName, err.message)), 'error');
});
},
// Handler for checkbox state change on a file
handleCheckboxChange: function(ev) {
// Update the set of selected items
var checkbox = ev.target;
var filePath = checkbox.getAttribute('data-file-path');
if (checkbox.checked) {
selectedItems.add(filePath);
} else {
selectedItems.delete(filePath);
}
this.updateDeleteSelectedButton();
this.updateSelectAllCheckbox();
},
// Update the "Delete Selected" button
updateDeleteSelectedButton: function() {
// Show or hide the button based on the number of selected items
var deleteSelectedButton = document.getElementById('delete-selected-button');
if (deleteSelectedButton) {
if (selectedItems.size > 0) {
deleteSelectedButton.style.display = '';
} else {
deleteSelectedButton.style.display = 'none';
}
}
},
// Update the "Select All" checkbox state
updateSelectAllCheckbox: function() {
var selectAllCheckbox = document.getElementById('select-all-checkbox');
var allCheckboxes = document.querySelectorAll('.select-checkbox');
var totalCheckboxes = allCheckboxes.length;
var checkedCheckboxes = 0;
allCheckboxes.forEach(function(checkbox) {
if (checkbox.checked) {
checkedCheckboxes++;
}
});
if (selectAllCheckbox) {
if (checkedCheckboxes === 0) {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = false;
} else if (checkedCheckboxes === totalCheckboxes) {
selectAllCheckbox.checked = true;
selectAllCheckbox.indeterminate = false;
} else {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = true;
}
}
},
// Handler for the "Select All" checkbox change
handleSelectAllChange: function(ev) {
// Logic to select or deselect all files
var self = this;
var selectAllCheckbox = ev.target;
var allCheckboxes = document.querySelectorAll('.select-checkbox');
selectedItems.clear();
allCheckboxes.forEach(function(checkbox) {
checkbox.checked = selectAllCheckbox.checked;
var filePath = checkbox.getAttribute('data-file-path');
if (selectAllCheckbox.checked) {
selectedItems.add(filePath);
}
});
this.updateDeleteSelectedButton();
},
// Handler for deleting selected items
handleDeleteSelected: function() {
// Delete selected files and directories
var self = this;
if (selectedItems.size === 0) {
return;
}
if (!confirm(_('Are you sure you want to delete the selected files and directories?'))) {
return;
}
var promises = [];
selectedItems.forEach(function(filePath) {
promises.push(fs.remove(filePath).catch(function(err) {
pop(null, E('p', _('Failed to delete %s: %s').format(filePath, err.message)), 'error');
}));
});
Promise.all(promises).then(function() {
pop(null, E('p', _('Selected files and directories deleted successfully.')), 5000, 'info');
selectedItems.clear();
self.updateDeleteSelectedButton();
self.loadFileList(currentPath).then(function() {
self.initResizableColumns();
});
}).catch(function(err) {
pop(null, E('p', _('Failed to delete selected files and directories: %s').format(err.message)), 'error');
});
},
// Function to load the file list
loadFileList: function(path) {
// Get the list of files and display them in the table
var self = this;
selectedItems.clear();
return getFileList(path).then(function(files) {
var fileList = document.getElementById('file-list');
if (!fileList) {
pop(null, E('p', _('Failed to display the file list.')), 'error');
return;
}
fileList.innerHTML = '';
files.sort(self.compareFiles.bind(self));
if (path !== '/') {
var parentPath = path.substring(0, path.lastIndexOf('/')) || '/';
var listItemUp = E('tr', {
'data-file-path': parentPath,
'data-file-type': 'directory'
}, [E('td', {
'colspan': 5
}, [E('a', {
'href': '#',
'click': function() {
self.handleDirectoryClick(parentPath);
}
}, '.. (Parent Directory)')])]);
fileList.appendChild(listItemUp);
}
files.forEach(function(file) {
var listItem;
var displaySize = (file.type === 'directory' || (file.type === 'symlink' && file.size === -1)) ? -1 : file.size;
var checkbox = E('input', {
'type': 'checkbox',
'class': 'select-checkbox',
'data-file-path': joinPath(path, file.name),
'change': function(ev) {
self.handleCheckboxChange(ev);
}
});
var actionButtons = [checkbox, E('span', {
'class': 'edit-button',
'click': function() {
self.handleEditFile(joinPath(path, file.name), file);
}
}, '✏️'), E('span', {
'class': 'duplicate-button',
'click': function() {
self.handleDuplicateFile(joinPath(path, file.name), file);
}
}, '📑'), E('span', {
'class': 'delete-button',
'click': function() {
self.handleDeleteFile(joinPath(path, file.name), file);
}
}, '🗑️')];
if (file.type === 'file') {
actionButtons.push(E('span', {
'class': 'download-button',
'click': function() {
self.handleDownloadFile(joinPath(path, file.name));
}
}, '⬇️'));
}
var actionTd = E('td', {}, actionButtons);
if (file.type === 'directory') {
listItem = E('tr', {
'data-file-path': joinPath(path, file.name),
'data-file-type': 'directory',
'data-permissions': file.permissions,
'data-numeric-permissions': file.numericPermissions,
'data-owner': file.owner,
'data-group': file.group,
'data-size': -1
}, [E('td', {}, [E('a', {
'href': '#',
'class': 'directory-link',
'click': function() {
self.handleDirectoryClick(joinPath(path, file.name));
}
}, file.name)]), E('td', {}, _('Directory')), E('td', {
'class': 'size-cell'
}, [E('span', {
'class': 'size-number'
}, '-'), E('span', {
'class': 'size-unit'
}, '')]), E('td', {}, new Date(file.mtime * 1000).toLocaleString()), actionTd]);
} else if (file.type === 'file') {
listItem = E('tr', {
'data-file-path': joinPath(path, file.name),
'data-file-type': 'file',
'data-permissions': file.permissions,
'data-numeric-permissions': file.numericPermissions,
'data-owner': file.owner,
'data-group': file.group,
'data-size': file.size
}, [E('td', {}, [E('a', {
'href': '#',
'class': 'file-link',
'click': function() {
event.preventDefault(); // Prevent the default link behavior
if (event.altKey) {
self.handleFileClick(joinPath(path, file.name), 'hex'); // Open in hex editor
} else {
self.handleFileClick(joinPath(path, file.name), 'text'); // Open in text editor
}
}
}, file.name)]), E('td', {}, _('File')), E('td', {
'class': 'size-cell'
}, [E('span', {
'class': 'size-number'
}, self.getFormattedSize(file.size).number), E('span', {
'class': 'size-unit'
}, self.getFormattedSize(file.size).unit)]), E('td', {}, new Date(file.mtime * 1000).toLocaleString()), actionTd]);
} else if (file.type === 'symlink') {
var symlinkName = file.name + ' -> ' + file.target;
var symlinkSize = (file.size === -1) ? -1 : file.size;
var sizeContent;
if (symlinkSize >= 0) {
var formattedSize = self.getFormattedSize(symlinkSize);
sizeContent = [E('span', {
'class': 'size-number'
}, formattedSize.number), E('span', {
'class': 'size-unit'
}, formattedSize.unit)];
} else {
sizeContent = [E('span', {
'class': 'size-number'
}, '-'), E('span', {
'class': 'size-unit'
}, '')];
}
listItem = E('tr', {
'data-file-path': joinPath(path, file.name),
'data-file-type': 'symlink',
'data-symlink-target': file.target,
'data-permissions': file.permissions,
'data-numeric-permissions': file.numericPermissions,
'data-owner': file.owner,
'data-group': file.group,
'data-size': symlinkSize
}, [E('td', {}, [E('a', {
'href': '#',
'class': 'symlink-name',
'click': function() {
event.preventDefault(); // Prevent the default link behavior
if (event.altKey) {
self.handleSymlinkClick(joinPath(path, file.name), file.target, 'hex'); // Open target in hex editor
} else {
self.handleSymlinkClick(joinPath(path, file.name), file.target, 'text');
}
}
}, symlinkName)]), E('td', {}, _('Symlink')), E('td', {
'class': 'size-cell'
}, sizeContent), E('td', {}, new Date(file.mtime * 1000).toLocaleString()), actionTd]);
} else {
listItem = E('tr', {
'data-file-path': joinPath(path, file.name),
'data-file-type': 'unknown'
}, [E('td', {}, file.name), E('td', {}, _('Unknown')), E('td', {
'class': 'size-cell'
}, [E('span', {
'class': 'size-number'
}, '-'), E('span', {
'class': 'size-unit'
}, '')]), E('td', {}, '-'), E('td', {}, '-')]);
}
if (listItem && listItem instanceof Node) {
fileList.appendChild(listItem);
} else {
console.error('listItem is not a Node:', listItem);
}
});
self.setInitialColumnWidths();
var statusInfo = document.getElementById('status-info');
var statusProgress = document.getElementById('status-progress');
if (statusInfo) {
statusInfo.textContent = _('No file selected.');
}
if (statusProgress) {
statusProgress.innerHTML = '';
}
self.updateSelectAllCheckbox();
self.updateDeleteSelectedButton();
return Promise.resolve();
}).catch(function(err) {
pop(null, E('p', _('Failed to load file list: %s').format(err.message)), 'error');
return Promise.reject(err);
});
},
// Function to format file size
getFormattedSize: function(size) {
// Convert the size to a human-readable format (KB, MB, GB)
var units = [' ', 'k', 'M', 'G'];
var unitIndex = 0;
var formattedSize = size;
while (formattedSize >= 1024 && unitIndex < units.length - 1) {
formattedSize /= 1024;
unitIndex++;
}
formattedSize = formattedSize.toFixed(2);
if (size === 0) {
formattedSize = '0.00';
unitIndex = 0;
}
formattedSize = formattedSize.toString().padStart(6, ' ');
return {
number: formattedSize,
unit: ' ' + units[unitIndex] + 'B'
};
},
// Function to sort files
sortBy: function(field) {
// Change the sort field and direction, and reload the file list
if (sortField === field) {
sortDirection = (sortDirection === 'asc') ? 'desc' : 'asc';
} else {
sortField = field;
sortDirection = 'asc';
}
this.loadFileList(currentPath);
},
// Function to compare files for sorting
compareFiles: function(a, b) {
// Compare files based on the selected field and direction
var order = (sortDirection === 'asc') ? 1 : -1;
var aValue = a[sortField];
var bValue = b[sortField];
if (sortField === 'size') {
aValue = (a.type === 'directory' || (a.type === 'symlink' && a.size === -1)) ? -1 : a.size;
bValue = (b.type === 'directory' || (b.type === 'symlink' && b.size === -1)) ? -1 : b.size;
}
if (aValue < bValue) return -1 * order;
if (aValue > bValue) return 1 * order;
return 0;
},
// Set initial column widths in the table
setInitialColumnWidths: function() {
// Apply column width settings to the file table
var table = document.getElementById('file-table');
if (!table) {
return;
}
var headers = table.querySelectorAll('th');
headers.forEach(function(header, index) {
var field = header.getAttribute('data-field');
if (field && config.columnWidths[field]) {
var width = config.columnWidths[field];
var minWidth = config.columnMinWidths[field] || 50;
var maxWidth = config.columnMaxWidths[field] || 500;
header.style.width = width + 'px';
header.style.minWidth = minWidth + 'px';
header.style.maxWidth = maxWidth + 'px';
var rows = table.querySelectorAll('tr');
rows.forEach(function(row, rowIndex) {
var cell = row.children[index];
if (cell) {
cell.style.width = width + 'px';
cell.style.minWidth = minWidth + 'px';
cell.style.maxWidth = maxWidth + 'px';
}
});
}
});
},
// Handler for clicking on a directory
handleDirectoryClick: function(newPath) {
// Navigate to the selected directory and update the file list
var self = this;
currentPath = newPath || '/';
var pathInput = document.getElementById('path-input');
if (pathInput) {
pathInput.value = currentPath;
}
this.loadFileList(currentPath).then(function() {
self.initResizableColumns();
});
},
/**
* Determines whether a given Uint8Array represents UTF-8 text data.
*
* @param {Uint8Array} uint8Array - The binary data to check.
* @returns {boolean} - Returns true if the data is UTF-8 text, false otherwise.
*/
isText: function(uint8Array) {
const len = uint8Array.length;
let i = 0;
while (i < len) {
const byte = uint8Array[i];
if (byte === 0) return false; // Null byte indicates binary
if (byte <= 0x7F) {
// ASCII character, no action needed
i++;
continue;
} else if ((byte & 0xE0) === 0xC0) {
// 2-byte sequence
if (i + 1 >= len || (uint8Array[i + 1] & 0xC0) !== 0x80) return false;
i += 2;
} else if ((byte & 0xF0) === 0xE0) {
// 3-byte sequence
if (
i + 2 >= len ||
(uint8Array[i + 1] & 0xC0) !== 0x80 ||
(uint8Array[i + 2] & 0xC0) !== 0x80
) {
return false;
}
i += 3;
} else if ((byte & 0xF8) === 0xF0) {
// 4-byte sequence
if (
i + 3 >= len ||
(uint8Array[i + 1] & 0xC0) !== 0x80 ||
(uint8Array[i + 2] & 0xC0) !== 0x80 ||
(uint8Array[i + 3] & 0xC0) !== 0x80
) {
return false;
}
i += 4;
} else {
// Invalid UTF-8 byte
return false;
}
}
return true;
},
// Function to handle clicking on a file to open it in the editor
handleFileClick: function(filePath, mode) {
const self = this;
const fileRow = document.querySelector(`tr[data-file-path='${filePath}']`);
const editorMessage = document.getElementById('editor-message');
// Set original file permissions
self.originalFilePermissions = fileRow ? fileRow.getAttribute('data-numeric-permissions') : '644';
self.editorMode = mode;
// Display loading message
if (editorMessage) editorMessage.textContent = _('Loading file...');
// Read the file as binary data
fs.read_direct(filePath, 'blob')
.then(blob => blob.arrayBuffer())
.then(arrayBuffer => {
const uint8Array = new Uint8Array(arrayBuffer);
self.fileData = uint8Array;
self.fileContent = ''; // Can be used for display or left empty
self.editorMode = 'hex';
self.textType = self.isText(uint8Array) ? 'text' : 'hex';
if (mode === 'text') {
// Determine if the file is text
if (self.textType === 'text') {
// If text, decode the content
self.fileContent = new TextDecoder().decode(uint8Array);
self.editorMode = 'text';
} else {
// If not text, show a warning and set mode to hex
if (editorMessage) {
editorMessage.textContent = _('The file does not contain valid text data. Opening in hex mode...');
}
pop(null, E('p', _('Opening file in hex mode since it is not a text file.')), 'warning');
}
}
})
.then(() => {
// Render the editor and switch to the editor tab
self.renderEditor(filePath);
self.switchToTab('editor');
})
.catch(err => {
// Handle errors during file reading
pop(null, E('p', _('Failed to open file: %s').format(err.message)), 'error');
});
},
// Adjust padding for line numbers in the editor
adjustLineNumbersPadding: function() {
// Update padding based on scrollbar size
var lineNumbersDiv = document.getElementById('line-numbers');
var editorTextarea = document.getElementById('editor-textarea');
if (!lineNumbersDiv || !editorTextarea) {
return;
}
var scrollbarHeight = editorTextarea.offsetHeight - editorTextarea.clientHeight;
lineNumbersDiv.style.paddingBottom = scrollbarHeight + 'px';
},
// Handler for downloading a file
handleDownloadFile: function(filePath) {
// Download the file to the user's local machine
var self = this;
var fileName = filePath.split('/').pop();
// Use the read_direct method to download the file
fs.read_direct(filePath, 'blob')
.then(function(blob) {
if (!(blob instanceof Blob)) {
throw new Error(_('Response is not a Blob'));
}
var url = window.URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
}).catch(function(err) {
pop(null, E('p', _('Failed to download file "%s": %s').format(fileName, err.message)), 'error');
});
},
// Handler for deleting a file
handleDeleteFile: function(filePath, fileInfo) {
// Delete the selected file or directory
var self = this;
var itemTypeLabel = '';
var itemName = filePath.split('/').pop();
if (fileInfo && fileInfo.type) {
if (fileInfo.type === 'directory') {
itemTypeLabel = _('directory');
} else if (fileInfo.type === 'file') {
itemTypeLabel = _('file');
} else if (fileInfo.type === 'symlink') {
itemTypeLabel = _('symbolic link');
} else {
itemTypeLabel = _('item');
}
} else {
itemTypeLabel = _('item');
}
if (confirm(_('Are you sure you want to delete this %s: "%s"?').format(itemTypeLabel, itemName))) {
fs.remove(filePath).then(function() {
pop(null, E('p', _('Successfully deleted %s: "%s".').format(itemTypeLabel, itemName)), 5000, 'info');
self.loadFileList(currentPath).then(function() {
self.initResizableColumns();
});
var statusInfo = document.getElementById('status-info');
if (statusInfo) {
statusInfo.textContent = _('Deleted %s: "%s".').format(itemTypeLabel, itemName);
}
}).catch(function(err) {
pop(null, E('p', _('Failed to delete %s "%s": %s').format(itemTypeLabel, itemName, err.message)), 'error');
});
}
},
// Update line numbers in the text editor
updateLineNumbers: function() {
// Update the line numbers display when the text changes
var lineNumbersDiv = document.getElementById('line-numbers');
var editorTextarea = document.getElementById('editor-textarea');
if (!lineNumbersDiv || !editorTextarea) {
return;
}
var content = editorTextarea.value;
var lines = content.split('\n').length;
var lineNumbersContent = '';
for (var i = 1; i <= lines; i++) {
lineNumbersContent += '<div>' + i + '</div>';
}
lineNumbersDiv.innerHTML = lineNumbersContent;
},
// Synchronize scrolling between line numbers and text
syncScroll: function() {
// Sync scrolling of line numbers with the text area
var lineNumbersDiv = document.getElementById('line-numbers');
var editorTextarea = document.getElementById('editor-textarea');
if (!lineNumbersDiv || !editorTextarea) {
return;
}
lineNumbersDiv.scrollTop = editorTextarea.scrollTop;
},
// Toggle line numbers display in the editor
toggleLineNumbers: function() {
// Ensure the editor is in Text Mode before toggling line numbers
if (this.editorMode !== 'text') {
console.warn('Toggle Line Numbers is only available in Text Mode.');
return;
}
// Get the line numbers div and the textarea
var lineNumbersDiv = document.getElementById('line-numbers');
var editorTextarea = document.getElementById('editor-textarea');
if (!lineNumbersDiv || !editorTextarea) {
console.error('Line numbers div or editor textarea not found.');
return;
}
// Toggle the display of line numbers
if (lineNumbersDiv.style.display === 'none' || !lineNumbersDiv.style.display) {
lineNumbersDiv.style.display = 'block';
this.updateLineNumbers();
this.adjustLineNumbersPadding();
this.syncScroll();
} else {
lineNumbersDiv.style.display = 'none';
lineNumbersDiv.innerHTML = '';
}
},
// Generate a name for a copy of a file
getCopyName: function(originalName, existingNames) {
// Create a new unique file name based on the original
var dotIndex = originalName.lastIndexOf('.');
var namePart, extension;
if (dotIndex > 0 && dotIndex !== originalName.length - 1) {
namePart = originalName.substring(0, dotIndex);
extension = originalName.substring(dotIndex);
} else {
namePart = originalName;
extension = '';
}
var copyName = namePart + ' (copy)' + extension;
var copyIndex = 1;
while (existingNames.includes(copyName)) {
copyIndex++;
copyName = namePart + ' (copy ' + copyIndex + ')' + extension;
}
return copyName;
},
// Handler for duplicating a file
handleDuplicateFile: function(filePath, fileInfo) {
// Copy the file or directory with a new name
var self = this;
getFileList(currentPath).then(function(files) {
var existingNames = files.map(function(f) {
return f.name;
});
var newName = self.getCopyName(fileInfo.name, existingNames);
var newPath = joinPath(currentPath, newName);
var command;
var args;
if (fileInfo.type === 'directory') {
command = 'cp';
args = ['-rp', filePath, newPath];
} else if (fileInfo.type === 'symlink') {
command = 'cp';
args = ['-Pp', filePath, newPath];
} else {
command = 'cp';
args = ['-p', filePath, newPath];
}
fs.exec(command, args).then(function(res) {
if (res.code !== 0) {
return Promise.reject(new Error(res.stderr.trim()));
}
pop(null, E('p', _('Successfully duplicated %s "%s" as "%s".').format(_('item'), fileInfo.name, newName)), 5000, 'info');
self.loadFileList(currentPath).then(function() {
self.initResizableColumns();
});
}).catch(function(err) {
pop(null, E('p', _('Failed to duplicate %s "%s": %s').format(_('item'), fileInfo.name, err.message)), 'error');
});
}).catch(function(err) {
pop(null, E('p', _('Failed to get file list: %s').format(err.message)), 'error');
});
},
// Handler for saving a file after editing
handleSaveFile: function(filePath) {
var self = this;
var contentBlob;
if (self.editorMode === 'text') {
var textarea = document.querySelector('#editor-container textarea');
if (!textarea) {
pop(null, E('p', _('Editor textarea not found.')), 'error');
return;
}
var content = textarea.value;
self.fileContent = content;
// Convert content to Uint8Array in chunks not exceeding 8KB
var CHUNK_SIZE = 8 * 1024; // 8KB
var totalLength = content.length;
var chunks = [];
for (var i = 0; i < totalLength; i += CHUNK_SIZE) {
var chunkStr = content.slice(i, i + CHUNK_SIZE);
var chunkBytes = new TextEncoder().encode(chunkStr);
chunks.push(chunkBytes);
}
// Concatenate chunks into a single Uint8Array
var totalBytes = chunks.reduce(function(prev, curr) {
return prev + curr.length;
}, 0);
var dataArray = new Uint8Array(totalBytes);
var offset = 0;
chunks.forEach(function(chunk) {
dataArray.set(chunk, offset);
offset += chunk.length;
});
self.fileData = dataArray; // Update binary data
contentBlob = new Blob([self.fileData], {
type: 'application/octet-stream'
});
} else if (self.editorMode === 'hex') {
// Get data from hex editor
self.fileData = self.hexEditorInstance.getData(); // Assuming getData method is implemented in HexEditor
contentBlob = new Blob([self.fileData], {
type: 'application/octet-stream'
});
}
var statusInfo = document.getElementById('status-info');
var statusProgress = document.getElementById('status-progress');
var fileName = filePath.split('/').pop();
if (statusInfo) {
statusInfo.textContent = _('Saving file: "%s"...').format(fileName);
}
if (statusProgress) {
statusProgress.innerHTML = '';
var progressBarContainer = E('div', {
'class': 'cbi-progressbar',
'title': '0%'
}, [E('div', {
'style': 'width:0%'
})]);
statusProgress.appendChild(progressBarContainer);
}
uploadFile(filePath, contentBlob, function(percent) {
if (statusProgress) {
var progressBar = statusProgress.querySelector('.cbi-progressbar div');
if (progressBar) {
progressBar.style.width = percent.toFixed(2) + '%';
statusProgress.querySelector('.cbi-progressbar').setAttribute('title', percent.toFixed(2) + '%');
}
}
}).then(function() {
var permissions = self.originalFilePermissions;
if (permissions !== undefined) {
return fs.exec('chmod', [permissions, filePath]).then(function(res) {
if (res.code !== 0) {
throw new Error(res.stderr.trim());
}
}).then(function() {
if (statusInfo) {
statusInfo.textContent = _('File "%s" uploaded successfully.').format(fileName);
}
pop(null, E('p', _('File "%s" uploaded successfully.').format(fileName)), 5000, 'info');
return self.loadFileList(currentPath).then(function() {
self.initResizableColumns();
});
}).catch(function(err) {
pop(null, E('p', _('Failed to apply permissions to file "%s": %s').format(fileName, err.message)), 'error');
});
} else {
if (statusInfo) {
statusInfo.textContent = _('File "%s" uploaded successfully.').format(fileName);
}
pop(null, E('p', _('File "%s" uploaded successfully.').format(fileName)), 5000, 'info');
return self.loadFileList(currentPath).then(function() {
self.initResizableColumns();
});
}
}).catch(function(err) {
if (statusProgress) {
statusProgress.innerHTML = '';
}
if (statusInfo) {
statusInfo.textContent = _('Failed to save file "%s": %s').format(fileName, err.message);
}
pop(null, E('p', _('Failed to save file "%s": %s').format(fileName, err.message)), 'error');
});
},
// Handler for clicking on a symbolic link
handleSymlinkClick: function(linkPath, targetPath, mode) {
// Navigate to the target of the symbolic link
var self = this;
if (!targetPath.startsWith('/')) {
targetPath = joinPath(currentPath, targetPath);
}
fs.stat(targetPath).then(function(stat) {
if (stat.type === 'directory') {
self.handleDirectoryClick(targetPath);
} else if (stat.type === 'file') {
self.handleFileClick(targetPath, mode);
} else {
pop(null, E('p', _('The symlink points to an unsupported type.')), 'error');
}
}).catch(function(err) {
pop(null, E('p', _('Failed to access symlink target: %s').format(err.message)), 'error');
});
var statusInfo = document.getElementById('status-info');
if (statusInfo) {
statusInfo.textContent = _('Symlink: ') + linkPath + ' -> ' + targetPath;
}
},
// Initialize resizable columns in the table
initResizableColumns: function() {
// Add handlers to adjust column widths
var self = this;
var table = document.getElementById('file-table');
if (!table) {
return;
}
var headers = table.querySelectorAll('th');
headers.forEach(function(header, index) {
var resizer = header.querySelector('.resizer');
if (resizer) {
resizer.removeEventListener('mousedown', header.resizeHandler);
header.resizeHandler = function(e) {
e.preventDefault();
var startX = e.pageX;
var startWidth = header.offsetWidth;
var field = header.getAttribute('data-field');
var minWidth = config.columnMinWidths[field] || 50;
var maxWidth = config.columnMaxWidths[field] || 500;
function doDrag(e) {
var currentX = e.pageX;
var newWidth = startWidth + (currentX - startX);
if (newWidth >= minWidth && newWidth <= maxWidth) {
header.style.width = newWidth + 'px';
if (field) {
config.columnWidths[field] = newWidth;
}
var rows = table.querySelectorAll('tr');
rows.forEach(function(row, rowIndex) {
var cell = row.children[index];
if (cell) {
cell.style.width = newWidth + 'px';
}
});
}
}
function stopDrag() {
document.removeEventListener('mousemove', doDrag, false);
document.removeEventListener('mouseup', stopDrag, false);
saveConfig();
}
document.addEventListener('mousemove', doDrag, false);
document.addEventListener('mouseup', stopDrag, false);
};
resizer.addEventListener('mousedown', header.resizeHandler, false);
}
});
},
// Handler for editing a file's properties (name, permissions, etc.)
handleEditFile: function(filePath, fileInfo) {
// Display a form to edit the file's properties
var self = this;
var statusInfo = document.getElementById('status-info');
var statusProgress = document.getElementById('status-progress');
if (statusInfo && statusProgress) {
statusInfo.innerHTML = '';
statusProgress.innerHTML = '';
var nameInput = E('input', {
'type': 'text',
'value': fileInfo.name,
'placeholder': fileInfo.name,
'style': 'margin-right: 10px;'
});
var permsInput = E('input', {
'type': 'text',
'placeholder': fileInfo.numericPermissions,
'style': 'margin-right: 10px; width: 80px;'
});
var ownerInput = E('input', {
'type': 'text',
'placeholder': fileInfo.owner,
'style': 'margin-right: 10px; width: 100px;'
});
var groupInput = E('input', {
'type': 'text',
'placeholder': fileInfo.group,
'style': 'margin-right: 10px; width: 100px;'
});
var saveButton = E('button', {
'class': 'btn',
'disabled': true,
'click': function() {
self.saveFileChanges(filePath, fileInfo, nameInput.value, permsInput.value, ownerInput.value, groupInput.value);
}
}, _('Save'));
[nameInput, permsInput, ownerInput, groupInput].forEach(function(input) {
input.addEventListener('input', function() {
if (nameInput.value !== fileInfo.name || permsInput.value || ownerInput.value || groupInput.value) {
saveButton.disabled = false;
} else {
saveButton.disabled = true;
}
});
});
statusInfo.appendChild(E('span', {}, _('Editing %s: "%s"').format(_('item'), fileInfo.name)));
statusInfo.appendChild(nameInput);
statusInfo.appendChild(permsInput);
statusInfo.appendChild(ownerInput);
statusInfo.appendChild(groupInput);
statusProgress.appendChild(saveButton);
}
},
// Save changes to a file's properties
saveFileChanges: function(filePath, fileInfo, newName, newPerms, newOwner, newGroup) {
// Apply changes and update the interface
var self = this;
var commands = [];
var originalPath = filePath;
var originalName = fileInfo.name;
var newItemName = newName || originalName;
if (newName && newName !== fileInfo.name) {
var newPath = joinPath(currentPath, newName);
commands.push(['mv', [filePath, newPath]]);
filePath = newPath;
}
if (newPerms) {
commands.push(['chmod', [newPerms, filePath]]);
}
if (newOwner || newGroup) {
var ownerGroup = '';
if (newOwner) {
ownerGroup += newOwner;
} else {
ownerGroup += fileInfo.owner;
}
ownerGroup += ':';
if (newGroup) {
ownerGroup += newGroup;
} else {
ownerGroup += fileInfo.group;
}
commands.push(['chown', [ownerGroup, filePath]]);
}
var promise = Promise.resolve();
commands.forEach(function(cmd) {
promise = promise.then(function() {
return fs.exec(cmd[0], cmd[1]).then(function(res) {
if (res.code !== 0) {
return Promise.reject(new Error(res.stderr.trim()));
}
});
});
});
promise.then(function() {
pop(null, E('p', _('Changes to %s "%s" uploaded successfully.').format(_('item'), newItemName)), 5000, 'info');
self.loadFileList(currentPath).then(function() {
self.initResizableColumns();
});
var statusInfo = document.getElementById('status-info');
var statusProgress = document.getElementById('status-progress');
if (statusInfo) statusInfo.textContent = _('No item selected.');
if (statusProgress) statusProgress.innerHTML = '';
}).catch(function(err) {
pop(null, E('p', _('Failed to save changes to %s "%s": %s').format(_('item'), newItemName, err.message)), 'error');
});
},
// Handler for saving interface settings
handleSaveSettings: function(ev) {
ev.preventDefault();
var self = this;
var inputs = {
columnWidths: document.getElementById('column-widths-input'),
columnMinWidths: document.getElementById('column-min-widths-input'),
columnMaxWidths: document.getElementById('column-max-widths-input'),
padding: document.getElementById('padding-input'),
paddingMin: document.getElementById('padding-min-input'),
paddingMax: document.getElementById('padding-max-input'),
currentDirectory: document.getElementById('current-directory-input'),
windowWidth: document.getElementById('window-width-input'),
windowHeight: document.getElementById('window-height-input'),
editorTextWidth: document.getElementById('editor-text-width-input'),
editorTextHeight: document.getElementById('editor-text-height-input'),
editorHexWidth: document.getElementById('editor-hex-width-input'),
editorHexHeight: document.getElementById('editor-hex-height-input')
};
function parseWidthSettings(inputValue, configKey) {
if (!inputValue) return;
inputValue.split(',').forEach(function(widthStr) {
var widthParts = widthStr.split(':');
if (widthParts.length === 2) {
var field = widthParts[0];
var width = parseInt(widthParts[1], 10);
if (!isNaN(width)) {
config[configKey][field] = width;
}
}
});
}
if (inputs.columnWidths && inputs.padding) {
parseWidthSettings(inputs.columnWidths.value.trim(), 'columnWidths');
parseWidthSettings(inputs.columnMinWidths.value.trim(), 'columnMinWidths');
parseWidthSettings(inputs.columnMaxWidths.value.trim(), 'columnMaxWidths');
var paddingValue = parseInt(inputs.padding.value.trim(), 10);
var paddingMinValue = parseInt(inputs.paddingMin.value.trim(), 10);
var paddingMaxValue = parseInt(inputs.paddingMax.value.trim(), 10);
if (!isNaN(paddingValue)) {
config.padding = paddingValue;
}
if (!isNaN(paddingMinValue)) {
config.paddingMin = paddingMinValue;
}
if (!isNaN(paddingMaxValue)) {
config.paddingMax = paddingMaxValue;
}
if (inputs.currentDirectory) {
var currentDirectoryValue = inputs.currentDirectory.value.trim();
if (currentDirectoryValue) {
config.currentDirectory = currentDirectoryValue;
}
}
if (inputs.windowWidth && inputs.windowHeight) {
var windowWidthValue = parseInt(inputs.windowWidth.value.trim(), 10);
var windowHeightValue = parseInt(inputs.windowHeight.value.trim(), 10);
if (!isNaN(windowWidthValue)) {
config.windowSizes.width = windowWidthValue;
}
if (!isNaN(windowHeightValue)) {
config.windowSizes.height = windowHeightValue;
}
}
if (inputs.editorTextWidth && inputs.editorTextHeight) {
var textWidth = parseInt(inputs.editorTextWidth.value.trim(), 10);
var textHeight = parseInt(inputs.editorTextHeight.value.trim(), 10);
if (!isNaN(textWidth) && !isNaN(textHeight)) {
config.editorContainerSizes.text.width = textWidth;
config.editorContainerSizes.text.height = textHeight;
}
}
if (inputs.editorHexWidth && inputs.editorHexHeight) {
var hexWidth = parseInt(inputs.editorHexWidth.value.trim(), 10);
var hexHeight = parseInt(inputs.editorHexHeight.value.trim(), 10);
if (!isNaN(hexWidth) && !isNaN(hexHeight)) {
config.editorContainerSizes.hex.width = hexWidth;
config.editorContainerSizes.hex.height = hexHeight;
}
}
saveConfig().then(function() {
pop(null, E('p', _('Settings uploaded successfully.')), 5000, 'info');
self.setInitialColumnWidths();
var styleElement = document.querySelector('style');
if (styleElement) {
styleElement.textContent = styleElement.textContent.replace(/padding: \d+px/g, 'padding: ' + config.padding + 'px');
}
var fileListContainer = document.getElementById('file-list-container');
if (fileListContainer) {
fileListContainer.style.width = config.windowSizes.width + 'px';
fileListContainer.style.height = config.windowSizes.height + 'px';
}
currentPath = config.currentDirectory || '/';
var pathInput = document.getElementById('path-input');
if (pathInput) {
pathInput.value = currentPath;
}
self.loadFileList(currentPath).then(function() {
self.initResizableColumns();
});
var editorContainer = document.getElementById('editor-container');
if (editorContainer) {
var editorMode = self.editorMode;
var editorSizes = config.editorContainerSizes[editorMode] || {
width: 850,
height: 550
};
editorContainer.style.width = editorSizes.width + 'px';
editorContainer.style.height = editorSizes.height + 'px';
}
}).catch(function(err) {
pop(null, E('p', _('Failed to save settings: %s').format(err.message)), 'error');
});
}
},
// Load settings into the settings form
// Load settings into the settings form
loadSettings: function() {
var inputs = {
columnWidths: document.getElementById('column-widths-input'),
columnMinWidths: document.getElementById('column-min-widths-input'),
columnMaxWidths: document.getElementById('column-max-widths-input'),
padding: document.getElementById('padding-input'),
paddingMin: document.getElementById('padding-min-input'),
paddingMax: document.getElementById('padding-max-input'),
currentDirectory: document.getElementById('current-directory-input'),
windowWidth: document.getElementById('window-width-input'),
windowHeight: document.getElementById('window-height-input'),
editorTextWidth: document.getElementById('editor-text-width-input'),
editorTextHeight: document.getElementById('editor-text-height-input'),
editorHexWidth: document.getElementById('editor-hex-width-input'),
editorHexHeight: document.getElementById('editor-hex-height-input')
};
// Populate the input fields with the current config values
if (inputs.columnWidths) {
inputs.columnWidths.value = Object.keys(config.columnWidths).map(function(field) {
return field + ':' + config.columnWidths[field];
}).join(',');
}
if (inputs.columnMinWidths) {
inputs.columnMinWidths.value = Object.keys(config.columnMinWidths).map(function(field) {
return field + ':' + config.columnMinWidths[field];
}).join(',');
}
if (inputs.columnMaxWidths) {
inputs.columnMaxWidths.value = Object.keys(config.columnMaxWidths).map(function(field) {
return field + ':' + config.columnMaxWidths[field];
}).join(',');
}
if (inputs.padding) {
inputs.padding.value = config.padding;
}
if (inputs.paddingMin) {
inputs.paddingMin.value = config.paddingMin;
}
if (inputs.paddingMax) {
inputs.paddingMax.value = config.paddingMax;
}
if (inputs.currentDirectory) {
inputs.currentDirectory.value = config.currentDirectory || '/';
}
if (inputs.windowWidth) {
inputs.windowWidth.value = config.windowSizes.width;
}
if (inputs.windowHeight) {
inputs.windowHeight.value = config.windowSizes.height;
}
if (inputs.editorTextWidth) {
inputs.editorTextWidth.value = config.editorContainerSizes.text.width;
}
if (inputs.editorTextHeight) {
inputs.editorTextHeight.value = config.editorContainerSizes.text.height;
}
if (inputs.editorHexWidth) {
inputs.editorHexWidth.value = config.editorContainerSizes.hex.width;
}
if (inputs.editorHexHeight) {
inputs.editorHexHeight.value = config.editorContainerSizes.hex.height;
}
},
renderEditor: function(filePath) {
var self = this;
var editorContainer = document.getElementById('editor-container');
// Clear the editor container
editorContainer.innerHTML = '';
// Get the sizes from the config
var mode = self.editorMode; // 'text' or 'hex'
var editorSizes = config.editorContainerSizes[mode] || {
width: 850,
height: 550
};
// Create the editor content container
var editorContentContainer = E('div', {
'class': 'editor-content',
'style': 'flex: 1; display: flex; overflow: hidden;'
}, []);
// Action buttons array
var actionButtons = [];
if (mode === 'text') {
// Create line numbers div (initially hidden)
var lineNumbersDiv = E('div', {
'id': 'line-numbers',
'class': 'line-numbers',
'style': 'display: none;' // Initially hidden
}, []);
// Create textarea for text editing
var editorTextarea = E('textarea', {
'wrap': 'off',
'id': 'editor-textarea',
'style': 'flex: 1; resize: none; border: none; padding: 0; margin: 0; overflow: auto;'
}, [self.fileContent || '']);
// Append line numbers and textarea to the editor content container
editorContentContainer.appendChild(lineNumbersDiv);
editorContentContainer.appendChild(editorTextarea);
// Add event listeners for updating line numbers and synchronizing scroll
editorTextarea.addEventListener('input', self.updateLineNumbers.bind(self));
editorTextarea.addEventListener('scroll', self.syncScroll.bind(self));
lineNumbersDiv.addEventListener('scroll', function() {
editorTextarea.scrollTop = lineNumbersDiv.scrollTop;
});
// Define action buttons specific to Text Mode
actionButtons = [
E('button', {
'class': 'btn cbi-button-save custom-save-button',
'click': function() {
self.handleSaveFile(filePath);
}
}, _('Save')),
E('button', {
'class': 'btn',
'id': 'toggle-hex-mode',
'style': 'margin-left: 10px;',
'click': function() {
self.toggleHexMode(filePath);
}
}, _('Toggle to Hex Mode')),
E('button', {
'class': 'btn',
'id': 'toggle-line-numbers',
'style': 'margin-left: 10px;',
'click': function() {
self.toggleLineNumbers();
}
}, _('Toggle Line Numbers'))
];
} else if (mode === 'hex') {
// Create hex editor container
var hexeditContainer = E('div', {
'id': 'hexedit-container',
'style': 'flex: 1; overflow: hidden; display: flex; flex-direction: column;'
});
// Append hex editor to the editor content container
editorContentContainer.appendChild(hexeditContainer);
// Initialize the HexEditor instance
self.hexEditorInstance = HE.initialize(hexeditContainer);
// Load data into the HexEditor
self.hexEditorInstance.setData(self.fileData); // self.fileData is a Uint8Array
// Define action buttons specific to Hex Mode
actionButtons = [
E('button', {
'class': 'btn cbi-button-save custom-save-button',
'click': function() {
self.handleSaveFile(filePath);
}
}, _('Save')),
...(self.textType !== 'hex' ? [
E('button', {
'class': 'btn',
'id': 'toggle-text-mode',
'style': 'margin-left: 10px;',
'click': function() {
self.toggleHexMode(filePath);
}
}, _('Toggle to ASCII Mode'))
] : [])
];
}
// Create the editor container with resizing and scrolling
var editor = E('div', {
'class': 'editor-container',
'style': 'display: flex; flex-direction: column; width: ' + editorSizes.width + 'px; height: ' + editorSizes.height + 'px; resize: both; overflow: hidden;'
}, [
editorContentContainer,
E('div', {
'class': 'cbi-page-actions'
}, actionButtons)
]);
// Append the editor to the editorContainer
editorContainer.appendChild(editor);
// Update status bar and message
var statusInfo = document.getElementById('status-info');
if (statusInfo) {
statusInfo.textContent = _('Editing: ') + filePath;
}
var editorMessage = document.getElementById('editor-message');
if (editorMessage) {
editorMessage.textContent = _('Editing: ') + filePath;
}
// Clear any progress messages
var statusProgress = document.getElementById('status-progress');
if (statusProgress) {
statusProgress.innerHTML = '';
}
// **Add ResizeObserver to editor-container to update config.editorContainerSizes**
if (typeof ResizeObserver !== 'undefined') {
// Disconnect existing observer if it exists to prevent multiple observers
if (self.editorResizeObserver) {
self.editorResizeObserver.disconnect();
self.editorResizeObserver = null;
}
// Initialize a new ResizeObserver instance
self.editorResizeObserver = new ResizeObserver((entries) => {
for (let entry of entries) {
let newWidth = Math.round(entry.contentRect.width);
let newHeight = Math.round(entry.contentRect.height);
// Update config only if newWidth and newHeight are greater than 0
if (newWidth > 0 && newHeight > 0) {
config.editorContainerSizes[mode].width = newWidth;
config.editorContainerSizes[mode].height = newHeight;
}
}
});
// Observe the editor container
self.editorResizeObserver.observe(editor);
}
},
/**
* Toggles the editor mode between text and hex.
*
* @param {string} filePath - The path of the file to be edited.
*/
toggleHexMode: function(filePath) {
const self = this;
if (self.editorMode === 'text') {
// Before switching to hex mode, update self.fileData from the textarea
const textarea = document.querySelector('#editor-container textarea');
if (textarea) {
const content = textarea.value;
self.fileContent = content;
// Convert content to Uint8Array
const encoder = new TextEncoder();
self.fileData = encoder.encode(content);
}
self.editorMode = 'hex';
} else {
// Before switching to text mode, check if the file is textual
if (self.textType !== 'text') {
pop(null, E('p', _('This file is not a text file and cannot be edited in text mode.')), 'error');
return; // Abort the toggle
}
// Before switching to text mode, update self.fileData from HexEditor
if (self.hexEditorInstance) {
const hexData = self.hexEditorInstance.getData();
if (hexData instanceof Uint8Array) {
self.fileData = hexData;
} else {
pop(null, E('p', _('Failed to retrieve data from Hex Editor.')), 'error');
return; // Abort the toggle if data retrieval fails
}
}
// Convert self.fileData to string
const decoder = new TextDecoder();
try {
self.fileContent = decoder.decode(self.fileData);
} catch (error) {
pop(null, E('p', _('Failed to decode file data to text: %s').format(error.message)), 'error');
return; // Abort the toggle if decoding fails
}
self.editorMode = 'text';
}
// Re-render the editor with the updated mode and content
self.renderEditor(filePath);
}
});