Backup
This commit is contained in:
parent
e07c06c7cc
commit
15186e0005
@ -1,49 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
|
||||||
|
|
||||||
use PHPNative\Framework\Application;
|
|
||||||
use PHPNative\Ui\Widget\Container;
|
|
||||||
use PHPNative\Ui\Window;
|
|
||||||
|
|
||||||
// Create application
|
|
||||||
$app = new Application();
|
|
||||||
$window = new Window('Alpha Blending Test', 400, 400);
|
|
||||||
|
|
||||||
// Custom container to test alpha blending
|
|
||||||
class AlphaTestContainer extends Container
|
|
||||||
{
|
|
||||||
public function render(&$renderer, ?\PHPNative\Framework\TextRenderer $textRenderer = null): void
|
|
||||||
{
|
|
||||||
parent::render($renderer, $textRenderer);
|
|
||||||
|
|
||||||
// Enable alpha blending for renderer
|
|
||||||
sdl_set_render_draw_blend_mode($renderer, SDL_BLENDMODE_BLEND);
|
|
||||||
|
|
||||||
// Draw 3 overlapping semi-transparent rectangles
|
|
||||||
// Red rectangle - alpha 30
|
|
||||||
sdl_set_render_draw_color($renderer, 255, 0, 0, 30);
|
|
||||||
sdl_render_fill_rect($renderer, ['x' => 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();
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
|
||||||
|
|
||||||
use PHPNative\Framework\Application;
|
|
||||||
use PHPNative\Ui\Widget\Button;
|
|
||||||
use PHPNative\Ui\Widget\Container;
|
|
||||||
use PHPNative\Ui\Window;
|
|
||||||
|
|
||||||
$app = new Application();
|
|
||||||
$window = new Window('Gap Test', 600, 400);
|
|
||||||
|
|
||||||
// Main container
|
|
||||||
$mainContainer = new Container('flex flex-col p-4 bg-gray-100 gap-4');
|
|
||||||
|
|
||||||
// Test 1: Row with gap-2
|
|
||||||
$test1 = new Container('flex flex-col gap-2');
|
|
||||||
$test1->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();
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
|
||||||
|
|
||||||
use PHPNative\Tailwind\StyleParser;
|
|
||||||
|
|
||||||
// Test shadow parsing
|
|
||||||
$testStyles = [
|
|
||||||
'shadow-sm',
|
|
||||||
'shadow',
|
|
||||||
'shadow-md',
|
|
||||||
'shadow-lg',
|
|
||||||
'shadow-xl',
|
|
||||||
'shadow-2xl',
|
|
||||||
'shadow-inner',
|
|
||||||
'shadow-none',
|
|
||||||
];
|
|
||||||
|
|
||||||
echo "Testing Shadow Parsing:\n";
|
|
||||||
echo str_repeat('=', 50) . "\n\n";
|
|
||||||
|
|
||||||
foreach ($testStyles as $styleString) {
|
|
||||||
echo "Testing: '{$styleString}'\n";
|
|
||||||
|
|
||||||
$styles = StyleParser::parse($styleString);
|
|
||||||
|
|
||||||
echo "Parsed StyleCollection:\n";
|
|
||||||
|
|
||||||
echo 'Number of styles: ' . $styles->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";
|
|
||||||
}
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
|
||||||
|
|
||||||
use PHPNative\Framework\Application;
|
|
||||||
use PHPNative\Ui\Widget\Container;
|
|
||||||
use PHPNative\Ui\Window;
|
|
||||||
|
|
||||||
// Create application
|
|
||||||
$app = new Application();
|
|
||||||
$window = new Window('Simple Shadow Test', 400, 400);
|
|
||||||
|
|
||||||
// Main container with light background
|
|
||||||
$mainContainer = new Container('flex items-center justify-center bg-gray-200');
|
|
||||||
|
|
||||||
// Simple box with shadow - add bg-white so it's visible
|
|
||||||
$box = new Container('w-48 h-48 bg-white rounded-lg shadow-lg shadow-rose-500');
|
|
||||||
|
|
||||||
$mainContainer->addComponent($box);
|
|
||||||
|
|
||||||
// Set window content and run
|
|
||||||
$window->setRoot($mainContainer);
|
|
||||||
$app->addWindow($window);
|
|
||||||
$app->run();
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
|
||||||
|
|
||||||
use PHPNative\Framework\Application;
|
|
||||||
use PHPNative\Ui\Widget\Container;
|
|
||||||
use PHPNative\Ui\Window;
|
|
||||||
|
|
||||||
// Create application
|
|
||||||
$app = new Application();
|
|
||||||
$window = new Window('Shadow Color & Opacity Test', 800, 600);
|
|
||||||
|
|
||||||
$mainContainer = new Container('flex flex-col gap-6 items-center justify-center bg-gray-100 p-8');
|
|
||||||
|
|
||||||
// Test 1: Default shadow (black)
|
|
||||||
$box1 = new Container('w-48 h-32 bg-white rounded-lg shadow-lg flex items-center justify-center');
|
|
||||||
|
|
||||||
// Test 2: Shadow with opacity modifier (50% opacity)
|
|
||||||
$box2 = new Container('w-48 h-32 bg-white rounded-lg shadow-lg/50 flex items-center justify-center');
|
|
||||||
|
|
||||||
// Test 3: Red shadow
|
|
||||||
$box3 = new Container('w-48 h-32 bg-white rounded-lg shadow-red-500 flex items-center justify-center');
|
|
||||||
|
|
||||||
// Test 4: Blue shadow with opacity
|
|
||||||
$box4 = new Container('w-48 h-32 bg-white rounded-lg shadow-blue-500/30 flex items-center justify-center');
|
|
||||||
|
|
||||||
// Test 5: Green shadow with large size
|
|
||||||
$box5 = new Container('w-48 h-32 bg-white rounded-lg shadow-xl shadow-green-500 flex items-center justify-center');
|
|
||||||
|
|
||||||
// Create row containers for better layout
|
|
||||||
$row1 = new Container('flex gap-6');
|
|
||||||
$row1->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();
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
|
||||||
|
|
||||||
use PHPNative\Framework\Application;
|
|
||||||
use PHPNative\Ui\Widget\Container;
|
|
||||||
use PHPNative\Ui\Widget\Label;
|
|
||||||
use PHPNative\Ui\Window;
|
|
||||||
|
|
||||||
define('DEBUG_RENDERING', true);
|
|
||||||
|
|
||||||
$app = new Application();
|
|
||||||
$window = new Window('Simple Container with Label', 600, 400);
|
|
||||||
|
|
||||||
// Main container
|
|
||||||
$mainContainer = new Container('p-4 bg-gray-100');
|
|
||||||
|
|
||||||
$label = new Label(
|
|
||||||
text: 'Test',
|
|
||||||
style: 'text-xl m-4 bg-lime-400 p-4',
|
|
||||||
);
|
|
||||||
|
|
||||||
$mainContainer->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();
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
|
||||||
|
|
||||||
use PHPNative\Framework\Application;
|
|
||||||
use PHPNative\Ui\Window;
|
|
||||||
use PHPNative\Ui\Widget\Container;
|
|
||||||
use PHPNative\Ui\Widget\Label;
|
|
||||||
|
|
||||||
$app = new Application();
|
|
||||||
$window = new Window('Flex-Col Test', 800, 600);
|
|
||||||
|
|
||||||
// Main container mit flex-col
|
|
||||||
$mainContainer = new Container('flex flex-col p-4 bg-gray-100');
|
|
||||||
|
|
||||||
// Container 1 - Feste Höhe
|
|
||||||
$container1 = new Container('bg-red-500 h-20 p-4');
|
|
||||||
$label1 = new Label('Container 1 - Fixed Height (h-20)', 'text-white text-xl');
|
|
||||||
$container1->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();
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
|
||||||
|
|
||||||
use PHPNative\Framework\Application;
|
|
||||||
use PHPNative\Ui\Widget\Button;
|
|
||||||
use PHPNative\Ui\Widget\Container;
|
|
||||||
use PHPNative\Ui\Widget\Label;
|
|
||||||
use PHPNative\Ui\Window;
|
|
||||||
|
|
||||||
$app = new Application();
|
|
||||||
$window = new Window('Simple Test', 400, 300);
|
|
||||||
|
|
||||||
$container = new Container('flex flex-col p-4 gap-4');
|
|
||||||
|
|
||||||
$label = new Label('Test Label', 'text-lg');
|
|
||||||
$container->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();
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
|
||||||
|
|
||||||
use PHPNative\Framework\Application;
|
|
||||||
use PHPNative\Ui\Widget\Button;
|
|
||||||
use PHPNative\Ui\Widget\Container;
|
|
||||||
use PHPNative\Ui\Widget\TextInput;
|
|
||||||
use PHPNative\Ui\Window;
|
|
||||||
|
|
||||||
$app = new Application();
|
|
||||||
$window = new Window('TextInput Test', 600, 400);
|
|
||||||
|
|
||||||
// Main container
|
|
||||||
$mainContainer = new Container('flex flex-col p-4 bg-gray-100');
|
|
||||||
|
|
||||||
// Container 1 - Red background mit fester Höhe
|
|
||||||
$container1 = new Container('bg-red-500 h-20 mb-4 p-2');
|
|
||||||
$mainContainer->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();
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
|
||||||
|
|
||||||
use PHPNative\Framework\Application;
|
|
||||||
use PHPNative\Ui\Widget\Button;
|
|
||||||
use PHPNative\Ui\Widget\Container;
|
|
||||||
use PHPNative\Ui\Widget\TextInput;
|
|
||||||
use PHPNative\Ui\Window;
|
|
||||||
|
|
||||||
$app = new Application();
|
|
||||||
$window = new Window('TextInput Test', 600, 400);
|
|
||||||
|
|
||||||
// Main container
|
|
||||||
$mainContainer = new Container('flex flex-col p-4 bg-gray-100');
|
|
||||||
|
|
||||||
// Container 2 - Mit TextInput und Button (flex-row)
|
|
||||||
$inputContainer = new Container('flex flex-row p-4 g-blue-200');
|
|
||||||
|
|
||||||
$input = new TextInput(
|
|
||||||
placeholder: 'Type something...',
|
|
||||||
style: 'flex-1 p-2 border border-gray-300 rounded mr-2',
|
|
||||||
);
|
|
||||||
|
|
||||||
$button = new Button('Submit', 'bg-green-500 text-white p-2 rounded-xl');
|
|
||||||
|
|
||||||
$inputContainer->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();
|
|
||||||
150
examples/todo_app.php
Normal file
150
examples/todo_app.php
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
use PHPNative\Framework\Application;
|
||||||
|
use PHPNative\Ui\Widget\Button;
|
||||||
|
use PHPNative\Ui\Widget\Container;
|
||||||
|
use PHPNative\Ui\Widget\Label;
|
||||||
|
use PHPNative\Ui\Widget\TextInput;
|
||||||
|
use PHPNative\Ui\Window;
|
||||||
|
|
||||||
|
$storagePath = __DIR__ . '/todo_data.json';
|
||||||
|
|
||||||
|
$loadTasks = static function (string $path): array {
|
||||||
|
if (!is_file($path)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$raw = file_get_contents($path);
|
||||||
|
$data = json_decode($raw ?: '[]', true);
|
||||||
|
|
||||||
|
if (!is_array($data)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize task structure
|
||||||
|
return array_values(array_map(static function ($task) {
|
||||||
|
return [
|
||||||
|
'id' => $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();
|
||||||
12
examples/todo_data.json
Normal file
12
examples/todo_data.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "task_69164e23f0d356.41043316",
|
||||||
|
"title": "Test",
|
||||||
|
"done": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "task_69164e28dae205.72302890",
|
||||||
|
"title": "Geht",
|
||||||
|
"done": true
|
||||||
|
}
|
||||||
|
]
|
||||||
@ -1,629 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
|
||||||
|
|
||||||
use PHPNative\Framework\Application;
|
|
||||||
use PHPNative\Framework\HetznerClient;
|
|
||||||
use PHPNative\Framework\IconFontRegistry;
|
|
||||||
use PHPNative\Framework\Profiler;
|
|
||||||
use PHPNative\Framework\Settings;
|
|
||||||
use PHPNative\Tailwind\Data\Icon as IconName;
|
|
||||||
use PHPNative\Ui\Widget\Button;
|
|
||||||
use PHPNative\Ui\Widget\Container;
|
|
||||||
use PHPNative\Ui\Widget\FileBrowser;
|
|
||||||
use PHPNative\Ui\Widget\Icon;
|
|
||||||
use PHPNative\Ui\Widget\Label;
|
|
||||||
use PHPNative\Ui\Widget\Menu;
|
|
||||||
use PHPNative\Ui\Widget\MenuBar;
|
|
||||||
use PHPNative\Ui\Widget\Modal;
|
|
||||||
use PHPNative\Ui\Widget\StatusBar;
|
|
||||||
use PHPNative\Ui\Widget\TabContainer;
|
|
||||||
use PHPNative\Ui\Widget\Table;
|
|
||||||
use PHPNative\Ui\Widget\TextInput;
|
|
||||||
use PHPNative\Ui\Window;
|
|
||||||
|
|
||||||
$iconFontCandidates = [
|
|
||||||
__DIR__ . '/../assets/fonts/fa-solid-900.ttf',
|
|
||||||
__DIR__ . '/../assets/fonts/fontawesome/fa7_freesolid_900.otf',
|
|
||||||
'/usr/share/fonts/truetype/fontawesome-webfont.ttf',
|
|
||||||
'/usr/share/fonts/truetype/fontawesome/fa-solid-900.ttf',
|
|
||||||
'/usr/share/fonts/truetype/fa-solid-900.ttf',
|
|
||||||
];
|
|
||||||
|
|
||||||
$iconFontPath = null;
|
|
||||||
foreach ($iconFontCandidates as $candidate) {
|
|
||||||
if (is_file($candidate)) {
|
|
||||||
$iconFontPath = $candidate;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($iconFontPath !== null) {
|
|
||||||
IconFontRegistry::setDefaultFontPath($iconFontPath);
|
|
||||||
} else {
|
|
||||||
echo "Hinweis: FontAwesome Font nicht gefunden. Icons werden ohne Symbol dargestellt.\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enable profiler to identify performance bottlenecks (disabled by default)
|
|
||||||
// Profiler::enable();
|
|
||||||
|
|
||||||
$app = new Application();
|
|
||||||
$window = new Window('Windows Application Example', 800, 600);
|
|
||||||
|
|
||||||
// Initialize settings
|
|
||||||
$settings = new Settings('WindowsAppExample');
|
|
||||||
$currentApiKey = $settings->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();
|
|
||||||
@ -1,73 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
|
||||||
|
|
||||||
use PHPNative\Framework\Application;
|
|
||||||
use PHPNative\Ui\Widget\Container;
|
|
||||||
use PHPNative\Ui\Widget\Label;
|
|
||||||
use PHPNative\Ui\Widget\Menu;
|
|
||||||
use PHPNative\Ui\Widget\MenuBar;
|
|
||||||
use PHPNative\Ui\Widget\StatusBar;
|
|
||||||
use PHPNative\Ui\Widget\TabContainer;
|
|
||||||
use PHPNative\Ui\Widget\Table;
|
|
||||||
use PHPNative\Ui\Window;
|
|
||||||
|
|
||||||
$app = new Application();
|
|
||||||
$window = new Window('Windows Application Example', 800, 600);
|
|
||||||
|
|
||||||
$mainContainer = new Container('bg-gray-100');
|
|
||||||
|
|
||||||
// === 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 () {
|
|
||||||
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();
|
|
||||||
Loading…
Reference in New Issue
Block a user