diff --git a/examples/windows_app_example.php b/examples/windows_app_example.php index 3a954c9..900b1da 100644 --- a/examples/windows_app_example.php +++ b/examples/windows_app_example.php @@ -66,7 +66,10 @@ $mainContainer = new Container('flex flex-col bg-gray-100'); // Modal dialog setup (hidden by default) $apiKeyInput = new TextInput('API Key', 'w-full border border-gray-300 rounded px-3 py-2 bg-white text-black'); -$privateKeyPathInput = new TextInput('Private Key Path', 'w-full border border-gray-300 rounded px-3 py-2 bg-white text-black'); +$privateKeyPathInput = new TextInput( + 'Private Key Path', + 'w-full border border-gray-300 rounded px-3 py-2 bg-white text-black', +); $modalDialog = new Container('bg-white rounded-lg p-6 flex flex-col w-96 gap-3'); $modalDialog->addComponent(new Label('API Einstellungen', 'text-xl font-bold text-black')); @@ -115,7 +118,14 @@ $fileMenu->addItem('Beenden', function () use ($app) { // Settings Menu $settingsMenu = new Menu(title: 'Einstellungen'); -$settingsMenu->addItem('Optionen', function () use ($menuBar, $modal, $apiKeyInput, $privateKeyPathInput, &$currentApiKey, &$currentPrivateKeyPath) { +$settingsMenu->addItem('Optionen', function () use ( + $menuBar, + $modal, + $apiKeyInput, + $privateKeyPathInput, + &$currentApiKey, + &$currentPrivateKeyPath, +) { $menuBar->closeAllMenus(); $apiKeyInput->setValue($currentApiKey); $privateKeyPathInput->setValue($currentPrivateKeyPath); @@ -147,6 +157,10 @@ $refreshButton->setIcon($refreshIcon); $leftSide->addComponent($refreshButton); +// Search input field +$searchInput = new TextInput('Suche...', 'w-full border border-gray-300 rounded px-3 py-2 bg-white text-black mb-2'); +$leftSide->addComponent($searchInput); + $table = new Table(style: ' flex-1'); // flex-1 to fill available height but not expand infinitely $table->setColumns([ @@ -168,7 +182,26 @@ for ($i = 1; $i <= 63; $i++) { 'ipv4' => sprintf('192.168.%d.%d', floor($i / 255), $i % 255), ]; } -$table->setData($testData); + +// Store current server data (will be updated when API loads servers) +$currentServerData = $testData; +$table->setData($currentServerData); + +// Add search functionality +$searchInput->setOnChange(function ($value) use ($table, &$currentServerData) { + $searchTerm = strtolower(trim($value)); + + if (empty($searchTerm)) { + // Show all data if search is empty + $table->setData($currentServerData); + } else { + // Filter by name + $filteredData = array_filter($currentServerData, function ($row) use ($searchTerm) { + return str_contains(strtolower($row['name']), $searchTerm); + }); + $table->setData(array_values($filteredData)); + } +}); $leftSide->addComponent($table); $tab1->addComponent($leftSide); @@ -197,11 +230,71 @@ $detailPanel->addComponent(new Label('IPv4:', 'text-xs text-gray-500 mt-2')); $detailPanel->addComponent($detailIpv4); // SFTP Manager Button -$sftpButton = new Button('SFTP Manager öffnen', 'w-full mt-4 px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 flex items-center justify-center'); +$sftpButton = new Button( + 'SFTP Manager öffnen', + 'w-full mt-4 px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 flex items-center justify-center', +); $sftpIcon = new Icon(IconName::home, 18, 'text-white mr-2'); $sftpButton->setIcon($sftpIcon); + +// Add synchronous click handler to switch tab immediately +$sftpButton->setOnClick(function () use ($tabContainer) { + $tabContainer->setActiveTab(3); // Switch to SFTP Manager tab immediately +}); + $detailPanel->addComponent($sftpButton); +// SSH Terminal Button +$sshTerminalButton = new Button( + 'SSH Terminal öffnen', + 'w-full mt-2 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 flex items-center justify-center', +); +$sshTerminalIcon = new Icon(IconName::home, 18, 'text-white mr-2'); +$sshTerminalButton->setIcon($sshTerminalIcon); + +// Add click handler to open SSH terminal +$sshTerminalButton->setOnClick(function () use (&$selectedServer, &$currentPrivateKeyPath, &$statusLabel) { + if ($selectedServer === null) { + $statusLabel->setText('Kein Server ausgewählt'); + return; + } + + if (empty($currentPrivateKeyPath) || !file_exists($currentPrivateKeyPath)) { + $statusLabel->setText('Private Key Pfad nicht konfiguriert'); + return; + } + + // Build SSH command with private key + $host = $selectedServer['ipv4']; + $keyPath = escapeshellarg($currentPrivateKeyPath); + $sshCommand = "ssh -i {$keyPath} root@{$host}"; + + // Try to open terminal with SSH connection + // Different terminal emulators for different systems + $terminals = [ + 'gnome-terminal -- ' . $sshCommand, + 'konsole -e ' . $sshCommand, + 'xterm -e ' . $sshCommand, + 'x-terminal-emulator -e ' . $sshCommand, + ]; + + $opened = false; + foreach ($terminals as $terminalCmd) { + exec($terminalCmd . ' > /dev/null 2>&1 &', $output, $returnCode); + if ($returnCode === 0) { + $opened = true; + $statusLabel->setText('SSH Terminal geöffnet für ' . $selectedServer['name']); + break; + } + } + + if (!$opened) { + $statusLabel->setText('Konnte kein Terminal öffnen. SSH Befehl: ' . $sshCommand); + } +}); + +$detailPanel->addComponent($sshTerminalButton); + $tab1->addComponent($detailPanel); // Row selection handler - update detail panel @@ -209,7 +302,15 @@ $statusLabel = new Label( text: 'Fenster: ' . $window->getViewport()->windowWidth . 'x' . $window->getViewport()->windowHeight, style: 'basis-4/8 text-black', ); -$table->setOnRowSelect(function ($index, $row) use (&$statusLabel, &$selectedServer, $detailId, $detailName, $detailStatus, $detailType, $detailIpv4) { +$table->setOnRowSelect(function ($index, $row) use ( + &$statusLabel, + &$selectedServer, + $detailId, + $detailName, + $detailStatus, + $detailType, + $detailIpv4, +) { if ($row) { $statusLabel->setText("Server: {$row['name']} - {$row['status']} ({$row['ipv4']})"); @@ -260,14 +361,29 @@ $loadServersAsync = function () use ($currentApiKey) { // Configure refresh button to load servers asynchronously $refreshButton->setOnClickAsync( $loadServersAsync, - function ($result) use ($table, $statusLabel) { + function ($result) use ($table, $statusLabel, &$currentServerData, $searchInput) { // Handle the result in the main thread (can access objects here) if (is_array($result)) { if (isset($result['error'])) { $statusLabel->setText('Fehler: ' . $result['error']); echo "Error: {$result['error']}\n"; } elseif (isset($result['success'], $result['servers'])) { - $table->setData($result['servers']); + // Update current server data + $currentServerData = $result['servers']; + + // Check if search is active + $searchTerm = strtolower(trim($searchInput->getValue())); + if (empty($searchTerm)) { + // No search, show all servers + $table->setData($currentServerData); + } else { + // Apply search filter to new data + $filteredData = array_filter($currentServerData, function ($row) use ($searchTerm) { + return str_contains(strtolower($row['name']), $searchTerm); + }); + $table->setData(array_values($filteredData)); + } + $statusLabel->setText('Server geladen: ' . $result['count'] . ' gefunden'); echo "Success: {$result['count']} servers loaded\n"; } @@ -282,19 +398,7 @@ $refreshButton->setOnClickAsync( $tabContainer->addTab('Server', $tab1); -// Tab 2: Some info -$tab2 = new Container('flex flex-col p-4'); -$tab2->addComponent(new Label('Dies ist Tab 2', 'text-xl font-bold mb-4')); -$tab2->addComponent(new Label('Hier könnte weiterer Inhalt stehen...', '')); -$tabContainer->addTab('Info', $tab2); - -// Tab 3: Settings -$tab3 = new Container('flex flex-col p-4'); -$tab3->addComponent(new Label('Einstellungen', 'text-xl font-bold mb-4')); -$tab3->addComponent(new Label('Konfigurationsoptionen...', '')); -$tabContainer->addTab('Einstellungen', $tab3); - -// Tab 4: SFTP Manager (will be populated dynamically when server is selected) +// Tab 2: SFTP Manager (will be populated dynamically when server is selected) $sftpTab = new Container('flex flex-row p-4 gap-4 bg-gray-50'); // Left side: Local file browser @@ -318,6 +422,80 @@ $sftpTab->addComponent($remoteBrowserContainer); $tabContainer->addTab('SFTP Manager', $sftpTab); +// Remote FileBrowser navigation handler - load directory asynchronously +$remoteFileBrowser->setOnFileSelect(function ($path, $row) use ( + $remoteFileBrowser, + &$selectedServer, + &$currentPrivateKeyPath, + &$statusLabel, +) { + if (!isset($row['isDir']) || !$row['isDir']) { + return; // Only handle directories + } + + // Load directory asynchronously via Button with async handler + $loadButton = new Button('Load', ''); + $loadButton->setOnClickAsync( + function () use ($path, &$selectedServer, &$currentPrivateKeyPath) { + if ($selectedServer === null || empty($currentPrivateKeyPath)) { + return ['error' => 'Not connected']; + } + + try { + $sftp = new \phpseclib3\Net\SFTP($selectedServer['ipv4']); + $key = \phpseclib3\Crypt\PublicKeyLoader::load(file_get_contents($currentPrivateKeyPath)); + + if (!$sftp->login('root', $key)) { + return ['error' => 'SFTP Login failed']; + } + + $files = $sftp->nlist($path); + if ($files === false) { + return ['error' => 'Cannot read directory']; + } + + $fileList = []; + foreach ($files as $file) { + if ($file === '.' || $file === '..') { + continue; + } + $fullPath = rtrim($path, '/') . '/' . $file; + $stat = $sftp->stat($fullPath); + $fileList[] = [ + 'name' => $file, + 'path' => $fullPath, + 'isDir' => ($stat['type'] ?? 0) === 2, + 'size' => $stat['size'] ?? 0, + 'mtime' => $stat['mtime'] ?? 0, + ]; + } + + return ['success' => true, 'path' => $path, 'files' => $fileList]; + } catch (\Exception $e) { + return ['error' => $e->getMessage()]; + } + }, + function ($result) use ($remoteFileBrowser, &$statusLabel) { + if (isset($result['error'])) { + $statusLabel->setText('SFTP Fehler: ' . $result['error']); + return; + } + + if (isset($result['success'])) { + $remoteFileBrowser->setPath($result['path']); + $remoteFileBrowser->setFileData($result['files']); + } + }, + function ($error) use (&$statusLabel) { + $errorMsg = is_string($error) ? $error : 'Unknown error'; + $statusLabel->setText('SFTP Error: ' . $errorMsg); + }, + ); + + // Trigger the async load + $loadButton->handleMouseClick(0, 0, 0); +}); + // SFTP Button Click Handler - Connect to server and load remote files $sftpButton->setOnClickAsync( function () use (&$selectedServer, &$currentPrivateKeyPath) { @@ -382,7 +560,9 @@ $sftpButton->setOnClickAsync( } if (isset($result['success']) && $result['success']) { - $connectionStatusLabel->setText('Verbunden mit: ' . $result['server']['name'] . ' (' . $result['server']['ipv4'] . ')'); + $connectionStatusLabel->setText( + 'Verbunden mit: ' . $result['server']['name'] . ' (' . $result['server']['ipv4'] . ')', + ); $statusLabel->setText('SFTP Verbindung erfolgreich zu ' . $result['server']['name']); // Populate remote file browser with the file list @@ -390,11 +570,13 @@ $sftpButton->setOnClickAsync( $remoteFileBrowser->setFileData($result['files']); // Switch to SFTP Manager tab - $tabContainer->setActiveTab(3); // Index 3 = SFTP Manager tab + $tabContainer->setActiveTab(1); // Index 3 = SFTP Manager tab } }, function ($error) use (&$statusLabel) { - $errorMsg = is_string($error) ? $error : (is_object($error) && method_exists($error, 'getMessage') ? $error->getMessage() : 'Unbekannter Fehler'); + $errorMsg = is_string($error) + ? $error + : (is_object($error) && method_exists($error, 'getMessage') ? $error->getMessage() : 'Unbekannter Fehler'); $statusLabel->setText('SFTP Async Fehler: ' . $errorMsg); }, ); @@ -424,7 +606,16 @@ $cancelButton->setOnClick(function () use ($menuBar, $modal) { $modal->setVisible(false); }); -$saveButton->setOnClick(function () use ($settings, &$currentApiKey, &$currentPrivateKeyPath, $apiKeyInput, $privateKeyPathInput, $menuBar, $modal, &$statusLabel) { +$saveButton->setOnClick(function () use ( + $settings, + &$currentApiKey, + &$currentPrivateKeyPath, + $apiKeyInput, + $privateKeyPathInput, + $menuBar, + $modal, + &$statusLabel, +) { $currentApiKey = trim($apiKeyInput->getValue()); $currentPrivateKeyPath = trim($privateKeyPathInput->getValue()); diff --git a/src/Ui/Widget/ClickableHeaderLabel.php b/src/Ui/Widget/ClickableHeaderLabel.php new file mode 100644 index 0000000..5941702 --- /dev/null +++ b/src/Ui/Widget/ClickableHeaderLabel.php @@ -0,0 +1,30 @@ +columnKey = $columnKey; + $this->table = $table; + parent::__construct($title, $style); + } + + public function handleMouseClick(float $mouseX, float $mouseY, int $button): bool + { + if ( + $mouseX >= $this->viewport->x && + $mouseX <= ($this->viewport->x + $this->viewport->width) && + $mouseY >= $this->viewport->y && + $mouseY <= ($this->viewport->y + $this->viewport->height) + ) { + $this->table->sortByColumn($this->columnKey); + return true; + } + return parent::handleMouseClick($mouseX, $mouseY, $button); + } +} diff --git a/src/Ui/Widget/FileBrowser.php b/src/Ui/Widget/FileBrowser.php index 9cf587a..5e241b4 100644 --- a/src/Ui/Widget/FileBrowser.php +++ b/src/Ui/Widget/FileBrowser.php @@ -269,5 +269,34 @@ class FileBrowser extends Container } $this->fileTable->setData($tableData); + + // Set up row selection handler AFTER data is set (for remote browsers) + // This needs to be done every time because setData might reset handlers + $this->setupRemoteNavigationHandler(); + } + + /** + * Setup navigation handler for remote file browser + */ + private function setupRemoteNavigationHandler(): void + { + if (!$this->isRemote) { + return; + } + + $fileBrowser = $this; + $this->fileTable->setOnRowSelect(function ($index, $row) use ($fileBrowser) { + if ($row && isset($row['isDir']) && $row['isDir'] && !empty($row['path'])) { + // Trigger the external callback for directory navigation + if ($fileBrowser->onFileSelect !== null) { + ($fileBrowser->onFileSelect)($row['path'], $row); + } + } elseif ($row && isset($row['path']) && !empty($row['path']) && !($row['isDir'] ?? false)) { + // File selected + if ($fileBrowser->onFileSelect !== null) { + ($fileBrowser->onFileSelect)($row['path'], $row); + } + } + }); } } diff --git a/src/Ui/Widget/Table.php b/src/Ui/Widget/Table.php index c211912..f396297 100644 --- a/src/Ui/Widget/Table.php +++ b/src/Ui/Widget/Table.php @@ -48,17 +48,18 @@ class Table extends Container $this->headerContainer->clearChildren(); foreach ($columns as $column) { - $title = $column['title'] ?? $column['key']; + $key = $column['key']; + $title = $column['title'] ?? $key; $width = $column['width'] ?? null; - $style = 'px-4 py-2 text-black font-bold border-r border-gray-300'; + $style = 'px-4 py-2 text-black font-bold border-r border-gray-300 hover:bg-gray-300 cursor-pointer'; if ($width) { $style .= ' w-' . ((int) ($width / 4)); } else { $style .= ' flex-1'; } - $headerLabel = new Label($title, $style); + $headerLabel = new ClickableHeaderLabel($key, $this, $title, $style); $this->headerContainer->addComponent($headerLabel); } }