summaryrefslogtreecommitdiffhomepage
path: root/applications/luci-app-filemanager/htdocs/luci-static/resources/view/system/filemanager.js
diff options
context:
space:
mode:
Diffstat (limited to 'applications/luci-app-filemanager/htdocs/luci-static/resources/view/system/filemanager.js')
-rw-r--r--applications/luci-app-filemanager/htdocs/luci-static/resources/view/system/filemanager.js2855
1 files changed, 2855 insertions, 0 deletions
diff --git a/applications/luci-app-filemanager/htdocs/luci-static/resources/view/system/filemanager.js b/applications/luci-app-filemanager/htdocs/luci-static/resources/view/system/filemanager.js
new file mode 100644
index 0000000000..e2362ed411
--- /dev/null
+++ b/applications/luci-app-filemanager/htdocs/luci-static/resources/view/system/filemanager.js
@@ -0,0 +1,2855 @@
+'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();
+ });
+ },
+
+ // Handler for clicking on a file to open it in the editor
+ handleFileClick: function(filePath, mode = 'text') {
+ var self = this;
+ var fileRow = document.querySelector("tr[data-file-path='" + filePath + "']");
+ var editorMessage = document.getElementById('editor-message');
+ var editorContainer = document.getElementById('editor-container');
+
+ // Set default permissions if file row is not found
+ if (fileRow) {
+ var permissions = fileRow.getAttribute('data-numeric-permissions');
+ self.originalFilePermissions = permissions;
+ } else {
+ self.originalFilePermissions = '644';
+ }
+
+ // Update message to indicate loading
+ if (editorMessage) {
+ editorMessage.textContent = _('Loading file...');
+ }
+
+ // Execute 'cat' to read the file content
+ fs.exec('cat', [filePath]).then(function(res) {
+ var content = '';
+ if (res.code !== 0) {
+ if (res.stderr.trim() !== '') {
+ return Promise.reject(new Error(res.stderr.trim()));
+ }
+ } else {
+ content = res.stdout || '';
+ }
+
+ // Store the content as a string
+ 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; // Store binary data as Uint8Array
+
+ self.editorMode = mode; // Set the initial editor mode to 'text'
+
+ // Render the editor
+ self.renderEditor(filePath);
+
+ // Switch to the editor tab
+ self.switchToTab('editor');
+
+ }).catch(function(err) {
+ // Handle file read errors
+ pop(null, E('p', _('Failed to open file: %s').format(err.message)), 'error');
+ if (editorMessage) {
+ editorMessage.textContent = _('Failed to open file: %s').format(err.message);
+ }
+ });
+ },
+
+ // 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();
+ fs.read(filePath, {
+ binary: true
+ }).then(function(content) {
+ var blob = new Blob([content], {
+ type: 'application/octet-stream'
+ });
+ var downloadLink = document.createElement('a');
+ downloadLink.href = URL.createObjectURL(blob);
+ downloadLink.download = fileName;
+ document.body.appendChild(downloadLink);
+ downloadLink.click();
+ document.body.removeChild(downloadLink);
+ var statusInfo = document.getElementById('status-info');
+ if (statusInfo) {
+ statusInfo.textContent = _('Downloaded file: "%s".').format(fileName);
+ }
+ }).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')),
+ 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);
+ }
+ },
+
+ toggleHexMode: function(filePath) {
+ var self = this;
+
+ if (self.editorMode === 'text') {
+ // Before switching to hex mode, update self.fileData from the textarea
+ var textarea = document.querySelector('#editor-container textarea');
+ if (textarea) {
+ var content = textarea.value;
+ self.fileContent = content;
+
+ // Convert content to Uint8Array
+ var encoder = new TextEncoder();
+ self.fileData = encoder.encode(content);
+ }
+ self.editorMode = 'hex';
+ } else {
+ // Before switching to text mode, update self.fileData from the HexEditor
+ if (self.hexEditorInstance) {
+ self.fileData = self.hexEditorInstance.getData();
+ }
+ // Convert self.fileData to string
+ var decoder = new TextDecoder();
+ self.fileContent = decoder.decode(self.fileData);
+ self.editorMode = 'text';
+ }
+
+ // Re-render the editor
+ self.renderEditor(filePath);
+ }
+
+
+});