This commit is contained in:
Thomas Peterson 2025-11-19 12:18:45 +01:00
parent c567194b1c
commit c38bffd4f9
9 changed files with 434 additions and 46 deletions

View File

@ -3,10 +3,14 @@
namespace ServerManager\UI; namespace ServerManager\UI;
use PHPNative\Framework\Settings; use PHPNative\Framework\Settings;
use PHPNative\Tailwind\Data\Icon as IconName;
use PHPNative\Ui\Widget\Button; use PHPNative\Ui\Widget\Button;
use PHPNative\Ui\Widget\Container; use PHPNative\Ui\Widget\Container;
use PHPNative\Ui\Widget\Icon;
use PHPNative\Ui\Widget\Label; use PHPNative\Ui\Widget\Label;
use PHPNative\Ui\Widget\TextInput; use PHPNative\Ui\Widget\TextInput;
use PHPNative\Ui\Widget\TextArea;
use PHPNative\Ui\Widget\Modal;
class KanbanTab class KanbanTab
{ {
@ -14,6 +18,12 @@ class KanbanTab
private Settings $settings; private Settings $settings;
private Container $boardsContainer; private Container $boardsContainer;
private TextInput $newBoardInput; private TextInput $newBoardInput;
private Modal $editModal;
private TextInput $editTitleInput;
private TextArea $editDetailsArea;
private Container $editBoardButtonsContainer;
private string $currentEditingBoard = 'neu';
private null|string $currentEditingTaskId = null;
public function __construct(Settings $settings) public function __construct(Settings $settings)
{ {
@ -69,6 +79,9 @@ class KanbanTab
$kanbanTab->renderBoards(); $kanbanTab->renderBoards();
}); });
// Edit modal for tasks
$this->createEditModal();
$this->renderBoards(); $this->renderBoards();
} }
@ -92,6 +105,173 @@ class KanbanTab
} }
} }
private function rebuildBoardButtons(): void
{
if (!isset($this->editBoardButtonsContainer)) {
return;
}
$this->editBoardButtonsContainer->clearChildren();
$boards = $this->settings->get('kanban.boards', []);
if (!is_array($boards) || empty($boards)) {
$boards = ['neu', 'in arbeit', 'fertig'];
}
foreach ($boards as $boardName) {
$isSelected = ($boardName === $this->currentEditingBoard);
$style = 'px-2 py-1 rounded text-xs border ';
if ($isSelected) {
$style .= 'bg-blue-600 text-white border-blue-600';
} else {
$style .= 'bg-white text-gray-700 border-gray-300 hover:bg-gray-100';
}
$button = new Button($boardName, $style);
$kanbanTab = $this;
$button->setOnClick(function () use ($kanbanTab, $boardName) {
$kanbanTab->currentEditingBoard = $boardName;
$kanbanTab->rebuildBoardButtons();
});
$this->editBoardButtonsContainer->addComponent($button);
}
}
private function createEditModal(): void
{
$content = new Container('flex flex-col bg-white rounded-lg shadow-xl p-4 gap-3 w-[480] max-h-[420]');
$content->setUseTextureCache(false);
$content->addComponent(new Label('Task bearbeiten', 'text-lg font-bold text-black'));
// Title
$content->addComponent(new Label('Titel', 'text-xs text-gray-600'));
$this->editTitleInput = new TextInput(
'Titel',
'w-full border border-gray-300 rounded px-3 py-2 bg-white text-black text-sm',
);
$content->addComponent($this->editTitleInput);
// Details
$content->addComponent(new Label('Details', 'text-xs text-gray-600'));
$this->editDetailsArea = new TextArea(
'',
'Details...',
'flex-1 border border-gray-300 rounded px-3 py-2 bg-white text-black text-xs',
);
$this->editDetailsArea->setUseTextureCache(false);
$content->addComponent($this->editDetailsArea);
// Board selection (selectbox-artig)
$content->addComponent(new Label('Board', 'text-xs text-gray-600'));
$this->editBoardButtonsContainer = new Container('flex flex-row flex-wrap gap-1');
$this->rebuildBoardButtons();
$content->addComponent($this->editBoardButtonsContainer);
// Buttons
$buttonRow = new Container('flex flex-row justify-end gap-2 mt-2');
$cancelButton = new Button(
'Abbrechen',
'px-3 py-2 bg-gray-300 rounded hover:bg-gray-400',
null,
'text-black text-sm',
);
$saveButton = new Button(
'Speichern',
'px-3 py-2 bg-green-600 rounded hover:bg-green-700',
null,
'text-white text-sm',
);
$kanbanTab = $this;
$cancelButton->setOnClick(function () use ($kanbanTab) {
$kanbanTab->editModal->setVisible(false);
});
$saveButton->setOnClick(function () use ($kanbanTab) {
$kanbanTab->saveEditedTask();
});
$buttonRow->addComponent($cancelButton);
$buttonRow->addComponent($saveButton);
$content->addComponent($buttonRow);
$this->editModal = new Modal($content);
$this->tab->addComponent($this->editModal);
}
private function openEditModal(array $task): void
{
$this->currentEditingTaskId = $task['id'] ?? null;
if ($this->currentEditingTaskId === null) {
return;
}
$title = (string) ($task['title'] ?? '');
$details = (string) ($task['details'] ?? '');
$board = (string) ($task['board'] ?? 'neu');
$this->editTitleInput->setValue($title);
$this->editDetailsArea->setValue($details);
$this->currentEditingBoard = $board;
$this->rebuildBoardButtons();
$this->editModal->setVisible(true);
}
private function saveEditedTask(): void
{
if ($this->currentEditingTaskId === null) {
$this->editModal->setVisible(false);
return;
}
$title = trim($this->editTitleInput->getValue());
$details = trim($this->editDetailsArea->getValue());
$board = trim($this->currentEditingBoard);
if ($title === '') {
$title = 'Ohne Titel';
}
if ($board === '') {
$board = 'neu';
}
$tasks = $this->settings->get('kanban.tasks', []);
if (!is_array($tasks)) {
$tasks = [];
}
foreach ($tasks as &$task) {
if (($task['id'] ?? null) === $this->currentEditingTaskId) {
$task['title'] = $title;
$task['details'] = $details;
$task['board'] = $board;
break;
}
}
unset($task);
// Ensure board exists
$boards = $this->settings->get('kanban.boards', []);
if (!is_array($boards)) {
$boards = [];
}
if (!in_array($board, $boards, true)) {
$boards[] = $board;
$this->settings->set('kanban.boards', $boards);
}
$this->settings->set('kanban.tasks', $tasks);
$this->settings->save();
$this->editModal->setVisible(false);
$this->renderBoards();
}
private function renderBoards(): void private function renderBoards(): void
{ {
$this->boardsContainer->clearChildren(); $this->boardsContainer->clearChildren();
@ -127,11 +307,66 @@ class KanbanTab
$title = (string) ($task['title'] ?? ''); $title = (string) ($task['title'] ?? '');
$serverId = $task['server_id'] ?? null; $serverId = $task['server_id'] ?? null;
$serverLabel = $serverId !== null ? ('Server #' . $serverId) : 'Kein Server'; $serverLabel = $serverId !== null ? ('Server #' . $serverId) : 'Kein Server';
$taskId = $task['id'] ?? null;
$card = new Container( $card = new Container(
'flex flex-col gap-1 px-3 py-2 bg-white border border-gray-200 rounded shadow-sm', 'flex flex-col gap-1 px-3 py-2 bg-white border border-gray-200 rounded shadow-sm',
); );
$card->addComponent(new Label($title, 'text-xs text-gray-900'));
// Header row with title and action icons
$headerRow = new Container('flex flex-row items-center gap-1');
$headerRow->addComponent(new Label($title, 'text-xs text-gray-900 flex-1'));
// Edit icon button
$editButton = new Button(
'',
'p-1 rounded hover:bg-blue-100',
null,
'text-blue-600',
);
$editIcon = new Icon(IconName::edit, 12, 'text-blue-600');
$editButton->setIcon($editIcon);
$kanbanTab = $this;
$editButton->setOnClick(function () use ($kanbanTab, $task) {
$kanbanTab->openEditModal($task);
});
// Delete icon button
$deleteButton = new Button(
'',
'p-1 rounded hover:bg-red-100',
null,
'text-red-600',
);
$deleteIcon = new Icon(IconName::trash, 12, 'text-red-600');
$deleteButton->setIcon($deleteIcon);
$deleteButton->setOnClick(function () use ($kanbanTab, $taskId) {
if ($taskId === null) {
return;
}
$tasks = $kanbanTab->settings->get('kanban.tasks', []);
if (!is_array($tasks)) {
return;
}
$tasks = array_values(array_filter(
$tasks,
static fn($t) => ($t['id'] ?? null) !== $taskId,
));
$kanbanTab->settings->set('kanban.tasks', $tasks);
$kanbanTab->settings->save();
$kanbanTab->renderBoards();
});
$headerRow->addComponent($editButton);
$headerRow->addComponent($deleteButton);
$card->addComponent($headerRow);
// Server label
$card->addComponent(new Label($serverLabel, 'text-[10px] text-gray-500')); $card->addComponent(new Label($serverLabel, 'text-[10px] text-gray-500'));
$columnBody->addComponent($card); $columnBody->addComponent($card);

View File

@ -82,13 +82,13 @@ class ServerListTab
// Bulk action buttons for selected servers // Bulk action buttons for selected servers
$rebootButton = new Button( $rebootButton = new Button(
'Reboot ausgewählte', 'Reboot ausgewählte',
'px-3 py-2 bg-red-600 rounded hover:bg-red-700', 'px-3 shadow-lg/50 py-2 bg-red-600 rounded hover:bg-red-700',
null, null,
'text-white', 'text-white',
); );
$updateButton = new Button( $updateButton = new Button(
'Update ausgewählte', 'Update ausgewählte',
'px-3 py-2 bg-amber-600 rounded hover:bg-amber-700', 'px-3 shadow-lg/50 hover:shadow-red-600 py-2 bg-amber-600 rounded hover:bg-amber-700',
null, null,
'text-white', 'text-white',
); );
@ -260,12 +260,7 @@ class ServerListTab
'Neue Task...', 'Neue Task...',
'flex-1 border border-gray-300 rounded px-3 py-2 bg-white text-black', 'flex-1 border border-gray-300 rounded px-3 py-2 bg-white text-black',
); );
$addTodoButton = new Button( $addTodoButton = new Button('+', 'px-3 py-2 bg-blue-600 rounded hover:bg-blue-700', null, 'text-white');
'+',
'px-3 py-2 bg-blue-600 rounded hover:bg-blue-700',
null,
'text-white',
);
$todoInputRow->addComponent($this->todoInput); $todoInputRow->addComponent($this->todoInput);
$todoInputRow->addComponent($addTodoButton); $todoInputRow->addComponent($addTodoButton);
$detailPanel->addComponent($todoInputRow); $detailPanel->addComponent($todoInputRow);
@ -306,7 +301,7 @@ class ServerListTab
// Load per-server settings (API key, todos) // Load per-server settings (API key, todos)
$serverId = $row['id'] ?? null; $serverId = $row['id'] ?? null;
$serverListTab->currentServerId = is_numeric($serverId) ? (int) $serverId : null; $serverListTab->currentServerId = is_numeric($serverId) ? ((int) $serverId) : null;
if ($serverListTab->currentServerId !== null) { if ($serverListTab->currentServerId !== null) {
$settingsKeyBase = 'servers.' . $serverListTab->currentServerId; $settingsKeyBase = 'servers.' . $serverListTab->currentServerId;
@ -388,20 +383,14 @@ class ServerListTab
echo "Error: {$result['error']}\n"; echo "Error: {$result['error']}\n";
} elseif (isset($result['success'], $result['servers'])) { } elseif (isset($result['success'], $result['servers'])) {
// Basisdaten setzen, Docker-Status initial auf "pending" // Basisdaten setzen, Docker-Status initial auf "pending"
$serverListTab->currentServerData = array_map( $serverListTab->currentServerData = array_map(static fn($row) => array_merge([
static fn($row) => array_merge(
[
'docker_status' => 'pending', 'docker_status' => 'pending',
'docker' => null, 'docker' => null,
'docker_error' => null, 'docker_error' => null,
'needs_reboot' => 'unbekannt', 'needs_reboot' => 'unbekannt',
'updates_available' => 'unbekannt', 'updates_available' => 'unbekannt',
'os_version' => 'unbekannt', 'os_version' => 'unbekannt',
], ], $row), $result['servers']);
$row,
),
$result['servers'],
);
$serverListTab->table->setData($serverListTab->currentServerData, false); $serverListTab->table->setData($serverListTab->currentServerData, false);
$serverListTab->statusLabel->setText('Server geladen: ' . $result['count'] . ' gefunden'); $serverListTab->statusLabel->setText('Server geladen: ' . $result['count'] . ' gefunden');
@ -788,9 +777,8 @@ class ServerListTab
$serverListTab->settings->set($settingsKeyBase . '.api_key', $apiKey); $serverListTab->settings->set($settingsKeyBase . '.api_key', $apiKey);
$serverListTab->settings->save(); $serverListTab->settings->save();
$serverListTab->statusLabel->setText( $serverListTab->statusLabel->setText('Server-Einstellungen gespeichert für Server #' .
'Server-Einstellungen gespeichert für Server #' . $serverListTab->currentServerId, $serverListTab->currentServerId);
);
}); });
// Add TODO for current server // Add TODO for current server
@ -918,9 +906,7 @@ class ServerListTab
$this->todoListContainer->clearChildren(); $this->todoListContainer->clearChildren();
if (empty($this->currentServerTodos)) { if (empty($this->currentServerTodos)) {
$this->todoListContainer->addComponent( $this->todoListContainer->addComponent(new Label('Keine Tasks', 'text-xs text-gray-500 italic'));
new Label('Keine Tasks', 'text-xs text-gray-500 italic'),
);
return; return;
} }
@ -929,10 +915,7 @@ class ServerListTab
$board = (string) ($task['board'] ?? 'neu'); $board = (string) ($task['board'] ?? 'neu');
$row = new Container('flex flex-row items-center gap-2'); $row = new Container('flex flex-row items-center gap-2');
$row->addComponent(new Label( $row->addComponent(new Label('- ' . $title . ' [' . $board . ']', 'text-xs text-gray-800 flex-1'));
'- ' . $title . ' [' . $board . ']',
'text-xs text-gray-800 flex-1',
));
$removeButton = new Button( $removeButton = new Button(
'x', 'x',
@ -953,10 +936,7 @@ class ServerListTab
return; return;
} }
$tasks = array_values(array_filter( $tasks = array_values(array_filter($tasks, static fn($t) => ($t['id'] ?? null) !== $taskId));
$tasks,
static fn($t) => ($t['id'] ?? null) !== $taskId,
));
$serverListTab->settings->set('kanban.tasks', $tasks); $serverListTab->settings->set('kanban.tasks', $tasks);
$serverListTab->settings->save(); $serverListTab->settings->save();
@ -1010,12 +990,9 @@ class ServerListTab
$tasks = []; $tasks = [];
} }
$this->currentServerTodos = array_values(array_filter( $this->currentServerTodos = array_values(array_filter($tasks, function ($task) {
$tasks, return ((int) ($task['server_id'] ?? 0)) === ((int) $this->currentServerId);
function ($task) { }));
return (int) ($task['server_id'] ?? 0) === (int) $this->currentServerId;
},
));
$this->renderTodoList(); $this->renderTodoList();
} }

