sdl3/examples/todo_app.php
2025-11-27 13:22:59 +01:00

222 lines
6.9 KiB
PHP

<?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);
// Setup Tray with callbacks
if (function_exists('tray_setup')) {
try {
tray_setup('', [
[
'label' => 'Neue Aufgabe',
'callback' => function ($idx) use (
&$tasks,
$input,
$saveTasks,
$storagePath,
$statusLabel,
$renderTasks,
) {
try {
error_log('Neue Aufgabe Callback called!');
$title = 'Tray: Neue Aufgabe';
error_log("Adding task: {$title}");
$tasks[] = [
'id' => uniqid('task_', true),
'title' => $title,
'done' => false,
];
error_log('Saving tasks...');
$saveTasks($storagePath, $tasks);
error_log('Updating UI...');
$statusLabel->setText('Aufgabe hinzugefügt: ' . $title);
$renderTasks();
error_log('Done!');
} catch (\Throwable $e) {
error_log('ERROR in Neue Aufgabe callback: ' . $e->getMessage());
error_log('Trace: ' . $e->getTraceAsString());
}
},
],
[
'label' => 'Fenster anzeigen',
'callback' => function ($idx) use ($window) {
// Show/focus window (TODO: implement window focus/show API)
},
],
[
'label' => 'Beenden',
'callback' => function ($idx) use ($app) {
$app->quit();
},
],
]);
} catch (\Throwable $e) {
error_log('Tray setup failed: ' . $e->getMessage());
}
}
$app->run();