diff --git a/examples/alpha_test.php b/examples/alpha_test.php deleted file mode 100644 index b822a5d..0000000 --- a/examples/alpha_test.php +++ /dev/null @@ -1,49 +0,0 @@ - 50, 'y' => 100, 'w' => 100, 'h' => 100]); - - // Green rectangle - alpha 30 - sdl_set_render_draw_color($renderer, 0, 255, 0, 30); - sdl_render_fill_rect($renderer, ['x' => 100, 'y' => 100, 'w' => 100, 'h' => 100]); - - // Blue rectangle - alpha 30 - sdl_set_render_draw_color($renderer, 0, 0, 255, 30); - sdl_render_fill_rect($renderer, ['x' => 150, 'y' => 100, 'w' => 100, 'h' => 100]); - - // Test: 10 black layers with alpha 3 - for ($i = 0; $i < 10; $i++) { - sdl_set_render_draw_color($renderer, 0, 0, 0, 3); - sdl_render_fill_rect($renderer, ['x' => 50, 'y' => 250, 'w' => 200 + $i * 10, 'h' => 100]); - } - } -} - -$mainContainer = new AlphaTestContainer('flex items-center justify-center bg-gray-200'); - -// Set window content and run -$window->setRoot($mainContainer); -$app->addWindow($window); -$app->run(); diff --git a/examples/gap_test.php b/examples/gap_test.php deleted file mode 100644 index 0a668bb..0000000 --- a/examples/gap_test.php +++ /dev/null @@ -1,62 +0,0 @@ -addComponent(new \PHPNative\Ui\Widget\Label('Test 1: flex-row gap-2', 'text-lg font-bold')); -$row1 = new Container('flex flex-row gap-2 bg-white p-4'); -$row1->addComponent(new Button('Button 1', 'px-4 py-2 bg-blue-600 text-white rounded')); -$row1->addComponent(new Button('Button 2', 'px-4 py-2 bg-green-600 text-white rounded')); -$test1->addComponent($row1); -$mainContainer->addComponent($test1); - -// Test 2: Row with gap-4 -$test2 = new Container('flex flex-col gap-2'); -$test2->addComponent(new \PHPNative\Ui\Widget\Label('Test 2: flex-row gap-4', 'text-lg font-bold')); -$row2 = new Container('flex flex-row gap-4 bg-white p-4'); -$row2->addComponent(new Button('Button A', 'px-4 py-2 bg-red-600 text-white rounded')); -$row2->addComponent(new Button('Button B', 'px-4 py-2 bg-purple-600 text-white rounded')); -$test2->addComponent($row2); -$mainContainer->addComponent($test2); - -// Test 3: Row with gap-8 -$test3 = new Container('flex flex-col gap-2'); -$test3->addComponent(new \PHPNative\Ui\Widget\Label('Test 3: flex-row gap-8', 'text-lg font-bold')); -$row3 = new Container('flex flex-row gap-8 bg-white p-4'); -$row3->addComponent(new Button('Btn X', 'px-4 py-2 bg-orange-600 text-white rounded')); -$row3->addComponent(new Button('Btn Y', 'px-4 py-2 bg-pink-600 text-white rounded')); -$test3->addComponent($row3); -$mainContainer->addComponent($test3); - -// Test 4: Column with gap-4 -$test4 = new Container('flex flex-col gap-2'); -$test4->addComponent(new \PHPNative\Ui\Widget\Label('Test 4: flex-col gap-4', 'text-lg font-bold')); -$col = new Container('flex flex-col gap-4 bg-white p-4'); -$col->addComponent(new Button('Top Button', 'px-4 py-2 bg-teal-600 text-white rounded')); -$col->addComponent(new Button('Bottom Button', 'px-4 py-2 bg-indigo-600 text-white rounded')); -$test4->addComponent($col); -$mainContainer->addComponent($test4); - -$window->setRoot($mainContainer); -$app->addWindow($window); - -echo "Gap Test Example\n"; -echo "You should see:\n"; -echo "- Row 1: 2 buttons with 8px gap (gap-2)\n"; -echo "- Row 2: 2 buttons with 16px gap (gap-4)\n"; -echo "- Row 3: 2 buttons with 32px gap (gap-8)\n"; -echo "- Column: 2 buttons stacked with 16px gap (gap-4)\n\n"; - -$app->run(); diff --git a/examples/shadow_debug.php b/examples/shadow_debug.php deleted file mode 100644 index a3e8639..0000000 --- a/examples/shadow_debug.php +++ /dev/null @@ -1,54 +0,0 @@ -count() . "\n"; - - foreach ($styles as $style) { - echo ' - Style type: ' . get_class($style->style) . "\n"; - if ($style->style instanceof \PHPNative\Tailwind\Style\Shadow) { - echo ' Shadow size: ' . $style->style->size . "\n"; - } - } - - echo "\n"; -} - -// Test combined styles -echo "\nTesting combined style:\n"; -echo str_repeat('=', 50) . "\n"; -$combined = 'px-6 py-3 bg-blue-500 text-white rounded-lg shadow-lg'; -echo "Testing: '{$combined}'\n"; -$styles = StyleParser::parse($combined); -echo 'Number of styles: ' . $styles->count() . "\n"; -foreach ($styles as $style) { - echo ' - ' . get_class($style->style); - if ($style->style instanceof \PHPNative\Tailwind\Style\Shadow) { - echo ' (size: ' . $style->style->size . ')'; - } - echo "\n"; -} diff --git a/examples/shadow_simple.php b/examples/shadow_simple.php deleted file mode 100644 index 51fa0fe..0000000 --- a/examples/shadow_simple.php +++ /dev/null @@ -1,24 +0,0 @@ -addComponent($box); - -// Set window content and run -$window->setRoot($mainContainer); -$app->addWindow($window); -$app->run(); diff --git a/examples/shadow_test.php b/examples/shadow_test.php deleted file mode 100644 index 1b0d240..0000000 --- a/examples/shadow_test.php +++ /dev/null @@ -1,46 +0,0 @@ -addComponent($box1); -$row1->addComponent($box2); -$row1->addComponent($box3); - -$row2 = new Container('flex gap-6'); -$row2->addComponent($box4); -$row2->addComponent($box5); - -$mainContainer->addComponent($row1); -$mainContainer->addComponent($row2); - -// Set window content and run -$window->setRoot($mainContainer); -$app->addWindow($window); -$app->run(); diff --git a/examples/simple_container.php b/examples/simple_container.php deleted file mode 100644 index 40ab82e..0000000 --- a/examples/simple_container.php +++ /dev/null @@ -1,33 +0,0 @@ -addComponent($label); - -$window->setRoot($mainContainer); -$app->addWindow($window); - -echo "TextInput Test started!\n"; -echo "- Red container (80px)\n"; -echo "- Blue container with TextInput and Button (40px)\n"; -echo "- Green container (80px)\n\n"; - -$app->run(); diff --git a/examples/test_flex_col.php b/examples/test_flex_col.php deleted file mode 100644 index 7134f31..0000000 --- a/examples/test_flex_col.php +++ /dev/null @@ -1,42 +0,0 @@ -addComponent($label1); -$mainContainer->addComponent($container1); - -// Container 2 - Mit flex-grow (sollte verfügbaren Platz einnehmen) -$container2 = new Container('flex-grow bg-blue-500 p-4'); -$label2 = new Label('Container 2 - Flex Grow (flex-grow)', 'text-white text-xl'); -$container2->addComponent($label2); -$mainContainer->addComponent($container2); - -// Container 3 - Natürliche Höhe (basierend auf Inhalt) -$container3 = new Container('bg-green-500 p-4'); -$label3 = new Label('Container 3 - Natural Height', 'text-white text-xl'); -$container3->addComponent($label3); -$mainContainer->addComponent($container3); - -$window->setRoot($mainContainer); -$app->addWindow($window); - -echo "Flex-Col Test started!\n"; -echo "- Red: Fixed height (80px)\n"; -echo "- Blue: Should grow to fill available space\n"; -echo "- Green: Natural height based on content\n\n"; - -$app->run(); diff --git a/examples/test_simple.php b/examples/test_simple.php deleted file mode 100644 index b33675b..0000000 --- a/examples/test_simple.php +++ /dev/null @@ -1,29 +0,0 @@ -addComponent($label); - -$button = new Button('Test Button', 'px-4 py-2 bg-blue-600 text-white rounded'); -$button->setOnClick(function() use ($label) { - $label->setText('Button clicked!'); -}); -$container->addComponent($button); - -$window->setRoot($container); -$app->addWindow($window); - -echo "Simple test starting...\n"; -$app->run(); diff --git a/examples/test_textinput.php b/examples/test_textinput.php deleted file mode 100644 index 8dbd3a7..0000000 --- a/examples/test_textinput.php +++ /dev/null @@ -1,47 +0,0 @@ -addComponent($container1); - -// Container 2 - Mit TextInput und Button (flex-row) -$inputContainer = new Container('flex flex-row mb-4 bg-blue-200'); - -$input = new TextInput( - placeholder: 'Type something...', - style: 'flex-1 p-2 border border-gray-300 rounded mr-2 h-10', -); - -$button = new Button('Submit', 'bg-green-500 text-white p-2 rounded'); - -$inputContainer->addComponent($input); -$inputContainer->addComponent($button); -$mainContainer->addComponent($inputContainer); - -// Container 3 - Green background mit fester Höhe -$container3 = new Container('bg-green-500 h-20 p-2'); -$mainContainer->addComponent($container3); - -$window->setRoot($mainContainer); -$app->addWindow($window); - -echo "TextInput Test started!\n"; -echo "- Red container (80px)\n"; -echo "- Blue container with TextInput and Button (40px)\n"; -echo "- Green container (80px)\n\n"; - -$app->run(); diff --git a/examples/test_textinput_simple.php b/examples/test_textinput_simple.php deleted file mode 100644 index efc41c9..0000000 --- a/examples/test_textinput_simple.php +++ /dev/null @@ -1,39 +0,0 @@ -addComponent($input); -$inputContainer->addComponent($button); -$mainContainer->addComponent($inputContainer); - -$window->setRoot($mainContainer); -$app->addWindow($window); - -echo "TextInput Test started!\n"; -echo "- Red container (80px)\n"; -echo "- Blue container with TextInput and Button (40px)\n"; -echo "- Green container (80px)\n\n"; - -$app->run(); diff --git a/examples/todo_app.php b/examples/todo_app.php new file mode 100644 index 0000000..9d8f208 --- /dev/null +++ b/examples/todo_app.php @@ -0,0 +1,150 @@ + $task['id'] ?? uniqid('task_', true), + 'title' => trim((string) ($task['title'] ?? '')), + 'done' => (bool) ($task['done'] ?? false), + ]; + }, $data)); +}; + +$saveTasks = static function (string $path, array $tasks): void { + file_put_contents( + $path, + json_encode($tasks, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), + LOCK_EX, + ); +}; + +$tasks = $loadTasks($storagePath); + +$app = new Application(); +$window = new Window('Todo Beispiel', 520, 720); + +$statusLabel = new Label('Bereit.', 'text-sm text-gray-600'); + +$main = new Container('flex flex-col bg-gray-100 gap-4 p-4 h-full w-full'); +$title = new Label('Todo Liste', 'text-2xl font-bold text-black'); +$main->addComponent($title); + +$input = new TextInput('Neue Aufgabe hinzufügen …', 'flex-1 border border-gray-300 rounded px-3 py-2 bg-white text-black'); +$addButton = new Button('Hinzufügen', 'px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700'); +$inputRow = new Container('flex flex-row gap-3 w-full'); +$inputRow->addComponent($input); +$inputRow->addComponent($addButton); +$main->addComponent($inputRow); + +$listWrapper = new Container('flex-1 w-full overflow-auto bg-white rounded border border-gray-300 p-2'); +$listContainer = new Container('flex flex-col gap-2 w-full'); +$listWrapper->addComponent($listContainer); +$main->addComponent($listWrapper); +$main->addComponent($statusLabel); + +$renderTasks = null; +$renderTasks = function () use (&$tasks, $listContainer, $statusLabel, $storagePath, $saveTasks, &$renderTasks) { + $listContainer->clearChildren(); + + if (empty($tasks)) { + $emptyLabel = new Label('Keine Aufgaben vorhanden. Erstelle die erste oben im Feld.', 'text-sm text-gray-500'); + $listContainer->addComponent($emptyLabel); + return; + } + + foreach ($tasks as $index => $task) { + $row = new Container('flex flex-row items-center gap-3 w-full border border-gray-200 rounded px-3 py-2 bg-white shadow-sm'); + $taskLabelStyles = $task['done'] + ? 'flex-1 text-gray-500 line-through' + : 'flex-1 text-black'; + $taskLabel = new Label($task['title'], $taskLabelStyles); + $row->addComponent($taskLabel); + + $toggleButton = new Button( + $task['done'] ? 'Reaktivieren' : 'Erledigt', + $task['done'] + ? 'px-3 py-1 text-sm bg-amber-500 text-white rounded hover:bg-amber-600' + : 'px-3 py-1 text-sm bg-emerald-500 text-white rounded hover:bg-emerald-600', + ); + + $toggleButton->setOnClick(function () use (&$tasks, $task, $storagePath, $saveTasks, $statusLabel, $renderTasks) { + foreach ($tasks as &$entry) { + if ($entry['id'] === $task['id']) { + $entry['done'] = !$entry['done']; + break; + } + } + + $saveTasks($storagePath, $tasks); + $statusLabel->setText(($task['done'] ? 'Aufgabe reaktiviert: ' : 'Aufgabe erledigt: ') . $task['title']); + $renderTasks(); + }); + + $deleteButton = new Button( + 'Löschen', + 'px-3 py-1 text-sm bg-red-500 text-white rounded hover:bg-red-600', + ); + + $deleteButton->setOnClick(function () use (&$tasks, $index, $task, $storagePath, $saveTasks, $statusLabel, $renderTasks) { + array_splice($tasks, $index, 1); + $saveTasks($storagePath, $tasks); + $statusLabel->setText('Aufgabe entfernt: ' . $task['title']); + $renderTasks(); + }); + + $row->addComponent($toggleButton); + $row->addComponent($deleteButton); + + $listContainer->addComponent($row); + } +}; + +$renderTasks(); + +$addButton->setOnClick(function () use (&$tasks, $input, $saveTasks, $storagePath, $statusLabel, $renderTasks) { + $title = trim($input->getValue()); + if ($title === '') { + $statusLabel->setText('Bitte zuerst einen Aufgabentext eingeben.'); + return; + } + + $tasks[] = [ + 'id' => uniqid('task_', true), + 'title' => $title, + 'done' => false, + ]; + + $saveTasks($storagePath, $tasks); + $input->setValue(''); + $statusLabel->setText('Aufgabe hinzugefügt: ' . $title); + $renderTasks(); +}); + +$window->setRoot($main); +$app->addWindow($window); +$app->run(); diff --git a/examples/todo_data.json b/examples/todo_data.json new file mode 100644 index 0000000..cef4a93 --- /dev/null +++ b/examples/todo_data.json @@ -0,0 +1,12 @@ +[ + { + "id": "task_69164e23f0d356.41043316", + "title": "Test", + "done": false + }, + { + "id": "task_69164e28dae205.72302890", + "title": "Geht", + "done": true + } +] \ No newline at end of file diff --git a/examples/windows_app_example.php b/examples/windows_app_example.php deleted file mode 100644 index 187c773..0000000 --- a/examples/windows_app_example.php +++ /dev/null @@ -1,629 +0,0 @@ -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'); -// Settings Menu -$settingsMenu = new Menu(title: 'Einstellungen'); -$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); - -// 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([ - ['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), - ]; -} - -// 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); - -// 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); - -// 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 -$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, &$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'])) { - // 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"; - } - } - }, - 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: 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); - -// 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) { - // 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(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'); - $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(); diff --git a/examples/windows_menu_example.php b/examples/windows_menu_example.php deleted file mode 100644 index c81edb4..0000000 --- a/examples/windows_menu_example.php +++ /dev/null @@ -1,73 +0,0 @@ -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 () { - echo "Optionen clicked\n"; -}); -$settingsMenu->addItem('Sprache', function () { - echo "Sprache clicked\n"; -}); -$menuBar->addMenu($fileMenu); -$menuBar->addMenu($settingsMenu); - -$mainContainer->addComponent($menuBar); - -$statusBar = new StatusBar(); -$statusLabel = new Label( - text: 'Fenster: ' . $window->getViewport()->windowWidth . 'x' . $window->getViewport()->windowHeight, - style: 'basis-2/8 text-black', -); -$statusBar->addSegment($statusLabel); -$statusBar->addSegment(new Label( - text: 'Zeilen: 10', - style: 'basis-4/8 text-black border-l', -)); // Fixed width -$statusBar->addSegment(new Label( - text: 'Version 1.0', - style: 'border-l text-black basis-2/8', -)); -$mainContainer->addComponent($statusBar); -$window->setOnResize(function (Window $window) use (&$statusLabel) { - $statusLabel->setText( - 'Fenster: ' . $window->getViewport()->windowWidth . 'x' . $window->getViewport()->windowHeight, - ); -}); -// Set root and run -$window->setRoot($mainContainer); -$app->addWindow($window); -$app->run();