Binary file not shown.

Binary file not shown.

View File

@ -3,4 +3,5 @@
# Created by configure # Created by configure
'./configure' \ './configure' \
'--enable-sdl3' \
"$@" "$@"

View File

@ -413,7 +413,7 @@ $config_headers
Report bugs to the package provider." Report bugs to the package provider."
ac_cs_config='' ac_cs_config='--enable-sdl3'
ac_cs_version="\ ac_cs_version="\
config.status config.status
configured by ./configure, generated by GNU Autoconf 2.72, configured by ./configure, generated by GNU Autoconf 2.72,
@ -494,7 +494,7 @@ if $ac_cs_silent; then
fi fi
if $ac_cs_recheck; then if $ac_cs_recheck; then
set X /bin/bash './configure' $ac_configure_extra_args --no-create --no-recursion set X /bin/bash './configure' '--enable-sdl3' $ac_configure_extra_args --no-create --no-recursion
shift shift
\printf "%s\n" "running CONFIG_SHELL=/bin/bash $*" >&6 \printf "%s\n" "running CONFIG_SHELL=/bin/bash $*" >&6
CONFIG_SHELL='/bin/bash' CONFIG_SHELL='/bin/bash'

Binary file not shown.

View File

@ -526,6 +526,153 @@ PHP_FUNCTION(sdl_set_texture_alpha_mod) {
RETURN_TRUE; RETURN_TRUE;
} }
PHP_FUNCTION(sdl_create_box_shadow_texture) {
zval *ren_res;
SDL_Renderer *renderer;
zend_long width, height, blurRadius, alpha, r, g, b;
if (zend_parse_parameters(
ZEND_NUM_ARGS(),
"rlllllll",
&ren_res,
&width,
&height,
&blurRadius,
&alpha,
&r,
&g,
&b) == FAILURE) {
RETURN_THROWS();
}
renderer = (SDL_Renderer *)zend_fetch_resource(Z_RES_P(ren_res), "SDL_Renderer", le_sdl_renderer);
if (!renderer) {
RETURN_FALSE;
}
if (width <= 0 || height <= 0) {
RETURN_FALSE;
}
int w = (int) width;
int h = (int) height;
int radius = (int) blurRadius;
if (radius < 0) {
radius = 0;
}
if (alpha < 0) {
alpha = 0;
}
if (alpha > 255) {
alpha = 255;
}
int size = w * h;
Uint8 *alphaMap = emalloc(size * sizeof(Uint8));
if (!alphaMap) {
RETURN_FALSE;
}
memset(alphaMap, 0, size * sizeof(Uint8));
int margin = radius + 2;
for (int y = margin; y < h - margin; y++) {
int rowOffset = y * w;
for (int x = margin; x < w - margin; x++) {
alphaMap[rowOffset + x] = (Uint8) alpha;
}
}
if (radius > 0) {
Uint8 *temp = emalloc(size * sizeof(Uint8));
if (!temp) {
efree(alphaMap);
RETURN_FALSE;
}
// Horizontal blur
for (int y = 0; y < h; y++) {
int rowOffset = y * w;
for (int x = 0; x < w; x++) {
int sum = 0;
int count = 0;
int xStart = x - radius;
int xEnd = x + radius;
if (xStart < 0) xStart = 0;
if (xEnd >= w) xEnd = w - 1;
for (int xi = xStart; xi <= xEnd; xi++) {
sum += alphaMap[rowOffset + xi];
count++;
}
temp[rowOffset + x] = count > 0 ? (Uint8)(sum / count) : 0;
}
}
// Vertical blur
for (int x = 0; x < w; x++) {
for (int y = 0; y < h; y++) {
int sum = 0;
int count = 0;
int yStart = y - radius;
int yEnd = y + radius;
if (yStart < 0) yStart = 0;
if (yEnd >= h) yEnd = h - 1;
for (int yi = yStart; yi <= yEnd; yi++) {
sum += temp[yi * w + x];
count++;
}
alphaMap[y * w + x] = count > 0 ? (Uint8)(sum / count) : 0;
}
}
efree(temp);
}
SDL_Texture *texture = SDL_CreateTexture(
renderer,
SDL_PIXELFORMAT_ARGB8888,
SDL_TEXTUREACCESS_STATIC,
w,
h);
if (!texture) {
efree(alphaMap);
php_error_docref(NULL, E_WARNING, "Failed to create shadow texture: %s", SDL_GetError());
RETURN_FALSE;
}
Uint32 *pixels = emalloc(size * sizeof(Uint32));
if (!pixels) {
efree(alphaMap);
SDL_DestroyTexture(texture);
RETURN_FALSE;
}
Uint8 red = (Uint8) r;
Uint8 green = (Uint8) g;
Uint8 blue = (Uint8) b;
for (int i = 0; i < size; i++) {
Uint8 a = alphaMap[i];
pixels[i] = ((Uint32)a << 24) | ((Uint32)red << 16) | ((Uint32)green << 8) | (Uint32)blue;
}
if (SDL_UpdateTexture(texture, NULL, pixels, w * 4) < 0) {
efree(alphaMap);
efree(pixels);
SDL_DestroyTexture(texture);
php_error_docref(NULL, E_WARNING, "Failed to update shadow texture: %s", SDL_GetError());
RETURN_FALSE;
}
efree(alphaMap);
efree(pixels);
SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_BLEND);
RETURN_RES(zend_register_resource(texture, le_sdl_texture));
}
PHP_FUNCTION(sdl_get_render_target) { PHP_FUNCTION(sdl_get_render_target) {
zval *ren_res; zval *ren_res;
SDL_Renderer *renderer; SDL_Renderer *renderer;
@ -994,6 +1141,17 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_set_texture_alpha_mod, 0, 0, 2)
ZEND_ARG_INFO(0, alpha) ZEND_ARG_INFO(0, alpha)
ZEND_END_ARG_INFO() ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_create_box_shadow_texture, 0, 0, 8)
ZEND_ARG_INFO(0, renderer)
ZEND_ARG_INFO(0, width)
ZEND_ARG_INFO(0, height)
ZEND_ARG_INFO(0, blurRadius)
ZEND_ARG_INFO(0, alpha)
ZEND_ARG_INFO(0, r)
ZEND_ARG_INFO(0, g)
ZEND_ARG_INFO(0, b)
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_get_render_target, 0, 0, 1) ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_get_render_target, 0, 0, 1)
ZEND_ARG_INFO(0, renderer) ZEND_ARG_INFO(0, renderer)
ZEND_END_ARG_INFO() ZEND_END_ARG_INFO()
@ -1101,6 +1259,7 @@ const zend_function_entry sdl3_functions[] = {
PHP_FE(sdl_update_texture, arginfo_sdl_update_texture) PHP_FE(sdl_update_texture, arginfo_sdl_update_texture)
PHP_FE(sdl_set_texture_blend_mode, arginfo_sdl_set_texture_blend_mode) PHP_FE(sdl_set_texture_blend_mode, arginfo_sdl_set_texture_blend_mode)
PHP_FE(sdl_set_texture_alpha_mod, arginfo_sdl_set_texture_alpha_mod) PHP_FE(sdl_set_texture_alpha_mod, arginfo_sdl_set_texture_alpha_mod)
PHP_FE(sdl_create_box_shadow_texture, arginfo_sdl_create_box_shadow_texture)
PHP_FE(sdl_get_render_target, arginfo_sdl_get_render_target) PHP_FE(sdl_get_render_target, arginfo_sdl_get_render_target)
PHP_FE(sdl_set_render_target, arginfo_sdl_set_render_target) PHP_FE(sdl_set_render_target, arginfo_sdl_set_render_target)
PHP_FE(sdl_rounded_box, arginfo_sdl_rounded_box) PHP_FE(sdl_rounded_box, arginfo_sdl_rounded_box)

View File

@ -931,6 +931,22 @@ abstract class Component
return null; return null;
} }
// If the optimized C helper is available, use it.
if (function_exists('sdl_create_box_shadow_texture')) {
return sdl_create_box_shadow_texture(
$renderer,
$width,
$height,
$blurRadius,
$alpha,
$r,
$g,
$b,
);
}
// Fallback: existing PHP implementation
// Create alpha map (single channel) - start with transparent // Create alpha map (single channel) - start with transparent
$alphaMap = array_fill(0, $width * $height, 0); $alphaMap = array_fill(0, $width * $height, 0);