658 lines
24 KiB
PHP
658 lines
24 KiB
PHP
<?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');
|
|
$fileMenu->addItem('Neu', function () {
|
|
echo "Neu clicked\n";
|
|
});
|
|
$fileMenu->addItem('Öffnen', function () {
|
|
echo "Öffnen clicked\n";
|
|
});
|
|
$fileMenu->addSeparator();
|
|
$fileMenu->addItem('Beenden', function () use ($app) {
|
|
echo "Beenden clicked\n";
|
|
exit(0);
|
|
});
|
|
|
|
// Settings Menu
|
|
$settingsMenu = new Menu(title: 'Einstellungen');
|
|
$settingsMenu->addItem('Optionen', function () use (
|
|
$menuBar,
|
|
$modal,
|
|
$apiKeyInput,
|
|
$privateKeyPathInput,
|
|
&$currentApiKey,
|
|
&$currentPrivateKeyPath,
|
|
) {
|
|
$menuBar->closeAllMenus();
|
|
$apiKeyInput->setValue($currentApiKey);
|
|
$privateKeyPathInput->setValue($currentPrivateKeyPath);
|
|
$modal->setVisible(true);
|
|
});
|
|
$settingsMenu->addItem('Sprache', function () {
|
|
echo "Sprache clicked\n";
|
|
});
|
|
$menuBar->addMenu($fileMenu);
|
|
$menuBar->addMenu($settingsMenu);
|
|
$mainContainer->addComponent($menuBar);
|
|
|
|
// === 2. Tab Container (flex-1) ===
|
|
$tabContainer = new TabContainer('flex-1');
|
|
|
|
// Tab 1: Table with server data (Master-Detail Layout)
|
|
$tab1 = new Container('flex flex-row p-4 gap-4'); // Changed to flex-row for side-by-side layout
|
|
|
|
// Left side: Table with refresh button
|
|
$leftSide = new Container('flex flex-col gap-2 flex-1'); // flex-1 to take remaining space
|
|
|
|
// Refresh button (will be configured after loadServers function is defined)
|
|
$refreshButton = new Button(
|
|
'Server aktualisieren',
|
|
'flex flex-row gap-2 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700',
|
|
);
|
|
$refreshIcon = new Icon(IconName::search, 16, 'text-white');
|
|
$refreshButton->setIcon($refreshIcon);
|
|
|
|
$leftSide->addComponent($refreshButton);
|
|
|
|
// 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();
|