get('api_key', ''); $currentPrivateKeyPath = $settings->get('private_key_path', ''); /** @var Label|null $statusLabel */ $statusLabel = null; // Store selected server for SFTP connection $selectedServer = null; // Main container (flex-col: menu, content, status) $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'); $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')); $modalDialog->addComponent(new Label( 'Bitte gib deinen API Key und den Pfad zum Private Key für SSH-Verbindungen ein.', 'text-sm text-gray-700', )); $fieldContainer = new Container('flex flex-col gap-1'); $fieldContainer->addComponent(new Label('API Key', 'text-sm text-gray-600')); $fieldContainer->addComponent($apiKeyInput); $modalDialog->addComponent($fieldContainer); $privateKeyFieldContainer = new Container('flex flex-col gap-1'); $privateKeyFieldContainer->addComponent(new Label('Private Key Pfad', 'text-sm text-gray-600')); $privateKeyFieldContainer->addComponent($privateKeyPathInput); $modalDialog->addComponent($privateKeyFieldContainer); $buttonRow = new Container('flex flex-row gap-2 justify-end'); $cancelButton = new Button('Abbrechen', 'px-4 py-2 bg-gray-200 text-black rounded hover:bg-gray-300'); $saveButton = new Button('Speichern', 'px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 flex items-center'); $saveIcon = new Icon(IconName::save, 18, 'text-white mr-2'); $saveButton->setIcon($saveIcon); $buttonRow->addComponent($cancelButton); $buttonRow->addComponent($saveButton); $modalDialog->addComponent($buttonRow); $modal = new Modal($modalDialog, 'flex items-center justify-center bg-black', 200); // === 1. MenuBar === $menuBar = new MenuBar(); // File Menu $fileMenu = new Menu(title: 'Datei'); $fileMenu->addItem('Neu', function () { echo "Neu clicked\n"; }); $fileMenu->addItem('Öffnen', function () { echo "Öffnen clicked\n"; }); $fileMenu->addSeparator(); $fileMenu->addItem('Beenden', function () use ($app) { echo "Beenden clicked\n"; exit(0); }); // Settings Menu $settingsMenu = new Menu(title: 'Einstellungen'); $settingsMenu->addItem('Optionen', function () use ($menuBar, $modal, $apiKeyInput, $privateKeyPathInput, &$currentApiKey, &$currentPrivateKeyPath) { $menuBar->closeAllMenus(); $apiKeyInput->setValue($currentApiKey); $privateKeyPathInput->setValue($currentPrivateKeyPath); $modal->setVisible(true); }); $settingsMenu->addItem('Sprache', function () { echo "Sprache clicked\n"; }); $menuBar->addMenu($fileMenu); $menuBar->addMenu($settingsMenu); $mainContainer->addComponent($menuBar); // === 2. Tab Container (flex-1) === $tabContainer = new TabContainer('flex-1'); // Tab 1: Table with server data (Master-Detail Layout) $tab1 = new Container('flex flex-row p-4 gap-4'); // Changed to flex-row for side-by-side layout // Left side: Table with refresh button $leftSide = new Container('flex flex-col gap-2 flex-1'); // flex-1 to take remaining space // Refresh button (will be configured after loadServers function is defined) $refreshButton = new Button( 'Server aktualisieren', 'flex flex-row gap-2 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700', ); $refreshIcon = new Icon(IconName::search, 16, 'text-white'); $refreshButton->setIcon($refreshIcon); $leftSide->addComponent($refreshButton); $table = new Table(style: ' flex-1'); // flex-1 to fill available height but not expand infinitely $table->setColumns([ ['key' => 'id', 'title' => 'ID', 'width' => 100], ['key' => 'name', 'title' => 'Name', 'width' => 400], ['key' => 'status', 'title' => 'Status', 'width' => 120], ['key' => 'type', 'title' => 'Typ', 'width' => 120], ['key' => 'ipv4', 'title' => 'IPv4'], ]); // Test data with 63 entries $testData = []; for ($i = 1; $i <= 63; $i++) { $testData[] = [ 'id' => $i, 'name' => "Server-{$i}", 'status' => ($i % 3) === 0 ? 'stopped' : 'running', 'type' => 'cx' . (11 + (($i % 4) * 10)), 'ipv4' => sprintf('192.168.%d.%d', floor($i / 255), $i % 255), ]; } $table->setData($testData); $leftSide->addComponent($table); $tab1->addComponent($leftSide); // Right side: Detail panel $detailPanel = new Container('flex flex-col gap-3 w-80 bg-white border-2 border-gray-300 rounded p-4'); $detailTitle = new Label('Server Details', 'text-xl font-bold text-black mb-2'); $detailPanel->addComponent($detailTitle); // Detail fields (initially empty) $detailId = new Label('Bitte einen Server auswählen', 'text-sm text-gray-600'); $detailName = new Label('', 'text-sm text-black'); $detailStatus = new Label('', 'text-sm text-black'); $detailType = new Label('', 'text-sm text-black'); $detailIpv4 = new Label('', 'text-sm text-black'); $detailPanel->addComponent(new Label('ID:', 'text-xs text-gray-500 mt-2')); $detailPanel->addComponent($detailId); $detailPanel->addComponent(new Label('Name:', 'text-xs text-gray-500 mt-2')); $detailPanel->addComponent($detailName); $detailPanel->addComponent(new Label('Status:', 'text-xs text-gray-500 mt-2')); $detailPanel->addComponent($detailStatus); $detailPanel->addComponent(new Label('Typ:', 'text-xs text-gray-500 mt-2')); $detailPanel->addComponent($detailType); $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'); $sftpIcon = new Icon(IconName::home, 18, 'text-white mr-2'); $sftpButton->setIcon($sftpIcon); $detailPanel->addComponent($sftpButton); $tab1->addComponent($detailPanel); // Row selection handler - update detail panel $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) { if ($row) { $statusLabel->setText("Server: {$row['name']} - {$row['status']} ({$row['ipv4']})"); // Update detail panel $detailId->setText("#{$row['id']}"); $detailName->setText($row['name']); $detailStatus->setText($row['status']); $detailType->setText($row['type']); $detailIpv4->setText($row['ipv4']); // Store selected server for SFTP connection $selectedServer = $row; } }); // Function to load servers from Hetzner API (only returns simple data, no objects) // Now uses HetznerClient class thanks to bootstrap.php in async runtime $loadServersAsync = function () use ($currentApiKey) { try { if (empty($currentApiKey)) { return ['error' => 'Kein API-Key konfiguriert']; } // Use HetznerClient class (autoloader is available in async runtime) $hetznerClient = new \LKDev\HetznerCloud\HetznerAPIClient($currentApiKey); $result = ['servers' => []]; foreach ($hetznerClient->servers()->all() as $server) { $result['servers'][] = [ 'id' => $server->id, 'name' => $server->name, 'status' => $server->status, 'type' => $server->serverType->name, 'ipv4' => $server->publicNet->ipv4->ip, ]; } if (isset($result['servers'])) { // Return only simple array data return ['success' => true, 'servers' => $result['servers'], 'count' => count($result['servers'])]; } return ['error' => 'Unerwartete Antwort']; } catch (\Exception $e) { return ['error' => 'Exception: ' . $e->getMessage()]; } }; // Configure refresh button to load servers asynchronously $refreshButton->setOnClickAsync( $loadServersAsync, function ($result) use ($table, $statusLabel) { // 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']); $statusLabel->setText('Server geladen: ' . $result['count'] . ' gefunden'); echo "Success: {$result['count']} servers loaded\n"; } } }, function ($error) use ($statusLabel) { $errorMsg = is_object($error) && method_exists($error, 'getMessage') ? $error->getMessage() : ((string) $error); $statusLabel->setText('Async Fehler: ' . $errorMsg); echo "Async error: {$errorMsg}\n"; }, ); $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) $sftpTab = new Container('flex flex-row p-4 gap-4 bg-gray-50'); // Left side: Local file browser $localBrowserContainer = new Container('flex flex-col flex-1 gap-2'); $localBrowserContainer->addComponent(new Label('Lokal', 'text-lg font-bold text-black mb-2')); $localFileBrowser = new FileBrowser(getcwd(), false, 'flex-1 bg-white border-2 border-gray-300 rounded p-2'); $localBrowserContainer->addComponent($localFileBrowser); // Right side: Remote file browser $remoteBrowserContainer = new Container('flex flex-col flex-1 gap-2'); $remoteBrowserContainer->addComponent(new Label('Remote', 'text-lg font-bold text-black mb-2')); $remoteFileBrowser = new FileBrowser('/', true, 'flex-1 bg-white border-2 border-gray-300 rounded p-2'); $remoteBrowserContainer->addComponent($remoteFileBrowser); // Connection status label $connectionStatusLabel = new Label('Nicht verbunden', 'text-sm text-gray-600 italic mb-2'); $remoteBrowserContainer->addComponent($connectionStatusLabel); $sftpTab->addComponent($localBrowserContainer); $sftpTab->addComponent($remoteBrowserContainer); $tabContainer->addTab('SFTP Manager', $sftpTab); // SFTP Button Click Handler - Connect to server and load remote files $sftpButton->setOnClickAsync( function () use (&$selectedServer, &$currentPrivateKeyPath) { // This runs in a separate thread if ($selectedServer === null) { return ['error' => 'Kein Server ausgewählt']; } if (empty($currentPrivateKeyPath) || !file_exists($currentPrivateKeyPath)) { return ['error' => 'Private Key Pfad nicht konfiguriert oder Datei nicht gefunden']; } try { $ssh = new \phpseclib3\Net\SSH2($selectedServer['ipv4']); $key = \phpseclib3\Crypt\PublicKeyLoader::load(file_get_contents($currentPrivateKeyPath)); if (!$ssh->login('root', $key)) { return ['error' => 'SSH Login fehlgeschlagen']; } // Create SFTP connection $sftp = new \phpseclib3\Net\SFTP($selectedServer['ipv4']); if (!$sftp->login('root', $key)) { return ['error' => 'SFTP Login fehlgeschlagen']; } // Read root directory $files = $sftp->nlist('/'); if ($files === false) { return ['error' => 'Kann Root-Verzeichnis nicht lesen']; } $fileList = []; foreach ($files as $file) { if ($file === '.' || $file === '..') { continue; } $stat = $sftp->stat('/' . $file); $fileList[] = [ 'name' => $file, 'path' => '/' . $file, 'isDir' => ($stat['type'] ?? 0) === 2, 'size' => $stat['size'] ?? 0, 'mtime' => $stat['mtime'] ?? 0, ]; } return [ 'success' => true, 'server' => $selectedServer, 'files' => $fileList, ]; } catch (\Exception $e) { return ['error' => 'Verbindung fehlgeschlagen: ' . $e->getMessage()]; } }, function ($result) use ($tabContainer, $remoteFileBrowser, $connectionStatusLabel, &$statusLabel) { // This runs in the main thread if (isset($result['error'])) { $statusLabel->setText('SFTP Fehler: ' . $result['error']); return; } if (isset($result['success']) && $result['success']) { $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 $remoteFileBrowser->setPath('/'); $remoteFileBrowser->setFileData($result['files']); // Switch to SFTP Manager tab $tabContainer->setActiveTab(3); // 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'); $statusLabel->setText('SFTP Async Fehler: ' . $errorMsg); }, ); $mainContainer->addComponent($tabContainer); // === 3. StatusBar === $statusBar = new StatusBar(); $statusBar->addSegment($statusLabel); $fpsLabel = new Label( text: 'FPS: --', style: 'basis-1/8 text-black border-l', ); $statusBar->addSegment(new Label( text: 'Zeilen: 10', style: 'basis-2/8 text-black border-l', )); // Fixed width $statusBar->addSegment($fpsLabel); $statusBar->addSegment(new Label( text: 'Version 1.0', style: 'border-l text-black basis-2/8', )); $mainContainer->addComponent($statusBar); $cancelButton->setOnClick(function () use ($menuBar, $modal) { $menuBar->closeAllMenus(); $modal->setVisible(false); }); $saveButton->setOnClick(function () use ($settings, &$currentApiKey, &$currentPrivateKeyPath, $apiKeyInput, $privateKeyPathInput, $menuBar, $modal, &$statusLabel) { $currentApiKey = trim($apiKeyInput->getValue()); $currentPrivateKeyPath = trim($privateKeyPathInput->getValue()); // Save to settings $settings->set('api_key', $currentApiKey); $settings->set('private_key_path', $currentPrivateKeyPath); $settings->save(); if ($statusLabel !== null) { $masked = strlen($currentApiKey) > 4 ? (str_repeat('*', max(0, strlen($currentApiKey) - 4)) . substr($currentApiKey, -4)) : $currentApiKey; $statusLabel->setText('Einstellungen gespeichert: API-Key ' . $masked . ' (' . $settings->getPath() . ')'); } $menuBar->closeAllMenus(); $modal->setVisible(false); }); $mainContainer->addComponent($modal); $window->setOnResize(function (Window $window) use (&$statusLabel) { $statusLabel->setText( 'Fenster: ' . $window->getViewport()->windowWidth . 'x' . $window->getViewport()->windowHeight, ); }); $window->setOnFpsChange(function (float $fps) use ($fpsLabel) { $fpsLabel->setText(sprintf('FPS: %d', max(0, (int) round($fps)))); }); // Set root and run $window->setRoot($mainContainer); $app->addWindow($window); echo "Windows Application Example started!\n"; echo "Features:\n"; echo "- MenuBar with 'Datei' and 'Einstellungen'\n"; echo "- Tab Container with 3 tabs\n"; echo "- Scrollable Table in first tab\n"; echo "- StatusBar at the bottom\n\n"; $app->run();