'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'; const callFileList = rpc.declare({ object: 'file', method: 'list', params: [ 'path' ] }); const fileTypes = { 'block' : _('Block device'), 'char' : _('Character device'), 'directory' : _('Directory'), 'fifo' : _('FIFO/Pipe'), 'file' : _('File'), 'socket' : _('Socket'), 'symlink' : _('Symlink'), } function pop(a, message, severity) { ui.addNotification(a, message, severity) } function popTimeout(a, message, timeout, severity) { ui.addTimeLimitedNotification(a, message, timeout, severity) } // Initialize global variables let currentPath = '/'; // Current path in the filesystem const selectedItems = new Set(); // Set of selected files/directories let sortField = 'name'; // Field to sort files by let sortAscending = true; // Sort direction (ascending/descending) let configFilePath = '/etc/config/filemanager'; // Path to the configuration file // Initialize drag counter let dragCounter = 0; // Configuration object to store interface settings let config = { // Column widths in the file table columnWidths: { 'name': 150, 'type': 100, 'size': 100, 'mtime': 150, 'permissions': 70, 'actions': 100, }, // Minimum column widths columnMinWidths: { 'name': 100, 'type': 80, 'size': 80, 'mtime': 120, 'permissions': 70, 'actions': 80, }, // Maximum column widths columnMaxWidths: { 'name': 300, 'type': 200, 'size': 200, 'mtime': 300, 'permissions': 70, 'actions': 200, }, // Padding and window sizes padding: 10, paddingMin: 5, paddingMax: 20, currentDirectory: '/', // Current directory windowHeight: 800, windowWidth: 400, texteditorHeight: 550, texteditorWidth: 850, hexeditorHeight: 550, hexeditorWidth: 850, // otherSettings: {} // Additional settings }; // Function to upload a file to the server function uploadFile(filename, filedata, onProgress) { return new Promise(function(resolve, reject) { let 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 let 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) { let percent = (event.loaded / event.total) * 100; onProgress(percent); // Call the progress callback with percentage } }; // Handle request completion xhr.onload = () => { if (xhr.status === 200) { resolve(xhr.responseText); // Upload successful } else { reject(new Error(xhr.statusText)); // Upload error } }; // Handle network errors xhr.onerror = () => { 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; default: config[key] = isNaN(value) ? value : parseInt(value, 10); } }); }); } catch (err) { console.error('Failed to load config: ' + err.message); } } // Function to save settings to the configuration file function saveConfig() { let configLines = ['config filemanager', '\toption columnWidths \'' + Object.keys(config.columnWidths).map((field) => { return field + ':' + config.columnWidths[field]; }).join(',') + '\'', '\toption columnMinWidths \'' + Object.keys(config.columnMinWidths).map((field) => { return field + ':' + config.columnMinWidths[field]; }).join(',') + '\'', '\toption columnMaxWidths \'' + Object.keys(config.columnMaxWidths).map((field) => { return field + ':' + config.columnMaxWidths[field]; }).join(',') + '\'', '\toption padding \'' + config.padding + '\'', '\toption paddingMin \'' + config.paddingMin + '\'', '\toption paddingMax \'' + config.paddingMax + '\'', '\toption currentDirectory \'' + config.currentDirectory + '\'', '\toption windowHeight \'' + config.windowHeight + '\'', '\toption windowWidth \'' + config.windowWidth + '\'', '\toption texteditorWidth \'' + config.texteditorWidth + '\'', '\toption texteditorHeight \'' + config.texteditorHeight + '\'', '\toption hexeditorWidth \'' + config.hexeditorWidth + '\'', '\toption hexeditorHeight \'' + config.hexeditorHeight + '\'', ]; const configContent = configLines.join('\n') + '\n'; // Write settings to file return fs.write(configFilePath, configContent).then(() => { return Promise.resolve(); }).catch((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 modeToRwx(mode) { const perms = mode & 0o777; // extract permission bits const toRwx = n => ((n & 4) ? 'r' : '-') + ((n & 2) ? 'w' : '-') + ((n & 1) ? 'x' : '-'); const owner = toRwx((perms >> 6) & 0b111); const group = toRwx((perms >> 3) & 0b111); const world = toRwx(perms & 0b111); return `${owner}${group}${world}`; } function modeToOctal(mode) { const perms = mode & 0o777; return perms.toString(8); } // Function to get a list of files in a directory function getFileList(path) { return callFileList(path).then((res) => { const files = []; res?.entries?.forEach((file) => { files.push({ ...file, permissions: modeToRwx(file.mode), numericPermissions: modeToOctal(file.mode), }); }); return files; }); } // Function to insert CSS styles into the document function insertCss(cssContent) { const styleElement = document.createElement('style'); styleElement.type = 'text/css'; styleElement.appendChild(document.createTextNode(cssContent)); document.head.appendChild(styleElement); } // CSS styles for the file manager interface const 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 { font-family: monospace; box-sizing: border-box; white-space: nowrap; 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.resizeable { 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() { const self = this; return loadConfig().then(() => { currentPath = config.currentDirectory || '/'; return getFileList(currentPath); // Load the file list for the current directory }); }, // Method to render the interface render(data) { const self = this; insertCss(cssContent); // Insert CSS styles const 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'(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 (() => { // Create the container for the file list and drag-and-drop functionality const fileListContainer = E('div', { 'id': 'file-list-container', 'class': 'resizeable', 'style': 'width: ' + config.windowWidth + 'px; height: ' + config.windowHeight + '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': 'permissions' }, [ _('Permissions'), E('button', { 'class': 'sort-button', 'data-field': 'permissions', 'title': _('Sort by Permissions') }, '↕'), 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', {'data-field': 'actions'}, [ 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': 'windowWidth-input', 'value': config.windowWidth, 'style': 'width:100%; margin-bottom:10px;' }) ]), E('div', {}, [ E('label', {}, _('Window Height:')), E('input', { 'type': 'number', 'id': 'windowHeight-input', 'value': config.windowHeight, 'style': 'width:100%; margin-bottom:10px;' }) ]), E('div', {}, [ E('label', {}, _('Text Editor Width:')), E('input', { 'type': 'number', 'id': 'texteditorWidth-input', 'value': config.texteditorWidth, 'style': 'width:100%; margin-bottom:10px;' }) ]), E('div', {}, [ E('label', {}, _('Text Editor Height:')), E('input', { 'type': 'number', 'id': 'texteditorHeight-input', 'value': config.texteditorHeight, 'style': 'width:100%; margin-bottom:10px;' }) ]), E('div', {}, [ E('label', {}, _('Hex Editor Width:')), E('input', { 'type': 'number', 'id': 'hexeditorWidth-input', 'value': config.hexeditorWidth, 'style': 'width:100%; margin-bottom:10px;' }) ]), E('div', {}, [ E('label', {}, _('Hex Editor Height:')), E('input', { 'type': 'number', 'id': 'hexeditorHeight-input', 'value': config.hexeditorHeight, 'style': 'width:100%; margin-bottom:10px;' }) ]), E('div', {}, [ E('label', {}, _('Column Widths (format: name:width,type:width,...):')), E('input', { 'type': 'text', 'id': 'columnWidths-input', 'value': Object.values(config.columnWidths).join(''), 'style': 'width:100%; margin-bottom:10px;' }) ]), E('div', {}, [ E('label', {}, _('Column Min Widths (format: name:minWidth,type:minWidth,...):')), E('input', { 'type': 'text', 'id': 'columnMinWidths-input', 'value': Object.values(config.columnMinWidths).join(''), 'style': 'width:100%; margin-bottom:10px;' }) ]), E('div', {}, [ E('label', {}, _('Column Max Widths (format: name:maxWidth,type:maxWidth,...):')), E('input', { 'type': 'text', 'id': 'columnMaxWidths-input', 'value': Object.values(config.columnMaxWidths).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': 'paddingMin-input', 'value': config.paddingMin, 'style': 'width:100%; margin-bottom:10px;' }) ]), E('div', {}, [ E('label', {}, _('Padding Max:')), E('input', { 'type': 'number', 'id': 'paddingMax-input', 'value': config.paddingMax, 'style': 'width:100%; margin-bottom:10px;' }) ]), E('div', {}, [ E('label', {}, _('Current Directory:')), E('input', { 'type': 'text', 'id': 'currentDirectory-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 const sortButtons = viewContainer.querySelectorAll('.sort-button[data-field]'); sortButtons.forEach((button) => { button.addEventListener('click', (event) => { event.preventDefault(); const field = button.getAttribute('data-field'); if (field) { self.sortBy(field); // Sort the file list by the selected field } }); }); // Load the file list and initialize resizeable columns this.loadFileList(currentPath).then(() => { self.initResizeableColumns(); const fileListContainer = document.getElementById('file-list-container'); if (fileListContainer && typeof ResizeObserver !== 'undefined') { // Initialize ResizeObserver only once if (!self.fileListResizeObserver) { self.fileListResizeObserver = new ResizeObserver((entries) => { for (let entry of entries) { const newWidth = entry.contentRect.width; const newHeight = entry.contentRect.height; // Update config only if newWidth and newHeight are greater than 0 if (newWidth > 0 && newHeight > 0) { config.windowWidth = newWidth; config.windowHeight = newHeight; } } }); self.fileListResizeObserver.observe(fileListContainer); } } }); return viewContainer; }, // Handler for the "Select All" checkbox click handleSelectAllClick(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() { const allCheckboxes = document.querySelectorAll('.select-checkbox'); allCheckboxes.forEach((checkbox) => { checkbox.checked = !checkbox.checked; const 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(tab) { // Retrieve the content containers for each tab const fileManagerContent = document.getElementById('content-filemanager'); const editorContent = document.getElementById('content-editor'); const settingsContent = document.getElementById('content-settings'); const helpContent = document.getElementById('content-help'); // Retrieve the tab elements const tabFileManager = document.getElementById('tab-filemanager'); const tabEditor = document.getElementById('tab-editor'); const tabSettings = document.getElementById('tab-settings'); const 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 resizeable columns after successfully loading the file list this.initResizeableColumns(); }) .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() { const self = this; // Convert Markdown to HTML const helpContentHTML = md.parseMarkdown(md_help.helpContentMarkdown); // Get the Help content container const helpContent = document.getElementById('content-help'); if (helpContent) { // Insert the converted HTML into the Help container helpContent.innerHTML = helpContentHTML; // Initialize resizeable functionality for the Help window self.initResizeableHelp(); } else { console.error('Help content container not found.'); pop(null, E('p', _('Failed to render Help content: Container not found.')), 'error'); } }, /** * Initializes the resizeable functionality for the Help window. */ initResizeableHelp() { const 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() { // Logic to navigate to the specified directory and update the file list const self = this; const pathInput = document.getElementById('path-input'); if (pathInput) { const newPath = pathInput.value.trim() || '/'; fs.stat(newPath).then((stat) => { if (stat.type === 'directory') { currentPath = newPath; pathInput.value = currentPath; self.loadFileList(currentPath).then(() => { self.initResizeableColumns(); }); } else { pop(null, E('p', _('The specified path does not appear to be a directory.')), 'error'); } }).catch((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(event) { event.preventDefault(); event.stopPropagation(); dragCounter++; const fileListContainer = document.getElementById('file-list-container'); const 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(event) { event.preventDefault(); event.stopPropagation(); event.dataTransfer.dropEffect = 'copy'; // Indicate copy action }, // Handler for leaving the drop zone handleDragLeave(event) { event.preventDefault(); event.stopPropagation(); dragCounter--; if (dragCounter === 0) { const fileListContainer = document.getElementById('file-list-container'); const 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(event) { event.preventDefault(); event.stopPropagation(); dragCounter = 0; // Reset counter const self = this; const files = event.dataTransfer.files; const fileListContainer = document.getElementById('file-list-container'); const 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(ev) { const self = this; const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.multiple = true; // Allow selecting multiple files fileInput.style.display = 'none'; document.body.appendChild(fileInput); fileInput.onchange = (event) => { const 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(files) { const self = this; const directoryPath = currentPath; const statusInfo = document.getElementById('status-info'); const statusProgress = document.getElementById('status-progress'); const totalFiles = files.length; let uploadedFiles = 0; function uploadNextFile(index) { if (index >= totalFiles) { self.loadFileList(currentPath).then(() => { self.initResizeableColumns(); }); return; } const file = files[index]; const fullFilePath = joinPath(directoryPath, file.name); if (statusInfo) { statusInfo.textContent = _('Uploading: "%s"...').format(file.name); } if (statusProgress) { statusProgress.innerHTML = ''; const progressBarContainer = E('div', { 'class': 'cbi-progressbar', 'title': '0%' }, [E('div', { 'style': 'width:0%' })]); statusProgress.appendChild(progressBarContainer); } uploadFile(fullFilePath, file, (percent) => { if (statusProgress) { const progressBar = statusProgress.querySelector('.cbi-progressbar div'); if (progressBar) { progressBar.style.width = percent.toFixed(2) + '%'; statusProgress.querySelector('.cbi-progressbar').setAttribute('title', percent.toFixed(2) + '%'); } } }).then(() => { if (statusProgress) { statusProgress.innerHTML = ''; } if (statusInfo) { statusInfo.textContent = _('File "%s" uploaded successfully.').format(file.name); } popTimeout(null, E('p', _('File "%s" uploaded successfully.').format(file.name)), 5000, 'info'); uploadedFiles++; uploadNextFile(index + 1); }).catch((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(ev) { // Logic to create a new directory const self = this; const statusInfo = document.getElementById('status-info'); const statusProgress = document.getElementById('status-progress'); if (statusInfo && statusProgress) { statusInfo.innerHTML = ''; statusProgress.innerHTML = ''; const dirNameInput = E('input', { 'type': 'text', 'placeholder': _('Directory Name'), 'style': 'margin-right: 10px;' }); const saveButton = E('button', { 'class': 'btn', 'disabled': true, 'click'() { self.createDirectory(dirNameInput.value); } }, _('Save')); dirNameInput.addEventListener('input', () => { 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(dirName) { // Execute the 'mkdir' command and update the interface const self = this; const trimmedDirName = dirName.trim(); const dirPath = joinPath(currentPath, trimmedDirName); fs.exec('mkdir', [dirPath]).then((res) => { if (res.code !== 0) { return Promise.reject(new Error(res.stderr.trim())); } popTimeout(null, E('p', _('Directory "%s" created successfully.').format(trimmedDirName)), 5000, 'info'); self.loadFileList(currentPath).then(() => { self.initResizeableColumns(); }); const statusInfo = document.getElementById('status-info'); const statusProgress = document.getElementById('status-progress'); if (statusInfo) statusInfo.textContent = _('No directory selected.'); if (statusProgress) statusProgress.innerHTML = ''; }).catch((err) => { pop(null, E('p', _('Failed to create directory "%s": %s').format(trimmedDirName, err.message)), 'error'); }); }, // Handler for creating a file handleCreateFileClick(ev) { // Logic to create a new file const self = this; const statusInfo = document.getElementById('status-info'); const statusProgress = document.getElementById('status-progress'); if (statusInfo && statusProgress) { statusInfo.innerHTML = ''; statusProgress.innerHTML = ''; const fileNameInput = E('input', { 'type': 'text', 'placeholder': _('File Name'), 'style': 'margin-right: 10px;' }); const createButton = E('button', { 'class': 'btn', 'disabled': true, 'click'() { self.createFile(fileNameInput.value); } }, _('Create')); fileNameInput.addEventListener('input', () => { 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(fileName) { // Execute the 'touch' command and update the interface const self = this; const trimmedFileName = fileName.trim(); const filePath = joinPath(currentPath, trimmedFileName); fs.exec('touch', [filePath]).then((res) => { if (res.code !== 0) { return Promise.reject(new Error(res.stderr.trim())); } popTimeout(null, E('p', _('File "%s" created successfully.').format(trimmedFileName)), 5000, 'info'); self.loadFileList(currentPath).then(() => { self.initResizeableColumns(); }); const statusInfo = document.getElementById('status-info'); const statusProgress = document.getElementById('status-progress'); if (statusInfo) statusInfo.textContent = _('No file selected.'); if (statusProgress) statusProgress.innerHTML = ''; }).catch((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(ev) { const cb = ev.target; const filePath = cb.dataset.filePath; cb.checked ? selectedItems.add(filePath) : selectedItems.delete(filePath); this.updateDeleteSelectedButton(); this.updateSelectAllCheckbox(); }, // Update the "Delete Selected" button updateDeleteSelectedButton() { const btn = document.getElementById('delete-selected-button'); if (!btn) return; btn.style.display = selectedItems.size > 0 ? '' : 'none'; }, // Update the "Select All" checkbox state updateSelectAllCheckbox() { const selectAll = document.getElementById('select-all-checkbox'); if (!selectAll) return; const checkboxes = [...document.querySelectorAll('.select-checkbox')]; if (checkboxes.length === 0) { selectAll.checked = false; selectAll.indeterminate = false; return; } const total = checkboxes.length; const checked = checkboxes.filter(cb => cb.checked).length; selectAll.checked = checked === total; selectAll.indeterminate = checked > 0 && checked < total; }, // Handler for the "Select All" checkbox change handleSelectAllChange(ev) { const checked = ev.target.checked; const checkboxes = [...document.querySelectorAll('.select-checkbox')]; selectedItems.clear(); checkboxes.forEach(cb => { cb.checked = checked; if (checked) selectedItems.add(cb.dataset.filePath); }); this.updateDeleteSelectedButton(); this.updateSelectAllCheckbox(); }, // Handler for deleting selected items handleDeleteSelected() { // Delete selected files and directories const self = this; if (selectedItems.size === 0) { return; } if (!confirm(_('Are you sure you want to delete the selected files and directories?'))) { return; } const promises = []; selectedItems.forEach((filePath) => { promises.push(fs.remove(filePath).catch((err) => { pop(null, E('p', _('Failed to delete %s: %s').format(filePath, err.message)), 'error'); })); }); Promise.all(promises).then(() => { popTimeout(null, E('p', _('Selected files and directories deleted successfully.')), 5000, 'info'); selectedItems.clear(); self.updateDeleteSelectedButton(); self.loadFileList(currentPath).then(() => { self.initResizeableColumns(); }); }).catch((err) => { pop(null, E('p', _('Failed to delete selected files and directories: %s').format(err.message)), 'error'); }); }, // Function to load the file list loadFileList(path) { const self = this; selectedItems.clear(); return getFileList(path).then(files => { // 1. Get column order dynamically from table header const columns = Array.from( document.querySelectorAll('#file-table thead th[data-field]') ).map(th => th.getAttribute('data-field')); const 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)); // // Add ".." parent row // if (path !== '/') { const parentPath = path.substring(0, path.lastIndexOf('/')) || '/'; const tr = E('tr', { 'data-file-path': parentPath, 'data-file-type': 'directory' }); // Create cells for *every* column for (const col of columns) { if (col === 'name') { tr.appendChild( E('td', { colspan: columns.length }, [ E('a', { href: '#', click: () => self.handleDirectoryClick(parentPath) }, '.. (Parent Directory)') ]) ); break; } else { tr.appendChild(E('td')); // empty cell } } fileList.appendChild(tr); } // // 2. For each file, create row dynamically // for (const file of files) { const fullPath = joinPath(path, file.name); const tr = E('tr', { 'data-file-path': fullPath, 'data-file-type': file.type, 'data-permissions': file.permissions, 'data-numeric-permissions': file.numericPermissions, 'data-owner': file?.user || file.uid, 'data-group': file?.group || file.gid, 'data-size': file.size }); // // Prebuild common reusable items // const nameLink = E('a', { href: '#', title: file.permissions, class: `${file.type}-link`, click(event) { if (file.type === 'directory' || file?.target?.type === 'directory') { self.handleDirectoryClick(fullPath); } else { event.preventDefault(); self.handleFileClick(fullPath, event.altKey ? 'hex' : 'text'); } } }, file?.target ? `${file.name} → ${file.target?.name}` : file.name); const actions = []; const checkbox = E('input', { type: 'checkbox', class: 'select-checkbox', 'data-file-path': fullPath, change: ev => self.handleCheckboxChange(ev) }); actions.push(checkbox); actions.push(E('span', { class: 'edit-button', title: _('Edit properties'), click: () => self.handleEditFile(fullPath, file) }, '✏️')); actions.push(E('span', { class: 'duplicate-button', title: _('Duplicate'), click: () => self.handleDuplicateFile(fullPath, file) }, '📑')); actions.push(E('span', { class: 'delete-button', title: _('Delete'), click: () => self.handleDeleteFile(fullPath, file) }, '🗑️')); if (file.type === 'file') { actions.push(E('span', { class: 'download-button', title: _('Download'), click: () => self.handleDownloadFile(fullPath) }, '⬇️')); } // // 3. Build `