From d543b4a85d165e96fc8510de168dba3618c213ac Mon Sep 17 00:00:00 2001
From: Dmitry R <rdmitry0911@gmail.com>
Date: Sat, 21 Dec 2024 09:08:52 -0500
Subject: [PATCH] luci-app-file-plug-manager: Full featured file manager with
 plugins  - minimal working set: file-plug-manager + Navigation plugin. All
 other plugins are optional  - Directory Navigation and Traversal  - File and
 Directory Operations  - Bulk Selection and Management  - drug'n'drop to/from
 local computer  - drug'n'drop to router local folders  - copy/move mode for
 drug'n'drop switching with 'Alt' key  - Editing text files with find/replace
 functionality  - Hex editor  - Save/load settings  - Dumb Terminal

Signed-off-by: Dmitry R <rdmitry0911@gmail.com>

luci-app-file-plug-manager: Fixed application name and monospace fonts usage across plugins

Signed-off-by: Dmitry R <rdmitry0911@gmail.com>
---
 .../luci-app-file-plug-manager/Makefile       |   13 +
 .../view/system/file-plug-manager.js          |  903 ++++++
 .../system/file-plug-manager/plugins/Edit+.js |  864 +++++
 .../file-plug-manager/plugins/Navigation.js   | 2848 +++++++++++++++++
 .../file-plug-manager/plugins/Settings.js     |  697 ++++
 .../file-plug-manager/plugins/hexEditor.js    | 1531 +++++++++
 .../system/file-plug-manager/plugins/term.js  |  354 ++
 .../po/templates/file-plug-manager.pot        |  531 +++
 .../menu.d/luci-app-file-plug-manager.json    |   13 +
 .../acl.d/luci-app-file-plug-manager.json     |   14 +
 10 files changed, 7768 insertions(+)
 create mode 100644 applications/luci-app-file-plug-manager/Makefile
 create mode 100644 applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager.js
 create mode 100644 applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Edit+.js
 create mode 100644 applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js
 create mode 100644 applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Settings.js
 create mode 100644 applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/hexEditor.js
 create mode 100644 applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/term.js
 create mode 100644 applications/luci-app-file-plug-manager/po/templates/file-plug-manager.pot
 create mode 100644 applications/luci-app-file-plug-manager/root/usr/share/luci/menu.d/luci-app-file-plug-manager.json
 create mode 100644 applications/luci-app-file-plug-manager/root/usr/share/rpcd/acl.d/luci-app-file-plug-manager.json

diff --git a/applications/luci-app-file-plug-manager/Makefile b/applications/luci-app-file-plug-manager/Makefile
new file mode 100644
index 000000000000..321a5f6549ce
--- /dev/null
+++ b/applications/luci-app-file-plug-manager/Makefile
@@ -0,0 +1,13 @@
+# This is free software, licensed under the Apache License, Version 2.0 .
+
+include $(TOPDIR)/rules.mk
+
+LUCI_TITLE:=LuCI File Plug Manager module
+LUCI_DEPENDS:=+luci-base
+
+PKG_LICENSE:=Apache-2.0
+PKG_MAINTAINER:=Dmitry R <rdmitry0911@gmail.com>
+
+include ../../luci.mk
+
+# call BuildPackage - OpenWrt buildroot signature
diff --git a/applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager.js b/applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager.js
new file mode 100644
index 000000000000..dfbbe839866d
--- /dev/null
+++ b/applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager.js
@@ -0,0 +1,903 @@
+/***************************************
+ * Main Dispatcher Plugin (main.js)
+ * This is the main (Dispatcher) plugin that:
+ * - Loads and manages other plugins
+ * - Sets default plugins and mark the default plugin tabs with а green button 
+ * - Initializes plugins in the correct order
+ * - Integrates with the default Settings plugin
+ * - sends personalised events to plugins
+ * - provides pop() for messaging
+ * - All comments and messages are in English
+ ***************************************/
+'use strict';
+'require view';
+'require ui';
+'require fs';
+'require dom';
+'require form';
+
+const PN = 'Main';
+
+
+return view.extend({
+	// Unique identifier counter for plugins
+	pluginUniqueIdCounter: 1,
+
+	// Default plugins storage
+	default_plugins: {
+		'Editor': null,
+		'Navigation': null,
+		'Settings': null,
+		'Help': null,
+		'Utility': null
+	},
+
+	// Registry of loaded plugins
+	pluginsRegistry: {},
+
+	// Supported plugin types
+	supportedPluginTypes: ['Editor', 'Navigation', 'Settings', 'Help', 'Utility'],
+	defaultTabsOrder: ['Navigation', 'Editor', 'Settings', 'Utility', 'Help'],
+	defaultStartPlugin: 'Navigation',
+
+	// References to UI containers
+	buttonsContainer: null,
+	contentsContainer: null,
+	logsContainer: null, // Added for logs container
+	infoContainer: null, // Added for informational messages
+
+	screen_log: false, // Initialization of screen_log variable
+	box_log: false, // Initialization of box_log variable
+
+	/**
+	 * pop(title, children, type)
+	 * Display notifications to the user.
+	 */
+	pop: function(title, children, type) {
+		// Get current time
+		var timestamp = new Date().toLocaleString();
+
+		// Create message with timestamp
+		var message = E('div', {
+			'class': 'log-entry'
+		}, [
+			E('span', {
+				'class': 'log-timestamp'
+			}, `[${timestamp}] `),
+			typeof children === 'string' ? children : children.outerHTML
+		]);
+
+		// Add message to Logs
+		if (this.logsContainer) {
+			this.logsContainer.appendChild(message);
+			// Scroll to the bottom to show the latest message
+			this.logsContainer.scrollTop = this.logsContainer.scrollHeight;
+		} else {
+			console.error(`[${PN}]: Logs container not found. Unable to display log message.`);
+		}
+
+		// If screen_log is enabled, duplicate the message via ui.addNotification
+		if (String(this.screen_log) === 'true') {
+			ui.addNotification(title, children, type);
+		}
+		// If box_log is true, display the message in the informational box
+		if (String(this.box_log) === 'true') {
+			this.displayInfoMessage(title, children, type);
+		}
+
+	},
+
+	/**
+	 * info()
+	 * Return metadata about this plugin.
+	 */
+	info: function() {
+		return {
+			name: PN, // Unique name
+			type: 'Dispatcher', // Plugin type
+			description: 'Main dispatcher module'
+		};
+	},
+
+	/**
+	 * start(container, pluginsRegistry, default_plugins)
+	 * Initialize the dispatcher if needed.
+	 */
+	start: function(container, pluginsRegistry, default_plugins) {
+		// Initialize screen_log and box_log from settings or default to false
+		const settings = this.get_settings();
+		this.screen_log = settings.screen_log || false;
+		this.box_log = settings.box_log || false;
+	},
+
+	/**
+	 * get_settings()
+	 * Return current settings for the dispatcher.
+	 */
+	get_settings: function() {
+		return {
+			screen_log: this.screen_log || false, // Default value: false
+			box_log: this.box_log || false // Default value: false
+		};
+	},
+
+	/**
+	 * set_settings(settings)
+	 * Apply settings to the dispatcher.
+	 */
+	set_settings: function(settings) {
+		if (typeof settings.screen_log !== 'undefined') {
+			this.screen_log = settings.screen_log;
+		}
+		if (typeof settings.box_log !== 'undefined') {
+			this.box_log = settings.box_log;
+		}
+	},
+
+	/**
+	 * render()
+	 * Render the main view, load plugins, and set up the UI.
+	 * Changed this function to async to await s.render().
+	 */
+	render: async function() {
+		var m, s, o;
+		// Create the JSONMap for form
+		m = new form.JSONMap({}, _('File Plug Manager'));
+
+		// Create informational container
+		this.infoContainer = E('div', {
+			'class': 'info-container'
+		});
+
+		// Create tabs container
+		var tabs = E('div', {
+			'class': 'cbi-tabs'
+		});
+
+		// Create containers for tab buttons and contents
+		this.buttonsContainer = E('div', {
+			'class': 'cbi-tabs-buttons'
+		});
+		this.contentsContainer = E('div', {
+			'class': 'cbi-tabs-contents'
+		});
+
+		tabs.appendChild(this.buttonsContainer);
+		tabs.appendChild(this.contentsContainer);
+
+		// Create Logs tab first to ensure logsContainer is available
+		this.createLogsTab();
+
+		// Load plugins
+		this.loadPlugins(this.buttonsContainer, this.contentsContainer);
+
+		// Determine current theme
+		var isDarkTheme = document.body.classList.contains('dark-theme');
+		if (isDarkTheme) {
+			tabs.classList.add('dark-theme');
+		} else {
+			tabs.classList.add('light-theme');
+		}
+
+		// Custom CSS for styling
+		var customCSS = `
+            /* Tabs container */
+            .cbi-tabs {
+                margin-top: 20px;
+            }
+
+            /* Tab buttons container */
+            .cbi-tabs-buttons {
+                display: flex;
+                border: 2px solid #0078d7;
+                border-radius: 5px;
+                background-color: #f9f9f9;
+                padding: 5px;
+                box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+            }
+
+            .dark-theme .cbi-tabs-buttons {
+                border: 2px solid #555;
+                background-color: #333;
+                box-shadow: 0 2px 4px rgba(255, 255, 255, 0.1);
+            }
+
+            .cbi-tab-button {
+                padding: 10px 20px;
+                cursor: pointer;
+                border: none;
+                background: none;
+                outline: none;
+                transition: background-color 0.3s, border-bottom 0.3s;
+                font-size: 16px;
+                margin-right: 5px;
+                position: relative;
+                user-select: none;
+                color: #000;
+            }
+
+            .dark-theme .cbi-tab-button {
+                color: #fff;
+            }
+
+            .cbi-tab-button:hover {
+                background-color: #e0e0e0;
+            }
+
+            .dark-theme .cbi-tab-button:hover {
+                background-color: #444;
+            }
+
+            .cbi-tab-button.active {
+                border-bottom: 3px solid #0078d7;
+                font-weight: bold;
+                background-color: #fff;
+                color: #000;
+            }
+
+            .dark-theme .cbi-tab-button.active {
+                border-bottom: 3px solid #1e90ff;
+                background-color: #555;
+                color: #fff;
+            }
+
+            .cbi-tabs-contents {
+                padding: 10px;
+                background-color: #fff;
+                color: #000;
+            }
+
+            .dark-theme .cbi-tabs-contents {
+                background-color: #2a2a2a;
+                color: #ddd;
+            }
+
+            .cbi-tab-content {
+                display: none;
+            }
+
+            .cbi-tab-content.active {
+                display: block;
+            }
+
+            .default-marker {
+                display: inline-block;
+                width: 10px;
+                height: 10px;
+                border-radius: 50%;
+                margin-left: 10px;
+                cursor: pointer;
+                background-color: gray;
+            }
+
+            .default-marker.active {
+                background-color: green;
+            }
+
+            .default-marker.inactive {
+                background-color: gray;
+            }
+
+            .dark-theme .navigation-plugin-table-container {
+                background-color: #222;
+                color: #fff;
+            }
+
+            .dark-theme .navigation-plugin-table th {
+                background-color: #333;
+                color: #fff;
+            }
+
+            .dark-theme .navigation-plugin-table td {
+                background-color: #222;
+                color: #fff;
+            }
+
+            .dark-theme .navigation-plugin-table tr:hover {
+                background-color: #444;
+            }
+
+            .dark-theme .navigation-plugin-table td.name a {
+                color: #1e90ff;
+                text-decoration: none;
+            }
+
+            /* Logs Tab */
+            .cbi-tab-button.logs-tab {
+                font-weight: bold;
+            }
+
+            .logs-container {
+                max-height: 400px;
+                overflow-y: auto;
+                background-color: #f1f1f1;
+                padding: 10px;
+                border: 1px solid #ccc;
+                border-radius: 5px;
+            }
+
+            .dark-theme .logs-container {
+                background-color: #1e1e1e;
+                color: #dcdcdc;
+                border-color: #555;
+            }
+
+            .log-entry {
+                margin-bottom: 5px;
+            }
+
+            .log-timestamp {
+                color: #888;
+                margin-right: 10px;
+            }
+
+        /* Informational Container */
+        .info-container {
+            margin-bottom: 20px;
+            padding: 10px;
+            border: 1px solid #0078d7;
+            border-radius: 5px;
+            background-color: #e7f3fe;
+            color: #31708f;
+            display: none; /* Hidden by default */
+            opacity: 0;
+            transition: opacity 0.5s ease-in-out;
+        }
+
+        .dark-theme .info-container {
+            background-color: #333;
+            border-color: #1e90ff;
+            color: #fff;
+        }
+
+        /* Blinking Animation */
+        @keyframes blink {
+            0% { opacity: 1; }
+            50% { opacity: 0; }
+            100% { opacity: 1; }
+        }
+
+        .blink {
+            animation: blink 1s step-start 5;
+        }
+
+        `;
+
+		var style = document.createElement('style');
+		style.type = 'text/css';
+		style.innerHTML = customCSS;
+		document.head.appendChild(style);
+		// Return the combined DOM
+		return E([], [
+			m.title ? E('h3', {}, m.title) : null,
+			this.infoContainer, // Add infoContainer above tabs
+			tabs
+		]);
+	},
+
+	/**
+	 * createLogsTab()
+	 * Create the Logs tab in the UI.
+	 */
+	createLogsTab: function() {
+		var self = this;
+
+		// Create Logs tab button with a green marker
+		var logButton = E('button', {
+			'class': 'cbi-tab-button logs-tab'
+		}, 'Logs');
+		var marker = E('span', {
+			'class': 'default-marker active',
+			'title': _('Logs are always active')
+		});
+		logButton.appendChild(marker);
+
+		// Create Logs container
+		this.logsContainer = E('div', {
+			'class': 'logs-container cbi-tab-content',
+			'id': 'tab-Logs'
+		});
+
+		// Append button and container to respective parents
+		this.buttonsContainer.appendChild(logButton);
+		this.contentsContainer.appendChild(this.logsContainer);
+
+		// Click handler for Logs tab
+		logButton.onclick = function() {
+			var allButtons = self.buttonsContainer.querySelectorAll('.cbi-tab-button');
+			allButtons.forEach(function(btn) {
+				btn.classList.remove('active');
+			});
+
+			var allContents = self.contentsContainer.querySelectorAll('.cbi-tab-content');
+			allContents.forEach(function(content) {
+				content.classList.remove('active');
+			});
+
+			logButton.classList.add('active');
+			self.logsContainer.classList.add('active');
+		};
+	},
+
+	/**
+	 * displayInfoMessage(title, message, type)
+	 * Display a message in the informational box with a blinking effect.
+	 * @param {string} title - The title of the message.
+	 * @param {string|HTMLElement} message - The message content.
+	 * @param {string} type - The type of message (e.g., 'success', 'error').
+	 */
+	displayInfoMessage: function(title, message, type) {
+		// Clear any existing messages
+		this.infoContainer.innerHTML = '';
+
+		// Create message element
+		var msg = E('div', {
+			'class': 'info-message'
+		}, [
+			title ? E('strong', {}, title + ': ') : ' ',
+			typeof message === 'string' ? message : message.outerHTML
+		]);
+
+		// Append message to infoContainer
+		this.infoContainer.appendChild(msg);
+
+		// Show the infoContainer
+		this.infoContainer.style.display = 'block';
+		// Trigger reflow to restart CSS animation
+		void this.infoContainer.offsetWidth;
+		// Add the blink class
+		msg.classList.add('blink');
+
+		// Show with opacity
+		this.infoContainer.style.opacity = '1';
+
+		// After 5 seconds, remove the blink class and hide the message
+		setTimeout(() => {
+			msg.classList.remove('blink');
+			// Fade out the infoContainer
+
+			/***
+			        this.infoContainer.style.opacity = '0';
+			        // After transition, hide the container
+			        setTimeout(() => {
+			            this.infoContainer.style.display = 'none';
+			            this.infoContainer.innerHTML = '';
+			        }, 500); // Match the CSS transition duration
+			***/
+		}, 3000); // 3 seconds
+	},
+
+
+	/**
+	 * Activate a plugin tab by plugin name.
+	 */
+	activatePlugin: function(pluginName) {
+		var self = this;
+		var pluginButton = Array.from(self.buttonsContainer.querySelectorAll('.cbi-tab-button'))
+			.find(btn => btn.firstChild.textContent === pluginName);
+
+		if (pluginButton) {
+			pluginButton.click();
+			self.pop(null, `[${PN}]: ` + _('Plugin "%s" has been activated.').format(pluginName), 'success');
+		} else {
+			self.pop(null, `[${PN}]: ` + _('Plugin "%s" not found.').format(pluginName), 'error');
+			console.warn('Plugin not found for activation:', pluginName);
+		}
+	},
+
+
+	/**
+	 * Load plugins from directory, initialize them, and set defaults.
+	 */
+	loadPlugins: function(buttonsContainer, contentsContainer) {
+		var self = this;
+
+		var dispatcherInfo = self.info();
+		self.pluginsRegistry[dispatcherInfo.name] = self;
+		self.default_plugins['Dispatcher'] = dispatcherInfo.name;
+
+		var pluginsPath = '/www/luci-static/resources/view/system/file-plug-manager/plugins/';
+
+		fs.exec('/bin/ls', [pluginsPath]).then(function(result) {
+			var pluginFiles = result.stdout.trim().split('\n');
+			var pluginTypes = {
+				'Editor': [],
+				'Navigation': [],
+				'Settings': [],
+				'Help': [],
+				'Utility': []
+			};
+
+			var loadPromises = pluginFiles.map(function(file) {
+				if (file.endsWith('.js')) {
+					var pluginName = file.slice(0, -3);
+
+					// Check for duplicate names
+					if (self.pluginsRegistry[pluginName]) {
+						self.pop(null, `[${PN}]: ` + _('Duplicate plugin name "%s" found. Skipping.').format(pluginName));
+						console.warn('Duplicate plugin name:', pluginName);
+						return Promise.resolve();
+					}
+
+					return L.require('view.system.file-plug-manager.plugins.' + pluginName).then(function(plugin) {
+						// Validate required functions
+						if (typeof plugin.info !== 'function' ||
+							typeof plugin.get_settings !== 'function' ||
+							typeof plugin.set_settings !== 'function') {
+							self.pop(null, `[${PN}]: ` + _('Plugin "%s" is missing required functions. Skipping.').format(pluginName));
+							console.warn('Plugin missing required functions:', pluginName);
+							return;
+						}
+
+						var info = plugin.info();
+						if (!info.name || !info.type) {
+							self.pop(null, `[${PN}]: ` + _('Plugin "%s" has invalid info. Skipping.').format(pluginName));
+							console.warn('Plugin has invalid info:', pluginName);
+							return;
+						}
+
+						if (!self.supportedPluginTypes.includes(info.type)) {
+							self.pop(null, `[${PN}]: ` + _('Plugin "%s" has unsupported type "%s". Skipping.').format(info.name, info.type));
+							console.warn('Unsupported plugin type for plugin:', info.name);
+							return;
+						}
+
+						if (info.type === 'Navigation') {
+							if (typeof plugin.read_file !== 'function' ||
+								typeof plugin.write_file !== 'function') {
+								self.pop(null, `[${PN}]: ` + _('Navigation plugin "%s" is missing required functions. Skipping.').format(info.name));
+								console.warn('Navigation plugin missing required functions:', info.name);
+								return;
+							}
+						}
+
+						if (info.type === 'Settings') {
+							if (typeof plugin.read_settings !== 'function') {
+								self.pop(null, `[${PN}]: ` + _('Settings plugin "%s" is missing read_settings. Skipping.').format(info.name));
+								console.warn('Settings plugin missing read_settings:', info.name);
+								return;
+							}
+						}
+
+						// Register plugin
+						self.pluginsRegistry[info.name] = plugin;
+						pluginTypes[info.type].push(info.name);
+
+						// Load plugin CSS if provided
+						// if (plugin.css) {
+						// self.loadCSS(plugin.css);
+						// }
+					}).catch(function(err) {
+						self.pop(null, `[${PN}]: ` + _('Error loading plugin "%s".').format(pluginName));
+						console.error('Error loading plugin:', pluginName, err);
+					});
+				} else {
+					// Non-JS file
+					self.pop(null, `[${PN}]: ` + _('Ignored non-JS file "%s" in plugins directory.').format(file));
+					return Promise.resolve();
+				}
+			});
+
+			Promise.all(loadPromises).then(function() {
+				self.setDefaultPlugins(pluginTypes);
+
+				// Organize plugins according to defaultTabsOrder
+				self.defaultTabsOrder.forEach(function(type) {
+					var pluginsOfType = pluginTypes[type];
+					if (!pluginsOfType || pluginsOfType.length === 0) {
+						return;
+					}
+
+					// Ensure default plugin is first
+					var defaultPlugin = self.default_plugins[type];
+					if (defaultPlugin && pluginsOfType.includes(defaultPlugin)) {
+						pluginsOfType.sort(function(a, b) {
+							if (a === defaultPlugin) return -1;
+							if (b === defaultPlugin) return 1;
+							return 0;
+						});
+					}
+
+					// Create tabs for each plugin in the sorted order
+					pluginsOfType.forEach(function(pluginName) {
+						var plugin = self.pluginsRegistry[pluginName];
+						if (plugin) {
+							var info = plugin.info();
+							self.createTab(buttonsContainer, contentsContainer, info, plugin);
+						}
+					});
+				});
+
+				// Start all plugins except Settings and Dispatcher
+				for (var pName in self.pluginsRegistry) {
+					if (self.pluginsRegistry.hasOwnProperty(pName)) {
+						var p = self.pluginsRegistry[pName];
+						if (p && typeof p.info === 'function') {
+							var pInfo = p.info();
+							if (pInfo.type !== 'Settings' && pInfo.type !== 'Dispatcher' && typeof p.start === 'function') {
+								var tabEl = document.getElementById('tab-' + pInfo.name);
+								if (tabEl) {
+									p.start(tabEl, self.pluginsRegistry, self.default_plugins, `${self.pluginUniqueIdCounter++}`);
+								} else {
+									console.warn(`[${PN}]: Tab element for plugin "${pInfo.name}" not found.`);
+								}
+							}
+						}
+					}
+				}
+
+				// Start the default Settings plugin last
+				if (self.default_plugins['Settings']) {
+					var settingsPlugin = self.pluginsRegistry[self.default_plugins['Settings']];
+					if (settingsPlugin && typeof settingsPlugin.start === 'function') {
+						var tabEl = document.getElementById('tab-' + self.default_plugins['Settings']);
+						if (tabEl) {
+							settingsPlugin.start(tabEl, self.pluginsRegistry, self.default_plugins, `${self.pluginUniqueIdCounter++}`);
+
+							// Read settings after starting the settings plugin
+							if (typeof settingsPlugin.read_settings === 'function') {
+								settingsPlugin.read_settings().then(function() {
+									self.pop(null, `[${PN}]: ` + _('Settings loaded successfully.'));
+								}).catch(function(err) {
+									self.pop(null, `[${PN}]: ` + _('Error reading settings.'), 'error');
+									console.error('Error reading settings:', err);
+								});
+							} else {
+								self.pop(null, `[${PN}]: ` + _('Settings plugin does not implement read_settings.'), 'error');
+							}
+						} else {
+							self.pop(null, `[${PN}]: ` + _('Tab for default Settings plugin not found.'), 'error');
+						}
+					} else {
+						self.pop(null, `[${PN}]: ` + _('Default Settings plugin not found or cannot be started.'), 'error');
+					}
+				} else {
+					self.pop(null, `[${PN}]: ` + _('No default Settings plugin available.'), 'error');
+				}
+
+				// Activate the default start plugin
+				if (self.defaultStartPlugin) {
+					self.activatePlugin(self.defaultStartPlugin);
+				}
+
+				self.updateMarkers();
+			});
+		}).catch(function(err) {
+			self.pop(null, `[${PN}]: ` + _('Error executing ls to load plugins.'));
+			console.error('Error executing ls:', err);
+		});
+	},
+
+	/**
+	 * setDefaultPlugins(pluginTypes)
+	 * Set default plugins for each type based on priority order.
+	 */
+	setDefaultPlugins: function(pluginTypes) {
+		var self = this;
+
+		// Ensure the Dispatcher is registered as a plugin
+		pluginTypes['Dispatcher'] = ['Main Dispatcher'];
+
+		var preferredDefaults = {
+			'Editor': 'Text Editor',
+			'Navigation': 'Navigation',
+			'Settings': 'Settings Manager',
+			'Help': 'Help Center',
+			'Utility': 'Utility Tool',
+			'Dispatcher': 'Main Dispatcher'
+		};
+
+		self.supportedPluginTypes.forEach(function(type) {
+			if (pluginTypes[type].includes(preferredDefaults[type])) {
+				self.default_plugins[type] = preferredDefaults[type];
+			} else if (pluginTypes[type].length > 0) {
+				self.default_plugins[type] = pluginTypes[type][0];
+			} else {
+				self.default_plugins[type] = null;
+				self.pop(null, `[${PN}]: ` + _('No plugins available for type "%s".').format(type));
+			}
+		});
+	},
+
+	/**
+	 * loadCSS(cssContent)
+	 * Load CSS from a plugin into the document head.
+	 */
+	loadCSS: function(cssContent) {
+		var style = document.createElement('style');
+		style.type = 'text/css';
+		style.innerHTML = cssContent;
+		document.head.appendChild(style);
+	},
+
+	/**
+	 * createTab(buttonsContainer, contentsContainer, info, plugin)
+	 * Create a tab for a plugin without starting it here.
+	 */
+	createTab: function(buttonsContainer, contentsContainer, info, plugin) {
+		var self = this;
+
+		var tabButton = E('button', {
+			'class': 'cbi-tab-button'
+		}, info.name);
+		var marker = E('span', {
+			'class': 'default-marker inactive',
+			'title': _('Set as default')
+		});
+		tabButton.appendChild(marker);
+
+		var tabContent = E('div', {
+			'class': 'cbi-tab-content',
+			'id': 'tab-' + info.name
+		});
+
+		buttonsContainer.appendChild(tabButton);
+		contentsContainer.appendChild(tabContent);
+
+		// Tab button click
+		tabButton.onclick = function() {
+			var allButtons = buttonsContainer.querySelectorAll('.cbi-tab-button');
+			allButtons.forEach(function(btn) {
+				btn.classList.remove('active');
+			});
+
+			var allContents = contentsContainer.querySelectorAll('.cbi-tab-content');
+			allContents.forEach(function(content) {
+				content.classList.remove('active');
+			});
+
+			tabButton.classList.add('active');
+			tabContent.classList.add('active');
+
+			var eventName = `tab-${info.name}`;
+			var event = new Event(eventName);
+			document.dispatchEvent(event);
+			console.log(`[Main Dispatcher] "${eventName}" Event sent.`);
+		};
+
+		// Marker click to set default
+		marker.onclick = function(e) {
+			e.stopPropagation();
+			if (self.supportedPluginTypes.includes(info.type)) {
+				self.default_plugins[info.type] = info.name;
+				self.updateMarkers();
+				self.pop(null, `[${PN}]: ` + _('Set "%s" as the default %s plugin.').format(info.name, info.type));
+			}
+		};
+
+		// Drag and drop for Editor or Utility
+		if (info.type === 'Editor' || info.type === 'Utility') {
+			tabButton.setAttribute('draggable', 'true');
+
+			tabButton.addEventListener('dragover', function(e) {
+				e.preventDefault();
+				e.dataTransfer.dropEffect = 'copy';
+			});
+
+			// Modify the drop event handler to listen for 'application/myapp-files'
+			tabButton.addEventListener('drop', function(e) {
+				e.preventDefault();
+
+				// Attempt to retrieve the custom MIME type data
+				var data = e.dataTransfer.getData('application/myapp-files');
+
+				if (data) {
+					try {
+						// Parse the JSON string to get the array of file paths
+						var filePaths = JSON.parse(data);
+
+						if (Array.isArray(filePaths)) {
+							// Handle multiple files
+							filePaths.forEach(function(filePath) {
+								self.openFileInPlugin(filePath, info.type, info.name);
+							});
+						} else {
+							// Handle single file
+							self.openFileInPlugin(data, info.type, info.name);
+						}
+					} catch (err) {
+						// If parsing fails, log the error
+						self.pop(null, `[${PN}]: ` + _('Error parsing dropped data.'));
+						console.error('Error parsing dropped data:', err);
+					}
+				} else {
+					// If custom MIME type data is not present, you can handle other drop types or ignore
+					self.pop(null, `[${PN}]: ` + _('Unsupported drop data.'));
+					console.warn('Unsupported drop data received.');
+				}
+			});
+		}
+
+		// Auto-activate first tab
+		if (buttonsContainer.querySelectorAll('.cbi-tab-button').length === 1) {
+			tabButton.click();
+		}
+	},
+
+	/**
+	 * updateMarkers()
+	 * Update the default markers to show which plugins are default.
+	 */
+	updateMarkers: function() {
+		var self = this;
+		var buttons = self.buttonsContainer.querySelectorAll('.cbi-tab-button');
+
+		buttons.forEach(function(btn) {
+			var pluginName = btn.firstChild.textContent;
+			var marker = btn.querySelector('.default-marker');
+
+			// If marker does not exist, skip this button
+			if (!marker) {
+				return;
+			}
+
+			// Special handling for Logs tab to keep its marker active
+			if (btn.classList.contains('logs-tab')) {
+				marker.classList.add('active');
+				marker.classList.remove('inactive');
+				return;
+			}
+
+			var pluginType = null;
+			for (var type in self.default_plugins) {
+				if (self.default_plugins[type] === pluginName) {
+					pluginType = type;
+					break;
+				}
+			}
+
+			if (pluginType && self.default_plugins[pluginType] === pluginName) {
+				marker.classList.add('active');
+				marker.classList.remove('inactive');
+			} else {
+				marker.classList.add('inactive');
+				marker.classList.remove('active');
+			}
+		});
+	},
+
+	/**
+	 * openFileInPlugin(filePath, pluginType, pluginName)
+	 * Opens a file in the specified plugin.
+	 */
+	openFileInPlugin: function(filePath, pluginType, pluginName) {
+		var self = this;
+
+		if (pluginType === 'Editor') {
+			var editorPlugin = self.pluginsRegistry[pluginName];
+			if (!editorPlugin || typeof editorPlugin.edit !== 'function') {
+				self.pop(null, `[${PN}]: ` + _('Target editor plugin does not support editing files.'));
+				return;
+			}
+
+			if (!self.default_plugins['Navigation']) {
+				self.pop(null, `[${PN}]: ` + _('No default Navigation plugin set.'));
+				return;
+			}
+
+			var navigationPlugin = self.pluginsRegistry[self.default_plugins['Navigation']];
+			if (!navigationPlugin || typeof navigationPlugin.read_file !== 'function') {
+				self.pop(null, `[${PN}]: ` + _('Default Navigation plugin does not support reading files.'));
+				return;
+			}
+
+			var editorInfo = editorPlugin.info();
+			var style = editorInfo.style || 'text';
+
+			navigationPlugin.read_file(filePath, style).then(function(fileData) {
+				editorPlugin.edit(filePath, fileData.content, style, fileData.permissions, fileData.GroupOwner);
+				self.activatePlugin(pluginName);
+				self.pop(null, `[${PN}]: ` + _('File "%s" opened in editor.').format(filePath), 'success');
+			}).catch(function(err) {
+				self.pop(null, `[${PN}]: ` + _('Error reading file "%s".').format(filePath), 'error');
+				console.error('Error reading file:', filePath, err);
+			});
+		} else if (pluginType === 'Navigation') {
+			self.pop(null, `[${PN}]: ` + _('Navigation plugin does not handle direct file opening.'));
+		}
+	},
+
+	handleSave: null,
+	handleSaveApply: null,
+	handleReset: null
+});
diff --git a/applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Edit+.js b/applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Edit+.js
new file mode 100644
index 000000000000..832a68f79fd3
--- /dev/null
+++ b/applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Edit+.js
@@ -0,0 +1,864 @@
+'use strict';
+'require ui';
+'require dom';
+
+/**
+ * Text Editor Plugin with Search & Replace functionality
+ * Provides a simple text editor with a resizable window, scrollbars, and save functionality.
+ * Supports loading external text content for editing and displays the filename.
+ * Includes configuration for window size.
+ * 
+ * Enhancements:
+ * - Prevents line wrapping with a horizontal scrollbar.
+ * - Displays line numbers alongside the textarea.
+ * - Search and Replace interface
+ * - A search pattern input field and "Find Next" and "Find Previous" buttons.
+ * - A replace pattern input field and "Replace This" and "Replace All" buttons.
+ * - A toggle switch to select between Normal and RegExp search modes.
+ * - Global search highlights all occurrences of the pattern in the text.
+ * - The matched text is highlighted in orange.
+ * - Info field shows total matches found and the index of the currently selected match.
+ * - "Find Next" scrolls to the next pattern occurrence.
+ * - "Find Previous" scrolls to the previous pattern occurrence.
+ * - "Replace This" replaces the current match and moves to the next one.
+ * - "Replace All" replaces all occurrences and scrolls to the end.
+ */
+
+// Define the plugin name as a constant
+const PN = 'Text Editor+';
+
+return Class.extend({
+	/**
+	 * Returns metadata about the plugin.
+	 * @returns {Object} Plugin information.
+	 */
+	info: function() {
+		return {
+			name: PN,
+			type: 'Editor',
+			style: 'Text',
+			description: 'A text editor plugin with search & replace, resizable window, scrollbars, save functionality, and search mode toggle (Normal/RegExp).'
+		};
+	},
+
+	/**
+	 * Generates CSS styles for the Text Editor plugin with a unique suffix.
+	 * @param {string} uniqueId - The unique identifier for this plugin instance.
+	 * @returns {string} - The CSS styles as a string.
+	 */
+	generateCss: function(uniqueId) {
+		return `
+            /* CSS for the Text Editor Plugin - Instance ${uniqueId} */
+            .text-editor-plugin-${uniqueId} {
+                padding: 10px;
+                background-color: #ffffff;
+                border: 1px solid #ccc;
+                resize: both;
+                overflow: hidden;
+                box-shadow: 2px 2px 5px rgba(0,0,0,0.1);
+                font-family: Arial, sans-serif;
+                font-size: 14px;
+                position: relative;
+                display: flex;
+                flex-direction: column;
+                height: 100%;
+            }
+
+            .text-editor-plugin-${uniqueId} .filename-display {
+                margin-bottom: 10px;
+                font-weight: bold;
+                color: #333;
+                font-size: 16px;
+            }
+
+            .text-editor-plugin-${uniqueId} .editor-container {
+                display: flex;
+                flex: 1;
+                overflow: auto; /* Allow only one scrollbar */
+                align-items: flex-start;
+                position: relative;
+            }
+
+            .text-editor-plugin-${uniqueId} .line-numbers {
+                width: 50px;
+                background-color: #f0f0f0;
+                color: #888;
+                text-align: right;
+                user-select: none;
+                border-right: 1px solid #ccc;
+                box-sizing: border-box;
+                font-family: monospace;
+                font-size: 14px;
+                line-height: 1.5;
+                white-space: pre;
+                padding: 5px 0;
+                position: sticky;
+                top: 0;
+                left: 0;
+            }
+
+            .text-editor-plugin-${uniqueId} .editable-content {
+                flex: 1;
+                font-family: monospace;
+                font-size: 14px;
+                line-height: 1.5;
+                padding: 5px 10px;
+                margin: 0;
+                border: none;
+                outline: none;
+                white-space: pre;
+                background-color: #ffffff;
+                color: #000000;
+            }
+
+            .text-editor-plugin-${uniqueId} .highlight {
+                background: none;
+                color: orange;
+                font-weight: bold;
+            }
+
+            .text-editor-plugin-${uniqueId} .highlight.current-highlight {
+                background: orange;
+                color: #ffffff; /* For better contrast */
+            }
+
+            .text-editor-plugin-${uniqueId} .controls-container {
+                display: flex;
+                justify-content: space-between;
+                align-items: center;
+                margin-top: 10px;
+                flex-wrap: wrap;
+                gap: 10px;
+            }
+
+            .text-editor-plugin-${uniqueId} .search-container {
+                display: flex;
+                flex-direction: row;
+                gap: 5px;
+                flex-wrap: wrap;
+                align-items: center;
+            }
+
+            .text-editor-plugin-${uniqueId} .search-info {
+                margin-top: 5px;
+                font-size: 14px;
+                color: #333;
+            }
+
+            .text-editor-plugin-${uniqueId} .button {
+                padding: 8px 16px;
+                background-color: #0078d7;
+                color: #fff;
+                border: none;
+                border-radius: 4px;
+                cursor: pointer;
+                font-size: 14px;
+            }
+
+            .text-editor-plugin-${uniqueId} .button:hover {
+                background-color: #005fa3;
+            }
+
+            .text-editor-plugin-${uniqueId} .toggle-container {
+                display: flex;
+                align-items: center;
+                gap: 5px;
+            }
+
+            .text-editor-plugin-${uniqueId} .switch {
+                position: relative;
+                display: inline-block;
+                width: 50px;
+                height: 24px;
+            }
+
+            .text-editor-plugin-${uniqueId} .switch input {
+                opacity: 0;
+                width: 0;
+                height: 0;
+            }
+
+            .text-editor-plugin-${uniqueId} .slider {
+                position: absolute;
+                cursor: pointer;
+                top: 0;
+                left: 0;
+                right: 0;
+                bottom: 0;
+                background-color: #ccc;
+                transition: 0.4s;
+                border-radius: 24px;
+            }
+
+            .text-editor-plugin-${uniqueId} .slider:before {
+                position: absolute;
+                content: "";
+                height: 18px;
+                width: 18px;
+                left: 3px;
+                bottom: 3px;
+                background-color: white;
+                transition: 0.4s;
+                border-radius: 50%;
+            }
+
+            .text-editor-plugin-${uniqueId} .switch input:checked + .slider {
+                background-color: #2196F3;
+            }
+
+            .text-editor-plugin-${uniqueId} .switch input:checked + .slider:before {
+                transform: translateX(26px);
+            }
+
+            .dark-theme .text-editor-plugin-${uniqueId} {
+                background-color: #2a2a2a;
+                border-color: #555;
+                color: #ddd;
+            }
+
+            .dark-theme .text-editor-plugin-${uniqueId} .filename-display {
+                color: #fff;
+            }
+
+            .dark-theme .text-editor-plugin-${uniqueId} .line-numbers {
+                background-color: #3a3a3a;
+                color: #ccc;
+                border-right: 1px solid #555;
+            }
+
+            .dark-theme .text-editor-plugin-${uniqueId} .editable-content {
+                background-color: #1e1e1e;
+                color: #f1f1f1;
+                border: 1px solid #555;
+            }
+
+            .dark-theme .text-editor-plugin-${uniqueId} .button {
+                background-color: #1e90ff;
+            }
+
+            .dark-theme .text-editor-plugin-${uniqueId} .button:hover {
+                background-color: #1c7ed6;
+            }
+
+            .dark-theme .text-editor-plugin-${uniqueId} .slider {
+                background-color: #555;
+            }
+
+            .dark-theme .text-editor-plugin-${uniqueId} .switch input:checked + .slider {
+                background-color: #1e90ff;
+            }
+        `;
+	},
+
+	/**
+	 * Escapes special characters in a string to be used in a regular expression.
+	 * @param {string} string - The string to escape.
+	 * @returns {string} - The escaped string.
+	 */
+	escapeRegExp: function(string) {
+		return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+	},
+
+	/**
+	 * Initializes the plugin within the given container.
+	 * @param {HTMLElement} container - The container element where the plugin will be rendered.
+	 * @param {Object} pluginsRegistry - The registry of all loaded plugins.
+	 * @param {Object} default_plugins - The default plugins for each type.
+	 * @param {string} uniqueId - A unique identifier for this plugin instance.
+	 */
+	start: function(container, pluginsRegistry, default_plugins, uniqueId) {
+		var self = this;
+
+		// Ensure initialization runs only once
+		if (self.initialized) {
+			return;
+		}
+		self.initialized = true;
+
+		self.pluginsRegistry = pluginsRegistry;
+		self.default_plugins = default_plugins;
+		self.uniqueId = uniqueId;
+
+		// Insert unique CSS
+		var styleTag = document.createElement('style');
+		styleTag.type = 'text/css';
+		styleTag.id = `text-editor-plugin-style-${uniqueId}`;
+		styleTag.innerHTML = self.generateCss(uniqueId);
+		document.head.appendChild(styleTag);
+		self.styleTag = styleTag;
+
+		// Main container
+		self.editorDiv = document.createElement('div');
+		self.editorDiv.className = `text-editor-plugin-${uniqueId}`;
+
+		// Initial size
+		self.width = self.settings && self.settings.width ? self.settings.width : '600px';
+		self.height = self.settings && self.settings.height ? self.settings.height : '400px';
+		self.editorDiv.style.width = self.width;
+		self.editorDiv.style.height = self.height;
+
+		// Filename display
+		self.filenameDisplay = document.createElement('div');
+		self.filenameDisplay.className = 'filename-display';
+		self.filenameDisplay.textContent = 'No file loaded.';
+
+		// Editor container
+		self.editorContainer = document.createElement('div');
+		self.editorContainer.className = 'editor-container';
+
+		// Line numbers
+		self.lineNumbers = document.createElement('div');
+		self.lineNumbers.className = 'line-numbers';
+		self.lineNumbers.textContent = '1';
+
+		// Editable content area
+		self.editableContent = document.createElement('div');
+		self.editableContent.className = 'editable-content';
+		self.editableContent.contentEditable = 'true';
+
+		// Append to editor container
+		self.editorContainer.appendChild(self.lineNumbers);
+		self.editorContainer.appendChild(self.editableContent);
+
+		// Controls container (Save button and Search controls)
+		self.controlsContainer = document.createElement('div');
+		self.controlsContainer.className = 'controls-container';
+
+		// Save button
+		self.saveButton = document.createElement('button');
+		self.saveButton.className = 'button';
+		self.saveButton.textContent = 'Save';
+		self.saveButton.onclick = self.saveFile.bind(this);
+
+		// Search & Replace UI
+		self.searchContainer = document.createElement('div');
+		self.searchContainer.className = 'search-container';
+
+		// Search input
+		self.searchInput = document.createElement('input');
+		self.searchInput.type = 'text';
+		self.searchInput.placeholder = 'Search pattern...';
+
+		// Find Next button
+		self.findNextButton = document.createElement('button');
+		self.findNextButton.className = 'button';
+		self.findNextButton.textContent = 'Find Next';
+		self.findNextButton.onclick = self.findNext.bind(self);
+
+		// Find Previous button
+		self.findPrevButton = document.createElement('button');
+		self.findPrevButton.className = 'button';
+		self.findPrevButton.textContent = 'Find Previous';
+		self.findPrevButton.onclick = self.findPrevious.bind(self);
+
+		// Replace input
+		self.replaceInput = document.createElement('input');
+		self.replaceInput.type = 'text';
+		self.replaceInput.placeholder = 'Replace with...';
+
+		// Replace This button
+		self.replaceThisButton = document.createElement('button');
+		self.replaceThisButton.className = 'button';
+		self.replaceThisButton.textContent = 'Replace This';
+		self.replaceThisButton.onclick = self.replaceThis.bind(self);
+
+		// Replace All button
+		self.replaceAllButton = document.createElement('button');
+		self.replaceAllButton.className = 'button';
+		self.replaceAllButton.textContent = 'Replace All';
+		self.replaceAllButton.onclick = self.replaceAll.bind(self);
+
+		// Toggle Search Type (Normal / RegExp)
+		self.toggleContainer = document.createElement('div');
+		self.toggleContainer.className = 'toggle-container';
+
+		// Toggle Switch
+		self.switchLabel = document.createElement('label');
+		self.switchLabel.className = 'switch';
+
+		self.switchInput = document.createElement('input');
+		self.switchInput.type = 'checkbox';
+		self.switchInput.id = `search-toggle-${uniqueId}`;
+		self.switchInput.onclick = self.toggleSearchType.bind(self);
+
+		self.switchSlider = document.createElement('span');
+		self.switchSlider.className = 'slider';
+
+		self.switchLabel.appendChild(self.switchInput);
+		self.switchLabel.appendChild(self.switchSlider);
+
+		// Toggle Label Text
+		self.toggleLabelText = document.createElement('span');
+		self.toggleLabelText.textContent = 'RegExp';
+
+		self.toggleContainer.appendChild(self.switchLabel);
+		self.toggleContainer.appendChild(self.toggleLabelText);
+
+		// Append search and replace elements
+		self.searchContainer.appendChild(self.searchInput);
+		self.searchContainer.appendChild(self.findNextButton);
+		self.searchContainer.appendChild(self.findPrevButton);
+		self.searchContainer.appendChild(self.replaceInput);
+		self.searchContainer.appendChild(self.replaceThisButton);
+		self.searchContainer.appendChild(self.replaceAllButton);
+		self.searchContainer.appendChild(self.toggleContainer);
+
+		// Append Save button and Search controls to controls container
+		self.controlsContainer.appendChild(self.searchContainer);
+		self.controlsContainer.appendChild(self.saveButton);
+
+		// Info field for matches
+		self.infoField = document.createElement('div');
+		self.infoField.className = 'search-info';
+
+		// Append elements to main editor div
+		self.editorDiv.appendChild(self.filenameDisplay);
+		self.editorDiv.appendChild(self.editorContainer);
+		self.editorDiv.appendChild(self.controlsContainer);
+		self.editorDiv.appendChild(self.infoField);
+
+		container.appendChild(self.editorDiv);
+
+		// Default dispatcher
+		var defaultDispatcherName = self.default_plugins['Dispatcher'];
+		if (defaultDispatcherName && self.pluginsRegistry[defaultDispatcherName]) {
+			var defaultDispatcher = self.pluginsRegistry[defaultDispatcherName];
+			self.popm = defaultDispatcher.pop.bind(defaultDispatcher);
+		}
+
+		// Navigation plugin for file operations
+		var navigationPluginName = self.default_plugins['Navigation'];
+		if (!navigationPluginName) {
+			self.popm(null, `[${PN}]: No default Navigation plugin set.`);
+			console.error('No default Navigation plugin set.');
+			return;
+		}
+
+		var navigationPlugin = self.pluginsRegistry[navigationPluginName];
+		if (!navigationPlugin || typeof navigationPlugin.write_file !== 'function') {
+			self.popm(null, `[${PN}]: Navigation plugin does not support writing files.`);
+			console.error('Navigation plugin is unavailable or missing write_file function.');
+			return;
+		}
+
+		// Bind write_file
+		self.write_file = navigationPlugin.write_file.bind(navigationPlugin);
+
+		// Set initial variables
+		self.textData = '';
+		self.matches = [];
+		self.currentMatchIndex = -1;
+		self.lastSearchPattern = '';
+		self.lastIsRegExp = false;
+		self.isRegExp = false;
+
+		// Sync scroll for line numbers
+		self.editableContent.addEventListener('scroll', function() {
+			const scrollTop = self.editableContent.scrollTop;
+			self.lineNumbers.style.transform = `translateY(-${scrollTop}px)`;
+		});
+
+		self.updating = false;
+
+		// Recalculate line numbers on input
+		self.editableContent.addEventListener('input', function() {
+			self.textData = self.getRawText();
+			self.updateLineNumbers();
+		});
+	},
+
+	/**
+	 * Opens a file in the editor.
+	 * @param {string} filePath - The path to the file to edit.
+	 * @param {string} content - The content of the file.
+	 * @param {string} style - The style of the content ('text' or 'bin').
+	 */
+	edit: function(filePath, content, style, permissions, ownerGroup) {
+		var self = this;
+
+		if (style.toLowerCase() !== 'text') {
+			self.popm(null, `[${PN}]: Unsupported style "${style}". Only "Text" is supported.`);
+			console.warn('Unsupported style:', style);
+			self.filenameDisplay.textContent = 'Unsupported file style.';
+			self.textData = '';
+			self.render();
+			return;
+		}
+
+		self.currentFilePath = filePath;
+		self.permissions = permissions;
+		self.ownerGroup = ownerGroup;
+
+		self.textData = content;
+		var parts = filePath.split('/');
+		var filename = parts[parts.length - 1];
+		self.filenameDisplay.textContent = `Editing: ${filename}`;
+
+		self.updateLineNumbers();
+
+		// Reset search-related variables
+		self.lastSearchPattern = '';
+		self.matches = [];
+		self.currentMatchIndex = -1;
+		self.lastIsRegExp = false;
+		self.isRegExp = false;
+
+		// Reset the toggle switch to Normal search
+		self.switchInput.checked = false;
+		self.toggleLabelText.textContent = 'RegExp';
+
+		self.render();
+		self.popm(null, `[${PN}]: Opened file "${filename}".`);
+	},
+
+	/**
+	 * Save the file using the Navigation plugin.
+	 */
+	saveFile: function(ev) {
+		var self = this;
+
+		if (!self.currentFilePath) {
+			self.popm(null, `[${PN}]: No file loaded to save.`);
+			return;
+		}
+
+		var content = self.getRawText();
+
+		self.write_file(self.currentFilePath, self.permissions, self.ownerGroup, content, 'text')
+			.then(function() {
+				self.popm(null, `[${PN}]: File saved successfully.`);
+			})
+			.catch(function(err) {
+				self.popm(null, `[${PN}]: Error saving file.`);
+				console.error('Error saving file:', err);
+			});
+	},
+
+	/**
+	 * Get current settings.
+	 */
+	get_settings: function() {
+		return {
+			width: this.editorDiv.style.width,
+			height: this.editorDiv.style.height
+		};
+	},
+
+	/**
+	 * Set plugin settings.
+	 */
+	set_settings: function(settings) {
+		if (settings.width) {
+			this.editorDiv.style.width = settings.width;
+		}
+		if (settings.height) {
+			this.editorDiv.style.height = settings.height;
+		}
+	},
+
+	/**
+	 * Destroy the plugin instance.
+	 */
+	destroy: function() {
+		var self = this;
+		if (self.styleTag) {
+			self.styleTag.remove();
+		}
+		if (self.editorDiv && self.editorDiv.parentNode) {
+			self.editorDiv.parentNode.removeChild(self.editorDiv);
+		}
+		self.initialized = false;
+	},
+
+	/**
+	 * Update line numbers according to the current text.
+	 */
+	updateLineNumbers: function() {
+		var self = this;
+		var linesCount = self.textData.split('\n').length;
+		var lineNumbersContent = '';
+		for (let i = 1; i <= linesCount; i++) {
+			lineNumbersContent += i + '\n';
+		}
+		self.lineNumbers.textContent = lineNumbersContent;
+	},
+
+	/**
+	 * Get the raw text without any highlighting from editable content.
+	 */
+	getRawText: function() {
+		return this.editableContent.textContent;
+	},
+
+	/**
+	 * Render the content with highlights.
+	 */
+	render: function() {
+		var self = this;
+		self.updating = true; // Begin of inner update
+
+		if (!self.matches || self.matches.length === 0) {
+			self.editableContent.innerHTML = self.escapeHtml(self.textData);
+		} else {
+			var htmlParts = [];
+			var lastIndex = 0;
+			for (var i = 0; i < self.matches.length; i++) {
+				var m = self.matches[i];
+				htmlParts.push(self.escapeHtml(self.textData.substring(lastIndex, m.start)));
+				if (i === self.currentMatchIndex) {
+					htmlParts.push('<span class="highlight current-highlight">');
+				} else {
+					htmlParts.push('<span class="highlight">');
+				}
+				htmlParts.push(self.escapeHtml(self.textData.substring(m.start, m.end)));
+				htmlParts.push('</span>');
+				lastIndex = m.end;
+			}
+			htmlParts.push(self.escapeHtml(self.textData.substring(lastIndex)));
+			self.editableContent.innerHTML = htmlParts.join('');
+		}
+		self.updating = false; // End of inner update
+		self.updateLineNumbers();
+		self.updateInfoField();
+		self.scrollToCurrentMatch();
+	},
+
+	/**
+	 * Escape HTML to prevent issues.
+	 */
+	escapeHtml: function(str) {
+		return str.replace(/&/g, '&amp;')
+			.replace(/</g, '&lt;')
+			.replace(/>/g, '&gt;')
+			.replace(/"/g, '&quot;');
+	},
+
+	/**
+	 * Toggle between Normal and RegExp search modes.
+	 */
+	toggleSearchType: function() {
+		var self = this;
+		self.isRegExp = self.switchInput.checked;
+		self.toggleLabelText.textContent = self.isRegExp ? 'RegExp' : 'Normal';
+
+		self.lastIsRegExp = self.isRegExp;
+		// Re-run search to reflect the new mode
+		self.performSearch();
+		self.render();
+	},
+
+	/**
+	 * Perform a global search on the textData and store matches.
+	 */
+	performSearch: function() {
+		var self = this;
+		var pattern = self.searchInput.value;
+
+		if (!pattern) {
+			self.matches = [];
+			self.currentMatchIndex = -1;
+			self.render();
+			return;
+		}
+
+		var re;
+		if (self.isRegExp) {
+			try {
+				re = new RegExp(pattern, 'g');
+			} catch (e) {
+				self.updateInfoField("Invalid RegExp pattern.");
+				return;
+			}
+		} else {
+			var escapedPattern = self.escapeRegExp(pattern);
+			re = new RegExp(escapedPattern, 'g');
+		}
+
+		self.matches = [];
+		var match;
+		while ((match = re.exec(self.textData)) !== null) {
+			self.matches.push({
+				start: match.index,
+				end: match.index + match[0].length
+			});
+			if (match.index === re.lastIndex) {
+				re.lastIndex++;
+			}
+		}
+
+		// Корректировка currentMatchIndex
+		if (self.matches.length > 0) {
+			if (self.currentMatchIndex >= self.matches.length) {
+				self.currentMatchIndex = self.matches.length - 1;
+			} else if (self.currentMatchIndex === -1) {
+				self.currentMatchIndex = 0;
+			}
+		} else {
+			self.currentMatchIndex = -1;
+		}
+
+		self.render();
+	},
+
+	/**
+	 * Move to the next match and re-render.
+	 */
+	findNext: function() {
+		var self = this;
+
+		// Выполняем поиск, чтобы обновить matches
+		self.performSearch();
+
+		if (self.matches.length === 0) {
+			self.updateInfoField("No matches found.");
+			return;
+		}
+
+		if (self.currentMatchIndex === -1) {
+			self.currentMatchIndex = 0;
+		} else if (self.currentMatchIndex < self.matches.length - 1) {
+			self.currentMatchIndex++;
+		} else {
+			self.currentMatchIndex = 0;
+		}
+
+		self.render();
+	},
+
+	/**
+	 * Move to the previous match and re-render.
+	 */
+	findPrevious: function() {
+		var self = this;
+
+		// Выполняем поиск, чтобы обновить matches
+		self.performSearch();
+
+		if (self.matches.length === 0) {
+			self.updateInfoField("No matches found.");
+			return;
+		}
+
+		if (self.currentMatchIndex === -1) {
+			self.currentMatchIndex = self.matches.length - 1;
+		} else if (self.currentMatchIndex > 0) {
+			self.currentMatchIndex--;
+		} else {
+			self.currentMatchIndex = self.matches.length - 1;
+		}
+
+		self.render();
+	},
+
+	/**
+	 * Replace the current match with the specified replacement text.
+	 * Then move to the next match.
+	 */
+	replaceThis: function() {
+		var self = this;
+
+		if (self.matches.length === 0 || self.currentMatchIndex === -1) {
+			self.updateInfoField("No matches available to replace.");
+			return;
+		}
+
+		var replacement = self.replaceInput.value || '';
+		var currentMatch = self.matches[self.currentMatchIndex];
+
+		// Выполняем замену в textData
+		self.textData = self.textData.substring(0, currentMatch.start) + replacement + self.textData.substring(currentMatch.end);
+
+		// Выполняем поиск, чтобы обновить matches
+		self.performSearch();
+
+		// Если после замены текущий индекс выходит за пределы, устанавливаем его на последний индекс
+		if (self.currentMatchIndex >= self.matches.length) {
+			self.currentMatchIndex = self.matches.length - 1;
+		}
+
+		self.render();
+	},
+
+	/**
+	 * Replace all occurrences of the search pattern with the replacement text.
+	 */
+	replaceAll: function() {
+		var self = this;
+		var pattern = self.searchInput.value;
+		if (!pattern) return;
+		var replacement = self.replaceInput.value || '';
+
+		var re;
+		if (self.isRegExp) {
+			try {
+				re = new RegExp(pattern, 'g');
+			} catch (e) {
+				self.updateInfoField("Invalid RegExp pattern.");
+				return;
+			}
+		} else {
+			var escapedPattern = self.escapeRegExp(pattern);
+			re = new RegExp(escapedPattern, 'g');
+		}
+
+		try {
+			self.textData = self.textData.replace(re, replacement);
+		} catch (e) {
+			self.updateInfoField("Error during replacement.");
+			console.error('Error during replacement:', e);
+			return;
+		}
+
+		// After replace all, re-search and scroll to the end
+		self.performSearch();
+		self.editableContent.scrollTop = self.editableContent.scrollHeight;
+	},
+
+	/**
+	 * Update the info field showing the total matches and current match index.
+	 */
+	updateInfoField: function(optionalMessage) {
+		var self = this;
+		if (optionalMessage) {
+			self.infoField.textContent = optionalMessage;
+			return;
+		}
+
+		if (!self.matches || self.matches.length === 0) {
+			self.infoField.textContent = 'No matches found.';
+		} else {
+			self.infoField.textContent = `Matches: ${self.matches.length}, Current: ${self.currentMatchIndex + 1}`;
+		}
+	},
+
+	/**
+	 * Scroll to the current match in the editableContent.
+	 */
+	scrollToCurrentMatch: function() {
+		var self = this;
+		if (self.currentMatchIndex === -1 || self.matches.length === 0) return;
+
+		var highlights = self.editableContent.querySelectorAll('.highlight');
+		if (highlights.length === 0) return;
+		var target = highlights[self.currentMatchIndex];
+		if (!target) return;
+
+		target.scrollIntoView({
+			block: 'center',
+			behavior: 'smooth'
+		});
+		self.selectText(target);
+	},
+
+	/**
+	 * Select the text within the target element.
+	 * @param {HTMLElement} element - The element containing the text to select.
+	 */
+	selectText: function(element) {
+		var range = document.createRange();
+		var sel = window.getSelection();
+		range.selectNodeContents(element);
+		sel.removeAllRanges();
+		sel.addRange(range);
+	},
+});
\ No newline at end of file
diff --git a/applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js b/applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js
new file mode 100644
index 000000000000..2bfe1fde7710
--- /dev/null
+++ b/applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js
@@ -0,0 +1,2848 @@
+'use strict';
+'require fs';
+'require ui';
+'require rpc';
+
+/***
+# Navigation Plugin: User Functionality Overview
+
+## 1. Directory Navigation and Traversal
+
+- **Path Input Field:**  
+  Users can enter a specific directory path directly into the input field and navigate to it by pressing "Enter" or clicking the "Go" button.
+
+- **Breadcrumb Navigation:**  
+  Displays the current directory path in a breadcrumb format, allowing users to quickly navigate to any parent directory by clicking on the respective breadcrumb link.
+
+- **Clickable Directory Names:**  
+  Users can navigate into subdirectories by clicking on directory names listed in the table.
+
+## 2. File and Directory Operations
+
+- **Upload Files:**
+  - **Upload Button:**  
+    Click to open a file dialog and select multiple files for upload.
+  - **Drag-and-Drop:**  
+    Drag files from the local system and drop them into the navigation area to initiate uploads.
+
+- **Download Files:**  
+  Click the download icon (⬇️) next to a file to download it to the local system.
+
+- **Create New Folder/File:**
+  - **Create Folder Button:**  
+    Prompts the user to enter a folder name and creates it in the current directory.
+  - **Create File Button:**  
+    Prompts the user to enter a file name and creates an empty file in the current directory.
+
+- **Delete Items:**
+  - **Select Items:**  
+    Use checkboxes to select individual files or directories.
+  - **Delete Selected Button:**  
+    Deletes all selected items after user confirmation.
+
+- **Copy and Move Items:**
+  - **Drag-and-Drop Within UI:**  
+    Drag selected files or directories to a target directory to copy or move them.
+    - **Copy Operation:**  
+      Hold the "Alt" key while dragging to copy items.
+    - **Move Operation:**  
+      Drag without holding any modifier keys to move items.
+
+## 3. Bulk Selection and Management
+
+- **Select/Deselect All Checkbox:**  
+  Located in the table header, allows users to select or deselect all items in the current directory view. Pressing with "Alt" inverses current selection
+
+- **Individual Selection:**  
+  Checkboxes next to each item enable selective management of files and directories.
+
+## 4. Editing Files attributes
+
+- **Edit Button (✏️):**  
+  Opens a window for file attributes editing.
+  - **File Attributes:**  
+    Users can rename the file, change its owner and group, and modify permissions directly from the edit interface.
+
+## 5. User Interface Customization
+
+- **Resizable Columns:**  
+  Users can adjust the width of table columns by dragging the resizers between column headers. The plugin enforces minimum widths to maintain usability.
+
+- **Themes:**
+  - **Light Theme:**  
+    Default styling with a white background and dark text.
+  - **Dark Theme:**  
+    Optional dark mode with dark backgrounds and light text for reduced eye strain.
+
+## 6. Drag-and-Drop Enhancements
+
+- **Internal Drag-and-Drop:**
+  - **Visual Indicators:**  
+    Highlight target directories during drag-over events to indicate valid drop zones.
+  - **Action Icons:**  
+    Displays a plus icon (➕) when holding the "Alt" key to signify a copy operation.
+
+- **External Drag-and-Drop:**
+  - **File Downloads:**  
+    Dragging files out of the navigation UI initiates their download to the local system.
+  - **File Uploads:**  
+    Dropping files into the navigation UI area uploads them to the current directory.
+
+## 7. Feedback and Status Indicators
+
+- **Loading Indicators:**  
+  Display a "Loading..." message while fetching directory contents.
+
+- **Progress Bars:**  
+  Show upload progress for individual files.
+
+- **Tooltips:**  
+  Provides additional information when hovering over overflowing text in file names.
+
+- **Status Messages:**  
+  Informs users of successful operations or errors through pop-up messages and inline notifications.
+
+## 8. Advanced Features
+
+- **Permissions and Ownership Management:**  
+  Allows users to view and modify file permissions and ownership directly from the UI.
+
+- **Symbolic Link Handling:**  
+  Properly displays and manages symbolic links, including their targets.
+
+- **Responsive Design:**  
+  Adapts to different screen sizes and container dimensions, ensuring usability across various devices.
+
+## 9. Integration with Other Plugins
+
+- **Editor Plugin Integration:**  
+  Seamlessly works with default editor plugins to provide in-browser file editing capabilities.
+
+- **Dispatcher Integration:**  
+  Utilizes dispatcher plugins for executing file system commands and handling asynchronous operations.
+
+## 10. Error Handling and Validation
+
+- **User Prompts:**  
+  Confirms critical actions like deletions to prevent accidental data loss.
+
+- **Error Notifications:**  
+  Clearly communicates issues such as failed uploads, permission errors, or invalid paths to the user.
+***/
+
+// Define the plugin name as a constant
+const PN = 'Navigation';
+
+return Class.extend({
+	/**
+	 * Provides metadata about the plugin.
+	 * @returns {Object} - Contains at least name, type, and description properties.
+	 */
+	info: function() {
+		return {
+			name: PN,
+			type: 'Navigation',
+			description: 'Enhanced file system navigator with additional functionalities'
+		};
+	},
+
+	/**
+	 * Retrieves the current configuration settings of the plugin.
+	 * @returns {Object} - Key-value pairs of settings.
+	 */
+	get_settings: function() {
+		return this.settings || {};
+	},
+
+	/**
+	 * Default settings for the plugin.
+	 */
+	defaultSettings: {
+		currentDir: '/',
+		width: 900, // Number
+		height: 800, // Number
+		defaultFilePermissions: '644',
+		defaultDirPermissions: '755',
+		defaultOwner: 'root',
+		defaultGroup: 'root',
+		columnWidths: {
+			'select': 30,
+			'name': 200,
+			'type': 100,
+			'size': 100,
+			'mtime': 150,
+			'actions': 150
+		},
+		mincolumnWidths: { // Adding minimum column widths
+			'select': 20,
+			'name': 110,
+			'type': 50,
+			'size': 50,
+			'mtime': 80,
+			'actions': 100
+		}
+	},
+
+	/**
+	 * Applies settings to internal properties and UI elements.
+	 */
+	applySettingsToUI: function() {
+		var self = this;
+
+		// Merging current settings with default settings
+		self.settings = Object.assign({}, self.defaultSettings, self.settings || {});
+
+		// Updating internal plugin properties
+		self.currentDir = self.settings.currentDir;
+		self.defaultFilePermissions = self.settings.defaultFilePermissions;
+		self.defaultDirPermissions = self.settings.defaultDirPermissions;
+		self.columnWidths = self.settings.columnWidths;
+		self.mincolumnWidths = self.settings.mincolumnWidths;
+
+		// Setting fixed sizes for the navigation container
+		if (self.settings.width) {
+			self.navDiv.style.width = self.settings.width + 'px';
+		}
+		if (self.settings.height) {
+			self.navDiv.style.height = self.settings.height + 'px';
+		}
+
+		// Setting sizes for tableContainer
+		self.tableContainer.style.width = '100%';
+		self.tableContainer.style.height = '100%';
+
+		// Applying column widths and calculating total table width
+		var totalWidth = 0;
+		if (self.settings.columnWidths) {
+			Object.keys(self.settings.columnWidths).forEach(function(field) {
+				var newWidth = self.settings.columnWidths[field];
+				var col = self.table.querySelector(`col[data-field="${field}"]`);
+				if (col) {
+					// Ensure that the width is not less than the minimum
+					var minWidth = self.mincolumnWidths[field] || 30; // If mincolumnWidths is not set, use 30px
+					if (newWidth < minWidth) {
+						newWidth = minWidth;
+						self.settings.columnWidths[field] = minWidth; // Update settings if width was reduced
+					}
+					col.style.width = newWidth + 'px';
+					totalWidth += newWidth;
+				}
+			});
+		}
+
+		// Setting the total table width
+		self.table.style.width = totalWidth + 'px';
+
+		// Update the table to apply new widths
+		// You can also call a redraw or recalculate elements if necessary
+
+		// console.log(`[Navigation Plugin] Applied settings to UI:`, self.settings);
+	},
+
+	/**
+	 * Sets the plugin's settings.
+	 * @param {Object} settings - Key-value pairs of settings to be applied.
+	 */
+	set_settings: function(settings) {
+		var self = this;
+
+		// Update settings
+		for (let key in settings) {
+			if (settings.hasOwnProperty(key)) {
+				const value = settings[key];
+				switch (key) {
+					case 'currentDir':
+					case 'defaultFilePermissions':
+					case 'defaultDirPermissions':
+					case 'defaultOwner': // Added
+					case 'defaultGroup': // Added
+						if (typeof value === 'string') {
+							self.settings[key] = value;
+						}
+						break;
+					case 'width':
+					case 'height':
+						// Convert strings to numbers
+						const numValue = parseInt(value, 10);
+						if (!isNaN(numValue)) {
+							self.settings[key] = numValue;
+						} else {
+							console.warn(`Invalid number for ${key}: ${value}`);
+						}
+						break;
+					case 'columnWidths':
+						if (typeof value === 'object' && value !== null) {
+							// Convert each value within columnWidths
+							let parsedColumnWidths = {};
+							for (let cwKey in value) {
+								if (value.hasOwnProperty(cwKey)) {
+									const cwValue = parseInt(value[cwKey], 10);
+									if (!isNaN(cwValue)) {
+										parsedColumnWidths[cwKey] = cwValue;
+									} else {
+										console.warn(`Invalid number for columnWidths.${cwKey}: ${value[cwKey]}`);
+									}
+								}
+							}
+							self.settings.columnWidths = Object.assign({}, self.settings.columnWidths, parsedColumnWidths);
+						}
+						break;
+					case 'mincolumnWidths':
+						if (typeof value === 'object' && value !== null) {
+							// Convert each value within mincolumnWidths
+							let parsedMinColumnWidths = {};
+							for (let mcwKey in value) {
+								if (value.hasOwnProperty(mcwKey)) {
+									const mcwValue = parseInt(value[mcwKey], 10);
+									if (!isNaN(mcwValue)) {
+										parsedMinColumnWidths[mcwKey] = mcwValue;
+									} else {
+										console.warn(`Invalid number for mincolumnWidths.${mcwKey}: ${value[cwKey]}`);
+									}
+								}
+							}
+							self.settings.mincolumnWidths = Object.assign({}, self.settings.mincolumnWidths, parsedMinColumnWidths);
+						}
+						break;
+					default:
+						// Handle unknown keys if necessary
+						console.warn(`Unknown setting key: ${key}`);
+				}
+			}
+		}
+
+		console.log(`[Navigation Plugin] Updated settings:`, self.settings);
+
+		// Apply settings to UI and internal properties
+		self.applySettingsToUI();
+
+		// If currentDir has changed, load the new directory
+		if (settings.hasOwnProperty('currentDir')) {
+			self.loadDirectory(self.currentDir);
+		}
+	},
+
+	// Helper method to get the file name from the path
+	basename: function(filePath) {
+		return filePath.split('/').pop();
+	},
+
+	/**
+	 * New method for requesting file content
+	 * type: 'text' or 'bin'
+	 * @param {string} filePath - The path to the file.
+	 * @param {string} type - The type of data to retrieve ('text' or 'bin').
+	 * @returns {Promise<string|ArrayBuffer>} - A promise that resolves with the file content.
+	 */
+	requestFileData: function(filePath, type) {
+		var self = this;
+
+		// Define the response type for read_direct
+		var responseType = (type === 'bin') ? 'blob' : 'text';
+
+		// Call read_direct to get the data
+		return fs.read_direct(filePath, responseType)
+			.then(function(response) {
+				if (type === 'bin') {
+					// If binary data is required, convert Blob to ArrayBuffer
+					return response.arrayBuffer();
+				} else {
+					// If text data, return it directly
+					return response;
+				}
+			})
+			.catch(function(error) {
+				// Handle errors
+				console.error('Failed to request file data:', error);
+				throw error;
+			});
+	},
+
+	/**
+	 * Reads the content of a file along with its permissions and ownership.
+	 * @param {String} filePath - The path to the file to read.
+	 * @param {String} type - The type of operation ('text' or 'bin').
+	 * @returns {Promise<Object>} - Resolves with an object containing content, permissions, owner, and group.
+	 */
+	read_file: function(filePath, type) {
+		var self = this;
+
+		// Execute both file data retrieval and ls command concurrently
+		return Promise.all([
+			self.requestFileData(filePath, type), // Retrieves the file content
+			fs.exec('/bin/ls', ['-lA', '--full-time', filePath]) // Executes ls to get file details
+		]).then(function([content, lsOutput]) {
+			// Split the ls output into lines and filter out any empty lines
+			var lines = lsOutput.stdout.split('\n').filter(line => line.trim() !== '');
+
+			if (lines.length === 0) {
+				throw new Error('No output from ls command');
+			}
+
+			// Parse the first line of ls output to get file details
+			var fileInfo = self.parseLsLine(lines[0]);
+
+			if (!fileInfo) {
+				throw new Error('Failed to parse ls output');
+			}
+
+			// Return the aggregated file information
+			return {
+				content: content,
+				permissions: fileInfo.permissions, // Numeric representation of permissions
+				GroupOwner: (fileInfo.owner + ':' + fileInfo.group) // Combined owner and group
+			};
+		}).catch(function(error) {
+			console.error('Failed to read file data and ls:', error);
+			throw error; // Propagate the error to the caller
+		});
+	},
+
+	/**
+	 * Writes data to a specified file on the server.
+	 * @param {String} filePath - The path to the file to write.
+	 * @param {String|ArrayBuffer} data - The data to write to the file.
+	 * @param {String} type - The type of operation ('text' or 'bin').
+	 * @returns {Promise} - Resolves when the write operation is complete.
+	 */
+	write_file: function(filePath, permissions, ownerGroup, data, type) {
+
+		var self = this;
+		// Define permissions and ownership
+		// var permissions = self.settings.defaultFilePermissions;
+		// var ownerGroup = self.settings.defaultOwner + ':' + self.settings.defaultGroup;
+
+		var blob;
+		if (type === 'text') {
+			blob = new Blob([data], {
+				type: 'text/plain'
+			});
+		} else {
+			// Assume that data is either ArrayBuffer, Uint8Array, etc.
+			blob = new Blob([data]);
+		}
+		return this.uploadFile(filePath, blob, permissions, ownerGroup, null);
+	},
+
+
+	/**
+	 * Helper function to concatenate directory and file names with proper slashes.
+	 * @param {string} dir - The directory path.
+	 * @param {string} name - The file or subdirectory name.
+	 * @returns {string} - The concatenated path.
+	 */
+	concatPath: function(dir, name) {
+		if (!dir.endsWith('/')) {
+			dir += '/';
+		}
+		return dir + name;
+	},
+
+	/**
+	 * Update the width of a specific column and persist the change in settings.
+	 * @param {string} field - The field name of the column.
+	 * @param {number} newWidth - The new width in pixels.
+	 */
+	updateColumnWidth: function(field, newWidth) {
+		var self = this;
+		// Update column element width
+		var col = self.table.querySelector(`col[data-field="${field}"]`);
+		if (col) {
+			col.style.width = newWidth + 'px';
+		} else {
+			console.warn(`No col found for field: ${field}`);
+		}
+
+		// Update settings and internal properties
+		self.columnWidths[field] = newWidth;
+		self.settings.columnWidths[field] = newWidth;
+		// IMPORTANT: Do not call applySettingsToUI() here.
+		// We'll call it once after column resizing finishes (on mouseup).
+	},
+
+	/**
+	 * CSS styles for the plugin.
+	 * Modified to include a unique suffix to prevent class name conflicts.
+	 */
+	css: function() {
+		var self = this;
+		var uniqueSuffix = self.uniqueSuffix; // e.g., '-123'
+
+		return `
+        /* Styles for the modal window */
+        .navigation-plugin-modal${uniqueSuffix} {
+            position: fixed;
+            top: 0;
+            left: 0;
+            width: 100%;
+            height: 100%;
+            background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent black background */
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            z-index: 1000; /* High z-index to appear on top */
+        }
+
+        /* Styles for the modal content */
+        .navigation-plugin-modal-content${uniqueSuffix} {
+            background-color: #fff; /* White background for content */
+            padding: 20px;
+            border-radius: 5px;
+            width: 400px;
+            position: relative;
+            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+        }
+
+        /* Close button for the modal */
+        .navigation-plugin-close-button${uniqueSuffix} {
+            position: absolute;
+            top: 10px;
+            right: 15px;
+            font-size: 24px;
+            font-weight: bold;
+            color: #aaa;
+            cursor: pointer;
+            transition: color 0.2s;
+        }
+
+        .navigation-plugin-close-button${uniqueSuffix}:hover {
+            color: #000;
+        }
+
+        /* Styles for elements inside the modal */
+        .navigation-plugin-modal-content${uniqueSuffix} label {
+            display: block;
+            margin-top: 10px;
+            font-weight: bold;
+        }
+
+        .navigation-plugin-modal-content${uniqueSuffix} input[type="text"] {
+            width: 100%;
+            padding: 8px;
+            margin-top: 5px;
+            box-sizing: border-box;
+        }
+
+        .navigation-plugin-modal-content${uniqueSuffix} button {
+            margin-top: 15px;
+            padding: 10px 20px;
+            font-size: 16px;
+            cursor: pointer;
+            border: none;
+            border-radius: 4px;
+        }
+
+        .navigation-plugin-modal-content${uniqueSuffix} button#edit-submit-button${uniqueSuffix} {
+            background-color: #4CAF50; /* Green background for "Submit" button */
+            color: white;
+        }
+
+        .navigation-plugin-modal-content${uniqueSuffix} button#edit-submit-button${uniqueSuffix}:hover {
+            background-color: #45a049;
+        }
+
+        .navigation-plugin-modal-content${uniqueSuffix} button#edit-cancel-button${uniqueSuffix} {
+            background-color: #f44336; /* Red background for "Cancel" button */
+            color: white;
+            margin-left: 10px;
+        }
+
+        .navigation-plugin-modal-content${uniqueSuffix} button#edit-cancel-button${uniqueSuffix}:hover {
+            background-color: #da190b;
+        }
+
+        /* Header styles */
+        .navigation-plugin-header${uniqueSuffix} {
+            display: flex;
+            align-items: center;
+            gap: 10px;
+            margin-bottom: 10px;
+        }
+
+        .navigation-plugin-header${uniqueSuffix} input[type="text"] {
+            flex-grow: 1;
+            padding: 5px;
+            font-size: 16px;
+            text-align: left; /* Align text to the left */
+            background-color: #fff;
+            border: 1px solid #ccc;
+            border-radius: 4px;
+        }
+
+        .navigation-plugin-header${uniqueSuffix} button {
+            padding: 12px 24px;
+            font-size: 18px;
+            cursor: pointer;
+            background-color: #007BFF;
+            color: white;
+            border: none;
+            border-radius: 4px;
+            transition: background-color 0.2s;
+        }
+
+        .navigation-plugin-header${uniqueSuffix} button:hover {
+            background-color: #0056b3;
+        }
+
+        /* Breadcrumb styles */
+        .navigation-plugin-breadcrumb${uniqueSuffix} {
+            margin-bottom: 10px;
+        }
+
+        /* Table container with fixed headers */
+        .navigation-plugin-table-container${uniqueSuffix} {
+            border: 1px solid #ccc;
+            resize: both;
+            overflow: auto;
+            position: relative;
+            box-sizing: border-box;
+        }
+
+        /* Table styles */
+        .navigation-plugin-table${uniqueSuffix} {
+            width: 100%; /* Set table width to 100% of container */
+            border-collapse: collapse;
+            table-layout: fixed;
+            min-width: 730px; /* Example: sum of column widths */
+        }
+
+        .navigation-plugin-table${uniqueSuffix} th, .navigation-plugin-table${uniqueSuffix} td {
+            box-sizing: border-box; /* Account for padding and border when calculating width */
+            padding: 8px; /* Add padding to improve appearance */
+            overflow: hidden; /* Hide overflow to prevent content from exceeding cell boundaries */
+            white-space: nowrap; /* Prevent text wrapping */
+            text-overflow: ellipsis; /* Add ellipsis if text is trimmed */
+            border: 1px solid #ddd; /* Add borders for visual column separation */
+            height: 40px; /* Fixed row height */
+            min-height: 40px; /* Or minimum height */
+        }
+
+        .navigation-plugin-table${uniqueSuffix} col {
+            min-width: 50px; /* Example minimum width */
+        }
+
+        .navigation-plugin-table${uniqueSuffix} thead th {
+            position: sticky;
+            top: 0;
+            background-color: #f2f2f2;
+            padding: 8px;
+            text-align: left;
+            vertical-align: middle;
+            border-bottom: 2px solid #aaa; /* Thicker and brighter border */
+            border-right: 1px solid #aaa;  /* Added vertical border */
+            z-index: 2; /* Ensure headers stay above body rows */
+        }
+
+        .navigation-plugin-table${uniqueSuffix} thead th:last-child {
+            border-right: none;
+        }
+
+        /* Styles for table cells */
+        .navigation-plugin-table${uniqueSuffix} tbody td {
+            overflow: hidden; /* Trim content that exceeds cell boundaries */
+            white-space: nowrap; /* Prevent text from wrapping to a new line */
+            text-overflow: ellipsis; /* Add ellipsis if text is trimmed */
+        }
+
+        /* Styles for links inside table cells */
+        .navigation-plugin-table${uniqueSuffix} td .file-name${uniqueSuffix},
+        .navigation-plugin-table${uniqueSuffix} td .directory-name${uniqueSuffix},
+        .navigation-plugin-table${uniqueSuffix} td .symlink-name${uniqueSuffix} {
+            display: block; /* Ensure the element occupies the full width of the cell */
+            width: 100%; /* Set the element's width to 100% of the cell */
+            overflow: hidden; /* Trim content that exceeds element boundaries */
+            white-space: nowrap; /* Prevent text from wrapping to a new line */
+            text-overflow: ellipsis; /* Add ellipsis if text is trimmed */
+        }
+
+        .navigation-plugin-table${uniqueSuffix} tbody td:last-child {
+            border-right: none;
+        }
+
+        /* Resizer styles */
+        .resizer${uniqueSuffix},
+        .navigation-plugin-table${uniqueSuffix} th .resizer {
+            position: absolute;
+            right: 0;
+            top: 0;
+            width: 5px;
+            height: 100%;
+            cursor: col-resize;
+            user-select: none;
+            background-color: transparent;
+            z-index: 10;
+            transition: background-color 0.2s;
+        }
+
+        .resizer${uniqueSuffix}:hover,
+        .navigation-plugin-table${uniqueSuffix} th .resizer:hover {
+            background-color: rgba(0, 0, 0, 0.1);
+        }
+
+        /* Overlay for drag and drop */
+        .navigation-plugin-drag-overlay${uniqueSuffix} {
+            display: none;
+            position: absolute;
+            top: 0;
+            left: 0;
+            right: 0;
+            bottom: 0;
+            background-color: rgba(0, 0, 0, 0.5);
+            color: white;
+            align-items: center;
+            justify-content: center;
+            font-size: 24px;
+            z-index: 500;
+            flex-direction: column;
+            pointer-events: none; /* Ensure the overlay doesn't block interactions */
+        }
+
+        /* Actions bar below the scrollable area */
+        .navigation-plugin-actions-bar${uniqueSuffix} {
+            display: flex;
+            align-items: center;
+            gap: 10px;
+            margin-top: 10px;
+        }
+
+        .navigation-plugin-actions-bar${uniqueSuffix} button {
+            padding: 12px 24px;
+            font-size: 18px;
+            cursor: pointer;
+            background-color: #007BFF;
+            color: white;
+            border: none;
+            border-radius: 4px;
+            transition: background-color 0.2s;
+        }
+
+        .navigation-plugin-actions-bar${uniqueSuffix} button:hover {
+            background-color: #0056b3;
+        }
+
+        /* Actions column styles */
+        .navigation-plugin-actions${uniqueSuffix} {
+            display: flex; /* Use flexbox for even distribution of icons */
+            align-items: center;
+            justify-content: flex-start; /* Align icons to the left */
+            gap: 8px; /* Even spacing between icons */
+            padding: 4px 8px; /* Add padding for better appearance */
+            box-sizing: border-box; /* Include padding in width calculation */
+        }
+
+        .navigation-plugin-actions${uniqueSuffix} .action-button${uniqueSuffix} {
+            cursor: pointer;
+            width: 24px;
+            height: 24px;
+            display: flex; 
+            align-items: center;
+            justify-content: center;
+            background: none;
+            font-size: 18px;
+            transition: background-color 0.2s, border-radius 0.2s;
+        }
+
+        .navigation-plugin-actions${uniqueSuffix} .action-button${uniqueSuffix}:hover {
+            background-color: #f0f0f0;
+            border-radius: 4px;
+        }
+
+        /* Dark theme */
+        .dark-theme .navigation-plugin${uniqueSuffix} {
+            background-color: #2a2a2a; 
+            border: 1px solid #555555;
+            color: #ffffff; 
+        }
+
+        .dark-theme .navigation-plugin-header${uniqueSuffix} input[type="text"] {
+            background-color: #444444;
+            color: #ffffff;
+            border: 1px solid #666666;
+        }
+
+        .dark-theme .navigation-plugin-header${uniqueSuffix} button {
+            background-color: #555555;
+            color: #ffffff;
+            border: 1px solid #666666;
+        }
+
+        .dark-theme .navigation-plugin-header${uniqueSuffix} button:hover {
+            background-color: #666666;
+        }
+
+        .dark-theme .navigation-plugin-table${uniqueSuffix} thead th {
+            background-color: #333333;
+            color: #ffffff;
+            border-bottom: 2px solid #888; /* Brighter color */
+            border-right: 1px solid #888;  /* Added vertical border */
+        }
+
+        .dark-theme .navigation-plugin-table-container${uniqueSuffix} {
+            border: 1px solid #555;
+        }
+
+        .dark-theme .navigation-plugin-table${uniqueSuffix} tbody td {
+            background-color: #2a2a2a;
+            color: #ffffff;
+            border-bottom: 1px solid #888; /* Brighter color */
+            border-right: 1px solid #888;  /* Added vertical border */
+        }
+
+        /* Row highlighting */
+        .navigation-plugin-table${uniqueSuffix} tbody tr:hover {
+            background-color: #f0f0f0; /* Lighter gray background */
+            color: #000000; /* Black text color */
+            cursor: pointer; /* Change cursor to pointer */
+            transition: background-color 0.3s, color 0.3s; /* Smooth transition */
+        }
+
+        .dark-theme .navigation-plugin-table${uniqueSuffix} tbody tr:hover {
+            background-color: #555555; /* Lighter gray background for dark theme */
+            color: #ffffff; /* White text color */
+            cursor: pointer;
+            transition: background-color 0.3s, color 0.3s;
+        }
+
+        .dark-theme .navigation-plugin-table${uniqueSuffix} .directory-name${uniqueSuffix} {
+            color: #1e90ff; 
+        }
+
+        .dark-theme .navigation-plugin-table${uniqueSuffix} .symlink-name${uniqueSuffix} {
+            color: #32cd32;
+        }
+
+        .navigation-plugin-table${uniqueSuffix} .directory-name${uniqueSuffix} {
+            color: #1e90ff; /* Blue for directories */
+        }
+
+        .navigation-plugin-table${uniqueSuffix} .symlink-name${uniqueSuffix} {
+            color: #32cd32; /* Green for symbolic links */
+        }
+
+        .navigation-plugin-table${uniqueSuffix} .file-name${uniqueSuffix} {
+            color: #000000; /* Black for regular files */
+        }
+
+        /* Cursor styles for draggable and clickable elements */
+        .navigation-plugin-table${uniqueSuffix} .file-name${uniqueSuffix}[draggable="true"],
+        .navigation-plugin-table${uniqueSuffix} .draggable${uniqueSuffix} {
+            cursor: grab;
+        }
+
+        .navigation-plugin-table${uniqueSuffix} .file-name${uniqueSuffix}[draggable="true"]:active,
+        .navigation-plugin-table${uniqueSuffix} .draggable${uniqueSuffix}:active {
+            cursor: grabbing;
+        }
+
+        .navigation-plugin-table${uniqueSuffix} .directory-name${uniqueSuffix},
+        .navigation-plugin-table${uniqueSuffix} .symlink-name${uniqueSuffix},
+        .navigation-plugin-table${uniqueSuffix} .file-name${uniqueSuffix} {
+            cursor: pointer;
+        }
+
+        /* Styles for custom tooltip */
+        .navigation-plugin-tooltip${uniqueSuffix} {
+            position: absolute;
+            background-color: rgba(0, 0, 0, 0.8); /* Dark background with transparency */
+            color: #fff; /* White text */
+            padding: 5px 10px;
+            border-radius: 4px;
+            font-size: 14px;
+            pointer-events: none; /* Tooltip does not interfere with interactions */
+            white-space: nowrap; /* Prevent text wrapping */
+            z-index: 1001; /* Above all other elements */
+            opacity: 0;
+            transition: opacity 0.2s ease-in-out;
+        }
+
+        .navigation-plugin-tooltip${uniqueSuffix}.visible {
+            opacity: 1;
+        }
+
+        /* Highlight directory row on dragover */
+        .navigation-plugin-table${uniqueSuffix} tbody tr.drag-over${uniqueSuffix} {
+            background-color: #d3d3d3; /* Light gray */
+        }
+
+        /* Highlight for copy action */
+        .navigation-plugin-table${uniqueSuffix} tbody tr.drag-over-copy${uniqueSuffix} {
+            background-color: #add8e6; /* Light blue */
+            cursor: copy;
+        }
+
+        /* Highlight for move action */
+        .navigation-plugin-table${uniqueSuffix} tbody tr.drag-over-move${uniqueSuffix} {
+            background-color: #90ee90; /* Light green */
+            cursor: move;
+        }
+
+        /* Cursor styles for copy and move */
+        .navigation-plugin-table${uniqueSuffix} tbody tr.drag-over-copy${uniqueSuffix} td,
+        .navigation-plugin-table${uniqueSuffix} tbody tr.drag-over-move${uniqueSuffix} td {
+            cursor: inherit; /* Inherit cursor from parent tr */
+        }
+
+        /* Single table with fixed headers */
+        .navigation-plugin-table${uniqueSuffix} {
+            /* Removed redundant width and table-layout properties */
+        }
+
+        /* Minimal width for navigation plugin */
+        .navigation-plugin${uniqueSuffix} {
+            min-width: 780px; /* Sum of columnWidths */
+            box-sizing: border-box;
+        }
+
+        /* Fixed row height to prevent height changes on column resize */
+        .navigation-plugin-table${uniqueSuffix} tbody tr {
+            height: 40px; /* Fixed height */
+            min-height: 40px; /* Ensure minimum height */
+        }
+
+        /* Ensuring the table container maintains its height */
+        .navigation-plugin-table-container${uniqueSuffix} {
+            height: 100%; /* Maintain full height */
+        }
+        .cbi-progressbar${uniqueSuffix} {
+            width: 100%;
+            background-color: #f3f3f3;
+            border: 1px solid #ccc;
+            border-radius: 5px;
+            height: 20px;
+            overflow: hidden;
+            margin-top: 10px; 
+        }
+
+        .cbi-progressbar${uniqueSuffix} div {
+            height: 100%;
+            background-color: #4caf50;
+            width: 0%;
+            transition: width 0.2s;
+        }
+
+        #status-info${uniqueSuffix} {
+            margin-bottom: 5px;
+            font-weight: bold;
+        }
+
+        #status-progress${uniqueSuffix} {
+            margin-bottom: 10px;
+        }
+
+        `;
+	},
+
+
+	/**
+	 * Refreshes the navigation by reloading the current directory.
+	 */
+	refresh: function() {
+		this.loadDirectory(this.currentDir);
+	},
+
+	/**
+	 * Initializes the plugin within the provided container.
+	 * @param {HTMLElement} container - The DOM element to contain the plugin UI.
+	 * @param {Object} pluginsRegistry - The registry of available plugins.
+	 * @param {Object} default_plugins - The default plugins for each type.
+	 * @param {string} uniqueSuffix - The unique suffix to append to class names and IDs.
+	 */
+	start: function(container, pluginsRegistry, default_plugins, uniqueSuffix) {
+		var self = this;
+		self.default_plugins = default_plugins;
+		self.pluginsRegistry = pluginsRegistry;
+		self.uniqueSuffix = `-${uniqueSuffix}`; // Store the unique suffix with a preceding dash
+
+		var defaultDispatcherName = self.default_plugins['Dispatcher'];
+
+		if (defaultDispatcherName && self.pluginsRegistry[defaultDispatcherName]) {
+			var defaultDispatcher = self.pluginsRegistry[defaultDispatcherName];
+
+			if (typeof defaultDispatcher.pop === 'function') {
+				self.popm = defaultDispatcher.pop.bind(defaultDispatcher);
+				console.log(`Pop function successfully retrieved from Dispatcher: ${defaultDispatcherName}`);
+			} else {
+				console.error(`Default Dispatcher "${defaultDispatcherName}" does not implement pop().`);
+				self.popm = function() {
+					console.warn(`Fallback: pop function is not available because Default Dispatcher "${defaultDispatcherName}" does not implement pop().`);
+				};
+			}
+		} else {
+			console.error('Default Dispatcher not found in pluginsRegistry.');
+			self.popm = function() {
+				console.warn('Fallback: pop function is not available because Default Dispatcher not found in pluginsRegistry.');
+			};
+		}
+
+		// Initialize settings by merging defaultSettings and existing settings
+		self.settings = Object.assign({}, self.defaultSettings, self.settings || {});
+
+		// Inject the modified CSS into the document
+		self.injectCSS();
+
+		// Create navDiv and other DOM elements with unique suffix
+		self.navDiv = document.createElement('div');
+		self.navDiv.className = 'navigation-plugin' + self.uniqueSuffix;
+
+		// Header with path input field and "Go" button
+		var headerDiv = document.createElement('div');
+		headerDiv.className = 'navigation-plugin-header' + self.uniqueSuffix;
+
+		var pathInput = document.createElement('input');
+		pathInput.type = 'text';
+		pathInput.value = self.currentDir;
+		pathInput.placeholder = 'Enter path...';
+
+		pathInput.addEventListener('keydown', function(event) {
+			if (event.key === 'Enter') {
+				self.navigateToPath(pathInput.value.trim());
+			}
+		});
+
+		var goButton = document.createElement('button');
+		goButton.textContent = 'Go';
+		goButton.onclick = function() {
+			self.navigateToPath(pathInput.value.trim());
+		};
+
+		headerDiv.appendChild(pathInput);
+		headerDiv.appendChild(goButton);
+		self.navDiv.appendChild(headerDiv);
+
+		// Breadcrumb below the header
+		self.breadcrumb = document.createElement('div');
+		self.breadcrumb.className = 'navigation-plugin-breadcrumb' + self.uniqueSuffix;
+		self.navDiv.appendChild(self.breadcrumb);
+
+		// Create tableContainer and table
+		self.tableContainer = document.createElement('div');
+		self.tableContainer.className = 'navigation-plugin-table-container' + self.uniqueSuffix;
+
+		self.table = document.createElement('table');
+		self.table.className = 'navigation-plugin-table' + self.uniqueSuffix;
+
+		// Add colgroup to manage column widths
+		self.colGroup = document.createElement('colgroup');
+		['select', 'name', 'type', 'size', 'mtime', 'actions'].forEach(function(field) {
+			var col = document.createElement('col');
+			// Set width from columnWidths or default value
+			col.style.width = (self.settings.columnWidths[field] || 100) + 'px';
+			col.dataset.field = field;
+			self.colGroup.appendChild(col);
+		});
+		self.table.appendChild(self.colGroup);
+
+		// Create table header
+		self.thead = document.createElement('thead');
+		var headerRow = document.createElement('tr');
+
+		// "Select All" column header
+		var selectAllHeader = document.createElement('th');
+		selectAllHeader.dataset.field = 'select';
+		selectAllHeader.style.width = (self.settings.columnWidths['select'] || 30) + 'px';
+
+		var selectAllCheckbox = document.createElement('input');
+		selectAllCheckbox.type = 'checkbox';
+		selectAllCheckbox.onclick = function(event) {
+			self.handleSelectAll(this.checked, event);
+		};
+		selectAllHeader.appendChild(selectAllCheckbox);
+
+		// Add resizer for the select column
+		var selectResizer = document.createElement('div');
+		selectResizer.className = 'resizer' + self.uniqueSuffix;
+		selectAllHeader.appendChild(selectResizer);
+		selectResizer.addEventListener('mousedown', function(e) {
+			self.initColumnResize(e, 'select');
+		});
+
+		headerRow.appendChild(selectAllHeader);
+
+		self.selectedItems = new Set();
+		self.sortField = 'name';
+		self.sortDirection = 'asc';
+
+		// Create other column headers
+		['Name', 'Type', 'Size', 'Modification Date'].forEach(function(title, index) {
+			var field = ['name', 'type', 'size', 'mtime'][index];
+			var sortableHeader = self.createSortableHeader(title, field);
+			headerRow.appendChild(sortableHeader);
+		});
+
+		// "Actions" column header
+		var actionsHeader = document.createElement('th');
+		actionsHeader.textContent = 'Actions'; // No sorting
+		actionsHeader.dataset.field = 'actions';
+		actionsHeader.style.width = (self.settings.columnWidths['actions'] || 200) + 'px';
+
+		// Add resizer for the Actions column
+		var actionsResizer = document.createElement('div');
+		actionsResizer.className = 'resizer' + self.uniqueSuffix;
+		actionsHeader.appendChild(actionsResizer);
+		actionsResizer.addEventListener('mousedown', function(e) {
+			self.initColumnResize(e, 'actions');
+		});
+
+		headerRow.appendChild(actionsHeader);
+
+		self.thead.appendChild(headerRow);
+		self.table.appendChild(self.thead);
+
+		// Create table body
+		self.tbody = document.createElement('tbody');
+		self.table.appendChild(self.tbody);
+
+		// Add footer for drag-and-drop
+		self.dragOverlay = document.createElement('div');
+		self.dragOverlay.className = 'navigation-plugin-drag-overlay' + self.uniqueSuffix;
+		self.dragOverlay.textContent = _('Drop files here to upload');
+		self.tableContainer.appendChild(self.dragOverlay);
+
+		self.tableContainer.appendChild(self.table);
+		self.navDiv.appendChild(self.tableContainer);
+
+		// Actions bar below the table
+		self.actionsBar = document.createElement('div');
+		self.actionsBar.className = 'navigation-plugin-actions-bar' + self.uniqueSuffix;
+
+		self.uploadButton = document.createElement('button');
+		self.uploadButton.textContent = _('Upload');
+		self.uploadButton.onclick = function() {
+			self.handleUploadClick();
+		};
+
+		self.createFolderButton = document.createElement('button');
+		self.createFolderButton.textContent = _('Create Folder');
+		self.createFolderButton.onclick = function() {
+			self.handleCreateFolderClick();
+		};
+
+		self.createFileButton = document.createElement('button');
+		self.createFileButton.textContent = _('Create File');
+		self.createFileButton.onclick = function() {
+			self.handleCreateFileClick();
+		};
+
+		self.deleteSelectedButton = document.createElement('button');
+		self.deleteSelectedButton.textContent = _('Delete Selected');
+		self.deleteSelectedButton.disabled = true;
+		self.deleteSelectedButton.onclick = function() {
+			self.handleDeleteSelectedClick();
+		};
+
+		self.actionsBar.appendChild(self.uploadButton);
+		self.actionsBar.appendChild(self.createFolderButton);
+		self.actionsBar.appendChild(self.createFileButton);
+		self.actionsBar.appendChild(self.deleteSelectedButton);
+
+		self.navDiv.appendChild(self.actionsBar);
+		container.appendChild(self.navDiv);
+
+		// Add drag-and-drop event handlers
+		self.addDragAndDropEvents();
+
+		document.addEventListener(('tab-' + `${PN}`), function(e) {
+			self.refresh();
+		});
+
+		// Now call applySettingsToUI after creating all DOM elements
+		self.applySettingsToUI();
+
+		// Load the current directory
+		self.loadDirectory(self.currentDir);
+
+		// Adding ResizeObserver for navDiv observing
+		if (typeof ResizeObserver !== 'undefined') {
+			self.resizeObserver = new ResizeObserver(entries => {
+				for (let entry of entries) {
+					const {
+						width,
+						height
+					} = entry.contentRect;
+					const newWidth = Math.round(width);
+					const newHeight = Math.round(height);
+
+					// Check if dimensions has changed noticably (> 10px)
+					const widthChanged = Math.abs(self.settings.width - newWidth) > 10;
+					const heightChanged = Math.abs(self.settings.height - newHeight) > 10;
+
+					if (widthChanged || heightChanged) {
+						// Update settings
+						self.settings.width = newWidth;
+						self.settings.height = newHeight;
+					}
+				}
+			});
+
+			// Start observing of navDiv
+			self.resizeObserver.observe(self.tableContainer);
+		} else {
+			console.warn(`[${PN}]: ResizeObserver is not supported in this browser`);
+		}
+
+		// Add the unique CSS to the document
+		self.injectCSS();
+	},
+
+	/**
+	 * Injects the CSS styles into the document.
+	 */
+	injectCSS: function() {
+		var self = this;
+		// Create a style element
+		var style = document.createElement('style');
+		style.type = 'text/css';
+		style.textContent = self.css();
+		// Append the style to the head
+		document.head.appendChild(style);
+	},
+
+	/**
+	 * Shows a tooltip with the specified text near the mouse cursor.
+	 * @param {MouseEvent} event - The mouse event.
+	 * @param {string} text - The tooltip text.
+	 */
+	showTooltip: function(event, text) {
+		var self = this;
+
+		// Create the tooltip element if it doesn't exist
+		if (!self.tooltipElement) {
+			self.tooltipElement = document.createElement('div');
+			self.tooltipElement.className = 'navigation-plugin-tooltip' + self.uniqueSuffix;
+			document.body.appendChild(self.tooltipElement);
+		}
+
+		self.tooltipElement.textContent = text;
+		self.tooltipElement.classList.add('visible');
+		self.positionTooltip(event);
+		self.currentTooltip = true;
+	},
+
+	/**
+	 * Hides the currently visible tooltip.
+	 */
+	hideTooltip: function() {
+		var self = this;
+		if (self.tooltipElement) {
+			self.tooltipElement.classList.remove('visible');
+			self.currentTooltip = false;
+		}
+	},
+
+	/**
+	 * Positions the tooltip relative to the mouse cursor.
+	 * @param {MouseEvent} event - The mouse event.
+	 */
+	positionTooltip: function(event) {
+		var self = this;
+		if (self.tooltipElement) {
+			var tooltip = self.tooltipElement;
+			var tooltipWidth = tooltip.offsetWidth;
+			var tooltipHeight = tooltip.offsetHeight;
+			var pageWidth = document.documentElement.clientWidth;
+			var pageHeight = document.documentElement.clientHeight;
+
+			var x = event.pageX + 10; // Offset to the right of the cursor
+			var y = event.pageY + 10; // Offset below the cursor
+
+			// Ensure the tooltip doesn't go beyond the right edge
+			if (x + tooltipWidth > pageWidth) {
+				x = event.pageX - tooltipWidth - 10;
+			}
+
+			// Ensure the tooltip doesn't go beyond the bottom edge
+			if (y + tooltipHeight > pageHeight) {
+				y = event.pageY - tooltipHeight - 10;
+			}
+
+			tooltip.style.left = x + 'px';
+			tooltip.style.top = y + 'px';
+		}
+	},
+
+	/**
+	 * Creates a sortable table header.
+	 * @param {string} title - The display title of the column.
+	 * @param {string} field - The field name associated with the column.
+	 * @returns {HTMLElement} - The created header element.
+	 */
+	createSortableHeader: function(title, field) {
+		var self = this;
+		var header = document.createElement('th');
+		header.dataset.field = field;
+		// header.style.position = 'relative';
+		header.style.textAlign = 'left'; // Align text to the left
+
+		var headerContent = document.createElement('div');
+		headerContent.style.display = 'inline-flex';
+		headerContent.style.alignItems = 'center';
+		headerContent.style.cursor = 'pointer';
+		headerContent.style.userSelect = 'none'; // Prevent text selection
+
+		var titleSpan = document.createElement('span');
+		titleSpan.textContent = title;
+
+		var sortIcon = document.createElement('span');
+		sortIcon.style.marginLeft = '5px';
+
+		if (self.sortField === field) {
+			sortIcon.textContent = self.sortDirection === 'asc' ? '▲' : '▼';
+		} else {
+			sortIcon.textContent = '⇅';
+		}
+
+		headerContent.appendChild(titleSpan);
+		headerContent.appendChild(sortIcon);
+		header.appendChild(headerContent);
+
+		var resizer = document.createElement('div');
+		resizer.className = 'resizer' + self.uniqueSuffix;
+		header.appendChild(resizer);
+
+		// Add event listener for column resizing
+		resizer.addEventListener('mousedown', function(e) {
+			self.initColumnResize(e, field);
+		});
+
+		// Add event listener for sorting
+		header.addEventListener('click', function(e) {
+			if (e.target.classList.contains('resizer' + self.uniqueSuffix)) return; // Ignore clicks on resizer
+			var clickedField = header.dataset.field;
+			if (self.sortField === clickedField) {
+				self.sortDirection = self.sortDirection === 'asc' ? 'desc' : 'asc';
+			} else {
+				self.sortField = clickedField;
+				self.sortDirection = 'asc';
+			}
+			self.loadDirectory(self.currentDir);
+		});
+
+		return header;
+	},
+
+	/**
+	 * Navigates to a specified path.
+	 * @param {string} path - The path to navigate to.
+	 */
+	navigateToPath: function(path) {
+		var self = this;
+		fs.stat(path).then(function(stat) {
+			if (stat.type === 'directory') {
+				self.currentDir = path.endsWith('/') ? path : path + '/';
+				self.settings.currentDir = self.currentDir;
+				// self.set_settings(self.settings);
+				self.loadDirectory(self.currentDir);
+			} else {
+				self.popm(null, `[${PN}]: ` + _('The specified path is not a directory.'), 'error');
+			}
+		}).catch(function(err) {
+			self.popm(null, `[${PN}]: ` + _('Failed to access the specified path: %s').format(err.message), 'error');
+		});
+	},
+
+	/**
+	 * Initializes column resizing.
+	 * @param {MouseEvent} e - The mouse event.
+	 * @param {string} field - The field name of the column being resized.
+	 */
+	initColumnResize: function(e, field) {
+		var self = this;
+		e.preventDefault();
+
+		self.resizingField = field;
+		self.startX = e.pageX;
+		// Get initial width from settings or from the col element
+		self.startWidth = self.columnWidths[field] || self.table.querySelector(`col[data-field="${field}"]`).offsetWidth;
+
+		self.boundOnColumnResize = self.onColumnResize.bind(self);
+		self.boundStopColumnResize = self.stopColumnResize.bind(self);
+
+		// Add listeners for mouse movement and mouse release
+		document.addEventListener('mousemove', self.boundOnColumnResize);
+		document.addEventListener('mouseup', self.boundStopColumnResize);
+
+		// Disable text selection during column resizing for better UX
+		document.body.style.userSelect = 'none';
+	},
+
+	/**
+	 * Handles the column resize movement.
+	 * @param {MouseEvent} e - The mouse event.
+	 */
+	onColumnResize: function(e) {
+		var self = this;
+		if (!self.resizingField) return;
+
+		var diffX = e.pageX - self.startX;
+		var newWidth = self.startWidth + diffX;
+
+		var minWidth = self.mincolumnWidths[self.resizingField] || 30;
+		if (newWidth < minWidth) {
+			newWidth = minWidth;
+		}
+
+		// Just update the column width directly, do not re-apply entire UI settings yet.
+		self.updateColumnWidth(self.resizingField, newWidth);
+	},
+
+	/**
+	 * Stops the column resizing process.
+	 * @param {MouseEvent} e - The mouse event.
+	 */
+	stopColumnResize: function(e) {
+		var self = this;
+		document.removeEventListener('mousemove', self.boundOnColumnResize);
+		document.removeEventListener('mouseup', self.boundStopColumnResize);
+		self.resizingField = null;
+
+		// Re-enable text selection
+		document.body.style.userSelect = '';
+
+		// After the user finishes resizing, apply settings to ensure all adjustments are correctly displayed.
+		self.applySettingsToUI();
+	},
+
+	/**
+	 * Adds drag and drop event listeners to the table container.
+	 */
+	addDragAndDropEvents: function() {
+		var self = this;
+		var counter = 0;
+
+		self.tableContainer.addEventListener('dragenter', function(e) {
+			e.preventDefault();
+			e.stopPropagation();
+			counter++;
+			self.dragOverlay.style.display = 'flex';
+		});
+
+		self.tableContainer.addEventListener('dragleave', function(e) {
+			e.preventDefault();
+			e.stopPropagation();
+			counter--;
+			if (counter === 0) {
+				self.dragOverlay.style.display = 'none';
+			}
+		});
+
+		self.tableContainer.addEventListener('dragover', function(e) {
+			e.preventDefault();
+			e.stopPropagation();
+		});
+
+		self.tableContainer.addEventListener('drop', function(e) {
+			e.preventDefault();
+			e.stopPropagation();
+			self.dragOverlay.style.display = 'none';
+			counter = 0;
+			var files = e.dataTransfer.files;
+			if (files.length > 0) {
+				self.uploadFiles(files);
+			}
+		});
+	},
+
+	/**
+	 * Handles the upload button click event.
+	 */
+	handleUploadClick: function() {
+		var self = this;
+		var fileInput = document.createElement('input');
+		fileInput.type = 'file';
+		fileInput.multiple = true;
+		fileInput.style.display = 'none';
+		document.body.appendChild(fileInput);
+		fileInput.onchange = function(e) {
+			var files = e.target.files;
+			if (files.length > 0) {
+				self.uploadFiles(files);
+			}
+			document.body.removeChild(fileInput);
+		};
+		fileInput.click();
+	},
+
+	/**
+	 * Uploads a single file.
+	 * @param {string} filename - The name of the file.
+	 * @param {File} filedata - The file data.
+	 * @param {string} permissions - File permissions (e.g., '644').
+	 * @param {string} ownerGroup - Ownership in the format 'owner:group' (e.g., 'root:root').
+	 * @param {function} onProgress - Callback for upload progress.
+	 * @returns {Promise} - Resolves on successful upload and setting permissions/ownership, rejects on failure.
+	 */
+	uploadFile: function(filename, filedata, permissions, ownerGroup, onProgress) {
+		var self = this;
+
+		self.perm = String(permissions || self.defaultFilePermissions);
+		self.oG = ownerGroup || (self.settings.defaultOwner + ':' + self.settings.defaultGroup);
+		return new Promise(function(resolve, reject) {
+			console.log("UploadFile filename:", filename);
+			var formData = new FormData();
+			formData.append('sessionid', rpc.getSessionID());
+			formData.append('filename', filename);
+			formData.append('filedata', filedata);
+
+			var xhr = new XMLHttpRequest();
+			xhr.open('POST', L.env.cgi_base + '/cgi-upload', true);
+
+			xhr.upload.onprogress = function(event) {
+				if (event.lengthComputable && onProgress) {
+					var percent = (event.loaded / event.total) * 100;
+					onProgress(percent);
+				}
+			};
+
+			xhr.onload = function() {
+				console.log("UploadFile Server response:", xhr.responseText);
+
+				if (xhr.status === 200) {
+					// After successful upload, set permissions and ownership
+					var chmodPromise = self.perm ? fs.exec('/bin/chmod', [self.perm, filename]) : Promise.resolve();
+					var chownPromise = self.oG ? fs.exec('/bin/chown', [self.oG, filename]) : Promise.resolve();
+					Promise.all([chmodPromise, chownPromise])
+						.then(function() {
+							resolve(xhr.responseText);
+						})
+						.catch(function(err) {
+							console.error(`[${PN}]: ` + _('Failed to set permissions or ownership:'), err);
+							reject(err);
+						});
+				} else {
+					reject(new Error(xhr.statusText));
+				}
+			};
+
+			xhr.onerror = function() {
+				reject(new Error(`[${PN}]: ` + _('Network error')));
+			};
+
+			xhr.send(formData);
+		});
+	},
+
+	/**
+	 * Uploads multiple files sequentially.
+	 * @param {FileList} files - The list of files to upload.
+	 */
+	uploadFiles: function(files) {
+		var self = this;
+		var directoryPath = self.currentDir;
+		var totalFiles = files.length;
+
+		var statusInfo = self.statusInfo;
+		var statusProgress = self.statusProgress;
+
+		if (!statusInfo) {
+			statusInfo = document.createElement('div');
+			statusInfo.id = 'status-info' + self.uniqueSuffix;
+			// Insert above tableContainer for visibility
+			self.tableContainer.parentNode.insertBefore(statusInfo, self.tableContainer);
+			self.statusInfo = statusInfo;
+		}
+
+		if (!statusProgress) {
+			statusProgress = document.createElement('div');
+			statusProgress.id = 'status-progress' + self.uniqueSuffix;
+			self.tableContainer.parentNode.insertBefore(statusProgress, self.tableContainer);
+			self.statusProgress = statusProgress;
+		}
+
+		/**
+		 * Uploads the next file in the queue.
+		 * @param {number} index - The current file index.
+		 */
+		function uploadNextFile(index) {
+			if (index >= totalFiles) {
+				self.loadDirectory(self.currentDir);
+				return;
+			}
+
+			var file = files[index];
+			var fullFilePath = self.concatPath(directoryPath, file.name);
+
+			if (statusInfo) {
+				statusInfo.textContent = `[${PN}]: ` + _('Uploading "%s"...').format(file.name);
+			}
+			if (statusProgress) {
+				statusProgress.innerHTML = '';
+				var progressBarContainer = E('div', {
+					'class': 'cbi-progressbar' + self.uniqueSuffix,
+					'title': '0%'
+				}, [E('div', {
+					'style': 'width:0%'
+				})]);
+				statusProgress.appendChild(progressBarContainer);
+			}
+
+			// Define permissions and ownership
+			var permissions = self.settings.defaultFilePermissions;
+			var ownerGroup = (self.settings.defaultOwner + ':' + self.settings.defaultGroup);
+
+			self.uploadFile(fullFilePath, file, permissions, ownerGroup, function(percent) {
+				if (statusProgress) {
+					var progressBar = statusProgress.querySelector('.cbi-progressbar' + self.uniqueSuffix + ' div');
+					if (progressBar) {
+						progressBar.style.width = percent.toFixed(2) + '%';
+						statusProgress.querySelector('.cbi-progressbar' + self.uniqueSuffix).setAttribute('title', percent.toFixed(2) + '%');
+					}
+				}
+			}).then(function() {
+				if (statusProgress) {
+					statusProgress.innerHTML = '';
+				}
+				if (statusInfo) {
+					statusInfo.textContent = `[${PN}]: ` + _('File "%s" uploaded successfully.').format(file.name);
+				}
+				self.popm(null, `[${PN}]: ` + _('File "%s" uploaded successfully.').format(file.name), 'info');
+				uploadNextFile(index + 1);
+			}).catch(function(err) {
+				if (statusProgress) {
+					statusProgress.innerHTML = '';
+				}
+				if (statusInfo) {
+					statusInfo.textContent = `[${PN}]: ` + _('Upload failed for file "%s".').format(file.name);
+				}
+				self.popm(null, `[${PN}]: ` + _('Error uploading file "%s".').format(file.name), 'error');
+				uploadNextFile(index + 1);
+			});
+		}
+
+		// Start uploading files sequentially
+		uploadNextFile(0);
+	},
+
+	/**
+	 * Handles the "Create Folder" button click event.
+	 */
+	handleCreateFolderClick: function() {
+		var self = this;
+		var folderName = prompt(`[${PN}]: ` + _('Enter folder name:'));
+		if (folderName) {
+			var folderPath = self.concatPath(self.currentDir, folderName);
+			fs.exec('/bin/mkdir', [folderPath]).then(function() {
+				return fs.exec('/bin/chmod', [self.settings.defaultDirPermissions, folderPath]);
+			}).then(function() {
+				self.popm(null, `[${PN}]: ` + _('Folder "%s" created successfully.').format(folderName), 'info');
+				self.settings.currentDir = self.currentDir;
+				// self.set_settings(self.settings);
+				self.loadDirectory(self.currentDir);
+			}).catch(function(err) {
+				self.popm(null, `[${PN}]: ` + _('Failed to create folder "%s": %s').format(folderName, err.message), 'error');
+			});
+		}
+	},
+
+	/**
+	 * Handles the "Create File" button click event.
+	 */
+	handleCreateFileClick: function() {
+		var self = this;
+		var fileName = prompt(`[${PN}]: ` + _('Enter file name:'));
+		if (fileName) {
+			var filePath = self.concatPath(self.currentDir, fileName);
+			fs.exec('/bin/touch', [filePath]).then(function() {
+				return fs.exec('/bin/chmod', [self.settings.defaultFilePermissions, filePath]);
+			}).then(function() {
+				self.popm(null, `[${PN}]: ` + _('File "%s" created successfully.').format(fileName), 'info');
+				self.settings.currentDir = self.currentDir;
+				// self.set_settings(self.settings);
+				self.loadDirectory(self.currentDir);
+			}).catch(function(err) {
+				self.popm(null, `[${PN}]: ` + _('Failed to create file "%s": %s').format(fileName, err.message), 'error');
+			});
+		}
+	},
+
+	/**
+	 * Handles the "Delete Selected" button click event.
+	 */
+	handleDeleteSelectedClick: function() {
+		var self = this;
+		if (self.selectedItems.size === 0) return;
+
+		if (confirm(`[${PN}]: ` + _('Are you sure you want to delete the selected items?'))) {
+			var deletePromises = [];
+			self.selectedItems.forEach(function(filePath) {
+				deletePromises.push(fs.remove(filePath));
+			});
+
+			Promise.allSettled(deletePromises).then(function(results) {
+				var successCount = 0;
+				var failureCount = 0;
+				var failedItems = [];
+
+				results.forEach(function(result, index) {
+					if (result.status === 'fulfilled') {
+						successCount++;
+					} else {
+						failureCount++;
+						failedItems.push(Array.from(self.selectedItems)[index]);
+					}
+				});
+
+				if (successCount > 0) {
+					self.popm(null, `[${PN}]: ` + _('Successfully deleted %d items.').format(successCount), 'info');
+				}
+				if (failureCount > 0) {
+					failedItems.forEach(function(item) {
+						self.popm(null, `[${PN}]: ` + _('Failed to delete "%s".').format(item), 'error');
+					});
+				}
+
+				self.loadDirectory(self.currentDir);
+				self.updateDeleteSelectedButtonState();
+			});
+		}
+	},
+
+	/**
+	 * Handles the "Select All" checkbox click event.
+	 * @param {boolean} checked - Whether the checkbox is checked.
+	 */
+	handleSelectAll: function(checked, event) {
+		var self = this;
+
+		// If Alt was pressed, invert selection
+		if (event && event.altKey) {
+			var checkboxes = self.tbody.querySelectorAll('.select-item' + self.uniqueSuffix);
+			checkboxes.forEach(function(checkbox) {
+				checkbox.checked = !checkbox.checked; // Invert current state
+				var filePath = checkbox.dataset.path;
+				if (checkbox.checked) {
+					self.selectedItems.add(filePath);
+				} else {
+					self.selectedItems.delete(filePath);
+				}
+			});
+		} else {
+			// Regular "Select All"
+			var checkboxes = self.tbody.querySelectorAll('.select-item' + self.uniqueSuffix);
+			checkboxes.forEach(function(checkbox) {
+				checkbox.checked = checked;
+				var filePath = checkbox.dataset.path;
+				if (checked) {
+					self.selectedItems.add(filePath);
+				} else {
+					self.selectedItems.delete(filePath);
+				}
+			});
+		}
+
+		self.updateDeleteSelectedButtonState();
+	},
+
+	/**
+	 * Updates the state of the "Delete Selected" button based on selected items.
+	 */
+	updateDeleteSelectedButtonState: function() {
+		var self = this;
+		if (self.deleteSelectedButton) {
+			self.deleteSelectedButton.disabled = self.selectedItems.size === 0;
+		}
+	},
+
+	convertPermissionsToNumeric: function(permissions) {
+		const mapping = {
+			'r': 4,
+			'w': 2,
+			'x': 1,
+			'-': 0
+		};
+		let specialBits = 0;
+
+		// Handling "special" bits (setuid, setgid, sticky bit)
+		if (permissions[2] === 's') specialBits += 4000; // setuid with execute permissions
+		if (permissions[2] === 'S') specialBits += 4000; // setuid without execute permissions
+		if (permissions[5] === 's') specialBits += 2000; // setgid with execute permissions
+		if (permissions[5] === 'S') specialBits += 2000; // setgid without execute permissions
+		if (permissions[8] === 't') specialBits += 1000; // sticky bit with execute permissions
+		if (permissions[8] === 'T') specialBits += 1000; // sticky bit without execute permissions
+
+		// Remove "s", "S", "t", "T" symbols before calculation
+		permissions = permissions
+			.replace(/s/g, 'x') // Replace `s` with `x`
+			.replace(/S/g, '-') // Replace `S` with `-`
+			.replace(/t/g, 'x') // Replace `t` with `x`
+			.replace(/T/g, '-'); // Replace `T` with `-`
+
+		// Convert to numeric format
+		const numericPermissions = permissions
+			.slice(0, 9) // Take only access rights, excluding file type
+			.match(/.{1,3}/g) // Split into groups of three characters (e.g., `rwx`, `r-x`)
+			.map(group => group.split('').reduce((sum, char) => sum + mapping[char], 0)) // Convert to numbers
+			.join('');
+
+		return specialBits + parseInt(numericPermissions, 10); // Add "special" bits
+	},
+
+	/**
+	 * Parses a single line of `ls -lA --full-time` output.
+	 * @param {string} line - A single line from the `ls` command output.
+	 * @returns {Object|null} - Returns an object with file information or null if parsing fails.
+	 */
+
+	parseLsLine: function(line) {
+		const regex = /^([\-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+(.+)$/;
+		const parts = line.match(regex);
+		if (!parts || parts.length < 7) {
+			console.warn('Failed to parse line:', line);
+			return null;
+		}
+
+		const typeChar = parts[1]; // File type
+		const owner = parts[2]; // Owner
+		const group = parts[3]; // Group
+		const size = parseInt(parts[4], 10); // Size in bytes
+		const mtime = new Date(parts[5]).toLocaleString(); // Modification date
+		let nameField = parts[6].trim(); // File or symbolic link name
+
+		const isDirectory = typeChar === 'd';
+		const isSymlink = typeChar === 'l';
+		let name = nameField;
+		let linkTarget = null;
+
+		// Handling symbolic links
+		if (isSymlink) {
+			const arrowIndex = nameField.indexOf(' -> ');
+			if (arrowIndex !== -1) {
+				name = nameField.substring(0, arrowIndex).trim();
+				linkTarget = nameField.substring(arrowIndex + 4).trim();
+			}
+		}
+
+		const type = isDirectory ? 'Directory' : isSymlink ? 'Symlink' : 'File';
+
+		return {
+			name: name,
+			size: size,
+			date: mtime,
+			type: type,
+			permissions: this.convertPermissionsToNumeric(line.slice(1, 10)), // Convert permissions
+			owner: owner,
+			group: group,
+			isDirectory: isDirectory,
+			isSymlink: isSymlink,
+			linkTarget: linkTarget,
+			mtime: new Date(parts[5]).getTime()
+		};
+	},
+
+	/**
+	 * Loads and displays the contents of a directory.
+	 * @param {string} dir - The directory path to load.
+	 */
+	loadDirectory: function(dir) {
+		var self = this;
+		self.lastLoadId = (self.lastLoadId || 0) + 1;
+		var loadId = self.lastLoadId;
+
+
+		// Do not clear selectedItems set, as it should persist across sorting
+		// self.selectedItems.clear();
+		self.updateDeleteSelectedButtonState();
+
+		if (!self.loadingIndicator) {
+			self.loadingIndicator = document.createElement('div');
+			self.loadingIndicator.className = 'navigation-plugin-loading' + self.uniqueSuffix;
+			self.loadingIndicator.textContent = `[${PN}]: ` + _('Loading...');
+			// Insert before breadcrumb for visibility
+			self.navDiv.insertBefore(self.loadingIndicator, self.breadcrumb);
+		}
+		self.loadingIndicator.style.display = 'block';
+
+		self.tbody.innerHTML = '';
+
+		var pathInput = self.navDiv.querySelector('.navigation-plugin-header' + self.uniqueSuffix + ' input[type="text"]');
+		if (pathInput) {
+			pathInput.value = self.currentDir;
+		}
+
+		fs.exec('/bin/ls', ['-lA', '--full-time', dir]).then(function(res) {
+			// Check load relevance
+			if (loadId !== self.lastLoadId) {
+				// Old result, ignore
+				return;
+			}
+
+			self.loadingIndicator.style.display = 'none';
+
+			if (res.code !== 0) {
+				self.popm(null, `[${PN}]: ` + _('Failed to list directory: %s').format(res.stderr.trim()), 'error');
+				self.tbody.innerHTML = '<tr><td colspan="6">' + `[${PN}]: ` + _('Error loading directory.') + '</td></tr>';
+				return;
+			}
+
+			var lines = res.stdout.split('\n').filter(line => line.trim() !== '');
+			var files = [];
+			lines.forEach(function(line) {
+				var file = self.parseLsLine(line);
+				if (file) {
+					files.push(file);
+				}
+			});
+
+			// Sort files based on current sort settings
+			files.sort(self.compareFiles.bind(self));
+
+			// Add parent directory entry if not in root
+			if (dir !== '/') {
+				self.addParentDirectoryEntry();
+			}
+
+			// Render each file row
+			files.forEach(function(file) {
+				self.renderFileRow(file);
+			});
+
+			// Update breadcrumb navigation
+			self.updateBreadcrumb();
+
+			// Update sort icons in headers
+			var headers = self.thead.querySelectorAll('th');
+			headers.forEach(function(header) {
+				var field = header.dataset.field;
+				if (field) { // Only sortable columns
+					var sortIcon = header.querySelector('span:nth-child(2)');
+					if (sortIcon) { // Check if sortIcon exists
+						if (self.sortField === field) {
+							sortIcon.textContent = self.sortDirection === 'asc' ? '▲' : '▼';
+						} else {
+							sortIcon.textContent = '⇅';
+						}
+					}
+				}
+			});
+			// Restore selection state after loading directory
+			self.updateSelectionState();
+		}).catch(function(err) {
+			if (loadId !== self.lastLoadId) return; // Old result, ignore
+
+			self.loadingIndicator.style.display = 'none';
+			console.error('Error listing directory:', err);
+			self.tbody.innerHTML = '<tr><td colspan="6">' + `[${PN}]: ` + _('Error loading directory.') + '</td></tr>';
+		});
+	},
+
+	/**
+	 * Renders a single file row in the table.
+	 * @param {object} file - The file object containing its properties.
+	 */
+	renderFileRow: function(file) {
+		var self = this;
+
+		// Create the table row element
+		var row = document.createElement('tr');
+		row.className = (file.isDirectory ? 'directory' : file.isSymlink ? 'symlink' : 'file') + self.uniqueSuffix;
+		row.dataset.filePath = self.concatPath(self.currentDir, file.name);
+
+		// Determine if this row represents the parent directory
+		var isParent = file.isParent || false;
+
+		// Checkbox cell for selecting items
+		var checkboxCell = document.createElement('td');
+		var checkbox = document.createElement('input');
+		checkbox.type = 'checkbox';
+		checkbox.className = 'select-item' + self.uniqueSuffix;
+		checkbox.dataset.path = self.concatPath(self.currentDir, file.name);
+		checkbox.onclick = function() {
+			if (this.checked) {
+				self.selectedItems.add(this.dataset.path);
+			} else {
+				self.selectedItems.delete(this.dataset.path);
+			}
+			self.updateDeleteSelectedButtonState();
+		};
+		checkboxCell.appendChild(checkbox);
+		row.appendChild(checkboxCell);
+
+		// Name cell with a clickable link
+		var nameCell = document.createElement('td');
+		nameCell.className = 'name' + self.uniqueSuffix;
+		var nameLink = document.createElement('a');
+
+		if (file.isDirectory) {
+			nameLink.className = 'directory-name' + self.uniqueSuffix;
+		} else if (file.isSymlink) {
+			nameLink.className = 'symlink-name' + self.uniqueSuffix;
+		} else {
+			nameLink.className = 'file-name' + self.uniqueSuffix;
+		}
+
+		nameLink.textContent = file.isSymlink ? `${file.name} -> ${file.linkTarget}` : file.name;
+		nameLink.onclick = function() {
+			if (file.isDirectory) {
+				self.enterDirectory(file.name); // Navigate into directory
+			} else {
+				self.openFileforEditing(
+					self.concatPath(self.currentDir, file.name),
+					file.permissions,
+					`${file.owner}:${file.group}`
+				); // Open file for editing
+			}
+		};
+
+		// Make the link draggable if it's a regular file
+		if (!file.isDirectory && !file.isSymlink) {
+			nameLink.setAttribute('draggable', 'true');
+			nameLink.classList.add('draggable' + self.uniqueSuffix);
+
+			// Handle 'dragstart' event for files
+			nameLink.addEventListener('dragstart', function(ev) {
+
+				// Get selected items or fallback to the current file
+				var selectedArray = Array.from(self.selectedItems);
+				if (selectedArray.length === 0) {
+					selectedArray = [self.concatPath(self.currentDir, file.name)];
+				}
+
+				// Set data in 'application/myapp-files' MIME types
+				var jsonData = JSON.stringify(selectedArray);
+				ev.dataTransfer.setData('application/myapp-files', jsonData);
+
+				ev.dataTransfer.effectAllowed = 'copyMove';
+
+				self.draggedFiles = selectedArray;
+
+				self.popm(
+					null,
+					`[${PN}]: ` + _('Dragging started. Drop onto a directory within this UI to copy/move files (Alt=copy), or drop outside the browser to download.'),
+					'info'
+				);
+
+			});
+
+			// Handle 'dragend' event to manage fallback download
+			nameLink.addEventListener('dragend', function(ev) {
+
+				// If dropEffect is 'none', initiate file download
+				if (self.draggedFiles && ev.dataTransfer.dropEffect === 'none') {
+					self.downloadFilesSequentially(self.draggedFiles);
+				}
+				self.draggedFiles = null;
+			});
+		}
+
+		// Tooltip handling for overflowing text (optional)
+		if (nameLink.scrollWidth > nameLink.clientWidth) {
+			nameLink.dataset.hasOverflow = 'true';
+
+			// Show tooltip on hover with a delay
+			let hoverTimer;
+
+			nameLink.addEventListener('mouseenter', function(e) {
+				hoverTimer = setTimeout(function() {
+					self.showTooltip(e, file.isSymlink ? `${file.name} -> ${file.linkTarget}` : file.name);
+				}, 500); // 500ms delay
+			});
+
+			// Update tooltip position on mouse move
+			nameLink.addEventListener('mousemove', function(e) {
+				if (self.currentTooltip) {
+					self.positionTooltip(e);
+				}
+			});
+
+			// Hide tooltip on mouse leave
+			nameLink.addEventListener('mouseleave', function(e) {
+				clearTimeout(hoverTimer);
+				self.hideTooltip();
+			});
+		}
+
+		nameCell.appendChild(nameLink);
+		row.appendChild(nameCell);
+
+		// Type cell
+		var typeCell = document.createElement('td');
+		typeCell.textContent = file.type;
+		row.appendChild(typeCell);
+
+		// Size cell
+		var sizeCell = document.createElement('td');
+		sizeCell.textContent = file.isDirectory ? '-' : self.formatSize(file.size);
+		row.appendChild(sizeCell);
+
+		// Modification date cell
+		var dateCell = document.createElement('td');
+		dateCell.textContent = file.date;
+		row.appendChild(dateCell);
+
+		// Actions cell with buttons (excluded for parent directory)
+		var actionsCell = document.createElement('td');
+		actionsCell.className = 'navigation-plugin-actions' + self.uniqueSuffix;
+
+		if (!isParent && (file.isDirectory || file.isSymlink || !file.isDirectory)) {
+			// Edit Button
+			var editButton = document.createElement('span');
+			editButton.className = 'action-button' + self.uniqueSuffix;
+			editButton.textContent = '✏️';
+			editButton.title = `[${PN}]: ` + _('Edit');
+			editButton.onclick = function() {
+				self.handleEditClick(file);
+			};
+			actionsCell.appendChild(editButton);
+
+			// Copy Button
+			var copyButton = document.createElement('span');
+			copyButton.className = 'action-button' + self.uniqueSuffix;
+			copyButton.textContent = '📋';
+			copyButton.title = `[${PN}]: ` + _('Copy');
+			copyButton.onclick = function() {
+				self.handleCopyClick(file);
+			};
+			actionsCell.appendChild(copyButton);
+
+			// Delete Button
+			var deleteButton = document.createElement('span');
+			deleteButton.className = 'action-button' + self.uniqueSuffix;
+			deleteButton.textContent = '🗑️';
+			deleteButton.title = `[${PN}]: ` + _('Delete');
+			deleteButton.onclick = function() {
+				self.handleDeleteClick(file.name);
+			};
+			actionsCell.appendChild(deleteButton);
+
+			// Download Button (only for regular files)
+			if (!file.isDirectory && !file.isSymlink) {
+				var downloadButton = document.createElement('span');
+				downloadButton.className = 'action-button' + self.uniqueSuffix;
+				downloadButton.textContent = '⬇️';
+				downloadButton.title = `[${PN}]: ` + _('Download');
+				downloadButton.onclick = function() {
+					self.handleDownloadClick(file.name);
+				};
+				actionsCell.appendChild(downloadButton);
+			}
+		}
+
+		row.appendChild(actionsCell);
+
+		// If the file is a directory, attach drag-and-drop handlers
+		if (file.isDirectory) {
+			var destinationDir = self.concatPath(self.currentDir, file.name);
+			self.attachDragDropHandlers(row, destinationDir);
+		}
+
+		self.tbody.appendChild(row);
+	},
+
+	/**
+	 * Compares two files based on the current sort field and direction.
+	 * @param {object} a - The first file object.
+	 * @param {object} b - The second file object.
+	 * @returns {number} - Comparison result.
+	 */
+	compareFiles: function(a, b) {
+		var self = this;
+		var field = self.sortField;
+		var direction = self.sortDirection === 'asc' ? 1 : -1;
+
+		var valueA = a[field];
+		var valueB = b[field];
+
+		if (field === 'size') {
+			valueA = a.isDirectory ? 0 : a.size;
+			valueB = b.isDirectory ? 0 : b.size;
+		} else if (field === 'mtime') {
+			valueA = a.mtime;
+			valueB = b.mtime;
+		} else {
+			valueA = String(valueA).toLowerCase();
+			valueB = String(valueB).toLowerCase();
+		}
+
+		if (valueA < valueB) return -1 * direction;
+		if (valueA > valueB) return 1 * direction;
+		return 0;
+	},
+
+	/**
+	 * Updates selected items' states after sorting.
+	 * This function restores checkboxes' states based on the preserved selection set.
+	 */
+	updateSelectionState: function() {
+		var self = this;
+		var checkboxes = self.tbody.querySelectorAll('.select-item' + self.uniqueSuffix);
+
+		checkboxes.forEach(function(checkbox) {
+			var filePath = checkbox.dataset.path;
+			checkbox.checked = self.selectedItems.has(filePath);
+		});
+	},
+
+	/**
+	 * Enters a specified directory.
+	 * @param {string} dirName - The name of the directory to enter.
+	 */
+	enterDirectory: function(dirName) {
+		var self = this;
+		var newDir = self.concatPath(self.currentDir, dirName);
+		self.currentDir = newDir.endsWith('/') ? newDir : newDir + '/';
+		self.settings.currentDir = self.currentDir;
+		// self.set_settings(self.settings);
+		self.loadDirectory(self.currentDir);
+	},
+
+	/**
+	 * Adds a parent directory entry ("..") to the table.
+	 */
+	addParentDirectoryEntry: function() {
+		var self = this;
+
+		// Create the table row element for the parent directory
+		var row = document.createElement('tr');
+		row.className = 'directory' + self.uniqueSuffix;
+		row.dataset.filePath = self.concatPath(self.currentDir, '..');
+		row.dataset.isParent = 'true'; // Flag to identify as parent directory
+
+		// Checkbox cell (empty for parent directory)
+		var checkboxCell = document.createElement('td');
+
+		// Name cell with a clickable link to navigate up
+		var nameCell = document.createElement('td');
+		nameCell.className = 'name' + self.uniqueSuffix;
+		var nameLink = document.createElement('a');
+		nameLink.className = 'directory-name' + self.uniqueSuffix;
+		nameLink.textContent = '.. (Parent Directory)';
+		nameLink.onclick = function() {
+			self.navigateUp(); // Navigate to parent directory
+		};
+		nameCell.appendChild(nameLink);
+
+		// Type cell (always 'Directory' for parent directory)
+		var typeCell = document.createElement('td');
+		typeCell.textContent = 'Directory';
+
+		// Size cell (empty for parent directory)
+		var sizeCell = document.createElement('td');
+		sizeCell.textContent = '-';
+
+		// Modification date cell (empty for parent directory)
+		var dateCell = document.createElement('td');
+		dateCell.textContent = '-';
+
+		// Actions cell (empty, no buttons)
+		var actionsCell = document.createElement('td');
+
+		// Append all cells to the row
+		row.appendChild(checkboxCell);
+		row.appendChild(nameCell);
+		row.appendChild(typeCell);
+		row.appendChild(sizeCell);
+		row.appendChild(dateCell);
+		row.appendChild(actionsCell);
+
+		// Attach drag-and-drop handlers for the parent directory
+		var parentDir = self.getParentDirectory(self.currentDir);
+		self.attachDragDropHandlers(row, parentDir);
+
+		// Append the row to the table body
+		self.tbody.appendChild(row);
+	},
+
+	/**
+	 * Helper function to get the parent directory of a given path.
+	 * @param {string} dir - The current directory path.
+	 * @returns {string} - The parent directory path.
+	 */
+	getParentDirectory: function(dir) {
+		if (dir === '/') return '/'; // Root directory has no parent
+
+		// Remove trailing slash and split the path
+		var pathParts = dir.slice(0, -1).split('/');
+		pathParts.pop(); // Remove the last part to get the parent
+
+		var parentDir = pathParts.join('/') || '/'; // Join back to form the parent path
+		parentDir = parentDir.endsWith('/') ? parentDir : parentDir + '/'; // Ensure trailing slash
+
+		return parentDir;
+	},
+
+	/**
+	 * Navigates up to the parent directory.
+	 */
+	navigateUp: function() {
+		var self = this;
+		if (self.currentDir === '/') return;
+
+		var pathParts = self.currentDir.slice(0, -1).split('/');
+		pathParts.pop();
+		var parentDir = pathParts.join('/') || '/';
+		self.currentDir = parentDir.endsWith('/') ? parentDir : parentDir + '/';
+		self.settings.currentDir = self.currentDir;
+		// self.set_settings(self.settings);
+		self.loadDirectory(self.currentDir);
+	},
+
+	/**
+	 * Handles the download button click event by sending a JSON request and downloading the file.
+	 * @param {string} fileName - The name of the file to download.
+	 */
+	handleDownloadClick: function(fileName) {
+		var self = this;
+		var filePath = self.concatPath(self.currentDir, fileName);
+
+		// Use the read_direct method to download the file
+		fs.read_direct(filePath, 'blob')
+			.then(function(blob) {
+				if (!(blob instanceof Blob)) {
+					throw new Error(`[${PN}]: ` + _('Response is not a Blob'));
+				}
+				var url = window.URL.createObjectURL(blob);
+				var a = document.createElement('a');
+				a.href = url;
+				a.download = fileName;
+				document.body.appendChild(a);
+				a.click();
+				a.remove();
+				window.URL.revokeObjectURL(url);
+			})
+			.catch(function(error) {
+				console.error(`[${PN}]: ` + _('Download failed:'), error);
+				alert(`[${PN}]: ` + _('Download failed: ') + error.message);
+			});
+	},
+
+	/**
+	 * Handles the delete button click event for a single file.
+	 * @param {string} fileName - The name of the file to delete.
+	 */
+	handleDeleteClick: function(fileName) {
+		var self = this;
+		var filePath = self.concatPath(self.currentDir, fileName);
+		if (confirm(`[${PN}]: ` + _('Are you sure you want to delete "%s"?').format(fileName))) {
+			fs.remove(filePath).then(function() {
+				self.popm(null, `[${PN}]: ` + _('File "%s" deleted successfully.').format(fileName), 'info');
+				self.loadDirectory(self.currentDir);
+			}).catch(function(err) {
+				self.popm(null, `[${PN}]: ` + _('Failed to delete file "%s": %s').format(fileName, err.message), 'error');
+			});
+		}
+	},
+
+	/**
+	 * Handles the copy button click event for a file.
+	 * @param {object} file - The file object to copy.
+	 */
+	handleCopyClick: function(file) {
+		var self = this;
+
+		// Construct the original file path
+		var originalPath = self.concatPath(self.currentDir, file.name);
+		var baseName = file.name;
+		var extension = '';
+		var nameWithoutExt = baseName;
+
+		// Split the filename into name and extension
+		var lastDot = baseName.lastIndexOf('.');
+		if (lastDot !== -1 && lastDot !== 0) {
+			nameWithoutExt = baseName.substring(0, lastDot);
+			extension = baseName.substring(lastDot);
+		}
+
+		/**
+		 * Recursively finds the next available copy number to avoid name conflicts.
+		 * @param {number} n - The current copy number to try.
+		 * @returns {Promise<number>} - Resolves with the next available copy number.
+		 */
+		function findNextCopyNumber(n) {
+			var newName = `${nameWithoutExt} (copy ${n})${extension}`;
+			var newPath = self.concatPath(self.currentDir, newName);
+
+			// Attempt to stat the new path to check if it exists
+			return fs.stat(newPath).then(function(stat) {
+				// If the path exists, try the next number
+				return findNextCopyNumber(n + 1);
+			}).catch(function(err) {
+				// Handle 'Resource not found' as the file does not exist
+				if (err.message === 'Resource not found') { // Adjust based on actual error message
+					return n;
+				} else {
+					// An unexpected error occurred
+					// Notify the user about the unexpected error
+					self.popm(null, `[${PN}]: ` + _('Error checking "%s": %s').format(newPath, err.message), 'error');
+					throw err;
+				}
+			});
+		}
+
+		// Start finding the next available copy number
+		findNextCopyNumber(1).then(function(n) {
+			var newName = `${nameWithoutExt} (copy ${n})${extension}`;
+			var newPath = self.concatPath(self.currentDir, newName);
+
+			if (file.isDirectory) {
+				// Use 'cp -r' to copy directories recursively
+				fs.exec('/bin/cp', ['-r', originalPath, newPath]).then(function(res) {
+					if (res.code === 0) {
+						self.popm(null, `[${PN}]: ` + _('Directory "%s" copied successfully as "%s".').format(file.name, newName), 'info');
+						self.loadDirectory(self.currentDir);
+					} else {
+						self.popm(null, `[${PN}]: ` + _('Failed to copy directory "%s": %s').format(file.name, res.stderr.trim()), 'error');
+					}
+				}).catch(function(err) {
+					self.popm(null, `[${PN}]: ` + _('Failed to copy directory "%s": %s').format(file.name, err.message), 'error');
+				});
+			} else if (file.isSymlink) {
+				// Use 'ln -s' to copy symbolic links
+				fs.exec('/bin/ln', ['-s', file.linkTarget, newPath]).then(function(res) {
+					if (res.code === 0) {
+						self.popm(null, `[${PN}]: ` + _('Symlink "%s" copied successfully as "%s".').format(file.name, newName), 'info');
+						self.loadDirectory(self.currentDir);
+					} else {
+						self.popm(null, `[${PN}]: ` + _('Failed to copy symlink "%s": %s').format(file.name, res.stderr.trim()), 'error');
+					}
+				}).catch(function(err) {
+					self.popm(null, `[${PN}]: ` + _('Failed to copy symlink "%s": %s').format(file.name, err.message), 'error');
+				});
+			} else {
+				// Use 'cp' to copy regular files
+				fs.exec('/bin/cp', [originalPath, newPath]).then(function(res) {
+					if (res.code === 0) {
+						self.popm(null, `[${PN}]: ` + _('File "%s" copied successfully as "%s".').format(file.name, newName), 'info');
+						self.loadDirectory(self.currentDir);
+					} else {
+						self.popm(null, `[${PN}]: ` + _('Failed to copy file "%s": %s').format(file.name, res.stderr.trim()), 'error');
+					}
+				}).catch(function(err) {
+					self.popm(null, `[${PN}]: ` + _('Failed to copy file "%s": %s').format(file.name, err.message), 'error');
+				});
+			}
+		}).catch(function(err) {
+			self.popm(null, `[${PN}]: ` + _('Failed to find copy number for "%s": %s').format(file.name, err.message), 'error');
+		});
+	},
+
+	/**
+	 * Handles the edit button click event for a file.
+	 * @param {object} file - The file object to edit.
+	 */
+	handleEditClick: function(file) {
+		var self = this;
+		var filePath = self.concatPath(self.currentDir, file.name);
+		var fileName = file.name;
+
+		var modal = E('div', {
+			'class': 'navigation-plugin-modal' + self.uniqueSuffix
+		}, [
+			E('div', {
+				'class': 'navigation-plugin-modal-content' + self.uniqueSuffix
+			}, [
+				E('span', {
+					'class': 'navigation-plugin-close-button' + self.uniqueSuffix,
+					'innerHTML': '&times;'
+				}),
+				E('h2', `[${PN}]: ` + _('Edit "%s"').format(fileName)),
+				E('label', `[${PN}]: ` + _('New Name:')),
+				E('input', {
+					type: 'text',
+					id: 'edit-new-name' + self.uniqueSuffix,
+					value: fileName
+				}),
+				E('label', `[${PN}]: ` + _('Owner:Group:')),
+				E('input', {
+					type: 'text',
+					id: 'edit-owner' + self.uniqueSuffix,
+					value: (file.owner + ':' + file.group)
+				}),
+				E('label', `[${PN}]: ` + _('Permissions:')),
+				E('input', {
+					type: 'text',
+					id: 'edit-permissions' + self.uniqueSuffix,
+					value: file.permissions
+				}),
+				E('button', {
+					id: 'edit-submit-button' + self.uniqueSuffix
+				}, _('Submit')),
+				E('button', {
+					id: 'edit-cancel-button' + self.uniqueSuffix
+				}, _('Cancel'))
+			])
+		]);
+
+		document.body.appendChild(modal);
+
+		var closeButton = modal.querySelector('.navigation-plugin-close-button' + self.uniqueSuffix);
+		var submitButton = modal.querySelector('#edit-submit-button' + self.uniqueSuffix);
+		var cancelButton = modal.querySelector('#edit-cancel-button' + self.uniqueSuffix);
+
+		/**
+		 * Closes the modal window.
+		 */
+		function closeModal() {
+			document.body.removeChild(modal);
+		}
+
+		closeButton.onclick = closeModal;
+		cancelButton.onclick = closeModal;
+
+		submitButton.onclick = function() {
+			var newName = modal.querySelector('#edit-new-name' + self.uniqueSuffix).value.trim();
+			var newOwner = modal.querySelector('#edit-owner' + self.uniqueSuffix).value.trim();
+			var newPermissions = modal.querySelector('#edit-permissions' + self.uniqueSuffix).value.trim();
+
+			if (newName === '') {
+				self.popm(null, `[${PN}]: ` + _('File name cannot be empty.'), 'error');
+				return;
+			}
+
+			var renamePromise = Promise.resolve();
+			if (newName !== fileName) {
+				var newPath = self.concatPath(self.currentDir, newName);
+				renamePromise = fs.exec('/bin/mv', [filePath, newPath]).then(function() {
+					filePath = newPath;
+				});
+			}
+
+			var ownerPromise = fs.exec('/bin/chown', [newOwner, filePath]);
+			var permissionsPromise = fs.exec('/bin/chmod', [newPermissions, filePath]);
+
+			renamePromise.then(function() {
+				return ownerPromise;
+			}).then(function() {
+				return permissionsPromise;
+			}).then(function() {
+				self.popm(null, `[${PN}]: ` + _('"%s" edited successfully.').format(newName), 'info');
+				closeModal();
+				self.loadDirectory(self.currentDir);
+			}).catch(function(err) {
+				self.popm(null, `[${PN}]: ` + _('Failed to edit "%s": %s').format(newName, err.message), 'error');
+			});
+		};
+	},
+
+	/**
+	 * Formats file size into a human-readable form.
+	 * @param {number} size - The file size in bytes.
+	 * @returns {string} - The formatted size string.
+	 */
+	formatSize: function(size) {
+		var bytes = parseInt(size, 10);
+		if (isNaN(bytes)) return size;
+
+		var sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
+		if (bytes === 0) return '0 B';
+		var i = Math.floor(Math.log(bytes) / Math.log(1024));
+		return parseFloat((bytes / Math.pow(1024, i)).toFixed(2)) + ' ' + sizes[i];
+	},
+
+	/**
+	 * Updates the breadcrumb navigation based on the current directory.
+	 */
+	updateBreadcrumb: function() {
+		var self = this;
+		self.breadcrumb.innerHTML = '';
+
+		var pathParts = self.currentDir.split('/').filter(part => part.length > 0);
+		var accumulatedPath = '/';
+
+		var rootLink = document.createElement('a');
+		rootLink.href = '#';
+		rootLink.textContent = `[${PN}]: ` + _('Root');
+		rootLink.onclick = function(e) {
+			e.preventDefault();
+			self.currentDir = '/';
+			self.settings.currentDir = self.currentDir;
+			// self.set_settings(self.settings);
+			self.loadDirectory(self.currentDir);
+		};
+		self.breadcrumb.appendChild(rootLink);
+
+		if (pathParts.length > 0) {
+			self.breadcrumb.appendChild(document.createTextNode(' / '));
+		}
+
+		pathParts.forEach(function(part, index) {
+			accumulatedPath = self.concatPath(accumulatedPath, part);
+			let currentPath = accumulatedPath;
+
+			var link = document.createElement('a');
+			link.href = '#';
+			link.textContent = part;
+			link.onclick = function(e) {
+				e.preventDefault();
+				self.currentDir = currentPath;
+				self.settings.currentDir = self.currentDir;
+				// self.set_settings(self.settings);
+				self.loadDirectory(self.currentDir);
+			};
+			self.breadcrumb.appendChild(link);
+
+			if (index < pathParts.length - 1) {
+				self.breadcrumb.appendChild(document.createTextNode(' / '));
+			}
+		});
+	},
+
+	// Store dragged files in an array when drag starts
+	handleDragStart: function(ev, fileName) {
+		var self = this;
+		ev.dataTransfer.effectAllowed = 'copy';
+
+		// Count selected files
+		var selectedArray = Array.from(self.selectedItems);
+		if (selectedArray.length === 0) {
+			// If no items are selected, consider the dragged file as the single target
+			selectedArray = [self.concatPath(self.currentDir, fileName)];
+		}
+
+		// Notify user that direct drag-and-drop isn't supported, but we'll handle it after drag ends
+		self.popm(null, `[${PN}]: Direct drag-and-drop to local storage is not supported. The file(s) will be downloaded when you release the mouse.`, 'info');
+
+		// Store these files for download after drag ends
+		self.draggedFiles = selectedArray;
+	},
+
+	// Download the stored files once the user ends the drag operation
+	handleDragEnd: function(ev) {
+		var self = this;
+
+		if (self.draggedFiles && self.draggedFiles.length > 0) {
+			// Now that the drag operation has ended, start downloading the files
+			self.downloadFilesSequentially(self.draggedFiles);
+			// Clear draggedFiles after processing
+			self.draggedFiles = null;
+		}
+	},
+
+	// Download multiple files sequentially
+	downloadFilesSequentially: function(filePaths) {
+		var self = this;
+
+		function downloadNext(index) {
+			if (index >= filePaths.length) {
+				return;
+			}
+
+			var filePath = filePaths[index];
+			var fileName = filePath.split('/').pop();
+
+			fs.read_direct(filePath, 'blob')
+				.then(function(blob) {
+					if (!(blob instanceof Blob)) {
+						throw new Error(`[${PN}]: ` + _('Response is not a Blob'));
+					}
+					var url = window.URL.createObjectURL(blob);
+					var a = document.createElement('a');
+					a.href = url;
+					a.download = fileName;
+					document.body.appendChild(a);
+					a.click();
+					a.remove();
+					window.URL.revokeObjectURL(url);
+
+					// Proceed to the next file
+					downloadNext(index + 1);
+				})
+				.catch(function(error) {
+					console.error(`[${PN}]: Download failed:`, error);
+					self.popm(null, `[${PN}]: Download failed: ${error.message}`, 'error');
+					// Continue with next file even if one fails
+					downloadNext(index + 1);
+				});
+		}
+
+		downloadNext(0);
+	},
+	// Внутри класса Navigation Plugin
+
+	/**
+	 * Показывает иконку плюс рядом с целевой директорией при удерживании клавиши Alt.
+	 * @param {MouseEvent} event - Событие мыши.
+	 * @param {HTMLElement} targetRow - Строка таблицы, представляющая директорию.
+	 */
+	showAltIcon: function(event, targetRow) {
+		var self = this;
+
+		// Удаляем существующую иконку, если она есть
+		self.hideAltIcon();
+
+		// Создаем элемент иконки плюс
+		var plusIcon = document.createElement('div');
+		plusIcon.className = 'navigation-plugin-alt-icon' + self.uniqueSuffix;
+		plusIcon.innerHTML = '➕'; // Юникод иконка плюса
+		plusIcon.style.position = 'absolute';
+		// Располагаем иконку по центру строки
+		plusIcon.style.top = '50%';
+		plusIcon.style.left = '50%';
+		plusIcon.style.transform = 'translate(-50%, -50%)';
+		plusIcon.style.pointerEvents = 'none'; // Иконка не перехватывает события
+		plusIcon.style.fontSize = '24px';
+		plusIcon.style.color = '#000'; // Цвет иконки
+		plusIcon.style.zIndex = '1001'; // Поверх других элементов
+
+		// Добавляем иконку в строку таблицы
+		targetRow.appendChild(plusIcon);
+		self.altIcon = plusIcon;
+	},
+
+	/**
+	 * Скрывает иконку плюс.
+	 */
+	hideAltIcon: function() {
+		var self = this;
+		if (self.altIcon) {
+			self.altIcon.remove();
+			self.altIcon = null;
+		}
+	},
+
+	/**
+	 * Attaches drag-and-drop event handlers to a directory row.
+	 * @param {HTMLElement} row - The table row element representing a directory.
+	 * @param {string} destinationDir - The directory path where files will be copied/moved.
+	 */
+	attachDragDropHandlers: function(row, destinationDir) {
+		var self = this;
+
+		// Handle 'dragover' event to allow dropping
+		row.addEventListener('dragover', function(e) {
+			e.preventDefault();
+			e.stopPropagation();
+			var isCopy = e.altKey; // Determine if the operation is copy based on Alt key
+			e.dataTransfer.dropEffect = isCopy ? 'copy' : 'move';
+
+			// Add visual indicators for drag-over state
+			row.classList.add('drag-over' + self.uniqueSuffix);
+			if (isCopy) {
+				row.classList.add('drag-over-copy' + self.uniqueSuffix);
+				row.classList.remove('drag-over-move' + self.uniqueSuffix);
+				self.showAltIcon(e, row); // Show copy icon
+			} else {
+				row.classList.add('drag-over-move' + self.uniqueSuffix);
+				row.classList.remove('drag-over-copy' + self.uniqueSuffix);
+				self.hideAltIcon(); // Hide copy icon
+			}
+		});
+
+		// Handle 'dragenter' event similarly to 'dragover'
+		row.addEventListener('dragenter', function(e) {
+			e.preventDefault();
+			e.stopPropagation();
+			var isCopy = e.altKey;
+			e.dataTransfer.dropEffect = isCopy ? 'copy' : 'move';
+
+			row.classList.add('drag-over' + self.uniqueSuffix);
+			if (isCopy) {
+				row.classList.add('drag-over-copy' + self.uniqueSuffix);
+				row.classList.remove('drag-over-move' + self.uniqueSuffix);
+				self.showAltIcon(e, row);
+			} else {
+				row.classList.add('drag-over-move' + self.uniqueSuffix);
+				row.classList.remove('drag-over-copy' + self.uniqueSuffix);
+				self.hideAltIcon();
+			}
+		});
+
+		// Handle 'dragleave' event to remove visual indicators
+		row.addEventListener('dragleave', function(e) {
+			e.preventDefault();
+			e.stopPropagation();
+			row.classList.remove('drag-over' + self.uniqueSuffix);
+			row.classList.remove('drag-over-copy' + self.uniqueSuffix);
+			row.classList.remove('drag-over-move' + self.uniqueSuffix);
+			self.hideAltIcon();
+		});
+
+		// Handle 'drop' event to perform copy/move operations
+		row.addEventListener('drop', function(e) {
+			e.preventDefault();
+			e.stopPropagation();
+
+			// Remove visual indicators
+			row.classList.remove('drag-over' + self.uniqueSuffix);
+			row.classList.remove('drag-over-copy' + self.uniqueSuffix);
+			row.classList.remove('drag-over-move' + self.uniqueSuffix);
+			self.hideAltIcon();
+
+			// Retrieve dragged files data
+			var draggedFilesJson = e.dataTransfer.getData('application/myapp-files');
+			if (!draggedFilesJson) {
+				// Fallback to 'text/plain' if custom MIME type is not available
+				draggedFilesJson = e.dataTransfer.getData('text/plain');
+			}
+
+			if (!draggedFilesJson) {
+				self.popm(null, `[${PN}]: ` + _('No files were dragged.'), 'error');
+				return;
+			}
+
+			var draggedFiles;
+			try {
+				draggedFiles = JSON.parse(draggedFilesJson);
+			} catch (err) {
+				self.popm(null, `[${PN}]: ` + _('Failed to parse dragged files data.'), 'error');
+				return;
+			}
+
+			var isCopy = e.altKey; // Determine operation type
+			var cmd = isCopy ? 'cp' : 'mv'; // Command to execute
+			var args = isCopy ? ['-r'] : []; // Recursive flag for copy
+
+			// Append source files and destination directory to arguments
+			args = args.concat(draggedFiles).concat([destinationDir]);
+
+
+			// Execute the command using fs.exec
+			fs.exec('/bin/' + cmd, args)
+				.then(function(res) {
+					if (res.code === 0) {
+						var action = isCopy ? 'copied' : 'moved';
+						self.popm(null, `[${PN}]: Successfully ${action} files to "${destinationDir}".`, 'info');
+						self.loadDirectory(self.currentDir); // Refresh directory view
+					} else {
+						self.popm(null, `[${PN}]: Failed to ${isCopy ? 'copy' : 'move'} files to "${destinationDir}": ${res.stderr.trim()}`, 'error');
+					}
+				})
+				.catch(function(err) {
+					self.popm(null, `[${PN}]: Failed to ${isCopy ? 'copy' : 'move'} files to "${destinationDir}": ${err.message}`, 'error');
+				});
+		});
+	},
+
+
+	/**
+	 * Opens a file in the default editor based on the editor's style.
+	 * @param {string} filePath - The path to the file to open.
+	 */
+	openFileforEditing: function(filePath, permissions, ownerGroup) {
+		var self = this;
+
+		// Retrieve the default editor plugin
+		var defaultEditorName = self.default_plugins['Editor'];
+		var defaultEditor = self.pluginsRegistry[defaultEditorName];
+
+		if (!defaultEditor) {
+			self.popm(null, `[${PN}]: ` + _('No default editor plugin found.'), 'error');
+			return;
+		}
+
+		// Get the editor's style ('text' or 'bin') from its info
+		var editorInfo = defaultEditor.info();
+		var style = (editorInfo.style || 'text').toLowerCase(); // Default to 'text' if not specified
+
+		// Read the file content using the Navigation Plugin's read_file function
+		self.read_file(filePath, style).then(function(fileData) {
+			// Check if the default editor has an 'edit' function
+			if (typeof defaultEditor.edit === 'function') {
+				// Call the editor's edit function with the file path, content, style, permissions, and ownerGroup
+				defaultEditor.edit(filePath, fileData.content, style, permissions, ownerGroup);
+				if (self.pluginsRegistry['Main'] && typeof self.pluginsRegistry['Main'].activatePlugin === 'function') {
+					self.pluginsRegistry['Main'].activatePlugin(defaultEditorName);
+					self.popm(null, `[${PN}]: ` + _('File "%s" opened in editor.').format(filePath), 'success');
+				} else {
+					self.popm(null, `[${PN}]: ` + _('Unable to activate editor plugin.'), 'error');
+					console.error(`[${PN}]: ` + _('Main Dispatcher or activatePlugin method not found.'));
+				}
+
+			} else {
+				// Notify the user if the default editor does not implement the 'edit' function
+				self.popm(null, `[${PN}]: ` + _('Default editor does not implement edit function.'), 'error');
+			}
+		}).catch(function(err) {
+			// Notify the user if reading the file fails
+			self.popm(null, `[${PN}]: ` + _('Failed to read file "%s": %s').format(filePath, err.message), 'error');
+		});
+	}
+});
diff --git a/applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Settings.js b/applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Settings.js
new file mode 100644
index 000000000000..cccb4ba75a5f
--- /dev/null
+++ b/applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Settings.js
@@ -0,0 +1,697 @@
+'use strict';
+'require ui';
+'require fs';
+'require dom';
+'require form';
+
+// Define the plugin name as a constant
+const PN = 'Settings';
+
+return L.Class.extend({
+	info: function() {
+		return {
+			name: PN,
+			type: 'Settings'
+		};
+	},
+
+	/**
+	 * flattenObject(obj, parentKey = '', separator = '.')
+	 * Recursively converts a nested object into a flat object with keys separated by separator.
+	 */
+	flattenObject: function(obj, parentKey = '', separator = '.') {
+		let flatObj = {};
+		for (let key in obj) {
+			if (!obj.hasOwnProperty(key)) continue;
+			let newKey = parentKey ? `${parentKey}${separator}${key}` : key;
+			if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
+				Object.assign(flatObj, this.flattenObject(obj[key], newKey, separator));
+			} else {
+				flatObj[newKey] = obj[key];
+			}
+		}
+		return flatObj;
+	},
+
+	/**
+	 * unflattenObject(flatObj, separator = '.')
+	 * Restores a nested object from a flat object with separated keys.
+	 */
+	unflattenObject: function(flatObj, separator = '.') {
+		let nestedObj = {};
+		for (let flatKey in flatObj) {
+			if (!flatObj.hasOwnProperty(flatKey)) continue;
+			let keys = flatKey.split(separator);
+			keys.reduce((acc, key, index) => {
+				if (index === keys.length - 1) {
+					acc[key] = flatObj[flatKey];
+				} else {
+					if (!acc[key] || typeof acc[key] !== 'object') {
+						acc[key] = {};
+					}
+				}
+				return acc[key];
+			}, nestedObj);
+		}
+		console.log(`[${PN}]: ` + `unflattenObject result:`, nestedObj); // Debug
+		return nestedObj;
+	},
+
+	/**
+	 * serializeUCI(configObject)
+	 * Serializes a settings object into UCI format.
+	 */
+	serializeUCI: function(configObject) {
+		let configStr = '';
+
+		for (let sectionName in configObject) {
+			if (!configObject.hasOwnProperty(sectionName)) continue;
+			let section = configObject[sectionName];
+
+			// Determine the section type
+			let sectionType = section.type || 'option';
+			// Create a copy of the section without the type
+			let sectionCopy = Object.assign({}, section);
+			delete sectionCopy.type; // Remove type from options
+
+			// Start of configuration section
+			configStr += `config '${sectionType}' '${sectionName}'\n`;
+
+			// Convert nested objects into flat format
+			let flatOptions = this.flattenObject(sectionCopy);
+
+			// Add options
+			for (let key in flatOptions) {
+				if (!flatOptions.hasOwnProperty(key)) continue;
+				let value = flatOptions[key];
+
+				// Convert value to string
+				if (typeof value !== 'string') {
+					value = String(value);
+				}
+
+				// Escape single quotes
+				value = value.replace(/'/g, `'\\''`);
+				configStr += `\toption '${key}' '${value}'\n`;
+			}
+		}
+
+		return configStr;
+	},
+
+	/**
+	 * parseUCI(configContent)
+	 * Parses the content of a UCI configuration file and returns a settings object.
+	 */
+	parseUCI: function(configContent) {
+		var self = this; // Save reference to this
+		var config = {};
+		var currentSection = null;
+		var sectionType = null;
+		var sectionName = null;
+
+		// Split content into lines
+		var lines = configContent.split('\n');
+
+		lines.forEach(function(line) {
+			// Trim spaces
+			line = line.trim();
+
+			// Ignore empty lines and comments
+			if (line.length === 0 || line.startsWith('#') || line.startsWith('//')) {
+				return;
+			}
+
+			// Check if the line starts with a new configuration section
+			var configRegex = /^config\s+'([^']+)'\s+'([^']+)'$/;
+			var match = configRegex.exec(line);
+			if (match) {
+				sectionType = match[1];
+				sectionName = match[2];
+				config[sectionName] = {
+					type: sectionType
+				};
+				currentSection = config[sectionName];
+				return;
+			}
+
+			// Check if the line is an option within a section
+			var optionRegex = /^option\s+'([^']+)'\s+'([^']+)'$/;
+			match = optionRegex.exec(line);
+			if (match && currentSection) {
+				var optionKey = match[1];
+				var optionValue = match[2].replace(/\\'/g, `'`); // Unescape single quotes
+
+				// Add the option to the current section
+				currentSection[optionKey] = optionValue;
+			}
+		});
+
+		// Restore nested objects
+		for (let sectionName in config) {
+			if (!config.hasOwnProperty(sectionName)) continue;
+			let section = config[sectionName];
+			let flatOptions = {};
+
+			for (let key in section) {
+				if (!section.hasOwnProperty(key)) continue;
+				if (key === 'type') continue; // Skip section type
+				flatOptions[key] = section[key];
+			}
+
+			// Restore nested objects using self
+			let nestedOptions = self.unflattenObject(flatOptions);
+			config[sectionName] = Object.assign({
+				type: section.type
+			}, nestedOptions);
+
+			// Debug
+			console.log(`[${PN}]: ` + `Section "${sectionName}" after unflatten:`, config[sectionName]);
+		}
+
+		console.log(`[${PN}]: ` + `Parsed configuration:`, config); // Debug
+		return config;
+	},
+
+	/**
+	 * get_settings()
+	 * Returns the current settings of the plugin.
+	 */
+	get_settings: function() {
+		var settingsPanel = document.getElementById(`settings-panel-${this.info().name}-${this.uniqueId}`);
+		if (settingsPanel) {
+			// Remove 'px' from width and height and combine with 'x'
+			var width = parseInt(settingsPanel.style.width, 10) || 800;
+			var height = parseInt(settingsPanel.style.height, 10) || 600;
+			var window_size = `${width}x${height}`;
+			return {
+				window_size: window_size
+			};
+		}
+		// Default values if the settings panel is not found
+		return {
+			window_size: this.window_size || '800x600'
+		};
+	},
+
+	/**
+	 * set_settings(settings)
+	 * Applies the given settings to the plugin.
+	 */
+	set_settings: function(settings) {
+		if (settings.window_size) {
+			this.window_size = settings.window_size;
+			console.log(`[${PN}]: ` + `Window size set to "${this.window_size}".`);
+			this.apply_window_size();
+		}
+	},
+
+	/**
+	 * apply_window_size()
+	 * Applies the window size settings to the settings panel.
+	 */
+	apply_window_size: function() {
+		var dimensions = this.window_size.split('x');
+		var width = dimensions[0] + 'px';
+		var height = dimensions[1] + 'px';
+
+		var settingsPanel = document.getElementById(`settings-panel-${this.info().name}-${this.uniqueId}`);
+		if (settingsPanel) {
+			settingsPanel.style.width = width;
+			settingsPanel.style.height = height;
+		}
+	},
+
+	/**
+	 * read_settings()
+	 * Reads settings from the configuration file using the Navigation plugin and applies them.
+	 */
+	read_settings: function() {
+		var self = this;
+		return new Promise(function(resolve, reject) {
+			console.log(`[${PN}]: ` + `Reading settings for plugin "${self.info().name}"...`);
+			var navPluginName = self.defaultPlugins['Navigation'];
+			var navigationPlugin = self.loadedPlugins[navPluginName] || null;
+
+			self.read_file('/etc/config/file-plug-manager', 'text').then(function(fileData) {
+				self.permissions = fileData.permissions;
+				self.GroupOwner = fileData.GroupOwner;
+
+				console.log('[Settings] Configuration file content:', fileData.content);
+				var parsedConfig = self.parseUCI(fileData.content);
+				console.log('[Settings] Parsed configuration:', parsedConfig);
+
+				// Iterate over all sections of the configuration file
+				for (let sectionName in parsedConfig) {
+					if (!parsedConfig.hasOwnProperty(sectionName)) continue;
+
+					// Skip the 'file-plug-manager' section if it does not contain plugin settings
+					if (sectionName === 'file-plug-manager') continue;
+
+					// Get settings for the current section
+					let section = parsedConfig[sectionName];
+					let pluginName = sectionName; // Assume the section name matches the plugin name
+
+					// Get the plugin by name
+					let plugin = self.loadedPlugins[pluginName];
+					if (plugin && typeof plugin.set_settings === 'function') {
+						try {
+							// Remove the section type before passing settings
+							let {
+								type,
+								...pluginSettings
+							} = section;
+							plugin.set_settings(pluginSettings);
+							console.log(`[${PN}]: ` + `Settings applied to plugin "${pluginName}":`, pluginSettings);
+						} catch (e) {
+							console.error(`[${PN}]: ` + `Error applying settings to plugin "${pluginName}":`, e);
+							self.popm(null, `[${PN}]: ` + _('Settings: Error applying settings to plugin "' + pluginName + '".'));
+						}
+					} else {
+						console.warn(`[${PN}]: ` + `Plugin "${pluginName}" not found or does not implement set_settings().`);
+					}
+				}
+
+				// Apply settings for the 'Settings' plugin itself, if they exist
+				if (parsedConfig['Settings']) {
+					self.set_settings(parsedConfig['Settings']);
+				}
+
+				resolve();
+			}).catch(function(err) {
+				if (err.code === 'ENOENT') {
+					console.warn('[Settings] Configuration file not found. Using default settings.');
+					self.popm(null, `[${PN}]: ` + _('Settings: Configuration file not found. Using default settings.'));
+					self.set_settings({
+						window_size: '800x600'
+					});
+					resolve();
+				} else {
+					console.error('[Settings] Error reading settings:', err);
+					self.popm(null, `[${PN}]: ` + _('Settings: Error reading settings.'));
+					reject(err);
+				}
+			});
+		});
+	},
+
+	/**
+	 * setNestedValue(obj, path, value)
+	 * Sets a nested value in an object based on the given path.
+	 * @param {Object} obj - Object to modify.
+	 * @param {Array<string>} path - Path to the value, e.g., ['columnWidths', 'name'].
+	 * @param {string} value - Value to set.
+	 */
+	setNestedValue: function(obj, path, value) {
+		var current = obj;
+		for (var i = 0; i < path.length - 1; i++) {
+			var key = path[i];
+			if (typeof current[key] !== 'object' || current[key] === null) {
+				current[key] = {}; // Create a nested object if it doesn't exist
+			}
+			current = current[key];
+		}
+
+		var finalKey = path[path.length - 1];
+
+		// Here you can add logic for type conversion if necessary
+		current[finalKey] = value;
+	},
+
+
+	// Define CSS styles
+	// CSS is dynamically generated to include the uniqueId in class names
+	get_css: function() {
+		return `
+            .settings-panel-${this.uniqueId} {
+                resize: both;
+                overflow: auto;
+                padding: 20px;
+                border: 1px solid #ccc;
+                border-radius: 5px;
+                width: 800px; /* Initial width, can be dynamically set from this.window_size */
+                height: 600px; /* Initial height */
+                box-sizing: border-box;
+                background-color: #f9f9f9;
+            }
+            .settings-panel-${this.uniqueId} h3 {
+                margin-top: 20px;
+                margin-bottom: 10px;
+                font-size: 1.2em;
+                border-bottom: 1px solid #ddd;
+                padding-bottom: 5px;
+            }
+            .settings-panel-${this.uniqueId} fieldset {
+                margin-left: 20px;
+                border: 1px solid #ddd;
+                padding: 10px;
+                border-radius: 5px;
+                background-color: #fff;
+            }
+            .settings-panel-${this.uniqueId} label {
+                display: inline-block;
+                width: 200px;
+                margin-right: 10px;
+                text-align: right;
+                vertical-align: top;
+            }
+            .settings-panel-${this.uniqueId} .form-field-${this.uniqueId} {
+                margin-bottom: 15px;
+                display: flex;
+                align-items: center;
+            }
+            .settings-panel-${this.uniqueId} input[type="text"] {
+                flex: 1;
+                padding: 5px;
+                border: 1px solid #ccc;
+                border-radius: 3px;
+            }
+            .save-button-${this.uniqueId} {
+                margin-top: 20px;
+                padding: 10px 20px;
+                background-color: #4CAF50;
+                color: white;
+                border: none;
+                border-radius: 5px;
+                cursor: pointer;
+                font-size: 1em;
+            }
+            .save-button-${this.uniqueId}:hover {
+                background-color: #45a049;
+            }
+        `;
+	},
+
+	/**
+	 * start(container, loadedPlugins, defaultPlugins, uniqueId)
+	 * Initializes the Settings plugin.
+	 * @param {HTMLElement} container - The DOM element to attach the plugin's UI.
+	 * @param {Object} loadedPlugins - Registry of loaded plugins.
+	 * @param {Object} defaultPlugins - Registry of default plugins.
+	 * @param {string} uniqueId - Unique identifier for this plugin instance.
+	 */
+	start: function(container, loadedPlugins, defaultPlugins, uniqueId) {
+		var self = this;
+
+		// Store the uniqueId for later use
+		self.uniqueId = uniqueId;
+
+		console.log(`[${PN}]: ` + `Plugin "${self.info().name}" started with unique ID "${self.uniqueId}".`);
+
+		// Inject the dynamically generated CSS into the document
+		var styleElement = document.createElement('style');
+		styleElement.type = 'text/css';
+		styleElement.innerHTML = this.get_css();
+		document.head.appendChild(styleElement);
+		console.log(`[${PN}]: ` + `CSS injected for unique ID "${self.uniqueId}".`);
+
+		// Create the settings panel with unique ID suffix
+		var settingsPanel = E('div', {
+			'class': `settings-panel-${self.uniqueId}`,
+			'id': `settings-panel-${self.info().name}-${self.uniqueId}`
+		});
+
+		// Create the save button with unique ID suffix
+		var saveButton = E('button', {
+			'class': `save-button-${self.uniqueId}`,
+			'id': `save-button-${self.info().name}-${self.uniqueId}`
+		}, _('Save Settings'));
+		saveButton.onclick = self.saveSettings.bind(self);
+
+		// Add the panel and button to the container
+		container.appendChild(settingsPanel);
+		container.appendChild(saveButton);
+
+		self.loadedPlugins = loadedPlugins || {};
+		self.defaultPlugins = defaultPlugins || {};
+
+		var pluginName = self.info().name;
+		var eventName = `tab-${pluginName}`;
+
+		// Add an event listener to display settings
+		document.addEventListener(eventName, self.displaySettings.bind(self));
+		console.log(`[${PN}]: ` + `Event listener for "${eventName}" added.`);
+
+		// Retrieve the default Dispatcher plugin
+		var defaultDispatcherName = self.defaultPlugins['Dispatcher'];
+		if (defaultDispatcherName && self.loadedPlugins[defaultDispatcherName]) {
+			var defaultDispatcher = self.loadedPlugins[defaultDispatcherName];
+			self.popm = defaultDispatcher.pop.bind(defaultDispatcher);
+		}
+
+		// Retrieve the default Navigation plugin
+		var navigationPluginName = self.defaultPlugins['Navigation'];
+		if (!navigationPluginName) {
+			self.popm(null, `[${PN}]: ` + _('No default Navigation plugin set.'));
+			console.error('No default Navigation plugin set.');
+			return;
+		}
+
+		var navigationPlugin = self.loadedPlugins[navigationPluginName];
+		if (!navigationPlugin || typeof navigationPlugin.write_file !== 'function') {
+			self.popm(null, `[${PN}]: ` + _('Navigation plugin does not support writing files.'));
+			console.error('Navigation plugin is unavailable or missing write_file function.');
+			return;
+		}
+
+		// Bind the write_file() and read_file() functions from the Navigation plugin
+		self.write_file = navigationPlugin.write_file.bind(navigationPlugin);
+		self.read_file = navigationPlugin.read_file.bind(navigationPlugin);
+	},
+
+	/**
+	 * displaySettings()
+	 * Displays the settings form for all loaded plugins.
+	 */
+	displaySettings: function() {
+		var self = this;
+		console.log(`[${PN}]: ` + `Displaying settings for plugin "${self.info().name}" with unique ID "${self.uniqueId}"...`);
+		var settingsPanel = document.getElementById(`settings-panel-${self.info().name}-${self.uniqueId}`);
+
+		if (!settingsPanel) {
+			console.error(`[${PN}]: ` + `settings-panel-${self.info().name}-${self.uniqueId} element not found.`);
+			self.popm(null, `[${PN}]: ` + _('Settings panel not found.'));
+			return;
+		}
+
+		// Clear previous content
+		settingsPanel.innerHTML = '';
+
+		var settingsForm = E('form', {
+			'id': `settings-form-${self.info().name}-${self.uniqueId}`
+		});
+		var allSettings = {};
+
+		for (var pluginName in self.loadedPlugins) {
+			if (self.loadedPlugins.hasOwnProperty(pluginName)) {
+				try {
+					var plugin = self.loadedPlugins[pluginName];
+					if (plugin && typeof plugin.get_settings === 'function') {
+						var pluginSettings = plugin.get_settings();
+						allSettings[pluginName] = pluginSettings;
+						console.log(`[${PN}]: ` + `Retrieved settings from plugin "${pluginName}":`, pluginSettings);
+
+						// Add settings to the form
+						self.addSettingsToForm(settingsForm, pluginName, pluginSettings, plugin.info().type);
+					} else {
+						console.warn(`[${PN}]: ` + `Plugin "${pluginName}" does not implement get_settings().`);
+					}
+				} catch (err) {
+					console.error(`[${PN}]: ` + `Error retrieving settings for plugin "${pluginName}":`, err);
+					self.popm(null, `[${PN}]: ` + _('Error retrieving settings for "' + pluginName + '".'));
+				}
+			}
+		}
+
+		if (Object.keys(allSettings).length === 0) {
+			settingsForm.innerHTML = '<p>' + _('No settings available.') + '</p>';
+		}
+
+		settingsPanel.appendChild(settingsForm);
+		console.log(`[${PN}]: ` + `Settings displayed for plugin "${self.info().name}" with unique ID "${self.uniqueId}".`);
+	},
+
+	/**
+	 * addSettingsToForm(settingsForm, pluginName, pluginSettings, pluginType)
+	 * Adds setting fields for a specific plugin to the form.
+	 */
+	addSettingsToForm: function(settingsForm, pluginName, pluginSettings, pluginType) {
+		var header = E('h3', {}, `${pluginName} (${pluginType || 'Unknown'})`);
+		settingsForm.appendChild(header);
+
+		var fieldsContainer = E('div', {
+			'class': `fields-container-${this.uniqueId}`
+		});
+
+		// Use a helper function to recursively add fields
+		this.renderSettingsFields(pluginName, fieldsContainer, pluginSettings, []);
+
+		settingsForm.appendChild(fieldsContainer);
+	},
+
+	/**
+	 * renderSettingsFields(pluginName, container, settings, path)
+	 * Recursively renders setting fields in nested objects.
+	 * @param {string} pluginName - Plugin name.
+	 * @param {HTMLElement} container - DOM element to add fields to.
+	 * @param {Object} settings - Settings object (can be nested).
+	 * @param {Array<string>} path - Current path to settings (used for unique field names).
+	 */
+	renderSettingsFields: function(pluginName, container, settings, path) {
+		for (var settingKey in settings) {
+			if (!settings.hasOwnProperty(settingKey)) continue;
+			var settingValue = settings[settingKey];
+
+			// Construct the full path for this parameter
+			var fullPath = path.concat(settingKey);
+
+			if (typeof settingValue === 'object' && settingValue !== null && !Array.isArray(settingValue)) {
+				// If the value is an object, create a nested fieldset
+				var subgroup = E('fieldset', {}, [
+					E('legend', {}, settingKey)
+				]);
+
+				// Recursively render nested fields
+				this.renderSettingsFields(pluginName, subgroup, settingValue, fullPath);
+
+				container.appendChild(subgroup);
+			} else {
+				// Primitive value (string, number, etc.)
+				var label = E('label', {
+					'for': `${pluginName}-${fullPath.join('-')}-${this.uniqueId}`
+				}, settingKey);
+
+				// Create an input field
+				var input = E('input', {
+					'type': 'text',
+					'id': `${pluginName}-${fullPath.join('-')}-${this.uniqueId}`,
+					'name': `${pluginName}-${fullPath.join('-')}-${this.uniqueId}`
+				});
+				input.value = (settingValue !== undefined && settingValue !== null) ? settingValue.toString() : '';
+
+				var fieldWrapper = E('div', {
+					'class': `form-field-${this.uniqueId}`
+				}, [label, input]);
+				container.appendChild(fieldWrapper);
+			}
+		}
+	},
+
+	/**
+	 * saveSettings()
+	 * Saves the current settings of all plugins to the configuration file.
+	 */
+	saveSettings: function() {
+		var self = this;
+		console.log(`[${PN}]: ` + `Saving settings for plugin "${self.info().name}" with unique ID "${self.uniqueId}"...`);
+		var settingsForm = document.getElementById(`settings-form-${self.info().name}-${self.uniqueId}`);
+
+		if (!settingsForm) {
+			self.popm(null, `[${PN}]: ` + _('Settings form not found.'));
+			return;
+		}
+
+		var formData = new FormData(settingsForm);
+		var updatedSettings = {};
+
+		// Parse form data
+		formData.forEach(function(value, key) {
+			// Key format: "pluginName-subkey-subsubkey-...-uniqueId"
+			var parts = key.split('-');
+			var uniqueIdFromKey = parts.pop(); // Remove the uniqueId part
+			var pluginName = parts.shift();
+			var settingPath = parts; // Remaining parts represent the path to settings
+
+			if (!updatedSettings[pluginName]) {
+				updatedSettings[pluginName] = {};
+			}
+
+			// Recreate the nested structure from settingPath
+			self.setNestedValue(updatedSettings[pluginName], settingPath, value);
+		});
+
+		console.log(`[${PN}]: ` + `Updated settings:`, updatedSettings);
+
+		var applySettingsPromises = [];
+
+		for (var pluginName in updatedSettings) {
+			if (updatedSettings.hasOwnProperty(pluginName)) {
+				var plugin = self.loadedPlugins[pluginName];
+				if (plugin && typeof plugin.set_settings === 'function') {
+					try {
+						var result = plugin.set_settings(updatedSettings[pluginName]);
+						if (result && typeof result.then === 'function') {
+							applySettingsPromises.push(result);
+						}
+						console.log(`[${PN}]: ` + `Applied settings to plugin "${pluginName}":`, updatedSettings[pluginName]);
+					} catch (err) {
+						console.error(`[${PN}]: ` + `Error applying settings to plugin "${pluginName}":`, err);
+						self.popm(null, `[${PN}]: ` + _('Error applying settings to plugin "' + pluginName + '".'));
+					}
+				}
+			}
+		}
+
+		Promise.all(applySettingsPromises).then(function() {
+			// After successfully applying settings to all plugins, save to the configuration file
+			self.saveToConfigFile(updatedSettings).then(function() {
+				self.popm(null, `[${PN}]: ` + _('Settings saved successfully.'));
+				console.log(`[${PN}]: ` + `Settings saved.`);
+			}).catch(function(err) {
+				console.error(`[${PN}]: ` + `Error saving settings to file:`, err);
+				self.popm(null, `[${PN}]: ` + _('Error saving settings to file.'));
+			});
+		}).catch(function(err) {
+			console.error(`[${PN}]: ` + `Error applying settings:`, err);
+			self.popm(null, `[${PN}]: ` + _('Error applying settings.'));
+		});
+	},
+
+	/**
+	 * saveToConfigFile(updatedSettings)
+	 * Saves the updated settings to the configuration file.
+	 * @param {Object} updatedSettings - Settings object to save.
+	 * @returns {Promise} - Resolves on successful save.
+	 */
+	saveToConfigFile: function(updatedSettings) {
+		var self = this;
+		return new Promise(function(resolve, reject) {
+			console.log('[Settings] Saving updated settings to configuration file.');
+
+			// Prepare the UCI configuration with separate sections for each plugin
+			var uciConfig = {};
+
+			// Add the main 'file-plug-manager' section
+			uciConfig['file-plug-manager'] = {
+				type: 'file-plug-manager'
+				// Add options for 'file-plug-manager' here, if any
+			};
+
+			// Add sections for each plugin
+			for (var pluginName in updatedSettings) {
+				if (!updatedSettings.hasOwnProperty(pluginName)) continue;
+				var pluginSettings = updatedSettings[pluginName];
+
+				// Get the plugin type via info()
+				var plugin = self.loadedPlugins[pluginName];
+				var pluginType = plugin && plugin.info && plugin.info().type ? plugin.info().type : 'option';
+
+				// Ensure type is set
+				uciConfig[pluginName] = Object.assign({
+					type: pluginType
+				}, pluginSettings);
+			}
+
+			var serializedConfig = self.serializeUCI(uciConfig);
+
+			// Write the serialized configuration to the file
+			self.write_file('/etc/config/file-plug-manager', self.permissions, self.ownerGroup, serializedConfig, 'text').then(function() {
+				console.log('[Settings] Configuration file written successfully.');
+				resolve();
+			}).catch(function(err) {
+				console.error('[Settings] Error writing configuration file:', err);
+				reject(err);
+			});
+		});
+	}
+});
\ No newline at end of file
diff --git a/applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/hexEditor.js b/applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/hexEditor.js
new file mode 100644
index 000000000000..eb8d155ea81d
--- /dev/null
+++ b/applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/hexEditor.js
@@ -0,0 +1,1531 @@
+'use strict';
+'require ui';
+'require dom';
+
+/**
+ * Hex Editor Plugin
+ * Provides a hex editor with search functionality, virtual scrolling, and editing capabilities.
+ * Supports loading external binary content for editing and displays the filename.
+ * Includes configuration for window size and a save button at the bottom, using Text Editor's approach to saving.
+ */
+
+const PN = 'Hex Editor';
+return Class.extend({
+
+	/**
+	 * Returns metadata about the plugin.
+	 * @returns {Object} Plugin information.
+	 */
+	info: function() {
+		return {
+			name: 'Hex Editor', // Unique plugin name
+			type: 'Editor', // Plugin type
+			style: 'Bin', // Kind of contents expected for editing
+			description: 'A hex editor plugin with search functionality, virtual scrolling, editing, and save button at bottom.'
+		};
+	},
+
+	/**
+	 * CSS styles for the Hex Editor plugin.
+	 * All class selectors are prefixed with .{rootClass} to ensure uniqueness.
+	 */
+	maincss: `
+        .{rootClass} {
+          position: relative;
+          display: flex;
+          flex-direction: column;
+          resize: both; /* Allows the window to be resizable */
+          overflow: hidden; /* Hide scrollbars at the plugin level */
+          box-shadow: 2px 2px 5px rgba(0,0,0,0.1);
+          font-family: 'Courier New', Courier, monospace; /* Changed to monospace font */
+        }
+
+        .{rootClass} .filename-display {
+            font-weight: bold;
+            color: #333;
+            font-size: 14px;
+            padding: 5px 10px;
+            background-color: #f5f5f5;
+            border-bottom: 1px solid #000;
+        }
+
+        .{rootClass} .save-button-container {
+            padding: 10px;
+            background-color: #f5f5f5;
+            border-top: 1px solid #000;
+            display: flex;
+            justify-content: flex-end;
+        }
+
+        .{rootClass} .save-button {
+            padding: 5px 10px;
+            background-color: #0078d7;
+            color: #fff;
+            border: none;
+            border-radius: 4px;
+            cursor: pointer;
+            font-size: 14px;
+        }
+
+        .{rootClass} .save-button:hover {
+            background-color: #005fa3;
+        }
+
+        :root {
+          --span-spacing: 0.25ch;
+          --clr-background: #f5f5f5;
+          --clr-selected: #c9daf8;
+          --clr-selected-editing: #6d9eeb;
+          --clr-non-printable: #999999;
+          --clr-border: #000000;
+          --clr-offset: #666666;
+          --clr-header: #333333;
+          --clr-highlight: yellow; 
+          --clr-cursor-active: blue; 
+          --clr-cursor-passive: lightblue; 
+          --animation-duration: 1s; 
+        }
+
+        .{rootClass} .hexedit *,
+        .{rootClass} .hexedit *::before,
+        .{rootClass} .hexedit *::after {
+          box-sizing: border-box;
+        }
+
+        .{rootClass} .hexedit {
+          display: flex;
+          flex-direction: column;
+          flex: 1; 
+  font-family: 'Courier New', Courier, monospace;
+          font-size: 14px;
+          line-height: 1.2em;
+          background-color: var(--clr-background);
+          border: 1px solid var(--clr-border);
+          width: 100%;
+          flex-grow: 1;
+        }
+
+        .{rootClass} .hexedit-headers {
+          display: flex;
+          background-color: var(--clr-background);
+          border-bottom: 2px solid var(--clr-border);
+  font-family: 'Courier New', Courier, monospace;
+        }
+
+        .{rootClass} .offsets-header,
+        .{rootClass} .hexview-header,
+        .{rootClass} .textview-header {
+          display: flex;
+          align-items: center;
+          padding: 5px;
+          box-sizing: border-box;
+          font-weight: bold;
+          color: var(--clr-header);
+          border-right: 2px solid var(--clr-border);
+        }
+
+        .{rootClass} .offsets-header {
+          width: 100px; 
+          text-align: left;
+        }
+
+        .{rootClass} .hexview-header {
+          width: calc(16 * 2ch + 20 * var(--span-spacing));
+          display: flex;
+        }
+
+        .{rootClass} .hexview-header span {
+          width: 2ch;
+          margin-right: var(--span-spacing);
+          text-align: center;
+        }
+
+        .{rootClass} .hexview-header span:last-child {
+          margin-right: 0;
+        }
+
+        .{rootClass} .textview-header {
+          flex: 1;
+          margin-left: 10px;
+          text-align: left;
+        }
+
+        .{rootClass} .hexedit-content {
+          display: flex;
+          height: 100%;
+          flex: 1 1 auto;
+          overflow: auto;
+          position: relative;
+          border-top: 2px solid var(--clr-border);
+  font-family: 'Courier New', Courier, monospace; 
+
+        }
+
+        .{rootClass} .offsets,
+        .{rootClass} .hexview,
+        .{rootClass} .textview {
+          flex-shrink: 0;
+          display: block;
+          padding: 5px;
+          position: relative;
+          border-right: 2px solid var(--clr-border);
+        }
+
+        .{rootClass} .offsets {
+          width: 100px;
+          display: flex;
+          flex-direction: column;
+          text-align: left;
+        }
+
+        .{rootClass} .offsets span {
+          display: block;
+          height: 1.2em;
+        }
+
+        .{rootClass} .hexview {
+          width: calc(16 * 2ch + 20 * var(--span-spacing));
+          text-align: center;
+        }
+
+        .{rootClass} .textview {
+          flex: 1;
+          margin-left: 10px;
+          text-align: left;
+          border-right: none;
+        }
+
+        .{rootClass} .hex-line,
+        .{rootClass} .text-line {
+          display: flex;
+          height: 1.2em;
+        }
+
+        .{rootClass} .hex-line span,
+        .{rootClass} .text-line span {
+          width: 2ch;
+          margin-right: var(--span-spacing);
+          text-align: center;
+          display: inline-block;
+          cursor: default;
+        }
+
+        .{rootClass} .hex-line span:last-child,
+        .{rootClass} .hexview-header span:last-child,
+        .{rootClass} .text-line span:last-child {
+          margin-right: 0;
+        }
+
+        .{rootClass} .selected {
+          background-color: var(--clr-selected);
+        }
+
+        .{rootClass} .selected-editing {
+          background-color: var(--clr-selected-editing);
+        }
+
+        .{rootClass} .non-printable {
+          color: var(--clr-non-printable);
+        }
+
+        .{rootClass} .offsets::-webkit-scrollbar,
+        .{rootClass} .hexview::-webkit-scrollbar,
+        .{rootClass} .textview::-webkit-scrollbar {
+          display: none;
+        }
+
+        .{rootClass} .offsets,
+        .{rootClass} .hexview,
+        .{rootClass} .textview {
+          scrollbar-width: none; 
+        }
+
+        .{rootClass} .hexedit .offsets,
+        .{rootClass} .hexedit .hexview,
+        .{rootClass} .hexedit .textview {
+          border-right: 2px solid var(--clr-border);
+        }
+
+        .{rootClass} .hexedit .textview {
+          border-right: none;
+        }
+
+        @media (max-width: 768px) {
+          .{rootClass} .hexedit {
+            font-size: 12px;
+          }
+
+          .{rootClass} .offsets {
+            width: 120px; 
+          }
+
+          .{rootClass} .hexview {
+            width: calc(16 * 2ch + 20 * var(--span-spacing));
+          }
+        }
+
+        .{rootClass} .hexedit-search-container {
+            padding: 10px;
+            background-color: #f9f9f9;
+            border-bottom: 1px solid #ccc; 
+            display: flex;
+            flex-direction: column;
+            gap: 10px;
+            width: 100%;
+            box-sizing: border-box;
+        }
+
+        .{rootClass} .hexedit-search-group {
+            display: flex;
+            align-items: center;
+            gap: 5px;
+            width: 100%;
+        }
+
+        .{rootClass} .hexedit-search-input {
+            flex: 1;
+            padding: 8px;
+            border: 1px solid #ddd;
+            border-radius: 4px;
+            font-size: 14px;
+        }
+
+        .{rootClass} .hexedit-search-status {
+            width: 50px;
+            text-align: center;
+            font-size: 14px;
+            color: #555;
+        }
+
+        .{rootClass} .hexedit-search-button {
+            padding: 8px 12px;
+            cursor: pointer;
+            background-color: #007bff;
+            color: white;
+            border: none;
+            border-radius: 4px;
+            font-size: 14px;
+            transition: background-color 0.3s ease;
+        }
+
+        .{rootClass} .hexedit-search-button:hover {
+            background-color: #0056b3;
+        }
+
+        .{rootClass} .search-highlight {
+            background-color: var(--clr-highlight);
+        }
+
+        @keyframes blink-blue {
+            0% { background-color: var(--clr-cursor-active); }
+            50% { background-color: white; }
+            100% { background-color: var(--clr-cursor-active); }
+        }
+
+        .{rootClass} .active-view-cursor {
+            animation: blink-blue var(--animation-duration) infinite;
+            background-color: var(--clr-cursor-active);
+        }
+
+        .{rootClass} .passive-view-cursor {
+            background-color: var(--clr-cursor-passive);
+        }
+
+        .{rootClass} .highlighted {
+            background-color: var(--clr-highlight);
+        }
+    `,
+
+	/**
+	 * Initializes and starts the Hex Editor plugin.
+	 * @param {HTMLElement} container - The container element for the plugin.
+	 * @param {Object} pluginsRegistry - Registry of available plugins.
+	 * @param {Object} default_plugins - Default plugins to be used.
+	 * @param {string} uniqueId - Unique identifier for this plugin instance.
+	 */
+	start: function(container, pluginsRegistry, default_plugins, uniqueId) {
+		var self = this;
+
+		// Ensure the plugin is only initialized once per uniqueId
+		if (self.initializedIds && self.initializedIds.includes(uniqueId)) {
+			return;
+		}
+		if (!self.initializedIds) {
+			self.initializedIds = [];
+		}
+		self.initializedIds.push(uniqueId);
+
+		// Store references for later use
+		self.pluginsRegistry = pluginsRegistry;
+		self.default_plugins = default_plugins;
+
+		// Process and inject CSS with uniqueId
+		const rootClass = `hex-editor-plugin-${uniqueId}`;
+		const processedCss = this.maincss.replace(/{rootClass}/g, rootClass);
+		// Inject the processed CSS into the document
+		const styleTag = document.createElement('style');
+		styleTag.textContent = processedCss;
+		document.head.appendChild(styleTag);
+
+		// Dynamically fetch the default Dispatcher plugin
+		var defaultDispatcherName = self.default_plugins['Dispatcher'];
+		if (defaultDispatcherName && self.pluginsRegistry[defaultDispatcherName]) {
+			var defaultDispatcher = self.pluginsRegistry[defaultDispatcherName];
+			self.popm = defaultDispatcher.pop.bind(defaultDispatcher);
+		}
+
+		// Dynamically fetch the default Navigation plugin
+		var navigationPluginName = self.default_plugins['Navigation'];
+		if (!navigationPluginName) {
+			self.popm(null, `[${PN}]: ` + _('No default Navigation plugin set.'));
+			console.error('No default Navigation plugin set.');
+			return;
+		}
+
+		var navigationPlugin = self.pluginsRegistry[navigationPluginName];
+		if (!navigationPlugin || typeof navigationPlugin.write_file !== 'function') {
+			self.popm(null, `[${PN}]: ` + _('Navigation plugin does not support writing files.'));
+			console.error('Navigation plugin is unavailable or missing write_file function.');
+			return;
+		}
+
+		self.write_file = navigationPlugin.write_file.bind(navigationPlugin);
+
+		// Create the main div for the hex editor with unique root class
+		self.editorDiv = document.createElement('div');
+		self.editorDiv.className = rootClass;
+
+		// Set initial size
+		self.width = self.settings && self.settings.width ? self.settings.width : '600px';
+		self.height = self.settings && self.settings.height ? self.settings.height : '400px';
+		self.editorDiv.style.width = self.width;
+		self.editorDiv.style.height = self.height;
+
+		// Filename display at the top
+		self.filenameDisplay = document.createElement('div');
+		self.filenameDisplay.className = 'filename-display';
+		self.filenameDisplay.textContent = 'No file loaded.';
+		self.editorDiv.appendChild(self.filenameDisplay);
+
+		// Initialize hex editor inside editorDiv
+		self.hexEditorInstance = self.initializeHexEditor(self.editorDiv, rootClass);
+
+		// Create the save button container at the bottom
+		self.saveButtonContainer = document.createElement('div');
+		self.saveButtonContainer.className = 'save-button-container';
+
+		self.saveButton = document.createElement('button');
+		self.saveButton.className = 'save-button';
+		self.saveButton.textContent = 'Save';
+		self.saveButton.onclick = function() {
+			if (!self.currentFilePath) {
+				self.popm(null, `[${PN}]: ` + _('No file loaded to save.'));
+				return;
+			}
+
+			var data = self.hexEditorInstance.getData();
+			var content = data.buffer;
+
+			// Attempt to save the file (similar to Text Editor approach)
+			self.write_file(self.currentFilePath, self.permissions, self.ownerGroup, content, 'bin')
+
+				.then(function() {
+					self.popm(null, `[${PN}]: ` + _('File saved successfully.'));
+				})
+				.catch(function(err) {
+					self.popm(null, `[${PN}]: ` + _('Error saving file.'));
+					console.error('Error saving file:', err);
+				});
+		};
+
+		self.saveButtonContainer.appendChild(self.saveButton);
+		self.editorDiv.appendChild(self.saveButtonContainer);
+
+		// Append the editor div to the provided container
+		container.appendChild(self.editorDiv);
+	},
+
+	byteToChar: function(b) {
+		return (b >= 32 && b <= 126) ? String.fromCharCode(b) : this._NON_PRINTABLE_CHAR;
+	},
+
+
+	/**
+	 * Initializes the HexEditor instance.
+	 * @param {HTMLElement} container - The container element for the hex editor.
+	 * @param {string} rootClass - Unique root class for scoping CSS.
+	 * @returns {HexEditor} - The initialized HexEditor instance.
+	 */
+	initializeHexEditor: function(container, rootClass) {
+		var self = this;
+
+		/**
+		 * HexEditor class to handle hex editing functionalities.
+		 */
+		class HexEditor {
+			/**
+			 * Constructs a HexEditor instance.
+			 *
+			 * @param {HTMLElement} hexeditDomObject - The DOM element for the hex editor.
+			 * @param {string} rootClass - Unique root class for scoping CSS.
+			 */
+			constructor(hexeditDomObject, rootClass) {
+				this.rootClass = rootClass;
+				this.hexedit = this.fillHexeditDom(hexeditDomObject);
+				this.offsets = this.hexedit.querySelector(`.${rootClass} .offsets`);
+				this.hexview = this.hexedit.querySelector(`.${rootClass} .hexview`);
+				this.textview = this.hexedit.querySelector(`.${rootClass} .textview`);
+				this.hexeditContent = this.hexedit.querySelector(`.${rootClass} .hexedit-content`);
+				this.hexeditHeaders = this.hexedit.querySelector(`.${rootClass} .hexedit-headers`);
+
+				this.bytesPerRow = 16;
+				this.startIndex = 0;
+				this.data = new Uint8Array(0);
+
+				this.selectedIndex = null;
+				this.editHex = true;
+				this.currentEdit = "";
+				this.readonly = false;
+				this.ctrlPressed = false;
+
+				this.matches = [];
+				this.currentMatchIndex = -1;
+				this.currentSearchType = null;
+				this.activeView = null;
+				this.previousSelectedIndex = null;
+
+				this.lastSearchPatterns = {
+					ascii: '',
+					hex: '',
+					regex: ''
+				};
+
+				this._NON_PRINTABLE_CHAR = "\u00B7";
+
+				this._registerEventHandlers();
+
+				this.resizeObserver = new ResizeObserver(() => {
+					this.calculateVisibleRows();
+				});
+				this.resizeObserver.observe(this.hexeditContent);
+
+				this.addSearchUI();
+			}
+
+			/**
+			 * Adds the search interface with input fields, status fields, and navigation buttons.
+			 */
+			addSearchUI() {
+				// Create search container
+				const searchContainer = document.createElement('div');
+				searchContainer.classList.add('hexedit-search-container');
+
+				// Helper function to create search groups
+				const createSearchGroup = (type, placeholder) => {
+					const container = document.createElement('div');
+					container.classList.add('hexedit-search-group');
+
+					const input = document.createElement('input');
+					input.type = 'text';
+					input.placeholder = placeholder;
+					input.classList.add('hexedit-search-input');
+					input.id = `hexedit-search-${type}`;
+
+					const status = document.createElement('span');
+					status.classList.add('hexedit-search-status');
+					status.id = `hexedit-search-status-${type}`;
+					status.textContent = '0/0'; // Initial status
+
+					const prevButton = document.createElement('button');
+					prevButton.innerHTML = '&#8593;'; // Up arrow
+					prevButton.classList.add('hexedit-search-button');
+					prevButton.title = `Previous ${type.toUpperCase()} Match`;
+
+					const nextButton = document.createElement('button');
+					nextButton.innerHTML = '&#8595;'; // Down arrow
+					nextButton.classList.add('hexedit-search-button');
+					nextButton.title = `Next ${type.toUpperCase()} Match`;
+
+					container.appendChild(input);
+					container.appendChild(status);
+					container.appendChild(prevButton);
+					container.appendChild(nextButton);
+
+					// Add event listeners for buttons
+					prevButton.addEventListener('click', () => this.handleFindPrevious(type));
+					nextButton.addEventListener('click', () => this.handleFindNext(type));
+
+					// Add event listener for Enter key
+					input.addEventListener('keydown', (e) => {
+						if (e.key === 'Enter') this.handleFindNext(type);
+					});
+
+					return container;
+				};
+
+				// Create ASCII search group
+				const asciiGroup = createSearchGroup('ascii', _('Search ASCII'));
+
+				// Create HEX search group
+				const hexGroup = createSearchGroup('hex', _('Search HEX (e.g., 4F6B)'));
+
+				// Create RegExp search group
+				const regexGroup = createSearchGroup('regex', _('Search RegExp (e.g., \\d{3})'));
+
+				// Append all search groups to the search container
+				searchContainer.appendChild(asciiGroup);
+				searchContainer.appendChild(hexGroup);
+				searchContainer.appendChild(regexGroup);
+
+				// Insert the search container above the hexedit headers
+				if (this.hexeditHeaders) {
+					this.hexedit.insertBefore(searchContainer, this.hexeditHeaders);
+				} else {
+					// Fallback: append to hexedit if headers are not found
+					this.hexeditContent.insertBefore(searchContainer, this.hexeditContent.firstChild);
+				}
+			}
+
+			/**
+			 * Handles the "Find Next" button click for a specific search type.
+			 *
+			 * @param {string} searchType - The type of search ('ascii', 'hex', 'regex').
+			 */
+			handleFindNext(searchType) {
+				const inputElement = document.getElementById(`hexedit-search-${searchType}`);
+				const currentPattern = inputElement.value.trim();
+
+				// Check if the search pattern has changed
+				if (this.lastSearchPatterns[searchType] !== currentPattern) {
+					// Update the last search pattern
+					this.lastSearchPatterns[searchType] = currentPattern;
+
+					// Set the current search type and active view
+					this.currentSearchType = searchType;
+					this.activeView = (searchType === 'hex') ? 'hex' : 'text';
+
+					// Perform search
+					this.performSearch(searchType);
+				} else {
+					// If the search pattern has not changed, just go to the next match
+					if (this.currentSearchType === searchType && this.matches.length > 0) {
+						// Set activeView based on currentSearchType
+						this.activeView = (this.currentSearchType === 'hex') ? 'hex' : 'text';
+
+						// Navigate to the next match relative to the current cursor position
+						const cursorPosition = this.selectedIndex !== null ? this.selectedIndex : 0;
+						const nextMatchIndex = this.findNextMatch(cursorPosition);
+						if (nextMatchIndex !== -1) {
+							this.navigateToMatch(nextMatchIndex);
+						} else {
+							// If there is no next match, go to the first one
+							this.navigateToMatch(0);
+						}
+					}
+				}
+			}
+
+			/**
+			 * Handles the "Find Previous" button click for a specific search type.
+			 *
+			 * @param {string} searchType - The type of search ('ascii', 'hex', 'regex').
+			 */
+			handleFindPrevious(searchType) {
+				const inputElement = document.getElementById(`hexedit-search-${searchType}`);
+				const currentPattern = inputElement.value.trim();
+
+				// Check if the search pattern has changed
+				if (this.lastSearchPatterns[searchType] !== currentPattern) {
+					// Update the last search pattern
+					this.lastSearchPatterns[searchType] = currentPattern;
+
+					// Set the current search type and active view
+					this.currentSearchType = searchType;
+					this.activeView = (searchType === 'hex') ? 'hex' : 'text';
+
+					// Perform search
+					this.performSearch(searchType);
+				} else {
+					// If the search pattern has not changed, just go to the previous match
+					if (this.currentSearchType === searchType && this.matches.length > 0) {
+						// Set activeView based on currentSearchType
+						this.activeView = (this.currentSearchType === 'hex') ? 'hex' : 'text';
+
+						// Navigate to the previous match relative to the current cursor position
+						const cursorPosition = this.selectedIndex !== null ? this.selectedIndex : this.data.length;
+						const prevMatchIndex = this.findPreviousMatch(cursorPosition);
+						if (prevMatchIndex !== -1) {
+							this.navigateToMatch(prevMatchIndex);
+						} else {
+							// If there is no previous match, go to the last one
+							this.navigateToMatch(this.matches.length - 1);
+						}
+					}
+				}
+			}
+
+			/**
+			 * Finds the index of the next match after the given cursor position.
+			 *
+			 * @param {number} cursorPosition - The current cursor position.
+			 * @returns {number} - The index in the matches array or -1 if not found.
+			 */
+			findNextMatch(cursorPosition) {
+				for (let i = 0; i < this.matches.length; i++) {
+					if (this.matches[i].index > cursorPosition) {
+						return i;
+					}
+				}
+				// If there are no matches after the cursor position, return -1
+				return -1;
+			}
+
+			/**
+			 * Finds the index of the previous match before the given cursor position.
+			 *
+			 * @param {number} cursorPosition - The current cursor position.
+			 * @returns {number} - The index in the matches array or -1 if not found.
+			 */
+			findPreviousMatch(cursorPosition) {
+				for (let i = this.matches.length - 1; i >= 0; i--) {
+					if (this.matches[i].index < cursorPosition) {
+						return i;
+					}
+				}
+				// If there are no matches before the cursor position, return -1
+				return -1;
+			}
+
+			/**
+			 * Performs the search based on the specified search type.
+			 *
+			 * @param {string} searchType - The type of search ('ascii', 'hex', 'regex').
+			 */
+			performSearch(searchType) {
+				let pattern = '';
+				switch (searchType) {
+					case 'ascii':
+						pattern = document.getElementById(`hexedit-search-${searchType}`).value.trim();
+						break;
+					case 'hex':
+						pattern = document.getElementById(`hexedit-search-${searchType}`).value.trim();
+						break;
+					case 'regex':
+						pattern = document.getElementById(`hexedit-search-${searchType}`).value.trim();
+						break;
+					default:
+						console.warn(`Unknown search type: ${searchType}`);
+						pattern = '';
+						break;
+				}
+
+				// Reset previous search results
+				this.clearSearchHighlights();
+				this.matches = [];
+				this.currentMatchIndex = -1;
+
+				if (!pattern) {
+					// Update status field to 0/0
+					this.updateSearchStatus(searchType, 0, 0);
+					console.log('No search pattern entered.');
+					return;
+				}
+
+				try {
+					// Determine search type and perform search
+					if (searchType === 'ascii') {
+						this.searchASCII(pattern);
+					} else if (searchType === 'hex') {
+						this.searchHEX(pattern);
+					} else if (searchType === 'regex') {
+						this.searchRegex(pattern);
+					}
+				} catch (error) {
+					console.log(`Error during search: ${error.message}`);
+					// Update status field to 0/0 on error
+					this.updateSearchStatus(searchType, 0, 0);
+					return;
+				}
+
+				// After searching, highlight all matches and navigate to the first one
+				if (this.matches.length > 0) {
+					this.highlightAllMatches(searchType);
+					this.currentMatchIndex = 0;
+					this.navigateToMatch(this.currentMatchIndex);
+					// Update status field with actual match count
+					this.updateSearchStatus(searchType, this.currentMatchIndex + 1, this.matches.length);
+					console.log(`Found ${this.matches.length} matches.`);
+				} else {
+					// Update status field to 0/0 if no matches found
+					this.updateSearchStatus(searchType, 0, 0);
+					console.log('No matches found.');
+				}
+			}
+
+			/**
+			 * Highlights all matched patterns in the hex and text views based on search type.
+			 *
+			 * @param {string} searchType - The type of search ('ascii', 'hex', 'regex').
+			 */
+			highlightAllMatches(searchType) {
+				// Rendering will handle highlights based on this.matches
+				this.searchTypeForHighlight = searchType; // Store current search type for rendering
+
+				// Set active view based on search type
+				if (searchType === 'ascii' || searchType === 'regex') {
+					this.activeView = 'text'; // Text view is active
+				} else if (searchType === 'hex') {
+					this.activeView = 'hex'; // Hex view is active
+				}
+
+				// Focus the corresponding view
+				this.focusActiveView();
+
+				this.renderDom(); // Re-render to apply the highlights
+			}
+
+			/**
+			 * Navigates to a specific match by its index.
+			 *
+			 * @param {number} matchIndex - The index in the matches array to navigate to.
+			 */
+			navigateToMatch(matchIndex) {
+				if (this.matches.length === 0) {
+					// Update status field to 0/0 if no matches
+					this.updateSearchStatus(this.currentSearchType, 0, 0);
+					console.log('No matches to navigate.');
+					return;
+				}
+
+				// Ensure matchIndex is within bounds
+				if (matchIndex < 0 || matchIndex >= this.matches.length) {
+					console.log('navigateToMatch: matchIndex out of bounds.');
+					return;
+				}
+
+				this.currentMatchIndex = matchIndex;
+				const match = this.matches[matchIndex];
+
+				// Set activeView based on currentSearchType during navigation
+				this.activeView = (this.currentSearchType === 'hex') ? 'hex' : 'text';
+
+				// Set selected index to the match start
+				this.setSelectedIndex(match.index);
+				console.log(`Navigated to match ${matchIndex + 1} at offset ${match.index.toString(16)}`);
+
+				// Update status field
+				this.updateSearchStatus(this.currentSearchType, this.currentMatchIndex + 1, this.matches.length);
+			}
+
+			/**
+			 * Searches for an ASCII pattern and stores all match positions.
+			 *
+			 * @param {string} pattern - The ASCII pattern to search for.
+			 */
+			searchASCII(pattern) {
+				const dataStr = new TextDecoder('iso-8859-1').decode(this.data);
+				let startIndex = 0;
+				let index;
+				while ((index = dataStr.indexOf(pattern, startIndex)) !== -1) {
+					this.matches.push({
+						index: index,
+						length: pattern.length
+					});
+					startIndex = index + pattern.length;
+				}
+				console.log(`searchASCII: Found ${this.matches.length} matches.`);
+			}
+
+			/**
+			 * Searches for a HEX pattern and stores all match positions.
+			 *
+			 * @param {string} pattern - The HEX pattern to search for (e.g., "4F6B").
+			 */
+			searchHEX(pattern) {
+				// Remove spaces and validate hex string
+				const cleanedPattern = pattern.replace(/\s+/g, '');
+				if (!/^[0-9a-fA-F]+$/.test(cleanedPattern)) {
+					throw new Error('Invalid HEX pattern.');
+				}
+				if (cleanedPattern.length % 2 !== 0) {
+					throw new Error('HEX pattern length must be even.');
+				}
+
+				// Convert hex string to byte array
+				const bytePattern = new Uint8Array(cleanedPattern.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
+
+				for (let i = 0; i <= this.data.length - bytePattern.length; i++) {
+					let found = true;
+					for (let j = 0; j < bytePattern.length; j++) {
+						if (this.data[i + j] !== bytePattern[j]) {
+							found = false;
+							break;
+						}
+					}
+					if (found) {
+						this.matches.push({
+							index: i,
+							length: bytePattern.length
+						});
+					}
+				}
+				console.log(`searchHEX: Found ${this.matches.length} matches.`);
+			}
+
+			/**
+			 * Searches using a regular expression and stores all match positions.
+			 *
+			 * @param {RegExp} regexPattern - The regular expression pattern to search for.
+			 */
+			searchRegex(regexPattern) {
+				const regex = new RegExp(regexPattern, 'g');
+				const dataStr = new TextDecoder('iso-8859-1').decode(this.data);
+				let match;
+				while ((match = regex.exec(dataStr)) !== null) {
+					const byteIndex = match.index; // With 'iso-8859-1', char index == byte index
+					const length = match[0].length;
+					this.matches.push({
+						index: byteIndex,
+						length: length
+					});
+					// Prevent infinite loops with zero-length matches
+					if (match.index === regex.lastIndex) {
+						regex.lastIndex++;
+					}
+				}
+				console.log(`searchRegex: Found ${this.matches.length} matches.`);
+			}
+
+			/**
+			 * Scrolls the editor to make the match at the specified index visible.
+			 *
+			 * @param {number} index - The byte index of the match.
+			 */
+			scrollToMatch(index) {
+				const lineNumber = Math.floor(index / this.bytesPerRow);
+				const lineHeight = 16; // Height of one row in pixels
+
+				// Calculate new scroll position to ensure the matched line is visible
+				const newScrollTop = Math.max(0, (lineNumber * lineHeight) - ((this.visibleRows / 2) * lineHeight));
+
+				console.log(`scrollToMatch called with index: ${index}`);
+				console.log(`lineNumber: ${lineNumber}`);
+				console.log(`newScrollTop: ${newScrollTop}`);
+
+				// Update the scrollTop property to trigger handleScroll
+				this.hexeditContent.scrollTop = newScrollTop;
+			}
+
+			/**
+			 * Clears previous search highlights.
+			 */
+			clearSearchHighlights() {
+				// Remove previous highlights
+				this.hexview.querySelectorAll('.search-highlight').forEach(span => {
+					span.classList.remove('search-highlight');
+				});
+				this.textview.querySelectorAll('.search-highlight').forEach(span => {
+					span.classList.remove('search-highlight');
+				});
+
+				// Reset active view
+				this.activeView = null;
+
+				// Reset all search status fields to 0/0
+				['ascii', 'hex', 'regex'].forEach(type => {
+					this.updateSearchStatus(type, 0, 0);
+				});
+			}
+
+			/**
+			 * Calculates the number of visible rows based on the container's height.
+			 */
+			calculateVisibleRows() {
+				const lineHeight = 16; // Height of one row in pixels
+				const containerHeight = this.hexeditContent.clientHeight;
+				this.visibleRows = Math.floor(containerHeight / lineHeight);
+				this.visibleByteCount = this.bytesPerRow * this.visibleRows;
+				// console.log(`calculateVisibleRows: visibleRows=${this.visibleRows}, visibleByteCount=${this.visibleByteCount}`);
+				this.renderDom(); // Re-render to apply the new rows
+			}
+
+			/**
+			 * Sets the data to be displayed in the hex editor.
+			 *
+			 * @param {Uint8Array} data - The data to set.
+			 */
+			setData(data) {
+				this.data = data;
+				this.totalRows = Math.ceil(this.data.length / this.bytesPerRow);
+				console.log(`setData: data length=${this.data.length}, totalRows=${this.totalRows}`);
+				this.calculateVisibleRows(); // Ensure visibleRows are calculated before rendering
+			}
+
+			/**
+			 * Retrieves the current data from the hex editor.
+			 *
+			 * @returns {Uint8Array} - The current data.
+			 */
+			getData() {
+				return this.data;
+			}
+
+			/**
+			 * Handles the scroll event for virtual scrolling.
+			 *
+			 * @param {Event} event - The scroll event.
+			 */
+			handleScroll(event) {
+				const scrollTop = this.hexeditContent.scrollTop;
+				const lineHeight = 16; // Approximate height of a byte row in pixels
+				const firstVisibleLine = Math.floor(scrollTop / lineHeight);
+				const newStartIndex = firstVisibleLine * this.bytesPerRow;
+
+				// console.log(`handleScroll: scrollTop=${scrollTop}, firstVisibleLine=${firstVisibleLine}, newStartIndex=${newStartIndex}`);
+
+				// Update startIndex and re-render the DOM if necessary
+				if (newStartIndex !== this.startIndex) {
+					this.startIndex = newStartIndex;
+					this.renderDom(); // Re-render visible data
+					// console.log(`handleScroll: Updated startIndex and rendered DOM.`);
+				}
+			}
+
+			/**
+			 * Renders the visible portion of the hex editor based on the current scroll position.
+			 */
+			renderDom() {
+				// Clear existing content
+				[this.offsets, this.hexview, this.textview].forEach(view => view.innerHTML = '');
+				const lineHeight = 16; // Approximate line height in pixels
+				const totalLines = Math.ceil(this.data.length / this.bytesPerRow);
+
+				// Set the height of the content area to simulate the total height
+				const contentHeight = totalLines * lineHeight;
+				[this.offsets, this.hexview, this.textview].forEach(view => view.style.height = `${contentHeight}px`);
+				// Create fragments to hold the visible content
+				const offsetsFragment = document.createDocumentFragment();
+				const hexviewFragment = document.createDocumentFragment();
+				const textviewFragment = document.createDocumentFragment();
+
+				// Calculate the start and end lines to render
+				const startLine = Math.floor(this.startIndex / this.bytesPerRow);
+				const endIndex = Math.min(this.startIndex + this.visibleByteCount, this.data.length);
+				const endLine = Math.ceil(endIndex / this.bytesPerRow);
+
+				const paddingTop = startLine * lineHeight;
+
+				// Apply padding to offset the content to the correct vertical position
+				this.offsets.style.paddingTop = paddingTop + 'px';
+				this.hexview.style.paddingTop = paddingTop + 'px';
+				this.textview.style.paddingTop = paddingTop + 'px';
+
+				// Render only the visible lines
+				for (let line = startLine; line < endLine; line++) {
+					const i = line * this.bytesPerRow;
+
+					// Offsets
+					const offsetSpan = document.createElement("span");
+					offsetSpan.innerText = i.toString(16).padStart(8, '0');
+					offsetsFragment.appendChild(offsetSpan);
+
+					// Hexview line
+					const hexLine = document.createElement('div');
+					hexLine.classList.add('hex-line');
+
+					// Textview line
+					const textLine = document.createElement('div');
+					textLine.classList.add('text-line');
+
+					for (let j = 0; j < this.bytesPerRow && i + j < this.data.length; j++) {
+						const index = i + j;
+						const byte = this.data[index];
+
+						// Create hex span
+						const hexSpan = document.createElement('span');
+						hexSpan.textContent = byte.toString(16).padStart(2, '0');
+						hexSpan.dataset.byteIndex = index;
+
+						// Apply search highlights based on search type
+						this.matches.forEach(match => {
+							if (index >= match.index && index < match.index + match.length) {
+								hexSpan.classList.add('search-highlight');
+							}
+						});
+
+						hexLine.appendChild(hexSpan);
+
+						// Create text span
+						const charSpan = document.createElement('span');
+						let text = self.byteToChar(byte);
+						if (text === " ") text = "\u00A0";
+						else if (text === "-") text = "\u2011";
+						charSpan.textContent = text;
+						charSpan.dataset.byteIndex = index;
+						if (text === this._NON_PRINTABLE_CHAR) {
+							charSpan.classList.add("non-printable");
+						}
+
+						// Apply search highlights based on search type
+						this.matches.forEach(match => {
+							if (index >= match.index && index < match.index + match.length) {
+								charSpan.classList.add('search-highlight');
+							}
+						});
+
+						textLine.appendChild(charSpan);
+					}
+
+					hexviewFragment.appendChild(hexLine);
+					textviewFragment.appendChild(textLine);
+				}
+
+				this.offsets.appendChild(offsetsFragment);
+				this.hexview.appendChild(hexviewFragment);
+				this.textview.appendChild(textviewFragment);
+
+				this.updateSelection();
+			}
+
+			/**
+			 * Updates the visual selection in the hex and text views.
+			 */
+			updateSelection() {
+				// Restore the background color of the previous selection if any
+				if (this.previousSelectedIndex !== null) {
+					const prevHexSpan = this.hexview.querySelector(`span[data-byte-index="${this.previousSelectedIndex}"]`);
+					const prevTextSpan = this.textview.querySelector(`span[data-byte-index="${this.previousSelectedIndex}"]`);
+					if (prevHexSpan && prevTextSpan) {
+						// Remove active cursor classes
+						prevHexSpan.classList.remove('active-view-cursor');
+						prevTextSpan.classList.remove('active-view-cursor');
+
+						// Restore background based on whether it was part of a match
+						const wasInMatch = this.matches.some(match => this.previousSelectedIndex >= match.index && this.previousSelectedIndex < match.index + match.length);
+						if (wasInMatch) {
+							prevHexSpan.classList.add('highlighted');
+							prevTextSpan.classList.add('highlighted');
+						} else {
+							prevHexSpan.classList.remove('highlighted');
+							prevTextSpan.classList.remove('highlighted');
+						}
+					}
+				}
+
+				// Clear previous selection classes from active and passive views
+				Array.from(this.hexedit.querySelectorAll(".active-view-cursor, .passive-view-cursor, .highlighted"))
+					.forEach(e => e.classList.remove("active-view-cursor", "passive-view-cursor", "highlighted"));
+
+				if (this.selectedIndex === null) return;
+
+				// Check if selectedIndex is within the rendered range
+				if (this.selectedIndex >= this.startIndex && this.selectedIndex < this.startIndex + this.visibleByteCount) {
+					const hexSpan = this.hexview.querySelector(`span[data-byte-index="${this.selectedIndex}"]`);
+					const textSpan = this.textview.querySelector(`span[data-byte-index="${this.selectedIndex}"]`);
+					if (hexSpan && textSpan) {
+						// Determine if the selected byte is part of a match
+						const isInMatch = this.matches.some(match => this.selectedIndex >= match.index && this.selectedIndex < match.index + match.length);
+
+						// Store current selected index as previous for next update
+						this.previousSelectedIndex = this.selectedIndex;
+
+						if (this.activeView === 'hex') {
+							// Active view is Hex
+							hexSpan.classList.add("active-view-cursor"); // Blinking blue
+							// Passive view (Text)
+							textSpan.classList.add("passive-view-cursor"); // Always light blue
+						} else if (this.activeView === 'text') {
+							// Active view is Text
+							textSpan.classList.add("active-view-cursor"); // Blinking blue
+							// Passive view (Hex)
+							hexSpan.classList.add("passive-view-cursor"); // Always light blue
+						}
+
+						// Highlight the selected byte if it was part of a match
+						if (isInMatch) {
+							if (this.activeView === 'hex') {
+								hexSpan.classList.add('highlighted');
+							} else if (this.activeView === 'text') {
+								textSpan.classList.add('highlighted');
+							}
+						}
+
+						// Enable immediate editing in active view
+						if (this.activeView === 'hex') {
+							this.editHex = true;
+						} else if (this.activeView === 'text') {
+							this.editHex = false;
+						}
+
+						// Focus the active view to enable immediate editing
+						this.focusActiveView();
+					}
+				}
+			}
+
+			/**
+			 * Focuses the active view (hex or text).
+			 */
+			focusActiveView() {
+				if (this.activeView === 'hex') {
+					this.hexview.focus();
+				} else if (this.activeView === 'text') {
+					this.textview.focus();
+				}
+			}
+
+			/**
+			 * Registers event handlers for the hex editor.
+			 */
+			_registerEventHandlers() {
+				// Make hexview and textview focusable by setting tabindex
+				this.hexview.tabIndex = 0;
+				this.textview.tabIndex = 0;
+
+				// Handle focus on hexview
+				this.hexview.addEventListener("focus", () => {
+					this.activeView = 'hex';
+					this.updateSelection();
+				});
+
+				// Handle focus on textview
+				this.textview.addEventListener("focus", () => {
+					this.activeView = 'text';
+					this.updateSelection();
+				});
+
+				// Handle click on hexview
+				this.hexview.addEventListener("click", e => {
+					if (e.target.dataset.byteIndex === undefined) return;
+					const index = parseInt(e.target.dataset.byteIndex);
+					this.currentEdit = "";
+					this.editHex = true;
+					this.setSelectedIndex(index);
+					this.hexview.focus(); // Ensure hexview gains focus
+				});
+
+				// Handle click on textview
+				this.textview.addEventListener("click", e => {
+					if (e.target.dataset.byteIndex === undefined) return;
+					const index = parseInt(e.target.dataset.byteIndex);
+					this.currentEdit = "";
+					this.editHex = false;
+					this.setSelectedIndex(index);
+					this.textview.focus(); // Ensure textview gains focus
+				});
+
+				// Handle keydown events
+				this.hexedit.addEventListener("keydown", e => {
+					// If the target is an input (search UI), do not handle hex editor key events
+					if (e.target.tagName.toLowerCase() === 'input') return;
+
+					if (e.key === "Control") this.ctrlPressed = true;
+					if (this.selectedIndex === null || this.ctrlPressed) return;
+					if (e.key === "Escape") {
+						this.currentEdit = "";
+						this.setSelectedIndex(null);
+						return;
+					}
+					if (this.readonly) {
+						const offsetChange = this._keyShouldApply(e) ?? 0;
+						this.setSelectedIndex(this.selectedIndex + offsetChange);
+						return;
+					}
+					// Handle key inputs
+					const key = e.key;
+					if (this.editHex && key.length === 1 && key.match(/[0-9a-fA-F]/)) {
+						this.currentEdit += key;
+						e.preventDefault();
+						if (this.currentEdit.length === 2) {
+							const value = parseInt(this.currentEdit, 16);
+							this.setValueAt(this.selectedIndex, value);
+							this.currentEdit = "";
+							this.setSelectedIndex(this.selectedIndex + 1);
+						}
+					} else if (!this.editHex && key.length === 1) {
+						const value = key.charCodeAt(0);
+						this.setValueAt(this.selectedIndex, value);
+						this.setSelectedIndex(this.selectedIndex + 1);
+						e.preventDefault();
+					} else {
+						const offsetChange = this._keyShouldApply(e);
+						if (offsetChange) {
+							this.setSelectedIndex(this.selectedIndex + offsetChange);
+							e.preventDefault();
+						}
+					}
+				});
+
+				// Handle keyup events
+				this.hexedit.addEventListener("keyup", e => {
+					if (e.key === "Control") this.ctrlPressed = false;
+				});
+
+				// Handle scrolling for virtual scrolling
+				this.hexeditContent.addEventListener('scroll', this.handleScroll.bind(this));
+			}
+
+			/**
+			 * Sets the value at a specific index in the data and updates the view if necessary.
+			 *
+			 * @param {number} index - The byte index to set.
+			 * @param {number} value - The value to set.
+			 */
+			setValueAt(index, value) {
+				this.data[index] = value;
+				// If the index is within the rendered range, update the display
+				if (index >= this.startIndex && index < this.startIndex + this.visibleByteCount) {
+					const hexSpan = this.hexview.querySelector(`span[data-byte-index="${index}"]`);
+					const textSpan = this.textview.querySelector(`span[data-byte-index="${index}"]`);
+					if (hexSpan) hexSpan.textContent = value.toString(16).padStart(2, '0');
+					if (textSpan) {
+						let text = self.byteToChar(value);
+						if (text === " ") text = "\u00A0";
+						else if (text === "-") text = "\u2011";
+						textSpan.textContent = text;
+						if (text === this._NON_PRINTABLE_CHAR) {
+							textSpan.classList.add("non-printable");
+						} else {
+							textSpan.classList.remove("non-printable");
+						}
+					}
+				}
+			}
+
+			/**
+			 * Sets the currently selected byte index and updates the view.
+			 *
+			 * @param {number|null} index - The byte index to select, or null to clear selection.
+			 */
+			setSelectedIndex(index) {
+				this.selectedIndex = index;
+				// console.log(`setSelectedIndex called with index: ${index}`);
+
+				if (index !== null) {
+					// Calculate the line number of the selected index
+					const lineNumber = Math.floor(index / this.bytesPerRow);
+					const lineHeight = 16; // Height of one row in pixels
+					const scrollTop = lineNumber * lineHeight;
+
+					// Determine visible range
+					const visibleStartLine = Math.floor(this.hexeditContent.scrollTop / lineHeight);
+					const visibleEndLine = visibleStartLine + this.visibleRows;
+
+					// console.log(`setSelectedIndex: lineNumber=${lineNumber}, visibleStartLine=${visibleStartLine}, visibleEndLine=${visibleEndLine}`);
+
+					// If the selected line is out of the visible range, update scrollTop
+					if (lineNumber < visibleStartLine || lineNumber >= visibleEndLine) {
+						const newScrollTop = Math.max(0, (lineNumber * lineHeight) - ((this.visibleRows / 2) * lineHeight));
+						this.hexeditContent.scrollTop = newScrollTop;
+						// console.log(`setSelectedIndex: Updated scrollTop to ${this.hexeditContent.scrollTop}`);
+					}
+				}
+
+				this.updateSelection();
+			}
+
+			/**
+			 * Updates the search status field for a given search type.
+			 *
+			 * @param {string} searchType - The type of search ('ascii', 'hex', 'regex').
+			 * @param {number} current - The current match index.
+			 * @param {number} total - The total number of matches.
+			 */
+			updateSearchStatus(searchType, current, total) {
+				// Update only the relevant search type status field
+				['ascii', 'hex', 'regex'].forEach(type => {
+					const statusElement = document.getElementById(`hexedit-search-status-${type}`);
+					if (type === searchType) {
+						statusElement.textContent = `${current}/${total}`;
+					} else {
+						statusElement.textContent = `0/0`;
+					}
+				});
+			}
+
+			/**
+			 * Determines if a key event should result in a byte index change.
+			 *
+			 * @param {KeyboardEvent} event - The keyboard event.
+			 * @returns {number|null} - The byte index change or null.
+			 */
+			_keyShouldApply(event) {
+				if (event.key === "Enter") return 1;
+				if (event.key === "Tab") return 1;
+				if (event.key === "Backspace") return -1;
+				if (event.key === "ArrowLeft") return -1;
+				if (event.key === "ArrowRight") return 1;
+				if (event.key === "ArrowUp") return -16;
+				if (event.key === "ArrowDown") return 16;
+				return null;
+			}
+
+			/**
+			 * Fills the hex editor DOM structure.
+			 *
+			 * @param {HTMLElement} hexedit - The DOM element for the hex editor.
+			 * @returns {HTMLElement} - The filled hex editor DOM element.
+			 */
+			fillHexeditDom(hexedit) {
+				hexedit.classList.add('hexedit');
+
+				// Create headers
+				const offsetsHeader = document.createElement("div");
+				offsetsHeader.classList.add("offsets-header");
+				offsetsHeader.innerText = _('Offset (h)');
+
+				const hexviewHeader = document.createElement("div");
+				hexviewHeader.classList.add("hexview-header");
+				for (let i = 0; i < 16; i++) {
+					const span = document.createElement("span");
+					span.innerText = i.toString(16).toUpperCase().padStart(2, "0");
+					hexviewHeader.appendChild(span);
+				}
+
+				const textviewHeader = document.createElement("div");
+				textviewHeader.classList.add("textview-header");
+				textviewHeader.innerText = _('Decoded Text');
+
+				// Header container
+				const headersContainer = document.createElement("div");
+				headersContainer.classList.add("hexedit-headers");
+				headersContainer.appendChild(offsetsHeader);
+				headersContainer.appendChild(hexviewHeader);
+				headersContainer.appendChild(textviewHeader);
+
+				// Create content areas
+				const offsets = document.createElement("div");
+				offsets.classList.add("offsets");
+
+				const hexview = document.createElement("div");
+				hexview.classList.add("hexview");
+
+				const textview = document.createElement("div");
+				textview.classList.add("textview");
+
+				// Content container
+				const contentContainer = document.createElement("div");
+				contentContainer.classList.add("hexedit-content");
+				contentContainer.appendChild(offsets);
+				contentContainer.appendChild(hexview);
+				contentContainer.appendChild(textview);
+
+				// Assemble hex editor
+				hexedit.appendChild(headersContainer);
+				hexedit.appendChild(contentContainer);
+
+				// Assign references
+				hexedit.offsets = offsets;
+				hexedit.hexview = hexview;
+				hexedit.textview = textview;
+				hexedit.headersContainer = headersContainer;
+				hexedit.contentContainer = contentContainer;
+
+				return hexedit;
+			}
+		}
+
+		// Instantiate HexEditor with rootClass
+		var hexEditorInstance = new HexEditor(container, rootClass);
+
+		return hexEditorInstance;
+	},
+
+	/**
+	 * Opens a file in the hex editor.
+	 * @param {string} filePath - The path to the file to edit.
+	 * @param {string | ArrayBuffer} content - The content of the file.
+	 * @param {string} style - The style of the content ('Text' or 'Bin').
+	 */
+	edit: function(filePath, content, style, permissions, ownerGroup) {
+		var self = this;
+
+		if (style.toLowerCase() !== 'bin') {
+			self.popm(null, `[${PN}]: ` + _('Unsupported style "' + style + '". Only "Bin" is supported.'));
+			console.warn('Unsupported style:', style);
+			return;
+		}
+
+		self.currentFilePath = filePath;
+		self.permissions = permissions;
+		self.ownerGroup = ownerGroup;
+
+		var data;
+		if (content instanceof ArrayBuffer) {
+			data = new Uint8Array(content);
+		} else if (typeof content === 'string') {
+			data = new Uint8Array(content.length);
+			for (var i = 0; i < content.length; i++) {
+				data[i] = content.charCodeAt(i);
+			}
+		} else {
+			self.popm(null, `[${PN}]: ` + _('Unsupported content type.'));
+			console.error('Unsupported content type:', typeof content);
+			return;
+		}
+
+		self.hexEditorInstance.setData(data);
+
+		var parts = filePath.split('/');
+		var filename = parts[parts.length - 1];
+		self.filenameDisplay.textContent = 'Editing: ' + filename;
+
+		self.popm(null, `[${PN}]: ` + _('Opened file "' + filename + '".'));
+	},
+
+	/**
+	 * Saves the current data in the hex editor using the Navigation plugin.
+	 */
+	saveFile: function() {
+		var self = this;
+
+		if (!self.currentFilePath) {
+			self.popm(
+				['$Hex Editor'], 'Hex Editor: No file loaded to save.', 'error'
+			);
+			return;
+		}
+
+		var data = self.hexEditorInstance.getData();
+		var content = data.buffer; // Get ArrayBuffer
+
+		// Attempt to save the file using the Navigation plugin's write_file function
+		// Assuming write_file returns a Promise
+		self.write_file(self.currentFilePath, self.permissions, self.ownerGroup, content, 'Bin')
+			.then(function() {
+				self.popm(
+					['$Hex Editor'], 'Hex Editor: File saved successfully.', 'success'
+				);
+			})
+			.catch(function(err) {
+				self.popm(
+					['$Hex Editor'], 'Hex Editor: Error saving file.', 'error'
+				);
+				console.error('Error saving file:', err);
+			});
+	},
+
+	/**
+	 * Retrieves the current settings of the plugin.
+	 * @returns {Object} - Current settings including window size.
+	 */
+	get_settings: function() {
+		return {
+			width: this.editorDiv.style.width,
+			height: this.editorDiv.style.height
+		};
+	},
+
+	/**
+	 * Applies settings to the plugin.
+	 * @param {Object} settings - Settings object containing window size.
+	 */
+	set_settings: function(settings) {
+		if (settings.width) {
+			this.editorDiv.style.width = settings.width;
+		}
+		if (settings.height) {
+			this.editorDiv.style.height = settings.height;
+		}
+	}
+});
diff --git a/applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/term.js b/applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/term.js
new file mode 100644
index 000000000000..200b3ceee760
--- /dev/null
+++ b/applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/term.js
@@ -0,0 +1,354 @@
+'use strict';
+'require ui';
+'require dom';
+'require fs';
+
+/**
+ * Dumb Terminal Plugin
+ * Emulates a simple terminal for interacting with OpenWRT.
+ * Supports sending commands and displaying output.
+ * Utilizes fs.exec() from LuCI's fs.js for command execution.
+ * 
+ * Enhancements:
+ * - Adds resizable window with scrollbars.
+ * - Introduces settings for configuring window size.
+ */
+
+const PN = 'Dumb Term.';
+
+return Class.extend({
+	/**
+	 * Returns metadata about the plugin.
+	 * @returns {Object} Plugin information.
+	 */
+	info: function() {
+		return {
+			name: PN,
+			type: 'Utility',
+			description: 'Emulates a simple terminal for OpenWRT, allowing sending commands and receiving output.'
+		};
+	},
+
+	/**
+	 * Generates CSS styles for the Dumb Terminal plugin with a unique suffix.
+	 * @param {string} uniqueId - The unique identifier for this plugin instance.
+	 * @returns {string} - The CSS styles as a string.
+	 */
+	generateCss: function(uniqueId) {
+		return `
+            /* CSS for Dumb Terminal Plugin - Instance ${uniqueId} */
+            .dumb-terminal-plugin-${uniqueId} {
+                padding: 10px;
+                background-color: #1e1e1e;
+                border: 1px solid #555;
+                resize: both; /* Allows the window to be resizable */
+                overflow: hidden; /* Hide scrollbars at the plugin level */
+                box-shadow: 2px 2px 5px rgba(0,0,0,0.3);
+                font-family: 'Courier New', Courier, 'Lucida Console', 'Liberation Mono', monospace;
+                color: #ffffff;
+                position: relative;
+                display: flex;
+                flex-direction: column;
+                width: ${this.width || '400px'};
+                height: ${this.height || '300px'};
+            }
+
+            .dumb-terminal-plugin-${uniqueId} .terminal-output {
+                flex-grow: 1;
+                background-color: #000000;
+                padding: 10px;
+                overflow-y: auto; /* Enables vertical scrollbar */
+                overflow-x: auto; /* Enables horizontal scrollbar */
+                border: 1px solid #333;
+                margin-bottom: 10px;
+                white-space: pre-wrap;
+                font-size: 14px;
+                font-family: inherit; /* Inherits the monospace font from the parent */
+            }
+
+            .dumb-terminal-plugin-${uniqueId} .terminal-input {
+                display: flex;
+            }
+
+            .dumb-terminal-plugin-${uniqueId} .terminal-input input {
+                flex-grow: 1;
+                padding: 8px;
+                background-color: #2a2a2a;
+                border: 1px solid #555;
+                color: #ffffff;
+                outline: none;
+                font-size: 14px;
+                font-family: inherit; /* Inherits the monospace font from the parent */
+            }
+
+            .dumb-terminal-plugin-${uniqueId} .terminal-input button {
+                padding: 8px 16px;
+                background-color: #0078d7;
+                color: #fff;
+                border: none;
+                cursor: pointer;
+                margin-left: 5px;
+                border-radius: 4px;
+                font-size: 14px;
+                font-family: inherit; /* Inherits the monospace font from the parent */
+            }
+
+            .dumb-terminal-plugin-${uniqueId} .terminal-input button:hover {
+                background-color: #005fa3;
+            }
+
+            /* Dark theme adjustments */
+            .dark-theme .dumb-terminal-plugin-${uniqueId} {
+                background-color: #2a2a2a;
+                border-color: #777;
+            }
+
+            .dark-theme .dumb-terminal-plugin-${uniqueId} .terminal-output {
+                background-color: #1e1e1e;
+                border-color: #555;
+            }
+
+            .dark-theme .dumb-terminal-plugin-${uniqueId} .terminal-input input {
+                background-color: #3a3a3a;
+                border-color: #555;
+                color: #fff;
+            }
+
+            .dark-theme .dumb-terminal-plugin-${uniqueId} .terminal-input button {
+                background-color: #1e90ff;
+            }
+
+            .dark-theme .dumb-terminal-plugin-${uniqueId} .terminal-input button:hover {
+                background-color: #1c7ed6;
+            }
+        `;
+	},
+
+	/**
+	 * Initializes the plugin within the given container.
+	 * @param {HTMLElement} container - The container element where the plugin will be rendered.
+	 * @param {Object} pluginsRegistry - The registry of all loaded plugins.
+	 * @param {Object} default_plugins - The default plugins for each type.
+	 * @param {string} uniqueId - A unique identifier for this plugin instance.
+	 */
+	start: function(container, pluginsRegistry, default_plugins, uniqueId) {
+		var self = this;
+
+		// Initialize command history
+		self.commandHistory = [];
+		self.historyIndex = -1;
+
+		// Ensure the plugin is only initialized once
+		if (self.initialized) {
+			return;
+		}
+		self.initialized = true;
+
+		// Store references for later use
+		self.pluginsRegistry = pluginsRegistry;
+		self.default_plugins = default_plugins;
+		self.uniqueId = uniqueId;
+
+		// Set default window size
+		self.width = '400px';
+		self.height = '300px';
+
+		// Create and inject the unique CSS for this plugin instance
+		var styleTag = document.createElement('style');
+		styleTag.type = 'text/css';
+		styleTag.id = `dumb-terminal-plugin-style-${uniqueId}`;
+		styleTag.innerHTML = self.generateCss(uniqueId);
+		document.head.appendChild(styleTag);
+		self.styleTag = styleTag; // Store reference for potential future removal
+
+		// Create the main div for the terminal with a unique class
+		self.terminalDiv = document.createElement('div');
+		self.terminalDiv.className = `dumb-terminal-plugin-${uniqueId}`;
+		self.terminalDiv.style.width = self.width;
+		self.terminalDiv.style.height = self.height;
+
+		// Create the terminal output area
+		self.outputDiv = document.createElement('div');
+		self.outputDiv.className = 'terminal-output';
+		self.outputDiv.textContent = 'Terminal initialized.\n';
+
+		// Create the input container
+		self.inputContainer = document.createElement('div');
+		self.inputContainer.className = 'terminal-input';
+
+		// Create the input field for commands
+		self.inputField = document.createElement('input');
+		self.inputField.type = 'text';
+		self.inputField.placeholder = 'Enter command...';
+		self.inputField.addEventListener('keypress', function(event) {
+			if (event.key === 'Enter') {
+				self.executeCommand();
+			}
+		});
+		self.inputField.addEventListener('keydown', function(event) {
+			if (event.key === 'ArrowUp') {
+				if (self.historyIndex > 0) {
+					self.historyIndex--;
+					self.inputField.value = self.commandHistory[self.historyIndex];
+				}
+				event.preventDefault();
+			} else if (event.key === 'ArrowDown') {
+				if (self.historyIndex < self.commandHistory.length - 1) {
+					self.historyIndex++;
+					self.inputField.value = self.commandHistory[self.historyIndex];
+				} else {
+					self.historyIndex = self.commandHistory.length;
+					self.inputField.value = '';
+				}
+				event.preventDefault();
+			}
+		});
+
+		// Create the execute button
+		self.executeButton = document.createElement('button');
+		self.executeButton.textContent = 'Run';
+		self.executeButton.onclick = self.executeCommand.bind(this);
+
+		// Create the clear button
+		self.clearButton = document.createElement('button');
+		self.clearButton.textContent = 'Clear';
+		self.clearButton.onclick = function() {
+			self.outputDiv.textContent = '';
+		};
+
+		// Append input fields and buttons to the input container
+		self.inputContainer.appendChild(self.inputField);
+		self.inputContainer.appendChild(self.executeButton);
+		self.inputContainer.appendChild(self.clearButton);
+
+		// Append output and input containers to the main terminal div
+		self.terminalDiv.appendChild(self.outputDiv);
+		self.terminalDiv.appendChild(self.inputContainer);
+
+		// Append the terminal div to the provided container
+		container.appendChild(self.terminalDiv);
+
+		// Retrieve the Dispatcher plugin for notifications (if available)
+		var dispatcherName = self.default_plugins['Dispatcher'];
+		if (dispatcherName && self.pluginsRegistry[dispatcherName]) {
+			var dispatcher = self.pluginsRegistry[dispatcherName];
+			self.popm = dispatcher.pop.bind(dispatcher);
+		}
+
+		// No need to retrieve Navigation plugin since fs.exec() is used directly
+	},
+
+	/**
+	 * Executes the command entered by the user using fs.exec().
+	 */
+	executeCommand: function() {
+		var self = this;
+		var commandInput = self.inputField.value.trim();
+		if (commandInput === '') return;
+
+		// Display the entered command in the output area
+		self.outputDiv.textContent += `> ${commandInput}\n`;
+		self.inputField.value = '';
+		self.inputField.focus();
+
+		// Add to command history
+		self.commandHistory.push(commandInput);
+		self.historyIndex = self.commandHistory.length;
+
+		// Split the command into command and arguments
+		var parts = self.parseCommand(commandInput);
+		var cmd = parts.cmd;
+		var args = parts.args;
+
+		// Execute the command using fs.exec()
+		// fs.exec(command, args, env) returns a Promise
+		// 'env' can be null if no environment variables are needed
+		fs.exec(cmd, args, null)
+			.then(function(result) {
+				// Assuming result has 'stdout' and 'stderr'
+				if (result.stdout && result.stdout.trim() !== '') {
+					self.outputDiv.textContent += `${result.stdout}\n`;
+				}
+				if (result.stderr && result.stderr.trim() !== '') {
+					self.outputDiv.textContent += `Error: ${result.stderr}\n`;
+				}
+				if (self.popm) {
+					self.popm(null, `[${PN}]: Command executed successfully.`);
+				}
+				self.outputDiv.scrollTop = self.outputDiv.scrollHeight;
+			})
+			.catch(function(error) {
+				// Handle errors from the RPC call
+				self.outputDiv.textContent += `Error: ${error.message}\n`;
+				if (self.popm) {
+					self.popm(null, `[${PN}]: Error executing command.`);
+				}
+				self.outputDiv.scrollTop = self.outputDiv.scrollHeight;
+				console.error('Error executing command:', error);
+			});
+
+		// Optional: Log the command execution attempt
+		console.log(`[${PN}]: Executed command - ${commandInput}`);
+	},
+
+	/**
+	 * Parses the command string into command and arguments.
+	 * @param {string} commandInput - The raw command string entered by the user.
+	 * @returns {Object} An object containing the command and an array of arguments.
+	 */
+	parseCommand: function(commandInput) {
+		// Simple parsing: split by spaces, handle quotes if necessary
+		// For more robust parsing, consider using a proper command-line parser
+
+		var regex = /[^\s"]+|"([^"]*)"/gi;
+		var args = [];
+		var match;
+		while ((match = regex.exec(commandInput)) !== null) {
+			args.push(match[1] ? match[1] : match[0]);
+		}
+
+		var cmd = args.shift(); // The first element is the command
+		return {
+			cmd: cmd,
+			args: args
+		};
+	},
+
+	/**
+	 * Retrieves the current settings of the plugin.
+	 * @returns {Object} - Current settings including window size.
+	 */
+	get_settings: function() {
+		return {
+			width: this.terminalDiv.style.width || '400px',
+			height: this.terminalDiv.style.height || '300px'
+		};
+	},
+
+	/**
+	 * Applies settings to the plugin.
+	 * @param {Object} settings - Settings object containing window size.
+	 */
+	set_settings: function(settings) {
+		if (settings.width) {
+			this.terminalDiv.style.width = settings.width;
+		}
+		if (settings.height) {
+			this.terminalDiv.style.height = settings.height;
+		}
+	},
+
+	/**
+	 * Cleans up the plugin instance by removing injected styles and elements.
+	 */
+	destroy: function() {
+		var self = this;
+		if (self.styleTag) {
+			self.styleTag.remove();
+		}
+		if (self.terminalDiv && self.terminalDiv.parentNode) {
+			self.terminalDiv.parentNode.removeChild(self.terminalDiv);
+		}
+		self.initialized = false;
+	}
+});
diff --git a/applications/luci-app-file-plug-manager/po/templates/file-plug-manager.pot b/applications/luci-app-file-plug-manager/po/templates/file-plug-manager.pot
new file mode 100644
index 000000000000..4089ac25d92b
--- /dev/null
+++ b/applications/luci-app-file-plug-manager/po/templates/file-plug-manager.pot
@@ -0,0 +1,531 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-12-21 09:04-0500\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager.js:145
+msgid "File Plug Manager"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager.js:391
+msgid "Logs are always active"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager.js:481
+#, javascript-format
+msgid "Plugin \"%s\" has been activated."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager.js:483
+#, javascript-format
+msgid "Plugin \"%s\" not found."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager.js:517
+#, javascript-format
+msgid "Duplicate plugin name \"%s\" found. Skipping."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager.js:527
+#, javascript-format
+msgid "Plugin \"%s\" is missing required functions. Skipping."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager.js:534
+#, javascript-format
+msgid "Plugin \"%s\" has invalid info. Skipping."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager.js:540
+#, javascript-format
+msgid "Plugin \"%s\" has unsupported type \"%s\". Skipping."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager.js:548
+#, javascript-format
+msgid "Navigation plugin \"%s\" is missing required functions. Skipping."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager.js:556
+#, javascript-format
+msgid "Settings plugin \"%s\" is missing read_settings. Skipping."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager.js:571
+#, javascript-format
+msgid "Error loading plugin \"%s\"."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager.js:576
+#, javascript-format
+msgid "Ignored non-JS file \"%s\" in plugins directory."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager.js:640
+msgid "Settings loaded successfully."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager.js:642
+msgid "Error reading settings."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager.js:646
+msgid "Settings plugin does not implement read_settings."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager.js:649
+msgid "Tab for default Settings plugin not found."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager.js:652
+msgid "Default Settings plugin not found or cannot be started."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager.js:655
+msgid "No default Settings plugin available."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager.js:666
+msgid "Error executing ls to load plugins."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager.js:697
+#, javascript-format
+msgid "No plugins available for type \"%s\"."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager.js:725
+msgid "Set as default"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager.js:764
+#, javascript-format
+msgid "Set \"%s\" as the default %s plugin."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager.js:800
+msgid "Error parsing dropped data."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager.js:805
+msgid "Unsupported drop data."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager.js:869
+msgid "Target editor plugin does not support editing files."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager.js:874
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/hexEditor.js:380
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Settings.js:445
+msgid "No default Navigation plugin set."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager.js:880
+msgid "Default Navigation plugin does not support reading files."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager.js:890
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2833
+#, javascript-format
+msgid "File \"%s\" opened in editor."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager.js:892
+#, javascript-format
+msgid "Error reading file \"%s\"."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager.js:896
+msgid "Navigation plugin does not handle direct file opening."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/hexEditor.js:387
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Settings.js:452
+msgid "Navigation plugin does not support writing files."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/hexEditor.js:422
+msgid "No file loaded to save."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/hexEditor.js:433
+msgid "File saved successfully."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/hexEditor.js:436
+msgid "Error saving file."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/hexEditor.js:567
+msgid "Search ASCII"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/hexEditor.js:570
+msgid "Search HEX (e.g., 4F6B)"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/hexEditor.js:573
+msgid "Search RegExp (e.g., \\d{3})"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/hexEditor.js:1375
+msgid "Offset (h)"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/hexEditor.js:1387
+msgid "Decoded Text"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/hexEditor.js:1444
+msgid "Unsupported style \""
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/hexEditor.js:1462
+msgid "Unsupported content type."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/hexEditor.js:1473
+msgid "Opened file \""
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:1098
+msgid "Drop files here to upload"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:1109
+msgid "Upload"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:1115
+msgid "Create Folder"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:1121
+msgid "Create File"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:1127
+msgid "Delete Selected"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:1336
+msgid "The specified path is not a directory."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:1339
+#, javascript-format
+msgid "Failed to access the specified path: %s"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:1508
+msgid "Failed to set permissions or ownership:"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:1517
+msgid "Network error"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:1565
+#, javascript-format
+msgid "Uploading \"%s\"..."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:1595
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:1597
+#, javascript-format
+msgid "File \"%s\" uploaded successfully."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:1604
+#, javascript-format
+msgid "Upload failed for file \"%s\"."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:1606
+#, javascript-format
+msgid "Error uploading file \"%s\"."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:1620
+msgid "Enter folder name:"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:1626
+#, javascript-format
+msgid "Folder \"%s\" created successfully."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:1631
+#, javascript-format
+msgid "Failed to create folder \"%s\": %s"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:1641
+msgid "Enter file name:"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:1647
+#, javascript-format
+msgid "File \"%s\" created successfully."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:1652
+#, javascript-format
+msgid "Failed to create file \"%s\": %s"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:1664
+msgid "Are you sure you want to delete the selected items?"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:1685
+#, javascript-format
+msgid "Successfully deleted %d items."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:1689
+#, javascript-format
+msgid "Failed to delete \"%s\"."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:1848
+msgid "Loading..."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:1871
+#, javascript-format
+msgid "Failed to list directory: %s"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:1872
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:1923
+msgid "Error loading directory."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2009
+msgid ""
+"Dragging started. Drop onto a directory within this UI to copy/move files "
+"(Alt=copy), or drop outside the browser to download."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2080
+msgid "Edit"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2090
+msgid "Copy"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2100
+msgid "Delete"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2111
+msgid "Download"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2290
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2624
+msgid "Response is not a Blob"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2302
+msgid "Download failed:"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2303
+msgid "Download failed: "
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2314
+#, javascript-format
+msgid "Are you sure you want to delete \"%s\"?"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2316
+#, javascript-format
+msgid "File \"%s\" deleted successfully."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2319
+#, javascript-format
+msgid "Failed to delete file \"%s\": %s"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2364
+#, javascript-format
+msgid "Error checking \"%s\": %s"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2379
+#, javascript-format
+msgid "Directory \"%s\" copied successfully as \"%s\"."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2382
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2385
+#, javascript-format
+msgid "Failed to copy directory \"%s\": %s"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2391
+#, javascript-format
+msgid "Symlink \"%s\" copied successfully as \"%s\"."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2394
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2397
+#, javascript-format
+msgid "Failed to copy symlink \"%s\": %s"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2403
+#, javascript-format
+msgid "File \"%s\" copied successfully as \"%s\"."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2406
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2409
+#, javascript-format
+msgid "Failed to copy file \"%s\": %s"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2413
+#, javascript-format
+msgid "Failed to find copy number for \"%s\": %s"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2436
+#, javascript-format
+msgid "Edit \"%s\""
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2437
+msgid "New Name:"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2443
+msgid "Owner:Group:"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2449
+msgid "Permissions:"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2457
+msgid "Submit"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2460
+msgid "Cancel"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2486
+msgid "File name cannot be empty."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2506
+#, javascript-format
+msgid "\"%s\" edited successfully."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2510
+#, javascript-format
+msgid "Failed to edit \"%s\": %s"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2542
+msgid "Root"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2767
+msgid "No files were dragged."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2775
+msgid "Failed to parse dragged files data."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2817
+msgid "No default editor plugin found."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2835
+msgid "Unable to activate editor plugin."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2836
+msgid "Main Dispatcher or activatePlugin method not found."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2841
+msgid "Default editor does not implement edit function."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Navigation.js:2845
+#, javascript-format
+msgid "Failed to read file \"%s\": %s"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Settings.js:268
+msgid "Settings: Error applying settings to plugin \""
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Settings.js:284
+msgid "Settings: Configuration file not found. Using default settings."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Settings.js:291
+msgid "Settings: Error reading settings."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Settings.js:418
+msgid "Save Settings"
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Settings.js:473
+msgid "Settings panel not found."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Settings.js:501
+msgid "Error retrieving settings for \""
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Settings.js:507
+msgid "No settings available."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Settings.js:590
+msgid "Settings form not found."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Settings.js:629
+msgid "Error applying settings to plugin \""
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Settings.js:638
+msgid "Settings saved successfully."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Settings.js:642
+msgid "Error saving settings to file."
+msgstr ""
+
+#: applications/luci-app-file-plug-manager/htdocs/luci-static/resources/view/system/file-plug-manager/plugins/Settings.js:646
+msgid "Error applying settings."
+msgstr ""
diff --git a/applications/luci-app-file-plug-manager/root/usr/share/luci/menu.d/luci-app-file-plug-manager.json b/applications/luci-app-file-plug-manager/root/usr/share/luci/menu.d/luci-app-file-plug-manager.json
new file mode 100644
index 000000000000..153f927093c4
--- /dev/null
+++ b/applications/luci-app-file-plug-manager/root/usr/share/luci/menu.d/luci-app-file-plug-manager.json
@@ -0,0 +1,13 @@
+{
+	"admin/system/file-plug-manager": {
+		"title": "File Manager Plugins",
+		"order": 80,
+		"action": {
+			"type": "view",
+			"path": "system/file-plug-manager"
+		},
+		"depends": {
+			"acl": [ "luci-app-file-plug-manager" ]
+		}
+	}
+}
diff --git a/applications/luci-app-file-plug-manager/root/usr/share/rpcd/acl.d/luci-app-file-plug-manager.json b/applications/luci-app-file-plug-manager/root/usr/share/rpcd/acl.d/luci-app-file-plug-manager.json
new file mode 100644
index 000000000000..2ac5c5c9ca73
--- /dev/null
+++ b/applications/luci-app-file-plug-manager/root/usr/share/rpcd/acl.d/luci-app-file-plug-manager.json
@@ -0,0 +1,14 @@
+{
+	"luci-app-file-plug-manager": {
+		"description": "Grant access to File Manager",
+		"write": {
+			"cgi-io": [ "upload", "download" ],
+			"ubus": {
+				"file": [ "*" ]
+			},
+			"file": {
+				"/*": [  "list", "read", "write", "exec" ]
+			}
+		}
+	}
+}